Skip to content
My Blog
Go back

第 10 课 · 毕业项目:写一个完整插件

前置:第 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}`
    },
  }))
}

对着这段代码核对前九课的知识点:

红线(postmortem 0001 的教训):命名空间插件不要加 export default apply——那会让 Loader 丢弃 inject,依赖静默失效。同时把可调参数做成配置而不是硬编码(根 AGENTS.md「No hardcoded tunables」)——把 prefix 的默认值从代码挪到插件 config 是本课的加分项。

10.2 装进 profile(20 分钟)

沿用第 07 课的自定义 profile(或新建):

  1. dsh plugin --profile web-tutorial add <你的插件路径或包名> —— 该命令在 profile 目录里转发 pnpm 安装依赖(out-of-tree 插件装进 profile 的 node_modules)。
  2. 编辑 profile 的 cordis.patch.yml,加一条 insert 行。行的确切字段以现有样例为准:参考 packages/bundle/base/cordis.patch.ymlapps/cli/config/examples/ 下的可选 overlay(它们是可复制的最小样例),语法细则见 docs/cordis-primer.md 的 Loader configuration 一节。
  3. 记住红线:裸插件名必须出现在依赖清单里verify-cordis-config 门禁强制,对应你 10.2 的第 1 步)。

10.3 组合验证(10 分钟,无 key)

pnpm dsh --profile web-tutorial --dump-config

验收点:

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/messagetool/calllesson_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 的文化里,「为什么这么做」和代码一样是一等交付物。

验收清单

延伸挑战(任选)

  1. 配置化:把 prefix 默认值改成插件 config 字段(schemastery 在挂载前校验,第 07 课),在补丁层给出不同 profile 不同前缀。
  2. 关卡体验:在 tools/pre-execute 挂一个监听器,对名为 lesson_echo 的调用返回 {kind:'deny'},重跑并观察 tool/result 的错误物化(第 09 课)。
  3. 守卫体验:给工具声明 timeoutMs,装上 guard 包的 timeout-policy,用一个慢执行验证截止时间来自关卡③的 signal 替换。

常见陷阱

延伸阅读


Share this post:

Previous Post
第 09 课 · 工具执行管线与安全三旋钮
Next Post
第 11 课 · 自扩展:skill 与 extensions