安装
在 DeepSeek Harness 里通过 dsh-market 安装
dsh plugin --profile web add dshmarket
或使用命令行
dsh plugin --profile web add github:Neptune810/dsh-power-button
装任何插件都等于在你的机器上跑第三方代码,权限和你本人一样大——能读你的文件、用你的凭据、访问网络。请先审阅源码,并尽量锁定 commit(github:owner/repo#sha)。
README
DeepSeek Harness 的 Web 插件:界面里一个可拖动的电源胶囊,带实时状态点,以及针对正在运行的
dsh web 服务的重启与结束两个动作。
纯宿主插件、零依赖、不需要构建。
为什么需要它
dsh web 不会自己退出。外面套了看门狗(启动器 / 脚本 / 托盘程序)的部署,通常要等一个空闲窗口
才动手关服务,于是最后一个浏览器标签关掉之后端口还会被占一段时间;这段时间里重启,打开的是一个
马上就要失效的页面。
这个插件把两个动作都放回你手上:界面里一个胶囊,外加一个告诉你宿主还在不在的状态点。
使用
一个半透明的小胶囊停在配置的角落(默认右下角)。
- 状态点 —— 上一次探活有回应就是绿色,没回应就是红色。探活是每 5 秒一次的同源
GET,不消耗任何模型 token。 - 按一下胶囊 —— 弹出一个小菜单,两个动作:
- 重启服务 —— 宿主原地重新拉起自己,端口恢复应答后自动打开新页面。
- 结束服务 —— 进程立刻退出,端口马上释放。
- 再按一下胶囊、或点页面别处,菜单就关掉。
- 设
restart: false则没有菜单,按一下直接结束。
- 按住拖动 —— 鼠标、触摸都行。位置存在
localStorage里,刷新页面后还在。 - 拖动不会误触发动作:指针移动超过 4px 才算拖动,浏览器随后补发的那次 click 会被吞掉。
- 拖出窗口会被夹回边缘,窗口变小后也会自动重新夹紧,永远点得到。
- 它挂在应用 React 根节点之外,任何重渲染都弄不丢。
实现方式
宿主插件。不构建任何前端 bundle,也不 import 任何 @deepseek-ai/* 包 —— 用 link:
安装的插件会被 Node 解析到真实路径,而 harness 的包并不在那里。它只用 Web 载体已有的两个扩展点:
| 扩展点 | 用途 |
|---|---|
webServer.tapIndex() |
把胶囊的 markup、CSS、脚本注入 index.html |
webServer.register() |
提供 /dsh-power-button/status、/restart、/stop |
结束走启动器提供的 ctx.appExit:和 Ctrl+C 同一条有界关闭路径,先优雅 dispose 整棵树,
再让进程退出。这条路径只有在事件循环真的排空时才结束进程,所以插件另外挂了一个
unref 的 2 秒看门狗 —— 干净退出时它不拖后腿,有别的插件握着句柄时它也保证进程一定消失。
重启没法由宿主自己做,因为要离开的正是宿主本身。重启路由会拉起一个脱离终端的辅助进程 (同一个 Node 二进制跑一段内联模块),它依次:
- 等这个进程消失,
- 等端口不再应答,
- 用同样的环境变量和工作目录,按记录下来的启动命令重新拉起,
- 需要的话,等端口恢复应答后打开页面。
辅助进程比宿主活得久(detached + stdio: "ignore" + unref()),所以它能活过触发它的
那次关闭。
Windows 11 / Node 24 实测:结束在 POST 返回 200 后约 0.2–0.5 秒释放端口;重启几秒内端口就回来。 失败也是安全的 —— 辅助进程起不来时路由回 500,服务保持运行。
安全
三个路由都必须同时满足:
- 带自定义请求头
x-dsh-power-button: stop—— 跨域页面要带这个头就得先过 CORS 预检, 而本服务从不批准预检,所以随便一个网页既重启不了也关不掉你的服务; Host是回环地址(127.0.0.1/localhost/[::1]),堵掉 DNS rebinding;- 请求若带
Origin,必须与该 Host 同源。
其余一律 403。status 只接受 GET/HEAD;stop 与 restart 只接受 POST,
方法不符一律 405。
插件不碰 profile 之外的任何东西:重启辅助进程只按本进程当初的启动命令,或你显式配置的
restartCommand 执行。
配置
全部可省略,写在插件行上:
`@yaml
- insert:
- id: power-button name: dsh-power-button config: label: 结束服务 # 胶囊文字与 tooltip position: bottom-right # 初始角落,或 bottom-left draggable: true # false 则钉死在初始角落 status: true # false 则去掉状态点、停止探活 statusPollMs: 5000 # 探活间隔,最小 1000 restart: true # false 则去掉重启动作和菜单 restartCommand: [] # argv;留空 = 按本次启动命令原样重来 restartOpenBrowser: true # 命令自己会开页面时设为 false restartWaitMs: 30000 # 辅助进程最多等端口多久 enabled: true # false 则完全不注入
`@
通过启动器重启
默认情况下辅助进程重来的是本进程自己那条命令 —— 同一个 Node、同一个脚本、同一组参数。 这是插件唯一能确定的事情,代价是重新拉起的宿主背后没有看门狗:它会一直活着,直到你按结束。
如果你的部署是由某个启动器拉起来的,而你希望它一起回来(比如它自带空闲看门狗),把
restartCommand 指向它,并让它自己负责开浏览器:
`@yaml
- insert:
- id: power-button name: dsh-power-button config: restartCommand: - 'C:\path\to\YourLauncher.exe' restartOpenBrowser: false
`@
安装
@sh dsh plugin --profile web add dsh-power-button @
装完必须重启 dsh web:插件改动不会影响已经跑起来的进程。
测试
@sh node --test test/plugin.test.js @
测试零依赖。注入到页面的脚本会被真的执行,跑在一套手搭的 DOM 上 —— 覆盖按下与拖动的区分、
移动阈值、边缘夹紧、位置持久化、菜单、重启动作、状态探活;三个路由则用假的 node:http
请求/响应来驱动,包含那条会静默丢掉 res.end 回调的压缩路径。脱离终端的辅助进程源码也会
按 ESM 做语法检查,坏掉的重启不可能发出去。
许可
MIT
评论
评论存放在 GitHub Discussions。用 GitHub 账号登录后可发表评论或点表情。