| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237 |
- 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()
- }
- cfg.AutoStart = next
- if err := saveConfig(cfg); err != nil {
- log.Printf("保存配置失败: %v", err)
- }
- log.Printf("开机自启 = %v", next)
- case <-mAutoUpdate.ClickedCh:
- next := !mAutoUpdate.Checked()
- if next {
- mAutoUpdate.Check()
- } else {
- mAutoUpdate.Uncheck()
- }
- cfg.AutoUpdate = next
- if err := saveConfig(cfg); err != nil {
- log.Printf("保存配置失败: %v", err)
- }
- 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:
- 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, ";")
- }
- // humanDuration 简短的时长展示(<1 分钟、x 分钟、x 小时 y 分)。
- func humanDuration(d time.Duration) string {
- if d <= 0 {
- return "0 秒"
- }
- sec := int(d.Seconds())
- switch {
- case sec < 60:
- return fmt.Sprintf("%d 秒", sec)
- case sec < 3600:
- return fmt.Sprintf("%d 分钟", sec/60)
- default:
- return fmt.Sprintf("%d 小时 %d 分", sec/3600, (sec%3600)/60)
- }
- }
- // shortURL 菜单标题只显示 origin,避免 token 撑爆标题。
- func shortURL(u string) string {
- if i := strings.Index(u, "?"); i > 0 {
- return u[:i]
- }
- return u
- }
- // 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()
- }
|