DNF90 服务端与启动器完整源码(含 GM 工具功能增强)

服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
hanfuit committed 2026-11-04 14:31:55 +08:00
commit 645b64fe21
4388 files changed
+1405457

No files matched your search

+190
View File
@@ -0,0 +1,190 @@
package services
import (
"fmt"
"os"
"path/filepath"
"strings"
)
func (s *MCPService) exportPrepare(a mcpArgs) (any, error) {
if err := s.check(a); err != nil {
return nil, err
}
if a.Mode != "direct" && a.Mode != "mod" {
return nil, fmt.Errorf("INVALID_ARGUMENT: mode=direct/mod")
}
if a.Source != "selection" && a.Source != "memory" && a.Source != "working" && a.Source != "commit" {
return nil, fmt.Errorf("INVALID_ARGUMENT: source")
}
if a.Source == "selection" && len(a.Scopes) == 0 {
return nil, fmt.Errorf("INVALID_ARGUMENT: scopes 必填,根目录使用空字符串")
}
s.export.mu.Lock()
pending := s.export.plan != nil && s.export.validLockedSafe()
s.export.mu.Unlock()
if pending {
return nil, fmt.Errorf("EXPORT_PLAN_PENDING")
}
preview, err := s.export.prepare(ExportRequest{Source: a.Source, Scopes: a.Scopes, Mode: a.Mode, Format: a.Format, Name: a.Name, Version: a.Version, ModID: a.ModID, CommitID: a.CommitID, IncludeDependencies: a.IncludeDependencies}, true)
if err != nil {
return nil, err
}
// Enforce limits before retaining an AI export plan, including binary content.
s.export.mu.Lock()
p := s.export.plan
var total int
for _, file := range p.files {
total += len(file.data)
}
for _, entry := range p.pkg.Entries {
total += len(entry.Data)
for _, pair := range entry.Pairs {
total += len(pair.Key) + len(pair.Value)
}
}
s.export.mu.Unlock()
if preview.FileCount > 10000 || total > 128<<20 {
s.export.Release(preview.ID)
return nil, fmt.Errorf("EXPORT_LIMIT: 最多 10000 文件 / 128 MiB")
}
s.mu.Lock()
s.exportID = preview.ID
s.mu.Unlock()
return map[string]any{"exportPlanId": preview.ID, "fileCount": preview.FileCount, "dependencyCount": preview.DependencyCount, "skippedDeletes": preview.SkippedDeletes, "warnings": exportWarningRows(preview.Warnings)}, nil
}
func exportWarningRows(warnings []string) []map[string]string {
rows := []map[string]string{}
for i, w := range warnings {
rows = append(rows, map[string]string{"warningId": fmt.Sprintf("w%d", i), "message": w})
}
return rows
}
func (s *MCPService) exportPreview(a mcpArgs) (any, error) {
s.mu.Lock()
id := s.exportID
s.mu.Unlock()
if id == "" || id != a.ExportPlanID {
return nil, fmt.Errorf("PLAN_NOT_FOUND")
}
s.export.mu.Lock()
defer s.export.mu.Unlock()
p := s.export.plan
if p == nil || !s.export.validLockedSafe() {
return nil, ErrExportStale
}
start := min(a.Cursor, len(p.preview.Files))
end := min(start+a.Limit, len(p.preview.Files))
next := -1
if end < len(p.preview.Files) {
next = end
}
rows := []map[string]any{}
for _, file := range p.preview.Files[start:end] {
rows = append(rows, map[string]any{"entryKey": file.Path, "path": file.Path, "required": file.Required, "dependencyCount": file.DependencyCount})
}
return map[string]any{"exportPlanId": id, "rows": rows, "total": len(p.preview.Files), "nextCursor": next, "warnings": exportWarningRows(p.preview.Warnings)}, nil
}
func (s *MCPService) exportExecute(a mcpArgs) (any, error) {
if err := absolutePath(a.OutputParent); err != nil {
return nil, err
}
s.mu.Lock()
id := s.exportID
s.mu.Unlock()
if id == "" || id != a.ExportPlanID {
return nil, fmt.Errorf("PLAN_NOT_FOUND")
}
s.export.mu.Lock()
p := s.export.plan
if p == nil || !s.export.validLockedSafe() {
s.export.mu.Unlock()
return nil, ErrExportStale
}
known := map[string]bool{}
for _, f := range p.preview.Files {
known[f.Path] = true
}
selected := map[string]bool{}
for _, key := range a.SelectedEntryKeys {
if !known[key] {
s.export.mu.Unlock()
return nil, fmt.Errorf("INVALID_SELECTION")
}
selected[key] = true
}
excluded := []string{}
for _, f := range p.preview.Files {
if !f.Required && !selected[f.Path] {
excluded = append(excluded, f.Path)
}
}
if len(selected) == 0 {
s.export.mu.Unlock()
return nil, fmt.Errorf("EMPTY_SELECTION")
}
ack := map[string]bool{}
for _, w := range a.AcknowledgedWarningIDs {
ack[w] = true
}
for i := range p.preview.Warnings {
if !ack[fmt.Sprintf("w%d", i)] {
s.export.mu.Unlock()
return nil, fmt.Errorf("WARNINGS_PENDING: 请先查看并回传 warningId")
}
}
if p.request.IncludeDependencies && len(excluded) > 0 && !ack["selection-dependencies"] {
s.export.mu.Unlock()
return nil, fmt.Errorf("WARNINGS_PENDING: warningId=selection-dependencies,排除文件可能移除依赖,服务不会自动补回")
}
mode := p.request.Mode
name := p.request.Name
if mode == "mod" && p.request.Format == "115CN-mod" {
name += ".zip"
}
s.export.mu.Unlock()
parent, err := filepath.EvalSymlinks(a.OutputParent)
if err != nil {
return nil, err
}
if mode == "direct" {
name = a.OutputName
if name == "" || name == "." || name == ".." || filepath.Base(name) != name || strings.ContainsAny(name, "/\\") {
return nil, fmt.Errorf("INVALID_ARGUMENT: outputName 需要单个目录名")
}
}
dest := filepath.Join(parent, name)
if _, err = os.Lstat(dest); err == nil {
return nil, fmt.Errorf("TARGET_EXISTS")
} else if !os.IsNotExist(err) {
return nil, err
}
var output string
if mode == "direct" {
temp, err := os.MkdirTemp(parent, ".pvfine-export-*")
if err != nil {
return nil, err
}
defer os.RemoveAll(temp)
// The underlying export consumes its plan on success; directory publication
// failure must therefore be reported distinctly and never blindly retried.
if _, err = s.export.executeSelectedTo(id, temp, excluded); err != nil {
return nil, err
}
if err = publishModDirectory(temp, dest); err != nil {
return nil, fmt.Errorf("PUBLISH_FAILED: 输出未发布,请重新准备导出: %w", err)
}
output = dest
} else {
output, err = s.export.executeSelectedTo(id, parent, excluded)
if err != nil {
return nil, err
}
}
s.mu.Lock()
s.exportID = ""
s.mu.Unlock()
emitEvent("mcp:exported", map[string]any{"outputPath": output, "mode": mode})
return map[string]any{"outputPath": output, "mode": mode, "published": true}, nil
}