# DSH 客户端插件契约(`__ModuleLoader__` / `dsh.client` / 槽位) > M2 Task 6 交付物。全部结论来自**本机内核源码**与**已在跑的第三方插件**,不是推测;每条都标了出处路径与行号,可复核。 > 目标内核版本:`0.2.0-rc.2`。内核源码解包位置:`<内核解包目录>\dsh\node_modules\@deepseek-ai\`。 --- ## 0. 结论摘要 客户端插件就是一个**手写的 CJS 风格 bundle**,外壳固定为: ```js window.__ModuleLoader__.load({ id: '<包名>', // 必须严格等于 boot entry id = package.json 的 name factory: (require) => { const React = require('react'); // 从平台种子表取,不是 npm 安装 return { inject: ['slots'], // Cordis 服务名(硬门) apply(ctx) { // 在这里注册槽位 }, }; }, }); ``` **不需要打包器**:`require` 只认平台种子表 + 已注册的 boot 条目,手写 `React.createElement` 即可(官方模板就是这么写的)。 **不需要装 `@deepseek-ai/*` 依赖**:那些 bare specifier 由宿主虚拟模块表解析,本机磁盘上根本没有 `@deepseek-ai/dsh-tools` 之类的包。 --- ## 1. 权威证据来源 | 证据 | 路径 | 说明 | | --- | --- | --- | | 官方模板(最小可用) | `@deepseek-ai/dsh-agent-preset/skills/cordis-plugin-development/templates/decoration/{client.js,index.js,package.json}` | 内核自带,22 行 client.js 就是完整契约 | | Loader 实现(带完整注释) | `@deepseek-ai/dsh-client-modules/lib/index.js`(981 行,未压缩) | 浏览器侧契约面 + 宿主侧服务 | | Loader 浏览器运行时 | `@deepseek-ai/dsh-client-modules/lib/client.js`(884 行,未压缩) | `window.__ModuleLoader__` 的真实实现 | | 平台种子表 | `@deepseek-ai/dsh-web-frontend/dist/assets/index-*.js` 中 `function rM()` | 9 个种子词的唯一权威清单 | | 官方槽位目录(90 条) | `@deepseek-ai/dsh-cordis-client-runner/lib/client.js` 约 5030 行起 | 每条槽位的 kind/scope/registerOptions/ownerProps/occupants/可运行示例 | | 真实第三方插件 | `C:\Users\x\.dsh\profiles\desktop\node_modules\dsh-context\lib\client.js` | 已在本机跑通的完整范例(侧边栏入口 + 标签页) | | 内核自带 UI 范例(**未压缩、可直接读**) | `@deepseek-ai/dsh-client-ui-plugin-manager/lib/client.js`(3786 行) | 「插件」页的 `sidebar.panellist` + `main` 注册全文 | | 侧边栏消费端 | `@deepseek-ai/dsh-client-ui-sidebar/lib/client.js:452-527` | 面板列表怎么读注册项、点击后干什么 | 槽位目录(90 条)可以从内核自己的 `dsh-cordis-client-runner/lib/client.js` 里再抽一遍, 抽取脚本没有随仓库发布(它读的是解包后的内核源码);本文第 7 节按用途列了常用的那些槽位。 > 为什么不把抽出来的 225 KB 目录 JSON 放进仓库:那是对内核 TypeScript 声明文件的**逐字拷贝**, > 属于第三方代码,放进本仓库(MIT)会造成许可混淆。要看全量目录,照上表的出处自己抽一遍即可。 --- ## 2. 加载模型:懒注册 + 物化 `dsh-client-modules/lib/index.js:16-31` 原文: - 执行一个插件 bundle **只是注册它的 factory**(`window.__ModuleLoader__.load({id, factory})`)。 - 模块体的所有副作用——**包括 CSS 注入**——都写在 factory 闭包里,在**物化(materialization)**时才跑,不是在 script 执行时。 - 物化 = `factory(require)` → exports,结果缓存在 `loadCache`,只跑一次。 - 一个 factory 去 require 另一个"已注册但未物化"的模块会**递归物化**,所以加载顺序不需要外部排序。 **实际影响**:把 CSS 字符串写在 factory 内部注入是安全的;写在 `load()` 外面(顶层)会在 bundle 一被 `