deepseek harness,关于桌面版无法打开vscode修复方案

TL;DR:桌面版宿主带着 ELECTRON_RUN_AS_NODE=1 运行,open-in-app 的环境清理函数没过滤这个变量,启动 VS Code 时原样透传。VS Code 是 Electron 应用,收到这个变量就进入 Node 模式、秒退,宿主回报 502 launch-failed。文末附已打包的修复插件,web / desktop 两个 profile 各装一次即可。所有结论对照 v0.2.1-rc.3 源码核验,插件附带 16 项断言冒烟测试。

一、现象

DeepSeek Harness 会话页右上角有一排"在应用中打开"按钮:VS Code、Cursor、Windsurf、GitHub Desktop、文件管理器......

启动方式 VS Code 按钮 资源管理器 JetBrains
源码版 dsh web(git clone + pnpm) ✅ 正常 ✅ ✅
桌面版应用 ❌ 无反应 ✅ ✅
桌面版附带的全局 dsh web ❌ 无反应 ✅ ✅

界面无任何报错,开发者工具里才能看到后端返回 502 launch-failed。Cursor、Windsurf、VS Code Insiders 同样中招------共性很明显:中招的全是 Electron 应用。

二、根因:ELECTRON_RUN_AS_NODE 漏网

ELECTRON_RUN_AS_NODE=1 是 Electron 的隐藏开关:环境里带着它,可执行文件启动时跳过 GUI,直接进入 Node.js 模式,把命令行参数当 Node 脚本执行。DeepSeek Harness 桌面版自己也依赖这个开关。

问题链路三步。

  1. 打包宿主带着变量跑。

    桌面版宿主启动时显式设置 ELECTRON_RUN_AS_NODE=1(apps/desktop/src/node-environment.ts);全局 dsh 命令同理,apps/desktop/cli/dsh.cmd 第 3 行就是 set "ELECTRON_RUN_AS_NODE=1",启动的是同一个打包宿主。源码版跑在普通 node.exe 上,环境里天然没这个变量------这就是分界线。

  2. 清理函数漏了它。

    open-in-app 启动外部应用前调用 scrubbedParentEnv() 清理父进程环境,但它只过滤名字带 KEY / PASSWORD / SECRET / TOKEN 的变量和 DSH_* 前缀,ELECTRON_RUN_AS_NODE 顺利漏网。

  3. VS Code 秒退。

    ELECTRON_RUN_AS_NODE=1 Code.exe "C:\工作区" → Code.exe 找不到名为工作区路径的"Node 脚本",非零退出 → 宿主回 502。

资源管理器、JetBrains 不是 Electron 应用,不读这个变量,所以没事。

佐证:DSH 自己的更新器测试脚本里明确写了 ^ELECTRON_RUN_AS_NODE$ 的过滤正则,说明这个坑上游是知道的,只是通用清理函数漏了。上游在 launchDetachedApp 里删一行即可根治(社区提案:discussions/8455)。

三、修复思路:在启动请求窗口内摘变量

处理 /open-in-app/open 请求的那一瞬间,临时从 process.env 摘掉 ELECTRON_RUN_AS_NODE;请求结束,立刻恢复。

原理:open-in-app 的 spawn 在请求 handler 调用栈内同步完成,只要 handler 执行期间环境干净,被启动的应用拿到的就是干净环境。桌面版自己依赖该变量的子进程(pnpm 运行时、沙箱)不在这个窗口内启动,不受影响。

开发过程中有两个插曲值得一说。

一是直接包装 child_process.spawn 的思路被证伪了:ESM 对内置模块的具名导入在 namespace 创建时就快照固化,插件激活时宿主早就创建过 node:child_process 的 namespace,事后 patch 对打包产物里的 import { spawn } 无效,直接 TypeError。只能上移一层,包装 WebServer 路由。

二是 Windows 环境变量大小写不敏感,process.env 里可能出现任意大小写变体,必须按 toUpperCase() 收集,不能精确匹配。

四、插件实现(139 行 index.js,四块)

第一块是包装启动路由,做三件事------摘变量、finally 恢复、打防重标记:

js 复制代码
function wrapRoute(route) {
  const handler = route.handler
  if (handler[PATCHED] === true) return
  const wrapped = async function (req, res) {
    const saved = Object.keys(process.env)
      .filter((key) => key.toUpperCase() === RUN_AS_NODE)
      .map((key) => [key, process.env[key]])
    for (const [key] of saved) delete process.env[key]
    try {
      return await handler.call(this, req, res)
    } finally {
      for (const [key, value] of saved) process.env[key] = value
    }
  }
  wrapped[PATCHED] = true
  wrapped[ORIGINAL_HANDLER] = handler
  route.handler = wrapped
  wrappedRoutes.add(route)
}

第二块是识别启动路由,只认 path === '/open-in-app/open'(wire 常量)且 handler 是函数。不多加条件,避免上游改注册方式时静默漏拦。

第三块是双保险拦截。插件激活顺序不可控(bundle 注释明确"行序无加载语义"),apply 里做两道:

js 复制代码
export function apply(ctx) {
  const server = ctx.webServer
  const proto = Object.getPrototypeOf(server)

  // 第一道:本插件先激活 → 包装 register,之后注册的启动路由都经过包装
  if (proto[PATCHED] !== true) {
    const originalRegister = proto.register
    proto.register = function (route) {
      if (isOpenRoute(route)) wrapRoute(route)
      return originalRegister.call(this, route)
    }
  }

  // 第二道:open-in-app 先激活 → 扫存量路由表,原位替换
  for (const tableName of ['exact', 'prefixes']) {
    const table = server[tableName]
    if (!(table instanceof Map)) continue
    for (const route of table.values()) {
      if (isOpenRoute(route)) wrapRoute(route)
    }
  }
}

路由表 exact 虽然在 TypeScript 里是 private,运行时只是普通 Map 属性,直接读写生效;读不到也不报错,第一道已兜底。

第四块是卸载还原,dispose 时逐路由换回 ORIGINAL_HANDLER、还原 prototype.register;老版本 cordis 没有 effect 就退回 ctx.on('dispose', cleanup)。

边界情况有三处。

GitHub Desktop 是唯一例外:它故意用 Node 模式启动自家 CLI,但其显式声明的变量在 scrub 后单独 overlay,插件只清继承层、不碰 overlay,行为不变(有专门测试用例锁定)。

并发请求的删除/恢复用保存值配对,互不干扰。

dispose 还原不留任何痕迹。

冒烟测试跑在 v0.2.1-rc.3 真实构建出的 WebServer 类上,16 项断言覆盖两种激活顺序、dispose 还原、并发请求、GitHub Desktop 显式 env 透传。

五、安装与验证

sh 复制代码
# 全局 dsh web 命令(web profile)
dsh plugin --profile web add ./dsh-open-in-app-electron-fix-0.1.0.tgz

# DeepSeek Harness 桌面版(desktop profile)
dsh plugin --profile desktop add ./dsh-open-in-app-electron-fix-0.1.0.tgz

注意 web 和 desktop 是两个 profile,要分别装,只装一处另一处照样打不开。装完重启 dsh web 或桌面版。

Sanity check(先确认机制没变):

sh 复制代码
ELECTRON_RUN_AS_NODE=1 "$LOCALAPPDATA/Programs/Microsoft VS Code/Code.exe" --version
# 输出 Node 版本号(而不是 VS Code 版本号)= 机制成立

然后开会话页点 VS Code 按钮,工作区应正常打开。每次升级 DSH 建议重跑这条 check:输出还是 Node 版本号、按钮却打不开了,说明上游改了启动路径,插件没拦到。

六、总结

  • 现象:源码版正常;桌面版应用与其全局 dsh web 点 VS Code 按钮无反应(502);
  • 根因:打包宿主带 ELECTRON_RUN_AS_NODE=1 运行,经 scrubbedParentEnv() 盲区泄漏给被启动的 Electron 应用 → Node 模式秒退;
  • 修复:包装 /open-in-app/open 路由,请求窗口内摘变量、结束恢复;web / desktop 两个 profile 各装一次;
  • 退役计划:上游合并 discussions/8455 修复后,插件清理全部变为无害 no-op,直接卸载即可。

插件仓库:github.com/LeviathanNo...

觉得有用的话点个赞再走,评论区欢迎补充你遇到的 Electron 环境变量坑。

相关推荐
ServBay18 小时前
DeepSeek 再次崩了,DeepSeek V5正在路上?
后端·deepseek
Dream-Y.ocean21 小时前
鸿蒙PC平台星图遥感适配实战:基于 Electron 壳方案的纯 JavaScript 遥感图像处理引擎开发
javascript·electron·harmonyos
薛定谔的猫-菜鸟程序员1 天前
Electron开发超详细教程(从入门到放弃)
前端·javascript·electron
大模型真好玩1 天前
DeepSeek Super-Agent核心特性详解与实战指南:把操作系统思想搬进智能体
人工智能·agent·deepseek
北城笑笑1 天前
Vue 106 ,上篇:前端桌面端应用实现方案( Electron 框架详细介绍,Vue + Electron )
前端·javascript·vue.js·python·electron
不可能片场2 天前
Electron 发布 别拿 load 事件当页面就绪
前端·electron
ytianxia62 天前
用 OpenClaw 搭建个人金融助手:从联网查询到双模型协同
deepseek
无责任此方_修行中2 天前
模型变强了,架构为什么反而要变?——从"折叠 subagent"到"Super-agent"
人工智能·ai编程·deepseek
AI日报派送佬3 天前
2026年11月01日AI行业日报|可灵AI198亿融资落地、DeepSeek开源超级智能体、Anthropic取消Ultra时长限制
人工智能·开源·openai·deepseek·anthropic·ai日报