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" + /> +
+ + +
+
+ +
+
+ ))} + +
+ +
+
+ ); + }; + + // 渲染对象编辑器(用于复杂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)} +
+ + +
+
+ +
+
+ ))} + +
+ +
+
+ ); + }; + + // 渲染参数值输入控件 + 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" + /> +
+ + +
+
+ +
+
+ ))} + +
+ +
+
+
+ ); + }; + + // 渲染可视化编辑器 + 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 && ( + + )} + + + + +
+
+ + {/* 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('设置说明')} - -