Files
image2api/backend/internal/http/handler/provider_admin.go
T

535 lines
15 KiB
Go

package handler
import (
"errors"
"net/http"
"strings"
"backend/internal/service"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type ProviderAdminHandler struct {
tokens *service.TokenService
refresh *service.RefreshProfileService
}
func NewProviderAdminHandler(tokens *service.TokenService, refresh *service.RefreshProfileService) *ProviderAdminHandler {
return &ProviderAdminHandler{
tokens: tokens,
refresh: refresh,
}
}
func (h *ProviderAdminHandler) TokensList(c *gin.Context) {
data, err := h.tokens.List(c.Request.Context())
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": "failed to load tokens"})
return
}
c.JSON(http.StatusOK, gin.H{"data": data})
}
func (h *ProviderAdminHandler) TokensCreate(c *gin.Context) {
var body struct {
Pool string `json:"pool"`
Value string `json:"value"`
ID string `json:"id"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": "invalid request body"})
return
}
item, err := h.tokens.Add(c.Request.Context(), body.Pool, body.Value, body.ID)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"ok": true, "id": item.ID})
}
func (h *ProviderAdminHandler) ImportChatGPTToken(c *gin.Context) {
var body struct {
AccessToken string `json:"access_token"`
Value string `json:"value"`
Name string `json:"name"`
ID string `json:"id"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": "invalid request body"})
return
}
token := body.AccessToken
if token == "" {
token = body.Value
}
name := body.Name
if name == "" {
name = body.ID
}
item, err := h.tokens.ImportChatGPTToken(c.Request.Context(), token, name)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"ok": true, "id": item.ID, "status": item.Status, "pending": item.Status == "pending"})
}
func (h *ProviderAdminHandler) ImportRunwayToken(c *gin.Context) {
var body struct {
AccessToken string `json:"access_token"`
Value string `json:"value"`
Name string `json:"name"`
ID string `json:"id"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": "invalid request body"})
return
}
token := body.AccessToken
if token == "" {
token = body.Value
}
name := body.Name
if name == "" {
name = body.ID
}
item, err := h.tokens.ImportRunwayToken(c.Request.Context(), token, name)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"ok": true, "id": item.ID, "status": item.Status, "pending": item.Status == "pending"})
}
func (h *ProviderAdminHandler) ImportGrokToken(c *gin.Context) {
var body struct {
AccessToken string `json:"access_token"`
Value string `json:"value"`
SSO string `json:"sso"`
Name string `json:"name"`
ID string `json:"id"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": "invalid request body"})
return
}
token := body.AccessToken
for _, v := range []string{body.Value, body.SSO} {
if token == "" {
token = v
}
}
name := body.Name
if name == "" {
name = body.ID
}
item, err := h.tokens.ImportGrokToken(c.Request.Context(), token, name)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"ok": true, "id": item.ID, "status": item.Status, "pending": item.Status == "pending"})
}
func (h *ProviderAdminHandler) ImportCustomAccount(c *gin.Context) {
var body struct {
BaseURL string `json:"base_url"`
URL string `json:"url"`
Key string `json:"key"`
APIKey string `json:"api_key"`
Models string `json:"models"`
Name string `json:"name"`
Weight int `json:"weight"`
Concurrency int `json:"concurrency"`
ID string `json:"id"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": "invalid request body"})
return
}
baseURL := body.BaseURL
if baseURL == "" {
baseURL = body.URL
}
key := body.Key
if key == "" {
key = body.APIKey
}
item, err := h.tokens.ImportCustomAccount(c.Request.Context(), baseURL, key, body.Models, body.Name, body.Weight, body.Concurrency, body.ID)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"ok": true, "id": item.ID, "status": item.Status})
}
func (h *ProviderAdminHandler) ImportKreaCookie(c *gin.Context) {
var body struct {
Cookie string `json:"cookie"`
Value string `json:"value"`
Name string `json:"name"`
ID string `json:"id"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": "invalid request body"})
return
}
cookie := body.Cookie
if cookie == "" {
cookie = body.Value
}
name := body.Name
if name == "" {
name = body.ID
}
item, err := h.tokens.ImportKreaCookie(c.Request.Context(), cookie, name)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"ok": true, "id": item.ID, "status": item.Status, "pending": item.Status == "pending"})
}
func (h *ProviderAdminHandler) ImportImagineToken(c *gin.Context) {
var body struct {
Cookie string `json:"cookie"`
Value string `json:"value"`
Name string `json:"name"`
ID string `json:"id"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": "invalid request body"})
return
}
cred := body.Cookie
if cred == "" {
cred = body.Value
}
name := body.Name
if name == "" {
name = body.ID
}
item, err := h.tokens.ImportImagineToken(c.Request.Context(), cred, name)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"ok": true, "id": item.ID, "status": item.Status, "pending": item.Status == "pending"})
}
func (h *ProviderAdminHandler) ImportLeonardoCookie(c *gin.Context) {
var body struct {
Cookie string `json:"cookie"`
Value string `json:"value"`
Name string `json:"name"`
ID string `json:"id"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": "invalid request body"})
return
}
cookie := body.Cookie
if cookie == "" {
cookie = body.Value
}
name := body.Name
if name == "" {
name = body.ID
}
item, err := h.tokens.ImportLeonardoCookie(c.Request.Context(), cookie, name)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"ok": true, "id": item.ID, "status": item.Status, "pending": item.Status == "pending"})
}
func (h *ProviderAdminHandler) ImportAdobeCookie(c *gin.Context) {
var body struct {
Cookie string `json:"cookie"`
Value string `json:"value"`
Name string `json:"name"`
ID string `json:"id"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": "invalid request body"})
return
}
cookie := body.Cookie
if cookie == "" {
cookie = body.Value
}
name := body.Name
if name == "" {
name = body.ID
}
item, profile, err := h.tokens.ImportAdobeCookie(c.Request.Context(), cookie, name)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"ok": true,
"profile_id": profile.ID,
"id": item.ID,
"status": item.Status,
"pending": item.Status == "pending",
})
}
func (h *ProviderAdminHandler) ImportCreativeFabricaCookie(c *gin.Context) {
var body struct {
Cookie string `json:"cookie"`
Value string `json:"value"`
Name string `json:"name"`
ID string `json:"id"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": "invalid request body"})
return
}
cookie := body.Cookie
if cookie == "" {
cookie = body.Value
}
name := body.Name
if name == "" {
name = body.ID
}
item, err := h.tokens.ImportCreativeFabricaCookie(c.Request.Context(), cookie, name)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"ok": true,
"id": item.ID,
"status": item.Status,
"pending": item.Status == "pending",
})
}
func (h *ProviderAdminHandler) TokenUpdate(c *gin.Context) {
var body map[string]any
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": "invalid request body"})
return
}
item, err := h.tokens.Update(c.Request.Context(), c.Param("pool"), c.Param("id"), body)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"ok": true, "data": item})
}
func (h *ProviderAdminHandler) TokenDelete(c *gin.Context) {
if err := h.tokens.Delete(c.Request.Context(), c.Param("pool"), c.Param("id")); err != nil {
if errors.Is(err, service.ErrNotFound) {
c.JSON(http.StatusNotFound, gin.H{"detail": "token not found"})
return
}
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"ok": true})
}
// TokenDeleteBulk removes multiple accounts in one call (account multi-select).
func (h *ProviderAdminHandler) TokenDeleteBulk(c *gin.Context) {
var body struct {
IDs []string `json:"ids"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": "invalid request body"})
return
}
if len(body.IDs) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"detail": "未选择任何账号"})
return
}
n, err := h.tokens.DeleteBulk(c.Request.Context(), body.IDs)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"ok": true, "deleted": n})
}
func (h *ProviderAdminHandler) AccountsList(c *gin.Context) {
data, err := h.tokens.Accounts(c.Request.Context())
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": "failed to load accounts"})
return
}
// KPI stats are computed over the FULL set (每个类型的 成功/失败/限额), independent
// of the current filter/page — mirrors the old client-side `stats` computed.
stats := accountsStats(data)
// Server-side filtering (类型 / 状态 / 搜索 邮箱·ID·类型 / dead) so pagination is
// correct across pages. ?dead=1 returns only 异常(已失效) accounts — used by
// 「删除异常账号」to collect every dead id regardless of the current page.
typeFilter := strings.TrimSpace(c.Query("type"))
statusFilter := strings.TrimSpace(c.Query("status"))
planFilter := strings.TrimSpace(c.Query("plan"))
deadOnly := c.Query("dead") == "1"
q := strings.ToLower(strings.TrimSpace(c.Query("q")))
filtered := make([]map[string]any, 0, len(data))
for _, row := range data {
if deadOnly && !rowBool(row, "dead") {
continue
}
if typeFilter != "" && rowStr(row, "type") != typeFilter {
continue
}
if statusFilter != "" && rowStr(row, "status") != statusFilter {
continue
}
// plan filter: vip=非free, free=free, sub=子号, master=vip且非子号
if planFilter != "" {
plan := strings.ToLower(rowStr(row, "plan"))
isSub := rowBool(row, "sub_account")
isFree := plan == "" || plan == "free"
switch planFilter {
case "vip":
if isFree { continue }
case "free":
if !isFree { continue }
case "sub":
if !isSub { continue }
case "master":
if isFree || isSub { continue }
}
}
if q != "" {
email := strings.ToLower(rowStr(row, "email"))
id := strings.ToLower(rowStr(row, "id"))
typ := strings.ToLower(rowStr(row, "type"))
if !strings.Contains(email, q) && !strings.Contains(id, q) && !strings.Contains(typ, q) {
continue
}
}
filtered = append(filtered, row)
}
total := len(filtered)
limit, offset := pageParams(c, 20)
page := pageSlice(filtered, limit, offset)
c.JSON(http.StatusOK, gin.H{"data": page, "total": total, "limit": limit, "offset": offset, "stats": stats})
}
// accountsStats reproduces the 账号 KPI strip: per-type 正常/失效/限额 counts plus a
// grand total and total dead count (drives 「删除异常账号 (N)」).
func accountsStats(rows []map[string]any) gin.H {
types := []string{"openai", "adobe", "runway", "leonardo", "krea", "imagine", "grok", "creativefabrica"}
by := map[string]*struct{ N, Ok, Dead, Quota int }{}
for _, t := range types {
by[t] = &struct{ N, Ok, Dead, Quota int }{}
}
deadTotal := 0
for _, row := range rows {
dead := rowBool(row, "dead")
if dead {
deadTotal++
}
g, ok := by[rowStr(row, "type")]
if !ok {
continue
}
g.N++
status := rowStr(row, "status")
switch {
case status == "active":
g.Ok++
case dead || status == "disabled":
g.Dead++
case status == "quota":
g.Quota++
}
}
out := gin.H{"total": len(rows), "dead_total": deadTotal}
for _, t := range types {
g := by[t]
out[t] = gin.H{"n": g.N, "ok": g.Ok, "dead": g.Dead, "quota": g.Quota}
}
return out
}
func rowStr(m map[string]any, key string) string {
s, _ := m[key].(string)
return s
}
func rowBool(m map[string]any, key string) bool {
b, _ := m[key].(bool)
return b
}
func (h *ProviderAdminHandler) AccountQuota(c *gin.Context) {
data, err := h.tokens.Quota(c.Request.Context(), c.Param("pool"), c.Param("id"))
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
c.JSON(http.StatusNotFound, gin.H{"detail": "account not found"})
return
}
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
return
}
c.JSON(http.StatusOK, data)
}
func (h *ProviderAdminHandler) AccountEmail(c *gin.Context) {
data, err := h.tokens.Email(c.Request.Context(), c.Param("pool"), c.Param("id"))
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
c.JSON(http.StatusNotFound, gin.H{"detail": "account not found"})
return
}
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
return
}
c.JSON(http.StatusOK, data)
}
func (h *ProviderAdminHandler) RefreshProfiles(c *gin.Context) {
items, err := h.refresh.List(c.Request.Context())
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": "failed to load refresh profiles"})
return
}
c.JSON(http.StatusOK, gin.H{"data": items})
}
func (h *ProviderAdminHandler) RefreshNow(c *gin.Context) {
if err := h.refresh.RefreshNow(c.Request.Context(), c.Param("profile_id")); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"ok": true})
}
func (h *ProviderAdminHandler) RefreshUpdate(c *gin.Context) {
var body map[string]any
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": "invalid request body"})
return
}
item, err := h.refresh.Update(c.Request.Context(), c.Param("profile_id"), body)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"ok": true, "data": item})
}
func (h *ProviderAdminHandler) RefreshDelete(c *gin.Context) {
if err := h.refresh.Delete(c.Request.Context(), c.Param("profile_id")); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"ok": true})
}