config.go 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119
  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. dataDirForced string // 测试注入:非空时直接使用(便于隔离文件系统)
  34. )
  35. // appDataDir 返回配置与日志目录(仅首次创建)。
  36. func appDataDir() string {
  37. if dataDirForced != "" {
  38. return dataDirForced
  39. }
  40. dataDirOnce.Do(func() {
  41. base := os.Getenv("APPDATA")
  42. if base == "" {
  43. if home, err := os.UserHomeDir(); err == nil {
  44. base = home
  45. } else {
  46. base = "."
  47. }
  48. }
  49. dataDirPath = filepath.Join(base, appDirName)
  50. _ = os.MkdirAll(dataDirPath, 0o755)
  51. })
  52. return dataDirPath
  53. }
  54. func configPath() string {
  55. return filepath.Join(appDataDir(), "config.json")
  56. }
  57. func logDir() string {
  58. return filepath.Join(appDataDir(), "logs")
  59. }
  60. // loadConfig 读取配置;缺失时落盘默认值,损坏时回退默认值。
  61. // 兼容 UTF-8 BOM(Windows 记事本编辑后会带 BOM)。
  62. func loadConfig() *Config {
  63. cfg := defaultConfig()
  64. data, err := os.ReadFile(configPath())
  65. if err != nil {
  66. _ = saveConfig(cfg)
  67. return cfg
  68. }
  69. data = bytes.TrimPrefix(data, []byte{0xEF, 0xBB, 0xBF})
  70. if err := json.Unmarshal(data, cfg); err != nil {
  71. return defaultConfig()
  72. }
  73. return cfg.normalized()
  74. }
  75. // normalized 修正越界字段,保证运行期取值始终合法。
  76. func (c *Config) normalized() *Config {
  77. d := defaultConfig()
  78. if c.WebPort <= 0 || c.WebPort > 65535 {
  79. c.WebPort = d.WebPort
  80. }
  81. if c.WebHost == "" {
  82. c.WebHost = d.WebHost
  83. }
  84. if c.UpdateIntervalH <= 0 {
  85. c.UpdateIntervalH = d.UpdateIntervalH
  86. }
  87. if c.RestartDelaySec <= 0 {
  88. c.RestartDelaySec = d.RestartDelaySec
  89. }
  90. return c
  91. }
  92. // saveConfig 原子写入配置(临时文件 + rename),避免中断导致文件损坏。
  93. func saveConfig(cfg *Config) error {
  94. data, err := json.MarshalIndent(cfg, "", " ")
  95. if err != nil {
  96. return fmt.Errorf("序列化配置失败: %w", err)
  97. }
  98. path := configPath()
  99. tmp := path + ".tmp"
  100. if err := os.WriteFile(tmp, data, 0o600); err != nil {
  101. return fmt.Errorf("写入临时配置失败: %w", err)
  102. }
  103. if err := os.Rename(tmp, path); err != nil {
  104. _ = os.Remove(tmp)
  105. return fmt.Errorf("替换配置失败: %w", err)
  106. }
  107. return nil
  108. }