Files
cc-switch/src/components/providers/forms/hooks/useCodexCommonConfig.ts
T
YoVinchen 2ff329f6c0 refactor(hooks): redesign common config hooks for runtime merge architecture
Refactor all three common config hooks (Claude, Codex, Gemini) to support
the new runtime merge architecture where:
- settingsConfig stores only custom (provider-specific) configuration
- commonConfigSnippet stores shared template configuration in database
- finalConfig is computed at runtime: merge(commonConfig, customConfig)
- Toggling common config only changes enabled state, not settingsConfig

Key changes across all hooks:
- Add finalConfig/finalEnv return value for merge preview
- Use configMerge utilities (computeFinalConfig, extractDifference)
- Remove direct config manipulation on toggle (no more inject/remove)
- Add proper TypeScript return type interfaces
- Simplify state management by removing unnecessary refs
- Improve code organization with clear section comments

This refactor enables:
- Clean separation between custom and common configuration
- Real-time merge preview in the UI
- Consistent behavior across Claude (JSON), Codex (TOML), and Gemini (env)
2026-01-26 14:09:13 +08:00

460 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect, useCallback, useRef, useMemo } from "react";
import { useTranslation } from "react-i18next";
import TOML from "smol-toml";
import { configApi } from "@/lib/api";
import {
computeFinalTomlConfig,
extractTomlDifference,
} from "@/utils/tomlConfigMerge";
import type { ProviderMeta } from "@/types";
const LEGACY_STORAGE_KEY = "cc-switch:codex-common-config-snippet";
const DEFAULT_CODEX_COMMON_CONFIG_SNIPPET = `# Common Codex config
# Add your common TOML configuration here`;
/** TOML 校验错误码 */
export type TomlValidationErrorCode =
| "TOML_SYNTAX_ERROR"
| "TOML_PARSE_FAILED"
| "";
/**
* 校验 TOML 格式
* @param tomlText - 待校验的 TOML 文本
* @returns 错误码,如果校验通过则返回空字符串
*/
function validateTomlFormat(tomlText: string): TomlValidationErrorCode {
// 空字符串或仅包含注释/空行视为合法
const lines = tomlText.split("\n");
const hasContent = lines.some((line) => {
const trimmed = line.trim();
return trimmed && !trimmed.startsWith("#");
});
if (!hasContent) {
return "";
}
try {
TOML.parse(tomlText);
return "";
} catch {
return "TOML_SYNTAX_ERROR";
}
}
interface UseCodexCommonConfigProps {
/**
* 当前 Codex 配置(JSON 格式,包含 auth 和 config
*/
codexConfig: string;
/**
* 配置变化回调
*/
onConfigChange: (config: string) => void;
/**
* 初始数据(编辑模式)
*/
initialData?: {
settingsConfig?: Record<string, unknown>;
meta?: ProviderMeta;
};
/**
* 当前选中的预设 ID
*/
selectedPresetId?: string;
/**
* 当前正在编辑的供应商 ID
*/
currentProviderId?: string;
}
export interface UseCodexCommonConfigReturn {
/** 是否启用通用配置 */
useCommonConfig: boolean;
/** 通用配置片段 (TOML 格式) */
commonConfigSnippet: string;
/** 通用配置错误信息 */
commonConfigError: string;
/** 是否正在加载 */
isLoading: boolean;
/** 是否正在提取 */
isExtracting: boolean;
/** 通用配置开关处理函数 */
handleCommonConfigToggle: (checked: boolean) => void;
/** 通用配置片段变化处理函数 */
handleCommonConfigSnippetChange: (snippet: string) => void;
/** 从当前配置提取通用配置 */
handleExtract: () => Promise<void>;
/** 最终配置(运行时合并结果,只读) */
finalConfig: string;
}
/**
* 管理 Codex 通用配置片段(重构版)
*
* 新架构:
* - codexConfig:传入当前配置(JSON 格式,包含 auth 和 config
* - commonConfigSnippet:存储在数据库中的通用 TOML 配置片段
* - finalConfig:运行时计算 = merge(commonConfig, config)
* - 开启/关闭通用配置只改变 enabled 状态,不修改 codexConfig
*/
export function useCodexCommonConfig({
codexConfig,
onConfigChange,
initialData,
selectedPresetId,
// currentProviderId is reserved for future use
}: UseCodexCommonConfigProps): UseCodexCommonConfigReturn {
const { t } = useTranslation();
// 内部管理的通用配置启用状态
const [useCommonConfig, setUseCommonConfig] = useState(false);
// 通用配置片段(从数据库加载)
const [commonConfigSnippet, setCommonConfigSnippetState] = useState<string>(
DEFAULT_CODEX_COMMON_CONFIG_SNIPPET,
);
const [commonConfigError, setCommonConfigError] = useState("");
const [isLoading, setIsLoading] = useState(true);
const [isExtracting, setIsExtracting] = useState(false);
// 用于避免异步保存乱序
const saveSequenceRef = useRef(0);
const saveQueueRef = useRef<Promise<void>>(Promise.resolve());
const enqueueSave = useCallback((saveFn: () => Promise<void>) => {
const next = saveQueueRef.current.then(saveFn);
saveQueueRef.current = next.catch(() => {});
return next;
}, []);
// 用于跟踪编辑模式是否已初始化
const hasInitializedEditMode = useRef(false);
// 用于跟踪新建模式是否已初始化
const hasInitializedNewMode = useRef(false);
// 检查 TOML 片段是否有实质内容
const hasSnippetContent = useCallback((snippet: string) => {
const lines = snippet.split("\n");
return lines.some((line) => {
const trimmed = line.trim();
return trimmed && !trimmed.startsWith("#");
});
}, []);
// 从 codexConfig 提取 config 字段(TOML 格式)
// 注意:codexConfig 可能是以下两种格式之一:
// 1. 直接的 TOML 字符串(来自 useCodexConfigState
// 2. JSON 字符串 { auth: {...}, config: "..." }(旧的 settingsConfig 格式)
const extractConfigToml = useCallback((configInput: string): string => {
if (!configInput || !configInput.trim()) {
return "";
}
// 尝试解析为 JSON(旧格式)
try {
const parsed = JSON.parse(configInput);
if (typeof parsed?.config === "string") {
return parsed.config;
}
// 如果是 JSON 对象但没有 config 字段,返回空
if (typeof parsed === "object" && parsed !== null) {
return "";
}
} catch {
// JSON 解析失败,说明是直接的 TOML 字符串
}
// 直接返回作为 TOML 字符串
return configInput;
}, []);
// 当预设变化时,重置初始化标记
useEffect(() => {
hasInitializedNewMode.current = false;
hasInitializedEditMode.current = false;
}, [selectedPresetId]);
// ============================================================================
// 加载通用配置片段(从数据库,支持 localStorage 迁移)
// ============================================================================
useEffect(() => {
let mounted = true;
const loadSnippet = async () => {
try {
const snippet = await configApi.getCommonConfigSnippet("codex");
if (snippet && snippet.trim()) {
if (mounted) {
setCommonConfigSnippetState(snippet);
}
} else {
// 尝试从 localStorage 迁移
if (typeof window !== "undefined") {
try {
const legacySnippet =
window.localStorage.getItem(LEGACY_STORAGE_KEY);
if (legacySnippet && legacySnippet.trim()) {
const tomlError = validateTomlFormat(legacySnippet);
if (!tomlError) {
await configApi.setCommonConfigSnippet(
"codex",
legacySnippet,
);
if (mounted) {
setCommonConfigSnippetState(legacySnippet);
}
window.localStorage.removeItem(LEGACY_STORAGE_KEY);
console.log(
"[迁移] Codex 通用配置已从 localStorage 迁移到数据库",
);
}
}
} catch (e) {
console.warn("[迁移] 从 localStorage 迁移失败:", e);
}
}
}
} catch (error) {
console.error("加载 Codex 通用配置失败:", error);
} finally {
if (mounted) {
setIsLoading(false);
}
}
};
loadSnippet();
return () => {
mounted = false;
};
}, []);
// ============================================================================
// 编辑模式初始化:从 meta 读取启用状态
// ============================================================================
useEffect(() => {
if (initialData && !isLoading && !hasInitializedEditMode.current) {
hasInitializedEditMode.current = true;
const metaByApp = initialData.meta?.commonConfigEnabledByApp;
const resolvedMetaEnabled =
metaByApp?.codex ?? initialData.meta?.commonConfigEnabled;
if (resolvedMetaEnabled !== undefined) {
if (!resolvedMetaEnabled) {
setUseCommonConfig(false);
return;
}
if (!hasSnippetContent(commonConfigSnippet)) {
setCommonConfigError(t("codexConfig.noCommonConfigToApply"));
setUseCommonConfig(false);
return;
}
const tomlError = validateTomlFormat(commonConfigSnippet);
if (tomlError) {
setCommonConfigError(
t("mcp.error.tomlInvalid", { defaultValue: "TOML 格式错误" }),
);
setUseCommonConfig(false);
return;
}
setCommonConfigError("");
setUseCommonConfig(true);
} else {
setUseCommonConfig(false);
}
}
}, [initialData, isLoading, commonConfigSnippet, hasSnippetContent, t]);
// ============================================================================
// 新建模式初始化:如果通用配置有效,默认启用
// ============================================================================
useEffect(() => {
if (!initialData && !isLoading && !hasInitializedNewMode.current) {
hasInitializedNewMode.current = true;
if (hasSnippetContent(commonConfigSnippet)) {
const tomlError = validateTomlFormat(commonConfigSnippet);
if (!tomlError) {
setUseCommonConfig(true);
}
}
}
}, [initialData, commonConfigSnippet, isLoading, hasSnippetContent]);
// ============================================================================
// 计算最终配置(运行时合并)
// ============================================================================
const finalConfig = useMemo((): string => {
const customToml = extractConfigToml(codexConfig);
if (!useCommonConfig) {
return customToml;
}
const result = computeFinalTomlConfig(
customToml,
commonConfigSnippet,
true,
);
if (result.error) {
// 合并失败时返回原配置
return customToml;
}
return result.finalConfig;
}, [codexConfig, commonConfigSnippet, useCommonConfig, extractConfigToml]);
// ============================================================================
// 处理通用配置开关
// ============================================================================
const handleCommonConfigToggle = useCallback(
(checked: boolean) => {
if (checked) {
if (!hasSnippetContent(commonConfigSnippet)) {
setCommonConfigError(t("codexConfig.noCommonConfigToApply"));
setUseCommonConfig(false);
return;
}
const tomlError = validateTomlFormat(commonConfigSnippet);
if (tomlError) {
setCommonConfigError(
t("mcp.error.tomlInvalid", { defaultValue: "TOML 格式错误" }),
);
setUseCommonConfig(false);
return;
}
}
setCommonConfigError("");
setUseCommonConfig(checked);
// 新架构:不修改 codexConfig,只改变 enabled 状态
},
[commonConfigSnippet, hasSnippetContent, t],
);
// ============================================================================
// 处理通用配置片段变化
// ============================================================================
const handleCommonConfigSnippetChange = useCallback(
(value: string) => {
setCommonConfigSnippetState(value);
if (!value.trim()) {
const saveId = ++saveSequenceRef.current;
setCommonConfigError("");
enqueueSave(() => configApi.setCommonConfigSnippet("codex", "")).catch(
(error) => {
if (saveSequenceRef.current !== saveId) return;
console.error("保存 Codex 通用配置失败:", error);
setCommonConfigError(
t("codexConfig.saveFailed", { error: String(error) }),
);
},
);
return;
}
// TOML 格式校验
const tomlError = validateTomlFormat(value);
if (tomlError) {
setCommonConfigError(
t("mcp.error.tomlInvalid", {
defaultValue: "TOML 格式错误,请检查语法",
}),
);
return;
}
const saveId = ++saveSequenceRef.current;
setCommonConfigError("");
enqueueSave(() => configApi.setCommonConfigSnippet("codex", value)).catch(
(error) => {
if (saveSequenceRef.current !== saveId) return;
console.error("保存 Codex 通用配置失败:", error);
setCommonConfigError(
t("codexConfig.saveFailed", { error: String(error) }),
);
},
);
// 注意:新架构下不再需要同步到其他供应商的 settingsConfig
// 因为 finalConfig 是运行时计算的
},
[enqueueSave, t],
);
// ============================================================================
// 从当前最终配置提取通用配置片段
// ============================================================================
const handleExtract = useCallback(async () => {
setIsExtracting(true);
setCommonConfigError("");
try {
const extracted = await configApi.extractCommonConfigSnippet("codex", {
settingsConfig: JSON.stringify({
config: finalConfig ?? "",
}),
});
if (!extracted || !extracted.trim()) {
setCommonConfigError(t("codexConfig.extractNoCommonConfig"));
return;
}
// 验证 TOML 格式
const tomlError = validateTomlFormat(extracted);
if (tomlError) {
setCommonConfigError(
t("mcp.error.tomlInvalid", {
defaultValue: "TOML 格式错误,请检查语法",
}),
);
return;
}
// 更新片段状态
setCommonConfigSnippetState(extracted);
// 保存到后端
await configApi.setCommonConfigSnippet("codex", extracted);
// 提取成功后,从 config 中移除与 extracted 相同的部分
const customToml = extractConfigToml(codexConfig);
const diffResult = extractTomlDifference(customToml, extracted);
if (!diffResult.error) {
// 更新 codexConfig 中的 config 字段
try {
const parsed = JSON.parse(codexConfig);
parsed.config = diffResult.customToml;
onConfigChange(JSON.stringify(parsed, null, 2));
} catch {
// 忽略解析错误
}
}
} catch (error) {
console.error("提取 Codex 通用配置失败:", error);
setCommonConfigError(
t("codexConfig.extractFailed", { error: String(error) }),
);
} finally {
setIsExtracting(false);
}
}, [finalConfig, codexConfig, onConfigChange, extractConfigToml, t]);
return {
useCommonConfig,
commonConfigSnippet,
commonConfigError,
isLoading,
isExtracting,
handleCommonConfigToggle,
handleCommonConfigSnippetChange,
handleExtract,
finalConfig,
};
}