/* Shared SSE client for /api/analyze/stream. * 统一 analyze.html 内联逻辑与 ai-panel.js 的 SSE 逻辑. * 事件契约 (后端 services 层下发): * step: {step:'context'|'prompt'|'complete', content:string} * token: {text:string} — 流式 LLM 输出, 分隔符已被后端剥离 * reasoning_start / conclusion_start: 无 payload, 切换当前 section * error: {error:string} — 业务错误帧 * onerror: 连接层错误 (非 JSON) * * callbacks 全部可选: * {onStep, onToken, onSectionStart, onError, onComplete} * onStep(step, content) — step === 'complete' 时也会先调 onStep 再调 onComplete * onToken(text, section) — section: 'reasoning' | 'conclusion' * onSectionStart(section) — 'reasoning' | 'conclusion' * onError(message, kind) — kind: 'server' (业务帧) | 'connection' * onComplete(content) — step === 'complete' * * 返回句柄 {close(), getSection()}, close() 幂等. * complete / error 帧后 client 自动 close 并失效 (与原两个实现一致). */ export function createAnalysisStream(scope, question, preset, callbacks = {}) { const { onStep, onToken, onSectionStart, onError, onComplete } = callbacks; let currentSection = "reasoning"; // 'reasoning' | 'conclusion', 由 SSE section 事件驱动 let closed = false; const params = new URLSearchParams({ scope }); if (question) params.append("question", question); if (preset) params.append("preset", preset); const es = new EventSource(`/api/analyze/stream?${params.toString()}`); function close() { if (closed) return; closed = true; es.close(); } es.addEventListener("step", (e) => { let data; try { data = JSON.parse(e.data); } catch (err) { return; // 非 JSON step 帧, 忽略 } const { step, content } = data; if (onStep) onStep(step, content); if (step === "complete") { if (onComplete) onComplete(content); close(); } }); // Section 事件: 后端识别 ===REASONING=== / ===CONCLUSION=== 后显式下发 es.addEventListener("reasoning_start", () => { currentSection = "reasoning"; if (onSectionStart) onSectionStart("reasoning"); }); es.addEventListener("conclusion_start", () => { currentSection = "conclusion"; if (onSectionStart) onSectionStart("conclusion"); }); es.addEventListener("token", (e) => { let data; try { data = JSON.parse(e.data); } catch (err) { return; } if (onToken) onToken(data.text || "", currentSection); }); es.addEventListener("error", (e) => { let msg = "未知错误"; try { const data = JSON.parse(e.data); msg = data.error || msg; } catch (ignore) { /* 非 JSON 错误帧 */ } if (onError) onError(msg, "server"); close(); }); es.onerror = () => { // 与旧实现一致: 仅当 stream 仍处于活跃状态时才上报连接错误, // 避免 complete 后浏览器触发 onerror 造成的误报. if (!closed) { if (onError) onError("连接错误, 请检查后端日志", "connection"); close(); } }; return { close, getSection: () => currentSection, }; }