← 返回列表
需源码安装
用于升级 DeepSeek Harness 核心以及某个配置文件的插件的工具。
暂不能直接安装(需源码编译或环境不满足):仓库缺少 package.json,无法用 dsh 插件安装命令安装。 · 最近上游提交 2026/9/19 · 已提供中文文档
保持你的 DSH 配置文件为最新状态:针对任意核心版本进行六项兼容性检查,在安装插件之前先检查它,对整个配置文件进行快照和恢复,只更新有更新版本的插件,重新检查延迟列表,并探测真正加载和响应的内容——导入、apply()、路由处理器、被遮蔽的 UI、失效的端点。菜单、CLI、JSON。
综合分
29.7
GitHub 分
29.7
用户评分
—
★ Stars
0
周下载量
—
安装插件(需先安装 dsh CLI 引擎:npm install -g @deepseek-ai/dsh)
dsh plugin --profile web add ThinkForge-core/dsh-upgrade-tools仓库缺少 package.json,无法用 dsh 插件安装命令安装,改用 GitHub 源安装
信任档位:已验证本站已于 1 天前真实安装成功
- 是什么
- dsh 原生插件 · market
- 装得上吗
- 本站已真实安装成功(非静态推断)
- 安全吗
- 本站尚未对该插件做风险分级(暂未覆盖,不等同于无风险)
- 还在维护吗
- 活跃:最近一次提交在 7 天前
档位由下列信号合成:本站实装验证(真实安装,当前最高到 L4)· 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。下方各区块是它的证据明细。 验证判据与等级说明 →
🟢实装验证通过· 2026/9/25
由本站实装验证器在真实 dsh 环境安装成功,非静态推断。
数据截至 2026/9/22(元数据每日更新 · 实装验证按队列轮转,单条结论的验证时间见上方)
安装兼容性检查需源码安装
以下结论由程序自动检查 npm 包、engines 声明与入口文件得出,未做人工实机验证——能装不等于用着没问题。
✗npm 包dsh-upgrade-tools(未发布到 npm,仅可源码安装)
✓Node 引擎未声明 engines.node
✓dsh CLI 依赖未声明 dsh 版本约束
✗入口文件缺少入口声明
仓库缺少 package.json,无法用 dsh 插件安装命令安装
验证方式:npm registry 存在性 + package.json 静态校验 · 最后验证 2026/9/22 00:11:23
用户评分
还没有人投票,来当第一个
订阅周报,不错过优质插件更新
每周一封 · 高评分插件 + 新用户活动
README
由 DeepSeek 最新模型翻译生成dsh-upgrade-tools
用于升级 DeepSeek Harness 核心以及某个配置文件的插件的工具。
仓库 · MIT ·
针对你已安装的 DSH 核心运行——不固定任何核心版本
(参见兼容性)。
以纯 Python 3 运行(仅使用标准库,无需 npm)。该工具本身启动时从不需要 Node;
它使用 PATH 上的 node 做两件事——运行时探测(verify)以及解析制品附带的代码——
在 node 缺失的情况下,解析会被报告为未检查,而不是被静默跳过。只读操作
(status、core-versions、plan、check、inspect)也可针对正在运行的 harness 使用——
它们只读取配置文件、已安装的核心和注册表。只有干净升级
(detach → 核心/插件安装 → attach)需要 DSH 关闭,因为配置文件
目录正在被重写。
要求:Python 3 以及 PATH 上的 dsh CLI,用于读取或更改
配置文件的操作(或 DSH_INSTALL_DIR 指向已安装的核心)。
该脚本会自行拉取检查所需的一切:通过 HTTP 访问 npm 注册表、
市场索引,以及——用于破坏性变更分析——核心版本的检出
(对 dsh-v 标签执行 git clone --depth 1:只有一个提交,因为比较只读取文件
而从不遍历历史)。默认情况下,该检出会放在系统临时目录
(/tmp)下:下次运行会复用它,而不是再次下载同一个标签,操作系统
会在重启时清除它。如果设置中(或通过 --checkouts)指定了目录,则会改用该目录。
主要思路是“全部卸载,升级核心,再全部装回来,并把
不兼容的插件放到一个列表中”:
check ──► snapshot ──► detach all plugins ──► core upgrade ──► install compatible ones
└─► the rest into the list
兼容性
不固定任何核心版本。 每个命令都会读取实际安装的核心——通过
PATH 上的 dsh CLI 找到,或由 DSH_INSTALL_DIR 指定——并从该核心
自身的字节中获取事实:包清单、声明其 wire 端点的生成 TYPERT 接口、其
生效的加载器树。没有任何内容假定某个发布版本,因此已安装的核心始终是基线,
而从旧版本升级是常规情况,而不是特殊模式。
已针对 0.1.x 系列的三个发布版本进行端到端验证:
| 已安装核心 | 宿主包 | 客户端行 | TYPERT 接口 | Wire 端点 | status | check |
|---|---|---|---|---|---|---|
| 0.1.0-rc.8 | 193 | 43 | 7 | 26 | exit 0 | exit 0 |
| 0.1.1-rc.2 | 194 | 43 | 7 | 26 | exit 0 | exit 0 |
| 0.1.5-rc.2 | 237 | 55 | 15 | 84 | exit 0 | exit 0 |
这些数字因发布版本而异,因为它们是从该发布版本中读取的,而不是假定的。一个核心,其
工具无法解读的表面会如实报告——空清单、表示未执行的线路检查——而不是猜测。
已安装的核心从测试框架可能产生的两种布局之一读取:一种是全局安装,其包嵌套在 /node_modules/@deepseek-ai 下;另一种是提升树,包位于核心包旁边。只读取其中一种会静默丢失部分清单——以及依赖它的判定结果。
有一个依赖版本的输入值得了解:当没有目标核心的检出可用时,内联纯净性规则(检查 6)会回退到从 0.1.5-rc.2 复制而来的内置分类。该集合刻意设置得较为宽泛,因此缺失条目会放宽检查,而不是凭空捏造不兼容性;用 --core 指定目标并让工具获取其检出,会用目标自身的规则替换内置集合。
快速开始
不带参数时会打开交互式菜单——每个操作都可以通过点击其编号访问,破坏性操作始终先以预览模式显示:
git clone https://github.com/ThinkForge-core/dsh-upgrade-tools.git
cd dsh-upgrade-tools
python3 dsh_upgrade.py # menu: status, plan, check, detach, install, pipeline…
python3 scripts/menu.py # the same thing
同一工具也可作为普通 CLI 使用(脚本、cron):
1. Current state: core version, tags, installed plugins
python3 dsh_upgrade.py status
2. Which core versions exist at all and what happens to plugins on each
python3 dsh_upgrade.py plan
3. Full check of the chosen version (with a checkout and code scans)
python3 dsh_upgrade.py check --core 0.1.5-rc.2 --verbose
4. Full pipeline: snapshot → detach → (core upgrade) → install
python3 dsh_upgrade.py pipeline --core 0.1.5-rc.2 --yes
5. Or plugins only, leaving the core alone
(updates the plugins that have a newer version; the rest are not touched)
python3 dsh_upgrade.py plugins --yes
5a. Including the plugins whose newer version is not confirmed for this core
python3 dsh_upgrade.py plugins --install-unknown --yes
6. Later: recheck the deferred ones and install the ones that became compatible
python3 dsh_upgrade.py recheck --install --yes
7. After an upgrade: do the installed copies actually load — and run?
python3 dsh_upgrade.py verify
菜单(不带参数运行)
dsh-upgrade — DSH core and profile plugin upgrade
core 0.1.1-rc.2 · profile web · plugins 16
target: auto — 0.1.5-rc.2 · mode: online · core directory: …/node_modules/@deepseek-ai/dsh
read-only — the profile is not touched
1 Status: core, tags, profile plugins — fast: cache and profile files only, nothing is executed
2 Core versions: what the registry offers
3 Plan: new core versions and what happens to plugins — quick, declarations only
4 Check: full compatibility matrix — checkout of the target + code scans
10 Incompatible list: show contents
11 设置:profile、target、模式
12 CLI 标志参考
13 检查:一个尚未安装的插件——一个目录或一个 .tgz;未安装任何内容
14 验证:已安装的插件能否加载——以及它们是否真的做任何事?——导入每一个,调用 apply(),调用它注册的路由处理器,报告被遮蔽的表面
0 退出
可能更改 profile
5 ! 快照并分离所有插件
6 ! 从快照安装插件
7 重新检查不兼容列表
8 ! 更新有更新版本的插件(核心不受影响)——profile 中其他任何内容都不会被触碰
9 ! 完整流水线:分离 → 核心 → 安装
提示:4——升级前检查,9——整个流水线。
* 标题始终显示上下文:核心版本、profile、插件数量、目标版本、模式。
* 各项按风险分组:先是“只读——不会触碰 profile”,然后是
所有可能更改 profile 的内容。! 标记和红色按键标示破坏性操作。
第 4 项(检查)是只读的:它读取 profile 并写入状态文件(不兼容列表、
报告、缓存),但不会安装或分离任何内容。
* 第 11 项会更改每个操作的参数:profile、目标核心(或“auto”)、
offline、“no clone”、详细输出、颜色、状态目录以及版本
检出所在位置。每次更改都会写入设置文件,并应用于后续运行
(见下文)。
* 路径提示是终端级别的。 第 13 项(检查)、第 6 项的快照文件以及第 7 项的
列表文件会像 shell 一样读取路径:Tab 补全文件和目录
(目录会带尾部斜杠,空格会被转义,file:/link: 说明符会在前缀之后补全),
方向键/Home/End 编辑该行,上箭头调出更早的路径。你输入内容中的引号、
反斜杠转义和 ~ 会在使用路径之前解析。在终端之外使用普通读取器,
因此脚本不会发生任何变化。
* 自动目标写作 auto — ,并且是最新发布的版本
(包括预发布版)——不是已安装的版本。已安装的核心只会在该行中作为
标记的回退项出现(auto — 0.1.1-rc.2 (installed fallback)),当完全无法访问
注册表时。标题会从注册表缓存中解析它,因此它永远不会阻塞。
* 第 5、6、8、9 项会先以 --dry-run 执行,并要求用单词
yes 确认;流水线中的核心升级(npm i -g)默认只打印。
* 第 5 项和第 6 项会先询问是否应将操作缩小到少数几个插件,
然后将这些名称作为 --only 传入,因此可以单独分离或恢复一个插件。
预览随后会准确显示该选择。第 8 项则改为询问两个问题:是否还要
安装一个未确认适用于此核心的更新版本(--install-unknown),以及是否
在安装新版本之前先移除每个插件(--detach-first,默认关闭——新版本会覆盖当前副本安装)。它不需要 --only 询问,因为它只会处理有新版本的插件;当你想只处理单个插件时,可在命令行上用 --only 缩小范围。
* 菜单不会重复逻辑:它组装相同的参数,并调用与 CLI 相同的 cmd_ 函数。
条目 14(verify)执行声明无法回答的问题:它导入每个已安装的插件副本,调用其 apply(),并报告每个插件注册了什么——以及任何 UI 宿主行被关闭的插件(见它们真的能工作吗?)。它还会询问是否针对正在运行的 DSH 运行 --live 探测。条目 1(status)根据已知信息显示两列以及被遮蔽表面部分——缓存的探测结论和配置文件——并且自身从不启动探测,因此概览保持快速;运行条目 14 以刷新结论。这也是将未确认插件变为已验证插件的步骤。
* 在终端之外(管道、cron、脚本)不带参数运行时,它不会挂起——它会打印操作映射(dsh-upgrade — available actions:)并以代码 0 退出。
设置会被记住
选项保存在一个小型 JSON 文件中,因此一次做出的选择不必在每次运行时作为标志重新输入:
| | |
|---|---|
| 位置 | $DSH_UPGRADE_CONFIG,否则 $XDG_CONFIG_HOME/dsh-upgrade/config.json,否则 ~/.config/dsh-upgrade/config.json |
| 写入者 | 设置屏幕(菜单项 11)——没有其他写入者:检查、计划或升级从不写入它 |
| 优先级 | 显式标志(仅该次运行)> 环境 > 保存的文件 > 内置默认值 |
在命令行上给出的标志绝不会被写回,因此 dsh_upgrade.py --profile other 仍然是一次性设置。条目 11 → 10(“忘记已保存的设置”)会删除该文件并恢复默认值;条目 11 → 9 会立即移除临时检出。
版本检出所在的位置就是这些已保存选项之一:
| 值 | 含义 |
|---|---|
| temp(默认) | /dsh-upgrade-checkouts-——位于系统临时目录下,因此操作系统会在重启时清除它;在多次运行之间复用,并且现在可用 --prune-checkouts(菜单项 11 → 9)移除 |
| keep | /checkouts,在多次运行之间保留 |
| 一个目录 | 你指向的任何位置:--checkouts DIR,或菜单项 11 → 8 |
检出是达到目的(比较)的手段,因此默认不会在永久位置累积。无论设置如何,现有检出都会被复用——先使用配置的位置,然后使用 /checkouts——只有当两者都没有时才克隆(使用 --depth 1)。DSH_CHECKOUTS_ROOT 优先于已保存的设置(见环境变量)。
检出目录不是临时文件——不要手动删除。 它是比较的输入,下一次运行会复用它,而不是再次下载标签;这正是该位置可配置的原因,也是 temp 是稳定的每用户路径而非随机目录的原因。
--prune-checkouts(菜单项 11 → 9)是唯一被允许的删除方式,而且必须主动请求。
为了“节省空间”而删除检出目录,会悄无声息地把下一次 check/inspect/plan 变成一次全新的
git clone——而在 /tmp 无法在命令之间保留的沙箱中,它可能使目标在本次会话的剩余时间内无法访问。
命令
| 命令 | 作用 |
|---|---|
| menu | 交互式菜单(不带参数时打开的也是同一个菜单)。 |
| status [--verify] [--loader] [--summary] | 核心版本和目录、标签、近期版本、插件及其来源的表格,以及 loads + surface 列——已安装副本是否可导入,以及它实际注册了什么。这些列显示上一次 verify 留在缓存中的判定结果;不带 --verify 时不会执行任何内容,因此该命令只是对配置文件和状态文件的快速读取。--verify 会填充缺失或过期的缓存;--loader 打印整个生效的加载器树,并标记被遮蔽插件所绘制的行;--summary 打印一行计数,而不是完整报告。 |
| verify [--cached] [--live] [--no-handlers] [--web-url URL] [--loader] [--summary \| --diff PATH] | 用 node 导入每个插件的已安装副本,调用其 apply(),然后调用 apply() 注册的每个路由处理器一次,并读取它抛出的内容和记录的内容——以及哪些插件具有被遮蔽的 surface(其 UI 宿主行被关闭)。--live 还会对运行中的 DSH(--web-url,默认 http://127.0.0.1:3080)GET 每个已注册路由,以证明该行已应用。--no-handlers 跳过处理器调用——这是一个较弱的答案,因此不会被缓存。--loader 打印生效的加载器树,并标记被遮蔽的行。--summary 打印一行计数;--diff PATH 只打印自先前保存的结果以来发生变化的内容。写入 state/verified-.json。 |
| core-versions | 所有核心版本和标签,以及有多少比已安装版本更新。 |
| plan [--limit N] [--all] | 在一次运行中:所有新的核心版本,以及每个版本上插件会发生什么(快速,仅声明)。 |
| check --core V [--update] [--summary] | 兼容性矩阵:每个插件在版本 V 上会发生什么。读取缓存的运行时判定结果,以评估探针在已安装核心上已证明的内容(见已验证);--update 还会查询注册表,并标记有更新已发布版本的插件(见可用更新);--summary 打印一行计数,而不是矩阵。写入不兼容列表。 |
| inspect PATH [--core V] [--since OLD] | 尚未安装的产物的兼容性——一个插件目录或一个 .tgz。只读取该产物;不读取配置文件,也不安装任何内容。 |
| detach [--only NAME…] [--yes] | 对整个配置文件进行快照并分离插件——全部分离,或使用 --only 仅分离指定的插件。不带 --yes 时仅显示计划并中止。 |
| attach [--from F] [--only NAME…] [--update] [--install-unknown] [--prune-failed] [--yes] | 从快照安装:安装兼容的插件,其余进入列表。--only 仅从快照恢复指定的插件。 |
| recheck [--file F] [--install] [--install-unknown] [--yes] | 重新检查不兼容列表并安装那些变得兼容的插件;使用 --install-unknown 时还包括未确认的插件(检查后)。 |
| plugins [--only NAME…] [--install-unknown] [--detach-first] [--yes] | 在当前核心上更新有更新版本的插件——全部更新,或使用 --only 仅更新指定的插件。配置文件中的其他内容不受影响:没有更新版本的插件不会被分离,也不会被重新安装(参见仅更新有更新的插件)。--install-unknown 还会安装未针对此核心确认的更新版本;--detach-first 在安装新版本之前移除每个插件。 |
| pipeline [--core V] [--update] [--install-unknown] [--run-core-upgrade] [--yes] | 从检查到检查后的完整流水线。--install-unknown 默认关闭:仅安装已验证兼容的插件,其他所有插件进入不兼容列表。在 Termux 上,核心升级之后、安装插件之前会执行补丁层——参见在 Termux 上。 |
每个步骤的包装脚本位于 scripts/ 中(menu.py、status.py、verify.py、plan.py、
check.py、artifact.py 用于 inspect、detach.py、attach.py、recheck.py、pipeline.py、
plugins.py、cores.py);参数会原样传递。inspect 的包装脚本名为
artifact.py,因为名为 inspect.py 的文件会遮蔽工具中每次导入所用的标准库 inspect 模块。
常用标志:--profile(默认 web)、--core、--offline、--no-clone、
--json、--verbose、--color auto|always|never、--state-dir、
--checkouts temp|keep|DIR、--prune-checkouts(删除临时检出并继续——或者
在没有给出命令时退出)、--termux / --no-termux 和 --termux-dir DIR(参见
在 Termux 上)。它们可以同时放在子命令之前
和之后——默认值在解析后应用,因为否则 argparse 会让子解析器的默认值覆盖在子命令之前给出的标志。
自动升级目标是已发布的最新核心版本。 在所有 --core 为
可选的地方(check、attach、recheck、pipeline),工具会解析已发布的最高
按 semver 确定版本,包含预发布版本,并将其打印为
auto target: 0.1.5-rc.2 — the newest published version (use --core to pick another one)。
故意不使用 latest dist-tag:在真实 registry 上,latest 指向
0.1.5-rc.1,而 0.1.5-rc.2 已经以 next 发布。已安装的 core 只是在
完全无法访问 registry 时的回退;plan 会列出所有候选。
目标必须是该工具能够获取的版本。 检查会将插件与某个版本进行比较,因此
该版本必须是已安装的 core、磁盘上已有的 checkout,或 registry 已发布的版本。
其他任何情况都会被拒绝,退出码为 1,并给出指明已发布内容的消息——一个
无人发布的版本没有可供比较的清单,而因为拼写错误就把每个插件都报告为“未确认”
会看起来像一个结果。--offline 和 --no-clone 缩小的是可获取的范围,
而不是可接受的范围:没有本地副本的版本会被报告为无法确认,而不是被猜测。
任何操作都可以缩小到某些插件。 plugins、detach 和 attach 接受
--only NAME…,然后只处理这些插件,profile 的其余部分保持不变。名称
会精确匹配,而 profile 中不存在的名称会报错并列出已安装的名称,因此
拼写错误不会变成静默的无操作。有两个细节源于快照的本质:detach --only
仍然会写入整个 profile 的快照(快照描述的是状态,而不是操作),并且
plugins --only 会通过从检查后重建不兼容列表,使其描述每个插件。在菜单中,同样的选择是在预览之前的一个问题,位于第 5 和第 6 项;第 8 项
只会处理有更新版本的插件,因此它改为询问 --install-unknown 和
--detach-first。
未确认的插件需要显式选择加入。 没有声明的插件既不能被证明兼容,
也不能被证明损坏,因此 pipeline、attach、recheck 和 plugins 只有在
--install-unknown(默认:关闭)时才会安装它。在菜单中,同样的选择是一个带有 [y/N]
默认值的问题。以这种方式安装的任何内容,之后都会由检查后根据实际落地的代码重新判定,
失败项会进入不兼容列表。对于 plugins,该标志也决定是否可以安装一个更新但未确认的版本;
没有它时,插件会保持原样,并在 Held back 部分中列出,绝不会被丢弃。
仅包含有更新的插件
plugins 是更新命令,不是修复命令:它只处理有更新版本的插件,
没有更新版本的插件不会被 detach、不会重新安装,也不会被丢弃。
因此,一次运行不会让插件从 profile 中消失,而整体修复 profile 是
detach 后接 attach 的用途。每个名称都会恰好落入三组之一,每组都会打印
其原因:
=== To update (2) ===
↑ dsh-alpha → 1.4.0
↑ dsh-read-url → 1.8.0 未针对此核心确认——因
--install-unknown 而安装,事后已检查
=== 已保留——存在更新版本(1) ===
· dsh-beta 1.2.0 → 1.3.0:1.3.0 已发布,但已证实与此核心不兼容
=== 未处理——无需更新(1) ===
· dsh-gamma 0.19.1:未发布更新版本
在当前核心上评估为 compatible 的更新版本会自动安装。而仅仅是未确认的更新版本——插件未声明 DSH 版本,或其声明不接纳此核心——会被保留并列出名称,因为安装它是一种选择:
· dsh-read-url 1.7.0 → 1.8.0:1.8.0 已发布,但未针对此核心确认
——传入 --install-unknown 仍可安装,并附带安装后代码检查
使用 --install-unknown 后,它会移入第一组,被安装,并且事后检查会对实际落地的内容重新运行代码检查;判定结果会进入不兼容列表。已证实为 incompatible 的版本绝不安装,无论是否带该标志——它会留在第二组。
只有 npm 源才可能有更新:file:、link: 或 github: 插件会从其自身的说明符重新安装,没有可比较的注册表版本,因此它始终位于第三组。所选版本会被固定(name@version),因此预览显示的内容与实际安装的内容不会出现分歧——包括已安装副本本身未确认的情况。
默认情况下,新版本会覆盖当前副本安装,这正是 dsh 自身的行为,也能避免安装失败导致插件缺失。--detach-first 会恢复旧的先 remove 再 install 顺序;在菜单中,它是第 8 项的第二问。
在 Termux 上
在 Termux/Android 上,当 npm i -g 返回时,核心升级尚未完成。该命令会恢复原始的上游目录树,而原始目录树在那里无法运行:Android 的 sepolicy 拒绝在应用私有存储中执行 link(2)(write/edit 工具发布文件以及保存会话和附件正是以此方式进行的),Bionic 没有 flock(2),sharp 不提供 android-arm64 二进制文件,并且若干 process.platform === "linux" 检查永远不会匹配 "android"。
这些修正位于一个单独的层中——即
deepseek-harness-termux
分支——其基于锚点的补丁程序会在每次核心安装后重新应用。该工具对此有所了解:
* 该层会被找到,或被获取。 它会在常见位置被发现(以及通过
$DSH_TERMUX_DIR);当不存在时,该分支会被克隆到
$DSH_HOME/termux-layer。--termux-dir DIR 可显式指定一个——而一个不是层的目录会被如实报告,而不会被另一个检出内容悄悄替换。
* 目标会被限制在该层所验证的版本。 补丁程序匹配
上游通过精确锚点进行验证,因此核心一旦超过其验证所针对的发布版本,就无法再打补丁。install.sh 将该发布版本记录为 VALIDATED_DSH_VERSION,自动目标会使用它,而不是最新发布的版本(并附有说明)。这个上限永远不会向后移动:落后于已安装核心的层会被报告出来,而不会被变成降级。
* 顺序在流水线本就存在的接缝处得到纠正。 如果不加
--run-core-upgrade,该工具会打印 npm 命令,然后打印其后的步骤——
fix-dsh-runtime.sh 在 attach 之前:
=== Step 3. Core upgrade ===
currently installed: 0.1.1-rc.2
command: npm i -g @deepseek-ai/dsh@0.1.5-rc.2
termux layer: /data/…/deepseek-harness-termux
Run this command yourself (it needs access outside the workspace), then:
1. bash /data/…/deepseek-harness-termux/fix-dsh-runtime.sh
2. …/dsh_upgrade.py attach --yes
加上 --run-core-upgrade 后,同样的事情会自动发生:先 npm,再打补丁程序,然后
是插件。这个顺序并非表面功夫——后置检查会依据插件所运行的核心来安装和评判插件,
而依据未打补丁的核心来评判插件,就等于依据一个在此平台上无法写入文件的核心来评判。
* 原生模块会被检查,只有在缺失时才会重建。 打补丁程序不会重新编译任何东西;
版本升级可能会用不带已构建附加组件的源码替换 node-pty/koffi。之后会对它们进行探测
(从核心目录执行 node -e require(...)),只有当探测确实失败时,该层的安装程序才会重建它们——
需要几分钟,但仅限那时。
status 会打印该层及其验证的版本(--json 会将其作为
core.termux 携带);菜单的设置界面将其显示为第 11 项。这一切都不依赖于平台被意外正确检测:
--termux 强制开启该纠正,--no-termux 为刻意保持纯净的核心关闭该纠正,而在任何非 Termux 的环境中,
整个主题都不会出现在输出中。
面向脚本和代理的报告
status、check、verify 和 inspect 都接受 --json。此时 stdout 只携带一个 JSON
文档,人类可读的报告和进度行则输出到 stderr,因此该文档可以直接管道传给解析器而无需过滤。
check --json 仍会写入 state/check-.json,它写入的路径会列在文档的 state 下。
status、check 和 verify 也接受 --summary。
原因代码。 check 和 inspect 的每个插件条目都会在其
reason 文本旁携带一个 reason_code,这样消费者无需解析自然语言就能对发现进行分类。
同时未通过多项检查的插件会携带最根本那项的代码;详细的命中项保留在该条目自己的列表中。
| 代码 | 发现 |
|---|---|
| SYNTAX_ERROR | 随附的代码不是有效的 JavaScript,因此宿主无法导入它 |
| PEER_RANGE_MISMATCH | 某个 peerDependencies 范围不接纳目标核心 |
| ENGINES_DSH_MISMATCH | engines.dsh 不接纳目标核心 |
| REMOVED_PACKAGE_REQUIRED | 代码所需的包,目标核心已不再提供 |
| REMOVED_CLIENT_SYMBOL | 代码使用的标准 prop,目标核心已不再声明 |
| SLOT_KIND_MISMATCH | 某注册不满足目标插槽声明的 kind |
| SLOT_UNKNOWN | 客户端半边注册进了目标核心已不再声明的插槽 |
| BROWSER_MODULE_TABLE_MISS | 某客户端 bundle 需要的名称在目标模块表中缺失 |
| DUPLICATE_FACTORY_REGISTRATION | 某客户端 bundle 注册了宿主已拥有的 factory id |
| DECLARATION_INTEGRITY_FAILURE | dsh.client 声明,或其承诺的 bundle,在任何核心上都不可加载 |
| INLINE_PURITY_VIOLATION | 某客户端 bundle 内联了必须来自模块表的包 |
| WIRE_ENDPOINT_DEAD | 某字面量 /api 调用指向了目标核心不提供的端点 |
| WIRE_METHOD_MISMATCH | 端点有提供,但信封自身的方法与之不一致 |
| HANDLER_REFERENCE_ERROR | 某已注册路由处理器在首次调用时抛出了 ReferenceError |
| UNKNOWN_DECLARATIONS | manifest 未声明任何 DSH 版本——无从比较 |
当没有任何检查产生带标识符的发现时,该字段为 null:一个兼容的插件,或一个完全无法读取的
manifest。
阻止核心升级。 check --json 的每个插件条目都带有 blocks_core_upgrade,即对“如果流水线运行,它会停下吗?”的回答。流水线会扣下不兼容的插件并安装其余部分,因此无论声明如何读取,插件级不兼容时该字段为 false。只有当目标核心本身不可达时——即版本比已安装的更旧,升级无法降级到该版本——它才为 true。
定论或线索。 在 verify --json 中,findings 列表收集来自全部三个来源的运行时发现——损坏的路由处理器、被遮蔽的表面以及核心不提供的调用——并为每一项给出 confidence:
{
"plugin": "example-plugin",
"surface": "handler!",
"confidence": "verdict",
"detail": "the first call threw: ReferenceError: SOME_CACHE is not defined",
"path": "/api/example",
"reason_code": "HANDLER_REFERENCE_ERROR"
}
"verdict" 是证明——探针看到了录制桩不可能导致的失败,或者核心自身的声明不包含该调用。"lead" 是读者仍需确认的证据。各插件结构(plugins、shadowed、wire)在文档中保持不变,且 shadow 与 wire 条目带有相同的 confidence 字段。
一行计数。 --summary 用一行替换报告:
5 plugins checked, 2 incompatible, 1 unknown, 0 verified, 1 wire-dead, 0 handler-failures
配合 --json 时,相同的数字是一个对象——total、incompatible、unknown、verified、
wire_dead、handler_failures 和 exit_code(命令返回的代码)。incompatible
统计具有明确否定状态的插件,unknown 统计未产生任何判定的插件,
verified 统计运行时探针已在已安装核心上验证过的插件,而 wire_dead 和
handler_failures 则统计调用已失效的插件,以及路由处理器在首次
调用时即失败的插件。status 和 verify 省略 verified:它们报告的是探针本身,而非目标核心。
比较两次运行。 verify --diff PATH 读取先前保存的结果(state/verified-.json,
或任何具有相同结构的文件),并仅打印有差异的部分:
=== Changes since 2026-09-12 ===
example-plugin
loads: no → yes (fixed)
surface: routes:3 → apply!
gone-plugin: removed
该比较涵盖每个插件的加载判定和表面,并将仅存在于一侧的插件报告为新增或移除。使用 --json 时,输出为
{"changed": [{"plugin": …, "fields": {"loads": {"old": …, "new": …}}}], "added": […],
"removed": […], "unchanged_count": N, "since": "…"}。退出代码即 verify 返回的代码:
当某个服务器条目无法加载时为 2。
plan 和 core-versions 仅打印其表格;它们不接受 --json。
输出:颜色、分组、换行
* 颜色。 在终端上,状态会着色(ok 绿色,NO 红色,?? 黄色,✓ 粗体
绿色),标题
为青色,路径和命令会高亮,注释为暗色。管道输出保持纯文本,因此日志
和 --json 不受影响。模式由 --color、DSH_UPGRADE_COLOR 环境
变量或 NO_COLOR 选择。
* 分组。 每个矩阵都按分组呈现,而不是一个扁平的大堆:兼容性表按
状态分组(incompatible → unconfirmed → compatible → verified),status 按插件来源
(npm / local / git)分组,
plan 按结果分组(safe → unconfirmed present → incompatible present),不兼容列表
查看器按状态分组。
* 长单元格会换行,绝不截断。 reason 列绝不会被截断:列宽
根据内容计算,按终端宽度按比例压缩,任何
放不下的内容都会在下一行继续——在其自身列下对齐。完整的原因始终
在任何宽度下都可见。
兼容性如何判定
九项独立检查——每项都能捕获其自身类别的破坏。检查 1 按设计复现
市场判定(参见 Attribution);检查 2–9 则超越它:
1. DSH 版本要求的声明。 读取宿主包的对等范围以及
engines.dsh——在两个位置:顶层的 engines.dsh(市场
引擎能看到它)和嵌套的 dsh.engines.dsh(没人能看到它:核心和市场
都看不到——这就是为什么仅在那里声明它的插件在市场
中可能看起来“未声明”)。失败的方向遵循市场策略:below-min,
exact-pin、above-explicit-max——一种确定的不兼容;above-implicit-ceiling
(形如 ^0.0.1 的范围)——则不是。
2. 已移除的包。 将基线核心的清单与目标版本的清单进行比较,然后在插件代码中搜索对这些名称的硬 require/import。此处忽略 dsh.client.inject 字段:它仅供参考——只有构建产物中真实的 require 才会导致崩溃。基线是已安装的核心;inspect --since 会覆盖它,因为对照已安装的核心进行比较是自指的,无法证明任何东西。TARGET 一侧被有意设计得比市场的主机策略更宽:已安装的树与目标检出清单的并集。仅已安装的树并非完整的目标——一个被打包进 shell 的平台模块(dsh-client-ui-primitives)没有自己的 node_modules 条目,而仅基于已安装内容的对比会将其称为“已移除”,并拒绝一个仅仅 require 了一个种子词的 bundle。
3. 浏览器模块表。 客户端 bundle 中的 require 调用会对照目标版本的种子词及其清单进行检查。未命中意味着在 bundle 物化期间抛出
client-modules: require("…") missed the module table。
4. 客户端 bundle 内部的工厂注册。 每个浏览器 bundle 都作为一行图加载,并且必须恰好注册一个工厂——它自己的。一个同时为另一个包注册工厂的 bundle(一个自注册的客户端 bundle 被内联,而不是从主机 require)会使加载器抛出
client-modules: duplicate factory registration for "" (bundle executed twice without invalidate?),并且 DSH 不会启动。声明得到满足,每个包都存在,类型检查通过——但仍然无法启动,这正是此检查存在的原因。
5. 声明完整性。 主机从 dsh.client 组合启动图,然后获取
exports["./client"],因此声明本身就是一份契约。检查内容:dsh.client 是对象,platform 是字符串,inject/external 是字符串数组,immediately 是布尔值,声明的客户端确实导出了 ./client,该导出是字符串或
{ default: string },该行不请求自己的包,并且所承诺的 bundle 存在于打包产物中。所有这些都是版本无关的:它在任何核心上都会失败。
6. 内联纯净性。 客户端 bundle 只能内联没有共享运行时标识的 wire 层。
它内联的任何 @deepseek-ai/ 包必须是模块表行、其自己声明的
dsh.client.external、一个内联安全的层、一个 vendored 库,或一个生成的 /remote
贡献——这是核心自身的构建时纯净性规则,从目标检出中读取
(checkout.inline_classification)。这是针对 STALE 产物的检查:在较旧的
规则,它内联了一个宿主现在作为自己行加载的模块,而第二份副本不共享任何
状态——不会抛出任何异常,重复副本只是不再通过
Symbol/instanceof/单例匹配,面板保持为空。该规则应用于被内联
模块的实际子路径,而从不应用于整个包。
7. 语法。 产物所附带的代码由将要导入它的同一个 Node 解析
(node --check),在一个复现产物自身布局的临时树中进行——相对
路径、扩展名及其 package.json,因此 Node 读取模块类型的方式与加载时完全一致。
此检查最先运行,而且它是其他检查无法替代的:检查
2–6 都以文本形式读取代码,因此一个不是有效 JavaScript 的文件没有可匹配的
说明符,并通过所有检查——而加载器永远不会到达 apply(),DSH 也不会
启动(SyntaxError: Unexpected token ':',来自遗留在 .js 文件中的 TypeScript 注解、
一个多余的合并标记、一次截断的写入)。它与版本无关,无需安装
依赖,也不执行任何内容。一个声明了没有 type 的产物中的 .js 文件,只有在
它既不解析为 ESM 也不解析为 CommonJS 时才会被报告——Node 在加载时决定这种情况,
而答案因 Node 版本而异。
8. 已移除的核心客户端符号。 宿主注入到每个基于插槽契约构建的浏览器
组件中的标准 props(declare module '@deepseek-ai/dsh-client-ui-slots' 内的
interface GlobalStandardProps,其中声明了诸如
useSessionPendingInteraction 之类的 hook)从基线和
目标中读取,而目标不再声明的成员会在插件的客户端
部分中搜索。插件的代码保持有效,宿主自身的骨架继续传递该 prop——
这就是为什么不会抛出任何异常,并且刻意没有其他东西看到它:消费者读取到 undefined,
而面板渲染错误或根本不渲染。与检查 2 一样,这是相对于基线的 DELTA
(已安装的核心,或 --since),因此插件自身声明的名称——或任何核心版本
从未声明过的名称——永远不会被报告。三分之二的声明被设计性地过滤掉:一个
短于六个字符的成员,或一个足够通用、会因插件自身原因出现在插件自己代码中的成员
(id、sessionId、open……),不会被归因于该契约。
9. 插槽契约。 核心的 SlotMap——每个插槽名称及其 kind 和 scope——以
相同方式读取,而客户端部分的 slots.inject('')、slots.register({ name: '' })
和 children: { '': … } 会与之匹配。两种失败,各对应一个 delta:一个
基线声明而目标丢弃的名称是一个惰性注册(卡片永远不会出现);一个
双方都声明但其 kind 发生变化的名称(一个真实案例:conversation.chat.turnTail 从 chain 变为
list)会让核心自身的注册表在 apply() 内部抛出异常——list slot "…" requires
options.id——而导入和路由探测都无法看到这一点。当目标的 ui-slots 源码存在时,会从中读取该选项规则,并带有内置回退,因此该发现是关于核心字节的事实,而非猜测。任何核心版本都未声明的槽位永远不会被报告:它可能属于另一个插件。
检查 4–6 的范围如何界定。 这三项都只读取已声明的客户端 bundle
(exports["./client"])——那正是加载器执行的文件。扫描包中的每个文件会把仅仅引用 facade 调用的文档和构建脚本也标记出来。检查 4
捕获为它并不拥有的行注册工厂的 bundle;检查 5 是与版本无关的那一半(宿主拒绝的声明在任何核心上都会失败);检查 6 是
依赖目标的那一半(在目标规则之外的规则下构建的产物)。检查 7 是
相反的情况,会读取每一个代码文件,因为它们中的任何一个都可能是加载器导入的那个——
尤其是入口点。它仅在 PATH 上有 node 时运行,若未运行,报告会说明这一点。检查 8 和 9 在插件有已声明的客户端 bundle 时读取它,否则回退到
所有文件以查找尚未构建的仓库链接;两者都需要核心在两侧的声明,因此没有目标检出(或已安装的目标)时它们不会报告任何内容,并会说明这一点——绝不会给出虚假的“干净”。
另外——产物来自何处。 对于具有本地源码
(file:/link:/workspace:/portal:)的插件,清单和代码是从产物
本身读取的——即 tarball(标准库 tarfile,不解包)或目录;该工具根本不会为此类插件去注册表查询。这一点至关重要:npm 中的同名包可能装着
不同的产品——本地构建的版本和已发布的版本可能携带不同的代码和
不同的 peer 固定版本——对它下结论就等于对别人的代码下结论。对
仓库目录的扫描不会深入 node_modules、.git、tests/ 和 examples/——
这样测试夹具就不会产生虚假的“NO”。
另外还有两个在实践中很重要的注意事项:
unknown 不是“兼容”——也不是“损坏”。?? 意味着清单对 DSH 版本
没有任何声明,因此没有什么可比较的:原因行正是这样说的
(“清单未声明 DSH 版本,因此没有什么可比较;代码检查是
干净的”)。这是声明中的空白,而非失败。此类插件可以用
--install-unknown 安装——安装后的后置检查会针对实际代码
重新运行检查并过滤掉坏的。要查明它们是否真的能工作,去问运行时而不是
声明:参见它们真的能工作吗?。当运行时
给出答案后,该插件就不再被报告为未确认——参见
已验证。
* 经验性判定。 如果检查针对的是插件已安装所在的同一核心运行,且代码是干净的,那么即使声明要求更新的版本,该插件也算作良好。因此,一个插件可能声明 ^0.1.2-rc.1(比已安装的核心更新),却能在 0.1.1-rc.2 上完美运行;因为严格的声明而破坏一个可用的安装是不可接受的。
已验证——已检查并确认可正常工作
声明检查说明清单承诺了什么,运行时探测说明代码实际做了什么。当两者对正在判定的核心意见一致时,工具就会如此标明:该插件是已验证(✓,最强状态),而不仅仅是 ok 或 ??。
只有当以下所有条件都成立时,插件才会被评定为 verified:
* 目标就是已安装的核心——运行时判定是关于某一个核心版本的证据,而缓存以指纹为键,该指纹涵盖核心及每一份已安装副本,因此在任一者发生变化之前取得的判定不会被复用;
* 缓存的探测看到服务器入口被导入、apply() 运行且未抛出异常,并且它注册的每个路由处理器都对其首次调用作出了应答;
* 声明检查和代码检查均未发现问题,并且未声明插件的代码检查确实运行过且结果干净——仅“未声明”本身不构成证据;
* 客户端部分不会绘制到配置文件已关闭的行中,并且插件的任何调用都不会访问此核心不提供的端点。
生成该评级不会执行任何操作:它读取先前 verify 留下的缓存。因此,对已安装核心运行 check 会为 verify 已证明的插件报告 verified,而如果没有新的判定,则再次将它们报告为 compatible 或 unconfirmed——当评级很重要时,请先运行 verify(菜单项 14)。已证实的不兼容绝不会被可用的安装所掩盖,而经验性判定会保留说明它覆盖了哪项声明的原因。
可用更新
注册表上发布的版本通过 --update 查询,并且始终由 plugins 查询——选择更新意味着比较版本,因此无论标志如何,plugins 都会查询注册表。只有 npm 源才可能有更新:从本地构件安装的插件没有可查询的注册表版本。填入该列后,矩阵的 version 单元格会被标记,以便有更新版本的插件一目了然,而颜色表示工具实际会做什么:
| 标记 | 含义 |
|---|---|
| ↑ 绿色/粗体 | 最新发布的版本评估为兼容——这是更新会自行安装的版本 |
| ↑ 黄色 | 存在更新的版本,但未确认(?? 或不兼容)——plugins 会暂缓安装它,仅在 --install-unknown 下安装(绝不会安装已证实不兼容的版本) |
latest 列包含版本本身,并以相同方式着色。表格下方的图例列出了
两组中的确切说明符,因此可安装的那些不必从两列中重建。
该标记有意不是一种承诺。它取自更新逻辑所使用的同一判定(best_compatible_version),因此绿色的 ↑ 是 plugins 真正会安装的版本,而黄色的则是在被要求时才会安装的版本——这一区别很重要,因为较新的发布版本可能会提高自身的最低核心要求。没有 --update 时,该列因缺少数据而保持为空(并非因为每个插件都是最新的),报告会明确说明这一点,而不是让空列被读作“一切均为最新”。标记仅为装饰:--color never、管道或日志文件仍会得到 ↑ 和 latest 值;只是颜色被去掉了。
与标记无关:本地插件可能与 npm 上发布的不同包同名。该同名包会在详细块中报告(npm has X under this name),绝不会作为更新——本地产物才是已安装的内容,并且它是按说明符安装的。
它们真的能工作吗?
声明说的是“可能运行”;它们无法说“确实运行”。导入也不能。 一个插件可以完美导入,却完全没有效果,有四种方式,任何声明扫描和导入探测都无法看到:
* 该行从未生效。 一个 cordis 插件的 inject 指定了部署未提供的服务,它就永远不会被应用——加载器让其 fiber 保持 pending,而 apply() 根本不会被调用。
* 它绘制到的表面已不存在。 增强另一个组件 DOM(或填充其插槽)的客户端半部分,仅在该组件挂载时才能继续工作。当生效的 profile 禁用或替换该行时,客户端半部分会加载,然后悄无声息地什么都不做。
* 它的调用指向核心不再提供的端点。 一个客户端半部分 POST 一个网关后来重命名的 RPC 路径,其代码看起来仍然有效,导入干净,并运行 apply()——而调用返回 404,因此该功能悄无声息地失效:每个请求都失败,而文件中没有任何地方看起来有问题。
* 路由已注册,但其背后的处理程序已损坏。 apply() 将一个闭包放入部署;它并不运行它。重构中删除的变量(SOME_CACHE is not defined)、重命名的服务方法、处理程序内部的错误路径——在请求到达之前,这些都不存在。当处理程序捕获自己的错误、记录它并返回 200 [] 时,表面显示空列表而不是失败,并且外部没有任何东西捕获它。
因此,该工具回答五个独立的问题,而不是一个:
| 列 | 问题 | 方式 |
|---|---|---|
| loads | 代码是否导入了? | verify(菜单项 14):每个插件一个 node 进程,cwd = 该 profile |
| surface | 它向部署中放入了什么? | verify 针对一个记录上下文调用 apply();--live 在运行中的 DSH 上确认路由 |
| surface(处理器) | 它在被调用时能正常工作吗? | verify 会用一个合成的 GET 调用每个已注册的路由处理器一次,并读取它抛出的内容和记录的内容 |
| shadowed | 它绘制到的宿主组件是否被关闭? | 有效的加载器树,从 bundle 补丁和 profile 自身的 cordis.patch.yml 读取 |
| wire | 核心是否仍然提供它所发起的调用? | 来自已安装核心生成的 TYPERT 面的端点集合,以及来自插件自身文件的调用 |
1. 运行时探测 —— verify(菜单项 14)。 每个已安装插件的挂载入口都由一个单独的 node 进程导入,
该进程的工作目录是 profile 目录,因此模块图的解析方式与 DSH 启动时完全一致。这正是捕获声明所遗漏的
故障的机制:指向核心不再提供的 @deepseek-ai/ 的链接、无法解析的 peer 依赖、不再解析的
exports 映射、入口文件中的语法错误。
然后它针对一个记录上下文调用 apply():插件所获取的每个服务都是一个代理,会记录方法名,
ctx.effect(cb) 和 ctx.inject(deps, cb) 回调会被调用(许多插件正是在这里进行注册),并且每个
webServer.register({path, handler}) 都会被收集为一个路由。一个插件可以挂载多个模块 —— 一个包、
它的一个子路径和另一个子路径 —— 因此每个挂载的说明符都会被探测并汇总为一个判定;只探测裸名称会把
一个正常工作的插件误判为空操作。
最后它会调用处理器(见下文“处理器调用”)—— 这一步用于捕获部分故障。
python3 dsh_upgrade.py verify # import + apply() + call the route handlers
writes state/verified-.json
python3 dsh_upgrade.py verify --cached # reuse the cached verdicts
python3 dsh_upgrade.py verify --live # also GET every registered route on the running DSH
python3 dsh_upgrade.py verify --no-handlers # skip the handler calls (weaker; not cached)
python3 dsh_upgrade.py status --verify # the same verdicts as the loads column of status
python3 dsh_upgrade.py status --loader # the whole effective loader tree, shadowed rows marked
--live 是实证性的,而且它是可选的,因为它确实会触达插件的处理器:对已注册路径的 GET 只要返回
除 404 以外的任何内容,就证明该行已在实时部署中生效 —— 这是任何导入和静态读取都无法展示的。
在代码中注册但在主机上返回 404 的路由会被报告为 404:N,这意味着代码没问题,但该行未被挂载。
注意它与 verify 的处理器调用的区别:--live 驱动的是正在运行的部署,而处理器调用运行在一次性
探测进程内部,这就是为什么后者默认开启。
诚实的范围说明:记录上下文是一个桩,因此它展示的是插件代码注册了什么,
不是对部署的模拟——插件需要但部署缺少的服务,从这里看不出来,因为插件只会拿到一个桩。这个桩被刻意设计得很宽容(未知的服务方法可调用并返回另一个桩,因此 ctx.get("credentials").resolve(...) 不会因探测自身的缺口而挂掉),但它终究只是个桩,而这会影响处理程序失败可能被称为什么——见下文。作为第二个参数传入的空配置模拟的是“一个带有空配置块的条目”(否则,一个根据 config === undefined 分支的插件会看起来像是死的)。用桩调用 apply() 永远无法证明启动时成功;--live 和 DSH 自己的日志可以。完全没有服务器入口的包(仅客户端的 dsh.client 包)不会被执行——它们被报告为 client,其代码在浏览器中运行。
2. status 的 surface 列(菜单项 1)。
| 值 | 含义 |
|---|---|
| live:N | 运行中的 DSH 提供 N 个已注册路由——它已应用 |
| routes:N | apply() 注册了 N 个表面(由代码证明,而非由宿主证明) |
| hooks:N | 没有路由,但 apply() 触及了服务、钩子或事件 |
| client | 其行为在浏览器包中;服务器部分为空或不存在 |
| declarative | 该条目有导入,但不导出 apply()——没有可运行的内容 |
| no-op | apply() 运行了,但没有注册任何可观察到的东西 |
| apply! | apply() 针对记录上下文抛出了异常 |
| 404:N | 在代码中已注册,但运行中的 DSH 返回 404 |
| shadowed | 其客户端部分所绘制到的宿主组件被禁用——见下文 |
| shadowed? | 被禁用行的名称出现在客户端部分中——是线索,不是定论 |
| wire:404 | 其运行时调用指向此核心不提供的端点——见下文 |
| handler! | 它注册的路由在第一次调用时失败——闭包中的 ReferenceError,见下文 |
| handler? | 处理程序发出了警告、记录了日志或抛出了桩也能产生的东西——是线索,不是定论 |
3. 处理程序调用——捕获自身错误的路由。 apply() 只注册路由处理程序;闭包直到请求到达时才会执行。因此在 apply() 之后,探测会用合成的 GET 调用每个收集到的处理程序一次——webServer.register({kind, path, handler}):一个可读取、可迭代、可监听的 req,以及一个记录用的 res。它捕获该调用抛出的内容、它记录的所有内容(级别和文本)以及它产生的应答。控制台仅在调用期间被包装,且调用是顺序进行的,因此每个处理程序的输出都明确无误地归属于它。
这是唯一能看到部分失败的东西。一个插件可以干净地导入、干净地应用、注册一个路由,并用 200 应答一个实时 GET,而其背后的处理程序却是坏的:
=== Route handlers that fail on the first call ===
apply() only registers the closure; the probe calls each route handler once with a GET and reads
它抛出了什么并记录了什么。一个捕获自身错误并返回 200 的处理器在其他地方看起来是健康的。
example-plugin
route: GET /example/items
第一次调用记录了一次失败:[example-plugin] items route: read failed: ReferenceError: SOME_CACHE is not defined
返回 200 "[]"
该声明在一次重构中被删除;处理器仍然读取它,捕获了错误,记录了它,
并返回 200 []。表面只显示一个空列表,别无其他——没有启动日志,没有
导入探测,没有 --live,没有插件自身的工作表面。
判定:handler! 是裁决,其他一切都是线索。 录制上下文是一个桩,而
桩会产生它自己的失败——当代理不是字符串时出现 resolved.value.trim is not a function,
在桩变得宽容之前出现 ctx.get is not a function,处理器按设计回退到的缓存文件出现 ENOENT。
把这些称为损坏会对一个正常工作的部署虚报狼来了。因此裁决只保留给桩不可能
产生的证据:一个 ReferenceError,因为未声明的绑定在每个上下文中都是未声明的。
其他一切——链中间的 TypeError、警告、错误级别的日志、在 1.5 秒窗口关闭时仍在运行的调用、
ENOENT——都会在 === Handler leads (not a verdict) === 下打印出确切的行,由读者决定。
该桩也是刻意宽容的:未知的服务方法可调用并返回另一个桩,结果可链式调用,
并且请求可异步迭代——因此探测不会因自身的缺口而责怪插件。
两个守卫保证调用安全,并且两者都会被报告,绝不静默:
* 路径名称包含变更操作(delete、save、reset、update、……)的路由会被记录但不会
调用——探测只会发送裸 GET。每次跳过都会出现在 === Probe notes === 下。
每个插件最多 12 个处理器,每个 1.5 秒,因此挂起的处理器会变成“未完成”线索,而不是
卡住的探测。
=== Probe notes === 下的一切都是上下文,绝不是裁决,并且每条注释都会连同其种类一起打印,
这样句子就不必被解码:
skipped by design——探测没有调用该路由(变更路径,或已达到处理器预算)。对它
两种情况都一无所知:既不是通过也不是失败。
* stub may be the cause——一个 ctx.effect/ctx.inject 注册回调在探测运行它时抛出,
或者服务查找进入了真实宿主可能跳过的分支。消息带有明确的提醒:录制桩是一个可能的
原因。桩能产生的消息类别很广,包括被代理触发的 Node 自身的参数验证——
The "path" argument must be of type string. Received function undefined 是桩到达
path.isAbsolute,不是插件缺陷。
* probe error——探测无法运行一个已注册的处理器。它对插件一无所言。
这个桩对名称和值同样宽容:ctx.get(name) 对任何名称都会返回一个桩,
因此,如果某个插件用这样的查找来守卫一个代码块,那么即使没有任何东西提供该服务,
该代码块也会被执行。这些查找会被记录下来(--json 中的 lookups),当其中某个查找
解释了某个回调失败时,报告会说明这一点——指出服务名称,并说明没有任何被探测的插件提供它,
已安装的核心也从未拼写过它,因此真实宿主会将其解析为 undefined,永远不会进入那个
分支。这就是一种只存在于探测内部的失败形态。
verify --no-handlers 会完全跳过这些调用。那是一种严格更弱的答案,因此它不会被
缓存:缓存绝不能丢失后来的读者会信赖的证据。--json 将这些调用携带为
handlers(path、error、logs、status、body、timedOut、ms),将具名查找携带为 lookups,
将注释携带为 notes——即原始句子,由 note_family()/note_line() 为读者渲染它们。
诚实的范围:这不是一个真实请求。处理程序看到的是 GET、没有请求体、没有身份验证,以及桩
服务,因此“第一次调用没有抛出异常”并不能证明处理程序能工作——只有“它抛出了
ReferenceError”才能证明它不能工作。
4. 被遮蔽的表面——当 UI 宿主行被关闭时。 该工具读取生效的加载器树:
按 bundle 顺序读取每个 bundle 的补丁层,然后读取 profile 自己的 cordis.patch.yml。它知道哪些行
存在、哪些被禁用,以及是谁禁用了它们。然后它将其与每个插件的客户端半边实际触及的内容进行交叉
引用。只有当三件事同时成立时,才会报告某个插件:它的客户端半边确实构建在某个 UI 表面之上,
被禁用行的 id 或模块名出现在它的代码或注释中(不是在面向用户的字符串内部),并且它本身不是
禁用该行的插件。
可以满足这一点的两种方式强度并不相同,报告会说明它使用了哪一种:
shadowed——一个判定。 package.json 的 dsh.client.inject 指定了一个加载器行处于关闭状态的模块。
不涉及任何启发式:客户端半边无法被组合,该插件无法工作。
shadowed?——一条线索。 该行的 id 或模块名只是出现在文本中,在代码或注释里。
一个客户端半边,其头部写着“该行 ⋮ 菜单由上游 ui-workspace 组件渲染”,与一个真实依赖
完全一样地匹配这一点,因此该行会随发现一起打印出来,由读者去核实。
一旦禁用该行的同一层中另一个已启用的行复现了客户端半边所选择的每一个 DOM 名称,这条线索就会
从该列中完全移除。这就是被替换的 UI 包的常见形态:替换者禁用了 ui-workspace 并挂载了自己的
会话列表,其各行携带与消费者所寻找的相同的 role="treeitem" 和 sessionRow 类。该模块已经
消失;契约
不是。该发现仍会被记录并打印——位于一条暗淡的“非损坏”行下,因此证据保持可见——但它不是警报。
text
=== 被遮蔽的表面 ===
example-plugin — 它所增强的 UI 未挂载
row: ui-workspace (@deepseek-ai/dsh-client-ui-workspace)
disabled by: example-replacement
evidence: package.json: dsh.client.inject names @deepseek-ai/dsh-client-ui-workspace
what to do: either you do not need the plugin, or the component it augmented has to be re-enabled
the whole tree, with the rows above marked: /usr/bin/python3 .../dsh_upgrade.py status --loader
references explained by a replacement (not breakage):
example-plugin names ui-workspace, and example-replacement re-mounts the same DOM contract
没有任何替代项能够解释的线索会单独成节,因为它值得一看,但并非证据:
text
=== 遮蔽线索(并非结论) ===
a switched-off row's name appears in the client half's text — a comment naming the component it augments reads the same way as a
real dependency. Check the line before acting on it; the tree below shows what is actually mounted.
example-plugin — the component it names may still be mounted
row: ui-workspace (@deepseek-ai/dsh-client-ui-workspace) is off
evidence: client.js:5: // The row ⋮ menu is rendered by the upstream ui-workspace component
no enabled row in that layer reproduces the DOM names this client half selects on
以 !!js 表达式禁用的行会被报告为条件性,而绝不会被报告为已禁用——这些由加载器决定,而本工具没有加载器。
解读树(以及为什么提示是一个完整路径)。 末尾的那一行是精确命令,用于复现该节内容:运行该工具的解释器、dsh_upgrade.py 的绝对路径,以及当报告并非针对默认 profile 生成时的 --profile。一个光秃秃的 dsh_upgrade.py status --loader 是一条只有在工具所在目录下才能工作的命令,而且读者还得猜测 profile——这就是为什么报告会打印完整命令。status 和 verify 都接受 --loader,因此无论该节出现在哪里,同一个标志都能工作;当某个插件被遮蔽时,树会标出关键的行:
text
row module state disabled by needed by
------------ -------------------------------- -------- --------------------- -----------------
ui-workspace @deepseek-ai/dsh-client-ui-works disabled example-replacement example-plugin
chat @deepseek-ai/dsh-client-ui-chat on @deepseek-ai/dsh-web —
(放不下的单元格会换行到续行——表格从不截断;上面两个模块名在此处缩短只是为了适应本页。)
needed by 列仅在存在被遮蔽的插件时出现,并列出绘制到其中的插件
关闭的行——该行就是“我必须重新启用什么”的答案,而一份光秃秃的行列表则不是。一个已启用的替代项已经解释了的引用会被有意排除在外:那个插件以不同的名称绘制到一个已挂载的组件中,而标记旧行会重新制造出 shadowed? 本就是为了避免的误报。--json --loader 以数据形式携带同一棵树(loader.rows[].state / .neededBy),因此当报告是机器可读时,该标志不会被悄悄丢弃。在菜单中,它只需按一次键:在报告发现一个被遮蔽的插件后,第 1 项和第 14 项会询问是否打印该树并在那里打印,这样读者就不必离开菜单、找到脚本并记住配置文件。
对于“该行是否已生效”的权威答案是运行中部署自身的插件清单——GUI 的 Settings → Plugins,由 @deepseek-ai/dsh-host-plugin-inventory 以 pluginInventory.list 提供。它需要 web token,因此该工具使用上面的离线等价物加上 --live 路由探测。
5. 线上契约——核心不再提供的调用。 运行时探测导入代码;此检查读取的是调用。已安装的核心为每个包生成一个 TYPERT 面,将其提供的每个线上端点声明为 namespace: '', method: '',因此所提供的集合是从核心自身的字节中读取的——无需任何主机运行,且只读命令保持只读。然后扫描每个已安装插件的客户端部分和主机入口,在带有 Connection client-request 信封的文件中查找字面量 /api/... 路径,并将每个路径对照该集合进行分类:
| 判定 | 含义 |
|---|---|
| ok | 该路径是已安装核心的一个端点(且信封的 method 与之相符) |
| dead | 它不是——当只有分隔符改变时,会指出后继者 |
| mismatch | 该路径是一个端点,但信封发送了不同的 method;主机会拒绝这些请求 |
=== Wire calls this core does not serve ===
read from the plugin's own files against the core's declared endpoints (its TYPERT faces): the call answers 404 at run time
example-plugin
dead: /api/items.list (method: "items.list")
at client.js:72
the legacy separator: this core serves "items/list" (the gateway claims /)
这就是此检查所针对的故障形态。共享的 /api 通道由 Typert 网关占用,其端点是 /;一个遗留的 items.list 不匹配任何拦截器,因此 POST 返回 404,fetch 抛出异常,该功能从未渲染——而插件却干净地导入、干净地应用,并提供了自己的 /api-ext/items.delete 路由。除了浏览器控制台,没有任何东西显示它。
诚实的范围:只读取字面量路径,因此由变量拼装而成的 URL 不会被追踪,也不会为其报告任何内容;只有带有 client-request 信封的文件才被视为 RPC 调用方,
因此,其他地方一个裸的 /api/... 路径会留给路由探测(那些是普通的 webServer 路由,而
/api-ext/... 按构造就是插件自己的扩展面);端点集合来自发布了 typert 导出的包。这项检查是静态的,因为它必须如此:对 /api 的未认证请求在路由之前就会返回 401,所以从外部看,一个被服务的路径和一个未被服务的路径看起来完全一样。
inspect 在工件安装之前运行同样的检查,因为那是对其采取行动成本最低的时刻:一个构建可以是干净的、声明了正确的版本并通过了所有其他检查,却调用了一个核心不再服务的端点。工件被归约为目录和 tarball 共享的同一个 name -> text 映射,因此 inspect ~/build/plugin.tgz 和 inspect ~/src/plugin 行为完全相同,而一个尚未构建的 .ts 源文件同样会被扫描。一个损坏的调用会让 inspect 以 2 退出(与不兼容声明相同的代码),并在 --json 中显示为 wire。
有一个限制没有变通办法,它会被报告而不是被猜测:当 --core 指定的版本不是已安装的版本时,wire 检查不会执行——端点集合是从 node_modules 下核心生成的面读取的,而这些只对已安装的核心存在;--checkouts 下的源码检出不会发布任何这些。报告会如实说明,而不是编造一个结论。
6. loads 列。
| 值 | 含义 |
|---|---|
| yes | 服务器入口在已安装的核心下成功导入 |
| no | 它无法导入——详细信息打印在表格下方 |
| client | 完全没有服务器入口:一个仅客户端的 bundle,由静态扫描覆盖 |
| — | 未安装在 profile 中 |
| ? | 尚未验证——运行 verify |
普通的 status 命令是只读的,从不执行插件代码:它显示上一次 verify 留在缓存中的内容。菜单的第 1 项会传入 --verify,并在缓存缺失或过期时进行探测。缓存以探测 schema 加上已安装的核心版本加上每个插件版本和清单时间戳为键,因此一次工具升级和一次核心升级各自只会刷新它一次,之后便是即时的。生效的 loader 树和 shadowed-surface 部分在每次运行时都从 profile 的补丁层读取——无探测、无缓存,始终最新。
你自己构建的插件(既不在 npm 上也不在 GitHub 上)
该工具会完整检查此类插件——无论是它们已经安装时还是它们处于分离状态时:
| 内容 | 工作方式 |
|---|---|
| 清单 | 从 tarball(file:…tgz)或目录(link:…)读取,而不是从注册表读取 |
| 声明(检查 1) | 来自该清单的 peerDependencies/engines.dsh |
| 核心差异(检查 2–6) | require/import 扫描在工件的代码上运行;无需解包 tarball |
| 快照与不兼容列表 | 存储 manifest 和 localPath,因此 recheck/attach 无需注册表即可工作 |
| 安装 | 严格安装记录的说明符(file:…tgz / link:…),版本不会被替换 |
| 版本更新(--update) | 按定义不可能:必须在其自己的仓库中更新并重新构建产物 |
该工具在非标准情况下的行为:
产物未找到(tarball 被删除/移动)——该插件被明确标记
(local source not found: ),并且不会被 attach 安装:“没有可安装的内容”。
为此,记录带有 installable: false 标记。
* npm 中名称已被占用——如果你以相同名称发布不同的产物,这会在 --verbose 下显示为
一条注释(“npm has X — that is a DIFFERENT artifact”),但它不影响判定或安装,
本地产物也绝不会被 npm 上的产物替换。
* 仓库目录——扫描时排除 node_modules、.git、tests/、examples/;
因此指向工作仓库的 link: 不会对测试夹具产生误报。
针对你自己的构建的实用顺序:为新核心重新构建 tarball → 将其放在相同
路径(或修正说明符)→ check --core → recheck --install --yes。
在安装之前检查产物
check 只能看到配置文件中已有的内容。inspect 直接接收产物,因此可以在构建
进入 node_modules 之前就对其进行判定——并拒绝它:
bash
python3 dsh_upgrade.py inspect ~/build/my-plugin-0.1.5-rc.2.tgz # 已构建的 tarball
python3 dsh_upgrade.py inspect ~/src/my-plugin # 仓库目录
python3 dsh_upgrade.py inspect file:~/build/my-plugin.tgz --since 0.1.1-rc.2 # 说明符形式
* 既不读取也不写入配置文件;不会安装任何内容。产物是唯一的
输入,这正是它可以安全地指向你尚未决定的构建的原因。
* 在菜单中(第 13 项),路径通过补全输入:Tab 列出并补全文件和
目录,空格会为你转义,~ 会被展开。手动加引号或转义同样
有效。
* 默认目标是已安装的核心——这里的问题是“它能否在我现有的
测试框架上运行”,而不是“最新版本是什么”。--core V 询问另一个版本。
* --since OLD 设置已移除包扫描的基线。针对已安装的核心,这种
比较是自引用的(X - X = ∅),证明不了任何东西;指定你的插件
所针对的发布版本,它才会变成真正的检查。
* 退出码与 check 一致:0——未证明不兼容,2——不兼容,1——路径
或清单无法读取。使用 --json 时,stdout 是单个 JSON 文档(分析
进度输出到 stderr),因此可以直接通过管道传入脚本。
* 因为该工件尚未在任何核心上运行,所以经验性升级和已验证等级在此均不适用:严格的声明会保持“未确认”状态,直到插件实际安装并正常工作。
安全
* 插件数据不会被删除。 分离操作就是在 profile 目录中执行 pnpm remove(正是 dsh plugin --profile web remove 所做的事);它只清理 node_modules。数据存放在 ~/.dsh(共享的 sessions、storages、skills、.agent-presets、settings.yaml、.credentials.yaml,以及插件在其下创建的任何目录)中,并保持原位——detach 命令会在操作前显示其映射。
* 快照总是在任何更改之前写入,并且对每个插件都包含名称、说明符、来源、版本、是否存在于 dsh.profile.bundles 中,以及完整的 package.json。即使在包已被分离之后,快照仍然可用:清单就在其中。
* 仅使用精确版本。 重新安装时,npm 包会以 name@version 的形式安装,而不是按记录的版本范围:像 ^0.3.16 这样的范围会拉取最新版本,而最新版本可能需要不同的核心(一个真实案例:插件的最新版本可能会提高其自身的最低核心要求)。
* 仅升级。 只有当版本通过检查时才会被提升,或者——在 plugins --install-unknown 下——如果它是读取器明确选择加入的较新版本,并且之后会由后检查重新判定;不存在降级。
* 缩窄的操作只触及它的选择范围。 --only 会在更改任何内容之前,针对 profile 检查每个名称,因此拼写错误会中止运行,而不是悄无声息地什么都不做。detach --only 仍会对整个 profile 进行快照,并且不兼容列表会根据整个 profile 的后检查重新构建,因此部分运行不会让其中任何一个只描述它所触及的内容。
* 后检查。 安装后会再次运行代码检查——这次是针对实际安装的文件。失败的会进入列表;attach --prune-failed 会立即移除它们,而 plugins 从不移除它安装的插件——一个通过 --install-unknown 选择加入但随后未通过后检查的未确认版本会留在 profile 中,并在列表中被点名。
状态文件
state/
├── snapshots/-.json|.md # “已安装内容”的快照
├── incompatible-.json|.md # 不兼容列表(由 recheck 读取)
├── check-.json # 上次检查的详细报告
├── verified-.json # 运行时验证判定(由 status 读取)
│ # 以探测模式 + 核心 + 每个插件副本为键
└── cache/ # 注册表和市场索引响应的缓存
设置文件(在多次运行之间记住的选项)是独立的,位于用户
配置目录,不在这里——参见上文“设置会被记住”。
列表示例:incompatible-0.1.5-rc.2.md——一个表格,包含状态(NO——已证实不兼容,??——未确认)、原因、要求以及接下来该怎么做的命令。只有既不兼容也未经核实的插件才会出现在那里,因此一次新的运行时判定就能清空该列表。
环境变量
| 变量 | 含义 |
|---|---|
| DSH_HOME | DSH 数据的根目录(默认 ~/.dsh)。 |
| DSH_INSTALL_DIR | 已安装核心的目录(否则会通过 PATH 中的 dsh 查找)。 |
| DSH_CHECKOUTS_ROOT | 版本检出所在的位置:temp(默认值)将它们保留在系统临时目录下,在多次运行之间复用,并在重启时清除;keep 是 /checkouts;其他任何值都是用于保留它们的目录。它优先于已保存的设置,只输给该次运行中给出的 --checkouts。 |
| DSH_UPGRADE_STATE | 状态目录(或 --state-dir 标志)。 |
| DSH_UPGRADE_CONFIG | 设置文件(默认 ~/.config/dsh-upgrade/config.json)。 |
| DSH_UPGRADE_COLOR | 颜色模式:auto(默认)、always、never。--color 标志优先于它。 |
| NO_COLOR | 设置后(设为任何非空值),在 auto 模式下禁用颜色。 |
检查与测试
bash
python3 -m unittest discover -s tests -v # 628 tests: semver, declarations, scans, registrations, local builds, core layouts, menu, settings, checkouts, verification, route-handler calls, the effective loader tree, shadowed surfaces, wire contracts, syntax, core client contracts, pasteable hints, completion, tables, target, update selection, verified grades
python3 dsh_upgrade.py check --core 0.1.5-rc.2 # exit code 2 if there are incompatible plugins
当一切兼容时,check 返回 0;当存在不兼容的插件时,返回 2——对脚本来说很方便。
该测试套件是自包含的:需要真实核心检出或一对真实插件产物的测试,在那些不存在时会跳过。要运行产物回归对,请将 DSH_UPGRADE_TEST_BROKEN_TGZ 和 DSH_UPGRADE_TEST_FIXED_TGZ 指向同一个客户端插件的两个构建——其中损坏的那个内联了自注册行 @deepseek-ai/dsh-api-session-controller——并将 DSH_CHECKOUTS_ROOT 指向一个存放核心检出的目录。
限制
* 核心仅在带有 pipeline --run-core-upgrade 标志(npm i -g …)时升级自身;默认情况下会打印该命令,因为安装核心会写入工作区之外,并替换正在运行的运行时。
* 内联纯净性(检查 6)读取的是某个 bundle 根据其 //#region node_modules/… 标记内联了什么——这是 tsdown/rolldown 发出的记录。由其他工具构建的 bundle,或者被压缩到足以丢弃这些标记的 bundle,就没有可读取的内容:此时该检查会保持沉默,而不是进行猜测(一个错误的“NO”会更糟——它会拒绝一个可用的产物)。它明确无误的那一半不受此影响:一个
一个内联了自注册客户端行的 bundle 仍会被检查 4 捕获,因为该检查读取的是 facade 调用本身。
* 对于 npm 插件,代码检查只有在插件已安装(或安装之后)时才可见。一旦它被分离,就只有声明对它有效——这正是清单被存储在快照中的原因。对于本地构建(file:/link:),扫描会在产物本身上运行,并且只要文件就位,就始终有效。
* 对于具有本地源(file:/link:)的插件,版本更新是不可能的:它们必须在自己的仓库中重新构建(参见“你自己构建的插件”一节)。
* verify 执行 apply() 并不能证明插件可用。 它构建的上下文是一个桩:它记录插件注册了什么,但插件需要而部署中缺失的服务只会以桩的形式到达,因此加载器本会保持为 pending 的行仍然可能看起来是忙碌的。探针能够证明的是反面和具体的事实:apply() 会抛出异常、它没有注册任何东西、某个路由处理器在首次调用时抛出 ReferenceError,以及——使用 --live 时——它声称的表面确实由正在运行的主机提供服务。关于“该行是否已应用”的最终结论是 DSH 自己的启动日志、GUI 的 Settings → Plugins 以及 --live。
* verified 是关于探针的声明,且仅针对一个核心。 它来自一次 verify 运行的缓存判定,并且仅当目标是已安装的核心时才被授予——正是同一个缓存将 compatible 和干净的未声明插件转变为 verified。因此该等级表示“探针在其运行所针对的核心上证明了此副本,且没有其他检查提出异议”,而绝不是“它将会工作”。更改插件、核心或探针 schema,指纹就会丢弃该判定:插件会回退到其声明状态,直到再次运行 verify。探针只能评定为线索(handler?、shadowed?)的插件也不属于已验证。
* 调用路由处理器并不是真实请求,而且只有 ReferenceError 才是判定。 处理器看到的是 GET、空请求体、无身份验证、桩服务和合成响应,因此“首次调用没有抛出异常”并不能证明它可用。所记录的调用会据此评定:ReferenceError 不可能是桩造成的(未声明的绑定在任何上下文中都是未声明的),并且是 handler! 判定;链中途的 TypeError、处理器按设计回退到的文件的 ENOENT、警告、错误级别日志,或当 1.5 秒窗口关闭时仍在运行的调用,都会作为 handler? 线索连同确切行号打印出来,因为桩也可能产生这些。路径名表示变更的路由会被记录但从不调用——探针只发送裸 GET——并且每次跳过都会在 === Probe notes === 下报告。--no-handlers 会完全跳过这些调用;该判定被有意不缓存。
* 补丁读取器不是 YAML 解析器。 它读取的是 DSH 实际使用的受限结构——一个顶层条目列表,每个条目要么是 insert: [...],要么是针对 id 的覆盖——并且只读取决定启用状态的键。锚点、流式集合和多行标量均未实现;config: 子树被有意忽略,因此其中的 name: 键不会被误认为是一行。用 !!js 表达式禁用的行会被报告为条件性,而绝不会是已禁用:这些由加载器决定,而此工具没有加载器。
* 被遮蔽的表面是一份报告,而非裁决。 检测器需要三个条件同时成立(一个构建在 UI 表面上的客户端半边、在其代码或注释中(而非面向用户的字符串中)被点名的已禁用行,以及该插件不是禁用该行的那个插件),并打印出它匹配到的证据行。请阅读该行:它是文本引用的证据,而非执行轨迹。这就是为什么代码匹配会打印为 shadowed?,而只有指名已禁用模块的 dsh.client.inject 才会打印为 shadowed。自动降级——同一层中一个已启用的行复现了消费者的 DOM 名称——也是一种启发式,并且它会在安全的方向上失败:无法识别的契约仍然只是一条线索。
* 线路检查读取字面量,且仅限核心自身的面。 插件内部由变量构建的路径不会被追踪,在带有 client-request 信封的文件之外发起的调用不会被当作 RPC,由已发布的 typert 面之外的东西提供的端点也不在比较集合中。它回答的是“这条路径是否是已安装核心所声明的”,这正是 404 所提出的问题,而不是“这次调用是否会成功”。
* --live 会触达真实处理器。 探测是一个不带请求体的普通 GET,但当插件注册了自身的路由处理器时,它确实会运行该处理器。这就是它需要显式启用的原因,也是它作为独立标志而非默认 verify 一部分的原因——它会驱动正在运行的部署。verify 内部的处理器调用则不同:它们在一次性的探测进程中运行,因此默认开启(--no-handlers 可将其关闭)。线路检查完全不触碰主机:未经认证的 /api 请求会在路由之前就返回 401。
结构
dsh_upgrade.py # 包含所有子命令的 CLI + 无参数时打开菜单
scripts/ # 每个子命令的包装脚本
dshupgrade/
├── paths.py # DSH 主目录、配置文件、核心目录(嵌套或提升)、检出位置、状态
├── config.py # 设置文件:在多次运行之间记住的选项
├── invocation.py # 可复现报告的确切命令(可粘贴的提示)
├── completion.py # 终端级路径输入(Tab 补全、反转义)
├── semver.py # 感知预发布版本的版本运算
├── registry.py # npm 注册表和市场索引(urllib + 缓存)
├── locals.py # 本地构建:file:/link:/tgz,来自产物的清单和代码
├── menu.py # 交互式菜单(不带参数打开时出现的同一个菜单)
├── host.py # 宿主包清单
├── compat.py # 九项兼容性检查
├── contracts.py # 核心客户端契约:标准属性 + 槽位表,来自
│ # 已安装的 faces 或某个 checkout 的源码
├── syntax.py # 用宿主自带的 Node 解析随附代码
├── checkout.py # 检出某个核心版本及其目录树中的事实
├── profile.py # 读取 profile、分离并安装插件
├── effects.py # 生效的 loader 树:哪些行存在、谁禁用了它们,
│ # 以及哪些插件绘制到已关闭的行中
├── wire.py # 插件发起的调用 vs 已安装
│ # 核心声明的端点(其生成的 TYPERT faces)
├── verify.py # 运行时探测:导入每个插件并调用其 apply()
├── snapshot.py # 快照和不兼容列表
├── analysis.py # 汇总判定
├── style.py # 颜色、终端宽度、文本换行
└── report.py # 宽度感知的表格和分组判定
tests/ # unittest(semver、locals、menu、settings、checkouts、
completion、report tables、target、registrations、
declaration integrity、inline purity、syntax、
core client contracts、core layouts、
verification、wire contracts、hints 和 loader tree)
state/ # 快照、列表、验证判定、缓存
归属
检查 1 中的兼容性计数是对 marketplace 插件引擎的刻意重新实现,而不是对它的导入。那个插件——
dshmarket(npm dshmarket,MIT,网站
dshmarket.com)——正是 harness 本身用来评判插件兼容性的东西。它无法在这里被复用:它是 Node 代码,而且在升级期间它会与所有其他插件一起被分离,因此决定要重新安装什么的工具将依赖于它刚刚移除的东西。
因此,这些规则被逐一移植到 stdlib Python 中,以便本工具的判定与 marketplace 的判定一致:
| 此处 | 上游(dshmarket) |
| --- | --- |
| compat.declarations_for | manifestFacts 加上 deriveHostCompatibility 的宿主包过滤器(lib/discovery-compatibility.js) |
| compat.evaluate | deriveHostCompatibility |
| compat.classify_failure — below-min、exact-pin、above-explicit-max、above-implicit-ceiling | classifyPeer(lib/compatibility.js):belowMin / aboveMax 仅在存在显式上界或精确锁定时才计为失败;高于隐式 caret/tilde 上界的新版宿主属于警告,而非失败 |
| semver.satisfies | satisfiesRange(version, range, { includePrerelease: true })(lib/check.js) |
| host.host_inventory | dshHostInfo() 背后的市场宿主包策略(lib/routes.js) |
| registry.MARKET_INDEX | 同一公共目录,awesome-dsh-plugin.com/plugins.json(lib/catalog-npm.js) |
没有从该插件中引入任何代码——这是一个独立的 Python 实现——但规则集是他们的,检查 1 旨在精确复现他们的判定结果。感谢 dshmarket 的作者们。
相关项目与现有成果
同一问题领域中的其他项目——与本工具相互独立,仅作列举,不构成比较或背书。它们在语言、范围以及介入时机上各有不同(在运行中的 harness 内部,或像本工具一样在其外部):
* Shizuku-keop/dsh-compat-guard
(dsh-compat-guard)——Node CLI:升级预检门禁、存储格式指纹识别、
$DSH_HOME 备份、会话迁移、按 profile 的 lockfile,以及插件 × DSH 兼容性
矩阵。
* whyihaveyou/dsh-suite——插件发现、
兼容性矩阵、SQLite 快照与升级差异(compatibility-radar)。
* zzy6-a/dsh-upgrade-guard——升级后插件
兼容性巡检,支持修复/禁用,以及宿主外监督救援。
* oh-my-dsh/dsh-plugin-upgrade-skill——
一个帮助插件跟上 dsh 版本升级的技能。
* ybl2020/dsh-upgrade——一个升级流程技能
(评估 → 审批 → 升级 → 验证),附带备份与回滚说明。
* william-jin-cmu/dsh-plugin-upgrade——
用于跨框架版本迁移插件的技能 + 脚本。
* @linxin666/dsh-doctor——事务性救援模式、
隔离恢复胶囊,以及 DSH profile 的回滚。
* @xiaoyuyu6420/dsh-backup——~/.dsh 的备份/恢复与
GitHub 同步,包括升级快照。
感谢 DeepSeek Harness 插件社区;上述项目均为其各自作者的作品。
许可证
MIT。参见 LICENSE。