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

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

254 lines
14 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);} }"}
}