chat/ 模块说明
目录
架构概览
chat/
├── renderer.js # 底层 DOM 渲染工具
├── persistent.js # 持久消息(永久保留在聊天历史中)
├── ephemeral.js # 瞬态消息(只显示最新一条,新状态覆盖旧状态)
└── stream.js # LLM 流式消息(处理中显示思考过程,结束后固化)
chat.js # 入口文件,统一 re-export 所有公开函数
设计原则:将持久消息和瞬态消息分离,避免聊天历史被中间状态消息堆积。
模块职责
renderer.js
最底层渲染工具,不维护任何状态。提供:
getChatMessages()— 获取#chat-messages容器scrollToBottom()— 滚动到底部createChatBubble(text, type, isUser)— 创建消息气泡 DOMcreateSystemBubble(text, type)— 创建系统气泡 DOMappendMessage(wrapper)— 将消息追加到容器并滚动
persistent.js
管理永久保留在聊天历史中的消息:
addChatMessage(text, type, isUser)— 添加普通聊天消息addTypingIndicator()/removeTypingIndicator()— 加载三点动画addFileMessage(filename)— 添加文件上传消息setFileProcessing(filename)— 文件处理中状态setFileDone(filename, summary)— 文件处理完成,展开提取摘要setFileCached(filename)— 文件使用缓存setFileError(filename, errorMsg)— 文件处理错误
依赖 App.fileMessageMap(定义在 state.js)维护文件名到 DOM 元素的映射。
ephemeral.js
管理瞬态状态消息,同一时刻只显示最新一条:
showStatus(text, type)— 显示/更新状态消息clearStatus()— 清除当前状态消息
内部维护 ephemeralState 对象记录当前活跃的状态气泡引用。重复调用 showStatus 时直接更新已有气泡的文本和样式,不创建新 DOM。
stream.js
管理 LLM 流式响应的气泡生命周期:
handleLLMStream(msg)— 根据 SSE 事件阶段分发处理
支持的阶段:
| phase | 说明 |
|---|---|
start |
创建流式气泡,初始化思考过程区域(默认展开) |
reasoning |
追加思考过程文本 |
chunk |
追加正式回复文本 |
end |
关闭气泡,切换为 done 样式,折叠思考过程区域,气泡保留在历史中 |
error |
切换为 error 样式,显示错误信息 |
内部维护 llmStreamState 对象记录当前活跃的流式气泡引用。
消息类型
持久消息
永久保留在聊天历史中,不会被自动清除:
- 用户输入的文字
- 文件上传记录及其处理状态
- LLM 流式响应的最终结果(
end阶段后固化) - 系统通知(如"信息完整,可以提交")
- 错误消息
调用 addChatMessage() 或 addFileMessage() 创建。
瞬态消息
只显示最新一条,新状态覆盖旧状态:
- "正在分析文件..."
- "正在校验信息完整性..."
- "请输入登录账号:"
- "配置信息已完整,开始处理发票……"
- "收到补充文件,正在重新分析..."
调用 showStatus() 创建/更新,调用 clearStatus() 清除。
数据流
外部模块 (agent.js / config.js / process.js / upload.js / sync.js)
│
├── import { addChatMessage, addFileMessage, ... } from './chat.js'
├── import { showStatus, clearStatus } from './chat.js'
└── import { handleLLMStream } from './chat.js'
│
▼
chat.js (re-export)
│
├── → chat/persistent.js ──→ chat/renderer.js
├── → chat/ephemeral.js ──→ chat/renderer.js
└── → chat/stream.js ──→ chat/renderer.js
- 外部模块统一从
chat.js导入函数 chat.js只做 re-export,不引入循环依赖- 三个子模块通过
renderer.js共享底层 DOM 操作 persistent.js额外依赖state.js的App.fileMessageMap
状态管理
ephemeralState (ephemeral.js)
{
wrapper: HTMLElement | null, // 状态消息的 wrapper 元素
bubble: HTMLElement | null, // 状态气泡元素
}
- 初始为
null showStatus首次调用时创建并记录引用- 后续调用直接更新
bubble.textContent和bubble.className clearStatus时移除 DOM 并重置为null
llmStreamState (stream.js)
{
wrapper: HTMLElement | null, // 流式消息 wrapper
bubble: HTMLElement | null, // 流式气泡
textContent: HTMLElement | null, // 正式文本容器
accumulated: string, // 累积的正式文本
reasoningContent: HTMLElement | null, // 思考过程容器
reasoningAccumulated: string, // 累积的思考文本
}
start阶段创建并记录引用chunk/reasoning阶段追加文本end/error阶段重置为null(DOM 保留在历史中)
App.fileMessageMap (state.js)
Map<string, { wrapper, bubble, nameRow, detailRow }>
- 文件名 → DOM 元素映射
addFileMessage创建时写入setFileProcessing/setFileDone/setFileCached/setFileError读取并更新对应文件的状态
注意事项
-
不要直接操作
#chat-messages容器。所有消息创建都通过本模块的 API 进行。 -
瞬态消息和持久消息不要混用。中间处理状态用
showStatus,最终结果用addChatMessage。 -
showStatus不需要手动清除。调用showStatus显示新状态时会自动覆盖旧状态;在处理流程结束时,后续的消息或状态会自然覆盖。 -
LLM 流式气泡在
end阶段后变为持久消息。不需要额外调用addChatMessage来保留结果。 -
SSE 事件必须包含
start和end阶段。缺少start会导致气泡未创建,缺少end会导致气泡一直处于处理中状态。详见.agents/docs/error-experience/2026-06-13-llm_query_text缺少start-end事件导致前端不显示.md。 -
handleLLMStream不处理普通日志行。SSE 的message事件中,只有type === 'llm_stream'的事件才会被转发到此模块。 -
文件消息的 DOM 生命周期由
fileMessageMap管理。文件处理完成后,摘要信息会展开显示在文件气泡下方。 -
所有模块通过
chat.js统一导入,不要直接从chat/子目录导入(外部模块层面)。子模块之间的内部导入不受此限制。