feat: add Codex OAuth configuration mode and refine agent panel layout
This commit is contained in:
1 parent
a8a0d54d90
commit
8a59c8bb2e
5 files changed
+261
-27
No files matched your search
+188
-25
@@ -1588,6 +1588,7 @@ async fn apply_agent_config(
|
||||
gui_config_state: tauri::State<'_, GuiConfigState>,
|
||||
client: String,
|
||||
model: String,
|
||||
oauth_configuration: bool,
|
||||
) -> Result<AgentConfigActionResult, String> {
|
||||
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<AgentConfigActionResult, String> {
|
||||
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<Vec<AgentFileUpdate>, 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<Vec<AgentFileUpdate>, 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<String, String> {
|
||||
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<String, String> {
|
||||
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<String, String> {
|
||||
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::<toml::Value>(&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::<serde_json::Map<_, _>>();
|
||||
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::<serde_json::Map<_, _>>();
|
||||
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<Vec<String>, 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<Vec<String>, 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<AgentConfigActionResult, String> {
|
||||
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<AgentConfigActionResult, String> {
|
||||
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<AgentConfigActionResult, String> {
|
||||
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<AgentConfigActionResult, String> {
|
||||
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::<toml::Value>(&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"]);
|
||||
|
||||
@@ -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': '設定を消去',
|
||||
|
||||
@@ -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<MessageKey, string> = {
|
||||
'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',
|
||||
|
||||
@@ -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<AgentConfigActionResult>('apply_agent_config', {
|
||||
client: selected,
|
||||
model,
|
||||
oauthConfiguration,
|
||||
});
|
||||
await reloadStatusesAfterAction();
|
||||
} catch (requestError) {
|
||||
@@ -741,6 +743,7 @@ export function AgentsPage() {
|
||||
await invoke<AgentConfigActionResult>('reset_agent_config_to_default', {
|
||||
client: selected,
|
||||
model,
|
||||
oauthConfiguration,
|
||||
});
|
||||
setDefaultConfirmOpen(false);
|
||||
await reloadStatusesAfterAction();
|
||||
@@ -965,6 +968,25 @@ export function AgentsPage() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="agent-modification-control">
|
||||
{selected === 'codex' ? (
|
||||
<label
|
||||
className="agent-oauth-configuration"
|
||||
title={t('agents.modify.oauthConfiguration')}
|
||||
>
|
||||
<span>{t('agents.modify.oauthConfiguration')}</span>
|
||||
<span className="switch-control">
|
||||
<input
|
||||
type="checkbox"
|
||||
role="switch"
|
||||
checked={oauthConfiguration}
|
||||
onChange={(event) => setOauthConfiguration(event.currentTarget.checked)}
|
||||
disabled={busy}
|
||||
aria-label={t('agents.modify.oauthConfiguration')}
|
||||
/>
|
||||
<span className="switch-track" />
|
||||
</span>
|
||||
</label>
|
||||
) : null}
|
||||
<div className={`agent-modification-buttons ${selected === 'codex' ? 'codex' : ''}`}>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
+48
-2
@@ -4858,6 +4858,49 @@ input:focus-visible {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.agent-oauth-configuration {
|
||||
display: flex;
|
||||
min-height: 42px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #d8d2c8;
|
||||
border-radius: 9px;
|
||||
background: #faf8f4;
|
||||
color: #5f5a53;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: border-color 160ms ease, background-color 160ms ease, box-shadow 160ms ease;
|
||||
}
|
||||
|
||||
.agent-oauth-configuration:hover {
|
||||
border-color: #bdb5a9;
|
||||
background: #f6f2ec;
|
||||
}
|
||||
|
||||
.agent-oauth-configuration:has(input:focus-visible) {
|
||||
border-color: #8d8377;
|
||||
box-shadow: 0 0 0 3px rgba(45, 42, 38, 0.1);
|
||||
}
|
||||
|
||||
.agent-oauth-configuration:has(input:checked) {
|
||||
border-color: #9db8a5;
|
||||
background: #f3f8f4;
|
||||
color: #2f6b3f;
|
||||
}
|
||||
|
||||
.agent-oauth-configuration:has(input:disabled) {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.agent-oauth-configuration .switch-control {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.agent-modification-buttons {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
@@ -5928,8 +5971,9 @@ input:focus-visible {
|
||||
|
||||
@container agent-panel (min-width: 900px) {
|
||||
.agent-core-config {
|
||||
grid-template-columns: minmax(0, 1.1fr) minmax(0, 0.9fr);
|
||||
align-items: start;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
column-gap: 8px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.agent-status-grid,
|
||||
@@ -5941,10 +5985,12 @@ input:focus-visible {
|
||||
|
||||
.agent-model-section {
|
||||
grid-column: 1;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.agent-modification-actions {
|
||||
grid-column: 2;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user