diff --git a/relay/common/override.go b/relay/common/override.go index 31101ef01..e9196e5b7 100644 --- a/relay/common/override.go +++ b/relay/common/override.go @@ -34,7 +34,7 @@ type ConditionOperation struct { type ParamOperation struct { Path string `json:"path"` - Mode string `json:"mode"` // delete, set, move, copy, prepend, append, trim_prefix, trim_suffix, ensure_prefix, ensure_suffix, trim_space, to_lower, to_upper, replace, regex_replace, return_error, prune_objects, set_header, delete_header, copy_header, move_header, sync_fields + Mode string `json:"mode"` // delete, set, move, copy, prepend, append, trim_prefix, trim_suffix, ensure_prefix, ensure_suffix, trim_space, to_lower, to_upper, replace, regex_replace, return_error, prune_objects, set_header, delete_header, copy_header, move_header, pass_headers, sync_fields Value interface{} `json:"value"` KeepOrigin bool `json:"keep_origin"` From string `json:"from,omitempty"` @@ -494,6 +494,19 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte if err == nil { contextJSON, err = marshalContextJSON(context) } + case "pass_headers": + headerNames, parseErr := parseHeaderPassThroughNames(op.Value) + if parseErr != nil { + return "", parseErr + } + for _, headerName := range headerNames { + if err = copyHeaderInContext(context, headerName, headerName, op.KeepOrigin); err != nil { + break + } + } + if err == nil { + contextJSON, err = marshalContextJSON(context) + } case "sync_fields": result, err = syncFieldsBetweenTargets(result, context, op.From, op.To) if err == nil { @@ -678,6 +691,80 @@ func deleteHeaderOverrideInContext(context map[string]interface{}, headerName st return nil } +func parseHeaderPassThroughNames(value interface{}) ([]string, error) { + normalizeNames := func(values []string) []string { + names := lo.FilterMap(values, func(item string, _ int) (string, bool) { + headerName := strings.TrimSpace(item) + if headerName == "" { + return "", false + } + return headerName, true + }) + return lo.Uniq(names) + } + + switch raw := value.(type) { + case nil: + return nil, fmt.Errorf("pass_headers value is required") + case string: + trimmed := strings.TrimSpace(raw) + if trimmed == "" { + return nil, fmt.Errorf("pass_headers value is required") + } + if strings.HasPrefix(trimmed, "[") || strings.HasPrefix(trimmed, "{") { + var parsed interface{} + if err := common.UnmarshalJsonStr(trimmed, &parsed); err == nil { + return parseHeaderPassThroughNames(parsed) + } + } + names := normalizeNames(strings.Split(trimmed, ",")) + if len(names) == 0 { + return nil, fmt.Errorf("pass_headers value is invalid") + } + return names, nil + case []interface{}: + names := lo.FilterMap(raw, func(item interface{}, _ int) (string, bool) { + headerName := strings.TrimSpace(fmt.Sprintf("%v", item)) + if headerName == "" { + return "", false + } + return headerName, true + }) + names = lo.Uniq(names) + if len(names) == 0 { + return nil, fmt.Errorf("pass_headers value is invalid") + } + return names, nil + case map[string]interface{}: + candidates := make([]string, 0, 8) + if headersRaw, ok := raw["headers"]; ok { + names, err := parseHeaderPassThroughNames(headersRaw) + if err == nil { + candidates = append(candidates, names...) + } + } + if namesRaw, ok := raw["names"]; ok { + names, err := parseHeaderPassThroughNames(namesRaw) + if err == nil { + candidates = append(candidates, names...) + } + } + if headerRaw, ok := raw["header"]; ok { + names, err := parseHeaderPassThroughNames(headerRaw) + if err == nil { + candidates = append(candidates, names...) + } + } + names := normalizeNames(candidates) + if len(names) == 0 { + return nil, fmt.Errorf("pass_headers value is invalid") + } + return names, nil + default: + return nil, fmt.Errorf("pass_headers value must be string, array or object") + } +} + type syncTarget struct { kind string key string diff --git a/web/src/components/table/channels/modals/EditChannelModal.jsx b/web/src/components/table/channels/modals/EditChannelModal.jsx index d2e77cf7e..f54b6c41a 100644 --- a/web/src/components/table/channels/modals/EditChannelModal.jsx +++ b/web/src/components/table/channels/modals/EditChannelModal.jsx @@ -71,6 +71,7 @@ import { IconServer, IconSetting, IconCode, + IconCopy, IconGlobe, IconBolt, IconSearch, @@ -95,6 +96,28 @@ const REGION_EXAMPLE = { 'claude-3-5-sonnet-20240620': 'europe-west1', }; +const PARAM_OVERRIDE_LEGACY_TEMPLATE = { + temperature: 0, +}; + +const PARAM_OVERRIDE_OPERATIONS_TEMPLATE = { + operations: [ + { + path: 'temperature', + mode: 'set', + value: 0.7, + conditions: [ + { + path: 'model', + mode: 'prefix', + value: 'openai/', + }, + ], + logic: 'AND', + }, + ], +}; + // 支持并且已适配通过接口获取模型列表的渠道类型 const MODEL_FETCHABLE_TYPES = new Set([ 1, 4, 14, 34, 17, 26, 27, 24, 47, 25, 20, 23, 31, 40, 42, 48, 43, @@ -270,7 +293,7 @@ const EditChannelModal = (props) => { }; } return { - tagLabel: 'Custom JSON', + tagLabel: t('自定义 JSON'), tagColor: 'orange', preview: pretty, }; @@ -608,6 +631,100 @@ const EditChannelModal = (props) => { } }; + const copyParamOverrideJson = async () => { + const raw = + typeof inputs.param_override === 'string' + ? inputs.param_override.trim() + : ''; + if (!raw) { + showInfo(t('暂无可复制 JSON')); + return; + } + + let content = raw; + if (verifyJSON(raw)) { + try { + content = JSON.stringify(JSON.parse(raw), null, 2); + } catch (error) { + content = raw; + } + } + + const ok = await copy(content); + if (ok) { + showSuccess(t('参数覆盖 JSON 已复制')); + } else { + showError(t('复制失败')); + } + }; + + const parseParamOverrideInput = () => { + const raw = + typeof inputs.param_override === 'string' + ? inputs.param_override.trim() + : ''; + if (!raw) return null; + if (!verifyJSON(raw)) { + throw new Error(t('当前参数覆盖不是合法的 JSON')); + } + return JSON.parse(raw); + }; + + const applyParamOverrideTemplate = ( + templateType = 'operations', + applyMode = 'fill', + ) => { + try { + const parsedCurrent = parseParamOverrideInput(); + if (templateType === 'legacy') { + if (applyMode === 'fill') { + handleInputChange( + 'param_override', + JSON.stringify(PARAM_OVERRIDE_LEGACY_TEMPLATE, null, 2), + ); + return; + } + const currentLegacy = + parsedCurrent && + typeof parsedCurrent === 'object' && + !Array.isArray(parsedCurrent) && + !Array.isArray(parsedCurrent.operations) + ? parsedCurrent + : {}; + const merged = { + ...PARAM_OVERRIDE_LEGACY_TEMPLATE, + ...currentLegacy, + }; + handleInputChange('param_override', JSON.stringify(merged, null, 2)); + return; + } + + if (applyMode === 'fill') { + handleInputChange( + 'param_override', + JSON.stringify(PARAM_OVERRIDE_OPERATIONS_TEMPLATE, null, 2), + ); + return; + } + const currentOperations = + parsedCurrent && + typeof parsedCurrent === 'object' && + !Array.isArray(parsedCurrent) && + Array.isArray(parsedCurrent.operations) + ? parsedCurrent.operations + : []; + const merged = { + operations: [ + ...currentOperations, + ...PARAM_OVERRIDE_OPERATIONS_TEMPLATE.operations, + ], + }; + handleInputChange('param_override', JSON.stringify(merged, null, 2)); + } catch (error) { + showError(error.message || t('模板应用失败')); + } + }; + const loadChannel = async () => { setLoading(true); let res = await API.get(`/api/channel/${channelId}`); @@ -3119,51 +3236,18 @@ const EditChannelModal = (props) => { - @@ -3171,20 +3255,33 @@ const EditChannelModal = (props) => { {t('此项可选,用于覆盖请求参数。不支持覆盖 stream 参数')}
{paramOverrideMeta.tagLabel} - + + + +
                           {paramOverrideMeta.preview}
diff --git a/web/src/components/table/channels/modals/ParamOverrideEditorModal.jsx b/web/src/components/table/channels/modals/ParamOverrideEditorModal.jsx
index e33d68bef..832c75833 100644
--- a/web/src/components/table/channels/modals/ParamOverrideEditorModal.jsx
+++ b/web/src/components/table/channels/modals/ParamOverrideEditorModal.jsx
@@ -35,33 +35,34 @@ import {
   Typography,
 } from '@douyinfe/semi-ui';
 import { IconDelete, IconPlus } from '@douyinfe/semi-icons';
-import { showError, verifyJSON } from '../../../../helpers';
+import { copy, showError, showSuccess, verifyJSON } from '../../../../helpers';
 
 const { Text } = Typography;
 
 const OPERATION_MODE_OPTIONS = [
-  { label: 'JSON · set', value: 'set' },
-  { label: 'JSON · delete', value: 'delete' },
-  { label: 'JSON · append', value: 'append' },
-  { label: 'JSON · prepend', value: 'prepend' },
-  { label: 'JSON · copy', value: 'copy' },
-  { label: 'JSON · move', value: 'move' },
-  { label: 'JSON · replace', value: 'replace' },
-  { label: 'JSON · regex_replace', value: 'regex_replace' },
-  { label: 'JSON · trim_prefix', value: 'trim_prefix' },
-  { label: 'JSON · trim_suffix', value: 'trim_suffix' },
-  { label: 'JSON · ensure_prefix', value: 'ensure_prefix' },
-  { label: 'JSON · ensure_suffix', value: 'ensure_suffix' },
-  { label: 'JSON · trim_space', value: 'trim_space' },
-  { label: 'JSON · to_lower', value: 'to_lower' },
-  { label: 'JSON · to_upper', value: 'to_upper' },
-  { label: 'Control · return_error', value: 'return_error' },
-  { label: 'Control · prune_objects', value: 'prune_objects' },
-  { label: 'Control · sync_fields', value: 'sync_fields' },
-  { label: 'Header · set_header', value: 'set_header' },
-  { label: 'Header · delete_header', value: 'delete_header' },
-  { label: 'Header · copy_header', value: 'copy_header' },
-  { label: 'Header · move_header', value: 'move_header' },
+  { label: '设置字段', value: 'set' },
+  { label: '删除字段', value: 'delete' },
+  { label: '追加到末尾', value: 'append' },
+  { label: '追加到开头', value: 'prepend' },
+  { label: '复制字段', value: 'copy' },
+  { label: '移动字段', value: 'move' },
+  { label: '字符串替换', value: 'replace' },
+  { label: '正则替换', value: 'regex_replace' },
+  { label: '裁剪前缀', value: 'trim_prefix' },
+  { label: '裁剪后缀', value: 'trim_suffix' },
+  { label: '确保前缀', value: 'ensure_prefix' },
+  { label: '确保后缀', value: 'ensure_suffix' },
+  { label: '去掉空白', value: 'trim_space' },
+  { label: '转小写', value: 'to_lower' },
+  { label: '转大写', value: 'to_upper' },
+  { label: '返回自定义错误', value: 'return_error' },
+  { label: '清理对象项', value: 'prune_objects' },
+  { label: '请求头透传', value: 'pass_headers' },
+  { label: '字段同步', value: 'sync_fields' },
+  { label: '设置请求头', value: 'set_header' },
+  { label: '删除请求头', value: 'delete_header' },
+  { label: '复制请求头', value: 'copy_header' },
+  { label: '移动请求头', value: 'move_header' },
 ];
 
 const OPERATION_MODE_VALUES = new Set(
@@ -69,14 +70,14 @@ const OPERATION_MODE_VALUES = new Set(
 );
 
 const CONDITION_MODE_OPTIONS = [
-  { label: 'full', value: 'full' },
-  { label: 'prefix', value: 'prefix' },
-  { label: 'suffix', value: 'suffix' },
-  { label: 'contains', value: 'contains' },
-  { label: 'gt', value: 'gt' },
-  { label: 'gte', value: 'gte' },
-  { label: 'lt', value: 'lt' },
-  { label: 'lte', value: 'lte' },
+  { label: '完全匹配', value: 'full' },
+  { label: '前缀匹配', value: 'prefix' },
+  { label: '后缀匹配', value: 'suffix' },
+  { label: '包含', value: 'contains' },
+  { label: '大于', value: 'gt' },
+  { label: '大于等于', value: 'gte' },
+  { label: '小于', value: 'lt' },
+  { label: '小于等于', value: 'lte' },
 ];
 
 const CONDITION_MODE_VALUES = new Set(
@@ -101,6 +102,7 @@ const MODE_META = {
   to_upper: { path: true },
   return_error: { value: true },
   prune_objects: { pathOptional: true, value: true },
+  pass_headers: { value: true, keepOrigin: true },
   sync_fields: { from: true, to: true },
   set_header: { path: true, value: true, keepOrigin: true },
   delete_header: { path: true },
@@ -116,6 +118,7 @@ const VALUE_REQUIRED_MODES = new Set([
   'set_header',
   'return_error',
   'prune_objects',
+  'pass_headers',
 ]);
 
 const FROM_REQUIRED_MODES = new Set([
@@ -137,49 +140,112 @@ const TO_REQUIRED_MODES = new Set([
 ]);
 
 const MODE_DESCRIPTIONS = {
-  set: 'Set JSON value at path',
-  delete: 'Delete JSON field at path',
-  append: 'Append value to array/string/object',
-  prepend: 'Prepend value to array/string/object',
-  copy: 'Copy JSON value from from -> to',
-  move: 'Move JSON value from from -> to',
-  replace: 'String replace on target path',
-  regex_replace: 'Regex replace on target path',
-  trim_prefix: 'Trim prefix on string value',
-  trim_suffix: 'Trim suffix on string value',
-  ensure_prefix: 'Ensure string starts with prefix',
-  ensure_suffix: 'Ensure string ends with suffix',
-  trim_space: 'Trim spaces/newlines on string value',
-  to_lower: 'Convert string to lower case',
-  to_upper: 'Convert string to upper case',
-  return_error: 'Stop processing and return custom error',
-  prune_objects: 'Remove objects matching conditions',
-  sync_fields: 'Sync two fields when one exists and the other is missing',
-  set_header: 'Set runtime override header',
-  delete_header: 'Delete runtime override header',
-  copy_header: 'Copy header from from -> to',
-  move_header: 'Move header from from -> to',
+  set: '把值写入目标字段',
+  delete: '删除目标字段',
+  append: '把值追加到数组 / 字符串 / 对象末尾',
+  prepend: '把值追加到数组 / 字符串 / 对象开头',
+  copy: '把来源字段复制到目标字段',
+  move: '把来源字段移动到目标字段',
+  replace: '在目标字段里做字符串替换',
+  regex_replace: '在目标字段里做正则替换',
+  trim_prefix: '去掉字符串前缀',
+  trim_suffix: '去掉字符串后缀',
+  ensure_prefix: '确保字符串有指定前缀',
+  ensure_suffix: '确保字符串有指定后缀',
+  trim_space: '去掉字符串头尾空白',
+  to_lower: '把字符串转成小写',
+  to_upper: '把字符串转成大写',
+  return_error: '立即返回自定义错误',
+  prune_objects: '按条件清理对象中的子项',
+  pass_headers: '把指定请求头透传到上游请求',
+  sync_fields: '在一个字段有值、另一个缺失时自动补齐',
+  set_header: '设置运行期请求头',
+  delete_header: '删除运行期请求头',
+  copy_header: '复制请求头',
+  move_header: '移动请求头',
+};
+
+const getModePathLabel = (mode) => {
+  if (mode === 'set_header' || mode === 'delete_header') {
+    return '请求头名称';
+  }
+  if (mode === 'prune_objects') {
+    return '目标路径(可选)';
+  }
+  return '目标字段路径';
+};
+
+const getModePathPlaceholder = (mode) => {
+  if (mode === 'set_header') return 'Authorization';
+  if (mode === 'delete_header') return 'X-Debug-Mode';
+  if (mode === 'prune_objects') return 'messages';
+  return 'temperature';
+};
+
+const getModeFromLabel = (mode) => {
+  if (mode === 'replace') return '匹配文本';
+  if (mode === 'regex_replace') return '正则表达式';
+  if (mode === 'copy_header' || mode === 'move_header') return '来源请求头';
+  return '来源字段';
+};
+
+const getModeFromPlaceholder = (mode) => {
+  if (mode === 'replace') return 'openai/';
+  if (mode === 'regex_replace') return '^gpt-';
+  if (mode === 'copy_header' || mode === 'move_header') return 'Authorization';
+  return 'model';
+};
+
+const getModeToLabel = (mode) => {
+  if (mode === 'replace' || mode === 'regex_replace') return '替换为';
+  if (mode === 'copy_header' || mode === 'move_header') return '目标请求头';
+  return '目标字段';
+};
+
+const getModeToPlaceholder = (mode) => {
+  if (mode === 'replace') return '(可留空)';
+  if (mode === 'regex_replace') return 'openai/gpt-';
+  if (mode === 'copy_header' || mode === 'move_header') return 'X-Upstream-Auth';
+  return 'original_model';
+};
+
+const getModeValueLabel = (mode) => {
+  if (mode === 'set_header') return '请求头值';
+  if (mode === 'pass_headers') return '透传请求头(支持逗号分隔或 JSON 数组)';
+  if (
+    mode === 'trim_prefix' ||
+    mode === 'trim_suffix' ||
+    mode === 'ensure_prefix' ||
+    mode === 'ensure_suffix'
+  ) {
+    return '前后缀文本';
+  }
+  if (mode === 'prune_objects') {
+    return '清理规则(字符串或 JSON 对象)';
+  }
+  return '值(支持 JSON 或普通文本)';
+};
+
+const getModeValuePlaceholder = (mode) => {
+  if (mode === 'set_header') return 'Bearer sk-xxx';
+  if (mode === 'pass_headers') return 'Authorization, X-Request-Id';
+  if (
+    mode === 'trim_prefix' ||
+    mode === 'trim_suffix' ||
+    mode === 'ensure_prefix' ||
+    mode === 'ensure_suffix'
+  ) {
+    return 'openai/';
+  }
+  if (mode === 'prune_objects') {
+    return '{"type":"redacted_thinking"}';
+  }
+  return '0.7';
 };
 
 const SYNC_TARGET_TYPE_OPTIONS = [
-  { label: 'JSON', value: 'json' },
-  { label: 'Header', value: 'header' },
-];
-
-const OPERATION_PATH_SUGGESTIONS = [
-  'model',
-  'temperature',
-  'max_tokens',
-  'messages.-1.content',
-  'metadata.conversation_id',
-];
-
-const CONDITION_PATH_SUGGESTIONS = [
-  'model',
-  'retry.is_retry',
-  'last_error.code',
-  'request_headers.authorization',
-  'header_override_normalized.x_debug_mode',
+  { label: '请求体字段', value: 'json' },
+  { label: '请求头字段', value: 'header' },
 ];
 
 const LEGACY_TEMPLATE = {
@@ -197,7 +263,7 @@ const OPERATION_TEMPLATE = {
         {
           path: 'model',
           mode: 'prefix',
-          value: 'gpt',
+          value: 'openai/',
         },
       ],
       logic: 'AND',
@@ -205,11 +271,120 @@ const OPERATION_TEMPLATE = {
   ],
 };
 
-const TEMPLATE_LIBRARY_OPTIONS = [
-  { label: 'Template · Operations', value: 'operations' },
-  { label: 'Template · Legacy Object', value: 'legacy' },
+const HEADER_PASSTHROUGH_TEMPLATE = {
+  operations: [
+    {
+      mode: 'pass_headers',
+      value: ['Authorization'],
+      keep_origin: true,
+    },
+  ],
+};
+
+const GEMINI_IMAGE_4K_TEMPLATE = {
+  operations: [
+    {
+      mode: 'set',
+      path: 'generationConfig.imageConfig.imageSize',
+      value: '4K',
+      conditions: [
+        {
+          path: 'original_model',
+          mode: 'contains',
+          value: 'gemini-3-pro-image-preview',
+        },
+      ],
+      logic: 'AND',
+    },
+  ],
+};
+
+const TEMPLATE_GROUP_OPTIONS = [
+  { label: '基础模板', value: 'basic' },
+  { label: '场景模板', value: 'scenario' },
 ];
 
+const TEMPLATE_PRESET_CONFIG = {
+  operations_default: {
+    group: 'basic',
+    label: '新格式模板(规则集)',
+    kind: 'operations',
+    payload: OPERATION_TEMPLATE,
+  },
+  legacy_default: {
+    group: 'basic',
+    label: '旧格式模板(JSON 对象)',
+    kind: 'legacy',
+    payload: LEGACY_TEMPLATE,
+  },
+  pass_headers_auth: {
+    group: 'scenario',
+    label: '请求头透传(Authorization)',
+    kind: 'operations',
+    payload: HEADER_PASSTHROUGH_TEMPLATE,
+  },
+  gemini_image_4k: {
+    group: 'scenario',
+    label: 'Gemini 图片 4K',
+    kind: 'operations',
+    payload: GEMINI_IMAGE_4K_TEMPLATE,
+  },
+};
+
+const FIELD_GUIDE_TARGET_OPTIONS = [
+  { label: '填入目标路径', value: 'path' },
+  { label: '填入来源字段', value: 'from' },
+  { label: '填入目标字段', value: 'to' },
+];
+
+const BUILTIN_FIELD_SECTIONS = [
+  {
+    title: '常用请求字段',
+    fields: [
+      {
+        key: 'model',
+        label: '模型名称',
+        tip: '支持多级模型名,例如 openai/gpt-4o-mini',
+      },
+      { key: 'temperature', label: '采样温度', tip: '控制输出随机性' },
+      { key: 'max_tokens', label: '最大输出 Token', tip: '控制输出长度上限' },
+      { key: 'messages.-1.content', label: '最后一条消息内容', tip: '常用于重写用户输入' },
+    ],
+  },
+  {
+    title: '上下文字段',
+    fields: [
+      { key: 'retry.is_retry', label: '是否重试', tip: 'true 表示重试请求' },
+      { key: 'last_error.code', label: '上次错误码', tip: '配合重试策略使用' },
+      {
+        key: 'metadata.conversation_id',
+        label: '会话 ID',
+        tip: '可用于路由或缓存命中',
+      },
+    ],
+  },
+  {
+    title: '请求头映射字段',
+    fields: [
+      {
+        key: 'header_override_normalized.authorization',
+        label: '标准化 Authorization',
+        tip: '统一小写后可稳定匹配',
+      },
+      {
+        key: 'header_override_normalized.x_debug_mode',
+        label: '标准化 X-Debug-Mode',
+        tip: '适合灰度 / 调试开关判断',
+      },
+    ],
+  },
+];
+
+const OPERATION_MODE_LABEL_MAP = OPERATION_MODE_OPTIONS.reduce((acc, item) => {
+  acc[item.value] = item.label;
+  return acc;
+}, {});
+
 let localIdSeed = 0;
 const nextLocalId = () => `param_override_${Date.now()}_${localIdSeed++}`;
 
@@ -233,6 +408,252 @@ const parseLooseValue = (valueText) => {
   }
 };
 
+const parsePassHeaderNames = (rawValue) => {
+  if (Array.isArray(rawValue)) {
+    return rawValue
+      .map((item) => String(item ?? '').trim())
+      .filter(Boolean);
+  }
+  if (rawValue && typeof rawValue === 'object') {
+    if (Array.isArray(rawValue.headers)) {
+      return rawValue.headers
+        .map((item) => String(item ?? '').trim())
+        .filter(Boolean);
+    }
+    if (rawValue.header !== undefined) {
+      const single = String(rawValue.header ?? '').trim();
+      return single ? [single] : [];
+    }
+    return [];
+  }
+  if (typeof rawValue === 'string') {
+    return rawValue
+      .split(',')
+      .map((item) => item.trim())
+      .filter(Boolean);
+  }
+  return [];
+};
+
+const parseReturnErrorDraft = (valueText) => {
+  const defaults = {
+    message: '',
+    statusCode: 400,
+    code: '',
+    type: '',
+    skipRetry: true,
+    simpleMode: true,
+  };
+
+  const raw = String(valueText ?? '').trim();
+  if (!raw) {
+    return defaults;
+  }
+
+  try {
+    const parsed = JSON.parse(raw);
+    if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
+      const statusRaw =
+        parsed.status_code !== undefined ? parsed.status_code : parsed.status;
+      const statusValue = Number(statusRaw);
+      return {
+        ...defaults,
+        message: String(parsed.message || parsed.msg || '').trim(),
+        statusCode:
+          Number.isInteger(statusValue) &&
+          statusValue >= 100 &&
+          statusValue <= 599
+            ? statusValue
+            : 400,
+        code: String(parsed.code || '').trim(),
+        type: String(parsed.type || '').trim(),
+        skipRetry: parsed.skip_retry !== false,
+        simpleMode: false,
+      };
+    }
+  } catch (error) {
+    // treat as plain text message
+  }
+
+  return {
+    ...defaults,
+    message: raw,
+    simpleMode: true,
+  };
+};
+
+const buildReturnErrorValueText = (draft = {}) => {
+  const message = String(draft.message || '').trim();
+  if (draft.simpleMode) {
+    return message;
+  }
+
+  const statusCode = Number(draft.statusCode);
+  const payload = {
+    message,
+    status_code:
+      Number.isInteger(statusCode) && statusCode >= 100 && statusCode <= 599
+        ? statusCode
+        : 400,
+  };
+  const code = String(draft.code || '').trim();
+  const type = String(draft.type || '').trim();
+  if (code) payload.code = code;
+  if (type) payload.type = type;
+  if (draft.skipRetry === false) {
+    payload.skip_retry = false;
+  }
+  return JSON.stringify(payload);
+};
+
+const normalizePruneRule = (rule = {}) => ({
+  id: nextLocalId(),
+  path: typeof rule.path === 'string' ? rule.path : '',
+  mode: CONDITION_MODE_VALUES.has(rule.mode) ? rule.mode : 'full',
+  value_text: toValueText(rule.value),
+  invert: rule.invert === true,
+  pass_missing_key: rule.pass_missing_key === true,
+});
+
+const parsePruneObjectsDraft = (valueText) => {
+  const defaults = {
+    simpleMode: true,
+    typeText: '',
+    logic: 'AND',
+    recursive: true,
+    rules: [],
+  };
+
+  const raw = String(valueText ?? '').trim();
+  if (!raw) {
+    return defaults;
+  }
+
+  try {
+    const parsed = JSON.parse(raw);
+    if (typeof parsed === 'string') {
+      return {
+        ...defaults,
+        simpleMode: true,
+        typeText: parsed.trim(),
+      };
+    }
+    if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
+      const rules = [];
+      if (parsed.where && typeof parsed.where === 'object' && !Array.isArray(parsed.where)) {
+        Object.entries(parsed.where).forEach(([path, value]) => {
+          rules.push(
+            normalizePruneRule({
+              path,
+              mode: 'full',
+              value,
+            }),
+          );
+        });
+      }
+      if (Array.isArray(parsed.conditions)) {
+        parsed.conditions.forEach((item) => {
+          if (item && typeof item === 'object') {
+            rules.push(normalizePruneRule(item));
+          }
+        });
+      } else if (
+        parsed.conditions &&
+        typeof parsed.conditions === 'object' &&
+        !Array.isArray(parsed.conditions)
+      ) {
+        Object.entries(parsed.conditions).forEach(([path, value]) => {
+          rules.push(
+            normalizePruneRule({
+              path,
+              mode: 'full',
+              value,
+            }),
+          );
+        });
+      }
+
+      const typeText =
+        parsed.type === undefined ? '' : String(parsed.type).trim();
+      const logic =
+        String(parsed.logic || 'AND').toUpperCase() === 'OR' ? 'OR' : 'AND';
+      const recursive = parsed.recursive !== false;
+      const hasAdvancedFields =
+        parsed.logic !== undefined ||
+        parsed.recursive !== undefined ||
+        parsed.where !== undefined ||
+        parsed.conditions !== undefined;
+
+      return {
+        ...defaults,
+        simpleMode: !hasAdvancedFields,
+        typeText,
+        logic,
+        recursive,
+        rules,
+      };
+    }
+    return {
+      ...defaults,
+      simpleMode: true,
+      typeText: String(parsed ?? '').trim(),
+    };
+  } catch (error) {
+    return {
+      ...defaults,
+      simpleMode: true,
+      typeText: raw,
+    };
+  }
+};
+
+const buildPruneObjectsValueText = (draft = {}) => {
+  const typeText = String(draft.typeText || '').trim();
+  if (draft.simpleMode) {
+    return typeText;
+  }
+
+  const payload = {};
+  if (typeText) {
+    payload.type = typeText;
+  }
+  if (String(draft.logic || 'AND').toUpperCase() === 'OR') {
+    payload.logic = 'OR';
+  }
+  if (draft.recursive === false) {
+    payload.recursive = false;
+  }
+
+  const conditions = (draft.rules || [])
+    .filter((rule) => String(rule.path || '').trim())
+    .map((rule) => {
+      const conditionPayload = {
+        path: String(rule.path || '').trim(),
+        mode: CONDITION_MODE_VALUES.has(rule.mode) ? rule.mode : 'full',
+      };
+      const valueRaw = String(rule.value_text || '').trim();
+      if (valueRaw !== '') {
+        conditionPayload.value = parseLooseValue(valueRaw);
+      }
+      if (rule.invert) {
+        conditionPayload.invert = true;
+      }
+      if (rule.pass_missing_key) {
+        conditionPayload.pass_missing_key = true;
+      }
+      return conditionPayload;
+    });
+
+  if (conditions.length > 0) {
+    payload.conditions = conditions;
+  }
+
+  if (!payload.type && !payload.conditions) {
+    return JSON.stringify({ logic: 'AND' });
+  }
+  return JSON.stringify(payload);
+};
+
 const parseSyncTargetSpec = (spec) => {
   const raw = String(spec ?? '').trim();
   if (!raw) return { type: 'json', key: '' };
@@ -282,15 +703,16 @@ const createDefaultOperation = () => normalizeOperation({ mode: 'set' });
 
 const getOperationSummary = (operation = {}, index = 0) => {
   const mode = operation.mode || 'set';
+  const modeLabel = OPERATION_MODE_LABEL_MAP[mode] || mode;
   if (mode === 'sync_fields') {
     const from = String(operation.from || '').trim();
     const to = String(operation.to || '').trim();
-    return `${index + 1}. ${mode} · ${from || to || '-'}`;
+    return `${index + 1}. ${modeLabel} · ${from || to || '-'}`;
   }
   const path = String(operation.path || '').trim();
   const from = String(operation.from || '').trim();
   const to = String(operation.to || '').trim();
-  return `${index + 1}. ${mode} · ${path || from || to || '-'}`;
+  return `${index + 1}. ${modeLabel} · ${path || from || to || '-'}`;
 };
 
 const getOperationModeTagColor = (mode = 'set') => {
@@ -323,7 +745,7 @@ const parseInitialState = (rawValue) => {
       legacyValue: '',
       operations: [createDefaultOperation()],
       jsonText: text,
-      jsonError: 'JSON format is invalid',
+      jsonError: 'JSON 格式不正确',
     };
   }
 
@@ -414,46 +836,92 @@ const validateOperations = (operations, t) => {
     const toValue = op.to.trim();
 
     if (meta.path && !pathValue) {
-      return t('第 {{line}} 条操作缺少 path', { line });
+      return t('第 {{line}} 条操作缺少目标路径', { line });
     }
     if (FROM_REQUIRED_MODES.has(mode) && !fromValue) {
       if (!(meta.pathAlias && pathValue)) {
-        return t('第 {{line}} 条操作缺少 from', { line });
+        return t('第 {{line}} 条操作缺少来源字段', { line });
       }
     }
     if (TO_REQUIRED_MODES.has(mode) && !toValue) {
       if (!(meta.pathAlias && pathValue)) {
-        return t('第 {{line}} 条操作缺少 to', { line });
+        return t('第 {{line}} 条操作缺少目标字段', { line });
       }
     }
     if (meta.from && !fromValue) {
-      return t('第 {{line}} 条操作缺少 from', { line });
+      return t('第 {{line}} 条操作缺少来源字段', { line });
     }
     if (meta.to && !toValue) {
-      return t('第 {{line}} 条操作缺少 to', { line });
+      return t('第 {{line}} 条操作缺少目标字段', { line });
     }
     if (
       VALUE_REQUIRED_MODES.has(mode) &&
       String(op.value_text ?? '').trim() === ''
     ) {
-      return t('第 {{line}} 条操作缺少 value', { line });
+      return t('第 {{line}} 条操作缺少值', { line });
     }
     if (mode === 'return_error') {
       const raw = String(op.value_text ?? '').trim();
       if (!raw) {
-        return t('第 {{line}} 条操作缺少 value', { line });
+        return t('第 {{line}} 条操作缺少值', { line });
       }
       try {
         const parsed = JSON.parse(raw);
         if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
           if (!String(parsed.message || '').trim()) {
-            return t('第 {{line}} 条 return_error 需要 message', { line });
+            return t('第 {{line}} 条 return_error 需要 message 字段', { line });
           }
         }
       } catch (error) {
         // plain string value is allowed
       }
     }
+
+    if (mode === 'prune_objects') {
+      const raw = String(op.value_text ?? '').trim();
+      if (!raw) {
+        return t('第 {{line}} 条 prune_objects 缺少条件', { line });
+      }
+      try {
+        const parsed = JSON.parse(raw);
+        if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
+          const hasType =
+            parsed.type !== undefined &&
+            String(parsed.type).trim() !== '';
+          const hasWhere =
+            parsed.where &&
+            typeof parsed.where === 'object' &&
+            !Array.isArray(parsed.where) &&
+            Object.keys(parsed.where).length > 0;
+          const hasConditionsArray =
+            Array.isArray(parsed.conditions) && parsed.conditions.length > 0;
+          const hasConditionsObject =
+            parsed.conditions &&
+            typeof parsed.conditions === 'object' &&
+            !Array.isArray(parsed.conditions) &&
+            Object.keys(parsed.conditions).length > 0;
+          if (!hasType && !hasWhere && !hasConditionsArray && !hasConditionsObject) {
+            return t('第 {{line}} 条 prune_objects 需要至少一个匹配条件', {
+              line,
+            });
+          }
+        }
+      } catch (error) {
+        // non-JSON string is treated as type string
+      }
+    }
+
+    if (mode === 'pass_headers') {
+      const raw = String(op.value_text ?? '').trim();
+      if (!raw) {
+        return t('第 {{line}} 条请求头透传缺少请求头名称', { line });
+      }
+      const parsed = parseLooseValue(raw);
+      const headers = parsePassHeaderNames(parsed);
+      if (headers.length === 0) {
+        return t('第 {{line}} 条请求头透传格式无效', { line });
+      }
+    }
   }
   return '';
 };
@@ -470,7 +938,11 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => {
   const [operationSearch, setOperationSearch] = useState('');
   const [selectedOperationId, setSelectedOperationId] = useState('');
   const [expandedConditionMap, setExpandedConditionMap] = useState({});
-  const [templateLibraryKey, setTemplateLibraryKey] = useState('operations');
+  const [templateGroupKey, setTemplateGroupKey] = useState('basic');
+  const [templatePresetKey, setTemplatePresetKey] = useState('operations_default');
+  const [fieldGuideVisible, setFieldGuideVisible] = useState(false);
+  const [fieldGuideTarget, setFieldGuideTarget] = useState('path');
+  const [fieldGuideKeyword, setFieldGuideKeyword] = useState('');
 
   useEffect(() => {
     if (!visible) return;
@@ -484,9 +956,16 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => {
     setOperationSearch('');
     setSelectedOperationId(nextState.operations[0]?.id || '');
     setExpandedConditionMap({});
-    setTemplateLibraryKey(
-      nextState.visualMode === 'legacy' ? 'legacy' : 'operations',
-    );
+    if (nextState.visualMode === 'legacy') {
+      setTemplateGroupKey('basic');
+      setTemplatePresetKey('legacy_default');
+    } else {
+      setTemplateGroupKey('basic');
+      setTemplatePresetKey('operations_default');
+    }
+    setFieldGuideVisible(false);
+    setFieldGuideTarget('path');
+    setFieldGuideKeyword('');
   }, [visible, value]);
 
   useEffect(() => {
@@ -499,9 +978,26 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => {
     }
   }, [operations, selectedOperationId]);
 
+  const templatePresetOptions = useMemo(
+    () =>
+      Object.entries(TEMPLATE_PRESET_CONFIG)
+        .filter(([, config]) => config.group === templateGroupKey)
+        .map(([value, config]) => ({
+          value,
+          label: config.label,
+        })),
+    [templateGroupKey],
+  );
+
   useEffect(() => {
-    setTemplateLibraryKey(visualMode === 'legacy' ? 'legacy' : 'operations');
-  }, [visualMode]);
+    if (templatePresetOptions.length === 0) return;
+    const exists = templatePresetOptions.some(
+      (item) => item.value === templatePresetKey,
+    );
+    if (!exists) {
+      setTemplatePresetKey(templatePresetOptions[0].value);
+    }
+  }, [templatePresetKey, templatePresetOptions]);
 
   const operationCount = useMemo(
     () => operations.filter((item) => !isOperationBlank(item)).length,
@@ -537,6 +1033,20 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => {
     [operations, selectedOperationId],
   );
 
+  const returnErrorDraft = useMemo(() => {
+    if (!selectedOperation || (selectedOperation.mode || '') !== 'return_error') {
+      return null;
+    }
+    return parseReturnErrorDraft(selectedOperation.value_text);
+  }, [selectedOperation]);
+
+  const pruneObjectsDraft = useMemo(() => {
+    if (!selectedOperation || (selectedOperation.mode || '') !== 'prune_objects') {
+      return null;
+    }
+    return parsePruneObjectsDraft(selectedOperation.value_text);
+  }, [selectedOperation]);
+
   const topOperationModes = useMemo(() => {
     const counts = operations.reduce((acc, operation) => {
       const mode = operation.mode || 'set';
@@ -548,6 +1058,73 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => {
       .slice(0, 4);
   }, [operations]);
 
+  const buildOperationsJson = useCallback(
+    (sourceOperations, options = {}) => {
+      const { validate = true } = options;
+      const filteredOps = sourceOperations.filter((item) => !isOperationBlank(item));
+      if (filteredOps.length === 0) return '';
+
+      if (validate) {
+        const message = validateOperations(filteredOps, t);
+        if (message) {
+          throw new Error(message);
+        }
+      }
+
+      const payloadOps = filteredOps.map((operation) => {
+        const mode = operation.mode || 'set';
+        const meta = MODE_META[mode] || MODE_META.set;
+        const pathValue = operation.path.trim();
+        const fromValue = operation.from.trim();
+        const toValue = operation.to.trim();
+        const payload = { mode };
+        if (meta.path) {
+          payload.path = pathValue;
+        }
+        if (meta.pathOptional && pathValue) {
+          payload.path = pathValue;
+        }
+        if (meta.value) {
+          payload.value = parseLooseValue(operation.value_text);
+        }
+        if (meta.keepOrigin && operation.keep_origin) {
+          payload.keep_origin = true;
+        }
+        if (meta.from) {
+          payload.from = fromValue;
+        }
+        if (!meta.to && operation.to.trim()) {
+          payload.to = toValue;
+        }
+        if (meta.to) {
+          payload.to = toValue;
+        }
+        if (meta.pathAlias) {
+          if (!payload.from && pathValue) {
+            payload.from = pathValue;
+          }
+          if (!payload.to && pathValue) {
+            payload.to = pathValue;
+          }
+        }
+
+        const conditions = (operation.conditions || [])
+          .map(buildConditionPayload)
+          .filter(Boolean);
+
+        if (conditions.length > 0) {
+          payload.conditions = conditions;
+          payload.logic = operation.logic === 'AND' ? 'AND' : 'OR';
+        }
+
+        return payload;
+      });
+
+      return JSON.stringify({ operations: payloadOps }, null, 2);
+    },
+    [t],
+  );
+
   const buildVisualJson = useCallback(() => {
     if (visualMode === 'legacy') {
       const trimmed = legacyValue.trim();
@@ -561,76 +1138,24 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => {
       }
       return JSON.stringify(parsed, null, 2);
     }
-
-    const filteredOps = operations.filter((item) => !isOperationBlank(item));
-    if (filteredOps.length === 0) return '';
-
-    const message = validateOperations(filteredOps, t);
-    if (message) {
-      throw new Error(message);
-    }
-
-    const payloadOps = filteredOps.map((operation) => {
-      const mode = operation.mode || 'set';
-      const meta = MODE_META[mode] || MODE_META.set;
-      const pathValue = operation.path.trim();
-      const fromValue = operation.from.trim();
-      const toValue = operation.to.trim();
-      const payload = { mode };
-      if (meta.path) {
-        payload.path = pathValue;
-      }
-      if (meta.pathOptional && pathValue) {
-        payload.path = pathValue;
-      }
-      if (meta.value) {
-        payload.value = parseLooseValue(operation.value_text);
-      }
-      if (meta.keepOrigin && operation.keep_origin) {
-        payload.keep_origin = true;
-      }
-      if (meta.from) {
-        payload.from = fromValue;
-      }
-      if (!meta.to && operation.to.trim()) {
-        payload.to = toValue;
-      }
-      if (meta.to) {
-        payload.to = toValue;
-      }
-      if (meta.pathAlias) {
-        if (!payload.from && pathValue) {
-          payload.from = pathValue;
-        }
-        if (!payload.to && pathValue) {
-          payload.to = pathValue;
-        }
-      }
-
-      const conditions = (operation.conditions || [])
-        .map(buildConditionPayload)
-        .filter(Boolean);
-
-      if (conditions.length > 0) {
-        payload.conditions = conditions;
-        payload.logic = operation.logic === 'AND' ? 'AND' : 'OR';
-      }
-
-      return payload;
-    });
-
-    return JSON.stringify({ operations: payloadOps }, null, 2);
-  }, [legacyValue, operations, t, visualMode]);
+    return buildOperationsJson(operations, { validate: true });
+  }, [buildOperationsJson, legacyValue, operations, t, visualMode]);
 
   const switchToJsonMode = () => {
     if (editMode === 'json') return;
     try {
       setJsonText(buildVisualJson());
       setJsonError('');
-      setEditMode('json');
     } catch (error) {
       showError(error.message);
+      if (visualMode === 'legacy') {
+        setJsonText(legacyValue);
+      } else {
+        setJsonText(buildOperationsJson(operations, { validate: false }));
+      }
+      setJsonError(error.message || t('参数配置有误'));
     }
+    setEditMode('json');
   };
 
   const switchToVisualMode = () => {
@@ -667,6 +1192,8 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => {
       setLegacyValue('');
       setJsonError('');
       setEditMode('visual');
+      setTemplateGroupKey('basic');
+      setTemplatePresetKey('operations_default');
       return;
     }
     if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
@@ -677,13 +1204,15 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => {
       setSelectedOperationId(fallback.id);
       setJsonError('');
       setEditMode('visual');
+      setTemplateGroupKey('basic');
+      setTemplatePresetKey('legacy_default');
       return;
     }
     showError(t('参数覆盖必须是合法的 JSON 对象'));
   };
 
-  const setOldTemplate = () => {
-    const text = JSON.stringify(LEGACY_TEMPLATE, null, 2);
+  const fillLegacyTemplate = (legacyPayload) => {
+    const text = JSON.stringify(legacyPayload, null, 2);
     const fallback = createDefaultOperation();
     setVisualMode('legacy');
     setLegacyValue(text);
@@ -693,20 +1222,70 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => {
     setJsonText(text);
     setJsonError('');
     setEditMode('visual');
-    setTemplateLibraryKey('legacy');
   };
 
-  const setNewTemplate = () => {
-    const nextOperations =
-      OPERATION_TEMPLATE.operations.map(normalizeOperation);
+  const fillOperationsTemplate = (operationsPayload) => {
+    const nextOperations = (operationsPayload || []).map(normalizeOperation);
+    const finalOperations =
+      nextOperations.length > 0 ? nextOperations : [createDefaultOperation()];
     setVisualMode('operations');
-    setOperations(nextOperations);
-    setSelectedOperationId(nextOperations[0]?.id || '');
+    setOperations(finalOperations);
+    setSelectedOperationId(finalOperations[0]?.id || '');
     setExpandedConditionMap({});
-    setJsonText(JSON.stringify(OPERATION_TEMPLATE, null, 2));
+    setJsonText(JSON.stringify({ operations: operationsPayload || [] }, null, 2));
     setJsonError('');
     setEditMode('visual');
-    setTemplateLibraryKey('operations');
+  };
+
+  const appendLegacyTemplate = (legacyPayload) => {
+    let parsedCurrent = {};
+    if (visualMode === 'legacy') {
+      const trimmed = legacyValue.trim();
+      if (trimmed) {
+        if (!verifyJSON(trimmed)) {
+          showError(t('当前旧格式 JSON 不合法,无法追加模板'));
+          return;
+        }
+        const parsed = JSON.parse(trimmed);
+        if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
+          showError(t('当前旧格式不是 JSON 对象,无法追加模板'));
+          return;
+        }
+        parsedCurrent = parsed;
+      }
+    }
+
+    const merged = {
+      ...(legacyPayload || {}),
+      ...parsedCurrent,
+    };
+    const text = JSON.stringify(merged, null, 2);
+    const fallback = createDefaultOperation();
+    setVisualMode('legacy');
+    setLegacyValue(text);
+    setOperations([fallback]);
+    setSelectedOperationId(fallback.id);
+    setExpandedConditionMap({});
+    setJsonText(text);
+    setJsonError('');
+    setEditMode('visual');
+  };
+
+  const appendOperationsTemplate = (operationsPayload) => {
+    const appended = (operationsPayload || []).map(normalizeOperation);
+    const existing =
+      visualMode === 'operations'
+        ? operations.filter((item) => !isOperationBlank(item))
+        : [];
+    const nextOperations = [...existing, ...appended];
+    setVisualMode('operations');
+    setOperations(nextOperations.length > 0 ? nextOperations : appended);
+    setSelectedOperationId(nextOperations[0]?.id || appended[0]?.id || '');
+    setExpandedConditionMap({});
+    setLegacyValue('');
+    setJsonError('');
+    setEditMode('visual');
+    setJsonText('');
   };
 
   const clearValue = () => {
@@ -718,15 +1297,30 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => {
     setExpandedConditionMap({});
     setJsonText('');
     setJsonError('');
-    setTemplateLibraryKey('operations');
+    setTemplateGroupKey('basic');
+    setTemplatePresetKey('operations_default');
   };
 
-  const applyTemplateFromLibrary = () => {
-    if (templateLibraryKey === 'legacy') {
-      setOldTemplate();
+  const getSelectedTemplatePreset = () =>
+    TEMPLATE_PRESET_CONFIG[templatePresetKey] ||
+    TEMPLATE_PRESET_CONFIG.operations_default;
+
+  const fillTemplateFromLibrary = () => {
+    const preset = getSelectedTemplatePreset();
+    if (preset.kind === 'legacy') {
+      fillLegacyTemplate(preset.payload || {});
       return;
     }
-    setNewTemplate();
+    fillOperationsTemplate(preset.payload?.operations || []);
+  };
+
+  const appendTemplateFromLibrary = () => {
+    const preset = getSelectedTemplatePreset();
+    if (preset.kind === 'legacy') {
+      appendLegacyTemplate(preset.payload || {});
+      return;
+    }
+    appendOperationsTemplate(preset.payload?.operations || []);
   };
 
   const resetEditorState = () => {
@@ -734,6 +1328,78 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => {
     setEditMode('visual');
   };
 
+  const applyBuiltinField = (fieldKey, target = 'path') => {
+    if (!selectedOperation) {
+      showError(t('请先选择一条规则'));
+      return;
+    }
+    const mode = selectedOperation.mode || 'set';
+    const meta = MODE_META[mode] || MODE_META.set;
+    if (target === 'path' && (meta.path || meta.pathOptional || meta.pathAlias)) {
+      updateOperation(selectedOperation.id, { path: fieldKey });
+      return;
+    }
+    if (target === 'from' && (meta.from || meta.pathAlias || mode === 'sync_fields')) {
+      updateOperation(selectedOperation.id, {
+        from: mode === 'sync_fields' ? buildSyncTargetSpec('json', fieldKey) : fieldKey,
+      });
+      return;
+    }
+    if (target === 'to' && (meta.to || mode === 'sync_fields')) {
+      updateOperation(selectedOperation.id, {
+        to: mode === 'sync_fields' ? buildSyncTargetSpec('json', fieldKey) : fieldKey,
+      });
+      return;
+    }
+    showError(t('当前规则不支持写入到该位置'));
+  };
+
+  const openFieldGuide = (target = 'path') => {
+    setFieldGuideTarget(target);
+    setFieldGuideVisible(true);
+  };
+
+  const copyBuiltinField = async (fieldKey) => {
+    const ok = await copy(fieldKey);
+    if (ok) {
+      showSuccess(t('已复制字段:{{name}}', { name: fieldKey }));
+    } else {
+      showError(t('复制失败'));
+    }
+  };
+
+  const filteredFieldGuideSections = useMemo(() => {
+    const keyword = fieldGuideKeyword.trim().toLowerCase();
+    if (!keyword) {
+      return BUILTIN_FIELD_SECTIONS;
+    }
+    return BUILTIN_FIELD_SECTIONS.map((section) => ({
+      ...section,
+      fields: section.fields.filter((field) =>
+        [field.key, field.label, field.tip]
+          .filter(Boolean)
+          .join(' ')
+          .toLowerCase()
+          .includes(keyword),
+      ),
+    })).filter((section) => section.fields.length > 0);
+  }, [fieldGuideKeyword]);
+
+  const fieldGuideActionLabel = useMemo(() => {
+    if (fieldGuideTarget === 'from') return t('填入来源');
+    if (fieldGuideTarget === 'to') return t('填入目标');
+    return t('填入路径');
+  }, [fieldGuideTarget, t]);
+
+  const fieldGuideFieldCount = useMemo(
+    () =>
+      filteredFieldGuideSections.reduce(
+        (total, section) => total + section.fields.length,
+        0,
+      ),
+    [filteredFieldGuideSections],
+  );
+
   const updateOperation = (operationId, patch) => {
     setOperations((prev) =>
       prev.map((item) =>
@@ -742,6 +1408,53 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => {
     );
   };
 
+  const updateReturnErrorDraft = (operationId, draftPatch = {}) => {
+    const current = operations.find((item) => item.id === operationId);
+    if (!current) return;
+    const draft = parseReturnErrorDraft(current.value_text);
+    const nextDraft = { ...draft, ...draftPatch };
+    updateOperation(operationId, {
+      value_text: buildReturnErrorValueText(nextDraft),
+    });
+  };
+
+  const updatePruneObjectsDraft = (operationId, updater) => {
+    const current = operations.find((item) => item.id === operationId);
+    if (!current) return;
+    const draft = parsePruneObjectsDraft(current.value_text);
+    const nextDraft =
+      typeof updater === 'function'
+        ? updater(draft)
+        : { ...draft, ...(updater || {}) };
+    updateOperation(operationId, {
+      value_text: buildPruneObjectsValueText(nextDraft),
+    });
+  };
+
+  const addPruneRule = (operationId) => {
+    updatePruneObjectsDraft(operationId, (draft) => ({
+      ...draft,
+      simpleMode: false,
+      rules: [...(draft.rules || []), normalizePruneRule({})],
+    }));
+  };
+
+  const updatePruneRule = (operationId, ruleId, patch) => {
+    updatePruneObjectsDraft(operationId, (draft) => ({
+      ...draft,
+      rules: (draft.rules || []).map((rule) =>
+        rule.id === ruleId ? { ...rule, ...patch } : rule,
+      ),
+    }));
+  };
+
+  const removePruneRule = (operationId, ruleId) => {
+    updatePruneObjectsDraft(operationId, (draft) => ({
+      ...draft,
+      rules: (draft.rules || []).filter((rule) => rule.id !== ruleId),
+    }));
+  };
+
   const addOperation = () => {
     const created = createDefaultOperation();
     setOperations((prev) => [...prev, created]);
@@ -910,16 +1623,17 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => {
     setJsonError('');
   };
 
-  const visualPreview = useMemo(() => {
-    if (editMode !== 'visual' || visualMode !== 'operations') {
+  const visualValidationError = useMemo(() => {
+    if (editMode !== 'visual') {
       return '';
     }
     try {
-      return buildVisualJson() || '';
+      buildVisualJson();
+      return '';
     } catch (error) {
-      return `// ${error.message}`;
+      return error?.message || t('参数配置有误');
     }
-  }, [buildVisualJson, editMode, visualMode]);
+  }, [buildVisualJson, editMode, t]);
 
   const handleSave = () => {
     try {
@@ -944,46 +1658,87 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => {
   };
 
   return (
-    
+      
-      
-        
-          
-          
-          
+                  setTemplateGroupKey(nextValue || 'basic')
+                }
+                style={{ width: 120 }}
+              />
+