213 lines
6.1 KiB
TypeScript
213 lines
6.1 KiB
TypeScript
import { NextResponse } from "next/server";
|
|
import { z } from "zod";
|
|
import { auth } from "@/app/(auth)/auth";
|
|
import {
|
|
getChatById,
|
|
getSystemSetting,
|
|
saveChat,
|
|
saveMessages,
|
|
} from "@/lib/db/queries";
|
|
import {
|
|
normalizeOpenClawChatConfig,
|
|
OPENCLAW_CHAT_CONFIG_KEY,
|
|
} from "@/lib/openclaw-chat/config";
|
|
import { generateUUID } from "@/lib/utils";
|
|
|
|
const requestSchema = z.object({
|
|
id: z.string().uuid(),
|
|
message: z.string().trim().min(1).max(8000),
|
|
userMessageId: z.string().uuid().optional(),
|
|
metadata: z.record(z.unknown()).optional(),
|
|
});
|
|
|
|
function buildChatTitle(message: string) {
|
|
const normalized = message.replace(/\s+/g, " ").trim();
|
|
if (!normalized) return "Clawd 对话";
|
|
return normalized.length > 80 ? `${normalized.slice(0, 80)}...` : normalized;
|
|
}
|
|
|
|
function createConversationId(userId: string, chatId: string) {
|
|
return `site-user-${userId}-canvas-${chatId}`;
|
|
}
|
|
|
|
async function readOpenClawConfig() {
|
|
const setting = await getSystemSetting({ key: OPENCLAW_CHAT_CONFIG_KEY }).catch(
|
|
() => null
|
|
);
|
|
return normalizeOpenClawChatConfig(setting?.value);
|
|
}
|
|
|
|
function getOpenClawErrorMessage(payload: unknown, fallback: string) {
|
|
if (payload && typeof payload === "object") {
|
|
const raw = payload as Record<string, unknown>;
|
|
const nestedError =
|
|
raw.error && typeof raw.error === "object"
|
|
? (raw.error as Record<string, unknown>)
|
|
: null;
|
|
const message = nestedError?.message || raw.message || raw.error;
|
|
if (typeof message === "string" && message.trim()) {
|
|
return message.trim();
|
|
}
|
|
}
|
|
return fallback;
|
|
}
|
|
|
|
export async function POST(request: Request) {
|
|
const session = await auth().catch(() => null);
|
|
|
|
if (!session?.user?.id) {
|
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
}
|
|
|
|
const parsed = requestSchema.safeParse(await request.json().catch(() => null));
|
|
if (!parsed.success) {
|
|
return NextResponse.json({ error: "请求参数不正确" }, { status: 400 });
|
|
}
|
|
|
|
const config = await readOpenClawConfig();
|
|
if (!config.enabled) {
|
|
return NextResponse.json({ error: "Clawd 模式未启用" }, { status: 403 });
|
|
}
|
|
if (!config.baseUrl || !config.token) {
|
|
return NextResponse.json(
|
|
{ error: "Clawd 接口地址或 Token 未配置" },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
|
|
const existingChat = await getChatById({ id: parsed.data.id }).catch(() => null);
|
|
if (existingChat && existingChat.userId !== session.user.id) {
|
|
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
|
}
|
|
|
|
if (!existingChat) {
|
|
await saveChat({
|
|
id: parsed.data.id,
|
|
userId: session.user.id,
|
|
title: buildChatTitle(parsed.data.message),
|
|
visibility: "private",
|
|
});
|
|
}
|
|
|
|
const userMessageId = parsed.data.userMessageId || generateUUID();
|
|
const assistantMessageId = generateUUID();
|
|
const now = new Date();
|
|
const conversationId = createConversationId(session.user.id, parsed.data.id);
|
|
const endpoint = `${config.baseUrl}/api/chat`;
|
|
|
|
let upstreamPayload: unknown = null;
|
|
let upstreamStatus = 502;
|
|
|
|
try {
|
|
const controller = new AbortController();
|
|
const timeout = setTimeout(() => controller.abort(), config.timeoutMs);
|
|
const response = await fetch(endpoint, {
|
|
method: "POST",
|
|
headers: {
|
|
Authorization: `Bearer ${config.token}`,
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({
|
|
conversation_id: conversationId,
|
|
user_id: session.user.id,
|
|
user_name: session.user.name || session.user.email || "",
|
|
message: parsed.data.message,
|
|
metadata: {
|
|
source: "ai_chatbot_canvas",
|
|
page: "/canvas",
|
|
tenant: "default",
|
|
chat_id: parsed.data.id,
|
|
...parsed.data.metadata,
|
|
},
|
|
}),
|
|
signal: controller.signal,
|
|
}).finally(() => clearTimeout(timeout));
|
|
|
|
upstreamStatus = response.status;
|
|
upstreamPayload = await response.json().catch(() => null);
|
|
|
|
if (!response.ok) {
|
|
return NextResponse.json(
|
|
{
|
|
error: getOpenClawErrorMessage(upstreamPayload, "大龙虾聊天接口调用失败"),
|
|
},
|
|
{ status: response.status }
|
|
);
|
|
}
|
|
} catch (error) {
|
|
const isAbortError =
|
|
error instanceof Error && error.name === "AbortError";
|
|
return NextResponse.json(
|
|
{
|
|
error: isAbortError
|
|
? "大龙虾聊天接口请求超时"
|
|
: "大龙虾聊天接口暂时不可用",
|
|
},
|
|
{ status: isAbortError ? 504 : upstreamStatus }
|
|
);
|
|
}
|
|
|
|
const answer =
|
|
upstreamPayload && typeof upstreamPayload === "object"
|
|
? String((upstreamPayload as Record<string, unknown>).answer || "").trim()
|
|
: "";
|
|
|
|
if (!answer) {
|
|
return NextResponse.json(
|
|
{ error: "大龙虾聊天接口未返回有效回答" },
|
|
{ status: 502 }
|
|
);
|
|
}
|
|
|
|
const userMessage = {
|
|
id: userMessageId,
|
|
chatId: parsed.data.id,
|
|
role: "user",
|
|
parts: [{ type: "text", text: parsed.data.message }],
|
|
attachments: [],
|
|
createdAt: now,
|
|
videoStorageStatus: null,
|
|
temporaryVideoUrl: null,
|
|
permanentVideoUrl: null,
|
|
archivedAt: null,
|
|
videoMetadata: null,
|
|
};
|
|
const assistantMessage = {
|
|
id: assistantMessageId,
|
|
chatId: parsed.data.id,
|
|
role: "assistant",
|
|
parts: [{ type: "text", text: answer }],
|
|
attachments: [],
|
|
createdAt: new Date(now.getTime() + 1),
|
|
videoStorageStatus: null,
|
|
temporaryVideoUrl: null,
|
|
permanentVideoUrl: null,
|
|
archivedAt: null,
|
|
videoMetadata: null,
|
|
};
|
|
|
|
await saveMessages({ messages: [userMessage, assistantMessage] });
|
|
|
|
return NextResponse.json({
|
|
conversationId,
|
|
requestId:
|
|
upstreamPayload && typeof upstreamPayload === "object"
|
|
? (upstreamPayload as Record<string, unknown>).request_id || null
|
|
: null,
|
|
messages: [
|
|
{
|
|
id: userMessage.id,
|
|
role: "user",
|
|
parts: userMessage.parts,
|
|
metadata: { createdAt: userMessage.createdAt.toISOString() },
|
|
},
|
|
{
|
|
id: assistantMessage.id,
|
|
role: "assistant",
|
|
parts: assistantMessage.parts,
|
|
metadata: { createdAt: assistantMessage.createdAt.toISOString() },
|
|
},
|
|
],
|
|
});
|
|
}
|