DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
commit
645b64fe21
4388 files changed
+1405457
No files matched your search
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user