返回 AiToEarn
relay.service.ts
根目录 / project / aitoearn-backend / apps / aitoearn-ai / src / core / ai / libs / relay / relay.service.ts
1 import { Injectable } from '@nestjs/common'
2 import { AppException, ResponseCode } from '@yikart/common'
3 import axios, { AxiosError, AxiosInstance, AxiosResponse } from 'axios'
4 import { AiAvailabilityService } from '../../../ai-availability'
5 import { RelayConfig } from './relay.config'
6 import { RelayVideoCallbackDto, RelayVideoGenerationRequest, RelayVideoSubmitResponse } from './relay.interface'
7
8 interface RelayCommonResponse<T> {
9 code?: number
10 message?: string
11 data?: T
12 }
13
14 @Injectable()
15 export class RelayLibService {
16 private readonly httpClient: AxiosInstance
17
18 constructor(
19 private readonly config: RelayConfig,
20 private readonly aiAvailability: AiAvailabilityService,
21 ) {
22 this.httpClient = axios.create({
23 baseURL: this.config.url,
24 timeout: this.config.timeout,
25 headers: {
26 'Content-Type': 'application/json',
27 'Accept': 'application/json',
28 'x-api-key': this.config.apiKey,
29 },
30 })
31
32 this.httpClient.interceptors.response.use(
33 response => this.normalizeResponse(response),
34 (error: AxiosError) => Promise.reject(this.normalizeError(error)),
35 )
36 }
37
38 private normalizeError(error: AxiosError): AxiosError {
39 const status = error.response?.status
40 const data = error.response?.data as Record<string, unknown> | undefined
41 const message = (data?.['message'] as string) || (data?.['error'] as string) || error.message
42 error.message = message
43 error.name = status ? `RelayApiError(${status})` : 'RelayApiError'
44 return error
45 }
46
47 private normalizeResponse<T>(response: AxiosResponse<RelayCommonResponse<T> | T>): AxiosResponse<T> {
48 response.data = this.unwrapResponse(response.data)
49 return response as AxiosResponse<T>
50 }
51
52 private unwrapResponse<T>(body: RelayCommonResponse<T> | T): T {
53 if (!this.isCommonResponse(body)) {
54 return body as T
55 }
56
57 if (body.code != null && body.code !== ResponseCode.Success) {
58 throw new AppException(body.code, body.message ?? 'Relay API request failed')
59 }
60
61 return body.data as T
62 }
63
64 private isCommonResponse<T>(body: RelayCommonResponse<T> | T): body is RelayCommonResponse<T> {
65 return typeof body === 'object'
66 && body !== null
67 && 'data' in body
68 }
69
70 private stringifyForError(value: unknown): string {
71 try {
72 return JSON.stringify(value)
73 }
74 catch {
75 return String(value)
76 }
77 }
78
79 /**
80 * 提交视频生成任务到上游 relay 服务端
81 * 对应上游 POST /ai/video/generations
82 */
83 async createVideo(request: RelayVideoGenerationRequest): Promise<RelayVideoSubmitResponse> {
84 return this.aiAvailability.execute(
85 { provider: 'relay', operation: 'createVideo', model: request.model },
86 async () => {
87 const response: AxiosResponse<RelayVideoSubmitResponse> = await this.httpClient.post(
88 '/api/ai/video/generations',
89 request,
90 )
91 const result = response.data
92 if (!result?.id) {
93 throw new AppException(ResponseCode.AiCallFailed, { error: `Relay video task id is missing: ${this.stringifyForError(result)}` })
94 }
95 return result
96 },
97 )
98 }
99
100 /**
101 * 轮询上游 relay 服务端的视频任务状态
102 * 对应上游 GET /ai/video/generations/:taskId
103 */
104 async getVideo(taskId: string): Promise<RelayVideoCallbackDto> {
105 return this.aiAvailability.execute(
106 { provider: 'relay', operation: 'getVideo' },
107 async () => {
108 const response: AxiosResponse<RelayVideoCallbackDto> = await this.httpClient.get(
109 `/api/ai/video/generations/${encodeURIComponent(taskId)}`,
110 )
111 return response.data
112 },
113 )
114 }
115 }
116
116 lines TYPESCRIPT