瀏覽代碼

feat: redesign param override editing with guided modal and Monaco JSON hints

Seefs 1 周之前
父節點
當前提交
81d9173027

+ 2 - 0
web/package.json

@@ -7,6 +7,7 @@
     "@douyinfe/semi-icons": "^2.63.1",
     "@douyinfe/semi-ui": "^2.69.1",
     "@lobehub/icons": "^2.0.0",
+    "@monaco-editor/react": "^4.7.0",
     "@visactor/react-vchart": "~1.8.8",
     "@visactor/vchart": "~1.8.8",
     "@visactor/vchart-semi-theme": "~1.8.8",
@@ -20,6 +21,7 @@
     "lucide-react": "^0.511.0",
     "marked": "^4.1.1",
     "mermaid": "^11.6.0",
+    "monaco-editor": "^0.55.1",
     "qrcode.react": "^4.2.0",
     "react": "^18.2.0",
     "react-dom": "^18.2.0",

+ 120 - 34
web/src/components/table/channels/modals/EditChannelModal.jsx

@@ -59,6 +59,7 @@ import ModelSelectModal from './ModelSelectModal';
 import SingleModelSelectModal from './SingleModelSelectModal';
 import OllamaModelModal from './OllamaModelModal';
 import CodexOAuthModal from './CodexOAuthModal';
+import ParamOverrideEditorModal from './ParamOverrideEditorModal';
 import JSONEditor from '../../../common/ui/JSONEditor';
 import SecureVerificationModal from '../../../common/modals/SecureVerificationModal';
 import ChannelKeyDisplay from '../../../common/ui/ChannelKeyDisplay';
@@ -143,6 +144,7 @@ const EditChannelModal = (props) => {
     base_url: '',
     other: '',
     model_mapping: '',
+    param_override: '',
     status_code_mapping: '',
     models: [],
     auto_ban: 1,
@@ -224,11 +226,69 @@ const EditChannelModal = (props) => {
       return [];
     }
   }, [inputs.model_mapping]);
+  const paramOverrideMeta = useMemo(() => {
+    const raw =
+      typeof inputs.param_override === 'string'
+        ? inputs.param_override.trim()
+        : '';
+    if (!raw) {
+      return {
+        tagLabel: t('不更改'),
+        tagColor: 'grey',
+        preview: t(
+          '此项可选,用于覆盖请求参数。不支持覆盖 stream 参数',
+        ),
+      };
+    }
+    if (!verifyJSON(raw)) {
+      return {
+        tagLabel: t('JSON格式错误'),
+        tagColor: 'red',
+        preview: raw,
+      };
+    }
+    try {
+      const parsed = JSON.parse(raw);
+      const pretty = JSON.stringify(parsed, null, 2);
+      if (
+        parsed &&
+        typeof parsed === 'object' &&
+        !Array.isArray(parsed) &&
+        Array.isArray(parsed.operations)
+      ) {
+        return {
+          tagLabel: `${t('新格式模板')} (${parsed.operations.length})`,
+          tagColor: 'cyan',
+          preview: pretty,
+        };
+      }
+      if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
+        return {
+          tagLabel: `${t('旧格式模板')} (${Object.keys(parsed).length})`,
+          tagColor: 'blue',
+          preview: pretty,
+        };
+      }
+      return {
+        tagLabel: 'Custom JSON',
+        tagColor: 'orange',
+        preview: pretty,
+      };
+    } catch (error) {
+      return {
+        tagLabel: t('JSON格式错误'),
+        tagColor: 'red',
+        preview: raw,
+      };
+    }
+  }, [inputs.param_override, t]);
   const [isIonetChannel, setIsIonetChannel] = useState(false);
   const [ionetMetadata, setIonetMetadata] = useState(null);
   const [codexOAuthModalVisible, setCodexOAuthModalVisible] = useState(false);
   const [codexCredentialRefreshing, setCodexCredentialRefreshing] =
     useState(false);
+  const [paramOverrideEditorVisible, setParamOverrideEditorVisible] =
+    useState(false);
 
   // 密钥显示状态
   const [keyDisplayState, setKeyDisplayState] = useState({
@@ -1170,6 +1230,7 @@ const EditChannelModal = (props) => {
   const submit = async () => {
     const formValues = formApiRef.current ? formApiRef.current.getValues() : {};
     let localInputs = { ...formValues };
+    localInputs.param_override = inputs.param_override;
 
     if (localInputs.type === 57) {
       if (batch) {
@@ -3043,28 +3104,20 @@ const EditChannelModal = (props) => {
                       initValue={autoBan}
                     />
 
-                    <Form.TextArea
-                      field='param_override'
-                      label={t('参数覆盖')}
-                      placeholder={
-                        t(
-                          '此项可选,用于覆盖请求参数。不支持覆盖 stream 参数',
-                        ) +
-                        '\n' +
-                        t('旧格式(直接覆盖):') +
-                        '\n{\n  "temperature": 0,\n  "max_tokens": 1000\n}' +
-                        '\n\n' +
-                        t('新格式(支持条件判断与json自定义):') +
-                        '\n{\n  "operations": [\n    {\n      "path": "temperature",\n      "mode": "set",\n      "value": 0.7,\n      "conditions": [\n        {\n          "path": "model",\n          "mode": "prefix",\n          "value": "gpt"\n        }\n      ]\n    }\n  ]\n}'
-                      }
-                      autosize
-                      onChange={(value) =>
-                        handleInputChange('param_override', value)
-                      }
-                      extraText={
-                        <div className='flex gap-2 flex-wrap'>
-                          <Text
-                            className='!text-semi-color-primary cursor-pointer'
+                    <div className='mb-4'>
+                      <div className='flex items-center justify-between gap-2 mb-1'>
+                        <Text className='text-sm font-medium'>{t('参数覆盖')}</Text>
+                        <Space wrap>
+                          <Button
+                            size='small'
+                            type='primary'
+                            icon={<IconCode size={14} />}
+                            onClick={() => setParamOverrideEditorVisible(true)}
+                          >
+                            {t('可视化编辑')}
+                          </Button>
+                          <Button
+                            size='small'
                             onClick={() =>
                               handleInputChange(
                                 'param_override',
@@ -3073,9 +3126,9 @@ const EditChannelModal = (props) => {
                             }
                           >
                             {t('旧格式模板')}
-                          </Text>
-                          <Text
-                            className='!text-semi-color-primary cursor-pointer'
+                          </Button>
+                          <Button
+                            size='small'
                             onClick={() =>
                               handleInputChange(
                                 'param_override',
@@ -3104,17 +3157,40 @@ const EditChannelModal = (props) => {
                             }
                           >
                             {t('新格式模板')}
-                          </Text>
-                          <Text
-                            className='!text-semi-color-primary cursor-pointer'
-                            onClick={() => formatJsonField('param_override')}
+                          </Button>
+                          <Button
+                            size='small'
+                            type='tertiary'
+                            onClick={() => handleInputChange('param_override', '')}
                           >
-                            {t('格式化')}
-                          </Text>
+                            {t('不更改')}
+                          </Button>
+                        </Space>
+                      </div>
+                      <Text type='tertiary' size='small'>
+                        {t('此项可选,用于覆盖请求参数。不支持覆盖 stream 参数')}
+                      </Text>
+                      <div
+                        className='mt-2 rounded-lg border p-3'
+                        style={{ backgroundColor: 'var(--semi-color-fill-0)' }}
+                      >
+                        <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>
                         </div>
-                      }
-                      showClear
-                    />
+                        <pre className='mb-0 text-xs leading-5 whitespace-pre-wrap break-all max-h-56 overflow-auto'>
+                          {paramOverrideMeta.preview}
+                        </pre>
+                      </div>
+                    </div>
 
                     <Form.TextArea
                       field='header_override'
@@ -3494,6 +3570,16 @@ const EditChannelModal = (props) => {
         />
       </Modal>
 
+      <ParamOverrideEditorModal
+        visible={paramOverrideEditorVisible}
+        value={inputs.param_override || ''}
+        onCancel={() => setParamOverrideEditorVisible(false)}
+        onSave={(nextValue) => {
+          handleInputChange('param_override', nextValue);
+          setParamOverrideEditorVisible(false);
+        }}
+      />
+
       <ModelSelectModal
         visible={modelModalVisible}
         models={fetchedModels}

+ 1409 - 0
web/src/components/table/channels/modals/ParamOverrideEditorModal.jsx

@@ -0,0 +1,1409 @@
+/*
+Copyright (C) 2025 QuantumNous
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+For commercial licensing, please contact support@quantumnous.com
+*/
+
+import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import MonacoEditor from '@monaco-editor/react';
+import { useTranslation } from 'react-i18next';
+import {
+  Banner,
+  Button,
+  Card,
+  Col,
+  Input,
+  Modal,
+  Row,
+  Select,
+  Space,
+  Switch,
+  Tag,
+  Typography,
+} from '@douyinfe/semi-ui';
+import { IconDelete, IconPlus } from '@douyinfe/semi-icons';
+import { showError, verifyJSON } from '../../../../helpers';
+import JSONEditor from '../../../common/ui/JSONEditor';
+
+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: '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' },
+];
+
+const OPERATION_MODE_VALUES = new Set(
+  OPERATION_MODE_OPTIONS.map((item) => item.value),
+);
+
+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' },
+];
+
+const CONDITION_MODE_VALUES = new Set(
+  CONDITION_MODE_OPTIONS.map((item) => item.value),
+);
+
+const MODE_META = {
+  delete: { path: true },
+  set: { path: true, value: true, keepOrigin: true },
+  append: { path: true, value: true, keepOrigin: true },
+  prepend: { path: true, value: true, keepOrigin: true },
+  copy: { from: true, to: true },
+  move: { from: true, to: true },
+  replace: { path: true, from: true, to: false },
+  regex_replace: { path: true, from: true, to: false },
+  trim_prefix: { path: true, value: true },
+  trim_suffix: { path: true, value: true },
+  ensure_prefix: { path: true, value: true },
+  ensure_suffix: { path: true, value: true },
+  trim_space: { path: true },
+  to_lower: { path: true },
+  to_upper: { path: true },
+  return_error: { value: true },
+  prune_objects: { pathOptional: true, value: true },
+  set_header: { path: true, value: true, keepOrigin: true },
+  delete_header: { path: true },
+  copy_header: { from: true, to: true, keepOrigin: true, pathAlias: true },
+  move_header: { from: true, to: true, keepOrigin: true, pathAlias: true },
+};
+
+const VALUE_REQUIRED_MODES = new Set([
+  'trim_prefix',
+  'trim_suffix',
+  'ensure_prefix',
+  'ensure_suffix',
+  'set_header',
+  'return_error',
+  'prune_objects',
+]);
+
+const FROM_REQUIRED_MODES = new Set([
+  'copy',
+  'move',
+  'replace',
+  'regex_replace',
+  'copy_header',
+  'move_header',
+]);
+
+const TO_REQUIRED_MODES = new Set(['copy', 'move', 'copy_header', 'move_header']);
+
+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',
+  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',
+};
+
+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',
+];
+
+const LEGACY_TEMPLATE = {
+  temperature: 0,
+  max_tokens: 1000,
+};
+
+const OPERATION_TEMPLATE = {
+  operations: [
+    {
+      path: 'temperature',
+      mode: 'set',
+      value: 0.7,
+      conditions: [
+        {
+          path: 'model',
+          mode: 'prefix',
+          value: 'gpt',
+        },
+      ],
+      logic: 'AND',
+    },
+  ],
+};
+
+const MONACO_SCHEMA_URI = 'https://new-api.local/schemas/param-override.schema.json';
+const MONACO_MODEL_URI = 'inmemory://new-api/param-override.json';
+
+const JSON_SCALAR_SCHEMA = {
+  oneOf: [
+    { type: 'string' },
+    { type: 'number' },
+    { type: 'boolean' },
+    { type: 'null' },
+    { type: 'array' },
+    { type: 'object' },
+  ],
+};
+
+const PARAM_OVERRIDE_JSON_SCHEMA = {
+  $schema: 'http://json-schema.org/draft-07/schema#',
+  type: 'object',
+  properties: {
+    operations: {
+      type: 'array',
+      description: 'Operation pipeline for new param override format.',
+      items: {
+        type: 'object',
+        properties: {
+          mode: {
+            type: 'string',
+            enum: OPERATION_MODE_OPTIONS.map((item) => item.value),
+          },
+          path: { type: 'string' },
+          from: { type: 'string' },
+          to: { type: 'string' },
+          keep_origin: { type: 'boolean' },
+          value: JSON_SCALAR_SCHEMA,
+          logic: { type: 'string', enum: ['AND', 'OR'] },
+          conditions: {
+            oneOf: [
+              {
+                type: 'array',
+                items: {
+                  type: 'object',
+                  properties: {
+                    path: { type: 'string' },
+                    mode: {
+                      type: 'string',
+                      enum: CONDITION_MODE_OPTIONS.map((item) => item.value),
+                    },
+                    value: JSON_SCALAR_SCHEMA,
+                    invert: { type: 'boolean' },
+                    pass_missing_key: { type: 'boolean' },
+                  },
+                  required: ['path', 'mode'],
+                  additionalProperties: false,
+                },
+              },
+              {
+                type: 'object',
+                additionalProperties: JSON_SCALAR_SCHEMA,
+              },
+            ],
+          },
+        },
+        required: ['mode'],
+        additionalProperties: false,
+        allOf: [
+          {
+            if: { properties: { mode: { const: 'set' } }, required: ['mode'] },
+            then: { required: ['path'] },
+          },
+          {
+            if: { properties: { mode: { const: 'delete' } }, required: ['mode'] },
+            then: { required: ['path'] },
+          },
+          {
+            if: { properties: { mode: { const: 'append' } }, required: ['mode'] },
+            then: { required: ['path'] },
+          },
+          {
+            if: { properties: { mode: { const: 'prepend' } }, required: ['mode'] },
+            then: { required: ['path'] },
+          },
+          {
+            if: { properties: { mode: { const: 'copy' } }, required: ['mode'] },
+            then: { required: ['from', 'to'] },
+          },
+          {
+            if: { properties: { mode: { const: 'move' } }, required: ['mode'] },
+            then: { required: ['from', 'to'] },
+          },
+          {
+            if: { properties: { mode: { const: 'replace' } }, required: ['mode'] },
+            then: { required: ['path', 'from'] },
+          },
+          {
+            if: {
+              properties: { mode: { const: 'regex_replace' } },
+              required: ['mode'],
+            },
+            then: { required: ['path', 'from'] },
+          },
+          {
+            if: {
+              properties: { mode: { const: 'trim_prefix' } },
+              required: ['mode'],
+            },
+            then: { required: ['path', 'value'] },
+          },
+          {
+            if: {
+              properties: { mode: { const: 'trim_suffix' } },
+              required: ['mode'],
+            },
+            then: { required: ['path', 'value'] },
+          },
+          {
+            if: {
+              properties: { mode: { const: 'ensure_prefix' } },
+              required: ['mode'],
+            },
+            then: { required: ['path', 'value'] },
+          },
+          {
+            if: {
+              properties: { mode: { const: 'ensure_suffix' } },
+              required: ['mode'],
+            },
+            then: { required: ['path', 'value'] },
+          },
+          {
+            if: {
+              properties: { mode: { const: 'trim_space' } },
+              required: ['mode'],
+            },
+            then: { required: ['path'] },
+          },
+          {
+            if: {
+              properties: { mode: { const: 'to_lower' } },
+              required: ['mode'],
+            },
+            then: { required: ['path'] },
+          },
+          {
+            if: {
+              properties: { mode: { const: 'to_upper' } },
+              required: ['mode'],
+            },
+            then: { required: ['path'] },
+          },
+          {
+            if: {
+              properties: { mode: { const: 'return_error' } },
+              required: ['mode'],
+            },
+            then: { required: ['value'] },
+          },
+          {
+            if: {
+              properties: { mode: { const: 'prune_objects' } },
+              required: ['mode'],
+            },
+            then: { required: ['value'] },
+          },
+          {
+            if: {
+              properties: { mode: { const: 'set_header' } },
+              required: ['mode'],
+            },
+            then: { required: ['path', 'value'] },
+          },
+          {
+            if: {
+              properties: { mode: { const: 'delete_header' } },
+              required: ['mode'],
+            },
+            then: { required: ['path'] },
+          },
+          {
+            if: {
+              properties: { mode: { const: 'copy_header' } },
+              required: ['mode'],
+            },
+            then: {
+              anyOf: [{ required: ['path'] }, { required: ['from', 'to'] }],
+            },
+          },
+          {
+            if: {
+              properties: { mode: { const: 'move_header' } },
+              required: ['mode'],
+            },
+            then: {
+              anyOf: [{ required: ['path'] }, { required: ['from', 'to'] }],
+            },
+          },
+        ],
+      },
+    },
+  },
+  additionalProperties: true,
+};
+
+let localIdSeed = 0;
+const nextLocalId = () => `param_override_${Date.now()}_${localIdSeed++}`;
+
+const toValueText = (value) => {
+  if (value === undefined) return '';
+  if (typeof value === 'string') return value;
+  try {
+    return JSON.stringify(value);
+  } catch (error) {
+    return String(value);
+  }
+};
+
+const parseLooseValue = (valueText) => {
+  const raw = String(valueText ?? '');
+  if (raw.trim() === '') return '';
+  try {
+    return JSON.parse(raw);
+  } catch (error) {
+    return raw;
+  }
+};
+
+const normalizeCondition = (condition = {}) => ({
+  id: nextLocalId(),
+  path: typeof condition.path === 'string' ? condition.path : '',
+  mode: CONDITION_MODE_VALUES.has(condition.mode) ? condition.mode : 'full',
+  value_text: toValueText(condition.value),
+  invert: condition.invert === true,
+  pass_missing_key: condition.pass_missing_key === true,
+});
+
+const createDefaultCondition = () => normalizeCondition({});
+
+const normalizeOperation = (operation = {}) => ({
+  id: nextLocalId(),
+  path: typeof operation.path === 'string' ? operation.path : '',
+  mode: OPERATION_MODE_VALUES.has(operation.mode) ? operation.mode : 'set',
+  value_text: toValueText(operation.value),
+  keep_origin: operation.keep_origin === true,
+  from: typeof operation.from === 'string' ? operation.from : '',
+  to: typeof operation.to === 'string' ? operation.to : '',
+  logic: String(operation.logic || 'OR').toUpperCase() === 'AND' ? 'AND' : 'OR',
+  conditions: Array.isArray(operation.conditions)
+    ? operation.conditions.map(normalizeCondition)
+    : [],
+});
+
+const createDefaultOperation = () => normalizeOperation({ mode: 'set' });
+
+const parseInitialState = (rawValue) => {
+  const text = typeof rawValue === 'string' ? rawValue : '';
+  const trimmed = text.trim();
+  if (!trimmed) {
+    return {
+      editMode: 'visual',
+      visualMode: 'operations',
+      legacyValue: '',
+      operations: [createDefaultOperation()],
+      jsonText: '',
+      jsonError: '',
+    };
+  }
+
+  if (!verifyJSON(trimmed)) {
+    return {
+      editMode: 'json',
+      visualMode: 'operations',
+      legacyValue: '',
+      operations: [createDefaultOperation()],
+      jsonText: text,
+      jsonError: 'JSON format is invalid',
+    };
+  }
+
+  const parsed = JSON.parse(trimmed);
+  const pretty = JSON.stringify(parsed, null, 2);
+
+  if (
+    parsed &&
+    typeof parsed === 'object' &&
+    !Array.isArray(parsed) &&
+    Array.isArray(parsed.operations)
+  ) {
+    return {
+      editMode: 'visual',
+      visualMode: 'operations',
+      legacyValue: '',
+      operations:
+        parsed.operations.length > 0
+          ? parsed.operations.map(normalizeOperation)
+          : [createDefaultOperation()],
+      jsonText: pretty,
+      jsonError: '',
+    };
+  }
+
+  if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
+    return {
+      editMode: 'visual',
+      visualMode: 'legacy',
+      legacyValue: pretty,
+      operations: [createDefaultOperation()],
+      jsonText: pretty,
+      jsonError: '',
+    };
+  }
+
+  return {
+    editMode: 'json',
+    visualMode: 'operations',
+    legacyValue: '',
+    operations: [createDefaultOperation()],
+    jsonText: pretty,
+    jsonError: '',
+  };
+};
+
+const isOperationBlank = (operation) => {
+  const hasCondition = (operation.conditions || []).some(
+    (condition) =>
+      condition.path.trim() ||
+      String(condition.value_text ?? '').trim() ||
+      condition.mode !== 'full' ||
+      condition.invert ||
+      condition.pass_missing_key,
+  );
+  return (
+    operation.mode === 'set' &&
+    !operation.path.trim() &&
+    !operation.from.trim() &&
+    !operation.to.trim() &&
+    String(operation.value_text ?? '').trim() === '' &&
+    !operation.keep_origin &&
+    !hasCondition
+  );
+};
+
+const buildConditionPayload = (condition) => {
+  const path = condition.path.trim();
+  if (!path) return null;
+  const payload = {
+    path,
+    mode: condition.mode || 'full',
+    value: parseLooseValue(condition.value_text),
+  };
+  if (condition.invert) payload.invert = true;
+  if (condition.pass_missing_key) payload.pass_missing_key = true;
+  return payload;
+};
+
+const validateOperations = (operations, t) => {
+  for (let i = 0; i < operations.length; i++) {
+    const op = operations[i];
+    const mode = op.mode || 'set';
+    const meta = MODE_META[mode] || MODE_META.set;
+    const line = i + 1;
+    const pathValue = op.path.trim();
+    const fromValue = op.from.trim();
+    const toValue = op.to.trim();
+
+    if (meta.path && !pathValue) {
+      return t('第 {{line}} 条操作缺少 path', { line });
+    }
+    if (FROM_REQUIRED_MODES.has(mode) && !fromValue) {
+      if (!(meta.pathAlias && pathValue)) {
+        return t('第 {{line}} 条操作缺少 from', { line });
+      }
+    }
+    if (TO_REQUIRED_MODES.has(mode) && !toValue) {
+      if (!(meta.pathAlias && pathValue)) {
+        return t('第 {{line}} 条操作缺少 to', { line });
+      }
+    }
+    if (meta.from && !fromValue) {
+      return t('第 {{line}} 条操作缺少 from', { line });
+    }
+    if (meta.to && !toValue) {
+      return t('第 {{line}} 条操作缺少 to', { line });
+    }
+    if (
+      VALUE_REQUIRED_MODES.has(mode) &&
+      String(op.value_text ?? '').trim() === ''
+    ) {
+      return t('第 {{line}} 条操作缺少 value', { line });
+    }
+    if (mode === 'return_error') {
+      const raw = String(op.value_text ?? '').trim();
+      if (!raw) {
+        return t('第 {{line}} 条操作缺少 value', { 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 });
+          }
+        }
+      } catch (error) {
+        // plain string value is allowed
+      }
+    }
+  }
+  return '';
+};
+
+const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => {
+  const { t } = useTranslation();
+
+  const [editMode, setEditMode] = useState('visual');
+  const [visualMode, setVisualMode] = useState('operations');
+  const [legacyValue, setLegacyValue] = useState('');
+  const [operations, setOperations] = useState([createDefaultOperation()]);
+  const [jsonText, setJsonText] = useState('');
+  const [jsonError, setJsonError] = useState('');
+  const monacoConfiguredRef = useRef(false);
+
+  const configureMonaco = useCallback((monaco) => {
+    if (monacoConfiguredRef.current) return;
+    monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
+      validate: true,
+      allowComments: false,
+      enableSchemaRequest: false,
+      schemas: [
+        {
+          uri: MONACO_SCHEMA_URI,
+          fileMatch: [MONACO_MODEL_URI, '*param-override*.json'],
+          schema: PARAM_OVERRIDE_JSON_SCHEMA,
+        },
+      ],
+    });
+    monacoConfiguredRef.current = true;
+  }, []);
+
+  useEffect(() => {
+    if (!visible) return;
+    const nextState = parseInitialState(value);
+    setEditMode(nextState.editMode);
+    setVisualMode(nextState.visualMode);
+    setLegacyValue(nextState.legacyValue);
+    setOperations(nextState.operations);
+    setJsonText(nextState.jsonText);
+    setJsonError(nextState.jsonError);
+  }, [visible, value]);
+
+  const operationCount = useMemo(
+    () => operations.filter((item) => !isOperationBlank(item)).length,
+    [operations],
+  );
+
+  const buildVisualJson = useCallback(() => {
+    if (visualMode === 'legacy') {
+      const trimmed = legacyValue.trim();
+      if (!trimmed) return '';
+      if (!verifyJSON(trimmed)) {
+        throw new Error(t('参数覆盖必须是合法的 JSON 格式!'));
+      }
+      const parsed = JSON.parse(trimmed);
+      if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
+        throw new Error(t('旧格式必须是 JSON 对象'));
+      }
+      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]);
+
+  const switchToJsonMode = () => {
+    if (editMode === 'json') return;
+    try {
+      setJsonText(buildVisualJson());
+      setJsonError('');
+      setEditMode('json');
+    } catch (error) {
+      showError(error.message);
+    }
+  };
+
+  const switchToVisualMode = () => {
+    if (editMode === 'visual') return;
+    const trimmed = jsonText.trim();
+    if (!trimmed) {
+      setVisualMode('operations');
+      setOperations([createDefaultOperation()]);
+      setLegacyValue('');
+      setJsonError('');
+      setEditMode('visual');
+      return;
+    }
+    if (!verifyJSON(trimmed)) {
+      showError(t('参数覆盖必须是合法的 JSON 格式!'));
+      return;
+    }
+    const parsed = JSON.parse(trimmed);
+    if (
+      parsed &&
+      typeof parsed === 'object' &&
+      !Array.isArray(parsed) &&
+      Array.isArray(parsed.operations)
+    ) {
+      setVisualMode('operations');
+      setOperations(
+        parsed.operations.length > 0
+          ? parsed.operations.map(normalizeOperation)
+          : [createDefaultOperation()],
+      );
+      setLegacyValue('');
+      setJsonError('');
+      setEditMode('visual');
+      return;
+    }
+    if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
+      setVisualMode('legacy');
+      setLegacyValue(JSON.stringify(parsed, null, 2));
+      setOperations([createDefaultOperation()]);
+      setJsonError('');
+      setEditMode('visual');
+      return;
+    }
+    showError(t('参数覆盖必须是合法的 JSON 对象'));
+  };
+
+  const setOldTemplate = () => {
+    const text = JSON.stringify(LEGACY_TEMPLATE, null, 2);
+    setVisualMode('legacy');
+    setLegacyValue(text);
+    setJsonText(text);
+    setJsonError('');
+    setEditMode('visual');
+  };
+
+  const setNewTemplate = () => {
+    setVisualMode('operations');
+    setOperations(OPERATION_TEMPLATE.operations.map(normalizeOperation));
+    setJsonText(JSON.stringify(OPERATION_TEMPLATE, null, 2));
+    setJsonError('');
+    setEditMode('visual');
+  };
+
+  const clearValue = () => {
+    setVisualMode('operations');
+    setLegacyValue('');
+    setOperations([createDefaultOperation()]);
+    setJsonText('');
+    setJsonError('');
+  };
+
+  const updateOperation = (operationId, patch) => {
+    setOperations((prev) =>
+      prev.map((item) =>
+        item.id === operationId ? { ...item, ...patch } : item,
+      ),
+    );
+  };
+
+  const addOperation = () => {
+    setOperations((prev) => [...prev, createDefaultOperation()]);
+  };
+
+  const duplicateOperation = (operationId) => {
+    setOperations((prev) => {
+      const index = prev.findIndex((item) => item.id === operationId);
+      if (index < 0) return prev;
+      const source = prev[index];
+      const cloned = normalizeOperation({
+        path: source.path,
+        mode: source.mode,
+        value: parseLooseValue(source.value_text),
+        keep_origin: source.keep_origin,
+        from: source.from,
+        to: source.to,
+        logic: source.logic,
+        conditions: (source.conditions || []).map((condition) => ({
+          path: condition.path,
+          mode: condition.mode,
+          value: parseLooseValue(condition.value_text),
+          invert: condition.invert,
+          pass_missing_key: condition.pass_missing_key,
+        })),
+      });
+      const next = [...prev];
+      next.splice(index + 1, 0, cloned);
+      return next;
+    });
+  };
+
+  const removeOperation = (operationId) => {
+    setOperations((prev) => {
+      if (prev.length <= 1) return [createDefaultOperation()];
+      return prev.filter((item) => item.id !== operationId);
+    });
+  };
+
+  const addCondition = (operationId) => {
+    setOperations((prev) =>
+      prev.map((operation) =>
+        operation.id === operationId
+          ? {
+              ...operation,
+              conditions: [...(operation.conditions || []), createDefaultCondition()],
+            }
+          : operation,
+      ),
+    );
+  };
+
+  const updateCondition = (operationId, conditionId, patch) => {
+    setOperations((prev) =>
+      prev.map((operation) => {
+        if (operation.id !== operationId) return operation;
+        return {
+          ...operation,
+          conditions: (operation.conditions || []).map((condition) =>
+            condition.id === conditionId
+              ? { ...condition, ...patch }
+              : condition,
+          ),
+        };
+      }),
+    );
+  };
+
+  const removeCondition = (operationId, conditionId) => {
+    setOperations((prev) =>
+      prev.map((operation) => {
+        if (operation.id !== operationId) return operation;
+        return {
+          ...operation,
+          conditions: (operation.conditions || []).filter(
+            (condition) => condition.id !== conditionId,
+          ),
+        };
+      }),
+    );
+  };
+
+  const handleJsonChange = (nextValue) => {
+    setJsonText(nextValue);
+    const trimmed = String(nextValue || '').trim();
+    if (!trimmed) {
+      setJsonError('');
+      return;
+    }
+    if (!verifyJSON(trimmed)) {
+      setJsonError(t('JSON格式错误'));
+      return;
+    }
+    setJsonError('');
+  };
+
+  const formatJson = () => {
+    const trimmed = jsonText.trim();
+    if (!trimmed) return;
+    if (!verifyJSON(trimmed)) {
+      showError(t('参数覆盖必须是合法的 JSON 格式!'));
+      return;
+    }
+    setJsonText(JSON.stringify(JSON.parse(trimmed), null, 2));
+    setJsonError('');
+  };
+
+  const visualPreview = useMemo(() => {
+    if (editMode !== 'visual' || visualMode !== 'operations') {
+      return '';
+    }
+    try {
+      return buildVisualJson() || '';
+    } catch (error) {
+      return `// ${error.message}`;
+    }
+  }, [buildVisualJson, editMode, visualMode]);
+
+  const handleSave = () => {
+    try {
+      let result = '';
+      if (editMode === 'json') {
+        const trimmed = jsonText.trim();
+        if (!trimmed) {
+          result = '';
+        } else {
+          if (!verifyJSON(trimmed)) {
+            throw new Error(t('参数覆盖必须是合法的 JSON 格式!'));
+          }
+          result = JSON.stringify(JSON.parse(trimmed), null, 2);
+        }
+      } else {
+        result = buildVisualJson();
+      }
+      onSave?.(result);
+    } catch (error) {
+      showError(error.message);
+    }
+  };
+
+  return (
+    <Modal
+      title={t('参数覆盖')}
+      visible={visible}
+      width={980}
+      onCancel={onCancel}
+      onOk={handleSave}
+      okText={t('保存')}
+      cancelText={t('取消')}
+    >
+      <Space vertical align='start' spacing={12} style={{ width: '100%' }}>
+        <Banner
+          fullMode={false}
+          type='info'
+          description={t(
+            '支持旧格式直接覆盖,也支持新格式 operations 条件编辑;可在可视化和 JSON 之间双向切换。',
+          )}
+        />
+
+        <Space wrap>
+          <Button
+            type={editMode === 'visual' ? 'primary' : 'tertiary'}
+            onClick={switchToVisualMode}
+          >
+            {t('可视化')}
+          </Button>
+          <Button
+            type={editMode === 'json' ? 'primary' : 'tertiary'}
+            onClick={switchToJsonMode}
+          >
+            {t('JSON 模式')}
+          </Button>
+          <Button onClick={setOldTemplate}>{t('旧格式模板')}</Button>
+          <Button onClick={setNewTemplate}>{t('新格式模板')}</Button>
+          <Button onClick={clearValue}>{t('不更改')}</Button>
+        </Space>
+
+        {editMode === 'visual' ? (
+          <div style={{ width: '100%' }}>
+            <Space wrap style={{ marginBottom: 12 }}>
+              <Button
+                type={visualMode === 'operations' ? 'primary' : 'tertiary'}
+                onClick={() => setVisualMode('operations')}
+              >
+                {t('新格式模板')}
+              </Button>
+              <Button
+                type={visualMode === 'legacy' ? 'primary' : 'tertiary'}
+                onClick={() => setVisualMode('legacy')}
+              >
+                {t('旧格式模板')}
+              </Button>
+            </Space>
+
+            {visualMode === 'legacy' ? (
+              <JSONEditor
+                field='param_override_legacy'
+                label={t('旧格式(直接覆盖):')}
+                placeholder={JSON.stringify(LEGACY_TEMPLATE, null, 2)}
+                value={legacyValue}
+                onChange={setLegacyValue}
+                template={LEGACY_TEMPLATE}
+                templateLabel={t('填入模板')}
+                editorType='keyValue'
+                showClear
+              />
+            ) : (
+              <div>
+                <div className='flex items-center justify-between mb-3'>
+                  <Space>
+                    <Text>{t('新格式(支持条件判断与json自定义):')}</Text>
+                    <Tag color='cyan'>
+                      {`${t('规则')}: ${operationCount}`}
+                    </Tag>
+                  </Space>
+                  <Button icon={<IconPlus />} onClick={addOperation}>
+                    {t('新增规则')}
+                  </Button>
+                </div>
+
+                <Space vertical spacing={8} style={{ width: '100%' }}>
+                  {operations.map((operation, index) => {
+                    const mode = operation.mode || 'set';
+                    const meta = MODE_META[mode] || MODE_META.set;
+                    const conditions = operation.conditions || [];
+                    return (
+                      <Card key={operation.id} className='!rounded-xl border'>
+                        <div className='flex items-center justify-between mb-2'>
+                          <Space>
+                            <Tag>{`#${index + 1}`}</Tag>
+                            <Text>{mode}</Text>
+                          </Space>
+                          <Space>
+                            <Button
+                              size='small'
+                              type='tertiary'
+                              onClick={() => duplicateOperation(operation.id)}
+                            >
+                              {t('复制')}
+                            </Button>
+                            <Button
+                              theme='borderless'
+                              type='danger'
+                              icon={<IconDelete />}
+                              onClick={() => removeOperation(operation.id)}
+                            />
+                          </Space>
+                        </div>
+
+                        <Row gutter={12}>
+                          <Col xs={24} md={8}>
+                            <Text type='tertiary' size='small'>
+                              mode
+                            </Text>
+                            <Select
+                              value={mode}
+                              optionList={OPERATION_MODE_OPTIONS}
+                              onChange={(nextMode) =>
+                                updateOperation(operation.id, { mode: nextMode })
+                              }
+                              style={{ width: '100%' }}
+                            />
+                          </Col>
+                          {meta.path || meta.pathOptional ? (
+                            <Col xs={24} md={16}>
+                              <Text type='tertiary' size='small'>
+                                {meta.pathOptional ? 'path (optional)' : 'path'}
+                              </Text>
+                              <Input
+                                value={operation.path}
+                                placeholder={
+                                  mode.includes('header')
+                                    ? 'X-Debug-Mode'
+                                    : mode === 'prune_objects'
+                                      ? 'messages (optional)'
+                                      : 'temperature'
+                                }
+                                onChange={(nextValue) =>
+                                  updateOperation(operation.id, {
+                                    path: nextValue,
+                                  })
+                                }
+                              />
+                              <Space wrap style={{ marginTop: 6 }}>
+                                {OPERATION_PATH_SUGGESTIONS.map((pathItem) => (
+                                  <Tag
+                                    key={`${operation.id}_${pathItem}`}
+                                    size='small'
+                                    color='grey'
+                                    className='cursor-pointer'
+                                    onClick={() =>
+                                      updateOperation(operation.id, {
+                                        path: pathItem,
+                                      })
+                                    }
+                                  >
+                                    {pathItem}
+                                  </Tag>
+                                ))}
+                              </Space>
+                            </Col>
+                          ) : null}
+                        </Row>
+                        <Text type='tertiary' size='small' className='mt-1 block'>
+                          {MODE_DESCRIPTIONS[mode] || ''}
+                        </Text>
+
+                        {meta.value ? (
+                          <div className='mt-2'>
+                            <Text type='tertiary' size='small'>
+                              value (JSON or plain text)
+                            </Text>
+                            <Input.TextArea
+                              value={operation.value_text}
+                              autosize={{ minRows: 1, maxRows: 4 }}
+                              placeholder='0.7'
+                              onChange={(nextValue) =>
+                                updateOperation(operation.id, {
+                                  value_text: nextValue,
+                                })
+                              }
+                            />
+                          </div>
+                        ) : null}
+
+                        {meta.keepOrigin ? (
+                          <div className='mt-2'>
+                            <Switch
+                              checked={operation.keep_origin}
+                              checkedText={t('开')}
+                              uncheckedText={t('关')}
+                              onChange={(nextValue) =>
+                                updateOperation(operation.id, {
+                                  keep_origin: nextValue,
+                                })
+                              }
+                            />
+                            <Text type='tertiary' size='small' className='ml-2'>
+                              keep_origin
+                            </Text>
+                          </div>
+                        ) : null}
+
+                        {meta.from || meta.to === false || meta.to ? (
+                          <Row gutter={12} style={{ marginTop: 8 }}>
+                            {meta.from || meta.to === false ? (
+                              <Col xs={24} md={12}>
+                                <Text type='tertiary' size='small'>
+                                  from
+                                </Text>
+                                <Input
+                                  value={operation.from}
+                                  placeholder={
+                                    mode.includes('header')
+                                      ? 'Authorization'
+                                      : 'model'
+                                  }
+                                  onChange={(nextValue) =>
+                                    updateOperation(operation.id, {
+                                      from: nextValue,
+                                    })
+                                  }
+                                />
+                              </Col>
+                            ) : null}
+                            {meta.to || meta.to === false ? (
+                              <Col xs={24} md={12}>
+                                <Text type='tertiary' size='small'>
+                                  to
+                                </Text>
+                                <Input
+                                  value={operation.to}
+                                  placeholder={
+                                    mode.includes('header')
+                                      ? 'X-Upstream-Auth'
+                                      : 'original_model'
+                                  }
+                                  onChange={(nextValue) =>
+                                    updateOperation(operation.id, { to: nextValue })
+                                  }
+                                />
+                              </Col>
+                            ) : null}
+                          </Row>
+                        ) : null}
+
+                        <div className='mt-3 border rounded-lg p-2'>
+                          <div className='flex items-center justify-between mb-2'>
+                            <Space>
+                              <Text>{t('条件')}</Text>
+                              <Select
+                                value={operation.logic || 'OR'}
+                                optionList={[
+                                  { label: 'OR', value: 'OR' },
+                                  { label: 'AND', value: 'AND' },
+                                ]}
+                                style={{ width: 96 }}
+                                onChange={(nextValue) =>
+                                  updateOperation(operation.id, {
+                                    logic: nextValue,
+                                  })
+                                }
+                              />
+                            </Space>
+                            <Button
+                              icon={<IconPlus />}
+                              size='small'
+                              onClick={() => addCondition(operation.id)}
+                            >
+                              {t('新增条件')}
+                            </Button>
+                          </div>
+
+                          {conditions.length === 0 ? (
+                            <Text type='tertiary' size='small'>
+                              {t('没有条件时,默认总是执行该操作。')}
+                            </Text>
+                          ) : (
+                            <Space vertical spacing={8} style={{ width: '100%' }}>
+                              {conditions.map((condition, conditionIndex) => (
+                                <Card
+                                  key={condition.id}
+                                  bodyStyle={{ padding: 10 }}
+                                  className='!rounded-lg'
+                                >
+                                  <div className='flex items-center justify-between mb-2'>
+                                    <Tag size='small'>{`C${conditionIndex + 1}`}</Tag>
+                                    <Button
+                                      theme='borderless'
+                                      type='danger'
+                                      icon={<IconDelete />}
+                                      size='small'
+                                      onClick={() =>
+                                        removeCondition(operation.id, condition.id)
+                                      }
+                                    />
+                                  </div>
+                                  <Row gutter={12}>
+                                    <Col xs={24} md={10}>
+                                      <Text type='tertiary' size='small'>
+                                        path
+                                      </Text>
+                                      <Input
+                                        value={condition.path}
+                                        placeholder='model'
+                                        onChange={(nextValue) =>
+                                          updateCondition(
+                                            operation.id,
+                                            condition.id,
+                                            { path: nextValue },
+                                          )
+                                        }
+                                      />
+                                      <Space wrap style={{ marginTop: 6 }}>
+                                        {CONDITION_PATH_SUGGESTIONS.map(
+                                          (pathItem) => (
+                                            <Tag
+                                              key={`${condition.id}_${pathItem}`}
+                                              size='small'
+                                              color='grey'
+                                              className='cursor-pointer'
+                                              onClick={() =>
+                                                updateCondition(
+                                                  operation.id,
+                                                  condition.id,
+                                                  { path: pathItem },
+                                                )
+                                              }
+                                            >
+                                              {pathItem}
+                                            </Tag>
+                                          ),
+                                        )}
+                                      </Space>
+                                    </Col>
+                                    <Col xs={24} md={8}>
+                                      <Text type='tertiary' size='small'>
+                                        mode
+                                      </Text>
+                                      <Select
+                                        value={condition.mode}
+                                        optionList={CONDITION_MODE_OPTIONS}
+                                        onChange={(nextValue) =>
+                                          updateCondition(
+                                            operation.id,
+                                            condition.id,
+                                            { mode: nextValue },
+                                          )
+                                        }
+                                        style={{ width: '100%' }}
+                                      />
+                                    </Col>
+                                    <Col xs={24} md={6}>
+                                      <Text type='tertiary' size='small'>
+                                        value
+                                      </Text>
+                                      <Input
+                                        value={condition.value_text}
+                                        placeholder='gpt'
+                                        onChange={(nextValue) =>
+                                          updateCondition(
+                                            operation.id,
+                                            condition.id,
+                                            { value_text: nextValue },
+                                          )
+                                        }
+                                      />
+                                    </Col>
+                                  </Row>
+                                  <Space style={{ marginTop: 8 }}>
+                                    <Switch
+                                      checked={condition.invert}
+                                      checkedText={t('开')}
+                                      uncheckedText={t('关')}
+                                      onChange={(nextValue) =>
+                                        updateCondition(
+                                          operation.id,
+                                          condition.id,
+                                          { invert: nextValue },
+                                        )
+                                      }
+                                    />
+                                    <Text type='tertiary' size='small'>
+                                      invert
+                                    </Text>
+                                    <Switch
+                                      checked={condition.pass_missing_key}
+                                      checkedText={t('开')}
+                                      uncheckedText={t('关')}
+                                      onChange={(nextValue) =>
+                                        updateCondition(
+                                          operation.id,
+                                          condition.id,
+                                          { pass_missing_key: nextValue },
+                                        )
+                                      }
+                                    />
+                                    <Text type='tertiary' size='small'>
+                                      pass_missing_key
+                                    </Text>
+                                  </Space>
+                                </Card>
+                              ))}
+                            </Space>
+                          )}
+                        </div>
+                      </Card>
+                    );
+                  })}
+                </Space>
+                <Card className='!rounded-xl border mt-3'>
+                  <div className='flex items-center justify-between mb-2'>
+                    <Text>{t('实时 JSON 预览')}</Text>
+                    <Tag color='grey'>{t('预览')}</Tag>
+                  </div>
+                  <pre className='mb-0 text-xs leading-5 whitespace-pre-wrap break-all max-h-64 overflow-auto'>
+                    {visualPreview || '{}'}
+                  </pre>
+                </Card>
+              </div>
+            )}
+          </div>
+        ) : (
+          <div style={{ width: '100%' }}>
+            <Space style={{ marginBottom: 8 }} wrap>
+              <Button onClick={formatJson}>{t('格式化')}</Button>
+              <Tag color='cyan'>{t('JSON 智能提示')}</Tag>
+            </Space>
+            <div className='border rounded-lg overflow-hidden'>
+              <MonacoEditor
+                beforeMount={configureMonaco}
+                path={MONACO_MODEL_URI}
+                language='json'
+                value={jsonText}
+                onChange={(nextValue) => handleJsonChange(nextValue ?? '')}
+                height='460px'
+                options={{
+                  minimap: { enabled: false },
+                  fontSize: 13,
+                  lineNumbers: 'on',
+                  automaticLayout: true,
+                  scrollBeyondLastLine: false,
+                  tabSize: 2,
+                  insertSpaces: true,
+                  wordWrap: 'on',
+                  formatOnPaste: true,
+                  formatOnType: true,
+                }}
+              />
+            </div>
+            <Text type='tertiary' size='small' className='mt-2 block'>
+              {t('支持 mode/conditions 字段补全与 JSON Schema 校验')}
+            </Text>
+            {jsonError ? (
+              <Text className='text-red-500 text-xs mt-2'>{jsonError}</Text>
+            ) : null}
+          </div>
+        )}
+      </Space>
+    </Modal>
+  );
+};
+
+export default ParamOverrideEditorModal;