# 侧边栏插件操作规范(给其他插件的 Agent) > 版本基线:DSH Desktop 2.0.17 / 宿主 `@deepseek-ai/*` 0.2.0-rc.2 > 用途:任何往 DSH 右侧栏挂 tab 的插件都按这份规范做,避免重复踩坑(每条都有实测根因) ## 一、硬规则(违反 = 静默失效,不报错) ### 1. `Config` 必须用宿主的 schemastery ⭐ 头号坑 ```ts import z from '@deepseek-ai/schemastery' // ✅ 宿主 fork,含 volatile 支持 import z from 'schemastery' // ❌ 公版 3.18.0,无 volatile ``` - 后果:`settings.update` **写进 `cordis.patch.yml` 成功**,但宿主 loader 静默 no-op —— 不生效、不报错、不重启、值冻结。 - 机制:宿主 `cordis.resolveConfig` 走 `runtime.Config['~standard'].validate()`,用的是**插件自己那份 schemastery** 的解析器;无 volatile 分支 ⇒ 没有 volatile 引用 ⇒ `cordis-plugin-loader._commitVolatile()` 首行 `if (!volatileEntries(fiber.config).length) return true` ⇒ 视作已提交。 - `package.json`:`peerDependencies` 加 `"@deepseek-ai/schemastery": "^3.18.1"`(宿主 mount 时提供),`devDependencies` 钉 `3.18.4`。 ### 2. 用户偏好字段必须标 volatile,且并入 `Config` ```ts tabsEnabled: z.dict(z.boolean()).default({}).extra('volatile', true) ``` - 0.2.0+ 移除了 `settings.register()`,**loader 只注册插件导出的 `Config`**;独立 `PrefsSchema` 是死代码。 - `settings.update` **只放行 volatile 字段**(非 volatile 路径抛 `Config field "" is not volatile`)。 - **部署字段**(limit / shell 之类)保持**非 volatile**——它们不该被设置页改写。 ### 3. settings ns = **profile 条目 id**,不是包名 `cordis.patch.yml` 里 `- id: ` 的那个 id。用包名会在 `settings.describe()` 里找不到自己(报 `No configurable plugin entry "<包名>"`)。 ### 4. tab id / map key **不要用冒号** `apesTasks` ✅ / `apes:tasks` ❌。冒号 key 在设置投影链路上出现过丢失。 (i18n key 不受影响,本来就是 camelCase。) ### 5. volatile 字段**不能直读** 修复后 `config.tabsEnabled` 是 `createVolatile()` 产出的**不透明引用**: ```js Object.freeze({ get: () => current, [Symbol.for('cosmokit.volatile.write')]: v => { current = v } }) ``` ⇒ 没有数据属性,`config.tabsEnabled.git` 是 `undefined`(不是旧值),`JSON.stringify` 出 `{}`。 **必须走 `settings.get()` / `settings.describe()`**(宿主对 ref 调 `plainConfig()` 解包)。 ## 二、0.2.0-rc.2 消失的宿主 API 与官方替代 | 老调用(会静默 no-op) | 官方替代(宿主自己的 UI 就是这么调的) | |---|---| | `sessions.openSubagent(address)` | `sessions.open(childSessionId)` | | `sessions.setSubagentCatalogOpen(id, open)` | `sessions.refreshProjections(id)` | | `sessions.refreshSubagents(id)` | `sessions.refreshProjections(id)` | 判定方法(别猜):在 app 内全量 grep 宿主包—— ```bash A="/Applications/DSH Desktop.app/Contents/Resources/app/node_modules/@deepseek-ai" grep -rn "" "$A"/*/lib/*.js | head # 0 命中 = 宿主没实现;再看官方自己的插件怎么调的(如下): sed -n '945,1005p' "$A/dsh-client-ui-subagent/lib/client.js" ``` `dsh-client-ui-subagent` 是**宿主自己的子代理 UI**,是找「官方正确姿势」的最权威样板。 ## 三、注册/生命周期 - 服务注册必须等服务、不能等槽声明:`ctx.inject(['sidebarRightTabs'], cb)` 驱动类型注册(槽声明早于服务 provide,按槽注册会静默什么都不注册且不重试);槽只用来挂 tab 体。 - 原生 tab 体宿主 `.paneBody` 是**有确定高度的块级滚动容器**,native 适配层要给每个 tab 体包 `height:100%` 的列 flex 宿主。 - 跨会话操作原生栏用 `openTabIn/openResourceIn/closeIn`(不在 `ISidebarRight` 接口里,需结构化探测)。 ## 四、自检清单(发版前逐条过) 1. `pnpm typecheck && pnpm lint && pnpm build && pnpm test` 全绿(构建 ≠ typecheck,tsdown 跳过 tsc 严格性)。 2. `grep '^import z' lib/index.js` → 必须是 `@deepseek-ai/schemastery`。 3. `grep -c 'schemastery' package.json` → `dependencies` 里**没有**公版 schemastery。 4. 设置开关实测:点两次,值**不再弹回**,且 `cordis.patch.yml` 里的新值等于 UI 显示值。 5. 改完配置**不需要重启 DSH**(volatile 就地生效);若需要重启才生效 = 第 1 条没做对。 6. 产物 rsync 到 `~/.dsh/profiles//node_modules//lib/` 后**逐文件 md5 比对**(tsdown 输出非确定性,字节数相同≠内容相同)。