diff --git a/src-tauri/src/agents/commands.rs b/src-tauri/src/agents/commands.rs index e32f480..9166778 100644 --- a/src-tauri/src/agents/commands.rs +++ b/src-tauri/src/agents/commands.rs @@ -1035,6 +1035,11 @@ pub(crate) async fn set_agent_config_enabled( let _guard = AGENT_CONFIG_FILE_LOCK .lock() .map_err(|_| "智能体配置文件锁已损坏".to_string())?; + let oauth_configuration = if client == AgentClient::Codex { + current_codex_oauth_configuration(&home)? + } else { + false + }; apply_agent_configuration_with_oauth( client, &home, @@ -1044,7 +1049,7 @@ pub(crate) async fn set_agent_config_enabled( AgentConfigurationOptions { models: &prepared.models, codex_catalog: prepared.codex_catalog.as_deref(), - oauth_configuration: false, + oauth_configuration, claude_code_model_mappings: claude_code_model_mappings.as_ref(), claude_desktop_model_mappings: claude_desktop_model_mappings.as_ref(), }, @@ -1095,6 +1100,11 @@ pub(crate) async fn update_agent_config( let _guard = AGENT_CONFIG_FILE_LOCK .lock() .map_err(|_| "智能体配置文件锁已损坏".to_string())?; + let oauth_configuration = if client == AgentClient::Codex { + current_codex_oauth_configuration(&home)? + } else { + false + }; apply_agent_configuration_with_oauth( client, &home, @@ -1104,7 +1114,7 @@ pub(crate) async fn update_agent_config( AgentConfigurationOptions { models: &prepared.models, codex_catalog: prepared.codex_catalog.as_deref(), - oauth_configuration: false, + oauth_configuration, claude_code_model_mappings: claude_code_model_mappings.as_ref(), claude_desktop_model_mappings: claude_desktop_model_mappings.as_ref(), }, diff --git a/src-tauri/src/agents/discovery.rs b/src-tauri/src/agents/discovery.rs index 3aa40bd..1bb764c 100644 --- a/src-tauri/src/agents/discovery.rs +++ b/src-tauri/src/agents/discovery.rs @@ -700,6 +700,25 @@ pub(crate) fn codex_configuration_directory(home: &Path) -> PathBuf { .unwrap_or_else(|| home.join(".codex")) } +pub(crate) fn current_codex_oauth_configuration(home: &Path) -> Result { + let path = codex_configuration_directory(home).join("config.toml"); + if !path.is_file() { + return Ok(false); + } + let root: toml::Value = toml::from_str( + &fs::read_to_string(&path).map_err(|error| format!("读取 Codex 配置失败: {error}"))?, + ) + .map_err(|error| format!("解析 Codex 配置失败: {error}"))?; + Ok(root + .get("model_providers") + .and_then(toml::Value::as_table) + .and_then(|providers| providers.get(MANAGED_AGENT_PROVIDER_ID)) + .and_then(toml::Value::as_table) + .and_then(|provider| provider.get("requires_openai_auth")) + .and_then(toml::Value::as_bool) + == Some(true)) +} + pub(crate) fn codex_auth_file_has_oauth_tokens(path: &Path) -> bool { let Ok(content) = fs::read_to_string(path) else { return false; diff --git a/src-tauri/src/tests/agent_configuration.rs b/src-tauri/src/tests/agent_configuration.rs index 829ffb8..6b6bc9c 100644 --- a/src-tauri/src/tests/agent_configuration.rs +++ b/src-tauri/src/tests/agent_configuration.rs @@ -345,6 +345,52 @@ fn codex_oauth_configuration_uses_openai_auth_with_bearer_token() { fs::remove_dir_all(home).unwrap(); } +#[test] +fn codex_oauth_configuration_is_preserved_for_legacy_updates() { + let home = agent_test_home("codex-oauth-preserved-update"); + let codex_dir = home.join(".codex"); + fs::create_dir_all(&codex_dir).unwrap(); + fs::write( + codex_dir.join("config.toml"), + "[model_providers.cpa-gui]\nrequires_openai_auth = true\n", + ) + .unwrap(); + fs::write( + codex_dir.join("auth.json"), + r#"{"tokens":{"access_token":"oauth-access-token"}}"#, + ) + .unwrap(); + + let oauth_configuration = current_codex_oauth_configuration(&home).unwrap(); + assert!(oauth_configuration); + + let models = test_agent_models(&["gpt-test"]); + let catalog = test_codex_models(&["gpt-test"]); + let updates = build_agent_updates_with_oauth( + AgentClient::Codex, + &home, + 8317, + DEFAULT_API_KEY, + "gpt-test", + AgentConfigurationOptions { + models: &models, + codex_catalog: Some(&catalog), + oauth_configuration, + claude_code_model_mappings: None, + claude_desktop_model_mappings: None, + }, + ) + .unwrap(); + + assert!(updates[0].after.contains("requires_openai_auth = true")); + assert!( + serde_json::from_str::(&updates[2].after).unwrap()["tokens"] + ["access_token"] + .is_string() + ); + fs::remove_dir_all(home).unwrap(); +} + #[test] fn codex_api_and_oauth_modes_write_the_same_catalog() { let home = agent_test_home("codex-auth-mode-catalog"); diff --git a/src/pages/AgentsPage.tsx b/src/pages/AgentsPage.tsx index 75509b4..259a315 100644 --- a/src/pages/AgentsPage.tsx +++ b/src/pages/AgentsPage.tsx @@ -169,6 +169,7 @@ const createClaudeBooleanByClient = (): Record(null); - const [oauthConfigurationDraft, setOauthConfigurationDraft] = useState(null); + const [oauthConfigurationDraft, setOauthConfigurationDraftState] = useState( + () => codexOauthConfigurationDraftCache, + ); const [piProviderUpdateStatus, setPiProviderUpdateStatus] = useState(null); const modelRequestRef = useRef(0); const piUpdateRequestRef = useRef(0); @@ -706,6 +709,11 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP }); }, []); + const setOauthConfigurationDraft = useCallback((value: boolean | null) => { + codexOauthConfigurationDraftCache = value; + setOauthConfigurationDraftState(value); + }, []); + const loadStatuses = useCallback(async (forceRefresh = false) => { const command = forceRefresh ? 'refresh_agent_config_statuses' @@ -803,9 +811,8 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP setLaunchDirectoryTarget(null); setLaunchDirectoryError(''); setOauthLoginRequiredAction(null); - setOauthConfigurationDraft(null); - // Preserve each Claude client's unsaved draft while navigating between clients. - // Its dirty flag is cleared only after a successful apply, close, or reset action. + // Preserve unsaved client-specific configuration while navigating between clients. + // Each configuration action decides whether its draft should be retained or cleared. }, [selected]); const activeDefinition = agentDefinitions.find((agent) => agent.id === selected)