diff --git a/web/package.json b/web/package.json
index 97c7c821..7d00d8c4 100644
--- a/web/package.json
+++ b/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",
diff --git a/web/src/components/table/channels/modals/EditChannelModal.jsx b/web/src/components/table/channels/modals/EditChannelModal.jsx
index 6e85ca98..d2e77cf7 100644
--- a/web/src/components/table/channels/modals/EditChannelModal.jsx
+++ b/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}
/>
-
- handleInputChange('param_override', value)
- }
- extraText={
-
-
+
+ {t('参数覆盖')}
+
+ }
+ onClick={() => setParamOverrideEditorVisible(true)}
+ >
+ {t('可视化编辑')}
+
+
+ handleInputChange('param_override', '')}
>
- {t('格式化')}
-
+ {t('不更改')}
+
+
+
+
+ {t('此项可选,用于覆盖请求参数。不支持覆盖 stream 参数')}
+
+
+
+
+ {paramOverrideMeta.tagLabel}
+
+ setParamOverrideEditorVisible(true)}
+ >
+ {t('编辑')}
+
- }
- showClear
- />
+
+ {paramOverrideMeta.preview}
+
+
+
{
/>
+ setParamOverrideEditorVisible(false)}
+ onSave={(nextValue) => {
+ handleInputChange('param_override', nextValue);
+ setParamOverrideEditorVisible(false);
+ }}
+ />
+
.
+
+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 (
+
+
+
+
+
+
+ {t('可视化')}
+
+
+ {t('JSON 模式')}
+
+ {t('旧格式模板')}
+ {t('新格式模板')}
+ {t('不更改')}
+
+
+ {editMode === 'visual' ? (
+
+
+ setVisualMode('operations')}
+ >
+ {t('新格式模板')}
+
+ setVisualMode('legacy')}
+ >
+ {t('旧格式模板')}
+
+
+
+ {visualMode === 'legacy' ? (
+
+ ) : (
+
+
+
+ {t('新格式(支持条件判断与json自定义):')}
+
+ {`${t('规则')}: ${operationCount}`}
+
+
+ } onClick={addOperation}>
+ {t('新增规则')}
+
+
+
+
+ {operations.map((operation, index) => {
+ const mode = operation.mode || 'set';
+ const meta = MODE_META[mode] || MODE_META.set;
+ const conditions = operation.conditions || [];
+ return (
+
+
+
+ {`#${index + 1}`}
+ {mode}
+
+
+ duplicateOperation(operation.id)}
+ >
+ {t('复制')}
+
+ }
+ onClick={() => removeOperation(operation.id)}
+ />
+
+
+
+
+
+
+ mode
+
+
+
+ {MODE_DESCRIPTIONS[mode] || ''}
+
+
+ {meta.value ? (
+
+
+ value (JSON or plain text)
+
+
+ updateOperation(operation.id, {
+ value_text: nextValue,
+ })
+ }
+ />
+
+ ) : null}
+
+ {meta.keepOrigin ? (
+
+
+ updateOperation(operation.id, {
+ keep_origin: nextValue,
+ })
+ }
+ />
+
+ keep_origin
+
+
+ ) : null}
+
+ {meta.from || meta.to === false || meta.to ? (
+
+ {meta.from || meta.to === false ? (
+
+
+ from
+
+
+ updateOperation(operation.id, {
+ from: nextValue,
+ })
+ }
+ />
+
+ ) : null}
+ {meta.to || meta.to === false ? (
+
+
+ to
+
+
+ updateOperation(operation.id, { to: nextValue })
+ }
+ />
+
+ ) : null}
+
+ ) : null}
+
+
+
+
+ {t('条件')}
+
+ }
+ size='small'
+ onClick={() => addCondition(operation.id)}
+ >
+ {t('新增条件')}
+
+
+
+ {conditions.length === 0 ? (
+
+ {t('没有条件时,默认总是执行该操作。')}
+
+ ) : (
+
+ {conditions.map((condition, conditionIndex) => (
+
+
+ {`C${conditionIndex + 1}`}
+ }
+ size='small'
+ onClick={() =>
+ removeCondition(operation.id, condition.id)
+ }
+ />
+
+
+
+
+ path
+
+
+ updateCondition(
+ operation.id,
+ condition.id,
+ { path: nextValue },
+ )
+ }
+ />
+
+ {CONDITION_PATH_SUGGESTIONS.map(
+ (pathItem) => (
+
+ updateCondition(
+ operation.id,
+ condition.id,
+ { path: pathItem },
+ )
+ }
+ >
+ {pathItem}
+
+ ),
+ )}
+
+
+
+
+ mode
+
+
+
+
+ updateCondition(
+ operation.id,
+ condition.id,
+ { invert: nextValue },
+ )
+ }
+ />
+
+ invert
+
+
+ updateCondition(
+ operation.id,
+ condition.id,
+ { pass_missing_key: nextValue },
+ )
+ }
+ />
+
+ pass_missing_key
+
+
+
+ ))}
+
+ )}
+
+
+ );
+ })}
+
+
+
+ {t('实时 JSON 预览')}
+ {t('预览')}
+
+
+ {visualPreview || '{}'}
+
+
+
+ )}
+
+ ) : (
+
+
+ {t('格式化')}
+ {t('JSON 智能提示')}
+
+
+ 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,
+ }}
+ />
+
+
+ {t('支持 mode/conditions 字段补全与 JSON Schema 校验')}
+
+ {jsonError ? (
+
{jsonError}
+ ) : null}
+
+ )}
+
+
+ );
+};
+
+export default ParamOverrideEditorModal;