Skip to content

Commit 750d607

Browse files
committed
feat(chat): onFinish 改 fetch 后端 /api/chat/sessions/save,不再直连 Prisma
背景:Neon → 自建 Docker PG 迁移后,前端 Prisma 还指向 Neon,AI 对话持久 化会写进旧库,和后端读自建 PG 分叉出脏数据。方案 A:把 chat + message 写 入挪到后端统一走,前端 onFinish 只发一次 HTTP。 - 删掉 import { prisma } from "@/lib/db",运行时再无 Prisma 依赖 - onFinish 原来三次 prisma 调用(chat upsert + user 消息 + assistant 消息) 合并成一次 fetch(BACKEND_URL + "/api/chat/sessions/save") - 后端接口匿名允许,登录时通过 satoken header 关联 userId,行为语义和原 Prisma 版完全一致(匿名写 userId=NULL,登录补挂 userId) - BACKEND_URL 未配或后端返回非 2xx 时 console.warn 不抛错,保持 "持久化失败不阻塞对话流式返回"的原语义 Vercel AI SDK 流式路径(streamText / convertToModelMessages 等)完全未动, 前端 UX 无感知。 配套后端 PR:InvolutionHell/involutionhell-backend#13
1 parent 62f25fe commit 750d607

1 file changed

Lines changed: 47 additions & 44 deletions

File tree

app/api/chat/route.ts

Lines changed: 47 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { prisma } from "@/lib/db";
21
import { streamText, UIMessage, convertToModelMessages } from "ai";
32
import { getModel, requiresApiKey, type AIProvider } from "@/lib/ai/models";
43
import { buildSystemMessage } from "@/lib/ai/prompt";
@@ -187,55 +186,59 @@ export async function POST(req: Request) {
187186
system: systemMessage,
188187
messages: await convertToModelMessages(messages || []),
189188
onFinish: async ({ text }) => {
189+
// 2026-04-17 起对话历史改由后端 /api/chat/sessions/save 统一持久化(事务保证
190+
// Chat + Message 一起落库)。原本 prisma 直连 Neon 的方案在 Neon → 自建 PG
191+
// 切换后会产生前后端双写不同库的脏数据,所以整条路径下掉。
190192
try {
191-
// 等待用户身份解析(与流式传输并行运行,此时大概率已完成)
192-
const userId = await userIdPromise;
193-
194-
// 1. 保存/更新会话,绑定用户 ID
195-
// update 也写入 userId:覆盖此前匿名创建的记录(用户登录后继续同一 chatId)
196-
await prisma.chat.upsert({
197-
where: { id: effectiveChatId },
198-
update: {
199-
updatedAt: new Date(),
200-
...(userId != null && { userId }),
201-
},
202-
create: {
203-
id: effectiveChatId,
204-
...(userId != null && { userId }),
205-
},
206-
});
193+
const backendUrl = process.env.BACKEND_URL;
194+
if (!backendUrl) {
195+
console.warn(
196+
"[Chat History] BACKEND_URL 未配置,跳过持久化(不阻塞流式返回)",
197+
);
198+
return;
199+
}
207200

208-
// 2. 保存用户消息 (取最后一条)
209-
// AI SDK v5 中,UIMessage 不再有 content 字段,内容在 parts 数组中
201+
// 从 parts 数组中提取最后一条 user 消息的纯文本;AI SDK v5 没有 content
202+
// 字段,需要自己拼。空消息(role 不是 user 或 parts 为空)就传 null,
203+
// 后端看到 null/空串会跳过插入,语义和原 Prisma 版 if 判断保持一致。
210204
const safeMessages = messages || [];
211205
const lastUserMessage = safeMessages[safeMessages.length - 1];
212-
if (lastUserMessage && lastUserMessage.role === "user") {
213-
// 从 parts 数组中提取所有文本内容并拼接
214-
const userContent = Array.isArray(lastUserMessage.parts)
215-
? lastUserMessage.parts
216-
.filter((part) => part.type === "text")
217-
.map((part) => (part as { type: "text"; text: string }).text)
218-
.join("\n")
219-
: (lastUserMessage as unknown as { content?: string })?.content ||
220-
"";
221-
222-
await prisma.message.create({
223-
data: {
224-
chatId: effectiveChatId,
225-
role: "user",
226-
content: userContent,
227-
},
228-
});
229-
}
230-
231-
// 3. 保存 AI 回复
232-
await prisma.message.create({
233-
data: {
234-
chatId: effectiveChatId,
235-
role: "assistant",
236-
content: text,
206+
const userContent =
207+
lastUserMessage && lastUserMessage.role === "user"
208+
? Array.isArray(lastUserMessage.parts)
209+
? lastUserMessage.parts
210+
.filter((part) => part.type === "text")
211+
.map(
212+
(part) => (part as { type: "text"; text: string }).text,
213+
)
214+
.join("\n")
215+
: (lastUserMessage as unknown as { content?: string })
216+
?.content || ""
217+
: "";
218+
219+
// 后端用 sa-token 从 header 取 userId 关联会话;匿名请求不带 satoken
220+
// 时后端自动把 userId 置 NULL,行为与原 prisma.chat.upsert 一致。
221+
// 静默 await 用户解析(原代码 await userIdPromise,这里保持阻塞等待
222+
// 以确保后端鉴权不会错用过期数据;失败已在 resolveUserId 内降级)。
223+
await userIdPromise;
224+
225+
const resp = await fetch(`${backendUrl}/api/chat/sessions/save`, {
226+
method: "POST",
227+
headers: {
228+
"Content-Type": "application/json",
229+
...(satoken ? { satoken } : {}),
237230
},
231+
body: JSON.stringify({
232+
chatId: effectiveChatId,
233+
userMessage: userContent,
234+
assistantMessage: text,
235+
}),
238236
});
237+
if (!resp.ok) {
238+
console.warn(
239+
`[Chat History] backend save returned ${resp.status}, history may be lost for chat ${effectiveChatId}`,
240+
);
241+
}
239242
} catch (error) {
240243
console.error("Failed to save chat history:", error);
241244
}

0 commit comments

Comments
 (0)