第六课:DeepSeek Harness 插件配置与生命周期
好,这一课我们用一个具体问题贯穿:
同样传入 6 个数字,为什么插件有时拒绝,有时可以计算?
我们会保持工具名称、计算算法和输入结构不变,只改变插件配置,并观察它什么时候生效。
本课仍使用 0.1.1-rc.2,继续修改你已经跑通的两个文件,不需要重新安装 Harness。
一、先分清两种输入
上一课,工具收到的是:
{
"values": [10, 12, 14]
}
这是“这一次要计算哪些数字”。
本课新增配置:
config:
maxItems: 5
这是“这个插件一次最多允许处理多少个数字”。
两者进入代码的位置不同:
| 内容 | 本例 | 进入哪里 |
|---|---|---|
| 插件配置 | maxItems: 5 | apply(ctx, config) 的 config |
| 单次调用参数 | values: [10,12,14] | execute(args, exec) 的 args |
这里的 maxItems 是我们自己给配置项起的名字,不是 Harness 内置的特殊关键字。
而加载条目中的 config,是框架支持的插件配置入口。官方配置教程
你可以先记住:
配置决定这次运行采用什么规则;调用参数提供这一次处理什么数据。
这句话针对我们当前的插件设计,不意味着所有业务都必须这样划分。具体哪些内容应该可配置,是插件开发者的设计选择。
二、先改造文件
先等当前 Agent 任务结束,在运行 Harness 的终端按一次 Ctrl+C,等到回到 PowerShell 提示符。
然后把现有的 index.mjs 和 cordis.patch.yml 复制一份作为上一课的备份,再修改原文件。
1. 修改配置文件
打开:
D:\DeepSeek\plugins\number-stats\cordis.patch.yml
改为:
- insert:
- id: lesson-number-stats
name: 'file:///D:/DeepSeek/plugins/number-stats/index.mjs'
config:
maxItems: 5
注意三个位置:
config与id、name同级。maxItems缩进在config下。- 保留你已经验证成功的
file:///D:/...路径写法。
现在,这个加载条目不仅告诉 Harness“加载哪个插件”,还附带了“给这个插件什么配置”。
2. 修改插件代码
本课继续采用不依赖额外 npm 包的写法。
配置检查由我们编写的普通函数 readConfig 完成。稍后会明确说明:这和框架自动进行配置校验有什么区别。
将 index.mjs 替换为下面的完整代码:
export const name = 'number-stats';
export const inject = ['tools'];
function readConfig(config) {
if (config === null || typeof config !== 'object' || Array.isArray(config)) {
throw new Error('配置错误:config 必须是对象');
}
for (const key of Object.keys(config)) {
if (key !== 'maxItems') {
throw new Error('配置错误:未知配置项 ' + key);
}
}
const maxItems = config.maxItems === undefined ? 1000 : config.maxItems;
if (!Number.isSafeInteger(maxItems) || maxItems < 1) {
throw new Error('配置错误:maxItems 必须是正的安全整数');
}
return { maxItems };
}
function calculateStats(values, maxItems) {
if (!Array.isArray(values)) {
throw new Error('调用参数错误:values 必须是数组');
}
if (values.length < 1 || values.length > maxItems) {
throw new Error(
'调用参数错误:values 数量应为 1 到 ' + maxItems +
',实际为 ' + values.length
);
}
let sum = 0;
for (const value of values) {
if (!Number.isFinite(value)) {
throw new Error('调用参数错误:每一项都必须是有限数字');
}
sum += value;
}
if (!Number.isFinite(sum)) {
throw new Error('计算错误:数值总和超出本示例支持的范围');
}
return { count: values.length, mean: sum / values.length };
}
export function apply(ctx, config = {}) {
const { maxItems } = readConfig(config);
let calls = 0;
console.info('[number-stats] apply maxItems=' + maxItems);
ctx.tools.register({
name: 'lesson_number_stats',
description: '计算数字的数量和算术平均值;每次接受 1 到 ' + maxItems + ' 个数字。',
parameters: {
type: 'object',
properties: {
values: {
type: 'array',
items: { type: 'number' },
description: '要统计的有限数字数组'
}
},
required: ['values'],
additionalProperties: false
},
output: {
schema: {
type: 'object',
properties: {
count: { type: 'integer' },
mean: { type: 'number' }
},
required: ['count', 'mean'],
additionalProperties: false
},
render(_args, value) {
return [{ type: 'text', text: JSON.stringify(value) }];
}
},
async execute(args, exec) {
const callNumber = ++calls;
console.info('[number-stats] execute #' + callNumber + ' start');
try {
exec.signal.throwIfAborted();
if (
args === null || typeof args !== 'object' || Array.isArray(args) ||
!Object.hasOwn(args, 'values') || Object.keys(args).length !== 1
) {
throw new Error('调用参数错误:参数必须是只包含 values 字段的对象');
}
const result = calculateStats(args.values, maxItems);
console.info('[number-stats] execute #' + callNumber + ' success');
return result;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.info('[number-stats] execute #' + callNumber + ' failed: ' + message);
throw error;
}
}
});
ctx.effect(() => {
return () => {
console.info(
'[number-stats] cleanup maxItems=' + maxItems + ', calls=' + calls
);
};
});
console.info('[number-stats] registered lesson_number_stats');
}
代码已经过本地模拟上下文测试,包括配置检查、不同上限、异常输入和清理回调。下面需要你在实际 Harness 中验证加载、调用及退出行为。
三、先理解新增的四处代码
上一课讲过的输入、输出结构没有改变。这次重点只看新增部分。
1. readConfig:检查规则是否有效
看这一行:
const maxItems = config.maxItems === undefined ? 1000 : config.maxItems;
含义是:
- 配置提供了
maxItems,就使用配置值。 - 没有提供,才使用默认值
1000。
这里的 1000 是“默认值”,不再是无法调整的固定限制。
随后:
if (!Number.isSafeInteger(maxItems) || maxItems < 1) {
throw new Error('配置错误:maxItems 必须是正的安全整数');
}
它拒绝不合适的配置,例如:
maxItems: 0
maxItems: -1
maxItems: '5'
最后一个虽然看起来像数字,但加引号后是字符串,我们没有设计自动转换。
另外,未知配置项也会被拒绝。例如把 maxItems 拼成 maxItem,不会默默使用默认值掩盖这个错误。
2. apply:取得配置,并建立本次运行的状态
export function apply(ctx, config = {}) {
const { maxItems } = readConfig(config);
let calls = 0;
当配置是:
config:
maxItems: 5
通过检查后,这次 apply 中就有:
maxItems = 5
calls = 0
其中:
maxItems保存本次激活所采用的上限。calls记录这次激活期间,执行函数被进入了多少次。
calls 是我们自己添加的教学计数器,不是 Harness 的 Step 数,也不是 Session 的消息数。
3. execute 为什么能使用 apply 里的变量?
执行函数中有:
const callNumber = ++calls;
以及:
const result = calculateStats(args.values, maxItems);
但 calls 和 maxItems 都是在外层 apply 中定义的。
这依靠 JavaScript 的闭包:
注册工具时,框架保存了执行函数;这个函数仍然可以访问它创建时所在作用域中的变量。
所以,不需要每调用一次工具,就重新执行一次 apply。
在没有发生重新激活的情况下:
- 第一次调用使用上限
5,计数变成1。 - 第二次调用仍使用上限
5,计数变成2。 - 第三次调用仍使用上限
5,计数变成3。
这里保存的是该插件本次激活的状态,不是每个聊天会话各有一份的状态。使用同一个插件实例的会话会共享这份计数;新建聊天不应被当作重置它的办法。
另外,工具描述也使用了 maxItems:
description: '计算数字的数量和算术平均值;每次接受 1 到 ' + maxItems + ' 个数字。'
这样模型看到的描述与当前运行规则一致。
但描述只是说明,真正拦截超量输入的,仍然是执行代码中的检查。
4. ctx.effect:登记“以后清理时要做什么”
这段代码分为两层函数:
ctx.effect(() => {
return () => {
console.info(
'[number-stats] cleanup maxItems=' + maxItems + ', calls=' + calls
);
};
});
它的执行时机是:
- 调用
ctx.effect时,外层函数立即执行。 - 外层函数返回一个清理函数。
- 框架保存这个清理函数,在所属插件卸载时执行它。官方 Effect 接口
所以,启动时不会立即打印 cleanup。
本例清理函数只打印日志,没有关闭数据库或释放文件句柄,因为我们的统计工具没有创建这些资源。
还要注意:
工具的自动注销来自
ctx.tools.register的框架管理,不是因为我们打印了一行cleanup。
以后插件创建连接、监听器等资源时,才需要根据资源类型设计相应的清理逻辑。官方插件生命周期说明
四、实验一:上限为 5,观察三次调用
第一步:检查语法并启动
先在powershell中执行:
node --check "D:\DeepSeek\plugins\number-stats\index.mjs"
没有报错,再执行:
npx.cmd @deepseek-ai/dsh@0.1.1-rc.2 --profile web --patch "D:\DeepSeek\plugins\number-stats\cordis.patch.yml"
启动过程中,预期出现:
[number-stats] apply maxItems=5
[number-stats] registered lesson_number_stats
这说明代码读到了配置中的 5,并完成了工具注册。
这些文字是我们自己编写的定位日志,不是 Harness 内置的状态字段。
第二步:调用一次合法输入
在 Web 界面使用 Standard 模式,发送:
请只调用一次 lesson_number_stats,参数为 {"values":[10,12,14]},然后报告工具返回的结果。
预期工具结果:

预期终端日志:
[number-stats] execute #1 start
[number-stats] execute #1 success
注意:不应该因为这次调用,再出现一次 apply maxItems=5。
这里的 success 表示我们的计算代码走到了成功返回的位置;最终仍应核对 Harness 中的工具结果。
第三步:测试超过上限的输入
发送:
这是一次工具参数边界测试。请只调用一次 lesson_number_stats,参数为 {"values":[1,2,3,4,5,6]}。不要拆分数组或修改参数。如果工具返回错误,直接报告错误,不要自动重试。
如果模型确实按要求发起了这次调用,预期终端日志是:

如果模型确实按要求发起了这次调用,预期终端日志是:
[number-stats] execute #2 start
[number-stats] execute #2 failed: 调用参数错误:values 数量应为 1 到 5,实际为 6
工具调用应返回错误,而不是统计结果。
这里的 catch 做了两件事:
console.info(...);
throw error;
先记录失败,再把错误继续交给 Harness 处理,而不是吞掉错误后假装执行成功。
**如果模型没有实际调用,或自行拆成两次调用,这个边界测试就没有按预期完成。**不能把模型口头说“超出上限”当作代码校验已经被执行。
第四步:再次调用合法输入
发送:
请只调用一次 lesson_number_stats,参数为 {"values":[-2,0,8]},报告工具返回的结果。
预期结果:
{"count":3,"mean":2}
如果前面的调用次数完全符合实验安排,终端应出现:
[number-stats] execute #3 start
[number-stats] execute #3 success
这证明了一件重要的事:
一次工具调用失败,不等于整个插件已经卸载或失效。
工具执行层会处理执行函数抛出的错误;这与插件初始化失败是不同的错误路径。官方工具执行约定
五、实验二:修改配置,观察旧状态结束、新状态建立
第一步:正常停止当前 Harness
等当前任务结束,在启动 Harness 的终端按一次 Ctrl+C,等待正常退出。
如果此前恰好进入执行函数三次,预期看到:
[number-stats] cleanup maxItems=5, calls=3
如果实际次数不同,以你的调用记录为准。模型重试、其他会话调用或插件重新激活,都可能使日志与示例不完全相同。
这行日志说明我们的清理回调被执行了,不是说整个应用的所有资源都已经完成清理。仍要等到 PowerShell 提示符重新出现。
CLI 的正常信号退出流程会先处置已挂载的根上下文。官方 CLI 退出行为
第二步:只修改配置中的数字
保持 index.mjs 不变。
将 cordis.patch.yml 改为:
- insert:
- id: lesson-number-stats
name: 'file:///D:/DeepSeek/plugins/number-stats/index.mjs'
config:
maxItems: 10
然后使用刚才同一条启动命令重新运行。
预期看到:
[number-stats] apply maxItems=10
[number-stats] registered lesson_number_stats
这一次,新的 apply 读取到了 10。
第三步:再次传入相同的 6 个数字
发送:
请只调用一次 lesson_number_stats,参数为 {"values":[1,2,3,4,5,6]},报告工具返回的结果。
预期结果:
{"count":6,"mean":3.5}
本次启动后的第一次调用,预期日志是:
这里同时验证了两件事:
- 上限变成
10,同样的数据现在可以处理。 - 计数重新从
1开始,因为重新执行apply时又创建了calls = 0。
计数不是保存到磁盘的历史记录。它只存在于本次激活建立的内存状态中。
为什么这里明确要求重启?
因为:
修改磁盘上的配置文件,不等于运行中的插件已经采用新配置。
本例的执行函数使用的是初始化时取得的 maxItems,不会每次调用都重新读取 YAML。
框架可以通过配置更新或热替换机制重新激活插件,但是否会自动检测你的文件变化,要看具体加载与监听机制。本课使用明确重启,先让执行时机可观察;不能因此总结成“Harness 修改配置永远必须重启整个应用”。官方插件更新接口
六、实验三:故意写错配置,观察错误发生在哪一层
这次会故意让启动失败。先正常停止当前 Harness,再进行修改。
把配置中的值改为:
config:
maxItems: 0
使用同一条命令启动。
预期错误中包含:
配置错误:maxItems 必须是正的安全整数
这次不需要发送聊天消息,启动过程中就会出错。
原因是代码顺序如下:
export function apply(ctx, config = {}) {
const { maxItems } = readConfig(config);
// 配置检查通过后,才会继续注册工具
}
readConfig 抛出异常后,后面的工具注册代码不会执行。
特别注意:我们把 apply maxItems=... 日志写在配置检查之后。因此,配置错误时看不到这条日志,并不意味着 apply 从未被调用;它已经进入,只是在检查配置时失败了。
现在把两种错误放在一起看:
| 情况 | 出错位置 | 本例中的影响 |
|---|---|---|
maxItems: 0 | apply 开头的配置检查 | 插件初始化失败,工具没有完成注册 |
| 上限为 5,调用传入 6 个数字 | execute 中的参数检查 | 这一次调用失败,后续合法调用仍可进行 |
实验完成后,把 maxItems 恢复为 10,再重新启动,留下一个可正常工作的版本。
七、把“配置校验”与官方机制对齐
这里需要补齐一个架构上的重要区别。
我们当前的写法是:
export function apply(ctx, config = {}) {
const { maxItems } = readConfig(config);
}
因此:
- 框架负责传入配置。
- 我们负责在
apply内校验,并补上默认值。
Cordis 还支持插件导出一个名为 Config 的配置校验器。这个导出是可选的;有合适的校验器时,框架可以在插件入口执行前进行配置校验。官方插件接口定义
官方教程通常使用 Schemastery 来编写这个校验器,并在校验器中声明默认值。官方配置校验示例
所以,不要混淆:
- 小写
config:传给插件的配置数据。 - 大写
Config:框架约定的可选校验器导出。
也不要这样写:
export const Config = {
maxItems: 1000
};
然后以为框架会把它当作默认配置。这里需要的是符合框架接口的校验器,不是随意放一个配置对象。
本课先把数据传递、校验位置和执行时机看清楚;后续工程化时,再把校验规则统一到正式的 Schema 中。
更多推荐


所有评论(0)