From 856beb3b7030bfbf7f096c8fe706168957c27b50 Mon Sep 17 00:00:00 2001 From: Jason Date: Thu, 16 Oct 2025 20:32:11 +0800 Subject: [PATCH] feat: add Claude common config snippet functionality - Create useCommonConfigSnippet hook to manage common config state - Create CommonConfigEditor component with modal for editing - Support merging/removing common config snippets from provider configs - Persist common config to localStorage for reuse across providers - Auto-detect if provider config contains common snippet - Replace JSON editor with CommonConfigEditor in ProviderForm --- .../providers/forms/CommonConfigEditor.tsx | 145 +++++++++++++ .../providers/forms/ProviderForm.tsx | 64 +++--- src/components/providers/forms/hooks/index.ts | 1 + .../forms/hooks/useCommonConfigSnippet.ts | 198 ++++++++++++++++++ 4 files changed, 373 insertions(+), 35 deletions(-) create mode 100644 src/components/providers/forms/CommonConfigEditor.tsx create mode 100644 src/components/providers/forms/hooks/useCommonConfigSnippet.ts diff --git a/src/components/providers/forms/CommonConfigEditor.tsx b/src/components/providers/forms/CommonConfigEditor.tsx new file mode 100644 index 0000000..665560c --- /dev/null +++ b/src/components/providers/forms/CommonConfigEditor.tsx @@ -0,0 +1,145 @@ +import { useTranslation } from "react-i18next"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Label } from "@/components/ui/label"; +import JsonEditor from "@/components/JsonEditor"; +import { useTheme } from "@/components/theme-provider"; +import { useMemo } from "react"; + +interface CommonConfigEditorProps { + value: string; + onChange: (value: string) => void; + useCommonConfig: boolean; + onCommonConfigToggle: (checked: boolean) => void; + commonConfigSnippet: string; + onCommonConfigSnippetChange: (value: string) => void; + commonConfigError: string; + onEditClick: () => void; + isModalOpen: boolean; + onModalClose: () => void; +} + +export function CommonConfigEditor({ + value, + onChange, + useCommonConfig, + onCommonConfigToggle, + commonConfigSnippet, + onCommonConfigSnippetChange, + commonConfigError, + onEditClick, + isModalOpen, + onModalClose, +}: CommonConfigEditorProps) { + const { t } = useTranslation(); + const { theme } = useTheme(); + + const isDarkMode = useMemo(() => { + if (theme === "dark") return true; + if (theme === "light") return false; + return typeof window !== "undefined" + ? window.document.documentElement.classList.contains("dark") + : false; + }, [theme]); + + return ( + <> +
+
+ +
+ +
+
+
+ +
+ {commonConfigError && !isModalOpen && ( +

+ {commonConfigError} +

+ )} +
+ +
+

+ {t("claudeConfig.fullSettingsHint", { + defaultValue: "请填写完整的 Claude Code 配置", + })} +

+
+ + !open && onModalClose()}> + + + + {t("claudeConfig.editCommonConfigTitle", { + defaultValue: "编辑通用配置片段", + })} + + +
+

+ {t("claudeConfig.commonConfigHint", { + defaultValue: + "通用配置片段将合并到所有启用它的供应商配置中", + })} +

+
+ +
+ {commonConfigError && ( +

+ {commonConfigError} +

+ )} +
+
+
+ + ); +} diff --git a/src/components/providers/forms/ProviderForm.tsx b/src/components/providers/forms/ProviderForm.tsx index 53d7929..155cd7a 100644 --- a/src/components/providers/forms/ProviderForm.tsx +++ b/src/components/providers/forms/ProviderForm.tsx @@ -12,8 +12,6 @@ import { FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; -import { useTheme } from "@/components/theme-provider"; -import JsonEditor from "@/components/JsonEditor"; import { providerSchema, type ProviderFormData } from "@/lib/schemas/provider"; import type { AppType } from "@/lib/api"; import type { ProviderCategory, CustomEndpoint } from "@/types"; @@ -27,6 +25,7 @@ import ApiKeyInput from "@/components/ProviderForm/ApiKeyInput"; import EndpointSpeedTest from "@/components/ProviderForm/EndpointSpeedTest"; import CodexConfigEditor from "@/components/ProviderForm/CodexConfigEditor"; import KimiModelSelector from "@/components/ProviderForm/KimiModelSelector"; +import { CommonConfigEditor } from "./CommonConfigEditor"; import { Zap } from "lucide-react"; import { useProviderCategory, @@ -38,6 +37,7 @@ import { useCustomEndpoints, useKimiModelSelector, useTemplateValues, + useCommonConfigSnippet, } from "./hooks"; const CLAUDE_DEFAULT_CONFIG = JSON.stringify({ env: {}, config: {} }, null, 2); @@ -68,7 +68,6 @@ export function ProviderForm({ initialData, }: ProviderFormProps) { const { t } = useTranslation(); - const { theme } = useTheme(); const isEditMode = Boolean(initialData); const [selectedPresetId, setSelectedPresetId] = useState( @@ -172,14 +171,6 @@ export function ProviderForm({ form.reset(defaultValues); }, [defaultValues, form]); - const isDarkMode = useMemo(() => { - if (theme === "dark") return true; - if (theme === "light") return false; - return typeof window !== "undefined" - ? window.document.documentElement.classList.contains("dark") - : false; - }, [theme]); - const presetCategoryLabels: Record = useMemo( () => ({ official: t("providerPreset.categoryOfficial", { @@ -243,6 +234,21 @@ export function ProviderForm({ onConfigChange: (config) => form.setValue("settingsConfig", config), }); + // 使用通用配置片段 hook (仅 Claude 模式) + const { + useCommonConfig, + commonConfigSnippet, + commonConfigError, + handleCommonConfigToggle, + handleCommonConfigSnippetChange, + } = useCommonConfigSnippet({ + settingsConfig: form.watch("settingsConfig"), + onConfigChange: (config) => form.setValue("settingsConfig", config), + initialData: appType === "claude" ? initialData : undefined, + }); + + const [isCommonConfigModalOpen, setIsCommonConfigModalOpen] = useState(false); + const handleSubmit = (values: ProviderFormData) => { // 验证模板变量(仅 Claude 模式) if (appType === "claude" && templateValueEntries.length > 0) { @@ -790,7 +796,7 @@ export function ProviderForm({ /> )} - {/* 配置编辑器:Claude 使用 JSON 编辑器,Codex 使用专用编辑器 */} + {/* 配置编辑器:Claude 使用通用配置编辑器,Codex 使用专用编辑器 */} {appType === "codex" ? ( ) : ( - ( - - - {t("provider.configJson", { defaultValue: "配置 JSON" })} - - -
- -
-
- -
- )} + form.setValue("settingsConfig", value)} + useCommonConfig={useCommonConfig} + onCommonConfigToggle={handleCommonConfigToggle} + commonConfigSnippet={commonConfigSnippet} + onCommonConfigSnippetChange={handleCommonConfigSnippetChange} + commonConfigError={commonConfigError} + onEditClick={() => setIsCommonConfigModalOpen(true)} + isModalOpen={isCommonConfigModalOpen} + onModalClose={() => setIsCommonConfigModalOpen(false)} /> )} diff --git a/src/components/providers/forms/hooks/index.ts b/src/components/providers/forms/hooks/index.ts index 5b3579b..41745ee 100644 --- a/src/components/providers/forms/hooks/index.ts +++ b/src/components/providers/forms/hooks/index.ts @@ -7,3 +7,4 @@ export { useApiKeyLink } from "./useApiKeyLink"; export { useCustomEndpoints } from "./useCustomEndpoints"; export { useKimiModelSelector } from "./useKimiModelSelector"; export { useTemplateValues } from "./useTemplateValues"; +export { useCommonConfigSnippet } from "./useCommonConfigSnippet"; diff --git a/src/components/providers/forms/hooks/useCommonConfigSnippet.ts b/src/components/providers/forms/hooks/useCommonConfigSnippet.ts new file mode 100644 index 0000000..4548241 --- /dev/null +++ b/src/components/providers/forms/hooks/useCommonConfigSnippet.ts @@ -0,0 +1,198 @@ +import { useState, useEffect, useCallback, useRef } from "react"; +import { + updateCommonConfigSnippet, + hasCommonConfigSnippet, + validateJsonConfig, +} from "@/utils/providerConfigUtils"; + +const COMMON_CONFIG_STORAGE_KEY = "cc-switch:common-config-snippet"; +const DEFAULT_COMMON_CONFIG_SNIPPET = `{ + "includeCoAuthoredBy": false +}`; + +interface UseCommonConfigSnippetProps { + settingsConfig: string; + onConfigChange: (config: string) => void; + initialData?: { + settingsConfig?: Record; + }; +} + +/** + * 管理 Claude 通用配置片段 + */ +export function useCommonConfigSnippet({ + settingsConfig, + onConfigChange, + initialData, +}: UseCommonConfigSnippetProps) { + const [useCommonConfig, setUseCommonConfig] = useState(false); + const [commonConfigSnippet, setCommonConfigSnippetState] = useState( + () => { + if (typeof window === "undefined") { + return DEFAULT_COMMON_CONFIG_SNIPPET; + } + try { + const stored = window.localStorage.getItem(COMMON_CONFIG_STORAGE_KEY); + if (stored && stored.trim()) { + return stored; + } + } catch { + // ignore localStorage 读取失败 + } + return DEFAULT_COMMON_CONFIG_SNIPPET; + }, + ); + const [commonConfigError, setCommonConfigError] = useState(""); + + // 用于跟踪是否正在通过通用配置更新 + const isUpdatingFromCommonConfig = useRef(false); + + // 初始化时检查通用配置片段(编辑模式) + useEffect(() => { + if (initialData) { + const configString = JSON.stringify( + initialData.settingsConfig, + null, + 2, + ); + const hasCommon = hasCommonConfigSnippet( + configString, + commonConfigSnippet, + ); + setUseCommonConfig(hasCommon); + } + }, [initialData, commonConfigSnippet]); + + // 同步本地存储的通用配置片段 + useEffect(() => { + if (typeof window === "undefined") return; + try { + if (commonConfigSnippet.trim()) { + window.localStorage.setItem( + COMMON_CONFIG_STORAGE_KEY, + commonConfigSnippet, + ); + } else { + window.localStorage.removeItem(COMMON_CONFIG_STORAGE_KEY); + } + } catch { + // ignore + } + }, [commonConfigSnippet]); + + // 处理通用配置开关 + const handleCommonConfigToggle = useCallback( + (checked: boolean) => { + 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, commonConfigSnippet, onConfigChange], + ); + + // 处理通用配置片段变化 + const handleCommonConfigSnippetChange = useCallback( + (value: string) => { + const previousSnippet = commonConfigSnippet; + setCommonConfigSnippetState(value); + + if (!value.trim()) { + setCommonConfigError(""); + if (useCommonConfig) { + const { updatedConfig } = updateCommonConfigSnippet( + settingsConfig, + previousSnippet, + false, + ); + onConfigChange(updatedConfig); + setUseCommonConfig(false); + } + return; + } + + // 验证JSON格式 + const validationError = validateJsonConfig(value, "通用配置片段"); + if (validationError) { + setCommonConfigError(validationError); + } else { + setCommonConfigError(""); + } + + // 若当前启用通用配置且格式正确,需要替换为最新片段 + if (useCommonConfig && !validationError) { + const removeResult = updateCommonConfigSnippet( + settingsConfig, + previousSnippet, + false, + ); + if (removeResult.error) { + setCommonConfigError(removeResult.error); + return; + } + const addResult = updateCommonConfigSnippet( + removeResult.updatedConfig, + value, + true, + ); + + if (addResult.error) { + setCommonConfigError(addResult.error); + return; + } + + // 标记正在通过通用配置更新,避免触发状态检查 + isUpdatingFromCommonConfig.current = true; + onConfigChange(addResult.updatedConfig); + // 在下一个事件循环中重置标记 + setTimeout(() => { + isUpdatingFromCommonConfig.current = false; + }, 0); + } + }, + [ + commonConfigSnippet, + settingsConfig, + useCommonConfig, + onConfigChange, + ], + ); + + // 当配置变化时检查是否包含通用配置(但避免在通过通用配置更新时检查) + useEffect(() => { + if (isUpdatingFromCommonConfig.current) { + return; + } + const hasCommon = hasCommonConfigSnippet( + settingsConfig, + commonConfigSnippet, + ); + setUseCommonConfig(hasCommon); + }, [settingsConfig, commonConfigSnippet]); + + return { + useCommonConfig, + commonConfigSnippet, + commonConfigError, + handleCommonConfigToggle, + handleCommonConfigSnippetChange, + }; +}