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,197 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
)
|
||||
|
||||
//go:embed builtin_skills
|
||||
var builtinSkills embed.FS
|
||||
|
||||
type BuiltinSkill struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
type SkillInstallResult struct {
|
||||
ID string `json:"id"`
|
||||
Path string `json:"path"`
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type SkillService struct{ mu sync.Mutex }
|
||||
|
||||
func NewSkillService() *SkillService { return &SkillService{} }
|
||||
|
||||
func (s *SkillService) List() []BuiltinSkill {
|
||||
return []BuiltinSkill{
|
||||
{"pvfine-drop-rates", "掉率读取与修改", "深渊、翻牌、普通与精英怪物基础掉率,以及全局掉落权重。"},
|
||||
{"pvfine-skill-values", "技能数值读取与修改", "区分地下城与 PVP,定位静态参数、等级数据和技能说明绑定。"},
|
||||
{"pvfine-upgrade", "强化、增幅与锻造", "识别不同版本的概率刻度、失败处理、材料与金币成本。"},
|
||||
{"pvfine-npc-shops", "NPC 商店读取与修改", "定位商店分页、分类与商品,修改金币价格和兑换材料。"},
|
||||
{"pvfine-quests", "任务读取与修改", "读取与修改任务条件、NPC、对话、固定奖励与自选奖励。"},
|
||||
}
|
||||
}
|
||||
|
||||
// ResolveDirectory returns the exact skill root shown before installation.
|
||||
func (s *SkillService) ResolveDirectory(target, directory string) (string, error) {
|
||||
switch target {
|
||||
case "global", "claude":
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
tool := ".agents"
|
||||
if target == "claude" {
|
||||
tool = ".claude"
|
||||
}
|
||||
return filepath.Join(home, tool, "skills"), nil
|
||||
case "project", "custom":
|
||||
directory = strings.TrimSpace(directory)
|
||||
if !filepath.IsAbs(directory) {
|
||||
return "", fmt.Errorf("请选择或输入绝对目录路径")
|
||||
}
|
||||
directory = filepath.Clean(directory)
|
||||
if target == "project" {
|
||||
return filepath.Join(directory, ".agents", "skills"), nil
|
||||
}
|
||||
return directory, nil
|
||||
default:
|
||||
return "", fmt.Errorf("未知的 skill 安装位置")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SkillService) ChooseDirectory() (string, error) {
|
||||
return application.Get().Dialog.OpenFile().CanChooseFiles(false).CanChooseDirectories(true).
|
||||
CanCreateDirectories(true).SetTitle("选择 skill 安装目录或项目根目录").PromptForSingleSelection()
|
||||
}
|
||||
|
||||
// Install skips existing skills unless overwrite is selected. Updates replace
|
||||
// bundled files while preserving extra user files and rejecting symlink conflicts.
|
||||
func (s *SkillService) Install(ids []string, target, directory string, overwrite bool) ([]SkillInstallResult, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
root, err := s.ResolveDirectory(target, directory)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(ids) == 0 {
|
||||
return nil, fmt.Errorf("请至少选择一个 skill")
|
||||
}
|
||||
allowed := map[string]bool{}
|
||||
for _, skill := range s.List() {
|
||||
allowed[skill.ID] = true
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for _, id := range ids {
|
||||
if !allowed[id] || seen[id] {
|
||||
return nil, fmt.Errorf("skill 选择无效或重复")
|
||||
}
|
||||
seen[id] = true
|
||||
}
|
||||
if err := os.MkdirAll(root, 0755); err != nil {
|
||||
return nil, fmt.Errorf("创建 skill 根目录失败: %w", err)
|
||||
}
|
||||
results := make([]SkillInstallResult, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
destination := filepath.Join(root, id)
|
||||
result := SkillInstallResult{ID: id, Path: destination, Status: "installed", Message: "安装成功"}
|
||||
created := false
|
||||
if err := os.Mkdir(destination, 0755); err != nil {
|
||||
if os.IsExist(err) && !overwrite {
|
||||
result.Status = "skipped"
|
||||
result.Message = "已存在,保留原内容"
|
||||
} else if os.IsExist(err) {
|
||||
result.Status = "updated"
|
||||
result.Message = "已覆盖更新"
|
||||
} else {
|
||||
result.Status = "failed"
|
||||
result.Message = err.Error()
|
||||
}
|
||||
} else {
|
||||
created = true
|
||||
}
|
||||
if result.Status == "installed" || result.Status == "updated" {
|
||||
if err := writeBuiltinSkill(id, destination); err != nil {
|
||||
if created {
|
||||
_ = os.RemoveAll(destination)
|
||||
}
|
||||
result.Status = "failed"
|
||||
result.Message = err.Error()
|
||||
}
|
||||
}
|
||||
results = append(results, result)
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func writeBuiltinSkill(id, destination string) error {
|
||||
source := "builtin_skills/" + id
|
||||
// Check every managed path before writing, so structural conflicts cannot
|
||||
// leave the entrypoint updated while a bundled reference remains outdated.
|
||||
err := fs.WalkDir(builtinSkills, source, func(path string, entry fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
relative := strings.TrimPrefix(strings.TrimPrefix(path, source), "/")
|
||||
output := filepath.Join(destination, filepath.FromSlash(relative))
|
||||
info, err := os.Lstat(output)
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info.Mode()&os.ModeSymlink != 0 || (entry.IsDir() && !info.IsDir()) || (!entry.IsDir() && !info.Mode().IsRegular()) {
|
||||
return fmt.Errorf("安装路径存在符号链接或文件类型冲突: %s", output)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fs.WalkDir(builtinSkills, source, func(path string, entry fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
relative := strings.TrimPrefix(strings.TrimPrefix(path, source), "/")
|
||||
output := filepath.Join(destination, filepath.FromSlash(relative))
|
||||
if entry.IsDir() {
|
||||
return os.MkdirAll(output, 0755)
|
||||
}
|
||||
content, err := builtinSkills.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return replaceBuiltinSkillFile(output, content)
|
||||
})
|
||||
}
|
||||
|
||||
func replaceBuiltinSkillFile(path string, content []byte) error {
|
||||
file, err := os.CreateTemp(filepath.Dir(path), ".pvfine-skill-*")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(file.Name())
|
||||
if _, err := file.Write(content); err != nil {
|
||||
file.Close()
|
||||
return err
|
||||
}
|
||||
if err := file.Chmod(0644); err != nil {
|
||||
file.Close()
|
||||
return err
|
||||
}
|
||||
if err := file.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Rename(file.Name(), path)
|
||||
}
|
||||
Reference in new issue
Block a user