Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/main/lib/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,7 @@ function normalizeLlmConnections(settings: AppSettings): AppSettings {
authMode: 'managed-login',
model: chatgpt?.model || '',
executablePath: chatgpt?.executablePath,
preferSystemCodex: chatgpt?.preferSystemCodex === true,
enabled: chatgpt?.enabled !== false,
setupStatus: chatgpt?.setupStatus ?? (chatGptWasActive ? settings.llmSetupStatus : 'unconfigured'),
lastVerifiedAt: chatgpt?.lastVerifiedAt ?? (chatGptWasActive ? settings.llmLastVerifiedAt : '')
Expand Down Expand Up @@ -560,6 +561,7 @@ export function setSettings(next: Partial<AppSettings>): AppSettings {
previous?.endpoint !== connection.endpoint ||
previous?.model !== connection.model ||
previous?.executablePath !== connection.executablePath ||
(previous?.preferSystemCodex === true) !== (connection.preferSystemCodex === true) ||
(connection.id === targetConnectionId && apiKeyChanged)
if (connection.id !== targetConnectionId) return connection
if (Object.prototype.hasOwnProperty.call(next, 'llmSetupStatus')) {
Expand Down
1 change: 1 addition & 0 deletions src/main/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ export interface LlmConnectionProfile {
model: string
endpoint?: string
executablePath?: string
preferSystemCodex?: boolean
enabled: boolean
setupStatus?: LlmSetupStatus
lastVerifiedAt?: string
Expand Down
4 changes: 2 additions & 2 deletions src/main/services/llm/codexAppServerAgentRuntime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ export class CodexAppServerAgentRuntime implements AgentRuntimeAdapter {

private readonly client: CodexAppServerClient

constructor(executablePath = '') {
this.client = new CodexAppServerClient(executablePath)
constructor(executablePath = '', preferSystemCodex = false) {
this.client = new CodexAppServerClient(executablePath, preferSystemCodex)
}

connectionState(connectionId: string): Promise<LlmConnectionState> {
Expand Down
11 changes: 8 additions & 3 deletions src/main/services/llm/codexAppServerClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { tmpdir } from 'node:os'
import { dirname, join, sep } from 'node:path'
import { createInterface } from 'node:readline'
import { appHome } from '@main/lib/store'
import { findCodexOnPath } from './codexPath'
import type {
LlmConnectionState,
LlmLoginStartResult,
Expand Down Expand Up @@ -124,12 +125,16 @@ function packagedCodexExecutable(): string | null {
return existsSync(executable) ? executable : null
}

export function resolveCodexExecutable(configuredPath = ''): string {
export function resolveCodexExecutable(configuredPath = '', preferSystemCodex = false): string {
const explicit = configuredPath.trim() || process.env.GOAGENT_CODEX_BIN?.trim()
if (explicit) {
if (!existsSync(explicit)) throw new Error(`找不到指定的 ChatGPT 运行组件:${explicit}`)
return explicit
}
if (preferSystemCodex) {
const fromPath = findCodexOnPath()
if (fromPath) return fromPath
}
const packaged = packagedCodexExecutable()
if (packaged) return packaged
const bundled = bundledCodexExecutable()
Expand Down Expand Up @@ -243,7 +248,7 @@ export class CodexAppServerClient {
private activeTurns = new Map<string, string>()
private stderrTail = ''

constructor(private executablePath = '') {}
constructor(private executablePath = '', private preferSystemCodex = false) {}

private async ensureStarted(): Promise<void> {
if (this.started) return this.started
Expand All @@ -255,7 +260,7 @@ export class CodexAppServerClient {
}

private async startProcess(): Promise<void> {
const command = resolveCodexExecutable(this.executablePath)
const command = resolveCodexExecutable(this.executablePath, this.preferSystemCodex)
this.stderrTail = ''
mkdirSync(CODEX_HOME, { recursive: true })
const configOverrides = [
Expand Down
54 changes: 54 additions & 0 deletions src/main/services/llm/codexPath.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { accessSync, constants, existsSync, statSync } from 'node:fs'
import { createRequire } from 'node:module'
import { delimiter, dirname, isAbsolute, join } from 'node:path'

function executableFile(path: string, platform: NodeJS.Platform): boolean {
try {
if (!statSync(path).isFile()) return false
accessSync(path, platform === 'win32' ? constants.F_OK : constants.X_OK)
return true
} catch {
return false
}
}

// npm installs a .cmd shim on Windows. Resolve its native payload instead of
// launching a shell: Electron cannot spawn .cmd files directly, and a shell
// would complicate quoting and leave an intermediate process on cancellation.
function npmCodexExecutable(directory: string, arch: string): string | null {
const triple = arch === 'x64' ? 'x86_64-pc-windows-msvc'
: arch === 'arm64' ? 'aarch64-pc-windows-msvc' : null
if (!triple) return null
const packageJson = join(directory, 'node_modules', '@openai', 'codex', 'package.json')
if (!existsSync(packageJson)) return null
const roots = [dirname(packageJson)]
try {
roots.unshift(dirname(createRequire(packageJson).resolve(`@openai/codex-win32-${arch}/package.json`)))
} catch {
// Older npm packages include vendor/ directly in @openai/codex.
}
for (const root of roots) {
const candidate = join(root, 'vendor', triple, 'bin', 'codex.exe')
if (executableFile(candidate, 'win32')) return candidate
}
return null
}

export function findCodexOnPath(
pathValue = process.env.PATH ?? '',
platform: NodeJS.Platform = process.platform,
arch: string = process.arch
): string | null {
for (const entry of pathValue.split(platform === 'win32' ? ';' : delimiter)) {
const directory = entry.trim().replace(/^"(.*)"$/, '$1')
// Do not accidentally discover an executable in the current project.
if (!directory || !isAbsolute(directory)) continue
const candidate = join(directory, platform === 'win32' ? 'codex.exe' : 'codex')
if (executableFile(candidate, platform)) return candidate
if (platform === 'win32' && existsSync(join(directory, 'codex.cmd'))) {
const native = npmCodexExecutable(directory, arch)
if (native) return native
}
}
return null
}
7 changes: 5 additions & 2 deletions src/main/services/llm/providerRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { OpenAICompatibleAgentRuntime } from './openAICompatibleAgentRuntime'
const openAICompatibleRuntime = new OpenAICompatibleAgentRuntime()
let codexRuntime: CodexAppServerAgentRuntime | null = null
let codexExecutablePath = ''
let codexPreferSystem = false

const CODEX_DOMAIN_TOOL_NAMES = new Set([
'library_findGames',
Expand Down Expand Up @@ -47,10 +48,12 @@ export function toolsForLlmConnection(profile: LlmConnectionProfile, tools: Chat

function codexRuntimeFor(profile: LlmConnectionProfile): CodexAppServerAgentRuntime {
const executablePath = profile.executablePath?.trim() || ''
if (!codexRuntime || executablePath !== codexExecutablePath) {
const preferSystemCodex = profile.preferSystemCodex === true
if (!codexRuntime || executablePath !== codexExecutablePath || preferSystemCodex !== codexPreferSystem) {
codexRuntime?.dispose()
codexExecutablePath = executablePath
codexRuntime = new CodexAppServerAgentRuntime(executablePath)
codexPreferSystem = preferSystemCodex
codexRuntime = new CodexAppServerAgentRuntime(executablePath, preferSystemCodex)
}
return codexRuntime
}
Expand Down
17 changes: 17 additions & 0 deletions src/renderer/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5262,6 +5262,23 @@ function SettingsDrawer({
<button className="primary-button" type="button" onClick={() => void loginWithChatGpt()}>{t('chatGptSignIn')}</button>
)}
</div>
<label>
<span className="settings-inline-toggle">
<input
type="checkbox"
checked={activeLlmConnection?.preferSystemCodex === true}
onChange={(event) => autoSave({
llmConnections: dashboard.settings.llmConnections.map((connection) =>
connection.id === dashboard.settings.activeLlmConnectionId
? { ...connection, preferSystemCodex: event.target.checked }
: connection
)
})}
/>
{t('chatGptPreferSystemCodex')}
</span>
<small>{t('chatGptPreferSystemCodexHelp')}</small>
</label>
<label>
{t('chatGptRuntimePath')}
<input
Expand Down
Loading