dsh-harness-updater
在 DeepSeek Harness 的 Web「设置」里加一页 「安装 / 更新」,把 Harness 自身的 检查版本 → 在线安装 / 更新 → 看日志 → 重启生效整条链路搬进应用内。
仓库:https://github.com/udisyue/dsh-update · 许可证:MIT
DeepSeek Harness(dsh)的插件树只在启动时合成,profile 里的插件由 dsh plugin 管理。
这个插件补上的是它自己的运维闭环:不必再开终端敲 npm install --prefix ...,
也不必切到 DeepSeekDesk 的原生设置页——在 Web 设置面板里就能看版本、查更新、装新版、重启生效。
功能与 DeepSeekDesk 的 HarnessInstaller 对齐(同一套 npm install --prefix <目录> @deepseek-ai/dsh@<版本> 路径)。
特性
- 状态一目了然:当前运行版本、安装目录(含探测来源)、Node / npm / 监听端口 / PID
- 检查更新:直接读 npm registry 的
dist-tags,不依赖本机 npm,离线也能看本地版本 - 在线安装 / 更新:实时日志流式回显,装到哪个版本按钮上直接写明(例:
在线更新到 v0.1.6-alpha.1) - 高级选项:更新通道(latest / next / alpha / 指定版本)、安装目录、registry、npm 路径,可持久化为默认
- 重启 Harness:等旧进程退出后用同一份启动参数拉起新实例
- 查看
harness.log:带脱敏,不会把进程 token 再发出去 - 零运行时依赖、零构建步骤:Node 端只用
node:*,浏览器端只require("react"); 所以从 git 安装不需要 pnpm 的构建脚本授权
安装
从 GitHub
dsh plugin --profile web add -w github:udisyue/dsh-update
-w(--workspace-root)是必需的:profile 目录本身是 pnpm workspace 根,不加会被
pnpm v11 以 ERR_PNPM_ADDING_TO_ROOT 拒绝。
本包没有 prepare 脚本(纯 JS,无需构建),所以不会触发 pnpm ≥10 对 git 依赖的构建授权提示。
若要锁定版本,用 github:udisyue/dsh-update#<sha>。
从本地目录(开发 / 离线)
dsh plugin --profile web add -w /path/to/dsh-update
目录会以 link: 链接进 profile:改完 Node 端代码重启 Harness 生效,改浏览器端代码刷新页面即可
(客户端包是按请求实时读盘的)。
卸载
dsh plugin --profile web remove -w dsh-harness-updater
卸载后同样需要重启 Harness 才生效。注意顺序:profile 的 dsh.profile.bundles 里若留着一个
解析不到的包名,Harness 会拒绝启动(cannot resolve profile bundle ...),所以先 remove,再删目录。
激活时机
dsh.profile.bundles → 各 bundle 的 cordis.patch.yml 只在 Harness 启动时合成,
所以安装 / 卸载后都要重启一次:
- DeepSeekDesk 原生设置里的「重启 / 启动 Harness」,或直接重启 App(推荐:App 会自己重读 token 并重连)
- 或在插件页面里的「重启 Harness」按钮(装上之后就一直可用)
使用
打开 设置 → 安装/更新:
- 状态卡:当前版本、安装目录与探测来源、Node / npm、端口、重启开关
- 检查更新:拉 registry 的
dist-tags,告诉你最高版本是哪个、来自哪个通道 - 在线安装 / 更新:主按钮写明目标版本;执行中实时刷日志,结束后刷新状态
- 高级选项:通道 / 指定版本 / 安装目录 / registry / npm 路径,可「保存为默认」
- 重启 Harness:两步确认;脚本会把新实例输出写回
harness.log - 查看 Harness 日志:尾部 96KB,token 与
_authToken已脱敏
配置(可选)
插件行支持 cordis.yml 配置;在自己的 profile ~/.dsh/profiles/web/cordis.patch.yml 里覆盖:
- id: dsh-harness-updater
name: dsh-harness-updater
config:
installDir: '/Users/me/Library/Application Support/DeepSeekDesk/harness'
registry: 'https://registry.npmjs.org'
npmPath: '/opt/homebrew/bin/npm'
channel: 'latest'
restartEnabled: true
| 字段 | 默认 | 说明 |
|---|---|---|
installDir |
自动探测 | 显式指定后优先于一切探测;留空则按下面的顺序探测 |
registry |
https://registry.npmjs.org |
也可用国内镜像 |
npmPath |
自动探测 | GUI 环境 PATH 精简时用于兜底 |
channel |
latest |
默认安装通道(dist-tag) |
restartEnabled |
true |
关闭后设置页不提供重启按钮 |
设置页里改的值持久化在 $DSH_HOME/harness-updater/state.json(默认 ~/.dsh/harness-updater/state.json,
权限 0600),优先级 设置页保存值 > 插件配置 > 自动探测。
行为细节(都是实测过的)
默认安装到哪个版本
「在线安装 / 更新」的默认目标 = registry 上所有 dist-tag 里的最高版本,与 DeepSeekDesk
的 HarnessInstaller 同策略——因为新版本经常先发在 next / alpha 上。实际后果是:
// 实测(2026-09-15)
{ "alpha": "0.1.6-alpha.1", "next": "0.1.5-rc.2", "latest": "0.1.5-rc.1" }
// → 默认目标是 0.1.6-alpha.1(来自 alpha 通道)
这个 tag 分布本身很反直觉(latest 反而最旧),所以界面上会写明目标版本和来源通道,
避免"点一下更新,结果装了个 alpha"。想锁定通道,在「高级选项」里选具体 tag 或填指定版本。
安装目录探测顺序
- 设置页保存的目录 / 插件配置的
installDir/ 环境变量DSH_HARNESS_DIR、DSH_INSTALL_DIR - 正在运行的进程入口(
node <root>/node_modules/.bin/dsh web) - 插件模块图解析到的
@deepseek-ai/dsh(profile 里通常是软链) - DeepSeekDesk 默认目录
~/Library/Application Support/DeepSeekDesk/harness ~/.dsh/profiles/node_modules/@deepseek-ai/dsh反推
显式指定的目录优先于一切探测:否则"装到新目录"会被机器上已有的旧安装顶掉,静默变成"更新旧目录"。
重启后需要重新拿 token 吗?不需要
dsh web 启动时打印的 ?token= 地址只是首次建立会话 cookie 的入口,两套凭据是分开的:
- launch token —— 每进程随机(
randomBytes(32),存在进程内WeakMap),重启即作废;只在GET /上被接受一次 - 会话 cookie —— 用
$DSH_HOME/.credentials.yaml里持久化的密钥做 HMAC 签名,绑定 authority(host:port),默认 30 天
临时 DSH_HOME + 3099 端口做的重启前后实测:
| 请求 | 结果 |
|---|---|
重启后用旧 cookie GET / |
200(仍然登录) |
无 cookie GET / |
401 |
| 用重启前的 token 换 cookie | 401(token 已作废) |
| 用重启后的新 token 换 cookie | 303 |
所以重启后刷新页面即可。需要用 ?token= 重新引导的只有:cookie 被清掉或超过 30 天、
端口变了、换了 DSH_HOME、或换了个浏览器。
若自动重启没起来,可用日志里那行地址手动执行:
nohup node "<安装目录>/node_modules/.bin/dsh" web > "<安装目录>/../harness.log" 2>&1 &
安全
每个路由都过 connection.requestRejection
这是必须的,不是可选项。 webServer.register 注册的裸路由不经过任何框架级围栏——
框架自己的 /api/* 有,第三方插件路由没有。本插件最初也踩了这个坑,实测:
| 请求 | 加门卫前 | 加门卫后 | 框架 /api/* |
|---|---|---|---|
| 无凭据 | 200 | 401 | 401 |
Host 伪造为外站 |
200 | 403 | 403 |
Origin 跨站 |
200 | 403 | 403 |
sec-fetch-site: cross-site |
200 | 403 | 403 |
无凭据 POST /settings(text/plain 免预检) |
写入成功 | 401,无副作用 | — |
没这道门时,POST /restart(任意网页一个免预检请求就能重启 = DoS)与 POST /install
(让机器按攻击者给的版本跑 npm install)都是可达的,GET /status、/log 还会把安装路径与日志
交给任何本机进程——而 harness.log 里有进程 token 明文。
现在 7 个处理器第一行都是 if (rejectIfUntrusted(req, res)) return;,委托给
ctx.connection.requestRejection(403 = Host/Origin 围栏,401 = 没有浏览器会话);
门卫本身抛错时一律拒绝(fail closed),不降级放行。插件因此 inject 了 connection。
浏览器侧无需特殊处理:同源 fetch 用 credentials: "same-origin" 自动带上会话 cookie。
数据流向
- 不触碰模型:不注册 system-prompt 段、不发会话事件、不开工具 → 没有任何数据进入模型上下文或会话日志
- 出网只有一处:写死的 registry
dist-tags查询(地址可配置)+ npm 子进程,无任何遥测 - 日志脱敏:
/log与任务日志里的token=、_authToken、user:pass@都会被抹掉
残余项(知情即可)
harness.log文件本身仍含进程 token 明文(需要时直接读文件;API 返回的是脱敏版)/status会返回安装目录、npm 路径、PID 等本机信息——仅对已通过认证的浏览器可见- 界面上把
/Users/<name>缩写成~,但悬停的title属性仍是完整路径
HTTP 接口
浏览器端消费的同源路由(JSON,全部需要浏览器会话):
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /dsh-harness-updater/status |
状态快照(版本 / 目录 / npm / 端口 / 重启开关) |
| GET | /dsh-harness-updater/releases |
registry 的 dist-tags 与最高版本 |
| POST | /dsh-harness-updater/install |
启动安装任务,body {version?, tag?, dir?, npmPath?, registry?} |
| GET | /dsh-harness-updater/job?id= |
任务进度与日志 |
| POST | /dsh-harness-updater/settings |
保存覆盖项(空字符串 = 清除该项) |
| GET | /dsh-harness-updater/log |
harness.log 尾部(已脱敏) |
| POST | /dsh-harness-updater/restart |
重启 Harness(需 {confirm: true}) |
开发与验证
三段测试都在工作区里跑,不需要启动 Harness,也不会碰你正在运行的那份安装:
# Node 端:假 ctx 直调全部路由。覆盖 路由注册 / 目录探测 / registry 查询 / 设置持久化 /
# 任务生命周期 / 重启守卫 / 门卫 401·403·fail-closed / 脱敏 / 版本比较 / 重启脚本生成。
# 含一次真实 npm 调用:用不存在的版本让它快速失败,验证 spawn 与日志捕获,不下载几百 MB。
node tests/smoke.mjs
# 浏览器端:用真实 React 18 物化 client.js,断言注册进 settings.section
# (id / order / 导航文案)并渲染出完整页面。
mkdir -p .tmp/render-test && cd .tmp/render-test \
&& printf '{"name":"hup-render-test","private":true}\n' > package.json \
&& npm install react@18 react-dom@18 && cd ../..
node tests/client.spec.mjs
# 线上:确认插件已经在正在运行的那个实例里生效(跑在重启之后)。
node tests/verify-live.mjs # 默认 http://127.0.0.1:3080
node tests/verify-live.mjs http://127.0.0.1:3099 # 指定实例
# 退出码 0 = 生效(4 项鉴权检查 + 状态探测 + 引导图 + 包内容一致 + 日志脱敏 全绿)
# 退出码 1 = 未生效(通常是 Harness 尚未重启)
文件
index.js Node 端:路由 + 探测 + registry 查询 + npm 安装任务 + 重启脚本
client.js 浏览器端:设置面板「安装 / 更新」页(__ModuleLoader__ 手写 CJS,无构建步骤)
cordis.patch.yml bundle 层:把 Node 端插件行插进 profile
tests/smoke.mjs Node 端冒烟(假 ctx,直调路由)
tests/client.spec.mjs 浏览器端冒烟(真实 React 渲染)
tests/verify-live.mjs 线上验证(对运行中的实例查鉴权 + 引导图 + 包内容 + 脱敏)
环境要求
- Node.js
^22.19 || >=24(跟随 Harness 自身要求) - DeepSeek Harness,且使用带 Web 界面的 profile(
dsh web,默认webprofile) - 安装 / 更新需要本机有
npm;检查更新不需要(直接走 registry HTTP)
许可
MIT
No comments yet. Be the first to write one.