From cd59a88d4412be29f75abadd0b4ebba2ef33cfc3 Mon Sep 17 00:00:00 2001 From: claude-code-best Date: Sun, 26 Apr 2026 21:58:30 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=9B=86=E6=88=90=20ChatGPT=20OAuth=20?= =?UTF-8?q?=E8=AE=A2=E9=98=85=E7=99=BB=E5=BD=95=E5=88=B0=20/login=20UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加 Codex ChatGPT 菜单项、OAuth 等待界面、手动 code 输入支持。 Co-Authored-By: Claude Opus 4.7 --- src/components/ConsoleOAuthFlow.tsx | 214 ++++++++++++++++++++++++++++ 1 file changed, 214 insertions(+) diff --git a/src/components/ConsoleOAuthFlow.tsx b/src/components/ConsoleOAuthFlow.tsx index 7b973fe75..236834401 100644 --- a/src/components/ConsoleOAuthFlow.tsx +++ b/src/components/ConsoleOAuthFlow.tsx @@ -10,6 +10,7 @@ import { useKeybinding } from '../keybindings/useKeybinding.js' import { getSSLErrorHint } from '@ant/model-provider' import { sendNotification } from '../services/notifier.js' import { OAuthService } from '../services/oauth/index.js' +import { performOpenAICodexLogin, parseManualCodeInput } from '../services/oauth/openai-codex.js' import { getOauthAccountInfo, validateForceLoginOrg } from '../utils/auth.js' import { logError } from '../utils/log.js' import { getSettings_DEPRECATED, updateSettingsForSource } from '../utils/settings/settings.js' @@ -55,6 +56,8 @@ type OAuthStatus = opusModel: string activeField: 'base_url' | 'api_key' | 'haiku_model' | 'sonnet_model' | 'opus_model' } // Gemini Generate Content API platform + | { state: 'codex_oauth_waiting'; url: string } // ChatGPT OAuth browser login in progress + | { state: 'codex_oauth_start' } // Trigger ChatGPT OAuth flow | { state: 'ready_to_start' } // Flow started, waiting for browser to open | { state: 'waiting_for_login'; url: string } // Browser opened, waiting for user to login | { state: 'creating_api_key' } // Got access token, creating API key @@ -108,6 +111,13 @@ export function ConsoleOAuthFlow({ const [showPastePrompt, setShowPastePrompt] = useState(false) const [urlCopied, setUrlCopied] = useState(false) + // Codex ChatGPT OAuth states + const [showCodexPastePrompt, setShowCodexPastePrompt] = useState(false) + const [codexUrlCopied, setCodexUrlCopied] = useState(false) + const [codexPastedCode, setCodexPastedCode] = useState('') + const [codexPastedCursor, setCodexPastedCursor] = useState(0) + const codexManualCodeResolveRef = useRef<((code: string) => void) | null>(null) + const textInputColumns = useTerminalSize().columns - PASTE_HERE_MSG.length - 1 // Log forced login method on mount @@ -186,6 +196,39 @@ export function ConsoleOAuthFlow({ } }, [pastedCode, oauthStatus, showPastePrompt, urlCopied]) + // Codex OAuth: copy URL on 'c' + useEffect(() => { + if ( + codexPastedCode === 'c' && + oauthStatus.state === 'codex_oauth_waiting' && + showCodexPastePrompt && + !codexUrlCopied + ) { + const url = (oauthStatus as { state: 'codex_oauth_waiting'; url: string }).url + void setClipboard(url).then(raw => { + if (raw) process.stdout.write(raw) + setCodexUrlCopied(true) + setTimeout(setCodexUrlCopied, 2000, false) + }) + setCodexPastedCode('') + } + }, [codexPastedCode, oauthStatus, showCodexPastePrompt, codexUrlCopied]) + + // Codex OAuth: submit pasted code + const handleCodexPasteSubmit = useCallback((value: string) => { + const code = parseManualCodeInput(value) + if (!code) { + setOAuthStatus({ + state: 'error', + message: 'Invalid code. Paste the full redirect URL or just the authorization code.', + toRetry: oauthStatus as any, + }) + return + } + codexManualCodeResolveRef.current?.(code) + codexManualCodeResolveRef.current = null + }, [oauthStatus]) + async function handleSubmitCode(value: string, url: string) { try { // Expecting format "authorizationCode#state" from the authorization callback URL @@ -301,6 +344,64 @@ export function ConsoleOAuthFlow({ } }, [oauthService, setShowPastePrompt, loginWithClaudeAi, mode, orgUUID]) + const startCodexOAuth = useCallback(async () => { + setShowCodexPastePrompt(false) + setCodexUrlCopied(false) + setCodexPastedCode('') + setCodexPastedCursor(0) + + let manualCodeResolve: ((code: string) => void) | null = null + const manualCodePromise = new Promise(resolve => { + manualCodeResolve = resolve + }) + codexManualCodeResolveRef.current = manualCodeResolve + + try { + const result = await performOpenAICodexLogin({ + onUrl: url => { + setOAuthStatus({ state: 'codex_oauth_waiting', url }) + setTimeout(setShowCodexPastePrompt, 3000, true) + }, + manualCode: manualCodePromise, + }) + + const env: Record = { + CODEX_API_KEY: result.apiKey ?? undefined, + CODEX_ACCESS_TOKEN: result.accessToken, + CODEX_REFRESH_TOKEN: result.refreshToken, + CODEX_LOGIN_METHOD: 'chatgpt_subscription', + } + updateSettingsForSource('userSettings', { + modelType: 'openai-responses' as any, + env, + } as any) + for (const [key, value] of Object.entries(env)) { + if (value !== undefined) { + process.env[key] = value + } + } + + setOAuthStatus({ state: 'success' }) + void sendNotification( + { + message: 'OpenAI Codex (ChatGPT) login successful', + notificationType: 'auth_success', + }, + terminal, + ) + onDone() + } catch (err) { + logError(err as Error) + setOAuthStatus({ + state: 'error', + message: (err as Error).message, + toRetry: { state: 'idle' }, + }) + } finally { + codexManualCodeResolveRef.current = null + } + }, [onDone]) + const pendingOAuthStartRef = useRef(false) useEffect(() => { @@ -316,6 +417,19 @@ export function ConsoleOAuthFlow({ } }, [oauthStatus.state, startOAuth]) + const pendingCodexOAuthRef = useRef(false) + useEffect(() => { + if ( + oauthStatus.state === 'codex_oauth_start' && + !pendingCodexOAuthRef.current + ) { + pendingCodexOAuthRef.current = true + void startCodexOAuth().finally(() => { + pendingCodexOAuthRef.current = false + }) + } + }, [oauthStatus.state, startCodexOAuth]) + // Auto-exit for setup-token mode useEffect(() => { if (mode === 'setup-token' && oauthStatus.state === 'success') { @@ -334,6 +448,20 @@ export function ConsoleOAuthFlow({ } }, [mode, oauthStatus, loginWithClaudeAi, onDone]) + // Cancel codex OAuth with Escape + useKeybinding( + 'confirm:no', + () => { + setShowCodexPastePrompt(false) + setCodexPastedCode('') + setOAuthStatus({ state: 'idle' }) + }, + { + context: 'Confirmation', + isActive: oauthStatus.state === 'codex_oauth_waiting', + }, + ) + // Cleanup OAuth service when component unmounts useEffect(() => { return () => { @@ -399,6 +527,13 @@ export function ConsoleOAuthFlow({ setOAuthStatus={setOAuthStatus} setLoginWithClaudeAi={setLoginWithClaudeAi} onDone={onDone} + showCodexPastePrompt={showCodexPastePrompt} + codexUrlCopied={codexUrlCopied} + codexPastedCode={codexPastedCode} + setCodexPastedCode={setCodexPastedCode} + codexPastedCursor={codexPastedCursor} + setCodexPastedCursor={setCodexPastedCursor} + handleCodexPasteSubmit={handleCodexPasteSubmit} /> @@ -420,6 +555,14 @@ type OAuthStatusMessageProps = { handleSubmitCode: (value: string, url: string) => void setOAuthStatus: (status: OAuthStatus) => void setLoginWithClaudeAi: (value: boolean) => void + // Codex ChatGPT OAuth props + showCodexPastePrompt: boolean + codexUrlCopied: boolean + codexPastedCode: string + setCodexPastedCode: (value: string) => void + codexPastedCursor: number + setCodexPastedCursor: (offset: number) => void + handleCodexPasteSubmit: (value: string) => void } function OAuthStatusMessage({ @@ -437,6 +580,13 @@ function OAuthStatusMessage({ setOAuthStatus, setLoginWithClaudeAi, onDone, + showCodexPastePrompt, + codexUrlCopied, + codexPastedCode, + setCodexPastedCode, + codexPastedCursor, + setCodexPastedCursor, + handleCodexPasteSubmit, }: OAuthStatusMessageProps): React.ReactNode { switch (oauthStatus.state) { case 'idle': @@ -475,6 +625,16 @@ function OAuthStatusMessage({ ), value: 'openai_chat_api', }, + { + label: ( + + OpenAI Codex (ChatGPT Subscription) -{' '} + Login with ChatGPT Plus/Pro + {'\n'} + + ), + value: 'codex_chatgpt', + }, { label: ( @@ -552,6 +712,9 @@ function OAuthStatusMessage({ opusModel: process.env.OPENAI_DEFAULT_OPUS_MODEL ?? '', activeField: 'base_url', }) + } else if (value === 'codex_chatgpt') { + logEvent('tengu_codex_chatgpt_selected', {}) + setOAuthStatus({ state: 'codex_oauth_start' }) } else if (value === 'gemini_api') { logEvent('tengu_gemini_api_selected', {}) setOAuthStatus({ @@ -1275,6 +1438,57 @@ function OAuthStatusMessage({ ) } + case 'codex_oauth_waiting': { + const { url } = oauthStatus as { state: 'codex_oauth_waiting'; url: string } + const codexPasteColumns = useTerminalSize().columns - PASTE_HERE_MSG.length - 1 + return ( + + {!showCodexPastePrompt && ( + + + Opening browser for ChatGPT login... + + )} + {showCodexPastePrompt && ( + + + + Browser didn't open? Use the url below to sign in{' '} + + {codexUrlCopied ? ( + (Copied!) + ) : ( + + + + )} + + + {url} + + + )} + {showCodexPastePrompt && ( + + {PASTE_HERE_MSG} + + + )} + + Press Esc to cancel + + + ) + } + case 'platform_setup': return (