第六课:DeepSeek Harness 插件配置与生命周期
好这一课我们用一个具体问题贯穿同样传入 6 个数字为什么插件有时拒绝有时可以计算我们会保持工具名称、计算算法和输入结构不变只改变插件配置并观察它什么时候生效。本课仍使用0.1.1-rc.2继续修改你已经跑通的两个文件不需要重新安装 Harness。一、先分清两种输入上一课工具收到的是{ values: [10, 12, 14] }这是“这一次要计算哪些数字”。本课新增配置config: maxItems: 5这是“这个插件一次最多允许处理多少个数字”。两者进入代码的位置不同内容本例进入哪里插件配置maxItems: 5apply(ctx, config)的config单次调用参数values: [10,12,14]execute(args, exec)的args这里的maxItems是我们自己给配置项起的名字不是 Harness 内置的特殊关键字。而加载条目中的config是框架支持的插件配置入口。官方配置教程你可以先记住配置决定这次运行采用什么规则调用参数提供这一次处理什么数据。这句话针对我们当前的插件设计不意味着所有业务都必须这样划分。具体哪些内容应该可配置是插件开发者的设计选择。二、先改造文件先等当前 Agent 任务结束在运行 Harness 的终端按一次CtrlC等到回到 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。这里保存的是该插件本次激活的状态不是每个聊天会话各有一份的状态。使用同一个插件实例的会话会共享这份计数新建聊天不应被当作重置它的办法。另外工具描述也使用了maxItemsdescription: 计算数字的数量和算术平均值每次接受 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/dsh0.1.1-rc.2 --profile web --patch D:\DeepSeek\plugins\number-stats\cordis.patch.yml启动过程中预期出现[number-stats] apply maxItems5 [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 maxItems5。这里的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 的终端按一次CtrlC等待正常退出。如果此前恰好进入执行函数三次预期看到[number-stats] cleanup maxItems5, calls3如果实际次数不同以你的调用记录为准。模型重试、其他会话调用或插件重新激活都可能使日志与示例不完全相同。这行日志说明我们的清理回调被执行了不是说整个应用的所有资源都已经完成清理。仍要等到 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 maxItems10 [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: 0apply开头的配置检查插件初始化失败工具没有完成注册上限为 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 中。