插件解剖
一句话版:一个 DSH 插件 = cordis 插件(
apply(ctx, config)或 class)+ package.json 的dsh字段清单:它声明能力(inject)、贡献什么、挂载在哪一层。DSH 几乎一切能力都是插件叠出来的。
读这一篇,你就懂了"插件到底是什么、有哪些形态、patch 层怎么工作、依赖怎么解析"。动手写见 dev 系列。
一、插件在 DSH 里意味着什么
DSH 的哲学是"一切皆插件":工具、服务、事件监听、UI 面板,都是插件。一个插件本质是两件事:
① cordis 插件(function apply(ctx, config) 或 class extends Service)
② 清单声明(package.json 的 dsh 字段)——告诉 loader 它怎么挂载、依赖什么
框架(@deepseek-ai/cordis,vendor 化的组合框架)负责:依赖注入、插件装载、生命周期、作用域。
二、插件形态
| 形态 | 声明 | 作用 |
|---|---|---|
| Bundle 插件 | dsh.bundle.patch | 自带 patch 层,作为一层加入 profile |
| Client / dual-face | dsh.client + exports["./client"] | 同时有 node 半部 + 浏览器半部 |
| Profile 依赖 | dsh.profile.bundles | 声明某 profile 依赖哪些 bundle |
不存在
dsh.plugin.json文件:真实清单只有 package.json 的dsh字段。Repository 插件的.dsh-plugin目录格式属于外部 plugin-registry 机制,非 DSH 核心,见 插件。
Client / dual-face 插件
一个包可同时提供 host 半部 + client 半部(浏览器):
- host 半部(node):扫描 host Loader 条目组成
window.__DSH_BOOT__启动图,伺服/plugins/<id>/client.js - client 半部(浏览器):懒 CJS 模块表:副作用在首 require 时才执行
- 声明:
exports["./client"]+dsh.client
详见 Web UI 架构。
三、最小插件
import { Context } from '@deepseek-ai/cordis'
export const name = 'my-plugin'
export const inject = ['tools'] // 声明依赖:未声明访问会被拒绝
export function apply(ctx: Context, config: {greeting: string}) {
const dispose = ctx.tools.register({
name: 'say_hello',
parameters: { text: { type: 'string' } },
output: {
schema: { type: 'string' },
render(_args, value) {
return [{ type: 'text', text: typeof value === 'string' ? value : 'ok' }]
},
},
async execute({ text }) {
return { content: `${config.greeting} ${text}` }
},
})
// ctx.plugin(...) / 返回 cleanup ...
}
四、插件两种形态:function vs class
| 形态 | 写法 | 依赖声明 | 典型 |
|---|---|---|---|
| 函数式 | export function apply(ctx, config) | export const inject = [...] | 挂工具/监听事件,无状态 |
| 类式 | class extends Service | static inject + @Inject | 提供服务能力(见 写服务) |
- 类插件的依赖从
static inject读取并经Inject.resolve归一化,与apply的inject不是同一来源 @Inject装饰器在类属性/方法上声明;也能在函数上给inject加config
五、inject 与 capability 安全
- 插件加载时静态声明要注入的服务(数组或对象)
- 未声明的服务访问,被上下文代理(Proxy)拒绝:capability-based
- 依赖以静态声明,loader 在加载时就能审查批准
- 注入是"先声明再使用";动态但已注入的服务可用
ctx.inject(['x'], cb)异步取
这和安全边界相关但不等于权限:inject 管"你够不够得着这个服务",权限/沙箱管"你做的事允不允许"。见 沙箱与安全。
六、patch 层语义(写对很关键)
cordis.patch.yml 是顶层 YAML 数组,一条目只有两种操作:
insert(缩进子列表):按 id 在目标 group 追加 1+ 行- 按 id 覆盖整行:不带
insert,id定位到已有行,可改name/config/disabled/inject/group/isolate/intercept
三条易踩约束:
| 约束 | 说明 |
|---|---|
config 是整行替换,非深合并 | patch 给多少字段,插件拿到的就是多少(缺的用 schema 默认) |
name 不符 → 静默跳过 | patch 带 name 且与目标行不符,loader 只 warn、条失效 |
没有 replace/ignore 动词 | insert 出的行可被后续 patch 按 id 配置/禁用 |
完整应用顺序
profile.bundles(bundle patch 按序)
→ win32 shell 层(仅 Windows)
→ profile 自身 cordis.patch.yml
→ $DSH_HOME/cordis.patch.yml(机器级,越权于每 profile)
→ --patch overlays
→ agent-presets roots
→ telemetry 开关
根 cordis.yml 每次启动被重写为空 []:真实配置树 100% 由 patch 层组成。
七、挂载与 reconcile:dsh plugin
dsh plugin --profile <name> <args> 是很薄的 pnpm 转发器:
首次使用 → initProfile(目录 + package.json + 空 patch + pnpm-workspace.yaml)
→ 在 profile 目录跑 pnpm <args>
→ reconcile dsh.profile.bundles 层列表 vs 已安装状态
(依赖解析到声明 dsh.bundle 的包 → 加入层栈;移除/无 bundle 的依赖 → 离开)
按已安装状态而非依赖 diff 来 reconcile:所以 update 能激活"在新版本里才多了 dsh.bundle 声明"的包。
八、依赖解析(rescope)
- 框架被 vendor 化并只发布为
@deepseek-ai/*scoped 包 - 插件应
import ... from '@deepseek-ai/cordis'、z from '@deepseek-ai/schemastery' - 新 loader 的装载闭包只含
@deepseek-ai/*包;用了裸名cordis/schemastery会解析失败(官方 issue #554) - workaround:在插件目录自身安装缺失裸名依赖
- profile 目录:
bundle 名双锚点解析(先安装目录、后 profile)+$DSH_HOME/profiles/node_modules扁平闭包 fallback,让 out-of-tree 插件解析到同一个 cordis 实例
九、验证
# 组合树里看插件挂载与层来源注释
dsh web --dump-config | grep -B1 -A2 "my-plugin"
# 看某个 profile 的 bundle 层列表
node -e "console.log(require('./profiles/web/package.json').dsh?.profile?.bundles)"