Fix Codex OAuth configuration and refine UI controls

This commit is contained in:
lzt404 committed 2026-07-29 14:58:25 +08:00
1 parent 1acbeb6f0a
commit 2c7030734e
7 files changed
+110 -115

No files matched your search

+49 -24
View File
@@ -584,6 +584,7 @@ struct AgentConfigStatus {
config_valid: bool,
configured: bool,
current_model: Option<String>,
oauth_configuration: bool,
modification_enabled: bool,
modification_state: String,
backup_available: bool,
@@ -2422,9 +2423,11 @@ fn inspect_agent_config(
let paths = agent_config_paths(client, home);
let config_exists = paths.iter().any(|path| path.is_file());
let result = inspect_agent_managed_config(client, &paths, port, api_key);
let (configured, current_model, config_valid, error) = match result {
Ok((configured, model)) => (configured, model, true, None),
Err(error) => (false, None, false, Some(error)),
let (configured, current_model, oauth_configuration, config_valid, error) = match result {
Ok((configured, model, oauth_configuration)) => {
(configured, model, oauth_configuration, true, None)
}
Err(error) => (false, None, false, false, Some(error)),
};
let launch_targets = agent_launch_targets(client, home);
let executable = find_agent_executable(client, home);
@@ -2458,6 +2461,7 @@ fn inspect_agent_config(
config_valid,
configured,
current_model,
oauth_configuration,
modification_enabled: modification.enabled,
modification_state: modification.state,
backup_available: modification.backup_available,
@@ -2498,17 +2502,22 @@ fn inspect_agent_managed_config(
paths: &[PathBuf],
port: u16,
api_key: &str,
) -> Result<(bool, Option<String>), String> {
) -> Result<(bool, Option<String>, bool), String> {
match client {
AgentClient::ClaudeCode => inspect_claude_agent_config(&paths[0], port, api_key),
AgentClient::ClaudeCode => inspect_claude_agent_config(&paths[0], port, api_key)
.map(|(configured, model)| (configured, model, false)),
AgentClient::ClaudeDesktop if client.supported_platform() => {
inspect_claude_desktop_agent_config(paths, port, api_key)
.map(|(configured, model)| (configured, model, false))
}
AgentClient::ClaudeDesktop => Ok((false, None)),
AgentClient::ClaudeDesktop => Ok((false, None, false)),
AgentClient::Codex => inspect_codex_agent_config(&paths[0], port, api_key),
AgentClient::OpenCode => inspect_opencode_agent_config(&paths[0], port, api_key),
AgentClient::OpenClaw => inspect_openclaw_agent_config(&paths[0], port, api_key),
AgentClient::Hermes => inspect_hermes_agent_config(&paths[0], port, api_key),
AgentClient::OpenCode => inspect_opencode_agent_config(&paths[0], port, api_key)
.map(|(configured, model)| (configured, model, false)),
AgentClient::OpenClaw => inspect_openclaw_agent_config(&paths[0], port, api_key)
.map(|(configured, model)| (configured, model, false)),
AgentClient::Hermes => inspect_hermes_agent_config(&paths[0], port, api_key)
.map(|(configured, model)| (configured, model, false)),
}
}
@@ -3410,9 +3419,9 @@ fn inspect_codex_agent_config(
path: &Path,
port: u16,
api_key: &str,
) -> Result<(bool, Option<String>), String> {
) -> Result<(bool, Option<String>, bool), String> {
if !path.is_file() {
return Ok((false, None));
return Ok((false, None, false));
}
let root: toml::Value = toml::from_str(
&fs::read_to_string(path).map_err(|error| format!("读取 Codex 配置失败: {error}"))?,
@@ -3428,11 +3437,10 @@ fn inspect_codex_agent_config(
.and_then(|provider| provider.get("experimental_bearer_token"))
.and_then(toml::Value::as_str)
== Some(api_key);
let uses_oauth = provider
let oauth_configuration = 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"));
== Some(true);
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)
@@ -3445,7 +3453,7 @@ fn inspect_codex_agent_config(
.and_then(|provider| provider.get("base_url"))
.and_then(toml::Value::as_str)
== Some(expected_base.as_str())
&& (uses_api_key || uses_oauth)
&& uses_api_key
&& provider
.and_then(|provider| provider.get("wire_api"))
.and_then(toml::Value::as_str)
@@ -3456,7 +3464,7 @@ fn inspect_codex_agent_config(
.map(str::trim)
.filter(|value| !value.is_empty())
.map(str::to_string);
Ok((configured, model))
Ok((configured, model, oauth_configuration))
}
fn inspect_opencode_agent_config(
@@ -4017,12 +4025,11 @@ fn build_codex_agent_config_with_oauth(
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)
@@ -5771,7 +5778,8 @@ fn enable_agent_modification(
Vec::new(),
));
}
let (_, current_model) = inspect_agent_managed_config(client, &paths, port, DEFAULT_API_KEY)?;
let (_, current_model, _) =
inspect_agent_managed_config(client, &paths, port, DEFAULT_API_KEY)?;
if agent_has_managed_marker(client, &paths)? {
if let Some(current_model) = current_model.as_deref() {
if let Some(record) = build_legacy_agent_record(client, home, port, current_model)? {
@@ -5849,7 +5857,8 @@ fn disable_agent_modification(
let mut record = match load_agent_record(client, &paths)? {
Some(record) => record,
None => {
let (_, model) = inspect_agent_managed_config(client, &paths, port, DEFAULT_API_KEY)?;
let (_, model, _) =
inspect_agent_managed_config(client, &paths, port, DEFAULT_API_KEY)?;
if !agent_has_managed_marker(client, &paths)? {
return Ok(action_result(
"disabled",
@@ -5934,7 +5943,7 @@ fn update_agent_modification(
let record = match load_agent_record(client, &paths)? {
Some(record) => record,
None => {
let (_, current_model) =
let (_, current_model, _) =
inspect_agent_managed_config(client, &paths, port, DEFAULT_API_KEY)?;
if !agent_has_managed_marker(client, &paths)? {
return Err("请先应用配置修改".to_string());
@@ -13938,7 +13947,9 @@ mod tests {
}
#[test]
fn codex_oauth_configuration_uses_openai_auth_without_bearer_token() {
fn codex_oauth_configuration_uses_openai_auth_with_bearer_token() {
let home = agent_test_home("codex-oauth-configuration");
let path = home.join(".codex/config.toml");
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",
@@ -13958,7 +13969,21 @@ mod tests {
.and_then(toml::Value::as_bool),
Some(true)
);
assert!(!provider.contains_key("experimental_bearer_token"));
assert_eq!(
provider
.get("experimental_bearer_token")
.and_then(toml::Value::as_str),
Some(DEFAULT_API_KEY)
);
fs::create_dir_all(path.parent().unwrap()).unwrap();
fs::write(&path, rendered).unwrap();
let (configured, model, oauth_configuration) =
inspect_codex_agent_config(&path, 8317, DEFAULT_API_KEY).unwrap();
assert!(configured);
assert_eq!(model.as_deref(), Some("gpt-test"));
assert!(oauth_configuration);
fs::remove_dir_all(home).unwrap();
}
#[test]
@@ -14620,7 +14645,7 @@ custom_option = "keep-original"
provider["custom_option"] = toml_edit::value("keep-updated");
fs::write(&path, externally_edited.to_string()).unwrap();
let (configured, current_model) =
let (configured, current_model, _) =
inspect_codex_agent_config(&path, 8317, DEFAULT_API_KEY).unwrap();
let inspection = inspect_agent_modification(
AgentClient::Codex,
+23 -17
View File
@@ -14,12 +14,10 @@ import {
Languages,
LogIn,
MessageCircle,
Moon,
Network,
PackageOpen,
ServerCog,
Settings,
Sun,
X,
} from 'lucide-react';
import appLogo from './assets/logo.jpg';
@@ -308,22 +306,30 @@ function AppContent() {
</nav>
<div className="sidebar-bottom">
<button
type="button"
className="sidebar-theme-toggle"
aria-label={theme === 'dark' ? t('app.theme.switchToLight') : t('app.theme.switchToDark')}
aria-pressed={theme === 'dark'}
title={theme === 'dark' ? t('app.theme.switchToLight') : t('app.theme.switchToDark')}
onClick={() => setTheme((current) => current === 'dark' ? 'light' : 'dark')}
<div
className="sidebar-theme-selector"
role="group"
aria-label={`${t('app.theme.light')} / ${t('app.theme.dark')}`}
>
{theme === 'dark'
? <Moon size={16} aria-hidden="true" />
: <Sun size={16} aria-hidden="true" />}
<span>{theme === 'dark' ? t('app.theme.dark') : t('app.theme.light')}</span>
<i className="theme-switch-track" aria-hidden="true">
<b />
</i>
</button>
<button
type="button"
className={theme === 'light' ? 'active' : ''}
aria-pressed={theme === 'light'}
title={t('app.theme.switchToLight')}
onClick={() => setTheme('light')}
>
{t('app.theme.light')}
</button>
<button
type="button"
className={theme === 'dark' ? 'active' : ''}
aria-pressed={theme === 'dark'}
title={t('app.theme.switchToDark')}
onClick={() => setTheme('dark')}
>
{t('app.theme.dark')}
</button>
</div>
<div ref={languageMenuRef} className="sidebar-language">
<button
ref={languageButtonRef}
-2
View File
@@ -445,8 +445,6 @@ export const jaOverrides = {
'agents.sessions.repairTitle': '過去のセッションを復元',
'agents.sessions.repairDescription': '他の Provider のセッションを現在のセッション Provider に同期します。',
'agents.sessions.autoRepair': 'Codex 起動前に過去のセッションを自動復元',
'agents.sessions.autoRepairEnabled': '起動前の自動復元を有効にしました。',
'agents.sessions.autoRepairDisabled': '起動前の自動復元を無効にしました。',
'agents.sessions.settingLoading': '読み込み中',
'agents.sessions.settingOn': 'オン',
'agents.sessions.settingOff': 'オフ',
-4
View File
@@ -448,8 +448,6 @@ export const zhCN = {
'agents.sessions.repairTitle': '恢复历史会话',
'agents.sessions.repairDescription': '将其他渠道的会话同步到当前会话',
'agents.sessions.autoRepair': '启动 Codex 前自动恢复历史会话',
'agents.sessions.autoRepairEnabled': '已开启启动前自动恢复历史会话。',
'agents.sessions.autoRepairDisabled': '已关闭启动前自动恢复历史会话。',
'agents.sessions.settingLoading': '读取中',
'agents.sessions.settingOn': '已开启',
'agents.sessions.settingOff': '已关闭',
@@ -1293,8 +1291,6 @@ export const en: Record<MessageKey, string> = {
'agents.sessions.repairTitle': 'Restore Historical Sessions',
'agents.sessions.repairDescription': 'Sync sessions from other providers to the current session provider.',
'agents.sessions.autoRepair': 'Restore historical sessions before launching Codex',
'agents.sessions.autoRepairEnabled': 'Automatic restoration before launch is enabled.',
'agents.sessions.autoRepairDisabled': 'Automatic restoration before launch is disabled.',
'agents.sessions.settingLoading': 'Loading',
'agents.sessions.settingOn': 'On',
'agents.sessions.settingOff': 'Off',
+14 -4
View File
@@ -62,6 +62,7 @@ type AgentConfigStatus = {
configValid: boolean;
configured: boolean;
currentModel: string | null;
oauthConfiguration: boolean;
modificationEnabled: boolean;
modificationState: AgentModificationState;
backupAvailable: boolean;
@@ -509,7 +510,7 @@ export function AgentsPage() {
const [closeAppError, setCloseAppError] = useState('');
const [closeAppNotice, setCloseAppNotice] = useState('');
const [closeAppConfirmOpen, setCloseAppConfirmOpen] = useState(false);
const [oauthConfiguration, setOauthConfiguration] = useState(false);
const [oauthConfigurationDraft, setOauthConfigurationDraft] = useState<boolean | null>(null);
const autoLoadedModelClientRef = useRef<AgentClientId | null>(null);
const loadStatuses = useCallback(async (forceRefresh = false) => {
@@ -620,6 +621,9 @@ export function AgentsPage() {
const activeDefinition = agentDefinitions.find((agent) => agent.id === selected)
?? agentDefinitions[0];
const activeStatus = statuses.find((status) => status.id === selected) ?? null;
const oauthConfiguration = oauthConfigurationDraft
?? activeStatus?.oauthConfiguration
?? false;
const savedSelectedModel = modelByClient[selected] ?? '';
const selectedModelOption = findAgentModel(models, savedSelectedModel);
const selectedModel = selectedModelOption?.name ?? '';
@@ -629,11 +633,14 @@ export function AgentsPage() {
(target) => target.id === selectedLaunchTargetId,
) ?? activeLaunchTargets[0] ?? null;
const appliedModel = activeStatus?.appliedModel ?? activeStatus?.currentModel ?? '';
const draftChanged = Boolean(
const modelDraftChanged = Boolean(
selectedModel.trim()
&& appliedModel.trim()
&& selectedModel.trim() !== appliedModel.trim(),
);
const oauthConfigurationChanged = selected === 'codex'
&& oauthConfiguration !== Boolean(activeStatus?.oauthConfiguration);
const draftChanged = modelDraftChanged || oauthConfigurationChanged;
const canEnable = Boolean(
activeStatus?.supportedPlatform
&& activeStatus.installed
@@ -730,6 +737,7 @@ export function AgentsPage() {
oauthConfiguration,
});
await reloadStatusesAfterAction();
setOauthConfigurationDraft(null);
} catch (requestError) {
setConfigurationError(String(requestError));
} finally {
@@ -751,6 +759,7 @@ export function AgentsPage() {
});
setDefaultConfirmOpen(false);
await reloadStatusesAfterAction();
setOauthConfigurationDraft(null);
} catch (requestError) {
setDefaultError(String(requestError));
} finally {
@@ -767,6 +776,7 @@ export function AgentsPage() {
setClearConfirmOpen(false);
setClearNotice(t('agents.clear.success'));
await reloadStatusesAfterAction();
setOauthConfigurationDraft(null);
} catch (requestError) {
setClearError(String(requestError));
} finally {
@@ -942,7 +952,7 @@ export function AgentsPage() {
<section className="agent-core-setting-section agent-model-section">
<div className="agent-section-heading">
<div><strong>{t('agents.useModel')}</strong></div>
{draftChanged ? <span className="agent-pending-badge">{t('agents.pending')}</span> : null}
{modelDraftChanged ? <span className="agent-pending-badge">{t('agents.pending')}</span> : null}
</div>
<AgentModelPicker
models={models}
@@ -983,7 +993,7 @@ export function AgentsPage() {
type="checkbox"
role="switch"
checked={oauthConfiguration}
onChange={(event) => setOauthConfiguration(event.currentTarget.checked)}
onChange={(event) => setOauthConfigurationDraft(event.currentTarget.checked)}
disabled={busy}
aria-label={t('agents.modify.oauthConfiguration')}
/>
+2 -11
View File
@@ -8,7 +8,6 @@ type RepairOnLaunchSetting = {
};
type StatusMessage = {
kind: 'success' | 'error';
text: string;
};
@@ -32,7 +31,6 @@ export function CodexSessionAutoRestoreCard() {
.catch((error) => {
if (!disposed) {
setStatus({
kind: 'error',
text: t('agents.sessions.settingLoadFailed', { error: errorMessage(error) }),
});
}
@@ -53,15 +51,8 @@ export function CodexSessionAutoRestoreCard() {
request: { enabled: nextEnabled },
});
setEnabled(result.enabled);
setStatus({
kind: 'success',
text: result.enabled
? t('agents.sessions.autoRepairEnabled')
: t('agents.sessions.autoRepairDisabled'),
});
} catch (error) {
setStatus({
kind: 'error',
text: t('agents.sessions.settingFailed', { error: errorMessage(error) }),
});
} finally {
@@ -102,8 +93,8 @@ export function CodexSessionAutoRestoreCard() {
</div>
{status ? (
<small
className={`codex-auto-restore-message ${status.kind}`}
role={status.kind === 'error' ? 'alert' : 'status'}
className="codex-auto-restore-message error"
role="alert"
>
{status.text}
</small>
+22 -53
View File
@@ -440,71 +440,40 @@ input:focus-visible {
border-top: 1px solid var(--theme-e3e1db);
}
.sidebar-theme-toggle {
.sidebar-theme-selector {
display: grid;
grid-template-columns: 18px minmax(0, 1fr) 36px;
align-items: center;
gap: 8px;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 3px;
width: 100%;
min-height: 40px;
padding: 0 10px;
padding: 3px;
border: 1px solid var(--theme-d8d4cc);
border-radius: 8px;
background: var(--theme-fffdf8);
color: var(--theme-716b61);
text-align: left;
}
.sidebar-theme-toggle:hover {
border-color: var(--theme-c7c2b8);
.sidebar-theme-selector button {
min-width: 0;
min-height: 32px;
padding: 0 8px;
border: 0;
border-radius: 5px;
background: transparent;
color: var(--theme-716b61);
font-size: 12px;
font-weight: 700;
transition: background 140ms ease, color 140ms ease, box-shadow 140ms ease;
white-space: nowrap;
}
.sidebar-theme-selector button:hover {
background: var(--theme-e8e5dc);
color: var(--theme-2d2a26);
}
.sidebar-theme-toggle > svg {
color: var(--theme-7a746c);
}
.sidebar-theme-toggle > span {
min-width: 0;
overflow: hidden;
font-size: 12px;
font-weight: 700;
text-overflow: ellipsis;
white-space: nowrap;
}
.theme-switch-track {
position: relative;
display: block;
width: 36px;
height: 20px;
border: 1px solid var(--theme-c7c2b8);
border-radius: 999px;
.sidebar-theme-selector button.active {
background: var(--theme-dfdcd4);
transition: border-color 160ms ease, background 160ms ease;
}
.theme-switch-track b {
position: absolute;
top: 2px;
left: 2px;
display: block;
width: 14px;
height: 14px;
border-radius: 50%;
background: var(--theme-fff);
box-shadow: 0 1px 3px rgba(45, 42, 38, 0.24);
transition: transform 180ms ease, background 160ms ease;
}
.sidebar-theme-toggle[aria-pressed='true'] .theme-switch-track {
border-color: var(--theme-8069ad);
background: var(--theme-654f91);
}
.sidebar-theme-toggle[aria-pressed='true'] .theme-switch-track b {
transform: translateX(16px);
box-shadow: inset 0 0 0 1px var(--theme-d5d2cb);
color: var(--theme-2d2a26);
}
.sidebar-language {