feat: implement key mode for multi-key channels with append/replace options

This commit is contained in:
CaIon
2025-08-02 10:57:03 +08:00
parent 953f1bdc3c
commit ef0db0f914
2 changed files with 168 additions and 47 deletions

View File

@@ -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)

View File

@@ -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 ? (
<Space>
<Checkbox
disabled={isEdit}
checked={batch}
onChange={(e) => {
const checked = e.target.checked;
{!isEdit && (
<Checkbox
disabled={isEdit}
checked={batch}
onChange={(e) => {
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('批量创建')}</Checkbox>
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('批量创建')}
</Checkbox>
)}
{batch && (
<Checkbox disabled={isEdit} checked={multiToSingle} onChange={() => {
setMultiToSingle(prev => !prev);
@@ -1032,7 +1044,16 @@ const EditChannelModal = (props) => {
autosize
autoComplete='new-password'
onChange={(value) => handleInputChange('key', value)}
extraText={batchExtra}
extraText={
<div className="flex items-center gap-2">
{isEdit && isMultiKeyChannel && keyMode === 'append' && (
<Text type="warning" size="small">
{t('追加模式:新密钥将添加到现有密钥列表的末尾')}
</Text>
)}
{batchExtra}
</div>
}
showClear
/>
)
@@ -1099,6 +1120,11 @@ const EditChannelModal = (props) => {
<Text type="tertiary" size="small">
{t('请输入完整的 JSON 格式密钥内容')}
</Text>
{isEdit && isMultiKeyChannel && keyMode === 'append' && (
<Text type="warning" size="small">
{t('追加模式:新密钥将添加到现有密钥列表的末尾')}
</Text>
)}
{batchExtra}
</div>
}
@@ -1132,13 +1158,44 @@ const EditChannelModal = (props) => {
rules={isEdit ? [] : [{ required: true, message: t('请输入密钥') }]}
autoComplete='new-password'
onChange={(value) => handleInputChange('key', value)}
extraText={batchExtra}
extraText={
<div className="flex items-center gap-2">
{isEdit && isMultiKeyChannel && keyMode === 'append' && (
<Text type="warning" size="small">
{t('追加模式:新密钥将添加到现有密钥列表的末尾')}
</Text>
)}
{batchExtra}
</div>
}
showClear
/>
)}
</>
)}
{isEdit && isMultiKeyChannel && (
<Form.Select
field='key_mode'
label={t('密钥更新模式')}
placeholder={t('请选择密钥更新模式')}
optionList={[
{ label: t('追加到现有密钥'), value: 'append' },
{ label: t('覆盖现有密钥'), value: 'replace' },
]}
style={{ width: '100%' }}
value={keyMode}
onChange={(value) => setKeyMode(value)}
extraText={
<Text type="tertiary" size="small">
{keyMode === 'replace'
? t('覆盖模式:将完全替换现有的所有密钥')
: t('追加模式:将新密钥添加到现有密钥列表末尾')
}
</Text>
}
/>
)}
{batch && multiToSingle && (
<>
<Form.Select