feat: implement key mode for multi-key channels with append/replace options
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user