DeepSeek Harness学习手册
0/33
阅读路径 / 阶段 5
Chapter 05 · Build & Ship

实战与发布

把能力拆成三角色,接任意模型,打成可安装、可分发的组合包。

6 节 · 约 3 小时 · 前置:阶段 4每节含目标 / 前置 / 要点 / 任务 / 自检
本阶段进度 0 / 6
5.1

实战:写一个可替换的能力

约 50 分钟实战 原文 ↗ 仍然成立
审计依据 ·

代码骨架与包结构来自教程;类型导入路径以当前仓库为准。 查看本节审计记录

学习目标

按 Definition → Provider → Consumer 实现 myCap,并在 cordis.yml 中组合加载。

前置知识

完成 4.4 与 3.5。

核心要点
  1. Definition:抽象类继承 Service,声明 execute(request) 与 Request/Result 类型。
  2. Provider:继承抽象类实现 execute,通过 ctx.plugin(MyCapLocal) 注册实现。
  3. Consumer:用 defineTool 暴露工具,inject: ['tools', 'myCap']。
  4. 组合:cordis.yml 同时加载 Provider 与 Consumer;模型只认识工具名,不认识服务。
  5. 数据流:模型 → Consumer → Definition → Provider;换 Provider 时前两层不动。
动手任务

实现一个把输入转大写的 myCap:三个包、一个工具、一次真实调用。然后写第二个 Provider(例如加前缀),只改 cordis.yml 完成替换。

自检
为什么 Consumer 不直接 import Provider?
直接依赖实现会让替换 Provider 时被迫改 Consumer;依赖 Definition 才能保持接口稳定。
Provider 注册为什么用 ctx.plugin?
它把实现类作为一个子插件加载,服务注册与清理都纳入 Fiber 生命周期。
进度保存在本机浏览器
5.2

LLM 适配器:接入任意模型

约 45 分钟模型 原文 ↗ 仍然成立
审计依据 ·

适配器骨架与字段表来自教程;具体类型以 <code>@deepseek-ai/dsh-llm</code> 为准。 查看本节审计记录

学习目标

实现最小 LlmAdapter,把自定义端点接入 dsh,并知道哪些字段不能静默丢弃。

前置知识

完成 4.1;了解流式 API。

核心要点
  1. 继承 LlmAdapter,实现 async *stream(options),逐片产出 StreamChunk。
  2. GenerateOptions 包含 provider、model、messages、system、tools、reasoning、signal 等字段。
  3. 用 ctx.llm.registerAdapter(['my-provider'], adapter) 绑定路由。
  4. 不支持的字段要抛带稳定 code 的 LlmError,不能静默丢弃,否则模型会拿到残缺结果。
  5. 可选覆写 listModels() / resolveModel(),提供模型与 reasoning 元数据。
动手任务

写一个 mock adapter:不调用真实 API,产出固定文本 + usage + finish。注册到 my-provider,在会话里选它并跑一次。再故意不实现 tools 字段,确认会报错而不是静默忽略。

自检
agent-loop 依赖适配器的什么?
它只消费提供方无关的流式生成服务与 StreamChunk 协议,不关心底层是哪家 API。
为什么要保留 signal?
取消与资源释放要靠它完全停稳;不传 signal 会让中止后仍有请求或进程残留。
进度保存在本机浏览器
5.3

StreamChunk 协议与错误处理

约 40 分钟协议 原文 ↗ 仍然成立
审计依据 ·

五条规则与错误处理来自教程;分片字段以 <code>@deepseek-ai/dsh-llm</code> 类型为准。 查看本节审计记录

学习目标

按协议产出分片,并用 LlmError 表达传输与协议故障。

前置知识

完成 5.2。

核心要点
  1. 顺序:block-start → delta → block-end → usage → finish;文本与工具调用各走一遍。
  2. index 从 0 递增;text-delta 与 tool-call-delta 都要带所属块 index,块之间不要交叉。
  3. 五条硬规则:start/end 成对、index 递增、argumentsDelta 是原始 JSON 增量、usage 在 finish 前、finish 必须最后。
  4. 错误用带稳定 code 的 LlmError;每个 HTTP 请求合并 attributionHeaders(),并把 options.signal 传给 fetch。
  5. 取消要完全停稳:不能只发 abort 就返回,要等待资源释放。
动手任务

用 exampleChunks 写一个 mock stream,让消费方解析出文本与工具调用;再故意在 finish 后追加一个 chunk,确认消费方报错。

自检
finish 之后还能发 chunk 吗?
不能。finish 必须是最后一个分片,之后任何分片都会破坏协议。
为什么 usage 必须在 finish 前?
usage 是本次生成的用量事实,finish 是结束信号;先报告用量再结束,消费方才能完整记账。
进度保存在本机浏览器
5.4

组合包 bundle 与 profile

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

manifest 键与目录结构来自教程;字段以当前 dsh CLI 为准。 查看本节审计记录

学习目标

区分 bundle 与 profile,并做出一个可安装的组合包。

前置知识

完成 3.2。

核心要点
  1. bundle 是附带配置层的 npm 包,manifest 声明 dsh.bundle.patch,回答「贡献什么」。
  2. profile 是 $DSH_HOME/profiles/<name> 下的启动配置,manifest 声明 dsh.profile.bundles,回答「由哪些 bundle 按什么顺序组成」。
  3. 没有东西同时是两者;profile 由 dsh plugin 创建维护,不需要手写。
  4. bundle 的 patch 用包名引用插件,安装后由 Node 模块解析找到;用相对源码路径只适合本地 --patch。
  5. 没有 dsh.bundle 声明的包仍然能安装,但只是普通依赖,不激活任何层。
动手任务

做 hello-plugin bundle:package.json 声明 dsh.bundle、cordis.patch.yml 用包名、index.js 导出 apply。装进 demo profile,dump-config 看到 # == dsh-hello-plugin 层。

自检
bundle 与 profile 的 manifest 分别是什么?
bundle 用 dsh.bundle.patch 声明贡献的 patch 文件;profile 用 dsh.profile.bundles 声明有序的 bundle 列表。
为什么 patch 里用包名?
安装后 pnpm 把包链接到 node_modules,Node 的模块解析能按包名找到代码;源码路径只适用于本地 overlay。
进度保存在本机浏览器
5.5

安装插件与配置加载顺序

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

四层顺序与按行替换来自教程;具体输出格式以当前 CLI 为准。 查看本节审计记录

学习目标

说清四层配置叠加顺序,并用 dump-config 验证某一层来自哪里。

前置知识

完成 5.4。

核心要点
  1. 顺序:profile 的 bundles 列表 → profile 的 cordis.patch.yml → home 级 patch → 每个 --patch overlay。
  2. 后应用的层按行胜出;patch 按 id 整行替换整个 config,不是深度合并。
  3. 覆盖一行时必须重述该行需要的所有键,否则丢掉未写的键。
  4. dsh plugin --profile demo add/remove 管理依赖与 bundles;--dump-config 打印叠加后的完整配置树。
  5. 用户可以在自己的 profile patch 里覆盖你的行,不需要改你的包。
动手任务

把 hello 装进 demo,dump-config 找到层标记;再在 profile patch 里覆盖同一行的某个键,确认整行替换的效果(未写的键丢失)。

自检
为什么是整行替换而不是深度合并?
整行替换让「最终生效值」可预测、可审计;深度合并会让某个键来自哪一层变得难以追踪。代价是必须重述所有键。
怎么确认配置真的生效?
用 dsh --profile demo --dump-config 看最终树与层标记,再启动验证实际能力;不要只看安装命令成功。
进度保存在本机浏览器
5.6

发布插件:npm、tarball 与 Git

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

三种方式与 allowBuilds 流程来自教程;pnpm 版本行为以官方文档为准。 查看本节审计记录

学习目标

选对三种分发方式,并避免 Git 安装的构建授权坑。

前置知识

完成 5.4 / 5.5。

核心要点
  1. npm 与 tarball 分发预构建产物,用户安装时不需要构建授权。
  2. Git 安装拉的是源码:作者要提供自包含的 prepare,用户要在 pnpm-workspace.yaml 里 allowBuilds 授权。
  3. 构建脚本会在用户机器上执行,且不在 Agent 沙箱内;只给可信源码授权。
  4. Git 安装要锁 commit(github:you/plugin#sha),防止默认分支变化。
  5. 发布前核对 files、main、dsh.bundle,确认用户拿到的是可加载的产物。
动手任务

用 pnpm pack 打一个 tarball,在干净 profile 里安装并验证工具可调用;再对比 Git 安装第一次失败的提示,写出授权修复步骤。

自检
为什么 Git 安装 TypeScript 包会失败?
Git 拉的是源码,没有任何环节自动运行 build,TypeScript 包没有 lib/ 输出;作者要提供 prepare,用户要允许构建。
哪种方式最适合普通用户?
npm 或 tarball:分发预构建产物,用户零构建授权,安装路径最短。
进度保存在本机浏览器