8 月 13 号晚上,DeepSeek 开源了自家第一款 Agent 产品,就是这个 Harness,发布不到一天,GitHub 星数冲到好几万。周末我把它装上,真就装了一堆插件。
然后问题来了:设置里的「插件列表」只显示插件名和启用状态。版本号看不到,git 提交看不到,作者是谁看不到。官方插件、社区插件混在一张表里,装了什么、靠不靠得住,根本分不出来。

我打算给这个列表加点信息。结果这件"顺手的事",把整套插件机制摸了个遍,还撞上了官方代码里的一个真 bug。
官方不接受 PR
我最开始的方案很直接:官方源码就在本地,直接改官方包
@deepseek-ai/dsh-host-plugin-inventory,加一个能返回版本、作者这些信息的接口,客户端再注册个页面。第一版真跑起来了。然后我寻思,改完提个 PR 吧。结果在
CONTRIBUTING.md 里看到这句话:We are sorry that we cannot accept external pull requests at the moment.
当前阶段,不接受外部 PR。白纸黑字。
这不只是"不给你合"。它意味着我改的官方包永远是我自己 fork 里的东西:合不进去,主线一升级就冲突,这个增强没有落地的一天。
所以思路必须换:**不碰官方代码,用官方公开的能力,做独立插件包。**第一版的代码基本白写,工程结构整个重来。
这是第一个认知:Harness 把「插件生态」当扩展边界,核心代码封闭。想加功能,就写插件。官方在 CONTRIBUTING 里也明说了,鼓励社区在
dsh-plugin 话题下共建生态,只是核心仓库的门关着。好在插件机制本身够开放,够第三方折腾。接下来看它长什么样。
插件长什么样
一个 dsh 插件包,拆成两部分:
- 服务端部分:跑在 Harness 进程里,负责读数据、算数据。
- 界面部分:一段打包好的 JS,负责在设置界面里渲染。
我这次要算的数据是"信任分级":
@deepseek-ai 开头的包算官方,其他带 scope 的算组织,没 scope 或本地文件算个人。逻辑不复杂,但前提是能读到每个已装插件的 package.json 和 git 提交——这活儿只能服务端干,界面部分只负责把结果画出来。两部分之间靠数据通道通信。官方有现成的 Remote,但数据面由官方定死;第三方想新增,挂载点又是官方硬编码的,插不进去。社区插件的标准做法是第三种:服务端自己注册一条 HTTP 路由,界面部分直接 fetch。
dsh-git-status 这类先例都是这么干的,官方代码一行不动。我照着走,少踩很多弯。界面怎么挂进去?靠 slot 系统。Harness 的界面是"插槽 + 条目":官方在设置区留了
settings.plugins.tab 这个槽位,谁往里注册条目,谁就有一个标签页。条目带优先级,同 id 的条目按优先级"影子覆盖",渲染时每格取优先级最低的那个;条目渲染崩了,会自动让位。界面部分最后打成一段 bundle,走一个固定契约:
react 这类外部依赖由运行时的模块表解析。打包时必须把 react、@deepseek-ai/dsh-client-ui-primitives 全部列进 externals,交给运行时解析。漏一个,打包器就内联一份进去,运行时跟官方各持一份 React,界面行为就诡异了。得维护一份完整的 externals 白名单,写清楚注释。
撞上官方代码里的 bug
第一版我偷了个懒:不新增标签页,而是"影子覆盖"官方那个「插件列表」标签。注册一个同 id、优先级 -1 的条目,它就把官方标签顶掉,变成合并后的增强列表。界面不用多一个标签,还带自动回退。
跑起来一看,设置页标签栏里出现了两个「插件列表」,一模一样。
我以为是自己注册姿势不对,顺着代码一查,才发现是官方代码的 bug。slot 的影子覆盖语义本身是对的,渲染器也遵守。但官方
ui-settings-plugins 这个包在投影标签栏时,用的是原始 entries(),没做去重。我注册的第二个同 id 条目,被原样投影成了第二个标签。修法其实就一行:把
entries() 换成 entriesOfSlot(),取每格胜者。但这一行在官方已发布的 rc.6 里没有。本地仓库改一下能用,装 npm 版立刻还原。这个 bug 挺典型:shadowing 语义文档写得很清楚,渲染器也遵守,偏偏消费标签栏投影的官方包自己没用对 API。文档和实现之间,永远以代码为准。
所以影子覆盖这条路,在这个版本上走不通。这是第二个认知:想影子覆盖官方标签,先确认消费这个 slot 的投影是不是用的
entriesOfSlot。(上游现在已修,8 月的提交里已经是 entriesOfSlot 了。但你装的若是带 bug 的版本,照样踩。)prefix 遮蔽 bundle,整页 Failed to load
数据通道选了自建路由,第二个大坑在这等着。
服务端注册路由时我用了
prefix:路径写 /plugins/dsh-plugin-list-plus,心想"这个前缀下的请求都归我"。结果浏览器整页报错:bundle script failed to load。原因有意思。Harness 的 web server 路由是最长前缀匹配:官方 bundle 服务器注册的是
/plugins 这个前缀,我注册的是 /plugins/dsh-plugin-list-plus 这个更长的前缀。我自己的 bundle(/plugins/dsh-plugin-list-plus/client.js)恰好也在这个长前缀下面。它被我自己注册的 handler 拦走了,返回 404。更要命的是启动机制。Harness 启动是 fail-loud:所有插件的 bundle 必须全部加载成功,整页才渲染;任何一个失败,页面就停在「Failed to load plugins」,白屏。启动阶段没有任何插件能回退——slot 的崩溃让位只覆盖启动之后的渲染期,文档把两者分开讲,我踩到才真正记住。
修法一句话:数据路由用
kind: 'exact',只精确匹配 /plugins/dsh-plugin-list-plus/list 这一条路径。以后验证也简单,先 curl 三件事:首页 HTML 200、bundle 200、数据接口 200。bundle 404 是"装上但没生效"最常见的根因。
数据接口还要配
cache-control: no-cache,客户端 fetch 带 cache: 'no-store'。快照是实时状态,被浏览器缓存住,重开设置全是旧数据。独立标签页,收工
折腾完这两遭,我放弃偷懒路线:不影子覆盖,新增一个独立标签页「插件列表 Plus」,id 是
plus。代价是界面多一个标签。好处是彻底干净:不依赖任何官方修改,官方标签原样保留,任何版本都能装。自建接口挂了,还能自动回退到官方列表,不会白屏。
装法在文末,一行命令。先看效果——官方列表缺的那几样,它都补上了。插件按信任分级:官方 / 组织 / 个人,分组可折叠:

每个插件一张完整详情卡片,版本、git 提交、开发者、描述、许可证、配置状态,都在上面:

测试和发布也各有各的坑,简单提两个。后台起的实例容易被 SIGHUP 杀掉,残留实例又占着端口报 EADDRINUSE;界面部分没法在 node/jsdom 里直接 import,测试得自包含、mock 掉 loader。真要写插件,这些早晚会碰到。
给想写 Harness 插件的人三句话
写这一个插件,比想象中费事。但收获是把整套插件机制吃透了。留三句话:
- 别碰官方代码。 官方不接受 PR,用公开 API 做独立包,主线升级跟你无关。
- 数据通道自建
/plugins路由,务必用exact。 prefix 会把自己 bundle 拦死。
- 想影子覆盖官方标签,先确认投影用没用
entriesOfSlot。
最后借之前聊过的一个观点收个尾:AI 编程里,模型是地板,harness 才是天花板。给 harness 写插件,就是在"天花板"这一层做文章。这层现在还很空,早期玩家还有位置。
这个插件已经发布到 npm 了,想直接体验,一行命令就能装:
装完重启 dsh web,打开 设置 → 插件 → 「插件列表 Plus」就能看到效果。
如果你也在用 Harness,觉得这个增强顺手,欢迎去 GitHub 点个 star ⭐:https://github.com/yibiner/dsh-plugin-list-plus
附录:最小插件模板(可直接抄)
一个完整的 dsh 插件 = 三个文件 + 两处声明。
src/index.ts —— 服务端部分:一条 exact 数据路由src/client/index.ts —— 界面部分:注册一个设置标签页package.json 关键声明:dsh.bundle.patch 指向 cordis.patch.yml(bundle 清单),peer 依赖列全 @deepseek-ai/*,发布前把 workspace:^ 换成真实 semver,否则 tarball 里还是 workspace:^,别人装不上。cordis.patch.yml:装进实例:
- 作者:Yibin
- 链接:https://yibin.dev/article/3be60b50-99a4-801b-9d35-d3e41afb7cbd
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。
相关文章







