Files
image2api/backend/internal/http/handler/channel_admin.go
T
chiyiandDevin 4632b1ab04 feat(channel): 对齐 vivid 最新渠道/模型管理设计
渠道管理(对齐 9db9971/9f6eeca/b607a8d/268a8f7/7f8a87e):
- 列表新增「运行中」(Redis 并发槽位实时计数,与调度同源)和「失败数」
  (event_logs 按渠道聚合)两列
- 行内「同步上游模型」恢复弹窗:默认全开、右侧开关、彩点+单行别名,
  底部「同步模型」批量导入;编辑表单去掉获取模型列表按钮(保存已自动
  全量同步);去掉勾选即解绑语义与 unbind-model 端点
- 导入加固:别名与上游同名报错、同批别名复用拒绝、本地类型冲突拒绝、
  单次上限 50

模型管理(对齐 5545195):
- 模型表单/CreateModel 补齐 max_reference_videos/audios 与
  max_ref_video/audio_seconds 透传

产物(对齐 2253e58):
- 下载结果仅信任 image/*/video/* 响应头,text/plain 等乱报一律按魔数
  嗅探定 MIME

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-19 19:14:25 +08:00

142 lines
4.5 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})
}
// 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()})
}
}