tray.go 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  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. if err := sup.SetAutoStart(next); err != nil {
  113. log.Printf("保存配置失败: %v", err)
  114. }
  115. log.Printf("开机自启 = %v", next)
  116. case <-mAutoUpdate.ClickedCh:
  117. next := !mAutoUpdate.Checked()
  118. if next {
  119. mAutoUpdate.Check()
  120. } else {
  121. mAutoUpdate.Uncheck()
  122. }
  123. if err := sup.SetAutoUpdate(next); err != nil {
  124. log.Printf("保存配置失败: %v", err)
  125. }
  126. log.Printf("定期自动更新 = %v", next)
  127. case <-mUpdateNow.ClickedCh:
  128. go func() {
  129. log.Printf("菜单:立即更新(先停服务再更新)")
  130. if _, err := sup.UpdateAndRestart(); err != nil {
  131. log.Printf("更新失败: %v", err)
  132. } else {
  133. log.Printf("更新完成,服务已恢复运行")
  134. }
  135. mVersion.SetTitle("版本:" + sup.installedVersion())
  136. }()
  137. case <-mLogs.ClickedCh:
  138. openTarget(logDir())
  139. case <-mConfig.ClickedCh:
  140. openTarget(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 statusTitle(st Status) string {
  154. switch {
  155. case st.Updating:
  156. return "状态:正在更新…"
  157. case st.Running:
  158. return fmt.Sprintf("状态:运行中 (PID %d)", st.PID)
  159. case st.External:
  160. return "状态:外部实例运行中"
  161. default:
  162. return "状态:已停止"
  163. }
  164. }
  165. func statusTooltip(st Status) string {
  166. parts := make([]string, 0, 3)
  167. if st.Running {
  168. parts = append(parts, "运行 "+humanDuration(st.Uptime))
  169. }
  170. if st.Restarts > 0 {
  171. parts = append(parts, fmt.Sprintf("自动重启 %d 次", st.Restarts))
  172. }
  173. if st.LastError != "" {
  174. parts = append(parts, "最近错误:"+st.LastError)
  175. }
  176. if len(parts) == 0 {
  177. return "服务未运行"
  178. }
  179. return strings.Join(parts, ";")
  180. }
  181. // openTarget 用系统默认程序打开 URL 或路径,不经 shell 解析(避免注入面)。
  182. func openTarget(target string) {
  183. if target == "" {
  184. return
  185. }
  186. var cmd *exec.Cmd
  187. if strings.HasPrefix(target, "http://") || strings.HasPrefix(target, "https://") {
  188. cmd = exec.Command("rundll32.exe", "url.dll,FileProtocolHandler", target)
  189. } else {
  190. cmd = exec.Command("explorer.exe", target)
  191. }
  192. hideWindow(cmd)
  193. _ = cmd.Start()
  194. }