Files
cc-switch/src/components/providers/forms/hooks/useCodexCommonConfig.ts
T
YoVinchen 0ef8c127c5 fix(common-config): improve error handling, layer separation, and reliability across sync system
P0 Fixes:
- Enable toml `preserve_order` feature for reliable key ordering
- Extract pure detection functions to utils/commonConfigDetection.ts (fix layer inversion)
- Return structured result from preserveCodexConfigFormat to prevent data loss

P1 Fixes:
- Handle pure TOML strings in Codex config instead of silent skip
- Surface extractDiff errors in useCodexCommonConfig instead of swallowing
- Log parse errors in detectCommonConfigEnabledByContent for debugging

P2 Fixes:
- Use parsing-based assertions in Rust TOML ordering tests
- Add schema warning when Codex config field is wrong type
- Add depth/node limits to isSubset function (prevent stack overflow)
- Replace hardcoded "{}" check with adapter.hasValidContent()

P3 Fixes:
- Refactor Gemini errors to structured GeminiConfigErrorInfo format
- Add mapGeminiErrorToI18n for type-safe error-to-i18n mapping
2026-01-31 20:39:28 +08:00

196 lines
6.0 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, useCallback } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import { configApi } from "@/lib/api";
import {
useCommonConfigBase,
type UseCommonConfigBaseReturn,
} from "@/hooks/useCommonConfigBase";
import {
codexAdapter,
preserveCodexConfigFormat,
} from "@/hooks/commonConfigAdapters";
import { extractTomlDifference } from "@/utils/tomlConfigMerge";
import type { ProviderMeta } from "@/types";
interface UseCodexCommonConfigProps {
/**
* 当前 Codex 配置(可能是纯 TOML 或 JSON wrapper 格式)
*/
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;
/** 是否有待保存的通用配置变更 */
hasUnsavedCommonConfig: boolean;
/** 获取待保存的通用配置片段(用于 handleSubmit */
getPendingCommonConfigSnippet: () => string | null;
/** 标记通用配置已保存 */
markCommonConfigSaved: () => void;
}
/**
* 管理 Codex 通用配置片段
*
* 基于 useCommonConfigBase 泛型 Hook + Codex TOML 适配器实现。
* 额外处理 Codex 双格式(纯 TOML / JSON wrapper)的写回逻辑。
*/
export function useCodexCommonConfig({
codexConfig,
onConfigChange,
initialData,
selectedPresetId,
}: UseCodexCommonConfigProps): UseCodexCommonConfigReturn {
const { t } = useTranslation();
// 额外的 isExtracting 状态(base hook 的 handleExtract 不适用于 Codex
const [localIsExtracting, setLocalIsExtracting] = useState(false);
const [localExtractError, setLocalExtractError] = useState("");
const base: UseCommonConfigBaseReturn<string> = useCommonConfigBase({
adapter: codexAdapter,
inputValue: codexConfig,
onInputChange: onConfigChange,
initialData,
selectedPresetId,
});
// Codex 自定义 handleExtract:处理双格式写回 + 状态管理
const handleExtract = useCallback(async () => {
setLocalIsExtracting(true);
setLocalExtractError("");
try {
const request = codexAdapter.buildExtractRequest(base.finalValue);
const extracted = await configApi.extractCommonConfigSnippet(
"codex",
request,
);
if (!extracted || !extracted.trim()) {
setLocalExtractError(t("codexConfig.extractNoCommonConfig"));
return;
}
// 验证 TOML 格式
const parseResult = codexAdapter.parseSnippet(extracted);
if (parseResult.error || parseResult.config === null) {
setLocalExtractError(
t("codexConfig.extractedTomlInvalid", {
defaultValue: "提取的配置 TOML 格式错误",
}),
);
return;
}
// 从 config 中移除与 extracted 相同的部分
const customToml = codexAdapter.parseInput(codexConfig);
const diffResult = extractTomlDifference(customToml, extracted);
if (diffResult.error) {
// 差异提取失败,显示错误而不是静默吞掉
setLocalExtractError(
t("codexConfig.extractDiffFailed", {
error: diffResult.error,
defaultValue: `差异提取失败: ${diffResult.error}`,
}),
);
return;
}
// 更新 snippet 状态(在差异提取成功后再更新,避免状态不一致)
base.handleCommonConfigSnippetChange(extracted);
// 使用共享的格式保留函数写回
const preserveResult = preserveCodexConfigFormat(
codexConfig,
diffResult.customToml,
);
if (preserveResult.error) {
// 格式保留失败,显示错误
setLocalExtractError(
t("codexConfig.preserveFormatFailed", {
error: preserveResult.error,
defaultValue: `配置格式保留失败: ${preserveResult.error}`,
}),
);
return;
}
onConfigChange(preserveResult.config);
toast.success(
t("codexConfig.extractSuccessNeedSave", {
defaultValue: "已提取通用配置,点击保存按钮完成保存",
}),
);
} catch (error) {
console.error("提取 Codex 通用配置失败:", error);
setLocalExtractError(
t("codexConfig.extractFailed", {
error: String(error),
defaultValue: "提取失败",
}),
);
} finally {
setLocalIsExtracting(false);
}
}, [base, codexConfig, onConfigChange, t]);
// 合并 error:优先显示 extract 错误,其次是 base 的错误
const combinedError = localExtractError || base.commonConfigError;
return {
useCommonConfig: base.useCommonConfig,
commonConfigSnippet: base.commonConfigSnippet,
commonConfigError: combinedError,
isLoading: base.isLoading,
isExtracting: localIsExtracting,
handleCommonConfigToggle: base.handleCommonConfigToggle,
handleCommonConfigSnippetChange: base.handleCommonConfigSnippetChange,
handleExtract,
finalConfig: base.finalValue,
hasUnsavedCommonConfig: base.hasUnsavedCommonConfig,
getPendingCommonConfigSnippet: base.getPendingCommonConfigSnippet,
markCommonConfigSaved: base.markCommonConfigSaved,
};
}