返回 AiToEarn
image.consumer.ts
根目录 / project / aitoearn-backend / apps / aitoearn-ai / src / core / ai / image / image.consumer.ts
1 import { OnWorkerEvent, WorkerHost } from '@nestjs/bullmq'
2 import { Logger } from '@nestjs/common'
3 import { QueueName, QueueProcessor } from '@yikart/aitoearn-queue'
4 import { getErrorMessage, getErrorStack, UserType } from '@yikart/common'
5 import { AiLogChannel, AiLogRepository, AiLogStatus, AiLogType } from '@yikart/mongodb'
6 import { Job } from 'bullmq'
7 import { runWithAiGenerationRetry } from '../ai-generation-retry.util'
8 import { ImageEditDto, ImageGenerationDto } from './image.dto'
9 import { ImageService } from './image.service'
10
11 type AsyncImageTaskType = 'generation' | 'edit'
12
13 interface AsyncTaskData {
14 logId: string
15 userId: string
16 userType: UserType
17 model: string
18 channel?: AiLogChannel
19 type: AiLogType
20 retry?: number
21 request: unknown
22 taskType: AsyncImageTaskType
23 }
24
25 @QueueProcessor(QueueName.AiImageAsync, {
26 concurrency: 3,
27 stalledInterval: 15000,
28 maxStalledCount: 1,
29 })
30 export class ImageConsumer extends WorkerHost {
31 private readonly logger = new Logger(ImageConsumer.name)
32
33 constructor(
34 private readonly imageService: ImageService,
35 private readonly aiLogRepo: AiLogRepository,
36 ) {
37 super()
38 }
39
40 /**
41 * 执行单次任务
42 */
43 private async executeTask(taskType: AsyncImageTaskType, request: unknown): Promise<unknown> {
44 switch (taskType) {
45 case 'generation':
46 return await this.imageService.generation(request as ImageGenerationDto)
47 case 'edit':
48 return await this.imageService.edit(request as ImageEditDto)
49 default:
50 throw new Error(`Unknown task type: ${taskType}`)
51 }
52 }
53
54 async process(job: Job<AsyncTaskData>): Promise<unknown> {
55 const { logId, retry, request, taskType } = job.data
56 this.logger.debug(`[log-${logId}] Processing async image task: ${taskType}`)
57
58 const aiLog = await this.aiLogRepo.getById(logId)
59 if (!aiLog || aiLog.status !== AiLogStatus.Generating) {
60 this.logger.warn(`[log-${logId}] Skipping async image task because AiLog is no longer pending`)
61 return undefined
62 }
63
64 const startedAt = new Date()
65 let attemptCount = 0
66
67 try {
68 const result = await runWithAiGenerationRetry(
69 async () => {
70 attemptCount++
71 return await this.executeTask(taskType, request)
72 },
73 retry,
74 (error, attempt, maxAttempts) => {
75 this.logger.warn(
76 `[log-${logId}] Attempt ${attempt} failed: ${getErrorMessage(error)}. Retrying ${attempt + 1}/${maxAttempts}...`,
77 )
78 },
79 )
80
81 const duration = Date.now() - startedAt.getTime()
82
83 // 更新日志为成功状态
84 const updatedAiLog = await this.aiLogRepo.updateByIdAndStatus(
85 logId,
86 AiLogStatus.Generating,
87 {
88 $set: {
89 duration,
90 status: AiLogStatus.Success,
91 response: result as Record<string, unknown>,
92 },
93 },
94 )
95
96 if (!updatedAiLog) {
97 this.logger.warn(`[log-${logId}] Skipping async image result update because AiLog status changed`)
98 return result
99 }
100
101 this.logger.debug(
102 `[log-${logId}] Task completed successfully${attemptCount > 1 ? ` after ${attemptCount} attempts` : ''}`,
103 )
104 return result
105 }
106 catch (error: unknown) {
107 const duration = Date.now() - startedAt.getTime()
108 const errorMessage = getErrorMessage(error)
109
110 // 更新日志为失败状态
111 const updatedAiLog = await this.aiLogRepo.updateByIdAndStatus(logId, AiLogStatus.Generating, {
112 $set: {
113 duration,
114 status: AiLogStatus.Failed,
115 errorMessage: attemptCount > 1
116 ? `${errorMessage} (已重试 ${attemptCount - 1} 次)`
117 : errorMessage,
118 },
119 })
120
121 if (!updatedAiLog) {
122 this.logger.warn(`[log-${logId}] Skipping async image failure update because AiLog status changed`)
123 }
124
125 this.logger.error(
126 `[log-${logId}] Task failed after ${attemptCount} attempts: ${errorMessage}`,
127 getErrorStack(error),
128 )
129 throw error
130 }
131 }
132
133 @OnWorkerEvent('completed')
134 async onCompleted(job: Job<AsyncTaskData>) {
135 const { logId } = job.data
136 this.logger.debug(`[log-${logId}] Job completed successfully`)
137 }
138
139 @OnWorkerEvent('failed')
140 async onFailed(job: Job<AsyncTaskData>, error: Error) {
141 const { logId } = job.data
142 this.logger.error(`[log-${logId}] Job failed: ${error.message}`)
143 }
144 }
145
145 lines TYPESCRIPT