From e01aef44f5c3ea70e45812ce4fbbb2df1b4ee031 Mon Sep 17 00:00:00 2001 From: lzt404 <2596933790@qq.com> Date: Fri, 31 Jul 2026 20:21:17 +0800 Subject: [PATCH] fix(codex): validate OAuth credentials before enabling configuration --- src-tauri/src/main.rs | 48 +++++++++++++++++++++--- src/i18n/ja.ts | 10 +++-- src/i18n/resources.ts | 20 +++++++--- src/pages/AgentsPage.tsx | 81 ++++++++++++++++++++++++++++++---------- src/styles.css | 5 +++ 5 files changed, 130 insertions(+), 34 deletions(-) diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 5bb7a18..c1d079d 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -1608,6 +1608,15 @@ async fn get_agent_models( Ok(fetch_prepared_agent_models(client, &config).await?.models) } +#[tauri::command] +fn check_codex_oauth_login(app: tauri::AppHandle) -> Result<(), String> { + let home = app + .path() + .home_dir() + .map_err(|error| format!("无法获取用户目录: {error}"))?; + validate_codex_oauth_login(&home) +} + #[tauri::command] async fn update_codex_model_catalog( app: tauri::AppHandle, @@ -1902,11 +1911,7 @@ async fn fetch_prepared_agent_models( let api_key = effective_agent_api_key(config); if client == AgentClient::Codex { let runtime_models = fetch_codex_runtime_models(config.port, api_key).await?; - let catalog = codex_catalog::prepare_catalog(&runtime_models)?; - Ok(PreparedAgentModels { - models: catalog.models, - codex_catalog: Some(catalog.json), - }) + prepare_codex_agent_models(&runtime_models) } else { Ok(PreparedAgentModels { models: fetch_agent_models(config.port, api_key).await?, @@ -1915,6 +1920,24 @@ async fn fetch_prepared_agent_models( } } +fn prepare_codex_agent_models( + runtime_models: &[codex_catalog::CodexRuntimeModel], +) -> Result { + match codex_catalog::prepare_catalog(runtime_models) { + Ok(catalog) => Ok(PreparedAgentModels { + models: catalog.models, + codex_catalog: Some(catalog.json), + }), + Err(error) if error.contains("CPA 当前没有可写入 Codex 的模型") => { + Ok(PreparedAgentModels { + models: Vec::new(), + codex_catalog: None, + }) + } + Err(error) => Err(error), + } +} + #[tauri::command] async fn apply_agent_config( app: tauri::AppHandle, @@ -1930,6 +1953,9 @@ async fn apply_agent_config( .map_err(|error| format!("无法获取用户目录: {error}"))?; let config = gui_config_state.snapshot()?; let api_key = effective_agent_api_key(&config); + if client == AgentClient::Codex && oauth_configuration { + validate_codex_oauth_login(&home)?; + } validate_agent_can_enable(client, &home, config.port, api_key)?; let prepared = fetch_prepared_agent_models(client, &config).await?; let model = resolve_available_agent_model(&prepared.models, &validate_agent_model(&model)?)?; @@ -1965,6 +1991,9 @@ async fn reset_agent_config_to_default( .map_err(|error| format!("无法获取用户目录: {error}"))?; let config = gui_config_state.snapshot()?; let api_key = effective_agent_api_key(&config); + if client == AgentClient::Codex && oauth_configuration { + validate_codex_oauth_login(&home)?; + } validate_agent_can_enable(client, &home, config.port, api_key)?; let prepared = fetch_prepared_agent_models(client, &config).await?; let model = resolve_available_agent_model(&prepared.models, &validate_agent_model(&model)?)?; @@ -13513,6 +13542,7 @@ fn main() { get_agent_config_statuses, refresh_agent_config_statuses, get_agent_models, + check_codex_oauth_login, update_codex_model_catalog, get_thinking_aliases, get_thinking_alias_sources, @@ -14480,6 +14510,14 @@ mod tests { assert!(parse_agent_model_options(&serde_json::json!({"data": null})).is_err()); } + #[test] + fn codex_model_list_is_empty_when_cpa_has_no_writable_models() { + let prepared = prepare_codex_agent_models(&[]).unwrap(); + + assert!(prepared.models.is_empty()); + assert!(prepared.codex_catalog.is_none()); + } + #[test] fn agent_model_validation_only_accepts_models_in_current_list() { let models = vec![AgentModelOption { diff --git a/src/i18n/ja.ts b/src/i18n/ja.ts index f9fabbf..20126fc 100644 --- a/src/i18n/ja.ts +++ b/src/i18n/ja.ts @@ -402,13 +402,13 @@ export const jaOverrides = { 'agents.model.loading': 'モデルを読み込んでいます', 'agents.model.loadFailed': 'モデルの取得に失敗しました', 'agents.model.select': 'モデルを選択', - 'agents.model.none': '選択可能なモデルなし', + 'agents.model.none': 'モデル一覧は空です', 'agents.model.search': 'モデル名またはエイリアスを検索', 'agents.model.clearSearch': '検索を消去', 'agents.model.refresh': 'モデルを更新', 'agents.model.fetching': 'モデルを取得しています', 'agents.model.noMatch': '一致するモデルがありません', - 'agents.model.unavailable': '現在利用可能なモデルはありません', + 'agents.model.unavailable': 'モデル一覧は空です', 'agents.model.tryKeywords': '別のキーワードをお試しください', 'agents.model.connectFirst': '先に利用可能なモデルを接続してから更新してください', 'agents.model.available': '利用可能なモデル', @@ -513,7 +513,11 @@ export const jaOverrides = { 'agents.modify.title': '設定変更', 'agents.modify.oauthConfiguration': 'OAuth 設定方式', 'agents.oauthLoginRequired.title': 'Codex へのログインが必要です', - 'agents.oauthLoginRequired.description': 'OAuth 設定方式が有効ですが、有効な Codex のログイン認証情報が見つかりません。起動する前に Codex で ChatGPT アカウントにログインしてください。', + 'agents.oauthLoginRequired.enableDescription': '有効な Codex のログイン認証情報が見つからないため、OAuth 設定方式を有効にできません。Codex アプリで ChatGPT アカウントにログインしてください。Codex アプリでログインを開始できない場合は、先に', + 'agents.oauthLoginRequired.enableClearConfiguration': '設定を消去', + 'agents.oauthLoginRequired.enableDescriptionSuffix': 'を使用してください。', + 'agents.oauthLoginRequired.applyDescription': '有効な Codex のログイン認証情報が見つからないため、OAuth 設定を適用できません。Codex で ChatGPT アカウントにログインしてから、設定を適用してください。', + 'agents.oauthLoginRequired.launchDescription': 'OAuth 設定方式が有効ですが、有効な Codex のログイン認証情報が見つかりません。起動する前に Codex で ChatGPT アカウントにログインしてください。', 'agents.oauthLoginRequired.confirm': '了解しました', 'agents.modify.apply': '設定変更を適用', 'agents.modify.default': 'デフォルト設定', diff --git a/src/i18n/resources.ts b/src/i18n/resources.ts index 8da4214..2555da7 100644 --- a/src/i18n/resources.ts +++ b/src/i18n/resources.ts @@ -405,13 +405,13 @@ export const zhCN = { 'agents.model.loading': '正在读取模型', 'agents.model.loadFailed': '获取模型失败', 'agents.model.select': '选择模型', - 'agents.model.none': '无可选模型', + 'agents.model.none': '模型列表为空', 'agents.model.search': '搜索模型名称或别名', 'agents.model.clearSearch': '清空搜索', 'agents.model.refresh': '刷新模型', 'agents.model.fetching': '正在获取模型', 'agents.model.noMatch': '没有匹配的模型', - 'agents.model.unavailable': '暂时没有可用模型', + 'agents.model.unavailable': '模型列表为空', 'agents.model.tryKeywords': '尝试其他关键词', 'agents.model.connectFirst': '请先接入可用模型后刷新', 'agents.model.available': '可用模型', @@ -516,7 +516,11 @@ export const zhCN = { 'agents.modify.title': '配置修改', 'agents.modify.oauthConfiguration': 'Oauth配置方式', 'agents.oauthLoginRequired.title': '需要登录 Codex', - 'agents.oauthLoginRequired.description': '已开启 OAuth 配置模式,但未检测到有效的 Codex 登录凭据。请先在 Codex 中登录 ChatGPT 账号,然后再启动。', + 'agents.oauthLoginRequired.enableDescription': '未检测到有效的 Codex 登录凭据,无法开启 OAuth 配置模式。请先在 Codex 应用中登录 ChatGPT 账号,如果 Codex 应用无法触发登录,请先使用', + 'agents.oauthLoginRequired.enableClearConfiguration': '清空配置', + 'agents.oauthLoginRequired.enableDescriptionSuffix': '功能。', + 'agents.oauthLoginRequired.applyDescription': '无法应用 OAuth 配置:未检测到有效的 Codex 登录凭据。请先在 Codex 中登录 ChatGPT 账号,然后再应用配置。', + 'agents.oauthLoginRequired.launchDescription': '已开启 OAuth 配置模式,但未检测到有效的 Codex 登录凭据。请先在 Codex 中登录 ChatGPT 账号,然后再启动。', 'agents.oauthLoginRequired.confirm': '我知道了', 'agents.modify.apply': '应用配置修改', 'agents.modify.default': '默认配置', @@ -1250,13 +1254,13 @@ export const en: Record = { 'agents.model.loading': 'Loading models', 'agents.model.loadFailed': 'Failed to load models', 'agents.model.select': 'Select a model', - 'agents.model.none': 'No models available', + 'agents.model.none': 'Model list is empty', 'agents.model.search': 'Search model names or aliases', 'agents.model.clearSearch': 'Clear search', 'agents.model.refresh': 'Refresh models', 'agents.model.fetching': 'Fetching models', 'agents.model.noMatch': 'No matching models', - 'agents.model.unavailable': 'No models are currently available', + 'agents.model.unavailable': 'Model list is empty', 'agents.model.tryKeywords': 'Try different keywords', 'agents.model.connectFirst': 'Connect an available model, then refresh', 'agents.model.available': 'Available model', @@ -1361,7 +1365,11 @@ export const en: Record = { 'agents.modify.title': 'Configuration Changes', 'agents.modify.oauthConfiguration': 'OAuth configuration mode', 'agents.oauthLoginRequired.title': 'Codex sign-in required', - 'agents.oauthLoginRequired.description': 'OAuth configuration mode is enabled, but no valid Codex sign-in credentials were found. Sign in to your ChatGPT account in Codex before launching it.', + 'agents.oauthLoginRequired.enableDescription': 'No valid Codex sign-in credentials were found, so OAuth configuration mode cannot be enabled. Sign in to your ChatGPT account in the Codex app. If the Codex app cannot trigger sign-in, use', + 'agents.oauthLoginRequired.enableClearConfiguration': 'Clear Configuration', + 'agents.oauthLoginRequired.enableDescriptionSuffix': 'first.', + 'agents.oauthLoginRequired.applyDescription': 'OAuth configuration cannot be applied because no valid Codex sign-in credentials were found. Sign in to your ChatGPT account in Codex, then apply the configuration.', + 'agents.oauthLoginRequired.launchDescription': 'OAuth configuration mode is enabled, but no valid Codex sign-in credentials were found. Sign in to your ChatGPT account in Codex before launching it.', 'agents.oauthLoginRequired.confirm': 'Got it', 'agents.modify.apply': 'Apply Configuration Changes', 'agents.modify.default': 'Default Configuration', diff --git a/src/pages/AgentsPage.tsx b/src/pages/AgentsPage.tsx index f9b846f..f44df33 100644 --- a/src/pages/AgentsPage.tsx +++ b/src/pages/AgentsPage.tsx @@ -92,6 +92,8 @@ type ChatGptCloseResult = { closedProcesses: number; }; +type OAuthLoginRequiredAction = 'enable' | 'apply' | 'launch'; + const CODEX_OAUTH_LOGIN_REQUIRED_ERROR = 'CODEX_OAUTH_LOGIN_REQUIRED'; type AgentDefinition = { @@ -488,7 +490,7 @@ export function AgentsPage() { const [loading, setLoading] = useState(true); const [modelLoading, setModelLoading] = useState(false); const [busyAction, setBusyAction] = useState< - 'apply' | 'default' | 'clear' | 'launch' | 'launch-cli' | 'launch-app' | 'close-app' | null + 'apply' | 'default' | 'clear' | 'launch' | 'launch-cli' | 'launch-app' | 'close-app' | 'oauth-check' | null >(null); const busy = busyAction !== null; const [detectionError, setDetectionError] = useState(''); @@ -504,7 +506,7 @@ export function AgentsPage() { const [closeAppError, setCloseAppError] = useState(''); const [closeAppNotice, setCloseAppNotice] = useState(''); const [closeAppConfirmOpen, setCloseAppConfirmOpen] = useState(false); - const [oauthLoginRequiredOpen, setOauthLoginRequiredOpen] = useState(false); + const [oauthLoginRequiredAction, setOauthLoginRequiredAction] = useState(null); const [oauthConfigurationDraft, setOauthConfigurationDraft] = useState(null); const modelRequestRef = useRef(0); @@ -602,7 +604,7 @@ export function AgentsPage() { setCloseAppError(''); setCloseAppNotice(''); setCloseAppConfirmOpen(false); - setOauthLoginRequiredOpen(false); + setOauthLoginRequiredAction(null); }, [selected]); const activeDefinition = agentDefinitions.find((agent) => agent.id === selected) @@ -696,6 +698,34 @@ export function AgentsPage() { return model.name; }; + const handleOAuthLoginError = (requestError: unknown, action: OAuthLoginRequiredAction) => { + const message = String(requestError); + if (message.includes(CODEX_OAUTH_LOGIN_REQUIRED_ERROR)) { + setOauthLoginRequiredAction(action); + return true; + } + return false; + }; + + const changeOauthConfiguration = async (enabled: boolean) => { + if (!enabled) { + setOauthConfigurationDraft(false); + return; + } + + setBusyAction('oauth-check'); + try { + await invoke('check_codex_oauth_login'); + setOauthConfigurationDraft(true); + } catch (requestError) { + if (!handleOAuthLoginError(requestError, 'enable')) { + setConfigurationError(String(requestError)); + } + } finally { + setBusyAction(null); + } + }; + const applyConfigurationChanges = async () => { setConfigurationError(''); const model = requireSelectedModel(); @@ -710,7 +740,9 @@ export function AgentsPage() { await reloadStatusesAfterAction(); setOauthConfigurationDraft(null); } catch (requestError) { - setConfigurationError(String(requestError)); + if (!handleOAuthLoginError(requestError, 'apply')) { + setConfigurationError(String(requestError)); + } } finally { setBusyAction(null); } @@ -731,7 +763,9 @@ export function AgentsPage() { await reloadStatusesAfterAction(); setOauthConfigurationDraft(null); } catch (requestError) { - setDefaultError(String(requestError)); + if (!handleOAuthLoginError(requestError, 'apply')) { + setDefaultError(String(requestError)); + } } finally { setBusyAction(null); } @@ -762,16 +796,12 @@ export function AgentsPage() { setBusyAction(launchAction); setLaunchError(''); try { - if (selected !== 'codex' && draftChanged) { + if (draftChanged) { throw new Error(t('agents.error.applyFirst')); } await invoke('launch_agent', { client: selected, target: target.id }); } catch (requestError) { - if (String(requestError) === CODEX_OAUTH_LOGIN_REQUIRED_ERROR) { - setOauthLoginRequiredOpen(true); - } else { - setLaunchError(String(requestError)); - } + if (!handleOAuthLoginError(requestError, 'launch')) setLaunchError(String(requestError)); } finally { setBusyAction(null); } @@ -827,6 +857,13 @@ export function AgentsPage() { const availableSubpages = agentSubpages.filter( (subpage) => !subpage.clients || subpage.clients.includes(selected), ); + const oauthLoginRequiredDescription = oauthLoginRequiredAction === 'enable' ? ( + <> + {t('agents.oauthLoginRequired.enableDescription')} + {t('agents.oauthLoginRequired.enableClearConfiguration')} + {t('agents.oauthLoginRequired.enableDescriptionSuffix')} + + ) : oauthLoginRequiredAction ? t(`agents.oauthLoginRequired.${oauthLoginRequiredAction}Description`) : ''; return (
@@ -973,7 +1010,7 @@ export function AgentsPage() { type="checkbox" role="switch" checked={oauthConfiguration} - onChange={(event) => setOauthConfigurationDraft(event.currentTarget.checked)} + onChange={(event) => void changeOauthConfiguration(event.currentTarget.checked)} disabled={busy} aria-label={t('agents.modify.oauthConfiguration')} /> @@ -1038,8 +1075,10 @@ export function AgentsPage() { type="button" className="secondary-button agent-launch-button" onClick={() => void launchAgent(cliLaunchTarget)} - disabled={busy || !canLaunchTarget(cliLaunchTarget)} - title={cliLaunchTarget?.detail ?? t('agents.launch.unavailable')} + disabled={busy || !canLaunchTarget(cliLaunchTarget) || draftChanged} + title={draftChanged + ? t('agents.launch.applyFirst') + : cliLaunchTarget?.detail ?? t('agents.launch.unavailable')} > {busyAction === 'launch-cli' ? : } {busyAction === 'launch-cli' ? t('agents.launch.starting') : t('agents.launch.startCli')} @@ -1048,8 +1087,10 @@ export function AgentsPage() { type="button" className="primary-button agent-launch-button" onClick={() => void launchAgent(appLaunchTarget)} - disabled={busy || !canLaunchTarget(appLaunchTarget)} - title={appLaunchTarget?.detail ?? t('agents.launch.unavailable')} + disabled={busy || !canLaunchTarget(appLaunchTarget) || draftChanged} + title={draftChanged + ? t('agents.launch.applyFirst') + : appLaunchTarget?.detail ?? t('agents.launch.unavailable')} > {busyAction === 'launch-app' ? : } {busyAction === 'launch-app' ? t('agents.launch.starting') : t('agents.launch.startApp')} @@ -1182,15 +1223,15 @@ export function AgentsPage() { ) : null} - {oauthLoginRequiredOpen ? ( + {oauthLoginRequiredAction ? (

{t('agents.oauthLoginRequired.title')}

-

{t('agents.oauthLoginRequired.description')}

-
- +

{oauthLoginRequiredDescription}

+
+
diff --git a/src/styles.css b/src/styles.css index 640d119..d9d9310 100644 --- a/src/styles.css +++ b/src/styles.css @@ -2194,6 +2194,11 @@ textarea:focus-visible { grid-template-columns: repeat(2, minmax(0, 1fr)); } +.config-dialog-actions.single-action { + grid-template-columns: 118px; + justify-content: end; +} + .config-dialog-actions button { gap: 7px; height: 40px;