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.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 (
-
+
-
-
-
-
-
+
+
+
+
+ {t('编辑方式')}
+
+
+ {t('模板')}
+
+ openFieldGuide('path')}
+ >
+ {t('字段速查')}
+
+
+
{editMode === 'visual' ? (
{visualMode === 'legacy' ? (
-
- {t('旧格式(直接覆盖):')}
+
+ {t('旧格式(JSON 对象)')}
+
) : (
- {t('新格式(支持条件判断与json自定义):')}
+ {t('新格式(规则 + 条件)')}
{`${t('规则')}: ${operationCount}`}
} onClick={addOperation}>
@@ -1033,7 +1788,7 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => {
size='small'
color={getOperationModeTagColor(mode)}
>
- {`${mode} · ${count}`}
+ {`${OPERATION_MODE_LABEL_MAP[mode] || mode} · ${count}`}
))}
@@ -1041,7 +1796,7 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => {
setOperationSearch(nextValue || '')
}
@@ -1120,10 +1875,14 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => {
operation.mode || 'set',
)}
>
- {operation.mode || 'set'}
+ {OPERATION_MODE_LABEL_MAP[
+ operation.mode || 'set'
+ ] ||
+ operation.mode ||
+ 'set'}
- {t('条件')}
+ {t('条件数')}
@@ -1191,7 +1950,7 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => {
- mode
+ {t('操作类型')}
{
{meta.pathOptional
- ? 'path (optional)'
- : 'path'}
+ ? t('目标路径(可选)')
+ : t(getModePathLabel(mode))}
updateOperation(selectedOperation.id, {
path: nextValue,
})
}
/>
-
- {OPERATION_PATH_SUGGESTIONS.map(
- (pathItem) => (
-
- updateOperation(
- selectedOperation.id,
- {
- path: pathItem,
- },
- )
- }
- >
- {pathItem}
-
- ),
- )}
-
) : null}
@@ -1261,25 +1992,564 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => {
{meta.value ? (
-
-
- value (JSON or plain text)
-
-
+ mode === 'return_error' && returnErrorDraft ? (
+
+
+ {t('自定义错误响应')}
+
+
+ {t('模式')}
+
+
+
+
+
+
+
+ {t('错误消息(必填)')}
+
+
+ ) : mode === 'prune_objects' && pruneObjectsDraft ? (
+
+
+ {t('对象清理规则')}
+
+
+ {t('模式')}
+
+
+
+
+
+
+
+ {t('类型(常用)')}
+
+
+ updatePruneObjectsDraft(
+ selectedOperation.id,
+ { typeText: nextValue },
+ )
+ }
+ />
+
+ {pruneObjectsDraft.simpleMode ? (
+
+ {t(
+ '简洁模式:按 type 全量清理对象,例如 redacted_thinking。',
+ )}
+
+ ) : (
+ <>
+
+
+
+ {t('逻辑')}
+
+
+ updatePruneObjectsDraft(
+ selectedOperation.id,
+ { logic: nextValue || 'AND' },
+ )
+ }
+ />
+
+
+
+ {t('递归策略')}
+
+
+
+
+
+
+
+
+
+
+
+ {t('附加条件')}
+
+ }
+ onClick={() =>
+ addPruneRule(selectedOperation.id)
+ }
+ >
+ {t('新增条件')}
+
+
+ {(pruneObjectsDraft.rules || []).length === 0 ? (
+
+ {t(
+ '未添加附加条件时,仅使用上方 type 进行清理。',
+ )}
+
+ ) : (
+
+ {(pruneObjectsDraft.rules || []).map(
+ (rule, ruleIndex) => (
+
+
+
+ {`R${ruleIndex + 1}`}
+
+ }
+ onClick={() =>
+ removePruneRule(
+ selectedOperation.id,
+ rule.id,
+ )
+ }
+ >
+ {t('删除条件')}
+
+
+
+
+
+ {t('字段路径')}
+
+
+ updatePruneRule(
+ selectedOperation.id,
+ rule.id,
+ { path: nextValue },
+ )
+ }
+ />
+
+
+
+ {t('匹配方式')}
+
+
+ updatePruneRule(
+ selectedOperation.id,
+ rule.id,
+ { mode: nextValue },
+ )
+ }
+ />
+
+
+
+ {t('匹配值(可选)')}
+
+
+ updatePruneRule(
+ selectedOperation.id,
+ rule.id,
+ {
+ value_text:
+ nextValue,
+ },
+ )
+ }
+ />
+
+
+
+
+
+
+
+ ),
+ )}
+
+ )}
+
+ >
+ )}
+
+ ) : (
+
+
+ {t(getModeValueLabel(mode))}
+
+
+ )
) : null}
{meta.keepOrigin ? (
-
+
{
- keep_origin
+ {t('保留原值(目标已有值时不覆盖)')}
) : null}
@@ -1305,12 +2575,12 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => {
{mode === 'sync_fields' ? (
- sync endpoints
+ {t('同步端点')}
- from endpoint
+ {t('来源端点')}
{
- to endpoint
+ {t('目标端点')}
{
{meta.from || meta.to === false ? (
- from
+ {t(getModeFromLabel(mode))}
updateOperation(selectedOperation.id, {
from: nextValue,
@@ -1443,15 +2709,11 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => {
{meta.to || meta.to === false ? (
- to
+ {t(getModeToLabel(mode))}
updateOperation(selectedOperation.id, {
to: nextValue,
@@ -1464,21 +2726,22 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => {
) : null}
-
- {t('条件')}
+
+ {t('条件规则')}
updateOperation(selectedOperation.id, {
logic: nextValue,
@@ -1540,13 +2803,16 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => {
{condition.path ||
- t('未设置 path')}
+ t('未设置路径')}
}
>
-
+
+
+ {t('条件项设置')}
+
@@ -1566,7 +2834,7 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => {
type='tertiary'
size='small'
>
- path
+ {t('字段路径')}
{
)
}
/>
-
- {CONDITION_PATH_SUGGESTIONS.map(
- (pathItem) => (
-
- updateCondition(
- selectedOperation.id,
- condition.id,
- { path: pathItem },
- )
- }
- >
- {pathItem}
-
- ),
- )}
-
- mode
+ {t('匹配方式')}
{
type='tertiary'
size='small'
>
- value
+ {t('匹配值')}
{
/>
-
-
- updateCondition(
- selectedOperation.id,
- condition.id,
- { invert: nextValue },
- )
- }
- />
-
- invert
-
-
- updateCondition(
- selectedOperation.id,
- condition.id,
- {
- pass_missing_key: nextValue,
- },
- )
- }
- />
-
- pass_missing_key
-
-
+
+
+
+ {t('条件取反')}
+
+
+ updateCondition(
+ selectedOperation.id,
+ condition.id,
+ { invert: nextValue },
+ )
+ }
+ />
+
+
+
+ {t('字段缺失视为命中')}
+
+
+ updateCondition(
+ selectedOperation.id,
+ condition.id,
+ {
+ pass_missing_key: nextValue,
+ },
+ )
+ }
+ />
+
+
),
@@ -1708,21 +2956,20 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => {
)}
-
-
- {t('实时 JSON 预览')}
- {t('预览')}
-
-
- {visualPreview || '{}'}
-
-
+ {visualValidationError ? (
+
+
+ {t('暂存错误')}
+ {visualValidationError}
+
+
+ ) : null}
@@ -1732,7 +2979,7 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => {
- {t('普通编辑')}
+ {t('高级文本编辑')}
)}
-
+
+
+ setFieldGuideVisible(false)}
+ bodyStyle={{
+ maxHeight: '72vh',
+ overflowY: 'auto',
+ padding: 16,
+ background: 'var(--semi-color-bg-0)',
+ }}
+ >
+
+
+
+
+ {t('字段速查')}
+
+
+ {t(
+ '先搜索,再一键复制字段名或填入当前规则。字段名为系统内部路径,可直接用于路径 / 来源 / 目标。',
+ )}
+
+
+ {`${fieldGuideFieldCount} ${t('个字段')}`}
+
+
+
+
+ setFieldGuideKeyword(nextValue || '')}
+ placeholder={t('搜索字段名 / 中文说明')}
+ showClear
+ style={{ flex: 1 }}
+ />
+
+ setFieldGuideTarget(nextValue || 'path')
+ }
+ style={{ width: 170 }}
+ />
+
+
+
+ {filteredFieldGuideSections.length === 0 ? (
+
+ {t('没有匹配的字段')}
+
+ ) : (
+
+ {filteredFieldGuideSections.map((section) => (
+
+
+
+ {section.title}
+
+ {`${section.fields.length} ${t('项')}`}
+
+
+ {section.fields.map((field, index) => (
+
+
+ {field.label}
+
+ {field.key}
+
+
+ {field.tip}
+
+
+
+
+
+
+
+ ))}
+
+
+ ))}
+
+ )}
+
+
+ >
);
};