DSH 桌面版用 DeepSeek 报 REQUEST_EXTENSION 的根因定位与修复
DSH 桌面版(DeepSeek Harness Desktop)里把模型切到 DeepSeek 后,每次发消息都失败:
本轮运行失败 DeepSeek request extension preparation failed,Tag 是REQUEST_EXTENSION。请求根本没发出去。本文从错误字符串一路翻到运行时源码,定位到一个 profile 清单缺字段引发的连锁报错,并给出零升级的本地修复方案和可复现的 A/B 验证。
一、现象
症状很干净,也很诡异:
- 桌面版默认模型是 MiniMax / 其他 provider 时,一切正常;
- 一旦在模型选择器里切到 DeepSeek (本文实测是
deepseek-account/deepseek-flash),发送任意消息(哪怕只是「你好」)都是:
text
本轮运行失败 DeepSeek request extension preparation failed REQUEST_EXTENSION
- 关键点:会话
assistant/attempt事件里,从request/header到报错只隔了 ~20ms 。这说明不是网络超时、不是鉴权失败------它是在本地、在任何 HTTP 之前就炸了。
json
{"type":"request/header","seq":182,"time":1791868045205,
"data":{"header":{"config":{"provider":"deepseek-account","model":"deepseek-flash","reasoningEffort":"high"}}}}
{"type":"assistant/attempt","seq":184,"time":1791868045225,
"data":{"turn":5,"step":1,"stream":[{"chunk":{"type":"finish",
"reason":{"kind":"error","failure":{"message":"DeepSeek request extension preparation failed","code":"REQUEST_EXTENSION"}}}}]}}
二、先搞清楚这个错误是谁抛的
DSH 的运行时是打进 app.asar 的。把 dsh-llm-deepseek/lib/index.js 抠出来,REQUEST_EXTENSION 只有一处:
js
async function prepareRequestExtensions(body, options, prepare, onOmitted) {
let extensions;
try {
extensions = await prepare({ body, ...options }); // ← 贡献者在这里跑
} catch (error) {
throw new LlmError("DeepSeek request extension preparation failed",
"REQUEST_EXTENSION", { cause: error }); // ← UI 只显示这层的 message
}
// ...再合并字段、序列化、派发
}
也就是说:UI 上那句错误只是外壳,真正的 cause 被吞掉了 。DeepSeek 官方适配器在发请求前会跑一个「请求扩展准备」阶段,把所有插件想往请求里注入的顶层字段准备好;任何一个贡献者抛异常,都会被包装成 REQUEST_EXTENSION。
那谁在往 DeepSeek 请求里塞字段?在运行时里搜 deepseekLlmApiExtensions.register,一共两个:
| 插件 | 注入字段 | 干什么 |
|---|---|---|
dsh-session-log-deepseek |
dsh_session_log |
增量上传会话日志 |
dsh-plugin-package-inventory-deepseek |
dsh_plugin_packages |
上报当前激活插件的包清单 |
而且这俩是 @deepseek-ai/dsh-base 这一层 bundle 的默认开启插件,所以每个 profile 都有。
yaml
# <runtime>/node_modules/@deepseek-ai/dsh-base/cordis.patch.yml
- id: deepseek-llm-api-extensions
name: '@deepseek-ai/dsh-deepseek-llm-api-extensions'
- id: session-log-deepseek
name: '@deepseek-ai/dsh-session-log-deepseek'
- id: plugin-package-inventory-deepseek
name: '@deepseek-ai/dsh-plugin-package-inventory-deepseek'
三、根因:一个「相对路径插件」撞上「缺 version 的 profile 清单」
看 dsh-plugin-package-inventory-deepseek 的 prepare,它会遍历所有激活的 loader 条目,解析每个条目的归属包:
js
ctx.deepseekLlmApiExtensions.register("dsh_plugin_packages", {
prepare: async (request) => ({
value: { version: 1,
packages: await collectActivePluginPackages(ctx, resolver, hostBaseUrl, request.sessionId) }
})
});
解析逻辑里,对「裸包名」和「相对/绝对路径模块」是两条路:相对路径的模块会就近向上找一个 package.json ,再用 identityFromManifest() 校验它:
js
function identityFromManifest(path, allowAnonymous) {
const manifest = JSON.parse(readFileSync(path, "utf8"));
if (allowAnonymous && manifest.name === void 0) return void 0;
if (typeof manifest.name !== "string" || manifest.name.length === 0 ||
typeof manifest.version !== "string" || manifest.version.length === 0) {
throw new Error(`plugin-package-inventory-deepseek: ${path} must declare non-empty name and version`);
}
return { name: manifest.name, version: manifest.version };
}
现在看这台机器上的 desktop profile。它里面恰好有一个相对路径插件------就是之前为了修「Open In VS Code 打开失败」加的那个本地副本:
yaml
# ~/.dsh/profiles/desktop/cordis.patch.yml
- id: open-in-app
disabled: true
- insert:
- id: open-in-app-patched
name: "./plugins/open-in-app.mjs" # ← 相对路径!
于是 plugin-package-inventory-deepseek 解析这个条目时:
./plugins/open-in-app.mjs不是裸包名 → 走就近 manifest 逻辑;- 从
plugins/往上一层,找到最近的package.json= profile 自己的~/.dsh/profiles/desktop/package.json; - 而 DSH 桌面版的 profile 初始化器生成的清单里没有
version字段:
js
// 桌面版 app.asar 内的 lib/main.js,initProfile()
const manifest = {
name: `dsh-profile-${basename(dir)}`,
private: true,
dependencies: {},
dsh: { profile: { bundles: [...bundles] } } // ← 没有 version
};
identityFromManifest走到typeof manifest.version !== "string"→ 抛错;- 异常从
prepare冒泡 → 被适配器包成REQUEST_EXTENSION。
于是链条闭合:只要 desktop profile 里挂着那个相对路径插件,任何一次 DeepSeek 请求都会在本地准备阶段必炸,跟模型、额度、网络、Cookie 统统无关。
两个很自然的问题也顺带解答了:
- 为什么换模型就好了? 因为「请求扩展准备」只存在于 DeepSeek 适配器里,MiniMax / GLM 等 provider 根本不跑这段代码。
- 为什么之前改 web profile 没用? 桌面版启动的是
desktopprofile(~/.dsh/profiles/desktop),不是web;改错对象了。
四、复现与验证(隔离 DSH_HOME + 桌面自带运行时)
桌面版自带一个 CLI 包装器,用应用内那份不可变运行时:
bat
:: <install>\resources\runtime\cli\bin\dsh.cmd
set "ELECTRON_RUN_AS_NODE=1"
"%~dp0..\..\..\..\DeepSeek Harness.exe" --expose-internals ^
"%~dp0..\..\..\app.asar\dsh\node_modules\@deepseek-ai\dsh-desktop-host\lib\cli.js" %*
为了不污染真实环境,复制一份独立 DSH_HOME,建一个最小 profile(dsh-base + dsh-headless 两个 bundle),默认模型指到 deepseek-official/deepseek-flash,然后只改一个变量 :有没有那个相对路径插件、profile 清单有没有 version。
| 组 | 相对插件 ./plugins/dummy.mjs |
profile package.json |
结果 |
|---|---|---|---|
| A | ✅ | 无 version |
❌ REQUEST_EXTENSION: DeepSeek request extension preparation failed |
| B | ❌ | 无 version |
✅ 正常返回「你好」 |
| C | ✅ | 加 "version": "0.0.0" |
✅ 正常返回「你好」 |
三组用同一条命令,只有 A 复现了用户看到的报错;B 证明「跟缺 version 无关,是相对插件触发的」;C 证明「补上 version 就没事了」。
powershell
$env:DSH_HOME = "<隔离目录>"
& "<install>\resources\runtime\cli\bin\dsh.cmd" --profile td "只回复两个字:你好"
# A 组输出: dsh: REQUEST_EXTENSION: DeepSeek request extension preparation failed
随后用真实 desktop profile 的补丁副本 (含 open-in-app-patched)重跑,也稳定拿到「你好」------修复在真实配置上成立。
五、修复:给 profile 清单补一个 version
根因不在业务代码,而在 profile 清单少了一个字段。最小改动:
json
// ~/.dsh/profiles/desktop/package.json
{
"name": "dsh-profile-desktop",
"version": "0.0.0",
"private": true,
"dependencies": {},
"dsh": {
"profile": {
"bundles": ["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-web-app"]
}
}
}
只加了 "version": "0.0.0" 一行,其余不动;open-in-app 的本地补丁继续保留(那是修另一个问题的)。改完重启 DSH 桌面版(profile 在启动时读取),再发一次 DeepSeek 消息即可,旧会话里的失败记录不影响新请求。
为什么不改插件、也不禁功能?禁用
session-log-deepseek/plugin-package-inventory-deepseek也能绕过去,但那是关掉两个默认功能换来的「不报错」,属于治标;补version才是治本,且不损失任何能力。
六、给上游的建议
这其实是 DSH 运行时里的一处不一致:
initProfile()生成的是name有、version无的清单;identityFromManifest(..., allowAnonymous=true)只放宽了「没有 name」的情况,对「有 name 没 version」是硬抛。
合理修法二选一:初始化器给 profile 清单补 version,或让 inventory 插件把「无 version 的 profile 自身清单」也按匿名模块跳过。在那之前,profile 里挂了相对路径插件的用户都会中招 ,~/.dsh/profiles/desktop/package.json 加 version 就是最省事的兜底。
小结
REQUEST_EXTENSION只是外壳,真正原因藏在被吞掉的cause里:DeepSeek 发请求前的「请求扩展准备」阶段有插件抛异常。- 触发链:
desktopprofile 挂了相对路径插件./plugins/open-in-app.mjs→ 被解析到 profile 自身package.json→ 该清单缺version→plugin-package-inventory-deepseek抛错。 - 只有 DeepSeek 适配器跑这段准备逻辑,所以「换模型就好、用 DeepSeek 必炸」。
- 修复:给
~/.dsh/profiles/desktop/package.json补"version": "0.0.0",重启桌面版。 - 排查心法:报错只隔 ~20ms、且发生在 HTTP 之前 → 别怀疑网络和凭据,去翻「请求构造/准备」阶段的本地插件代码。