import { access, realpath } from 'node:fs/promises'; import { constants } from 'node:fs'; import { createRequire } from 'node:module'; import { dirname, join } from 'node:path'; import { spawnSync } from 'node:child_process'; import { getBaseDir } from 'ee-core/ps'; /** 移植自 Noobi.ai src/main/codexLocator.ts,改动:去掉 ChatGPT.app 分支、改用 CJS 可用的 require 基准、增加不抛错的探测入口 */ const TARGETS: Record = { 'darwin-arm64': { packageName: '@openai/codex-darwin-arm64', triple: 'aarch64-apple-darwin', executable: 'codex', }, 'darwin-x64': { packageName: '@openai/codex-darwin-x64', triple: 'x86_64-apple-darwin', executable: 'codex', }, 'linux-arm64': { packageName: '@openai/codex-linux-arm64', triple: 'aarch64-unknown-linux-musl', executable: 'codex', }, 'linux-x64': { packageName: '@openai/codex-linux-x64', triple: 'x86_64-unknown-linux-musl', executable: 'codex', }, 'win32-arm64': { packageName: '@openai/codex-win32-arm64', triple: 'aarch64-pc-windows-msvc', executable: 'codex.exe', }, 'win32-x64': { packageName: '@openai/codex-win32-x64', triple: 'x86_64-pc-windows-msvc', executable: 'codex.exe', }, }; export interface CodexBinaryProbe { available: boolean; binaryPath: string | null; version: string | null; reason: string | null; } /** 探测顺序:环境变量 → 平台包 vendor → 应用根 node_modules → 系统 PATH */ function collectCandidates(): string[] { const candidates: string[] = []; if (process.env.ZSJZ_CODEX_BIN) candidates.push(process.env.ZSJZ_CODEX_BIN); const target = TARGETS[`${process.platform}-${process.arch}`]; if (target) { // ee-bin 把主进程 bundle 成单文件 CJS,这里不能依赖 import.meta.url try { const packageJson = createRequire(__filename).resolve(`${target.packageName}/package.json`); candidates.push(join(dirname(packageJson), 'vendor', target.triple, 'bin', target.executable)); } catch { // 平台包是 optionalDependency,缺失时继续尝试下面的候选 } candidates.push(join(getBaseDir(), 'node_modules', target.packageName, 'vendor', target.triple, 'bin', target.executable)); } const which = spawnSync(process.platform === 'win32' ? 'where' : 'which', ['codex'], { encoding: 'utf8', timeout: 3_000, }); if (which.status === 0 && which.stdout.trim()) { candidates.push(which.stdout.trim().split(/\r?\n/u)[0]!); } return deduplicate(candidates); } /** 打包后 node_modules 在 app.asar 内,可执行文件必须走 asar.unpacked(builder.json 的 asarUnpack) */ export function unpackedAsarPath(value: string): string { return value.replace(/([/\\])app\.asar([/\\])/u, '$1app.asar.unpacked$2'); } function deduplicate(values: string[]): string[] { return [...new Set(values.filter(Boolean))]; } export async function locateCodexBinary(): Promise { const failures: string[] = []; for (const lexical of collectCandidates()) { const candidate = unpackedAsarPath(lexical); try { await access(candidate, constants.X_OK); return await realpath(candidate); } catch (error) { failures.push(`${candidate}: ${error instanceof Error ? error.message : String(error)}`); } } throw new Error( `找不到可执行的 Codex 二进制,请设置 ZSJZ_CODEX_BIN。已检查 ${failures.length} 个候选路径。`, ); } /** 供 ping 使用:任何失败都降级成 available:false + reason,不抛错 */ export async function probeCodexBinary(): Promise { try { const binaryPath = await locateCodexBinary(); return { available: true, binaryPath, version: readCodexVersion(binaryPath), reason: null }; } catch (error) { return { available: false, binaryPath: null, version: null, reason: error instanceof Error ? error.message : String(error), }; } } export function readCodexVersion(binaryPath: string): string | null { const result = spawnSync(binaryPath, ['--version'], { encoding: 'utf8', timeout: 5_000 }); return result.status === 0 ? result.stdout.trim() || null : null; }