batch_image.go 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416
  1. package service
  2. import (
  3. "context"
  4. "crypto/rand"
  5. "encoding/hex"
  6. "net/http"
  7. "time"
  8. infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
  9. )
  10. const (
  11. BatchImageProviderGeminiAPI = "gemini_api"
  12. BatchImageProviderVertex = "vertex"
  13. )
  14. const (
  15. BatchImageJobStatusCreated = "created"
  16. BatchImageJobStatusUploading = "uploading"
  17. BatchImageJobStatusSubmitted = "submitted"
  18. BatchImageJobStatusRunning = "running"
  19. BatchImageJobStatusIndexing = "indexing"
  20. BatchImageJobStatusSettling = "settling"
  21. BatchImageJobStatusCompleted = "completed"
  22. BatchImageJobStatusFailed = "failed"
  23. BatchImageJobStatusCancelled = "cancelled"
  24. BatchImageJobStatusOutputDeleted = "output_deleted"
  25. )
  26. const (
  27. BatchImageItemStatusPending = "pending"
  28. BatchImageItemStatusSuccess = "success"
  29. BatchImageItemStatusFailed = "failed"
  30. BatchImageItemStatusCancelled = "cancelled"
  31. )
  32. var (
  33. ErrBatchImageJobNotFound = infraerrors.New(http.StatusNotFound, "BATCH_IMAGE_JOB_NOT_FOUND", "batch image job not found")
  34. ErrBatchImageJobExists = infraerrors.New(http.StatusConflict, "BATCH_IMAGE_JOB_EXISTS", "batch image job already exists")
  35. ErrBatchImageItemExists = infraerrors.New(http.StatusConflict, "BATCH_IMAGE_ITEM_EXISTS", "batch image item already exists")
  36. ErrBatchImageInvalidTransition = infraerrors.New(http.StatusBadRequest, "BATCH_IMAGE_INVALID_TRANSITION", "invalid batch image job status transition")
  37. ErrBatchImageInvalidProvider = infraerrors.New(http.StatusBadRequest, "BATCH_IMAGE_INVALID_PROVIDER", "invalid batch image provider")
  38. ErrBatchImageMissingProviderJobName = infraerrors.New(http.StatusBadRequest, "BATCH_IMAGE_MISSING_PROVIDER_JOB_NAME", "batch image provider job name is missing")
  39. ErrBatchImageMissingAccountID = infraerrors.New(http.StatusBadRequest, "BATCH_IMAGE_MISSING_ACCOUNT_ID", "batch image account id is missing")
  40. ErrBatchImageUnsupportedProvider = infraerrors.New(http.StatusBadRequest, "BATCH_IMAGE_UNSUPPORTED_PROVIDER", "unsupported batch image provider")
  41. ErrBatchImageIndexOutputMissing = infraerrors.New(http.StatusBadGateway, "BATCH_IMAGE_INDEX_OUTPUT_MISSING", "batch image provider output is missing")
  42. ErrBatchImageIndexParseFailed = infraerrors.New(http.StatusBadGateway, "BATCH_IMAGE_INDEX_PARSE_FAILED", "batch image provider output parse failed")
  43. ErrBatchImageIndexNoResultLines = infraerrors.New(http.StatusBadGateway, "BATCH_IMAGE_INDEX_NO_RESULT_LINES", "batch image provider output has no result lines")
  44. ErrBatchImageDuplicateCustomID = infraerrors.New(http.StatusBadGateway, "DUPLICATE_CUSTOM_ID_IN_OUTPUT", "batch image provider output contains duplicate custom id")
  45. ErrBatchImageIndexStateConflict = infraerrors.New(http.StatusConflict, "BATCH_IMAGE_INDEX_STATE_CONFLICT", "batch image job is no longer in indexing state")
  46. ErrBatchImageSettlementInvalidStatus = infraerrors.New(http.StatusBadRequest, "BATCH_IMAGE_SETTLEMENT_INVALID_STATUS", "batch image job is not ready for settlement")
  47. ErrBatchImageSettlementManifestConflict = infraerrors.New(http.StatusConflict, "BATCH_IMAGE_SETTLEMENT_MANIFEST_CONFLICT", "batch image settlement manifest hash conflict")
  48. ErrBatchImageSettlementPricingMissing = infraerrors.New(http.StatusBadRequest, "BATCH_IMAGE_SETTLEMENT_PRICING_MISSING", "batch image settlement pricing is missing")
  49. ErrBatchImageSettlementBillingFailed = infraerrors.New(http.StatusBadGateway, "BATCH_IMAGE_SETTLEMENT_BILLING_FAILED", "batch image settlement billing failed")
  50. ErrBatchImageAlreadySettled = infraerrors.New(http.StatusConflict, "BATCH_IMAGE_ALREADY_SETTLED", "batch image job is already settled")
  51. ErrBatchImageSettlementMissingAPIKeyID = infraerrors.New(http.StatusBadRequest, "BATCH_IMAGE_SETTLEMENT_MISSING_API_KEY_ID", "batch image settlement api key id is missing")
  52. ErrBatchImageSettlementMissingAccountID = infraerrors.New(http.StatusBadRequest, "BATCH_IMAGE_SETTLEMENT_MISSING_ACCOUNT_ID", "batch image settlement account id is missing")
  53. ErrBatchImageSettlementInvalidCounts = infraerrors.New(http.StatusBadRequest, "BATCH_IMAGE_SETTLEMENT_INVALID_COUNTS", "batch image settlement counts are invalid")
  54. ErrBatchImageSettlementCostExceedsHold = infraerrors.New(http.StatusConflict, "BATCH_IMAGE_SETTLEMENT_COST_EXCEEDS_HOLD", "batch image settlement cost exceeds held balance")
  55. ErrBatchImageBillingHoldFailed = infraerrors.New(http.StatusBadGateway, "BATCH_IMAGE_BILLING_HOLD_FAILED", "batch image balance hold failed")
  56. ErrBatchImageInsufficientBalance = infraerrors.New(http.StatusPaymentRequired, "BATCH_IMAGE_INSUFFICIENT_BALANCE", "insufficient balance for batch image hold")
  57. ErrBatchImageDisabled = infraerrors.New(http.StatusNotFound, "BATCH_IMAGE_DISABLED", "batch image API is disabled")
  58. ErrBatchImageGroupDisabled = infraerrors.New(http.StatusForbidden, "BATCH_IMAGE_GROUP_DISABLED", "batch image API is disabled for this group")
  59. ErrBatchImageInvalidModel = infraerrors.New(http.StatusBadRequest, "BATCH_IMAGE_INVALID_MODEL", "batch image model is required")
  60. ErrBatchImageNoAccountAvailable = infraerrors.New(http.StatusBadGateway, "BATCH_IMAGE_NO_ACCOUNT_AVAILABLE", "no compatible batch image account is available")
  61. ErrBatchImageInvalidItems = infraerrors.New(http.StatusBadRequest, "BATCH_IMAGE_INVALID_ITEMS", "batch image items are invalid")
  62. ErrBatchImageDuplicateCustomIDInRequest = infraerrors.New(http.StatusBadRequest, "BATCH_IMAGE_DUPLICATE_CUSTOM_ID", "batch image custom ids must be unique")
  63. ErrBatchImagePromptTooLong = infraerrors.New(http.StatusBadRequest, "BATCH_IMAGE_PROMPT_TOO_LONG", "batch image prompt is too long")
  64. ErrBatchImageInvalidReferenceImage = infraerrors.New(http.StatusBadRequest, "BATCH_IMAGE_INVALID_REFERENCE_IMAGE", "batch image reference image is invalid")
  65. ErrBatchImageTooManyReferenceImages = infraerrors.New(http.StatusBadRequest, "BATCH_IMAGE_TOO_MANY_REFERENCE_IMAGES", "too many batch image reference images for this model")
  66. ErrBatchImageReferenceImagesTooLarge = infraerrors.New(http.StatusBadRequest, "BATCH_IMAGE_REFERENCE_IMAGES_TOO_LARGE", "batch image reference images are too large")
  67. ErrBatchImageTooManyOutputImages = infraerrors.New(http.StatusBadRequest, "BATCH_IMAGE_TOO_MANY_OUTPUT_IMAGES", "too many batch image output images")
  68. ErrBatchImageProviderSubmitFailed = infraerrors.New(http.StatusBadGateway, "BATCH_IMAGE_PROVIDER_SUBMIT_FAILED", "batch image provider submit failed")
  69. ErrBatchImageQueueFailed = infraerrors.New(http.StatusBadGateway, "BATCH_IMAGE_QUEUE_FAILED", "batch image queue failed")
  70. ErrBatchImageIdempotencyConflict = infraerrors.New(http.StatusConflict, "BATCH_IMAGE_IDEMPOTENCY_CONFLICT", "idempotency key reused with different batch image request")
  71. ErrBatchImageCancelFailed = infraerrors.New(http.StatusBadGateway, "BATCH_IMAGE_CANCEL_FAILED", "batch image cancel failed")
  72. ErrBatchImageVertexGCSBucketMissing = infraerrors.New(http.StatusBadGateway, "BATCH_IMAGE_VERTEX_GCS_BUCKET_MISSING", "Vertex managed GCS bucket is not configured")
  73. ErrBatchImageNotReady = infraerrors.New(http.StatusConflict, "BATCH_IMAGE_NOT_READY", "batch image job is not completed")
  74. ErrBatchImageOutputDeleted = infraerrors.New(http.StatusGone, "BATCH_IMAGE_OUTPUT_DELETED", "batch image output has been deleted")
  75. ErrBatchImageItemNotFound = infraerrors.New(http.StatusNotFound, "BATCH_IMAGE_ITEM_NOT_FOUND", "batch image item not found")
  76. ErrBatchImageItemFailed = infraerrors.New(http.StatusConflict, "BATCH_IMAGE_ITEM_FAILED", "batch image item did not succeed")
  77. ErrBatchImageResultMissing = infraerrors.New(http.StatusInternalServerError, "BATCH_IMAGE_RESULT_MISSING", "batch image result is missing")
  78. ErrBatchImageDownloadLimited = infraerrors.New(http.StatusTooManyRequests, "BATCH_IMAGE_DOWNLOAD_LIMITED", "too many batch image downloads")
  79. ErrBatchImageDownloadFailed = infraerrors.New(http.StatusInternalServerError, "BATCH_IMAGE_DOWNLOAD_FAILED", "batch image download failed")
  80. ErrBatchImageDownloadTooLarge = infraerrors.New(http.StatusBadRequest, "BATCH_IMAGE_DOWNLOAD_TOO_LARGE", "batch image download is too large")
  81. ErrBatchImageItemImageIndexOutOfRange = infraerrors.New(http.StatusBadRequest, "BATCH_IMAGE_ITEM_IMAGE_INDEX_OUT_OF_RANGE", "batch image item image index is out of range")
  82. ErrBatchImageZipTooManyItems = infraerrors.New(http.StatusBadRequest, "BATCH_IMAGE_ZIP_TOO_MANY_ITEMS", "batch image ZIP contains too many items; use single item downloads")
  83. ErrBatchImageOutputDeleteNotReady = infraerrors.New(http.StatusConflict, "BATCH_IMAGE_OUTPUT_DELETE_NOT_READY", "batch image output can only be deleted after completion")
  84. ErrBatchImageRecordDeleteNotReady = infraerrors.New(http.StatusConflict, "BATCH_IMAGE_RECORD_DELETE_NOT_READY", "batch image record can only be deleted after the job finishes")
  85. ErrBatchImageCleanupFailed = infraerrors.New(http.StatusBadGateway, "BATCH_IMAGE_CLEANUP_FAILED", "batch image cleanup failed")
  86. ErrBatchImageCleanupUnsafePath = infraerrors.New(http.StatusBadRequest, "BATCH_IMAGE_CLEANUP_UNSAFE_PATH", "batch image cleanup path is unsafe")
  87. ErrBatchImageProviderCleanupFailed = infraerrors.New(http.StatusBadGateway, "BATCH_IMAGE_PROVIDER_CLEANUP_FAILED", "batch image provider cleanup failed")
  88. )
  89. type BatchImageJob struct {
  90. ID int64
  91. BatchID string
  92. UserID int64
  93. APIKeyID *int64
  94. AccountID *int64
  95. Provider string
  96. Model string
  97. TaskName string
  98. ParentBatchID *string
  99. Status string
  100. ProviderJobName *string
  101. ProviderInputRef *string
  102. ProviderOutputRef *string
  103. GCSInputURI *string
  104. GCSOutputURI *string
  105. ItemCount int
  106. SuccessCount int
  107. FailCount int
  108. CancelledCount int
  109. EstimatedCost float64
  110. HoldAmount *float64
  111. ActualCost *float64
  112. BaseUnitPrice float64
  113. GroupRateMultiplier float64
  114. AccountRateMultiplier float64
  115. BatchDiscountMultiplier float64
  116. HoldMultiplier float64
  117. BillableUnitPrice float64
  118. HoldUnitPrice float64
  119. PricingSnapshotVersion int
  120. Currency string
  121. HoldID *string
  122. IdempotencyKey *string
  123. RequestHash *string
  124. ManifestHash *string
  125. SessionID *string
  126. RetryCount int
  127. Version int
  128. OutputExpiresAt *time.Time
  129. InputDeletedAt *time.Time
  130. OutputDeletedAt *time.Time
  131. DownloadedAt *time.Time
  132. UserDeletedAt *time.Time
  133. LastErrorCode *string
  134. LastErrorMessage *string
  135. CreatedAt time.Time
  136. UpdatedAt time.Time
  137. SubmittedAt *time.Time
  138. StartedAt *time.Time
  139. FinishedAt *time.Time
  140. SettledAt *time.Time
  141. }
  142. type CreateBatchImageJobParams struct {
  143. BatchID string
  144. UserID int64
  145. APIKeyID *int64
  146. AccountID *int64
  147. Provider string
  148. Model string
  149. TaskName string
  150. ParentBatchID *string
  151. Status string
  152. ProviderJobName *string
  153. ProviderInputRef *string
  154. ProviderOutputRef *string
  155. GCSInputURI *string
  156. GCSOutputURI *string
  157. ItemCount int
  158. SuccessCount int
  159. FailCount int
  160. CancelledCount int
  161. EstimatedCost float64
  162. HoldAmount *float64
  163. ActualCost *float64
  164. BaseUnitPrice float64
  165. GroupRateMultiplier float64
  166. AccountRateMultiplier float64
  167. BatchDiscountMultiplier float64
  168. HoldMultiplier float64
  169. BillableUnitPrice float64
  170. HoldUnitPrice float64
  171. PricingSnapshotVersion int
  172. Currency string
  173. HoldID *string
  174. IdempotencyKey *string
  175. RequestHash *string
  176. ManifestHash *string
  177. SessionID *string
  178. RetryCount int
  179. OutputExpiresAt *time.Time
  180. }
  181. type BatchImageItem struct {
  182. ID int64
  183. JobID string
  184. CustomID string
  185. Status string
  186. RequestHash *string
  187. PromptPreview *string
  188. ProviderSourceObject *string
  189. SourceLineNumber *int
  190. SourceByteOffset *int64
  191. SourceByteLength *int64
  192. MimeType *string
  193. FileExtension *string
  194. ImageCount int
  195. ErrorCode *string
  196. ErrorMessage *string
  197. BilledAmount *float64
  198. CreatedAt time.Time
  199. IndexedAt *time.Time
  200. }
  201. type CreateBatchImageItemParams struct {
  202. JobID string
  203. CustomID string
  204. Status string
  205. RequestHash *string
  206. PromptPreview *string
  207. ProviderSourceObject *string
  208. SourceLineNumber *int
  209. SourceByteOffset *int64
  210. SourceByteLength *int64
  211. MimeType *string
  212. FileExtension *string
  213. ImageCount int
  214. ErrorCode *string
  215. ErrorMessage *string
  216. BilledAmount *float64
  217. IndexedAt *time.Time
  218. }
  219. type BatchImageItemFilter struct {
  220. Status string
  221. Limit int
  222. Offset int
  223. }
  224. type BatchImageJobFilter struct {
  225. Status string
  226. TaskNameLike string
  227. Downloaded *bool
  228. CreatedAfter *time.Time
  229. CreatedBefore *time.Time
  230. ExcludeDeleted bool
  231. Limit int
  232. Offset int
  233. }
  234. type BatchImageCounts struct {
  235. SuccessCount int
  236. FailCount int
  237. }
  238. type UpdateBatchImageJobProviderSubmitParams struct {
  239. BatchID string
  240. ProviderJobName string
  241. ProviderInputRef string
  242. ProviderOutputRef string
  243. GCSInputURI string
  244. GCSOutputURI string
  245. EventPayload any
  246. }
  247. type BatchImageTransitionOptions struct {
  248. EventType string
  249. EventPayload any
  250. ErrorCode *string
  251. ErrorMessage *string
  252. Now *time.Time
  253. }
  254. type MarkBatchImageJobSettledParams struct {
  255. BatchID string
  256. ActualCost float64
  257. ManifestHash string
  258. EventPayload any
  259. Now *time.Time
  260. OutputExpiresAt *time.Time
  261. }
  262. type BatchImageEvent struct {
  263. ID int64
  264. JobID string
  265. EventType string
  266. Payload []byte
  267. EventHash *string
  268. CreatedAt time.Time
  269. }
  270. type BatchImageRepository interface {
  271. CreateBatchImageJob(ctx context.Context, params CreateBatchImageJobParams) (*BatchImageJob, error)
  272. GetBatchImageJobByBatchID(ctx context.Context, batchID string) (*BatchImageJob, error)
  273. GetBatchImageJobByIdempotencyKey(ctx context.Context, userID, apiKeyID int64, key string) (*BatchImageJob, error)
  274. GetBatchImageJobByBatchIDForOwner(ctx context.Context, userID, apiKeyID int64, batchID string) (*BatchImageJob, error)
  275. GetBatchImageJobByID(ctx context.Context, id int64) (*BatchImageJob, error)
  276. ListBatchImageJobsForOwner(ctx context.Context, userID, apiKeyID int64, filter BatchImageJobFilter) ([]*BatchImageJob, error)
  277. TransitionBatchImageJobStatus(ctx context.Context, batchID, toStatus string, opts BatchImageTransitionOptions) error
  278. // TouchBatchImageJobSubmitting 刷新未提交(created/uploading)job 的 updated_at,
  279. // 作为慢提交期间的心跳,防止被 stale 恢复扫描误杀。
  280. TouchBatchImageJobSubmitting(ctx context.Context, batchID string) error
  281. // FailStaleUnsubmittedBatchImageJob 原子地将仍处于 created/uploading 且
  282. // provider_job_name 为空、updated_at 早于 cutoff 的 job 转为 failed。
  283. // 返回 false 表示 job 已被并发推进(如已提交成功),调用方不得释放冻结。
  284. FailStaleUnsubmittedBatchImageJob(ctx context.Context, batchID string, cutoff time.Time, code, message string) (bool, error)
  285. UpdateBatchImageJobProviderOutputRef(ctx context.Context, batchID, providerOutputRef string) error
  286. UpdateBatchImageJobProviderSubmit(ctx context.Context, params UpdateBatchImageJobProviderSubmitParams) error
  287. RecordBatchImageJobSubmitFailure(ctx context.Context, batchID, code, message string, markFailed bool) error
  288. MarkBatchImageJobSettled(ctx context.Context, params MarkBatchImageJobSettledParams) error
  289. SetBatchImageJobSettlementFailed(ctx context.Context, batchID, code, message string) (int, error)
  290. CreateBatchImageItem(ctx context.Context, params CreateBatchImageItemParams) (*BatchImageItem, error)
  291. BulkCreateBatchImageItems(ctx context.Context, params []CreateBatchImageItemParams) error
  292. ReplaceBatchImageItemsForJob(ctx context.Context, batchID string, items []CreateBatchImageItemParams, counts BatchImageCounts) error
  293. ListBatchImageItems(ctx context.Context, batchID string, filter BatchImageItemFilter) ([]*BatchImageItem, error)
  294. ListBatchImageItemsForOwner(ctx context.Context, userID, apiKeyID int64, batchID string, filter BatchImageItemFilter) ([]*BatchImageItem, error)
  295. GetBatchImageJobForDownload(ctx context.Context, userID, apiKeyID int64, batchID string) (*BatchImageJob, error)
  296. GetBatchImageItemForDownload(ctx context.Context, batchID, customID string) (*BatchImageItem, error)
  297. ListBatchImageItemsForDownload(ctx context.Context, batchID string, status string, limit int) ([]*BatchImageItem, error)
  298. ListBatchImageJobsDueForInputCleanup(ctx context.Context, cutoff time.Time, limit int) ([]*BatchImageJob, error)
  299. ListBatchImageJobsDueForOutputCleanup(ctx context.Context, now time.Time, limit int) ([]*BatchImageJob, error)
  300. ListStaleUnsubmittedBatchImageJobs(ctx context.Context, cutoff time.Time, limit int) ([]*BatchImageJob, error)
  301. MarkBatchImageInputDeleted(ctx context.Context, batchID string, deletedAt time.Time) error
  302. MarkBatchImageOutputDeleted(ctx context.Context, batchID string, deletedAt time.Time) error
  303. MarkBatchImageDownloaded(ctx context.Context, batchID string, downloadedAt time.Time) error
  304. MarkBatchImageJobUserDeleted(ctx context.Context, userID, apiKeyID int64, batchID string, deletedAt time.Time) error
  305. SetBatchImageOutputExpiresAt(ctx context.Context, batchID string, expiresAt time.Time) error
  306. RecordBatchImageCleanupFailure(ctx context.Context, batchID, code, message string) error
  307. AppendBatchImageEvent(ctx context.Context, batchID, eventType string, payload any) error
  308. }
  309. func NewBatchImageID() (string, error) {
  310. var b [16]byte
  311. if _, err := rand.Read(b[:]); err != nil {
  312. return "", err
  313. }
  314. return "imgbatch_" + hex.EncodeToString(b[:]), nil
  315. }
  316. func IsSupportedBatchImageProvider(provider string) bool {
  317. switch provider {
  318. case BatchImageProviderGeminiAPI, BatchImageProviderVertex:
  319. return true
  320. default:
  321. return false
  322. }
  323. }
  324. func IsTerminalBatchImageJobStatus(status string) bool {
  325. switch status {
  326. case BatchImageJobStatusCompleted, BatchImageJobStatusFailed, BatchImageJobStatusCancelled, BatchImageJobStatusOutputDeleted:
  327. return true
  328. default:
  329. return false
  330. }
  331. }
  332. func CanTransitionBatchImageJob(from, to string) bool {
  333. if from == "" || to == "" {
  334. return false
  335. }
  336. if IsTerminalBatchImageJobStatus(from) {
  337. return to == BatchImageJobStatusOutputDeleted &&
  338. from != BatchImageJobStatusOutputDeleted &&
  339. (from == BatchImageJobStatusCompleted || from == BatchImageJobStatusFailed || from == BatchImageJobStatusCancelled)
  340. }
  341. if to == BatchImageJobStatusFailed {
  342. return true
  343. }
  344. allowed := map[string]map[string]struct{}{
  345. BatchImageJobStatusCreated: {
  346. BatchImageJobStatusUploading: {},
  347. BatchImageJobStatusSubmitted: {},
  348. BatchImageJobStatusCancelled: {},
  349. },
  350. BatchImageJobStatusUploading: {
  351. BatchImageJobStatusSubmitted: {},
  352. BatchImageJobStatusCancelled: {},
  353. },
  354. BatchImageJobStatusSubmitted: {
  355. BatchImageJobStatusRunning: {},
  356. BatchImageJobStatusIndexing: {},
  357. BatchImageJobStatusFailed: {},
  358. BatchImageJobStatusCancelled: {},
  359. },
  360. BatchImageJobStatusRunning: {
  361. BatchImageJobStatusRunning: {},
  362. BatchImageJobStatusIndexing: {},
  363. BatchImageJobStatusFailed: {},
  364. BatchImageJobStatusCancelled: {},
  365. },
  366. BatchImageJobStatusIndexing: {
  367. BatchImageJobStatusSettling: {},
  368. BatchImageJobStatusFailed: {},
  369. },
  370. BatchImageJobStatusSettling: {
  371. BatchImageJobStatusCompleted: {},
  372. },
  373. }
  374. _, ok := allowed[from][to]
  375. return ok
  376. }