mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-27 16:26:16 +08:00
feat(codex): preserve OAuth login state during third-party provider switching
Codex provider switches now only write config.toml for third-party providers, injecting the API key as experimental_bearer_token. The user's auth.json (ChatGPT OAuth tokens) is preserved. Official providers with login material still write auth.json normally. Backfill restores bearer tokens into stored provider auth.OPENAI_API_KEY to maintain canonical shape.
This commit is contained in:
@@ -8,7 +8,10 @@ import { usageApi, settingsApi, type AppId } from "@/lib/api";
|
||||
import { copilotGetUsage, copilotGetUsageForAccount } from "@/lib/api/copilot";
|
||||
import { useSettingsQuery } from "@/lib/query";
|
||||
import { resolveManagedAccountId } from "@/lib/authBinding";
|
||||
import { extractCodexBaseUrl } from "@/utils/providerConfigUtils";
|
||||
import {
|
||||
extractCodexBaseUrl,
|
||||
extractCodexExperimentalBearerToken,
|
||||
} from "@/utils/providerConfigUtils";
|
||||
import JsonEditor from "./JsonEditor";
|
||||
import * as prettier from "prettier/standalone";
|
||||
import * as parserBabel from "prettier/parser-babel";
|
||||
@@ -173,8 +176,13 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
|
||||
// Codex: { auth: { OPENAI_API_KEY }, config: TOML string with base_url }
|
||||
const auth = (config as any).auth || {};
|
||||
const configToml = (config as any).config || "";
|
||||
const apiKey =
|
||||
typeof auth.OPENAI_API_KEY === "string" &&
|
||||
auth.OPENAI_API_KEY.trim()
|
||||
? auth.OPENAI_API_KEY
|
||||
: extractCodexExperimentalBearerToken(configToml);
|
||||
return {
|
||||
apiKey: auth.OPENAI_API_KEY,
|
||||
apiKey,
|
||||
baseUrl: extractCodexBaseUrl(configToml),
|
||||
};
|
||||
} else if (appId === "gemini") {
|
||||
|
||||
@@ -20,6 +20,7 @@ import { ProviderHealthBadge } from "@/components/providers/ProviderHealthBadge"
|
||||
import { FailoverPriorityBadge } from "@/components/providers/FailoverPriorityBadge";
|
||||
import {
|
||||
extractCodexBaseUrl,
|
||||
extractCodexExperimentalBearerToken,
|
||||
extractCodexWireApi,
|
||||
isCodexChatWireApi,
|
||||
} from "@/utils/providerConfigUtils";
|
||||
@@ -78,7 +79,14 @@ function isOfficialProvider(provider: Provider, appId: AppId): boolean {
|
||||
if (appId === "codex") {
|
||||
// 无 OPENAI_API_KEY → 使用 Codex CLI 内置 OAuth(官方)
|
||||
const apiKey = config?.auth?.OPENAI_API_KEY;
|
||||
return !apiKey || (typeof apiKey === "string" && apiKey.trim() === "");
|
||||
const bearerToken =
|
||||
typeof config?.config === "string"
|
||||
? extractCodexExperimentalBearerToken(config.config)
|
||||
: undefined;
|
||||
return (
|
||||
!bearerToken &&
|
||||
(!apiKey || (typeof apiKey === "string" && apiKey.trim() === ""))
|
||||
);
|
||||
}
|
||||
if (appId === "gemini") {
|
||||
// 无 GEMINI_API_KEY 且无 GOOGLE_GEMINI_BASE_URL → Google OAuth 官方模式
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { useState, useCallback, useEffect, useRef } from "react";
|
||||
import {
|
||||
extractCodexBaseUrl,
|
||||
extractCodexExperimentalBearerToken,
|
||||
setCodexBaseUrl as setCodexBaseUrlInConfig,
|
||||
updateCodexExperimentalBearerToken,
|
||||
} from "@/utils/providerConfigUtils";
|
||||
import { normalizeTomlText } from "@/utils/textNormalization";
|
||||
import type { CodexCatalogModel } from "@/types";
|
||||
@@ -12,6 +14,19 @@ interface UseCodexConfigStateProps {
|
||||
};
|
||||
}
|
||||
|
||||
// auth.json 缺 OPENAI_API_KEY 时回退到 config.toml 的 experimental_bearer_token
|
||||
// (Mobile 兼容形态:保留 ChatGPT 登录态但用第三方 token)
|
||||
function pickCodexApiKey(
|
||||
authObj: { OPENAI_API_KEY?: unknown } | null | undefined,
|
||||
configText: string,
|
||||
): string {
|
||||
if (authObj && typeof authObj.OPENAI_API_KEY === "string") {
|
||||
const key = authObj.OPENAI_API_KEY;
|
||||
if (key) return key;
|
||||
}
|
||||
return extractCodexExperimentalBearerToken(configText) || "";
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理 Codex 配置状态
|
||||
* Codex 配置包含两部分:auth.json (JSON) 和 config.toml (TOML 字符串)
|
||||
@@ -77,14 +92,7 @@ export function useCodexConfigState({ initialData }: UseCodexConfigStateProps) {
|
||||
setCodexBaseUrl(initialBaseUrl);
|
||||
}
|
||||
|
||||
// 提取 API Key
|
||||
try {
|
||||
if (auth && typeof auth.OPENAI_API_KEY === "string") {
|
||||
setCodexApiKey(auth.OPENAI_API_KEY);
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
setCodexApiKey(pickCodexApiKey(auth, configStr));
|
||||
}
|
||||
}, [initialData]);
|
||||
|
||||
@@ -109,11 +117,15 @@ export function useCodexConfigState({ initialData }: UseCodexConfigStateProps) {
|
||||
|
||||
// 从 codexAuth 中提取并同步 API Key
|
||||
useEffect(() => {
|
||||
const extractedKey = getCodexAuthApiKey(codexAuth);
|
||||
if (extractedKey !== codexApiKey) {
|
||||
setCodexApiKey(extractedKey);
|
||||
let parsed: { OPENAI_API_KEY?: unknown } | null = null;
|
||||
try {
|
||||
parsed = JSON.parse(codexAuth || "{}");
|
||||
} catch {
|
||||
parsed = null;
|
||||
}
|
||||
}, [codexAuth, codexApiKey]);
|
||||
const extractedKey = pickCodexApiKey(parsed, codexConfig);
|
||||
setCodexApiKey((prev) => (prev === extractedKey ? prev : extractedKey));
|
||||
}, [codexAuth, codexConfig]);
|
||||
|
||||
// 验证 Codex Auth JSON
|
||||
const validateCodexAuth = useCallback((value: string): string => {
|
||||
@@ -151,6 +163,8 @@ export function useCodexConfigState({ initialData }: UseCodexConfigStateProps) {
|
||||
);
|
||||
|
||||
// 处理 Codex API Key 输入并写回 auth.json
|
||||
// 同步: 若 config.toml 当前含 experimental_bearer_token (Mobile 兼容形态),
|
||||
// 也一并更新/清除——否则用户清空输入框会被 pickCodexApiKey 的 fallback 又填回去
|
||||
const handleCodexApiKeyChange = useCallback(
|
||||
(key: string) => {
|
||||
const trimmed = key.trim();
|
||||
@@ -162,8 +176,11 @@ export function useCodexConfigState({ initialData }: UseCodexConfigStateProps) {
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
setCodexConfig((prev) =>
|
||||
updateCodexExperimentalBearerToken(prev, trimmed),
|
||||
);
|
||||
},
|
||||
[codexAuth, setCodexAuth],
|
||||
[codexAuth, setCodexAuth, setCodexConfig],
|
||||
);
|
||||
|
||||
// 处理 Codex Base URL 变化
|
||||
@@ -213,16 +230,7 @@ export function useCodexConfigState({ initialData }: UseCodexConfigStateProps) {
|
||||
const baseUrl = extractCodexBaseUrl(config);
|
||||
setCodexBaseUrl(baseUrl || "");
|
||||
|
||||
// 提取 API Key
|
||||
try {
|
||||
if (auth && typeof auth.OPENAI_API_KEY === "string") {
|
||||
setCodexApiKey(auth.OPENAI_API_KEY);
|
||||
} else {
|
||||
setCodexApiKey("");
|
||||
}
|
||||
} catch {
|
||||
setCodexApiKey("");
|
||||
}
|
||||
setCodexApiKey(pickCodexApiKey(auth, config));
|
||||
},
|
||||
[setCodexAuth, setCodexConfig, setCodexCatalogModels],
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user