diff --git a/controller/channel.go b/controller/channel.go index 855affa1..4c45574f 100644 --- a/controller/channel.go +++ b/controller/channel.go @@ -709,18 +709,22 @@ func UpdateChannel(c *gin.Context) { } } } + // Preserve existing ChannelInfo to ensure multi-key channels keep correct state even if the client does not send ChannelInfo in the request. + originChannel, err := model.GetChannelById(channel.Id, false) + if err != nil { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": err.Error(), + }) + return + } + + // Always copy the original ChannelInfo so that fields like IsMultiKey and MultiKeySize are retained. + channel.ChannelInfo = originChannel.ChannelInfo + + // If the request explicitly specifies a new MultiKeyMode, apply it on top of the original info. if channel.MultiKeyMode != nil && *channel.MultiKeyMode != "" { - originChannel, err := model.GetChannelById(channel.Id, false) - if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) - } - if originChannel.ChannelInfo.IsMultiKey { - channel.ChannelInfo = originChannel.ChannelInfo - channel.ChannelInfo.MultiKeyMode = constant.MultiKeyMode(*channel.MultiKeyMode) - } + channel.ChannelInfo.MultiKeyMode = constant.MultiKeyMode(*channel.MultiKeyMode) } err = channel.Update() if err != nil { diff --git a/model/channel.go b/model/channel.go index a63d1fd2..6277fcda 100644 --- a/model/channel.go +++ b/model/channel.go @@ -71,7 +71,19 @@ func (channel *Channel) getKeys() []string { if channel.Key == "" { return []string{} } - // use \n to split keys + trimmed := strings.TrimSpace(channel.Key) + // If the key starts with '[', try to parse it as a JSON array (e.g., for Vertex AI scenarios) + if strings.HasPrefix(trimmed, "[") { + var arr []json.RawMessage + if err := json.Unmarshal([]byte(trimmed), &arr); err == nil { + res := make([]string, len(arr)) + for i, v := range arr { + res[i] = string(v) + } + return res + } + } + // Otherwise, fall back to splitting by newline keys := strings.Split(strings.Trim(channel.Key, "\n"), "\n") return keys } @@ -396,23 +408,36 @@ func (channel *Channel) Insert() error { } func (channel *Channel) Update() error { - // 如果是多密钥渠道,则根据当前 key 列表重新计算 MultiKeySize,避免编辑密钥后数量未同步 + // If this is a multi-key channel, recalculate MultiKeySize based on the current key list to avoid inconsistency after editing keys if channel.ChannelInfo.IsMultiKey { var keyStr string if channel.Key != "" { keyStr = channel.Key } else { - // 如果当前未提供 key,读取数据库中的现有 key + // If key is not provided, read the existing key from the database if existing, err := GetChannelById(channel.Id, true); err == nil { keyStr = existing.Key } } + // Parse the key list (supports newline separation or JSON array) keys := []string{} if keyStr != "" { - keys = strings.Split(strings.Trim(keyStr, "\n"), "\n") + trimmed := strings.TrimSpace(keyStr) + if strings.HasPrefix(trimmed, "[") { + var arr []json.RawMessage + if err := json.Unmarshal([]byte(trimmed), &arr); err == nil { + keys = make([]string, len(arr)) + for i, v := range arr { + keys[i] = string(v) + } + } + } + if len(keys) == 0 { // fallback to newline split + keys = strings.Split(strings.Trim(keyStr, "\n"), "\n") + } } channel.ChannelInfo.MultiKeySize = len(keys) - // 清理超过新 key 数量范围的状态数据,防止索引越界 + // Clean up status data that exceeds the new key count to prevent index out of range if channel.ChannelInfo.MultiKeyStatusList != nil { for idx := range channel.ChannelInfo.MultiKeyStatusList { if idx >= channel.ChannelInfo.MultiKeySize { diff --git a/web/src/components/table/ChannelsTable.js b/web/src/components/table/ChannelsTable.js index 2d409b09..3299f61f 100644 --- a/web/src/components/table/ChannelsTable.js +++ b/web/src/components/table/ChannelsTable.js @@ -987,7 +987,6 @@ const ChannelsTable = () => { }; useEffect(() => { - // console.log('default effect') const localIdSort = localStorage.getItem('id-sort') === 'true'; const localPageSize = parseInt(localStorage.getItem('page-size')) || ITEMS_PER_PAGE; diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index 61cc3782..e1837de9 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -1770,10 +1770,15 @@ "轮询": "Polling", "密钥文件 (.json)": "Key file (.json)", "点击上传文件或拖拽文件到这里": "Click to upload file or drag and drop file here", + "仅支持 JSON 文件": "Only JSON files are supported", "仅支持 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}", "其他": "Other", - "未知渠道": "Unknown channel" + "未知渠道": "Unknown channel", + "切换为单密钥模式": "Switch to single key mode", + "将仅保留第一个密钥文件,其余文件将被移除,是否继续?": "Only the first key file will be retained, and the remaining files will be removed. Continue?", + "自定义模型名称": "Custom model name", + "启用全部密钥": "Enable all keys" } \ No newline at end of file diff --git a/web/src/pages/Channel/EditChannel.js b/web/src/pages/Channel/EditChannel.js index 9264a2d2..d5682b0e 100644 --- a/web/src/pages/Channel/EditChannel.js +++ b/web/src/pages/Channel/EditChannel.js @@ -26,7 +26,6 @@ import { Form, Row, Col, - Upload, } from '@douyinfe/semi-ui'; import { getChannelModels, copy, getChannelIcon, getModelCategories } from '../../helpers'; import { @@ -424,9 +423,10 @@ const EditChannel = (props) => { }, [props.visible, channelId]); const handleVertexUploadChange = ({ fileList }) => { + vertexErroredNames.current.clear(); (async () => { - const validFiles = []; - const keys = []; + let validFiles = []; + let keys = []; const errorNames = []; for (const item of fileList) { const fileObj = item.fileInstance; @@ -434,7 +434,7 @@ const EditChannel = (props) => { try { const txt = await fileObj.text(); keys.push(JSON.parse(txt)); - validFiles.push(item); // 仅合法文件加入列表 + validFiles.push(item); } catch (err) { if (!vertexErroredNames.current.has(item.name)) { errorNames.push(item.name); @@ -443,6 +443,12 @@ const EditChannel = (props) => { } } + // 非批量模式下只保留一个文件(最新选择的),避免重复叠加 + if (!batch && validFiles.length > 1) { + validFiles = [validFiles[validFiles.length - 1]]; + keys = [keys[keys.length - 1]]; + } + setVertexKeys(keys); setVertexFileList(validFiles); if (formApiRef.current) { @@ -603,13 +609,45 @@ const EditChannel = (props) => { const batchAllowed = !isEdit || isMultiKeyChannel; const batchExtra = batchAllowed ? ( - { - setBatch(!batch); - if (batch) { - setMultiToSingle(false); - setMultiKeyMode('random'); - } - }}>{t('批量创建')} + { + const checked = e.target.checked; + + if (!checked && vertexFileList.length > 1) { + Modal.confirm({ + title: t('切换为单密钥模式'), + content: t('将仅保留第一个密钥文件,其余文件将被移除,是否继续?'), + onOk: () => { + const firstFile = vertexFileList[0]; + const firstKey = vertexKeys[0] ? [vertexKeys[0]] : []; + + setVertexFileList([firstFile]); + setVertexKeys(firstKey); + + formApiRef.current?.setValue('vertex_files', [firstFile]); + setInputs((prev) => ({ ...prev, vertex_files: [firstFile] })); + + setBatch(false); + setMultiToSingle(false); + setMultiKeyMode('random'); + }, + onCancel: () => { + setBatch(true); + }, + centered: true, + }); + return; + } + + setBatch(checked); + if (!checked) { + setMultiToSingle(false); + setMultiKeyMode('random'); + } + }} + >{t('批量创建')} {batch && ( { setMultiToSingle(prev => !prev);