Skip to content

流式响应处理器 ​

本文引用的源码文件

本文引用的文件

目录 ​

  1. 引言
  2. 项目结构中的流式处理位置
  3. 核心组件总览
  4. 架构总览:从模型响应到侧面板渲染
  5. SSE 连接与生命周期管理
  6. 增量更新算法
  7. 流式数据解析与完整性校验
  8. 错误恢复机制
  9. 暂停、恢复与状态同步
  10. 性能优化
  11. 调试与监控
  12. 使用示例与最佳实践
  13. 故障排查清单
  14. 结论

引言 ​

本文件面向“流式响应处理器”,聚焦本项目中基于 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解析 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

图表来源

章节来源

核心组件总览 ​

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

图表来源

章节来源

架构总览:从模型响应到侧面板渲染 ​

下图展示了 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 统计、状态更新

图表来源

章节来源

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 做断线重连。

章节来源

优雅关闭 ​

优雅关闭的关键点如下:

  1. 用户点击“停止生成”时,ReAct 循环会通过 abortSignal 中断 reader.read()。
  2. readSSEStream 捕获 AbortError,不调用 controller.finish(),避免发送残留 STREAM_DONE。
  3. 空闲超时时,先 reader.cancel() 释放底层流,再调用 controller.finish(),最后抛出异常。
  4. 正常结束时,调用 controller.finish(),发送 STREAM_DONE,包含最终内容、推理内容、usage 和规范化后的 tool_calls。
  5. 侧面板在流结束后清理流式状态,并将思考指示器替换为思考结果。

章节来源

增量更新算法 ​

文本流式渲染 ​

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 会:

  1. 规范化 toolCalls。
  2. 先 flush 剩余内容,确保思考内容先显示。
  3. 发送 STREAM_TOOL_CALL,携带累积的 toolCalls 和 fullContent。
  4. 返回 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 结果

图表来源

章节来源

进度条更新 ​

当前代码中没有显式的“进度条”组件。流式进度主要通过以下方式体现:

  • “思考中”“输出中”状态。
  • 工具卡片上的“执行中…”状态。
  • 工具完成后显示成功/失败状态和耗时。
  • 极快完成的工具会设置最小显示延迟,确保用户能看到过渡状态。
  • 滚动到底部按钮的 badge 提示有新内容待查看。

如果需要真正的进度条,应基于 STREAM_START、STREAM_TOOL_CALL、STREAM_DONE 以及工具执行时长来计算,而不是依赖模型 SSE 的固定进度字段,因为模型流本身不提供统一进度值。

章节来源

流式数据解析与完整性校验 ​

JSON 流解析 ​

StreamController.feedLine 的处理顺序如下:

  1. 忽略空行和注释行。
  2. 提取 data: 前缀后的内容。
  3. 识别 [DONE]。
  4. 尝试 JSON.parse,失败则忽略该行。
  5. 优先提取 chunk.usage,即使后续 choices 为空。
  6. 取 choices[0],不存在则忽略。
  7. 累积 reasoning_content。
  8. 累积 content。
  9. 累积 tool_calls。
  10. 根据 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_DONEAgent 命令输出非模型 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 暂时不可见场景,但不等同于网络层断点续传。

章节来源

暂停、恢复与状态同步 ​

用户交互控制 ​

用户可以通过以下方式控制流式响应:

  • 点击流式消息中的“停止生成”按钮,触发取消。
  • 点击工具卡片中的“终止等待”,跳过当前工具等待。
  • 命令执行工具支持“终止命令”,弹出确认对话框。
  • 滚动到底部按钮支持拖拽定位,并在新内容到达时提示。

取消路径:

  1. 用户点击停止按钮。
  2. ReAct 层通过 abortSignal 中断请求。
  3. readSSEStream 捕获 AbortError。
  4. 不发送 STREAM_DONE,避免残留状态。
  5. ReAct 层记录“用户取消”错误并更新执行日志。

章节来源

后台处理 ​

后台处理包括:

  • ReAct 循环包装 SW 保活心跳,防止 Service Worker 在长任务中被系统回收。
  • Agent 心跳每 60 秒刷新代理端认证时间。
  • 流式节流通过 streamChunkDelay 控制。
  • 空闲超时保护流式数据卡住。

Service Worker 保活心跳由测试用例验证,确保任务执行期间定期调用轻量扩展 API,重置闲置计时器。

章节来源

状态同步 ​

状态同步通过以下机制保证:

  • sessionId 隔离流式元素和累积内容。
  • createStreamingElementAccessor 提供按会话访问流式元素的函数。
  • deleteStreamingState 清理指定会话的流式状态。
  • getStreamedContent / setStreamedContent 读写累积内容。
  • 工具卡片通过 data-tool-call-id 关联执行结果。
  • 思考过程支持筛选成功/失败节点。

章节来源

性能优化 ​

虚拟滚动 ​

当前代码中没有实现完整的虚拟滚动。主要优化集中在:

  • 智能滚动:只在用户位于底部时自动跟随,避免频繁滚动干扰阅读。
  • 防抖滚动:程序滚动时忽略 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 防护。

章节来源

调试与监控 ​

连接状态跟踪 ​

可通过以下方式跟踪流式连接状态:

  • 观察 STREAM_START 是否发送。
  • 观察 STREAM_CHUNK 频率和内容。
  • 观察 STREAM_TOOL_CALL 是否在工具调用时触发。
  • 观察 STREAM_DONE 是否正常结束。
  • 检查 AbortError 和 StreamIdleTimeout 是否出现。
  • 检查执行日志中的 API 调用状态、耗时、finish_reason 和 token 用量。

章节来源

延迟分析 ​

延迟来源包括:

  • 模型首包延迟。
  • 模型增量间隔。
  • 后台解析与节流延迟。
  • 扩展消息传递延迟。
  • 侧面板 Markdown 渲染延迟。
  • DOM 更新和滚动延迟。

优化建议:

  • 调整 streamChunkDelay,平衡流畅度和渲染压力。
  • 对极快工具设置最小显示延迟,避免状态闪烁。
  • 使用 requestAnimationFrame 避免阻塞主线程。
  • 对长 Markdown 内容考虑懒加载。

章节来源

错误诊断 ​

常见错误及定位方式:

现象可能原因定位方式
一直显示“思考中”模型未返回内容或网络卡住检查 STREAM_CHUNK 和空闲超时
工具卡片未显示finish_reason 不是 tool_calls 或 tool_calls 为空检查 STREAM_TOOL_CALL 和 toolCalls 规范化
流式内容丢失DOM 脱落且未重连检查 reconnectStreamingElement
停止生成后仍有 DONE旧版本或消息泄漏检查 AbortError 是否阻止 STREAM_DONE
工具参数解析失败arguments 格式异常检查 JSON 解析和防御逻辑

章节来源

使用示例与最佳实践 ​

建立 SSE 连接 ​

在本项目中,不建议直接复用 readSSEStream 作为通用 SSE 客户端,因为它与 StreamController 和扩展消息协议耦合较深。推荐做法:

  1. 使用 fetch 发起模型 API 请求。
  2. 判断响应是否支持流式。
  3. 若支持,使用 response.body.getReader() 读取流。
  4. 使用自定义解析器或复用 StreamController。
  5. 通过 chrome.runtime.sendMessage 向侧面板推送事件。
  6. 处理 AbortError、空闲超时和网络错误。

参考路径:

处理增量数据 ​

最佳实践:

  • 不要假设每个 chunk 都有 choices[0].delta.content。
  • 优先提取 usage,即使后续 choices 为空。
  • 累积 tool_calls 时容忍 index 跳号。
  • 在 finish_reason 之前处理 delta,避免丢失最后一个 chunk。
  • 使用节流机制避免频繁 DOM 更新。
  • 在侧面板使用累积全文渲染,而不是纯追加 delta。

参考路径:

实现断点续传 ​

由于模型 SSE 不支持传统断点续传,建议采用“会话级恢复”方案:

  1. 在任务开始前保存 checkpoint。
  2. 任务中断后保留会话上下文。
  3. 恢复时重新发起 API 请求,而不是续传旧流。
  4. 对已执行的工具调用做幂等处理或跳过。
  5. 对用户可见的状态做重建,例如消息列表、工具卡片、执行日志。

参考路径:

配置流式行为 ​

流式行为可通过配置控制:

配置项作用默认行为
streamEnabled是否启用流式模式默认启用,除非显式设为 false
streamChunkDelay流式 chunk 节流延迟默认 0,表示尽可能快
streamExpandTools工具卡片是否默认展开由侧面板状态控制

参考路径:

章节来源

故障排查清单 ​

问题检查项处理方式
流式无输出是否发送 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

章节来源

结论 ​

本项目的流式响应处理器以 StreamController 为核心,结合 readSSEStream 的流式读取、空闲超时保护和工具调用检测,实现了从模型 SSE 到侧面板渲染的完整链路。其优势在于:

  • 支持 OpenAI 兼容格式的 SSE 流。
  • 支持文本增量、推理内容、工具调用和 Token 用量。
  • 支持节流渲染、工具卡片实时展示和多轮 ReAct。
  • 支持用户取消、空闲超时和优雅关闭。
  • 支持侧面板流式元素重连和状态同步。

需要注意的限制包括:

  • 模型 SSE 没有内置自动重连。
  • 没有传统意义上的断点续传。
  • 流式渲染使用累积全文替换,可能对超长消息造成性能压力。
  • 心跳机制区分模型 SSE 和 Agent 保活,不应混淆。

未来优化方向可包括:

  • 增加更严格的 SSE 数据校验。
  • 引入虚拟滚动优化长消息列表。
  • 对超长 Markdown 内容做分块或懒加载。
  • 增加流式连接监控指标,如首包延迟、增量间隔、错误率。
  • 在业务层实现更完善的会话恢复和工具调用幂等控制。