对齐 vivid 6039423/5802749/a1f808f/cfb6d49/aba8474/b01636f: - 新增 POST /channels/sync-models:已保存渠道手动全量同步,上游不可达/ 空清单直接报错(保存路径仍降级为告警);渠道行按钮不再弹勾选框, 表单内对已保存渠道同样直同步,新渠道保持预览+勾选即绑定 - 渠道并发 0 = 不限制(原 0=默认5),上限放开到 int32,槽位 Lua 本就 max=0 放行 - 别名与上游模型同名提交即报错「留空即同名」,前后端双校验 - 渠道列表去掉上游地址与模型两列(同 vivid),状态徽章悬浮显示失败原因 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
160 lines
5.1 KiB
Go
160 lines
5.1 KiB
Go
package handler
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"backend/internal/service"
|
|
"backend/internal/upstream"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// ChannelAdminHandler exposes channel CRUD + upstream model import under
|
|
// /admin/api/channels*. Replaces the old token/account admin surface.
|
|
type ChannelAdminHandler struct {
|
|
channels *service.ChannelService
|
|
}
|
|
|
|
func NewChannelAdminHandler(channels *service.ChannelService) *ChannelAdminHandler {
|
|
return &ChannelAdminHandler{channels: channels}
|
|
}
|
|
|
|
func (h *ChannelAdminHandler) List(c *gin.Context) {
|
|
data, err := h.channels.List(c.Request.Context())
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"detail": "failed to load channels"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"data": data})
|
|
}
|
|
|
|
func (h *ChannelAdminHandler) Save(c *gin.Context) {
|
|
var body service.SaveChannelDto
|
|
if err := c.ShouldBindJSON(&body); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"detail": "invalid request body"})
|
|
return
|
|
}
|
|
body.BaseURL = strings.TrimSpace(body.BaseURL)
|
|
ch, warning, err := h.channels.Save(c.Request.Context(), body)
|
|
if err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
c.JSON(http.StatusNotFound, gin.H{"detail": "channel not found"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"ok": true, "id": ch.ID, "warning": warning})
|
|
}
|
|
|
|
func (h *ChannelAdminHandler) Delete(c *gin.Context) {
|
|
var body struct {
|
|
ID string `json:"id"`
|
|
}
|
|
if err := c.ShouldBindJSON(&body); err != nil || strings.TrimSpace(body.ID) == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"detail": "channel id required"})
|
|
return
|
|
}
|
|
if err := h.channels.Delete(c.Request.Context(), body.ID); err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
c.JSON(http.StatusNotFound, gin.H{"detail": "channel not found"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"ok": true})
|
|
}
|
|
|
|
// FetchModels proxies GET /v1/models on a channel (stored creds or ad-hoc
|
|
// base_url+api_key for a not-yet-saved channel form).
|
|
func (h *ChannelAdminHandler) FetchModels(c *gin.Context) {
|
|
var body service.FetchModelsDto
|
|
if err := c.ShouldBindJSON(&body); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"detail": "invalid request body"})
|
|
return
|
|
}
|
|
items, err := h.channels.FetchModels(c.Request.Context(), body)
|
|
if err != nil {
|
|
writeUpstreamErr(c, err)
|
|
return
|
|
}
|
|
if items == nil {
|
|
items = []upstream.RemoteModel{}
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"data": items})
|
|
}
|
|
|
|
// SyncModels manually re-runs the full upstream model sync on a saved channel
|
|
// (new models bound, disappeared ones unbound, orphans deleted). Fetch
|
|
// failures are errors here — unlike the save path which degrades to a warning.
|
|
func (h *ChannelAdminHandler) SyncModels(c *gin.Context) {
|
|
var body struct {
|
|
ID string `json:"id"`
|
|
}
|
|
if err := c.ShouldBindJSON(&body); err != nil || strings.TrimSpace(body.ID) == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"detail": "channel id required"})
|
|
return
|
|
}
|
|
msg, err := h.channels.SyncModels(c.Request.Context(), body.ID)
|
|
if err != nil {
|
|
writeUpstreamErr(c, err)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"ok": true, "message": msg})
|
|
}
|
|
|
|
// ImportModels binds the ticked upstream models to the channel (creating local
|
|
// model rows on first import).
|
|
func (h *ChannelAdminHandler) ImportModels(c *gin.Context) {
|
|
var body struct {
|
|
ID string `json:"id"`
|
|
Models []service.ChannelModelSpec `json:"models"`
|
|
}
|
|
if err := c.ShouldBindJSON(&body); err != nil || strings.TrimSpace(body.ID) == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"detail": "channel id required"})
|
|
return
|
|
}
|
|
if err := h.channels.ImportModels(c.Request.Context(), body.ID, body.Models); err != nil {
|
|
writeUpstreamErr(c, err)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"ok": true})
|
|
}
|
|
|
|
// UnbindModel detaches one upstream model from a channel; the local model row
|
|
// is deleted too when no other channel still binds it.
|
|
func (h *ChannelAdminHandler) UnbindModel(c *gin.Context) {
|
|
var body struct {
|
|
ID string `json:"id"`
|
|
Model string `json:"model"`
|
|
}
|
|
if err := c.ShouldBindJSON(&body); err != nil || strings.TrimSpace(body.ID) == "" || strings.TrimSpace(body.Model) == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"detail": "id and model required"})
|
|
return
|
|
}
|
|
if err := h.channels.UnbindModel(c.Request.Context(), body.ID, body.Model); err != nil {
|
|
writeUpstreamErr(c, err)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"ok": true})
|
|
}
|
|
|
|
// writeUpstreamErr translates upstream-protocol errors into sensible admin
|
|
// responses (auth → 400 with the upstream's message, temporary → 502).
|
|
func writeUpstreamErr(c *gin.Context, err error) {
|
|
switch {
|
|
case errors.Is(err, upstream.ErrAuth):
|
|
c.JSON(http.StatusBadRequest, gin.H{"detail": "上游鉴权失败:" + err.Error()})
|
|
case errors.Is(err, upstream.ErrInvalid):
|
|
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
|
|
case errors.Is(err, gorm.ErrRecordNotFound):
|
|
c.JSON(http.StatusNotFound, gin.H{"detail": "channel not found"})
|
|
default:
|
|
c.JSON(http.StatusBadGateway, gin.H{"detail": "上游请求失败:" + err.Error()})
|
|
}
|
|
}
|