# dsh-thinking-notifier 一个 [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness) 插件:在屏幕**右下角**显示一个**置顶桌面悬浮窗**,实时显示 AI 思考状态;当 AI **请求权限**、**等你做选择**或**完成思考**时提醒你。即使你切到其它网页、桌面或桌面应用,也能看到统一风格的弹窗。 > English version: [README.md](./README.md) ## 功能 | 状态 | 悬浮窗 | 声音 | | --- | --- | --- | | AI 正在思考 | DeepSeek 蓝呼吸点 + `AI 正在思考…` + 计时;5 秒后自动收起为右侧 25 像素边缘状态条,悬停或点击可展开 | 无 | | AI 等你做选择 | 紫色呼吸点 + `AI 正在等待你的选择` + 问题标题、第一个问题的选项(`选项:A / B`,可多选时显示`(可多选)`,一次问多个问题时显示`(共 N 问)`)与已等待时长;保持展开直到你回答 | 系统 Question | | AI 请求权限 | 琥珀色呼吸点 + `AI 请求权限` + 会话/工具名;保持展开直到你处理 | 系统 Exclamation | | AI 完成思考 | 绿色点 + `AI 已完成思考`,12 秒后自动隐藏 | 系统 Asterisk | | 空闲 | 自动隐藏 | 无 | - DeepSeek 风格深色卡片(`#1E2235` / `#4D6BFE`)。 - 无边框置顶窗口,右下角显示,覆盖所有应用。 - 思考中显示 5 秒后自动收起为右侧 25 像素边缘状态条,既保持可见又不遮挡桌面按键。 - 鼠标悬停或点击边缘状态条可手动展开;新的思考、提问、权限请求或完成状态会自动展开。 - 点击右上角 **X** 可提前关闭当前弹窗;后续新的思考、提问、权限请求或完成状态会重新显示。 - 提问会连选项一起显示,因此不必切回窗口就能知道 AI 在问你什么;回答仍然在 DSH Web UI 里完成(悬浮窗是只读提示)。 - 多个会话自动汇总显示。 - 弹窗进程在 DSH 运行期间常驻;状态服务不可达约 15 秒后自动退出。 - 不需要浏览器扩展,弹窗是原生桌面窗口。 ## 工作原理 ``` DSH 进程 └─ lib/index.js(主机插件) ├─ 监听 session/event:turn/start、turn/end、approval/asked、approval/decided、session/title ├─ 启动时从 ctx.sessions.list() 重建已有会话状态 │ (日志停在未闭合 turn 里的会话按“没在思考”处理,避免崩溃遗留导致永久误报) ├─ 包装 ctx.userQuestions.ask 以观测 ask_user_question │ (该状态不产生 session 事件,且只允许一个 UI provider,无法再注册一个) ├─ 在 http://127.0.0.1:7389/status 提供本地状态 JSON └─ 拉起桌面弹窗进程 ├─ Windows: desktop-popup.ps1(PowerShell + WPF) └─ macOS/Linux: desktop-popup.py(Python 3 + tkinter) 桌面弹窗 ├─ 每 1 秒轮询 /status ├─ 渲染 DeepSeek 风格卡片(question/pending/running/done/idle) ├─ question/pending/done 状态切换时播放系统提示音 ├─ 思考中自动收起为右侧 25 像素边缘状态条;等你操作时保持展开 └─ 空闲时隐藏,进程保持运行 ``` ### 状态 JSON `GET http://127.0.0.1:7389/status` 返回: ```json { "ok": true, "mode": "question", "statusText": "AI 正在等待你的选择", "sessionText": "Choose Mode", "detailText": "选项:样式 A / 样式 B(可多选)", "timeText": "已等待 00:12", "pendingKind": "question", "pendingTool": null, "runningSince": null, "doneAt": null, "questionCount": 1, "questionSince": 1791374946369, "appUrl": "http://127.0.0.1:3080", "now": 1791374946369 } ``` `mode` 优先级:`pending`(等权限)> `question`(等选择)> `running` > `done`(显示 12 秒)> `idle`。 `pendingKind` 为 `approval` 或 `question`。`timeText` 在 `running` 时是思考计时、在 `question` 时是已等待时长;`detailText` 携带选项摘要。 ## 安装 ### 本地安装 ```sh cd dsh-thinking-notifier dsh plugin --profile web add . # 重启 DSH Web 以加载主机插件 dsh web ``` ### 发布到 npm 后安装 ```sh dsh plugin --profile web add dsh-thinking-notifier dsh web ``` ### 卸载 ```sh dsh plugin --profile web remove dsh-thinking-notifier ``` ## 配置 | 环境变量 | 默认值 | 说明 | | --- | --- | --- | | `DSH_THINKING_NOTIFIER_PORT` | `7389` | 本地状态服务端口,被占用时自动顺延 10 个端口 | | `DSH_THINKING_NOTIFIER_NO_POPUP` | 未设置 | 设为 `1` 时只跑状态服务、不拉起桌面弹窗(无桌面环境/测试用) | ## 环境要求 - DeepSeek Harness Web UI(`dsh web`) - Node.js `^22.19.0 || >=24.0.0` - Windows:Win10/11 自带 PowerShell 5.1+ - macOS/Linux:Python 3 + `tkinter` ## 验证情况(如实说明) | 项目 | 状态 | | --- | --- | | 思考中/已完成状态、弹窗几何、提示音、单实例 | 已在 Windows 10/11(PowerShell 5.1 + WPF)实测 | | 提问状态(`ask_user_question`) | 已在真实 DSH 会话里端到端验证(真实提问让弹窗进入等待态,作答后自动清除),并在真实 cordis 上下文对真实 `ctx.userQuestions` 服务验证、有冒烟测试覆盖 | | 权限提醒(琥珀色) | **仅用模拟状态服务验证过。** 当前 Windows 部署下 bash 没有 confining sandbox,越界写入不会被拦截,运行时不会自发产生审批;只有工具显式请求 `sandbox_permissions` 升级,或 pre-execute 钩子返回 `ask` 时才会出现该状态 | | `desktop-popup.py`(macOS/Linux) | **从未在真实机器上运行**,只做过语法检查 | | 反复重启 `dsh web` 后的弹窗叠加 | 启动前会先清理**同端口**的旧弹窗进程;已覆盖单次重启场景 | ## 开发 ```sh # 状态机冒烟测试(假 cordis 上下文、不拉弹窗、端口 17389) npm test # 用模拟状态服务看弹窗渲染,用空闲端口 node tools/mock-status.mjs question --port 17389 powershell -NoProfile -ExecutionPolicy Bypass -File desktop-popup.ps1 -Port 17389 # Windows python3 desktop-popup.py --port 17389 # macOS/Linux ``` 做弹窗实验请用空闲端口:插件只会清理**自己端口**的弹窗进程,`7389` 上真正在用的弹窗不会被影响。 ## 项目结构 ``` dsh-thinking-notifier/ ├── lib/ │ └── index.js # 主机插件:状态机 + 状态服务 + 弹窗拉起 ├── desktop-popup.ps1 # Windows 原生弹窗(PowerShell + WPF) ├── desktop-popup.py # macOS/Linux 原生弹窗(Python 3 + tkinter) ├── cordis.patch.yml # DSH bundle patch,用于持久挂载 ├── test/ │ └── status-smoke.mjs # 状态机冒烟测试 ├── tools/ │ └── mock-status.mjs # 模拟状态服务,用于离线看弹窗 ├── package.json # npm 包 + dsh.bundle 声明 ├── README.md # 英文说明 ├── README.zh.md # 中文说明 ├── CHANGELOG.md ├── CONTRIBUTING.md └── LICENSE ``` ## 排错 调试日志位置: - `~/.dsh/tn-plugin.log` — 主机插件生命周期、弹窗启动器、提问包装。 - `~/.dsh/tn-launcher.log` — 启动器创建的弹窗进程 PID。 - `~/.dsh/tn-popup-debug.log` — 弹窗脚本生命周期与轮询错误。 重启 `dsh web` 后如果看不到弹窗,请先查看以上日志。 注意 `tn-plugin.log` 里的 `popup launcher exit: code=0` 是正常的:插件先启动一个一次性启动器,由它创建真正常驻的弹窗进程然后退出。 ## License [MIT](./LICENSE)