二次开发 · 一切皆插件

DeepSeek Harness 二次开发实战:从「一切皆插件」到写出你自己的 Tool

Cordis 插件系统 · 自定义 Tool · 火山方舟官方 API

技术架构 插件开发 代码实战

前面几篇聊了 DeepSeek Harness 的定位、桌面版体验和场景实战。但真正让它在开发者圈子里封神的,是另一件事:它是目前少有的、把"插件架构"写进论文的开源 Agent 框架——Everything is a plugin,一切皆插件。

这篇文章从技术架构讲起,手把手带你完成一次完整的二次开发:写 Hello World、写自定义 Tool、加可配置项、管理外部资源、打包成可安装 Bundle,最后解决几个真实的工程问题。

技术架构

一、技术架构:先看懂这台「乐高机器」

1.1 架构总纲:Agent = Model + Harness

DeepSeek Harness(dsh)是连接模型与真实环境、调度工具完成任务的中介层。模型是 Agent 的灵魂,Harness 给予 Agent 理解环境、使用工具的能力。

落到架构上,就是一句话:模型、工具、技能、会话、沙箱、存储、循环、调度、UI,全部由插件组合而成。没有"不可替换的核心"。

1.2 心脏:Cordis 插件系统

Harness 的底座是 Cordis——一个只负责插件"插进去、拔出来、管依赖"的微内核。它最早在机器人生态 Koishi 里跑了四年,如今被 DeepSeek 选中成为 Agent 框架的心脏,配套论文《A Programming Paradigm for Spatiotemporal Composability》以预印本发布,署名单位为北京大学与 DeepSeek-AI。

Cordis 的职责非常聚焦,只有三件事:

1.3 插件的本质:一个 apply 函数

在 DeepSeek Harness 里,一个插件就是一个导出 apply 函数的 TypeScript 模块:

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

export const name = 'my-plugin'

export function apply(ctx: Context) {
  // 在这里注册能力
}

框架加载插件时调用 apply,传入 ctx(上下文,即服务容器)。你在 ctx 上注册的一切能力,插件卸载时会被自动清理——这是 Harness 插件和"随手写个脚本"最本质的区别:生命周期有始有终,不会泄漏。

1.4 插件的三种形态

形态适用场景写法
函数形式绝大多数场景,注册能力export function apply(ctx) {...}
对象形式需要携带额外元信息export default { name, inject, apply }
类形式(Service)要"提供服务"给别的插件依赖class MyService extends Service

类形式最强大——它能把你的能力暴露成 ctx.myService,让其他插件通过 inject: ['myService'] 来依赖和使用。

1.5 依赖注入:inject 与 ctx

typescript
export const inject = ['tools']

含义:我这个插件依赖 ctx.tools 这个服务。Cordis 会保证:服务就绪后才调用 apply;如果 provider 被热替换,插件会自动 dispose 并重新 apply。

1.6 事件系统:emit / serial / waterfall

类型行为用途
emit广播通知,监听器不影响流程打日志、埋点
serial按注册顺序依次执行有序的副作用
waterfall链式传递,必须调 next() 才能继续拦截、改写、审批

1.7 Tool 执行管道

模型返回 tool_call → tools/pre-execute → tools/execute → execute() → post-execute → tool/result

权限策略可以在 tools/pre-execute 拦截工具,审批机制可以在执行前要求确认,结果自动记录到 session log。

1.8 配置层:按层叠加,后层胜出

加载顺序:profile.bundles → profile 的 cordis.patch.yml → $DSH_HOME/cordis.patch.yml(机器级)→ --patch 命令行 overlay。后应用的层按行胜出,且整体替换 config,不是深度合并。

准备环境

二、二次开发准备:源码编译安装

bash
git clone https://github.com/deepseek-ai/deepseek-harness.git
cd deepseek-harness
corepack enable
pnpm install

前置要求:Node.js 22 或 24、pnpm。装好后用 pnpm dsh --version 验证。

Hello World

三、实战一:Hello World 插件(5 分钟)

创建插件文件 scratch-plugin/src/my-plugin.ts:

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

export const name = 'hello-plugin'

export function apply(ctx: Context) {
  console.log('[hello-plugin] plugin loaded!')
}

注册到 cordis.yml:

yaml
- insert:
    - id: hello
      name: '/absolute/path/to/deepseek-harness/scratch-plugin/src/my-plugin.ts'

插件路径必须是绝对路径。patch 文件只贡献配置,不改变 loader 的模块解析基目录。

启动:

bash
pnpm dsh web --patch ./scratch-plugin/cordis.yml

终端打印 [hello-plugin] plugin loaded! 即为成功。

自定义 Tool

四、实战二:写一个自定义 Tool

真正有用的是给 Agent 加一个它不会的能力——自定义 Tool。替换 my-plugin.ts:

typescript
import type { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'

export const name = 'greet-tool'
export const inject = ['tools']

export function apply(ctx: Context) {
  ctx.tools.register(defineTool({
    name: 'greet',
    description: 'Greet someone by name.',
    parameters: {
      name: { type: 'string', required: true, description: 'The name to greet' },
    },
    output: {
      schema: { type: 'string' },
      render: (_args, value) => [{ type: 'text', text: value }],
    },
    async execute(args) {
      return `Hello, ${args.name}!`
    },
  }))
}

defineTool 的关键字段,每一个都是"模型接口契约":

字段作用
name模型可见的工具名,64 字符内,只允许 [A-Za-z0-9_-]
description模型用来判断是否调用该工具的描述
parametersJSON Schema,自动推导 args 类型并做运行时校验
output.schema声明工具输出的 schema
output.render把规范输出转成模型可见的 ContentBlock
execute实际执行逻辑,接收校验后的 args

ctx.tools.register() 会返回一个 disposer,插件卸载时自动把 tool 从注册表移除。验证:在 Web UI 输入 Use the greet tool to greet Ada.,模型会调用 greet 返回 Hello, Ada!。

可配置

五、实战三:让插件可配置

用 Schemastery 给插件加配置项:

typescript
import Schema from '@deepseek-ai/schemastery'

export interface Config {
  greeting: string
  emoji: boolean
}

// 导出同名 Schema(Cordis 用它校验 + 填充默认值)
export const Config: Schema<Config> = Schema.object({
  greeting: Schema.string().default('Hello'),
  emoji: Schema.boolean().default(true),
})

// apply 的第二个参数就是 config
export function apply(ctx: Context, config: Config) {
  // ... 用 config.greeting / config.emoji
}

配置写入 cordis.yml:

yaml
- insert:
    - id: hello
      name: '/absolute/path/.../my-plugin.ts'
      config:
        greeting: 'Hey'
        emoji: false

设计原则:凡是不同部署可能需要不同值的参数,都应定义为 Config 字段;Schema 在插件加载时执行校验,配置错了会响亮地失败(加载即报错),而不是运行时静默异常。

资源管理

六、实战四:管理外部资源(数据库连接池)

如果 tool 需要维持一个长连接,用 ctx.effect 管理生命周期:

typescript
export function apply(ctx: Context) {
  let pool: ConnectionPool | undefined

  ctx.effect(() => {
    pool = createConnectionPool({ host: 'localhost', port: 5432 })
    return () => {   // 清理函数,自动执行
      pool?.close()
      pool = undefined
    }
  })

  ctx.tools.register(defineTool({
    name: 'db_query',
    description: 'Run a read-only SQL query.',
    parameters: { sql: { type: 'string', required: true } },
    output: {
      schema: { type: 'array', items: { type: 'object' } },
      render: (_args, rows) => [{ type: 'text', text: JSON.stringify(rows, null, 2) }],
    },
    async execute(args) {
      if (!pool) throw new Error('Database pool not available')
      return await pool.query(args.sql)
    },
  }))
}

ctx.effect 返回的清理函数会在这些场景自动执行:插件被手动 dispose、依赖服务消失(热替换)、HMR 触发配置变更、整个应用关闭。连接池绝不会泄漏。

打包 Bundle

七、实战五:打包为可安装的 Bundle

开发完要交付,打包成标准 Bundle:

text
hello-plugin/
├── package.json       # 声明 dsh.bundle
├── cordis.patch.yml   # 该 bundle 贡献的配置层
└── index.js           # 插件入口
json
{
  "name": "dsh-hello-plugin",
  "version": "0.1.0",
  "type": "module",
  "main": "index.js",
  "files": ["index.js", "cordis.patch.yml"],
  "dsh": { "bundle": { "patch": "./cordis.patch.yml" } }
}

安装到 profile 并验证:

bash
dsh plugin --profile demo add ./hello-plugin
dsh --profile demo --dump-config
dsh --profile demo

--dump-config 是二次开发的黄金命令——随时打印最终配置树,排查"配置到底生效没有"的疑难杂症。

提供服务

八、实战六:提供服务给别的插件(类形式)

想让自己的插件成为基础设施,被其他插件依赖,用类形式:

typescript
import { Service, type Context } from '@deepseek-ai/cordis'

declare module '@deepseek-ai/cordis' {
  interface Context {
    myService: MyService
  }
}

export default class MyService extends Service {
  static inject = ['tools']
  constructor(ctx: Context) {
    super(ctx, 'myService')
  }
  doSomething() { /* ... */ }
}

之后别的插件就能 inject: ['myService'],调用 ctx.myService.doSomething()。这一步把"写工具"升级成了"写框架"。

完整案例

九、实战七:完整案例——代码仓库浏览插件

前面六个实战都是"零件级"演示。这一章把它们串成一个完整、可运行、解决真实问题的插件:repo-explorer(代码仓库浏览插件)。

要解决的问题:让 Harness 的 Agent 自主浏览一个本地代码仓库——列出目录结构、读取关键文件、理解项目技术栈。这在"让 AI 接手一个陌生项目"的场景里极其实用,也是社区"本地代码依赖分析"玩法的核心。

9.1 完整代码

typescript
import { readdir, readFile, stat } from 'node:fs/promises'
import type { Context } from '@deepseek-ai/cordis'
import Schema from '@deepseek-ai/schemastery'
import { defineTool } from '@deepseek-ai/dsh-tools'

export const name = 'repo-explorer'
export const inject = ['tools']

// 可配置项:文件大小上限 + 忽略目录
export interface Config {
  maxFileBytes: number
  ignore: string[]
}

export const Config: Schema<Config> = Schema.object({
  maxFileBytes: Schema.number().default(128 * 1024),
  ignore: Schema.array(Schema.string()).default(['node_modules', '.git', 'dist', '.next']),
})

export function apply(ctx: Context, config: Config) {
  // 工具一:列出目录内容
  ctx.tools.register(defineTool({
    name: 'list_dir',
    description: 'List files and subdirectories in a directory.',
    parameters: {
      path: { type: 'string', required: true, description: 'Absolute path to the directory' },
    },
    output: {
      schema: { type: 'array', items: { type: 'string' } },
      render: (_args, value) => [{ type: 'text', text: value.join('\n') }],
    },
    async execute(args, exec) {
      const entries = await readdir(args.path, { signal: exec.signal })
      return entries
        .filter((entry) => !config.ignore.includes(entry))
        .sort()
    },
  }))

  // 工具二:读取文件内容(带大小上限与取消信号)
  ctx.tools.register(defineTool({
    name: 'read_file',
    description: 'Read a text file from disk. Returns its content as text.',
    parameters: {
      path: { type: 'string', required: true, description: 'Absolute path to the file' },
    },
    output: {
      schema: { type: 'string' },
      render: (_args, value) => [{ type: 'text', text: value }],
    },
    async execute(args, exec) {
      const info = await stat(args.path)
      if (info.size > config.maxFileBytes) {
        throw new Error(`File too large (${info.size} bytes, limit ${config.maxFileBytes})`)
      }
      return readFile(args.path, { encoding: 'utf8', signal: exec.signal })
    },
  }))
}

9.2 逐块拆解:为什么它是「完整的」

设计点实现价值
两个 Tool 组合list_dir + read_fileAgent 先"看目录"再"读文件",形成完整浏览闭环
可配置项maxFileBytes、ignore不同项目可调,防误读 node_modules 这类巨型目录
大小上限stat 后判断 info.size安全边界:避免一次性读入几百 MB 文件撑爆上下文
取消信号exec.signal 传给 readdir/readFile用户打断时,底层 I/O 能真正中断,不浪费资源
输出渲染output.render数组类型结果转成模型可见的文本

9.3 注册配置

yaml
- insert:
    - id: repo-explorer
      name: '/absolute/path/to/deepseek-harness/scratch-plugin/src/repo-explorer.ts'
      config:
        maxFileBytes: 65536
        ignore: [node_modules, .git, dist, .next, build]

9.4 启动与验证

bash
pnpm dsh web --patch ./scratch-plugin/cordis.yml

在 Web UI 输入:

text
浏览 /home/user/my-project 目录,找到入口文件,
总结这个项目的技术栈和目录结构。

你会看到 Agent 自主完成一串动作:list_dir 看根目录 → 发现 package.json → read_file 读取 → 继续深入 src/ → 最终输出技术栈和结构总结。整个过程无需你手动贴代码。

安全提示:read_file 能读任意路径,是高风险工具。生产环境务必叠加权限拦截,在 tools/pre-execute 里把路径白名单限定在工作区目录内,防止 Agent 越权读取敏感文件。

解决实际问题

十、解决实际问题:拦截、审批、可观测

二次开发最常见的目标不是"加功能",而是"加管控"。结合 Tool 执行管道和事件系统,可以做三件实事:

权限拦截(tools/pre-execute waterfall)

typescript
export const inject = ['tools']

export function apply(ctx: Context) {
  ctx.waterfall('tools/pre-execute', async (toolName, args, next) => {
    if (toolName === 'bash' && isDangerous(args)) {
      throw new Error('Blocked by safety policy')
    }
    return next()
  })
}

审批确认:执行前要求人工确认。可观测性:ctx.on('tools/post-execute', ...) 打日志。再配合 --dump-config 和 HMR 热更新,二次开发的调试体验相当顺滑。

场景清单

十、二次开发的典型场景清单

场景用什么实现
接入企业内部系统(ERP、CRM、工单)自定义 Tool + ctx.effect 管理连接
数据查询 / 报表生成Tool 对接数据库 / API
安全管控与权限审批tools/pre-execute waterfall 拦截
自定义模型接入(私有化模型)模型适配器插件
定时任务 / 工作流编排调度插件
审计与合规事件监听 + session log 回放
团队内部共享能力类形式 Service + Bundle 分发

核心思路一句话:凡是 Harness 本身不会、但你的业务需要的,写成一个插件塞进去就行。

成本账

十二、别忘了:跑起来要 Token

二次开发写得再漂亮,最终要烧 Token。Harness 本身免费开源,但每次 Agent 推理、每次工具调用都要走模型计费。省钱的关键还是那句老话:

获取 API Key:打开 https://ark.tokenrize.cn/ 注册充值,创建 Key 后填入 Harness 的 设置 → 模型 即可。

Harness 走 OpenAI 兼容协议,接入地址 https://ark.cn-beijing.volces.com/api/v3。

总结

写在最后

DeepSeek Harness 真正打动二次开发者的,不是它有多少现成功能,而是它把"改框架"的权力还给了开发者——模型、工具、主循环、UI,没有一处是你不能碰的。

从 Hello World 到自定义 Tool,从可配置插件到提供 Service,再到用事件管道做权限拦截,这条路径走下来,你收获的不只是一个插件,而是理解了"一切皆插件"背后那套优雅的工程范式。

关于向量云
向量云(南京)科技有限公司

面向大模型应用落地的技术服务型企业,依托字节跳动火山方舟能力,为合作方提供低折扣模型 Token、可独立部署的运营支撑平台及全链路运营支持。

火山方舟官方 API 低折扣 Token 官方 API Key 按量计费 · 即用即扣 API 网关接入 充值开票服务

常见问题(FAQ)

火山方舟官方 API 与官方 Key 直连 · 向量云 · 按量计费享 Token 折扣

Q1. DeepSeek Harness 的技术架构是什么?

A. 以 Cordis 插件系统为心脏,配合依赖注入、事件系统与 Tool 执行管道,整体仍然是 Agent = Model + Harness。

Q2. 一个插件的本质是什么?

A. 本质是一个 apply 函数:通过 ctx 拿到服务、用 inject 声明依赖、用事件参与执行流程。

Q3. 怎么写出自己的 Tool?

A. 文中从 Hello World 起步,依次覆盖自定义 Tool、可配置插件、资源管理、Bundle 打包、Service 服务与权限拦截。