DSH 桌面版用 DeepSeek 报 REQUEST_EXTENSION 的根因定位与修复

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 解析这个条目时:

  1. ./plugins/open-in-app.mjs 不是裸包名 → 走就近 manifest 逻辑;
  2. 从 plugins/ 往上一层,找到最近的 package.json = profile 自己的 ~/.dsh/profiles/desktop/package.json;
  3. 而 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
};
  1. identityFromManifest 走到 typeof manifest.version !== "string" → 抛错;
  2. 异常从 prepare 冒泡 → 被适配器包成 REQUEST_EXTENSION。

于是链条闭合:只要 desktop profile 里挂着那个相对路径插件,任何一次 DeepSeek 请求都会在本地准备阶段必炸,跟模型、额度、网络、Cookie 统统无关。

两个很自然的问题也顺带解答了:

  • 为什么换模型就好了? 因为「请求扩展准备」只存在于 DeepSeek 适配器里,MiniMax / GLM 等 provider 根本不跑这段代码。
  • 为什么之前改 web profile 没用? 桌面版启动的是 desktop profile(~/.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 发请求前的「请求扩展准备」阶段有插件抛异常。
  • 触发链:desktop profile 挂了相对路径插件 ./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 之前 → 别怀疑网络和凭据,去翻「请求构造/准备」阶段的本地插件代码。
相关推荐
斯晨的AI笔记13 小时前
在服务器上跑 npm ci 四十分钟没装完,我打包把 node_modules 扔了过去
服务器·node.js
cpolar技术支持15 小时前
第三方平台回调总说收不到?把本机 Webhook 收单页跑通,用 cpolar 固定二级子域名给对面看实时请求
node.js·内网穿透·webhook·cpolar·回调对接
Jarming15 小时前
infinite-creation本地AI短剧带货项目推荐-本地运行无费用0负担
agent·qwen·skill·minimax·deepseek·skills·agent-skills·minimax-h3
猿来如此呀15 小时前
DeepSeek Harness(DSH)保姆级入门与原理详解:从跑到用再到讲得清
dsh
mont evergreen18 小时前
一封从外部世界寄来的信:二维码数据如何穿越三重门,抵达你的屏幕
electron·vue·事件
damoluomu20 小时前
Node.js CMS 的官网和仓库,22 个一次收齐(五)
node.js
论文复现现场20 小时前
DeepSeek V4.1 Flash 各平台价格怎么比?缓存、峰谷计费与 Python 成本计算
开发语言·python·缓存·大模型·成本优化·deepseek
Flynt1 天前
Deno 官宣一年后停更:我把它的招牌特性在 Node 26 和 Bun 里挨个找了一遍
node.js·deno·bun