# deepseek-tray DeepSeek Harness **Web 版桌面包装**(Windows 系统托盘 + 守护进程)。 把 `dsh web` 变成常驻托盘应用:**全程隐藏终端**、异常自动重启、一键开机自启、一键更新。 > 开机自启为**静默启动**:不弹终端、不打开浏览器。需要时从托盘菜单"打开 Web 界面"(自动带 token,直接进入已认证界面)。 ## 特性 - **隐藏终端**:以 `node /lib/bin.js web --no-open` 直接启动(无 shell 包装),子进程使用 `CREATE_NO_WINDOW` + `HideWindow` - **守护重启**:异常退出自动重启,**指数退避**(基准可配,封顶 2 分钟;稳定运行 30s 后复位),杜绝重启风暴 - **端口预检**:端口已被占用(例如已有实例)时不启动,改为温和等待(封顶 30s 轮询),避免端口冲突 - **启停管理**:托盘一键启动 / 停止 / 重启;停止为**同步等待**(最多 8s),重启前等待端口释放 - **开机自启**:托盘勾选或 `autostart on|off`(写 HKCU Run 键,无需管理员权限) - **一键更新**:执行 `npm install -g @deepseek-ai/dsh@latest --allow-scripts=...`,**先停服务再更新**(避免 Windows 上 native 模块文件占用导致安装失败),无论成败都恢复原有运行状态;可开启定期后台更新(**常驻单例循环**:关闭仅暂停计时,再次开启立即恢复) - **带 token 打开**:捕获 `dsh web` 输出的访问地址;菜单标题只显示 origin,token 只存内存 - **安全**:日志自动脱敏 `token=***`;打开 URL/路径不经 shell 解析(`rundll32` / `explorer`) - **健壮**:配置原子写入(临时文件 + rename)且**写入线程安全**、支持 UTF-8 BOM、损坏配置回退默认值;日志按天轮转;进程树终止失败时退化为直接终止;**停止与启动过程的竞态已处理**(停止请求落在启动窗口时不会残留进程) - **单实例**:`Local` 命名互斥体(普通用户可创建,避免 `Global` 权限失败导致失效) - **可验证**:11 个单元测试覆盖脱敏、URL 提取、配置读写与归一、端口探测、退避、日志轮转、守护生命周期 - **低依赖**:第三方仅 2 个(`getlantern/systray`、`golang.org/x/sys`),单文件分发约 5 MB ## 构建与测试 ```powershell .\build.ps1 # 一键:格式检查 + go vet + go test + 构建 ``` 分步执行: ```powershell gofmt -l . # 格式检查(应无输出) go vet ./... # 静态检查 go test ./... # 单元测试 go build -trimpath -ldflags "-H:windowsgui -s -w" -o deepseek-tray.exe . ``` ## 运行方式 | 命令 | 说明 | |---|---| | `deepseek-tray.exe` | 托盘模式(默认) | | `deepseek-tray.exe --silent` | 静默托盘(开机自启使用,无窗口、不打开浏览器) | | `deepseek-tray.exe daemon` | 无 UI 守护模式(支持优雅退出信号,适合注册为 Windows 服务) | | `deepseek-tray.exe status` | 状态查询(同时写日志) | | `deepseek-tray.exe autostart [on\|off]` | 查看/设置开机自启 | | `deepseek-tray.exe version` | 版本号 | > CLI 子命令在真实终端中输出(通过 `AttachConsole`);无终端场景可从日志回溯。 ### 注册为 Windows 服务(可选) ```powershell nssm install DeepSeekTray "C:\path\to\deepseek-tray.exe" daemon nssm start DeepSeekTray ``` ## 托盘菜单 - 状态:运行中 · PID / 启动中 · PID / 已停止 / 外部实例运行中 / 正在更新 (操作结果会在状态区**临时回显**,如"已是最新版本 x.y.z"、"更新完成"、"失败(详见日志)") - 打开 Web 界面(**就绪前置灰**;就绪后点击直达,自动带 token;URL 显示在悬停提示里) - 启动 / 停止 / 重启服务 - 开机自启(勾选) - 定期自动更新(勾选)、立即更新 DSH(已是最新则秒回,不重启服务) - 版本:当前安装的 dsh 版本 - 查看日志 / 打开配置文件 - 退出(停止服务并退出) > 菜单标题统一保持短文本(长 URL 会撑宽整个菜单),细节放在各项的鼠标悬停提示中。 ## 配置 位置:`%APPDATA%\deepseek-tray\config.json`(首次运行自动生成,原子写入) ```json { "webPort": 3080, "webHost": "127.0.0.1", "nodePath": "", "dshBinJs": "", "autoStart": false, "autoUpdate": false, "updateIntervalHours": 24, "restartDelaySeconds": 5 } ``` | 字段 | 说明 | |---|---| | `webPort` / `webHost` | dsh web 监听端口与地址(默认 3080 / 127.0.0.1) | | `nodePath` / `dshBinJs` | 留空自动探测(`node` + `npm root -g` 下的 dsh 入口) | | `autoStart` | 开机自启(与托盘勾选同步) | | `autoUpdate` / `updateIntervalHours` | 定期后台更新开关与间隔 | | `restartDelaySeconds` | 重启退避的基准延迟(按失败次数指数放大,封顶 2 分钟) | > 修改配置文件后需重启程序生效。 ## 源码结构 | 文件 | 职责 | |---|---| | `main.go` | 入口与 CLI 子命令(托盘 / daemon / status / autostart) | | `supervisor.go` | 守护核心:端口预检、隐藏启动、退避重启、状态快照 | | `probe.go` | 环境探测:node 路径、dsh 入口、已装版本(带缓存与串行化) | | `updater.go` | 更新与定期更新循环 | | `tray.go` | 托盘菜单与交互 | | `config.go` / `logger.go` / `npm.go` / `util.go` | 配置、日志、npm 调用、纯工具 | | `*_windows.go` | Win32 细节(隐藏窗口、进程树终止、单实例、控制台附加) | | `util_test.go` | 单元测试 | ## 日志 `%APPDATA%\deepseek-tray\logs\tray-YYYYMMDD.log`(**按天轮转**;含 dsh web 的 stdout/stderr;**token 已脱敏**) ## 验证结论 ### 单元测试(`go test`) | 测试 | 覆盖 | |---|---| | TestRedact | token 脱敏(多 token / 终止符 / 无 token / 空串) | | TestExtractHTTPURL | URL 提取与误报拒绝(httpx) | | TestShortURL / TestHumanDuration | 展示格式化 | | TestConfigNormalized | 配置越界回退默认值 | | TestIsPortOpen | 真实监听端口的探测 | | TestBackoff | 退避序列与封顶(**此测试发现并修复了上限未生效问题**) | | TestLoggerRotate | 日志按天轮转 | | TestConfigRoundTrip | 配置首次生成 / 保存重载 / BOM 容忍 / 损坏回退(隔离到临时目录) | | TestSetAutoUpdatePersists | 线程安全 setter 同时更新内存与磁盘 | | TestSupervisorLifecycle | 守护状态机:启动 / 自动重启 / 停止 | ### 运行时验证(v2.3.0;一次观测的实测结果,非持续保证) | 维度 | 验证项 | 结果 | 证据形式 | |---|---|---|---| | 可靠性 | 隐藏终端 | daemon / dsh web 的**顶层窗口句柄**为 0 | 进程属性测量 | | 可靠性 | 自动重启 | 假命令下观测到 启动 6 / 退出 4 / 重启 4 | 日志计数 | | 可靠性 | 单实例 | 第二个实例立即退出并记录 | 日志 | | 可靠性 | 启动/停止竞态 | 已处理(停止请求落在启动窗口时终止该进程) | **仅代码审查**;窗口毫秒级,未确定性复现 | | 健壮性 | 端口被占用 | 不启动冲突进程,退避 6s→12s→24s | 日志时间戳 | | 健壮性 | 配置损坏 / UTF-8 BOM | 回退默认值 / 正确读取 | 单元测试 + CLI | | 安全性 | 日志 token | 输出 `token=***`(真实值仅存内存) | 日志 | | 安全性 | 打开 URL/路径 | 不经 shell(ShellExecuteW) | 探针测试通过 | | 可靠性 | 打开日志/配置 | 窗口正常显示(不再被 SW_HIDE 隐藏) | 探针调用成功(目视确认由用户完成) | | 效率 | status 查询 / 二进制 | 0.4 秒 / 约 5.2 MB | 计时与文件大小 | > 复现方式:`build.ps1` 跑单元测试;上表运行时项可用 `--port 3099` 与假命令分别复现。未标注"实测"的行为代码审查结论。 ## 环境要求 - Windows 10/11 - 已全局安装 dsh:`npm install -g @deepseek-ai/dsh@latest --allow-scripts='@deepseek-ai/dsh-subprocess-local,koffi,node-pty,@google/genai,protobufjs'` - `node` 在 PATH 中 ## 版本 - v2.5.0 — 依据实际使用反馈优化:**菜单宽度**(长 URL 移入悬停提示)、**操作结果回显**(解决"点更新没反应"的观感问题)、**版本号改由构建脚本注入**(避免源码与版本号不一致)、更新流程合并为唯一入口(去掉重复查询)、耗时操作防重入 - v2.4.0 — 修复实测发现的三个问题:**打开日志/配置无反应**(误用 SW_HIDE 把 explorer 窗口隐藏了 → 改用 ShellExecuteW)、**Web 界面启动期不可用**(新增就绪状态,启动中禁用点击而非打开无效地址)、**更新白等 5 分钟**(版本相同时跳过整包重装与重启);菜单耗时操作后台执行不再阻塞;日志记录 Web 启动耗时 - v2.3.0 — 三次迭代:修复**停止/启动竞态**(不再残留进程)、配置写入线程安全、更新前先停服务、配置读写单元测试与守护生命周期测试、`build.ps1` 一键构建 - v2.2.0 — 二次迭代:**新增单元测试**(并借其修复退避上限问题)、修复定期更新"关闭后再开启"失效、职责拆分(probe/util)、日志按天轮转、进程终止兜底、daemon 优雅退出 - v2.1.0 — 自检优化:端口预检 + 指数退避 + 定期更新单例 + `Local` 互斥体 + 日志脱敏 + 原子配置 + `autostart` 子命令 - v2.0.0 — 重写为 `dsh web` 托盘守护(旧版余额查询功能归档于 `legacy-v1` 标签) ## 许可证 见 [LICENSE](LICENSE)。