36 lines
1.5 KiB
TypeScript
36 lines
1.5 KiB
TypeScript
// 不自动运行(Next.js 路由处理器,npm run dev 后 POST /api/chat)
|
||
// 服务端调用大模型并以流的形式返回;模型密钥只在服务端,浏览器拿不到。
|
||
export const runtime = "nodejs";
|
||
|
||
async function* fakeLLM(question: string, signal: AbortSignal): AsyncGenerator<string> {
|
||
// 真实项目:调用模型 SDK 的流式接口,并把 signal 传进去
|
||
const words = `关于“${question}”:签收 7 天内可以无理由退货。`.split("");
|
||
for (const w of words) {
|
||
if (signal.aborted) return; // 用户停止或断开:不再继续生成(节省费用)
|
||
await new Promise((r) => setTimeout(r, 30));
|
||
yield w;
|
||
}
|
||
}
|
||
|
||
export async function POST(request: Request): Promise<Response> {
|
||
const { question } = (await request.json()) as { question?: unknown };
|
||
if (typeof question !== "string" || question.length === 0 || question.length > 500) {
|
||
return Response.json({ error: "question 必须是 1~500 字的字符串" }, { status: 400 });
|
||
}
|
||
const encoder = new TextEncoder();
|
||
const stream = new ReadableStream<Uint8Array>({
|
||
async start(controller) {
|
||
try {
|
||
for await (const token of fakeLLM(question, request.signal)) {
|
||
controller.enqueue(encoder.encode(token));
|
||
}
|
||
} finally {
|
||
controller.close();
|
||
}
|
||
},
|
||
});
|
||
return new Response(stream, {
|
||
headers: { "Content-Type": "text/plain; charset=utf-8", "Cache-Control": "no-cache", "X-Accel-Buffering": "no" },
|
||
});
|
||
}
|