diff --git a/web/src/components/common/JSONEditor.js b/web/src/components/common/JSONEditor.js
new file mode 100644
index 00000000..d0c159b2
--- /dev/null
+++ b/web/src/components/common/JSONEditor.js
@@ -0,0 +1,609 @@
+import React, { useState, useEffect, useCallback } from 'react';
+import { useTranslation } from 'react-i18next';
+import {
+ Space,
+ Button,
+ Form,
+ Card,
+ Typography,
+ Banner,
+ Row,
+ Col,
+ InputNumber,
+ Switch,
+ Select,
+ Input,
+} from '@douyinfe/semi-ui';
+import {
+ IconCode,
+ IconEdit,
+ IconPlus,
+ IconDelete,
+ IconSetting,
+} from '@douyinfe/semi-icons';
+
+const { Text } = Typography;
+
+const JSONEditor = ({
+ value = '',
+ onChange,
+ field,
+ label,
+ placeholder,
+ extraText,
+ showClear = true,
+ template,
+ templateLabel,
+ editorType = 'keyValue', // keyValue, object, region
+ autosize = true,
+ rules = [],
+ formApi = null,
+ ...props
+}) => {
+ const { t } = useTranslation();
+
+ // 初始化JSON数据
+ const [jsonData, setJsonData] = useState(() => {
+ // 初始化时解析JSON数据
+ if (value && value.trim()) {
+ try {
+ const parsed = JSON.parse(value);
+ return parsed;
+ } catch (error) {
+ return {};
+ }
+ }
+ return {};
+ });
+
+ // 根据键数量决定默认编辑模式
+ const [editMode, setEditMode] = useState(() => {
+ // 如果初始JSON数据的键数量大于10个,则默认使用手动模式
+ if (value && value.trim()) {
+ try {
+ const parsed = JSON.parse(value);
+ const keyCount = Object.keys(parsed).length;
+ return keyCount > 10 ? 'manual' : 'visual';
+ } catch (error) {
+ return 'visual';
+ }
+ }
+ return 'visual';
+ });
+ const [jsonError, setJsonError] = useState('');
+
+ // 数据同步 - 当value变化时总是更新jsonData(如果JSON有效)
+ useEffect(() => {
+ try {
+ const parsed = value && value.trim() ? JSON.parse(value) : {};
+ setJsonData(parsed);
+ setJsonError('');
+ } catch (error) {
+ console.log('JSON解析失败:', error.message);
+ setJsonError(error.message);
+ // JSON格式错误时不更新jsonData
+ }
+ }, [value]);
+
+
+ // 处理可视化编辑的数据变化
+ const handleVisualChange = useCallback((newData) => {
+ setJsonData(newData);
+ setJsonError('');
+ const jsonString = Object.keys(newData).length === 0 ? '' : JSON.stringify(newData, null, 2);
+
+ // 通过formApi设置值(如果提供的话)
+ if (formApi && field) {
+ formApi.setValue(field, jsonString);
+ }
+
+ onChange?.(jsonString);
+ }, [onChange, formApi, field]);
+
+ // 处理手动编辑的数据变化
+ const handleManualChange = useCallback((newValue) => {
+ onChange?.(newValue);
+ // 验证JSON格式
+ if (newValue && newValue.trim()) {
+ try {
+ const parsed = JSON.parse(newValue);
+ setJsonError('');
+ // 预先准备可视化数据,但不立即应用
+ // 这样切换到可视化模式时数据已经准备好了
+ } catch (error) {
+ setJsonError(error.message);
+ }
+ } else {
+ setJsonError('');
+ }
+ }, [onChange]);
+
+ // 切换编辑模式
+ const toggleEditMode = useCallback(() => {
+ if (editMode === 'visual') {
+ // 从可视化模式切换到手动模式
+ setEditMode('manual');
+ } else {
+ // 从手动模式切换到可视化模式,需要验证JSON
+ try {
+ const parsed = value && value.trim() ? JSON.parse(value) : {};
+ setJsonData(parsed);
+ setJsonError('');
+ setEditMode('visual');
+ } catch (error) {
+ setJsonError(error.message);
+ // JSON格式错误时不切换模式
+ return;
+ }
+ }
+ }, [editMode, value]);
+
+ // 添加键值对
+ const addKeyValue = useCallback(() => {
+ const newData = { ...jsonData };
+ const keys = Object.keys(newData);
+ let newKey = 'key';
+ let counter = 1;
+ while (newData.hasOwnProperty(newKey)) {
+ newKey = `key${counter}`;
+ counter++;
+ }
+ newData[newKey] = '';
+ handleVisualChange(newData);
+ }, [jsonData, handleVisualChange]);
+
+ // 删除键值对
+ const removeKeyValue = useCallback((keyToRemove) => {
+ const newData = { ...jsonData };
+ delete newData[keyToRemove];
+ handleVisualChange(newData);
+ }, [jsonData, handleVisualChange]);
+
+ // 更新键名
+ const updateKey = useCallback((oldKey, newKey) => {
+ if (oldKey === newKey) return;
+ const newData = { ...jsonData };
+ const value = newData[oldKey];
+ delete newData[oldKey];
+ newData[newKey] = value;
+ handleVisualChange(newData);
+ }, [jsonData, handleVisualChange]);
+
+ // 更新值
+ const updateValue = useCallback((key, newValue) => {
+ const newData = { ...jsonData };
+ newData[key] = newValue;
+ handleVisualChange(newData);
+ }, [jsonData, handleVisualChange]);
+
+ // 填入模板
+ const fillTemplate = useCallback(() => {
+ if (template) {
+ const templateString = JSON.stringify(template, null, 2);
+
+ // 通过formApi设置值(如果提供的话)
+ if (formApi && field) {
+ formApi.setValue(field, templateString);
+ }
+
+ // 无论哪种模式都要更新值
+ onChange?.(templateString);
+
+ // 如果是可视化模式,同时更新jsonData
+ if (editMode === 'visual') {
+ setJsonData(template);
+ }
+
+ // 清除错误状态
+ setJsonError('');
+ }
+ }, [template, onChange, editMode, formApi, field]);
+
+ // 渲染键值对编辑器
+ const renderKeyValueEditor = () => {
+ const entries = Object.entries(jsonData);
+
+ return (
+
+ {entries.length === 0 && (
+
+
+
+
+
+ {t('暂无数据,点击下方按钮添加键值对')}
+
+
+ )}
+
+ {entries.map(([key, value], index) => (
+
+
+
+
+ {t('键名')}
+ updateKey(key, newKey)}
+ size="small"
+ />
+
+
+
+
+ {t('值')}
+ updateValue(key, newValue)}
+ size="small"
+ />
+
+
+
+
+ }
+ type="danger"
+ theme="borderless"
+ size="small"
+ onClick={() => removeKeyValue(key)}
+ className="hover:bg-red-50"
+ />
+
+
+
+
+ ))}
+
+
+ }
+ onClick={addKeyValue}
+ size="small"
+ theme="solid"
+ type="primary"
+ className="shadow-sm hover:shadow-md transition-shadow px-4"
+ >
+ {t('添加键值对')}
+
+
+
+ );
+ };
+
+ // 渲染对象编辑器(用于复杂JSON)
+ const renderObjectEditor = () => {
+ const entries = Object.entries(jsonData);
+
+ return (
+
+ {entries.length === 0 && (
+
+
+
+
+
+ {t('暂无参数,点击下方按钮添加请求参数')}
+
+
+ )}
+
+ {entries.map(([key, value], index) => (
+
+
+
+
+ {t('参数名')}
+ updateKey(key, newKey)}
+ size="small"
+ />
+
+
+
+
+ {t('参数值')} ({typeof value})
+ {renderValueInput(key, value)}
+
+
+
+
+ }
+ type="danger"
+ theme="borderless"
+ size="small"
+ onClick={() => removeKeyValue(key)}
+ className="hover:bg-red-50"
+ />
+
+
+
+
+ ))}
+
+
+ }
+ onClick={addKeyValue}
+ size="small"
+ theme="solid"
+ type="primary"
+ className="shadow-sm hover:shadow-md transition-shadow px-4"
+ >
+ {t('添加参数')}
+
+
+
+ );
+ };
+
+ // 渲染参数值输入控件
+ const renderValueInput = (key, value) => {
+ const valueType = typeof value;
+
+ if (valueType === 'boolean') {
+ return (
+
+ updateValue(key, newValue)}
+ size="small"
+ />
+
+ {value ? t('true') : t('false')}
+
+
+ );
+ }
+
+ if (valueType === 'number') {
+ return (
+ updateValue(key, newValue)}
+ size="small"
+ style={{ width: '100%' }}
+ step={key === 'temperature' ? 0.1 : 1}
+ precision={key === 'temperature' ? 2 : 0}
+ placeholder={t('输入数字')}
+ />
+ );
+ }
+
+ // 字符串类型或其他类型
+ return (
+ {
+ // 尝试转换为适当的类型
+ let convertedValue = newValue;
+ if (newValue === 'true') convertedValue = true;
+ else if (newValue === 'false') convertedValue = false;
+ else if (!isNaN(newValue) && newValue !== '' && newValue !== '0') {
+ convertedValue = Number(newValue);
+ }
+
+ updateValue(key, convertedValue);
+ }}
+ size="small"
+ />
+ );
+ };
+
+ // 渲染区域编辑器(特殊格式)
+ const renderRegionEditor = () => {
+ const entries = Object.entries(jsonData);
+ const defaultEntry = entries.find(([key]) => key === 'default');
+ const modelEntries = entries.filter(([key]) => key !== 'default');
+
+ return (
+
+ {/* 默认区域 */}
+
+
+ {t('默认区域')}
+
+ updateValue('default', value)}
+ size="small"
+ />
+
+
+ {/* 模型专用区域 */}
+
+
{t('模型专用区域')}
+ {modelEntries.map(([modelName, region], index) => (
+
+
+
+
+ {t('模型名称')}
+ updateKey(modelName, newKey)}
+ size="small"
+ />
+
+
+
+
+ {t('区域')}
+ updateValue(modelName, newValue)}
+ size="small"
+ />
+
+
+
+
+ }
+ type="danger"
+ theme="borderless"
+ size="small"
+ onClick={() => removeKeyValue(modelName)}
+ className="hover:bg-red-50"
+ />
+
+
+
+
+ ))}
+
+
+ }
+ onClick={addKeyValue}
+ size="small"
+ theme="solid"
+ type="primary"
+ className="shadow-sm hover:shadow-md transition-shadow px-4"
+ >
+ {t('添加模型区域')}
+
+
+
+
+ );
+ };
+
+ // 渲染可视化编辑器
+ const renderVisualEditor = () => {
+ switch (editorType) {
+ case 'region':
+ return renderRegionEditor();
+ case 'object':
+ return renderObjectEditor();
+ case 'keyValue':
+ default:
+ return renderKeyValueEditor();
+ }
+ };
+
+ const hasJsonError = jsonError && jsonError.trim() !== '';
+
+ return (
+
+ {/* Label统一显示在上方 */}
+ {label && (
+
+ {label}
+
+ )}
+
+ {/* 编辑模式切换 */}
+
+
+ {editMode === 'visual' && (
+
+ {t('可视化模式')}
+
+ )}
+ {editMode === 'manual' && (
+
+ {t('手动编辑模式')}
+
+ )}
+
+
+ {template && templateLabel && (
+
+ )}
+
+ }
+ onClick={toggleEditMode}
+ disabled={editMode === 'manual' && hasJsonError}
+ className={editMode === 'visual' ? 'shadow-sm' : ''}
+ >
+ {t('可视化')}
+
+ }
+ onClick={toggleEditMode}
+ className={editMode === 'manual' ? 'shadow-sm' : ''}
+ >
+ {t('手动编辑')}
+
+
+
+
+
+ {/* JSON错误提示 */}
+ {hasJsonError && (
+
+ )}
+
+ {/* 编辑器内容 */}
+ {editMode === 'visual' ? (
+
+
+ {renderVisualEditor()}
+
+ {/* 可视化模式下的额外文本显示在下方 */}
+ {extraText && (
+
+ {extraText}
+
+ )}
+ {/* 隐藏的Form字段用于验证和数据绑定 */}
+
+
+ ) : (
+
+ )}
+
+ {/* 额外文本在手动编辑模式下显示 */}
+ {extraText && editMode === 'manual' && (
+
+ {extraText}
+
+ )}
+
+ );
+};
+
+export default JSONEditor;
\ No newline at end of file
diff --git a/web/src/components/table/channels/modals/EditChannelModal.jsx b/web/src/components/table/channels/modals/EditChannelModal.jsx
index a3f09166..37e9af75 100644
--- a/web/src/components/table/channels/modals/EditChannelModal.jsx
+++ b/web/src/components/table/channels/modals/EditChannelModal.jsx
@@ -48,6 +48,7 @@ import {
} from '@douyinfe/semi-ui';
import { getChannelModels, copy, getChannelIcon, getModelCategories, selectFilter } from '../../../../helpers';
import ModelSelectModal from './ModelSelectModal';
+import JSONEditor from '../../../common/JSONEditor';
import {
IconSave,
IconClose,
@@ -69,7 +70,9 @@ const STATUS_CODE_MAPPING_EXAMPLE = {
};
const REGION_EXAMPLE = {
- default: 'us-central1',
+ "default": 'global',
+ "gemini-1.5-pro-002": "europe-west2",
+ "gemini-1.5-flash-002": "europe-west2",
'claude-3-5-sonnet-20240620': 'europe-west1',
};
@@ -1174,24 +1177,24 @@ const EditChannelModal = (props) => {
)}
{inputs.type === 41 && (
- handleInputChange('other', value)}
rules={[{ required: true, message: t('请填写部署地区') }]}
+ template={REGION_EXAMPLE}
+ templateLabel={t('填入模板')}
+ editorType="region"
+ formApi={formApiRef.current}
extraText={
- handleInputChange('other', JSON.stringify(REGION_EXAMPLE, null, 2))}
- >
- {t('填入模板')}
+
+ {t('设置默认地区和特定模型的专用地区')}
}
- showClear
/>
)}
@@ -1447,24 +1450,24 @@ const EditChannelModal = (props) => {
showClear
/>
- handleInputChange('model_mapping', value)}
+ template={MODEL_MAPPING_EXAMPLE}
+ templateLabel={t('填入模板')}
+ editorType="keyValue"
+ formApi={formApiRef.current}
extraText={
- handleInputChange('model_mapping', JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2))}
- >
- {t('填入模板')}
+
+ {t('键为请求中的模型名称,值为要替换的模型名称')}
}
- showClear
/>
@@ -1554,7 +1557,7 @@ const EditChannelModal = (props) => {
showClear
/>
- {
'\n' +
JSON.stringify(STATUS_CODE_MAPPING_EXAMPLE, null, 2)
}
- autosize
+ value={inputs.status_code_mapping || ''}
onChange={(value) => handleInputChange('status_code_mapping', value)}
+ template={STATUS_CODE_MAPPING_EXAMPLE}
+ templateLabel={t('填入模板')}
+ editorType="keyValue"
+ formApi={formApiRef.current}
extraText={
- handleInputChange('status_code_mapping', JSON.stringify(STATUS_CODE_MAPPING_EXAMPLE, null, 2))}
- >
- {t('填入模板')}
+
+ {t('键为原状态码,值为要复写的状态码,仅影响本地判断')}
}
- showClear
/>
@@ -1585,14 +1588,6 @@ const EditChannelModal = (props) => {
{t('渠道额外设置')}
-
- window.open('https://github.com/QuantumNous/new-api/blob/main/docs/channel/other_setting.md')}
- >
- {t('设置说明')}
-
-