DeepSeek Harness学习手册
0/33
阅读路径 / 阶段 3
Chapter 03 · Plugin Basics

插件基础

从 apply(ctx) 到生命周期:写出可加载、可清理、可配置的插件。

8 节 · 约 3.5 小时 · 前置:阶段 2每节含目标 / 前置 / 要点 / 任务 / 自检
本阶段进度 0 / 8
3.1

第一个插件:apply(ctx) 与三种形态

约 35 分钟插件 原文 ↗ 仍然成立
审计依据 ·

与教程及官方事故复盘 0001 的描述一致。 查看本节审计记录

学习目标

写出最小插件,理解 ctx 的作用,并避开 default export 丢 inject 的坑。

前置知识

源码安装完成;会 TypeScript 基础。

核心要点
  1. 插件导出 name 与 apply(ctx);框架加载时调用 apply,ctx 既是注册入口也是资源账本。
  2. 三种形态:函数、对象、Service 子类;大多数插件用函数形式,需要提供服务时用类形式。
  3. 对象形态不要再写 export default apply:Loader 会取 default 裸函数,命名空间上的 inject 会整个丢失(官方复盘 0001)。
  4. 本地开发时插件路径必须是绝对路径;相对路径不会被 Loader 按预期解析。
  5. apply 里不要做长时间阻塞;重活应注册成工具或后台任务。
动手任务

写一个 hello-plugin,加载时打印日志;再改成对象形态并声明 inject: ['tools'],用 dump-config 或日志确认 inject 没有丢。

跟着做

从已完成安装和构建的官方源码目录开始。这个插件只输出加载日志,不依赖模型或 API Key。

  1. 在源码根目录创建 scratch-plugin/src 目录。
  2. 将下面的完整模块保存为 scratch-plugin/src/study-plugin.ts。
  3. 接着按 3.2 的配置加载它;仅保存文件不会自动加载插件。
scratch-plugin/src/study-plugin.ts
export const name = 'study-plugin'

export function apply() {
  console.log('[study-plugin] ready')
}

预期结果

完成 3.2 的加载后,终端出现 [study-plugin] ready。当前示例没有注册工具,工具列表不增加条目。

没有得到预期结果?
  • 没有日志:先检查是否传入 3.2 的 patch 文件。
  • 模块无法解析:确认文件名与配置中的绝对路径一致。
  • 只有文件保存成功:继续完成加载步骤,再判断插件是否工作。

文档对照: · 官方示例与说明。示例未在你的环境或真实模型端点运行。

自检
ctx 到底是什么?
框架传给插件的上下文对象:你通过它注册事件、工具、服务,它也记录这些注册,卸载时按作用域清理。
为什么 default export 会丢 inject?
Loader 优先取模块的 default 导出;如果 default 是裸函数,命名空间上声明的 name/inject 不会被一起取走,插件就在空依赖下启动。
进度保存在本机浏览器
3.2

加载本地插件:cordis.yml + --patch

约 30 分钟插件 原文 ↗ 仍然成立
审计依据 ·

路径与 patch 行为来自教程;不同版本的 Loader 报错文案可能不同。 查看本节审计记录

学习目标

用 patch overlay 把本地插件加载进 Web UI,并解释 profile 与 patch 的分工。

前置知识

完成 3.1;有源码仓库。

核心要点
  1. cordis.yml 是 patch overlay;insert 用于插入插件行。
  2. name 写绝对路径;patch 文件只贡献配置,不改变 Loader 解析模块路径的 profile 目录。
  3. 启动:pnpm dsh web --patch ./scratch-plugin/cordis.yml。
  4. profile 决定应用由哪些组合包组成;patch 只是在启动时叠加的一层。
  5. 交付给别人时改用组合包(bundle),不要让用户手改绝对路径。
动手任务

加载 hello 插件,确认三件事:终端出现加载日志、插件列表能看到它、dump-config 里有对应层。然后把插件路径改成相对路径,观察失败。

跟着做

延续 3.1 的 study-plugin.ts。在官方源码根目录执行 pwd,取得实际绝对路径,并替换 YAML 中的 /absolute/path/to/deepseek-harness。

  1. 将第一段保存为 scratch-plugin/cordis.yml,替换绝对路径前缀。
  2. 在已构建的源码根目录执行第二段命令。
  3. 查看终端加载日志,再核对 Web UI。
scratch-plugin/cordis.yml
- insert:
    - id: study-hello
      name: '/absolute/path/to/deepseek-harness/scratch-plugin/src/study-plugin.ts'
终端 · 加载插件
pnpm dsh web --patch ./scratch-plugin/cordis.yml

预期结果

终端输出 [study-plugin] ready,Web UI 能打开;停止该次启动后,插件不会凭此 patch 自动加入其它启动配置。

没有得到预期结果?
  • 模块不存在:确认替换了 /absolute/path/to/deepseek-harness,并检查文件确实存在。
  • YAML 解析失败:使用空格缩进,不要混入 Tab。
  • 缺少构建产物:先在官方源码目录完成 pnpm install 和 pnpm run build。

文档对照: · 官方示例与说明。示例未在你的环境或真实模型端点运行。

自检
为什么必须是绝对路径?
Loader 解析模块时以 profile 目录为基准,相对路径会指到错误位置;patch 本身不会改变这个基准。
--patch 和 bundle 怎么选?
本地开发调试用 --patch;要分发给别人、需要版本与依赖管理时用 bundle。
进度保存在本机浏览器
3.3

自动清理:ctx.effect 与资源账本

约 30 分钟生命周期 原文 ↗ 仍然成立
审计依据 ·

清理机制与异步处置器行为来自教程;具体 disposer 调度以 Cordis 当前实现为准。 查看本节审计记录

学习目标

用 ctx.effect 管理定时器等外部资源,并验证插件卸载后资源真的停止。

前置知识

完成 3.1 / 3.2。

核心要点
  1. 通过 ctx 注册的监听、工具、LLM 适配器会自动撤销;卸载时按注册顺序的逆序清理。
  2. 定时器、网络连接等不在自动列表里的资源,用 ctx.effect(() => disposer) 显式清理。
  3. 异步 disposer 会并发执行,不保证逐个完成;有顺序依赖的清理步骤要放进同一个 effect,由它串行等待。
  4. 先注册后清理:后注册的资源先被清理。
  5. 资源泄漏的典型症状是重复监听、重复注册、卸载后仍有输出。
动手任务

写一个每 5 秒打印心跳的插件,加载后卸载;观察 15 秒内是否还有输出。再把定时器改成不返回 disposer,复现泄漏。

自检
通过 ctx 注册的定时器还要手动 clearInterval 吗?
如果定时器是通过 ctx.effect 创建并返回 disposer,不需要;直接使用 Node API 创建而不交给 ctx 的,框架不一定能追踪。
为什么清理顺序重要?
后注册的资源可能依赖先注册的资源;逆序清理能先拆掉上层依赖,再释放底层资源。
进度保存在本机浏览器
3.4

声明依赖:inject 与可选服务

约 30 分钟依赖 原文 ↗ 仍然成立
审计依据 ·

必需/可选依赖与服务消失行为与教程一致。 查看本节审计记录

学习目标

区分必需依赖与可选依赖,并解释服务消失后插件为什么会自动重载。

前置知识

完成 3.1。

核心要点
  1. export const inject = ['tools'] 声明必需依赖;服务未就绪时 Fiber 停在 PENDING,不执行 apply。
  2. 可选依赖不写 inject,在使用处用 ctx.get('metrics') 查询,可能得到 undefined。
  3. 运行中必需服务消失 → 依赖它的插件自动 dispose;服务回来 → 自动重新加载。
  4. 自动重载不等于恢复运行时数据;需要保留的状态要外置。
  5. 内置服务:ctx.tools(工具)、ctx.llm(模型)、ctx.agents(子智能体)。
动手任务

给插件加一个可选的 metrics 依赖:没有服务时正常加载并跳过记录;有服务时调用 record。再用 dump-config 确认依赖声明。

自检
PENDING 是错误吗?
不是。它表示插件已声明但依赖未就绪;依赖一直不来就一直等。若配置错误则会进入 FAILED,两者要分开看。
自动重载会保留内存状态吗?
不会。重载创建新的 Fiber,旧注册被清理;需要延续的状态放数据库、文件或外部服务。
进度保存在本机浏览器
3.5

第一个工具:defineTool

约 35 分钟工具 原文 ↗ 仍然成立
审计依据 ·

defineTool 字段与行为来自教程;复杂 schema 能力以 <code>@deepseek-ai/dsh-tools</code> 类型为准。 查看本节审计记录

学习目标

用 defineTool 定义 greet 工具,让模型真正调用,并理解 schema 如何进入提示词。

前置知识

完成 3.4;tools 服务可用。

核心要点
  1. 字段:name、description、parameters、output.schema、output.render、execute。
  2. schema 会自动组装进模型提示词;description 决定模型「什么时候选它」,parameters 决定「怎么填」。
  3. execute 返回规范值;render 把规范值转成模型可见内容,两者解耦,同一规范值可以有不同渲染。
  4. 参数在进入策略前已被冻结;工具层不要再改写参数,历史、审计与执行必须一致。
  5. 高风险工具要配权限门禁或审批,不要只靠 description 里写「请小心」。
动手任务

写一个 word_count 工具:输入文本,返回字符数与词数;测试空串、超长文本、缺参数三种情况,记录错误是否可读。

自检
规范值和渲染内容有什么区别?
规范值是 execute 返回、output.schema 声明的结构化值;渲染内容是模型看到的形式。前者用于校验与审计,后者用于展示。
模型怎么知道要调用 greet?
name、description、parameters、output 会自动进入提示词;模型根据这些信息判断是否调用、传什么参数。
进度保存在本机浏览器
3.6

插件配置:Config 与 Schemastery

约 30 分钟配置 原文 ↗ 仍然成立
审计依据 ·

Config/schema 约定与 HMR 行为来自教程。 查看本节审计记录

学习目标

用 Config 接口 + 同名 schema 接收配置,并让非法配置在加载时失败。

前置知识

完成 3.5。

核心要点
  1. 导出同名 Config 接口与 Schemastery schema;默认值写在 schema 字段上。
  2. apply(ctx, config) 的第二个参数就是校验后的配置。
  3. 必填、枚举、范围等约束在 schema 表达;非法配置会让插件加载失败并给出明确错误。
  4. 原则:凡是不同部署可能取不同值的参数,都定义成配置字段,不要硬编码。
  5. 配置变更会触发 HMR;重载会清理旧注册,注意别把运行时状态只放在内存。
动手任务

给 greet 加 greeting 与 maxRetries 两个配置:测默认值、cordis.yml 覆盖、非法类型三组,确认错误发生在加载时。

自检
为什么不能导出普通对象作为 Config?
Cordis 要求 Standard Schema 接口,普通对象没有校验与默认值能力,加载时不会按预期处理。
HMR 会保留插件状态吗?
不会自动保留。热替换会卸载旧实例、加载新实例;需要延续的状态要外置或显式持久化。
进度保存在本机浏览器
3.7

生命周期:Fiber 状态机

约 35 分钟生命周期 原文 ↗ 仍然成立
审计依据 ·

状态机与清理顺序来自教程;Cordis 后续版本可能增加状态。 查看本节审计记录

学习目标

说出 Fiber 的状态流转,并能用状态区分「缺依赖」与「加载失败」。

前置知识

完成 3.4。

核心要点
  1. 状态:PENDING → LOADING → ACTIVE → UNLOADING → DISPOSED;初始化异常进入 FAILED。
  2. 一个插件模块可以有多个 Fiber:同一插件在不同上下文中加载,各自有独立生命周期。
  3. Fiber 保存校验后的配置、服务依赖与注册资源;卸载时执行清理。
  4. 依赖未满足停在 PENDING;配置/初始化错误进入 FAILED,不能都当成缺依赖。
  5. 自动重载是新 Fiber,不是恢复旧内存。
动手任务

在插件里打印 ctx.fiber.state。分别制造「缺依赖」与「配置类型错误」,记录两次状态与日志差异。

自检
PENDING 和 FAILED 怎么区分?
PENDING 是等待依赖,通常日志安静;FAILED 是初始化或配置校验抛错,会有明确错误。先看错误信息,不要盲目加依赖。
卸载时资源按什么顺序清理?
按注册顺序的逆序撤销;异步 disposer 并发执行,有顺序要求的清理要放进同一个 effect。
进度保存在本机浏览器
3.8

依赖驱动加载与嵌套上下文

约 30 分钟依赖 原文 ↗ 仍然成立
审计依据 ·

依赖驱动与子上下文行为来自教程;重载语义以 Cordis 当前实现为准。 查看本节审计记录

学习目标

用依赖表达加载顺序,用 ctx.plugin 创建子插件,并验证子插件随父卸载。

前置知识

完成 3.7。

核心要点
  1. 加载顺序由服务依赖决定,不需要手写 setTimeout 或启动编排。
  2. 服务替换会触发依赖插件自动重载;旧监听、工具、适配器不会残留。
  3. ctx.plugin(child) 创建子 Fiber:继承父上下文,但有独立生命周期。
  4. 子插件随父插件卸载;适合按功能分组,不要用它逃避依赖声明。
  5. 自动重载防止调用已不存在的服务,也会让旧状态丢失。
动手任务

写一个父插件,在 apply 里 ctx.plugin(child) 注册子插件;子插件注册一个工具。卸载父插件后,确认工具消失、子 Fiber 也 disposed。

自检
依赖驱动加载解决了什么?
它把「谁先启动」从人工编排变成声明式依赖:服务就绪才加载,服务消失就卸载,服务回来再重载。
子插件能独立卸载吗?
子 Fiber 有独立生命周期,可以由父上下文管理;父插件卸载时子插件一定被处置。
进度保存在本机浏览器