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. 扩展指南

新增协议

  1. 创建 block-handlers/<protocol>.js
  2. 实现 export function register<Protocol>Handlers(ctx) { ... }
  3. 在 useAcpBlocks.js 中 import + 调用 register + 合并到路由表

新增 Block 类型

  1. 在 handler 中 push { type: '<new_type>', ... }
  2. 创建 blocks/<NewType>Block.vue 渲染组件
  3. 在 MessageBlocks.vue 的 blockComponentMap 中添加一行映射

新增工具渲染

  1. 创建 tool-call/ToolCall<Name>.vue
  2. 在 ToolCallBlock.vue 的 rendererMap 中添加一行映射