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