// 不自动运行(依赖 vue,需要 npm install 后用 vitest 运行 useChatStream.test.ts) // Vue 3 组合式函数:POST + 流式读取、状态机、停止生成、断线后带 Last-Event-ID 重试。 import { ref, shallowRef, readonly, onScopeDispose } from "vue"; import { createSSEParser, parseFinal, type ChatStatus, type FinalPayload } from "./sse.ts"; export interface ChatOptions { url: string; fetchImpl?: typeof fetch; maxRetries?: number; } export function useChatStream(opts: ChatOptions) { const status = ref("idle"); const text = ref(""); const progress = ref(""); const final = shallowRef(null); const error = ref(""); let controller: AbortController | null = null; let lastEventId: string | undefined; const doFetch = opts.fetchImpl ?? fetch; async function send(question: string, attempt = 0): Promise { if (attempt === 0) { controller?.abort(); // 新问题:取消上一次的生成 text.value = ""; final.value = null; error.value = ""; lastEventId = undefined; } controller = new AbortController(); status.value = "connecting"; try { const resp = await doFetch(opts.url, { method: "POST", headers: { "Content-Type": "application/json", ...(lastEventId ? { "Last-Event-ID": lastEventId } : {}) }, body: JSON.stringify({ question }), signal: controller.signal, }); if (!resp.ok || !resp.body) throw new Error(`HTTP ${resp.status}`); status.value = "streaming"; const feed = createSSEParser((e) => { if (e.id) lastEventId = e.id; // 记录进度,断线重连时服务端从这里继续 if (e.event === "token") text.value += e.data; else if (e.event === "progress") progress.value = JSON.parse(e.data).step; else if (e.event === "final") final.value = parseFinal(e.data); else if (e.event === "error") throw new Error(e.data); }); const reader = resp.body.pipeThrough(new TextDecoderStream()).getReader(); for (;;) { const { value, done } = await reader.read(); if (done) break; feed(value); } status.value = final.value ? "done" : "error"; if (!final.value) error.value = "连接提前结束"; } catch (e) { if ((e as Error).name === "AbortError") { status.value = "aborted"; return; } if (attempt < (opts.maxRetries ?? 2)) { await new Promise((r) => setTimeout(r, 300 * 2 ** attempt)); return send(question, attempt + 1); } status.value = "error"; error.value = (e as Error).message; } } function stop() { controller?.abort(); } onScopeDispose(stop); // 组件卸载时停止,避免泄漏和继续计费 return { status: readonly(status), text: readonly(text), progress: readonly(progress), final, error: readonly(error), send, stop, }; }