feat!: 账号池改渠道网关,对外协议换成 vivid /v1
- 新增 channels/channel_models:上游 URL + AES-GCM 加密 key +
并发/权重/本地统计,模型经渠道 /v1/models 导入绑定
- internal/upstream:vivid 协议客户端(NDJSON 流 + jobId 断流重连 +
uploads 预签名 + models + balance + 错误分类)
- service/v1 重写:权重分组调度、Redis 并发槽 conc:c:、故障转移,
站内/测试/对外统一走渠道执行器
- 对外端点换成 vivid 四件套:POST /v1/generate(NDJSON+jobId 重连)、
POST /v1/uploads(SigV4 预签名 PUT)、GET /v1/models、GET /v1/balance,
错误包 {"error":{"code","message"}}
- API key 新发改 vk_+48hex;删除全部内置 provider 与 token 池
- 管理端:渠道 CRUD/拉模型/导入/余额端点,前端账号页换渠道页,
概览/文档/测试弹窗适配
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
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, 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})
|
||||
}
|
||||
|
||||
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})
|
||||
}
|
||||
|
||||
// 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()})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user