|
@@ -26,8 +26,9 @@ func runTray(cfg *Config, log *Logger) {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
func trayReady(sup *Supervisor, cfg *Config, log *Logger) {
|
|
func trayReady(sup *Supervisor, cfg *Config, log *Logger) {
|
|
|
- mStatus := systray.AddMenuItem("状态:初始化…", "当前服务状态")
|
|
|
|
|
- mURL := systray.AddMenuItem("Web 界面:等待服务就绪…", "服务就绪后打开(自动带 token)")
|
|
|
|
|
|
|
+ // 菜单标题保持短文本(长 URL 会撑宽整个菜单、显得不协调),详情放 tooltip。
|
|
|
|
|
+ mStatus := systray.AddMenuItem("初始化…", "当前服务状态")
|
|
|
|
|
+ mURL := systray.AddMenuItem("打开 Web 界面", "服务就绪后可用(自动带 token)")
|
|
|
systray.AddSeparator()
|
|
systray.AddSeparator()
|
|
|
mStart := systray.AddMenuItem("启动服务", "启动 dsh web")
|
|
mStart := systray.AddMenuItem("启动服务", "启动 dsh web")
|
|
|
mStop := systray.AddMenuItem("停止服务", "停止 dsh web")
|
|
mStop := systray.AddMenuItem("停止服务", "停止 dsh web")
|
|
@@ -35,7 +36,7 @@ func trayReady(sup *Supervisor, cfg *Config, log *Logger) {
|
|
|
systray.AddSeparator()
|
|
systray.AddSeparator()
|
|
|
mAutoStart := systray.AddMenuItemCheckbox("开机自启", "登录 Windows 时自动启动托盘并守护服务(无窗口、不打开浏览器)", isAutoStartEnabled())
|
|
mAutoStart := systray.AddMenuItemCheckbox("开机自启", "登录 Windows 时自动启动托盘并守护服务(无窗口、不打开浏览器)", isAutoStartEnabled())
|
|
|
mAutoUpdate := systray.AddMenuItemCheckbox("定期自动更新", "按间隔检查并更新 dsh(已是最新则跳过)", cfg.AutoUpdate)
|
|
mAutoUpdate := systray.AddMenuItemCheckbox("定期自动更新", "按间隔检查并更新 dsh(已是最新则跳过)", cfg.AutoUpdate)
|
|
|
- mUpdateNow := systray.AddMenuItem("立即更新 DSH", "npm install -g @deepseek-ai/dsh@latest(先停服务)")
|
|
|
|
|
|
|
+ mUpdateNow := systray.AddMenuItem("立即更新 DSH", "检查并安装最新版(先停服务;已是最新则秒回)")
|
|
|
systray.AddSeparator()
|
|
systray.AddSeparator()
|
|
|
mVersion := systray.AddMenuItem("版本:查询中…", "已安装的 DSH 版本")
|
|
mVersion := systray.AddMenuItem("版本:查询中…", "已安装的 DSH 版本")
|
|
|
mLogs := systray.AddMenuItem("查看日志", "打开日志目录")
|
|
mLogs := systray.AddMenuItem("查看日志", "打开日志目录")
|
|
@@ -46,12 +47,33 @@ func trayReady(sup *Supervisor, cfg *Config, log *Logger) {
|
|
|
mStop.Disable()
|
|
mStop.Disable()
|
|
|
mURL.Disable() // 服务就绪前不可点(避免打开无 token 的无效地址)
|
|
mURL.Disable() // 服务就绪前不可点(避免打开无 token 的无效地址)
|
|
|
|
|
|
|
|
- // runOp 在后台执行耗时操作(停止/重启可能等待数秒),避免阻塞菜单响应;同一时刻只允许一个。
|
|
|
|
|
|
|
+ // 临时通知:覆盖状态标题若干秒,让"点完看不到变化"的操作有回显。
|
|
|
|
|
+ var noticeMu sync.Mutex
|
|
|
|
|
+ noticeText := ""
|
|
|
|
|
+ var noticeUntil time.Time
|
|
|
|
|
+ setNotice := func(text string, d time.Duration) {
|
|
|
|
|
+ noticeMu.Lock()
|
|
|
|
|
+ noticeText, noticeUntil = text, time.Now().Add(d)
|
|
|
|
|
+ noticeMu.Unlock()
|
|
|
|
|
+ }
|
|
|
|
|
+ takeNotice := func() string {
|
|
|
|
|
+ noticeMu.Lock()
|
|
|
|
|
+ defer noticeMu.Unlock()
|
|
|
|
|
+ if noticeText == "" || time.Now().After(noticeUntil) {
|
|
|
|
|
+ noticeText = ""
|
|
|
|
|
+ return ""
|
|
|
|
|
+ }
|
|
|
|
|
+ return noticeText
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // runOp 后台执行耗时操作(停止/重启/更新可能等待数秒到数分钟),
|
|
|
|
|
+ // 保证菜单点击立即响应;同一时刻只允许一个长操作。
|
|
|
var opMu sync.Mutex
|
|
var opMu sync.Mutex
|
|
|
runOp := func(name string, fn func()) {
|
|
runOp := func(name string, fn func()) {
|
|
|
go func() {
|
|
go func() {
|
|
|
if !opMu.TryLock() {
|
|
if !opMu.TryLock() {
|
|
|
log.Printf("%s:已有操作进行中,忽略本次", name)
|
|
log.Printf("%s:已有操作进行中,忽略本次", name)
|
|
|
|
|
+ setNotice("已有操作进行中…", 5*time.Second)
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
defer opMu.Unlock()
|
|
defer opMu.Unlock()
|
|
@@ -63,12 +85,16 @@ func trayReady(sup *Supervisor, cfg *Config, log *Logger) {
|
|
|
go func() {
|
|
go func() {
|
|
|
ticker := time.NewTicker(3 * time.Second)
|
|
ticker := time.NewTicker(3 * time.Second)
|
|
|
defer ticker.Stop()
|
|
defer ticker.Stop()
|
|
|
- var lastTitle, lastTip, lastURL string
|
|
|
|
|
|
|
+ var lastTitle, lastTip, lastWebTip string
|
|
|
lastRunning, lastWebEnabled := false, false
|
|
lastRunning, lastWebEnabled := false, false
|
|
|
for range ticker.C {
|
|
for range ticker.C {
|
|
|
st := sup.Status()
|
|
st := sup.Status()
|
|
|
|
|
|
|
|
- if title := statusTitle(st); title != lastTitle {
|
|
|
|
|
|
|
+ title := statusTitle(st)
|
|
|
|
|
+ if n := takeNotice(); n != "" {
|
|
|
|
|
+ title = n
|
|
|
|
|
+ }
|
|
|
|
|
+ if title != lastTitle {
|
|
|
mStatus.SetTitle(title)
|
|
mStatus.SetTitle(title)
|
|
|
lastTitle = title
|
|
lastTitle = title
|
|
|
}
|
|
}
|
|
@@ -77,29 +103,29 @@ func trayReady(sup *Supervisor, cfg *Config, log *Logger) {
|
|
|
lastTip = tip
|
|
lastTip = tip
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // Web 菜单:就绪前禁用,避免"打开后无法访问"
|
|
|
|
|
- webText, webEnabled := "", true
|
|
|
|
|
|
|
+ // Web 菜单:标题固定,仅切换可用状态与 tooltip
|
|
|
|
|
+ webEnable, webTip := false, ""
|
|
|
switch {
|
|
switch {
|
|
|
case st.Ready:
|
|
case st.Ready:
|
|
|
- webText = "打开 Web 界面:" + shortURL(st.URL)
|
|
|
|
|
|
|
+ webEnable, webTip = true, "打开:"+st.URL
|
|
|
case st.External:
|
|
case st.External:
|
|
|
- webText = "打开 Web 界面(外部实例):" + shortURL(st.URL)
|
|
|
|
|
|
|
+ webEnable, webTip = true, "外部实例:"+st.URL
|
|
|
case st.Running:
|
|
case st.Running:
|
|
|
- webText, webEnabled = "Web 界面:服务启动中…", false
|
|
|
|
|
|
|
+ webTip = "服务启动中,就绪后自动可用(首次约 10~40 秒)"
|
|
|
default:
|
|
default:
|
|
|
- webText, webEnabled = "打开 Web 界面(服务未运行)", false
|
|
|
|
|
- }
|
|
|
|
|
- if webText != lastURL {
|
|
|
|
|
- mURL.SetTitle(webText)
|
|
|
|
|
- lastURL = webText
|
|
|
|
|
|
|
+ webTip = "服务未运行"
|
|
|
}
|
|
}
|
|
|
- if webEnabled != lastWebEnabled {
|
|
|
|
|
- if webEnabled {
|
|
|
|
|
|
|
+ if webEnable != lastWebEnabled {
|
|
|
|
|
+ if webEnable {
|
|
|
mURL.Enable()
|
|
mURL.Enable()
|
|
|
} else {
|
|
} else {
|
|
|
mURL.Disable()
|
|
mURL.Disable()
|
|
|
}
|
|
}
|
|
|
- lastWebEnabled = webEnabled
|
|
|
|
|
|
|
+ lastWebEnabled = webEnable
|
|
|
|
|
+ }
|
|
|
|
|
+ if webTip != lastWebTip {
|
|
|
|
|
+ mURL.SetTooltip(webTip)
|
|
|
|
|
+ lastWebTip = webTip
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if st.Running != lastRunning {
|
|
if st.Running != lastRunning {
|
|
@@ -144,16 +170,19 @@ func trayReady(sup *Supervisor, cfg *Config, log *Logger) {
|
|
|
st := sup.Status()
|
|
st := sup.Status()
|
|
|
if !st.Ready && !st.External {
|
|
if !st.Ready && !st.External {
|
|
|
log.Printf("菜单:打开 Web 界面被忽略(服务未就绪)")
|
|
log.Printf("菜单:打开 Web 界面被忽略(服务未就绪)")
|
|
|
|
|
+ setNotice("Web 界面启动中,请稍候…", 4*time.Second)
|
|
|
continue
|
|
continue
|
|
|
}
|
|
}
|
|
|
log.Printf("菜单:打开 Web 界面 %s", shortURL(st.URL))
|
|
log.Printf("菜单:打开 Web 界面 %s", shortURL(st.URL))
|
|
|
if err := openExternal(st.URL); err != nil {
|
|
if err := openExternal(st.URL); err != nil {
|
|
|
log.Printf("打开 Web 界面失败: %v", err)
|
|
log.Printf("打开 Web 界面失败: %v", err)
|
|
|
|
|
+ setNotice("打开失败(详见日志)", 6*time.Second)
|
|
|
}
|
|
}
|
|
|
case <-mAutoStart.ClickedCh:
|
|
case <-mAutoStart.ClickedCh:
|
|
|
next := !mAutoStart.Checked()
|
|
next := !mAutoStart.Checked()
|
|
|
if err := setAutoStart(next); err != nil {
|
|
if err := setAutoStart(next); err != nil {
|
|
|
log.Printf("设置开机自启失败: %v", err)
|
|
log.Printf("设置开机自启失败: %v", err)
|
|
|
|
|
+ setNotice("设置开机自启失败", 6*time.Second)
|
|
|
continue
|
|
continue
|
|
|
}
|
|
}
|
|
|
if next {
|
|
if next {
|
|
@@ -165,6 +194,11 @@ func trayReady(sup *Supervisor, cfg *Config, log *Logger) {
|
|
|
log.Printf("保存配置失败: %v", err)
|
|
log.Printf("保存配置失败: %v", err)
|
|
|
}
|
|
}
|
|
|
log.Printf("开机自启 = %v", next)
|
|
log.Printf("开机自启 = %v", next)
|
|
|
|
|
+ if next {
|
|
|
|
|
+ setNotice("开机自启:已开启", 4*time.Second)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ setNotice("开机自启:已关闭", 4*time.Second)
|
|
|
|
|
+ }
|
|
|
case <-mAutoUpdate.ClickedCh:
|
|
case <-mAutoUpdate.ClickedCh:
|
|
|
next := !mAutoUpdate.Checked()
|
|
next := !mAutoUpdate.Checked()
|
|
|
if next {
|
|
if next {
|
|
@@ -176,26 +210,35 @@ func trayReady(sup *Supervisor, cfg *Config, log *Logger) {
|
|
|
log.Printf("保存配置失败: %v", err)
|
|
log.Printf("保存配置失败: %v", err)
|
|
|
}
|
|
}
|
|
|
log.Printf("定期自动更新 = %v", next)
|
|
log.Printf("定期自动更新 = %v", next)
|
|
|
|
|
+ if next {
|
|
|
|
|
+ setNotice("定期自动更新:已开启", 4*time.Second)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ setNotice("定期自动更新:已关闭", 4*time.Second)
|
|
|
|
|
+ }
|
|
|
case <-mUpdateNow.ClickedCh:
|
|
case <-mUpdateNow.ClickedCh:
|
|
|
- go func() {
|
|
|
|
|
- log.Printf("菜单:立即更新(先停服务再更新)")
|
|
|
|
|
|
|
+ log.Printf("菜单:立即更新")
|
|
|
|
|
+ runOp("立即更新", func() {
|
|
|
text, err := sup.UpdateAndRestart()
|
|
text, err := sup.UpdateAndRestart()
|
|
|
if err != nil {
|
|
if err != nil {
|
|
|
log.Printf("更新失败: %v", err)
|
|
log.Printf("更新失败: %v", err)
|
|
|
|
|
+ setNotice("更新失败(详见日志)", 8*time.Second)
|
|
|
} else {
|
|
} else {
|
|
|
log.Printf("更新完成: %s", firstLine(text))
|
|
log.Printf("更新完成: %s", firstLine(text))
|
|
|
|
|
+ setNotice(firstLine(text), 8*time.Second)
|
|
|
}
|
|
}
|
|
|
mVersion.SetTitle("版本:" + sup.installedVersion())
|
|
mVersion.SetTitle("版本:" + sup.installedVersion())
|
|
|
- }()
|
|
|
|
|
|
|
+ })
|
|
|
case <-mLogs.ClickedCh:
|
|
case <-mLogs.ClickedCh:
|
|
|
log.Printf("菜单:查看日志 %s", logDir())
|
|
log.Printf("菜单:查看日志 %s", logDir())
|
|
|
if err := openExternal(logDir()); err != nil {
|
|
if err := openExternal(logDir()); err != nil {
|
|
|
log.Printf("打开日志失败: %v", err)
|
|
log.Printf("打开日志失败: %v", err)
|
|
|
|
|
+ setNotice("打开日志失败(详见日志)", 6*time.Second)
|
|
|
}
|
|
}
|
|
|
case <-mConfig.ClickedCh:
|
|
case <-mConfig.ClickedCh:
|
|
|
log.Printf("菜单:打开配置文件 %s", configPath())
|
|
log.Printf("菜单:打开配置文件 %s", configPath())
|
|
|
if err := openExternal(configPath()); err != nil {
|
|
if err := openExternal(configPath()); err != nil {
|
|
|
log.Printf("打开配置失败: %v", err)
|
|
log.Printf("打开配置失败: %v", err)
|
|
|
|
|
+ setNotice("打开配置失败(详见日志)", 6*time.Second)
|
|
|
}
|
|
}
|
|
|
case <-mQuit.ClickedCh:
|
|
case <-mQuit.ClickedCh:
|
|
|
log.Printf("菜单:退出")
|
|
log.Printf("菜单:退出")
|
|
@@ -214,25 +257,25 @@ func trayReady(sup *Supervisor, cfg *Config, log *Logger) {
|
|
|
func statusTitle(st Status) string {
|
|
func statusTitle(st Status) string {
|
|
|
switch {
|
|
switch {
|
|
|
case st.Updating:
|
|
case st.Updating:
|
|
|
- return "状态:正在更新…"
|
|
|
|
|
|
|
+ return "正在更新…"
|
|
|
case st.Running && !st.Ready:
|
|
case st.Running && !st.Ready:
|
|
|
- return fmt.Sprintf("状态:启动中 (PID %d)", st.PID)
|
|
|
|
|
|
|
+ return fmt.Sprintf("启动中 · PID %d", st.PID)
|
|
|
case st.Running:
|
|
case st.Running:
|
|
|
- return fmt.Sprintf("状态:运行中 (PID %d)", st.PID)
|
|
|
|
|
|
|
+ return fmt.Sprintf("运行中 · PID %d", st.PID)
|
|
|
case st.External:
|
|
case st.External:
|
|
|
- return "状态:外部实例运行中"
|
|
|
|
|
|
|
+ return "外部实例运行中"
|
|
|
default:
|
|
default:
|
|
|
- return "状态:已停止"
|
|
|
|
|
|
|
+ return "已停止"
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
func statusTooltip(st Status) string {
|
|
func statusTooltip(st Status) string {
|
|
|
- parts := make([]string, 0, 3)
|
|
|
|
|
|
|
+ parts := make([]string, 0, 4)
|
|
|
if st.Running {
|
|
if st.Running {
|
|
|
parts = append(parts, "运行 "+humanDuration(st.Uptime))
|
|
parts = append(parts, "运行 "+humanDuration(st.Uptime))
|
|
|
}
|
|
}
|
|
|
if st.Running && !st.Ready {
|
|
if st.Running && !st.Ready {
|
|
|
- parts = append(parts, "Web 界面启动中(首次约需 30 秒)")
|
|
|
|
|
|
|
+ parts = append(parts, "Web 界面启动中")
|
|
|
}
|
|
}
|
|
|
if st.Restarts > 0 {
|
|
if st.Restarts > 0 {
|
|
|
parts = append(parts, fmt.Sprintf("自动重启 %d 次", st.Restarts))
|
|
parts = append(parts, fmt.Sprintf("自动重启 %d 次", st.Restarts))
|