diff --git a/controller/channel.go b/controller/channel.go index d3bfa202..513e3024 100644 --- a/controller/channel.go +++ b/controller/channel.go @@ -669,6 +669,7 @@ func DeleteChannelBatch(c *gin.Context) { type PatchChannel struct { model.Channel MultiKeyMode *string `json:"multi_key_mode"` + KeyMode *string `json:"key_mode"` // 多key模式下密钥覆盖或者追加 } func UpdateChannel(c *gin.Context) { @@ -688,7 +689,7 @@ func UpdateChannel(c *gin.Context) { return } // 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) + originChannel, err := model.GetChannelById(channel.Id, true) if err != nil { c.JSON(http.StatusOK, gin.H{ "success": false, @@ -704,6 +705,69 @@ func UpdateChannel(c *gin.Context) { if channel.MultiKeyMode != nil && *channel.MultiKeyMode != "" { channel.ChannelInfo.MultiKeyMode = constant.MultiKeyMode(*channel.MultiKeyMode) } + + // 处理多key模式下的密钥追加/覆盖逻辑 + if channel.KeyMode != nil && channel.ChannelInfo.IsMultiKey { + switch *channel.KeyMode { + case "append": + // 追加模式:将新密钥添加到现有密钥列表 + if originChannel.Key != "" { + var newKeys []string + var existingKeys []string + + // 解析现有密钥 + if strings.HasPrefix(strings.TrimSpace(originChannel.Key), "[") { + // JSON数组格式 + var arr []json.RawMessage + if err := json.Unmarshal([]byte(strings.TrimSpace(originChannel.Key)), &arr); err == nil { + existingKeys = make([]string, len(arr)) + for i, v := range arr { + existingKeys[i] = string(v) + } + } + } else { + // 换行分隔格式 + existingKeys = strings.Split(strings.Trim(originChannel.Key, "\n"), "\n") + } + + // 处理 Vertex AI 的特殊情况 + if channel.Type == constant.ChannelTypeVertexAi { + // 尝试解析新密钥为JSON数组 + if strings.HasPrefix(strings.TrimSpace(channel.Key), "[") { + array, err := getVertexArrayKeys(channel.Key) + if err != nil { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": "追加密钥解析失败: " + err.Error(), + }) + return + } + newKeys = array + } else { + // 单个JSON密钥 + newKeys = []string{channel.Key} + } + // 合并密钥 + allKeys := append(existingKeys, newKeys...) + channel.Key = strings.Join(allKeys, "\n") + } else { + // 普通渠道的处理 + inputKeys := strings.Split(channel.Key, "\n") + for _, key := range inputKeys { + key = strings.TrimSpace(key) + if key != "" { + newKeys = append(newKeys, key) + } + } + // 合并密钥 + allKeys := append(existingKeys, newKeys...) + channel.Key = strings.Join(allKeys, "\n") + } + } + case "replace": + // 覆盖模式:直接使用新密钥(默认行为,不需要特殊处理) + } + } err = channel.Update() if err != nil { common.ApiError(c, err) diff --git a/web/src/components/table/channels/modals/EditChannelModal.jsx b/web/src/components/table/channels/modals/EditChannelModal.jsx index 37e9af75..8c8bdb70 100644 --- a/web/src/components/table/channels/modals/EditChannelModal.jsx +++ b/web/src/components/table/channels/modals/EditChannelModal.jsx @@ -154,6 +154,7 @@ const EditChannelModal = (props) => { const [isMultiKeyChannel, setIsMultiKeyChannel] = useState(false); const [channelSearchValue, setChannelSearchValue] = useState(''); const [useManualInput, setUseManualInput] = useState(false); // 是否使用手动输入模式 + const [keyMode, setKeyMode] = useState('append'); // 密钥模式:replace(覆盖)或 append(追加) // 渠道额外设置状态 const [channelSettings, setChannelSettings] = useState({ force_format: false, @@ -560,6 +561,12 @@ const EditChannelModal = (props) => { pass_through_body_enabled: false, system_prompt: '', }); + // 重置密钥模式状态 + setKeyMode('append'); + // 清空表单中的key_mode字段 + if (formApiRef.current) { + formApiRef.current.setValue('key_mode', undefined); + } } }, [props.visible, channelId]); @@ -725,6 +732,7 @@ const EditChannelModal = (props) => { res = await API.put(`/api/channel/`, { ...localInputs, id: parseInt(channelId), + key_mode: isMultiKeyChannel ? keyMode : undefined, // 只在多key模式下传递 }); } else { res = await API.post(`/api/channel/`, { @@ -787,55 +795,59 @@ const EditChannelModal = (props) => { const batchAllowed = !isEdit || isMultiKeyChannel; const batchExtra = batchAllowed ? ( - { - const checked = e.target.checked; + {!isEdit && ( + { + 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]] : []; + 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); + setVertexFileList([firstFile]); + setVertexKeys(firstKey); - formApiRef.current?.setValue('vertex_files', [firstFile]); - setInputs((prev) => ({ ...prev, vertex_files: [firstFile] })); + 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'); - } else { - // 批量模式下禁用手动输入,并清空手动输入的内容 - setUseManualInput(false); - if (inputs.type === 41) { - // 清空手动输入的密钥内容 - if (formApiRef.current) { - formApiRef.current.setValue('key', ''); - } - handleInputChange('key', ''); + setBatch(false); + setMultiToSingle(false); + setMultiKeyMode('random'); + }, + onCancel: () => { + setBatch(true); + }, + centered: true, + }); + return; } - } - }} - >{t('批量创建')} + + setBatch(checked); + if (!checked) { + setMultiToSingle(false); + setMultiKeyMode('random'); + } else { + // 批量模式下禁用手动输入,并清空手动输入的内容 + setUseManualInput(false); + if (inputs.type === 41) { + // 清空手动输入的密钥内容 + if (formApiRef.current) { + formApiRef.current.setValue('key', ''); + } + handleInputChange('key', ''); + } + } + }} + > + {t('批量创建')} + + )} {batch && ( { setMultiToSingle(prev => !prev); @@ -1032,7 +1044,16 @@ const EditChannelModal = (props) => { autosize autoComplete='new-password' onChange={(value) => handleInputChange('key', value)} - extraText={batchExtra} + extraText={ +
+ {isEdit && isMultiKeyChannel && keyMode === 'append' && ( + + {t('追加模式:新密钥将添加到现有密钥列表的末尾')} + + )} + {batchExtra} +
+ } showClear /> ) @@ -1099,6 +1120,11 @@ const EditChannelModal = (props) => { {t('请输入完整的 JSON 格式密钥内容')} + {isEdit && isMultiKeyChannel && keyMode === 'append' && ( + + {t('追加模式:新密钥将添加到现有密钥列表的末尾')} + + )} {batchExtra} } @@ -1132,13 +1158,44 @@ const EditChannelModal = (props) => { rules={isEdit ? [] : [{ required: true, message: t('请输入密钥') }]} autoComplete='new-password' onChange={(value) => handleInputChange('key', value)} - extraText={batchExtra} + extraText={ +
+ {isEdit && isMultiKeyChannel && keyMode === 'append' && ( + + {t('追加模式:新密钥将添加到现有密钥列表的末尾')} + + )} + {batchExtra} +
+ } showClear /> )} )} + {isEdit && isMultiKeyChannel && ( + setKeyMode(value)} + extraText={ + + {keyMode === 'replace' + ? t('覆盖模式:将完全替换现有的所有密钥') + : t('追加模式:将新密钥添加到现有密钥列表末尾') + } + + } + /> + )} {batch && multiToSingle && ( <>