From 0089157b83633a0892e5fe2b0960e7791f213d31 Mon Sep 17 00:00:00 2001 From: CaIon <1808837298@qq.com> Date: Mon, 16 Jun 2025 00:37:22 +0800 Subject: [PATCH 01/67] =?UTF-8?q?=E2=9C=A8=20feat(channel):=20enhance=20Ad?= =?UTF-8?q?dChannel=20functionality=20with=20structured=20request=20handli?= =?UTF-8?q?ng?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- controller/channel.go | 107 +++++++++++++++++++++++++++++++++--------- model/channel.go | 9 +++- model/main.go | 2 +- 3 files changed, 94 insertions(+), 24 deletions(-) diff --git a/controller/channel.go b/controller/channel.go index 1cfb7906..f2b9ad7e 100644 --- a/controller/channel.go +++ b/controller/channel.go @@ -250,9 +250,14 @@ func GetChannel(c *gin.Context) { return } +type AddChannelRequest struct { + Mode string `json:"mode"` + Channel *model.Channel `json:"channel"` +} + func AddChannel(c *gin.Context) { - channel := model.Channel{} - err := c.ShouldBindJSON(&channel) + addChannelRequest := AddChannelRequest{} + err := c.ShouldBindJSON(&addChannelRequest) if err != nil { c.JSON(http.StatusOK, gin.H{ "success": false, @@ -260,19 +265,35 @@ func AddChannel(c *gin.Context) { }) return } - channel.CreatedTime = common.GetTimestamp() - keys := strings.Split(channel.Key, "\n") - if channel.Type == common.ChannelTypeVertexAi { - if channel.Other == "" { + if addChannelRequest.Channel == nil || addChannelRequest.Channel.Key == "" { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": "channel cannot be empty", + }) + return + } + + // Validate the length of the model name + for _, m := range addChannelRequest.Channel.GetModels() { + if len(m) > 255 { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": fmt.Sprintf("模型名称过长: %s", m), + }) + return + } + } + if addChannelRequest.Channel.Type == common.ChannelTypeVertexAi { + if addChannelRequest.Channel.Other == "" { c.JSON(http.StatusOK, gin.H{ "success": false, "message": "部署地区不能为空", }) return } else { - if common.IsJsonStr(channel.Other) { + if common.IsJsonStr(addChannelRequest.Channel.Other) { // must have default - regionMap := common.StrToMap(channel.Other) + regionMap := common.StrToMap(addChannelRequest.Channel.Other) if regionMap["default"] == nil { c.JSON(http.StatusOK, gin.H{ "success": false, @@ -282,27 +303,69 @@ func AddChannel(c *gin.Context) { } } } - keys = []string{channel.Key} } + + addChannelRequest.Channel.CreatedTime = common.GetTimestamp() + keys := make([]string, 0) + switch addChannelRequest.Mode { + case "multi_to_single": + addChannelRequest.Channel.ChannelInfo.MultiKeyMode = true + if addChannelRequest.Channel.Type == common.ChannelTypeVertexAi { + if !common.IsJsonStr(addChannelRequest.Channel.Key) { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": "Vertex AI 批量添加模式必须使用标准的JsonArray格式,例如[{key1}, {key2}...],请检查输入", + }) + return + } + } + keys = []string{addChannelRequest.Channel.Key} + case "batch": + if addChannelRequest.Channel.Type == common.ChannelTypeVertexAi { + // multi json + if !common.IsJsonStr(addChannelRequest.Channel.Key) { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": "Vertex AI 批量添加模式必须使用标准的JsonArray格式,例如[{key1}, {key2}...],请检查输入", + }) + return + } + toMap := common.StrToMap(addChannelRequest.Channel.Key) + if toMap == nil { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": "Vertex AI 批量添加模式必须使用标准的JsonArray格式,例如[{key1}, {key2}...],请检查输入", + }) + return + } + keys = make([]string, 0, len(toMap)) + for k := range toMap { + if k == "" { + continue + } + keys = append(keys, k) + } + } else { + keys = strings.Split(addChannelRequest.Channel.Key, "\n") + } + case "single": + keys = []string{addChannelRequest.Channel.Key} + default: + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": "不支持的添加模式", + }) + return + } + channels := make([]model.Channel, 0, len(keys)) for _, key := range keys { if key == "" { continue } - localChannel := channel + localChannel := addChannelRequest.Channel localChannel.Key = key - // Validate the length of the model name - models := strings.Split(localChannel.Models, ",") - for _, model := range models { - if len(model) > 255 { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": fmt.Sprintf("模型名称过长: %s", model), - }) - return - } - } - channels = append(channels, localChannel) + channels = append(channels, *localChannel) } err = model.BatchInsertChannels(channels) if err != nil { diff --git a/model/channel.go b/model/channel.go index b5503eee..755bd0b2 100644 --- a/model/channel.go +++ b/model/channel.go @@ -9,6 +9,11 @@ import ( "gorm.io/gorm" ) +type ChannelInfo struct { + MultiKeyMode bool `json:"multi_key_mode"` // 是否多Key模式 + MultiKeyStatusList map[int]int `json:"multi_key_status_list"` // key状态列表,key index -> status +} + type Channel struct { Id int `json:"id"` Type int `json:"type" gorm:"default:0"` @@ -35,8 +40,10 @@ type Channel struct { AutoBan *int `json:"auto_ban" gorm:"default:1"` OtherInfo string `json:"other_info"` Tag *string `json:"tag" gorm:"index"` - Setting *string `json:"setting" gorm:"type:text"` + Setting *string `json:"setting" gorm:"type:text"` // 渠道额外设置 ParamOverride *string `json:"param_override" gorm:"type:text"` + // add after v0.8.5 + ChannelInfo ChannelInfo `json:"channel_info" gorm:"type:json"` } func (channel *Channel) GetModels() []string { diff --git a/model/main.go b/model/main.go index 965bba93..b7a5af5d 100644 --- a/model/main.go +++ b/model/main.go @@ -48,7 +48,7 @@ func initCol() { } } // log sql type and database type - common.SysLog("Using Log SQL Type: " + common.LogSqlType) + //common.SysLog("Using Log SQL Type: " + common.LogSqlType) } var DB *gorm.DB From aa793088eda9dc4ed8ff47e0d455d8771c2745ea Mon Sep 17 00:00:00 2001 From: "Apple\\Apple" Date: Mon, 16 Jun 2025 00:58:47 +0800 Subject: [PATCH 02/67] =?UTF-8?q?=E2=9C=A8=20fix(playground):=20send=20sel?= =?UTF-8?q?ected=20group=20in=20API=20payload?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, the Playground UI allowed users to pick a group, but the request body sent to `/pg/chat/completions` did not include this information, so the backend always fell back to the user’s default group. Changes introduced • web/src/helpers/api.js – added `group: inputs.group` to the `payload` built in `buildApiPayload`. Outcome • Selected group is now transmitted to the backend, enabling proper channel routing and pricing logic based on group ratios. • Resolves the issue where group selection appeared ineffective in the Playground. --- web/src/helpers/api.js | 1 + 1 file changed, 1 insertion(+) diff --git a/web/src/helpers/api.js b/web/src/helpers/api.js index aef01287..5b9c03e1 100644 --- a/web/src/helpers/api.js +++ b/web/src/helpers/api.js @@ -82,6 +82,7 @@ export const buildApiPayload = (messages, systemPrompt, inputs, parameterEnabled const payload = { model: inputs.model, + group: inputs.group, messages: processedMessages, stream: inputs.stream, }; From 617c8e8f4fdf45ef7cb4f35bc0823c7c64935b77 Mon Sep 17 00:00:00 2001 From: "Apple\\Apple" Date: Mon, 16 Jun 2025 01:47:41 +0800 Subject: [PATCH 03/67] =?UTF-8?q?=E2=9C=A8=20feat(channel-ui):=20support?= =?UTF-8?q?=20multi-JSON=20batch=20creation=20for=20Vertex=20AI=20&=20mult?= =?UTF-8?q?i-to-single=20mode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WHY • Backend (0089157) now accepts structured request `{ mode, channel }`, including new `multi_to_single`. • Need front-end to upload multiple service-account JSON files and generate correct `channel.key`. • Improve UX: avoid red “uploadFail” state and offer drag-and-drop UI. WHAT 1. EditChannel.js • Added Upload drag-area with IconBolt; `uploadTrigger="custom"`. • `handleJsonFileUpload` reads file, pushes content to `jsonFiles`, returns `{ shouldUpload:false, status:'success' }`. • New states: `batch`, `mergeToSingle`, `jsonFiles`. • Dynamic mode resolver: `single` | `batch` | `multi_to_single`. • Builds `channel.key` as JSON-object whose keys are the raw credential texts. • UI: – “Batch create” checkbox (new build only). – Nested “Merge to single channel (multi-key mode)” checkbox enabled when batch=true. – Real-time file count display. 2. Upload UX • Drag-and-drop, accepts `.json,application/json`. • Custom texts: “Click or drop files here” / “JSON credentials only”. • Eliminated mandatory `action` warning (`action="#"`). 3. Misc • Included IconBolt import. • Safeguard toggles reset logic to prevent stale state. RESULT Front-end now fully aligns with enhanced AddChannel API: • Supports Vertex AI multi JSON batch creation. • Supports new `multi_to_single` flow. • Clean user feedback with successful file status. --- web/src/pages/Channel/EditChannel.js | 212 ++++++++++++++++++--------- 1 file changed, 146 insertions(+), 66 deletions(-) diff --git a/web/src/pages/Channel/EditChannel.js b/web/src/pages/Channel/EditChannel.js index 8bfe5812..fa77be81 100644 --- a/web/src/pages/Channel/EditChannel.js +++ b/web/src/pages/Channel/EditChannel.js @@ -25,6 +25,7 @@ import { ImagePreview, Card, Tag, + Upload, } from '@douyinfe/semi-ui'; import { getChannelModels } from '../../helpers'; import { @@ -34,6 +35,7 @@ import { IconSetting, IconCode, IconGlobe, + IconBolt, } from '@douyinfe/semi-icons'; const { Text, Title } = Typography; @@ -97,8 +99,9 @@ const EditChannel = (props) => { tag: '', }; const [batch, setBatch] = useState(false); + const [mergeToSingle, setMergeToSingle] = useState(false); const [autoBan, setAutoBan] = useState(true); - // const [autoBan, setAutoBan] = useState(true); + const [jsonFiles, setJsonFiles] = useState([]); const [inputs, setInputs] = useState(originInputs); const [originModelOptions, setOriginModelOptions] = useState([]); const [modelOptions, setModelOptions] = useState([]); @@ -325,9 +328,20 @@ const EditChannel = (props) => { }, [props.editingChannel.id]); const submit = async () => { - if (!isEdit && (inputs.name === '' || inputs.key === '')) { - showInfo(t('请填写渠道名称和渠道密钥!')); - return; + if (!isEdit) { + if (inputs.name === '') { + showInfo(t('请填写渠道名称!')); + return; + } + if (inputs.type === 41 && batch) { + if (jsonFiles.length === 0) { + showInfo(t('请至少选择一个 JSON 凭证文件!')); + return; + } + } else if (inputs.key === '') { + showInfo(t('请填写渠道密钥!')); + return; + } } if (inputs.models.length === 0) { showInfo(t('请至少选择一个模型!')); @@ -356,13 +370,32 @@ const EditChannel = (props) => { localInputs.auto_ban = autoBan ? 1 : 0; localInputs.models = localInputs.models.join(','); localInputs.group = localInputs.groups.join(','); + + if (inputs.type === 41 && batch) { + const keyObj = {}; + jsonFiles.forEach((content, idx) => { + keyObj[content] = idx; + }); + localInputs.key = JSON.stringify(keyObj); + } + + let mode = 'single'; + if (batch) { + mode = mergeToSingle ? 'multi_to_single' : 'batch'; + } + + const payload = { + mode, + channel: localInputs, + }; + 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/`, payload); } const { success, message } = res.data; if (success) { @@ -415,6 +448,18 @@ const EditChannel = (props) => { } }; + const handleJsonFileUpload = (file) => { + return new Promise((resolve) => { + const reader = new FileReader(); + reader.onload = (e) => { + const content = e.target.result; + setJsonFiles((prev) => [...prev, content]); + resolve({ shouldUpload: false, status: 'success' }); + }; + reader.readAsText(file); + }); + }; + return ( <> {
{t('密钥')} {batch ? ( -