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

+253
View File
@@ -0,0 +1,253 @@
package services
import (
"context"
"encoding/json"
"fmt"
"strings"
"pvfine/internal/mcpserver"
)
type mcpArgs struct {
WorkspaceID string `json:"workspaceId"`
ExpectedRevision uint64 `json:"expectedRevision"`
ExpectedWorkspaceEpoch uint64 `json:"expectedWorkspaceEpoch"`
RequestID string `json:"requestId"`
Path string `json:"path"`
Query string `json:"query"`
Scope string `json:"scope"`
Mode string `json:"mode"`
Source string `json:"source"`
Label string `json:"label"`
Topic string `json:"topic"`
TaskID string `json:"taskId"`
PlanID string `json:"planId"`
ImportPlanID string `json:"importPlanId"`
ExportPlanID string `json:"exportPlanId"`
Filter string `json:"filter"`
Cursor int `json:"cursor"`
Limit int `json:"limit"`
StartLine int `json:"startLine"`
DirtyPolicy string `json:"dirtyPolicy"`
Overwrite bool `json:"overwrite"`
TargetDigest string `json:"targetDigest"`
ChangeKeys []string `json:"changeKeys"`
SelectedKeys []string `json:"selectedKeys"`
OverwriteKeys []string `json:"overwriteKeys"`
SourcePaths []string `json:"sourcePaths"`
TargetDir string `json:"targetDir"`
Scopes []string `json:"scopes"`
Format string `json:"format"`
Name string `json:"name"`
Version string `json:"version"`
ModID string `json:"modId"`
CommitID string `json:"commitId"`
IncludeDependencies bool `json:"includeDependencies"`
OutputParent string `json:"outputParent"`
OutputName string `json:"outputName"`
SelectedEntryKeys []string `json:"selectedEntryKeys"`
AcknowledgedWarningIDs []string `json:"acknowledgedWarningIds"`
}
func (s *MCPService) tools() []mcpserver.Tool {
properties := map[string]any{}
for _, key := range strings.Fields("workspaceId requestId path query scope mode source label topic taskId planId importPlanId exportPlanId filter dirtyPolicy targetDigest targetDir format name version modId commitId outputParent outputName") {
properties[key] = map[string]any{"type": "string", "maxLength": 4096}
}
properties["source"] = map[string]any{"type": "string", "maxLength": 1 << 20}
for _, key := range strings.Fields("expectedRevision expectedWorkspaceEpoch cursor startLine") {
properties[key] = map[string]any{"type": "integer", "minimum": 0}
}
properties["limit"] = map[string]any{"type": "integer", "minimum": 1, "maximum": 100}
for _, key := range []string{"overwrite", "includeDependencies"} {
properties[key] = map[string]any{"type": "boolean"}
}
for _, key := range strings.Fields("changeKeys selectedKeys overwriteKeys sourcePaths scopes selectedEntryKeys acknowledgedWarningIds") {
properties[key] = map[string]any{"type": "array", "items": map[string]any{"type": "string", "maxLength": 4096}, "maxItems": 10000}
}
type definition struct {
name, description, fields, required string
readonly bool
}
defs := []definition{
{"workspace_info", "获取当前 PVF 工作区身份、修订号和未保存状态。首次调用此工具。", "", "", true},
{"search", "搜索当前归档。mode=path 使用路径通配符,content 使用字符串索引;结果分页。", "workspaceId expectedRevision query scope mode cursor limit", "workspaceId expectedRevision query", true},
{"read_file", "按归档内部路径读取脚本文本,startLine 从 1 开始;返回分页行,不接受本机路径。", "workspaceId expectedRevision path startLine limit", "workspaceId expectedRevision path", true},
{"script_help", "查看沙箱 pvf JavaScript API 和示例。topic=overview/files/sections/lists/strings/types;types 返回编辑器同源的完整类型声明。", "topic", "", true},
{"script_validate", "仅校验 JavaScript 语法,不运行。", "source", "source", true},
{"script_run", "在隔离事务运行 pvf JavaScript,返回任务 ID;完成后先查看预览再应用。pvf.log 用于查询输出。", "workspaceId expectedRevision source label requestId", "workspaceId expectedRevision source requestId", false},
{"script_status", "查询脚本任务状态及最终结果(含日志、诊断和 planId)。", "taskId", "taskId", true},
{"task_status", "查询归档/导入/导出任务状态与最终结果。", "taskId", "taskId", true},
{"script_preview", "分页查看脚本差异与 changeKey。", "planId filter cursor limit", "planId", true},
{"script_apply", "应用明确选择的脚本变更到工作区,不自动保存 PVF。字符串表依赖需一起选择。", "workspaceId planId expectedRevision changeKeys requestId", "workspaceId planId expectedRevision changeKeys requestId", false},
{"script_cancel", "只取消 MCP 发起的指定脚本任务。", "taskId", "taskId", false},
{"script_discard", "释放指定 MCP 脚本预览。", "planId", "planId", false},
{"archive_open", "按本机绝对路径打开/切换 PVF。dirtyPolicy=reject/save/discard,默认 reject;始终保护前端草稿。", "path workspaceId expectedRevision expectedWorkspaceEpoch dirtyPolicy requestId", "path expectedWorkspaceEpoch requestId", false},
{"archive_close", "关闭当前归档,保留 MCP 服务。dirtyPolicy=reject/save/discard。", "workspaceId expectedRevision dirtyPolicy requestId", "workspaceId expectedRevision requestId", false},
{"archive_save", "保存当前工作区到源文件,检查外部修改。", "workspaceId expectedRevision requestId", "workspaceId expectedRevision requestId", false},
{"archive_save_as", "将当前内容生成副本,后续保存仍写原文件;保留源文件未保存状态。覆盖已有目标需 overwrite 和 targetDigest。", "workspaceId expectedRevision path overwrite targetDigest requestId", "workspaceId expectedRevision path requestId", false},
{"import_prepare", "冻结普通文件/目录并预览导入。mode=text/raw;targetDir 是归档内部目录。目录保留所选目录名。", "workspaceId expectedRevision sourcePaths targetDir mode requestId", "workspaceId expectedRevision sourcePaths mode requestId", false},
{"import_preview", "分页查看冻结导入的路径、覆盖与差异。", "importPlanId cursor limit", "importPlanId", true},
{"import_apply", "将冻结内容导入工作区,不保存 PVF。selectedKeys 选定条目,覆盖必须同时列入 overwriteKeys。", "workspaceId expectedRevision importPlanId selectedKeys overwriteKeys requestId", "workspaceId expectedRevision importPlanId selectedKeys requestId", false},
{"import_discard", "释放导入计划。", "importPlanId", "importPlanId", false},
{"export_formats", "列出可用 MOD 格式及支持操作。", "", "", true},
{"export_prepare", "准备导出。mode=direct/mod;source=selection/memory/working/commit;MOD 需 format/name,115CN-mod 另需唯一 modId,支持依赖补齐;不保存 PVF。", "workspaceId expectedRevision source scopes mode format name version modId commitId includeDependencies requestId", "workspaceId expectedRevision source mode includeDependencies requestId", false},
{"export_preview", "分页查看导出树、entryKey 和 warningId。", "exportPlanId cursor limit", "exportPlanId", true},
{"export_execute", "发布已选内容。direct 需 outputName;115CN-mod 输出 name.zip,其他 MOD 输出 name 目录,不覆盖。需回传已查看的 warningId。", "exportPlanId outputParent outputName selectedEntryKeys acknowledgedWarningIds requestId", "exportPlanId outputParent selectedEntryKeys requestId", false},
{"export_discard", "释放导出预览。", "exportPlanId", "exportPlanId", false},
}
result := []mcpserver.Tool{}
for _, def := range defs {
d := def
props := map[string]any{}
for _, key := range strings.Fields(d.fields) {
props[key] = properties[key]
}
schema := map[string]any{"type": "object", "properties": props, "additionalProperties": false, "required": strings.Fields(d.required)}
result = append(result, mcpserver.Tool{Name: "pvf_" + d.name, Description: d.description, Schema: schema, ReadOnly: d.readonly, Call: func(ctx context.Context, raw json.RawMessage) (any, error) {
var a mcpArgs
if err := json.Unmarshal(raw, &a); err != nil {
return nil, fmt.Errorf("INVALID_ARGUMENT")
}
if a.Limit == 0 {
a.Limit = 50
}
return s.call(ctx, d.name, raw, a)
}})
}
return append(result, s.annotationTools()...)
}
func (s *MCPService) call(ctx context.Context, name string, raw json.RawMessage, a mcpArgs) (any, error) {
switch name {
case "workspace_info":
return s.workspace(), nil
case "script_help":
if a.Topic == "types" {
return map[string]any{"topic": "types", "declarations": s.apiDeclaration}, nil
}
return scriptHelp(a.Topic), nil
case "script_validate":
return s.script.Compile(a.Source), nil
case "task_status", "script_status":
return s.task(a.TaskID)
case "export_formats":
return s.export.Formats(), nil
case "search":
return s.search(a)
case "read_file":
return s.readFile(a)
case "script_preview":
if err := s.ownScript(a.PlanID); err != nil {
return nil, err
}
page, err := s.script.PreviewPage(a.PlanID, a.Filter, a.Cursor, a.Limit)
if err != nil {
return nil, err
}
return boundedScriptPage(page), nil
case "import_preview":
return s.importPreview(a)
case "export_preview":
return s.exportPreview(a)
case "script_cancel":
s.mu.Lock()
task := s.tasks[a.TaskID]
own := task != nil && task.Kind == "script_run" && task.Status == "running"
cancel := s.cancel
s.mu.Unlock()
if !own || cancel == nil {
return nil, fmt.Errorf("TASK_NOT_RUNNING")
}
cancel()
return map[string]any{"cancelRequested": true}, nil
case "script_discard", "import_discard", "export_discard":
s.mu.Lock()
busy := s.active
s.mu.Unlock()
if busy {
return nil, fmt.Errorf("TASK_BUSY")
}
switch name {
case "script_discard":
if err := s.ownScript(a.PlanID); err != nil {
return nil, err
}
s.script.Discard(a.PlanID)
s.c.mu.Lock()
s.c.mcpScriptPlan = ""
s.c.mu.Unlock()
emitEvent("mcp:script-cleared", map[string]string{"planId": a.PlanID})
case "import_discard":
s.mu.Lock()
delete(s.imports, a.ImportPlanID)
s.mu.Unlock()
s.c.mu.Lock()
if s.c.mcpImportPlan == a.ImportPlanID {
s.c.mcpImportPlan = ""
}
s.c.mu.Unlock()
case "export_discard":
s.mu.Lock()
id := s.exportID
if id == a.ExportPlanID {
s.exportID = ""
}
s.mu.Unlock()
if id != a.ExportPlanID {
return nil, fmt.Errorf("PLAN_NOT_FOUND")
}
s.export.Release(id)
}
return map[string]any{"discarded": true}, nil
}
return s.submit(name, raw, a.RequestID, func(taskCtx context.Context) (any, error) {
switch name {
case "archive_open":
return s.open(a)
case "archive_close":
return s.close(a)
case "archive_save", "archive_save_as":
return s.save(a, name == "archive_save_as")
case "script_run":
return s.run(taskCtx, a)
case "script_apply":
if err := s.check(a); err != nil {
return nil, err
}
if err := s.ownScript(a.PlanID); err != nil {
return nil, err
}
r, err := s.script.Apply(a.PlanID, a.ChangeKeys)
if err == nil {
s.c.mu.Lock()
s.c.mcpScriptPlan = ""
s.c.mu.Unlock()
emitEvent("mcp:script-cleared", map[string]string{"planId": a.PlanID})
}
return r, err
case "import_prepare":
return s.importPrepare(a)
case "import_apply":
return s.importApply(a)
case "export_prepare":
return s.exportPrepare(a)
case "export_execute":
return s.exportExecute(a)
}
return nil, fmt.Errorf("UNKNOWN_TOOL")
})
}
func scriptHelp(topic string) any {
docs := map[string]string{
"overview": "沙箱内只有 pvf API;没有 require/process/fetch/文件系统。查询使用 pvf.log(JSON.stringify(result))。修改隔离运行,完成后查看 planId,预览后选择 changeKey 应用。使用文件内部路径,不使用文件索引做长期标识。",
"files": "pvf.find(path) 返回文件或 null;pvf.glob(pattern)、pvf.files() 返回文件数组。file.text()/setText(text) 读取/修改;file.parse() 得到结构化文档,修改后 file.write(doc)。pvf.createFile(path,pvf.types.script,text)、copyFile(from,to,overwrite=false)、deleteFile(path);增删后旧句柄失效,重新查询。",
"sections": "doc.sections(path) 返回全部同名 section;doc.section(path,occurrence=0) 返回指定项或 null。section.get(index=0)、getValue(index=0)、set(value,index=0)、values()、setValues(values)、append(value)、children()、removeChildren(name)、delete()。doc.set(path,value,{occurrence,valueIndex,create,endTag})、appendSection(name,values,{endTag})。重复 section 应遍历 sections()。值可用精确对象 {type:'integer'|'float'|'string'|'quoted'|'block8',value:...}。",
"lists": "pvf.lst(path) 获取列表,支持 .get()、forEach((id,path)=>...)、set(id,path)、mset({id:path})、unset(id)、getId(path)。set 要求目标文件存在;按归档布局处理路径。删除文件不会自动注销列表。",
"strings": "doc/section.getOriStrValue 解析字符串引用;pvf.getOriStrValue(reference) 解析 <表号::键名>。set(string) 在支持格式中自动注册字符串引用;setStrValue 强制旧式内联文本。预览中的 .str 依赖必须与引用文件一起选择。文件集修改目前不向 MCP 开放。",
}
if topic == "" {
topic = "overview"
}
return map[string]any{"topic": topic, "documentation": docs[topic], "topics": []string{"overview", "files", "sections", "lists", "strings", "types"}, "example": "for (const f of pvf.glob('equipment/**/*.equ')) { const d=f.parse(); const p=d.section('price'); if(p && typeof p.get()==='number' && p.get()<10){p.set(10); f.write(d);} }"}
}