| 1 | import { HttpException } from '@nestjs/common' |
| 2 | import { AppException, getErrorMessage, ResponseCode } from '@yikart/common' |
| 3 | import { AxiosError } from 'axios' |
| 4 | |
| 5 | export function isNonRetryableAiRequestError(error: unknown): boolean { |
| 6 | const message = getErrorMessage(error) |
| 7 | if (isContentSafetyMessage(message)) { |
| 8 | return false |
| 9 | } |
| 10 | |
| 11 | if (error instanceof AppException) { |
| 12 | return [ |
| 13 | ResponseCode.InvalidModel, |
| 14 | ResponseCode.InvalidAiTaskId, |
| 15 | ResponseCode.VideoUploadInvalidInput, |
| 16 | ResponseCode.AiLogNotFound, |
| 17 | ].includes(error.code) |
| 18 | } |
| 19 | |
| 20 | const status = getHttpStatus(error) |
| 21 | if (status != null && [400, 401, 403, 404, 422].includes(status)) { |
| 22 | return true |
| 23 | } |
| 24 | |
| 25 | const lowerMessage = message.toLowerCase() |
| 26 | return [ |
| 27 | 'invalid parameter', |
| 28 | 'missing required', |
| 29 | 'bad request', |
| 30 | 'unauthorized', |
| 31 | 'forbidden', |
| 32 | 'not found', |
| 33 | 'insufficient balance', |
| 34 | 'unsupported', |
| 35 | 'model not found', |
| 36 | 'image size is not supported', |
| 37 | 'invalid image', |
| 38 | 'invalid input', |
| 39 | ].some(pattern => lowerMessage.includes(pattern)) |
| 40 | } |
| 41 | |
| 42 | export async function runWithAiGenerationRetry<T>( |
| 43 | run: () => Promise<T>, |
| 44 | retry: number | undefined, |
| 45 | onRetry?: (error: unknown, attempt: number, maxAttempts: number) => void, |
| 46 | ): Promise<T> { |
| 47 | const maxAttempts = 1 + Math.max(0, retry ?? 0) |
| 48 | let lastError: unknown |
| 49 | |
| 50 | for (let attempt = 1; attempt <= maxAttempts; attempt++) { |
| 51 | try { |
| 52 | return await run() |
| 53 | } |
| 54 | catch (error) { |
| 55 | lastError = error |
| 56 | if (attempt >= maxAttempts || isNonRetryableAiRequestError(error)) { |
| 57 | throw error |
| 58 | } |
| 59 | onRetry?.(error, attempt, maxAttempts) |
| 60 | } |
| 61 | } |
| 62 | |
| 63 | throw lastError |
| 64 | } |
| 65 | |
| 66 | function getHttpStatus(error: unknown): number | undefined { |
| 67 | if (error instanceof HttpException) { |
| 68 | return error.getStatus() |
| 69 | } |
| 70 | if (error instanceof AxiosError || looksLikeAxiosError(error)) { |
| 71 | return (error as AxiosError).response?.status |
| 72 | } |
| 73 | return undefined |
| 74 | } |
| 75 | |
| 76 | function looksLikeAxiosError(error: unknown): boolean { |
| 77 | return typeof error === 'object' && error != null && 'isAxiosError' in error |
| 78 | } |
| 79 | |
| 80 | function isContentSafetyMessage(message: string): boolean { |
| 81 | const lowerMessage = message.toLowerCase() |
| 82 | return [ |
| 83 | 'safety', |
| 84 | 'policy', |
| 85 | 'content filter', |
| 86 | 'risk', |
| 87 | 'violation', |
| 88 | 'sensitive', |
| 89 | ].some(pattern => lowerMessage.includes(pattern)) |
| 90 | || ['违规', '风控', '内容安全'].some(pattern => message.includes(pattern)) |
| 91 | } |
| 92 |