Issue 01중국 AI
AC POST
중국 AI 목록
掘金2026년 11월 4일 22:27중국어 → 한국어

deepseek harness 데스크톱에서 VS Code 안 열리는 문제 해결법

원인은 ELECTRON_RUN_AS_NODE 환경변수 누출이며, 패키징된 수정 플러그인을 web과 desktop 프로필에 각각 설치하면 된다.

중국어 원문을 AI로 번역했습니다. 고유명사와 수치는 원문 표기를 우선하며, 중요한 판단에는 아래 출처 원문을 함께 확인하세요.

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 데스크톱판 자신도 이 스위치에 의존한다.

문제의 사슬은 세 단계다.

패키징된 호스트가 변수를 지닌 채로 실행된다.

데스크톱판 호스트는 시작 시 명시적으로 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 위에서 실행되어 환경에 자연히 이 변수가 없다——이것이 분기선이다.

정리 함수가 이것을 빠뜨렸다.

open-in-app 이 외부 애플리케이션을 실행하기 전에 scrubbedParentEnv() 를 호출해 부모 프로세스 환경을 정리하지만, 이것은 이름에 KEY / PASSWORD / SECRET / TOKEN 이 들어간 변수와 DSH_* 접두사만 걸러낼 뿐, ELECTRON_RUN_AS_NODE 는 무사히 빠져나간다.

VS Code 가 즉시 종료된다.

ELECTRON_RUN_AS_NODE=1 Code.exe "C:\작업 영역" → Code.exe 가 "작업 영역 경로"라는 이름의 "Node 스크립트"를 찾지 못하고 0이 아닌 값으로 종료 → 호스트가 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 은 내장 모듈에 대한 명명된 import 를 namespace 생성 시점에 스냅샷으로 고정하는데, 플러그인 활성화 시점에 호스트는 이미 node:child_process 의 namespace 를 생성해 둔 상태이므로, 사후 패치는 패키징 산출물 안의 import { spawn } 에 무효이며 곧바로 TypeError 가 난다. 한 층 위로 올려 WebServer 라우트를 감쌀 수밖에 없다.

둘째, Windows 환경 변수는 대소문자를 구분하지 않으므로 process.env 안에 임의의 대소문자 변형이 나타날 수 있어, 반드시 toUpperCase() 로 수집해야 하고 정확히 매칭하면 안 된다.

四. 플러그인 구현(139행 index.js, 네 블록)

첫 번째 블록은 실행 라우트를 감싸 세 가지를 한다——변수 제거, finally 복원, 중복 방지 표시:

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 안에서 두 가지를 둔다.

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 전달을 커버한다.

五、설치 및 검증

전역 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(먼저 메커니즘이 바뀌지 않았음을 확인):

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 두 프로필에 각각 한 번씩 설치;

- 퇴역 계획: 업스트림에서 discussions/8455 수정이 병합되면, 플러그인 정리가 전부 무해한 no-op이 되어 그대로 제거하면 됨.

플러그인 저장소: github.com/LeviathanNo…

도움이 되었다면 좋아요를 누르고 가시고, 댓글창에서는 여러분이 겪은 Electron 환경 변수 함정을 환영합니다.