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

198 lines
5.9 KiB
Go

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)
}