mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-08-04 11:43:57 +08:00
fix(utils): add type validation and error codes for config utilities
- Add isPlainObject validation in replaceGeminiCommonConfigSnippet - Return error codes instead of hardcoded Chinese strings - Add CONFIG_NOT_OBJECT, ENV_NOT_OBJECT, COMMON_CONFIG_JSON_INVALID codes - Use TOML marker constants to avoid hardcoded strings
This commit is contained in:
@@ -3,6 +3,14 @@
|
|||||||
import type { TemplateValueConfig } from "../config/claudeProviderPresets";
|
import type { TemplateValueConfig } from "../config/claudeProviderPresets";
|
||||||
import { normalizeQuotes } from "@/utils/textNormalization";
|
import { normalizeQuotes } from "@/utils/textNormalization";
|
||||||
|
|
||||||
|
// Gemini 通用配置禁止的键(共享常量,供 hook 和同步逻辑复用)
|
||||||
|
export const GEMINI_COMMON_ENV_FORBIDDEN_KEYS = [
|
||||||
|
"GOOGLE_GEMINI_BASE_URL",
|
||||||
|
"GEMINI_API_KEY",
|
||||||
|
] as const;
|
||||||
|
export type GeminiForbiddenEnvKey =
|
||||||
|
(typeof GEMINI_COMMON_ENV_FORBIDDEN_KEYS)[number];
|
||||||
|
|
||||||
const isPlainObject = (value: unknown): value is Record<string, any> => {
|
const isPlainObject = (value: unknown): value is Record<string, any> => {
|
||||||
return Object.prototype.toString.call(value) === "[object Object]";
|
return Object.prototype.toString.call(value) === "[object Object]";
|
||||||
};
|
};
|
||||||
@@ -165,6 +173,164 @@ export const hasCommonConfigSnippet = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 检查 Gemini 配置是否已包含通用配置片段(env JSON)
|
||||||
|
export const hasGeminiCommonConfigSnippet = (
|
||||||
|
jsonString: string,
|
||||||
|
snippetString: string,
|
||||||
|
): boolean => {
|
||||||
|
try {
|
||||||
|
if (!snippetString.trim()) return false;
|
||||||
|
const config = jsonString ? JSON.parse(jsonString) : {};
|
||||||
|
if (!isPlainObject(config)) return false;
|
||||||
|
const envValue = (config as Record<string, unknown>).env;
|
||||||
|
if (envValue !== undefined && !isPlainObject(envValue)) return false;
|
||||||
|
const env = (isPlainObject(envValue) ? envValue : {}) as Record<
|
||||||
|
string,
|
||||||
|
unknown
|
||||||
|
>;
|
||||||
|
|
||||||
|
const parsed = JSON.parse(snippetString);
|
||||||
|
if (!isPlainObject(parsed)) return false;
|
||||||
|
|
||||||
|
const entries = Object.entries(parsed).filter(([key, value]) => {
|
||||||
|
if (
|
||||||
|
GEMINI_COMMON_ENV_FORBIDDEN_KEYS.includes(
|
||||||
|
key as GeminiForbiddenEnvKey,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (typeof value !== "string") return false;
|
||||||
|
return value.trim().length > 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (entries.length === 0) return false;
|
||||||
|
|
||||||
|
return entries.every(([key, value]) => {
|
||||||
|
const current = env[key];
|
||||||
|
return typeof current === "string" && current === value.trim();
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 替换通用配置片段(用于同步更新)
|
||||||
|
* 先移除旧的通用配置,再添加新的通用配置
|
||||||
|
*/
|
||||||
|
export const replaceCommonConfigSnippet = (
|
||||||
|
jsonString: string,
|
||||||
|
oldSnippet: string,
|
||||||
|
newSnippet: string,
|
||||||
|
): UpdateCommonConfigResult => {
|
||||||
|
// 先移除旧的通用配置
|
||||||
|
const removeResult = updateCommonConfigSnippet(jsonString, oldSnippet, false);
|
||||||
|
if (removeResult.error) {
|
||||||
|
return removeResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 再添加新的通用配置
|
||||||
|
return updateCommonConfigSnippet(
|
||||||
|
removeResult.updatedConfig,
|
||||||
|
newSnippet,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 替换 Gemini 通用配置片段(用于同步更新)
|
||||||
|
* Gemini 的通用配置是 JSON 格式的 env 对象
|
||||||
|
*/
|
||||||
|
export const replaceGeminiCommonConfigSnippet = (
|
||||||
|
jsonString: string,
|
||||||
|
oldSnippet: string,
|
||||||
|
newSnippet: string,
|
||||||
|
): UpdateCommonConfigResult => {
|
||||||
|
try {
|
||||||
|
const config = jsonString ? JSON.parse(jsonString) : {};
|
||||||
|
|
||||||
|
// 校验 config 是对象类型
|
||||||
|
if (!isPlainObject(config)) {
|
||||||
|
return {
|
||||||
|
updatedConfig: jsonString,
|
||||||
|
error: "CONFIG_NOT_OBJECT",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const env = config.env ?? {};
|
||||||
|
|
||||||
|
// 校验 env 是对象类型
|
||||||
|
if (!isPlainObject(env)) {
|
||||||
|
return {
|
||||||
|
updatedConfig: jsonString,
|
||||||
|
error: "ENV_NOT_OBJECT",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析旧的通用配置
|
||||||
|
let oldEnv: Record<string, string> = {};
|
||||||
|
if (oldSnippet.trim()) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(oldSnippet);
|
||||||
|
if (isPlainObject(parsed)) {
|
||||||
|
oldEnv = parsed as Record<string, string>;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore parse error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析新的通用配置
|
||||||
|
let newEnv: Record<string, string> = {};
|
||||||
|
if (newSnippet.trim()) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(newSnippet);
|
||||||
|
if (isPlainObject(parsed)) {
|
||||||
|
// 过滤掉禁止的键
|
||||||
|
for (const [key, value] of Object.entries(parsed)) {
|
||||||
|
if (
|
||||||
|
typeof value === "string" &&
|
||||||
|
!GEMINI_COMMON_ENV_FORBIDDEN_KEYS.includes(
|
||||||
|
key as GeminiForbiddenEnvKey,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
newEnv[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return {
|
||||||
|
updatedConfig: jsonString,
|
||||||
|
error: "COMMON_CONFIG_JSON_INVALID",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除旧的通用配置键值对
|
||||||
|
const updatedEnv = { ...env };
|
||||||
|
for (const [key, value] of Object.entries(oldEnv)) {
|
||||||
|
if (updatedEnv[key] === value) {
|
||||||
|
delete updatedEnv[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加新的通用配置键值对
|
||||||
|
for (const [key, value] of Object.entries(newEnv)) {
|
||||||
|
updatedEnv[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
updatedConfig: JSON.stringify({ ...config, env: updatedEnv }, null, 2),
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
updatedConfig: jsonString,
|
||||||
|
error: "CONFIG_JSON_PARSE_FAILED",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 读取配置中的 API Key(支持 Claude, Codex, Gemini)
|
// 读取配置中的 API Key(支持 Claude, Codex, Gemini)
|
||||||
export const getApiKeyFromConfig = (
|
export const getApiKeyFromConfig = (
|
||||||
jsonString: string,
|
jsonString: string,
|
||||||
@@ -336,8 +502,19 @@ export interface UpdateTomlCommonConfigResult {
|
|||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存之前的通用配置片段,用于替换操作
|
const TOML_COMMON_CONFIG_START = "# cc-switch:common-config:start";
|
||||||
let previousCommonSnippet = "";
|
const TOML_COMMON_CONFIG_END = "# cc-switch:common-config:end";
|
||||||
|
|
||||||
|
const stripTomlCommonConfigBlock = (tomlString: string): string => {
|
||||||
|
const startIndex = tomlString.indexOf(TOML_COMMON_CONFIG_START);
|
||||||
|
const endIndex = tomlString.indexOf(TOML_COMMON_CONFIG_END);
|
||||||
|
if (startIndex === -1 || endIndex === -1 || endIndex < startIndex) {
|
||||||
|
return tomlString;
|
||||||
|
}
|
||||||
|
const before = tomlString.slice(0, startIndex);
|
||||||
|
const after = tomlString.slice(endIndex + TOML_COMMON_CONFIG_END.length);
|
||||||
|
return `${before}${after}`;
|
||||||
|
};
|
||||||
|
|
||||||
// 将通用配置片段写入/移除 TOML 配置
|
// 将通用配置片段写入/移除 TOML 配置
|
||||||
export const updateTomlCommonConfigSnippet = (
|
export const updateTomlCommonConfigSnippet = (
|
||||||
@@ -345,43 +522,49 @@ export const updateTomlCommonConfigSnippet = (
|
|||||||
snippetString: string,
|
snippetString: string,
|
||||||
enabled: boolean,
|
enabled: boolean,
|
||||||
): UpdateTomlCommonConfigResult => {
|
): UpdateTomlCommonConfigResult => {
|
||||||
if (!snippetString.trim()) {
|
|
||||||
// 如果片段为空,直接返回原始配置
|
|
||||||
return {
|
|
||||||
updatedConfig: tomlString,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
|
if (!snippetString.trim()) {
|
||||||
|
// 如果片段为空,直接返回原始配置
|
||||||
|
return {
|
||||||
|
updatedConfig: tomlString,
|
||||||
|
};
|
||||||
|
}
|
||||||
// 添加通用配置
|
// 添加通用配置
|
||||||
// 先移除旧的通用配置(如果有)
|
// 先移除旧的通用配置(如果有)
|
||||||
let updatedConfig = tomlString;
|
let updatedConfig = stripTomlCommonConfigBlock(tomlString);
|
||||||
if (previousCommonSnippet && tomlString.includes(previousCommonSnippet)) {
|
const trimmedSnippet = snippetString.trim();
|
||||||
updatedConfig = tomlString.replace(previousCommonSnippet, "");
|
if (updatedConfig.includes(trimmedSnippet)) {
|
||||||
|
updatedConfig = updatedConfig.replace(trimmedSnippet, "");
|
||||||
|
updatedConfig = updatedConfig.replace(/\n{3,}/g, "\n\n").trim();
|
||||||
|
if (updatedConfig) {
|
||||||
|
updatedConfig += "\n";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const block = `${TOML_COMMON_CONFIG_START}\n${trimmedSnippet}\n${TOML_COMMON_CONFIG_END}`;
|
||||||
|
|
||||||
// 在文件末尾添加新的通用配置
|
// 在文件末尾添加新的通用配置
|
||||||
// 确保有适当的换行
|
// 确保有适当的换行
|
||||||
const needsNewline = updatedConfig && !updatedConfig.endsWith("\n");
|
const needsNewline = updatedConfig && !updatedConfig.endsWith("\n");
|
||||||
updatedConfig =
|
updatedConfig = updatedConfig + (needsNewline ? "\n\n" : "\n") + block;
|
||||||
updatedConfig + (needsNewline ? "\n\n" : "\n") + snippetString;
|
|
||||||
|
|
||||||
// 保存当前通用配置片段
|
|
||||||
previousCommonSnippet = snippetString;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
updatedConfig: updatedConfig.trim() + "\n",
|
updatedConfig: updatedConfig.trim() + "\n",
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
// 移除通用配置
|
// 移除通用配置
|
||||||
if (tomlString.includes(snippetString)) {
|
if (tomlString.includes(TOML_COMMON_CONFIG_START)) {
|
||||||
const updatedConfig = tomlString.replace(snippetString, "");
|
const updatedConfig = stripTomlCommonConfigBlock(tomlString);
|
||||||
|
const cleaned = updatedConfig.replace(/\n{3,}/g, "\n\n").trim();
|
||||||
|
return {
|
||||||
|
updatedConfig: cleaned ? cleaned + "\n" : "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (snippetString.trim() && tomlString.includes(snippetString.trim())) {
|
||||||
|
const updatedConfig = tomlString.replace(snippetString.trim(), "");
|
||||||
// 清理多余的空行
|
// 清理多余的空行
|
||||||
const cleaned = updatedConfig.replace(/\n{3,}/g, "\n\n").trim();
|
const cleaned = updatedConfig.replace(/\n{3,}/g, "\n\n").trim();
|
||||||
|
|
||||||
// 清空保存的状态
|
|
||||||
previousCommonSnippet = "";
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
updatedConfig: cleaned ? cleaned + "\n" : "",
|
updatedConfig: cleaned ? cleaned + "\n" : "",
|
||||||
};
|
};
|
||||||
@@ -398,6 +581,12 @@ export const hasTomlCommonConfigSnippet = (
|
|||||||
snippetString: string,
|
snippetString: string,
|
||||||
): boolean => {
|
): boolean => {
|
||||||
if (!snippetString.trim()) return false;
|
if (!snippetString.trim()) return false;
|
||||||
|
if (
|
||||||
|
tomlString.includes(TOML_COMMON_CONFIG_START) &&
|
||||||
|
tomlString.includes(TOML_COMMON_CONFIG_END)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// 简单检查配置是否包含片段内容
|
// 简单检查配置是否包含片段内容
|
||||||
// 去除空白字符后比较,避免格式差异影响
|
// 去除空白字符后比较,避免格式差异影响
|
||||||
@@ -408,6 +597,47 @@ export const hasTomlCommonConfigSnippet = (
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 替换 TOML 通用配置片段(用于同步更新)
|
||||||
|
* 先移除旧的通用配置,再添加新的通用配置
|
||||||
|
* 注意:优先使用 marker 块移除,若 marker 不存在则回退到 oldSnippet 内容匹配
|
||||||
|
*/
|
||||||
|
export const replaceTomlCommonConfigSnippet = (
|
||||||
|
tomlString: string,
|
||||||
|
oldSnippet: string,
|
||||||
|
newSnippet: string,
|
||||||
|
): UpdateTomlCommonConfigResult => {
|
||||||
|
// 检查是否有 marker 块(使用常量避免硬编码重复)
|
||||||
|
const hasMarker =
|
||||||
|
tomlString.includes(TOML_COMMON_CONFIG_START) &&
|
||||||
|
tomlString.includes(TOML_COMMON_CONFIG_END);
|
||||||
|
|
||||||
|
let configWithoutOld = tomlString;
|
||||||
|
|
||||||
|
if (hasMarker) {
|
||||||
|
// 有 marker,使用标准移除逻辑
|
||||||
|
const removeResult = updateTomlCommonConfigSnippet(tomlString, "", false);
|
||||||
|
if (removeResult.error) {
|
||||||
|
return removeResult;
|
||||||
|
}
|
||||||
|
configWithoutOld = removeResult.updatedConfig;
|
||||||
|
} else if (oldSnippet.trim()) {
|
||||||
|
// 无 marker,回退到 oldSnippet 内容匹配移除
|
||||||
|
// 尝试直接匹配并移除旧片段
|
||||||
|
if (tomlString.includes(oldSnippet.trim())) {
|
||||||
|
configWithoutOld = tomlString.replace(oldSnippet.trim(), "");
|
||||||
|
// 清理多余的空行
|
||||||
|
configWithoutOld = configWithoutOld.replace(/\n{3,}/g, "\n\n").trim();
|
||||||
|
if (configWithoutOld) {
|
||||||
|
configWithoutOld += "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加新的通用配置
|
||||||
|
return updateTomlCommonConfigSnippet(configWithoutOld, newSnippet, true);
|
||||||
|
};
|
||||||
|
|
||||||
// ========== Codex base_url utils ==========
|
// ========== Codex base_url utils ==========
|
||||||
|
|
||||||
// 从 Codex 的 TOML 配置文本中提取 base_url(支持单/双引号)
|
// 从 Codex 的 TOML 配置文本中提取 base_url(支持单/双引号)
|
||||||
|
|||||||
Reference in New Issue
Block a user