feat(blindbox): AI 计划生成流式进度与渐进式文案
- 新增 runPlanGeneration 与 onProgress 回调 (blindboxPlanGen.ts) - 新增 POST /api/blindbox/plan/stream 推送 SSE 进度事件 - 前端优先走流式接口,实时展示「分析想法→搜索地点→规划路线→快好了」 - 流式失败时回退普通 POST,客户端轮播进度文案作为后备 - 规划阶段 UI 显示 planStatusMessage 替代静态文案
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
import { requireMembership } from "@/lib/blindbox";
|
||||
import { requireUserId } from "@/lib/api";
|
||||
import { runPlanGeneration } from "@/lib/blindboxPlanGen";
|
||||
|
||||
function encodeSSE(event: string, data: string): string {
|
||||
return `event: ${event}\ndata: ${data}\n\n`;
|
||||
}
|
||||
|
||||
export async function POST(req: Request): Promise<Response> {
|
||||
let roomId: string;
|
||||
let userId: string;
|
||||
let availableTime: { date: string; startHour: number; endHour: number };
|
||||
|
||||
try {
|
||||
const body = await req.json();
|
||||
roomId = body.roomId;
|
||||
userId = body.userId;
|
||||
availableTime = body.availableTime;
|
||||
|
||||
requireUserId(userId);
|
||||
if (!roomId) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: "roomId 不能为空" }),
|
||||
{ status: 400, headers: { "Content-Type": "application/json" } },
|
||||
);
|
||||
}
|
||||
|
||||
await requireMembership(roomId, userId);
|
||||
|
||||
const at = availableTime;
|
||||
if (
|
||||
!at?.date ||
|
||||
typeof at.startHour !== "number" ||
|
||||
typeof at.endHour !== "number" ||
|
||||
at.endHour <= at.startHour
|
||||
) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: "请选择有效的可用时间" }),
|
||||
{ status: 400, headers: { "Content-Type": "application/json" } },
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
const message = e instanceof Error ? e.message : "请求参数错误";
|
||||
return new Response(JSON.stringify({ error: message }), {
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
const stream = new ReadableStream({
|
||||
async start(controller) {
|
||||
const encoder = new TextEncoder();
|
||||
const push = (event: string, data: string) => {
|
||||
controller.enqueue(encoder.encode(encodeSSE(event, data)));
|
||||
};
|
||||
|
||||
try {
|
||||
const result = await runPlanGeneration(roomId!, userId!, availableTime!, (message) => {
|
||||
push("status", message);
|
||||
});
|
||||
push("plan", JSON.stringify({ id: result.id, days: result.days, createdAt: result.createdAt }));
|
||||
} catch (e) {
|
||||
const message = e instanceof Error ? e.message : "生成计划失败";
|
||||
push("error", message);
|
||||
} finally {
|
||||
controller.close();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return new Response(stream, {
|
||||
headers: {
|
||||
"Content-Type": "text/event-stream",
|
||||
"Cache-Control": "no-cache, no-transform",
|
||||
Connection: "keep-alive",
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user