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

+536
View File
@@ -0,0 +1,536 @@
package services
import (
"context"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"sort"
"strings"
ann "pvfine/internal/annotations"
"pvfine/internal/mcpserver"
"pvfine/internal/ruleconfig"
)
type annotationArgs struct {
Query string `json:"query"`
FilePath string `json:"filePath"`
PathKind string `json:"pathKind"`
Extensions []string `json:"extensions"`
Section string `json:"section"`
Kinds []string `json:"kinds"`
TargetKinds []string `json:"targetKinds"`
AnnotationTypes []string `json:"annotationTypes"`
Relation string `json:"relation"`
PVFVersion string `json:"pvfVersion"`
WorkspaceID string `json:"workspaceId"`
Status string `json:"status"`
Cursor string `json:"cursor"`
Limit int `json:"limit"`
DocumentID string `json:"documentId"`
RulesRevision string `json:"rulesRevision"`
Value *string `json:"value"`
ValuesCursor string `json:"valuesCursor"`
ValuesLimit int `json:"valuesLimit"`
ExpectedRevision uint64 `json:"expectedRevision"`
Occurrence *int `json:"occurrence"`
TokenIndex *int `json:"tokenIndex"`
StartLine *int `json:"startLine"`
EndLine *int `json:"endLine"`
}
func (s *MCPService) annotationTools() []mcpserver.Tool {
props := map[string]any{}
for _, k := range strings.Fields("query filePath pathKind section relation pvfVersion workspaceId status cursor documentId rulesRevision value valuesCursor") {
props[k] = map[string]any{"type": "string", "maxLength": 4096}
}
for _, k := range strings.Fields("extensions kinds targetKinds annotationTypes") {
props[k] = map[string]any{"type": "array", "maxItems": 100, "items": map[string]any{"type": "string", "maxLength": 256}}
}
for _, k := range strings.Fields("expectedRevision occurrence tokenIndex startLine endLine") {
min := 0
if k == "startLine" || k == "endLine" {
min = 1
}
props[k] = map[string]any{"type": "integer", "minimum": min}
}
for _, k := range []string{"limit", "valuesLimit"} {
props[k] = map[string]any{"type": "integer", "minimum": 1, "maximum": 100}
}
for k, values := range map[string][]string{"pathKind": {"file", "directory"}, "status": {"effective", "all"}} {
props[k] = map[string]any{"type": "string", "enum": values}
}
result := []mcpserver.Tool{}
for _, d := range []struct{ name, description, fields, required string }{
{"search", "搜索已激活的 PVF 标注规则与共享字段说明;section 留空检索全部类别。版本默认 current(需 workspaceId),all 可离线浏览。返回文档身份与分页摘要,不扫描归档。", "query filePath pathKind extensions section kinds targetKinds annotationTypes relation pvfVersion workspaceId status cursor limit", ""},
{"get", "读取标注文档完整布局、整块继承后的定义、分页枚举和关联定义。需搜索返回的 documentId/rulesRevision;value 按字符串精确查找。", "documentId rulesRevision value valuesCursor valuesLimit", "documentId rulesRevision"},
{"explain", "解释当前工作区已提交脚本的实际标注。位置为 UTF-16;occurrence/tokenIndex 从 0 开始。与 read_file 同源,不包含前端草稿,不冻结界面。", "workspaceId expectedRevision filePath rulesRevision section occurrence tokenIndex startLine endLine cursor limit", "workspaceId expectedRevision filePath"},
} {
d := d
properties := map[string]any{}
for _, k := range strings.Fields(d.fields) {
properties[k] = props[k]
}
result = append(result, mcpserver.Tool{Name: "pvf_annotation_" + d.name, Description: d.description, ReadOnly: true, Schema: map[string]any{"type": "object", "properties": properties, "required": strings.Fields(d.required), "additionalProperties": false}, Call: func(ctx context.Context, raw json.RawMessage) (any, error) {
var a annotationArgs
if err := json.Unmarshal(raw, &a); err != nil {
return nil, fmt.Errorf("INVALID_ARGUMENT")
}
s.c.mu.Lock()
defer s.c.mu.Unlock()
switch d.name {
case "search":
return s.annotationSearch(a)
case "get":
return s.annotationGet(a)
default:
return s.annotationExplain(a)
}
}})
}
return result
}
type annotationDocument struct {
Entry ruleconfig.Entry
Rule ann.Rule
Original any
Preview *ann.PreviewSpec
DocumentID string
ResolutionError string
}
type annotationCatalog struct {
Revision string
Document ann.Document
Items []annotationDocument
}
func annotationHash(v any) string {
b, _ := json.Marshal(v)
h := sha256.Sum256(b)
return hex.EncodeToString(h[:])
}
func (c *core) annotationCatalogLocked() (annotationCatalog, error) {
if c.annotationErr != nil {
return annotationCatalog{}, c.annotationErr
}
if c.annotationEngine == nil {
return annotationCatalog{}, fmt.Errorf("RULES_UNAVAILABLE")
}
if c.annotationDocumentation != nil && c.annotationDocumentationEngine == c.annotationEngine && c.annotationDocumentationSnapshot == c.annotationSnapshot {
return *c.annotationDocumentation, nil
}
doc := c.annotationEngine.Document()
entries := []ruleconfig.Entry{}
revision := annotationHash(doc)
if c.annotationSnapshot != nil {
revision = c.annotationSnapshot.Revision
entries = c.annotationSnapshot.Entries
}
if len(entries) == 0 {
for _, r := range doc.Rules {
b, _ := json.Marshal(r)
entries = append(entries, ruleconfig.Entry{ID: r.ID, Kind: "rule", JSON: string(b), Source: "unknown", Effective: true})
}
for _, f := range doc.Fields {
b, _ := json.Marshal(f)
entries = append(entries, ruleconfig.Entry{ID: f.ID, Kind: "field", JSON: string(b), Source: "unknown", Effective: true})
}
}
out := annotationCatalog{Revision: revision, Document: doc, Items: []annotationDocument{}}
for i, e := range entries {
d := annotationDocument{Entry: e, DocumentID: annotationHash([]any{revision, e.Kind, e.ID, e.Source, i})}
if e.Kind == "field" {
var f ann.FieldDefinition
if json.Unmarshal([]byte(e.JSON), &f) != nil {
continue
}
d.Original = f
d.Preview = f.Preview
d.Rule = ann.Rule{ID: f.ID, PVFVersions: f.PVFVersions, Match: f.Match, Target: f.Target, Annotation: f.Annotation}
} else if e.Kind == "rule" {
var r ann.Rule
if json.Unmarshal([]byte(e.JSON), &r) != nil {
continue
}
d.Original = r
d.Rule = r
if resolved, err := ann.ResolveDocumentationRule(r, doc.Fields); err == nil {
d.Rule = resolved
} else {
d.ResolutionError = err.Error()
}
for _, f := range doc.Fields {
if f.ID == r.Field {
d.Preview = f.Preview
break
}
}
} else {
continue
}
out.Items = append(out.Items, d)
}
c.annotationDocumentation = &out
c.annotationDocumentationEngine = c.annotationEngine
c.annotationDocumentationSnapshot = c.annotationSnapshot
return out, nil
}
// Read-only cursors bind all filters and the activated snapshots; no filesystem state is read.
type annotationCursor struct {
Revision string `json:"r"`
Binding string `json:"b"`
Offset int `json:"o"`
Workspace string `json:"w,omitempty"`
Text string `json:"t,omitempty"`
}
func annotationPage(cursor, revision, binding, workspace, text string, n, limit int) (int, int, error) {
start := 0
if cursor != "" {
b, err := base64.RawURLEncoding.DecodeString(cursor)
var c annotationCursor
if err != nil || json.Unmarshal(b, &c) != nil {
return 0, 0, fmt.Errorf("INVALID_CURSOR")
}
if c.Revision != revision {
return 0, 0, fmt.Errorf("STALE_RULES")
}
if c.Workspace != workspace {
return 0, 0, fmt.Errorf("STALE_WORKSPACE")
}
if c.Text != text {
return 0, 0, fmt.Errorf("STALE_TEXT")
}
if c.Binding != binding || c.Offset < 0 || c.Offset > n {
return 0, 0, fmt.Errorf("INVALID_CURSOR")
}
start = c.Offset
}
return start, min(start+limit, n), nil
}
func annotationNext(end, n int, revision, binding, workspace, text string) any {
if end >= n {
return nil
}
b, _ := json.Marshal(annotationCursor{revision, binding, end, workspace, text})
return base64.RawURLEncoding.EncodeToString(b)
}
func annotationSection(s string) string {
return strings.ToLower(strings.TrimSpace(strings.TrimSuffix(strings.TrimPrefix(strings.TrimSpace(s), "["), "]")))
}
func annotationIn(xs []string, s string) bool {
if len(xs) == 0 {
return true
}
for _, x := range xs {
if strings.EqualFold(x, s) {
return true
}
}
return false
}
func annotationSource(source string) string {
switch source {
case "annotations.user.json", "user":
return "user"
case "annotations.json", "builtin":
return "builtin"
case "paths.json", "sections.json", "generated":
return "generated"
default:
return "unknown"
}
}
func annotationTerms(values []string) []string {
unique := map[string]bool{}
out := []string{}
for _, v := range values {
v = strings.ToLower(strings.TrimSpace(v))
if !unique[v] {
unique[v] = true
out = append(out, v)
}
}
sort.Strings(out)
return out
}
func annotationShort(s string) (string, bool) {
r := []rune(s)
if len(r) > 512 {
return string(r[:512]), true
}
return s, false
}
func annotationVersion(d annotationDocument, doc ann.Document, v string) bool {
if v == "all" {
return true
}
if !annotationIn(d.Rule.PVFVersions, v) {
return false
}
if d.Rule.Field != "" {
for _, f := range doc.Fields {
if f.ID == d.Rule.Field {
return annotationIn(f.PVFVersions, v)
}
}
return false
}
return true
}
func (s *MCPService) annotationSearch(a annotationArgs) (any, error) {
cat, err := s.c.annotationCatalogLocked()
if err != nil {
return nil, err
}
if a.PVFVersion == "" {
a.PVFVersion = "current"
}
if a.Status == "" {
a.Status = "effective"
}
if a.PathKind == "" {
a.PathKind = "file"
}
if a.Limit == 0 {
a.Limit = 20
}
if a.PVFVersion == "current" {
if s.c.archive == nil {
return nil, fmt.Errorf("NO_ARCHIVE: 请指定 pvfVersion 或 all")
}
if a.WorkspaceID == "" || a.WorkspaceID != s.c.mcpWorkspaceID {
return nil, fmt.Errorf("STALE_WORKSPACE")
}
a.PVFVersion = s.c.archive.ClientVersion()
} else if a.PVFVersion != "all" && !ann.ValidPVFVersion(a.PVFVersion) {
return nil, fmt.Errorf("INVALID_VERSION: 支持 90US,90CN,110US,115US,115CN,100CN,TW")
}
cursor := a.Cursor
a.Cursor = ""
a.Limit = max(1, min(a.Limit, 100))
a.Section = annotationSection(a.Section)
for _, p := range []*[]string{&a.Extensions, &a.Kinds, &a.TargetKinds, &a.AnnotationTypes} {
*p = annotationTerms(*p)
}
a.Query = strings.Join(annotationTerms(strings.Fields(a.Query)), " ")
a.FilePath = strings.ToLower(strings.Trim(strings.ReplaceAll(a.FilePath, "\\", "/"), "/"))
binding := annotationHash(a)
type hit struct {
item map[string]any
score int
key string
}
hits := []hit{}
terms := strings.Fields(strings.ToLower(a.Query))
for _, d := range cat.Items {
r := d.Rule
effective := d.Entry.Effective && annotationVersion(d, cat.Document, a.PVFVersion)
if a.Status != "all" && !effective {
continue
}
if !annotationIn(a.Kinds, d.Entry.Kind) || !annotationIn(a.TargetKinds, r.Target.Kind) || !annotationIn(a.AnnotationTypes, r.Annotation.Type) {
continue
}
if a.Section != "" && a.Section != annotationSection(r.Target.Section) {
continue
}
if a.Relation != "" && r.Annotation.Relation != a.Relation {
continue
}
applicability := "unknown"
reasons := []string{}
if a.FilePath != "" {
if !ann.DocumentationMatches(r.Match, a.FilePath, a.PathKind == "directory") {
continue
}
applicability = "matched"
reasons = append(reasons, "filePath")
}
if a.PathKind == "directory" && r.Target.Kind != "path" {
continue
}
if len(a.Extensions) > 0 && len(r.Match.Extensions) > 0 {
matched := false
for _, ext := range a.Extensions {
if annotationIn(r.Match.Extensions, ext) {
matched = true
}
}
if !matched {
continue
}
}
if a.PVFVersion == "all" || !effective {
applicability = "unknown"
}
raw, _ := json.Marshal([]any{r.ID, r.Description, r.Field, r.Target.Section, r.Annotation.Relation, r.Annotation})
hay := strings.ToLower(string(raw))
matched := true
score := 0
for _, term := range terms {
if !strings.Contains(hay, term) {
matched = false
break
}
score++
for _, v := range []string{r.ID, r.Target.Section, r.Annotation.Title} {
if strings.EqualFold(v, term) {
score += 10
}
}
}
if !matched {
continue
}
summary, truncated := annotationShort(strings.TrimSpace(r.Description + "\n" + r.Annotation.Content))
why := []string{}
if d.Entry.Disabled {
why = append(why, "disabled")
}
if d.Entry.Overridden {
why = append(why, "overridden")
}
if !annotationVersion(d, cat.Document, a.PVFVersion) {
why = append(why, "version_mismatch")
}
if len(terms) > 0 {
reasons = append(reasons, "query")
}
if a.Section != "" {
reasons = append(reasons, "section")
}
values := []map[string]string{}
keys := []string{}
for k := range r.Annotation.Values {
keys = append(keys, k)
}
sort.Strings(keys)
if len(terms) > 0 {
for _, k := range keys {
label := r.Annotation.Values[k]
for _, term := range terms {
if strings.Contains(strings.ToLower(k+" "+label), term) {
short, _ := annotationShort(label)
values = append(values, map[string]string{"value": k, "label": short})
break
}
}
if len(values) >= 5 {
break
}
}
}
item := map[string]any{"documentId": d.DocumentID, "kind": d.Entry.Kind, "ruleId": r.ID, "fieldId": r.Field, "title": r.Annotation.Title, "summary": summary, "summaryTruncated": truncated, "section": r.Target.Section, "targetKind": r.Target.Kind, "annotationType": r.Annotation.Type, "pvfVersions": r.PVFVersions, "effective": effective, "applicability": applicability, "matchReasons": reasons, "inactiveReasons": why, "source": annotationSource(d.Entry.Source), "valueCount": len(r.Annotation.Values), "matchedValues": values, "relation": r.Annotation.Relation}
hits = append(hits, hit{item, score, d.Entry.Kind + "\x00" + r.ID + "\x00" + d.Entry.Source + d.DocumentID})
}
sort.SliceStable(hits, func(i, j int) bool {
if hits[i].score != hits[j].score {
return hits[i].score > hits[j].score
}
return hits[i].key < hits[j].key
})
start, end, err := annotationPage(cursor, cat.Revision, binding, a.WorkspaceID, "", len(hits), a.Limit)
if err != nil {
return nil, err
}
items := []map[string]any{}
for _, h := range hits[start:end] {
items = append(items, h.item)
}
return map[string]any{"schemaVersion": 1, "rulesRevision": cat.Revision, "context": map[string]any{"workspaceId": a.WorkspaceID, "pvfVersion": a.PVFVersion, "filePath": a.FilePath}, "items": items, "totalMatches": len(hits), "nextCursor": annotationNext(end, len(hits), cat.Revision, binding, a.WorkspaceID, "")}, nil
}
func (s *MCPService) annotationGet(a annotationArgs) (any, error) {
cat, err := s.c.annotationCatalogLocked()
if err != nil {
return nil, err
}
if a.RulesRevision != cat.Revision {
return nil, fmt.Errorf("STALE_RULES")
}
var selected *annotationDocument
for i := range cat.Items {
if cat.Items[i].DocumentID == a.DocumentID {
selected = &cat.Items[i]
break
}
}
if selected == nil {
return nil, fmt.Errorf("DOCUMENT_NOT_FOUND")
}
d := *selected
r := d.Rule
if a.ValuesLimit == 0 {
a.ValuesLimit = 50
}
a.ValuesLimit = max(1, min(a.ValuesLimit, 100))
cursor := a.ValuesCursor
a.ValuesCursor = ""
binding := annotationHash(a)
keys := []string{}
for k := range r.Annotation.Values {
keys = append(keys, k)
}
sort.Strings(keys)
start, end, err := annotationPage(cursor, cat.Revision, binding, "", "", len(keys), a.ValuesLimit)
if err != nil {
return nil, err
}
values := []map[string]string{}
for _, k := range keys[start:end] {
values = append(values, map[string]string{"value": k, "label": r.Annotation.Values[k]})
}
// Enum maps are paged separately, never duplicated inside raw definitions.
raw, _ := json.Marshal(d.Original)
var original map[string]any
_ = json.Unmarshal(raw, &original)
if annotation, ok := original["annotation"].(map[string]any); ok {
delete(annotation, "values")
}
resolved := r
resolved.Annotation.Values = nil
relations := map[string]ann.RelationSpec{}
warnings := []string{}
if d.ResolutionError != "" {
warnings = append(warnings, d.ResolutionError)
}
visiting := map[string]bool{}
var visit func(string)
visit = func(name string) {
if name == "" {
return
}
if visiting[name] {
warnings = append(warnings, "relation_cycle:"+name)
return
}
if _, ok := relations[name]; ok {
return
}
spec, ok := cat.Document.Relations[name]
if !ok {
warnings = append(warnings, "missing_relation:"+name)
return
}
visiting[name] = true
relations[name] = spec
for _, child := range spec.Relations {
visit(child)
}
visiting[name] = false
}
visit(r.Annotation.Relation)
result := map[string]any{"schemaVersion": 1, "rulesRevision": cat.Revision, "documentId": d.DocumentID, "kind": d.Entry.Kind, "ruleId": r.ID, "source": annotationSource(d.Entry.Source), "effective": d.Entry.Effective, "disabled": d.Entry.Disabled, "overridden": d.Entry.Overridden, "originalDefinition": original, "resolvedDefinition": resolved, "preview": d.Preview, "valueCount": len(keys), "values": values, "definitionsOmitValues": true, "nextValuesCursor": annotationNext(end, len(keys), cat.Revision, binding, "", ""), "relations": relations, "warnings": warnings}
if a.Value != nil {
label, found := r.Annotation.Values[*a.Value]
result["value"] = map[string]any{"value": *a.Value, "found": found, "label": label}
}
return result, nil
}