一切皆插件:我为 DeepSeek Harness 开源了 12 个插件
导读: 上一篇介绍了 DeepSeek Harness 的桌面版,解决的是安装问题。这一篇是它的延伸:装上之后如何补齐能力。起点是我自己第一次跑 dsh 时撞上的一个故障——接的不是官方直发的 key,而是腾讯云上的 DeepSeek 接入点,结果一次已经完整送达的回复被判成了失败。定位之后有两条路,改源码或写插件,我选了后者,因为改源码的代价会摊到之后的每一次升级上。那个修正不到二十行,后来围绕同样的扩展机制又补齐了另外十一个。仓库地址 github.com/huyang218/dsh-plugins,MIT 协议。
01 第一次跑通 harness,就撞上了一个兼容性故障
这个仓库的起点是一次失败的首跑。
我第一次把 dsh 跑起来时,没有用 DeepSeek 官网直发的 key,而是配了腾讯云上的 DeepSeek 接入点——一个 OpenAI 兼容的网关地址。配好、发问、模型开始输出,一切正常,直到输出结束的那一瞬间:界面报错,这一轮被标记为失败。重试,结果相同。
现象很具体:答案逐字吐完,内容完整可读,我甚至已经读完了,然后它告诉我这次调用失败了。
故障点不在模型,而在最后一个内容块之后。
流式返回走的是 SSE。按 OpenAI 的约定,服务端发完内容还需再发一个 data: [DONE] 作为结束哨兵。部分网关在最后一段正文之后直接关闭连接,不发这个哨兵。dsh 的模型适配器是严格实现,将其如实报为 STREAM_CLOSED——按协议它没有错判,但结果是一次完整送达的回复被记为事故。

一个简短的类比:信件已经完整投进门缝,只是投递方没有按那下门铃,收信一侧因此认定投递失败。
这里需要把归因说准确,因为很容易被读成「第三方网关不可靠」。转发这层架构本身是中性的,问题出在协议实现的宽严不一:严格的一方按标准报错,宽松的一方省略一个哨兵,各自都能自洽,组合起来才成为故障。真正需要评估的是这层网关由谁运营、出问题能否追责,而不是「转发」这个动作本身。
定位到这一步,摆在面前的是两条路。
一条是改源码:找到适配器里判定 STREAM_CLOSED 的那几行,把这种情况放过去。改动量最小,当场就能用。另一条是写插件,在不碰上游代码的前提下,从外面把这个判定纠正过来。
我选了后者,理由只有一个:改源码的代价不在这一次,而在之后的每一次。dsh 还在 rc 阶段,版本更新很密,本地改过的文件在每次升级时都要重新合一遍——冲突要自己解,解错了排查成本还高。更麻烦的是这类补丁一旦生效就容易被忘记,几个版本之后遇到别的问题,谁也说不清是上游的行为还是自己当初动过的那几行。插件是外挂的:升级照常走,插件独立更新,不想要了卸载即可,上游代码始终保持原样。
gateway-compat 就是这么来的。它的处理范围刻意收窄:包裹 llm/stream waterfall,仅将这一种终止错误改写为正常的 stop,且必须同时满足两个条件——确实已收到正文内容,且没有工具调用处于进行中。真正的中途断流与被截断的工具调用仍然照常失败,并保留原有的重试资格。
装它不需要任何配置,它也不注册工具,对模型不可见。从检出目录安装:
git clone https://github.com/huyang218/dsh-plugins.git cd dsh-plugins dsh plugin --profile web add ./packages/gateway-compat 用桌面版的话更直接:菜单打开插件管理,把 packages/gateway-compat 的绝对路径粘进去,点安装。
装完重启服务,可以先确认它确实进了组合后的配置树,再去试对话:
dsh --profile web --dump-config # 输出里应能看到 gateway-compat 这一行 之后同一个网关、同一个 key,回答写完就是正常结束。
02 扩展点决定了补丁的成本
上面那个修正之所以能写得这么小,取决于 dsh 的扩展模型。
「一切皆插件」的工程含义是:harness 在关键路径上留出了明确的扩展点,插件挂载在扩展点上包裹经过的数据,而不必 fork 或修改上游源码。模型请求经过 llm/stream,工具分发经过 tools/execute,工具结果广播在 tools/result。前两者是 waterfall,可以改写经过的内容;后者是 emit 观测点,只能观察、不能改写返回值——这个区别决定了一个插件能做什么、不能做什么。
仓库中的插件按扩展对象分为三类,分类同时决定了它对谁可见:
tools/ | ||
runtime/ | ||
ui/ |

这个分类不只是归档方式。每注册一个模型可见的工具,都会占用系统提示词预算,且每次请求都要支付这份成本。因此能在 runtime 层完成的能力,不应做成工具。
十二个插件的分布如下:
[DONE] 哨兵导致的误判 | |||
astock_data 结果渲染为带成交量的 K 线卡片 | |||

03 tools:模型可调用的四个
astock 提供 A 股数据,共 10 个工具,覆盖单只标的的行情、K 线与技术指标,用于筛选的全市场批量接口,以及财务报表、资金流向与可转债。其中 5 个走东方财富公开接口,无需凭证;其余依赖 Tushare。
一个值得说明的设计:每个工具在自身描述中写明是否需要 token。模型据此可以在存在免费替代时优先选择免费路径,在缺少凭据时明确报出缺什么,而不是调用中途才暴露权限不足。
ainfo 负责信息面,即一家公司「被如何评价」,而非成交在什么价位:新闻、券商研报的评级与目标价、业绩预告、分红、高管增减持、股东户数。它与 astock 拆开,直接原因就是提示词预算——筛选四十日最低价的任务用不到研报工具,没有必要让它们常驻在每一次请求里。
aportfolio 让 agent 跨会话保留持仓与自选,实时定价并给出逐只盈亏、占比与目标价触发情况。两条设计约束:状态写入存储层而非对话记录,避免从聊天历史中重读仓位导致的读错;写入采用整条替换而非增量,用户未明确说明的内容一律不推断,从而避免一句被误解的表述悄然改变账目。
vision 允许纯文本 agent 在任务中途调用多模态模型:将单个图片文件发送至 Qwen、Kimi、OpenAI、Claude、Gemini 或自建端点,返回该模型的判读结果。关键在于图片本身不进入主模型上下文,因此既有的文本路由不受影响,查看一张图的代价是一次工具调用,而不是切换模型。
默认的结构化模式要求视觉模型返回固定形状的证据而非散文:总览、逐字 OCR、版面区域、命名实体,以及一项「模型无法确定的内容」。最后一项是这个 schema 的意义所在——要求写散文时,视觉模型倾向于把读不出的部分顺过去;给出明确的不确定项字段,它就有位置如实申报。schema 中刻意不含检测框与置信度,这两项是视觉模型最容易虚构的输出。
该插件复制自 gloryxpnv/dsh-tool-vision(MIT),在本仓库继续维护,与上游的差异记录在 README 中。

04 runtime:模型不可见,却决定任务能否完成
这一组解决的都是运行期问题,也是从「能跑通」到「能长期用」之间的主要差距。
tool-retry 补的是一处空缺:dsh 已经重试模型请求,也已经强制工具超时,但没有任何一层重试工具调用本身。数据源在扫描中途关闭连接,整个任务即告结束,而 agent 收到的信息只是「数据不可用」。
它默认不重试任何工具,名单为空。原因是重复一次写入过、下过单、发送过内容的工具,等于把该动作执行两遍,而工具契约中并不存在「幂等」这一声明——isConcurrencySafe 表达的是能否并发,不是能否重复。因此只能由运维显式点名:
- id: tool-retry config: retryTools: ['astock_*', 'ainfo_*', 'web_fetch'] 匹配只支持完整名称或结尾通配,刻意不支持正则:这份名单决定了什么可以被执行两次,一个误写成匹配全部的表达式不属于需要支持的用法。重试判据也做了区分——socket 重置、429/5xx、限流提示与超时属于「值得重试」;参数错误、权限拒绝、文件不存在重试多少次结果都相同,只会让用户白等。退避为指数增长并封顶,因为值得重试的失败多数源自对端过载,紧凑循环只会把对方的短暂拥塞变成自己的故障。还有一条底线:重试耗尽后返回真实的失败,仅附加尝试次数,不将失败包装为成功。
tool-health 跨会话记录哪些工具持续失败,并在下一次会话开始工作前告知模型。agent 遇到已失效的端点时,默认的学习方式成本最高——一次一个失败调用,发生在任务中途,并且会在多个会话中重复。
它监听 tools/result,因此不改变工具返回值,只记账:调用总数、失败数、当前连续失败次数、最后一次错误与最近一次成功时间,通过 storage 域持久化。三个判据值得说明:以连续失败次数而非失败率作为信号(五十次中错一次是健康的,连续错三次不是,而失败率无法区分这两者);失败记录会过期,默认保留 24 小时,上周的故障不能说明本次会话的状态;一切正常时报告为空串,对提示词零贡献——常驻一句「所有工具正常」等于在每次请求上花 token 说废话。
tool-usage 计量一次会话在工具上的实际开销:调用次数、逐工具耗时分位与失败数。真实输出如下:
Tool usage: 3 calls, 0 failed, 1.7s spent in tools. astock_market_bars: 1× total 1.4s mean 1.4s p95 1.4s astock_financials: 1× total 203ms mean 203ms p95 203ms astock_quote: 1× total 107ms mean 107ms p95 107ms 它包裹 tools/execute 但不改变调用本身,转发 next() 并原样返回结果;抛异常的分发同样计入,因为它一样消耗了时间。排序按总耗时而非调用次数:需要关注的是会话把墙钟时间花在了哪里。它以服务而非工具的形式暴露——注册一个模型可见的报表工具,等于在每次请求的提示词里描述一份没人索取的报表。可选的预算阈值触发后,提示词中会出现一段说明,指出时间花在何处以及可采取的收敛动作。
tushare 是金融插件共享的凭据层,不注册任何工具。每个插件各带一个 token 字段当然可行,但用户需要把同一个 token 填四遍;更关键的是 Tushare 按账号计量配额,四个插件各自限流、各自「守规矩」,合计仍然会超。一个共享服务意味着一份 token、一个配额闸、一份交易日历,以及一致的失败语义——错误信息会区分是积分权限不足还是配额需要稍候。
im 把入口从桌面延伸到手机:在飞书、企业微信、钉钉或 QQ 中发送消息,即在一个真实会话中驱动一次真实的 agent 回合,回复返回同一个聊天,每个聊天维持独立会话,跨消息与重启保持。
部署前需要先确认的是网络方向:钉钉与 QQ 采用本机主动建立的长连接,设备位于 NAT 之后、不暴露任何端口也可使用;飞书与企业微信是回调进入,dsh 必须可从公网访问,需要在前面放置隧道或反向代理。
个人微信是刻意不做的。个人号没有官方机器人接口,能接入的实现依赖模拟客户端类网关,违反服务条款,风险由账号承担;企业微信是这条路径上唯一的官方接口,消息同样在手机端收发。权限方面采用默认拒绝:未配置则不启用任何渠道,未写入白名单的发送者一律不受理。
gateway-compat 已在第 01 节说明,此处不再重复。
05 ui:面向使用者的两个
astock-chart 将 K 线渲染在回复中,而不是返回一张数字表格。几个绘图判断:价格坐标只覆盖可见区间的最高最低价,不从零起——否则涨幅 2% 的标的会被渲染成空图顶部的一条直线;采用 A 股惯例的红涨绿跌,与多数西方图表相反;成交量按窗口内自身峰值缩放,表达相对活跃度。一处权衡是卡片数据需随结果持久化,因此只投影一屏 120 根,而非调用方可能请求的全部历史。
它对运行模式有前置要求:agent 必须能够原生调用 astock_data。纯 Code Mode 预设下每次调用都经由 run_code,子调用不携带卡片元数据,结果会退回通用表格。
shortcuts 为 Web 客户端提供键盘快捷键,34 项功能覆盖会话、视图、剪贴板、模型、权限与系统六组,全部绑定可自行录制并保存在浏览器本地。该插件复制自 Ricketts-Guo/dsh-shortcuts(MIT),在本仓库维护。
06 安装与两处易错点
第 01 节已经演示过一次安装,这里补齐通用形式和两处易错点。
需要先说明:这些包尚未发布到 npm,因此当前只能从源码安装。README 中按包名安装的写法适用于发布之后,现在照此执行会提示找不到包。
从检出目录安装,把最后一段路径换成你要的插件即可:
dsh plugin --profile web add ./packages/astock 安装形式是软链接,因此修改代码后重启服务即生效,开发与使用共用同一条路径。使用桌面版的话,可在插件管理窗口中填入该目录的绝对路径完成安装;插件若声明了配置 schema,窗口会据此生成表单,填写的值写入 profile 的 plugin-config.json 并镜像进 cordis.patch.yml,无需手改 YAML。
两处容易出错的地方:
工具呈现模式。它是按 agent 预设选定、对整个会话生效的,不是按插件配置的。需要卡片的插件要求 native 或 both;both 既保留 run_code 处理批量任务,又让单次查询走原生调用。
配置覆盖是整行替换。patch 中覆盖某一行的 config 时会替换该行的全部配置,而不是按键合并,因此需要把这一行用到的键全部重写。
07 仓库的几条工程约定
面向希望修改或新增插件的读者。
全部为纯 ESM,无构建步骤,因此从 npm 安装、从 git 安装、从正在编辑的检出目录安装,运行的是同一份代码。测试使用 Node 内置的 node:test,零测试依赖,且执行的是真实发布入口与真实的工具定义,schema 违规会在单元测试阶段失败,而不是等到 dsh 启动时暴露。
插件必须使用具名导出。写成默认导出会导致加载器丢弃 inject,随后的失败表现与真实原因相去甚远,排查成本很高。
另有四个名字需要保持一致:npm 包名、代码中导出的短名、patch 中的行 id、patch 中引用的包名。它们用途不同,混用会产生难以定位的错误,贡献指南中列了对照表。
仓库另附 CATALOG.md,收录由他人维护、不在本仓库内的插件,按类别索引。这个生态成型不过一周多,互相指路比各自重复造轮子更有价值。
写在最后
上一篇解决的是安装门槛,这一篇解决的是安装之后的能力缺口。两件事指向同一个判断:开源项目真正的门槛,往往不在核心代码的质量,而在从「能运行」到「能承担工作」之间那段缺少维护的路径。
dsh 把扩展点留得足够明确,这段路径才具备被第三方补齐的条件。这十二个插件里,比任何单个功能更值得关注的,是它们共同验证了一件事:「一切皆插件」在这套架构里是可执行的工程约定,不是宣传语。
评论区聊聊:如果要给自己的 agent 补一个插件,你最需要的是哪一类能力?我按呼声最高的写。
仓库地址:github.com/huyang218/dsh-plugins,issue 与 PR 均欢迎。
参考资料:
• 本文仓库:https://github.com/huyang218/dsh-plugins
• 上游项目 DeepSeek Harness:https://github.com/deepseek-ai/deepseek-harness
• 桌面版(上一篇):https://github.com/huyang218/dsh-desktop
• vision 插件上游:https://github.com/gloryxpnv/dsh-tool-vision
• shortcuts 插件上游:https://github.com/Ricketts-Guo/dsh-shortcuts
说明:文中封面与三张插图为 AI 生成,仓库截图为实拍。各插件的功能与代价以其 README 为准;dsh 处于 rc 阶段,接口仍可能变化。
夜雨聆风