import { NextRequest, NextResponse } from "next/server"; import { auth } from "@/app/(auth)/auth"; import { prependImageModelBadge } from "@/lib/ai/runtime/agent-media-markdown"; import { isPendingFalImageEditTaskMetadata, type FalImageEditTaskMetadata, } from "@/lib/ai/runtime/fal-image-edit-task"; import { saveGeneratedImages } from "@/lib/ai/runtime/generated-image-persistence"; import { getChatById, getMessagesByChatId, getAiProviderById, saveMessages, updateMessageById, } from "@/lib/db/queries"; import type { DBMessage } from "@/lib/db/schema"; import { getFalImageTaskStatus, resolveFalImageTaskToMarkdown, } from "@/lib/ai/providers/fal-image"; import { isAgentModeScopedChatId, isScopedEmptyHistoryRequest, isValidMessagesChatId, normalizeMessagesChatId, } from "@/lib/chat/messages-route-access"; export const dynamic = "force-dynamic"; export const revalidate = 0; const FAL_IMAGE_TASK_POLL_INTERVAL_MS = 2_000; const falTaskLastPolledAt = new Map(); const falTaskSyncInFlight = new Set(); function toPublicVideoMetadata(value: DBMessage["videoMetadata"]) { if (!value || typeof value !== "object" || Array.isArray(value)) return value; const metadata = value as Record; if (metadata.taskKind !== "clawd_chat") return value; const publicMetadata = { ...metadata }; delete publicMetadata.upstreamTaskKey; return publicMetadata; } function createEmptyMessagesResponse() { return NextResponse.json( { messages: [] }, { headers: { "Cache-Control": "no-store, max-age=0", }, } ); } async function markFalTaskFailed(params: { message: DBMessage; metadata: FalImageEditTaskMetadata; errorText: string; }) { await updateMessageById({ messageId: params.message.id, chatId: params.message.chatId, parts: [{ type: "text", text: params.errorText }], videoMetadata: { ...params.metadata, status: "failed", updatedAt: Date.now(), error: params.errorText, } as any, }); } async function syncPendingFalImageTask(params: { userId: string; message: DBMessage; }): Promise { const metadata = params.message.videoMetadata; if (!isPendingFalImageEditTaskMetadata(metadata)) return false; const taskKey = `${params.message.id}:${metadata.requestId}`; const now = Date.now(); const lastPolledAt = falTaskLastPolledAt.get(taskKey) || 0; if (now - lastPolledAt < FAL_IMAGE_TASK_POLL_INTERVAL_MS) { return false; } if (falTaskSyncInFlight.has(taskKey)) { return false; } falTaskSyncInFlight.add(taskKey); falTaskLastPolledAt.set(taskKey, now); try { const provider = await getAiProviderById({ id: metadata.providerId }).catch(() => undefined); const apiKey = String(provider?.apiKey || "").trim(); if (!apiKey) { await markFalTaskFailed({ message: params.message, metadata, errorText: "FAL 图片编辑失败:未找到可用的服务商配置", }); falTaskLastPolledAt.delete(taskKey); return true; } let queueStatus: Awaited>; try { queueStatus = await getFalImageTaskStatus({ apiKey, endpointId: metadata.endpointId, requestId: metadata.requestId, }); } catch (error) { console.error("[Messages API] Failed to poll FAL task status:", { messageId: params.message.id, requestId: metadata.requestId, error, }); return false; } if (queueStatus.status === "COMPLETED") { try { const imageMarkdown = await resolveFalImageTaskToMarkdown({ apiKey, endpointId: metadata.endpointId, requestId: metadata.requestId, imageCount: metadata.requestedImageCount, outputSizeLabel: metadata.outputSizeLabel, sourceImageUrls: metadata.sourceImageUrls || [], }); const finalText = prependImageModelBadge( imageMarkdown, metadata.modelDisplayName || metadata.modelId, ); await updateMessageById({ messageId: params.message.id, chatId: params.message.chatId, parts: [{ type: "text", text: finalText }], videoMetadata: { ...metadata, status: "completed", updatedAt: Date.now(), queuePosition: null, lastLogs: Array.isArray((queueStatus as any)?.logs) ? (queueStatus as any).logs : [], error: null, } as any, }); await saveGeneratedImages( finalText, params.userId, params.message.chatId, params.message.id, metadata.userPrompt || metadata.prompt, metadata.modelRef || metadata.modelId, { sourceAssetIds: metadata.sourceAssetIds || [], sourceImageUrls: metadata.sourceImageUrls || [], relationType: metadata.relationType || "edited_from", sourceType: metadata.sourceType || "generated", projectId: metadata.projectId || null, optimizedPrompt: metadata.optimizedPrompt || metadata.prompt, userPrompt: metadata.userPrompt || metadata.prompt, requestedImageCount: metadata.requestedImageCount, aspectRatio: metadata.aspectRatio, imageSize: metadata.imageSize, }, ); falTaskLastPolledAt.delete(taskKey); return true; } catch (error) { console.error("[Messages API] Failed to finalize completed FAL task:", { messageId: params.message.id, requestId: metadata.requestId, error, }); await markFalTaskFailed({ message: params.message, metadata, errorText: "FAL 图片编辑结果处理失败,请重新提交任务", }); falTaskLastPolledAt.delete(taskKey); return true; } } const nextStatus = queueStatus.status === "IN_QUEUE" ? "queued" : "running"; const nextQueuePosition = queueStatus.status === "IN_QUEUE" && typeof (queueStatus as any).queue_position === "number" ? (queueStatus as any).queue_position : null; const hasMeaningfulChange = metadata.status !== nextStatus || (metadata.queuePosition ?? null) !== nextQueuePosition; if (hasMeaningfulChange) { await updateMessageById({ messageId: params.message.id, chatId: params.message.chatId, videoMetadata: { ...metadata, status: nextStatus, updatedAt: Date.now(), queuePosition: nextQueuePosition, lastLogs: Array.isArray((queueStatus as any)?.logs) ? (queueStatus as any).logs : [], } as any, }); return true; } return false; } finally { falTaskSyncInFlight.delete(taskKey); } } /** * 获取聊天消息 * 用于从数据库重新加载消息 */ export async function GET(request: NextRequest) { try { const session = await auth(); if (!session?.user?.id) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } const requestedChatId = request.nextUrl.searchParams.get("chatId"); if (!requestedChatId) { return NextResponse.json( { error: "Missing required parameter: chatId" }, { status: 400 } ); } const chatId = normalizeMessagesChatId(requestedChatId); if (!isValidMessagesChatId(chatId)) { return NextResponse.json({ error: "Forbidden" }, { status: 403 }); } const scope = request.nextUrl.searchParams.get("scope"); const selectedChat = await getChatById({ id: chatId }); if (!selectedChat) { if ( isAgentModeScopedChatId(chatId) || isScopedEmptyHistoryRequest({ chatId, scope }) ) { return createEmptyMessagesResponse(); } return NextResponse.json({ error: "Forbidden" }, { status: 403 }); } if (selectedChat.userId !== session.user.id) { return NextResponse.json({ error: "Forbidden" }, { status: 403 }); } let messages = await getMessagesByChatId({ id: chatId }); let mutated = false; for (const message of messages) { if (message.role !== "assistant") continue; if (!isPendingFalImageEditTaskMetadata(message.videoMetadata)) continue; const changed = await syncPendingFalImageTask({ userId: session.user.id, message, }); mutated = mutated || changed; } if (mutated) { messages = await getMessagesByChatId({ id: chatId }); } // 转换为前端格式 const formattedMessages = messages.map((msg: DBMessage) => { const parts = msg.parts as any[] || []; const firstPart = parts[0] as any; return { id: msg.id, role: msg.role, content: firstPart?.text || "", createdAt: msg.createdAt, parts: parts, attachments: msg.attachments || [], videoMetadata: toPublicVideoMetadata(msg.videoMetadata) || null, }; }); return NextResponse.json( { messages: formattedMessages }, { headers: { "Cache-Control": "no-store, max-age=0", }, } ); } catch (error) { console.error("[Messages API] GET Error:", error); return NextResponse.json( { error: "Failed to get messages" }, { status: 500 } ); } } /** * 保存消息到数据库的API * 用于图片处理等场景中保存助手回复消息 */ export async function POST(request: NextRequest) { try { const session = await auth(); if (!session?.user?.id) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } const body = await request.json(); const { chatId, message } = body; if (!chatId || !message) { return NextResponse.json( { error: "Missing required fields: chatId and message" }, { status: 400 } ); } // 构造DBMessage对象 const dbMessage: DBMessage = { id: message.id, chatId, role: message.role || "assistant", parts: message.parts || [], attachments: message.attachments || [], createdAt: message.createdAt ? new Date(message.createdAt) : new Date(), videoStorageStatus: null, temporaryVideoUrl: null, permanentVideoUrl: null, archivedAt: null, videoMetadata: null, }; // 保存消息到数据库 await saveMessages({ messages: [dbMessage] }); return NextResponse.json({ success: true }); } catch (error) { console.error("[Messages API] Error:", error); return NextResponse.json( { error: "Failed to save message" }, { status: 500 } ); } }