# 消息渲染架构 > 版本: 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)` 函数: ```js // block-handlers/codex.js export function registerCodexHandlers(ctx) { const { blocks, appendText } = ctx; // ... handler 函数定义 ... return { notificationHandlers }; // 返回要注册的 handler map } ``` 主文件合并注册: ```js // 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. 扩展指南 ### 新增协议 1. 创建 `block-handlers/.js` 2. 实现 `export function registerHandlers(ctx) { ... }` 3. 在 `useAcpBlocks.js` 中 import + 调用 register + 合并到路由表 ### 新增 Block 类型 1. 在 handler 中 push `{ type: '', ... }` 2. 创建 `blocks/Block.vue` 渲染组件 3. 在 `MessageBlocks.vue` 的 `blockComponentMap` 中添加一行映射 ### 新增工具渲染 1. 创建 `tool-call/ToolCall.vue` 2. 在 `ToolCallBlock.vue` 的 `rendererMap` 中添加一行映射