config.go 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
  1. package main
  2. import (
  3. "bytes"
  4. "encoding/json"
  5. "fmt"
  6. "os"
  7. "path/filepath"
  8. "sync"
  9. )
  10. const appDirName = "deepseek-tray"
  11. // Config 托盘程序配置,存于 %APPDATA%\deepseek-tray\config.json。
  12. type Config struct {
  13. WebPort int `json:"webPort"` // dsh web 监听端口(默认 3080)
  14. WebHost string `json:"webHost"` // 绑定地址(默认 127.0.0.1)
  15. NodePath string `json:"nodePath"` // node 可执行路径(留空自动探测)
  16. DshBinJS string `json:"dshBinJs"` // dsh 入口 JS(留空自动探测)
  17. AutoStart bool `json:"autoStart"` // 开机自启(注册表 Run 键)
  18. AutoUpdate bool `json:"autoUpdate"` // 定期后台更新
  19. UpdateIntervalH int `json:"updateIntervalHours"` // 定期更新间隔(小时)
  20. RestartDelaySec int `json:"restartDelaySeconds"` // 异常退出后重启基准延迟(秒)
  21. }
  22. func defaultConfig() *Config {
  23. return &Config{
  24. WebPort: 3080,
  25. WebHost: "127.0.0.1",
  26. UpdateIntervalH: 24,
  27. RestartDelaySec: 5,
  28. }
  29. }
  30. var (
  31. dataDirOnce sync.Once
  32. dataDirPath string
  33. )
  34. // appDataDir 返回配置与日志目录(仅首次创建)。
  35. func appDataDir() string {
  36. dataDirOnce.Do(func() {
  37. base := os.Getenv("APPDATA")
  38. if base == "" {
  39. if home, err := os.UserHomeDir(); err == nil {
  40. base = home
  41. } else {
  42. base = "."
  43. }
  44. }
  45. dataDirPath = filepath.Join(base, appDirName)
  46. _ = os.MkdirAll(dataDirPath, 0o755)
  47. })
  48. return dataDirPath
  49. }
  50. func configPath() string {
  51. return filepath.Join(appDataDir(), "config.json")
  52. }
  53. func logDir() string {
  54. return filepath.Join(appDataDir(), "logs")
  55. }
  56. // loadConfig 读取配置;缺失时落盘默认值,损坏时回退默认值。
  57. // 兼容 UTF-8 BOM(Windows 记事本编辑后会带 BOM)。
  58. func loadConfig() *Config {
  59. cfg := defaultConfig()
  60. data, err := os.ReadFile(configPath())
  61. if err != nil {
  62. _ = saveConfig(cfg)
  63. return cfg
  64. }
  65. data = bytes.TrimPrefix(data, []byte{0xEF, 0xBB, 0xBF})
  66. if err := json.Unmarshal(data, cfg); err != nil {
  67. return defaultConfig()
  68. }
  69. return cfg.normalized()
  70. }
  71. // normalized 修正越界字段,保证运行期取值始终合法。
  72. func (c *Config) normalized() *Config {
  73. d := defaultConfig()
  74. if c.WebPort <= 0 || c.WebPort > 65535 {
  75. c.WebPort = d.WebPort
  76. }
  77. if c.WebHost == "" {
  78. c.WebHost = d.WebHost
  79. }
  80. if c.UpdateIntervalH <= 0 {
  81. c.UpdateIntervalH = d.UpdateIntervalH
  82. }
  83. if c.RestartDelaySec <= 0 {
  84. c.RestartDelaySec = d.RestartDelaySec
  85. }
  86. return c
  87. }
  88. // saveConfig 原子写入配置(临时文件 + rename),避免中断导致文件损坏。
  89. func saveConfig(cfg *Config) error {
  90. data, err := json.MarshalIndent(cfg, "", " ")
  91. if err != nil {
  92. return fmt.Errorf("序列化配置失败: %w", err)
  93. }
  94. path := configPath()
  95. tmp := path + ".tmp"
  96. if err := os.WriteFile(tmp, data, 0o600); err != nil {
  97. return fmt.Errorf("写入临时配置失败: %w", err)
  98. }
  99. if err := os.Rename(tmp, path); err != nil {
  100. _ = os.Remove(tmp)
  101. return fmt.Errorf("替换配置失败: %w", err)
  102. }
  103. return nil
  104. }