import { NextResponse } from "next/server"; import { z } from "zod"; import { auth } from "@/app/(auth)/auth"; import { getChatById, getSystemSetting } from "@/lib/db/queries"; import { buildClawdCancelKey, hasActiveClawdRequest, registerActiveClawdRequest, unregisterActiveClawdRequest, } from "@/lib/openclaw-chat/cancel-registry"; import { normalizeOpenClawChatConfig, OPENCLAW_CHAT_CONFIG_KEY, } from "@/lib/openclaw-chat/config"; import { OPENCLAW_AUTH_REQUIRED_CODE, OPENCLAW_AUTH_REQUIRED_MESSAGE, } from "@/lib/openclaw-chat/errors"; import { getPersistedClawdTask, markClawdTaskCanceled, markClawdTaskCompleted, markClawdTaskFailed, } from "@/lib/openclaw-chat/task-store"; import { isPendingClawdTaskStatus } from "@/lib/openclaw-chat/task-state"; import { getPersistedUpstreamTaskState, refreshOpenClawDurableTask, } from "@/lib/openclaw-chat/upstream-task-client"; const statusSchema = z.object({ id: z.string().uuid(), userMessageId: z.string().uuid().optional(), }); export const dynamic = "force-dynamic"; export const revalidate = 0; export const maxDuration = 60; async function reconcileDetachedOpenClawTask(params: { chatId: string; userId: string; task: NonNullable>>; }) { if (!isPendingClawdTaskStatus(params.task.metadata.status)) return; const upstreamTask = getPersistedUpstreamTaskState(params.task.metadata); if (!upstreamTask) return; const cancelKey = buildClawdCancelKey({ userId: params.userId, chatId: params.chatId, userMessageId: params.task.metadata.userMessageId, }); if (hasActiveClawdRequest(cancelKey)) return; const controller = new AbortController(); registerActiveClawdRequest(cancelKey, controller, 30_000); const target = { chatId: params.chatId, assistantMessageId: params.task.metadata.assistantMessageId, }; try { const setting = await getSystemSetting({ key: OPENCLAW_CHAT_CONFIG_KEY, }).catch(() => null); const config = normalizeOpenClawChatConfig(setting?.value); if (!config.enabled || !config.baseUrl || !config.token) return; const outcome = await refreshOpenClawDurableTask({ config, upstreamTask, target, signal: controller.signal, }); if (outcome.status === "completed") { await markClawdTaskCompleted({ ...target, answer: outcome.answer, requestId: outcome.requestId, }); } else if (outcome.status === "failed") { await markClawdTaskFailed({ ...target, error: outcome.error }); } else if (outcome.status === "canceled") { await markClawdTaskCanceled({ ...target, message: outcome.error }); } } catch (error) { if (!controller.signal.aborted) { console.warn("[ClawdChat Status] Upstream reconciliation deferred:", { chatId: params.chatId, taskId: upstreamTask.taskId, error, }); } } finally { unregisterActiveClawdRequest(cancelKey); } } export async function GET(request: Request) { const session = await auth().catch(() => null); if (!session?.user?.id) { return NextResponse.json( { error: OPENCLAW_AUTH_REQUIRED_MESSAGE, code: OPENCLAW_AUTH_REQUIRED_CODE, }, { status: 401 } ); } const url = new URL(request.url); const parsed = statusSchema.safeParse({ id: url.searchParams.get("id"), userMessageId: url.searchParams.get("userMessageId") || undefined, }); if (!parsed.success) { return NextResponse.json({ error: "请求参数不正确" }, { status: 400 }); } const chat = await getChatById({ id: parsed.data.id }).catch(() => null); if (!chat) { return NextResponse.json({ task: null }, { status: 404 }); } if (chat.userId !== session.user.id) { return NextResponse.json({ error: "Forbidden" }, { status: 403 }); } let task = await getPersistedClawdTask({ chatId: parsed.data.id, userMessageId: parsed.data.userMessageId, }); if (!task) { return NextResponse.json( { task: null }, { headers: { "Cache-Control": "no-store, max-age=0" } } ); } await reconcileDetachedOpenClawTask({ chatId: parsed.data.id, userId: session.user.id, task, }); task = (await getPersistedClawdTask({ chatId: parsed.data.id, userMessageId: parsed.data.userMessageId, })) || task; const userTextPart = Array.isArray(task.userMessage?.parts) ? task.userMessage.parts.find( (part) => part && typeof part === "object" && !Array.isArray(part) && (part as { type?: unknown }).type === "text" ) : null; const userText = userTextPart && typeof userTextPart === "object" ? String((userTextPart as { text?: unknown }).text || "") : ""; return NextResponse.json( { task: { chatId: parsed.data.id, userMessageId: task.metadata.userMessageId, assistantMessageId: task.metadata.assistantMessageId, status: task.metadata.status, processingHint: task.metadata.processingHint, progressEvents: task.metadata.progressEvents, skillTagId: task.metadata.skillTagId || null, skillTagName: task.metadata.skillTagName || null, requestId: task.metadata.requestId || null, error: task.metadata.error || null, createdAt: task.metadata.createdAt, updatedAt: task.metadata.updatedAt, userMessage: { text: userText, attachments: task.userMessage?.attachments || [], }, }, }, { headers: { "Cache-Control": "no-store, max-age=0", "Retry-After": "2", }, } ); }