返回 AiToEarn
video-task-status.scheduler.ts
根目录 / project / aitoearn-backend / apps / aitoearn-ai / src / core / ai / video / video-task-status.scheduler.ts
1 import type { VideoAiLogByChannel } from './video-ai-log.interface'
2 import { Injectable, Logger, Optional } from '@nestjs/common'
3 import { Cron, CronExpression } from '@nestjs/schedule'
4 import { getErrorMessage, WithLoggerContext } from '@yikart/common'
5 import { AiLog, AiLogChannel, AiLogRepository, AiLogType } from '@yikart/mongodb'
6 import { Redlock } from '@yikart/redlock'
7 import { AxiosError } from 'axios'
8 import { RedlockKey } from '../../../common'
9 import { isNonRetryableAiRequestError } from '../ai-generation-retry.util'
10 import { DashscopeService as DashscopeLibService } from '../libs/dashscope'
11 import { GrokLibService, GrokVideoTaskStatus } from '../libs/grok'
12 import { OpenaiService } from '../libs/openai'
13 import { RelayLibService } from '../libs/relay'
14 import { VolcengineService } from '../libs/volcengine'
15 import { DashscopeVideoService } from './dashscope'
16 import { GrokVideoService } from './grok'
17 import { OpenAIVideoService } from './openai'
18 import { RelayVideoService } from './relay/relay-video.service'
19 import { VideoService } from './video.service'
20 import { VolcengineVideoService } from './volcengine/volcengine.service'
21
22 @Injectable()
23 export class VideoTaskStatusScheduler {
24 private readonly logger = new Logger(VideoTaskStatusScheduler.name)
25
26 constructor(
27 private readonly aiLogRepo: AiLogRepository,
28 private readonly videoService: VideoService,
29 private readonly volcengineVideoService: VolcengineVideoService,
30 private readonly openaiVideoService: OpenAIVideoService,
31 private readonly volcengineLibService: VolcengineService,
32 private readonly openaiLibService: OpenaiService,
33 private readonly grokLibService: GrokLibService,
34 private readonly grokVideoService: GrokVideoService,
35 private readonly dashscopeLibService: DashscopeLibService,
36 private readonly dashscopeVideoService: DashscopeVideoService,
37 @Optional() private readonly relayLibService?: RelayLibService,
38 @Optional() private readonly relayVideoService?: RelayVideoService,
39 ) { }
40
41 /**
42 * 每30秒检查一次正在生成中的视频任务状态
43 */
44 @Cron(CronExpression.EVERY_30_SECONDS)
45 @Redlock(RedlockKey.VideoTaskStatusCheck, 600, { throwOnFailure: false })
46 @WithLoggerContext()
47 async processVideoTaskStatus() {
48 this.logger.debug('开始检查视频生成任务状态')
49
50 const generatingTasks = await this.aiLogRepo.listGeneratingByType(AiLogType.Video)
51
52 if (generatingTasks.length === 0) {
53 return
54 }
55
56 this.logger.debug(`找到 ${generatingTasks.length} 个正在生成中的视频任务`)
57
58 for (const task of generatingTasks) {
59 await this.processTask(task)
60 }
61 }
62
63 /**
64 * 处理单个任务
65 */
66 @Redlock(task => `${RedlockKey.VideoTaskStatusCheck}:${(task as AiLog).id}`, 60, { throwOnFailure: false })
67 private async processTask(task: AiLog) {
68 const taskId = task.taskId!
69 const channel = task.channel
70
71 if (channel === AiLogChannel.Volcengine) {
72 const result = await this.volcengineLibService.getVideoGenerationTask(taskId)
73 await this.volcengineVideoService.callback(result)
74 }
75 else if (channel === AiLogChannel.OpenAI) {
76 const result = await this.openaiLibService.retrieveVideo(taskId)
77 await this.openaiVideoService.callback(result)
78 }
79 else if (channel === AiLogChannel.Grok) {
80 const grokTask = task as VideoAiLogByChannel<AiLogChannel.Grok>
81 try {
82 const result = await this.grokLibService.getVideoStatus(taskId)
83 await this.grokVideoService.callback(result, grokTask)
84 }
85 catch (e) {
86 if (!isNonRetryableAiRequestError(e)) {
87 this.logger.warn(
88 { error: e, taskId, aiLogId: task.id },
89 'Grok video status query failed, waiting for next poll',
90 )
91 return
92 }
93 let errorMessage = getErrorMessage(e)
94 let code = '500'
95 if (e instanceof AxiosError) {
96 const status = e?.response?.status
97 if (status && status >= 400 && status < 500) {
98 const data = e.response?.data
99 errorMessage = data?.error || data?.code || `Grok API error (${status})`
100 code = data?.code || `HTTP_${status}`
101 }
102 }
103 await this.grokVideoService.callback({
104 status: GrokVideoTaskStatus.Failed,
105 error: { code, message: errorMessage },
106 }, grokTask)
107 }
108 }
109 else if (channel === AiLogChannel.Dashscope) {
110 const result = await this.dashscopeLibService.getVideoTask(taskId)
111 await this.dashscopeVideoService.callback(result)
112 }
113 else if (channel === AiLogChannel.Relay) {
114 if (!this.relayLibService || !this.relayVideoService) {
115 return this.skipUnregistered(task, channel)
116 }
117 const result = await this.relayLibService.getVideo(taskId)
118 await this.relayVideoService.callback(result)
119 }
120 else {
121 this.logger.warn(`任务 ${task.id} 未知的 channel: ${channel},跳过检查`)
122 return
123 }
124
125 await this.videoService.ensureSavedMediaByAiLogId(task.id)
126 }
127
128 private skipUnregistered(task: AiLog, channel: AiLogChannel): void {
129 this.logger.warn(`任务 ${task.id} 的 channel ${channel} 未注册(配置缺失),跳过检查`)
130 }
131 }
132
132 lines TYPESCRIPT