package main import ( _ "embed" "fmt" "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 界面", "") 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() // 状态刷新:仅在有变化时写 UI(减少 systray 调用) go func() { ticker := time.NewTicker(3 * time.Second) defer ticker.Stop() var lastTitle, lastTip, lastURL string lastRunning := false for range ticker.C { st := sup.Status() if title := statusTitle(st); title != lastTitle { mStatus.SetTitle(title) lastTitle = title } if tip := statusTooltip(st); tip != lastTip { mStatus.SetTooltip(tip) lastTip = tip } if u := "打开 Web 界面:" + shortURL(st.URL); u != lastURL { mURL.SetTitle(u) lastURL = u } if st.Running != lastRunning { if st.Running { mStart.Disable() mStop.Enable() } else { mStart.Enable() mStop.Disable() } lastRunning = st.Running } } }() // 版本刷新(启动一次 + 每 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: target := sup.Status().URL log.Printf("菜单:打开 Web 界面") openTarget(target) case <-mAutoStart.ClickedCh: next := !mAutoStart.Checked() if err := setAutoStart(next); err != nil { log.Printf("设置开机自启失败: %v", err) continue } if next { mAutoStart.Check() } else { mAutoStart.Uncheck() } if err := sup.SetAutoStart(next); err != nil { log.Printf("保存配置失败: %v", err) } log.Printf("开机自启 = %v", next) case <-mAutoUpdate.ClickedCh: next := !mAutoUpdate.Checked() if next { mAutoUpdate.Check() } else { mAutoUpdate.Uncheck() } if err := sup.SetAutoUpdate(next); err != nil { log.Printf("保存配置失败: %v", err) } log.Printf("定期自动更新 = %v", next) case <-mUpdateNow.ClickedCh: go func() { log.Printf("菜单:立即更新(先停服务再更新)") if _, err := sup.UpdateAndRestart(); err != nil { log.Printf("更新失败: %v", err) } else { log.Printf("更新完成,服务已恢复运行") } mVersion.SetTitle("版本:" + sup.installedVersion()) }() case <-mLogs.ClickedCh: openTarget(logDir()) case <-mConfig.ClickedCh: openTarget(configPath()) case <-mQuit.ClickedCh: log.Printf("菜单:退出") systray.Quit() return } } }() sup.Start() if cfg.AutoUpdate { sup.startAutoUpdate() } } func statusTitle(st Status) string { switch { case st.Updating: return "状态:正在更新…" case st.Running: return fmt.Sprintf("状态:运行中 (PID %d)", st.PID) case st.External: return "状态:外部实例运行中" default: return "状态:已停止" } } func statusTooltip(st Status) string { parts := make([]string, 0, 3) if st.Running { parts = append(parts, "运行 "+humanDuration(st.Uptime)) } if st.Restarts > 0 { parts = append(parts, fmt.Sprintf("自动重启 %d 次", st.Restarts)) } if st.LastError != "" { parts = append(parts, "最近错误:"+st.LastError) } if len(parts) == 0 { return "服务未运行" } return strings.Join(parts, ";") } // openTarget 用系统默认程序打开 URL 或路径,不经 shell 解析(避免注入面)。 func openTarget(target string) { if target == "" { return } var cmd *exec.Cmd if strings.HasPrefix(target, "http://") || strings.HasPrefix(target, "https://") { cmd = exec.Command("rundll32.exe", "url.dll,FileProtocolHandler", target) } else { cmd = exec.Command("explorer.exe", target) } hideWindow(cmd) _ = cmd.Start() }