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:
YoVinchen
2026-01-25 03:39:02 +08:00
Unverified
parent b692bb4053
commit 788e138ece
+252 -22
View File
@@ -3,6 +3,14 @@
import type { TemplateValueConfig } from "../config/claudeProviderPresets";
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> => {
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
export const getApiKeyFromConfig = (
jsonString: string,
@@ -336,8 +502,19 @@ export interface UpdateTomlCommonConfigResult {
error?: string;
}
// 保存之前的通用配置片段,用于替换操作
let previousCommonSnippet = "";
const TOML_COMMON_CONFIG_START = "# cc-switch:common-config:start";
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 配置
export const updateTomlCommonConfigSnippet = (
@@ -345,43 +522,49 @@ export const updateTomlCommonConfigSnippet = (
snippetString: string,
enabled: boolean,
): UpdateTomlCommonConfigResult => {
if (!snippetString.trim()) {
// 如果片段为空,直接返回原始配置
return {
updatedConfig: tomlString,
};
}
if (enabled) {
if (!snippetString.trim()) {
// 如果片段为空,直接返回原始配置
return {
updatedConfig: tomlString,
};
}
// 添加通用配置
// 先移除旧的通用配置(如果有)
let updatedConfig = tomlString;
if (previousCommonSnippet && tomlString.includes(previousCommonSnippet)) {
updatedConfig = tomlString.replace(previousCommonSnippet, "");
let updatedConfig = stripTomlCommonConfigBlock(tomlString);
const trimmedSnippet = snippetString.trim();
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");
updatedConfig =
updatedConfig + (needsNewline ? "\n\n" : "\n") + snippetString;
// 保存当前通用配置片段
previousCommonSnippet = snippetString;
updatedConfig = updatedConfig + (needsNewline ? "\n\n" : "\n") + block;
return {
updatedConfig: updatedConfig.trim() + "\n",
};
} else {
// 移除通用配置
if (tomlString.includes(snippetString)) {
const updatedConfig = tomlString.replace(snippetString, "");
if (tomlString.includes(TOML_COMMON_CONFIG_START)) {
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();
// 清空保存的状态
previousCommonSnippet = "";
return {
updatedConfig: cleaned ? cleaned + "\n" : "",
};
@@ -398,6 +581,12 @@ export const hasTomlCommonConfigSnippet = (
snippetString: string,
): boolean => {
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 的 TOML 配置文本中提取 base_url(支持单/双引号)