🎁 福利专区全网大模型免费应用 + 新用户福利 + 注册活动入口,低成本玩转 AI

版本更新管理 SuCriss/dsh-version-update

DeepSeek 客户端v1.4.4
SuCriss34 天前有提交已提供中文文档扫描:含敏感能力兼容 / 相关生态

设置面板一级菜单「版本更新」,展示当前安装版本与安装目录。

33.6/100
综合分
5
GitHub 星标
294
npm 周下载
—/5
用户评分

数据截至 2026/10/2 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。

相关生态插件(非 dsh 原生,按其对应运行时安装)
git clone https://github.com/SuCriss/dsh-version-update.git

状态速览

判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态
信任档位:待复验
是什么[目录]—
生态插件
可安装,未声明 dsh 能力
暂无时间戳·判据 →
装得上吗[实装验证]—
曾在 dsh 0.2.1-rc.2 下真实安装成功,但当前 dsh 已更新至 0.2.2-rc.1,本站尚未在新版下复验
L4 · 真实安装等级说明 →
数据截至 2026/10/26·判据 →
安全吗[自动扫描]—
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
数据截至 2026/11/05·判据 →
还在维护吗[仓库]—
更新放缓:最近一次提交在 34 天前
暂无时间戳·判据 →
图例:✓ 通过! 需注意— 未判定 / 无数据· 「未判定」= 本站尚无数据,不代表插件有问题
档位由哪些信号合成

本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。

验证判据与等级说明 →

可信度证据

安装兼容性检查✓ 自动检查通过
展开查看检查明细 ↓

以下结论由程序自动检查 npm 包、engines 声明与入口文件得出,未做人工实机验证——能装不等于用着没问题。

✓npm 包dsh-version-update @ 1.4.4
✓Node 引擎要求 ^22.19.0 || >=24.0.0 · 基线 Node 22.19 满足
✓dsh CLI 依赖未声明 dsh 版本约束
✓入口文件main/exports/bin 已声明

验证方式:npm registry 存在性 + package.json 静态校验 · 最后检查 2026/10/28 17:42:33

安全扫描自动 · 静态分析源码
v2.7.0
🔵 扫描:含敏感能力尚无人工评估
展开查看扫描说明与证据 ↓

静态扫描发现插件具备触达本地系统的能力:执行外部命令、读写/删除本地文件、读取剪贴板或密钥类环境变量(至少其一)。这是能力清单,不是危险判定——具体能力项与证据见下方,安装前请自行判断是否需要。

可执行外部命令读写/删除本地文件发起外部网络请求
查看扫描证据(共 87 处,展示前 66 条)
  • 读取本地文件
    lib/index.js:198
    const stat = statSync(historyPath)
  • 读取本地文件
    lib/index.js:300
    ? { downloadBytes: async () => existsSync(npmScratch) ? (await measureTree(npmScratch)).bytes : undefined }
  • 读取本地文件
    lib/index.js:864
    if (!existsSync(policyPath)) {
  • 发起网络请求
    lib/client.js:95
    const response = await fetch(path, init)
  • 读取本地文件
    lib/updater.js:93
    manifest = JSON.parse(readFileSync(manifestPath, 'utf8'))
  • 读取本地文件
    lib/updater.js:100
    if (!existsSync(join(args.installDir, 'lib', 'bin.js'))) {
  • 读取本地文件
    lib/updater.js:238
    .find(candidate => existsSync(candidate))
  • 读取本地文件
    lib/updater.js:286
    const cache = caches.find(dir => existsSync(dir)) ?? caches[0]
  • 写入本地文件
    lib/restarter.js:200
    writeFileSync(payloadPath, JSON.stringify({
  • 读取本地文件
    lib/core.js:268
    return globalCandidates(deps).find(dir => existsSync(join(dir, 'package.json')))
  • 读取本地文件
    lib/core.js:279
    const manifest = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8'))
  • 读取本地文件
    lib/core.js:321
    const manifest = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8'))
  • 读取本地文件
    lib/history.js:63
    if (!existsSync(path)) return []
  • 读取本地文件
    lib/history.js:65
    const parsed = JSON.parse(readFileSync(path, 'utf8'))
  • 写入本地文件
    lib/history.js:86
    mkdirSync(dir, { recursive: true })
  • 写入本地文件
    lib/history.js:89
    writeFileSync(temp, `${JSON.stringify(entries, null, 1)}\n`, 'utf8')
  • 写入本地文件
    lib/history.js:90
    renameSync(temp, path)
  • 删除本地文件
    lib/history.js:95
    rmSync(temp, { force: true })
  • 读取本地文件
    lib/policy.js:45
    if (!existsSync(path)) return structuredClone(DEFAULT_POLICY)
  • 读取本地文件
    lib/policy.js:47
    return repairPolicy(JSON.parse(readFileSync(path, 'utf8')))
  • 写入本地文件
    lib/policy.js:62
    mkdirSync(dir, { recursive: true })
  • 写入本地文件
    lib/policy.js:64
    writeFileSync(temp, `${JSON.stringify(policy, null, 1)}\n`, 'utf8')
  • 写入本地文件
    lib/policy.js:65
    renameSync(temp, path)
  • 读取本地文件
    lib/snapshot.js:69
    const meta = JSON.parse(readFileSync(join(dir, 'meta.json'), 'utf8'))
  • 读取本地文件
    lib/snapshot.js:113
    if (!existsSync(join(dir, 'package.json')) || !existsSync(join(dir, 'meta.json'))) return false
  • 读取本地文件
    lib/snapshot.js:147
    if (!existsSync(snapshotsDir)) return []
  • 读取本地文件
    lib/snapshot.js:152
    names = readdirSync(snapshotsDir)
  • 读取本地文件
    lib/snapshot.js:164
    stats = statSync(dir)
  • 删除本地文件递归
    lib/snapshot.js:211
    const task = rmAsync(dir, { recursive: true, force: true })
  • 读取本地文件
    lib/snapshot.js:233
    if (!existsSync(dir)) return false
  • 写入本地文件
    lib/snapshot.js:236
    renameSync(dir, trash)
  • 删除本地文件递归
    lib/snapshot.js:241
    rmSync(dir, { recursive: true, force: true })
  • 删除本地文件递归
    lib/snapshot.js:269
    await rmAsync(join(snapshotsDir, name), { recursive: true, force: true }).catch(() => {})
  • 写入本地文件
    lib/snapshot.js:355
    mkdirSync(snapshotsDir, { recursive: true })
  • 删除本地文件递归
    lib/snapshot.js:356
    rmSync(dest, { recursive: true, force: true })
  • 写入本地文件
    lib/snapshot.js:360
    writeFileSync(join(temp, 'meta.json'), `${JSON.stringify({ version, at: (deps.now ?? Date.now)(), format: 2, files, bytes: inventoryBytes(files) })}\n
  • 写入本地文件
    lib/snapshot.js:361
    renameSync(temp, dest)
  • 写入本地文件
    lib/snapshot.js:451
    renameSync(installDir, stale)
  • 删除本地文件递归
    lib/snapshot.js:475
    rmSync(installDir, { recursive: true, force: true })
  • 写入本地文件
    lib/snapshot.js:476
    renameSync(stale, installDir)
  • 删除本地文件递归
    lib/snapshot.js:488
    if (moved) rmSync(stale, { recursive: true, force: true })
  • 读取本地文件
    lib/updatelock.js:105
    return readFileSync(path, 'utf8')
  • 写入本地文件
    lib/updatelock.js:132
    writeFileSync(temp, JSON.stringify(record), 'utf8')
  • 删除本地文件
    lib/updatelock.js:145
    try { rmSync(temp, { force: true }) } catch { }
  • 写入本地文件
    lib/updatelock.js:194
    renameSync(lockPath, tomb)
  • 删除本地文件
    lib/updatelock.js:206
    try { rmSync(tomb, { force: true }) } catch { }
  • 删除本地文件
    lib/updatelock.js:215
    try { rmSync(tomb, { force: true }) } catch { }
  • 读取本地文件
    lib/updatelock.js:236
    raw = readFileSync(lockPath, 'utf8')
  • 删除本地文件
    lib/updatelock.js:244
    rmSync(lockPath, { force: true })
  • 读取本地文件
    lib/tree-health.js:83
    names = readdirSync(dir)
  • 读取本地文件
    lib/tree-health.js:98
    parentMtimeMs = statSync(dir).mtimeMs
  • 读取本地文件
    lib/tree-health.js:108
    stats = statSync(path)
  • 读取本地文件
    lib/tree-health.js:148
    entries = readdirSync(bundled)
  • 读取本地文件
    lib/tree-health.js:180
    launcherOk: existsSync(join(installDir, 'lib', 'bin.js')),
  • 删除本地文件递归
    lib/tree-health.js:261
    rmSync(entry.path, { recursive: true, force: true })
  • 读取本地文件
    lib/restart-log.js:14
    const info = lstatSync(path)
  • 读取本地文件
    lib/snapshot-inventory.js:13
    for (const name of readdirSync(join(dir, prefix))) {
  • 读取本地文件
    lib/snapshot-inventory.js:17
    const stat = lstatSync(full)
  • 读取本地文件
    lib/snapshot-inventory.js:33
    for (const name of await readdir(join(dir, prefix))) {
  • 读取本地文件
    lib/snapshot-inventory.js:37
    const stat = await lstat(full)
  • 读取本地文件
    lib/snapshot-inventory.js:38
    if (stat.isSymbolicLink()) files.push({ path, size: 0, link: await readlink(full) })
  • 写入本地文件
    lib/relaunch.js:72
    appendFileSync(logPath, `[${new Date().toISOString()}] relaunch: ${line}\n`)
  • 读取本地文件
    lib/relaunch.js:150
    payload = JSON.parse(readFileSync(payloadPath, 'utf8'))
  • 删除本地文件
    lib/relaunch.js:153
    rmSync(payloadPath, { force: true })
  • 删除本地文件
    lib/relaunch.js:158
    rmSync(payloadPath, { force: true })
  • 执行外部命令来自配置/变量
    lib/relaunch.js:229
    child = spawn(payload.execPath, payload.args, replacementSpawnOptions({
扫描于 2026/11/5(0 天前) · 范围 入口闭包 + 全包扫描 · 来源 npm 发布包

静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。

社区实装还没有人打卡 · 装过的第一个就是你
依赖的 DSH / Cordis 模块
@deepseek-ai/schemastery
给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个

功能亮点

  • 展示当前安装版本
  • 显示安装目录
  • 检查发布版并一键安装

README

由 DeepSeek 最新模型翻译生成
dsh-version-update DeepSeek Harness Web GUI 的「版本更新」设置菜单 —— v1.0 全面重写版。除了查看与安装 @deepseek-ai/dsh 的任意已发布版本,这一代插件把「更新」升级为一套可自动化的版本管理策略:静默自动更新、执行时间窗、每日定时检查、dist-tag 与版本线跟踪,以及基于本地快照的离线回滚——不依赖 npm 与网络,回滚本身也能再回滚。 功能 版本管理(保留并增强) - 设置面板一级菜单「版本更新」,展示当前安装版本与安装目录。 - 列出 npm dist-tag 通道(latest / next)与全部已发布版本,任选其一一键安装或降级(降级按钮与确认卡明确标注方向)。 - 确认卡片读取目标版本的 GitHub Release 说明(dsh-v 标签优先),附完整说明链接;失败或缺失时静默省略,绝不阻塞安装。 - 一键安装后台运行 npm install -g @deepseek-ai/dsh@<精确版本>。只接受精确版本号,npm 以无 shell 方式 spawn,registry 文本无法进入命令行。 - 进度条,而不是一直滚的日志:面板展示阶段(准备 / 快照 / 下载 / 解压 / 完成)、百分比或已见字节数、已用时间与停滞时长。
展开完整文档(README 剩余 1 节)
dsh-version-update

DeepSeek Harness Web GUI 的「版本更新」设置菜单 —— v1.0 全面重写版。除了查看与安装 @deepseek-ai/dsh 的任意已发布版本,这一代插件把「更新」升级为一套可自动化的版本管理策略:静默自动更新、执行时间窗、每日定时检查、dist-tag 与版本线跟踪,以及基于本地快照的离线回滚——不依赖 npm 与网络,回滚本身也能再回滚。

功能

版本管理(保留并增强)

- 设置面板一级菜单「版本更新」,展示当前安装版本与安装目录。
- 列出 npm dist-tag 通道(latest / next)与全部已发布版本,任选其一一键安装或降级(降级按钮与确认卡明确标注方向)。
- 确认卡片读取目标版本的 GitHub Release 说明(dsh-v 标签优先),附完整说明链接;失败或缺失时静默省略,绝不阻塞安装。
- 一键安装后台运行 npm install -g @deepseek-ai/dsh@<精确版本>。只接受精确版本号,npm 以无 shell 方式 spawn,registry 文本无法进入命令行。
- 进度条,而不是一直滚的日志:面板展示阶段(准备 / 快照 / 下载 / 解压 / 完成)、百分比或已见字节数、已用时间与停滞时长。没有分母的阶段如实标注「无法测量」并只报已见字节数,绝不编造百分比;没有任何信号时也不谎报「卡住」。npm 的原始输出收进「查看详细日志」,默认折叠。
- 安装源可选,慢源可换:确认卡上选「自动 / 官方 / 淘宝镜像」。auto 用本次实际读到版本的那个 registry(读取失败时落到的镜像会被记住并沿用)。长时间无进展时安装卡给出提示——但不会中途杀掉 npm(reify 半途被杀会留下半提交的安装树),等它结束后可用「用淘宝镜像重试」对同一版本重跑,只需重下。
- 安装失败不破坏你原来的版本:安装前强制创建快照(requireSnapshot 默认开启,快照写不成就不让 npm 动树),失败后从快照修复,并额外校验启动入口 lib/bin.js 是否还在。回滚不依赖 npm、不需要网络。
- 安装成功后不会自动重启,也不会弹任何窗:面板上出现「立即重启」按钮,点了才重启。宿主三步交接(payload 文件 → 脱离进程的 relaunch 助手等端口释放 → 原样 argv 拉起新进程)→ 页面 watchdog 等新进程应答后自动 reload。

快照回滚(全新)

- 每次安装开始前自动为当前版本创建本地快照(存于 ~/.dsh-version-update/snapshots/<版本>/),失败只记入日志、绝不阻塞安装。
- 回滚 = 把快照复制回安装目录:不依赖 npm、不需要网络。恢复采用「先改名旧目录再拷贝」的顺序,拷贝失败会自动还原原目录。代价是要整棵复制:26 513 文件 / 551 MB 的安装实测约 88 秒,所以恢复期间面板显示 正在恢复 <版本>,并把这一条请求的超时从 15 秒放宽到 10 分钟——否则一次正在进行的成功会被当成失败报出来。
- 被替换掉的那个版本会被收编成快照(同卷改名,不复制),因此「恢复」这个动作本身也可以再恢复回去;目标版本已有完好快照时跳过,不重复占地方。
- 面板「快照与回滚」卡片列出全部可用快照,一键恢复;恢复同样走确认卡 + 重启流程。
- 快照按配置数量自动修剪(默认保留 5 个),损坏的快照优先清除且在列表中标注不可用。
- 可选的 recoverOnFailedRestart:重启后新进程 60 秒内始终不可达时,relaunch 助手自动从快照恢复上一版本并重新拉起——同样无需网络。

更新策略引擎(全新)

策略持久化于 ~/.dsh-version-update/policy.json,在面板上修改即时生效:

| 字段 | 取值 | 说明 |
| mode | off / notify / auto | 发现新版后:仅显示 / 显著提醒 / 静默自动安装 |
| track | {kind:'tag', tag} / {kind:'line', range} / {kind:'pin'} | 跟随 dist-tag(含自定义标签)/ 跟随 ^x.y.z、~x.y.z 版本线(仅稳定版)/ 固定当前 |
| window | null 或 {start,end} (HH:MM) | auto 模式的执行时间窗;支持跨午夜(22:00–06:00),起止相同表示全天;窗外发现的版本会被「泊车」,窗开启时自动续装 |
| checkAt | null 或 HH:MM | 每日定时检查时刻;null 表示不设每日定时 |

调度器是三个朴素定时器加一组纯函数决策(resolveTarget / inWindow),全部核心逻辑可独立测试。自动检查有三个触发时机:host 启动后 60 秒(仅 auto 模式,每次启动一次)、配置的每日 checkAt 时刻、以及打开设置面板时。启动检查是 auto 模式可靠性的关键——checkAt 只是一个时刻,机器在那个时刻没开机就会整天错过,而启动后那一枪保证每次开机都会检查一次。发现新版本时面板状态行与徽标同步更新;auto 模式在窗外只会「泊车」等待,绝不越窗安装。

- 待安装管理:面板显示等待中的自动安装目标版本与开始等待的时间,可取消当前等待(同时解除时间窗唤醒 / 忙碌重试)。不终止已开始的安装,也不修改策略或每日检查;后续自动检查仍可发现并安排更新。

移除的能力

- v0.x 的 agent 系统提示通告机制(announceToAgent、能力段落注入、待处理通告)已整体移除——本插件现在是纯粹面向用户的面板设施,不再向模型注入任何内容。

为什么必须重启,而不是只刷新

npm install -g(以及快照恢复)覆盖的正是运行中的 dsh web 提供前端资源的那个包目录:

- 已打开页面持有的 /assets/index-<hash>.js 在新目录树中不存在,SPA 兜底会回 HTML 导致模块解析失败;
- bundle watcher 触发热替换链,主题令牌与 React renderer 都可能被拆掉。
因此 host 记录进程启动时的 running 与磁盘上的 installed,二者不一致即 stale;needsRestart 更宽——本进程里刚完成的任务本身就是「代码已被取代」的证明。重启 overlay 用裸 DOM + 字面量颜色构建(跟随 prefers-color-scheme 媒体查询),保证在热替换拆掉一切之后依然可见可读。

组成

同一个包里的三个半区:

- Host 半区(lib/,exports .)注册 loopback-only 路由族:
- GET /check — 只读:本机事实 + registry 通道/全量版本 + 任务视图 + ambient(上次检查结论、下次计划时间、泊车目标、近期活动);registry 失败时降级为 publishedError,本机信息照常返回。它只刷新面板看到的东西,永远不会安装、也不会泊车——那两条显式路径见下
- POST /check/run、POST /check/auto — 显式检查,区别只有一个:/check/run 只观察(面板「检查」按钮、取消泊车后的刷新),/check/auto 会按当前策略行动、可能静默安装(面板打开时调一次)。后者必须是 POST:一个能替换整棵安装树的 GET,会被预取、第二个标签页、或任何扫本机端口的进程顺手触发。三者都会刷新 lastCheck
- POST /update — {version, source?} 启动一次安装(trigger 固定记为 manual;source 只接受 auto/official/mirror 三个标识符,不接受 URL——registry 地址留在 host 配置里,非法值 400)
- GET /status — 任务视图(running/stale/needsRestart/restartable)+ ambient
- POST /restart — 三步交接重启(面板「立即重启」按钮调用)
- GET /notes?version= — GitHub 发布说明(releaseNotes 开启且能解析出仓库时挂载)
- POST /pending/cancel — 取消当前待自动安装及唤醒 / 重试计时器,保留每日检查;返回 {result:{cancelled:true}}。仅本机 POST(其他方法 405),未注入取消操作的独立路由组合不挂载该端点。
- GET|POST /policy — 读取 / 打补丁式修改策略(校验失败的每个字段都会被点名,400 返回)
- GET /snapshots、POST /restore、POST /snapshots/delete — 快照列表 / 恢复 / 删除单个快照(删除同样走机器锁但不碰安装树,成功时把剩余列表一并带回;面板里同一行点两次才删);恢复与安装争用同一把机器锁,本机有安装在跑或其他宿主持锁都返回 409
- 浏览器半区(lib/client.js,exports ./client):字典、设置页(状态卡 / 策略表单 / 版本列表 / 安装进度条与源选择 / 可折叠日志 / 快照中心 / 活动历史 / 安装树健康)、导航图标标记、重启 watchdog。安装卡只在有任务时出现,日志只在你点开时渲染。
- 脱离父进程的重启助手(lib/relaunch.js):等旧 pid 消失、端口释放后原样拉起新进程;armed recovery 时驻留观察新进程可达性,必要时快照恢复再拉起。

安装

dsh plugin --profile web add dsh-version-update

或从源码:

dsh plugin --profile web add github:SuCriss/dsh-version-update

重启 dsh web 后菜单出现(host 半区需要重启才会挂载路由);未挂载前面板会给出明确的「宿主路由尚未挂载」提示。

配置(cordis entry config)

- registry(默认 https://registry.npmjs.org)— 读取与安装共用的 registry 基地址,必须是绝对 http(s) URL。若该地址在网络层失败,读取会落到内置镜像并记住它:随后的安装按实际读到版本的那个 registry 执行,不会回去问刚刚超时的地址。
- mirrorRegistry(默认 https://registry.npmmirror.com,淘宝镜像)— 备用源地址。面板上选「淘宝镜像」或点「用淘宝镜像重试」时使用它;auto 不受影响。
- requireSnapshot(默认 true)— 要求安装前必须成功写下快照。开启时快照失败会直接终止安装(FatalPreparationError),npm 根本不会启动,因为「没有回滚点的安装」正是会破坏原版本的那一类。关掉它换回旧行为:快照失败只记日志、照常安装。
- allowRestart(默认 true)— 关闭则不提供重启路由。
- releaseNotes(默认 true)— 是否读取并展示 GitHub 发布说明。
- snapshotKeep(默认 5,1–10)— 快照保留数量。
- snapshotMaxBytes(默认 0 = 不限容量)— 快照载荷总字节配额(不含元数据和符号链接目标)。创建成功后先按数量清理损坏/过旧快照,再按创建时间从旧到新删除可用快照以满足容量限制;始终保留刚创建的快照,即使单个快照已超出配额。旧格式快照缺少体积记录时会现场测量。
- recoverOnFailedRestart(默认 false)— 重启失败时由助手做快照恢复式救援。
- dataDir(默认空 = ~/.dsh-version-update)— 状态目录(policy/history/snapshots) relocatable,便于便携部署。

运行时行为(模式、跟踪、窗口、计划)一律走面板 → /policy,不进 entry config。

新增管理与诊断能力

- 活动时间线:内存保留最近 200 条安装结果、快照恢复与树修复事件;检查、操作结束或手动刷新时读取。GET /api/dsh-version-update/operations?since=N 仅限本机、仅支持 GET,按单调序号增量返回 {result:{events,cursor}};宿主重新加载后清空。安装只记结束结果,不虚构开始事件。
- 更新预检:安装确认卡通过本机限定的 GET /api/dsh-version-update/preflight 检查 npm、安装目录父级可写性、可用磁盘空间与快照目录(遵循 dataDir);逐项失败返回警告,未知磁盘空间为 null,仅供参考、不阻止安装,也不运行 npm 或联网。

- 快照列表展示新快照的载荷体积,可确认删除;安装进行中会拒绝删除(刚创建的那个快照正是失败时的回退点)。删除本身只做改名,面板不会等 unlink。
- 新快照记录文件路径、大小及符号链接目标,校验分两级:列表与修剪只看元数据(meta.json 自洽,且目录里声明的版本与目录名一致),只有「恢复」和「复用一份已有快照」这两处不可挽回的路径才做全量清单比对,缺失/大小变化即拒绝。这不是内容哈希校验;旧快照标记为仅元数据校验,体积可能未知。
- 之所以不统一用全量校验:26 513 文件的清单遍历实测 1.6 秒,而列表在面板每次检查时都要读——同步跑会把整个宿主事件循环按住。快照只会在拷贝完成之后才改名进版本名空间,所以撕裂的拷贝住在 .tmp- 里、根本不在列表中,元数据级校验对列表足够。
- 面板「检查」使用 POST /check/run,按当前跟踪策略更新检查记录;即使开启自动更新,手动检查也不会直接安装或新增等待任务。能让策略真正动手的只有 POST /check/auto(面板打开时调一次,以及调度器自己的定时器)——它原本骑在 GET /check 上,等于让一个读接口具备安装副作用,方向是反的。
- 重启诊断卡按需读取状态目录的 restart.log,最多返回末尾 16 KiB / 100 行,过滤常见凭据格式。日志可能包含本机路径,分享前仍须检查。
- npm CLI 缺失和 EACCES/EPERM 权限失败会给出终端、全局前缀、缓存权限或文件锁排查建议;不会自动提权。
- 新接口:POST /snapshots/delete({version})、POST /check/run、POST /check/auto、GET /restart/diagnostics,均位于 /api/dsh-version-update 且沿用本机访问限制。
- 删除快照改为先改名成 .trash- 墓碑、再后台异步 unlink:一个 200 MB 级快照的递归删除在 Windows 上要好几秒到几十秒,同步做会冻住整个宿主、并超过面板 15 s 的请求超时,看上去就像按钮没反应。现在版本名立刻从列表消失,字节在后台回收;进程被杀留下的 .tmp- / .trash-* 目录会在宿主启动时清扫。

开发

npm test # node:test,262 个用例覆盖协议/域逻辑/路由/组装/浏览器控制器/重启助手
npm run typecheck # tsc --checkJs strict,无构建产物的类型安全

测试刻意覆盖了几类容易腐化的契约:浏览器端 semver 镜像与 host 排序的一致性、策略归一化的逐字段回退、快照元数据校验与剪枝顺序、单槽位跨 fiber 重载的排他性、mock 时钟下的倒计时/watchdog 链路、机器锁记录的所有权校验(release 只能删掉自己那条)、泊车中的自动更新必然存在下一次唤醒、重启助手两段等待各自的预算与可拨测的探针地址,以及降级不得伪装成结论(registry 读不到时不许说「已是最新」)。

本轮新增的契约同样按「哪一步会悄悄退化」来挑:进度模型不许编数(无分母的阶段必须 indeterminate,无信号的阶段不许报 stalled)、source 只收标识符且非法值 400(URL 不许从浏览器进来)、快照写不成时 npm 一次都不能被 spawn、以及 manifest 可读但启动入口缺失时也必须从快照修复。

再一轮修复补的契约同理:列表不许把字节校验搬回热路径(改坏一份快照的字节后,列表仍报可用、恢复必须拒绝且不碰活树)、恢复必须留下退路(被替换的版本收编成快照,于是回滚本身可以再回滚;已有完好快照时不重复占位;收编的修剪不许淘汰恢复目标)、只有字节校验能发现的坏快照不许阻塞修复(按新旧顺序逐个尝试,且修复成功后不把失败候选算进健康判定),以及被拒的操作不许静默(status: 'ready' 下的 error 也要渲染到面板上)。

这一轮补的是读写方向:GET 不许有安装副作用(GET /check 不碰自动决策,也不新增泊车)、唯一能让策略动手的读必须是 POST(POST /check/auto)、打开面板的那一次才允许行动(挂载用 check({act:true}),且渲染时打的确实是 /check/auto)、以及回滚后的刷新只许观察(否则 mode: 'auto' 下恢复会在几秒内把自己装回去)。这几条都用「把修复回退掉、断言必须失败」实测过,不是空转断言。

最近一轮补的是年龄证据的来源:退休目录的新鲜度必须取自它所在的目录,而不是它自己。npm 的 retire 是一次改名(arborist lib/retire-path.js),而改名只会更新父目录的 mtime,被改名目录自身的 mtime 原样不动——于是一个一秒前刚被退休的目录,报告的是它上次被写入的时间,对几天前安装的包就是几天前,10 分钟闸门因此形同虚设,引导修复会把仍在运行的 reify 的退休副本删掉、或在它的树还在写的时候恢复快照。测试用真实的 renameSync 复现这一步(夹具不再用 utimesSync 给子目录造年龄),并且这条修复连带暴露了第二个问题:延后清扫的定时器没有 unref(),而它等待的正是整个闸门(十分钟),会让已经退出的宿主进程继续挂着——现在按这个代码库其他所有定时器的约定 unref。

再一轮补的是谁有权写这棵树:三条会写安装树的修复路径(失败后修复、引导修复、延后清扫)现在共用同一把机器级锁,抽成一个原语 repairWhenFree。原先只有失败后那条拿锁,引导修复和延后清扫只看 updater.busy()——而那只覆盖本进程,挡不住「桌面端正在更新、用户在终端又起了一个 dsh web」这种日常场景;年龄闸门也不能替代锁,因为它只约束删除,约束不了恢复分支。被占用时是延后而非跳过:引导路径按 6 秒重试(有界),延后清扫在让出后重试,且仍然保持一次性、不自旋——删不掉的旧目录(句柄被占)只上报不重试,否则会变成每秒一轮的循环。这组契约同样做了「去掉锁、断言必须失败」的验证:去掉后引导修复确实把快照覆盖到了另一个宿主正在写的树上。
最近一轮补的是运行身份:「我这轮还是当前这轮吗」必须比身份,不能比 task.state。settle 和 start 都会整体替换 task 对象,于是被硬超时抛弃的那条流水线(它的槽位在快照还在拷贝时就被释放了)恢复执行时,从共享绑定里读到的是下一轮的 running,接着为自己的陈旧目标 spawn npm,并随后把新一轮判成成功——面板与 history.json 双双记下一个从未发生的安装,而用户真正点的那个版本一次都没被 spawn。现在每轮持有单调令牌(activeRun),settle 按令牌归属、陈旧那轮的结算完全惰性,task.state 只剩显示用途。验证照旧「把修复回退掉、断言必须失败」:回退后那条被抛弃的流水线确实为陈旧目标 spawn 了 npm。

再一轮补的是锁文件自身的两个两步操作:发布必须原子,抢占必须证明「删掉的正是被判陈旧的那一条」。原先 open(path,'wx') 之后再写记录,路径上存在一个空文件窗口——而空锁被每个读者当作「损坏即陈旧」抢走,两个同时启动的宿主可能都以为自己拿到了锁;现在记录先写进私有临时文件、再 link 进锁路径(link 遇占用报 EEXIST,且路径一旦可见内容就是完整的),不支持硬链接的文件系统退化为原来的排他创建。抢占则先 rename 成私有墓碑(只有一个进程能赢),再核对实际拿到的是不是那条被判陈旧的记录:是则丢弃,不是(说明文件在判定与删除之间已被别人换成新锁)就用 link 放回去并如实报出持有者——原来的裸 rmSync 会把新持有者的锁删掉,于是两个宿主同时安装。这条有精确复现:用 isAlive 接缝在判定与删除之间换入一条活动记录,回退修复后那条活动记录会被删除并被抢占者顶替。

最近一轮补的是审计记录不许把「事件种类」和「事件做了什么」塞进同一个字段:restored: true 原先对每一次树修复都置位,于是一次只删残留目录、根本没碰版本的修复,在面板上读作「快照恢复」——一条从未发生过的版本迁移。现在拆成三个字段:repair 标记事件种类,restored 只在真的把快照拷回树上时才置位,removed 记录清理掉的残留数量;面板相应渲染成「树修复 · 快照恢复」或「树修复 · 清理 N 个残留目录」。验证照旧:回退后那次纯清理不再带计数,用例失败。
预览分享卡片长什么样 ↓SuCriss/dsh-version-update
分享 · DeepSeek Harness Hub
SuCriss/dsh-version-update

设置面板一级菜单「版本更新」,展示当前安装版本与安装目录。

这个插件是你维护的吗?
作者本人可认领并核对本条目;认领通过后,详情页会显示「作者已认领」标记。
认领条目 →
回到顶部