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