第 10 卷
子(tool/call、turn/start 等),用于监听或注入事件流。想改请求参数用前者,想监听对话流用后者。
**Q6:为什么我的插件装上去没反应?怎么调试?**
三步排查:① 确认 `pnpm install` 无报错且 `node_modules` 里有你的插件;② 确认 `cordis.patch.yml` 的 id/name 拼写正确;③ 重启 `dsh web` 后看进程日志有没有插件加载
信息。如果 host 半有 `console.log`,日志里应该能看到输出。
---
**下一章**:[第 4 章:插件开发实战](./04-plugin-dev.md)(规划中)—— 从零写第一个 host 插件。
第 4 章:插件开发实战
> 本章目标:从零写一个**真实可用的 host 插件**——通过 `agent/request` 扩展点自动调节推理档位。这是一个提速插件的完整拆解,所有代码可运行、可测试。
TL;DR(本章核心,30 秒版)
- **核心问题**:dsh 每次工具调用前模型都重新思考,50 步任务 90%+ 时间在思考
——降档是最快提速
- **验证三层**:纯函数单测(决策正确)→ waterfall 契约测试(接线正确)→ 实机
验证(真实循环生效)
- **`agent/request` waterfall**:每次模型请求前触发,监听者返回值传给下一个监听
者,实现"保留原配置 + 覆盖某字段"
- **`next()` 是 Promise,必须 await**:不 await 直接 spread 会得到空对象,
provider/model 丢失报错
- **开发纪律**:先找扩展点(90% 行为有官方钩子)、逻辑抽纯函数、实机验证不能
省 <details><summary>本章导航</summary>
- [4.1 我们要做什么](#41-我们要做什么)
- [4.2 项目骨架](#42-项目骨架)
- [4.3 纯函数:决策逻辑(零依赖,可单测)](#43-纯函数决策逻辑零依赖可单测)
- [4.4 插件主体:接入 `agent/request` waterfall](#44-插件主体接入-agentrequest-
waterfall)
- [4.5 测试](#45-测试)
- [4.6 给新手的三条开发纪律](#46-给新手的三条开发纪律)
</details>
4.1 我们要做什么
**问题**:dsh 在每次工具调用前模型都会重新思考(`reasoning_effort`)。一个 50 步工具链任务,"思考"占 90%+ 墙钟时间。
**方案**:一个 host 插件,监听 `agent/request` waterfall,根据当前步骤最近的工具调用,把简单轮次的 `reasoning_effort` 从 `high` 降到 `low`。
4.2 项目骨架
<!-- [style] 目录树代码块统一补 text 语言标签 -->
dsh-speed-plugin/ ├── package.json# host 插件声明 ├── tsconfig.json ├── src/ │ ├── effort-decision.ts# 纯函数:决策逻辑(零依赖,可单测) │└── index.ts#
apply(ctx):接入扩展点 └── tests/└── effort-decision.spec.ts
`package.json` 关键字段:
{"name": "dsh-speed-plugin","type": "module","main": "src/index.ts","exports":
{".": { "types": "./src/index.ts", "default": "./src/index.ts" }}, "peerDependencies": {"@deepseek-ai/cordis": "^4.0.1","@deepseek-ai/dsh-agent":
"^0.1.0-rc.6"} }
> ⚠️ 依赖版本务必用 `^0.1.0-rc.6` 线——rc.1 线的 npm 依赖链是断的(见第 3 章常见坑)。
4.3 纯函数:决策逻辑(零依赖,可单测)
`src/effort-decision.ts`:
| export type EffortId = 'low' | 'high' | 'max' export interface ToolCallSample { | name: |
|---|---|
| string //工具名,如'write'、'read'、'bash' | argsSize: number //参数大小(字符数)} |
| export interface EffortDecisionInput { recentCalls: readonly ToolCallSample[] | selected: |
| EffortId //用户基线档 allowDowngrade: boolean | allowUpgrade: boolean } const |
SIMPLE_TOOL_RE = /^(fs|bash|terminal|read|write|grep|glob|edit|ls|cat|rm|cp|touch|mkdir| pwd)/i const HEAVY_ARGS = 800 export function decideEffort(input: EffortDecisionInput):
EffortId {const { recentCalls, selected, allowDowngrade, allowUpgrade } = inputif (recentCalls.length === 0) return selected// 全新提示:保持基线const ratio =
| recentCalls.filter(c => | SIMPLE_TOOL_RE.test(c.name) && c.argsSize < |
|---|---|
| HEAVY_ARGS, ).length / recentCalls.length | const heaviest = recentCalls.reduce((m, c) => |
| Math.max(m, c.argsSize), 0) | if (ratio >= 0.75 && allowDowngrade) return 'low' if |
| (heaviest >= HEAVY_ARGS * 4 && allowUpgrade) return 'max' | if (ratio < 0.75) return |
| allowUpgrade ? 'high' : selected | return selected } |
**为什么拆成纯函数**:决策逻辑与 dsh 运行时解耦——单元测试零依赖、毫秒级、覆盖所有分支,实机只需要验证"注入是否真的发生"。
4.4 插件主体:接入 `agent/request` waterfall
`src/index.ts`:
import type { Context } from '@deepseek-ai/cordis' import { decideEffort, type ToolCallSample } from './effort-decision.ts' export interface SpeedPluginConfig
{enabled: booleanallowDowngrade: booleanallowUpgrade: booleanbaseline: 'low' | 'high' | 'max' } export const DEFAULT_CONFIG: SpeedPluginConfig = {enabled: true,
allowDowngrade: true, allowUpgrade: false, baseline: 'high', } const WINDOW = 8 function
| recentToolCalls(agent: unknown): ToolCallSample[] { | const events = (agent as { session?: |
|---|---|
| { events?: readonly unknown[] } }).session?.events ?? [] | const out: ToolCallSample[] = [] |
| for (let i = events.length - 1; i >= 0 && out.length < WINDOW; i--) { | const e = |
events[i] as { type?: string; data?: { name?: string; arguments?: unknown } } | undefined if (e?.type !== 'tool/call') continueout.push({name: e.data?.name ?? 'tool',
argsSize: typeof e.data?.arguments === 'string' ? e.data.arguments.length : 0,})} return out.reverse() } export function apply(ctx: Context, config: SpeedPluginConfig =
DEFAULT_CONFIG): void {if (!config.enabled) return// 边界适配:npm 包未 re-export 官方事件类型增强,这里放宽签名(第 3 章常见坑)const on = ctx.on as unknown as (event:
string,handler: (payload: Record<string, unknown>, next: () => unknown) => unknown | Promise<unknown>,) => voidon('agent/request', async (payload, next) => {const
seed = await next() as { reasoningEffort?: unknown }// ⚠️ 必须 await!const calls =
| recentToolCalls(payload.agent) | const effort = decideEffort({ | recentCalls: calls, |
|---|---|---|
| selected: config.baseline, | allowDowngrade: config.allowDowngrade, | allowUpgrade: |
| config.allowUpgrade, | }) console.log(`[speed-plugin] calls=${JSON.stringify(calls)} | |
| => reasoningEffort=${effort}`) | return { ...seed, reasoningEffort: effort } | }) } |
**三个关键点**(都是真实踩过的坑):
- **`next()` 是 Promise**:`await next()` 拿到当前配置;不 await 直接 spread 会得
到空对象 → provider/model 丢失 → 报错。
全站分卷索引 · 43 卷
- 1第 1 卷
- 2第 2 卷
- 3第 3 卷
- 4第 4 卷
- 5第 5 卷
- 6第 6 卷
- 7第 7 卷
- 8第 8 卷
- 9第 9 卷
- 10第 10 卷
- 11第 11 卷
- 12第 12 卷
- 13第 13 卷
- 14第 14 卷
- 15第 15 卷
- 16第 16 卷
- 17第 17 卷
- 18第 18 卷
- 19第 19 卷
- 20第 20 卷
- 21第 21 卷
- 22第 22 卷
- 23第 23 卷
- 24第 24 卷
- 25第 25 卷
- 26第 26 卷
- 27第 27 卷
- 28第 28 卷
- 29第 29 卷
- 30第 30 卷
- 31第 31 卷
- 32第 32 卷
- 33第 33 卷
- 34第 34 卷
- 35第 35 卷
- 36第 36 卷
- 37第 37 卷
- 38第 38 卷
- 39第 39 卷
- 40第 40 卷
- 41第 41 卷
- 42第 42 卷
- 43第 43 卷