فهرست منبع

fix: v2.6.0 修复悬停看不到 URL + 新增复制地址

- 查源码确认 getlantern/systray 的 Windows 后端只有图标级 tooltip,MenuItem.SetTooltip 是空操作

  (v2.5.0 把 URL 放菜单项 tooltip 的修复在 Windows 上无效,属基于假设的修复)

- 改为:完整地址放入托盘图标 tooltip,置于首行(上限 128 字符,避免截断)

- 新增 复制 Web 地址:临时文件 + clip.exe 重定向(文本不经 shell),用后即删

- 探针验证剪贴板写入(Get-Clipboard 读回一致)

- README 修正不准确的悬停描述
caesar 18 ساعت پیش
والد
کامیت
a6022642c8
4فایلهای تغییر یافته به همراه75 افزوده شده و 17 حذف شده
  1. 8 2
      README.md
  2. 1 1
      build.ps1
  3. 28 0
      proc_windows.go
  4. 38 14
      tray.go

+ 8 - 2
README.md

@@ -60,7 +60,8 @@ nssm start DeepSeekTray
 
 - 状态:运行中 · PID / 启动中 · PID / 已停止 / 外部实例运行中 / 正在更新
   (操作结果会在状态区**临时回显**,如"已是最新版本 x.y.z"、"更新完成"、"失败(详见日志)")
-- 打开 Web 界面(**就绪前置灰**;就绪后点击直达,自动带 token;URL 显示在悬停提示里)
+- 打开 Web 界面(**就绪前置灰**;就绪后点击直达,自动带 token;标题同时显示 host:port)
+- 复制 Web 地址(把含 token 的完整地址复制到剪贴板,便于贴到别处使用)
 - 启动 / 停止 / 重启服务
 - 开机自启(勾选)
 - 定期自动更新(勾选)、立即更新 DSH(已是最新则秒回,不重启服务)
@@ -68,7 +69,11 @@ nssm start DeepSeekTray
 - 查看日志 / 打开配置文件
 - 退出(停止服务并退出)
 
-> 菜单标题统一保持短文本(长 URL 会撑宽整个菜单),细节放在各项的鼠标悬停提示中。
+> 菜单标题统一保持短文本(长 URL 会撑宽整个菜单)。
+>
+> **关于查看访问地址**:`getlantern/systray` 在 Windows 上**不支持菜单项的悬停提示**(只有图标级 tooltip 有效)。
+> 完整访问地址显示在**托盘图标的悬停提示**中(鼠标移到托盘图标上即可看到;地址置于首行,避免 128 字符上限将其截断),
+> 也可使用菜单中的「复制 Web 地址」。
 
 ## 配置
 
@@ -156,6 +161,7 @@ nssm start DeepSeekTray
 
 ## 版本
 
+- v2.6.0 — 修复"悬停看不到 URL":查源码确认 **getlantern/systray 在 Windows 上不支持菜单项 tooltip**(`MenuItem.SetTooltip` 是空操作),改为把完整地址放进**托盘图标 tooltip**(地址置于首行,规避 128 字符上限截断),并新增**「复制 Web 地址」**(临时文件 + clip.exe,文本不经 shell 解析)
 - v2.5.0 — 依据实际使用反馈优化:**菜单宽度**(长 URL 移入悬停提示)、**操作结果回显**(解决"点更新没反应"的观感问题)、**版本号改由构建脚本注入**(避免源码与版本号不一致)、更新流程合并为唯一入口(去掉重复查询)、耗时操作防重入
 - v2.4.0 — 修复实测发现的三个问题:**打开日志/配置无反应**(误用 SW_HIDE 把 explorer 窗口隐藏了 → 改用 ShellExecuteW)、**Web 界面启动期不可用**(新增就绪状态,启动中禁用点击而非打开无效地址)、**更新白等 5 分钟**(版本相同时跳过整包重装与重启);菜单耗时操作后台执行不再阻塞;日志记录 Web 启动耗时
 - v2.3.0 — 三次迭代:修复**停止/启动竞态**(不再残留进程)、配置写入线程安全、更新前先停服务、配置读写单元测试与守护生命周期测试、`build.ps1` 一键构建

+ 1 - 1
build.ps1

@@ -1,6 +1,6 @@
 # deepseek-tray 构建脚本:格式检查 -> 静态检查 -> 单元测试 -> 构建
 # 版本号唯一维护点(README 的版本历史与此保持一致)
-$Version = '2.5.0'
+$Version = '2.6.0'
 
 $ErrorActionPreference = 'Stop'
 Push-Location $PSScriptRoot

+ 28 - 0
proc_windows.go

@@ -128,6 +128,34 @@ func (s *singleInstance) release() {
 	}
 }
 
+// copyToClipboard 把文本放入系统剪贴板。
+// 实现:写临时文件后交给 clip.exe 重定向读入——文本内容不经过 shell 解析;
+// 临时文件用后即删(内容含访问 token)。
+func copyToClipboard(text string) error {
+	if text == "" {
+		return fmt.Errorf("无可复制内容")
+	}
+	tmp := os.TempDir() + "\\deepseek-tray-url.txt"
+	if err := os.WriteFile(tmp, []byte(text), 0o600); err != nil {
+		return err
+	}
+	defer os.Remove(tmp)
+
+	f, err := os.Open(tmp)
+	if err != nil {
+		return err
+	}
+	defer f.Close()
+
+	cmd := exec.Command("cmd", "/c", "clip")
+	cmd.Stdin = f
+	hideWindow(cmd)
+	if err := cmd.Run(); err != nil {
+		return fmt.Errorf("写入剪贴板失败: %w", err)
+	}
+	return nil
+}
+
 // attachConsole 让 windowsgui 子系统程序能输出到调用它的终端(仅 CLI 子命令使用)。
 func attachConsole() {
 	const attachParentProcess = 0xFFFFFFFF

+ 38 - 14
tray.go

@@ -26,9 +26,12 @@ func runTray(cfg *Config, log *Logger) {
 }
 
 func trayReady(sup *Supervisor, cfg *Config, log *Logger) {
-	// 菜单标题保持短文本(长 URL 会撑宽整个菜单、显得不协调),详情放 tooltip。
+	// 菜单标题保持短文本(长 URL 会撑宽整个菜单)。
+	// 注意:getlantern/systray 在 Windows 上不支持"菜单项 tooltip"(只有图标 tooltip 有效),
+	// 因此完整地址通过"托盘图标悬停提示"与"复制 Web 地址"提供。
 	mStatus := systray.AddMenuItem("初始化…", "当前服务状态")
 	mURL := systray.AddMenuItem("打开 Web 界面", "服务就绪后可用(自动带 token)")
+	mCopyURL := systray.AddMenuItem("复制 Web 地址", "把含 token 的完整地址复制到剪贴板")
 	systray.AddSeparator()
 	mStart := systray.AddMenuItem("启动服务", "启动 dsh web")
 	mStop := systray.AddMenuItem("停止服务", "停止 dsh web")
@@ -46,6 +49,7 @@ func trayReady(sup *Supervisor, cfg *Config, log *Logger) {
 
 	mStop.Disable()
 	mURL.Disable() // 服务就绪前不可点(避免打开无 token 的无效地址)
+	mCopyURL.Disable()
 
 	// 临时通知:覆盖状态标题若干秒,让"点完看不到变化"的操作有回显。
 	var noticeMu sync.Mutex
@@ -85,7 +89,7 @@ func trayReady(sup *Supervisor, cfg *Config, log *Logger) {
 	go func() {
 		ticker := time.NewTicker(3 * time.Second)
 		defer ticker.Stop()
-		var lastTitle, lastTip, lastWebTip string
+		var lastTitle, lastTip, lastURL, lastIconTip string
 		lastRunning, lastWebEnabled := false, false
 		for range ticker.C {
 			st := sup.Status()
@@ -103,29 +107,36 @@ func trayReady(sup *Supervisor, cfg *Config, log *Logger) {
 				lastTip = tip
 			}
 
-			// Web 菜单:标题固定,仅切换可用状态与 tooltip
-			webEnable, webTip := false, ""
+			// Web 相关菜单:标题短文本;完整地址走托盘图标 tooltip(Windows 上唯一有效的悬停提示)
+			addr := fmt.Sprintf("%s:%d", cfg.WebHost, cfg.WebPort)
+			webEnable, webText := false, "打开 Web 界面(服务未运行)"
+			iconTip := "DeepSeek Tray · " + statusTitle(st)
 			switch {
-			case st.Ready:
-				webEnable, webTip = true, "打开:"+st.URL
-			case st.External:
-				webEnable, webTip = true, "外部实例:"+st.URL
+			case st.Ready || st.External:
+				webEnable, webText = true, "打开 Web 界面 ("+addr+")"
+				// 地址放在最前:Windows 图标 tooltip 上限 128 字符,避免地址被截断
+				iconTip = st.URL + "\n" + statusTitle(st)
 			case st.Running:
-				webTip = "服务启动中,就绪后自动可用(首次约 10~40 秒)"
-			default:
-				webTip = "服务未运行"
+				webText = "打开 Web 界面(启动中…)"
+				iconTip = "DeepSeek Tray · 启动中…(就绪后此处显示地址)"
+			}
+			if webText != lastURL {
+				mURL.SetTitle(webText)
+				lastURL = webText
 			}
 			if webEnable != lastWebEnabled {
 				if webEnable {
 					mURL.Enable()
+					mCopyURL.Enable()
 				} else {
 					mURL.Disable()
+					mCopyURL.Disable()
 				}
 				lastWebEnabled = webEnable
 			}
-			if webTip != lastWebTip {
-				mURL.SetTooltip(webTip)
-				lastWebTip = webTip
+			if iconTip != lastIconTip {
+				systray.SetTooltip(iconTip)
+				lastIconTip = iconTip
 			}
 
 			if st.Running != lastRunning {
@@ -178,6 +189,19 @@ func trayReady(sup *Supervisor, cfg *Config, log *Logger) {
 					log.Printf("打开 Web 界面失败: %v", err)
 					setNotice("打开失败(详见日志)", 6*time.Second)
 				}
+			case <-mCopyURL.ClickedCh:
+				st := sup.Status()
+				if !st.Ready && !st.External {
+					log.Printf("菜单:复制 Web 地址被忽略(服务未就绪)")
+					continue
+				}
+				log.Printf("菜单:复制 Web 地址")
+				if err := copyToClipboard(st.URL); err != nil {
+					log.Printf("复制 Web 地址失败: %v", err)
+					setNotice("复制失败(详见日志)", 6*time.Second)
+				} else {
+					setNotice("Web 地址已复制到剪贴板", 4*time.Second)
+				}
 			case <-mAutoStart.ClickedCh:
 				next := !mAutoStart.Checked()
 				if err := setAutoStart(next); err != nil {