tray.go 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. package main
  2. import (
  3. _ "embed"
  4. "fmt"
  5. "os/exec"
  6. "strings"
  7. "time"
  8. "github.com/getlantern/systray"
  9. )
  10. //go:embed icon.ico
  11. var iconData []byte
  12. func runTray(cfg *Config, log *Logger) {
  13. sup := NewSupervisor(cfg, log)
  14. systray.Run(func() {
  15. systray.SetIcon(iconData)
  16. systray.SetTooltip("DeepSeek Tray - dsh web 守护")
  17. trayReady(sup, cfg, log)
  18. }, func() {
  19. log.Printf("托盘退出,停止服务")
  20. sup.Stop()
  21. })
  22. }
  23. func trayReady(sup *Supervisor, cfg *Config, log *Logger) {
  24. mStatus := systray.AddMenuItem("状态:初始化…", "当前服务状态")
  25. mURL := systray.AddMenuItem("打开 Web 界面", "")
  26. systray.AddSeparator()
  27. mStart := systray.AddMenuItem("启动服务", "启动 dsh web")
  28. mStop := systray.AddMenuItem("停止服务", "停止 dsh web")
  29. mRestart := systray.AddMenuItem("重启服务", "重启 dsh web")
  30. systray.AddSeparator()
  31. mAutoStart := systray.AddMenuItemCheckbox("开机自启", "登录 Windows 时自动启动托盘并守护服务(无窗口、不打开浏览器)", isAutoStartEnabled())
  32. mAutoUpdate := systray.AddMenuItemCheckbox("定期自动更新", "按间隔检查并更新 dsh", cfg.AutoUpdate)
  33. mUpdateNow := systray.AddMenuItem("立即更新 DSH", "npm install -g @deepseek-ai/dsh@latest")
  34. systray.AddSeparator()
  35. mVersion := systray.AddMenuItem("版本:查询中…", "已安装的 DSH 版本")
  36. mLogs := systray.AddMenuItem("查看日志", "打开日志目录")
  37. mConfig := systray.AddMenuItem("打开配置文件", "打开 config.json")
  38. systray.AddSeparator()
  39. mQuit := systray.AddMenuItem("退出", "停止服务并退出托盘")
  40. mStop.Disable()
  41. // 状态刷新:仅在有变化时写 UI(减少 systray 调用)
  42. go func() {
  43. ticker := time.NewTicker(3 * time.Second)
  44. defer ticker.Stop()
  45. var lastTitle, lastTip, lastURL string
  46. lastRunning := false
  47. for range ticker.C {
  48. st := sup.Status()
  49. if title := statusTitle(st); title != lastTitle {
  50. mStatus.SetTitle(title)
  51. lastTitle = title
  52. }
  53. if tip := statusTooltip(st); tip != lastTip {
  54. mStatus.SetTooltip(tip)
  55. lastTip = tip
  56. }
  57. if u := "打开 Web 界面:" + shortURL(st.URL); u != lastURL {
  58. mURL.SetTitle(u)
  59. lastURL = u
  60. }
  61. if st.Running != lastRunning {
  62. if st.Running {
  63. mStart.Disable()
  64. mStop.Enable()
  65. } else {
  66. mStart.Enable()
  67. mStop.Disable()
  68. }
  69. lastRunning = st.Running
  70. }
  71. }
  72. }()
  73. // 版本刷新(启动一次 + 每 10 分钟)
  74. go func() {
  75. for {
  76. v := sup.installedVersion()
  77. if v == "" {
  78. v = "未知"
  79. }
  80. mVersion.SetTitle("版本:" + v)
  81. time.Sleep(10 * time.Minute)
  82. }
  83. }()
  84. // 菜单事件
  85. go func() {
  86. for {
  87. select {
  88. case <-mStart.ClickedCh:
  89. log.Printf("菜单:启动服务")
  90. sup.Start()
  91. case <-mStop.ClickedCh:
  92. log.Printf("菜单:停止服务")
  93. sup.Stop()
  94. case <-mRestart.ClickedCh:
  95. log.Printf("菜单:重启服务")
  96. sup.Restart()
  97. case <-mURL.ClickedCh:
  98. target := sup.Status().URL
  99. log.Printf("菜单:打开 Web 界面")
  100. openTarget(target)
  101. case <-mAutoStart.ClickedCh:
  102. next := !mAutoStart.Checked()
  103. if err := setAutoStart(next); err != nil {
  104. log.Printf("设置开机自启失败: %v", err)
  105. continue
  106. }
  107. if next {
  108. mAutoStart.Check()
  109. } else {
  110. mAutoStart.Uncheck()
  111. }
  112. cfg.AutoStart = next
  113. if err := saveConfig(cfg); err != nil {
  114. log.Printf("保存配置失败: %v", err)
  115. }
  116. log.Printf("开机自启 = %v", next)
  117. case <-mAutoUpdate.ClickedCh:
  118. next := !mAutoUpdate.Checked()
  119. if next {
  120. mAutoUpdate.Check()
  121. } else {
  122. mAutoUpdate.Uncheck()
  123. }
  124. cfg.AutoUpdate = next
  125. if err := saveConfig(cfg); err != nil {
  126. log.Printf("保存配置失败: %v", err)
  127. }
  128. log.Printf("定期自动更新 = %v(间隔 %d 小时)", next, cfg.UpdateIntervalH)
  129. if next {
  130. sup.startAutoUpdate()
  131. }
  132. case <-mUpdateNow.ClickedCh:
  133. go func() {
  134. log.Printf("菜单:立即更新")
  135. if _, err := sup.Update(); err != nil {
  136. log.Printf("更新失败: %v", err)
  137. return
  138. }
  139. log.Printf("更新完成,重启服务")
  140. sup.Restart()
  141. mVersion.SetTitle("版本:" + sup.installedVersion())
  142. }()
  143. case <-mLogs.ClickedCh:
  144. openTarget(logDir())
  145. case <-mConfig.ClickedCh:
  146. openTarget(configPath())
  147. case <-mQuit.ClickedCh:
  148. log.Printf("菜单:退出")
  149. systray.Quit()
  150. return
  151. }
  152. }
  153. }()
  154. sup.Start()
  155. if cfg.AutoUpdate {
  156. sup.startAutoUpdate()
  157. }
  158. }
  159. func statusTitle(st Status) string {
  160. switch {
  161. case st.Updating:
  162. return "状态:正在更新…"
  163. case st.Running:
  164. return fmt.Sprintf("状态:运行中 (PID %d)", st.PID)
  165. case st.External:
  166. return "状态:外部实例运行中"
  167. default:
  168. return "状态:已停止"
  169. }
  170. }
  171. func statusTooltip(st Status) string {
  172. parts := make([]string, 0, 3)
  173. if st.Running {
  174. parts = append(parts, "运行 "+humanDuration(st.Uptime))
  175. }
  176. if st.Restarts > 0 {
  177. parts = append(parts, fmt.Sprintf("自动重启 %d 次", st.Restarts))
  178. }
  179. if st.LastError != "" {
  180. parts = append(parts, "最近错误:"+st.LastError)
  181. }
  182. if len(parts) == 0 {
  183. return "服务未运行"
  184. }
  185. return strings.Join(parts, ";")
  186. }
  187. // openTarget 用系统默认程序打开 URL 或路径,不经 shell 解析(避免注入面)。
  188. func openTarget(target string) {
  189. if target == "" {
  190. return
  191. }
  192. var cmd *exec.Cmd
  193. if strings.HasPrefix(target, "http://") || strings.HasPrefix(target, "https://") {
  194. cmd = exec.Command("rundll32.exe", "url.dll,FileProtocolHandler", target)
  195. } else {
  196. cmd = exec.Command("explorer.exe", target)
  197. }
  198. hideWindow(cmd)
  199. _ = cmd.Start()
  200. }