主题
流式响应处理器
本文引用的源码文件
本文引用的文件
目录
- 引言
- 项目结构中的流式处理位置
- 核心组件总览
- 架构总览:从模型响应到侧面板渲染
- SSE 连接与生命周期管理
- 增量更新算法
- 流式数据解析与完整性校验
- 错误恢复机制
- 暂停、恢复与状态同步
- 性能优化
- 调试与监控
- 使用示例与最佳实践
- 故障排查清单
- 结论
引言
本文件面向“流式响应处理器”,聚焦本项目中基于 SSE(Server-Sent Events)的流式输出链路。该链路的核心目标是:在扩展端后台进程读取模型 API 的流式响应后,将文本增量、工具调用、推理内容、Token 用量等数据实时推送到侧面板,并在 UI 上实现思考过程、工具卡片、执行结果和最终消息的渐进式呈现。
需要特别区分两个概念:
- 模型 API 的 SSE 流:由
StreamController与readSSEStream负责解析 OpenAI 兼容格式的data:行,提取choices.delta.content、tool_calls、finish_reason与usage。 - 扩展内部的消息通道:通过
chrome.runtime.sendMessage向侧面板发送STREAM_START、STREAM_CHUNK、STREAM_TOOL_CALL、STREAM_DONE等事件;这不是浏览器原生EventSource,而是扩展进程间消息协议。
此外,项目中还存在 Agent 端心跳与健康检查逻辑,但它是针对本地代理进程的保活机制,并非模型 SSE 连接的心跳。文档会在相应章节明确区分这两者。
章节来源
- src/background/stream-controller.js#L1-L10
- src/background/stream-controller.js#L287-L409
- src/background/index.js#L2250-L2306
项目结构中的流式处理位置
流式响应处理主要分布在三个层次:
| 层次 | 文件 | 职责 |
|---|---|---|
| 后台流式控制器 | src/background/stream-controller.js | 解析 SSE 行、合并增量、节流发送、检测工具调用、提取 usage、构造最终结果 |
| ReAct 循环入口 | src/background/react-loop.js | 发起 fetch、选择流式或非流式分支、包装 StreamController、记录执行日志、处理上下文超限自愈 |
| 侧面板渲染层 | src/side_panel/chat-streaming.js | 创建流式消息容器、增量 Markdown 渲染、工具卡片展示、滚动控制、状态同步 |
graph TB Background["后台进程<br/>react-loop.js"] --> StreamController["流式控制器<br/>stream-controller.js"] StreamController --> APIServer["模型 API<br/>OpenAI 兼容 SSE"] StreamController --> SidePanel["侧面板<br/>chat-streaming.js"] Background --> SidePanel
图表来源
- src/background/react-loop.js#L1460-L1488
- src/background/stream-controller.js#L36-L281
- src/side_panel/chat-streaming.js#L677-L726
章节来源
- src/background/stream-controller.js#L1-L53
- src/background/react-loop.js#L1460-L1488
- src/side_panel/chat-streaming.js#L1-L19
核心组件总览
StreamController:SSE 解析与增量聚合
StreamController 是流式响应的核心对象。它维护以下关键状态:
| 字段或方法 | 作用 | 复杂度说明 |
|---|---|---|
fullContent | 累积完整文本内容 | 字符串拼接,时间复杂度与增量长度线性相关 |
reasoningContent | 累积推理内容(如 DeepSeek thinking mode) | 同上 |
usage | 最终 Token 用量 | 通常只被最后一次独立 chunk 覆盖 |
toolCalls | 按 index 累积工具调用片段 | 可能形成稀疏数组,需规范化 |
isStreaming | 标记当前是否处于流式阶段 | 用于避免重复发送 start/done |
_pendingChunk | 节流缓冲区 | 减少频繁 DOM 更新 |
feedLine(line) | 解析单行 SSE 数据 | 每次调用 O(1),JSON 解析成本取决于 chunk 大小 |
sendChunk(content) | 节流合并并发送 STREAM_CHUNK | 根据 streamChunkDelay 控制频率 |
finish() | 发送 STREAM_DONE 并返回最终结果 | 包含 tool_calls 规范化 |
sendToolCallNotification() | 发送 STREAM_TOOL_CALL | 在检测到 finish_reason === 'tool_calls' 时触发 |
classDiagram
class StreamController {
+string sessionId
+string callId
+object config
+string fullContent
+string reasoningContent
+object usage
+array toolCalls
+boolean isStreaming
+number lastSendTime
-string _pendingChunk
-boolean _streamStarted
-boolean _streamDone
-function _sendFn
-string _typePrefix
+constructor(sessionId, streamConfig, options)
+feedLine(line) object
+sendStart() void
+sendChunk(content) void
-_flushChunk() void
+finish() void
+sendToolCallNotification() void
+reset() void
+getResult() object
}
图表来源
章节来源
readSSEStream:ReadableStream 读取与空闲超时保护
readSSEStream 负责从 fetchResponse.body.getReader() 中逐块读取二进制数据,解码为文本,按行拆分,再交给 StreamController.feedLine。其关键特性包括:
- 立即调用
controller.sendStart(),让前端尽早显示“思考中”。 - 使用
TextDecoder进行流式解码。 - 使用
Promise.race同时监听reader.read()、空闲超时和abortSignal。 - 空闲超时默认 120 秒,名称为
StreamIdleTimeout。 - 遇到
AbortError不发送STREAM_DONE,避免残留消息污染后续任务。 - 遇到空闲超时主动
reader.cancel(),然后调用controller.finish()并抛出异常。 - 正常结束时调用
controller.finish(),返回{ status: 'done', ...result }。 - 当检测到
finish_reason === 'tool_calls'时提前返回{ status: 'tool_calls', ...result },供 ReAct 循环继续执行工具。
flowchart TD
Start(["进入 readSSEStream"]) --> SendStart["发送 STREAM_START"]
SendStart --> Loop{"还有数据?"}
Loop --> |否| Finish["调用 controller.finish()"]
Finish --> Done["返回 done 结果"]
Loop --> |是| Read["reader.read()"]
Read --> Race["Promise.race<br/>空闲超时 / Abort / 数据"]
Race --> Decode["解码并拼接 buffer"]
Decode --> Split["按换行符拆分"]
Split --> Feed["controller.feedLine(line)"]
Feed --> TypeCheck{"事件类型?"}
TypeCheck --> |content| SendChunk["节流发送 STREAM_CHUNK"]
TypeCheck --> |tool_calls| ToolCall["发送 STREAM_TOOL_CALL 并返回 tool_calls 结果"]
TypeCheck --> |done| MarkDone["标记 _streamDone"]
TypeCheck --> |ignore| Next["继续下一行"]
SendChunk --> Loop
MarkDone --> Loop
ToolCall --> End(["结束"])
Next --> Loop
图表来源
章节来源
chat-streaming.js:侧面板流式渲染
侧面板负责把后台推送的事件转化为可交互的 UI。核心职责包括:
- 创建流式消息容器,包含思考指示器、停止按钮、状态区域。
- 增量渲染 Markdown 内容,支持多轮 ReAct 迭代。
- 隐藏思考指示器并插入“思考结果”徽章。
- 渲染工具调用卡片,支持展开/折叠、复制参数、终止命令、终止等待。
- 智能滚动到底部,尊重用户手动上滚。
- 重连流式元素,补充脱落期间缺失的内容。
- 管理流式状态访问器,防止多会话并行串台。
sequenceDiagram participant BG as "后台 react-loop.js" participant SC as "StreamController" participant SP as "侧面板 chat-streaming.js" BG->>SC : 创建 StreamController SC-->>SP : STREAM_START SP->>SP : 创建流式消息容器 loop 每个 SSE chunk SC-->>SP : STREAM_CHUNK SP->>SP : updateStreamingMessage(fullContent) end SC-->>SP : STREAM_TOOL_CALL SP->>SP : appendToolCallItems(toolCalls) SC-->>SP : STREAM_DONE SP->>SP : finalizeStreamingMessage
图表来源
- src/background/stream-controller.js#L171-L252
- src/side_panel/chat-streaming.js#L677-L726
- src/side_panel/chat-streaming.js#L887-L1116
章节来源
架构总览:从模型响应到侧面板渲染
下图展示了 ReAct 循环中一次 API 调用的完整流程,包括流式与非流式分支、工具调用检测和最终结果组装。
sequenceDiagram participant RL as "react-loop.js" participant HTTP as "HTTP 请求" participant SC as "StreamController" participant SP as "chat-streaming.js" RL->>HTTP : 发起模型 API 请求 alt 流式模式且存在 body RL->>SC : new StreamController(sessionId, streamConfig, options) RL->>SC : readSSEStream(reader, controller, abortSignal) SC->>SP : STREAM_START loop SSE data 行 SC->>SC : feedLine(data) alt content SC->>SP : STREAM_CHUNK(delta) else tool_calls SC->>SP : STREAM_TOOL_CALL(toolCalls) SC-->>RL : 返回 tool_calls 结果 else done SC->>SP : STREAM_DONE(finalContent, usage, toolCalls) SC-->>RL : 返回 done 结果 end end else 非流式模式 HTTP-->>RL : 完整 JSON 响应 RL->>RL : 解析 JSON 并规范化 tool_calls end RL->>RL : 记录执行日志、Token 统计、状态更新
图表来源
- src/background/react-loop.js#L1460-L1488
- src/background/stream-controller.js#L287-L409
- src/side_panel/chat-streaming.js#L677-L726
章节来源
SSE 连接与生命周期管理
连接建立
本项目没有使用浏览器原生 EventSource 来连接模型 API,而是使用 fetch 获取 Response.body,再通过 getReader() 获得 ReadableStream。readSSEStream 直接消费该流,因此:
- 连接建立由
fetch完成。 - 流读取由
readSSEStream管理。 - 后端解析由
StreamController完成。 - 前端渲染由
chat-streaming.js消费STREAM_*消息完成。
这种设计允许更细粒度的控制,例如自定义空闲超时、主动取消 reader、在工具调用中途退出流式读取。
心跳检测
需要严格区分两类心跳:
| 类型 | 目标 | 实现位置 | 是否 SSE 心跳 |
|---|---|---|---|
| 模型 SSE 空闲超时 | 检测流式数据卡住 | stream-controller.js 中 STREAM_IDLE_TIMEOUT_MS | 不是传统 SSE 心跳,而是“无数据超时”保护 |
| Agent 健康检查与心跳 | 检测本地代理进程 | src/background/index.js 中 startAgentHealthCheck、performAgentHeartbeat | 不是模型 SSE 心跳,是扩展与 Agent 之间的保活 |
模型 SSE 空闲超时默认 120 秒,每次收到数据块都会重置计时器。如果持续无 SSE 事件,则抛出 StreamIdleTimeout。
Agent 心跳每 60 秒向活跃代理发送一次轻量 GET 请求,用于刷新代理端的认证时间,使其感知插件在线。
stateDiagram-v2 [*] --> 未连接 未连接 --> 已连接 : "fetch 成功" 已连接 --> 接收数据 : "reader.read() 返回数据" 接收数据 --> 已连接 : "重置空闲计时器" 接收数据 --> 空闲超时 : "超过 STREAM_IDLE_TIMEOUT_MS" 空闲超时 --> 已关闭 : "reader.cancel()" 已连接 --> 已关闭 : "用户取消 / AbortError" 已连接 --> 工具调用 : "finish_reason = tool_calls" 工具调用 --> 已关闭 : "ReAct 继续执行工具" 接收数据 --> 已完成 : "[DONE] 或流结束" 已完成 --> 已关闭 : "controller.finish()"
图表来源
自动重连
在当前代码中,模型 SSE 流本身没有内置自动重连逻辑。readSSEStream 在空闲超时或网络错误时会抛出异常,由上层 react-loop.js 捕获并记录错误。
不过,项目实现了“上下文超限自愈”:当 HTTP 400 且错误信息包含 context、token、too long 等特征时,会强制裁剪消息并重试本轮。这属于业务层重试,而不是底层 SSE 重连。
对于 Agent 端,健康检查和心跳主要用于保持代理进程在线感知,并不直接对模型 SSE 做断线重连。
章节来源
- src/background/stream-controller.js#L287-L409
- src/background/react-loop.js#L1514-L1585
- src/background/index.js#L2250-L2306
优雅关闭
优雅关闭的关键点如下:
- 用户点击“停止生成”时,ReAct 循环会通过
abortSignal中断reader.read()。 readSSEStream捕获AbortError,不调用controller.finish(),避免发送残留STREAM_DONE。- 空闲超时时,先
reader.cancel()释放底层流,再调用controller.finish(),最后抛出异常。 - 正常结束时,调用
controller.finish(),发送STREAM_DONE,包含最终内容、推理内容、usage 和规范化后的 tool_calls。 - 侧面板在流结束后清理流式状态,并将思考指示器替换为思考结果。
章节来源
- src/background/stream-controller.js#L391-L409
- src/background/stream-controller.js#L220-L237
- src/side_panel/chat-streaming.js#L728-L780
增量更新算法
文本流式渲染
StreamController.feedLine 会累积 delta.content 到 fullContent,然后通过 sendChunk 节流发送 STREAM_CHUNK。侧面板的 updateStreamingMessage 接收完整累积内容,并使用 Markdown 渲染函数重新渲染当前轮次的思考内容。
关键点:
- 增量来自模型 SSE,但前端渲染的是累积全文,而非仅追加最后一个 delta。
- 思考过程中,思考指示器保持可见,首次出现内容时将“思考中”改为“输出中”。
- 多轮 ReAct 会追加多个
thinking-content块,每轮结束后插入“思考结果”徽章。 - 滚动控制区分程序滚动和用户滚动,避免误判。
flowchart TD
Chunk["收到 STREAM_CHUNK"] --> Accumulate["累积 fullContent"]
Accumulate --> Render["updateStreamingMessage(fullContent)"]
Render --> Thinking{"是否正在思考?"}
Thinking --> |是| UpdateThinking["更新 thinking-content"]
Thinking --> |否| UpdateFinal["更新最终思考内容"]
UpdateThinking --> Scroll["智能滚动到底部"]
UpdateFinal --> Scroll
图表来源
章节来源
工具调用实时显示
当模型返回 finish_reason === 'tool_calls' 时,StreamController 会:
- 规范化
toolCalls。 - 先 flush 剩余内容,确保思考内容先显示。
- 发送
STREAM_TOOL_CALL,携带累积的toolCalls和fullContent。 - 返回
status: 'tool_calls',让 ReAct 循环继续执行工具。
侧面板收到 STREAM_TOOL_CALL 后,会隐藏思考指示器,插入“思考结果”徽章,并为每个工具调用创建卡片。卡片包含工具名、参数摘要、执行中状态、终止按钮和展开/折叠能力。
sequenceDiagram participant SC as "StreamController" participant RL as "react-loop.js" participant SP as "chat-streaming.js" SC->>SC : 检测到 finish_reason = tool_calls SC->>SC : normalizeToolCalls(toolCalls) SC->>SP : STREAM_TOOL_CALL(toolCalls, thinkingContent) SP->>SP : 隐藏思考指示器 SP->>SP : 插入思考结果徽章 SP->>SP : 渲染工具卡片 SC-->>RL : 返回 tool_calls 结果
图表来源
- src/background/stream-controller.js#L149-L165
- src/background/stream-controller.js#L347-L370
- src/side_panel/chat-streaming.js#L887-L1116
章节来源
- src/background/stream-controller.js#L149-L165
- src/background/stream-controller.js#L347-L370
- src/side_panel/chat-streaming.js#L887-L1116
进度条更新
当前代码中没有显式的“进度条”组件。流式进度主要通过以下方式体现:
- “思考中”“输出中”状态。
- 工具卡片上的“执行中…”状态。
- 工具完成后显示成功/失败状态和耗时。
- 极快完成的工具会设置最小显示延迟,确保用户能看到过渡状态。
- 滚动到底部按钮的 badge 提示有新内容待查看。
如果需要真正的进度条,应基于 STREAM_START、STREAM_TOOL_CALL、STREAM_DONE 以及工具执行时长来计算,而不是依赖模型 SSE 的固定进度字段,因为模型流本身不提供统一进度值。
章节来源
流式数据解析与完整性校验
JSON 流解析
StreamController.feedLine 的处理顺序如下:
- 忽略空行和注释行。
- 提取
data:前缀后的内容。 - 识别
[DONE]。 - 尝试
JSON.parse,失败则忽略该行。 - 优先提取
chunk.usage,即使后续choices为空。 - 取
choices[0],不存在则忽略。 - 累积
reasoning_content。 - 累积
content。 - 累积
tool_calls。 - 根据
finish_reason决定返回类型。
flowchart TD
Line["SSE 行"] --> Prefix{"是否 data: 行?"}
Prefix --> |否| Ignore["忽略"]
Prefix --> |是| DoneCheck{"是否为 [DONE]?"}
DoneCheck --> |是| Done["返回 done"]
DoneCheck --> |否| Parse["JSON.parse"]
Parse --> Valid{"解析成功?"}
Valid --> |否| Ignore
Valid --> |是| Usage["提取 usage"]
Usage --> Choice{"是否存在 choices[0]?"}
Choice --> |否| Ignore
Choice --> |是| Delta["处理 delta"]
Delta --> Reasoning["累积 reasoning_content"]
Delta --> Content["累积 content"]
Delta --> Tools["累积 tool_calls"]
Tools --> Finish{"finish_reason?"}
Finish --> Stop["stop / length"]
Finish --> ToolCalls["tool_calls"]
Finish --> More["继续累积"]
图表来源
章节来源
事件类型识别
流式事件分为以下几类:
| 事件 | 含义 | 触发条件 |
|---|---|---|
STREAM_START | 开始流式输出 | 首次 flush 或 readSSEStream 启动时 |
STREAM_CHUNK | 文本增量 | delta.content 存在且未达到节流阈值 |
STREAM_TOOL_CALL | 工具调用通知 | finish_reason === 'tool_calls' |
STREAM_DONE | 流结束 | [DONE]、流结束或空闲超时后 |
AGENT_STREAM / AGENT_STREAM_DONE | Agent 命令输出 | 非模型 SSE,而是命令执行流 |
章节来源
数据完整性校验
当前实现的完整性校验相对轻量:
- 对无效 JSON 行直接忽略。
- 对缺少
choices[0]的 chunk 忽略。 - 对
tool_calls使用normalizeToolCalls过滤空洞和无效元素,并补齐缺失的id。 - 对 arguments 字段做防御性处理,兼容字符串和已解析对象。
- 对
[DONE]和finish_reason分别处理,避免丢失最后一个 chunk 的数据。
不足点:
- 没有对 SSE 行的完整性做强校验,例如
data:后必须是合法 JSON。 - 没有对
choices的结构做深度校验。 - 没有对
tool_calls.index的连续性做严格校验,只是容忍稀疏数组。 - 没有对
usage的字段做 schema 校验。
章节来源
错误恢复机制
网络中断检测
网络中断可能表现为:
reader.read()抛出异常。fetch返回非 OK 状态。- 长时间无数据触发空闲超时。
当前处理策略:
AbortError:视为用户取消,不发送STREAM_DONE,直接抛出异常。StreamIdleTimeout:主动 cancel reader,调用controller.finish(),抛出带idleMs的错误。- 其他错误:记录日志,调用
controller.finish(),抛出异常。 - ReAct 层区分用户取消、空闲超时和普通错误,并写入执行日志。
flowchart TD
Error["流读取错误"] --> Name{"错误类型?"}
Name --> |AbortError| Cancel["不发送 STREAM_DONE"]
Cancel --> ThrowAborted["抛出 AbortError"]
Name --> |StreamIdleTimeout| CancelReader["reader.cancel()"]
CancelReader --> Finish["controller.finish()"]
Finish --> ThrowTimeout["抛出 StreamIdleTimeout"]
Name --> |其他| FinishOther["controller.finish()"]
FinishOther --> ThrowOther["抛出普通错误"]
图表来源
章节来源
断点续传
当前代码中没有实现模型 SSE 的断点续传。原因包括:
- 模型 SSE 通常是单向流,客户端无法像下载文件一样通过 Range 头续传。
- 流式响应包含对话上下文,不能简单从某个字节偏移恢复。
- 工具调用可能已经执行,盲目续传可能导致副作用重复。
项目提供的“恢复”能力更接近“会话级恢复”和“上下文自愈”:
- 上下文超限自愈:400 错误时裁剪消息并重试。
- 自动续跑:通过后台闹钟和通知机制提醒用户或自动恢复任务。
- 流式元素重连:侧面板在 DOM 脱落后补回缺失内容,但这不是网络断点续传。
章节来源
状态重建
状态重建主要体现在侧面板的流式元素重连逻辑中:
- 如果流式元素脱离 DOM,
reconnectStreamingElement会将其重新插入。 - 补充
_streamedContentMap中缓存的累积内容。 - 修复残留的思考指示器,插入“思考结果”徽章。
- 滚动到底部。
这适用于 Service Worker 或页面生命周期导致的 DOM 暂时不可见场景,但不等同于网络层断点续传。
章节来源
暂停、恢复与状态同步
用户交互控制
用户可以通过以下方式控制流式响应:
- 点击流式消息中的“停止生成”按钮,触发取消。
- 点击工具卡片中的“终止等待”,跳过当前工具等待。
- 命令执行工具支持“终止命令”,弹出确认对话框。
- 滚动到底部按钮支持拖拽定位,并在新内容到达时提示。
取消路径:
- 用户点击停止按钮。
- ReAct 层通过
abortSignal中断请求。 readSSEStream捕获AbortError。- 不发送
STREAM_DONE,避免残留状态。 - ReAct 层记录“用户取消”错误并更新执行日志。
章节来源
- src/side_panel/chat-streaming.js#L717-L726
- src/side_panel/chat-streaming.js#L1075-L1097
- src/background/stream-controller.js#L391-L397
- src/background/react-loop.js#L1551-L1585
后台处理
后台处理包括:
- ReAct 循环包装 SW 保活心跳,防止 Service Worker 在长任务中被系统回收。
- Agent 心跳每 60 秒刷新代理端认证时间。
- 流式节流通过
streamChunkDelay控制。 - 空闲超时保护流式数据卡住。
Service Worker 保活心跳由测试用例验证,确保任务执行期间定期调用轻量扩展 API,重置闲置计时器。
章节来源
- src/background/react-loop.js#L728-L746
- src/background/index.js#L2250-L2306
- test/unit/sw-heartbeat.unit.test.js#L1-L18
状态同步
状态同步通过以下机制保证:
sessionId隔离流式元素和累积内容。createStreamingElementAccessor提供按会话访问流式元素的函数。deleteStreamingState清理指定会话的流式状态。getStreamedContent/setStreamedContent读写累积内容。- 工具卡片通过
data-tool-call-id关联执行结果。 - 思考过程支持筛选成功/失败节点。
章节来源
- src/side_panel/chat-streaming.js#L584-L665
- src/side_panel/chat-streaming.js#L603-L625
- src/side_panel/chat-streaming.js#L953-L1116
性能优化
虚拟滚动
当前代码中没有实现完整的虚拟滚动。主要优化集中在:
- 智能滚动:只在用户位于底部时自动跟随,避免频繁滚动干扰阅读。
- 防抖滚动:程序滚动时忽略 scroll 事件,防止误判为用户滚动。
- 补帧滚动:使用
requestAnimationFrame确保布局稳定后再滚动。
如果要引入虚拟滚动,应重点优化长消息列表,而不是单个流式消息内部的 Markdown 渲染。
章节来源
增量 DOM 更新
流式渲染采用“累积全文 + 局部替换”的策略:
- 每次
STREAM_CHUNK都传入完整fullContent。 updateStreamingMessage查找当前轮的thinking-content并替换其 innerHTML。- 多轮 ReAct 会追加新的
thinking-content,而不是无限增长单个节点。 - 工具卡片按需创建,支持展开/折叠减少初始渲染压力。
这种方式比纯追加 delta 更容易处理 Markdown 结构变化,但代价是每次都要重新渲染整段内容。对于超长回答,可考虑分块渲染或懒加载代码块。
章节来源
内存管理
关键内存管理点:
fullContent和reasoningContent会持续增长,直到会话结束。_streamedContentMap缓存累积内容,用于 DOM 重连。_streamingElements缓存流式 DOM 元素。agentNameCache缓存 Agent 名称,避免重复查询。- 流式节流减少频繁 DOM 操作。
- 工具卡片默认折叠,减少初始渲染体积。
建议:
- 会话结束时清理
_streamingElements和_streamedContentMap。 - 对超长消息考虑截断或分页展示。
- 对工具参数大对象避免重复序列化。
- 谨慎使用 innerHTML,注意 XSS 防护。
章节来源
- src/side_panel/chat-streaming.js#L584-L665
- src/side_panel/chat-streaming.js#L548-L566
- src/background/stream-controller.js#L36-L53
调试与监控
连接状态跟踪
可通过以下方式跟踪流式连接状态:
- 观察
STREAM_START是否发送。 - 观察
STREAM_CHUNK频率和内容。 - 观察
STREAM_TOOL_CALL是否在工具调用时触发。 - 观察
STREAM_DONE是否正常结束。 - 检查
AbortError和StreamIdleTimeout是否出现。 - 检查执行日志中的 API 调用状态、耗时、finish_reason 和 token 用量。
章节来源
延迟分析
延迟来源包括:
- 模型首包延迟。
- 模型增量间隔。
- 后台解析与节流延迟。
- 扩展消息传递延迟。
- 侧面板 Markdown 渲染延迟。
- DOM 更新和滚动延迟。
优化建议:
- 调整
streamChunkDelay,平衡流畅度和渲染压力。 - 对极快工具设置最小显示延迟,避免状态闪烁。
- 使用
requestAnimationFrame避免阻塞主线程。 - 对长 Markdown 内容考虑懒加载。
章节来源
- src/background/stream-controller.js#L184-L217
- src/side_panel/chat-streaming.js#L1154-L1161
- src/side_panel/chat-streaming.js#L866-L874
错误诊断
常见错误及定位方式:
| 现象 | 可能原因 | 定位方式 |
|---|---|---|
| 一直显示“思考中” | 模型未返回内容或网络卡住 | 检查 STREAM_CHUNK 和空闲超时 |
| 工具卡片未显示 | finish_reason 不是 tool_calls 或 tool_calls 为空 | 检查 STREAM_TOOL_CALL 和 toolCalls 规范化 |
| 流式内容丢失 | DOM 脱落且未重连 | 检查 reconnectStreamingElement |
| 停止生成后仍有 DONE | 旧版本或消息泄漏 | 检查 AbortError 是否阻止 STREAM_DONE |
| 工具参数解析失败 | arguments 格式异常 | 检查 JSON 解析和防御逻辑 |
章节来源
- src/background/stream-controller.js#L76-L130
- src/side_panel/chat-streaming.js#L728-L780
- src/side_panel/chat-streaming.js#L953-L963
使用示例与最佳实践
建立 SSE 连接
在本项目中,不建议直接复用 readSSEStream 作为通用 SSE 客户端,因为它与 StreamController 和扩展消息协议耦合较深。推荐做法:
- 使用
fetch发起模型 API 请求。 - 判断响应是否支持流式。
- 若支持,使用
response.body.getReader()读取流。 - 使用自定义解析器或复用
StreamController。 - 通过
chrome.runtime.sendMessage向侧面板推送事件。 - 处理
AbortError、空闲超时和网络错误。
参考路径:
处理增量数据
最佳实践:
- 不要假设每个 chunk 都有
choices[0].delta.content。 - 优先提取
usage,即使后续 choices 为空。 - 累积
tool_calls时容忍 index 跳号。 - 在
finish_reason之前处理 delta,避免丢失最后一个 chunk。 - 使用节流机制避免频繁 DOM 更新。
- 在侧面板使用累积全文渲染,而不是纯追加 delta。
参考路径:
实现断点续传
由于模型 SSE 不支持传统断点续传,建议采用“会话级恢复”方案:
- 在任务开始前保存 checkpoint。
- 任务中断后保留会话上下文。
- 恢复时重新发起 API 请求,而不是续传旧流。
- 对已执行的工具调用做幂等处理或跳过。
- 对用户可见的状态做重建,例如消息列表、工具卡片、执行日志。
参考路径:
配置流式行为
流式行为可通过配置控制:
| 配置项 | 作用 | 默认行为 |
|---|---|---|
streamEnabled | 是否启用流式模式 | 默认启用,除非显式设为 false |
streamChunkDelay | 流式 chunk 节流延迟 | 默认 0,表示尽可能快 |
streamExpandTools | 工具卡片是否默认展开 | 由侧面板状态控制 |
参考路径:
- src/config/constants.js#L29
- src/background/stream-controller.js#L37-L41
- src/side_panel/chat-streaming.js#L1030-L1031
章节来源
- src/background/react-loop.js#L1460-L1488
- src/background/stream-controller.js#L37-L41
- src/config/constants.js#L29
- src/side_panel/chat-streaming.js#L1030-L1031
故障排查清单
| 问题 | 检查项 | 处理方式 |
|---|---|---|
| 流式无输出 | 是否发送 STREAM_START | 检查 StreamController 初始化 |
| 流式卡顿 | 是否触发空闲超时 | 检查网络、模型响应、STREAM_IDLE_TIMEOUT_MS |
| 工具卡片不显示 | 是否发送 STREAM_TOOL_CALL | 检查 finish_reason 和 tool_calls |
| 停止生成无效 | 是否抛出 AbortError | 检查 abortSignal 和取消逻辑 |
| 流式内容丢失 | 是否 DOM 脱落 | 检查 reconnectStreamingElement |
| 工具参数解析失败 | arguments 是否合法 JSON | 检查解析和防御逻辑 |
| 滚动异常 | 是否误判用户滚动 | 检查 _userScrolledUp 和 _isProgramScroll |
| Service Worker 被回收 | 是否开启 SW 保活心跳 | 检查 startSwHeartbeat |
章节来源
- src/background/stream-controller.js#L287-L409
- src/side_panel/chat-streaming.js#L728-L780
- src/side_panel/chat-streaming.js#L159-L276
- src/background/react-loop.js#L728-L746
结论
本项目的流式响应处理器以 StreamController 为核心,结合 readSSEStream 的流式读取、空闲超时保护和工具调用检测,实现了从模型 SSE 到侧面板渲染的完整链路。其优势在于:
- 支持 OpenAI 兼容格式的 SSE 流。
- 支持文本增量、推理内容、工具调用和 Token 用量。
- 支持节流渲染、工具卡片实时展示和多轮 ReAct。
- 支持用户取消、空闲超时和优雅关闭。
- 支持侧面板流式元素重连和状态同步。
需要注意的限制包括:
- 模型 SSE 没有内置自动重连。
- 没有传统意义上的断点续传。
- 流式渲染使用累积全文替换,可能对超长消息造成性能压力。
- 心跳机制区分模型 SSE 和 Agent 保活,不应混淆。
未来优化方向可包括:
- 增加更严格的 SSE 数据校验。
- 引入虚拟滚动优化长消息列表。
- 对超长 Markdown 内容做分块或懒加载。
- 增加流式连接监控指标,如首包延迟、增量间隔、错误率。
- 在业务层实现更完善的会话恢复和工具调用幂等控制。