diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index 80f7f3cd..a2657b31 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -1142,7 +1142,7 @@ "鉴权json": "Authentication JSON", "请输入鉴权json": "Please enter authentication JSON", "组织": "Organization", - "组织,可选,不填则为默认组织": "Organization (optional), default if empty", + "组织,不填则为默认组织": "Organization, default if empty", "请输入组织org-xxx": "Please enter organization org-xxx", "默认测试模型": "Default Test Model", "不填则为模型列表第一个": "First model in list if empty", @@ -1756,5 +1756,14 @@ "生成数量必须大于0": "Generation quantity must be greater than 0", "创建后可在编辑渠道时获取上游模型列表": "After creation, you can get the upstream model list when editing the channel", "可用端点类型": "Supported endpoint types", - "未登录,使用默认分组倍率:": "Not logged in, using default group ratio: " + "未登录,使用默认分组倍率:": "Not logged in, using default group ratio: ", + "密钥聚合模式": "Key aggregation mode", + "随机": "Random", + "轮询": "Polling", + "密钥文件 (.json)": "Key file (.json)", + "点击上传文件或拖拽文件到这里": "Click to upload file or drag and drop file here", + "仅支持 JSON 文件,支持多文件": "Only JSON files are supported, multiple files are supported", + "请上传密钥文件": "Please upload the key file", + "请填写部署地区": "Please fill in the deployment region", + "请输入部署地区,例如:us-central1\n支持使用模型映射格式\n{\n \"default\": \"us-central1\",\n \"claude-3-5-sonnet-20240620\": \"europe-west1\"\n}": "Please enter the deployment region, for example: us-central1\nSupports using model mapping format\n{\n \"default\": \"us-central1\",\n \"claude-3-5-sonnet-20240620\": \"europe-west1\"\n}" } \ No newline at end of file diff --git a/web/src/pages/Channel/EditChannel.js b/web/src/pages/Channel/EditChannel.js index cfed54e4..a12777fc 100644 --- a/web/src/pages/Channel/EditChannel.js +++ b/web/src/pages/Channel/EditChannel.js @@ -26,6 +26,7 @@ import { Form, Row, Col, + Upload, } from '@douyinfe/semi-ui'; import { getChannelModels, copy } from '../../helpers'; import { @@ -35,6 +36,7 @@ import { IconSetting, IconCode, IconGlobe, + IconBolt, } from '@douyinfe/semi-icons'; const { Text, Title } = Typography; @@ -100,8 +102,11 @@ const EditChannel = (props) => { priority: 0, weight: 0, tag: '', + multi_key_mode: 'random', }; const [batch, setBatch] = useState(false); + const [multiToSingle, setMultiToSingle] = useState(false); + const [multiKeyMode, setMultiKeyMode] = useState('random'); const [autoBan, setAutoBan] = useState(true); // const [autoBan, setAutoBan] = useState(true); const [inputs, setInputs] = useState(originInputs); @@ -114,6 +119,9 @@ const EditChannel = (props) => { const [modalImageUrl, setModalImageUrl] = useState(''); const [isModalOpenurl, setIsModalOpenurl] = useState(false); const formApiRef = useRef(null); + const [vertexKeys, setVertexKeys] = useState([]); + const [vertexFileList, setVertexFileList] = useState([]); + const vertexErroredNames = useRef(new Set()); // 避免重复报错 const getInitValues = () => ({ ...originInputs }); const handleInputChange = (name, value) => { if (formApiRef.current) { @@ -377,10 +385,72 @@ const EditChannel = (props) => { } }, [props.visible, channelId]); + const handleVertexUploadChange = ({ fileList }) => { + (async () => { + const validFiles = []; + const keys = []; + const errorNames = []; + for (const item of fileList) { + const fileObj = item.fileInstance; + if (!fileObj) continue; + try { + const txt = await fileObj.text(); + keys.push(JSON.parse(txt)); + validFiles.push(item); // 仅合法文件加入列表 + } catch (err) { + if (!vertexErroredNames.current.has(item.name)) { + errorNames.push(item.name); + vertexErroredNames.current.add(item.name); + } + } + } + + setVertexKeys(keys); + setVertexFileList(validFiles); + if (formApiRef.current) { + formApiRef.current.setValue('vertex_files', validFiles); + } + setInputs((prev) => ({ ...prev, vertex_files: validFiles })); + + if (errorNames.length > 0) { + showError(t('以下文件解析失败,已忽略:{{list}}', { list: errorNames.join(', ') })); + } + })(); + }; + const submit = async () => { const formValues = formApiRef.current ? formApiRef.current.getValues() : {}; let localInputs = { ...formValues }; + if (localInputs.type === 41 && batch) { + let keys = vertexKeys; + if (keys.length === 0) { + // 确保提交时也能解析,避免因异步延迟导致 keys 为空 + try { + const parsed = await Promise.all( + vertexFileList.map(async (item) => { + const fileObj = item.fileInstance; + if (!fileObj) return null; + const txt = await fileObj.text(); + return JSON.parse(txt); + }) + ); + keys = parsed.filter(Boolean); + } catch (err) { + showError(t('解析密钥文件失败: {{msg}}', { msg: err.message })); + return; + } + } + + if (keys.length === 0) { + showInfo(t('请上传密钥文件!')); + return; + } + + localInputs.key = JSON.stringify(keys); + } + delete localInputs.vertex_files; + if (!isEdit && (!localInputs.name || !localInputs.key)) { showInfo(t('请填写渠道名称和渠道密钥!')); return; @@ -406,13 +476,23 @@ const EditChannel = (props) => { localInputs.auto_ban = localInputs.auto_ban ? 1 : 0; localInputs.models = localInputs.models.join(','); localInputs.group = (localInputs.groups || []).join(','); + + let mode = 'single'; + if (batch) { + mode = multiToSingle ? 'multi_to_single' : 'batch'; + } + if (isEdit) { res = await API.put(`/api/channel/`, { ...localInputs, id: parseInt(channelId), }); } else { - res = await API.post(`/api/channel/`, localInputs); + res = await API.post(`/api/channel/`, { + mode: mode, + multi_key_mode: mode === 'multi_to_single' ? multiKeyMode : undefined, + channel: localInputs, + }); } const { success, message } = res.data; if (success) { @@ -465,9 +545,31 @@ const EditChannel = (props) => { } }; - const batchAllowed = !isEdit && inputs.type !== 41; + const batchAllowed = !isEdit; const batchExtra = batchAllowed ? ( - setBatch(!batch)}>{t('批量创建')} + + { + setBatch(!batch); + if (batch) { + setMultiToSingle(false); + setMultiKeyMode('random'); + } + }}>{t('批量创建')} + {batch && ( + { + setMultiToSingle(prev => !prev); + setInputs(prev => { + const newInputs = { ...prev }; + if (!multiToSingle) { + newInputs.multi_key_mode = multiKeyMode; + } else { + delete newInputs.multi_key_mode; + } + return newInputs; + }); + }}>{t('密钥聚合模式')} + )} + ) : null; return ( @@ -553,16 +655,37 @@ const EditChannel = (props) => { /> {batch ? ( - handleInputChange('key', value)} - extraText={batchExtra} - /> + inputs.type === 41 ? ( + } + dragMainText={t('点击上传文件或拖拽文件到这里')} + dragSubText={t('仅支持 JSON 文件,支持多文件')} + style={{ marginTop: 10 }} + uploadTrigger='custom' + beforeUpload={() => false} + onChange={handleVertexUploadChange} + fileList={vertexFileList} + rules={isEdit ? [] : [{ required: true, message: t('请上传密钥文件') }]} + extraText={batchExtra} + /> + ) : ( + handleInputChange('key', value)} + extraText={batchExtra} + showClear + /> + ) ) : ( <> {inputs.type === 41 ? ( @@ -585,10 +708,11 @@ const EditChannel = (props) => { '}' } rules={isEdit ? [] : [{ required: true, message: t('请输入密钥') }]} - autosize={{ minRows: 10 }} + autosize autoComplete='new-password' onChange={(value) => handleInputChange('key', value)} extraText={batchExtra} + showClear /> ) : ( { autoComplete='new-password' onChange={(value) => handleInputChange('key', value)} extraText={batchExtra} + showClear /> )} )} + + {batch && multiToSingle && ( + { + setMultiKeyMode(value); + handleInputChange('multi_key_mode', value); + }} + /> + )} + + {inputs.type === 18 && ( + handleInputChange('other', value)} + showClear + /> + )} + + {inputs.type === 41 && ( + handleInputChange('other', value)} + rules={[{ required: true, message: t('请填写部署地区') }]} + extraText={ + handleInputChange('other', JSON.stringify(REGION_EXAMPLE, null, 2))} + > + {t('填入模板')} + + } + showClear + /> + )} + + {inputs.type === 21 && ( + handleInputChange('other', value)} + showClear + /> + )} + + {inputs.type === 39 && ( + handleInputChange('other', value)} + showClear + /> + )} + + {inputs.type === 49 && ( + handleInputChange('other', value)} + showClear + /> + )} + + {inputs.type === 1 && ( + handleInputChange('openai_organization', value)} + /> + )} {/* API Configuration Card */} @@ -860,77 +1076,6 @@ const EditChannel = (props) => { onChange={(value) => handleInputChange('groups', value)} /> - {inputs.type === 18 && ( - handleInputChange('other', value)} - showClear - /> - )} - - {inputs.type === 41 && ( - handleInputChange('other', value)} - extraText={ - handleInputChange('other', JSON.stringify(REGION_EXAMPLE, null, 2))} - > - {t('填入模板')} - - } - /> - )} - - {inputs.type === 21 && ( - handleInputChange('other', value)} - showClear - /> - )} - - {inputs.type === 39 && ( - handleInputChange('other', value)} - showClear - /> - )} - - {inputs.type === 49 && ( - handleInputChange('other', value)} - showClear - /> - )} - - {inputs.type === 1 && ( - handleInputChange('openai_organization', value)} - /> - )} -