| name | dsh-plugin-authoring |
| description | 写或改 DeepSeek Harness(dsh)插件本体时使用——新建插件、加 Config、提供或消费 Cordis 服务、挂事件监听、处理生命周期与资源清理、给 ctx 加类型、决定拆几个包、给服务/类取名。涉及 apply/inject/ctx.effect/ctx.plugin/Service 基类/waterfall 等概念时都适用。不覆盖工具定义(见 dsh-tool-authoring)和打包安装(见 dsh-bundle-and-profile)。 |
写 dsh 插件
dsh 没有特权内核。插件挂在别的插件旁边,注册全是可撤销的副作用。写插件的全部
自由度就在:往 ctx 上注册什么、声明依赖什么、监听哪个事件。
先运行 python3 tools/check-harness-drift.py。语义以目标版本 checkout 的
vendor/deepseek-harness/docs/architecture.zh.md、docs/user/develop/ 和
docs/cordis-api/ 为准;签名以正在修改的插件实际安装的
node_modules/@deepseek-ai/*/**/*.d.ts 为准。上游可运行形态见
docs/cookbook/extension-cookbook.zh.md 和对应 packages/ 实现;本地已有插件只能作为
同版本验证过的参考,不能代替上游契约。
最小插件
import type { Context } from '@deepseek-ai/cordis'
export const name = 'my-plugin'
export const inject = ['tools']
export function apply(ctx: Context): void {
}
三种形态,按需选:
- 函数形态(上面这种)—— 默认选它。
- 对象形态 ——
export default { name, inject, apply }。
- 类形态 ——
extends Service,用于对外提供服务。
⚠️ 用命名导出 apply / inject / name 的函数形态时不要同时给 default
export:某些 Loader 版本会因此丢掉命名空间。以目标 preview 的 Loader 测试确认。
Config:没有硬编码可调参数
判据:能不能在 cordis.yml 里改这个值而不改代码? 能改的就必须是 Config
字段。
import Schema from '@deepseek-ai/schemastery'
export interface Config {
greeting: string
maxRetries: number
}
export const Config: Schema<Config> = Schema.object({
greeting: Schema.string().default('Hello'),
maxRetries: Schema.number().default(3),
})
export function apply(ctx: Context, config: Config): void { }
- 默认值写在 schema 里,不写在代码里。
- 不要导出普通对象当
Config —— 它不满足 Cordis 要的 Standard Schema 接口。
- 约束要自包含,让非法配置在加载时就大声失败,而不是运行到一半才炸。
- 改 config 会触发热替换:旧实例卸载 → 新实例加载。注册都是 effect,所以不会
残留。
生命周期与清理
Fiber 状态机:PENDING → LOADING → ACTIVE(apply 抛异常则 FAILED),
ACTIVE → UNLOADING → DISPOSED。
自动回收的:ctx.on()、ctx.tools.register()、ctx.llm.registerAdapter()、
ctx.effect() 返回的 disposer、ctx.timer.*。
ctx.effect(() => {
const conn = createConnection()
return () => conn.close()
}, 'my connection')
顺序敏感的清理必须放进同一个 effect 的 disposer 里串行 await。 多个
disposer 之间是逆序开始但并发执行的,不保证逐个完成——跨 effect 依赖顺序会随机
出错。
必需依赖消失时(提供方被卸载),依赖它的插件会自动 dispose,服务回来后自动重新
加载。所以别缓存服务引用到模块作用域。
服务
消费
export const inject = ['tools']
const metrics = ctx.get('metrics')
metrics?.record('loaded', 1)
服务可能晚于 apply 才挂上。要等某个服务出现再做事,用 ctx.inject() 开一个
子 fiber:
ctx.inject(['tools'], (inner) => {
registerMyTools(inner)
})
提供
import { Service, type Context } from '@deepseek-ai/cordis'
declare module '@deepseek-ai/cordis' {
interface Context {
metrics: MetricsService
}
}
export default class MetricsService extends Service {
static inject = ['llm']
constructor(ctx: Context) {
super(ctx, 'metrics')
}
record(event: string, value: number): void { }
}
挂载用 ctx.plugin(MetricsService)。fiber 不会在 ctx.plugin() 调用内启动,
所以紧接着同步读 ctx.metrics 会拿到 undefined——要用 ctx.inject([...]) 等。
⚠️ host 半和 client 半不能复用同一个 Context key。 两侧运行时虽然独立,
TypeScript 声明合并会同时看到两个类型。
事件
分发模式是事件公开约定的一部分,只能用对应方法分发:
| 模式 | await? | 顺序 | 有返回值? | 用途 |
|---|
emit | 否 | 注册序 | 否 | 广播通知 |
bail | 否 | 注册序 | 是 | 第一个非 null/false/undefined 的返回值胜出 |
parallel | 是 | 并发 | 否 | 异步扇出 |
serial | 是 | 注册序 | 是 | 顺序执行,首个有效返回值终止 |
waterfall | 是 | 注册序 | 是 | 环绕中间件,监听器必须调 next() |
waterfall 是环绕式的:监听器签名 (...args, next),调 next() 才委托给下游,
下游返回值可被本层包装后再往外返。不调 next() 就是短路——这是拦截/网关的
故意设计,不是 bug。只做标注和观察的监听器必须委托。只有必须跑在普通注册之前时
才用 prepend: true。
类型化事件同样靠声明合并:
declare module '@deepseek-ai/cordis' {
interface Events {
'my-plugin/ready': (payload: { id: string }) => void
'my-plugin/transform': (input: string, next: () => Promise<string>) => Promise<string>
}
}
别把持久会话事件和同名 Cordis 事件搞混。 turn/*、step/*、tool/call、
tool/result、compaction/* 是持久化的会话事件类型,不是 Cordis 事件;要
观察它们就监听 session/event 再判 event.type。
新行为该挂在哪
先查 vendor/deepseek-harness/docs/architecture.zh.md 的「新行为的归属位置」表。高频几条:
| 目标 | 机制 |
|---|
| 加模型可调用能力 | ctx.tools.register(),schema 自动进提示词组装 |
| 加模型提供方 | ctx.llm 上 registerAdapter |
| 拦截请求/工具/轮次 | 对应的 agent/* / tools/* waterfall |
| 加模型可见上下文 | agent.inject(),落到下一次获准的请求 |
| 加人类命令(无需模型轮次) | ctx.commands |
| 加后台工作 | ctx.jobs |
| 加持久会话状态 | 扩 SessionEventMap,从日志渲染和回放 |
| 加 UI / 编辑器集成 | 驱动 ctx.agents,从 session/event 渲染 |
拆几个包
不要预防性拆分。 单一用途插件就一个包。只有当一项能力需要可替换的提供
方、且三种角色需要独立演进时才拆:
- Service Definition —— 定义 Cordis 服务 + Request/Result 类型(拥有这些类型)
- Service Provider —— 一个实现
- Consumer —— 通常是面向模型的工具
Provider 和 Consumer 都只依赖 Definition,彼此不依赖。三者合起来才是一个
seam,单一角色不是 seam。模板:dsh-shell / dsh-bash-local / dsh-tool-bash。
命名
名字必须描述当前稳定职责,不能用首个实现、可能的未来、或 Cordis 基类命名。
选角色词前查 vendor/deepseek-harness/docs/cookbook/adding-a-package.zh.md 的完整
表,常用几个:
Registry —— 拥有一组动态具名注册 + 查询/优先级/生命周期
Runtime —— 跑实时工作,拥有分派、取消、provider 协调
Store —— 拥有一组数据,提供 CRUD/snapshot/subscription(类里有个 map 不等于 store)
Policy —— 决定允许/选择/限制什么(不执行该决定)
Executor —— 执行一个明确请求或已解析 spec
Provider / Backend —— 能力定义的一个实现
Service —— 上面都不诚实描述时才用(不要因为继承了 Cordis Service 就叫它)
单数 key 给 engine/runtime/policy/store/config;复数 key 给 registry 或有多个具名
成员的服务。类的角色和 key 的单复数必须一致。
自查