52 lines
2.1 KiB
TypeScript
52 lines
2.1 KiB
TypeScript
// 增量 SSE 解析器:网络分块可能把一个事件切成两半,必须缓冲到空行再解析。
|
|
// 浏览器的 EventSource 只支持 GET 且不能带自定义请求头,AI 对话通常用 fetch + POST,再自己解析 SSE。
|
|
// 格式参考 HTML 标准:https://html.spec.whatwg.org/multipage/server-sent-events.html
|
|
|
|
export interface SSEEvent {
|
|
event: string; // 默认 "message"
|
|
data: string;
|
|
id?: string;
|
|
}
|
|
|
|
export function createSSEParser(onEvent: (e: SSEEvent) => void) {
|
|
let buffer = "";
|
|
return (chunk: string): void => {
|
|
buffer += chunk.replace(/\r\n?/g, "\n");
|
|
let sep: number;
|
|
while ((sep = buffer.indexOf("\n\n")) >= 0) {
|
|
const block = buffer.slice(0, sep);
|
|
buffer = buffer.slice(sep + 2);
|
|
const ev: SSEEvent = { event: "message", data: "" };
|
|
const data: string[] = [];
|
|
for (const line of block.split("\n")) {
|
|
if (!line || line.startsWith(":")) continue; // 空行、注释(常用作心跳)
|
|
const i = line.indexOf(":");
|
|
const field = i < 0 ? line : line.slice(0, i);
|
|
const value = i < 0 ? "" : line.slice(i + 1).replace(/^ /, "");
|
|
if (field === "event") ev.event = value;
|
|
else if (field === "data") data.push(value);
|
|
else if (field === "id") ev.id = value;
|
|
}
|
|
if (data.length) {
|
|
ev.data = data.join("\n");
|
|
onEvent(ev);
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
// 对话任务的状态:前端据此显示“连接中 / 生成中 / 完成 / 失败(可重试)/ 已停止”
|
|
export type ChatStatus = "idle" | "connecting" | "streaming" | "done" | "error" | "aborted";
|
|
|
|
// 服务端约定的事件:token(增量文本)、progress(任务进度)、final(结构化结果)、error
|
|
export interface FinalPayload {
|
|
answer: string;
|
|
citations: { id: number; title: string }[];
|
|
}
|
|
|
|
export function parseFinal(data: string): FinalPayload {
|
|
const v = JSON.parse(data) as Partial<FinalPayload>;
|
|
if (typeof v.answer !== "string" || !Array.isArray(v.citations)) throw new Error("final 事件格式错误");
|
|
return { answer: v.answer, citations: v.citations };
|
|
}
|