解剖 DSH 官方插件:tool-bash 与 tool-fs 的能力分层、源码地图与生产级写法
CFzjy AI Engineer |
|
| 「 让 AI 交互更智能,让技术落地更高效 」|欢迎技术探讨 / 项目合作,关注解锁大模型与智能交互的无限可能! | |
解剖 DSH 官方插件:tool-bash 与 tool-fs 的能力分层、源码地图与生产级写法
📚《DeepSeek Harness 插件架构》连载专栏 · 第 10 篇
原理篇(0105)、导航篇(06)、开发实战篇(0709)都结束了。从这篇开始进阶实战篇:不再自己造轮子,而是把官方已经交付的生产级插件逐层拆开,看它们是怎么把前九篇讲过的契约(生命周期、依赖注入、事件、工具、客户端)组合成一套能扛住真实部署的代码。官方文档里最值钱的一句话是:“packages/shell/tool-bash 是生产级的三包示例。”——本篇就用tool-bash和tool-fs两个官方工具插件做标本,拆出它们共同的架构秘密:能力分层(capability seams)。
痛点一:自己写的工具插件"能跑",但一到换环境、加沙箱、上后台任务就散架。因为把"能力定义、能力实现、能力消费"三件事塞进了一个插件里。
痛点二:看不懂官方插件的源码。一打开tool-bash的目录,一堆文件不知道谁在干什么——因为缺一张"源码地图"。
痛点三:命名、包结构、README 全靠拍脑袋。结果插件市场上不去、CI 过不了、别人看不懂——因为不懂官方那套"角色命名表"和"包不变式"。
网上讲 DSH 的教程大多停留在"写个 greet 工具",本篇带你直接读官方生产级代码。读完你会拿到:能力分层三件套的完整地图、tool-bash四条设计哲学 + 源码地图、tool-fs的"工具即执行器、策略即门禁"机制、一张生产级写法的纪律清单。想追完整系列(下一篇:改造官方插件为己用),可以先关注我的 CSDN 主页。
文章目录
- 解剖 DSH 官方插件:tool-bash 与 tool-fs 的能力分层、源码地图与生产级写法
- 一、为什么解剖官方插件:从"能跑"到"生产级"的鸿沟
- 二、能力分层三件套:Service Definition / Provider / Consumer
- 2.1 shell 三组件:DSH 官方给的标准模板
- 2.2 tool-fs 的三件套 + 一个策略包
- 三、tool-bash 解剖:四条设计哲学与源码地图
- 3.1 官方自述的四条设计哲学
- 3.2 源码地图:一个生产级工具的三个文件
- 3.3 最小组合:把三层拼起来
- 四、tool-fs 解剖:工具就是执行器,策略是事件门禁
- 4.1 官方自述的核心一句话
- 4.2 四个工具 + 稳定错误码
- 4.3 源码地图:九个文件的薄切片
- 五、生产级写法清单:从官方插件学到的纪律
- 5.1 命名角色表:不要乱起类名
- 5.2 包不变式:package.json 的硬约束
- 5.3 高频避坑速查表
- 结语
一、为什么解剖官方插件:从"能跑"到"生产级"的鸿沟
第 08 篇你写出了一个能注册、能执行的greet工具。但把它和官方tool-bash放一起,差距立现:官方工具能切换执行器(本机 / 沙箱)、能跑后台任务、能升权、能把退出码报告给模型而不是抛错、能生成可回放的 UI 卡片。这些能力没有一个来自"写得更多",而是来自一个结构决策——把可替换的能力拆成三个独立演进的包。
官方《实操手册:添加 workspace 包》把这个决策写成了原则:“对于可替换的能力,当 Service Definition / Service Provider / Consumer 角色需要独立演进时,将它们拆分到不同包中——shell 三组件是模板。”这就是 DSH 官方插件最大的架构秘密:能力分层。一个"可替换的能力"(比如"跑 bash")被拆成三层,各自是独立 npm 包,各自能独立发版、独立替换。
用一个类比:能力分层像"插座标准 vs 插头 vs 电器"。插座标准(Service Definition)规定了"电压 220V、三孔";插头(Service Provider)有国标、欧标、美标多种实现;电器(Consumer)只认插座标准,插哪个插头都能用。换插头(换执行器)不需要改电器(工具)——这正是官方插件能"换沙箱、换后端、换模型"而不动核心代码的原因。
二、能力分层三件套:Service Definition / Provider / Consumer
2.1 shell 三组件:DSH 官方给的标准模板
tool-bash家族是官方钦定的"三包模板",它的分层长这样:
三层职责一句话各归各:Definition 定义约定(ctx.shell长什么样、resolve()返回什么);Provider 提供实现(本机执行还是沙箱执行,同一约定下可互换);Consumer 消费能力(把 shell 能力注册成模型可调的bash工具)。dsh-tool-bash官方自述说得很清楚:“本工具是 bash 能力的消费方角色:它注册 bash schema、渲染结果并解析每次调用的策略,进程机制归执行器 seam 所有。”
2.2 tool-fs 的三件套 + 一个策略包
tool-fs也走同一套分层,但多了一个关键的策略包:
| 层 | shell(bash)家族 | fs(文件)家族 | 职责 |
|---|---|---|---|
| Definition | dsh-shell | dsh-fs | 定义ctx.shell/ctx.fs约定 |
| Provider | dsh-bash-local/dsh-bash-sandbox | dsh-fs-local/dsh-fs-sandbox | 执行器 / 后端,可互换 |
| Consumer | dsh-tool-bash | dsh-tool-fs | 把能力注册成模型可调工具 |
| Policy(可选) | — | dsh-fs-observation-policy | 事件门禁:先读后写、防陈旧覆盖 |
注意这个表格的最后一层——tool-fs比tool-bash多出的策略包,是理解 DSH 官方设计的第二把钥匙(第四章细讲)。
三、tool-bash 解剖:四条设计哲学与源码地图
3.1 官方自述的四条设计哲学
tool-bash的 README 在"理解实现"一节里,把设计哲学写成了四条,每条都对应一个踩坑:
| 哲学 | 一句话 | 解决的真实问题 |
|---|---|---|
| shell seam 的模型侧消费方 | 工具只管注册 schema + 渲染结果,进程机制归执行器 | 想换沙箱/本机,不用改工具代码 |
| 请求只来自命名参数 | 工具从不暴露 stdin / env / stdoutMaxBytes | 模型传的键无法替换受管值,杜绝注入 |
| 非零退出只报告、不失败 | 只有 spawn 错误才抛工具错误,退出码交给模型解读 | 命令失败≠基础设施故障,模型要能继续排查 |
| 组合了任务运行时,每条命令都归它 | 调用一启动就登记到ctx.jobs | 后台运行、超时交接、界面杀停统一成一条链路 |
其中"请求只来自命名参数"最值得展开:工具只用command/workdir/timeoutMs/信号字段,加上注册表收集的dshEnv来构建请求,模型的输入键无法覆盖受管的环境变量。这是安全设计——DSH_*环境由系统注入,模型碰不到。
3.2 源码地图:一个生产级工具的三个文件
官方的源码分得极薄,每个文件一个职责:
| 文件 | 职责 |
|---|---|
src/index.ts | 插件入口:工具注册、提示词区段、参数校验、升权、请求组装 |
src/background.ts | 管理异步 shell 准备、把进程结算映射为任务结果、输出环读取渲染 |
src/render.ts | 模型侧结果文本:流、标记、截断通知 |
第 08 篇讲过defineTool的四件套,tool-bash的index.ts就是那套契约的满血版:注册bash工具、注入tools/shell/systemPrompt/shellEnv四个服务、在校验后组装请求、处理沙箱升权。而render.ts印证了第 08 篇的"渲染与执行分离":模型看到的结果文本 = stdout +[stderr]区段 + 一堆条件标记(截断、超时、信号、退出码),每个标记占一行,且退出标记同时充当 UI 卡片的退出状态 pill——dsh-shell共享的parseExitStatus从输出体里消费它,回放时不重复显示。
3.3 最小组合:把三层拼起来
把 shell 三件套加后台任务拼成一个最小可用组合:
-name:'@deepseek-ai/dsh-bash-local'# Provider:本机执行器-name:'@deepseek-ai/dsh-shell-env'# 受管 DSH_* 环境注册表-name:'@deepseek-ai/dsh-tool-bash'# Consumer:bash 工具# Optional: background jobs(后台任务运行时 + 控制工具)-name:'@deepseek-ai/dsh-jobs-local'-name:'@deepseek-ai/dsh-tool-jobs'这个 YAML 本身就是一张"依赖注入实战图":dsh-tool-bash的inject声明了tools/shell/systemPrompt/shellEnv四个服务,只有这些提供方(dsh-bash-local、dsh-shell-env)挂载后,工具才会脱离 PENDING 进入 ACTIVE。缺了执行器,工具"永远不会激活"——这正是第 09 篇讲的"装死"在官方插件里的标准答案。
四、tool-fs 解剖:工具就是执行器,策略是事件门禁
4.1 官方自述的核心一句话
tool-fs的设计哲学比tool-bash更凝练,一句话点破:“工具就是执行器;策略是事件门禁。”拆开理解:工具本身不注入策略服务、不检查任何缓存——它每次变更都通过ctx.waterfall向单一意图槽位请求防护,每个操作只在成功后发出fs/observed事件。读写是否被允许、版本是否陈旧,全由外部的策略插件(dsh-fs-observation-policy)监听事件来决定。
这正好是第 04 篇"事件系统"和第 08 篇"策略别内建进工具"思想的生产级落地:策略被彻底外置成独立插件,工具回归纯粹的执行器。
4.2 四个工具 + 稳定错误码
tool-fs注册四个工具,参数和行为都写得清清楚楚:
| 工具 | 参数 | 行为 |
|---|---|---|
read | file_path、offset?、limit? | 带行号的 UTF-8 内容 + 分页 footer,offset从 1 开始 |
read_image | file_path | 读 PNG/JPEG/WebP/GIF,按文件签名识别格式 |
write | file_path、content | 创建或原子替换;有策略时覆盖需先读 |
edit | file_path、old_string、new_string、replace_all? | 字面量替换,默认要求唯一匹配 |
失败的"恢复指令"设计尤其值得学:FS_NOT_OBSERVED规范化为“cannot modify “FS_STALE_VERSION追加“— re-read the file, then retry”。错误码是稳定的、消息带恢复动作——模型读到就能自己修复,而不是卡死在"出错了"三个字上。
4.3 源码地图:九个文件的薄切片
tool-fs的源码地图更细,九个文件各管一件事,是"薄切片"的教科书:
| 文件 | 职责 |
|---|---|
src/index.ts | 插件入口:Config、工具组合、read_image附件门禁 |
src/read.ts/read-image.ts/write.ts/edit.ts | 四个执行器:一次 stat、意图 waterfall、原子写入、观察 |
src/read-render.ts | 不依赖 Cordis 的窗口构建与信封格式化 |
src/sandbox.ts | write/edit 共享的升权 API |
src/error.ts | 防护变更失败的稳定模型侧诊断 |
src/mutation-result.ts | 成功变更的观察记录与规范化结果路径 |
一个反直觉的细节值得记:“读取恰好执行一次 stat,变更一次也不执行”——write/edit不做 stat,因为防护来自意图槽位,提供方在锁内重新检查。这个设计把"竞态安全"从工具层下放到了提供方层,工具本身保持无状态、可并发。
五、生产级写法清单:从官方插件学到的纪律
5.1 命名角色表:不要乱起类名
官方给了一张 17 行的角色命名表,规定什么词该用在什么场景。摘几个最常用、也最容易用错的:
| 角色词 | 适用条件 | 不适用条件 |
|---|---|---|
Provider | 提供一项能力定义的一个实现 | 表示能力定义、registry 或消费方 runtime |
Backend | 在已定义接口后实现可替换的底层持久化/传输/执行 | 面向用户的服务或实时资源引用 |
Policy | 决定允许、选择、限制或观察什么 | 执行该决定所允许的机制 |
Registry | 拥有一组动态具名注册 + 优先级/生命周期 | 主要约定是分派、执行、取消或编排 |
Executor | 在一项能力中运行一个明确请求或已解析 spec | 拥有广泛应用生命周期或 provider 目录 |
这张表解决的是"命名即架构"的问题:一个类叫Provider还是Registry,直接暴露了它在能力分层里的位置。用错词,后来人(包括三个月后的你)就会误判它的职责边界。
5.2 包不变式:package.json 的硬约束
官方通过 CI 强制执行一组"包不变式"(pnpm run constraints),新包必须满足:
| 字段 | 硬约束 |
|---|---|
private | 必须true(monorepo 内不独立发版) |
version | 与根package.json一致 |
type/main/types | module/lib/index.js/lib/types/index.d.ts |
| peer 依赖 | @deepseek-ai/cordis同时进 peerDependencies + devDependencies(相同范围),每个 dsh peer 都要在 devDependencies 镜像 |
files | 精确列出lib/index.js、lib/types/**/*.d.ts等,不发布 src |
这一套和你在第 07 篇自己搭骨架时的坑一一对应:files漏了产物 → 线上坏;peer 不镜像 → 类型检查/测试孤岛。官方用 CI 把经验固化成门禁,你发布自己的插件时也该照抄这套"不变式清单"。
5.3 高频避坑速查表
| 序号 | 现象 | 根因 | 一句话解法 |
|---|---|---|---|
| 1 | 工具装了永远不激活 | 执行器/提供方没挂载,Consumer 停在 PENDING | 按最小组合 YAML 补齐 Definition+Provider |
| 2 | 想换沙箱要改工具代码 | 能力定义/实现/消费塞进了一个包 | 拆成 shell 三件套 |
| 3 | 模型能覆盖环境变量 | 暴露了 stdin/env 参数 | 请求只来自命名参数,受管值不暴露 |
| 4 | 命令失败把整个调用标成错误 | 把非零退出当异常抛 | 非零退出只报告,模型解读退出码 |
| 5 | 读改写出现陈旧覆盖 | 没做观察门禁 | 挂 fs-observation-policy,走意图槽位 |
| 6 | 错误信息"出错了"没法自愈 | 没定义稳定错误码 + 恢复指令 | 学 FS_NOT_OBSERVED 带 retry 指引 |
| 7 | 类名乱起导致职责误判 | 不懂角色命名表 | 对照 Provider/Backend/Policy 表命名 |
这张表的心法和前九篇完全一致:DSH 把"正确架构"写成了约定,官方插件只是这些约定的忠实实现。解剖官方的意义,不是抄它的代码,而是读懂它背后那套"能力分层 + 事件门禁 + 稳定契约"的思维,再迁移到你自己的插件里。
结语
这一篇把两个官方生产级插件拆成了可复用的架构:能力分层三件套(Definition / Provider / Consumer,shell 是模板)、tool-bash 的四条设计哲学(模型侧消费方、命名参数、非零只报告、任务运行时)、tool-fs 的工具/策略分离(工具即执行器、策略即门禁、稳定错误码)、以及生产级写法的纪律清单(角色命名表 + 包不变式)。核心一句话:官方插件的强大不来自代码量,来自它把"可替换的能力"拆成了能独立演进的三层——换执行器、换后端、换策略,都不用动工具本体。
知识要点文字回顾:① 能力分层 = Service Definition / Provider / Consumer 三件套,shell 三组件是模板;② tool-bash 四哲学:模型侧消费方、请求只来自命名参数、非零退出只报告、每条命令归任务运行时;③ tool-fs 核心:工具就是执行器、策略是事件门禁,fs/observed 事件外置策略;④ 稳定错误码带恢复指令(FS_NOT_OBSERVED→read then retry);⑤ 生产级纪律 = 角色命名表 + package.json 不变式 + 薄切片源码地图。
下一篇连载预告:《改造官方插件:把 tool-bash / tool-fs 裁剪成你自己的定制工具》——从"读官方"到"改官方",关注 CSDN 主页 不错过更新。
你写过工具插件吗?当时是"能跑就行"还是拆了能力分层?评论区聊聊你踩过的"换环境就散架"现场,我挨个回。本文事实基于 DeepSeek Harness 官方文档(tool-bash / tool-fs 包参考、添加 workspace 包手册,2026-10 版本),API 迭代快,有出入评论区提醒我。
参考资料(正文数据来源):DeepSeek Harness 官方文档《@deepseek-ai/dsh-tool-bash 包参考》《@deepseek-ai/dsh-tool-fs 包参考》《实操手册:添加 workspace 包》。