| 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 |