这个教程假设你已经 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 生态,路径很朴素:

  1. 推到公开 GitHub 仓库;
  2. 打上 dsh-plugin topic;
  3. README 写清楚:干什么、怎么装、要什么权限;
  4. 别人用 dsh plugin --profile web add github:owner/repo 安装。

这正是 dsh-plugin.work 索引依赖的信号集:有没有 README、许可证清不清楚、最近有没有活跃。索引追踪的不是仓库名,是「这个插件是否可信、是否在维护」的证据。

写在最后

跑通这个流程用不了十分钟,但理解它值得多花点时间:插件是代码,组合靠声明依赖,生命周期交给框架托管。DSH 把插件生态的工程复杂度压到了作者几乎感知不到的程度,剩下的工作,就是把好东西写进 apply(ctx)