← 返回目录

第 10 卷

第 10 / 43 卷作者:Un-named日期:

子(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 秒版)

  1. **核心问题**:dsh 每次工具调用前模型都重新思考,50 步任务 90%+ 时间在思考

——降档是最快提速

  1. **验证三层**:纯函数单测(决策正确)→ waterfall 契约测试(接线正确)→ 实机

验证(真实循环生效)

  1. **`agent/request` waterfall**:每次模型请求前触发,监听者返回值传给下一个监听

者,实现"保留原配置 + 覆盖某字段"

  1. **`next()` 是 Promise,必须 await**:不 await 直接 spread 会得到空对象,

provider/model 丢失报错

  1. **开发纪律**:先找扩展点(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: booleanallowUpgrade: 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.lengthconst 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' : selectedreturn 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 }}) }

**三个关键点**(都是真实踩过的坑):

  1. **`next()` 是 Promise**:`await next()` 拿到当前配置;不 await 直接 spread 会得

到空对象 → provider/model 丢失 → 报错。

全站分卷索引 · 43 卷
  1. 1第 1 卷
  2. 2第 2 卷
  3. 3第 3 卷
  4. 4第 4 卷
  5. 5第 5 卷
  6. 6第 6 卷
  7. 7第 7 卷
  8. 8第 8 卷
  9. 9第 9 卷
  10. 10第 10 卷
  11. 11第 11 卷
  12. 12第 12 卷
  13. 13第 13 卷
  14. 14第 14 卷
  15. 15第 15 卷
  16. 16第 16 卷
  17. 17第 17 卷
  18. 18第 18 卷
  19. 19第 19 卷
  20. 20第 20 卷
  21. 21第 21 卷
  22. 22第 22 卷
  23. 23第 23 卷
  24. 24第 24 卷
  25. 25第 25 卷
  26. 26第 26 卷
  27. 27第 27 卷
  28. 28第 28 卷
  29. 29第 29 卷
  30. 30第 30 卷
  31. 31第 31 卷
  32. 32第 32 卷
  33. 33第 33 卷
  34. 34第 34 卷
  35. 35第 35 卷
  36. 36第 36 卷
  37. 37第 37 卷
  38. 38第 38 卷
  39. 39第 39 卷
  40. 40第 40 卷
  41. 41第 41 卷
  42. 42第 42 卷
  43. 43第 43 卷