前置:第 07、08、09 课 | 预计时长:120–180 分钟 | 动手环节:本课就是动手
你将学会
从零走完一次「扩展 dsh」的完整闭环:写插件 → 装进 profile → 组合验证 → 行为验证 → 留下决策记录。这是本课程的毕业项目:前九课学没学会,做一遍就见分晓。
项目目标
写一个 echo 工具插件(模型可调用:把一段文本原样返回,并附带可配置前缀),装进你自己的 profile,让它出现在组合树里,并验证其执行链路。全程不修改 deepseek-harness/ 仓库的任何文件——这正是 out-of-tree 扩展的意义。
步骤
10.1 最小插件形态(15 分钟)
dsh 插件就是 Cordis 插件。权威的最小形态见 docs/cookbook/adding-a-tool.md(分步教程见 docs/user/develop/basic/tool.zh.md,生产级三包示例是 packages/shell/tool-bash):
// dsh-lesson-echo/src/index.ts
import type { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
export const name = 'dsh-lesson-echo'
export const inject = ['tools']
export function apply(ctx: Context) {
ctx.tools.register(defineTool({
name: 'lesson_echo',
description: 'Echo the given text back, with an optional prefix.',
parameters: {
text: { type: 'string', required: true, description: 'Text to echo' },
prefix: { type: 'string' },
},
output: {
schema: { type: 'string' },
render: (_args, value) => [{ type: 'text', text: value }],
},
async execute(args, exec) {
return `${args.prefix ?? ''}${args.text}`
},
}))
}
对着这段代码核对前九课的知识点:
export function apply(ctx)—— 插件入口;inject = ['tools']声明依赖(第 02 课:可用性驱动加载);ctx.tools.register(...)基于副作用注册:插件 fiber 被拆除时工具自动注销(第 02 课:注册即可逆);defineTool在execute运行前按 schema 校验模型生成的arguments,args是已校验的只读输入;exec.signal是必须遵守的取消信号(第 09 课关卡③);- schema 只投影
name/description/parameters给模型,自动进入系统提示词组装(第 09 课 9.1)。
红线(postmortem 0001 的教训):命名空间插件不要加 export default apply——那会让 Loader 丢弃 inject,依赖静默失效。同时把可调参数做成配置而不是硬编码(根 AGENTS.md「No hardcoded tunables」)——把 prefix 的默认值从代码挪到插件 config 是本课的加分项。
10.2 装进 profile(20 分钟)
沿用第 07 课的自定义 profile(或新建):
dsh plugin --profile web-tutorial add <你的插件路径或包名>—— 该命令在 profile 目录里转发 pnpm 安装依赖(out-of-tree 插件装进 profile 的node_modules)。- 编辑 profile 的
cordis.patch.yml,加一条 insert 行。行的确切字段以现有样例为准:参考packages/bundle/base/cordis.patch.yml与apps/cli/config/examples/下的可选 overlay(它们是可复制的最小样例),语法细则见docs/cordis-primer.md的 Loader configuration 一节。 - 记住红线:裸插件名必须出现在依赖清单里(
verify-cordis-config门禁强制,对应你 10.2 的第 1 步)。
10.3 组合验证(10 分钟,无 key)
pnpm dsh --profile web-tutorial --dump-config
验收点:
- 组合树里出现你的插件条目,且 provenance 指向你 profile 的补丁层;
- 故意把补丁里的插件名打错再 dump 一次,确认看到
entry not found警告(第 07 课:未命中不静默)。
10.4 行为验证(30 分钟)
有 DEEPSEEK_API_KEY:真实调用要走 headless 形态,所以从头就把插件装进一个基于 headless 的 profile(把 $DSH_HOME/profiles/headless 复制为 headless-tutorial,10.2 的两步对它执行),然后:
pnpm dsh --profile headless-tutorial "用 lesson_echo 工具回显一句 'graduation',前缀 'dsh: '"
然后到会话存储里翻这一次会话的日志,按 seq 找出事件链:user/message → tool/call(lesson_echo)→ 你的工具执行 → tool/result(第 04、09 课的事件序)。若工具策略是 ask,还会看到 approval/asked / approval/decided 事件对(第 09 课 9.3)。
无 key:给插件包加一个最小单测(vitest),直接调用 execute 验证返回值与参数校验(喂一个缺 text 的参数,确认 defineTool 的校验生效)。再跑 pnpm dsh --profile web-tutorial --dump-config 确认条目注入。
10.5 留下决策记录(20 分钟)
仿照 .agents/notes/ 的制度(第 13 课展开),为你的插件写一条 Agent Note 草稿:
# Agent Note: <你的插件主题>
Status: implemented
## Problem // 为什么需要这个工具/插件
## Decision // 做了什么(用现在时)
## Alternatives considered // 必填:放弃了什么、为什么
## Consequences // 后果与验证方式
用 pnpm run verify-agent-note-format 校验格式。这一步的意义:dsh 的文化里,「为什么这么做」和代码一样是一等交付物。
验收清单
- 插件不含 default export;
inject只声明真正需要的键; -
--dump-config能看到条目及来源层;打错名能看到警告; - 工具出现在模型的工具投影里(只有三字段);
- 有 key:日志里能看到完整
tool/call → tool/result链;无 key:单测通过 + dump 验证; - 写了一条 Agent Note 且通过格式校验。
延伸挑战(任选)
- 配置化:把
prefix默认值改成插件config字段(schemastery 在挂载前校验,第 07 课),在补丁层给出不同 profile 不同前缀。 - 关卡体验:在
tools/pre-execute挂一个监听器,对名为lesson_echo的调用返回{kind:'deny'},重跑并观察tool/result的错误物化(第 09 课)。 - 守卫体验:给工具声明
timeoutMs,装上guard包的timeout-policy,用一个慢执行验证截止时间来自关卡③的 signal 替换。
常见陷阱
- 加了
export default→ Loader 丢inject,插件可能「加载了但不工作」(postmortem 0001 全貌见第 11 课)。 - 只改了补丁没装依赖 → 组合期 import 失败大声报错(第 07 课 fail-loud)。
- 在
execute里读args之外的可变共享状态当同步事实 → 回顾docs/defensive-patterns.md(第 11 课细讲)。
延伸阅读
- 仓库:
docs/cookbook/adding-a-tool.md(工具约定权威)、docs/user/develop/basic/tool.zh.md(分步教程)、docs/cookbook/adding-a-package.md(在仓库内加包的对应流程) - 维基:《capability-seam》
- 下一课:第 11 课 · 自扩展:skill 与 extensions