# DSH 插件接入机制说明书 > **定位**:回答"DeepSeek Harness 接入一个插件,到底是什么逻辑"。 > **读者**:给 DSH 写插件的作者、排查 DSH 页面故障的维护者、需要读懂本仓库 DSH 分支的宿主 Agent。 > **实证基线**:DSH `0.1.6-alpha.1`(web profile),2026-09-18。所有结论取自 DSH runtime 源码、官方产物样例与浏览器/服务端对照实验;未实证的推断均已显式标注。 > **关联文档**:[dsh-compatibility.md](dsh-compatibility.md)(本仓库的兼容修复档案)、[deploy/deepseek-harness.md](deploy/deepseek-harness.md)(部署步骤)、[code-architecture.md](code-architecture.md)(本仓库分路径加载架构)。 --- ## 0. 一页速查 **DSH 没有"一种插件"。一个插件包最多有四张脸,各自独立接入:** | # | 面 | 接入方式 | 宿主侧落点 | 失败后果 | |---|---|---|---|---| | ① | **host 半**(Node 代码) | `package.json` 的 `dsh.bundle.patch` → 自己的 `cordis.patch.yml` | Loader entry → fiber | 该 entry 不激活(日志警告);required id 失败则整进程退出 | | ② | **client 半**(浏览器代码) | `package.json` 的 `dsh.client` + `exports["./client"]` | 产物拼进 `/plugins/??…` combo | **整条 combo 解析失败 → 同 combo 全部插件一起崩** | | ③ | **MCP 桥**(外部进程) | patch 里 `insert` 一行 `@deepseek-ai/dsh-mcp-client` 配置 | stdio 子进程 + 工具表 | 该行不激活;`failOnStartupError: true` 时 boot 直接失败 | | ④ | **纯声明**(无代码) | profile 的 `cordis.patch.yml` 或 `--patch` overlay | 直接改 entry 树 | patch 未命中会 warn;文件格式错则 boot 失败 | **三条最容易踩的硬规则**(各对应一次真实事故): 1. `insert:` 的值必须是**挂载行数组** `[{id, name, config?}]`——写成元数据映射会让整个 profile 崩(`patch.insert?.forEach is not a function`)。 2. client 产物必须是 **Lazy-CJS 工厂**,**禁止任何顶层 `import`/`export`**——一处违规连坐同 combo 全部插件(实测 56 个)。 3. `--patch` **只接受单文件**且可重复传;逗号分隔多文件会被当成一个路径直接 ENOENT。 **诊断第一原则**:host 半与 client 半的失败**互不通知**。服务端日志正常、MCP 已启动,页面照样可能整页崩。**先看浏览器 console,再谈服务端。** --- ## 1. host 半:四层叠加的 Loader 树 ### 1.1 声明 ```jsonc // 插件包 package.json { "name": "my-dsh-plugin", "dsh": { "manifestVersion": 1, // 可选,清单格式版本(当前为 1) "bundle": { "patch": "./cordis.patch.yml" } // ★ 声明"我是 profile 的一层" }, "engines": { "dsh": ">=0.1.6" } // 可选,作者声明的兼容范围 } ``` `dsh.bundle.patch` 是**唯一**让包进入 profile 层栈的钥匙。装了包但没这个字段,只会得到一条"installed as a plain dependency, not a profile layer"的 warning。 ### 1.2 安装:`dsh plugin add` 是"pnpm 转发 + 事后对账" ``` dsh plugin --profile web add ├─ 首次使用:initProfile() 生成 profiles/web/{package.json, cordis.yml, cordis.patch.yml} ├─ 把相对路径 spec 按调用目录锚定(`add .` 不会自链 profile) ├─ spawnSync("pnpm", [...]),cwd = profile 目录 └─ reconcilePlugins():跑完后按"已装依赖里谁声明了 dsh.bundle"重写 dsh.profile.bundles ``` 对账是**按已安装状态**而非依赖差异——所以 `update` 能让一个在新版本里才获得 `dsh.bundle` 的包自动入栈;反过来,依赖被移除或不再声明 bundle 的包会**自动出栈**。 ### 1.3 合成:四层叠加 ``` $DSH_HOME/profiles//package.json → dsh.profile.bundles: [bundleA, bundleB, …] │ ├─(1) 逐 bundle:resolveBundleDir → readFile(pkg.dsh.bundle.patch) → 解析 patch list │ 解析锚点顺序:安装锚点(dsh 包自身位置)→ profile 目录 ├─(2) profile 自己的 cordis.patch.yml (用户 tweak 层) ├─(3) $DSH_HOME/cordis.patch.yml (home 级;优先级高于 profile 级) └─(4) CLI --patch (可重复;overlay 层,最后应用) │ ▼ applyEntryPatches:逐层合并 ▼ mountRootInclude(cordis:include) → Loader 建 entry → 每 entry 一个 fiber ▼ auditStartupEntries:清点激活结果 ``` **补丁语义**(`applyEntryPatches`,全树只此一处算法): | 写法 | 语义 | |---|---| | `- id: ` + 其它字段 | **整段替换**目标行的该字段(`config` 是整体替换,不是深度合并——要保留的字段必须重述) | | `- id: ` + `insert: [...]` | 向该 group 追加子行 | | `- insert: [...]`(无 id) | 向顶层追加行 | | `name` 与目标不符 | 跳过并 warn(防误伤) | | `id` 未命中 | warn,继续(不是错误) | **注意**:`insert` 行的 `name` 若以 `./`、`../` 或绝对路径书写,会在解析时**转成 file URL 并按 patch 文件所在目录锚定**;包名保持字面量。 ### 1.4 装配与失败语义 Loader 并发挂载所有 entry。启动审计(`auditStartupEntries`)在树稳定后清点: | 失败模式 | 可选 entry | **required entry** | 说明 | |---|---|---|---| | 模块 import 失败 / 模块求值抛错 | warn,继续 | 停止启动 | `plugin tree failed to load` | | 配置 schema 校验失败 | warn,继续 | 停止启动 | 新 entry 保持不激活 | | `apply()` 同步/异步抛错 | warn,继续 | 停止启动 | 异步版在 settle 后报 | | 注入的 service 不可用 | warn,等待依赖 | 停止启动 | 补上 provider 可激活 | | `apply()` 之外产生 unhandled rejection | **致命**,无论 entry id | **致命** | 停进程并退出非零 | **required 名单**(全局,与具体 profile 无关):`agent-loop`、`webserver`、`modules`、`connection`、`headless-runner`、`acp`、`sdk-jsonrpc-server`。 **缺失或显式 disabled 的 required id 不影响启动**——只有"enabled 但激活失败"才算。 > **可操作判据**:如果你看到"**N entries did not activate**"且 N 是个大数,几乎不可能是"host 半某一处写错"——host 半的失败是**逐 entry 隔离**的。大数集体失败指向共享的公共依赖(见 §2 的 combo 连坐,或模块解析层断裂)。 --- ## 2. client 半:完全独立的第二套契约 ### 2.1 声明 ```jsonc { "dsh": { "client": { "platform": "web", // 当前只有 web "inject": ["@deepseek-ai/dsh-client-ui-conversation", …], // 依赖到达顺序(这些行的 factory 先注册) "external": ["some-non-baseline-pkg"] // 非基线模块请求(精确名) } }, "exports": { "./client": { "default": "./lib/client.js" } } } ``` ### 2.2 合成与加载 ``` host 半扫描 enabled Loader entries → 合成 boot graph → 注入 window.__DSH_BOOT__ + 把各 client 产物按 combo 分组 → /plugins/??,…&rev= 浏览器:按 classic