mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-27 16:26:16 +08:00
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)
This commit is contained in:
@@ -6,9 +6,11 @@ export { useCodexConfigState } from "./useCodexConfigState";
|
||||
export { useApiKeyLink } from "./useApiKeyLink";
|
||||
export { useCustomEndpoints } from "./useCustomEndpoints";
|
||||
export { useTemplateValues } from "./useTemplateValues";
|
||||
export { useCommonConfigSnippet } from "./useCommonConfigSnippet";
|
||||
export { useCodexCommonConfig } from "./useCodexCommonConfig";
|
||||
export { useSpeedTestEndpoints } from "./useSpeedTestEndpoints";
|
||||
export { useCodexTomlValidation } from "./useCodexTomlValidation";
|
||||
export { useGeminiConfigState } from "./useGeminiConfigState";
|
||||
|
||||
// Common Config Hooks
|
||||
export { useCommonConfigSnippet } from "./useCommonConfigSnippet";
|
||||
export { useCodexCommonConfig } from "./useCodexCommonConfig";
|
||||
export { useGeminiCommonConfig } from "./useGeminiCommonConfig";
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { useState, useEffect, useCallback, useRef } from "react";
|
||||
import { useState, useEffect, useCallback, useRef, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
import TOML from "smol-toml";
|
||||
import {
|
||||
updateTomlCommonConfigSnippet,
|
||||
hasTomlCommonConfigSnippet,
|
||||
replaceTomlCommonConfigSnippet,
|
||||
} from "@/utils/providerConfigUtils";
|
||||
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";
|
||||
@@ -45,30 +43,74 @@ function validateTomlFormat(tomlText: string): TomlValidationErrorCode {
|
||||
}
|
||||
|
||||
interface UseCodexCommonConfigProps {
|
||||
/**
|
||||
* 当前 Codex 配置(JSON 格式,包含 auth 和 config)
|
||||
*/
|
||||
codexConfig: string;
|
||||
/**
|
||||
* 配置变化回调
|
||||
*/
|
||||
onConfigChange: (config: string) => void;
|
||||
/**
|
||||
* 初始数据(编辑模式)
|
||||
*/
|
||||
initialData?: {
|
||||
settingsConfig?: Record<string, unknown>;
|
||||
meta?: ProviderMeta;
|
||||
};
|
||||
/**
|
||||
* 当前选中的预设 ID
|
||||
*/
|
||||
selectedPresetId?: string;
|
||||
/** 当前正在编辑的供应商 ID(用于同步时跳过) */
|
||||
/**
|
||||
* 当前正在编辑的供应商 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 通用配置片段 (TOML 格式)
|
||||
* 从数据库读取和保存,支持从 localStorage 平滑迁移
|
||||
* 管理 Codex 通用配置片段(重构版)
|
||||
*
|
||||
* 新架构:
|
||||
* - codexConfig:传入当前配置(JSON 格式,包含 auth 和 config)
|
||||
* - commonConfigSnippet:存储在数据库中的通用 TOML 配置片段
|
||||
* - finalConfig:运行时计算 = merge(commonConfig, config)
|
||||
* - 开启/关闭通用配置只改变 enabled 状态,不修改 codexConfig
|
||||
*/
|
||||
export function useCodexCommonConfig({
|
||||
codexConfig,
|
||||
onConfigChange,
|
||||
initialData,
|
||||
selectedPresetId,
|
||||
currentProviderId,
|
||||
}: UseCodexCommonConfigProps) {
|
||||
// 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,
|
||||
);
|
||||
@@ -76,40 +118,7 @@ export function useCodexCommonConfig({
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isExtracting, setIsExtracting] = useState(false);
|
||||
|
||||
const hasSnippetContent = useCallback((snippet: string) => {
|
||||
const lines = snippet.split("\n");
|
||||
return lines.some((line) => {
|
||||
const trimmed = line.trim();
|
||||
return trimmed && !trimmed.startsWith("#");
|
||||
});
|
||||
}, []);
|
||||
|
||||
const getSnippetApplyError = useCallback(
|
||||
(snippet: string) => {
|
||||
if (!hasSnippetContent(snippet)) {
|
||||
return t("codexConfig.noCommonConfigToApply");
|
||||
}
|
||||
// 校验 TOML 语法
|
||||
const tomlError = validateTomlFormat(snippet);
|
||||
if (tomlError) {
|
||||
return t("mcp.error.tomlInvalid", {
|
||||
defaultValue: "TOML 格式错误,请检查语法",
|
||||
});
|
||||
}
|
||||
return "";
|
||||
},
|
||||
[hasSnippetContent, t],
|
||||
);
|
||||
|
||||
// 用于跟踪是否正在通过通用配置更新
|
||||
const isUpdatingFromCommonConfig = useRef(false);
|
||||
// 用于跟踪用户是否手动切换,避免自动检测覆盖用户意图
|
||||
const hasUserToggledCommonConfig = useRef(false);
|
||||
// 用于跟踪新建模式是否已初始化默认勾选
|
||||
const hasInitializedNewMode = useRef(false);
|
||||
// 用于跟踪编辑模式是否已初始化(避免反复覆盖用户切换)
|
||||
const hasInitializedEditMode = useRef(false);
|
||||
// 用于避免异步保存乱序导致的过期同步
|
||||
// 用于避免异步保存乱序
|
||||
const saveSequenceRef = useRef(0);
|
||||
const saveQueueRef = useRef<Promise<void>>(Promise.resolve());
|
||||
const enqueueSave = useCallback((saveFn: () => Promise<void>) => {
|
||||
@@ -118,20 +127,61 @@ export function useCodexCommonConfig({
|
||||
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;
|
||||
hasUserToggledCommonConfig.current = false;
|
||||
}, [selectedPresetId]);
|
||||
|
||||
// 初始化:从数据库加载,支持从 localStorage 迁移
|
||||
// ============================================================================
|
||||
// 加载通用配置片段(从数据库,支持 localStorage 迁移)
|
||||
// ============================================================================
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
|
||||
const loadSnippet = async () => {
|
||||
try {
|
||||
// 使用统一 API 加载
|
||||
const snippet = await configApi.getCommonConfigSnippet("codex");
|
||||
|
||||
if (snippet && snippet.trim()) {
|
||||
@@ -139,22 +189,26 @@ export function useCodexCommonConfig({
|
||||
setCommonConfigSnippetState(snippet);
|
||||
}
|
||||
} else {
|
||||
// 如果数据库中没有,尝试从 localStorage 迁移
|
||||
// 尝试从 localStorage 迁移
|
||||
if (typeof window !== "undefined") {
|
||||
try {
|
||||
const legacySnippet =
|
||||
window.localStorage.getItem(LEGACY_STORAGE_KEY);
|
||||
if (legacySnippet && legacySnippet.trim()) {
|
||||
// 迁移到 config.json
|
||||
await configApi.setCommonConfigSnippet("codex", legacySnippet);
|
||||
if (mounted) {
|
||||
setCommonConfigSnippetState(legacySnippet);
|
||||
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 迁移到数据库",
|
||||
);
|
||||
}
|
||||
// 清理 localStorage
|
||||
window.localStorage.removeItem(LEGACY_STORAGE_KEY);
|
||||
console.log(
|
||||
"[迁移] Codex 通用配置已从 localStorage 迁移到数据库",
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("[迁移] 从 localStorage 迁移失败:", e);
|
||||
@@ -177,13 +231,13 @@ export function useCodexCommonConfig({
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 初始化时从 meta 读取启用状态(编辑模式)
|
||||
// 优先使用 meta,若 meta 未定义则回退到内容检测
|
||||
// ============================================================================
|
||||
// 编辑模式初始化:从 meta 读取启用状态
|
||||
// ============================================================================
|
||||
useEffect(() => {
|
||||
if (initialData && !isLoading && !hasInitializedEditMode.current) {
|
||||
hasInitializedEditMode.current = true;
|
||||
|
||||
// 使用 meta 中记录的按 app 启用状态
|
||||
const metaByApp = initialData.meta?.commonConfigEnabledByApp;
|
||||
const resolvedMetaEnabled =
|
||||
metaByApp?.codex ?? initialData.meta?.commonConfigEnabled;
|
||||
@@ -193,260 +247,148 @@ export function useCodexCommonConfig({
|
||||
setUseCommonConfig(false);
|
||||
return;
|
||||
}
|
||||
const snippetError = getSnippetApplyError(commonConfigSnippet);
|
||||
if (snippetError) {
|
||||
setCommonConfigError(snippetError);
|
||||
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);
|
||||
return;
|
||||
} else {
|
||||
// meta 未定义,回退到内容检测
|
||||
// Codex 使用 TOML 格式,从 settingsConfig.config 获取
|
||||
const codexConfigStr =
|
||||
typeof initialData.settingsConfig === "object" &&
|
||||
initialData.settingsConfig !== null
|
||||
? String(
|
||||
(initialData.settingsConfig as Record<string, unknown>)
|
||||
.config ?? "",
|
||||
)
|
||||
: "";
|
||||
const detected = hasTomlCommonConfigSnippet(
|
||||
codexConfigStr,
|
||||
commonConfigSnippet,
|
||||
);
|
||||
setUseCommonConfig(detected);
|
||||
setUseCommonConfig(false);
|
||||
}
|
||||
}
|
||||
}, [initialData, isLoading, commonConfigSnippet, getSnippetApplyError]);
|
||||
}, [initialData, isLoading, commonConfigSnippet, hasSnippetContent, t]);
|
||||
|
||||
// 新建模式:如果通用配置片段存在且有效,默认启用
|
||||
// ============================================================================
|
||||
// 新建模式初始化:如果通用配置有效,默认启用
|
||||
// ============================================================================
|
||||
useEffect(() => {
|
||||
// 仅新建模式、加载完成、尚未初始化过
|
||||
if (!initialData && !isLoading && !hasInitializedNewMode.current) {
|
||||
hasInitializedNewMode.current = true;
|
||||
|
||||
// 检查 TOML 片段是否有实质内容(不只是注释和空行)
|
||||
const hasContent = hasSnippetContent(commonConfigSnippet);
|
||||
|
||||
if (hasContent) {
|
||||
setUseCommonConfig(true);
|
||||
// 合并通用配置到当前配置
|
||||
const { updatedConfig, error } = updateTomlCommonConfigSnippet(
|
||||
codexConfig,
|
||||
commonConfigSnippet,
|
||||
true,
|
||||
);
|
||||
if (!error) {
|
||||
isUpdatingFromCommonConfig.current = true;
|
||||
onConfigChange(updatedConfig);
|
||||
setTimeout(() => {
|
||||
isUpdatingFromCommonConfig.current = false;
|
||||
}, 0);
|
||||
if (hasSnippetContent(commonConfigSnippet)) {
|
||||
const tomlError = validateTomlFormat(commonConfigSnippet);
|
||||
if (!tomlError) {
|
||||
setUseCommonConfig(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [
|
||||
initialData,
|
||||
commonConfigSnippet,
|
||||
isLoading,
|
||||
codexConfig,
|
||||
onConfigChange,
|
||||
hasSnippetContent,
|
||||
]);
|
||||
}, [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) => {
|
||||
hasUserToggledCommonConfig.current = true;
|
||||
if (checked) {
|
||||
const snippetError = getSnippetApplyError(commonConfigSnippet);
|
||||
if (snippetError) {
|
||||
setCommonConfigError(snippetError);
|
||||
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;
|
||||
}
|
||||
}
|
||||
const { updatedConfig, error: snippetError } =
|
||||
updateTomlCommonConfigSnippet(
|
||||
codexConfig,
|
||||
commonConfigSnippet,
|
||||
checked,
|
||||
);
|
||||
|
||||
if (snippetError) {
|
||||
setCommonConfigError(snippetError);
|
||||
setUseCommonConfig(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setCommonConfigError("");
|
||||
setUseCommonConfig(checked);
|
||||
// 标记正在通过通用配置更新
|
||||
isUpdatingFromCommonConfig.current = true;
|
||||
onConfigChange(updatedConfig);
|
||||
// 在下一个事件循环中重置标记
|
||||
setTimeout(() => {
|
||||
isUpdatingFromCommonConfig.current = false;
|
||||
}, 0);
|
||||
// 新架构:不修改 codexConfig,只改变 enabled 状态
|
||||
},
|
||||
[codexConfig, commonConfigSnippet, onConfigChange, getSnippetApplyError],
|
||||
[commonConfigSnippet, hasSnippetContent, t],
|
||||
);
|
||||
|
||||
// ============================================================================
|
||||
// 处理通用配置片段变化
|
||||
// ============================================================================
|
||||
const handleCommonConfigSnippetChange = useCallback(
|
||||
(value: string) => {
|
||||
const previousSnippet = commonConfigSnippet;
|
||||
setCommonConfigSnippetState(value);
|
||||
|
||||
if (!value.trim()) {
|
||||
const saveId = ++saveSequenceRef.current;
|
||||
setCommonConfigError("");
|
||||
// 保存到数据库(清空)
|
||||
enqueueSave(() => configApi.setCommonConfigSnippet("codex", ""))
|
||||
.then(() => {
|
||||
if (saveSequenceRef.current !== saveId) return;
|
||||
// 清空时也需要同步:移除所有供应商的通用配置片段
|
||||
configApi.syncCommonConfigToProviders(
|
||||
"codex",
|
||||
previousSnippet,
|
||||
"", // newSnippet 为空表示移除
|
||||
replaceTomlCommonConfigSnippet,
|
||||
currentProviderId,
|
||||
(result) => {
|
||||
if (saveSequenceRef.current !== saveId) return;
|
||||
if (result.error) {
|
||||
toast.error(t("providerForm.commonConfigSyncFailed"));
|
||||
}
|
||||
},
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
enqueueSave(() => configApi.setCommonConfigSnippet("codex", "")).catch(
|
||||
(error) => {
|
||||
if (saveSequenceRef.current !== saveId) return;
|
||||
console.error("保存 Codex 通用配置失败:", error);
|
||||
setCommonConfigError(
|
||||
t("codexConfig.saveFailed", { error: String(error) }),
|
||||
);
|
||||
});
|
||||
|
||||
if (useCommonConfig) {
|
||||
const { updatedConfig } = updateTomlCommonConfigSnippet(
|
||||
codexConfig,
|
||||
previousSnippet,
|
||||
false,
|
||||
);
|
||||
onConfigChange(updatedConfig);
|
||||
setUseCommonConfig(false);
|
||||
}
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// TOML 格式校验 - 在保存和同步前校验,避免传播无效配置
|
||||
// TOML 格式校验
|
||||
const tomlError = validateTomlFormat(value);
|
||||
if (tomlError) {
|
||||
console.warn("Codex 通用配置 TOML 校验失败:", tomlError);
|
||||
setCommonConfigError(
|
||||
t("mcp.error.tomlInvalid", {
|
||||
defaultValue: "TOML 格式错误,请检查语法",
|
||||
}),
|
||||
);
|
||||
// 不保存、不同步,仅显示错误
|
||||
return;
|
||||
}
|
||||
|
||||
const saveId = ++saveSequenceRef.current;
|
||||
setCommonConfigError("");
|
||||
// 保存到 config.json
|
||||
enqueueSave(() => configApi.setCommonConfigSnippet("codex", value))
|
||||
.then(() => {
|
||||
if (saveSequenceRef.current !== saveId) return;
|
||||
// 保存成功后,同步更新所有启用了通用配置的供应商
|
||||
configApi.syncCommonConfigToProviders(
|
||||
"codex",
|
||||
previousSnippet,
|
||||
value,
|
||||
replaceTomlCommonConfigSnippet,
|
||||
currentProviderId,
|
||||
(result) => {
|
||||
if (saveSequenceRef.current !== saveId) return;
|
||||
if (result.error) {
|
||||
toast.error(t("providerForm.commonConfigSyncFailed"));
|
||||
}
|
||||
},
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
enqueueSave(() => configApi.setCommonConfigSnippet("codex", value)).catch(
|
||||
(error) => {
|
||||
if (saveSequenceRef.current !== saveId) return;
|
||||
console.error("保存 Codex 通用配置失败:", error);
|
||||
setCommonConfigError(
|
||||
t("codexConfig.saveFailed", { error: String(error) }),
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// 若当前启用通用配置,需要替换为最新片段
|
||||
if (useCommonConfig) {
|
||||
const removeResult = updateTomlCommonConfigSnippet(
|
||||
codexConfig,
|
||||
previousSnippet,
|
||||
false,
|
||||
);
|
||||
if (removeResult.error) {
|
||||
setCommonConfigError(removeResult.error);
|
||||
return;
|
||||
}
|
||||
const addResult = updateTomlCommonConfigSnippet(
|
||||
removeResult.updatedConfig,
|
||||
value,
|
||||
true,
|
||||
);
|
||||
|
||||
if (addResult.error) {
|
||||
setCommonConfigError(addResult.error);
|
||||
return;
|
||||
}
|
||||
|
||||
// 标记正在通过通用配置更新,避免触发状态检查
|
||||
isUpdatingFromCommonConfig.current = true;
|
||||
onConfigChange(addResult.updatedConfig);
|
||||
// 在下一个事件循环中重置标记
|
||||
setTimeout(() => {
|
||||
isUpdatingFromCommonConfig.current = false;
|
||||
}, 0);
|
||||
}
|
||||
// 注意:新架构下不再需要同步到其他供应商的 settingsConfig
|
||||
// 因为 finalConfig 是运行时计算的
|
||||
},
|
||||
[
|
||||
commonConfigSnippet,
|
||||
codexConfig,
|
||||
useCommonConfig,
|
||||
onConfigChange,
|
||||
currentProviderId,
|
||||
enqueueSave,
|
||||
t,
|
||||
],
|
||||
[enqueueSave, t],
|
||||
);
|
||||
|
||||
// 当配置变化时检查是否包含通用配置(避免通过通用配置更新时反复覆盖)
|
||||
useEffect(() => {
|
||||
if (isUpdatingFromCommonConfig.current || isLoading) {
|
||||
return;
|
||||
}
|
||||
const metaByApp = initialData?.meta?.commonConfigEnabledByApp;
|
||||
const hasExplicitMeta =
|
||||
metaByApp?.codex !== undefined ||
|
||||
initialData?.meta?.commonConfigEnabled !== undefined;
|
||||
if (hasExplicitMeta || hasUserToggledCommonConfig.current) {
|
||||
return;
|
||||
}
|
||||
const hasCommon = hasTomlCommonConfigSnippet(
|
||||
codexConfig,
|
||||
commonConfigSnippet,
|
||||
);
|
||||
setUseCommonConfig(hasCommon);
|
||||
}, [codexConfig, commonConfigSnippet, isLoading, initialData]);
|
||||
|
||||
// 从编辑器当前内容提取通用配置片段
|
||||
// ============================================================================
|
||||
// 从当前最终配置提取通用配置片段
|
||||
// ============================================================================
|
||||
const handleExtract = useCallback(async () => {
|
||||
setIsExtracting(true);
|
||||
setCommonConfigError("");
|
||||
@@ -454,7 +396,7 @@ export function useCodexCommonConfig({
|
||||
try {
|
||||
const extracted = await configApi.extractCommonConfigSnippet("codex", {
|
||||
settingsConfig: JSON.stringify({
|
||||
config: codexConfig ?? "",
|
||||
config: finalConfig ?? "",
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -463,11 +405,36 @@ export function useCodexCommonConfig({
|
||||
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(
|
||||
@@ -476,7 +443,7 @@ export function useCodexCommonConfig({
|
||||
} finally {
|
||||
setIsExtracting(false);
|
||||
}
|
||||
}, [codexConfig, t]);
|
||||
}, [finalConfig, codexConfig, onConfigChange, extractConfigToml, t]);
|
||||
|
||||
return {
|
||||
useCommonConfig,
|
||||
@@ -487,5 +454,6 @@ export function useCodexCommonConfig({
|
||||
handleCommonConfigToggle,
|
||||
handleCommonConfigSnippetChange,
|
||||
handleExtract,
|
||||
finalConfig,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { useState, useEffect, useCallback, useRef } from "react";
|
||||
import { useState, useEffect, useCallback, useRef, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
updateCommonConfigSnippet,
|
||||
validateJsonConfig,
|
||||
hasCommonConfigSnippet,
|
||||
replaceCommonConfigSnippet,
|
||||
} from "@/utils/providerConfigUtils";
|
||||
import { validateJsonConfig } from "@/utils/providerConfigUtils";
|
||||
import { configApi } from "@/lib/api";
|
||||
import {
|
||||
computeFinalConfig,
|
||||
extractDifference,
|
||||
isPlainObject,
|
||||
} from "@/utils/configMerge";
|
||||
import type { ProviderMeta } from "@/types";
|
||||
|
||||
const LEGACY_STORAGE_KEY = "cc-switch:common-config-snippet";
|
||||
@@ -16,22 +15,65 @@ const DEFAULT_COMMON_CONFIG_SNIPPET = `{
|
||||
}`;
|
||||
|
||||
interface UseCommonConfigSnippetProps {
|
||||
/**
|
||||
* 当前配置(用于显示和运行时合并)
|
||||
* 新架构:传入自定义配置,返回最终配置
|
||||
*/
|
||||
settingsConfig: string;
|
||||
/**
|
||||
* 配置变化回调
|
||||
*/
|
||||
onConfigChange: (config: string) => void;
|
||||
/**
|
||||
* 初始数据(编辑模式)
|
||||
*/
|
||||
initialData?: {
|
||||
settingsConfig?: Record<string, unknown>;
|
||||
meta?: ProviderMeta;
|
||||
};
|
||||
/**
|
||||
* 当前选中的预设 ID
|
||||
*/
|
||||
selectedPresetId?: string;
|
||||
/** When false, the hook skips all logic and returns disabled state. Default: true */
|
||||
/**
|
||||
* 当 false 时跳过所有逻辑,返回禁用状态。默认:true
|
||||
*/
|
||||
enabled?: boolean;
|
||||
/** 当前正在编辑的供应商 ID(用于同步时跳过) */
|
||||
/**
|
||||
* 当前正在编辑的供应商 ID
|
||||
*/
|
||||
currentProviderId?: string;
|
||||
}
|
||||
|
||||
export interface UseCommonConfigSnippetReturn {
|
||||
/** 是否启用通用配置 */
|
||||
useCommonConfig: boolean;
|
||||
/** 通用配置片段 (JSON 格式) */
|
||||
commonConfigSnippet: string;
|
||||
/** 通用配置错误信息 */
|
||||
commonConfigError: string;
|
||||
/** 是否正在加载 */
|
||||
isLoading: boolean;
|
||||
/** 是否正在提取 */
|
||||
isExtracting: boolean;
|
||||
/** 通用配置开关处理函数 */
|
||||
handleCommonConfigToggle: (checked: boolean) => void;
|
||||
/** 通用配置片段变化处理函数 */
|
||||
handleCommonConfigSnippetChange: (snippet: string) => void;
|
||||
/** 从当前配置提取通用配置 */
|
||||
handleExtract: () => Promise<void>;
|
||||
/** 最终配置(运行时合并结果,只读) */
|
||||
finalConfig: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理 Claude 通用配置片段
|
||||
* 从数据库读取和保存,支持从 localStorage 平滑迁移
|
||||
* 管理 Claude 通用配置片段(重构版)
|
||||
*
|
||||
* 新架构:
|
||||
* - settingsConfig:传入自定义配置(供应商独有部分)
|
||||
* - commonConfigSnippet:存储在数据库中的通用配置片段
|
||||
* - finalConfig:运行时计算 = merge(commonConfig, settingsConfig)
|
||||
* - 开启/关闭通用配置只改变 enabled 状态,不修改 settingsConfig
|
||||
*/
|
||||
export function useCommonConfigSnippet({
|
||||
settingsConfig,
|
||||
@@ -39,10 +81,14 @@ export function useCommonConfigSnippet({
|
||||
initialData,
|
||||
selectedPresetId,
|
||||
enabled = true,
|
||||
currentProviderId,
|
||||
}: UseCommonConfigSnippetProps) {
|
||||
// currentProviderId is reserved for future use
|
||||
}: UseCommonConfigSnippetProps): UseCommonConfigSnippetReturn {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// 内部管理的通用配置启用状态
|
||||
const [useCommonConfig, setUseCommonConfig] = useState(false);
|
||||
|
||||
// 通用配置片段(从数据库加载)
|
||||
const [commonConfigSnippet, setCommonConfigSnippetState] = useState<string>(
|
||||
DEFAULT_COMMON_CONFIG_SNIPPET,
|
||||
);
|
||||
@@ -50,6 +96,51 @@ export function useCommonConfigSnippet({
|
||||
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);
|
||||
|
||||
// 当预设变化时,重置初始化标记
|
||||
useEffect(() => {
|
||||
if (!enabled) return;
|
||||
hasInitializedNewMode.current = false;
|
||||
hasInitializedEditMode.current = false;
|
||||
}, [selectedPresetId, enabled]);
|
||||
|
||||
// 解析 JSON 配置片段
|
||||
const parseSnippet = useCallback(
|
||||
(
|
||||
snippetString: string,
|
||||
): { config: Record<string, unknown>; error?: string } => {
|
||||
const trimmed = snippetString.trim();
|
||||
if (!trimmed) {
|
||||
return { config: {} };
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed);
|
||||
if (!isPlainObject(parsed)) {
|
||||
return { config: {}, error: t("claudeConfig.invalidJsonFormat") };
|
||||
}
|
||||
return { config: parsed };
|
||||
} catch {
|
||||
return { config: {}, error: t("claudeConfig.invalidJsonFormat") };
|
||||
}
|
||||
},
|
||||
[t],
|
||||
);
|
||||
|
||||
// 获取片段应用错误
|
||||
const getSnippetApplyError = useCallback(
|
||||
(snippet: string) => {
|
||||
if (!snippet.trim()) {
|
||||
@@ -72,32 +163,9 @@ export function useCommonConfigSnippet({
|
||||
[t],
|
||||
);
|
||||
|
||||
// 用于跟踪是否正在通过通用配置更新
|
||||
const isUpdatingFromCommonConfig = useRef(false);
|
||||
// 用于跟踪用户是否手动切换,避免自动检测覆盖用户意图
|
||||
const hasUserToggledCommonConfig = useRef(false);
|
||||
// 用于跟踪新建模式是否已初始化默认勾选
|
||||
const hasInitializedNewMode = useRef(false);
|
||||
// 用于跟踪编辑模式是否已初始化(避免反复覆盖用户切换)
|
||||
const hasInitializedEditMode = useRef(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;
|
||||
}, []);
|
||||
|
||||
// 当预设变化时,重置初始化标记,使新预设能够重新触发初始化逻辑
|
||||
useEffect(() => {
|
||||
if (!enabled) return;
|
||||
hasInitializedNewMode.current = false;
|
||||
hasInitializedEditMode.current = false;
|
||||
hasUserToggledCommonConfig.current = false;
|
||||
}, [selectedPresetId, enabled]);
|
||||
|
||||
// 初始化:从数据库加载,支持从 localStorage 迁移
|
||||
// ============================================================================
|
||||
// 加载通用配置片段(从数据库,支持 localStorage 迁移)
|
||||
// ============================================================================
|
||||
useEffect(() => {
|
||||
if (!enabled) {
|
||||
setIsLoading(false);
|
||||
@@ -107,7 +175,6 @@ export function useCommonConfigSnippet({
|
||||
|
||||
const loadSnippet = async () => {
|
||||
try {
|
||||
// 使用统一 API 加载
|
||||
const snippet = await configApi.getCommonConfigSnippet("claude");
|
||||
|
||||
if (snippet && snippet.trim()) {
|
||||
@@ -115,22 +182,26 @@ export function useCommonConfigSnippet({
|
||||
setCommonConfigSnippetState(snippet);
|
||||
}
|
||||
} else {
|
||||
// 如果数据库中没有,尝试从 localStorage 迁移
|
||||
// 尝试从 localStorage 迁移
|
||||
if (typeof window !== "undefined") {
|
||||
try {
|
||||
const legacySnippet =
|
||||
window.localStorage.getItem(LEGACY_STORAGE_KEY);
|
||||
if (legacySnippet && legacySnippet.trim()) {
|
||||
// 迁移到 config.json
|
||||
await configApi.setCommonConfigSnippet("claude", legacySnippet);
|
||||
if (mounted) {
|
||||
setCommonConfigSnippetState(legacySnippet);
|
||||
const parsed = parseSnippet(legacySnippet);
|
||||
if (!parsed.error) {
|
||||
await configApi.setCommonConfigSnippet(
|
||||
"claude",
|
||||
legacySnippet,
|
||||
);
|
||||
if (mounted) {
|
||||
setCommonConfigSnippetState(legacySnippet);
|
||||
}
|
||||
window.localStorage.removeItem(LEGACY_STORAGE_KEY);
|
||||
console.log(
|
||||
"[迁移] Claude 通用配置已从 localStorage 迁移到数据库",
|
||||
);
|
||||
}
|
||||
// 清理 localStorage
|
||||
window.localStorage.removeItem(LEGACY_STORAGE_KEY);
|
||||
console.log(
|
||||
"[迁移] Claude 通用配置已从 localStorage 迁移到数据库",
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("[迁移] 从 localStorage 迁移失败:", e);
|
||||
@@ -151,16 +222,16 @@ export function useCommonConfigSnippet({
|
||||
return () => {
|
||||
mounted = false;
|
||||
};
|
||||
}, [enabled]);
|
||||
}, [enabled, parseSnippet]);
|
||||
|
||||
// 初始化时从 meta 读取启用状态(编辑模式)
|
||||
// 优先使用 meta,若 meta 未定义则回退到内容检测
|
||||
// ============================================================================
|
||||
// 编辑模式初始化:从 meta 读取启用状态
|
||||
// ============================================================================
|
||||
useEffect(() => {
|
||||
if (!enabled) return;
|
||||
if (initialData && !isLoading && !hasInitializedEditMode.current) {
|
||||
hasInitializedEditMode.current = true;
|
||||
|
||||
// 使用 meta 中记录的按 app 启用状态
|
||||
const metaByApp = initialData.meta?.commonConfigEnabledByApp;
|
||||
const resolvedMetaEnabled =
|
||||
metaByApp?.claude ?? initialData.meta?.commonConfigEnabled;
|
||||
@@ -178,18 +249,8 @@ export function useCommonConfigSnippet({
|
||||
}
|
||||
setCommonConfigError("");
|
||||
setUseCommonConfig(true);
|
||||
return;
|
||||
} else {
|
||||
// meta 未定义,回退到内容检测
|
||||
const settingsConfigStr =
|
||||
typeof initialData.settingsConfig === "string"
|
||||
? initialData.settingsConfig
|
||||
: JSON.stringify(initialData.settingsConfig ?? {});
|
||||
const detected = hasCommonConfigSnippet(
|
||||
settingsConfigStr,
|
||||
commonConfigSnippet,
|
||||
);
|
||||
setUseCommonConfig(detected);
|
||||
setUseCommonConfig(false);
|
||||
}
|
||||
}
|
||||
}, [
|
||||
@@ -200,50 +261,69 @@ export function useCommonConfigSnippet({
|
||||
getSnippetApplyError,
|
||||
]);
|
||||
|
||||
// 新建模式:如果通用配置片段存在且有效,默认启用
|
||||
// ============================================================================
|
||||
// 新建模式初始化:如果通用配置有效,默认启用
|
||||
// ============================================================================
|
||||
useEffect(() => {
|
||||
if (!enabled) return;
|
||||
// 仅新建模式、加载完成、尚未初始化过
|
||||
if (!initialData && !isLoading && !hasInitializedNewMode.current) {
|
||||
hasInitializedNewMode.current = true;
|
||||
|
||||
// 检查片段是否有实质内容
|
||||
try {
|
||||
const snippetObj = JSON.parse(commonConfigSnippet);
|
||||
const hasContent = Object.keys(snippetObj).length > 0;
|
||||
if (hasContent) {
|
||||
setUseCommonConfig(true);
|
||||
// 合并通用配置到当前配置
|
||||
const { updatedConfig, error } = updateCommonConfigSnippet(
|
||||
settingsConfig,
|
||||
commonConfigSnippet,
|
||||
true,
|
||||
);
|
||||
if (!error) {
|
||||
isUpdatingFromCommonConfig.current = true;
|
||||
onConfigChange(updatedConfig);
|
||||
setTimeout(() => {
|
||||
isUpdatingFromCommonConfig.current = false;
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ignore parse error
|
||||
const parsed = parseSnippet(commonConfigSnippet);
|
||||
if (!parsed.error && Object.keys(parsed.config).length > 0) {
|
||||
setUseCommonConfig(true);
|
||||
}
|
||||
}
|
||||
}, [enabled, initialData, commonConfigSnippet, isLoading, parseSnippet]);
|
||||
|
||||
// ============================================================================
|
||||
// 计算最终配置(运行时合并)
|
||||
// ============================================================================
|
||||
const finalConfig = useMemo((): string => {
|
||||
if (!enabled) return settingsConfig;
|
||||
|
||||
try {
|
||||
const customParsed = settingsConfig ? JSON.parse(settingsConfig) : {};
|
||||
if (!isPlainObject(customParsed)) {
|
||||
return settingsConfig;
|
||||
}
|
||||
|
||||
if (!useCommonConfig) {
|
||||
return settingsConfig;
|
||||
}
|
||||
|
||||
const snippetParsed = parseSnippet(commonConfigSnippet);
|
||||
if (
|
||||
snippetParsed.error ||
|
||||
Object.keys(snippetParsed.config).length === 0
|
||||
) {
|
||||
return settingsConfig;
|
||||
}
|
||||
|
||||
// 通用配置作为 base,自定义配置覆盖
|
||||
const merged = computeFinalConfig(
|
||||
customParsed,
|
||||
snippetParsed.config,
|
||||
true,
|
||||
);
|
||||
|
||||
return JSON.stringify(merged, null, 2);
|
||||
} catch {
|
||||
return settingsConfig;
|
||||
}
|
||||
}, [
|
||||
enabled,
|
||||
initialData,
|
||||
commonConfigSnippet,
|
||||
isLoading,
|
||||
settingsConfig,
|
||||
onConfigChange,
|
||||
commonConfigSnippet,
|
||||
useCommonConfig,
|
||||
parseSnippet,
|
||||
]);
|
||||
|
||||
// ============================================================================
|
||||
// 处理通用配置开关
|
||||
// ============================================================================
|
||||
const handleCommonConfigToggle = useCallback(
|
||||
(checked: boolean) => {
|
||||
hasUserToggledCommonConfig.current = true;
|
||||
if (checked) {
|
||||
const snippetError = getSnippetApplyError(commonConfigSnippet);
|
||||
if (snippetError) {
|
||||
@@ -252,185 +332,70 @@ export function useCommonConfigSnippet({
|
||||
return;
|
||||
}
|
||||
}
|
||||
const { updatedConfig, error: snippetError } = updateCommonConfigSnippet(
|
||||
settingsConfig,
|
||||
commonConfigSnippet,
|
||||
checked,
|
||||
);
|
||||
|
||||
if (snippetError) {
|
||||
setCommonConfigError(snippetError);
|
||||
setUseCommonConfig(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setCommonConfigError("");
|
||||
setUseCommonConfig(checked);
|
||||
// 标记正在通过通用配置更新
|
||||
isUpdatingFromCommonConfig.current = true;
|
||||
onConfigChange(updatedConfig);
|
||||
// 在下一个事件循环中重置标记
|
||||
setTimeout(() => {
|
||||
isUpdatingFromCommonConfig.current = false;
|
||||
}, 0);
|
||||
// 新架构:不修改 settingsConfig,只改变 enabled 状态
|
||||
},
|
||||
[settingsConfig, commonConfigSnippet, onConfigChange, getSnippetApplyError],
|
||||
[commonConfigSnippet, getSnippetApplyError],
|
||||
);
|
||||
|
||||
// ============================================================================
|
||||
// 处理通用配置片段变化
|
||||
// ============================================================================
|
||||
const handleCommonConfigSnippetChange = useCallback(
|
||||
(value: string) => {
|
||||
const previousSnippet = commonConfigSnippet;
|
||||
setCommonConfigSnippetState(value);
|
||||
|
||||
if (!value.trim()) {
|
||||
const saveId = ++saveSequenceRef.current;
|
||||
setCommonConfigError("");
|
||||
// 保存到数据库(清空)
|
||||
enqueueSave(() => configApi.setCommonConfigSnippet("claude", ""))
|
||||
.then(() => {
|
||||
if (saveSequenceRef.current !== saveId) return;
|
||||
// 清空时也需要同步:移除所有供应商的通用配置片段
|
||||
configApi.syncCommonConfigToProviders(
|
||||
"claude",
|
||||
previousSnippet,
|
||||
"", // newSnippet 为空表示移除
|
||||
replaceCommonConfigSnippet,
|
||||
currentProviderId,
|
||||
(result) => {
|
||||
if (saveSequenceRef.current !== saveId) return;
|
||||
if (result.error) {
|
||||
toast.error(t("providerForm.commonConfigSyncFailed"));
|
||||
}
|
||||
},
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
enqueueSave(() => configApi.setCommonConfigSnippet("claude", "")).catch(
|
||||
(error) => {
|
||||
if (saveSequenceRef.current !== saveId) return;
|
||||
console.error("保存通用配置失败:", error);
|
||||
setCommonConfigError(
|
||||
t("claudeConfig.saveFailed", { error: String(error) }),
|
||||
);
|
||||
});
|
||||
|
||||
if (useCommonConfig) {
|
||||
const { updatedConfig } = updateCommonConfigSnippet(
|
||||
settingsConfig,
|
||||
previousSnippet,
|
||||
false,
|
||||
);
|
||||
onConfigChange(updatedConfig);
|
||||
setUseCommonConfig(false);
|
||||
}
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证JSON格式
|
||||
// JSON 格式校验
|
||||
const validationError = validateJsonConfig(value, "通用配置片段");
|
||||
if (validationError) {
|
||||
setCommonConfigError(validationError);
|
||||
return;
|
||||
} else {
|
||||
const saveId = ++saveSequenceRef.current;
|
||||
setCommonConfigError("");
|
||||
// 保存到数据库
|
||||
enqueueSave(() => configApi.setCommonConfigSnippet("claude", value))
|
||||
.then(() => {
|
||||
if (saveSequenceRef.current !== saveId) return;
|
||||
// 保存成功后,同步更新所有启用了通用配置的供应商
|
||||
configApi.syncCommonConfigToProviders(
|
||||
"claude",
|
||||
previousSnippet,
|
||||
value,
|
||||
replaceCommonConfigSnippet,
|
||||
currentProviderId,
|
||||
(result) => {
|
||||
if (saveSequenceRef.current !== saveId) return;
|
||||
if (result.error) {
|
||||
toast.error(t("providerForm.commonConfigSyncFailed"));
|
||||
}
|
||||
},
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
if (saveSequenceRef.current !== saveId) return;
|
||||
console.error("保存通用配置失败:", error);
|
||||
setCommonConfigError(
|
||||
t("claudeConfig.saveFailed", { error: String(error) }),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// 若当前启用通用配置且格式正确,需要替换为最新片段
|
||||
if (useCommonConfig && !validationError) {
|
||||
const removeResult = updateCommonConfigSnippet(
|
||||
settingsConfig,
|
||||
previousSnippet,
|
||||
false,
|
||||
);
|
||||
if (removeResult.error) {
|
||||
setCommonConfigError(removeResult.error);
|
||||
return;
|
||||
}
|
||||
const addResult = updateCommonConfigSnippet(
|
||||
removeResult.updatedConfig,
|
||||
value,
|
||||
true,
|
||||
const saveId = ++saveSequenceRef.current;
|
||||
setCommonConfigError("");
|
||||
enqueueSave(() =>
|
||||
configApi.setCommonConfigSnippet("claude", value),
|
||||
).catch((error) => {
|
||||
if (saveSequenceRef.current !== saveId) return;
|
||||
console.error("保存通用配置失败:", error);
|
||||
setCommonConfigError(
|
||||
t("claudeConfig.saveFailed", { error: String(error) }),
|
||||
);
|
||||
});
|
||||
|
||||
if (addResult.error) {
|
||||
setCommonConfigError(addResult.error);
|
||||
return;
|
||||
}
|
||||
|
||||
// 标记正在通过通用配置更新,避免触发状态检查
|
||||
isUpdatingFromCommonConfig.current = true;
|
||||
onConfigChange(addResult.updatedConfig);
|
||||
// 在下一个事件循环中重置标记
|
||||
setTimeout(() => {
|
||||
isUpdatingFromCommonConfig.current = false;
|
||||
}, 0);
|
||||
}
|
||||
// 注意:新架构下不再需要同步到其他供应商的 settingsConfig
|
||||
// 因为 finalConfig 是运行时计算的
|
||||
},
|
||||
[
|
||||
commonConfigSnippet,
|
||||
settingsConfig,
|
||||
useCommonConfig,
|
||||
onConfigChange,
|
||||
currentProviderId,
|
||||
enqueueSave,
|
||||
t,
|
||||
],
|
||||
[enqueueSave, t],
|
||||
);
|
||||
|
||||
// 当配置变化时检查是否包含通用配置(避免通过通用配置更新时反复覆盖)
|
||||
useEffect(() => {
|
||||
if (!enabled) return;
|
||||
if (isUpdatingFromCommonConfig.current || isLoading) {
|
||||
return;
|
||||
}
|
||||
const metaByApp = initialData?.meta?.commonConfigEnabledByApp;
|
||||
const hasExplicitMeta =
|
||||
metaByApp?.claude !== undefined ||
|
||||
initialData?.meta?.commonConfigEnabled !== undefined;
|
||||
if (hasExplicitMeta || hasUserToggledCommonConfig.current) {
|
||||
return;
|
||||
}
|
||||
const hasCommon = hasCommonConfigSnippet(
|
||||
settingsConfig,
|
||||
commonConfigSnippet,
|
||||
);
|
||||
setUseCommonConfig(hasCommon);
|
||||
}, [enabled, settingsConfig, commonConfigSnippet, isLoading, initialData]);
|
||||
|
||||
// 从编辑器当前内容提取通用配置片段
|
||||
// ============================================================================
|
||||
// 从当前最终配置提取通用配置片段
|
||||
// ============================================================================
|
||||
const handleExtract = useCallback(async () => {
|
||||
setIsExtracting(true);
|
||||
setCommonConfigError("");
|
||||
|
||||
try {
|
||||
const extracted = await configApi.extractCommonConfigSnippet("claude", {
|
||||
settingsConfig,
|
||||
settingsConfig: finalConfig,
|
||||
});
|
||||
|
||||
if (!extracted || extracted === "{}") {
|
||||
@@ -441,7 +406,7 @@ export function useCommonConfigSnippet({
|
||||
// 验证 JSON 格式
|
||||
const validationError = validateJsonConfig(extracted, "提取的配置");
|
||||
if (validationError) {
|
||||
setCommonConfigError(validationError);
|
||||
setCommonConfigError(t("claudeConfig.extractedConfigInvalid"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -450,6 +415,19 @@ export function useCommonConfigSnippet({
|
||||
|
||||
// 保存到后端
|
||||
await configApi.setCommonConfigSnippet("claude", extracted);
|
||||
|
||||
// 提取成功后,从 settingsConfig 中移除与 extracted 相同的部分
|
||||
try {
|
||||
const customParsed = settingsConfig ? JSON.parse(settingsConfig) : {};
|
||||
const extractedParsed = JSON.parse(extracted);
|
||||
|
||||
if (isPlainObject(customParsed) && isPlainObject(extractedParsed)) {
|
||||
const diffResult = extractDifference(customParsed, extractedParsed);
|
||||
onConfigChange(JSON.stringify(diffResult.customConfig, null, 2));
|
||||
}
|
||||
} catch {
|
||||
// 忽略解析错误
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("提取通用配置失败:", error);
|
||||
setCommonConfigError(
|
||||
@@ -458,7 +436,7 @@ export function useCommonConfigSnippet({
|
||||
} finally {
|
||||
setIsExtracting(false);
|
||||
}
|
||||
}, [settingsConfig, t]);
|
||||
}, [finalConfig, settingsConfig, onConfigChange, t]);
|
||||
|
||||
return {
|
||||
useCommonConfig,
|
||||
@@ -469,5 +447,6 @@ export function useCommonConfigSnippet({
|
||||
handleCommonConfigToggle,
|
||||
handleCommonConfigSnippetChange,
|
||||
handleExtract,
|
||||
finalConfig,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,45 +1,83 @@
|
||||
import { useState, useEffect, useCallback, useRef } from "react";
|
||||
import { useState, useEffect, useCallback, useRef, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
import { configApi } from "@/lib/api";
|
||||
import {
|
||||
replaceGeminiCommonConfigSnippet,
|
||||
GEMINI_COMMON_ENV_FORBIDDEN_KEYS,
|
||||
type GeminiForbiddenEnvKey,
|
||||
} from "@/utils/providerConfigUtils";
|
||||
import {
|
||||
computeFinalConfig,
|
||||
extractDifference,
|
||||
isPlainObject,
|
||||
} from "@/utils/configMerge";
|
||||
import type { ProviderMeta } from "@/types";
|
||||
|
||||
const LEGACY_STORAGE_KEY = "cc-switch:gemini-common-config-snippet";
|
||||
const DEFAULT_GEMINI_COMMON_CONFIG_SNIPPET = "{}";
|
||||
|
||||
interface UseGeminiCommonConfigProps {
|
||||
/**
|
||||
* 当前 env 值(字符串格式,如 "KEY=VALUE\nKEY2=VALUE2")
|
||||
*/
|
||||
envValue: string;
|
||||
/**
|
||||
* env 变化回调
|
||||
*/
|
||||
onEnvChange: (env: string) => void;
|
||||
/**
|
||||
* 字符串转对象
|
||||
*/
|
||||
envStringToObj: (envString: string) => Record<string, string>;
|
||||
/**
|
||||
* 对象转字符串
|
||||
*/
|
||||
envObjToString: (envObj: Record<string, unknown>) => string;
|
||||
/**
|
||||
* 初始数据(编辑模式)
|
||||
*/
|
||||
initialData?: {
|
||||
settingsConfig?: Record<string, unknown>;
|
||||
meta?: ProviderMeta;
|
||||
};
|
||||
/**
|
||||
* 当前选中的预设 ID
|
||||
*/
|
||||
selectedPresetId?: string;
|
||||
/** 当前正在编辑的供应商 ID(用于同步时跳过) */
|
||||
/**
|
||||
* 当前正在编辑的供应商 ID
|
||||
*/
|
||||
currentProviderId?: string;
|
||||
}
|
||||
|
||||
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||
return (
|
||||
typeof value === "object" &&
|
||||
value !== null &&
|
||||
!Array.isArray(value) &&
|
||||
Object.prototype.toString.call(value) === "[object Object]"
|
||||
);
|
||||
export interface UseGeminiCommonConfigReturn {
|
||||
/** 是否启用通用配置 */
|
||||
useCommonConfig: boolean;
|
||||
/** 通用配置片段 (JSON 格式) */
|
||||
commonConfigSnippet: string;
|
||||
/** 通用配置错误信息 */
|
||||
commonConfigError: string;
|
||||
/** 是否正在加载 */
|
||||
isLoading: boolean;
|
||||
/** 是否正在提取 */
|
||||
isExtracting: boolean;
|
||||
/** 通用配置开关处理函数 */
|
||||
handleCommonConfigToggle: (checked: boolean) => void;
|
||||
/** 通用配置片段变化处理函数 */
|
||||
handleCommonConfigSnippetChange: (snippet: string) => void;
|
||||
/** 从当前配置提取通用配置 */
|
||||
handleExtract: () => Promise<void>;
|
||||
/** 最终 env 对象(运行时合并结果,只读) */
|
||||
finalEnv: Record<string, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理 Gemini 通用配置片段 (JSON 格式)
|
||||
* 写入 Gemini 的 .env,但会排除以下敏感字段:
|
||||
* - GOOGLE_GEMINI_BASE_URL
|
||||
* - GEMINI_API_KEY
|
||||
* 管理 Gemini 通用配置片段(重构版)
|
||||
*
|
||||
* 新架构:
|
||||
* - envValue:传入当前 env 字符串
|
||||
* - commonConfigSnippet:存储在数据库中的通用配置片段
|
||||
* - finalEnv:运行时计算 = merge(commonConfig, customEnv)
|
||||
* - 开启/关闭通用配置只改变 enabled 状态,不修改 envValue
|
||||
*/
|
||||
export function useGeminiCommonConfig({
|
||||
envValue,
|
||||
@@ -48,10 +86,14 @@ export function useGeminiCommonConfig({
|
||||
envObjToString,
|
||||
initialData,
|
||||
selectedPresetId,
|
||||
currentProviderId,
|
||||
}: UseGeminiCommonConfigProps) {
|
||||
// currentProviderId is reserved for future use
|
||||
}: UseGeminiCommonConfigProps): UseGeminiCommonConfigReturn {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// 内部管理的通用配置启用状态
|
||||
const [useCommonConfig, setUseCommonConfig] = useState(false);
|
||||
|
||||
// 通用配置片段(从数据库加载)
|
||||
const [commonConfigSnippet, setCommonConfigSnippetState] = useState<string>(
|
||||
DEFAULT_GEMINI_COMMON_CONFIG_SNIPPET,
|
||||
);
|
||||
@@ -59,15 +101,7 @@ export function useGeminiCommonConfig({
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isExtracting, setIsExtracting] = useState(false);
|
||||
|
||||
// 用于跟踪是否正在通过通用配置更新
|
||||
const isUpdatingFromCommonConfig = useRef(false);
|
||||
// 用于跟踪用户是否手动切换,避免自动检测覆盖用户意图
|
||||
const hasUserToggledCommonConfig = useRef(false);
|
||||
// 用于跟踪新建模式是否已初始化默认勾选
|
||||
const hasInitializedNewMode = useRef(false);
|
||||
// 用于跟踪编辑模式是否已初始化(避免反复覆盖用户切换)
|
||||
const hasInitializedEditMode = useRef(false);
|
||||
// 用于避免异步保存乱序导致的过期同步
|
||||
// 用于避免异步保存乱序
|
||||
const saveSequenceRef = useRef(0);
|
||||
const saveQueueRef = useRef<Promise<void>>(Promise.resolve());
|
||||
const enqueueSave = useCallback((saveFn: () => Promise<void>) => {
|
||||
@@ -76,13 +110,24 @@ export function useGeminiCommonConfig({
|
||||
return next;
|
||||
}, []);
|
||||
|
||||
// 当预设变化时,重置初始化标记,使新预设能够重新触发初始化逻辑
|
||||
// 用于跟踪编辑模式是否已初始化
|
||||
const hasInitializedEditMode = useRef(false);
|
||||
// 用于跟踪新建模式是否已初始化
|
||||
const hasInitializedNewMode = useRef(false);
|
||||
|
||||
// 将 envValue 字符串转换为对象
|
||||
const customEnv = useMemo(
|
||||
() => envStringToObj(envValue),
|
||||
[envValue, envStringToObj],
|
||||
);
|
||||
|
||||
// 当预设变化时,重置初始化标记
|
||||
useEffect(() => {
|
||||
hasInitializedNewMode.current = false;
|
||||
hasInitializedEditMode.current = false;
|
||||
hasUserToggledCommonConfig.current = false;
|
||||
}, [selectedPresetId]);
|
||||
|
||||
// 解析通用配置片段
|
||||
const parseSnippetEnv = useCallback(
|
||||
(
|
||||
snippetString: string,
|
||||
@@ -134,39 +179,29 @@ export function useGeminiCommonConfig({
|
||||
[t],
|
||||
);
|
||||
|
||||
const applySnippetToEnv = useCallback(
|
||||
(envObj: Record<string, string>, snippetEnv: Record<string, string>) => {
|
||||
const updated = { ...envObj };
|
||||
for (const [key, value] of Object.entries(snippetEnv)) {
|
||||
if (typeof value === "string") {
|
||||
updated[key] = value;
|
||||
}
|
||||
// 获取片段应用错误
|
||||
const getSnippetApplyError = useCallback(
|
||||
(snippet: string) => {
|
||||
const parsed = parseSnippetEnv(snippet);
|
||||
if (parsed.error) {
|
||||
return parsed.error;
|
||||
}
|
||||
return updated;
|
||||
if (Object.keys(parsed.env).length === 0) {
|
||||
return t("geminiConfig.noCommonConfigToApply");
|
||||
}
|
||||
return "";
|
||||
},
|
||||
[],
|
||||
[parseSnippetEnv, t],
|
||||
);
|
||||
|
||||
const removeSnippetFromEnv = useCallback(
|
||||
(envObj: Record<string, string>, snippetEnv: Record<string, string>) => {
|
||||
const updated = { ...envObj };
|
||||
for (const [key, value] of Object.entries(snippetEnv)) {
|
||||
if (typeof value === "string" && updated[key] === value) {
|
||||
delete updated[key];
|
||||
}
|
||||
}
|
||||
return updated;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
// 初始化:从数据库加载,支持从 localStorage 迁移
|
||||
// ============================================================================
|
||||
// 加载通用配置片段(从数据库,支持 localStorage 迁移)
|
||||
// ============================================================================
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
|
||||
const loadSnippet = async () => {
|
||||
try {
|
||||
// 使用统一 API 加载
|
||||
const snippet = await configApi.getCommonConfigSnippet("gemini");
|
||||
|
||||
if (snippet && snippet.trim()) {
|
||||
@@ -174,29 +209,26 @@ export function useGeminiCommonConfig({
|
||||
setCommonConfigSnippetState(snippet);
|
||||
}
|
||||
} else {
|
||||
// 如果数据库中没有,尝试从 localStorage 迁移
|
||||
// 尝试从 localStorage 迁移
|
||||
if (typeof window !== "undefined") {
|
||||
try {
|
||||
const legacySnippet =
|
||||
window.localStorage.getItem(LEGACY_STORAGE_KEY);
|
||||
if (legacySnippet && legacySnippet.trim()) {
|
||||
const parsed = parseSnippetEnv(legacySnippet);
|
||||
if (parsed.error) {
|
||||
console.warn(
|
||||
"[迁移] legacy Gemini 通用配置片段格式不符合当前规则,跳过迁移",
|
||||
if (!parsed.error) {
|
||||
await configApi.setCommonConfigSnippet(
|
||||
"gemini",
|
||||
legacySnippet,
|
||||
);
|
||||
if (mounted) {
|
||||
setCommonConfigSnippetState(legacySnippet);
|
||||
}
|
||||
window.localStorage.removeItem(LEGACY_STORAGE_KEY);
|
||||
console.log(
|
||||
"[迁移] Gemini 通用配置已从 localStorage 迁移到数据库",
|
||||
);
|
||||
return;
|
||||
}
|
||||
// 迁移到 config.json
|
||||
await configApi.setCommonConfigSnippet("gemini", legacySnippet);
|
||||
if (mounted) {
|
||||
setCommonConfigSnippetState(legacySnippet);
|
||||
}
|
||||
// 清理 localStorage
|
||||
window.localStorage.removeItem(LEGACY_STORAGE_KEY);
|
||||
console.log(
|
||||
"[迁移] Gemini 通用配置已从 localStorage 迁移到数据库",
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("[迁移] 从 localStorage 迁移失败:", e);
|
||||
@@ -219,13 +251,13 @@ export function useGeminiCommonConfig({
|
||||
};
|
||||
}, [parseSnippetEnv]);
|
||||
|
||||
// 初始化时从 meta 读取启用状态(编辑模式)
|
||||
// 优先使用 meta,若 meta 未定义则回退到内容检测
|
||||
// ============================================================================
|
||||
// 编辑模式初始化:从 meta 读取启用状态
|
||||
// ============================================================================
|
||||
useEffect(() => {
|
||||
if (initialData && !isLoading && !hasInitializedEditMode.current) {
|
||||
hasInitializedEditMode.current = true;
|
||||
|
||||
// 使用 meta 中记录的按 app 启用状态
|
||||
const metaByApp = initialData.meta?.commonConfigEnabledByApp;
|
||||
const resolvedMetaEnabled =
|
||||
metaByApp?.gemini ?? initialData.meta?.commonConfigEnabled;
|
||||
@@ -235,171 +267,107 @@ export function useGeminiCommonConfig({
|
||||
setUseCommonConfig(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const parsed = parseSnippetEnv(commonConfigSnippet);
|
||||
if (parsed.error || Object.keys(parsed.env).length === 0) {
|
||||
setCommonConfigError(
|
||||
parsed.error ?? t("geminiConfig.noCommonConfigToApply"),
|
||||
);
|
||||
const snippetError = getSnippetApplyError(commonConfigSnippet);
|
||||
if (snippetError) {
|
||||
setCommonConfigError(snippetError);
|
||||
setUseCommonConfig(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setCommonConfigError("");
|
||||
setUseCommonConfig(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// meta 未定义,回退到内容检测
|
||||
// Gemini 使用 JSON 格式的 env,检测通用配置片段是否已应用
|
||||
const parsed = parseSnippetEnv(commonConfigSnippet);
|
||||
if (parsed.error || Object.keys(parsed.env).length === 0) {
|
||||
setUseCommonConfig(false);
|
||||
} else {
|
||||
// 检查当前 env 是否包含通用配置中的所有键值对
|
||||
const currentEnv = envStringToObj(envValue);
|
||||
const allKeysMatch = Object.entries(parsed.env).every(
|
||||
([key, value]) => currentEnv[key] === value,
|
||||
);
|
||||
setUseCommonConfig(allKeysMatch);
|
||||
setUseCommonConfig(false);
|
||||
}
|
||||
}
|
||||
}, [
|
||||
initialData,
|
||||
isLoading,
|
||||
commonConfigSnippet,
|
||||
parseSnippetEnv,
|
||||
envStringToObj,
|
||||
envValue,
|
||||
t,
|
||||
]);
|
||||
}, [initialData, isLoading, commonConfigSnippet, getSnippetApplyError]);
|
||||
|
||||
// 新建模式:如果通用配置片段存在且有效,默认启用
|
||||
// ============================================================================
|
||||
// 新建模式初始化:如果通用配置有效,默认启用
|
||||
// ============================================================================
|
||||
useEffect(() => {
|
||||
// 仅新建模式、加载完成、尚未初始化过
|
||||
if (!initialData && !isLoading && !hasInitializedNewMode.current) {
|
||||
hasInitializedNewMode.current = true;
|
||||
|
||||
const parsed = parseSnippetEnv(commonConfigSnippet);
|
||||
if (parsed.error) return;
|
||||
const hasContent = Object.keys(parsed.env).length > 0;
|
||||
if (!hasContent) return;
|
||||
|
||||
setUseCommonConfig(true);
|
||||
const currentEnv = envStringToObj(envValue);
|
||||
const merged = applySnippetToEnv(currentEnv, parsed.env);
|
||||
const nextEnvString = envObjToString(merged);
|
||||
|
||||
isUpdatingFromCommonConfig.current = true;
|
||||
onEnvChange(nextEnvString);
|
||||
setTimeout(() => {
|
||||
isUpdatingFromCommonConfig.current = false;
|
||||
}, 0);
|
||||
if (!parsed.error && Object.keys(parsed.env).length > 0) {
|
||||
setUseCommonConfig(true);
|
||||
}
|
||||
}
|
||||
}, [
|
||||
initialData,
|
||||
isLoading,
|
||||
commonConfigSnippet,
|
||||
envValue,
|
||||
envStringToObj,
|
||||
envObjToString,
|
||||
applySnippetToEnv,
|
||||
onEnvChange,
|
||||
parseSnippetEnv,
|
||||
]);
|
||||
}, [initialData, commonConfigSnippet, isLoading, parseSnippetEnv]);
|
||||
|
||||
// ============================================================================
|
||||
// 计算最终 env(运行时合并)
|
||||
// ============================================================================
|
||||
const finalEnv = useMemo((): Record<string, string> => {
|
||||
if (!useCommonConfig) {
|
||||
return customEnv;
|
||||
}
|
||||
|
||||
const parsed = parseSnippetEnv(commonConfigSnippet);
|
||||
if (parsed.error || Object.keys(parsed.env).length === 0) {
|
||||
return customEnv;
|
||||
}
|
||||
|
||||
// 通用配置作为 base,自定义 env 覆盖
|
||||
const merged = computeFinalConfig(
|
||||
customEnv as Record<string, unknown>,
|
||||
parsed.env as Record<string, unknown>,
|
||||
true,
|
||||
);
|
||||
|
||||
// 转换回 Record<string, string>
|
||||
const result: Record<string, string> = {};
|
||||
for (const [key, value] of Object.entries(merged)) {
|
||||
if (typeof value === "string") {
|
||||
result[key] = value;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}, [customEnv, commonConfigSnippet, useCommonConfig, parseSnippetEnv]);
|
||||
|
||||
// ============================================================================
|
||||
// 处理通用配置开关
|
||||
// ============================================================================
|
||||
const handleCommonConfigToggle = useCallback(
|
||||
(checked: boolean) => {
|
||||
hasUserToggledCommonConfig.current = true;
|
||||
const parsed = parseSnippetEnv(commonConfigSnippet);
|
||||
if (parsed.error) {
|
||||
setCommonConfigError(parsed.error);
|
||||
setUseCommonConfig(false);
|
||||
return;
|
||||
if (checked) {
|
||||
const snippetError = getSnippetApplyError(commonConfigSnippet);
|
||||
if (snippetError) {
|
||||
setCommonConfigError(snippetError);
|
||||
setUseCommonConfig(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (Object.keys(parsed.env).length === 0) {
|
||||
setCommonConfigError(t("geminiConfig.noCommonConfigToApply"));
|
||||
setUseCommonConfig(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const currentEnv = envStringToObj(envValue);
|
||||
const updatedEnvObj = checked
|
||||
? applySnippetToEnv(currentEnv, parsed.env)
|
||||
: removeSnippetFromEnv(currentEnv, parsed.env);
|
||||
|
||||
setCommonConfigError("");
|
||||
setUseCommonConfig(checked);
|
||||
|
||||
isUpdatingFromCommonConfig.current = true;
|
||||
onEnvChange(envObjToString(updatedEnvObj));
|
||||
setTimeout(() => {
|
||||
isUpdatingFromCommonConfig.current = false;
|
||||
}, 0);
|
||||
// 新架构:不修改 envValue,只改变 enabled 状态
|
||||
},
|
||||
[
|
||||
applySnippetToEnv,
|
||||
commonConfigSnippet,
|
||||
envObjToString,
|
||||
envStringToObj,
|
||||
envValue,
|
||||
onEnvChange,
|
||||
parseSnippetEnv,
|
||||
removeSnippetFromEnv,
|
||||
t,
|
||||
],
|
||||
[commonConfigSnippet, getSnippetApplyError],
|
||||
);
|
||||
|
||||
// ============================================================================
|
||||
// 处理通用配置片段变化
|
||||
// ============================================================================
|
||||
const handleCommonConfigSnippetChange = useCallback(
|
||||
(value: string) => {
|
||||
const previousSnippet = commonConfigSnippet;
|
||||
setCommonConfigSnippetState(value);
|
||||
|
||||
if (!value.trim()) {
|
||||
const saveId = ++saveSequenceRef.current;
|
||||
setCommonConfigError("");
|
||||
// 保存到 config.json(清空)
|
||||
enqueueSave(() => configApi.setCommonConfigSnippet("gemini", ""))
|
||||
.then(() => {
|
||||
if (saveSequenceRef.current !== saveId) return;
|
||||
// 清空时也需要同步:移除所有供应商的通用配置片段
|
||||
configApi.syncCommonConfigToProviders(
|
||||
"gemini",
|
||||
previousSnippet,
|
||||
"", // newSnippet 为空表示移除
|
||||
replaceGeminiCommonConfigSnippet,
|
||||
currentProviderId,
|
||||
(result) => {
|
||||
if (saveSequenceRef.current !== saveId) return;
|
||||
if (result.error) {
|
||||
toast.error(t("providerForm.commonConfigSyncFailed"));
|
||||
}
|
||||
},
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
enqueueSave(() => configApi.setCommonConfigSnippet("gemini", "")).catch(
|
||||
(error) => {
|
||||
if (saveSequenceRef.current !== saveId) return;
|
||||
console.error("保存 Gemini 通用配置失败:", error);
|
||||
setCommonConfigError(
|
||||
t("geminiConfig.saveFailed", { error: String(error) }),
|
||||
);
|
||||
});
|
||||
|
||||
if (useCommonConfig) {
|
||||
const parsed = parseSnippetEnv(previousSnippet);
|
||||
if (!parsed.error && Object.keys(parsed.env).length > 0) {
|
||||
const currentEnv = envStringToObj(envValue);
|
||||
const updatedEnv = removeSnippetFromEnv(currentEnv, parsed.env);
|
||||
onEnvChange(envObjToString(updatedEnv));
|
||||
}
|
||||
setUseCommonConfig(false);
|
||||
}
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// 校验 JSON 格式
|
||||
// JSON 格式校验
|
||||
const parsed = parseSnippetEnv(value);
|
||||
if (parsed.error) {
|
||||
setCommonConfigError(parsed.error);
|
||||
@@ -408,103 +376,25 @@ export function useGeminiCommonConfig({
|
||||
|
||||
const saveId = ++saveSequenceRef.current;
|
||||
setCommonConfigError("");
|
||||
enqueueSave(() => configApi.setCommonConfigSnippet("gemini", value))
|
||||
.then(() => {
|
||||
if (saveSequenceRef.current !== saveId) return;
|
||||
// 保存成功后,同步更新所有启用了通用配置的供应商
|
||||
configApi.syncCommonConfigToProviders(
|
||||
"gemini",
|
||||
previousSnippet,
|
||||
value,
|
||||
replaceGeminiCommonConfigSnippet,
|
||||
currentProviderId,
|
||||
(result) => {
|
||||
if (saveSequenceRef.current !== saveId) return;
|
||||
if (result.error) {
|
||||
toast.error(t("providerForm.commonConfigSyncFailed"));
|
||||
}
|
||||
},
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
if (saveSequenceRef.current !== saveId) return;
|
||||
console.error("保存 Gemini 通用配置失败:", error);
|
||||
setCommonConfigError(
|
||||
t("geminiConfig.saveFailed", { error: String(error) }),
|
||||
);
|
||||
});
|
||||
enqueueSave(() =>
|
||||
configApi.setCommonConfigSnippet("gemini", value),
|
||||
).catch((error) => {
|
||||
if (saveSequenceRef.current !== saveId) return;
|
||||
console.error("保存 Gemini 通用配置失败:", error);
|
||||
setCommonConfigError(
|
||||
t("geminiConfig.saveFailed", { error: String(error) }),
|
||||
);
|
||||
});
|
||||
|
||||
// 若当前启用通用配置,需要替换为最新片段
|
||||
if (useCommonConfig) {
|
||||
const prevParsed = parseSnippetEnv(previousSnippet);
|
||||
const prevEnv = prevParsed.error ? {} : prevParsed.env;
|
||||
const nextEnv = parsed.env;
|
||||
const currentEnv = envStringToObj(envValue);
|
||||
|
||||
const withoutOld =
|
||||
Object.keys(prevEnv).length > 0
|
||||
? removeSnippetFromEnv(currentEnv, prevEnv)
|
||||
: currentEnv;
|
||||
const withNew =
|
||||
Object.keys(nextEnv).length > 0
|
||||
? applySnippetToEnv(withoutOld, nextEnv)
|
||||
: withoutOld;
|
||||
|
||||
isUpdatingFromCommonConfig.current = true;
|
||||
onEnvChange(envObjToString(withNew));
|
||||
setTimeout(() => {
|
||||
isUpdatingFromCommonConfig.current = false;
|
||||
}, 0);
|
||||
}
|
||||
// 注意:新架构下不再需要同步到其他供应商的 settingsConfig
|
||||
// 因为 finalEnv 是运行时计算的
|
||||
},
|
||||
[
|
||||
applySnippetToEnv,
|
||||
commonConfigSnippet,
|
||||
currentProviderId,
|
||||
envObjToString,
|
||||
envStringToObj,
|
||||
envValue,
|
||||
onEnvChange,
|
||||
enqueueSave,
|
||||
parseSnippetEnv,
|
||||
removeSnippetFromEnv,
|
||||
t,
|
||||
useCommonConfig,
|
||||
],
|
||||
[enqueueSave, parseSnippetEnv, t],
|
||||
);
|
||||
|
||||
// 当 env 变化时检查是否包含通用配置(避免通过通用配置更新时反复覆盖)
|
||||
useEffect(() => {
|
||||
if (isUpdatingFromCommonConfig.current || isLoading) {
|
||||
return;
|
||||
}
|
||||
const metaByApp = initialData?.meta?.commonConfigEnabledByApp;
|
||||
const hasExplicitMeta =
|
||||
metaByApp?.gemini !== undefined ||
|
||||
initialData?.meta?.commonConfigEnabled !== undefined;
|
||||
if (hasExplicitMeta || hasUserToggledCommonConfig.current) {
|
||||
return;
|
||||
}
|
||||
const parsed = parseSnippetEnv(commonConfigSnippet);
|
||||
if (parsed.error || Object.keys(parsed.env).length === 0) {
|
||||
setUseCommonConfig(false);
|
||||
return;
|
||||
}
|
||||
const envObj = envStringToObj(envValue);
|
||||
const hasCommon = Object.entries(parsed.env).every(
|
||||
([key, value]) => envObj[key] === value,
|
||||
);
|
||||
setUseCommonConfig(hasCommon);
|
||||
}, [
|
||||
envValue,
|
||||
commonConfigSnippet,
|
||||
envStringToObj,
|
||||
isLoading,
|
||||
parseSnippetEnv,
|
||||
initialData,
|
||||
]);
|
||||
|
||||
// 从编辑器当前内容提取通用配置片段
|
||||
// ============================================================================
|
||||
// 从当前最终 env 提取通用配置片段
|
||||
// ============================================================================
|
||||
const handleExtract = useCallback(async () => {
|
||||
setIsExtracting(true);
|
||||
setCommonConfigError("");
|
||||
@@ -512,7 +402,7 @@ export function useGeminiCommonConfig({
|
||||
try {
|
||||
const extracted = await configApi.extractCommonConfigSnippet("gemini", {
|
||||
settingsConfig: JSON.stringify({
|
||||
env: envStringToObj(envValue),
|
||||
env: finalEnv,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -533,6 +423,19 @@ export function useGeminiCommonConfig({
|
||||
|
||||
// 保存到后端
|
||||
await configApi.setCommonConfigSnippet("gemini", extracted);
|
||||
|
||||
// 提取成功后,从 customEnv 中移除与 extracted 相同的部分
|
||||
const diffResult = extractDifference(
|
||||
customEnv as Record<string, unknown>,
|
||||
parsed.env as Record<string, unknown>,
|
||||
);
|
||||
const newCustomEnv: Record<string, string> = {};
|
||||
for (const [key, value] of Object.entries(diffResult.customConfig)) {
|
||||
if (typeof value === "string") {
|
||||
newCustomEnv[key] = value;
|
||||
}
|
||||
}
|
||||
onEnvChange(envObjToString(newCustomEnv));
|
||||
} catch (error) {
|
||||
console.error("提取 Gemini 通用配置失败:", error);
|
||||
setCommonConfigError(
|
||||
@@ -541,7 +444,7 @@ export function useGeminiCommonConfig({
|
||||
} finally {
|
||||
setIsExtracting(false);
|
||||
}
|
||||
}, [envStringToObj, envValue, parseSnippetEnv, t]);
|
||||
}, [finalEnv, customEnv, onEnvChange, envObjToString, parseSnippetEnv, t]);
|
||||
|
||||
return {
|
||||
useCommonConfig,
|
||||
@@ -552,5 +455,6 @@ export function useGeminiCommonConfig({
|
||||
handleCommonConfigToggle,
|
||||
handleCommonConfigSnippetChange,
|
||||
handleExtract,
|
||||
finalEnv,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user