diff --git a/desktop/scripts/build-sidecars.ts b/desktop/scripts/build-sidecars.ts index 261f25d3..d98597df 100644 --- a/desktop/scripts/build-sidecars.ts +++ b/desktop/scripts/build-sidecars.ts @@ -26,21 +26,17 @@ if (scanExit !== 0) { await mkdir(binariesDir, { recursive: true }) +// 单一合并 sidecar:server / cli 共享一份 bun runtime + 共享依赖代码。 +// 调用方(Tauri lib.rs / conversationService)通过第一个 positional 参数 +// 选择 'server' 或 'cli' 模式,详见 desktop/sidecars/claude-sidecar.ts。 await compileExecutable({ - entrypoint: path.join(desktopRoot, 'sidecars/server-launcher.ts'), - outfileBase: path.join(binariesDir, `claude-server-${targetTriple}`), - productName: 'Claude Code Server', + entrypoint: path.join(desktopRoot, 'sidecars/claude-sidecar.ts'), + outfileBase: path.join(binariesDir, `claude-sidecar-${targetTriple}`), + productName: 'Claude Code Sidecar', bunTarget, }) -await compileExecutable({ - entrypoint: path.join(desktopRoot, 'sidecars/cli-launcher.ts'), - outfileBase: path.join(binariesDir, `claude-cli-${targetTriple}`), - productName: 'Claude Code CLI', - bunTarget, -}) - -console.log(`[build-sidecars] Built desktop sidecars for ${targetTriple} (${bunTarget})`) +console.log(`[build-sidecars] Built desktop sidecar for ${targetTriple} (${bunTarget})`) async function detectHostTriple() { const proc = Bun.spawn(['rustc', '-vV'], { diff --git a/desktop/sidecars/claude-sidecar.ts b/desktop/sidecars/claude-sidecar.ts new file mode 100644 index 00000000..363abc67 --- /dev/null +++ b/desktop/sidecars/claude-sidecar.ts @@ -0,0 +1,64 @@ +/** + * Claude Code 桌面端合并 sidecar 入口。 + * + * 历史上 server 和 cli 是两个独立的 bun-compile 二进制,各自携带一份 + * ~55MB 的 bun runtime。把两份合并成一个二进制只保留一份 runtime, + * 是 P0 之后剩余空间的最大优化点。 + * + * 调用约定(必须由调用方传第一个 positional 参数选择模式): + * + * claude-sidecar server --app-root --host 127.0.0.1 --port 12345 + * claude-sidecar cli --app-root [其它 CLI 参数...] + * + * 任何模式都必须先做 process.env / process.argv 设置,再 await 进入相应的 + * 子模块树,原因和 server-launcher.ts / cli-launcher.ts 注释一致 —— + * src/server/index.ts 顶层立刻读 process.argv,必须在它求值前 splice 掉 + * --app-root 和 mode 这些 launcher-only 参数。 + */ + +const rawArgs = process.argv.slice(2) +if (rawArgs.length === 0) { + console.error('claude-sidecar: missing mode argument (expected "server" or "cli")') + process.exit(2) +} +const mode = rawArgs[0]! +const restArgs = rawArgs.slice(1) + +const { appRoot, args } = parseLauncherArgs(restArgs) + +process.env.CLAUDE_APP_ROOT = appRoot +process.env.CALLER_DIR ||= process.cwd() +process.argv = [process.argv[0]!, process.argv[1]!, ...args] + +await import('../../preload.ts') + +if (mode === 'server') { + const { startServer } = await import('../../src/server/index.ts') + startServer() +} else if (mode === 'cli') { + await import('../../src/entrypoints/cli.tsx') +} else { + console.error(`claude-sidecar: unknown mode "${mode}" (expected "server" or "cli")`) + process.exit(2) +} + +function parseLauncherArgs(rawArgs: string[]): { appRoot: string; args: string[] } { + const nextArgs: string[] = [] + let appRoot: string | null = process.env.CLAUDE_APP_ROOT ?? null + + for (let index = 0; index < rawArgs.length; index++) { + const arg = rawArgs[index] + if (arg === '--app-root') { + appRoot = rawArgs[index + 1] ?? null + index += 1 + continue + } + nextArgs.push(arg!) + } + + if (!appRoot) { + throw new Error('Missing --app-root for claude-sidecar') + } + + return { appRoot, args: nextArgs } +} diff --git a/desktop/sidecars/cli-launcher.ts b/desktop/sidecars/cli-launcher.ts deleted file mode 100644 index 06019bb3..00000000 --- a/desktop/sidecars/cli-launcher.ts +++ /dev/null @@ -1,38 +0,0 @@ -/** - * Claude Code 桌面端 CLI sidecar 入口。 - * - * 与 server-launcher.ts 同理:通过字面量 specifier 的 dynamic import - * 让 `bun build --compile` 把整棵 src/entrypoints/cli.tsx 依赖图静态 - * 内联进二进制。运行时不再依赖磁盘上的 src/ 或 node_modules/。 - */ - -const { appRoot, args } = parseLauncherArgs() - -process.env.CLAUDE_APP_ROOT = appRoot -process.env.CALLER_DIR ||= process.cwd() -process.argv = [process.argv[0]!, process.argv[1]!, ...args] - -await import('../../preload.ts') -await import('../../src/entrypoints/cli.tsx') - -function parseLauncherArgs() { - const rawArgs = process.argv.slice(2) - const nextArgs: string[] = [] - let appRoot: string | null = process.env.CLAUDE_APP_ROOT ?? null - - for (let index = 0; index < rawArgs.length; index++) { - const arg = rawArgs[index] - if (arg === '--app-root') { - appRoot = rawArgs[index + 1] ?? null - index += 1 - continue - } - nextArgs.push(arg) - } - - if (!appRoot) { - throw new Error('Missing --app-root for claude-cli sidecar') - } - - return { appRoot, args: nextArgs } -} diff --git a/desktop/sidecars/server-launcher.ts b/desktop/sidecars/server-launcher.ts deleted file mode 100644 index eadf059c..00000000 --- a/desktop/sidecars/server-launcher.ts +++ /dev/null @@ -1,44 +0,0 @@ -/** - * Claude Code 桌面端 server sidecar 入口。 - * - * 这里把 src/server 这棵树通过字面量 specifier 的 dynamic import 拉进来, - * `bun build --compile` 会把它们作为静态依赖完整内联进单个二进制 —— - * 编译后运行时不再需要磁盘上的 src/ 或 node_modules/。 - * - * 注意:先做 process.env / process.argv 设置,再 await import,这样 - * - src/server/index.ts 顶层读 process.argv 时拿到的是被剥过 --app-root 的值; - * - preload.ts 设置的 MACRO 全局在 server 模块求值前到位。 - */ - -const { appRoot, args } = parseLauncherArgs() - -// 维持 conversationService → CLI 子进程的兼容契约:CLI sidecar 仍然 -// 接受 --app-root 参数,所以 server 这边把它原样透传到环境变量里。 -process.env.CLAUDE_APP_ROOT = appRoot -process.argv = [process.argv[0]!, process.argv[1]!, ...args] - -await import('../../preload.ts') -const { startServer } = await import('../../src/server/index.ts') -startServer() - -function parseLauncherArgs() { - const rawArgs = process.argv.slice(2) - const nextArgs: string[] = [] - let appRoot: string | null = process.env.CLAUDE_APP_ROOT ?? null - - for (let index = 0; index < rawArgs.length; index++) { - const arg = rawArgs[index] - if (arg === '--app-root') { - appRoot = rawArgs[index + 1] ?? null - index += 1 - continue - } - nextArgs.push(arg) - } - - if (!appRoot) { - throw new Error('Missing --app-root for claude-server sidecar') - } - - return { appRoot, args: nextArgs } -} diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index aed1b733..da5803c7 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -105,11 +105,13 @@ fn start_server_sidecar(app: &AppHandle) -> Result { let app_root = resolve_app_root(app)?; let app_root_arg = app_root.to_string_lossy().to_string(); + // 单一合并 sidecar:第一个参数选 server / cli 模式。 let sidecar = app .shell() - .sidecar("claude-server") - .map_err(|err| format!("resolve server sidecar: {err}"))? + .sidecar("claude-sidecar") + .map_err(|err| format!("resolve sidecar: {err}"))? .args([ + "server", "--app-root", &app_root_arg, "--host", diff --git a/desktop/src-tauri/tauri.conf.json b/desktop/src-tauri/tauri.conf.json index dfc09528..80c24ddf 100644 --- a/desktop/src-tauri/tauri.conf.json +++ b/desktop/src-tauri/tauri.conf.json @@ -44,8 +44,7 @@ "targets": "all", "createUpdaterArtifacts": true, "externalBin": [ - "binaries/claude-server", - "binaries/claude-cli" + "binaries/claude-sidecar" ], "resources": {}, "icon": [ diff --git a/src/server/services/conversationService.ts b/src/server/services/conversationService.ts index 0fff4098..a7ee07eb 100644 --- a/src/server/services/conversationService.ts +++ b/src/server/services/conversationService.ts @@ -448,19 +448,29 @@ export class ConversationService { } private resolveBundledCliPath(): string | null { + // 桌面端 P0+P2 之后只有一个合并的 sidecar 二进制 —— `claude-sidecar`, + // 它通过第一个 positional 参数 (server / cli) 选模式。当前进程要么 + // 已经是这个 sidecar 自己(spawn 子 CLI 时复用同一个文件),要么是 + // 旧 dev 模式下走 bin/claude-haha。这里支持两种命名: + // - 桌面端 prod build:进程名 claude-sidecar* + // - 旧 server-only 二进制(向后兼容):claude-server* const execPath = process.execPath const execName = path.basename(execPath) - if (!execName.startsWith('claude-server')) { - return null + if (execName.startsWith('claude-sidecar')) { + // 复用同一个二进制,调用 cli 模式 + return execPath } - const bundledCliPath = path.join( - path.dirname(execPath), - execName.replace(/^claude-server/, 'claude-cli'), - ) + if (execName.startsWith('claude-server')) { + const bundledCliPath = path.join( + path.dirname(execPath), + execName.replace(/^claude-server/, 'claude-cli'), + ) + return fs.existsSync(bundledCliPath) ? bundledCliPath : null + } - return fs.existsSync(bundledCliPath) ? bundledCliPath : null + return null } private resolveCliArgs(baseArgs: string[]): string[] { @@ -473,9 +483,21 @@ export class ConversationService { return ['bun', cliCommand, ...baseArgs] } + const cliBaseName = path.basename(cliCommand) + + // 合并 sidecar 模式:第一个参数必须是 'cli',后面跟 --app-root 透传 + if (cliBaseName.startsWith('claude-sidecar')) { + const args = ['cli', ...baseArgs] + if (process.env.CLAUDE_APP_ROOT) { + return [cliCommand, 'cli', '--app-root', process.env.CLAUDE_APP_ROOT, ...baseArgs] + } + return [cliCommand, ...args] + } + + // 旧两段式 sidecar:claude-cli 二进制需要 --app-root if ( process.env.CLAUDE_APP_ROOT && - path.basename(cliCommand).startsWith('claude-cli') + cliBaseName.startsWith('claude-cli') ) { return [ cliCommand,