tray.go 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. package main
  2. import (
  3. _ "embed"
  4. "fmt"
  5. "os"
  6. "os/exec"
  7. "strings"
  8. "time"
  9. "github.com/getlantern/systray"
  10. )
  11. //go:embed icon.ico
  12. var iconData []byte
  13. func runTray(cfg *Config, log *Logger) {
  14. sup := NewSupervisor(cfg, log)
  15. systray.Run(func() {
  16. systray.SetIcon(iconData)
  17. systray.SetTooltip("DeepSeek Tray - dsh web 守护")
  18. trayReady(sup, cfg, log)
  19. }, func() {
  20. log.Printf("托盘退出,停止服务")
  21. sup.Stop()
  22. })
  23. }
  24. func trayReady(sup *Supervisor, cfg *Config, log *Logger) {
  25. mStatus := systray.AddMenuItem("状态:初始化…", "当前服务状态")
  26. mURL := systray.AddMenuItem("打开 Web 界面", cfgURL(cfg))
  27. systray.AddSeparator()
  28. mStart := systray.AddMenuItem("启动服务", "启动 dsh web")
  29. mStop := systray.AddMenuItem("停止服务", "停止 dsh web")
  30. mRestart := systray.AddMenuItem("重启服务", "重启 dsh web")
  31. systray.AddSeparator()
  32. mAutoStart := systray.AddMenuItemCheckbox("开机自启", "登录 Windows 时自动启动托盘并守护服务", isAutoStartEnabled())
  33. mAutoUpdate := systray.AddMenuItemCheckbox("定期自动更新", "按间隔检查并更新 dsh", cfg.AutoUpdate)
  34. mUpdateNow := systray.AddMenuItem("立即更新 DSH", "npm install -g @deepseek-ai/dsh@latest")
  35. systray.AddSeparator()
  36. mVersion := systray.AddMenuItem("版本:查询中…", "已安装的 DSH 版本")
  37. mLogs := systray.AddMenuItem("查看日志", "打开日志目录")
  38. mConfig := systray.AddMenuItem("打开配置文件", "打开 config.json")
  39. systray.AddSeparator()
  40. mQuit := systray.AddMenuItem("退出", "停止服务并退出托盘")
  41. mStop.Disable()
  42. // 状态刷新(3 秒)
  43. go func() {
  44. ticker := time.NewTicker(3 * time.Second)
  45. defer ticker.Stop()
  46. for range ticker.C {
  47. st := sup.Status()
  48. switch {
  49. case st.Updating:
  50. mStatus.SetTitle("状态:正在更新…")
  51. case st.Running:
  52. mStatus.SetTitle(fmt.Sprintf("状态:运行中 (PID %d)", st.PID))
  53. default:
  54. mStatus.SetTitle("状态:已停止")
  55. }
  56. if st.Restarts > 0 {
  57. mStatus.SetTooltip(fmt.Sprintf("自动重启次数:%d;最近错误:%s", st.Restarts, st.LastError))
  58. } else {
  59. mStatus.SetTooltip("服务运行正常")
  60. }
  61. mURL.SetTitle("打开 Web 界面:" + shortURL(st.URL))
  62. if st.Running {
  63. mStart.Disable()
  64. mStop.Enable()
  65. } else {
  66. mStart.Enable()
  67. mStop.Disable()
  68. }
  69. }
  70. }()
  71. // 版本刷新(启动一次 + 每 10 分钟)
  72. go func() {
  73. for {
  74. v := sup.installedVersion()
  75. if v == "" {
  76. v = "未知"
  77. }
  78. mVersion.SetTitle("版本:" + v)
  79. time.Sleep(10 * time.Minute)
  80. }
  81. }()
  82. // 菜单事件
  83. go func() {
  84. for {
  85. select {
  86. case <-mStart.ClickedCh:
  87. log.Printf("菜单:启动服务")
  88. sup.Start()
  89. case <-mStop.ClickedCh:
  90. log.Printf("菜单:停止服务")
  91. sup.Stop()
  92. case <-mRestart.ClickedCh:
  93. log.Printf("菜单:重启服务")
  94. sup.Restart()
  95. case <-mURL.ClickedCh:
  96. status := sup.Status()
  97. log.Printf("菜单:打开 Web %s", status.URL)
  98. openURL(status.URL)
  99. case <-mAutoStart.ClickedCh:
  100. next := !mAutoStart.Checked()
  101. if err := setAutoStart(next); err != nil {
  102. log.Printf("设置开机自启失败: %v", err)
  103. continue
  104. }
  105. if next {
  106. mAutoStart.Check()
  107. } else {
  108. mAutoStart.Uncheck()
  109. }
  110. cfg.AutoStart = next
  111. _ = saveConfig(cfg)
  112. log.Printf("开机自启 = %v", next)
  113. case <-mAutoUpdate.ClickedCh:
  114. next := !mAutoUpdate.Checked()
  115. if next {
  116. mAutoUpdate.Check()
  117. } else {
  118. mAutoUpdate.Uncheck()
  119. }
  120. cfg.AutoUpdate = next
  121. _ = saveConfig(cfg)
  122. log.Printf("定期自动更新 = %v(间隔 %d 小时)", next, cfg.UpdateIntervalH)
  123. if next {
  124. sup.startAutoUpdate()
  125. }
  126. case <-mUpdateNow.ClickedCh:
  127. go func() {
  128. log.Printf("菜单:立即更新")
  129. if _, err := sup.Update(); err != nil {
  130. log.Printf("更新失败: %v", err)
  131. return
  132. }
  133. log.Printf("更新完成,重启服务")
  134. sup.Restart()
  135. mVersion.SetTitle("版本:" + sup.installedVersion())
  136. }()
  137. case <-mLogs.ClickedCh:
  138. openURL(appDataDir() + string(os.PathSeparator) + "logs")
  139. case <-mConfig.ClickedCh:
  140. openURL(configPath())
  141. case <-mQuit.ClickedCh:
  142. log.Printf("菜单:退出")
  143. systray.Quit()
  144. return
  145. }
  146. }
  147. }()
  148. sup.Start()
  149. if cfg.AutoUpdate {
  150. sup.startAutoUpdate()
  151. }
  152. }
  153. func cfgURL(cfg *Config) string {
  154. host := cfg.WebHost
  155. if host == "" {
  156. host = "127.0.0.1"
  157. }
  158. return fmt.Sprintf("打开 Web 界面:http://%s:%d", host, cfg.WebPort)
  159. }
  160. // shortURL 菜单标题只显示 origin,避免 token 撑爆标题。
  161. func shortURL(u string) string {
  162. if i := strings.Index(u, "?"); i > 0 {
  163. return u[:i]
  164. }
  165. return u
  166. }
  167. // openURL 用系统默认程序打开 URL 或路径(隐藏窗口)。
  168. func openURL(target string) {
  169. cmd := exec.Command("cmd", "/c", "start", "", target)
  170. hideWindow(cmd)
  171. _ = cmd.Start()
  172. }