DeepSeek Harness: 第一个插件
最后更新:2026-08-31
编写第一个插件是深入理解 DSH 的关键一步——从"使用框架"跃升到"扩展框架"。本课从创建本地项目开始,逐步完成一个可加载、可运行的 Cordis 插件。
name 和 apply 两个东西。框架调用 apply(ctx) 时,插件通过 ctx 注册能力;插件卸载时,ctx 上注册的资源自动回收。
📋 前置知识:已完成 08-community-plugins.md,了解插件生态概况
1. 你将学到
- 创建本地插件项目结构
- 插件的本质:导出 apply 函数的 TypeScript 模块
export const name与export function apply(ctx)的含义- 插件三种形态:函数、对象、类
- 注册到 cordis.yml 并加载
pnpm dsh web --patch启动与验证
2. 创建本地项目
(1) 初始化项目目录
每个 DSH 插件本质上是一个 Node.js 包。我们从零搭建:
mkdir -p scratch-plugin/src
cd scratch-plugin
pnpm init
初始化后的 package.json:
{
"name": "scratch-plugin",
"version": "0.1.0",
"main": "src/index.ts"
}
(2) ▶ 示例 2
pnpm add -D @deepseek-ai/cordis typescript
项目结构:
scratch-plugin/
├── src/
│ └── index.ts ← 插件主入口
├── package.json
└── node_modules/
(3) TypeScript 配置
创建 tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src"]
}
3. 插件的核心协议
(1) 最小插件
一个 DSH 插件只需满足两个条件:
- 导出
name字符串——插件唯一标识 - 导出
apply函数——插件入口
import { Context } from '@deepseek-ai/cordis'
export const name = 'my-plugin'
export function apply(ctx: Context) {
ctx.logger.info('my-plugin loaded!')
}
这就是一个完整的插件。框架加载后调用 apply(ctx),ctx.logger.info() 输出日志。
(2) ▶ 示例 2
graph LR
LOAD[框架加载插件] --> CALL[调用 apply<br/>ctx 是插件的"世界"]
CALL --> RUN[插件运行中]
UNLOAD[插件卸载] --> CLEAN[ctx 上注册的资源<br/>自动回收]
apply 只在插件加载时调用一次。如果插件需要持续运行,就在 apply 内注册定时器、监听器等。
(3) name 的作用
name 是插件的身份标识,用于:
- 日志前缀:
[my-plugin] loaded! - 配置命名空间:
plugins.my-plugin.config - 依赖声明:其他插件通过 name 引用
export const name = 'my-plugin'
⚠️ name 必须全局唯一,与已有插件重名会导致加载失败。
4. 插件三种形态
Cordis 支持三种插件写法,功能等价,选择取决于复杂度:
(1) 函数形态(最简)
import { Context } from '@deepseek-ai/cordis'
export const name = 'hello-fn'
export function apply(ctx: Context) {
ctx.logger.info('hello from function plugin')
}
适用场景:简单工具、一次性注册。
(2) 对象形态
import { Context } from '@deepseek-ai/cordis'
export default {
name: 'hello-obj',
apply(ctx: Context) {
ctx.logger.info('hello from object plugin')
}
}
适用场景:需要导出多个字段(如 Config、inject)的中等复杂插件。
(3) 类形态
import { Context } from '@deepseek-ai/cordis'
export default class HelloClass {
static name = 'hello-class'
constructor(private ctx: Context) {
ctx.logger.info('hello from class plugin')
}
}
适用场景:需要维护内部状态、实现服务基类的复杂插件。
(4) 三种形态对比
| 维度 | 函数 | 对象 | 类 |
|---|---|---|---|
| 复杂度 | 低 | 中 | 高 |
| 状态管理 | 闭包 | 闭包 | 实例属性 |
| 导出 Config | 独立导出 | 对象内字段 | 静态属性 |
| 继承 | 不支持 | 不支持 | 支持 |
| 适用 | 工具插件 | 标准插件 | 服务插件 |
5. 让插件"做点事"
(1) 注册定时日志
import { Context } from '@deepseek-ai/cordis'
export const name = 'heartbeat'
export function apply(ctx: Context) {
ctx.setInterval(() => {
ctx.logger.info('heartbeat tick')
}, 60000)
}
ctx.setInterval 注册的定时器会在插件卸载时自动清除——这是 Cordis 自动清理的核心优势。
(2) 监听事件
import { Context } from '@deepseek-ai/cordis'
export const name = 'welcome'
export function apply(ctx: Context) {
ctx.on('session/created', (session) => {
ctx.logger.info(`new session: ${session.id}`)
})
}
ctx.on 注册的监听器同样在卸载时自动移除。
(3) 注册命令
import { Context } from '@deepseek-ai/cordis'
export const name = 'hello-cmd'
export function apply(ctx: Context) {
ctx.command('hello <name:text>')
.action(({ session }, name) => {
return `Hello, ${name}!`
})
}
6. 注册到 cordis.yml 并加载
(1) cordis.yml 配置
在 DSH 项目根目录的 cordis.yml 中注册本地插件:
plugins:
my-plugin:
$insert: /absolute/path/to/scratch-plugin
$insert 操作将本地插件注入插件列表。路径必须是绝对路径。
(2) 绝对路径 vs 相对路径
plugins:
my-plugin:
$insert: /home/alice/plugins/scratch-plugin # ✅ 绝对路径
# $insert: ./scratch-plugin # ⚠️ 相对路径也可用,但不推荐
推荐绝对路径的原因:
- 路径解析不受工作目录影响
- 在不同启动方式下行为一致
- 调试时一目了然
(3) 启动并加载
pnpm dsh web --patch
--patch 参数让 DSH 读取 cordis.yml 中的 $insert 等覆盖操作,将本地插件叠加到默认配置上。
(4) 验证加载
启动后查看终端日志:
[my-plugin] loaded!
或在 Web UI 的插件列表中搜索 my-plugin。
▶ 示例 7 问候插件
把前面学到的知识组合成一个完整示例:
import { Context } from '@deepseek-ai/cordis'
export const name = 'greeter'
export function apply(ctx: Context) {
ctx.logger.info('greeter plugin loaded')
ctx.on('session/created', (session) => {
ctx.logger.info(`session started: ${session.id}`)
})
ctx.setInterval(() => {
ctx.logger.info('greeter heartbeat')
}, 300000)
}
cordis.yml 配置:
plugins:
greeter:
$insert: /home/alice/projects/scratch-plugin
启动验证:
pnpm dsh web --patch
# [greeter] greeter plugin loaded
# [greeter] session started: abc-123
❓ 常见问题
my-plugin 是合法的 name。但建议使用小写字母和中划线,避免驼峰命名。async function apply(ctx) 完全合法,框架会 await 异步 apply 完成。但注意:异步 apply 未完成前,插件处于 pending 状态,依赖它的插件不会被加载。[error] plugin not found: /wrong/path/to/plugin 的提示。typescript export const name = 'my-plugin' export const Config = Schema.object({ ... }) export function apply(ctx: Context) { ... } 📖 小节
- 插件是导出
name+apply(ctx)的 TypeScript 模块,框架加载时调用 apply - 通过
ctx注册定时器、事件监听器、命令等,卸载时自动回收 - 三种插件形态:函数(最简)、对象(标准)、类(复杂/需继承)
cordis.yml中用$insert+ 绝对路径注册本地插件pnpm dsh web --patch启动并加载覆盖配置- name 必须全局唯一,重名会导致加载失败
📝 作业
1. ⭐ 基础题:按照本课步骤,创建一个函数形态的 hello-world 插件,在 apply 中输出日志 "hello world!",注册到 cordis.yml 并启动验证。
2. ⭐⭐ 进阶题:将 hello-world 插件改写为对象形态和类形态两种版本,分别加载运行,确认三者输出一致。
3. ⭐⭐⭐ 挑战题:编写一个 uptime 插件,记录插件加载时间,通过 ctx.setInterval 每分钟输出一次"已运行 N 分钟"。思考:如果插件卸载后重新加载,计时器应该重置吗?为什么?