安装
在 DeepSeek Harness 里通过 dsh-market 安装
dsh plugin --profile web add dshmarket
或使用命令行
dsh plugin --profile web add @zhengjunyao/dsh-updater
装任何插件都等于在你的机器上跑第三方代码,权限和你本人一样大——能读你的文件、用你的凭据、访问网络。请先审阅源码,并尽量锁定 commit(github:owner/repo#sha)。
README
English | 中文
跟踪 DeepSeek Harness 自身的版本并一键更新:跟 npm dist-tags(latest / next / alpha)与
GitHub Release(tag dsh-v*,双语正文),在 Web GUI 里显示当前版本到目标版本之间每一个版本的更新点
和一份分级风险清单,然后把安装交给一个分离助手——用 mv 把当前安装瞬时移开做备份 →
npm install --prefix <当前安装前缀> → 校验落地版本 → 用完全相同的命令重启 →
启动失败自动回滚。
一句话定位
DSH 的更新频率高(0.1.x-rc.x 通道多),而 dsh 自身不带任何自更新机制。
npm i -g @deepseek-ai/dsh之后还得自己重启一次,而且重启命令必须和当初启动时一模一样 (端口、profile、cwd、node 路径都可能被改过)。- 本机可能有多份 DSH 安装:
process.argv[1]反查出的运行安装、PATH里最先命中的那份、 以及npm config get prefix指向的那份,三者完全可以是三个不同目录——一次天真的npm i -g会更新到最后那个,然后什么变化都不会发生。 - npm 发布频繁但只有 GitHub Release 写变更说明,且 npm 上有的版本不一定有对应 release; 反过来,只在 GitHub 发布的版本根本装不上。
本插件把「看版本 → 读更新点 → 评风险 → 装 → 校验 → 重启 → 失败回滚」串成一条链,并让 正在运行的安装目录成为唯一的更新目标。
四个能力
1. 跟踪最新版本
- 三个通道:npm dist-tag
latest/next/alpha,由配置项channel决定跟随哪一个。 - 后台定时检查:
checkIntervalMinutes(默认 180 分钟,0 关闭)+checkOnBoot(启动后 8 秒跑一次)。 后台检查的目的不是打扰,而是让缓存里的 registry / releases 快照保持温热, 打开面板或问 Agent 时不用等两次网络往返。 - 缓存:
cacheTtlMinutes(默认 30 分钟)内的结果直接复用,落地在cache/registry.json与cache/releases.json;force参数可绕过缓存。 - 上游故障不致命:registry 或 GitHub 拉取失败时,沿用上一次成功的结果并在
upstream.warnings里标注「可能是旧的」——「连不上 GitHub」不会被显示成「没有新版本」。 - dist-tag 缺失或滞后时按通道阶梯回退:
next/alpha没打 tag 时,回退到latest之上最新的预发布版本,避免「明明有更新的预发布却报无更新」。 - 面板同时显示
newerElsewhere:当前通道之外是否存在更新的版本。
2. 显示更新点
- 版本之间的事实来自 npm(能不能装),变更说明来自 GitHub Release(改了什么)。
- 更新点覆盖
(当前版本, 目标版本]区间内每一个已发布版本,不只是一个 diff; 用户跑latest常常一次跨过好几个发布。 - 每个版本按 GitHub Release 正文里的
<h3>/#标题分组渲染条目; 正文是双语的(中文段 +---+ 英文段),解析优先取中文段,缺失时回退英文段。 - 拿不到对应 release 的版本明确标出「没有找到该版本的发布说明」(
notesMissing), 不假装没有变化。 - 只在 GitHub 发布、npm 上不存在的版本会被排除出可选目标(
installMissing)并说明原因。 - GitHub 未认证限流是 60 次/小时;配置
githubToken可提高上限,面板会显示剩余额度与重置时间。
3. 一键更新
入口是设置页「DSH 更新」卡片里的按钮或侧边栏入口,也可以由 dsh_update 工具触发。
真正的安装由分离助手 helper/update-helper.mjs 完成,因为一个进程无法安全替换自己正在运行的代码:
- 宿主写
pending-spec.json,spawn 分离助手,回 202 之后才退出自己; - 助手等旧宿主释放端口(超时后 SIGKILL 旧进程兜底);
mv(rename)把当前安装原子移开做备份——不是复制三万个文件;同目录 rename 是瞬时且原子的, 因此回滚不依赖剩余空间;npm install -g --prefix <当前安装前缀> --registry <registry> --no-audit --no-fund @deepseek-ai/dsh@<版本>(--prefix永远显式传入,不用npm config get prefix);npm 输出实时流入日志;- 校验落地结果:读
package.json版本必须等于目标版本、入口脚本<root>/lib/bin.js必须存在、node_modules/@deepseek-ai下必须有包(npm 退出码为 0 也可能留下半装目录); - 用完全相同的调用重启(
process.execArgv+argv[1:]+ 原 cwd + 原可执行文件), 所以dsh web --port 3080、自定义 profile、自定义路径都原样保留; - 启动失败则把备份
rename回原位并拉起旧版本; - 期间恢复控制台始终可用(默认
http://127.0.0.1:3098,被占用时依次试 +1…+9)。
4. 更新风险提示
版本号本身不足以回答「该不该按这个按钮」。风险引擎把目标版本、本机安装拓扑、 已装插件契约与本次操作的可行性汇总成一份有序清单,分三级:
high:不要盲目执行,默认拦截一键更新直到显式确认;warn:先读一遍;info:背景信息。
检查项(每一条都来自代码,不是猜测):
| 风险项 | 检查内容 | 级别 |
|---|---|---|
prerelease |
目标版本带预发布标识(rc / alpha / beta)。跟随 latest 通道却拿到预发布时升为 high |
warn / high |
breaking |
扫描更新点文本里的破坏性措辞(BREAKING、不兼容、破坏性、重大变更、不再支持、已移除、移除了、废弃、已弃用、必须升级、需要手动、要迁移、升级指南、deprecat、backwards-incompatib 等) |
high(命中时);否则一条 info 说明「未发现」 |
migration |
更新点里提到迁移 / 改名(migrat、迁移、改为、改名、重命名、rename) |
warn |
version-jump / version-gap |
跨版本幅度:跨 ≥2 个次版本或跨主版本记 warn(跨 ≥4 个次版本或跨主版本时);小跨度只记 info |
warn / info |
notes-missing |
区间内有版本在 npm 上存在却没有对应 GitHub release,变化内容未知 | warn |
not-on-npm |
区间内有版本只在 GitHub 发布、npm 上没有,无法安装,已从可选目标排除 | info |
install-unknown |
反查 process.argv[1] 得不到安装根,更新不知道要替换哪个目录 |
high |
multi-install |
本机存在多份 DSH 安装:说明本次只更新正在运行的那一份,并列出其它份;若 PATH 首选与运行中的不是同一个目录,明确指出「更新完重启后手动敲 dsh 可能又跑回旧的那一份」 |
high |
path-mismatch |
只有一份安装,但 PATH 首选仍与运行中的不一致 |
warn |
prefix-drift |
npm config get prefix 与运行安装的前缀不一致(直接 npm i -g 会装到前者) |
info |
preflight:* |
预检结果逐条并入清单(见下) | 按检查项 |
plugin-incompatible |
已装插件在各自 package.json 的 dsh.engines.dsh 里声明的范围是否覆盖目标版本;不覆盖时提示「bundle 加载失败会让插件树整体起不来」 |
high(有插件不兼容时);否则一条 info 汇总(含未声明范围的插件数) |
backup |
更新前是否备份;关闭备份时明确写出「npm 会留下半装目录且无法自动恢复」 | info / warn |
session-interrupt |
更新必然中断当前会话,并说明自动回滚是否开启 | info |
预检项(preflight,全部来自 src/installs.ts,无论通过与否都会逐条返回):
| 预检项 | 通过条件 | 不通过时级别 |
|---|---|---|
target |
从 npm registry 解析到了目标版本号 | high |
install-root |
从 process.argv[1] 反查到了 @deepseek-ai/dsh 包根 |
high |
install-writable |
当前用户对安装根(及其前缀)有写权限,不需要 sudo | high |
install-complete |
安装根下 node_modules/@deepseek-ai 有包(否则疑似损坏,更新会顺带修复,但回滚点也是这个损坏状态) |
warn |
npm |
PATH 里能找到 npm |
high |
prefix |
能从安装目录推导出 npm 前缀(<prefix>/lib/node_modules 布局) |
warn |
node |
Node 主版本 ≥ 20(@deepseek-ai/dsh 自己不声明 engines,npm 不会替你拦低版本 Node,所以这里自设下限);解析不出主版本号时跳过 |
high |
tarball |
目标版本的 dist.tarball HEAD 返回成功(镜像/网络异常只记 warn,明确 404 才 high) |
warn / high |
disk |
剩余空间 ≥ 2 GB(一份新安装 + 一份等大的旧安装备份 + npm 的下载副本);读不到空间时跳过 | high |
分级与拦截:
level是所有风险项里最严重的一级;score是 0–100 的加权分(high45 分 /warn12 分 /info2 分,上限 100),供面板画表盘。vetted为false表示清单里仍有high项——「还没被审过」。confirmOn决定一键更新的拦截门槛:high(默认)只拦high;warn连warn也拦;never完全关闭拦截(仍然显示清单)。- 被拦时:HTTP 返回 428(面板)或工具返回
requiresRiskConfirmation: true(Agent), 并附上最严重的前几项原因;确认后带confirmRisk: true重试。 - 检查在点击更新的那一刻重新跑一遍:用户确认过的那份报告,必须是即将执行的那次操作对应的报告。
安装
# 从 GitHub(仓库带 dsh-plugin topic)
dsh plugin --profile web add github:zhengjy01/dsh-updater
# 本地开发:link / file 两种写法
dsh plugin --profile web add link:/path/to/dsh-updater
dsh plugin --profile web add file:/path/to/dsh-updater
# 发布到 npm 之后
dsh plugin --profile web add @zhengjunyao/dsh-updater
装完需要重启一次 dsh web 才会加载宿主侧代码。
界面位置
- 设置页「DSH 更新」卡片(
settings.section,idupdater,order 339):当前版本与运行安装目录、 本机安装份数、各通道目标版本、更新点、风险清单、一键更新按钮、更新历史、上次失败报告、插件设置。 - 侧边栏入口:由
entry控制——sidebar(默认,内联在侧边栏)/ball(固定在角落的球)/both(两者都有)/off(不挂载入口)。 - 更新期间显示全屏遮罩(
showOverlay,默认开),新宿主应答后自动刷新页面(autoReload,默认开)。
侧边栏入口的 tooltip / aria-label 与全屏遮罩的标题同样是「更新」文案,界面文字与上面描述一致。
界面截图
三张实机截图(本机 macOS,DSH 0.1.5-rc.1,窗口宽约 800px)。点击可看全尺寸。
① 默认 latest 通道:已是最新
注意画面里那块琥珀色的安装诊断:latest 通道下当前已是最新、没有任何更新可做,
但它照样显示——因为「本机有两份安装、PATH 首选与运行中的不是同一份」这件事与「有没有更新」无关,
用户随时都该看见。这是本机真实命中的情况,不是演示数据。
② 折叠区:更新记录 / 更新日志 / 失败报告 / 可用备份 / 插件设置
③ 切到 next 通道:出现可更新版本与逐条更新点
徽章从「已是最新」变成「可更新到 0.1.5-rc.2」,右上角目标版本随通道切换,
下方展开「更新点 0.1.5-rc.1 → 0.1.5-rc.2 · 跨 1 个版本」——每个版本一个可折叠区块,
带预发布徽章、发布日期与 GitHub release 链接,正文按 体验优化 等分组逐条列出。
HTTP 路由
全部注册在 /api/dsh-updater/*,loopback-only(来源地址必须是 127.0.0.1 / ::1 /
::ffff:127.0.0.1,Host 必须是 127.0.0.1 / localhost / [::1],Sec-Fetch-Site: cross-site
被拒,带 Origin 时必须同源):
| 方法 | 路径 | 作用 | 关键状态码 |
|---|---|---|---|
| GET / HEAD | /api/dsh-updater/probe |
极小存活探针,页面重连时高频轮询 | 200;非 loopback 403;其它方法 405 |
| GET | /api/dsh-updater/status |
宿主 + 助手实时状态 + 配置 + 安装列表 + 备份 + 历史 + 失败报告 | 200;403 / 405 |
| GET | /api/dsh-updater/check |
解析通道、拉取更新点、给出风险清单(参数 channel / version / force) |
200 |
| POST | /api/dsh-updater/update |
交接更新:写 spec、spawn 助手,回 202 之后退出宿主 | 202 已安排;400 无目标版本 / 识别不出安装目录 / 无 npm / 找不到助手脚本;409 已经是最新或目标不比当前新;428 需要风险确认;500 交接失败 |
| GET | /api/dsh-updater/logs |
日志尾部 + 疑似报错行(参数 lines、which = latest / auto / spec / 绝对路径) |
200 |
| GET | /api/dsh-updater/history |
更新记录(参数 limit,上限为 historyLimit) |
200 |
| GET | /api/dsh-updater/failure |
上次失败报告;?format=md 返回 text/markdown |
200 |
| GET | /api/dsh-updater/installs |
本机每一份 DSH 安装 + 工具链事实 | 200 |
| POST | /api/dsh-updater/config |
打补丁改配置;{"reset": true} 恢复默认 |
200;400 请求体不是合法 JSON |
| GET | /api/dsh-updater/helper |
经宿主读取助手实时状态 | 200 |
| POST | /api/dsh-updater/helper/{retry|rollback|recheck} |
把命令转发给恢复控制台 | 200;404 未知命令;502 控制台不可达 |
/update 明确先回包再退出进程:回复上线之后才调用 commit(),浏览器永远拿得到确定的答复。
Agent 工具
| 工具 | 读写 | 参数 | 返回要点 |
|---|---|---|---|
dsh_update_status |
只读 | channel、force |
运行版本与它实际加载的目录、本机安装份数、PATH 首选根、各通道目标版本、是否有更新、最近 6 条历史、可用备份、助手阶段与上次结果、失败报告正文、上游告警 |
dsh_update_check |
只读 | channel、version、force |
逐版本更新点(含「没有找到该版本的发布说明」标记)、风险清单([级别] 标题|详情)、riskLevel / riskScore / vetted、不兼容插件、未通过的预检项 |
dsh_update |
写:真正安装并重启 | confirm、confirmRisk、channel、version、reason、delayMs |
目标版本、助手 pid、备份目录、日志路径、恢复控制台地址、约多少毫秒后退出 |
dsh_update 的门槛:
confirm: true是必要条件——未经用户同意不得更新。本机铁律同样适用:不得私自重启或关闭 DSH, 而未提供confirm时工具只返回提示、不执行任何动作。- 风险清单里有
high项时还需要confirmRisk: true(被拦时返回requiresRiskConfirmation: true与最严重的前几项)。 - 推荐流程:先
dsh_update_check,把更新点与风险讲给用户 → 得到明确同意 → 再调用dsh_update。 - 更新必然中断当前回合并断开本会话连接;网页端会自动重连刷新。
配置
文件:DSH_HOME/dsh-updater/config.json(默认 ~/.dsh/dsh-updater/config.json,0600)。
首次加载时按插件行里的种子生成一次,之后文件为准(每次更新前重新读取)。
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
enabled |
boolean | true |
总开关;为 false 时不挂载路由与工具 |
announceToAgent |
boolean | true |
是否在 Agent 系统提示里公告本插件(工具与行为) |
entry |
'sidebar' | 'ball' | 'both' | 'off' |
'sidebar' |
面板入口在 GUI 里的位置 |
channel |
'latest' | 'next' | 'alpha' |
'latest' |
解析目标版本时跟随的 npm dist-tag |
checkIntervalMinutes |
number | 180 |
后台重新检查的间隔(分钟),0 关闭定时器 |
checkOnBoot |
boolean | true |
宿主启动后不久跑一次检查 |
registry |
string | 'https://registry.npmjs.org' |
npm registry 基础地址 |
apiBase |
string | 'https://api.github.com' |
GitHub API 基础地址 |
githubToken |
string | '' |
可选 GitHub token,提高未认证的 60 次/小时 release 限额 |
cacheTtlMinutes |
number | 30 |
registry / releases 缓存的新鲜时长(分钟) |
npmArgsExtra |
string[] | [] |
追加到 npm install 命令后的额外参数(最多 20 项) |
backupWhenUpdating |
boolean | true |
替换前是否把当前安装完整备份(rename 移开) |
backupCount |
number | 3 |
保留几份更新前备份 |
autoRollback |
boolean | true |
新版本启动失败时是否自动恢复备份 |
confirmOn |
'warn' | 'high' | 'never' |
'high' |
哪一级风险需要显式确认后才允许更新 |
fallbackPort |
number | 3098 |
分离恢复控制台的端口(被占用时依次试 +1…+9) |
bootTimeoutMs |
number | 180000 |
新宿主多久没应答算这次尝试失败 |
maxAttempts |
number | 2 |
单次更新请求的安装/启动尝试次数(1 = 不自动重试) |
killGraceMs |
number | 6000 |
SIGTERM 旧宿主之后、助手 SIGKILL 之前的宽限 |
portFreeTimeoutMs |
number | 25000 |
等旧宿主释放端口的上限 |
lingerMs |
number | 4000 |
成功后助手保留控制台多久再退出 |
readyConfirmMs |
number | 4000 |
端口应答后、判定「已就绪」前必须守住的时长 |
bootWatchMs |
number | 30000 |
已报就绪后继续观察新宿主、把「起来又死」改判为失败的时间窗 |
installTimeoutMs |
number | 600000 |
npm install 本身允许运行多久 |
logLines |
number | 400 |
面板与接口保留/显示的日志行数 |
autoReload |
boolean | true |
新宿主应答后自动刷新页面 |
showOverlay |
boolean | true |
等待期间显示全屏遮罩 |
probeIntervalMs |
number | 1200 |
重连探测间隔(毫秒) |
historyLimit |
number | 40 |
history.json 里保留的更新记录条数 |
每个字段都会被钳制到合理区间(例如 maxAttempts 1–5、backupCount 0–20、
installTimeoutMs 30000–3600000),非法值静默回落到默认值,配置文件不会让插件抛错。
更新流程
面板按钮 / dsh_update 工具
│ POST /api/dsh-updater/update(先重新跑一遍检查与风险分级)
▼
宿主(旧进程)
├─ 写 pending-spec.json(0600)+ 追加一条 history 记录(outcome: pending)
├─ spawn 分离助手 helper/update-helper.mjs --spec <spec>(detached, stdio ignore)
├─ 回 202(helperPid / logFile / consoleUrl / backupDir / exitInMs)
└─ 回复上线后才 SIGTERM 自己(~700ms;15s 后硬退出兜底)
▼
助手(零依赖纯 Node ESM,宿主死后继续活)
├─ 起恢复控制台(3098,占用则 +1…+9)
├─ 等 127.0.0.1:<port> 释放(超时则 SIGKILL 旧 pid)
├─ mv 当前安装 → 同级 .dsh-updater-backup-<版本>-<时间>(rename,原子)
├─ npm install -g --prefix <前缀> --registry <registry> --no-audit --no-fund @deepseek-ai/dsh@<版本>
├─ 校验:package.json 版本 == 目标;lib/bin.js 存在;node_modules/@deepseek-ai 非空
├─ 用完全相同的 file/argv/cwd/env 拉起新宿主(stdout+stderr → logs/<时间>-<pid>.log)
├─ 三重就绪判定:端口应答 + 进程存活 + 启动输出无 boot 致命行
│ 先守 readyConfirmMs(4s)再确认
├─ 就绪后继续观察 bootWatchMs(30s):期间掉线或出现致命行 → 重新判为失败
├─ 失败 → rename 恢复备份 → 拉起旧版本(attempt 预算重置,再试一轮)
├─ 仍失败 → 停在 failed,控制台留着等人工处理
└─ 成功 → 清理超出 backupCount 的旧备份 → 停留 lingerMs 后退出
▼
页面轮询 /api/dsh-updater/probe → 新宿主应答 → location.reload()
助手与宿主通过 status.json 交换状态。宿主无法读到自己死后写下的结果,
所以每次启动时会做一次 history 对账:只有当 status.json 里的目标版本与来源版本
和最新那条记录吻合时,才把 outcome / landedVersion 补回历史。
文件与目录
插件自身的东西都在 DSH_HOME/dsh-updater/ 下(可用 DSH_UPDATER_HOME 覆盖;
配置文件另可用 DSH_UPDATER_CONFIG 覆盖):
| 路径 | 内容 |
|---|---|
config.json |
插件设置(0600) |
history.json |
每次更新请求一条记录,最新在前(上限 historyLimit) |
status.json |
由分离助手写入的实时更新状态 |
pending-spec.json |
下一次助手运行的交接载荷(0600) |
logs/<时间>-<pid>.log |
一次更新的 npm 与宿主输出 |
cache/registry.json |
最近一次 npm registry 拉取(带 TTL) |
cache/releases.json |
最近一次 GitHub releases 拉取(带 TTL) |
last-failure.md |
失败时写成的一份可整段复制的报告(成功启动时会被删除) |
更新前备份不在这里,而是刻意放在被保护安装的同级目录:
<安装根>/../.dsh-updater-backup-<版本>-<时间戳>/
例:/opt/homebrew/lib/node_modules/@deepseek-ai/.dsh-updater-backup-0.1.1-20260913-205812/
同目录才能保证 rename 成功,取出与放回都是单次系统调用;该目录名被 npm 视为普通目录而不会清理它。
backupCount 之外的旧备份在更新成功后会被删除。
安全与风险边界
诚实地讲清楚:
- 更新会中断当前回合、断开浏览器连接并重启宿主。这是替换宿主进程的必然结果,不是可选项。
- 回滚恢复的是「更新前那一份安装」,也就是旧代码;它不保证与更新后新版本写下的数据或配置兼容。 「回滚成功」等于旧版本能起来,不等于一切回到更新之前。
confirmOn: 'never'会关掉风险拦截——风险清单照常显示,但一键更新不再被high项挡住。 这是给用户对自己机器做的选择,默认值是high。- 预发布版本(
-rc.*/-alpha.*/-beta.*)官方并未标为稳定版;接口与插件契约可能随时再变, 回滚也不保证插件兼容。 - 路由只在 loopback 上注册,且有同源与
Sec-Fetch-Site守卫;恢复控制台则是开放 CORS 的本地 HTTP 服务(Access-Control-Allow-Origin: *),任何能在本机访问该端口的进程都能读到 日志与失败报告,也能 POST 重试/回滚。它只应存在于一次更新的存活期间。 - 备份与安装目录必须在同一个文件系统上:备份是
rename,跨文件系统无法完成, 此时takeBackup()会失败并中止安装(不会降级为慢速复制)。 - 安装目录不可写时预检直接给
high:分离助手无法输入密码,需要 sudo 的场景只能手动在终端执行。 - 助手不会注册任何 OS 级后台服务;它只活一次更新,成功后停留数秒即退出,
失败时留在原地等待处理(可随时
kill)。
故障排查
恢复控制台(默认 http://127.0.0.1:3098,被占用时端口号会 +1…+9;
DSH_HOME/dsh-updater/status.json 与 /api/dsh-updater/helper 里的 fallbackPort 是权威值):
| 控制台路由 | 内容 |
|---|---|
GET / |
页面:阶段与已用时、当前安装步骤(完整的 npm 命令行)、npm 输出、启动日志、疑似报错行、失败原因、退出码 |
GET /status |
助手的状态快照(JSON) |
GET /report |
完整失败报告(markdown),供整段复制 |
GET /log?lines=N |
原始日志尾部(纯文本) |
POST /retry |
重试启动(重置尝试预算、清掉上次失败信息) |
POST /rollback |
恢复备份并拉起旧版本 |
POST /recheck |
不重新拉起,只重新探测主端口 |
页面上的三个按钮(重试启动 / 回滚到旧版本 / 重新检测)就是最后三个路由。
主宿主还活着时,也可以用 POST /api/dsh-updater/helper/{retry|rollback|recheck} 把命令转发过去。
其他材料:
DSH_HOME/dsh-updater/last-failure.md— 失败报告,比原始日志更适合整段贴给别人看。DSH_HOME/dsh-updater/logs/— 每次更新一个日志文件,npm 输出与宿主 stdout/stderr 都在里面。GET /api/dsh-updater/logs或面板的日志区 — 尾部 + 疑似报错行(Error/EADDRINUSE/MODULE_NOT_FOUND/npm ERR/Cannot find module/ 真实调用栈帧等)。- 更新后宿主直接起不来(新版本也没能拉起来)时,日志里的
lib/bin.js与依赖检查结果就是第一步; 控制台报告会同时列出安装根、前缀、npm 命令与落地版本。
未验证:宿主自身的启动日志位置没有写进 README,因为插件把它作为「本次更新的日志」 记录在
logs/与恢复控制台里,而 DSH 自己写到哪儿取决于启动方式(终端、launchd 等), 本仓库代码没有读取那个文件。
可移植性验证
本仓库遵循「发布前必须过可移植性门禁」:插件是给别人用的,只在开发机的 link: + 自己的
profile 下能跑不算通过。
npm run build # tsc -p tsconfig.build.json && tsdown
npm run verify # node scripts/portability.mjs --health /api/dsh-updater/probe
npm run verify:quick # --skip-audit --stability 5
npm run verify:full # --stability 30
当前状态:npm run verify 连续两次 ✅ 通过(隔离 DSH_HOME → tarball 装进空 profile →
独立实例就绪 → 健康路由 200 → 客户端 bundle 进 __DSH_BOOT__.entries → 就绪后再守 15 秒稳定,
各 7–8 次探测全部命中同一 pid)。
scripts/portability.mjs 会在隔离的临时 DSH_HOME 里新建空 profile、
用 npm pack 出来的 tarball 安装(不走 link:)、起一个独立实例、打健康路由
/api/dsh-updater/probe、抓 index 确认客户端 bundle 进入了 __DSH_BOOT__.entries,
并在就绪后再守一段稳定性观察窗。必须隔离 DSH_HOME:同机第二个 DSH 实例会等待
~/.dsh/.credentials.yaml 的写锁直到超时(atomic-write: timed out waiting for the writer lock)
而启动失败,不隔离的话验证结果全是假的。
写动作(一键更新)有意不纳入脚本门禁:它会对本机真实的全局部署执行 npm install
并重启真实宿主,自动化门禁不能触发这种副作用。
写动作的正确性由 tests/helper.mjs 在 /tmp 里端到端验证:每个场景都自建一个假安装根
与一个假 npm(按真实 npm 的行为写入包、或失败、或写错内容),再对它们运行真的
helper/update-helper.mjs,检查发生了什么。覆盖 ok(装完校验通过并拉起新版本)、
wrong-version(npm 退出 0 但版本不对)、missing-entry(版本对但入口脚本缺失)、
empty-deps(版本对但依赖域为空)、npm-fails、boot-fails(装好了但起不来 → 回滚 + 拉起旧版本)、
boot-really-fails(都起不来 → 控制台留着等人工)、prune(成功后清理超出 backupCount 的备份)、
restart-only(install: false 时绝不调用 npm)。
npm test # smoke → versions → risk → helper → routes → handoff
全部测试都建立在临时目录与假 npm 之上,不会碰本机真实的 DSH 安装。当前工作树上
npm run build && npm test 的实测结果是 517 项断言全部通过(smoke 97 / versions 84 /
risk 64 / helper 65 / routes 110 / handoff 97)。
兼容性
- 要求:DeepSeek Harness
>=0.1.5-rc.1(即package.json的dsh.engines.dsh)。 - Node:
^22.19.0 || >=24.0.0。 - peer:
@deepseek-ai/dsh-*与react/react-dom见package.json的peerDependencies。 - 平台:代码在 macOS 上开发。安装根枚举覆盖 Homebrew 前缀、
/usr/local、~/.local以及 Node 所在前缀;宿主由 launchd 托管时走observe模式——助手执行托管方的 kick 命令、 跟随托管方写入的 stdout/stderr 日志判断存活,并刻意不做长观察(守住端口是托管方的职责), 绝不自己再 spawn 一个宿主去抢端口。Linux / Windows 未实测。 - 与本作者的
dsh-restart共用分离式助手模式(同一套「宿主先回包、助手接管、控制台兜底」结构)。 本版起在peerDependencies中显式声明兼容 DSH 0.2.0-rc.2(官方 DSH 包的版本范围已含^0.2.0-rc.2),在 0.2.0-rc.2 上不会再出现兼容告警;功能与行为无变化。
License
MIT — 见 LICENSE。
评论
评论存放在 GitHub Discussions。用 GitHub 账号登录后可发表评论或点表情。