192 lines
5.6 KiB
TypeScript

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<Awaited<ReturnType<typeof getPersistedClawdTask>>>;
}) {
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,
artifacts: outcome.artifacts,
});
} 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",
},
}
);
}