返回 AiToEarn
ai.api.ts
根目录 / project / aitoearn-web / src / api / ai / ai.api.ts
1 import type { AgentTaskRatingResponse, AgentTaskShareVo, AiChatStreamParams, AiLogListParams, AiPaginationParams, ChatModel, CreateAgentTaskParams, CreateDraftFromVideoUrlDto, CreateDraftFromVideoUrlVo, CreateDraftGenerationVo, CreateImageTextDraftGenerationParams, CreateTaskResponse, CreateVideoDraftGenerationParams, DraftGenerationPricingVo, DraftGenerationStats, DraftGenerationTask, DraftGenerationTaskListVo, GetTaskListParams, SSEMessage, SubmitAgentTaskRatingPayload, TaskDetail, TaskListResponse, TaskMessagesVo, VideoGenerationHistoryListVo } from './ai.types'
2 import type { AssetListVo } from '@/types/agent-asset'
3 import { fetchEventSource } from '@microsoft/fetch-event-source'
4 import { useAgentStore } from '@/store/agent'
5 import { useUserStore } from '@/store/user'
6 import http from '@/utils/request'
7
8 /** AI 生成任务、草稿生成、模型配置与任务消息接口。 */
9 export const agentApi = {
10 /**
11 * 创建AI生成任务并通过 SSE 接收实时消息
12 * @param params
13 * @param onMessage SSE 消息回调
14 * @param onError 错误回调
15 * @param onDone 完成回调
16 * @returns 返回一个 abort 函数,用于中断 SSE 连接
17 */
18 async createTaskWithSSE(
19 params: CreateAgentTaskParams,
20 onMessage: (message: SSEMessage) => void,
21 onError: (error: Error) => void,
22 onDone: (sessionId?: string) => void,
23 ): Promise<() => void> {
24 const apiUrl = process.env.NEXT_PUBLIC_API_URL
25 const url = `${apiUrl}/agent/tasks`
26
27 let sessionId: string | undefined
28 const abortController = new AbortController()
29 // 用于消息去重,防止重复处理
30 const processedMessageIds = new Set<string>()
31 // 标记是否已经完成,防止重复调用 onDone
32 let isCompleted = false
33
34 // 返回 abort 函数
35 const abort = () => {
36 abortController.abort()
37 }
38
39 // Debug 模式拦截: 如果处于 debug 模式且还有文件可用,使用本地文件回放
40 try {
41 const store = useAgentStore.getState()
42 if (store.debugFiles.length > 0 && store.debugMessageIndex < store.debugFiles.length) {
43 const debugFilePath = useAgentStore.getState().consumeDebugFile()
44
45 if (debugFilePath) {
46 let isAborted = false
47 const abort = () => {
48 isAborted = true
49 }
50
51 ;(async () => {
52 try {
53 const resp = await fetch(debugFilePath)
54 if (!resp.ok) {
55 console.warn('[SSE] Debug replay: failed to fetch file:', debugFilePath)
56 onDone?.()
57 return
58 }
59
60 const raw = await resp.text()
61
62 // 解析 SSE 格式的数据块
63 const blocks = raw
64 .split(/\r?\n\r?\n+/)
65 .map(b => b.trim())
66 .filter(Boolean)
67
68 for (let i = 0; i < blocks.length; i++) {
69 if (isAborted)
70 break
71 const block = blocks[i]
72 const dataLine = block.split(/\r?\n/).find(l => l.startsWith('data:'))
73 if (!dataLine)
74 continue
75 const jsonPart = dataLine.replace(/^data:\s*/, '')
76 try {
77 const data = JSON.parse(jsonPart)
78 onMessage(data as SSEMessage)
79 }
80 catch (e) {
81 console.warn('[SSE] Debug replay: failed to parse block', e)
82 }
83 // 模拟流式推送的小延迟
84 await new Promise(r => setTimeout(r, 40))
85 }
86
87 if (!isAborted) {
88 onDone?.()
89 }
90 }
91 catch (e) {
92 console.error('[SSE] Debug replay error:', e)
93 onDone?.()
94 }
95 })()
96
97 return abort
98 }
99 }
100 }
101 catch (e) {
102 console.warn('[SSE] Debug mode check failed', e)
103 }
104
105 try {
106 // 获取语言设置
107 const lng = useUserStore.getState().lang || 'en'
108
109 await fetchEventSource(url, {
110 method: 'POST',
111 headers: {
112 'Content-Type': 'application/json',
113 'Authorization': `Bearer ${useUserStore.getState().token || ''}`,
114 'Accept-Language': lng,
115 },
116 body: JSON.stringify(params),
117 signal: abortController.signal,
118 openWhenHidden: true,
119
120 // 当连接打开时
121 async onopen(response) {
122 if (response.ok) {
123 return // 一切正常,继续处理消息
124 }
125
126 // 处理错误响应
127 if (response.status >= 400 && response.status < 500 && response.status !== 429) {
128 // 客户端错误,不重试
129 const errorText = await response.text()
130 console.error('[SSE] Client error:', response.status, errorText)
131 throw new Error(`HTTP ${response.status}: ${errorText}`)
132 }
133 else {
134 // 服务器错误或其他问题,不自动重试,直接抛出错误
135 console.error('[SSE] Server error:', response.status)
136 throw new Error(`HTTP ${response.status}`)
137 }
138 },
139
140 // 当收到消息时
141 onmessage(event) {
142 // 如果已完成,忽略后续消息
143 if (isCompleted) {
144 return
145 }
146
147 // 如果没有数据,跳过
148 if (!event.data) {
149 return
150 }
151
152 try {
153 const data = JSON.parse(event.data)
154
155 // 消息去重:基于 uuid 或生成唯一标识
156 const messageId
157 = data.uuid
158 || data.message?.uuid
159 || `${data.type}-${JSON.stringify(data).slice(0, 100)}`
160 if (processedMessageIds.has(messageId)) {
161 return
162 }
163 processedMessageIds.add(messageId)
164
165 // 保存 sessionId
166 if (data.sessionId) {
167 sessionId = data.sessionId
168 }
169
170 // 调用消息回调
171 onMessage(data)
172
173 // 如果收到结束信号,关闭连接
174 if (data.type === 'done' || data.type === 'error') {
175 isCompleted = true
176 abortController.abort()
177 }
178 }
179 catch (error) {
180 console.error('[SSE] Failed to parse message:', event.data, error)
181 }
182 },
183
184 // 当连接关闭时
185 onclose() {
186 if (!isCompleted) {
187 isCompleted = true
188 onDone(sessionId)
189 }
190 },
191
192 // 当发生错误时
193 onerror(error) {
194 console.error('[SSE] Error occurred:', error)
195
196 // 如果是手动中止,不抛出错误
197 if (abortController.signal.aborted) {
198 return
199 }
200
201 // 标记为已完成,防止重复处理
202 if (!isCompleted) {
203 isCompleted = true
204 // 其他错误,调用错误回调
205 onError(error instanceof Error ? error : new Error(String(error)))
206 }
207
208 // 抛出错误以停止重试
209 throw error
210 },
211 })
212 }
213 catch (error) {
214 console.error('[SSE] fetchEventSource failed:', error)
215
216 // 如果不是手动中止的错误且未完成,调用错误回调
217 if (!abortController.signal.aborted && !isCompleted) {
218 isCompleted = true
219 onError(error instanceof Error ? error : new Error(String(error)))
220 }
221 }
222
223 // 返回 abort 函数
224 return abort
225 },
226
227 /**
228 * 创建AI生成任务(旧方法,保留兼容性)
229 * @param params
230 */
231 async createTask(params: CreateAgentTaskParams) {
232 const res = await http.post<CreateTaskResponse>('agent/tasks', params)
233 return res
234 },
235
236 /**
237 * 获取任务详情
238 * @param taskId 任务ID
239 */
240 async getTaskDetail(taskId: string) {
241 const res = await http.get<TaskDetail>(`agent/tasks/${taskId}`)
242 return res
243 },
244
245 /**
246 * 获取任务消息(增量)
247 * @param taskId 任务ID
248 * @param lastMessageId 上次获取的最后一条消息 UUID,可选
249 */
250 async getTaskMessages(taskId: string, lastMessageId?: string) {
251 const params = lastMessageId ? { lastMessageId } : undefined
252 const res = await http.get<TaskMessagesVo>(`agent/tasks/${taskId}/messages`, params)
253 return res
254 },
255
256 /**
257 * 获取任务评分
258 * @param taskId 任务ID
259 */
260 async getTaskRating(taskId: string) {
261 const res = await http.get<AgentTaskRatingResponse>(
262 `agent/tasks/${taskId}/rating`,
263 )
264 return res
265 },
266
267 /**
268 * 提交或更新任务评分
269 * @param taskId 任务ID
270 * @param payload 评分内容
271 * @param payload.rating 评分值
272 * @param payload.comment 评分备注
273 */
274 async submitTaskRating(taskId: string, payload: SubmitAgentTaskRatingPayload) {
275 const res = await http.post(`agent/tasks/${taskId}/rating`, payload)
276 return res
277 },
278
279 /**
280 * 停止/取消任务
281 * @param taskId 任务ID
282 */
283 async stopTask(taskId: string) {
284 const res = await http.delete(`agent/tasks/${taskId}`)
285 return res
286 },
287
288 /**
289 * 中断内容生成任务
290 * @param taskId 任务ID
291 */
292 async abortTask(taskId: string) {
293 const res = await http.post(`agent/tasks/${taskId}/abort`)
294 return res
295 },
296
297 /**
298 * 获取任务列表
299 * @param params 查询参数
300 */
301 async getTaskList(params: GetTaskListParams = {}) {
302 const { page = 1, pageSize = 10, keyword, favoriteOnly } = params
303 const queryParams: Record<string, string | number | boolean> = { page, pageSize }
304
305 // 添加搜索关键词(截取前100字符)
306 if (keyword?.trim()) {
307 queryParams.keyword = keyword.trim().slice(0, 100)
308 }
309 // 添加收藏筛选
310 if (favoriteOnly) {
311 queryParams.favoriteOnly = true
312 }
313
314 const res = await http.get<TaskListResponse>('agent/tasks', queryParams)
315 return res
316 },
317
318 /**
319 * 删除任务
320 * @param taskId 任务ID
321 */
322 async deleteTask(taskId: string) {
323 const res = await http.delete(`agent/tasks/${taskId}`)
324 return res
325 },
326
327 /**
328 * 更新任务标题
329 * @param taskId 任务ID
330 * @param title 新标题
331 */
332 async updateTaskTitle(taskId: string, title: string) {
333 const res = await http.patch(`agent/tasks/${taskId}`, { title })
334 return res
335 },
336 /**
337 * 为任务创建或更新评分
338 * @param taskId 任务ID
339 * @param rating 评分值(1-5)
340 * @param comment 可选评论
341 */
342 async createRating(taskId: string, rating: number, comment?: string) {
343 const res = await http.post(`agent/tasks/${taskId}/rating`, { rating, comment })
344 return res
345 },
346 /**
347 * Create a public share token for a task
348 * @param taskId
349 * @param ttlSeconds optional validity in seconds
350 */
351 async createPublicShare(taskId: string, ttlSeconds?: number) {
352 const body = typeof ttlSeconds === 'number' ? { ttlSeconds } : undefined
353 const res = await http.post<AgentTaskShareVo>(
354 `agent/tasks/${taskId}/share`,
355 body,
356 )
357 return res
358 },
359
360 /**
361 * Get task by public share token (no authentication required)
362 * @param token Share token
363 */
364 async getTaskByShareToken(token: string) {
365 const res = await http.get<TaskDetail>(`agent/tasks/shared/${token}`)
366 return res
367 },
368
369 /**
370 * 收藏任务
371 * @param taskId 任务ID
372 */
373 async favoriteTask(taskId: string) {
374 const res = await http.post(`agent/tasks/${taskId}/favorite`)
375 return res
376 },
377
378 /**
379 * 取消收藏任务
380 * @param taskId 任务ID
381 */
382 async unfavoriteTask(taskId: string) {
383 const res = await http.delete(`agent/tasks/${taskId}/favorite`)
384 return res
385 },
386 }
387
388 // Source: ai.ts
389 /**
390 * Get Chat Model Parameters
391 */
392 export function getChatModels(scene?: 'comment' | 'web', silent?: boolean) {
393 return http.get<ChatModel[]>(`ai/models/chat?scene=${scene || 'web'}`, undefined, silent)
394 }
395
396 /**
397 * List Video Tasks
398 */
399 export function getVideoGenerations(params?: AiPaginationParams) {
400 return http.get<VideoGenerationHistoryListVo>('ai/video/generations', params)
401 }
402
403 /**
404 * 获取用户 AI 活动日志
405 */
406 export function getLogs(params?: AiLogListParams) {
407 return http.get('ai/logs', params)
408 }
409
410 /**
411 * AI聊天接口 - 支持流式和非流式响应
412 */
413 export async function aiChatStream(data: AiChatStreamParams) {
414 const token = useUserStore.getState().token
415 const lang = useUserStore.getState().lang
416
417 const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/ai/chat`, {
418 method: 'POST',
419 headers: {
420 'Content-Type': 'application/json',
421 'Authorization': token ? `Bearer ${token}` : '',
422 'Accept-Language': lang || 'en',
423 },
424 body: JSON.stringify({
425 stream: false, // 使用非流式响应
426 model: 'gpt-5.1-all',
427 temperature: 1,
428 presence_penalty: 0,
429 frequency_penalty: 0,
430 top_p: 1,
431 max_tokens: 8000, // 增加到8000以支持更长的响应(包括base64图片)
432 ...data,
433 }),
434 })
435
436 if (!response.ok) {
437 throw new Error(`HTTP error! status: ${response.status}`)
438 }
439
440 return response
441 }
442
443 /**
444 * 获取 Agent 生成的素材列表
445 * @param params - 分页参数
446 * @param params.page - 页码
447 * @param params.pageSize - 每页数量
448 * @returns 素材列表
449 */
450 export function getAgentAssets(params?: AiPaginationParams) {
451 return http.get<AssetListVo>('ai/assets', params)
452 }
453
454 /** 创建 AI 批量生成草稿任务 */
455 export function apiCreateDraftGeneration(data: CreateVideoDraftGenerationParams) {
456 return http.post<CreateDraftGenerationVo>('ai/draft-generation/v2', data)
457 }
458
459 /** 创建 AI 图文草稿生成任务 */
460 export function apiCreateImageTextDraft(data: CreateImageTextDraftGenerationParams) {
461 return http.post<CreateDraftGenerationVo>('ai/draft-generation/image-text', data)
462 }
463
464 /** 获取图片模型定价信息 */
465 export function apiGetDraftGenerationPricing() {
466 return http.get<DraftGenerationPricingVo>('ai/draft-generation/pricing')
467 }
468
469 /** 根据视频 URL 生成草稿 */
470 export function apiCreateDraftFromVideoUrl(data: CreateDraftFromVideoUrlDto) {
471 return http.post<CreateDraftFromVideoUrlVo>('ai/draft-generation/from-video-url', data)
472 }
473
474 /**
475 * 获取生成中任务数量统计(轮询用,静默模式不弹错误提示)
476 */
477 export function apiGetDraftGenerationStats() {
478 return http.get<DraftGenerationStats>('ai/draft-generation/stats', undefined, true)
479 }
480
481 /**
482 * 根据任务 ID 批量查询生成任务状态(轮询用,静默模式不弹错误提示)
483 */
484 export function apiQueryDraftGenerationTasks(taskIds: string[]) {
485 return http.post<DraftGenerationTask[]>('ai/draft-generation/query', { taskIds }, true)
486 }
487
488 /**
489 * 获取生成任务列表(分页)
490 * @param page 页码
491 * @param pageSize 每页数量
492 */
493 export function apiGetDraftGenerationList(page: number = 1, pageSize: number = 10) {
494 return http.get<DraftGenerationTaskListVo>(
495 'ai/draft-generation/',
496 { page, pageSize },
497 )
498 }
499
499 lines TYPESCRIPT