DeepSeek Harness: 第一个插件

最后更新:2026-08-31

编写第一个插件是深入理解 DSH 的关键一步——从"使用框架"跃升到"扩展框架"。本课从创建本地项目开始,逐步完成一个可加载、可运行的 Cordis 插件。

💡 提示:DSH 插件的核心协议极简——只需导出 nameapply 两个东西。框架调用 apply(ctx) 时,插件通过 ctx 注册能力;插件卸载时,ctx 上注册的资源自动回收。

📋 前置知识:已完成 08-community-plugins.md,了解插件生态概况

1. 你将学到


2. 创建本地项目

插件目录结构

(1) 初始化项目目录

每个 DSH 插件本质上是一个 Node.js 包。我们从零搭建:

BASH
mkdir -p scratch-plugin/src
cd scratch-plugin
pnpm init

初始化后的 package.json

JSON
{
  "name": "scratch-plugin",
  "version": "0.1.0",
  "main": "src/index.ts"
}

(2) ▶ 示例 2

BASH
pnpm add -D @deepseek-ai/cordis typescript

项目结构:

TEXT 📖 仅展示
scratch-plugin/
├── src/
│   └── index.ts      ← 插件主入口
├── package.json
└── node_modules/

(3) TypeScript 配置

创建 tsconfig.json

JSON
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "include": ["src"]
}

3. 插件的核心协议

(1) 最小插件

一个 DSH 插件只需满足两个条件:

  1. 导出 name 字符串——插件唯一标识
  2. 导出 apply 函数——插件入口
TYPESCRIPT
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

100%
graph LR
    LOAD[框架加载插件] --> CALL[调用 apply<br/>ctx 是插件的"世界"]
    CALL --> RUN[插件运行中]
    UNLOAD[插件卸载] --> CLEAN[ctx 上注册的资源<br/>自动回收]

apply 只在插件加载时调用一次。如果插件需要持续运行,就在 apply 内注册定时器、监听器等。

(3) name 的作用

name 是插件的身份标识,用于:

TYPESCRIPT
export const name = 'my-plugin'

⚠️ name 必须全局唯一,与已有插件重名会导致加载失败。


4. 插件三种形态

Cordis 支持三种插件写法,功能等价,选择取决于复杂度:

(1) 函数形态(最简)

TYPESCRIPT
import { Context } from '@deepseek-ai/cordis'

export const name = 'hello-fn'

export function apply(ctx: Context) {
  ctx.logger.info('hello from function plugin')
}

适用场景:简单工具、一次性注册。

(2) 对象形态

TYPESCRIPT
import { Context } from '@deepseek-ai/cordis'

export default {
  name: 'hello-obj',
  apply(ctx: Context) {
    ctx.logger.info('hello from object plugin')
  }
}

适用场景:需要导出多个字段(如 Configinject)的中等复杂插件。

(3) 类形态

TYPESCRIPT
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) 注册定时日志

TYPESCRIPT
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) 监听事件

TYPESCRIPT
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) 注册命令

TYPESCRIPT
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 中注册本地插件:

YAML
plugins:
  my-plugin:
    $insert: /absolute/path/to/scratch-plugin

$insert 操作将本地插件注入插件列表。路径必须是绝对路径

(2) 绝对路径 vs 相对路径

YAML
plugins:
  my-plugin:
    $insert: /home/alice/plugins/scratch-plugin   # ✅ 绝对路径
    # $insert: ./scratch-plugin                    # ⚠️ 相对路径也可用,但不推荐

推荐绝对路径的原因:

(3) 启动并加载

BASH
pnpm dsh web --patch

--patch 参数让 DSH 读取 cordis.yml 中的 $insert 等覆盖操作,将本地插件叠加到默认配置上。

(4) 验证加载

启动后查看终端日志:

TEXT 📖 仅展示
[my-plugin] loaded!

或在 Web UI 的插件列表中搜索 my-plugin


▶ 示例 7 问候插件

把前面学到的知识组合成一个完整示例:

TYPESCRIPT
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 配置:

YAML
plugins:
  greeter:
    $insert: /home/alice/projects/scratch-plugin

启动验证:

BASH
pnpm dsh web --patch
# [greeter] greeter plugin loaded
# [greeter] session started: abc-123

❓ 常见问题

Q 插件的 name 可以包含中划线吗?
A 可以,如 my-plugin 是合法的 name。但建议使用小写字母和中划线,避免驼峰命名。
Q apply 函数可以是异步的吗?
A 可以。async function apply(ctx) 完全合法,框架会 await 异步 apply 完成。但注意:异步 apply 未完成前,插件处于 pending 状态,依赖它的插件不会被加载。
Q $insert 路径写错了会怎样?
A DSH 启动时会报错并跳过该插件,不会导致整体崩溃。终端会输出类似 [error] plugin not found: /wrong/path/to/plugin 的提示。
Q 函数形态插件怎么导出 Config?
A 独立导出即可: typescript export const name = 'my-plugin' export const Config = Schema.object({ ... }) export function apply(ctx: Context) { ... }
Q 同一个插件可以加载多次吗?
A 默认不行——name 全局唯一。如果需要多实例,用 isolate 配置创建隔离作用域(详见 20-scope.md)。
Q 本地开发时每次改代码都要重启吗?
A 是的,pnpm dsh web --patch 不支持热重载。开发时可以结合 --dump-config 验证配置,或参考 18-hot-reload.md 了解 HMR 机制。 ---

📖 小节


📝 作业

1. ⭐ 基础题:按照本课步骤,创建一个函数形态的 hello-world 插件,在 apply 中输出日志 "hello world!",注册到 cordis.yml 并启动验证。

2. ⭐⭐ 进阶题:将 hello-world 插件改写为对象形态和类形态两种版本,分别加载运行,确认三者输出一致。

3. ⭐⭐⭐ 挑战题:编写一个 uptime 插件,记录插件加载时间,通过 ctx.setInterval 每分钟输出一次"已运行 N 分钟"。思考:如果插件卸载后重新加载,计时器应该重置吗?为什么?

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏