codexLocator.ts 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119
  1. import { access, realpath } from 'node:fs/promises';
  2. import { constants } from 'node:fs';
  3. import { createRequire } from 'node:module';
  4. import { dirname, join } from 'node:path';
  5. import { spawnSync } from 'node:child_process';
  6. import { getBaseDir } from 'ee-core/ps';
  7. /** 移植自 Noobi.ai src/main/codexLocator.ts,改动:去掉 ChatGPT.app 分支、改用 CJS 可用的 require 基准、增加不抛错的探测入口 */
  8. const TARGETS: Record<string, { packageName: string; triple: string; executable: string }> = {
  9. 'darwin-arm64': {
  10. packageName: '@openai/codex-darwin-arm64',
  11. triple: 'aarch64-apple-darwin',
  12. executable: 'codex',
  13. },
  14. 'darwin-x64': {
  15. packageName: '@openai/codex-darwin-x64',
  16. triple: 'x86_64-apple-darwin',
  17. executable: 'codex',
  18. },
  19. 'linux-arm64': {
  20. packageName: '@openai/codex-linux-arm64',
  21. triple: 'aarch64-unknown-linux-musl',
  22. executable: 'codex',
  23. },
  24. 'linux-x64': {
  25. packageName: '@openai/codex-linux-x64',
  26. triple: 'x86_64-unknown-linux-musl',
  27. executable: 'codex',
  28. },
  29. 'win32-arm64': {
  30. packageName: '@openai/codex-win32-arm64',
  31. triple: 'aarch64-pc-windows-msvc',
  32. executable: 'codex.exe',
  33. },
  34. 'win32-x64': {
  35. packageName: '@openai/codex-win32-x64',
  36. triple: 'x86_64-pc-windows-msvc',
  37. executable: 'codex.exe',
  38. },
  39. };
  40. export interface CodexBinaryProbe {
  41. available: boolean;
  42. binaryPath: string | null;
  43. version: string | null;
  44. reason: string | null;
  45. }
  46. /** 探测顺序:环境变量 → 平台包 vendor → 应用根 node_modules → 系统 PATH */
  47. function collectCandidates(): string[] {
  48. const candidates: string[] = [];
  49. if (process.env.ZSJZ_CODEX_BIN) candidates.push(process.env.ZSJZ_CODEX_BIN);
  50. const target = TARGETS[`${process.platform}-${process.arch}`];
  51. if (target) {
  52. // ee-bin 把主进程 bundle 成单文件 CJS,这里不能依赖 import.meta.url
  53. try {
  54. const packageJson = createRequire(__filename).resolve(`${target.packageName}/package.json`);
  55. candidates.push(join(dirname(packageJson), 'vendor', target.triple, 'bin', target.executable));
  56. } catch {
  57. // 平台包是 optionalDependency,缺失时继续尝试下面的候选
  58. }
  59. candidates.push(join(getBaseDir(), 'node_modules', target.packageName, 'vendor', target.triple, 'bin', target.executable));
  60. }
  61. const which = spawnSync(process.platform === 'win32' ? 'where' : 'which', ['codex'], {
  62. encoding: 'utf8',
  63. timeout: 3_000,
  64. });
  65. if (which.status === 0 && which.stdout.trim()) {
  66. candidates.push(which.stdout.trim().split(/\r?\n/u)[0]!);
  67. }
  68. return deduplicate(candidates);
  69. }
  70. /** 打包后 node_modules 在 app.asar 内,可执行文件必须走 asar.unpacked(builder.json 的 asarUnpack) */
  71. export function unpackedAsarPath(value: string): string {
  72. return value.replace(/([/\\])app\.asar([/\\])/u, '$1app.asar.unpacked$2');
  73. }
  74. function deduplicate(values: string[]): string[] {
  75. return [...new Set(values.filter(Boolean))];
  76. }
  77. export async function locateCodexBinary(): Promise<string> {
  78. const failures: string[] = [];
  79. for (const lexical of collectCandidates()) {
  80. const candidate = unpackedAsarPath(lexical);
  81. try {
  82. await access(candidate, constants.X_OK);
  83. return await realpath(candidate);
  84. } catch (error) {
  85. failures.push(`${candidate}: ${error instanceof Error ? error.message : String(error)}`);
  86. }
  87. }
  88. throw new Error(
  89. `找不到可执行的 Codex 二进制,请设置 ZSJZ_CODEX_BIN。已检查 ${failures.length} 个候选路径。`,
  90. );
  91. }
  92. /** 供 ping 使用:任何失败都降级成 available:false + reason,不抛错 */
  93. export async function probeCodexBinary(): Promise<CodexBinaryProbe> {
  94. try {
  95. const binaryPath = await locateCodexBinary();
  96. return { available: true, binaryPath, version: readCodexVersion(binaryPath), reason: null };
  97. } catch (error) {
  98. return {
  99. available: false,
  100. binaryPath: null,
  101. version: null,
  102. reason: error instanceof Error ? error.message : String(error),
  103. };
  104. }
  105. }
  106. export function readCodexVersion(binaryPath: string): string | null {
  107. const result = spawnSync(binaryPath, ['--version'], { encoding: 'utf8', timeout: 5_000 });
  108. return result.status === 0 ? result.stdout.trim() || null : null;
  109. }