# DSH 插件开发技术笔记 本插件开发过程中实测验证的 DSH(DeepSeek Harness)桌面版插件机制硬知识。 全部结论来自 0.2.1-rc 系列源码取证与真机验证(2026-10);机制层结论对 0.1.0-rc.6 至 `<0.3.0` 适配窗口内预计稳定,敏感于版本的部分已标注探针/兜底策略。 ## 1. 双半边架构 DSH 插件 = 宿主半边 + 浏览器半边,同一个 npm 包内: | | 宿主半边 `lib/index.js` | 浏览器半边 `lib/client.js` | |---|---|---| | 运行环境 | Node(Electron 主进程侧 cordis) | 渲染进程(`window.__ModuleLoader__` 免构建 CJS) | | 入口 | `export function apply(ctx)` + `export const name` | 工厂 `(require)=>{…}`,`exports.apply` | | 服务授权 | `export const inject = ["webServer"]` | `exports.inject = ["slots","locale"]` | | 包声明 | — | `package.json` 的 `dsh.client`(见 §3) | 浏览器半边由 `dsh-client-modules` 经 boot manifest import,官方 `ui-*` 包同形; 宿主半边经 `cordis.patch.yml` 插入加载树: ```yaml - insert: - id: font-color name: 'dsh-plugin-font-color' ``` ## 2. cordis 服务授权的两层机制(最大的坑) **坑**:不声明服务授权时,`ctx.locale` / `ctx.slots` / `ctx.webServer` 全部抛 `cannot get property "…" without inject`——浏览器半边表现为**设置行静默不渲染** (无异常徽标,仅 DevTools 可见)。 两层缺一不可,且各有各的写法: 1. **模块级导出**(cordis 服务授权,声明"我要用哪些服务"): - 浏览器半边:`exports.inject = ["slots", "locale"]` - 宿主半边:`export const inject = ["webServer"]` 2. **包级声明**(`package.json` 的 `dsh.client.inject`,值是**包名**,声明"哪些包 要先于本包装载并提供服务"): ```json "dsh": { "client": { "platform": "web", "immediately": true, "inject": ["@deepseek-ai/dsh-client-runtime", "@deepseek-ai/dsh-client-locale"] } } ``` **反向坑**:`cordis.patch.yml` 的加载树条目**不得**写 `inject` 字段——空数组 `inject: []` 会把 client 半边的服务访问永久锁死(症状同上,极难排查)。 模块级 `export const inject` 是唯一正确的授权层。 ## 3. 设置页插槽(slots) ```js ctx.slots.inject("settings.general.item", () => ctx.slots.register( { name: "settings.general.item", id: "font-color", order: 11.5, locale: NS, inject: () => ({ hooks: {} }) }, Component )); ``` - 列表按 `(priority ?? 0) || (order ?? 0)` 排序;官方「字号大小」order 11、 「工作步骤展示」order 12 → `order: 11.5` 正好插中间。 - 插槽渲染的容器带 `data-slot="settings.general.item"` 属性(本插件的正文作用域 正是借用了对话视图容器的同类属性,见 §5)。 - 文案走 `ctx.locale.register(NS, { zh, en })`,组件内 `props.t(key)`, 需自带回退字典。 ## 4. CSS token 体系与作用域杠杆 官方 `base.css`(`@deepseek-ai/dsh-client-ui-theme` 的 STYLES 产物)把 `--dsw-alias-*` token 定义在 `body{}` 与 `body[data-ds-dark-theme]{}` 上—— **元素自身定义遮蔽一切祖先继承**(`html:root{}` 杠杆因此必然失效),且 dark 块 特异性 0-1-1 会打平后插入顺序。 可靠杠杆全部用**子树内重定义**(自定义属性按"最近祖先定义胜出"解析, 精确局部、不碰全局): | 作用域 | 选择器 | 覆盖的 token | |---|---|---| | 对话区正文 | `[data-slot="conversation.view"], [data-chat-flow-kind]` | `--dsw-alias-label-primary` | | 代码块/行内代码/工具终端块 | `.shiki` + `[data-terminal]` + `code` | `--shiki-foreground`、`color` | | 侧栏终端 | `.xterm` | `color`(DOM 渲染,前景继承容器) | | 工作步骤条目 | `[data-chat-flow-kind="turn-process"], [data-chat-group-key]` | `--dsw-alias-label-secondary/tertiary/caption` | 补充实测: - 宿主 shiki 配置为 `css-variables` 主题(`variablePrefix: "--shiki-"`),且全应用 **无任何 `--shiki-*` 定义点** → 代码块前景完全由该变量驱动,覆盖即全量生效。 - 步骤条目的浅灰白文字吃 secondary/tertiary/caption(非 primary),与正文作用域天然正交。 - `conversation.view` 是对话视图容器的官方插槽属性(`dsh-conversation-rewind` 即注入 该插槽);`[data-chat-flow-kind]` 兼作插槽名漂移时的兜底。 - 零哈希类名:以上全部是稳定属性/类钩子,宿主版本演进风险低。 ## 5. 桌面版 localStorage 必丢与宿主文件持久化 **根因(实测定案)**:DSH 桌面版每次启动把 web server 绑到**系统随机分配端口** (profile 层强制 `port: 0`,启动器传 `--port 0`)→ GUI 的 origin = scheme+host+port 每启必变 → localStorage 按 origin 隔离,**数据仍在磁盘 leveldb 里,但落在上一次启动那个端口的 origin 下,新启动读到空库**。 症状:插件设置"重启即丢"。任何以 localStorage 为权威存储的 DSH 桌面插件都有此问题。 **修复模式(与 dsh-dream-skin 生产实现同构)**——三层存储: ``` 内存态 ──写──> localStorage(同源首屏缓存,仅加速) │ └──防抖──> 宿主状态文件 $DSH_HOME/dsh-font-color.json(权威,跨启动稳定) ▲ └── 回环 API POST /font-color/api ``` 宿主半边(`inject = ["webServer"]` 后注册): ```js ctx.effect(() => ctx.webServer.register({ kind: "prefix", path: "/font-color/api", handler: async (req, res) => { /* 原生 Node req/res */ } }), "…"); ``` - 协议:`{"method":"get"}` → `{ok:true,value:{…}}`; `{"method":"set","patch":{key: value|null}}` → **合并补丁**(设值/删键/其余不动, 多窗口安全)。 - 信任栅栏:Host 必须回环(127.0.0.1/localhost/::1,防 DNS-rebinding)+ `sec-fetch-site ≠ cross-site` + 仅 `application/json` + 请求体 ≤4KB。 - 状态文件**原子写**(tmp + rename),损坏按空对象处理,schema 版本键防老文件误读。 浏览器半边时序: 1. 启动先读 localStorage 立即恢复(首屏不等网络); 2. 异步拉宿主权威值(4s 超时,失败保持 localStorage-only 降级): - 采纳权威值,但**跳过本会话已改的键**(用户手速快于网络时不得回卷); - 文件里没有的键同步清掉(文件为唯一权威,自愈历史缓存); - 文件为空而 localStorage 有值(升级遗留)→ 一次性迁移上推; 3. UI 挂载可能早于宿主拉取返回 → 注册状态泵(模块级 updater + `useEffect`)同步晚到的采纳。 ## 6. 启动安全防线 - 宿主半边 `apply` 全量 try/catch,任何异常不逃出(加载器虽有隔离,但插件自身 也要保证"半边挂了另一边能活"); - 浏览器半边所有外部依赖访问前 `typeof` 探测(`fetch`/`requestIdleCallback`/ `document` 等),缺失即降级; - 样式注入幂等(固定 `style[data-plugin-css]` 标签复用,重复 apply 不堆积); - 颜色值出包前一律过 `normalizeHex`(仅 `#rrggbb` 进 CSS 文本与状态文件)。 ## 7. 版本兼容策略 - `package.json` peerDependencies 全 optional + 窗口声明(`>=0.1.0-rc.6 <0.3.0-0`); - 真正的门是运行时能力探针(一次性 idle `querySelectorAll` 命中计数,结果挂 `window.__DFC_STATUS__` 并 `console.info`,排障零依赖 DevTools); - 选择器全部为稳定属性钩子,宿主改名时探针计数归零即可定位,替换兜底即可修复。 ## 8. 安装形态 DSH 插件管理器(设置 → 插件)支持三种安装源:npm 包名(含中国大陆镜像)、 Git 仓库 URL、本地路径。手工安装等价操作:把包拷贝为 `$DSH_HOME/profiles//node_modules/<包名>/`,并在该 profile 的 `package.json` 里把包名加进 `dsh.profile.bundles` 数组,重启生效。