| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416 |
- package service
- import (
- "context"
- "crypto/rand"
- "encoding/hex"
- "net/http"
- "time"
- infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
- )
- const (
- BatchImageProviderGeminiAPI = "gemini_api"
- BatchImageProviderVertex = "vertex"
- )
- const (
- BatchImageJobStatusCreated = "created"
- BatchImageJobStatusUploading = "uploading"
- BatchImageJobStatusSubmitted = "submitted"
- BatchImageJobStatusRunning = "running"
- BatchImageJobStatusIndexing = "indexing"
- BatchImageJobStatusSettling = "settling"
- BatchImageJobStatusCompleted = "completed"
- BatchImageJobStatusFailed = "failed"
- BatchImageJobStatusCancelled = "cancelled"
- BatchImageJobStatusOutputDeleted = "output_deleted"
- )
- const (
- BatchImageItemStatusPending = "pending"
- BatchImageItemStatusSuccess = "success"
- BatchImageItemStatusFailed = "failed"
- BatchImageItemStatusCancelled = "cancelled"
- )
- var (
- ErrBatchImageJobNotFound = infraerrors.New(http.StatusNotFound, "BATCH_IMAGE_JOB_NOT_FOUND", "batch image job not found")
- ErrBatchImageJobExists = infraerrors.New(http.StatusConflict, "BATCH_IMAGE_JOB_EXISTS", "batch image job already exists")
- ErrBatchImageItemExists = infraerrors.New(http.StatusConflict, "BATCH_IMAGE_ITEM_EXISTS", "batch image item already exists")
- ErrBatchImageInvalidTransition = infraerrors.New(http.StatusBadRequest, "BATCH_IMAGE_INVALID_TRANSITION", "invalid batch image job status transition")
- ErrBatchImageInvalidProvider = infraerrors.New(http.StatusBadRequest, "BATCH_IMAGE_INVALID_PROVIDER", "invalid batch image provider")
- ErrBatchImageMissingProviderJobName = infraerrors.New(http.StatusBadRequest, "BATCH_IMAGE_MISSING_PROVIDER_JOB_NAME", "batch image provider job name is missing")
- ErrBatchImageMissingAccountID = infraerrors.New(http.StatusBadRequest, "BATCH_IMAGE_MISSING_ACCOUNT_ID", "batch image account id is missing")
- ErrBatchImageUnsupportedProvider = infraerrors.New(http.StatusBadRequest, "BATCH_IMAGE_UNSUPPORTED_PROVIDER", "unsupported batch image provider")
- ErrBatchImageIndexOutputMissing = infraerrors.New(http.StatusBadGateway, "BATCH_IMAGE_INDEX_OUTPUT_MISSING", "batch image provider output is missing")
- ErrBatchImageIndexParseFailed = infraerrors.New(http.StatusBadGateway, "BATCH_IMAGE_INDEX_PARSE_FAILED", "batch image provider output parse failed")
- ErrBatchImageIndexNoResultLines = infraerrors.New(http.StatusBadGateway, "BATCH_IMAGE_INDEX_NO_RESULT_LINES", "batch image provider output has no result lines")
- ErrBatchImageDuplicateCustomID = infraerrors.New(http.StatusBadGateway, "DUPLICATE_CUSTOM_ID_IN_OUTPUT", "batch image provider output contains duplicate custom id")
- ErrBatchImageIndexStateConflict = infraerrors.New(http.StatusConflict, "BATCH_IMAGE_INDEX_STATE_CONFLICT", "batch image job is no longer in indexing state")
- ErrBatchImageSettlementInvalidStatus = infraerrors.New(http.StatusBadRequest, "BATCH_IMAGE_SETTLEMENT_INVALID_STATUS", "batch image job is not ready for settlement")
- ErrBatchImageSettlementManifestConflict = infraerrors.New(http.StatusConflict, "BATCH_IMAGE_SETTLEMENT_MANIFEST_CONFLICT", "batch image settlement manifest hash conflict")
- ErrBatchImageSettlementPricingMissing = infraerrors.New(http.StatusBadRequest, "BATCH_IMAGE_SETTLEMENT_PRICING_MISSING", "batch image settlement pricing is missing")
- ErrBatchImageSettlementBillingFailed = infraerrors.New(http.StatusBadGateway, "BATCH_IMAGE_SETTLEMENT_BILLING_FAILED", "batch image settlement billing failed")
- ErrBatchImageAlreadySettled = infraerrors.New(http.StatusConflict, "BATCH_IMAGE_ALREADY_SETTLED", "batch image job is already settled")
- ErrBatchImageSettlementMissingAPIKeyID = infraerrors.New(http.StatusBadRequest, "BATCH_IMAGE_SETTLEMENT_MISSING_API_KEY_ID", "batch image settlement api key id is missing")
- ErrBatchImageSettlementMissingAccountID = infraerrors.New(http.StatusBadRequest, "BATCH_IMAGE_SETTLEMENT_MISSING_ACCOUNT_ID", "batch image settlement account id is missing")
- ErrBatchImageSettlementInvalidCounts = infraerrors.New(http.StatusBadRequest, "BATCH_IMAGE_SETTLEMENT_INVALID_COUNTS", "batch image settlement counts are invalid")
- ErrBatchImageSettlementCostExceedsHold = infraerrors.New(http.StatusConflict, "BATCH_IMAGE_SETTLEMENT_COST_EXCEEDS_HOLD", "batch image settlement cost exceeds held balance")
- ErrBatchImageBillingHoldFailed = infraerrors.New(http.StatusBadGateway, "BATCH_IMAGE_BILLING_HOLD_FAILED", "batch image balance hold failed")
- ErrBatchImageInsufficientBalance = infraerrors.New(http.StatusPaymentRequired, "BATCH_IMAGE_INSUFFICIENT_BALANCE", "insufficient balance for batch image hold")
- ErrBatchImageDisabled = infraerrors.New(http.StatusNotFound, "BATCH_IMAGE_DISABLED", "batch image API is disabled")
- ErrBatchImageGroupDisabled = infraerrors.New(http.StatusForbidden, "BATCH_IMAGE_GROUP_DISABLED", "batch image API is disabled for this group")
- ErrBatchImageInvalidModel = infraerrors.New(http.StatusBadRequest, "BATCH_IMAGE_INVALID_MODEL", "batch image model is required")
- ErrBatchImageNoAccountAvailable = infraerrors.New(http.StatusBadGateway, "BATCH_IMAGE_NO_ACCOUNT_AVAILABLE", "no compatible batch image account is available")
- ErrBatchImageInvalidItems = infraerrors.New(http.StatusBadRequest, "BATCH_IMAGE_INVALID_ITEMS", "batch image items are invalid")
- ErrBatchImageDuplicateCustomIDInRequest = infraerrors.New(http.StatusBadRequest, "BATCH_IMAGE_DUPLICATE_CUSTOM_ID", "batch image custom ids must be unique")
- ErrBatchImagePromptTooLong = infraerrors.New(http.StatusBadRequest, "BATCH_IMAGE_PROMPT_TOO_LONG", "batch image prompt is too long")
- ErrBatchImageInvalidReferenceImage = infraerrors.New(http.StatusBadRequest, "BATCH_IMAGE_INVALID_REFERENCE_IMAGE", "batch image reference image is invalid")
- ErrBatchImageTooManyReferenceImages = infraerrors.New(http.StatusBadRequest, "BATCH_IMAGE_TOO_MANY_REFERENCE_IMAGES", "too many batch image reference images for this model")
- ErrBatchImageReferenceImagesTooLarge = infraerrors.New(http.StatusBadRequest, "BATCH_IMAGE_REFERENCE_IMAGES_TOO_LARGE", "batch image reference images are too large")
- ErrBatchImageTooManyOutputImages = infraerrors.New(http.StatusBadRequest, "BATCH_IMAGE_TOO_MANY_OUTPUT_IMAGES", "too many batch image output images")
- ErrBatchImageProviderSubmitFailed = infraerrors.New(http.StatusBadGateway, "BATCH_IMAGE_PROVIDER_SUBMIT_FAILED", "batch image provider submit failed")
- ErrBatchImageQueueFailed = infraerrors.New(http.StatusBadGateway, "BATCH_IMAGE_QUEUE_FAILED", "batch image queue failed")
- ErrBatchImageIdempotencyConflict = infraerrors.New(http.StatusConflict, "BATCH_IMAGE_IDEMPOTENCY_CONFLICT", "idempotency key reused with different batch image request")
- ErrBatchImageCancelFailed = infraerrors.New(http.StatusBadGateway, "BATCH_IMAGE_CANCEL_FAILED", "batch image cancel failed")
- ErrBatchImageVertexGCSBucketMissing = infraerrors.New(http.StatusBadGateway, "BATCH_IMAGE_VERTEX_GCS_BUCKET_MISSING", "Vertex managed GCS bucket is not configured")
- ErrBatchImageNotReady = infraerrors.New(http.StatusConflict, "BATCH_IMAGE_NOT_READY", "batch image job is not completed")
- ErrBatchImageOutputDeleted = infraerrors.New(http.StatusGone, "BATCH_IMAGE_OUTPUT_DELETED", "batch image output has been deleted")
- ErrBatchImageItemNotFound = infraerrors.New(http.StatusNotFound, "BATCH_IMAGE_ITEM_NOT_FOUND", "batch image item not found")
- ErrBatchImageItemFailed = infraerrors.New(http.StatusConflict, "BATCH_IMAGE_ITEM_FAILED", "batch image item did not succeed")
- ErrBatchImageResultMissing = infraerrors.New(http.StatusInternalServerError, "BATCH_IMAGE_RESULT_MISSING", "batch image result is missing")
- ErrBatchImageDownloadLimited = infraerrors.New(http.StatusTooManyRequests, "BATCH_IMAGE_DOWNLOAD_LIMITED", "too many batch image downloads")
- ErrBatchImageDownloadFailed = infraerrors.New(http.StatusInternalServerError, "BATCH_IMAGE_DOWNLOAD_FAILED", "batch image download failed")
- ErrBatchImageDownloadTooLarge = infraerrors.New(http.StatusBadRequest, "BATCH_IMAGE_DOWNLOAD_TOO_LARGE", "batch image download is too large")
- ErrBatchImageItemImageIndexOutOfRange = infraerrors.New(http.StatusBadRequest, "BATCH_IMAGE_ITEM_IMAGE_INDEX_OUT_OF_RANGE", "batch image item image index is out of range")
- ErrBatchImageZipTooManyItems = infraerrors.New(http.StatusBadRequest, "BATCH_IMAGE_ZIP_TOO_MANY_ITEMS", "batch image ZIP contains too many items; use single item downloads")
- ErrBatchImageOutputDeleteNotReady = infraerrors.New(http.StatusConflict, "BATCH_IMAGE_OUTPUT_DELETE_NOT_READY", "batch image output can only be deleted after completion")
- ErrBatchImageRecordDeleteNotReady = infraerrors.New(http.StatusConflict, "BATCH_IMAGE_RECORD_DELETE_NOT_READY", "batch image record can only be deleted after the job finishes")
- ErrBatchImageCleanupFailed = infraerrors.New(http.StatusBadGateway, "BATCH_IMAGE_CLEANUP_FAILED", "batch image cleanup failed")
- ErrBatchImageCleanupUnsafePath = infraerrors.New(http.StatusBadRequest, "BATCH_IMAGE_CLEANUP_UNSAFE_PATH", "batch image cleanup path is unsafe")
- ErrBatchImageProviderCleanupFailed = infraerrors.New(http.StatusBadGateway, "BATCH_IMAGE_PROVIDER_CLEANUP_FAILED", "batch image provider cleanup failed")
- )
- type BatchImageJob struct {
- ID int64
- BatchID string
- UserID int64
- APIKeyID *int64
- AccountID *int64
- Provider string
- Model string
- TaskName string
- ParentBatchID *string
- Status string
- ProviderJobName *string
- ProviderInputRef *string
- ProviderOutputRef *string
- GCSInputURI *string
- GCSOutputURI *string
- ItemCount int
- SuccessCount int
- FailCount int
- CancelledCount int
- EstimatedCost float64
- HoldAmount *float64
- ActualCost *float64
- BaseUnitPrice float64
- GroupRateMultiplier float64
- AccountRateMultiplier float64
- BatchDiscountMultiplier float64
- HoldMultiplier float64
- BillableUnitPrice float64
- HoldUnitPrice float64
- PricingSnapshotVersion int
- Currency string
- HoldID *string
- IdempotencyKey *string
- RequestHash *string
- ManifestHash *string
- SessionID *string
- RetryCount int
- Version int
- OutputExpiresAt *time.Time
- InputDeletedAt *time.Time
- OutputDeletedAt *time.Time
- DownloadedAt *time.Time
- UserDeletedAt *time.Time
- LastErrorCode *string
- LastErrorMessage *string
- CreatedAt time.Time
- UpdatedAt time.Time
- SubmittedAt *time.Time
- StartedAt *time.Time
- FinishedAt *time.Time
- SettledAt *time.Time
- }
- type CreateBatchImageJobParams struct {
- BatchID string
- UserID int64
- APIKeyID *int64
- AccountID *int64
- Provider string
- Model string
- TaskName string
- ParentBatchID *string
- Status string
- ProviderJobName *string
- ProviderInputRef *string
- ProviderOutputRef *string
- GCSInputURI *string
- GCSOutputURI *string
- ItemCount int
- SuccessCount int
- FailCount int
- CancelledCount int
- EstimatedCost float64
- HoldAmount *float64
- ActualCost *float64
- BaseUnitPrice float64
- GroupRateMultiplier float64
- AccountRateMultiplier float64
- BatchDiscountMultiplier float64
- HoldMultiplier float64
- BillableUnitPrice float64
- HoldUnitPrice float64
- PricingSnapshotVersion int
- Currency string
- HoldID *string
- IdempotencyKey *string
- RequestHash *string
- ManifestHash *string
- SessionID *string
- RetryCount int
- OutputExpiresAt *time.Time
- }
- type BatchImageItem struct {
- ID int64
- JobID string
- CustomID string
- Status string
- RequestHash *string
- PromptPreview *string
- ProviderSourceObject *string
- SourceLineNumber *int
- SourceByteOffset *int64
- SourceByteLength *int64
- MimeType *string
- FileExtension *string
- ImageCount int
- ErrorCode *string
- ErrorMessage *string
- BilledAmount *float64
- CreatedAt time.Time
- IndexedAt *time.Time
- }
- type CreateBatchImageItemParams struct {
- JobID string
- CustomID string
- Status string
- RequestHash *string
- PromptPreview *string
- ProviderSourceObject *string
- SourceLineNumber *int
- SourceByteOffset *int64
- SourceByteLength *int64
- MimeType *string
- FileExtension *string
- ImageCount int
- ErrorCode *string
- ErrorMessage *string
- BilledAmount *float64
- IndexedAt *time.Time
- }
- type BatchImageItemFilter struct {
- Status string
- Limit int
- Offset int
- }
- type BatchImageJobFilter struct {
- Status string
- TaskNameLike string
- Downloaded *bool
- CreatedAfter *time.Time
- CreatedBefore *time.Time
- ExcludeDeleted bool
- Limit int
- Offset int
- }
- type BatchImageCounts struct {
- SuccessCount int
- FailCount int
- }
- type UpdateBatchImageJobProviderSubmitParams struct {
- BatchID string
- ProviderJobName string
- ProviderInputRef string
- ProviderOutputRef string
- GCSInputURI string
- GCSOutputURI string
- EventPayload any
- }
- type BatchImageTransitionOptions struct {
- EventType string
- EventPayload any
- ErrorCode *string
- ErrorMessage *string
- Now *time.Time
- }
- type MarkBatchImageJobSettledParams struct {
- BatchID string
- ActualCost float64
- ManifestHash string
- EventPayload any
- Now *time.Time
- OutputExpiresAt *time.Time
- }
- type BatchImageEvent struct {
- ID int64
- JobID string
- EventType string
- Payload []byte
- EventHash *string
- CreatedAt time.Time
- }
- type BatchImageRepository interface {
- CreateBatchImageJob(ctx context.Context, params CreateBatchImageJobParams) (*BatchImageJob, error)
- GetBatchImageJobByBatchID(ctx context.Context, batchID string) (*BatchImageJob, error)
- GetBatchImageJobByIdempotencyKey(ctx context.Context, userID, apiKeyID int64, key string) (*BatchImageJob, error)
- GetBatchImageJobByBatchIDForOwner(ctx context.Context, userID, apiKeyID int64, batchID string) (*BatchImageJob, error)
- GetBatchImageJobByID(ctx context.Context, id int64) (*BatchImageJob, error)
- ListBatchImageJobsForOwner(ctx context.Context, userID, apiKeyID int64, filter BatchImageJobFilter) ([]*BatchImageJob, error)
- TransitionBatchImageJobStatus(ctx context.Context, batchID, toStatus string, opts BatchImageTransitionOptions) error
- // TouchBatchImageJobSubmitting 刷新未提交(created/uploading)job 的 updated_at,
- // 作为慢提交期间的心跳,防止被 stale 恢复扫描误杀。
- TouchBatchImageJobSubmitting(ctx context.Context, batchID string) error
- // FailStaleUnsubmittedBatchImageJob 原子地将仍处于 created/uploading 且
- // provider_job_name 为空、updated_at 早于 cutoff 的 job 转为 failed。
- // 返回 false 表示 job 已被并发推进(如已提交成功),调用方不得释放冻结。
- FailStaleUnsubmittedBatchImageJob(ctx context.Context, batchID string, cutoff time.Time, code, message string) (bool, error)
- UpdateBatchImageJobProviderOutputRef(ctx context.Context, batchID, providerOutputRef string) error
- UpdateBatchImageJobProviderSubmit(ctx context.Context, params UpdateBatchImageJobProviderSubmitParams) error
- RecordBatchImageJobSubmitFailure(ctx context.Context, batchID, code, message string, markFailed bool) error
- MarkBatchImageJobSettled(ctx context.Context, params MarkBatchImageJobSettledParams) error
- SetBatchImageJobSettlementFailed(ctx context.Context, batchID, code, message string) (int, error)
- CreateBatchImageItem(ctx context.Context, params CreateBatchImageItemParams) (*BatchImageItem, error)
- BulkCreateBatchImageItems(ctx context.Context, params []CreateBatchImageItemParams) error
- ReplaceBatchImageItemsForJob(ctx context.Context, batchID string, items []CreateBatchImageItemParams, counts BatchImageCounts) error
- ListBatchImageItems(ctx context.Context, batchID string, filter BatchImageItemFilter) ([]*BatchImageItem, error)
- ListBatchImageItemsForOwner(ctx context.Context, userID, apiKeyID int64, batchID string, filter BatchImageItemFilter) ([]*BatchImageItem, error)
- GetBatchImageJobForDownload(ctx context.Context, userID, apiKeyID int64, batchID string) (*BatchImageJob, error)
- GetBatchImageItemForDownload(ctx context.Context, batchID, customID string) (*BatchImageItem, error)
- ListBatchImageItemsForDownload(ctx context.Context, batchID string, status string, limit int) ([]*BatchImageItem, error)
- ListBatchImageJobsDueForInputCleanup(ctx context.Context, cutoff time.Time, limit int) ([]*BatchImageJob, error)
- ListBatchImageJobsDueForOutputCleanup(ctx context.Context, now time.Time, limit int) ([]*BatchImageJob, error)
- ListStaleUnsubmittedBatchImageJobs(ctx context.Context, cutoff time.Time, limit int) ([]*BatchImageJob, error)
- MarkBatchImageInputDeleted(ctx context.Context, batchID string, deletedAt time.Time) error
- MarkBatchImageOutputDeleted(ctx context.Context, batchID string, deletedAt time.Time) error
- MarkBatchImageDownloaded(ctx context.Context, batchID string, downloadedAt time.Time) error
- MarkBatchImageJobUserDeleted(ctx context.Context, userID, apiKeyID int64, batchID string, deletedAt time.Time) error
- SetBatchImageOutputExpiresAt(ctx context.Context, batchID string, expiresAt time.Time) error
- RecordBatchImageCleanupFailure(ctx context.Context, batchID, code, message string) error
- AppendBatchImageEvent(ctx context.Context, batchID, eventType string, payload any) error
- }
- func NewBatchImageID() (string, error) {
- var b [16]byte
- if _, err := rand.Read(b[:]); err != nil {
- return "", err
- }
- return "imgbatch_" + hex.EncodeToString(b[:]), nil
- }
- func IsSupportedBatchImageProvider(provider string) bool {
- switch provider {
- case BatchImageProviderGeminiAPI, BatchImageProviderVertex:
- return true
- default:
- return false
- }
- }
- func IsTerminalBatchImageJobStatus(status string) bool {
- switch status {
- case BatchImageJobStatusCompleted, BatchImageJobStatusFailed, BatchImageJobStatusCancelled, BatchImageJobStatusOutputDeleted:
- return true
- default:
- return false
- }
- }
- func CanTransitionBatchImageJob(from, to string) bool {
- if from == "" || to == "" {
- return false
- }
- if IsTerminalBatchImageJobStatus(from) {
- return to == BatchImageJobStatusOutputDeleted &&
- from != BatchImageJobStatusOutputDeleted &&
- (from == BatchImageJobStatusCompleted || from == BatchImageJobStatusFailed || from == BatchImageJobStatusCancelled)
- }
- if to == BatchImageJobStatusFailed {
- return true
- }
- allowed := map[string]map[string]struct{}{
- BatchImageJobStatusCreated: {
- BatchImageJobStatusUploading: {},
- BatchImageJobStatusSubmitted: {},
- BatchImageJobStatusCancelled: {},
- },
- BatchImageJobStatusUploading: {
- BatchImageJobStatusSubmitted: {},
- BatchImageJobStatusCancelled: {},
- },
- BatchImageJobStatusSubmitted: {
- BatchImageJobStatusRunning: {},
- BatchImageJobStatusIndexing: {},
- BatchImageJobStatusFailed: {},
- BatchImageJobStatusCancelled: {},
- },
- BatchImageJobStatusRunning: {
- BatchImageJobStatusRunning: {},
- BatchImageJobStatusIndexing: {},
- BatchImageJobStatusFailed: {},
- BatchImageJobStatusCancelled: {},
- },
- BatchImageJobStatusIndexing: {
- BatchImageJobStatusSettling: {},
- BatchImageJobStatusFailed: {},
- },
- BatchImageJobStatusSettling: {
- BatchImageJobStatusCompleted: {},
- },
- }
- _, ok := allowed[from][to]
- return ok
- }
|