在上一章中,我们创建了一个 greet 工具,它能根据传入的名字返回一句问候语。但有一个小问题:问候语本身是硬编码的。
看看 execute 函数里的代码:
async execute(args) {
return `Hello, ${args.name}!` // "Hello" 是写死的
}如果用户想把问候语改成 "Hi" 或者 "Greetings",该怎么办?目前唯一的办法是修改源代码,然后重新启动。这对于一个插件来说显然不够灵活——不同的人、不同的场景,可能希望用不同的问候风格。
这一章,我们将解决这个问题。插件配置让用户可以在不修改代码的情况下,通过 cordis.yml 文件来调整插件的行为。我们将把问候语从硬编码变成可配置的,让用户自由定制。
更重要的是,配置机制不仅适用于问候语这种简单的场景。在实际项目中,API 密钥、超时时间、重试次数、功能开关——几乎所有的“可调参数”都应该通过配置来管理。这是让插件从“玩具”走向“生产级”的关键一步。
从硬编码到可配置
先来看一个对比。硬编码的方式是这样的:
// 硬编码:想改就得改代码
const GREETING = 'Hello'可配置的方式是这样的:
// 可配置:用户可以在 cordis.yml 中自由设定
export interface Config {
greeting: string
}
export const Config = Schema.object({
greeting: Schema.string().default('Hello'),
})
export function apply(ctx: Context, config: Config) {
// config.greeting 来自用户配置,或使用默认值 'Hello'
}区别一目了然:硬编码把决定权留给了开发者,而可配置把决定权交给了用户。Harness 的设计哲学是——凡是不同部署可能需要不同值的参数,都应该设计为配置字段。
第一步:定义 Config 类型
打开 scratch-plugin/src/my-plugin.ts,在文件顶部添加 Config 接口的定义:
import type { Context } from '@deepseek-ai/cordis'
import Schema from '@deepseek-ai/schemastery'
import { defineTool } from '@deepseek-ai/dsh-tools'
export const name = 'greet-tool'
export const inject = ['tools']
export interface Config {
greeting: string
maxRetries: number
verbose?: boolean
}Config 接口定义了插件接受哪些配置项:
字段 | 类型 | 说明 |
|---|---|---|
greeting | string | 问候语,比如 "Hello" 或 "Hi" |
maxRetries | number | 最大重试次数 |
verbose | boolean? | 是否输出详细日志(可选,? 表示非必填) |
给初中级开发者的提示:接口中的 ? 表示这个字段是可选的。用户如果没提供,框架也不会报错。但 greeting 和 maxRetries 没有 ?,表示它们是必填的——不过别担心,我们接下来会用 default() 给它们设置默认值,所以用户即使不填也能正常工作。
第二步:编写 Config Schema
有了类型定义,接下来需要编写一个同名的 Schema——注意,它和接口名字一样,都叫 Config:
export const Config: Schema = Schema.object({
greeting: Schema.string().default('Hello'),
maxRetries: Schema.number().default(3),
verbose: Schema.boolean().default(false),
})这段代码做了三件事:
Schema.object({ ... }):声明配置是一个对象,包含多个字段。
Schema.string().default('Hello'):greeting 必须是字符串,如果用户没提供,默认用 'Hello'。
Schema.number().default(3) 和 Schema.boolean().default(false):同理,为 maxRetries 和 verbose 提供默认值。
这里有一个非常重要的概念需要理解:Config 接口和 Config Schema 是成对出现的。接口负责在 TypeScript 层面提供类型检查,Schema 负责在运行时校验用户传入的值。两者名字必须相同——Cordis 框架会通过导出的同名 Schema 来校验配置。
重要提示:不要导出一个普通对象作为 Config,因为它不满足 Cordis 要求的 Standard Schema 接口。必须使用 Schemastery 的 Schema.object() 来创建 Schema。
Schemastery 是 Cordis 框架内置的校验库。它支持的校验方法非常丰富:
方法 | 用途 | 示例 |
|---|---|---|
.required() | 标记为必填 | Schema.string().required() |
.default(value) | 设置默认值 | Schema.number().default(3) |
.min(n) / .max(n) | 数值或长度范围 | Schema.number().min(1).max(10) |
.regex(/pattern/) | 正则匹配 | Schema.string().regex(/^[A-Z]/) |
Schema.union([...]) | 枚举值限定 | Schema.union(['fast', 'accurate']) |
我们会在稍后的“Schema 校验”部分看到这些方法的实际应用。
第三步:在 apply 中接收配置
修改 apply 函数,让它接收 config 参数:
export function apply(ctx: Context, config: Config) {
console.log(`[greet-tool] greeting configured as: "${config.greeting}"`)
console.log(`[greet-tool] maxRetries: ${config.maxRetries}`)
console.log(`[greet-tool] verbose: ${config.verbose}`)
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 `${config.greeting}, ${args.name}!`
},
})
)
}两个关键变化:
apply 函数多了第二个参数 config: Config:框架在调用 apply 时,会把经过校验的配置对象传进来。
execute 中的问候语变成了 ${config.greeting}:工具的执行逻辑现在可以访问插件配置了。
这里有一个重要的闭包现象:execute 函数定义在 apply 内部,所以它可以“捕获”外层的 config 变量。当工具被调用时,config 的值已经被固定下来了——就是插件加载时用户配置的那个值。
给初中级开发者的提示:这种“函数内部访问外部变量”的机制叫做闭包。在这个场景中,它让工具的执行逻辑能够访问到插件配置,非常实用且优雅。
第四步:在 cordis.yml 中传入配置
现在插件已经准备好接收配置了,我们需要在 cordis.yml 中把配置传进去。
打开 scratch-plugin/cordis.yml,修改为:
- insert:
- id: hello
name: 'file:///E:/ProjectsAI/deepseek-harness/scratch-plugin/src/my-plugin.ts'
config:
greeting: 'Hi there'
maxRetries: 5config 下面的内容就是用户传入的配置值。这里我们把 greeting 设成了 'Hi there',把 maxRetries 设成了 5。verbose 没有提供,所以会使用默认值 false。
运行并验证
启动服务,看看配置是否生效:
pnpm dsh web --patch ./scratch-plugin/cordis.yml在启动日志中,你应该能看到类似这样的输出:
[greet-tool] greeting configured as: "Hi there"
[greet-tool] maxRetries: 5
[greet-tool] verbose: false然后打开 Web UI,在对话框中输入:
Use the greet tool to greet Ada.这次模型会调用工具并返回 Hi there, Ada!——问候语已经变成了你配置的样子。
试着修改 cordis.yml 中的 greeting 值,重新启动,你会发现问候语随之改变,而插件的源代码一行都没动。这就是配置的力量。
Schema 校验:让配置更安全
目前我们的配置还比较宽松——greeting 可以是任何字符串,maxRetries 可以是任何数字。但在实际项目中,你可能希望对配置做更严格的限制。比如:
greeting 不能为空字符串。
maxRetries 必须在 1 到 10 之间。
mode 只能是 'fast' 或 'accurate' 二选一。
Schemastery 提供了丰富的校验方法来满足这些需求:
import type { Context } from '@deepseek-ai/cordis'
import Schema from '@deepseek-ai/schemastery'
export const name = 'validated-plugin'
export interface Config {
apiKey: string
timeout: number
mode: 'fast' | 'accurate'
}
export const Config = Schema.object({
// 必填:用户必须提供 apiKey
apiKey: Schema.string().required(),
// 有默认值:用户不填就用 30000
timeout: Schema.number().default(30000),
// 枚举值:只能是 'fast' 或 'accurate'
mode: Schema.union(['fast', 'accurate']).default('fast'),
})
export function apply(ctx: Context, config: Config) {
// config 已经通过校验,类型安全,可以放心使用
}校验发生在插件加载时。如果用户传入的配置不合法——比如 apiKey 没传,或者 mode 传了 'slow'——插件会加载失败,并给出明确的错误信息,而不是在运行时悄悄出问题。
这种“启动时校验”的设计理念是:配置错误要响亮。与其在运行时因为配置问题而行为异常,不如在启动时就大声报错,让问题尽早暴露、尽快修复。
设计原则:让配置成为“一等公民”
基于 Harness 的配置设计理念,这里总结两条重要的设计原则:
原则一:无硬编码可调参数
凡是不同部署可能需要采用不同值的参数,都必须定义为配置字段。
反例(硬编码):
// 错误:超时时间写死在代码里
const TIMEOUT = 30000正例(可配置):
// 正确:超时时间由用户配置
export interface Config {
timeoutMs: number // 默认值 30000
}检验标准很简单:这个值是否可以在 cordis.yml 中改变,而需要修改代码?如果答案是可以,那这个参数就应该被配置化。
原则二:配置错误要响亮
Schema 中应内建完备的约束,以便插件加载时,就能尽早暴露无效配置。而服务或已注册资源的引用,则统一走依赖注入。
反例(静默失败):
// 错误:配置不合法时悄悄使用默认值,用户毫不知情
const timeout = config.timeout || 30000正例(响亮失败):
// 正确:Schema 校验不通过时,插件直接加载失败并报错
export const Config = Schema.object({
timeout: Schema.number().min(1000).max(60000).default(30000)
})配合 HMR
Harness 支持 HMR(Hot Module Replacement)——这意味着你修改 cordis.yml 配置后无需重启整个服务,框架会自动重新加载受影响的插件。
当你修改了某个插件的 config 后:
框架检测到配置变更。
卸载旧版本的插件实例(所有通过 ctx 注册的资源自动清理)。
用新配置加载新版本的插件实例。
整个过程对用户几乎是透明的。由于所有注册都属于 effect 并会自动清理,替换后不会残留旧实例的注册。你之前学到的“自动清理”机制在这里发挥了重要作用——它让 HMR 变得安全、可靠。
完整代码
以下是本章完成后的完整插件代码(scratch-plugin/src/my-plugin.ts):
import type { Context } from '@deepseek-ai/cordis'
import Schema from '@deepseek-ai/schemastery'
import { defineTool } from '@deepseek-ai/dsh-tools'
export const name = 'greet-tool'
export const inject = ['tools']
export interface Config {
greeting: string
maxRetries: number
verbose?: boolean
}
export const Config = Schema.object({
greeting: Schema.string().default('Hello'),
maxRetries: Schema.number().default(3),
verbose: Schema.boolean().default(false),
})
export function apply(ctx: Context, config: Config) {
console.log(`[greet-tool] greeting configured as: "${config.greeting}"`)
console.log(`[greet-tool] maxRetries: ${config.maxRetries}`)
console.log(`[greet-tool] verbose: ${config.verbose}`)
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 `${config.greeting}, ${args.name}!`
},
})
)
}
对应的 cordis.yml:
- insert:
- id: hello
name: 'file:///E:/ProjectsAI/deepseek-harness/scratch-plugin/src/my-plugin.ts'
config:
greeting: 'Hi there'
maxRetries: 5总结
本章我们完成了从“硬编码”到“可配置”的转变。核心收获如下:
概念 | 说明 |
|---|---|
Config 接口 | TypeScript 类型定义,提供编译时的类型检查 |
Config Schema | Schemastery 校验规则,提供运行时的配置校验和默认值 |
apply(ctx, config) | 插件入口函数接收配置对象作为第二个参数 |
cordis.yml 中的 config | 用户传入配置值的地方 |
Schema.required() | 标记字段为必填 |
Schema.default() | 为字段设置默认值 |
Schema.union() | 限制字段只能取枚举值 |
无硬编码原则 | 所有可调参数都应配置化 |
响亮失败原则 | 配置错误应在加载时暴露,而非运行时 |
HMR | 配置变更自动触发插件重载,无需手动重启 |
你现在已经掌握了一个生产级插件必不可少的能力——配置管理。无论是 API 密钥、超时时间、功能开关还是业务参数,都可以通过配置来让用户自由定制。
下一章,我们将学习如何将你的插件打包成一个可分发的组合包(bundle),方便他人轻松安装和使用。