فهرست منبع

feat: add pass_headers op, grouped presets (incl. Gemini 4K), and robust JSON fallback

Seefs 1 هفته پیش
والد
کامیت
303fff44e7

+ 88 - 1
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

+ 144 - 47
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) => {
                           <Button
                             size='small'
                             onClick={() =>
-                              handleInputChange(
-                                'param_override',
-                                JSON.stringify({ temperature: 0 }, null, 2),
-                              )
+                              applyParamOverrideTemplate('operations', 'fill')
                             }
                           >
-                            {t('旧格式模板')}
+                            {t('填充新模板')}
                           </Button>
                           <Button
                             size='small'
                             onClick={() =>
-                              handleInputChange(
-                                'param_override',
-                                JSON.stringify(
-                                  {
-                                    operations: [
-                                      {
-                                        path: 'temperature',
-                                        mode: 'set',
-                                        value: 0.7,
-                                        conditions: [
-                                          {
-                                            path: 'model',
-                                            mode: 'prefix',
-                                            value: 'gpt',
-                                          },
-                                        ],
-                                        logic: 'AND',
-                                      },
-                                    ],
-                                  },
-                                  null,
-                                  2,
-                                ),
-                              )
+                              applyParamOverrideTemplate('legacy', 'fill')
                             }
                           >
-                            {t('新格式模板')}
-                          </Button>
-                          <Button
-                            size='small'
-                            type='tertiary'
-                            onClick={() => handleInputChange('param_override', '')}
-                          >
-                            {t('不更改')}
+                            {t('填充旧模板')}
                           </Button>
                         </Space>
                       </div>
@@ -3171,20 +3255,33 @@ const EditChannelModal = (props) => {
                         {t('此项可选,用于覆盖请求参数。不支持覆盖 stream 参数')}
                       </Text>
                       <div
-                        className='mt-2 rounded-lg border p-3'
-                        style={{ backgroundColor: 'var(--semi-color-fill-0)' }}
+                        className='mt-2 rounded-xl p-3'
+                        style={{
+                          backgroundColor: 'var(--semi-color-fill-0)',
+                          border: '1px solid var(--semi-color-fill-2)',
+                        }}
                       >
                         <div className='flex items-center justify-between mb-2'>
                           <Tag color={paramOverrideMeta.tagColor}>
                             {paramOverrideMeta.tagLabel}
                           </Tag>
-                          <Button
-                            size='small'
-                            type='tertiary'
-                            onClick={() => setParamOverrideEditorVisible(true)}
-                          >
-                            {t('编辑')}
-                          </Button>
+                          <Space spacing={8}>
+                            <Button
+                              size='small'
+                              icon={<IconCopy />}
+                              type='tertiary'
+                              onClick={copyParamOverrideJson}
+                            >
+                              {t('复制')}
+                            </Button>
+                            <Button
+                              size='small'
+                              type='tertiary'
+                              onClick={() => setParamOverrideEditorVisible(true)}
+                            >
+                              {t('编辑')}
+                            </Button>
+                          </Space>
                         </div>
                         <pre className='mb-0 text-xs leading-5 whitespace-pre-wrap break-all max-h-56 overflow-auto'>
                           {paramOverrideMeta.preview}

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 677 - 152
web/src/components/table/channels/modals/ParamOverrideEditorModal.jsx


برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است