message-rendering-architecture.md
9.72 KB
消息渲染架构
版本: v1.0 | 日期: 2026-02-27
1. 设计目标
核心原则: 查表路由 + 零 if-else + 协议隔离
- 数据层 (
useAcpBlocks) 将异构协议事件解析为统一的 block 数组 - 路由层 通过查表将
block.type映射到对应渲染组件,零分支判断 - 渲染层 每种 block 类型一个独立组件,互不耦合
- 扩展性 新增协议只需:① 新建 handler 文件 ② 新建渲染组件 ③ 各加一行注册
2. 三层架构
┌─────────────────────────────────────────────────────────────────┐
│ 数据层 (composables/) │
│ │
│ useAcpBlocks.js ─ 骨架:blocks ref、路由合并、工具函数 │
│ ├── block-handlers/claude.js ─ Claude 流式/assistant/user │
│ ├── block-handlers/codex.js ─ Codex notification │
│ └── block-handlers/session.js ─ Session update notification │
│ │
│ 输出: blocks = ref([{ type, ... }, ...]) │
├─────────────────────────────────────────────────────────────────┤
│ 路由层 (components/) │
│ │
│ MessageBlocks.vue ─ 顶层路由: block.type → 组件 │
│ ├── AcpEventBlock.vue ─ 二级路由: event.type:method → 组件 │
│ └── ToolCallBlock.vue ─ 二级路由: tool.name → 组件 │
├─────────────────────────────────────────────────────────────────┤
│ 渲染层 (components/claude-chat/blocks/) │
│ │
│ TextBlock.vue ─ 文本输出 │
│ CodexReasoningBlock.vue ─ 思考过程(Claude/Codex 共用) │
│ CodexCommandBlock.vue ─ Codex 命令执行 │
│ ClaudeUsageBlock.vue ─ Claude token 用量统计 │
│ UnknownBlock.vue ─ 未识别 block 的 JSON 兜底 │
│ events/*.vue ─ ACP 原始事件展示组件 │
│ tool-call/*.vue ─ 工具调用展示组件 │
└─────────────────────────────────────────────────────────────────┘
3. 文件结构
web/src/
├── composables/
│ ├── useAcpBlocks.js # 骨架 (~130 行)
│ ├── useTaskDetail.js # 调用方:管理 execution 与 blocks 生命周期
│ └── block-handlers/
│ ├── claude.js # Claude 协议 handler (~190 行)
│ ├── codex.js # Codex 协议 handler (~75 行)
│ └── session.js # Session update handler (~110 行)
│
├── components/
│ └── claude-chat/
│ ├── MessageBlocks.vue # 顶层路由组件
│ ├── ToolCallBlock.vue # 工具调用路由
│ ├── blocks/
│ │ ├── TextBlock.vue # 文本
│ │ ├── CodexReasoningBlock.vue # 思考过程
│ │ ├── CodexCommandBlock.vue # 命令执行
│ │ ├── ClaudeUsageBlock.vue # Token 统计
│ │ ├── AcpEventBlock.vue # ACP 事件路由
│ │ ├── UnknownBlock.vue # 未知兜底
│ │ └── events/ # ACP 原始事件组件 (12 个)
│ └── tool-call/
│ ├── ToolCallRenderer.vue # 通用工具调用渲染器
│ ├── ToolCallShell.vue # Shell 命令
│ ├── ToolCallRead.vue # 文件读取
│ ├── ToolCallWrite.vue # 文件写入
│ └── ToolCallGeneric.vue # 通用工具
│
└── components/task/
└── AgentOutput.vue # 外壳:头部、输入折叠、blocks 容器
4. 数据流
4.1 实时流式
SSE 推送
│
▼
useTaskDetail.handleEvent()
│ event.type === 'acp_event'
▼
ensureParser(executionId).handleAcpEvent(event.event)
│
▼
useAcpBlocks.handleAcpEvent(acpEvent)
│ 查 topLevelHandlers[acpEvent.type]
│
├── stream_event → claude.handleStreamEvent()
│ ├── content_block_start → push text/thinking/tool_call block
│ ├── content_block_delta → 追加文本/thinking/JSON
│ └── content_block_stop → 清理 state
│
├── assistant → claude.handleAssistantEvent()
│ ├── thinking fallback → push claude_thinking block
│ └── usage → push claude_usage block
│
├── user → claude.handleUserEvent()
│ └── tool_result → 更新 tool_call block
│
└── notification → 分发到子路由
├── codex/* → codex handler
│ ├── thread/turn lifecycle → suppress
│ └── item.started/completed → reasoning/text/command block
│
└── session/update → session handler
├── agent_message_chunk → appendText
├── tool_call → push tool_call block
├── tool_call_update → 更新 input
└── tool_result → 更新 result
4.2 历史回放
API: GET /api/tasks/:id/executions/:execId
│
▼
detail.acpEvents[] (原始事件数组)
│
▼
parser.reset()
for (event of acpEvents) parser.handleAcpEvent(event)
│
▼
同一套 handler → 同样的 blocks 输出
5. 路由查表
5.1 顶层路由 (useAcpBlocks → topLevelHandlers)
| acpEvent.type | handler 来源 | 说明 |
|---|---|---|
stream_event |
claude.js |
Claude 流式 |
assistant |
claude.js |
Claude usage |
user |
claude.js |
工具结果 |
notification |
合并路由 | Codex + Session |
| 其他 | appendEvent |
作为 acp_event 兜底 |
5.2 组件路由 (MessageBlocks → blockComponentMap)
| block.type | 组件 | 来源 |
|---|---|---|
text |
TextBlock | Claude/Codex |
tool_call |
ToolCallBlock | Claude/Session |
acp_event |
AcpEventBlock | 未识别事件 |
raw_event |
AcpEventBlock | 原始事件 |
codex_reasoning |
CodexReasoningBlock | Codex |
claude_thinking |
CodexReasoningBlock | Claude |
codex_command |
CodexCommandBlock | Codex |
claude_usage |
ClaudeUsageBlock | Claude |
| 其他 | UnknownBlock | 兜底 |
6. Handler 注册机制
每个 handler 模块导出 register(ctx) 函数:
// block-handlers/codex.js
export function registerCodexHandlers(ctx) {
const { blocks, appendText } = ctx;
// ... handler 函数定义 ...
return { notificationHandlers }; // 返回要注册的 handler map
}
主文件合并注册:
// useAcpBlocks.js
const claude = registerClaudeHandlers(ctx); // → topLevelHandlers
const codex = registerCodexHandlers(ctx); // → notificationHandlers
const session = registerSessionHandlers(ctx); // → notificationHandlers
const topLevelHandlers = { ...claude.topLevelHandlers };
const notificationHandlers = {
...codex.notificationHandlers,
...session.notificationHandlers,
};
topLevelHandlers.notification = (acpEvent) => {
const handler = notificationHandlers[acpEvent?.method];
return !!handler && handler(acpEvent) === true;
};
7. 共享上下文 (ctx)
handler 模块通过 ctx 访问共享状态和工具函数:
| 属性 | 类型 | 说明 |
|---|---|---|
blocks |
Ref<[]> |
响应式 block 数组 |
appendText(text) |
Function |
追加/合并文本 block |
appendEvent(evt) |
Function |
追加原始事件 block |
streamToolBuffers |
Object |
Claude 流式工具 JSON 缓冲 |
streamBlockStates |
Object |
Claude 流式 block 状态跟踪 |
sessionToolBuffers |
Object |
Session 工具 JSON 缓冲 |
parseMaybeJson |
Function |
安全 JSON 解析 |
mergeChunk |
Function |
增量文本合并 |
extractUpdateText |
Function |
提取 update.content.text |
8. 扩展指南
新增协议
- 创建
block-handlers/<protocol>.js - 实现
export function register<Protocol>Handlers(ctx) { ... } - 在
useAcpBlocks.js中 import + 调用 register + 合并到路由表
新增 Block 类型
- 在 handler 中 push
{ type: '<new_type>', ... } - 创建
blocks/<NewType>Block.vue渲染组件 - 在
MessageBlocks.vue的blockComponentMap中添加一行映射
新增工具渲染
- 创建
tool-call/ToolCall<Name>.vue - 在
ToolCallBlock.vue的rendererMap中添加一行映射