package main import ( _ "embed" "fmt" "os" "os/exec" "strings" "time" "github.com/getlantern/systray" ) //go:embed icon.ico var iconData []byte func runTray(cfg *Config, log *Logger) { sup := NewSupervisor(cfg, log) systray.Run(func() { systray.SetIcon(iconData) systray.SetTooltip("DeepSeek Tray - dsh web 守护") trayReady(sup, cfg, log) }, func() { log.Printf("托盘退出,停止服务") sup.Stop() }) } func trayReady(sup *Supervisor, cfg *Config, log *Logger) { mStatus := systray.AddMenuItem("状态:初始化…", "当前服务状态") mURL := systray.AddMenuItem("打开 Web 界面", cfgURL(cfg)) systray.AddSeparator() mStart := systray.AddMenuItem("启动服务", "启动 dsh web") mStop := systray.AddMenuItem("停止服务", "停止 dsh web") mRestart := systray.AddMenuItem("重启服务", "重启 dsh web") systray.AddSeparator() mAutoStart := systray.AddMenuItemCheckbox("开机自启", "登录 Windows 时自动启动托盘并守护服务", isAutoStartEnabled()) mAutoUpdate := systray.AddMenuItemCheckbox("定期自动更新", "按间隔检查并更新 dsh", cfg.AutoUpdate) mUpdateNow := systray.AddMenuItem("立即更新 DSH", "npm install -g @deepseek-ai/dsh@latest") systray.AddSeparator() mVersion := systray.AddMenuItem("版本:查询中…", "已安装的 DSH 版本") mLogs := systray.AddMenuItem("查看日志", "打开日志目录") mConfig := systray.AddMenuItem("打开配置文件", "打开 config.json") systray.AddSeparator() mQuit := systray.AddMenuItem("退出", "停止服务并退出托盘") mStop.Disable() // 状态刷新(3 秒) go func() { ticker := time.NewTicker(3 * time.Second) defer ticker.Stop() for range ticker.C { st := sup.Status() switch { case st.Updating: mStatus.SetTitle("状态:正在更新…") case st.Running: mStatus.SetTitle(fmt.Sprintf("状态:运行中 (PID %d)", st.PID)) default: mStatus.SetTitle("状态:已停止") } if st.Restarts > 0 { mStatus.SetTooltip(fmt.Sprintf("自动重启次数:%d;最近错误:%s", st.Restarts, st.LastError)) } else { mStatus.SetTooltip("服务运行正常") } mURL.SetTitle("打开 Web 界面:" + shortURL(st.URL)) if st.Running { mStart.Disable() mStop.Enable() } else { mStart.Enable() mStop.Disable() } } }() // 版本刷新(启动一次 + 每 10 分钟) go func() { for { v := sup.installedVersion() if v == "" { v = "未知" } mVersion.SetTitle("版本:" + v) time.Sleep(10 * time.Minute) } }() // 菜单事件 go func() { for { select { case <-mStart.ClickedCh: log.Printf("菜单:启动服务") sup.Start() case <-mStop.ClickedCh: log.Printf("菜单:停止服务") sup.Stop() case <-mRestart.ClickedCh: log.Printf("菜单:重启服务") sup.Restart() case <-mURL.ClickedCh: status := sup.Status() log.Printf("菜单:打开 Web %s", status.URL) openURL(status.URL) case <-mAutoStart.ClickedCh: next := !mAutoStart.Checked() if err := setAutoStart(next); err != nil { log.Printf("设置开机自启失败: %v", err) continue } if next { mAutoStart.Check() } else { mAutoStart.Uncheck() } cfg.AutoStart = next _ = saveConfig(cfg) log.Printf("开机自启 = %v", next) case <-mAutoUpdate.ClickedCh: next := !mAutoUpdate.Checked() if next { mAutoUpdate.Check() } else { mAutoUpdate.Uncheck() } cfg.AutoUpdate = next _ = saveConfig(cfg) log.Printf("定期自动更新 = %v(间隔 %d 小时)", next, cfg.UpdateIntervalH) if next { sup.startAutoUpdate() } case <-mUpdateNow.ClickedCh: go func() { log.Printf("菜单:立即更新") if _, err := sup.Update(); err != nil { log.Printf("更新失败: %v", err) return } log.Printf("更新完成,重启服务") sup.Restart() mVersion.SetTitle("版本:" + sup.installedVersion()) }() case <-mLogs.ClickedCh: openURL(appDataDir() + string(os.PathSeparator) + "logs") case <-mConfig.ClickedCh: openURL(configPath()) case <-mQuit.ClickedCh: log.Printf("菜单:退出") systray.Quit() return } } }() sup.Start() if cfg.AutoUpdate { sup.startAutoUpdate() } } func cfgURL(cfg *Config) string { host := cfg.WebHost if host == "" { host = "127.0.0.1" } return fmt.Sprintf("打开 Web 界面:http://%s:%d", host, cfg.WebPort) } // shortURL 菜单标题只显示 origin,避免 token 撑爆标题。 func shortURL(u string) string { if i := strings.Index(u, "?"); i > 0 { return u[:i] } return u } // openURL 用系统默认程序打开 URL 或路径(隐藏窗口)。 func openURL(target string) { cmd := exec.Command("cmd", "/c", "start", "", target) hideWindow(cmd) _ = cmd.Start() }