4
0
0
插件开发与扩展
2026-08-14
2026-09-07

文章摘要
|
入门10:插件开发与扩展
这是入门系列的最后一篇。我们将从零开发一个完整的"天气查询"插件。
如果你写过 Vue 插件或 Express 中间件,那这篇对你来说会很轻松。
1. 插件开发模式
Harness 所有插件遵循"能力缝设计模式"——其实就是接口 + 实现 + 消费三层:
| 角色 | 类比前端 | 干啥的 |
|---|---|---|
| Service Definition | 定义一个接口/抽象类 | 说清楚"这个能力长啥样" |
| Service Provider | 实现这个接口 | 真正干活 |
| Consumer | 调用方 | 使用这个能力 |
2. 实战:开发一个"天气查询"插件
我们要做三个包:
packages/weather/
├── weather-service/ # 1. 定义"查天气"这个接口
├── weather-service-local/ # 2. 真正调用天气 API
└── tool-weather/ # 3. 注册成 AI 能用的工具
2.1 第一步:定义接口
就像写一个 TypeScript 的 interface,或者 Vue 的 composable 类型定义。
packages/weather/weather-service/src/index.ts:
import { Context, Service } from 'cordis'
// 1. 定义一个抽象类(就像 Vue 的 composable 的接口定义)
export abstract class WeatherService extends Service {
constructor(ctx: Context) {
// 注册到 ctx.weather——就像 provide('weather', this)
super(ctx, 'weather')
}
// 查当前天气
abstract getCurrentWeather(city: string): Promise<WeatherResult>
// 查天气预报
abstract getForecast(city: string): Promise<ForecastResult>
}
// 返回的数据类型
export interface WeatherResult {
temperature: number
humidity: number
description: string
windSpeed: number
}
export interface ForecastResult {
city: string
daily: Array<{
date: string
high: number
low: number
description: string
}>
}
// 2. 声明类型——让 ctx.weather 有类型提示
declare module 'cordis' {
interface Context {
weather: WeatherService
}
}
package.json:
{
"name": "@deepseek-ai/dsh-weather-service",
"version": "0.1.0",
"type": "module",
"main": "src/index.ts",
"peerDependencies": {
"cordis": "workspace:^"
}
}
2.2 第二步:实现服务
就像写一个 class implements 接口,或者写一个具体的 composable。
packages/weather/weather-service-local/src/index.ts:
import { Context } from 'cordis'
import {
WeatherService,
WeatherResult,
ForecastResult,
} from '@deepseek-ai/dsh-weather-service'
// 实现天气服务
export class WeatherServiceLocal extends WeatherService {
constructor(ctx: Context) {
super(ctx)
// 声明我需要 ctx.web 这个依赖
ctx.inject(['web'], this)
}
async getCurrentWeather(city: string): Promise<WeatherResult> {
// 调用天气 API(就像用 axios 调接口)
const response = await ctx.web.fetch(
`https://api.weather.com/current?city=${encodeURIComponent(city)}`
)
const data = JSON.parse(response)
return {
temperature: data.temp,
humidity: data.humidity,
description: data.condition,
windSpeed: data.wind,
}
}
async getForecast(city: string): Promise<ForecastResult> {
const response = await ctx.web.fetch(
`https://api.weather.com/forecast?city=${encodeURIComponent(city)}`
)
const data = JSON.parse(response)
return {
city: data.city,
daily: data.daily.map((d) => ({
date: d.date,
high: d.temp_max,
low: d.temp_min,
description: d.condition,
})),
}
}
}
// 导出插件
export default WeatherServiceLocal
2.3 第三步:注册成 AI 工具
就像用
app.component()注册一个全局组件,AI 就能用了。
packages/weather/tool-weather/src/index.ts:
import { Context } from 'cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
export function apply(ctx: Context) {
// 声明需要 ctx.weather
ctx.inject(['weather'], ctx)
// 注册工具——就像 Vue 的 app.component('weather_query', ...)
ctx.tools.define('weather_query', defineTool({
name: 'weather_query',
description: '查询指定城市的当前天气',
parameters: {
type: 'object',
properties: {
city: {
type: 'string',
description: '城市名称,如 "北京"、"上海"',
},
},
required: ['city'],
},
async execute(args: { city: string }) {
try {
const result = await ctx.weather.getCurrentWeather(args.city)
return {
success: true,
data: `${args.city}: ${result.description},${result.temperature}°C`,
}
} catch (error) {
return {
success: false,
error: `查天气失败了:${error.message}`,
}
}
},
}))
}
3. 注册插件
在 cordis.yml 里注册:
plugins:
- name: weather-service
path: @deepseek-ai/dsh-weather-service-local
config:
apiKey: your-api-key
- name: tool-weather
path: @deepseek-ai/dsh-tool-weather
4. 配置验证
用 Schemastery 做配置验证——就像 Vue 的 props 校验:
import { Schema } from 'schemastery'
// 定义配置格式
export const Config = Schema.object({
apiKey: Schema.string().description('API 密钥').required(),
units: Schema.union(['metric', 'imperial'])
.description('温度单位')
.default('metric'),
cacheDuration: Schema.number()
.description('缓存时间(秒)')
.default(300),
})
// 在插件里用
export function apply(ctx: Context, config: Config) {
// config 现在有正确的类型和验证过的值
console.log('API Key:', config.apiKey)
}
5. 用 Waterfall 扩展核心流程
就像 Express 中间件——在核心流程里"插一脚"。
// 在 AI 调用前加个自定义指令
ctx.waterfall('llm/stream', (options) => {
options.messages.unshift({
role: 'system',
content: '今天是 ' + new Date().toLocaleDateString(),
})
return options
})
// 在工具执行后记日志
ctx.waterfall('tools/post-execute', (execution) => {
console.log(`工具 ${execution.name} 执行耗时 ${execution.duration}ms`)
return execution
})
// 在拼提示词时加自定义节
ctx.waterfall('system-prompt/assemble', (assembly) => {
assembly.sections.push({
priority: 50,
content: '注意:当前为严格模式。',
})
return assembly
})
6. 生命周期管理
就像 Vue 的
onMounted/onUnmounted,插件也有完整的生命周期。
import { Context } from 'cordis'
export function apply(ctx: Context) {
// 启动时
console.log('插件启动')
// 注册副作用——就像 React 的 useEffect,自动清理
ctx.effect(() => {
const timer = setInterval(() => {
console.log('心跳')
}, 30000)
// 插件销毁时自动执行
return () => {
clearInterval(timer)
console.log('插件已清理')
}
})
// 监听事件——就像 Vue 的 $on
ctx.on('session/created', (session) => {
console.log('新会话:', session.header.sessionId)
})
// 支持 HMR 热更新
if (ctx.hmr) {
ctx.hmr.accept()
}
}
7. 发布与安装
# 发布到 npm
cd packages/weather/weather-service
npm publish
# 或者从 git 安装
# cordis.yml 支持 git 安装
plugins:
- name: weather
path: https://github.com/user/dsh-weather.git
config:
apiKey: '${WEATHER_API_KEY}'
8. 参考示例
项目提供了 6 个完整示例,适合作为参考:
| 示例 | 说明 |
|---|---|
examples/headless-agent/ | 非交互式 Agent,跑一次就退出 |
examples/jsonrpc-agent/ | 通过 Python SDK 驱动的 Agent |
examples/mcp-memory/ | 连接 MCP 记忆服务 |
examples/web-cordis/ | 自引用 Agent,可以修改自己的插件 |
examples/web-schedule/ | 定时提醒插件 |
examples/acp-agent/ | ACP 协议的自动化服务器 |
恭喜!你已完成入门系列的学习
现在你应该对 DeepSeek Harness 有了全面的理解。接下来可以:
- 读官方文档:
docs/目录有完整的架构、教程、指南 - 跑示例项目:
examples/目录有 6 个可运行示例 - 读源码:
packages/下每个包都可以深入阅读 - 写自己的插件:按本篇教程,写一个属于你的插件
- 参与社区:GitHub Discussions 和 Discord
进阶阅读
- 名词速查:附录-AI 概念名词速查手册
要点总结
- 插件开发三步骤:定义接口 → 实现功能 → 注册工具
- 能力缝模式 = 接口 + 实现 + 消费,三层分离
- 用
ctx.effect()管理生命周期,自动清理 - 用 Waterfall 事件在核心流程里"插一脚"
- 用 Schemastery 做配置验证
- 从
cordis.yml注册插件,支持 npm 和 git 安装 - 参考
examples/目录获取完整示例