diff --git a/controller/channel.go b/controller/channel.go index ee6ddeba..126b7877 100644 --- a/controller/channel.go +++ b/controller/channel.go @@ -943,3 +943,52 @@ func GetTagModels(c *gin.Context) { }) return } + +// CopyChannel handles cloning an existing channel with its key. +// POST /api/channel/copy/:id +// Optional query params: +// suffix - string appended to the original name (default "_复制") +// reset_balance - bool, when true will reset balance & used_quota to 0 (default true) +func CopyChannel(c *gin.Context) { + id, err := strconv.Atoi(c.Param("id")) + if err != nil { + c.JSON(http.StatusOK, gin.H{"success": false, "message": "invalid id"}) + return + } + + suffix := c.DefaultQuery("suffix", "_复制") + resetBalance := true + if rbStr := c.DefaultQuery("reset_balance", "true"); rbStr != "" { + if v, err := strconv.ParseBool(rbStr); err == nil { + resetBalance = v + } + } + + // fetch original channel with key + origin, err := model.GetChannelById(id, true) + if err != nil { + c.JSON(http.StatusOK, gin.H{"success": false, "message": err.Error()}) + return + } + + // clone channel + clone := *origin // shallow copy is sufficient as we will overwrite primitives + clone.Id = 0 // let DB auto-generate + clone.CreatedTime = common.GetTimestamp() + clone.Name = origin.Name + suffix + clone.TestTime = 0 + clone.ResponseTime = 0 + if resetBalance { + clone.Balance = 0 + clone.UsedQuota = 0 + } + + // insert + if err := model.BatchInsertChannels([]model.Channel{clone}); err != nil { + c.JSON(http.StatusOK, gin.H{"success": false, "message": err.Error()}) + return + } + + // success + c.JSON(http.StatusOK, gin.H{"success": true, "message": "", "data": gin.H{"id": clone.Id}}) +} diff --git a/router/api-router.go b/router/api-router.go index db4c3898..4bd2faff 100644 --- a/router/api-router.go +++ b/router/api-router.go @@ -115,6 +115,7 @@ func SetApiRouter(router *gin.Engine) { channelRoute.POST("/fetch_models", controller.FetchModels) channelRoute.POST("/batch/tag", controller.BatchSetChannelTag) channelRoute.GET("/tag/models", controller.GetTagModels) + channelRoute.POST("/copy/:id", controller.CopyChannel) } tokenRoute := apiRouter.Group("/token") tokenRoute.Use(middleware.UserAuth()) diff --git a/web/src/components/table/ChannelsTable.js b/web/src/components/table/ChannelsTable.js index 2582b950..2d409b09 100644 --- a/web/src/components/table/ChannelsTable.js +++ b/web/src/components/table/ChannelsTable.js @@ -964,28 +964,16 @@ const ChannelsTable = () => { }; const copySelectedChannel = async (record) => { - const channelToCopy = { ...record }; - channelToCopy.name += t('_复制'); - channelToCopy.created_time = null; - channelToCopy.balance = 0; - channelToCopy.used_quota = 0; - delete channelToCopy.test_time; - delete channelToCopy.response_time; - if (!channelToCopy) { - showError(t('渠道未找到,请刷新页面后重试。')); - return; - } try { - const newChannel = { ...channelToCopy, id: undefined }; - const response = await API.post('/api/channel/', newChannel); - if (response.data.success) { + const res = await API.post(`/api/channel/copy/${record.id}`); + if (res?.data?.success) { showSuccess(t('渠道复制成功')); await refresh(); } else { - showError(response.data.message); + showError(res?.data?.message || t('渠道复制失败')); } } catch (error) { - showError(t('渠道复制失败: ') + error.message); + showError(t('渠道复制失败: ') + (error?.response?.data?.message || error?.message || error)); } };