这个教程假设你已经 clone 了 deepseek-harness 仓库并走完了 run-from-source 的安装流程。除此之外,只需要两个文件。
文件一:插件本体
scratch-plugin/src/my-plugin.ts
import type { Context } from '@deepseek-ai/cordis'
export const name = 'hello-plugin'
export function apply(ctx: Context) {
console.log('[hello-plugin] plugin loaded!')
}
这就是整个插件。没有入口注册、没有配置模板、没有样板代码。导出 name,导出 apply,完事。
文件二:让框架知道你
patch 文件告诉 Web UI 插入这个本地插件:
scratch-plugin/cordis.yml
- insert:
- id: hello
name: '/absolute/path/to/deepseek-harness/scratch-plugin/src/my-plugin.ts'
路径必须是绝对路径:patch 只贡献配置,不会改变 loader 解析模块的目录。
跑起来
pnpm dsh web --patch ./scratch-plugin/cordis.yml
打开 http://127.0.0.1:3080,终端里出现 [hello-plugin] plugin loaded!——你的插件已经跑在 harness 里了。
让它干点活
光打日志不算能力。典型的做法是通过 ctx 注册点东西。文档里的标准例子是注册工具,先声明 tools 服务:
export const inject = ['tools']
export function apply(ctx: Context) {
ctx.tools.register(/* ... */)
}
框架会等 tools 就绪再调用 apply,所以 ctx.tools 直接可用,你这边不需要任何等待。(工具 DSL 的具体写法在官方文档的 tool 章节;这里的关键是注册路径:inject 声明 → ctx 注册。)
学会清理
注册在 ctx 上的东西卸载时自动清理。需要显式释放的资源用 ctx.effect() 提供 disposer:
ctx.effect(() => {
const timer = setInterval(() => console.log('heartbeat'), 5000)
return () => clearInterval(timer)
})
返回的函数在插件卸载时执行。像 setInterval 这种其实不写也会被自动清理,但网络连接这类资源,用 ctx.effect 才是正确姿势。经验法则:凡是有关闭/释放步骤的资源,就包进 ctx.effect。
发布到生态
写好的插件进入 DSH 生态,路径很朴素:
- 推到公开 GitHub 仓库;
- 打上
dsh-plugintopic; - README 写清楚:干什么、怎么装、要什么权限;
- 别人用
dsh plugin --profile web add github:owner/repo安装。
这正是 dsh-plugin.work 索引依赖的信号集:有没有 README、许可证清不清楚、最近有没有活跃。索引追踪的不是仓库名,是「这个插件是否可信、是否在维护」的证据。
写在最后
跑通这个流程用不了十分钟,但理解它值得多花点时间:插件是代码,组合靠声明依赖,生命周期交给框架托管。DSH 把插件生态的工程复杂度压到了作者几乎感知不到的程度,剩下的工作,就是把好东西写进 apply(ctx)。