#!/usr/bin/env node /** * 防回归:可安装为 DSH 插件的包必须用 `peerDependencies` 声明与当前仓 * cohort 列表一致的 DSH 兼容范围。 * * 两个字段各管一件事,本脚本同时守: * - **强制**:`peerDependencies` 里名为 `@deepseek-ai/dsh` 或以 `@deepseek-ai/dsh-` * 开头的项。DSH 在**安装前**(plugin-manager)与**启动时**(app-boot 的 * compatibility preflight)都用 * `semver.satisfies(runtimeVersion, range, { includePrerelease: true })` 判定; * 预发布参与范围匹配(这一点与 npm 默认语义不同)。范围必须**逐 cohort 写全** * (`^` 用 `||` 连起来):只写一段就会把另一个仍在服役的 cohort 判死—— * 0.2.0-rc.1 那轮就是这么暴露的(`^0.1.7-rc.2` 展开是 `>=0.1.7-rc.2 <0.2.0-0`, * `0.2.0-rc.1` 差在 `-0` 上,连 `includePrerelease` 也救不回来)。 * - **展示**:`dsh.engines.dsh`(**嵌在 `dsh` 下的这一层**)。这一层至今没有任何宿主 * 读取方(rc.1 的 app-boot README 原文:这些检查使用 peer 声明,而不是 engines.dsh), * 但插件市场 / 社区条目那类外部消费方仍按它展示「兼容 / 不兼容」。保留它只为市场口径, * 值必须与 peer 下限一致——否则就是一条与事实不符的声明,比不声明更糟。市场解析器只认 * 单段 `>=X.Y.Z[-预发布]`,所以它写的是**最低档**,不跟随多段 peer。 * * **2026-10-16 复核补记:顶层位与本层不是一回事。** `0.2.1-alpha.4` 起 app-boot 的兼容性 * 判定会读**顶层** `engines.dsh`,而且它一旦存在就**短路 `peerDependencies`**。实测(判定器 * runtime `0.2.1-alpha.5`):顶层 `engines.dsh: ">=999"` + peer 正常 → 判不兼容、且要求只报 * `engines.dsh`;顶层正常 + peer 非法 → 放行;而嵌在 `dsh` 下的这一层写 999 仍被判「无要求」 * (**真读它的是顶层**)。旁证:`dsh-plugin-manager` 从 registry manifest 同时取 `engines` 与 * `peerDependencies`,`dsh-plugin-market` 更是**只**用顶层 `version.engines.dsh` 造合成 * manifest(等于对不写顶层位的包一律判「兼容」)。下面那条「顶层 `engines.dsh` 残留」判据是 * 按「宿主不读它」的旧事实定的;要不要改成也写顶层位(并相应放宽该判据)是**政策决定**, * 尚未定——在那之前本脚本维持原判据(本仓因此始终由多段 peer 范围决定判定结果,见 * `docs/troubleshooting.md` 的兼容性一节)。 * - 被拒的插件启动时整行 `disabled`(bundle 整体跳过),安装时抛 * `incompatible-version`;豁免要写进 profile 自己的 `compatibility.json`。 * * 覆盖范围:`scripts/publish-targets.mjs` 里声明了 `dsh.bundle.patch` 的包 * (= 能被 `dsh plugin` 挂载的插件,含 packages/all 与根 bundle)+ `templates/hello` * 模板 + `EXTRA_PLUGIN_DIRS` 里那些**私有、不发布但宿主可加载**的插件(目录不存在就跳过)。 * 纯库包(如 `@hyzyn/dsh-kit`)不参与挂载,不加这些字段。 * * 用法: * node scripts/check-dsh-peers.mjs * node scripts/check-dsh-peers.mjs --app-boot /path/to/@deepseek-ai/dsh-app-boot * # 用 DSH 自己的判定器逐包 × 逐 cohort 核对(COHORTS 里每一档都要放行) * node scripts/check-dsh-peers.mjs --with-app-boot * # 同上,但**自动定位**那份判定器(`scripts/dsh-app-boot.mjs`:从 `which dsh` 的真身上溯, * # 兜底 `npm root -g`);定位不到就**报错退出**,不静默跳过。CI 用的是这条。 */ import { existsSync, readFileSync } from 'node:fs' import { dirname, join, resolve } from 'node:path' import { fileURLToPath, pathToFileURL } from 'node:url' import { APP_BOOT_ENV_VAR, describeAppBootSearch, findAppBoot } from './dsh-app-boot.mjs' import { targets } from './publish-targets.mjs' const root = resolve(dirname(fileURLToPath(import.meta.url)), '..') /** * 本仓统一适配的 DSH cohort 列表,**旧 → 新**;新增/退役 cohort 时改这一处 + 各包 peer 范围。 * * 每个可安装插件的 peer 必须**逐 cohort 覆盖**(`||` 连起来),宿主侧逐个判定,命中任一段 * 即放行。相邻 cohort 的插件 API 面都逐文件比对过: * - 0.2.0-rc.1 vs 0.1.7-rc.2:本仓 import 的 `dsh-tools` / `dsh-client-ui-slots` / * `dsh-client-locale` / `dsh-host-webserver` / `dsh-subprocess-local` / * `dsh-client-modules` / `dsh-package-manifest` 的 d.ts 逐字节相同; * - 0.2.0-rc.2 vs 0.2.0-rc.1:本仓 import 的宿主包**零代码变化**(cli + web-app 的全部 * 依赖包 166 个里只有 40 个有真实改动,与本仓相关的只有 `dsh-cordis-client-runner` * 的 slot 目录:97 → 98 条,**只增不减**,新增 `sidebar.right.tab.files.actions`, * 本仓注册的 4 个槽逐字节相同)。 * - 0.2.1-alpha.1 vs 0.2.0-rc.2:CLI 包 20 个文件里只有 package.json 变;本仓 import 的 * 宿主包**零代码变化**;`dsh-tools` 只改 `run_code` 的参数顺序与文案、并删掉 `./invariant` * 子路径导出(本仓零引用,`index.d.ts` 逐字节相同),`evaluatePluginCompatibility` 语义 * 等价;`dsh-cordis-client-runner` 的 slot 目录 98 → 100 条,**只增不减** * (新增 `plugins.add.actions` / `shell.bottom`)。 * - 0.2.1-alpha.2 vs 0.2.1-alpha.1:本仓 import 的宿主包里 `dsh-mcp-client`(MCP 托管行)/ * `dsh-settings` / `dsh-credentials` / `dsh-launch-environment` / `dsh-sandbox` / `dsh-scope` / * `dsh-user-approval` / `dsh-agent` / `dsh-client-locale` / `dsh-subprocess` * **除 `package.json` 外逐字节相同**;`dsh-tools` 里本仓用的 `defineTool` 导出行逐字节相同, * 改动只有 `ToolPresentationMode` 去掉 `'both'`(本仓零使用)与 PTC 内部桥 * `RunCodeBridgeOptions` 多一个必填的 `resolveWorkingDirectory`(本仓没有实现方); * 客户端槽面**只增不减**——新增 `conversation.chat.flow` / `conversation.chat.reasoning.body` * 两条,零删除零改名,本仓注册的槽(含给老宿主留的回退槽)一个没被拿掉; * `dsh-host-webserver` 与 `dsh-web-app` 的改动都在**宿主配置面**(监听地址只收环回或具体 * 网卡、新增 TLS 配置、`webRuntime` → `webStartup`),本仓对这些名字零引用。 * - 0.2.1-alpha.5 vs 0.2.1-alpha.2:**这一段不是"零差异"**(前三档那种「只改声明」的说法在这里不成立), * 它是本仓第一档明确带着**宿主行为变化**进来的 cohort——四条都已落进本仓的适配或记录: * · 判定器字段名 `PluginCompatibility.peers` → `requirements`(**alpha.4** 起): * 本脚本的诊断行曾据此读 `issue.peers` 而打出 `undefined`,已改成 `requirements ?? peers`; * · 顶层 `engines.dsh` 开始参与判定、且**短路 `peerDependencies`**(alpha.4 起): * 本仓因此仍只写嵌套展示位、判定仍靠多段 peer(见文件头「两个字段各管一件事」); * · 凭据 seam 层级翻面(**alpha.3** 起:`$DSH_HOME/.credentials.yaml > inherited env`): * `packages/env` 的启动期同步预注入随之对齐(存储值覆盖继承值); * · macOS 原生 picker 换成 JXA + `NSOpenPanel`、取消改由空 stdout 表达(alpha.5): * `docs/os-consent-plan.md` §2.1 事实 3 已按新口径改写并补了守卫。 * 插件 API 面(逐文件比对 18 个内联包的 130 个 d.ts,115 个逐字节相同):本仓 import 的 * `dsh-mcp-client` / `dsh-base` / `dsh-settings` / `dsh-client-locale` / `dsh-session-query` / * `dsh-sandbox` / `dsh-launch-environment` / `dsh-headless` / `dsh-user-approval` / * `dsh-subprocess` 的 **lib/ 逐字节相同**;`dsh-tools` 里本仓用的 `defineTool` 未变 * (`createRunCodeTool` 多一个必填的 `ctx` 但没从 `index.d.ts` 再导出,`renderToolsSdk` * 多一个可选参而本仓零引用);`dsh-app-boot` 删掉的 `HARNESS_SOURCE_SECTION` / * `addHarnessSourceSection`、`dsh-subprocess-local` 改签名的几个导出、 * `dsh-host-webserver` 的 `get port(): number | undefined`(本仓按结构用,只调 `register` / * `registerUpgrade`)、`dsh-credentials-local` 新增的 `ignoredKeys`——**全部零引用或零影响**。 * - 0.2.1-rc.1 vs 0.2.1-alpha.5:**纯显式声明,宿主对本仓零差异**——`0.2.1-rc.1` 本来就被 * `^0.2.1-alpha.5` 覆盖(同一个 `0.2.1` 版本元组,预发布之间不比大小),官方判定器实测放行。 * 逐文件比对(两档各装一份临时依赖树,逐 `lib/` 比对):本仓 import 的 14 个宿主包 * (`dsh-tools` / `dsh-mcp-client` / `dsh-credentials` / `dsh-credentials-local` / * `dsh-client-locale` / `dsh-base` / `dsh-web-app` / `dsh-user-approval` / `dsh-settings` / * `dsh-session-query` / `dsh-sandbox` / `dsh-launch-environment` / `dsh-headless` / `dsh`) * **`lib/` 全部逐字节相同**,判定器 `dsh-app-boot` 的 `lib/` 同样逐字节相同;整套 304 个包里 * 只有 5 个客户端产物不同,且**差异只有版本字符串**(`settings-general` / `sidebar` / * `settings-account` 里的 `0.2.1-alpha.5` → `0.2.1-rc.1`、`dsh-client-modules` 里的 commit 哈希、 * `dsh-web-frontend` 只有 `.map`)。所以这一档**不改任何插件运行代码**,也不需要新的行为适配。 * `cordis` 4.0.4 与 `schemastery` 3.18.4 在前三档同版本;第四、五档的宿主带的是 * 4.0.5-alpha.1 与 3.18.5-alpha.1;**第六档(alpha.5)带 ~4.0.5-alpha.2 / ~3.18.5-alpha.2, * 实际解析到 4.0.5 / 3.18.5**——与第四、五档解析到的同名版本,两份 tarball 除 `package.json` * 外**逐字节相同**,且 app-boot 用 `Symbol.for('schemastery')` 判定原生 schema(不是 `instanceof`), * 所以同一条范围同时声明多段是如实表述,而不是「先放宽再说」。**第七档(rc.1)仍是 * 4.0.5 / 3.18.5**(与第六档同一对,逐字节相同)。 * - 0.2.1-rc.2 vs 0.2.1-rc.1:**同样是纯显式声明**(`^0.2.1-rc.1` 按 semver 预发布规则 * 本来就含 rc.2,官方判定器实测放行;本机 `dsh --version` = `0.2.1-rc.2`)。 * 逐文件比对(rc.1 装临时树 + 本机 rc.2,304 个宿主包逐个 `lib/`):**有差异的只有 9 个 * 文件**,且逐个查清后**没有一处影响本仓**—— * · 纯版本串 4 处:`dsh-client-modules`(commit hash)、`client-ui-settings-account` / * `-general` / `client-ui-sidebar`(`0.2.1-rc.1` → `rc.2`); * · `dsh-web`:`searchProvider` / `fetchProvider` 改成 `.volatile()`(设置页可热改)。 * 本仓对这两个字段**零引用**(全仓 grep 无命中); * · `dsh-web-search-deepseek`:请求多带 `thinking: {type:'disabled'}`,报错文案改指向 * 新设置页路径。本仓零引用; * · `client-ui-settings-web-search`:新增「Use the new Web Search API」开关与文案。 * 本仓零引用(不碰 `dsh-client-store`); * · `dsh-cordis-client-runner`:slot 占用表新增官方 `deepseek-model-label` 一条 * (`shadows-shipped-ui`)。本仓注册的槽未被动。 * `cordis` / `schemastery` 仍解析到 **4.0.5 / 3.18.5**(与第六、七档同一对)。 * 包集合**只多两个**(`dsh-client-ui-deepseek-model-label-1017` / `dsh-web-deepseek-api`), * 均为宿主自带、不改变本仓 import 的任何包。 * 真机复核:`live-smoke` 18/18、`verify-issue-13-14`(logs-test)16/16,均无回归。 * - 0.2.1-rc.3 vs 0.2.1-rc.2(2026-10-24):**仍是纯显式声明**——`^0.2.1-rc.2` 展开是 * `>=0.2.1-rc.2 <0.3.0-0`,`semver.satisfies('0.2.1-rc.3', …, { includePrerelease: true })` * 为 **true**,官方判定器实测放行;**不改任何插件运行代码**。 * 逐文件比对(rc.2 用 `npm install` 装一份完整依赖树,306 个宿主包逐个 `lib/`): * **有差异的 17 个包,逐个查清后没有一处落在本仓引用面内**—— * · **本仓 import 的 22 个宿主包 `lib/` 全部逐字节相同**:`cordis` / `cordis-plugin-loader` / * `cosmokit` / `dsh-agent` / `dsh-app-boot`(判定器)/ `dsh-base` / `dsh-client-locale` / * `dsh-client-ui-theme` / `dsh-credentials` / `dsh-credentials-local` / `dsh-headless` / * `dsh-launch-environment` / `dsh-mcp-client` / `dsh-sandbox` / `dsh-scope` / * `dsh-session-query` / `dsh-settings` / `dsh-system-prompt` / `dsh-tools` / * `dsh-user-approval` / `dsh-web-app` / `schemastery`; * · 有差异的 17 个包全是**本仓零引用**的:`dsh-api-remotes`(`lib/client.js`)、 * `dsh-api-session-controller`(`.d.ts`)、`dsh-client-modules`(`lib/index.js`)、 * `dsh-client-product-analytics`(新增 `typert.*` 与 events `.d.ts`)、 * `dsh-client-ui-agent-preset` / `-conversation` / `-plugin-manager` / * `-settings-account` / `-settings-general` / `-sidebar` / `-sidebar-right` / `-workspace` * (客户端产物与槽 `.d.ts`)、`dsh-cordis-client-runner`(`client.js`)、 * `dsh-llm-deepseek` / `-account` / `-api-key`(LLM 目录:新增 * `listed-catalog` / `listing-cache` / `resolve-catalog` 等 `.d.ts`)、 * `dsh-tool-cordis`(`api-catalog.js`)。 * `cordis` / `schemastery` 仍是 **4.0.5 / 3.18.5**(与第六~八档同一对)。 * 包集合 306 → 306:只多一个 `dsh-client-ui-entry-customization`(宿主自带、本仓不引用), * 同时少掉的 `dsh` 是打包形状差异(rc.2 那份带 CLI 包、rc.3 的不带),不是删除。 * ⇒ 结论:这一档对宿主行为**零影响**,加档只是把「适配到哪一档就写到哪一档」落全。 * - 0.2.1-rc.4 vs 0.2.1-rc.3(2026-10-27):**仍是纯显式声明**——`^0.2.1-rc.3` 展开是 * `>=0.2.1-rc.3 <0.3.0-0`,官方判定器对运行时 `0.2.1-rc.4` 实测放行;**不改任何插件运行代码** * (`pnpm -r build` 之后 `lib/` 与 `client.js` 产物零 diff)。边界性证据:把**九段**范围直接喂给 * rc.4 的 `evaluatePluginCompatibility` 也判 compatible,**新段没有移动任何接受/拒绝边界**。 * 逐文件比对(rc.3 用 `npm install` 装一份完整依赖树 + 本机全局 rc.4,307 个宿主包逐个 `lib/`): * 本仓引用的 **27 个**宿主包(16 个代码引用 + 11 个仅文档提及)里 **26 个 `lib/` 逐字节相同** * (含判定器 `dsh-app-boot` 25/25、`dsh-tools` 21/21、`dsh-session` 21/21、`cordis` 19/19、 * `dsh-client-ui-theme` 18/18、`schemastery` 4/4);**唯一有改动的是 `dsh-mcp-client` * (`lib/index.js` +148 B)**,而改动落在本仓**零引用**的路径上——MCP **resources** 处理器新增 * `listable = getServerCapabilities()?.resources !== undefined` 守卫(`resources/list` / * `resources/templates/list` 在宿主未声明 resources 能力时直接返回空列表,不再打给 server)。 * 本仓用的是 MCP **tools**(`mcp__*`),对 `listResources` / `readResource` / `resources/*` * 全仓零引用,且该包导出面只有 `{Config, apply, createMcpToolDefinition, inject, name}`。 * 全量 307 个包里只有 **8 个**有真实内容差异,另外 7 个**全部本仓零引用**:`dsh-client-modules` * (commit 戳)、`dsh-client-ui-conversation`(View 页 tooltip 与一个宿主 locale 键)、 * `-settings-account` / `-settings-general` / `-sidebar`(版本串)、`dsh-llm-deepseek-account` * (`console.info` → `console.error`)、`dsh-mcp-resources`(仅 README);**没有任何文件被增删**。 * 客户端槽面**完全没动**:`dsh-cordis-client-runner` 的 slot 目录 **103 → 103**、顺序一致、 * 0 增 0 删 0 改名(本仓注册的槽全在,且 `lib/client.js` 逐字节相同)。 * `cordis` / `schemastery` 仍是 **4.0.5 / 3.18.5**,且两个包**整个目录(含 `package.json`) * 逐字节相同**——比前几档「除 `package.json` 外相同」更强。包集合 **307 → 307**:无增无删。 * ⇒ 结论:这一档对宿主行为**零影响**,加档只是把「适配到哪一档就写到哪一档」落全。 * 真机复核(本机 `test` profile / 端口 3082):宿主起在 `http://127.0.0.1:3082/`, * **10/10 个 `@hyzyn` 插件 mounted、0 行 `skipping profile bundle`、0 行 `disabling profile * plugin row`**;`pnpm live-smoke`(模板 profile 即本机 `test`)**18/18 PASS**; * 主题令牌快照重新生成后 403 个令牌与 rc.4 逐字一致(只有 `dshVersion` / `generatedAt` * 两个元数据位变化)。 * - **0.2.1-rc.5(2026-10-30 实测)**:同样是**纯显式声明**,对宿主行为零影响。 * `@deepseek-ai/dsh` 的依赖集合 **86 → 86,0 增 0 删**,只有版本串从 rc.4 抬到 rc.5; * 本仓唯一 import 的两个宿主运行时包 `@deepseek-ai/cordis`(仍 `~4.0.5`)与 * `@deepseek-ai/schemastery`(仍 `~3.18.5`)两档声明一致。本仓引用面逐文件比对: * `dsh-tools`(28 个文件)、`dsh-mcp-client`(11 个文件)**除 `package.json` 外逐字节相同**; * `dsh-cordis-client-runner/lib/client.js` 的槽定义 **104 → 104**、`name` 集合 **368 → 368** * 一致,归一化 `source` 行号后**唯一**的语义差异是 `ReasoningBodyOwnerProps` 多了 `rawText` * (reasoning 正文槽改成「原始 + 展示态」双字段)——**本仓不注册该槽**(本仓注册的是 * `plugins.bundle.config` / `plugins.row.config` / `sessions` / `settings.kit.item` / * `settings.plugin.item` / `sidebar.right.pane.tab`),零引用。 * 真机复核(本机 `test` profile / 端口 3082):宿主起在 `http://127.0.0.1:3082/`, * **10/10 个 `@hyzyn` 插件 mounted、0 行 `skipping profile bundle`、0 行 `disabling profile * plugin row`**;`pnpm live-smoke --bootstrap --strict`(模板 profile 即本机 `test`) * **18/18 PASS**;`dsh plugin add` 不需要任何 `compatibility.json` 豁免; * `node scripts/check-dsh-peers.mjs --app-boot` 用 rc.5 的官方判定器逐包核对通过; * 主题令牌快照与 rc.5 逐字一致(403 个令牌,只有 `dshVersion` 元数据位变化)。 * - **0.2.2-alpha.3(2026-11-04 实测)**:**不是零差异**——这一档是 0.2.2 线的首档, * 宿主新增 **25** 个包(`dsh-client-ui-user-questions` / `dsh-tool-executor-*` / * `dsh-official-bundles` 等)、移除 1 个客户端包(`dsh-client-ui-deepseek-model-label-1017`), * `@deepseek-ai/dsh` 的依赖集合 **86 → 90**;运行时依赖 `cordis` 4.0.5 → 4.0.6-alpha.2、 * `schemastery` 3.18.5 → 3.18.6-alpha.2(两个包的 `lib/` 仍**逐字节相同**,只有版本串变)。 * 本仓引用面 **26** 个宿主包里 **12 个 `lib/` 逐字节相同、14 个有改动**;逐条核对后只有两处 * 真实影响: * · `dsh-client-ui-theme` 移除了 `--dsw-specific-sidebar-nav-item-hover`(rss / search * 侧栏 hover 底色会整条失效、tty 退到自家 fallback)——三处已改用宿主侧栏自己在用的 * `--dsw-alias-interactive-bg-hover`(已适配的每一档都存在),主题令牌快照 403 → 414 * (移除 1、新增 12);`dsh-client-ui-theme` 的新增令牌本仓一个都没用。 * · `dsh-client-ui-user-questions` 的 CSS 自定义属性 `--question-copy` 命中了同意方案的 * 裸 `--question` 确认框判据——核对后确认它是**页内提问卡片**(`ctx.userQuestions` + * `dsh-tool-ask-user`),不是 OS 级确认框;判据收窄为 `--question(?![\w-])`, * `docs/os-consent-plan.md` 的复核基线同步到 alpha.3。 * 其余改动均为内部实现或类型面:`dsh-tools` 的 `lib/` 改 29 个文件,但 `defineTool` / * `DefineToolOptions` 的形状未变(只新增 `projectToolSchema` 导出);`dsh-mcp-client` / * `dsh-subprocess(-local)` / `dsh-credentials(-local)` / `dsh-launch-environment` / * `dsh-web-app` / `dsh-session-query` / `dsh-base` / `dsh-scope` 的 `lib/` 逐字节相同; * `dsh-settings.configure()` 的所有者形态由 fiber 改成 `owner.ctx.fiber`,本仓传 fiber 时 * `fiber.ctx.fiber === fiber`,实测注册正常。客户端槽定义**只增不减**(字符串面 95 → 99, * 新增 `conversation.chat.text-interaction` / `conversation.chat.user.before` / * `settings.section.icon` / `sidebar.session.row.trailing`;本仓注册的 6 个槽全在)。 * 真机:官方判定器 **12 档 × 14 条目全放行**;`live-smoke --bootstrap --strict --render * --chrome-arg --no-sandbox` **23/23 PASS**(18 条 HTTP + 5 条真浏览器);全量 profile 装上 * 11 个可安装包后 **11/11 全部加载(10 行 `mounted` + `dsh-tty` 的 * `agent tools registered`)、0 行 `skipping profile bundle`、 * 0 行 `disabling profile plugin row`**;`verify-client-ui --mode boot` **6/6 PASS**(11 个 * `@hyzyn` bundle 真加载、0 未捕获异常、0 失败子请求)。⇒ 这一档**不能**照抄「纯显式声明 / * 零差异」的写法:它改了客户端令牌,必须带着上述适配一起声明。 * - **0.2.2-rc.1(第十三档基线,2026-11-05 实测)**:**纯显式声明、零差异**——`0.2.2-rc.1` * 本来就被 `^0.2.2-alpha.3` 覆盖(同一个 `0.2.2` 版本元组,预发布之间不比大小), * 官方判定器实测放行。逐文件比对(alpha.3 装一份完整依赖树 + 本机 rc.1,各 331 / 330 个 * 宿主包逐个 `lib/`):**有差异的只有 4 个包,且差异只有版本串**——`dsh-client-modules` * (commit 哈希)与 `dsh-client-ui-settings-account` / `-settings-general` / `-sidebar` * (`0.2.2-alpha.3` → `0.2.2-rc.1`,各 2 行);其余 **327 个逐字节相同**,其中含本仓 import * 的全部 26 个宿主包、`dsh` CLI 自己的 `lib/`(20 个文件)与 `dsh-cordis-client-runner` * 的槽目录(`lib/client.js` 435100 字节逐字节相同,字符串面 99 → 99、0 增 0 删 0 改名)。 * `@deepseek-ai/dsh` 的依赖集合 **90 → 90**(0 增 0 删),宿主包集合不变;运行时 * `cordis` 4.0.6-alpha.2 → **4.1.0**、`schemastery` 3.18.6-alpha.2 → **3.19.0**、 * `cosmokit` 1.8.7-alpha.2 → 1.9.0、`cordis-plugin-loader` 1.0.7-alpha.2 → 1.1.0 —— * 这四个的 `lib/` 也**逐字节相同**(预发布转正,只有版本串变),`engines.node` * 仍是 `>=24.9.0`。主题令牌 **414 → 414**、`--check` 与宿主逐字一致(快照只差 * `dshVersion` / `generatedAt` 两个元数据位)。⇒ 这一档对宿主行为零影响,加档只是把 * 「适配到哪一档就写到哪一档」落全(`RELEASING.md` 的加档规则:零差异不是显式化的定义、 * 只是常态)。实测证据:官方判定器(自报运行时 `0.2.2-rc.1`)**十三档 × 14 条目全放行**; * 隔离 `DSH_HOME` 起真宿主后 **11/11 个可安装包全部加载、0 行 `skipping profile bundle`、 * 0 行 `disabling profile plugin row`**;发版前的全面测试里 `live-smoke --render` **23/23**、 * `verify-client-ui` **17/1/0**、`verify-rss-opml` **9/9**、`verify-codegraph-host-contract` * **42/42**、`verify-issue-13-14` **15/2/0**(逐条落点见 `packages/codegraph/ROADMAP.md` 的 * 第十三档条目)。 * ⚠️ **2026-11-05 全面测试补正**:「零差异」只对宿主**包**(`lib/` 逐字节)成立,对宿主的 * **使用面**不成立——0.2.2 线起 `dsh-tools` 删掉了 `ToolRuntime` 的 `mode` 配置项(要原生 * 执行得单独 mount `@deepseek-ai/dsh-tool-executor-native`)、`tools.execute()` 改成**批** API、 * `AgentRegistry` 新增 `static inject = ["sessionProjections"]`。五个进程内真机脚本与两处 * 浏览器夹具因此红的红、瞎的瞎,已在本轮一并修好;判据与清单见 ROADMAP 第十三档条目。 */ const DSH_COHORTS = ['0.1.7-rc.2', '0.2.0-rc.1', '0.2.0-rc.2', '0.2.1-alpha.1', '0.2.1-alpha.2', '0.2.1-alpha.5', '0.2.1-rc.1', '0.2.1-rc.2', '0.2.1-rc.3', '0.2.1-rc.4', '0.2.1-rc.5', '0.2.2-alpha.3', '0.2.2-rc.1'] /** 成熟下限:最低支持的 cohort,市场展示位与它一致,也是本仓历史文档里的基线。 */ const DSH_COHORT = DSH_COHORTS[0] /** * 唯一支持的 peer 写法:逐 cohort 的 `^` 用 `||` 连起来(预发布参与匹配,见文件头)。 * 单 cohort 时就是 `^0.1.7-rc.2` 这种老写法,多 cohort 时是 * `^0.1.7-rc.2 || ^0.2.0-rc.1 || ^0.2.0-rc.2 || ^0.2.1-alpha.1 || ^0.2.1-alpha.2 || ^0.2.1-alpha.5 || ^0.2.1-rc.1`。 */ const EXPECTED_RANGE = DSH_COHORTS.map((cohort) => `^${cohort}`).join(' || ') /** * 市场展示位的下限写法。市场解析器只认 `>=X.Y.Z[-预发布]` 一种形式,所以这里不能 * 用 `^`,也不能写 `||`(多段式区间会判成「无法验证」而 fail-closed,见 * scripts/windows/README.md);下限必须是 DSH_COHORT——也就是 peer 覆盖的最低档。 */ const EXPECTED_ENGINES = `>=${DSH_COHORT}` /** 从 `^0.1.7-rc.2` / `>=0.1.7-rc.2` / `~0.1.7-rc.2` 取出版本号;其它形式返回 undefined。 */ function floorOf(range) { const match = /^(?:\^|~|>=)\s*(\S+)$/.exec(range.trim()) return match === null ? undefined : match[1] } /** * `--app-boot ` 指向一份 `@deepseek-ai/dsh-app-boot` 包目录时做真判定器核对。 * * `--with-app-boot`(2026-10-04 加,CI 用这条)**自动定位**那份判定器:这条路径此前 * 存在但**从没进 CI**——每次都要人手拼路径当参数传,于是退化成「靠人记得跑」。 * 自动定位的实现与理由见 `scripts/dsh-app-boot.mjs`;定位不到时**报错退出**, * 不静默跳过(跳过它等于把 CI 的结论降级成「我自己写的规则说没问题」)。 */ const argv = process.argv.slice(2) const appBootIndex = argv.indexOf('--app-boot') const appBootExplicit = appBootIndex === -1 ? undefined : argv[appBootIndex + 1] const wantAppBoot = argv.includes('--with-app-boot') || appBootExplicit !== undefined /** * 定位失败时统一报错退出。 * * `source` 说明是**哪一路**没成:`explicit` / `env` 是「你点了名但那儿没有」—— * 这种必须报错(否则会静默换成另一份判定器,把「我验的是哪个判定器」变成运气); * `undefined` 是自动搜索都没命中——报出试过的路径。 */ function failAppBoot(found) { const byUser = found.source === 'explicit' || found.source === 'env' const what = found.source === 'explicit' ? '--app-boot 指定的目录' : `$${APP_BOOT_ENV_VAR} 指定的目录` console.error( (byUser ? `✘ ${what}不是一份可用的 @deepseek-ai/dsh-app-boot(要同时有 package.json 与 lib/index.js)。\n` : '✘ 没找到 @deepseek-ai/dsh-app-boot(DSH 的官方兼容性判定器)。\n') + '试过这些路径:\n' + describeAppBootSearch(found.tried) + '\n修法:装一份 dsh(`npm install -g @deepseek-ai/dsh@`),' + '或用 DSH_APP_BOOT_DIR= / --app-boot 显式指定。\n' + '**不要改成静默跳过**:这条路径是「用宿主真正会跑的那段代码复核声明」,' + '跳过它等于把结论降级成「我自己写的规则说没问题」。', ) process.exit(1) } let appBootDir = appBootExplicit if (wantAppBoot) { const found = appBootExplicit === undefined ? findAppBoot() : findAppBoot({ explicit: appBootExplicit }) if (found.dir === null) failAppBoot(found) appBootDir = found.dir // 报出**用的是哪一份**:自动定位之后,日志里必须能看出验的是哪个判定器, // 否则「定位到了另一份」也没人会发现(来源见 dsh-app-boot.mjs 的三级优先级)。 console.log(`[check-dsh-peers] 判定器来源:${found.source} → ${found.dir}`) } /** 新插件模板也必须合规,否则每个新包都带着同一个缺口出厂。 */ const TEMPLATE = 'templates/hello/package.json' function readJson(rel) { return JSON.parse(readFileSync(join(root, rel), 'utf8')) } const problems = [] const ranges = new Map() /** 校验一个「可挂载插件」包的 peer 声明。 */ function checkPlugin(rel, name) { let pkg try { pkg = readJson(rel) } catch (err) { problems.push({ name, rel, kind: '读取失败', detail: err.message }) return undefined } if (pkg.dsh?.bundle?.patch === undefined) return undefined if (pkg.dsh?.manifestVersion !== 1) { problems.push({ name, rel, kind: '缺少 dsh.manifestVersion', detail: '公开 manifest 格式版本应显式声明为 1', }) } // 市场展示位:必须存在,且下限与 peer 一致(两条一起升 cohort,别各自漂移)。 const enginesDsh = pkg.dsh?.engines?.dsh if (enginesDsh === undefined) { problems.push({ name, rel, kind: '缺少 dsh.engines.dsh', detail: `市场侧兼容性展示读它;应为 ${JSON.stringify(EXPECTED_ENGINES)}`, }) } else if (floorOf(enginesDsh) !== DSH_COHORT) { problems.push({ name, rel, kind: 'dsh.engines.dsh 与 cohort 不一致', detail: `${JSON.stringify(enginesDsh)} —— 应为 ${JSON.stringify(EXPECTED_ENGINES)}`, }) } // 顶层 engines.dsh 是插件管理器兼容读取的旧备用位,统一写进 dsh.engines.dsh。 if (pkg.engines?.dsh !== undefined) { problems.push({ name, rel, kind: '顶层 engines.dsh 残留', detail: `${JSON.stringify(pkg.engines.dsh)} —— 统一写进 dsh.engines.dsh,删掉顶层这个备用位`, }) } const peers = pkg.peerDependencies ?? {} const dshPeers = Object.entries(peers).filter( ([key]) => key === '@deepseek-ai/dsh' || key.startsWith('@deepseek-ai/dsh-'), ) if (!Object.hasOwn(peers, '@deepseek-ai/dsh')) { problems.push({ name, rel, kind: '缺少 @deepseek-ai/dsh peer', detail: 'DSH 只在声明了 peer 时才施加版本约束;不声明等于把兼容性留成「未知」', }) } for (const [key, range] of dshPeers) { if (range.trim() !== EXPECTED_RANGE) { problems.push({ name, rel, kind: 'peer 范围与本仓 cohort 不一致', detail: `${key}: ${JSON.stringify(range)} —— 应为 ${JSON.stringify(EXPECTED_RANGE)}(逐 cohort 的 ^ 范围用 || 连起来)`, }) } else { ranges.set(name, EXPECTED_RANGE) } } return EXPECTED_RANGE } for (const [dir, name] of targets) { const rel = dir === '.' ? 'package.json' : `${dir}/package.json` checkPlugin(rel, name) } /** * 私有、不发布、但宿主**可加载**的插件:它们不在 `publish-targets.mjs` 里(那是发布清单, * 塞进去会去发布一个私有包),所以单独列在这里。目录不存在时跳过——`devfront` 目前只存在于 * 开发套件分支上,main 上还没有它;一旦它随合并进来,peer 忘了跟齐这里就会红。 */ const EXTRA_PLUGIN_DIRS = ['packages/devfront'] for (const dir of EXTRA_PLUGIN_DIRS) { if (!existsSync(join(root, dir, 'package.json'))) continue checkPlugin(`${dir}/package.json`, `${dir}(私有、不发布,但宿主可加载)`) } // 模板不在发布清单里,单独查 checkPlugin(TEMPLATE, '@hyzyn/dsh-hello(模板)') if (problems.length > 0) { console.error(`✘ dsh peer 兼容性检查未通过:${String(problems.length)} 处问题\n`) for (const p of problems) { console.error(` ${p.name} (${p.rel})`) console.error(` ${p.kind}:${p.detail}`) } console.error( '\n修复:在可安装插件的 package.json 里写\n' + ' "dsh": {\n' + ` "manifestVersion": 1,\n` + ' "bundle": { "patch": "./cordis.patch.yml" },\n' + ` "engines": { "dsh": "${EXPECTED_ENGINES}" }\n` + ' },\n' + ' "peerDependencies": {\n' + ` "@deepseek-ai/dsh": "${EXPECTED_RANGE}"\n` + ' },\n' + ' "peerDependenciesMeta": {\n' + ' "@deepseek-ai/dsh": { "optional": true }\n' + ' }\n' + '(optional 只影响 pnpm 的 unmet-peer 噪音;DSH 判定器不看它,照旧强制。)\n' + 'dsh.engines.dsh 是市场展示位,rc.1 宿主不读它;它写最低档,必须与 peer 覆盖的最低档一致。\n' + 'peer 范围逐 cohort 写全(^ 用 || 连起来);新增/退役 cohort 时同步改全部包与脚本里的 DSH_COHORTS。\n', ) process.exit(1) } console.log(`✔ dsh peer 兼容性检查通过:${String(ranges.size)} 个可安装插件的 DSH peer 都钉在 ${EXPECTED_RANGE}`) if (appBootDir !== undefined) { const entry = join(resolve(appBootDir), 'lib', 'index.js') const { evaluatePluginCompatibility, getDshRuntimeVersion } = await import(pathToFileURL(entry).href) /* * 报出**用的是哪一份判定器**:`--with-app-boot` 自动定位之后,日志里必须能看出 * 「验的是哪个版本的官方判定器」——否则路径拼错了也不会有人发现(它会静默用另一份)。 * `getDshRuntimeVersion()` 是判定器自己报的运行时版本,比读 package.json 更权威。 */ let evaluatorVersion = '(未知)' try { evaluatorVersion = getDshRuntimeVersion() } catch { /* 读不到就保持「未知」——不影响下面的核对 */ } console.log(`[check-dsh-peers] 用官方判定器核对:${appBootDir}(自报运行时 ${evaluatorVersion})`) // 逐个 cohort 都用 DSH 自己的判定器复核一遍:范围里写了几段,就得有几段真的放行。 const incompatible = [] for (const cohort of DSH_COHORTS) { for (const [dir, name] of targets) { const rel = dir === '.' ? 'package.json' : `${dir}/package.json` const pkg = readJson(rel) if (pkg.dsh?.bundle?.patch === undefined) continue const issue = evaluatePluginCompatibility(pkg, {}, cohort) if (issue !== undefined) incompatible.push({ name, cohort, issue }) } const tpl = readJson(TEMPLATE) const tplIssue = evaluatePluginCompatibility(tpl, {}, cohort) if (tplIssue !== undefined) incompatible.push({ name: '@hyzyn/dsh-hello(模板)', cohort, issue: tplIssue }) } if (incompatible.length > 0) { console.error(`✘ DSH 判定器核对未通过:${String(incompatible.length)} 处(包 × cohort)incompatible`) for (const { name, cohort, issue } of incompatible) { /* * 字段名跨档不同,**别退回单一写法**(2026-10-17 复核发现):判定器返回的「未满足项」 * 在 `0.1.7-rc.2` ~ `0.2.1-alpha.3` 叫 `peers`,`0.2.1-alpha.4` 起改叫 `requirements` * (同轮 `dsh.engines.dsh` 也开始参与判定,见本文件头部的两字段说明)。 * * 判定本身不看这个字段(走的是上面的 `issue !== undefined`),但这条诊断**只在出问题时打印** * ——退回 `issue.peers` 就会在本仓最需要它的时候打出一句 `undefined`。而它此前一直没暴露, * 是因为 CI 的 pin 停在 `0.2.1-alpha.2`(那一档返回 `peers`);**pin 一抬到 alpha.4+ 就会静默降级**。 */ const unmet = issue.requirements ?? issue.peers console.error(` ${name} @ ${cohort}: requirements=${JSON.stringify(unmet)}`) } process.exit(1) } console.log(`✔ DSH 官方判定器核对通过:全部可安装包在 ${DSH_COHORTS.join(' / ')} 上都没有 incompatible peer`) }