跳到主要内容
路径文档

插件解剖

一句话版:一个 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-facedsh.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 Servicestatic inject + @Inject提供服务能力(见 写服务)
  • 类插件的依赖从 static inject 读取并经 Inject.resolve 归一化,与 applyinject 不是同一来源
  • @Inject 装饰器在类属性/方法上声明;也能在函数上给 injectconfig

五、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)"

下一步