mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 12:44:18 +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:
@@ -417,13 +417,23 @@ export const hasTomlCommonConfigSnippet = (
|
||||
const TOML_SECTION_HEADER_PATTERN = /^\s*\[([^\]\r\n]+)\]\s*$/;
|
||||
const TOML_BASE_URL_PATTERN =
|
||||
/^\s*base_url\s*=\s*(["'])([^"'\r\n]+)\1\s*(?:#.*)?$/;
|
||||
const TOML_EXPERIMENTAL_BEARER_TOKEN_PATTERN =
|
||||
/^\s*experimental_bearer_token\s*=\s*(["'])([^"'\r\n]+)\1\s*(?:#.*)?$/;
|
||||
const TOML_EXPERIMENTAL_BEARER_TOKEN_REPLACE_PATTERN =
|
||||
/^(\s*experimental_bearer_token\s*=\s*)(?:"(?:\\.|[^"\\\r\n])*"|'[^'\r\n]*')(\s*(?:#.*)?)$/;
|
||||
const TOML_MODEL_PATTERN = /^\s*model\s*=\s*(["'])([^"'\r\n]+)\1\s*(?:#.*)?$/;
|
||||
const TOML_WIRE_API_PATTERN =
|
||||
/^\s*wire_api\s*=\s*(["'])([^"'\r\n]+)\1\s*(?:#.*)?$/;
|
||||
const TOML_MODEL_PROVIDER_LINE_PATTERN =
|
||||
/^\s*model_provider\s*=\s*(["'])([^"'\r\n]+)\1\s*(?:#.*)?$/;
|
||||
const TOML_MODEL_PROVIDER_PATTERN =
|
||||
/^\s*model_provider\s*=\s*(["'])([^"'\r\n]+)\1\s*(?:#.*)?$/m;
|
||||
const CODEX_RESERVED_MODEL_PROVIDER_IDS = new Set([
|
||||
"amazon-bedrock",
|
||||
"openai",
|
||||
"ollama",
|
||||
"lmstudio",
|
||||
"oss",
|
||||
"ollama-chat",
|
||||
]);
|
||||
|
||||
interface TomlSectionRange {
|
||||
bodyEndIndex: number;
|
||||
@@ -499,7 +509,22 @@ const getTomlSectionInsertIndex = (
|
||||
};
|
||||
|
||||
const getCodexModelProviderName = (configText: string): string | undefined => {
|
||||
const match = configText.match(TOML_MODEL_PROVIDER_PATTERN);
|
||||
const normalized = normalizeTomlText(configText);
|
||||
try {
|
||||
const parsed = parseToml(normalized) as Record<string, any>;
|
||||
const providerName =
|
||||
typeof parsed.model_provider === "string"
|
||||
? parsed.model_provider.trim()
|
||||
: undefined;
|
||||
if (providerName) return providerName;
|
||||
} catch {
|
||||
// Fall back to a top-level line scan while the user is editing invalid TOML.
|
||||
}
|
||||
|
||||
const lines = normalized.split("\n");
|
||||
const index = getTopLevelModelProviderLineIndex(lines);
|
||||
if (index === -1) return undefined;
|
||||
const match = lines[index].match(TOML_MODEL_PROVIDER_LINE_PATTERN);
|
||||
const providerName = match?.[2]?.trim();
|
||||
return providerName || undefined;
|
||||
};
|
||||
@@ -511,6 +536,20 @@ const getCodexProviderSectionName = (
|
||||
return providerName ? `model_providers.${providerName}` : undefined;
|
||||
};
|
||||
|
||||
const isCustomCodexModelProviderId = (providerName: string): boolean => {
|
||||
const id = providerName.trim().toLowerCase();
|
||||
return Boolean(id) && !CODEX_RESERVED_MODEL_PROVIDER_IDS.has(id);
|
||||
};
|
||||
|
||||
const getCodexCustomProviderSectionName = (
|
||||
configText: string,
|
||||
): string | undefined => {
|
||||
const providerName = getCodexModelProviderName(configText);
|
||||
return providerName && isCustomCodexModelProviderId(providerName)
|
||||
? `model_providers.${providerName}`
|
||||
: undefined;
|
||||
};
|
||||
|
||||
const findTomlAssignmentInRange = (
|
||||
lines: string[],
|
||||
pattern: RegExp,
|
||||
@@ -532,6 +571,21 @@ const findTomlAssignmentInRange = (
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const findTomlLineInRange = (
|
||||
lines: string[],
|
||||
pattern: RegExp,
|
||||
startIndex: number,
|
||||
endIndex: number,
|
||||
): number => {
|
||||
for (let index = startIndex; index < endIndex; index += 1) {
|
||||
if (pattern.test(lines[index])) {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
};
|
||||
|
||||
const findTomlAssignments = (
|
||||
lines: string[],
|
||||
pattern: RegExp,
|
||||
@@ -601,6 +655,23 @@ const getTopLevelModelProviderLineIndex = (lines: string[]): number => {
|
||||
return -1;
|
||||
};
|
||||
|
||||
const TOML_BASIC_STRING_ESCAPES: Record<string, string> = {
|
||||
'"': '\\"',
|
||||
"\\": "\\\\",
|
||||
"\b": "\\b",
|
||||
"\t": "\\t",
|
||||
"\n": "\\n",
|
||||
"\f": "\\f",
|
||||
"\r": "\\r",
|
||||
};
|
||||
|
||||
const escapeTomlBasicString = (value: string): string =>
|
||||
value.replace(/["\\\u0000-\u001f]/g, (ch) => {
|
||||
const escaped = TOML_BASIC_STRING_ESCAPES[ch];
|
||||
if (escaped) return escaped;
|
||||
return `\\u${ch.charCodeAt(0).toString(16).padStart(4, "0")}`;
|
||||
});
|
||||
|
||||
const CODEX_CHAT_WIRE_API_VALUES = new Set([
|
||||
"chat",
|
||||
"chat_completions",
|
||||
@@ -792,6 +863,134 @@ export const extractCodexBaseUrl = (
|
||||
}
|
||||
};
|
||||
|
||||
// 从 Codex 的 TOML 配置文本中提取 experimental_bearer_token(兼容 Mobile 模式)
|
||||
export const extractCodexExperimentalBearerToken = (
|
||||
configText: string | undefined | null,
|
||||
): string | undefined => {
|
||||
try {
|
||||
const raw = typeof configText === "string" ? configText : "";
|
||||
const text = normalizeTomlText(raw);
|
||||
if (!text) return undefined;
|
||||
|
||||
try {
|
||||
const parsed = parseToml(text) as Record<string, any>;
|
||||
const providerName =
|
||||
typeof parsed.model_provider === "string"
|
||||
? parsed.model_provider.trim()
|
||||
: undefined;
|
||||
const providerToken =
|
||||
providerName &&
|
||||
isCustomCodexModelProviderId(providerName) &&
|
||||
parsed.model_providers &&
|
||||
typeof parsed.model_providers === "object" &&
|
||||
typeof parsed.model_providers[providerName]
|
||||
?.experimental_bearer_token === "string"
|
||||
? parsed.model_providers[
|
||||
providerName
|
||||
].experimental_bearer_token.trim()
|
||||
: undefined;
|
||||
if (providerToken) return providerToken;
|
||||
const topLevelToken =
|
||||
typeof parsed.experimental_bearer_token === "string"
|
||||
? parsed.experimental_bearer_token.trim()
|
||||
: undefined;
|
||||
if (topLevelToken) return topLevelToken;
|
||||
} catch {
|
||||
// Fall back to the line scanner for partially edited TOML.
|
||||
}
|
||||
|
||||
const lines = text.split("\n");
|
||||
const targetSectionName = getCodexCustomProviderSectionName(text);
|
||||
|
||||
if (targetSectionName) {
|
||||
const sectionRange = getTomlSectionRange(lines, targetSectionName);
|
||||
if (sectionRange) {
|
||||
const match = findTomlAssignmentInRange(
|
||||
lines,
|
||||
TOML_EXPERIMENTAL_BEARER_TOKEN_PATTERN,
|
||||
sectionRange.bodyStartIndex,
|
||||
sectionRange.bodyEndIndex,
|
||||
targetSectionName,
|
||||
);
|
||||
if (match?.value) {
|
||||
return match.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const topLevelMatch = findTomlAssignmentInRange(
|
||||
lines,
|
||||
TOML_EXPERIMENTAL_BEARER_TOKEN_PATTERN,
|
||||
0,
|
||||
getTopLevelEndIndex(lines),
|
||||
);
|
||||
return topLevelMatch?.value;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
// 同步更新 Codex config.toml 中已有的 experimental_bearer_token
|
||||
// 仅修改已存在的条目, 不主动新增——避免破坏未使用 Mobile 兼容模式的普通 third-party 配置
|
||||
// token 为空时删除该行 (让用户能真正清空 API key, 而不是被 pickCodexApiKey 的 fallback 又填回去)
|
||||
export const updateCodexExperimentalBearerToken = (
|
||||
configText: string,
|
||||
token: string,
|
||||
): string => {
|
||||
const normalizedText = normalizeTomlText(configText);
|
||||
if (
|
||||
!normalizedText ||
|
||||
!normalizedText.includes("experimental_bearer_token")
|
||||
) {
|
||||
return configText;
|
||||
}
|
||||
|
||||
const lines = normalizedText.split("\n");
|
||||
const targetSectionName = getCodexCustomProviderSectionName(normalizedText);
|
||||
|
||||
let tokenLineIndex = -1;
|
||||
if (targetSectionName) {
|
||||
const sectionRange = getTomlSectionRange(lines, targetSectionName);
|
||||
if (sectionRange) {
|
||||
const index = findTomlLineInRange(
|
||||
lines,
|
||||
TOML_EXPERIMENTAL_BEARER_TOKEN_REPLACE_PATTERN,
|
||||
sectionRange.bodyStartIndex,
|
||||
sectionRange.bodyEndIndex,
|
||||
);
|
||||
if (index !== -1) tokenLineIndex = index;
|
||||
}
|
||||
}
|
||||
if (tokenLineIndex === -1) {
|
||||
const topLevelIndex = findTomlLineInRange(
|
||||
lines,
|
||||
TOML_EXPERIMENTAL_BEARER_TOKEN_REPLACE_PATTERN,
|
||||
0,
|
||||
getTopLevelEndIndex(lines),
|
||||
);
|
||||
if (topLevelIndex !== -1) tokenLineIndex = topLevelIndex;
|
||||
}
|
||||
|
||||
if (tokenLineIndex === -1) return configText;
|
||||
|
||||
const trimmed = token.trim();
|
||||
if (!trimmed) {
|
||||
lines.splice(tokenLineIndex, 1);
|
||||
} else {
|
||||
const escaped = escapeTomlBasicString(trimmed);
|
||||
const existingLine = lines[tokenLineIndex];
|
||||
lines[tokenLineIndex] = TOML_EXPERIMENTAL_BEARER_TOKEN_REPLACE_PATTERN.test(
|
||||
existingLine,
|
||||
)
|
||||
? existingLine.replace(
|
||||
TOML_EXPERIMENTAL_BEARER_TOKEN_REPLACE_PATTERN,
|
||||
`$1"${escaped}"$2`,
|
||||
)
|
||||
: `experimental_bearer_token = "${escaped}"`;
|
||||
}
|
||||
return finalizeTomlText(lines);
|
||||
};
|
||||
|
||||
// 从 Provider 对象中提取 Codex base_url(当 settingsConfig.config 为 TOML 字符串时)
|
||||
export const getCodexBaseUrl = (
|
||||
provider: { settingsConfig?: Record<string, any> } | undefined | null,
|
||||
|
||||
Reference in New Issue
Block a user