From bf845301f924940344e82f9d4952968c37e0b44e Mon Sep 17 00:00:00 2001 From: shijie-todd <55285084+shijie-todd@users.noreply.github.com> Date: Thu, 9 Apr 2026 21:15:18 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20windows=E4=B8=8B=E6=89=A7=E8=A1=8Csp?= =?UTF-8?q?awn=E6=8A=A5=E9=94=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/src/server/ai-provider-claude.ts | 43 ++++++++++++++++++- .../core/types/server/ai-provider-claude.d.ts | 2 +- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/packages/core/src/server/ai-provider-claude.ts b/packages/core/src/server/ai-provider-claude.ts index 7d00a06e..c3902038 100644 --- a/packages/core/src/server/ai-provider-claude.ts +++ b/packages/core/src/server/ai-provider-claude.ts @@ -241,6 +241,7 @@ export async function getModelInfo( { stdio: ['pipe', 'pipe', 'pipe'], env: getEnvVars(), + ...(cliPathNeedsShellSpawnOnWindows(cliPath) ? { shell: true } : {}), }, ); child.stdin?.end(); @@ -537,6 +538,42 @@ export function handleClaudeRequest( /** 缓存的 CLI 路径 */ let cachedCliPath: string | null | undefined = undefined; +/** + * Windows 上 `where claude` 常指向 npm 目录里无后缀的 `claude`(实为 Unix shell 脚本)。 + * `child_process.spawn` 无法将其作为可执行文件启动,会报 ENOENT;同目录下的 `claude.cmd` 才是可 spawn 的入口。 + */ +function normalizeClaudeCliPathForWindowsSpawn(resolvedPath: string): string { + if (process.platform !== 'win32') { + return resolvedPath; + } + const ext = path.extname(resolvedPath).toLowerCase(); + if ( + ext === '.cmd' || + ext === '.bat' || + ext === '.exe' || + ext === '.com' || + ext === '.ps1' + ) { + return resolvedPath; + } + if (path.basename(resolvedPath) !== 'claude') { + return resolvedPath; + } + const cmdPath = path.join(path.dirname(resolvedPath), 'claude.cmd'); + return fs.existsSync(cmdPath) ? cmdPath : resolvedPath; +} + +/** + * Windows 上不能直接 spawn `.cmd`/`.bat`/`.ps1`(会 EINVAL),需 `shell: true` 经 cmd 解释。 + */ +function cliPathNeedsShellSpawnOnWindows(cliPath: string): boolean { + if (process.platform !== 'win32') { + return false; + } + const ext = path.extname(cliPath).toLowerCase(); + return ext === '.cmd' || ext === '.bat' || ext === '.ps1'; +} + /** * 查找本地 Claude Code CLI 路径 */ @@ -553,7 +590,8 @@ function findClaudeCodeCli(): string | null { stdio: ['pipe', 'pipe', 'pipe'], }).trim(); if (result) { - cachedCliPath = result.split('\n')[0]; + const first = result.split(/\r?\n/)[0].trim(); + cachedCliPath = normalizeClaudeCliPathForWindowsSpawn(first); return cachedCliPath; } } catch { @@ -571,7 +609,7 @@ function findClaudeCodeCli(): string | null { for (const p of possiblePaths) { if (fs.existsSync(p)) { - cachedCliPath = p; + cachedCliPath = normalizeClaudeCliPathForWindowsSpawn(p); return cachedCliPath; } } @@ -666,6 +704,7 @@ function queryViaCli( cwd, env, stdio: ['pipe', 'pipe', 'pipe'], + ...(cliPathNeedsShellSpawnOnWindows(cliPath) ? { shell: true } : {}), }); if (inputMessage) { diff --git a/packages/core/types/server/ai-provider-claude.d.ts b/packages/core/types/server/ai-provider-claude.d.ts index 77c4e5d5..6b195a01 100644 --- a/packages/core/types/server/ai-provider-claude.d.ts +++ b/packages/core/types/server/ai-provider-claude.d.ts @@ -88,7 +88,7 @@ declare function findClaudeCodeCli(): string | null; */ declare function queryViaCli(cliPath: string, prompt: string, inputMessage: ClaudeCliInputMessage | undefined, cwd: string, aiOptions: ClaudeCodeOptions | undefined, onData: (data: string) => void, onError: (error: string) => void, onEnd: () => void, sessionId?: string, onSessionId?: (id: string) => void): ChildProcess; declare function getClaudeQuery(): Promise; -declare function setupSdkEnvironment(aiOptions?: ClaudeCodeOptions): void; +declare function setupSdkEnvironment(aiOptions?: ClaudeCodeOptions): () => void; declare function buildSdkQueryOptions(aiOptions: ClaudeCodeOptions | undefined, cwd: string, sessionId?: string): Record; declare function queryViaSdk(prompt: string | AsyncIterable, cwd: string, aiOptions: ClaudeCodeOptions | undefined, sessionId: string | undefined, sendSSE: (data: object | string) => void, isAborted: () => boolean): Promise<{ timedOut: boolean; From c6378f35fae8ab554bb0367fa30133e7a7a58fed Mon Sep 17 00:00:00 2001 From: shijie-todd <55285084+shijie-todd@users.noreply.github.com> Date: Fri, 10 Apr 2026 10:39:48 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8Dspawn=E5=85=A5?= =?UTF-8?q?=E5=8F=82=E8=A2=AB=E5=88=87=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/src/server/ai-provider-claude.ts | 79 +++++++++++++++++-- 1 file changed, 73 insertions(+), 6 deletions(-) diff --git a/packages/core/src/server/ai-provider-claude.ts b/packages/core/src/server/ai-provider-claude.ts index c3902038..d1fa4a2d 100644 --- a/packages/core/src/server/ai-provider-claude.ts +++ b/packages/core/src/server/ai-provider-claude.ts @@ -51,6 +51,35 @@ interface ClaudeCliInputMessage { const INLINE_IMAGE_DATA_URL_REGEX = /data:(image\/[a-zA-Z0-9.+-]+);base64,([A-Za-z0-9+/=]+)/g; +const CLAUDE_MODEL_LOG_MAX_CHARS = 1800; + +function truncateForLog(text: string, max = CLAUDE_MODEL_LOG_MAX_CHARS): string { + if (!text) return ''; + if (text.length <= max) return text; + return `${text.slice(0, max)}...[truncated ${text.length - max} chars]`; +} + +function stringifyForLog(value: unknown): string { + if (typeof value === 'string') { + return truncateForLog(value); + } + try { + return truncateForLog(JSON.stringify(value)); + } catch { + return truncateForLog(String(value)); + } +} + +function logClaudeModelIO(stage: string, payload: unknown): void { + const prefix = chalk.gray('[claude-model-io]'); + const output = stringifyForLog(payload); + if (output) { + console.log(`${prefix} ${stage}: ${output}`); + } else { + console.log(`${prefix} ${stage}`); + } +} + function stripInlineImageDataUrls(text: string): string { return text.replace( INLINE_IMAGE_DATA_URL_REGEX, @@ -235,13 +264,20 @@ export async function getModelInfo( try { const model = await new Promise((resolve) => { + const probeArgs = ['-p', 'hi', '--output-format', 'stream-json', '--verbose']; + const probeUseShell = cliPathNeedsShellSpawnOnWindows(cliPath); + logClaudeModelIO('probe.spawn', { + cliPath, + args: probeArgs, + shell: probeUseShell, + }); const child = spawn( cliPath, - ['-p', 'hi', '--output-format', 'stream-json', '--verbose'], + probeArgs, { stdio: ['pipe', 'pipe', 'pipe'], env: getEnvVars(), - ...(cliPathNeedsShellSpawnOnWindows(cliPath) ? { shell: true } : {}), + ...(probeUseShell ? { shell: true } : {}), }, ); child.stdin?.end(); @@ -259,6 +295,7 @@ export async function getModelInfo( for (const line of lines) { if (!line.trim()) continue; + logClaudeModelIO('probe.stdout', line); try { const event = JSON.parse(line); if (event.type === 'system' && event.model) { @@ -634,7 +671,16 @@ function queryViaCli( onSessionId?: (id: string) => void, ): ChildProcess { const opts = getClaudeCliOptions(aiOptions); - const args = inputMessage + // On Windows with shell spawning, long multiline prompt passed as CLI args can + // be mangled by cmd parsing. Force stream-json stdin input to avoid arg parsing issues. + const forceStreamInputOnWindows = process.platform === 'win32'; + const effectiveInputMessage = + inputMessage || + (forceStreamInputOnWindows + ? buildClaudeCliInputMessage(prompt, [], sessionId) + : undefined); + + const args = effectiveInputMessage ? [ '-p', '--output-format', @@ -699,16 +745,28 @@ function queryViaCli( } const env = { ...getEnvVars(), ...opts?.env }; + const useShell = cliPathNeedsShellSpawnOnWindows(cliPath); + logClaudeModelIO('cli.spawn', { + cliPath, + args, + cwd, + shell: useShell, + hasInputMessage: Boolean(effectiveInputMessage), + promptPreview: effectiveInputMessage ? '[sent via stream-json stdin]' : prompt, + }); const child = spawn(cliPath, args, { cwd, env, stdio: ['pipe', 'pipe', 'pipe'], - ...(cliPathNeedsShellSpawnOnWindows(cliPath) ? { shell: true } : {}), + ...(useShell ? { shell: true } : {}), }); - if (inputMessage) { - child.stdin?.write(`${JSON.stringify(inputMessage)}\n`); + if (effectiveInputMessage) { + logClaudeModelIO('cli.stdin', effectiveInputMessage); + child.stdin?.write(`${JSON.stringify(effectiveInputMessage)}\n`); + } else { + logClaudeModelIO('cli.prompt', prompt); } child.stdin?.end(); @@ -728,6 +786,7 @@ function queryViaCli( for (const line of lines) { if (!line.trim()) continue; + logClaudeModelIO('cli.stdout', line); try { const event = JSON.parse(line); @@ -1036,10 +1095,17 @@ async function queryViaSdk( } const queryOptions = buildSdkQueryOptions(aiOptions, cwd, sessionId); + logClaudeModelIO('sdk.query.options', queryOptions); + if (typeof prompt === 'string') { + logClaudeModelIO('sdk.query.prompt', prompt); + } else { + logClaudeModelIO('sdk.query.prompt', '[async iterable multimodal input]'); + } if (typeof queryOptions.stderr !== 'function') { queryOptions.stderr = (data: string) => { const text = String(data || '').trim(); if (text) { + logClaudeModelIO('sdk.stderr', text); sendSSE({ type: 'info', message: text }); } }; @@ -1081,6 +1147,7 @@ async function queryViaSdk( try { for await (const sdkMessage of conversation) { + logClaudeModelIO('sdk.event', sdkMessage); if (isAborted()) { conversation.interrupt(); break;