/** * 演示专用的假 SSE 流。 * * `useAiChat` 要求 `request` 返回一个 `Response`,真实的实现由后端提供。 * 这里用 ReadableStream 按内置协议逐条吐出事件,让文档站无需后端也能跑通 * 流式对话的完整链路。 * * 内置协议的事件名与载荷字段(见 `parseDefaultEvent` / `extractText` 等): * - `thinking` 载荷取 `thought` / `thinking` / `reasoning` * - `answer` 载荷取 `answer` / `text` / `content` * - `tool-status` 载荷取 `text` / `message` / `status` * - `citation` 载荷为引用对象 * - `done` 载荷可带 `conversationId` 与 `duration` */ /** 单条 SSE 事件。 */ interface MockEvent { event: string; data: Record; } /** 每个事件之间的间隔,模拟真实的流式节奏。 */ const STEP_DELAY = 420; function buildEvents(question: string): MockEvent[] { return [ { event: "thinking", data: { thought: `先理解问题:「${question}」` } }, { event: "thinking", data: { thought: "然后确认需要的数据口径与时间范围。" } }, { event: "tool-status", data: { text: "正在查询数据" } }, { event: "answer", data: { text: "根据当前口径," } }, { event: "answer", data: { text: `关于「${question}」的结论如下:` } }, { event: "answer", data: { text: "\n\n- 近 30 天整体**环比上升 12%**" } }, { event: "answer", data: { text: "\n- 其中移动端贡献了主要增量" } }, { event: "citation", data: { source: "分析中心", dataDomain: "订单域", timeRange: "近 30 天" } }, { event: "done", data: { conversationId: "demo-conversation", duration: STEP_DELAY * 9 } } ]; } function sleep(ms: number): Promise { return new Promise(resolve => setTimeout(resolve, ms)); } /** * 生成一段假的流式回复。 * * @param question 用户提问,会回显在回答里 * @param signal 取消信号,用户点「停止」时中断 */ export function createMockReply(question: string, signal?: AbortSignal): Response { const encoder = new TextEncoder(); const events = buildEvents(question); let index = 0; const stream = new ReadableStream({ async pull(controller) { if (signal?.aborted) { controller.error(new DOMException("已停止生成", "AbortError")); return; } if (index >= events.length) { controller.close(); return; } const item = events[index]; index += 1; controller.enqueue(encoder.encode(`event: ${item.event}\ndata: ${JSON.stringify(item.data)}\n\n`)); await sleep(STEP_DELAY); } }); return new Response(stream, { headers: { "Content-Type": "text/event-stream" } }); } /** 构造一条用于静态展示的消息。 */ export function createMessage( id: number, role: "user" | "assistant", content: string, extra: Partial<{ thinkingList: Array<{ id: number; content: string }>; citationList: unknown[] }> = {} ) { return { id, role, content, thinkingList: extra.thinkingList ?? [], citationList: extra.citationList ?? [], createdAt: Date.now() - (10 - id) * 60_000, statusText: "", duration: role === "assistant" ? 2400 : undefined }; }