给 DeepSeek Harness 装插件的排障全记录:pnpm、GitHub 网络与"版本线"
🐛 给 DeepSeek Harness 装插件的排障全记录:pnpm、GitHub 网络与”版本线”
一条
dsh plugin add命令,引出了一连串意料之外的坑:pnpm 的全量重解析、死活连不上的 GitHub、藏在 lockfile 里的玄机,以及”插件版本跟基础库版本对不上”这种最容易忽略的兼容性问题。这篇文章完整记录了我的排查过程与最终解法。
起因:我想装三个插件
最近在折腾 DeepSeek Harness(以下简称 DSH)的 Web GUI,想给它装几个社区插件:
- dshmarket —— 可视化插件市场
- dsh-agent-teams —— AgentTeams
- dsh-better-sidebar —— VSCode 风格侧边栏
DSH 官方提供了插件安装命令,看起来很简单:
1 | |
然而从第一条命令开始,我就掉进了排障的兔子洞。前两个插件最终顺利装上,第三个却让整个 Web GUI 每次启动都崩溃。这篇博客把我踩过的每一个坑、用过的每一个”偏方”都写出来,希望能帮你省下这些弯路。
第一幕:pnpm add 为什么要去连 GitHub?
症状
执行:
1 | |
输出:
1 | |
(第一次是在沙箱环境里被文件权限拦住,放开后)变成:
1 | |
我当时一脸懵:我只是想加一个来自 npm 的包,为什么 pnpm 要去连 GitHub? 更莫名其妙的是,报错的仓库 Small-tailqwq/dsh-deep-whale 跟我正要装的插件毫无关系。
破案:dsh plugin 到底是什么
先读 DSH 的源码(lib/plugin-*.js),真相大白:
dsh plugin --profile <name> <args>本质上是一个 pnpm 转发器:在 profile 目录里执行pnpm <args>,装完后把”声明了dsh.bundle的已安装依赖”同步进package.json的dsh.profile.bundles层列表。
我的 web profile 位于 C:\Users\xxx\.dsh\profiles\web,里面有:
1 | |
关键机制在于:pnpm add 会触发一次”非冻结”的完整依赖解析,而不是只解析新加的包。也就是说,即使 lockfile 完好、旧依赖一个没动,pnpm 仍然会把 package.json 里所有依赖重新过一遍 resolver——其中就包括那三个 github: 直连的皮肤插件。解析它们需要 git ls-remote 访问 GitHub,而当时的环境里 GitHub 完全不可达:
| 目标 | 探测结果 |
|---|---|
registry.npmmirror.com(npm 镜像) |
✅ HTTP 200 |
registry.npmjs.org |
✅ DNS 正常 |
github.com |
❌ DNS ENOTFOUND |
GitHub 官方 IP 直连(curl --resolve) |
❌ 全部 TCP 超时(exit 28) |
关键结论
pnpm install --frozen-lockfile能跳过解析(”Lockfile is up to date, resolution step is skipped”),全程不碰网络,对已有依赖零风险;- 而
pnpm add一定会全量重解析,环境里只要有任何一个github:依赖,且 GitHub 不可达,就必然失败——跟你装什么包无关。
所以那条命令失败不是”装不了新插件”,而是”旧依赖把整个解析拖下水了”。
第二幕:不碰 GitHub 的安装法 —— “lockfile 预置 + 冻结安装”
思路
既然 pnpm add 会因重解析而死,那就绕开它,让 pnpm 走”零解析”的冻结安装路径:
- 先用 pnpm 生成”权威”的 lockfile 片段:在一个临时工程里只声明新插件,跑一次
pnpm install(临时工程里没有 GitHub 依赖,镜像可达,能顺利解析),得到 pnpm 亲自写出来的、格式绝对正确的 lockfile 条目; - 把片段”并”进真实 profile 的 lockfile:同时把依赖写进
package.json。此时 manifest 与 lockfile 完全一致; - 执行
pnpm install --frozen-lockfile:解析被跳过 → 不会碰 GitHub 旧依赖 → 只从 npm 镜像拉取并安装新增的包; - 手动补上
dsh plugin本来会自动做的 reconcile:把新插件追加到dsh.profile.bundles。
具体怎么做
临时工程:
1 | |
1 | |
它产出的 pnpm-lock.yaml 就是”标准答案”——包含 importer 条目、packages: 条目、snapshots: 条目,格式、缩进、排序、integrity 全部是 pnpm 亲自写的。
合并时我写了个小脚本做文本级 block merge:
- 以行首
两个空格 + 非空格作为条目边界,把packages:/snapshots:切分成”条目块”; - 用 js-yaml 解析两边做重叠检测(重复 key 必须内容一致,否则直接报错退出);
- 把不重复的新条目块按 pnpm 的规范格式插进真实 lockfile;
- 最后再用 js-yaml 重新解析一次做结构校验(包数量 = 原有 + 新增、importer 依赖齐全)。
⚠️ 血的教训:永远不要手写 pnpm-lock.yaml。它的条目(peerDependencies、peerDependenciesMeta、integrity、snapshots 的依赖锁定关系)环环相扣,差一个字符冻结校验就会失败。让 pnpm 自己生成、你再机械搬运,才是可靠做法。
然后更新 package.json 并执行:
1 | |
最后把插件名追加进 package.json 的 dsh.profile.bundles(等效于 dsh plugin 的 reconcile 步骤)。
前两个插件(dshmarket、dsh-agent-teams)就这么顺利装上了。我甚至总结出了一套”离线安装 DSH 插件”的通用流程。
第三幕:装上了,但每次启动都崩
新症状
better-sidebar 装好(v0.17.1)之后,每次运行:
1 | |
都在插件树加载阶段崩溃:
1 | |
安装明明成功了,node_modules 里包也在,为什么一加载就报”模块没有这个导出”?
破案:两个”版本线”的错位
我做了三件事:
- 找到基础库到底装在哪、是什么版本。DSH 的 profile 有个共享基础层
C:\Users\xxx\.dsh\profiles\node_modules\@deepseek-ai\,插件import "@deepseek-ai/dsh-settings"在 Node 的模块解析里会向上逐级找到这一层。实测版本:dsh-settings@0.1.2-alpha.5; - 确认这个版本到底有没有
settingsNamespace。grep 它的lib/index.js:里面只有一个内部函数parseSettingsNamespace,并没有导出settingsNamespace; - 对比插件声明的基础版本。看 npm 上 better-sidebar 各版本的
peerDependencies:
| better-sidebar 版本 | 要求的 @deepseek-ai/dsh-settings |
对应基础线 |
|---|---|---|
| ≤ 0.13.0 | ^0.1.0-rc.6 |
老 rc 线 |
| 0.13.1 | ^0.1.0-rc.7 |
rc.7 线 |
| 0.14.0 ~ 0.17.1(latest) | ^0.1.0-rc.8 |
rc.8 线(有 settingsNamespace) |
| 0.18.0-alpha.0(alpha) | ^0.1.2-alpha.2 |
alpha 线(我装的基础就是它!) |
真相是:DSH 同时存在多条”版本线”(rc.6 / rc.7 / rc.8 / alpha.2…),每条线的基础库 API 都不完全一样。npm 的 latest 标签并不代表”适合你的环境”——better-sidebar@0.17.1 是给 rc.8 基础线编译的,代码里直接 import { settingsNamespace },而我的基础层是 alpha.5,API 没有这个名字,于是一 import 就炸。
修复
换成同一版本线的配套版本:dsh-better-sidebar@0.18.0-alpha.0(作者为 alpha 基础线发布的 alpha 版,peer 声明 ^0.1.2-alpha.2,正好覆盖已装的 0.1.2-alpha.5)。
做法与第二幕相同,但这次更彻底——以”装 0.17.1 之前的 lockfile 备份”为基底重建 lockfile,再并入 alpha 版的子图:
1 | |
验证(两个关键技巧)
技巧一:写一个”复现 loader 解析路径”的 import 探针
插件加载失败的本质是 ESM import 解析问题,那就在同样的解析位置直接 import 一次:
1 | |
node 从这个文件出发向上找 node_modules,会命中基础层——与 cordis-plugin-loader 的实际解析路径一致。结果:
1 | |
技巧二:真实启动,看到”端口占用”就算赢
跑 dsh --profile web,如果插件树能加载,就会走到 webserver 监听那一步——而 3080 端口正被正在运行的 Web GUI 占着,于是:
1 | |
这个报错其实是好消息:它证明插件树已经全部加载成功,只是第二个实例抢不到端口。跟之前”插件树加载阶段直接崩溃”完全是两回事。
经验清单
把这一路的收获浓缩成几条,下次能少走弯路:
dsh plugin= 在 profile 目录里跑 pnpm,装完还要把包同步进dsh.profile.bundles(可以手动做等效操作)。pnpm add会对整个依赖树做非冻结重解析,环境中任何github:/git:依赖都可能成为拦路虎;而--frozen-lockfile会完全跳过解析——这是离线/受限网络环境下最可靠的安装路径。- lockfile 别手写,要”搬运”:用同版本 pnpm 在干净工程里生成权威条目,再以文本块粒度机械合并,配合 js-yaml 结构校验。
- npm 的
latest不等于”适合你”。DSH 生态同时存在多条版本线(rc.6/rc.7/rc.8/alpha…),选插件版本要看它的peerDependencies是否覆盖你基础层的实际版本——插件作者通常会给每条线发一个配套版本(看dist-tags的alpha/beta标签)。 - 报错信息里藏着定位线索:
does not provide an export named 'xxx'= 代码是按另一个 API 版本编译的;先在lib/index.js里确认导出,再对比双方版本线。 - 模块解析要看物理路径:插件在
profiles/web/node_modules,@deepseek-ai/*会向上解析到profiles/node_modules的基础层——探针放在同样的位置才能复现真实行为。
复盘:这条”心路历程”本身的价值
回头看,这个问题的本质是两个环境约束叠加:
- 网络约束:GitHub 不可达、npm 镜像可达,导致”官方命令”这条路被旧依赖堵死;
- 版本约束:插件与基础库不在同一条版本线,导致”装上了也跑不起来”。
单独看任何一个都不难,但它们组合在一起,就需要你同时理解 pnpm 的解析模型、DSH profile 的目录与分层机制、Node 的模块解析规则,以及 DSH 多版本线并行的发布策略——这也正是调试的乐趣所在:每解开一层,你对整个系统的理解就更深一层。
最后感谢 DSH 开源的代码与社区插件作者们,没有他们,就没有这篇踩坑记录 😄
(文中路径做了脱敏处理,实际路径中的用户名请替换为你自己的)