package main import ( "bytes" "encoding/json" "fmt" "os" "path/filepath" "sync" ) const appDirName = "deepseek-tray" // Config 托盘程序配置,存于 %APPDATA%\deepseek-tray\config.json。 type Config struct { WebPort int `json:"webPort"` // dsh web 监听端口(默认 3080) WebHost string `json:"webHost"` // 绑定地址(默认 127.0.0.1) NodePath string `json:"nodePath"` // node 可执行路径(留空自动探测) DshBinJS string `json:"dshBinJs"` // dsh 入口 JS(留空自动探测) AutoStart bool `json:"autoStart"` // 开机自启(注册表 Run 键) AutoUpdate bool `json:"autoUpdate"` // 定期后台更新 UpdateIntervalH int `json:"updateIntervalHours"` // 定期更新间隔(小时) RestartDelaySec int `json:"restartDelaySeconds"` // 异常退出后重启基准延迟(秒) } func defaultConfig() *Config { return &Config{ WebPort: 3080, WebHost: "127.0.0.1", UpdateIntervalH: 24, RestartDelaySec: 5, } } var ( dataDirOnce sync.Once dataDirPath string ) // appDataDir 返回配置与日志目录(仅首次创建)。 func appDataDir() string { dataDirOnce.Do(func() { base := os.Getenv("APPDATA") if base == "" { if home, err := os.UserHomeDir(); err == nil { base = home } else { base = "." } } dataDirPath = filepath.Join(base, appDirName) _ = os.MkdirAll(dataDirPath, 0o755) }) return dataDirPath } func configPath() string { return filepath.Join(appDataDir(), "config.json") } func logDir() string { return filepath.Join(appDataDir(), "logs") } // loadConfig 读取配置;缺失时落盘默认值,损坏时回退默认值。 // 兼容 UTF-8 BOM(Windows 记事本编辑后会带 BOM)。 func loadConfig() *Config { cfg := defaultConfig() data, err := os.ReadFile(configPath()) if err != nil { _ = saveConfig(cfg) return cfg } data = bytes.TrimPrefix(data, []byte{0xEF, 0xBB, 0xBF}) if err := json.Unmarshal(data, cfg); err != nil { return defaultConfig() } return cfg.normalized() } // normalized 修正越界字段,保证运行期取值始终合法。 func (c *Config) normalized() *Config { d := defaultConfig() if c.WebPort <= 0 || c.WebPort > 65535 { c.WebPort = d.WebPort } if c.WebHost == "" { c.WebHost = d.WebHost } if c.UpdateIntervalH <= 0 { c.UpdateIntervalH = d.UpdateIntervalH } if c.RestartDelaySec <= 0 { c.RestartDelaySec = d.RestartDelaySec } return c } // saveConfig 原子写入配置(临时文件 + rename),避免中断导致文件损坏。 func saveConfig(cfg *Config) error { data, err := json.MarshalIndent(cfg, "", " ") if err != nil { return fmt.Errorf("序列化配置失败: %w", err) } path := configPath() tmp := path + ".tmp" if err := os.WriteFile(tmp, data, 0o600); err != nil { return fmt.Errorf("写入临时配置失败: %w", err) } if err := os.Rename(tmp, path); err != nil { _ = os.Remove(tmp) return fmt.Errorf("替换配置失败: %w", err) } return nil }