mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-06-16 13:34:04 +08:00
Fix Codex OAuth auth being cleared during preserve-mode takeover
When "preserve official auth on switch" is enabled, proxy takeover routes the PROXY_MANAGED placeholder into config.toml's experimental_bearer_token and leaves auth.json (the ChatGPT OAuth login) untouched. Takeover detection only inspected auth.json's OPENAI_API_KEY, so it never recognized this state and returned a false negative, which led downstream paths to clobber the preserved OAuth login. - Detection: is_codex_live_taken_over now also matches a config.toml experimental_bearer_token equal to the placeholder, fixing detect/cleanup/ restore/startup-recovery in one place. - Cleanup: remove the config.toml bearer token only when it equals the placeholder (new remove_codex_experimental_bearer_token_if predicate), so a real third-party key is never stripped. - Write: under preservation, the None-provider takeover path writes only config.toml and keeps auth.json intact, matching the provider path. - Settings: rename the section to "Codex App Enhancements" and reword the description across all four locales. - Add tests covering OAuth preservation on takeover and placeholder-only cleanup.
This commit is contained in:
@@ -739,7 +739,10 @@ fn set_codex_experimental_bearer_token(config_text: &str, token: &str) -> Result
|
||||
Ok(doc.to_string())
|
||||
}
|
||||
|
||||
fn remove_codex_experimental_bearer_token(config_text: &str) -> Result<String, AppError> {
|
||||
pub fn remove_codex_experimental_bearer_token_if(
|
||||
config_text: &str,
|
||||
predicate: impl Fn(&str) -> bool,
|
||||
) -> Result<String, AppError> {
|
||||
if config_text.trim().is_empty() || !config_text.contains("experimental_bearer_token") {
|
||||
return Ok(config_text.to_string());
|
||||
}
|
||||
@@ -755,14 +758,32 @@ fn remove_codex_experimental_bearer_token(config_text: &str) -> Result<String, A
|
||||
.and_then(|table| table.get_mut(provider_id.as_str()))
|
||||
.and_then(|item| item.as_table_mut())
|
||||
{
|
||||
provider_table.remove("experimental_bearer_token");
|
||||
let should_remove = provider_table
|
||||
.get("experimental_bearer_token")
|
||||
.and_then(|item| item.as_str())
|
||||
.map(str::trim)
|
||||
.is_some_and(&predicate);
|
||||
if should_remove {
|
||||
provider_table.remove("experimental_bearer_token");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
doc.as_table_mut().remove("experimental_bearer_token");
|
||||
let should_remove_top_level = doc
|
||||
.get("experimental_bearer_token")
|
||||
.and_then(|item| item.as_str())
|
||||
.map(str::trim)
|
||||
.is_some_and(&predicate);
|
||||
if should_remove_top_level {
|
||||
doc.as_table_mut().remove("experimental_bearer_token");
|
||||
}
|
||||
Ok(doc.to_string())
|
||||
}
|
||||
|
||||
fn remove_codex_experimental_bearer_token(config_text: &str) -> Result<String, AppError> {
|
||||
remove_codex_experimental_bearer_token_if(config_text, |_| true)
|
||||
}
|
||||
|
||||
/// Read the current Codex live settings as a `{ auth, config }` object.
|
||||
///
|
||||
/// Missing `auth.json` collapses to `{}` so a config-only third-party install
|
||||
|
||||
+190
-12
@@ -1242,22 +1242,19 @@ impl ProxyService {
|
||||
.get("config")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("");
|
||||
let codex_provider = self
|
||||
.get_current_provider_for_app(&AppType::Codex)
|
||||
.ok()
|
||||
.flatten();
|
||||
let codex_provider = self.require_current_provider_for_app(&AppType::Codex)?;
|
||||
let updated_config = Self::apply_codex_proxy_toml_config_for_provider(
|
||||
config_str,
|
||||
&proxy_codex_base_url,
|
||||
codex_provider.as_ref(),
|
||||
Some(&codex_provider),
|
||||
);
|
||||
live_config["config"] = json!(updated_config);
|
||||
Self::attach_codex_model_catalog_from_provider(
|
||||
&mut live_config,
|
||||
codex_provider.as_ref(),
|
||||
Some(&codex_provider),
|
||||
);
|
||||
|
||||
self.write_codex_live_for_provider(&live_config, codex_provider.as_ref())?;
|
||||
self.write_codex_live_for_provider(&live_config, Some(&codex_provider))?;
|
||||
log::info!("Codex Live 配置已接管,代理地址: {proxy_codex_base_url}");
|
||||
}
|
||||
AppType::Gemini => {
|
||||
@@ -1602,6 +1599,11 @@ impl ProxyService {
|
||||
|
||||
if let Some(cfg_str) = config.get("config").and_then(|v| v.as_str()) {
|
||||
let updated = Self::remove_local_toml_base_url(cfg_str);
|
||||
let updated =
|
||||
crate::codex_config::remove_codex_experimental_bearer_token_if(&updated, |token| {
|
||||
token == PROXY_TOKEN_PLACEHOLDER
|
||||
})
|
||||
.map_err(|e| format!("清理 Codex 接管占位符失败: {e}"))?;
|
||||
config["config"] = json!(updated);
|
||||
}
|
||||
|
||||
@@ -1715,11 +1717,22 @@ impl ProxyService {
|
||||
}
|
||||
|
||||
fn is_codex_live_taken_over(config: &Value) -> bool {
|
||||
let auth = match config.get("auth").and_then(|v| v.as_object()) {
|
||||
Some(auth) => auth,
|
||||
None => return false,
|
||||
};
|
||||
auth.get("OPENAI_API_KEY").and_then(|v| v.as_str()) == Some(PROXY_TOKEN_PLACEHOLDER)
|
||||
if config
|
||||
.get("auth")
|
||||
.and_then(|v| v.as_object())
|
||||
.and_then(|auth| auth.get("OPENAI_API_KEY"))
|
||||
.and_then(|v| v.as_str())
|
||||
== Some(PROXY_TOKEN_PLACEHOLDER)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
config
|
||||
.get("config")
|
||||
.and_then(|v| v.as_str())
|
||||
.and_then(crate::codex_config::extract_codex_experimental_bearer_token)
|
||||
.as_deref()
|
||||
== Some(PROXY_TOKEN_PLACEHOLDER)
|
||||
}
|
||||
|
||||
fn is_gemini_live_taken_over(config: &Value) -> bool {
|
||||
@@ -2055,6 +2068,25 @@ impl ProxyService {
|
||||
provider: Option<&Provider>,
|
||||
) -> Result<(), String> {
|
||||
let Some(provider) = provider else {
|
||||
if crate::settings::preserve_codex_official_auth_on_switch() {
|
||||
if let (Some(auth), Some(config_str)) = (
|
||||
config.get("auth"),
|
||||
config.get("config").and_then(|v| v.as_str()),
|
||||
) {
|
||||
if auth.get("OPENAI_API_KEY").and_then(|v| v.as_str())
|
||||
== Some(PROXY_TOKEN_PLACEHOLDER)
|
||||
{
|
||||
let live_config = crate::codex_config::prepare_codex_provider_live_config(
|
||||
auth, config_str,
|
||||
)
|
||||
.map_err(|e| format!("写入 Codex 配置失败: {e}"))?;
|
||||
crate::codex_config::write_codex_live_config_atomic(Some(&live_config))
|
||||
.map_err(|e| format!("写入 Codex 配置失败: {e}"))?;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return self.write_codex_live_verbatim(config);
|
||||
};
|
||||
|
||||
@@ -2645,6 +2677,152 @@ wire_api = "responses"
|
||||
.expect("reset settings");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn codex_takeover_preserves_oauth_auth_json_when_preserve_enabled() {
|
||||
let _home = TempHome::new();
|
||||
crate::settings::reload_settings().expect("reload settings");
|
||||
crate::settings::update_settings(crate::settings::AppSettings {
|
||||
preserve_codex_official_auth_on_switch: true,
|
||||
..Default::default()
|
||||
})
|
||||
.expect("enable Codex official auth preservation");
|
||||
|
||||
let db = Arc::new(Database::memory().expect("init db"));
|
||||
let service = ProxyService::new(db.clone());
|
||||
let oauth_auth = json!({
|
||||
"auth_mode": "chatgpt",
|
||||
"tokens": {
|
||||
"id_token": "oauth-id",
|
||||
"access_token": "oauth-access"
|
||||
}
|
||||
});
|
||||
let deepseek_live_config = r#"model_provider = "deepseek"
|
||||
model = "deepseek-v4-flash"
|
||||
|
||||
[model_providers.deepseek]
|
||||
name = "DeepSeek"
|
||||
base_url = "https://api.deepseek.com/v1"
|
||||
wire_api = "responses"
|
||||
experimental_bearer_token = "deepseek-key"
|
||||
"#;
|
||||
crate::codex_config::write_codex_live_atomic(&oauth_auth, Some(deepseek_live_config))
|
||||
.expect("seed live OAuth auth with DeepSeek config");
|
||||
|
||||
let mut provider = Provider::with_id(
|
||||
"deepseek".to_string(),
|
||||
"DeepSeek".to_string(),
|
||||
json!({
|
||||
"auth": {
|
||||
"OPENAI_API_KEY": "deepseek-key"
|
||||
},
|
||||
"config": r#"model_provider = "deepseek"
|
||||
model = "deepseek-v4-flash"
|
||||
|
||||
[model_providers.deepseek]
|
||||
name = "DeepSeek"
|
||||
base_url = "https://api.deepseek.com/v1"
|
||||
wire_api = "responses"
|
||||
"#
|
||||
}),
|
||||
None,
|
||||
);
|
||||
provider.category = Some("cn_official".to_string());
|
||||
db.save_provider("codex", &provider)
|
||||
.expect("save DeepSeek provider");
|
||||
db.set_current_provider("codex", "deepseek")
|
||||
.expect("set current provider");
|
||||
crate::settings::set_current_provider(&AppType::Codex, Some("deepseek"))
|
||||
.expect("set local current provider");
|
||||
|
||||
service
|
||||
.takeover_live_config_strict(&AppType::Codex)
|
||||
.await
|
||||
.expect("take over Codex live config");
|
||||
|
||||
let live_auth: Value =
|
||||
crate::config::read_json_file(&crate::codex_config::get_codex_auth_path())
|
||||
.expect("read live auth");
|
||||
assert_eq!(
|
||||
live_auth, oauth_auth,
|
||||
"Codex takeover should not overwrite ChatGPT OAuth auth when preservation is enabled"
|
||||
);
|
||||
|
||||
let live_config = std::fs::read_to_string(crate::codex_config::get_codex_config_path())
|
||||
.expect("read live config");
|
||||
assert!(
|
||||
live_config.contains(PROXY_TOKEN_PLACEHOLDER),
|
||||
"takeover placeholder should move into config.toml"
|
||||
);
|
||||
assert!(
|
||||
service.detect_takeover_in_live_config_for_app(&AppType::Codex),
|
||||
"Codex takeover detection should recognize config.toml placeholders"
|
||||
);
|
||||
|
||||
crate::settings::update_settings(crate::settings::AppSettings::default())
|
||||
.expect("reset settings");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn codex_takeover_cleanup_removes_config_placeholder_without_touching_oauth_auth() {
|
||||
let _home = TempHome::new();
|
||||
crate::settings::reload_settings().expect("reload settings");
|
||||
|
||||
let db = Arc::new(Database::memory().expect("init db"));
|
||||
let service = ProxyService::new(db);
|
||||
let oauth_auth = json!({
|
||||
"auth_mode": "chatgpt",
|
||||
"tokens": {
|
||||
"id_token": "oauth-id",
|
||||
"access_token": "oauth-access"
|
||||
}
|
||||
});
|
||||
crate::codex_config::write_codex_live_atomic(
|
||||
&oauth_auth,
|
||||
Some(
|
||||
r#"model_provider = "deepseek"
|
||||
model = "deepseek-v4-flash"
|
||||
|
||||
[model_providers.deepseek]
|
||||
name = "DeepSeek"
|
||||
base_url = "http://127.0.0.1:15721/v1"
|
||||
wire_api = "responses"
|
||||
experimental_bearer_token = "PROXY_MANAGED"
|
||||
"#,
|
||||
),
|
||||
)
|
||||
.expect("seed taken-over Codex live config");
|
||||
|
||||
assert!(
|
||||
service.detect_takeover_in_live_config_for_app(&AppType::Codex),
|
||||
"config.toml placeholder should be detected before cleanup"
|
||||
);
|
||||
|
||||
service
|
||||
.cleanup_codex_takeover_placeholders_in_live()
|
||||
.expect("cleanup Codex takeover placeholders");
|
||||
|
||||
let live_auth: Value =
|
||||
crate::config::read_json_file(&crate::codex_config::get_codex_auth_path())
|
||||
.expect("read live auth");
|
||||
assert_eq!(
|
||||
live_auth, oauth_auth,
|
||||
"cleanup should preserve ChatGPT OAuth auth"
|
||||
);
|
||||
|
||||
let live_config = std::fs::read_to_string(crate::codex_config::get_codex_config_path())
|
||||
.expect("read live config");
|
||||
assert!(
|
||||
!live_config.contains(PROXY_TOKEN_PLACEHOLDER),
|
||||
"cleanup should remove config.toml proxy bearer placeholder"
|
||||
);
|
||||
assert!(
|
||||
!live_config.contains("http://127.0.0.1:15721"),
|
||||
"cleanup should remove local proxy base_url"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn codex_custom_provider_live_write_can_overwrite_auth_when_preserve_disabled() {
|
||||
|
||||
@@ -242,11 +242,11 @@ export function SettingsPage({
|
||||
handleAutoSave({ skillSyncMethod: method })
|
||||
}
|
||||
/>
|
||||
<WindowSettings
|
||||
<CodexAuthSettings
|
||||
settings={settings}
|
||||
onChange={handleAutoSave}
|
||||
/>
|
||||
<CodexAuthSettings
|
||||
<WindowSettings
|
||||
settings={settings}
|
||||
onChange={handleAutoSave}
|
||||
/>
|
||||
|
||||
@@ -562,9 +562,9 @@
|
||||
"enableClaudePluginIntegrationDescription": "When enabled, the VS Code Claude Code extension provider will switch with this app",
|
||||
"skipClaudeOnboarding": "Skip Claude Code first-run confirmation",
|
||||
"skipClaudeOnboardingDescription": "When enabled, Claude Code will skip the first-run confirmation",
|
||||
"codexAuth": "Codex Authentication",
|
||||
"codexAuth": "Codex App Enhancements",
|
||||
"preserveCodexOfficialAuthOnSwitch": "Keep official login when switching third-party providers",
|
||||
"preserveCodexOfficialAuthOnSwitchDescription": "When enabled, third-party Codex switches update only config.toml and keep the ChatGPT login in auth.json. When disabled, auth.json is overwritten with the active provider auth.",
|
||||
"preserveCodexOfficialAuthOnSwitchDescription": "When enabled, you can use the Codex app's official plugins, mobile remote control, and other features while using third-party APIs.",
|
||||
"appVisibility": {
|
||||
"title": "Homepage Display",
|
||||
"description": "Choose which apps to show on the homepage",
|
||||
|
||||
@@ -562,9 +562,9 @@
|
||||
"enableClaudePluginIntegrationDescription": "オンにすると VS Code の Claude Code 拡張のプロバイダーも同期します",
|
||||
"skipClaudeOnboarding": "Claude Code の初回確認をスキップ",
|
||||
"skipClaudeOnboardingDescription": "オンにすると Claude Code の初回インストール確認をスキップします",
|
||||
"codexAuth": "Codex 認証",
|
||||
"codexAuth": "Codex アプリ拡張",
|
||||
"preserveCodexOfficialAuthOnSwitch": "サードパーティ切替時に公式ログインを保持",
|
||||
"preserveCodexOfficialAuthOnSwitchDescription": "オンにするとサードパーティ Codex への切替では config.toml のみ更新し、auth.json の ChatGPT ログインを保持します。オフにすると auth.json を現在のプロバイダー認証で上書きします。",
|
||||
"preserveCodexOfficialAuthOnSwitchDescription": "オンにすると、サードパーティ API を使用する際に Codex アプリの公式プラグインやスマホからのリモート操作などの機能を利用できます。",
|
||||
"appVisibility": {
|
||||
"title": "ホームページ表示",
|
||||
"description": "ホームページに表示するアプリを選択",
|
||||
|
||||
@@ -562,9 +562,9 @@
|
||||
"enableClaudePluginIntegrationDescription": "開啟後 Vscode Claude Code 外掛程式的供應商將隨本軟體切換",
|
||||
"skipClaudeOnboarding": "跳過 Claude Code 初次安裝確認",
|
||||
"skipClaudeOnboardingDescription": "開啟後跳過 Claude Code 初次安裝確認",
|
||||
"codexAuth": "Codex 認證",
|
||||
"codexAuth": "Codex 應用增強",
|
||||
"preserveCodexOfficialAuthOnSwitch": "切換第三方時保留官方登入",
|
||||
"preserveCodexOfficialAuthOnSwitchDescription": "開啟後切換第三方 Codex 供應商只更新 config.toml,並保留 auth.json 中的 ChatGPT 登入狀態;關閉後會用目前供應商 auth 覆寫 auth.json。",
|
||||
"preserveCodexOfficialAuthOnSwitchDescription": "開啟後可以在使用第三方 API 的時候使用 Codex 應用的官方外掛、手機遠端操作等功能",
|
||||
"appVisibility": {
|
||||
"title": "主頁面顯示",
|
||||
"description": "選擇在主頁面顯示的應用程式",
|
||||
|
||||
@@ -562,9 +562,9 @@
|
||||
"enableClaudePluginIntegrationDescription": "开启后 Vscode Claude Code 插件的供应商将随本软件切换",
|
||||
"skipClaudeOnboarding": "跳过 Claude Code 初次安装确认",
|
||||
"skipClaudeOnboardingDescription": "开启后跳过 Claude Code 初次安装确认",
|
||||
"codexAuth": "Codex 认证",
|
||||
"codexAuth": "Codex 应用增强",
|
||||
"preserveCodexOfficialAuthOnSwitch": "切换第三方时保留官方登录",
|
||||
"preserveCodexOfficialAuthOnSwitchDescription": "开启后切换第三方 Codex 供应商只更新 config.toml,并保留 auth.json 中的 ChatGPT 登录态;关闭后会用当前供应商 auth 覆盖 auth.json。",
|
||||
"preserveCodexOfficialAuthOnSwitchDescription": "开启后可以在使用第三方 API 的时候使用 Codex 应用的官方插件、手机远程操作等功能",
|
||||
"appVisibility": {
|
||||
"title": "主页面显示",
|
||||
"description": "选择在主页面显示的应用",
|
||||
|
||||
Reference in New Issue
Block a user