fix(codex): validate OAuth credentials before enabling configuration
This commit is contained in:
1 parent
66f9ce71e6
commit
e01aef44f5
5 files changed
+130
-34
No files matched your search
+43
-5
@@ -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<PreparedAgentModels, String> {
|
||||
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 {
|
||||
|
||||
+7
-3
@@ -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': 'デフォルト設定',
|
||||
|
||||
+14
-6
@@ -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<MessageKey, string> = {
|
||||
'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<MessageKey, string> = {
|
||||
'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',
|
||||
|
||||
+61
-20
@@ -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<OAuthLoginRequiredAction | null>(null);
|
||||
const [oauthConfigurationDraft, setOauthConfigurationDraft] = useState<boolean | null>(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')}
|
||||
<strong>{t('agents.oauthLoginRequired.enableClearConfiguration')}</strong>
|
||||
{t('agents.oauthLoginRequired.enableDescriptionSuffix')}
|
||||
</>
|
||||
) : oauthLoginRequiredAction ? t(`agents.oauthLoginRequired.${oauthLoginRequiredAction}Description`) : '';
|
||||
|
||||
return (
|
||||
<section className="page management-page agents-page">
|
||||
@@ -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' ? <LoaderCircle size={16} className="spin" /> : <Terminal size={16} />}
|
||||
{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' ? <LoaderCircle size={16} className="spin" /> : <AppWindow size={16} />}
|
||||
{busyAction === 'launch-app' ? t('agents.launch.starting') : t('agents.launch.startApp')}
|
||||
@@ -1182,15 +1223,15 @@ export function AgentsPage() {
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{oauthLoginRequiredOpen ? (
|
||||
{oauthLoginRequiredAction ? (
|
||||
<div className="config-dialog-backdrop">
|
||||
<section className="config-dialog agent-restore-dialog" role="alertdialog" aria-modal="true" aria-labelledby="agent-oauth-login-required-title">
|
||||
<div className="config-dialog-heading">
|
||||
<div><AlertTriangle size={19} /><h2 id="agent-oauth-login-required-title">{t('agents.oauthLoginRequired.title')}</h2></div>
|
||||
</div>
|
||||
<p>{t('agents.oauthLoginRequired.description')}</p>
|
||||
<div className="config-dialog-actions">
|
||||
<button type="button" className="primary-button" onClick={() => setOauthLoginRequiredOpen(false)}>{t('agents.oauthLoginRequired.confirm')}</button>
|
||||
<p>{oauthLoginRequiredDescription}</p>
|
||||
<div className="config-dialog-actions single-action">
|
||||
<button type="button" className="primary-button" onClick={() => setOauthLoginRequiredAction(null)}>{t('agents.oauthLoginRequired.confirm')}</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user