dsh Cordis 与插件开发 · 交互手册
0 / 11 章 ← 首页
DeepSeek Harness · 一切皆插件
封面:Cordis 之心与插件树插画

Cordis:dsh 的心脏,与你的第一个插件

DeepSeek Harness(命令行里叫 dsh)是 DeepSeek 开源的 agent 框架,它的口号是「一切皆插件」——工具、模型、会话、沙箱、界面,全部是插件。而管理这成百上千个插件装、卸、互相协作的底座,叫 Cordis。这本手册讲清楚两件事:Cordis 到底是什么,以及怎么亲手写一个 dsh 插件。

8 个交互组件 · 全部可点击 代码示例取自 dsh 官方教程 · 可直接运行
01

Cordis 是什么 —— 一块带总闸的智能插线板

先把术语说清楚,再打比方:Cordis 是一个插件框架。它自己不实现任何业务功能,只回答三件事——插件怎么装进来、插件怎么干净地卸出去、插件之间怎么找到对方。名字来自拉丁语 cor(心)的所有格,意思是「心脏」:它是 Koishi 的心脏,如今也是 dsh 的心脏。

打个比方(比喻跟在定义后面):Cordis 像一块带总闸的智能插线板。每个插件 = 一个电器:插上就通电(加载);拔掉时总闸保证不留「暗电流」——定时器、监听器、连接全部自动回收;电器之间互不认识,全都只从插线板取电(服务)。dsh 的每一个工具、模型适配器、UI 面板,都是插在这块板上的电器。

为什么插件系统需要一个框架

假设你写了一个聊天机器人,功能变多后开始拆模块。但「模块化」只解决代码怎么组织,解决不了另外四件事——点击下面的卡片,看 Cordis 分别怎么回答:

问题 1安装:新功能怎么被「接」进系统?点击翻面
Cordis 的回答一个文件、一个 apply 函数,在配置里登记一行。不需要改框架源码,不需要写启动代码——框架来调你,而不是你调框架。
问题 2配置:同一功能在不同环境用不同配置,写在哪里?点击翻面
Cordis 的回答写在 cordis.yml 的条目里。插件用 Schema 声明配置结构,框架在加载前校验——配置非法就拒绝启动,绝不「半启动」。
问题 3卸载:功能下线时,定时器、监听器、连接由谁清理?点击翻面
Cordis 的回答框架清理,不用你记得。注册时留下「怎么撤销」的说明书(disposer),无论什么原因卸载,Cordis 都替你回滚。这是框架级保证,不是自觉。(第 5 章)
问题 4协作:功能 A 依赖功能 B,但 B 可能还没启动、之后还可能被替换?点击翻面
Cordis 的回答A 只声明「我需要名为 B 的服务」,然后等着;B 就绪 A 自动启动,B 消失 A 自动卸载,B 恢复 A 自动重载。A 不 import B 的代码。(第 6 章)

这四个问题里,「卸载」和「协作」最难——它们决定了插件能不能安全地动态装卸。Cordis 的特别之处:把这两件事从「插件作者的自觉」上升为「框架级保证」。

它不是什么:不是传统 DI 容器 传统依赖注入(Spring 那类)回答「谁创建谁」,隐含假设是「一旦绑定,服务就一直在」。Cordis 回答一个更难的问题:「谁在什么时候活着」——服务可以随时出现、随时消失、被替换,依赖它的一切自动跟着调整。论文给它的定位是 meta-framework(元框架):规定副作用如何组合、依赖如何解析,但不预设任何业务领域。QQ 机器人能用,Agent 运行时也能用。

出身:从 QQ 机器人到 DeepSeek 的地基

时间事件
2020.01Shigma 发布跨平台聊天机器人框架 Koishi,Cordis 是它抽出来的插件内核
2022.04cordis 包独立登上 npm,成为通用插件框架
2024.11Cordis 4 开始预发布:彻底重构,引入基于 fiber 的生命周期体系(第 4 章)
2026DeepSeek Harness 以 Cordis 为地基发布(vendor 进仓库);北大 & DeepSeek-AI 发 88 页论文把它形式化
Cordis 是什么:插件框架三件事图解
本节内容的图形化小结:Cordis 这个插件框架只管三件事——装进来、卸干净、找得到;dsh 的一切能力都是插在它上面的插件
Cordis 生态规模数字卡片
生态规模一瞥(2026-08 实测):Koishi 社区插件 4100+、cordis 年下载 51 万次、awesome-dsh 收录 174 个、dsh 内置 160 个
02

第一个插件:一个文件 + 一行配置

写 Cordis 插件最爽的一点:完全没有框架启动代码。一个导出 apply 函数的文件,加一行配置,就能跑。

hello.ts
// 一个最小的 Cordis / dsh 插件
import type { Context } from '@deepseek-ai/cordis'

export const name = 'hello'   // 可选:诊断信息里的显示名

export function apply(ctx: Context) {
  console.log('hello from my first plugin')
}
cordis.yml —— 应用本身就是一个配置文件
- name: './hello.ts'

跑起来(dsh 自带的小启动器:创建根 Context → 挂载 Loader 插件 → 读取 cordis.yml),输出 hello from my first plugin。注意分工:插件只描述贡献,应用长什么样由配置决定——这叫「配置即组合」。

交互 · 插件是怎么被启动的6 步流程
cordis.yml声明要挂载的条目
Loader 插件读取并解析配置
解析模块找到 ./hello.ts
ctx.plugin()挂为子插件,建 fiber
apply(ctx)框架调用你的函数
贡献生效hello from my first plugin
点击「下一步」逐步走一遍。任何配置项列表里的条目都是并发启动的:谁先跑由依赖关系(inject)决定,与书写顺序无关。

插件的三种形态

shapes.ts
import { Service, type Context } from '@deepseek-ai/cordis'

// 1. 函数形态(最常用,没有对外服务时一直用它)
export function apply(ctx: Context) {}

// 2. 对象形态:带名字的对象 + apply 方法
export const objectPlugin = {
  name: 'object-plugin',
  apply(ctx: Context) {},
}

// 3. 类形态:要对外提供服务时用(第 6 章)
export class MyService extends Service {
  constructor(ctx: Context) {
    super(ctx, 'myService')
  }
}
形态什么时候用dsh 里的例子
函数只做贡献(注册工具 / 监听事件),不对外暴露能力绝大多数 dsh-tool-*
对象同函数,但想要一个显式名字小型内部插件
类(Service)要把一项能力挂到 ctx.<key> 上供别人取用ctx.tools、ctx.llm 的提供方
新手第一坑:加了插件没反应,先查拼写 如果配置里的路径或包名拼错了(模块解析失败),Cordis 只会通过 logger 服务报一条错误,进程不会崩——启动早期这条消息甚至可能还没接到控制台就丢了。而如果 apply 抛异常,进程会明确报错退出。「解析不到」静默、「执行出错」炸裂,两种行为要分清。
03

ctx 与插件树:一切操作的入口

在 Cordis 里你几乎只跟一个东西打交道:ctx。大白话讲,ctx 是插件进厂时领到的工作台——上面有事件插孔、副作用登记处、子插件挂钩。插件能做的一切,都是往这张工作台上操作。

ctx 上最常用的五个操作
export function apply(ctx: Context) {
  ctx.on('some/event', (payload) => {})  // 监听事件(卸载时自动移除)
  ctx.effect(() => { ... })              // 登记副作用(卸载时自动回滚,第 5 章)
  ctx.plugin(SomePlugin)                 // 挂载子插件(随父插件卸载)
  ctx.get('someService')                // 读服务:没有则 undefined(可选依赖探测)
  ctx.provide('someValue', 42)          // 提供服务
}

关键在 ctx.plugin(child):它不是简单的「注册」,而是派生出一个子上下文。于是插件不是平铺的列表,而是一棵树——子上下文能看到父上下文的一切(继承),但卸载是按层级的:父插件卸载,所有子插件递归卸载;子插件卸载,不影响兄弟和父级。这棵树是 Cordis 一切生命周期语义的骨架。

插件树结构图解
插件树图解:灰色虚线的 dsh-jobs 表示已随父插件卸载的子树——下面这个模拟器就是这棵树的可交互版,点它
交互 · 插件树装卸模拟器点击任意节点 = 卸载 / 重装它
运行中 已卸载(含整个子树) 子上下文继承父级可见性;卸载按层级递归
点击 dsh-agent 试试:它的子插件 dsh-jobs 会被递归卸载,而兄弟节点 dsh-llm、my-tool 完全不受影响。
再点一次已卸载的节点 = 重新挂载。真实的 dsh 进程里有 160+ 个这样的节点,语义完全相同。
04

fiber:插件的一生

先大白话定义:fiber(纤维)是框架给每个已挂载插件实例开的「档案卡 + 生命体征监护仪」。你声明了插件、它加载到哪一步、为什么没动静、卸载到什么程度——全记录在 fiber 上。Cordis 4 为每个插件实例维护一个 fiber,状态机如下:

状态大白话技术含义
PENDING菜没上齐,干等已声明,但 inject 的服务尚未就绪(第 6 章)
LOADING正在开工apply 正在执行
ACTIVE正常营业apply 完成,贡献已生效
FAILED当场翻车apply 抛异常,或配置校验失败
UNLOADING → DISPOSED退租中 → 已退干净disposer 执行中 → 一切已拆除
fiber 状态机图解
fiber 状态机图解:无论因何种原因卸载(改配置 / 热重载 / 显式 dispose / 依赖服务消失),清理都是自动的
交互 · fiber 状态机动画选一个剧本,点播放
PENDING等依赖就绪
LOADINGapply 执行中
ACTIVE运行中
UNLOADING清理中
DISPOSED已拆除
FAILED报错,不静默跳过
一个插件「加载了却没反应」,多半就是蹲在 PENDING——dsh 的 cordis_inspect 工具巡检的就是每个 fiber 的状态。
剧本 B 是排障最高频场景:inject 指定的服务无人提供 → 永远 PENDING、不报错、不崩溃(PENDING 的 fiber 不会让进程保持存活)。
05

effect:可逆副作用 ——「改配置不用重启」的秘密

先大白话定义:effect = 注册时同时交一张「撤销说明书」。effect 的主体在插件加载时执行;它返回的那个函数(叫 disposer,清理函数)在插件卸载时由框架自动执行。你永远不需要自己调用清理函数——不管插件因为什么原因被卸载。

比喻:像酒店入住时前台就登记好「退房清单」——不管你什么时候走、为什么走(退房、续住换房、被消防疏散),清单上的事项都会被照单执行。

effect-demo.ts
ctx.effect(() => {
  const conn = createConnection()   // 加载时:建立连接
  return () => conn.close()          // disposer:卸载时如何撤销
})

更关键的事实:Cordis 的内置 API 本身就是 effect——ctx.on() 注册的监听器随插件卸载、ctx.plugin() 的子插件递归卸载、服务注册随提供方消失。论文的实现章节有一个结论:Cordis 中所有对上下文的变更,最终都归结为 ctx.effect 这一个原语;提供服务、挂载插件、注册监听器,全是它的特例。所以「任何通过上下文进行的操作都自动可追踪、可恢复」不是口号,是结构事实。

插件作者的唯一铁律 凡是自己创建、Cordis 不管的资源(setInterval、WebSocket、文件 watcher……),都包进 ctx.effect() 并返回 disposer。做不到的代价就是泄漏:插件卸了,定时器还在跑。

撤销说明按逆序执行(先注册的后撤销,天然 LIFO——后进先出,像摞盘子:最后摞上的最先拿走)。多个 effect 各自独立时,可以只撤掉其中一个而不惊动别人——这就是「单个插件可热卸载,邻居不受影响」的底层依据。

effect 注册与 LIFO 回滚图解
effect 图解:加载时执行主体、卸载时自动执行 disposer;多个 effect 按注册的逆序回滚(LIFO)
交互 · effect 回滚动画注册三个资源,再卸载插件看逆序回滚
栈内 effect0
已回滚0
[log] 插件 ACTIVE,等待注册 effect…
注意回滚顺序:注册顺序 定时器→连接→监听,回滚一定是 监听→连接→定时器(LIFO)。
所以「改配置不用重启」是理所当然的 改配置 → 旧插件卸载(所有 effect 自动回滚)→ 新插件加载。进程从头到尾不重启。因为副作用可逆,插件可以被安全地卸了再装——热重载(HMR)、故障自动恢复、测试隔离,全部建立在这一行原语上。
06

服务与 inject:会自动重连的依赖

先大白话定义:Service(服务)就是把一项能力挂到 ctx 的某个名字上(比如 ctx.tools、ctx.llm),别的插件按名字取用,而不是 import 它的代码。inject 就是声明「我需要哪几个名字」——声明之后,插件会一直保持 PENDING,直到所列服务全部就绪才启动。

greeter.ts —— 提供服务
import { Service, type Context } from '@deepseek-ai/cordis'

// 编译时:声明合并,让 ctx.greeter 在各处都有类型
declare module '@deepseek-ai/cordis' {
  interface Context { greeter: GreeterService }
}

export class GreeterService extends Service {
  constructor(ctx: Context) {
    super(ctx, 'greeter')   // 运行时:以 'greeter' 注册,谁都能 ctx.greeter 拿到
  }
  greet(who: string) { return `Hello, ${who}!` }
}

export const name = 'greeter'
export function apply(ctx: Context) { ctx.plugin(GreeterService) }
consumer.ts —— 只声明名字,不 import 实现
export const name = 'consumer'
export const inject = ['greeter']      // 声明依赖

export function apply(ctx: Context) {
  console.log(ctx.greeter.greet('world'))   // 此时 greeter 必定就绪
}

cordis.yml 里两行的书写顺序无关紧要:决定插件何时启动的是依赖关系,不是文件顺序。把 greeter 整个删掉?consumer 保持 PENDING,不输出、不崩溃、也不「只运行一半」。

与传统 DI 的本质区别:服务会消失是常态

传统 DI 假设「一旦绑定,服务就一直在」;Cordis 的假设是「服务可以随时出现,也可以随时消失」。这在 Agent 场景里是日常:LLM 提供方被限流、MCP 服务器崩溃导致工具被注销、文件 watcher 被系统杀死。Cordis 的处理:提供方卸载 → 所有依赖它的插件自动卸载(effect 回滚);提供方恢复 → 自动重载。依赖方一行重连代码都不用写。

服务与 inject 图解
服务与 inject 图解:消费方只声明名字、不 import 实现;提供方卸载 → 依赖方自动卸载,恢复 → 自动重载
交互 · 依赖联动模拟器拨动提供方开关,看下游插件状态联动

服务提供方(点击开关)

dsh-llm
提供 llm 服务
dsh-tools
提供 tools 注册表
dsh-bash-local
提供 shell 服务

消费方(inject 决定状态)

dsh-agentinject: [llm, tools]
dsh-tool-bashinject: [tools, shell]
my-greet-toolinject: [tools]
my-observer无 inject,只监听事件
关掉 dsh-tools:所有 inject 里带 tools 的插件立刻转入 PENDING;重新打开,它们自动恢复 ACTIVE——不需要任何重连代码。
这也是「换提供方不改消费方」的原理:卸载 dsh-bash-local、挂载一个沙箱 shell 提供方,所有 inject 'shell' 的插件自动重启并使用新实现。

还有两个配套机制,知道名字即可:ctx.isolate(key, realm) 隔离——让某作用域内一项服务解析到独立实例(每个会话各自的模型 / 沙箱,互不干扰);ctx.intercept(key, meta) 拦截——外层上下文给依赖访问附加元数据(沙箱策略插件约束所有用 shell 的人),都不用改组件本身。

07

事件与 waterfall:喊一嗓子,与一条决策链

服务适合「直接打电话」(我知道找谁),但很多时候插件只想喊一嗓子或者拦一下,不关心谁在听——这就是事件。Cordis 的事件是类型化的:事件名和监听器签名靠 TypeScript 声明合并获得全链路类型安全。

声明 / 发出 / 监听
declare module '@deepseek-ai/cordis' {
  interface Events {
    'stats/report'(name: string, count: number): void
  }
}

ctx.emit('stats/report', 'tool_call', 42)    // 发出
ctx.on('stats/report', (name, count) => {}) // 监听(卸载时自动移除)

五种分发模式:事件的「公开契约」

模式是否 await语义
emit否同步广播;不等待、不收集返回值
parallel是所有监听器并发执行并一起等待
serial是按序执行;第一个非空返回值胜出并停止后续
bail否serial 的同步版本
waterfall否环绕中间件:每个监听器拿到参数和 next(),可包装、可短路

waterfall 是 dsh 用得最多的模式,本质是把 Koa / Express 的中间件搬进事件系统:每个监听器收到参数和一个 next()。调用 next() = 放行给下游;不调 next() 直接返回 = 否决,整条链短路。多个互不相识的插件,就这样组成一条决策链。

waterfall 监听器的两种姿势
ctx.on('some/decision', async (input, next) => {
  if (!hasPermission(input)) return { denied: true }  // 不调 next():否决,短路
  return next()                                       // 调 next():放行
})
waterfall 决策链图解
waterfall 决策链图解:调 next() 放行、不调 = 否决短路;只负责观察的监听器必须调用 next()
交互 · waterfall 决策链模拟器一次 bash 工具调用的过闸之旅
请求一次 bash 工具调用
监听器 1权限检查
监听器 2审批
监听器 3审计 / 日志
默认行为最内层:真正执行
选一个场景点播放。注意「next()」标签:它是链上每一跳的接力棒。
dsh 里的真实 waterfall 链:tools/pre-execute → tools/execute → tools/post-execute,以及 approval/request、agent/request。
dsh 明文纪律:只负责观察 / 记录的 waterfall 监听器,必须调用 next() 不调 next() 直接返回代表「有意否决」。如果一个日志监听器忘了调 next(),它会无声地吞掉下游所有默认行为——工具莫名其妙不执行了,还没有任何报错。场景 3 演示的就是这个坑。
08

配置即程序:cordis.yml、patch 层与 HMR

第 2 章说过「应用长什么样由配置决定」。这一章把这句话推到底:cordis.yml 里的每个条目(entry)不只承载「挂哪个插件」,还承载它的配置、开关和身份——修改配置就是修改应用,而且是热修改。

cordis.yml —— 条目的完整形态
- id: greeter          # 稳定身份:loader 靠它区分「修改」与「删了重加」
  name: './greeter.ts'
- id: consumer
  name: './consumer.ts'
  disabled: true       # 保留条目但不挂载;改回后自动加载
  config:              # 传给 apply 的第二个参数
    greeting: 你好

三个要点:① id 是增量更新的前提——不带 id 的条目每次读取都会获得新 id,于是任何编辑都会被当成「先删后加」整体重挂;带 id 才能精准到「只改这一个」。② Schema 校验——插件导出 Config schema(用的正是 Shigma 写的 schemastery),框架在调 apply 前校验,配置非法则加载失败并给出精确到字段的错误,插件绝不会半启动。③ HMR——挂上 cordis-plugin-hmr 后保存文件即触发热重载,而且不需要像 Webpack / Vite 那样手工标注 accept 边界:fiber 本身就界定了组件全部效果的边界,替换一个组件 = 一次 fiber 操作(dispose 旧实例 → effect 全回滚 → 挂载新实例),失败还会整体回退,绝不半重载。

dsh 的做法:应用 = 一层层叠加上去的 patch

dsh 把「配置即程序」工程化成了 Profile + Bundle + patch 覆盖层:一个 Bundle 就是一个 npm 包,其 package.json 声明一份 patch(一组 insert);核心组合包 @deepseek-ai/dsh-base 的 patch 一次性 insert 几十个核心插件(真实文件约 500 行);部署方想改默认行为,不用碰任何源码——在自己的 patch 层按 id 覆盖一行即可(后写覆盖先写)。

patch 叠层图解
patch 叠层图解:四层按顺序叠加出生效配置树——按 id 寻址、后写覆盖先写;改应用 = 叠一层 patch
交互 · patch 叠层演示按顺序点击各层,看生效配置树实时变化
第 0 层空根cordis.yml = [],什么都没有,等着被叠
第 1 层 · bundle@deepseek-ai/dsh-base一次 insert 五个核心插件:timer / hmr(disabled) / llm / session / agent
第 2 层 · 用户cordis.patch.yml按 id 覆盖:把 hmr 的 disabled 改回 false;新增 my-tool
第 3 层 · 临时--patch ./dev.yml命令行临时层:再插入 greet-tool
// 生效配置树(点击左侧层来叠加)
要点:按 id 寻址、后写覆盖先写。第 2 层只改了 hmr 一行,timer/llm/session/agent 原样保留——这就是「改应用 = 叠一层 patch」。
服务槽位注册表图解
dsh 的扩展点是一张服务注册表:八个常用槽位,任何插件都能注册新服务、也能替换已有提供方
Service Definition / Provider / Consumer 三种角色图解
三种角色:Definition 定契约、Provider 可替换、Consumer 只认契约——换提供方 = cordis.yml 改一行(配合第 6 章的联动模拟器)
09

动手写一个 dsh 插件:三条路线与两个实战

前面的机制全部服务于这一章的实操。先回答「怎么装进去」——三种入口,从最快到最正式:

入口怎么用适合
① --patch 覆盖层写个 YAML,dsh web --patch ./my-plugins.yml 启动时插入本地开发,最快见效
② profile 常驻层把条目写进 $DSH_HOME/profiles/<名字>/cordis.patch.yml日常自用,常驻生效
③ bundle 包打包成 npm 包,package.json 声明 dsh.bundle.patch发布给别人 dsh plugin add
my-plugins.yml —— 第 ① 种入口
- insert:
    - id: my-greet-tool
      name: '/abs/path/to/greet-tool.ts'

实战一:给 Agent 注册一个工具

Agent 的能力边界是 ctx.tools 服务。注册一个工具 = inject 等注册表就绪 + ctx.tools.register()。注册本身是 effect,插件卸载时工具自动注销。

greet-tool.ts —— 完整可运行
import type { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'

export const name = 'greet-tool'
export const inject = ['tools']          // 等工具注册表就绪(第 6 章)

export function apply(ctx: Context) {
  ctx.tools.register(defineTool({
    name: 'greet',
    description: 'Greet the named person.',
    parameters: {
      name: { type: 'string', required: true, description: 'Who to greet' },
    },
    async execute(args) {
      return `Hello, ${args.name}!`
    },
  }))
}

defineTool 会把 parameters 规约转成给模型看的 JSON Schema,并在 execute 前校验模型给的参数。想观察每一次工具调用?再写一个互相不认识的观察插件——靠事件解耦:

tool-logger.ts —— 观察者:不认识执行者
export const name = 'tool-logger'
export const inject = ['tools']

export function apply(ctx: Context) {
  ctx.on('tools/result', (exec, result) => {
    console.log(`[tool-logger] ${exec.name} -> done`)
  })
}

实战二:写一个钩子插件(权限门禁)

挂到 tools/pre-execute 这条 waterfall 链上,就有权对每次工具调用说「不」——沙箱、权限、plan-mode 插件用的都是这个扩展点:

permission-gate.ts
import type { Context } from '@deepseek-ai/cordis'
import type { PreToolDecision, ToolExecution } from '@deepseek-ai/dsh-tools'

export const name = 'permission-gate'

export function apply(ctx: Context) {
  ctx.on('tools/pre-execute', async (exec: ToolExecution, next) => {
    if (!(await isAllowed(exec))) {
      return { kind: 'deny', reason: 'Denied by policy.' }  // 否决:短路
    }
    return next()                                          // 放行
  })
}

槽位:扩展点全部是服务

dsh 的扩展点不是一份「API 列表」,而是一张 Cordis 服务注册表:任何插件都能注册新服务,也能替换已有服务的提供方。每个可替换能力都遵循固定模式(上图):Definition 只声明契约几乎不变,Provider 可整只替换,Consumer 与 Provider 互不依赖。换提供方 = cordis.yml 改一行,所有依赖方自动卸载重载(第 6 章的模拟器)。

交互 · 槽位探索器点击槽位卡片
完整槽位表(执行 / 模型 / 智能 / 数据 / 环境 / 治理 / 编排 / 自指 / 前端九大类)见 harness 文档 capability-seams 与子系统页面。

排障三连:插件「没反应」时按序检查

  • 查拼写——模块解析失败(路径 / 包名错)只在 logger 报一条,进程不崩(第 2 章);
  • 查 fiber 状态——多半是蹲在 PENDING:inject 的服务无人提供。用 ctx.registry 遍历 fiber,或直接让 Agent 调 cordis_inspect 工具做只读巡检;
  • 补提供方——把缺的服务提供方加进组合,依赖它的插件会自动激活,无需重启。
通关条件 · 点亮你的进度
✓跑通第一个插件:hello.ts + cordis.yml,看到插件自己的输出。
展开看参考答案
大白话版

建一个文件只写一个 apply 函数,在 cordis.yml 里写上文件路径,跑启动器。核心是体会「框架调你」:你没写任何 main 函数,是 Loader 读配置后来调用你的 apply。

专业版

导出 apply(ctx: Context)(函数形态),cordis.yml 条目 - name: './hello.ts'。启动器创建根 Context → 挂 Loader → 解析模块 → ctx.plugin() 建 fiber → apply 执行。补充:想改配置可用 config 字段 + 导出 Config schema。

✓注册一个工具:greet-tool 被 Agent 真实调用。
展开看参考答案
大白话版

照第 9 章实战一抄 greet-tool,用 --patch 塞进 dsh,然后对 Agent 说「用 greet 打个招呼」。工具的 description 和 parameters 就是给模型看的说明书,写得越清楚模型用得越准。

专业版

inject: ['tools'] 保证注册表就绪;ctx.tools.register(defineTool({...})) 把工具挂入注册表并附着 disposer。组合里要有 dsh-tools(及它依赖的 systemPrompt 提供方),否则你的插件停在 PENDING。观察 tools/result 事件可另写 observer 插件。

✓写一个钩子:在 tools/pre-execute 上拦截某次工具调用并给出理由。
展开看参考答案
大白话版

照实战二写 permission-gate,把 isAllowed 换成你自己的规则(比如「禁止 rm -rf」)。要点只有一个:想拦截就不调 next() 直接返回 deny;其余情况必须 return next(),否则第 7 章场景 3 的「无声吞掉」就会找上你。

专业版

监听 'tools/pre-execute' waterfall,返回类型化决策 PreToolDecision。观察型监听器必须委托 next();策略监听器在拥有决策权时才可短路。需要单调「最终拒绝」的场合改用 ctx.tools.guard()。

10

终极测验

五道题覆盖全部核心机制。答完每题立即出解析;4 题以上正确算出师。

QUESTION 1 / 50 / 5
11

参考资料

本手册整理自以下来源:微信文章正文与配图、dsh 仓库官方文档与真实源码(base bundle patch、vendor Cordis)、以及公开的插件开发教程。

延伸阅读 · 本站相关手册

手册关联点
pi 扩展开发另一个 coding agent 的插件体系:pi 用 ExtensionAPI + 事件生命周期,与 Cordis 的「服务 + 可逆副作用」互为对照
pi Agent Loopdsh 的 agentLoop 服务干的是同一层事:循环驱动本身也是插件
LLM 前缀缓存dsh 里 ctx.llm 适配器注册表背后的模型调用层
回到总目录
know-as-ui · 还有 7 个可交互知识点
↗