| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335 |
- package handler
- import (
- "encoding/json"
- "errors"
- "io"
- "net/http"
- "strconv"
- "strings"
- infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
- "github.com/Wei-Shaw/sub2api/internal/pkg/logger"
- "github.com/Wei-Shaw/sub2api/internal/server/middleware"
- "github.com/Wei-Shaw/sub2api/internal/service"
- "github.com/gin-gonic/gin"
- "go.uber.org/zap"
- )
- type BatchImageHandler struct {
- service *service.BatchImagePublicService
- download *service.BatchImageDownloadService
- cleanup *service.BatchImageCleanupService
- openAI *OpenAIGatewayHandler
- }
- func NewBatchImageHandler(service *service.BatchImagePublicService, download *service.BatchImageDownloadService, cleanup *service.BatchImageCleanupService) *BatchImageHandler {
- return &BatchImageHandler{service: service, download: download, cleanup: cleanup}
- }
- func (h *BatchImageHandler) Submit(c *gin.Context) {
- var req service.BatchImageSubmitRequest
- if err := c.ShouldBindJSON(&req); err != nil {
- batchImageError(c, service.ErrBatchImageInvalidItems)
- return
- }
- owner, ok := batchImageOwnerFromContext(c)
- if !ok {
- batchImageError(c, infraerrors.New(http.StatusUnauthorized, "API_KEY_REQUIRED", "API key is required"))
- return
- }
- if !h.checkSecurityAuditBeforeSubmit(c, &req) {
- return
- }
- if sessionID := service.ExtractClientSessionID(c); sessionID != "" {
- req.SessionID = &sessionID
- }
- got, err := h.service.Submit(c.Request.Context(), owner, req, c.GetHeader("Idempotency-Key"))
- if err != nil {
- batchImageError(c, err)
- return
- }
- c.JSON(http.StatusOK, got)
- }
- func (h *BatchImageHandler) checkSecurityAuditBeforeSubmit(c *gin.Context, req *service.BatchImageSubmitRequest) bool {
- if h == nil || h.openAI == nil || req == nil {
- return true
- }
- apiKey, ok := middleware.GetAPIKeyFromContext(c)
- if !ok || apiKey == nil {
- batchImageError(c, infraerrors.New(http.StatusUnauthorized, "API_KEY_REQUIRED", "API key is required"))
- return false
- }
- subject, ok := middleware.GetAuthSubjectFromContext(c)
- if !ok {
- batchImageError(c, infraerrors.New(http.StatusInternalServerError, "USER_CONTEXT_REQUIRED", "User context not found"))
- return false
- }
- items := make([]map[string]string, 0, len(req.Items))
- for _, item := range req.Items {
- if prompt := strings.TrimSpace(item.Prompt); prompt != "" {
- items = append(items, map[string]string{"prompt": prompt})
- }
- }
- if len(items) == 0 {
- return true
- }
- body, err := json.Marshal(map[string]any{"request": map[string]any{"items": items}})
- if err != nil {
- batchImageError(c, infraerrors.New(http.StatusBadRequest, "INVALID_BATCH_PROMPT", "batch prompts are invalid"))
- return false
- }
- reqLog := requestLogger(c, "handler.batch_image.security_audit",
- zap.Int64("user_id", subject.UserID), zap.Int64("api_key_id", apiKey.ID), zap.String("model", req.Model))
- decision := h.openAI.checkSecurityAudit(c, reqLog, apiKey, subject, service.ContentModerationProtocolOpenAIImages, req.Model, body)
- if decision != nil && !decision.AllowNextStage {
- h.openAI.openAISecurityAuditError(c, decision)
- return false
- }
- return true
- }
- func (h *BatchImageHandler) Get(c *gin.Context) {
- owner, ok := batchImageOwnerFromContext(c)
- if !ok {
- batchImageError(c, infraerrors.New(http.StatusUnauthorized, "API_KEY_REQUIRED", "API key is required"))
- return
- }
- got, err := h.service.Get(c.Request.Context(), owner, c.Param("id"))
- if err != nil {
- batchImageError(c, err)
- return
- }
- c.JSON(http.StatusOK, got)
- }
- func (h *BatchImageHandler) List(c *gin.Context) {
- owner, ok := batchImageOwnerFromContext(c)
- if !ok {
- batchImageError(c, infraerrors.New(http.StatusUnauthorized, "API_KEY_REQUIRED", "API key is required"))
- return
- }
- limit, _ := strconv.Atoi(c.Query("limit"))
- got, err := h.service.List(c.Request.Context(), owner, service.BatchImageJobsQuery{
- Status: c.Query("status"),
- TaskName: c.Query("task_name"),
- Downloaded: c.Query("downloaded"),
- From: c.Query("from"),
- To: c.Query("to"),
- Limit: limit,
- Cursor: c.Query("cursor"),
- })
- if err != nil {
- batchImageError(c, err)
- return
- }
- c.JSON(http.StatusOK, got)
- }
- func (h *BatchImageHandler) Models(c *gin.Context) {
- owner, ok := batchImageOwnerFromContext(c)
- if !ok {
- batchImageError(c, infraerrors.New(http.StatusUnauthorized, "API_KEY_REQUIRED", "API key is required"))
- return
- }
- got, err := h.service.ListModels(c.Request.Context(), owner)
- if err != nil {
- batchImageError(c, err)
- return
- }
- // 分组级模型白名单:ListModels 已按分组账号枚举,这里再按白名单过滤一次。
- if apiKey, ok := middleware.GetAPIKeyFromContext(c); ok && apiKey != nil && apiKey.Group != nil {
- got.Data = filterBatchImageModelsByAllowlist(got.Data, apiKey.Group.ModelAllowlist)
- }
- c.JSON(http.StatusOK, got)
- }
- // filterBatchImageModelsByAllowlist 按分组模型白名单过滤批量生图模型列表。
- // 白名单未开启时原样返回。
- func filterBatchImageModelsByAllowlist(models []service.BatchImagePublicModel, allowlist service.GroupModelAllowlist) []service.BatchImagePublicModel {
- if !allowlist.Enabled {
- return models
- }
- filtered := make([]service.BatchImagePublicModel, 0, len(models))
- for _, model := range models {
- if allowlist.Allows(model.ID) {
- filtered = append(filtered, model)
- }
- }
- return filtered
- }
- func (h *BatchImageHandler) Items(c *gin.Context) {
- owner, ok := batchImageOwnerFromContext(c)
- if !ok {
- batchImageError(c, infraerrors.New(http.StatusUnauthorized, "API_KEY_REQUIRED", "API key is required"))
- return
- }
- limit, _ := strconv.Atoi(c.Query("limit"))
- got, err := h.service.ListItems(c.Request.Context(), owner, c.Param("id"), service.BatchImageItemsQuery{
- Status: c.Query("status"),
- Limit: limit,
- Cursor: c.Query("cursor"),
- })
- if err != nil {
- batchImageError(c, err)
- return
- }
- c.JSON(http.StatusOK, got)
- }
- func (h *BatchImageHandler) Cancel(c *gin.Context) {
- owner, ok := batchImageOwnerFromContext(c)
- if !ok {
- batchImageError(c, infraerrors.New(http.StatusUnauthorized, "API_KEY_REQUIRED", "API key is required"))
- return
- }
- got, err := h.service.Cancel(c.Request.Context(), owner, c.Param("id"))
- if err != nil {
- batchImageError(c, err)
- return
- }
- c.JSON(http.StatusOK, got)
- }
- func (h *BatchImageHandler) ItemContent(c *gin.Context) {
- owner, ok := batchImageOwnerFromContext(c)
- if !ok {
- batchImageError(c, infraerrors.New(http.StatusUnauthorized, "API_KEY_REQUIRED", "API key is required"))
- return
- }
- imageIndex := 0
- if raw := c.Query("image_index"); raw != "" {
- parsed, err := strconv.Atoi(raw)
- if err != nil {
- batchImageError(c, service.ErrBatchImageItemImageIndexOutOfRange)
- return
- }
- imageIndex = parsed
- }
- stream, err := h.download.OpenItemContent(c.Request.Context(), owner, c.Param("id"), c.Param("custom_id"), imageIndex)
- if err != nil {
- batchImageError(c, err)
- return
- }
- defer func() { _ = stream.Reader.Close() }()
- c.Header("Content-Type", stream.ContentType)
- c.Header("Content-Disposition", service.BatchImageContentDispositionAttachment(stream.Filename))
- c.Header("Cache-Control", "private, max-age=300")
- c.Header("X-Content-Type-Options", "nosniff")
- if stream.ContentLength != nil && *stream.ContentLength >= 0 {
- c.Header("Content-Length", strconv.FormatInt(*stream.ContentLength, 10))
- }
- c.Status(http.StatusOK)
- if _, err := io.Copy(c.Writer, stream.Reader); err != nil {
- return
- }
- h.markDownloadedBestEffort(c, owner)
- }
- // markDownloadedBestEffort 在响应体已写出后标记下载状态;
- // 此时无法再向客户端返回错误,失败只能记日志(不能静默丢弃)。
- func (h *BatchImageHandler) markDownloadedBestEffort(c *gin.Context, owner service.BatchImageOwner) {
- if err := h.service.MarkDownloaded(c.Request.Context(), owner, c.Param("id")); err != nil {
- logger.L().Warn("batch_image.mark_downloaded_failed",
- zap.String("batch_id", c.Param("id")),
- zap.Error(err),
- )
- }
- }
- func (h *BatchImageHandler) Download(c *gin.Context) {
- owner, ok := batchImageOwnerFromContext(c)
- if !ok {
- batchImageError(c, infraerrors.New(http.StatusUnauthorized, "API_KEY_REQUIRED", "API key is required"))
- return
- }
- maxItems, _ := strconv.Atoi(c.Query("max_items"))
- c.Header("Content-Type", "application/zip")
- c.Header("Content-Disposition", service.BatchImageContentDispositionAttachment(c.Param("id")+".zip"))
- c.Header("Cache-Control", "private, no-store")
- c.Header("X-Content-Type-Options", "nosniff")
- result, err := h.download.StreamZip(c.Request.Context(), owner, c.Param("id"), service.BatchImageZipOptions{
- Status: c.Query("status"),
- MaxItems: maxItems,
- IncludeManifest: true,
- }, c.Writer)
- if err != nil {
- if result == nil || !c.Writer.Written() {
- batchImageError(c, err)
- }
- return
- }
- h.markDownloadedBestEffort(c, owner)
- }
- func (h *BatchImageHandler) DeleteRecord(c *gin.Context) {
- owner, ok := batchImageOwnerFromContext(c)
- if !ok {
- batchImageError(c, infraerrors.New(http.StatusUnauthorized, "API_KEY_REQUIRED", "API key is required"))
- return
- }
- if err := h.service.DeleteRecord(c.Request.Context(), owner, c.Param("id")); err != nil {
- batchImageError(c, err)
- return
- }
- c.Status(http.StatusNoContent)
- }
- func (h *BatchImageHandler) DeleteOutputs(c *gin.Context) {
- owner, ok := batchImageOwnerFromContext(c)
- if !ok {
- batchImageError(c, infraerrors.New(http.StatusUnauthorized, "API_KEY_REQUIRED", "API key is required"))
- return
- }
- got, err := h.cleanup.DeleteOutputsForOwner(c.Request.Context(), owner, c.Param("id"))
- if err != nil {
- batchImageError(c, err)
- return
- }
- c.JSON(http.StatusOK, got)
- }
- func batchImageOwnerFromContext(c *gin.Context) (service.BatchImageOwner, bool) {
- apiKey, ok := middleware.GetAPIKeyFromContext(c)
- if !ok || apiKey == nil || apiKey.ID <= 0 || apiKey.UserID <= 0 {
- return service.BatchImageOwner{}, false
- }
- return service.BatchImageOwner{
- UserID: apiKey.UserID,
- APIKeyID: apiKey.ID,
- GroupID: apiKey.GroupID,
- }, true
- }
- func batchImageError(c *gin.Context, err error) {
- status := infraerrors.Code(err)
- code := infraerrors.Reason(err)
- message := infraerrors.Message(err)
- if err == nil {
- status = http.StatusInternalServerError
- code = "INTERNAL_ERROR"
- message = "internal error"
- }
- if status == 0 || (status == http.StatusInternalServerError && strings.TrimSpace(code) == "") {
- status = http.StatusInternalServerError
- code = "INTERNAL_ERROR"
- message = "internal error"
- }
- if errors.Is(err, service.ErrBatchImageJobNotFound) {
- status = http.StatusNotFound
- code = "BATCH_IMAGE_NOT_FOUND"
- message = "batch image job not found"
- }
- c.JSON(status, gin.H{
- "error": gin.H{
- "type": "invalid_request_error",
- "code": code,
- "message": message,
- },
- })
- }
|