mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-26 23:56:02 +08:00
Add Codex auth preservation setting
This commit is contained in:
@@ -788,9 +788,9 @@ pub fn read_codex_live_settings() -> Result<Value, AppError> {
|
||||
|
||||
/// Route a Codex live write between full auth+config or config-only.
|
||||
///
|
||||
/// Official providers with usable login material own `auth.json`; everyone
|
||||
/// else only touches `config.toml` so the user's ChatGPT login cache survives
|
||||
/// third-party switches.
|
||||
/// Official providers with usable login material own `auth.json`. Third-party
|
||||
/// providers only touch `config.toml` when the compatibility setting is enabled
|
||||
/// so the user's ChatGPT login cache survives provider switches.
|
||||
pub fn write_codex_live_for_provider(
|
||||
category: Option<&str>,
|
||||
auth: &Value,
|
||||
@@ -798,6 +798,10 @@ pub fn write_codex_live_for_provider(
|
||||
) -> Result<(), AppError> {
|
||||
if category == Some("official") && codex_auth_has_login_material(auth) {
|
||||
write_codex_live_atomic(auth, config_text)
|
||||
} else if category != Some("official")
|
||||
&& !crate::settings::preserve_codex_official_auth_on_switch()
|
||||
{
|
||||
write_codex_live_atomic(auth, config_text)
|
||||
} else {
|
||||
let live_config = prepare_codex_provider_live_config(auth, config_text.unwrap_or(""))?;
|
||||
write_codex_live_config_atomic(Some(&live_config))
|
||||
|
||||
@@ -2637,6 +2637,96 @@ wire_api = "responses"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn codex_custom_provider_live_write_can_overwrite_auth_when_preserve_disabled() {
|
||||
let _home = TempHome::new();
|
||||
crate::settings::reload_settings().expect("reload settings");
|
||||
crate::settings::update_settings(crate::settings::AppSettings {
|
||||
preserve_codex_official_auth_on_switch: false,
|
||||
..Default::default()
|
||||
})
|
||||
.expect("disable Codex official auth preservation");
|
||||
|
||||
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 = "openai"
|
||||
model = "gpt-5-codex"
|
||||
"#,
|
||||
),
|
||||
)
|
||||
.expect("seed live OAuth auth");
|
||||
|
||||
let mut provider = Provider::with_id(
|
||||
"rightcode".to_string(),
|
||||
"RightCode".to_string(),
|
||||
json!({
|
||||
"auth": {
|
||||
"OPENAI_API_KEY": "rightcode-key"
|
||||
},
|
||||
"config": r#"model_provider = "rightcode"
|
||||
model = "gpt-5-codex"
|
||||
|
||||
[model_providers.rightcode]
|
||||
name = "RightCode"
|
||||
base_url = "https://rightcode.example/v1"
|
||||
wire_api = "responses"
|
||||
"#
|
||||
}),
|
||||
None,
|
||||
);
|
||||
provider.category = Some("custom".to_string());
|
||||
let takeover_auth = json!({
|
||||
"OPENAI_API_KEY": PROXY_TOKEN_PLACEHOLDER
|
||||
});
|
||||
let takeover_settings = json!({
|
||||
"auth": takeover_auth,
|
||||
"config": r#"model_provider = "rightcode"
|
||||
model = "gpt-5-codex"
|
||||
|
||||
[model_providers.rightcode]
|
||||
name = "RightCode"
|
||||
base_url = "http://127.0.0.1:15721/v1"
|
||||
wire_api = "responses"
|
||||
"#
|
||||
});
|
||||
|
||||
service
|
||||
.write_codex_live_for_provider(&takeover_settings, Some(&provider))
|
||||
.expect("write provider-driven 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,
|
||||
json!({
|
||||
"OPENAI_API_KEY": PROXY_TOKEN_PLACEHOLDER
|
||||
}),
|
||||
"disabled preservation should let third-party switches overwrite auth.json"
|
||||
);
|
||||
|
||||
let live_config = std::fs::read_to_string(crate::codex_config::get_codex_config_path())
|
||||
.expect("read live config");
|
||||
assert!(
|
||||
!live_config.contains("experimental_bearer_token"),
|
||||
"provider token should stay in auth.json when preservation is disabled"
|
||||
);
|
||||
|
||||
crate::settings::update_settings(crate::settings::AppSettings::default())
|
||||
.expect("reset settings");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_toml_base_url_updates_active_model_provider_base_url() {
|
||||
let input = r#"
|
||||
|
||||
@@ -253,6 +253,9 @@ pub struct AppSettings {
|
||||
/// Whether to show the failover toggle independently on the main page
|
||||
#[serde(default)]
|
||||
pub enable_failover_toggle: bool,
|
||||
/// Keep Codex ChatGPT login material in auth.json when switching to third-party providers
|
||||
#[serde(default = "default_true")]
|
||||
pub preserve_codex_official_auth_on_switch: bool,
|
||||
/// User has confirmed the failover toggle first-run notice
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub failover_confirmed: Option<bool>,
|
||||
@@ -366,6 +369,7 @@ impl Default for AppSettings {
|
||||
usage_confirmed: None,
|
||||
stream_check_confirmed: None,
|
||||
enable_failover_toggle: false,
|
||||
preserve_codex_official_auth_on_switch: true,
|
||||
failover_confirmed: None,
|
||||
first_run_notice_confirmed: None,
|
||||
common_config_confirmed: None,
|
||||
@@ -699,6 +703,16 @@ pub fn get_hermes_override_dir() -> Option<PathBuf> {
|
||||
.map(|p| resolve_override_path(p))
|
||||
}
|
||||
|
||||
pub fn preserve_codex_official_auth_on_switch() -> bool {
|
||||
settings_store()
|
||||
.read()
|
||||
.unwrap_or_else(|e| {
|
||||
log::warn!("设置锁已毒化,使用恢复值: {e}");
|
||||
e.into_inner()
|
||||
})
|
||||
.preserve_codex_official_auth_on_switch
|
||||
}
|
||||
|
||||
// ===== 当前供应商管理函数 =====
|
||||
|
||||
/// 获取指定应用类型的当前供应商 ID(从本地 settings 读取)
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { KeyRound } from "lucide-react";
|
||||
import type { SettingsFormState } from "@/hooks/useSettings";
|
||||
import { ToggleRow } from "@/components/ui/toggle-row";
|
||||
|
||||
interface CodexAuthSettingsProps {
|
||||
settings: SettingsFormState;
|
||||
onChange: (updates: Partial<SettingsFormState>) => void;
|
||||
}
|
||||
|
||||
export function CodexAuthSettings({
|
||||
settings,
|
||||
onChange,
|
||||
}: CodexAuthSettingsProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<section className="space-y-4">
|
||||
<div className="flex items-center gap-2 pb-2 border-b border-border/40">
|
||||
<KeyRound className="h-4 w-4 text-primary" />
|
||||
<h3 className="text-sm font-medium">{t("settings.codexAuth")}</h3>
|
||||
</div>
|
||||
|
||||
<ToggleRow
|
||||
icon={<KeyRound className="h-4 w-4 text-emerald-500" />}
|
||||
title={t("settings.preserveCodexOfficialAuthOnSwitch")}
|
||||
description={t("settings.preserveCodexOfficialAuthOnSwitchDescription")}
|
||||
checked={settings.preserveCodexOfficialAuthOnSwitch ?? true}
|
||||
onCheckedChange={(value) =>
|
||||
onChange({ preserveCodexOfficialAuthOnSwitch: value })
|
||||
}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -44,6 +44,7 @@ import { ModelTestConfigPanel } from "@/components/usage/ModelTestConfigPanel";
|
||||
import { UsageDashboard } from "@/components/usage/UsageDashboard";
|
||||
import { LogConfigPanel } from "@/components/settings/LogConfigPanel";
|
||||
import { AuthCenterPanel } from "@/components/settings/AuthCenterPanel";
|
||||
import { CodexAuthSettings } from "@/components/settings/CodexAuthSettings";
|
||||
import { useInstalledSkills } from "@/hooks/useSkills";
|
||||
import { useSettings } from "@/hooks/useSettings";
|
||||
import { useImportExport } from "@/hooks/useImportExport";
|
||||
@@ -245,6 +246,10 @@ export function SettingsPage({
|
||||
settings={settings}
|
||||
onChange={handleAutoSave}
|
||||
/>
|
||||
<CodexAuthSettings
|
||||
settings={settings}
|
||||
onChange={handleAutoSave}
|
||||
/>
|
||||
<TerminalSettings
|
||||
value={settings.preferredTerminal}
|
||||
onChange={(terminal) =>
|
||||
|
||||
@@ -116,6 +116,8 @@ export function useSettingsForm(): UseSettingsFormResult {
|
||||
data.enableClaudePluginIntegration ?? false,
|
||||
silentStartup: data.silentStartup ?? false,
|
||||
skipClaudeOnboarding: data.skipClaudeOnboarding ?? false,
|
||||
preserveCodexOfficialAuthOnSwitch:
|
||||
data.preserveCodexOfficialAuthOnSwitch ?? true,
|
||||
claudeConfigDir: sanitizeDir(data.claudeConfigDir),
|
||||
codexConfigDir: sanitizeDir(data.codexConfigDir),
|
||||
geminiConfigDir: sanitizeDir(data.geminiConfigDir),
|
||||
@@ -140,6 +142,7 @@ export function useSettingsForm(): UseSettingsFormResult {
|
||||
useAppWindowControls: false,
|
||||
enableClaudePluginIntegration: false,
|
||||
skipClaudeOnboarding: false,
|
||||
preserveCodexOfficialAuthOnSwitch: true,
|
||||
language: readPersistedLanguage(),
|
||||
} as SettingsFormState);
|
||||
|
||||
@@ -177,6 +180,8 @@ export function useSettingsForm(): UseSettingsFormResult {
|
||||
serverData.enableClaudePluginIntegration ?? false,
|
||||
silentStartup: serverData.silentStartup ?? false,
|
||||
skipClaudeOnboarding: serverData.skipClaudeOnboarding ?? false,
|
||||
preserveCodexOfficialAuthOnSwitch:
|
||||
serverData.preserveCodexOfficialAuthOnSwitch ?? true,
|
||||
claudeConfigDir: sanitizeDir(serverData.claudeConfigDir),
|
||||
codexConfigDir: sanitizeDir(serverData.codexConfigDir),
|
||||
geminiConfigDir: sanitizeDir(serverData.geminiConfigDir),
|
||||
|
||||
@@ -561,6 +561,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",
|
||||
"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.",
|
||||
"appVisibility": {
|
||||
"title": "Homepage Display",
|
||||
"description": "Choose which apps to show on the homepage",
|
||||
|
||||
@@ -561,6 +561,9 @@
|
||||
"enableClaudePluginIntegrationDescription": "オンにすると VS Code の Claude Code 拡張のプロバイダーも同期します",
|
||||
"skipClaudeOnboarding": "Claude Code の初回確認をスキップ",
|
||||
"skipClaudeOnboardingDescription": "オンにすると Claude Code の初回インストール確認をスキップします",
|
||||
"codexAuth": "Codex 認証",
|
||||
"preserveCodexOfficialAuthOnSwitch": "サードパーティ切替時に公式ログインを保持",
|
||||
"preserveCodexOfficialAuthOnSwitchDescription": "オンにするとサードパーティ Codex への切替では config.toml のみ更新し、auth.json の ChatGPT ログインを保持します。オフにすると auth.json を現在のプロバイダー認証で上書きします。",
|
||||
"appVisibility": {
|
||||
"title": "ホームページ表示",
|
||||
"description": "ホームページに表示するアプリを選択",
|
||||
|
||||
@@ -561,6 +561,9 @@
|
||||
"enableClaudePluginIntegrationDescription": "開啟後 Vscode Claude Code 外掛程式的供應商將隨本軟體切換",
|
||||
"skipClaudeOnboarding": "跳過 Claude Code 初次安裝確認",
|
||||
"skipClaudeOnboardingDescription": "開啟後跳過 Claude Code 初次安裝確認",
|
||||
"codexAuth": "Codex 認證",
|
||||
"preserveCodexOfficialAuthOnSwitch": "切換第三方時保留官方登入",
|
||||
"preserveCodexOfficialAuthOnSwitchDescription": "開啟後切換第三方 Codex 供應商只更新 config.toml,並保留 auth.json 中的 ChatGPT 登入狀態;關閉後會用目前供應商 auth 覆寫 auth.json。",
|
||||
"appVisibility": {
|
||||
"title": "主頁面顯示",
|
||||
"description": "選擇在主頁面顯示的應用程式",
|
||||
|
||||
@@ -561,6 +561,9 @@
|
||||
"enableClaudePluginIntegrationDescription": "开启后 Vscode Claude Code 插件的供应商将随本软件切换",
|
||||
"skipClaudeOnboarding": "跳过 Claude Code 初次安装确认",
|
||||
"skipClaudeOnboardingDescription": "开启后跳过 Claude Code 初次安装确认",
|
||||
"codexAuth": "Codex 认证",
|
||||
"preserveCodexOfficialAuthOnSwitch": "切换第三方时保留官方登录",
|
||||
"preserveCodexOfficialAuthOnSwitchDescription": "开启后切换第三方 Codex 供应商只更新 config.toml,并保留 auth.json 中的 ChatGPT 登录态;关闭后会用当前供应商 auth 覆盖 auth.json。",
|
||||
"appVisibility": {
|
||||
"title": "主页面显示",
|
||||
"description": "选择在主页面显示的应用",
|
||||
|
||||
@@ -15,6 +15,7 @@ export const settingsSchema = z.object({
|
||||
skipClaudeOnboarding: z.boolean().optional(),
|
||||
launchOnStartup: z.boolean().optional(),
|
||||
enableLocalProxy: z.boolean().optional(),
|
||||
preserveCodexOfficialAuthOnSwitch: z.boolean().optional(),
|
||||
language: z.enum(["en", "zh", "zh-TW", "ja"]).optional(),
|
||||
|
||||
// 设备级目录覆盖
|
||||
|
||||
@@ -333,6 +333,8 @@ export interface Settings {
|
||||
streamCheckConfirmed?: boolean;
|
||||
// Whether to show the failover toggle independently on the main page
|
||||
enableFailoverToggle?: boolean;
|
||||
// Preserve Codex ChatGPT login in auth.json when switching third-party providers
|
||||
preserveCodexOfficialAuthOnSwitch?: boolean;
|
||||
// User has confirmed the failover toggle first-run notice
|
||||
failoverConfirmed?: boolean;
|
||||
// User has confirmed the first-run welcome notice
|
||||
|
||||
Reference in New Issue
Block a user