diff --git a/src/components/providers/forms/ProviderForm.tsx b/src/components/providers/forms/ProviderForm.tsx
index 5d7ffc9..53d7929 100644
--- a/src/components/providers/forms/ProviderForm.tsx
+++ b/src/components/providers/forms/ProviderForm.tsx
@@ -37,6 +37,7 @@ import {
useApiKeyLink,
useCustomEndpoints,
useKimiModelSelector,
+ useTemplateValues,
} from "./hooks";
const CLAUDE_DEFAULT_CONFIG = JSON.stringify({ env: {}, config: {} }, null, 2);
@@ -228,7 +229,36 @@ export function ProviderForm({
: "",
});
+ // 使用模板变量 hook (仅 Claude 模式)
+ const {
+ templateValues,
+ templateValueEntries,
+ selectedPreset: templatePreset,
+ handleTemplateValueChange,
+ validateTemplateValues,
+ } = useTemplateValues({
+ selectedPresetId: appType === "claude" ? selectedPresetId : null,
+ presetEntries: appType === "claude" ? presetEntries : [],
+ settingsConfig: form.watch("settingsConfig"),
+ onConfigChange: (config) => form.setValue("settingsConfig", config),
+ });
+
const handleSubmit = (values: ProviderFormData) => {
+ // 验证模板变量(仅 Claude 模式)
+ if (appType === "claude" && templateValueEntries.length > 0) {
+ const validation = validateTemplateValues();
+ if (!validation.isValid && validation.missingField) {
+ form.setError("settingsConfig", {
+ type: "manual",
+ message: t("providerForm.fillParameter", {
+ label: validation.missingField.label,
+ defaultValue: `请填写 ${validation.missingField.label}`,
+ }),
+ });
+ return;
+ }
+ }
+
let settingsConfig: string;
// Codex: 组合 auth 和 config
@@ -510,6 +540,43 @@ export function ProviderForm({
)}
+ {/* 模板变量输入(仅 Claude 且有模板变量时显示) */}
+ {appType === "claude" && templateValueEntries.length > 0 && (
+
+
+ {t("providerForm.parameterConfig", {
+ name: templatePreset?.name || "",
+ defaultValue: `${templatePreset?.name || ""} 参数配置`,
+ })}
+
+
+ {templateValueEntries.map(([key, config]) => (
+
+
+ {config.label}
+
+
+ handleTemplateValueChange(key, e.target.value)
+ }
+ placeholder={config.placeholder || config.label}
+ autoComplete="off"
+ />
+
+ ))}
+
+
+ )}
+
{/* Base URL 输入框(仅 Claude 第三方/自定义显示) */}
{appType === "claude" && shouldShowSpeedTest && (
diff --git a/src/components/providers/forms/hooks/index.ts b/src/components/providers/forms/hooks/index.ts
index 9b51276..5b3579b 100644
--- a/src/components/providers/forms/hooks/index.ts
+++ b/src/components/providers/forms/hooks/index.ts
@@ -6,3 +6,4 @@ export { useCodexConfigState } from "./useCodexConfigState";
export { useApiKeyLink } from "./useApiKeyLink";
export { useCustomEndpoints } from "./useCustomEndpoints";
export { useKimiModelSelector } from "./useKimiModelSelector";
+export { useTemplateValues } from "./useTemplateValues";
diff --git a/src/components/providers/forms/hooks/useTemplateValues.ts b/src/components/providers/forms/hooks/useTemplateValues.ts
new file mode 100644
index 0000000..22b39cc
--- /dev/null
+++ b/src/components/providers/forms/hooks/useTemplateValues.ts
@@ -0,0 +1,288 @@
+import { useState, useEffect, useCallback, useMemo } from "react";
+import type { ProviderPreset, TemplateValueConfig } from "@/config/providerPresets";
+import type { CodexProviderPreset } from "@/config/codexProviderPresets";
+import { applyTemplateValues } from "@/utils/providerConfigUtils";
+
+type TemplatePath = Array
;
+type TemplateValueMap = Record;
+
+interface PresetEntry {
+ id: string;
+ preset: ProviderPreset | CodexProviderPreset;
+}
+
+interface UseTemplateValuesProps {
+ selectedPresetId: string | null;
+ presetEntries: PresetEntry[];
+ settingsConfig: string;
+ onConfigChange: (config: string) => void;
+}
+
+/**
+ * 收集配置中包含模板占位符的路径
+ */
+const collectTemplatePaths = (
+ source: unknown,
+ templateKeys: string[],
+ currentPath: TemplatePath = [],
+ acc: TemplatePath[] = [],
+): TemplatePath[] => {
+ if (typeof source === "string") {
+ const hasPlaceholder = templateKeys.some((key) =>
+ source.includes(`\${${key}}`),
+ );
+ if (hasPlaceholder) {
+ acc.push([...currentPath]);
+ }
+ return acc;
+ }
+
+ if (Array.isArray(source)) {
+ source.forEach((item, index) =>
+ collectTemplatePaths(item, templateKeys, [...currentPath, index], acc),
+ );
+ return acc;
+ }
+
+ if (source && typeof source === "object") {
+ Object.entries(source).forEach(([key, value]) =>
+ collectTemplatePaths(value, templateKeys, [...currentPath, key], acc),
+ );
+ }
+
+ return acc;
+};
+
+/**
+ * 根据路径获取值
+ */
+const getValueAtPath = (source: any, path: TemplatePath) => {
+ return path.reduce((acc, key) => {
+ if (acc === undefined || acc === null) {
+ return undefined;
+ }
+ return acc[key as keyof typeof acc];
+ }, source);
+};
+
+/**
+ * 根据路径设置值
+ */
+const setValueAtPath = (
+ target: any,
+ path: TemplatePath,
+ value: unknown,
+): any => {
+ if (path.length === 0) {
+ return value;
+ }
+
+ let current = target;
+
+ for (let i = 0; i < path.length - 1; i++) {
+ const key = path[i];
+ const nextKey = path[i + 1];
+ const isNextIndex = typeof nextKey === "number";
+
+ if (current[key as keyof typeof current] === undefined) {
+ current[key as keyof typeof current] = isNextIndex ? [] : {};
+ } else {
+ const currentValue = current[key as keyof typeof current];
+ if (isNextIndex && !Array.isArray(currentValue)) {
+ current[key as keyof typeof current] = [];
+ } else if (
+ !isNextIndex &&
+ (typeof currentValue !== "object" || currentValue === null)
+ ) {
+ current[key as keyof typeof current] = {};
+ }
+ }
+
+ current = current[key as keyof typeof current];
+ }
+
+ const finalKey = path[path.length - 1];
+ current[finalKey as keyof typeof current] = value;
+ return target;
+};
+
+/**
+ * 应用模板值到配置字符串(只更新模板占位符所在的字段)
+ */
+const applyTemplateValuesToConfigString = (
+ presetConfig: any,
+ currentConfigString: string,
+ values: TemplateValueMap,
+) => {
+ const replacedConfig = applyTemplateValues(presetConfig, values);
+ const templateKeys = Object.keys(values);
+ if (templateKeys.length === 0) {
+ return JSON.stringify(replacedConfig, null, 2);
+ }
+
+ const placeholderPaths = collectTemplatePaths(presetConfig, templateKeys);
+
+ try {
+ const parsedConfig = currentConfigString.trim()
+ ? JSON.parse(currentConfigString)
+ : {};
+ let targetConfig: any;
+ if (Array.isArray(parsedConfig)) {
+ targetConfig = [...parsedConfig];
+ } else if (parsedConfig && typeof parsedConfig === "object") {
+ targetConfig = JSON.parse(JSON.stringify(parsedConfig));
+ } else {
+ targetConfig = {};
+ }
+
+ if (placeholderPaths.length === 0) {
+ return JSON.stringify(targetConfig, null, 2);
+ }
+
+ let mutatedConfig = targetConfig;
+
+ for (const path of placeholderPaths) {
+ const nextValue = getValueAtPath(replacedConfig, path);
+ if (path.length === 0) {
+ mutatedConfig = nextValue;
+ } else {
+ setValueAtPath(mutatedConfig, path, nextValue);
+ }
+ }
+
+ return JSON.stringify(mutatedConfig, null, 2);
+ } catch {
+ return JSON.stringify(replacedConfig, null, 2);
+ }
+};
+
+/**
+ * 管理模板变量的状态和逻辑
+ */
+export function useTemplateValues({
+ selectedPresetId,
+ presetEntries,
+ settingsConfig,
+ onConfigChange,
+}: UseTemplateValuesProps) {
+ const [templateValues, setTemplateValues] = useState({});
+
+ // 获取当前选中的预设
+ const selectedPreset = useMemo(() => {
+ if (!selectedPresetId || selectedPresetId === "custom") {
+ return null;
+ }
+ const entry = presetEntries.find((item) => item.id === selectedPresetId);
+ // 只处理 ProviderPreset (Claude 预设)
+ if (entry && "settingsConfig" in entry.preset) {
+ return entry.preset as ProviderPreset;
+ }
+ return null;
+ }, [selectedPresetId, presetEntries]);
+
+ // 获取模板变量条目
+ const templateValueEntries = useMemo(() => {
+ if (!selectedPreset?.templateValues) {
+ return [];
+ }
+ return Object.entries(selectedPreset.templateValues) as Array<
+ [string, TemplateValueConfig]
+ >;
+ }, [selectedPreset]);
+
+ // 当选择预设时,初始化模板值
+ useEffect(() => {
+ if (selectedPreset?.templateValues) {
+ const initialValues = Object.fromEntries(
+ Object.entries(selectedPreset.templateValues).map(([key, config]) => [
+ key,
+ {
+ ...config,
+ editorValue: config.editorValue || config.defaultValue || "",
+ },
+ ]),
+ );
+ setTemplateValues(initialValues);
+ } else {
+ setTemplateValues({});
+ }
+ }, [selectedPreset]);
+
+ // 处理模板值变化
+ const handleTemplateValueChange = useCallback(
+ (key: string, value: string) => {
+ if (!selectedPreset?.templateValues) {
+ return;
+ }
+
+ const config = selectedPreset.templateValues[key];
+ if (!config) {
+ return;
+ }
+
+ setTemplateValues((prev) => {
+ const prevEntry = prev[key];
+ const nextEntry: TemplateValueConfig = {
+ ...config,
+ ...(prevEntry ?? {}),
+ editorValue: value,
+ };
+ const nextValues: TemplateValueMap = {
+ ...prev,
+ [key]: nextEntry,
+ };
+
+ // 应用模板值到配置
+ try {
+ const configString = applyTemplateValuesToConfigString(
+ selectedPreset.settingsConfig,
+ settingsConfig,
+ nextValues,
+ );
+ onConfigChange(configString);
+ } catch (err) {
+ console.error("更新模板值失败:", err);
+ }
+
+ return nextValues;
+ });
+ },
+ [selectedPreset, settingsConfig, onConfigChange],
+ );
+
+ // 验证所有模板值是否已填写
+ const validateTemplateValues = useCallback((): {
+ isValid: boolean;
+ missingField?: { key: string; label: string };
+ } => {
+ if (templateValueEntries.length === 0) {
+ return { isValid: true };
+ }
+
+ for (const [key, config] of templateValueEntries) {
+ const entry = templateValues[key];
+ const resolvedValue = (
+ entry?.editorValue ??
+ entry?.defaultValue ??
+ config.defaultValue ??
+ ""
+ ).trim();
+ if (!resolvedValue) {
+ return {
+ isValid: false,
+ missingField: { key, label: config.label },
+ };
+ }
+ }
+
+ return { isValid: true };
+ }, [templateValueEntries, templateValues]);
+
+ return {
+ templateValues,
+ templateValueEntries,
+ selectedPreset,
+ handleTemplateValueChange,
+ validateTemplateValues,
+ };
+}