From 8a59c8bb2e01aefca073ddfe81085e77ff028f0c Mon Sep 17 00:00:00 2001 From: lzt404 <2596933790@qq.com> Date: Mon, 27 Jul 2026 01:13:15 +0800 Subject: [PATCH] feat: add Codex OAuth configuration mode and refine agent panel layout --- src-tauri/src/main.rs | 213 ++++++++++++++++++++++++++++++++++----- src/i18n/ja.ts | 1 + src/i18n/resources.ts | 2 + src/pages/AgentsPage.tsx | 22 ++++ src/styles.css | 50 ++++++++- 5 files changed, 261 insertions(+), 27 deletions(-) diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 77199da..76dbbf1 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -1588,6 +1588,7 @@ async fn apply_agent_config( gui_config_state: tauri::State<'_, GuiConfigState>, client: String, model: String, + oauth_configuration: bool, ) -> Result { let client = AgentClient::parse(&client)?; let home = app @@ -1607,7 +1608,7 @@ async fn apply_agent_config( let _guard = AGENT_CONFIG_FILE_LOCK .lock() .map_err(|_| "智能体配置文件锁已损坏".to_string())?; - apply_agent_configuration( + apply_agent_configuration_with_oauth( client, &home, config.port, @@ -1615,6 +1616,7 @@ async fn apply_agent_config( &model, &available_models, codex_models.as_deref(), + oauth_configuration, ) } @@ -1624,6 +1626,7 @@ async fn reset_agent_config_to_default( gui_config_state: tauri::State<'_, GuiConfigState>, client: String, model: String, + oauth_configuration: bool, ) -> Result { let client = AgentClient::parse(&client)?; let home = app @@ -1643,13 +1646,14 @@ async fn reset_agent_config_to_default( let _guard = AGENT_CONFIG_FILE_LOCK .lock() .map_err(|_| "智能体配置文件锁已损坏".to_string())?; - reset_agent_configuration_to_default( + reset_agent_configuration_to_default_with_oauth( client, &home, config.port, api_key, &model, codex_models.as_deref(), + oauth_configuration, ) } @@ -3293,6 +3297,15 @@ fn inspect_codex_agent_config( .and_then(toml::Value::as_table) .and_then(|providers| providers.get(MANAGED_AGENT_PROVIDER_ID)) .and_then(toml::Value::as_table); + let uses_api_key = provider + .and_then(|provider| provider.get("experimental_bearer_token")) + .and_then(toml::Value::as_str) + == Some(api_key); + let uses_oauth = provider + .and_then(|provider| provider.get("requires_openai_auth")) + .and_then(toml::Value::as_bool) + == Some(true) + && provider.is_some_and(|provider| !provider.contains_key("experimental_bearer_token")); let configured = root.get("model_provider").and_then(toml::Value::as_str) == Some(MANAGED_AGENT_PROVIDER_ID) && root.get("model_catalog_json").and_then(toml::Value::as_str) @@ -3305,10 +3318,7 @@ fn inspect_codex_agent_config( .and_then(|provider| provider.get("base_url")) .and_then(toml::Value::as_str) == Some(expected_base.as_str()) - && provider - .and_then(|provider| provider.get("experimental_bearer_token")) - .and_then(toml::Value::as_str) - == Some(api_key) + && (uses_api_key || uses_oauth) && provider .and_then(|provider| provider.get("wire_api")) .and_then(toml::Value::as_str) @@ -3444,6 +3454,7 @@ fn inspect_hermes_agent_config( Ok((configured, model)) } +#[cfg(test)] fn build_agent_updates( client: AgentClient, home: &Path, @@ -3452,6 +3463,28 @@ fn build_agent_updates( model: &str, models: &[AgentModelOption], codex_models: Option<&[CodexModelCatalogSpec]>, +) -> Result, String> { + build_agent_updates_with_oauth( + client, + home, + port, + api_key, + model, + models, + codex_models, + false, + ) +} + +fn build_agent_updates_with_oauth( + client: AgentClient, + home: &Path, + port: u16, + api_key: &str, + model: &str, + models: &[AgentModelOption], + codex_models: Option<&[CodexModelCatalogSpec]>, + oauth_configuration: bool, ) -> Result, String> { let paths = agent_config_paths(client, home); let root_base = format!("http://127.0.0.1:{port}"); @@ -3507,9 +3540,22 @@ fn build_agent_updates( } AgentClient::Codex => { let before = read_optional_text(&paths[0])?; - let after = - build_codex_agent_config(before.as_deref(), &openai_base, api_key, model) - .or_else(|_| build_codex_agent_config(None, &openai_base, api_key, model))?; + let after = build_codex_agent_config_with_oauth( + before.as_deref(), + &openai_base, + api_key, + model, + oauth_configuration, + ) + .or_else(|_| { + build_codex_agent_config_with_oauth( + None, + &openai_base, + api_key, + model, + oauth_configuration, + ) + })?; let mut updates = vec![AgentFileUpdate { path: paths[0].clone(), after, @@ -3779,11 +3825,22 @@ fn build_claude_desktop_meta(existing: Option<&str>) -> Result { render_agent_json(root, "Claude Desktop 配置索引") } +#[cfg(test)] fn build_codex_agent_config( existing: Option<&str>, base_url: &str, api_key: &str, model: &str, +) -> Result { + build_codex_agent_config_with_oauth(existing, base_url, api_key, model, false) +} + +fn build_codex_agent_config_with_oauth( + existing: Option<&str>, + base_url: &str, + api_key: &str, + model: &str, + oauth_configuration: bool, ) -> Result { use toml_edit::{value, Document, Item, Table}; @@ -3828,7 +3885,13 @@ fn build_codex_agent_config( set_codex_table_item(provider, "name", value("EasyCLIProxyAPI")); set_codex_table_item(provider, "base_url", value(base_url)); set_codex_table_item(provider, "wire_api", value("responses")); - set_codex_table_item(provider, "experimental_bearer_token", value(api_key)); + if oauth_configuration { + provider.remove("experimental_bearer_token"); + set_codex_table_item(provider, "requires_openai_auth", value(true)); + } else { + provider.remove("requires_openai_auth"); + set_codex_table_item(provider, "experimental_bearer_token", value(api_key)); + } let rendered = document.to_string(); toml::from_str::(&rendered) .map_err(|error| format!("验证 Codex 配置失败: {error}"))?; @@ -3838,8 +3901,13 @@ fn build_codex_agent_config( #[cfg(test)] const CODEX_MANAGED_ROOT_KEYS: [&str; 3] = ["model_provider", "model", "model_catalog_json"]; #[cfg(test)] -const CODEX_MANAGED_PROVIDER_KEYS: [&str; 4] = - ["name", "base_url", "wire_api", "experimental_bearer_token"]; +const CODEX_MANAGED_PROVIDER_KEYS: [&str; 5] = [ + "name", + "base_url", + "wire_api", + "experimental_bearer_token", + "requires_openai_auth", +]; fn set_codex_table_item(table: &mut toml_edit::Table, key: &str, mut item: toml_edit::Item) { if let (Some(current), Some(next)) = ( @@ -4477,16 +4545,22 @@ fn agent_managed_projection( .get("model_providers") .and_then(toml::Value::as_table) .and_then(|providers| providers.get(MANAGED_AGENT_PROVIDER_ID)); - let provider_projection = ["name", "base_url", "wire_api", "experimental_bearer_token"] - .into_iter() - .map(|key| { - let value = provider - .and_then(|provider| provider.get(key)) - .and_then(|value| serde_json::to_value(value).ok()) - .unwrap_or(serde_json::Value::Null); - (key.to_string(), value) - }) - .collect::>(); + let provider_projection = [ + "name", + "base_url", + "wire_api", + "experimental_bearer_token", + "requires_openai_auth", + ] + .into_iter() + .map(|key| { + let value = provider + .and_then(|provider| provider.get(key)) + .and_then(|value| serde_json::to_value(value).ok()) + .unwrap_or(serde_json::Value::Null); + (key.to_string(), value) + }) + .collect::>(); let root_value = |key: &str| { root.get(key) .and_then(|value| serde_json::to_value(value).ok()) @@ -5067,11 +5141,22 @@ fn inspect_agent_modification( } } +#[cfg(test)] fn fresh_agent_contents( client: AgentClient, port: u16, api_key: &str, model: &str, +) -> Result, String> { + fresh_agent_contents_with_oauth(client, port, api_key, model, false) +} + +fn fresh_agent_contents_with_oauth( + client: AgentClient, + port: u16, + api_key: &str, + model: &str, + oauth_configuration: bool, ) -> Result, String> { let root_base = format!("http://127.0.0.1:{port}"); let openai_base = format!("{root_base}/v1"); @@ -5089,11 +5174,12 @@ fn fresh_agent_contents( build_claude_desktop_profile(None, &root_base, api_key, model, &models)?, build_claude_desktop_meta(None)?, ]), - AgentClient::Codex => Ok(vec![build_codex_agent_config( + AgentClient::Codex => Ok(vec![build_codex_agent_config_with_oauth( None, &openai_base, api_key, model, + oauth_configuration, )?]), AgentClient::OpenCode => Ok(vec![build_opencode_agent_config( None, @@ -5653,10 +5739,42 @@ fn apply_agent_configuration( models: &[AgentModelOption], codex_models: Option<&[CodexModelCatalogSpec]>, ) -> Result { - let updates = build_agent_updates(client, home, port, api_key, model, models, codex_models)?; + apply_agent_configuration_with_oauth( + client, + home, + port, + api_key, + model, + models, + codex_models, + false, + ) +} + +fn apply_agent_configuration_with_oauth( + client: AgentClient, + home: &Path, + port: u16, + api_key: &str, + model: &str, + models: &[AgentModelOption], + codex_models: Option<&[CodexModelCatalogSpec]>, + oauth_configuration: bool, +) -> Result { + let updates = build_agent_updates_with_oauth( + client, + home, + port, + api_key, + model, + models, + codex_models, + oauth_configuration, + )?; commit_agent_configuration(client, home, model, &updates, "applied") } +#[cfg(test)] fn reset_agent_configuration_to_default( client: AgentClient, home: &Path, @@ -5664,9 +5782,30 @@ fn reset_agent_configuration_to_default( api_key: &str, model: &str, codex_models: Option<&[CodexModelCatalogSpec]>, +) -> Result { + reset_agent_configuration_to_default_with_oauth( + client, + home, + port, + api_key, + model, + codex_models, + false, + ) +} + +fn reset_agent_configuration_to_default_with_oauth( + client: AgentClient, + home: &Path, + port: u16, + api_key: &str, + model: &str, + codex_models: Option<&[CodexModelCatalogSpec]>, + oauth_configuration: bool, ) -> Result { let paths = agent_config_paths(client, home); - let contents = fresh_agent_contents(client, port, api_key, model)?; + let contents = + fresh_agent_contents_with_oauth(client, port, api_key, model, oauth_configuration)?; if paths.len() != contents.len() { return Err("智能体默认配置文件数量不匹配".to_string()); } @@ -13383,6 +13522,30 @@ mod tests { ); } + #[test] + fn codex_oauth_configuration_uses_openai_auth_without_bearer_token() { + let rendered = build_codex_agent_config_with_oauth( + Some("[model_providers.cpa-gui]\nexperimental_bearer_token = \"old-key\"\n"), + "http://127.0.0.1:8317/v1", + DEFAULT_API_KEY, + "gpt-test", + true, + ) + .unwrap(); + let value = toml::from_str::(&rendered).unwrap(); + let provider = value["model_providers"][MANAGED_AGENT_PROVIDER_ID] + .as_table() + .unwrap(); + + assert_eq!( + provider + .get("requires_openai_auth") + .and_then(toml::Value::as_bool), + Some(true) + ); + assert!(!provider.contains_key("experimental_bearer_token")); + } + #[test] fn claude_desktop_config_builds_gateway_profile_and_index() { let models = test_agent_models(&["claude-sonnet-test", "claude-opus-test"]); diff --git a/src/i18n/ja.ts b/src/i18n/ja.ts index 9880d02..aef4fa2 100644 --- a/src/i18n/ja.ts +++ b/src/i18n/ja.ts @@ -457,6 +457,7 @@ export const jaOverrides = { 'agents.model.current': '現在適用中:{model}', 'agents.model.firstSelection': '利用可能なモデル {count} 件。初回は先頭を選択します', 'agents.modify.title': '設定変更', + 'agents.modify.oauthConfiguration': 'OAuth 設定方式', 'agents.modify.apply': '設定変更を適用', 'agents.modify.default': 'デフォルト設定', 'agents.modify.clear': '設定を消去', diff --git a/src/i18n/resources.ts b/src/i18n/resources.ts index 8c3ae15..67a3417 100644 --- a/src/i18n/resources.ts +++ b/src/i18n/resources.ts @@ -460,6 +460,7 @@ export const zhCN = { 'agents.model.current': '当前已应用:{model}', 'agents.model.firstSelection': '{count} 个可用模型,首次默认选择第一项', 'agents.modify.title': '配置修改', + 'agents.modify.oauthConfiguration': 'Oauth配置方式', 'agents.modify.apply': '应用配置修改', 'agents.modify.default': '默认配置', 'agents.modify.clear': '清空配置', @@ -1246,6 +1247,7 @@ export const en: Record = { 'agents.model.current': 'Currently applied: {model}', 'agents.model.firstSelection': '{count} models available; the first is selected by default', 'agents.modify.title': 'Configuration Changes', + 'agents.modify.oauthConfiguration': 'OAuth configuration mode', 'agents.modify.apply': 'Apply Configuration Changes', 'agents.modify.default': 'Default Configuration', 'agents.modify.clear': 'Clear Configuration', diff --git a/src/pages/AgentsPage.tsx b/src/pages/AgentsPage.tsx index 498b24e..10244df 100644 --- a/src/pages/AgentsPage.tsx +++ b/src/pages/AgentsPage.tsx @@ -510,6 +510,7 @@ export function AgentsPage() { const [closeAppError, setCloseAppError] = useState(''); const [closeAppNotice, setCloseAppNotice] = useState(''); const [closeAppConfirmOpen, setCloseAppConfirmOpen] = useState(false); + const [oauthConfiguration, setOauthConfiguration] = useState(false); const loadStatuses = useCallback(async (forceRefresh = false) => { const command = forceRefresh @@ -723,6 +724,7 @@ export function AgentsPage() { await invoke('apply_agent_config', { client: selected, model, + oauthConfiguration, }); await reloadStatusesAfterAction(); } catch (requestError) { @@ -741,6 +743,7 @@ export function AgentsPage() { await invoke('reset_agent_config_to_default', { client: selected, model, + oauthConfiguration, }); setDefaultConfirmOpen(false); await reloadStatusesAfterAction(); @@ -965,6 +968,25 @@ export function AgentsPage() {
+ {selected === 'codex' ? ( + + ) : null}