静态缓存页面 · 查看动态版本 · 登录
智柴论坛 登录 | 注册
← 返回列表

Crush A2UI 支持方案(TUI 子集 Catalog 渲染器)

✨步子哥 @steper · 2026-04-05 00:37 · 25浏览

本文档描述在 Crush 终端界面中实现 Google A2UI(Agent to UI) 协议的完整技术路线:以 声明式 JSON 为输入,由 受信任的 TUI 组件子集(catalog) 渲染,并支持 用户操作回传 至 agent 循环。

  • 协议参考:A2UI 官网google/A2UI
  • Crush 架构参考:internal/ui/AGENTS.mdinternal/message/content.gointernal/ui/chat/
---

1. 目标与非目标

1.1 目标

1. 协议对齐:能解析并应用 A2UI 消息流中的核心概念(Surface、组件列表、数据模型、根节点、版本字段),与 v0.8(稳定)为主,预留 v0.9(草案)适配层。 2. 安全渲染:仅渲染 catalog 中显式允许 的组件类型;拒绝未知组件、超限深度、非法路径绑定。 3. 终端可用:在 Bubble Tea 体系内,以 字符串渲染 + 可选交互 的方式呈现表单类界面(非 Web 级像素布局)。 4. 可演进:解析、布局、交互、与 agent 的耦合分层清晰,便于后续扩展组件或对接外部渲染器。

1.2 非目标(首版可不实现)

  • 完整实现 A2UI 全量标准 catalog(含复杂图表、富媒体、精确主题 token)。
  • 与 Web/React/Flutter 渲染器 像素级一致
  • 在终端内嵌 WebView 或执行任意脚本。
  • 替代现有 Markdown 主流程;A2UI 作为 补充内容类型 存在。
---

2. A2UI 与 Crush 的映射关系

A2UI 概念Crush 侧落点
Message(JSON 对象流 / JSONL)助手流式输出中的 独立通道结构化 ContentPart(见 §5)。
Surface会话内 surfaceId → 运行时状态(组件表、数据模型、root、版本)。
Catalog仓库内 TUI Catalog 清单(Go 常量 + JSON Schema 子集或手写校验)。
Component 树内存中的 有向图(id → component),布局阶段展开为 终端行块
Data model / path 绑定子集 JSON Pointer 或 v0.9 的 path 语义,映射到 map[string]any 或 typed 视图。
User action转为 结构化用户消息专用 tool result,由 QueryEngine 下一轮消费。
---

3. TUI Catalog 子集(建议首版)

下列组件与 A2UI 常见命名对齐;实现时以 官方 schema/文档 为准做字段级对照,下列为 能力规划

3.1 只读 / 结构

组件TUI 表现备注
Text纯文本行,支持 usageHint 映射为标题/加粗/次要样式宽度受 cappedMessageWidth 约束
Divider单行分隔符(Unicode 或 styled line)
Row / Column(若协议或扩展中有容器语义)垂直/水平拼接 lipgloss 块若无原生名,可用 Card 内嵌或自定义 Stack 扩展(仅 TUI catalog)

3.2 表单与交互(核心)

组件TUI 表现输入验证
TextField单行输入;焦点内接收按键最大长度、可选 pattern
MultilineTextField简化为「多行模板 + 外部编辑器打开」或仅单行 MVP首版可降级为 TextField
CheckboxSpace 切换绑定 bool
RadioGroup / Select数字键或 j/k 选择 + Enter 确认枚举值
ButtonEnter 触发 action与 focus 环集成
DateTimeInput降级:ISO8601 文本输入 + 格式校验提示无图形日历为预期限制

3.3 显式不支持(首版拒绝并记录)

  • 任意 HTML/Web 专用组件。
  • 需精确触控手势的控件。
  • 未在 TUI catalog 注册的 component 类型。
策略:解析器遇到未知类型 → 跳过该子树并输出一行可诊断警告(开发模式可 verbose),避免整屏失败。

---

4. 总体架构

┌─────────────────────────────────────────────────────────────────┐
│  LLM / Provider 流式输出                                         │
└───────────────────────────────┬─────────────────────────────────┘
                                │
                     ┌──────────▼──────────┐
                     │  A2UI 分帧器        │  JSONL / 增量 chunk 解析
                     │  (internal/a2ui)    │
                     └──────────┬──────────┘
                                │
              ┌─────────────────┼─────────────────┐
              │                 │                 │
     ┌────────▼────────┐ ┌──────▼──────┐ ┌───────▼────────┐
     │ Surface 运行时   │ │ 数据模型     │ │ 布局 + 样式    │
     │ surface store   │ │ path 绑定   │ │ lipgloss 块   │
     └────────┬────────┘ └──────┬──────┘ └───────┬────────┘
              │                 │                 │
              └─────────────────┼─────────────────┘
                                │
                     ┌──────────▼──────────┐
                     │  A2UIMessageItem    │  list.Item / MessageItem
                     │  + KeyEventHandler  │  与 Chat 列表集成
                     └──────────┬──────────┘
                                │
                     ┌──────────▼──────────┐
                     │  UI 主模型        │  焦点路由、回传 cmd
                     │  model/ui.go      │
                     └───────────────────┘

建议新增 Go 包(名称可调整):

  • internal/a2ui/协议无关核心 — 消息解析、校验、Surface 状态机、path 读写。
  • internal/ui/a2ui/视图层 — TUI 布局、组件渲染、与 styles.Styles 集成。
  • (可选)internal/a2ui/catalog/tui_v1.json:机器可读 catalog 描述,供测试与文档生成。
与现有代码的锚点:
  • 消息:internal/message 中新增 ContentPart 实现或并行存储策略(§5)。
  • 聊天项:internal/ui/chat/messages.goExtractMessageItems 扩展分支。
  • 按键:MessageItemKeyEventHandler(见 messages.go)与 UI.Update 焦点路由。
---

5. 消息载体设计(关键决策)

两种主流方案,推荐 A + 后期 B

5.1 方案 A:专用 ContentPart(推荐)

新增例如 A2UIContent struct { Raw string; Version string; SurfaceID string },或按帧存储 []A2UIFrame

  • 优点:与 TextContent 分离,历史记录清晰;持久化/sqlc 扩展路径明确。
  • 缺点:需在 fantasy/各 provider 的流式处理中 识别并剥离 A2UI 块(若模型混排文本与 JSONL)。

5.2 方案 B:文本围栏(过渡)

助手 TextContent 内使用约定围栏,例如:

a2ui { "surfaceUpdate": ... }
  • 优点:无需改 provider 协议层即可试验。
  • 缺点:模型易破坏围栏;与流式切片拼接复杂;不利于二进制安全边界。
建议路径:Phase 1 用 B 做 spike;Phase 2 起迁移到 A,并在系统提示词中规定 A2UI 独占通道工具输出

5.3 方案 C:专用 Tool(可选)

定义内置 tool a2ui_emit,参数为 JSONL 字符串,结果由 UI 拦截不展示原始 JSON。

  • 优点:与 tool 循环天然一致,权限模型可复用。
  • 缺点:占用 tool 轮次;对「纯对话输出 UI」的模型不友好。
结论:长期以 A 为主C 可作为企业场景或「强制结构化」选项。

---

6. 解析与流式语义

6.1 输入形态

  • 支持 JSONL:每行一个完整 JSON 对象(与官方示例一致)。
  • 支持 不完整行缓冲:流式 chunk 拼接至 \njson.Unmarshal

6.2 状态机(按 v0.8 概念命名,实现时对照官方)

1. 收到 surfaceUpdate(或 v0.9 的 updateComponents)→ 合并组件表(按 id 覆盖)。 2. 收到 dataModelUpdate / updateDataModel → 合并数据到对应 surface。 3. 收到 beginRendering / createSurface → 标记可渲染,记录 root。 4. surfaceId 缺失或重复创建 → 校验规则在 §8。

6.3 与助手「正在输入」的 UI

  • 未完成 beginRendering 前:可显示 「正在构建界面…」 占位(类似 tool pending)。
  • 允许 增量 更新:每帧重算布局缓存失效策略(§11)。
---

7. 布局引擎(TUI)

7.1 原则

  • 块级布局:每个组件渲染为 lipgloss 字符串,父级做 JoinVertical / JoinHorizontal
  • 宽度:统一使用现有 cappedMessageWidth(width)(见 chat/messages.go),子组件再内缩进。
  • 焦点顺序:DFS 或按 YAML 声明的 tabIndex(若协议无则按组件数组顺序)。

7.2 缓存

  • A2UIMessageItem 嵌入 cachedMessageItem 模式(同 user.go / assistant.go)。
  • 失效条件:任一帧导致 surface 版本递增、数据模型变更、终端宽度变化。
---

8. 安全与校验

1. 组件白名单:仅 catalog 内类型;json.RawMessage 延迟解析具体字段。 2. 深度与数量上限:最大组件数、最大嵌套(通过 child id 解析)、最大字符串长度。 3. path 约束:禁止 .. 或越界写入;仅允许绑定到 surface 私有数据树根键。 4. action 名[a-zA-Z0-9_.-]+ 长度上限;payload 仅 JSON 对象且键白名单(若协议允许 payload)。 5. 日志:拒绝原因写入审计(若启用 permission v2 审计通道),开发日志大写首句(符合项目 log 规范)。

---

9. 用户操作回传(闭环)

9.1 事件模型

用户聚焦 Button 按 Enter → 生成事件:

{
  "type": "a2ui_action",
  "surface_id": "booking",
  "action": "confirm_booking",
  "context": { "path": "/booking", "snapshot": { ... } }
}

9.2 注入 QueryEngine 的方式(二选一或配置)

1. 合成 User 消息:文本为简短说明 + 上述 JSON 围栏,系统提示要求 coder 将 JSON 视为机器事件。 2. 内部通道:扩展 SendMsg 或等价结构,不经过用户可见文本,直接交给 QueryEngine 的下一轮(需改 agent 层,侵入性较高)。

建议:首版用 1(实现快);迭代 2(体验与多模态一致)。

9.3 与焦点的关系

遵循 internal/ui/AGENTS.md不在子组件内开独立 UpdateA2UIMessageItem.HandleKeyEvent 返回 tea.Cmd,由 UI.Update 聚合后触发「发送事件」命令。

---

10. 持久化与历史

  • ContentPart 持久化:扩展 SQLite 序列化(与现有 Parts 存储方式一致,需跟 internal/db 迁移对齐)。
  • 会话重载:从 DB 还原 surface 状态;若仅存储原始帧,启动时重放 状态机重建运行时。
---

11. 测试策略

层级内容
单元测试JSONL 分帧、状态机合并、path 读写、白名单拒绝
金样 / 快照固定 surface 渲染字符串(注意 ANSI 稳定性,可 Strip 后比)
集成测试Mock provider 返回 A2UI 流,验证 MessageItem 数量与按键回传
使用项目既有 mock provider 模式(AGENTS.mdconfig.UseMockProviders)。

---

12. Step-by-step 实施阶段(完整步骤)

以下每阶段结束应有 可运行二进制 + 可演示路径 + 测试

Phase 0:规格冻结与对齐(1 周量级,视人力)

步骤:

1. 阅读 A2UI v0.8 官方消息类型列表,建立 字段对照表(Google Sheet 或 docs/a2ui-field-mapping.md)。 2. 冻结 TUI Catalog v1 列表(§3)与 明确不支持 集合。 3. 选定 消息载体(§5):确认采用 A 或 A+C。

验收: 评审通过的 markdown + 样例 JSONL(含错误样例)。

---

Phase 1:解析包与状态机(无 UI)

步骤:

1. 新建 internal/a2ui,实现 Decoder:字节流 → []Message 或强类型 sum type。 2. 实现 SurfaceStoreApply(msg) error,覆盖 v0.8 核心消息。 3. 单元测试:官方文档中的最小 booking 示例重放后,root 与数据模型一致。

验收: go test ./internal/a2ui/... 全绿,不依赖 Bubble Tea。

---

Phase 2:校验与白名单

步骤:

1. 实现 Catalog 接口:ValidateComponent(type string, raw json.RawMessage) error。 2. 默认 TUI catalog 注册表;未知类型策略(跳过 + 诊断)。 3. 限制深度、组件数、字符串长度。

验收: Fuzz 或 table-driven 测试覆盖恶意/畸形输入。

---

Phase 3:只读渲染(无交互)

步骤:

1. 新建 internal/ui/a2ui/renderer.go:输入 Surface + width → string。 2. 仅支持 TextDivider、简单 Column 式垂直栈。 3. 样式接入 styles.Styles(标题/正文/muted)。

验收: 在最小 demo 程序或临时测试中输出固定字符串快照。

---

Phase 4:Chat 集成(只读)

步骤:

1. 扩展 message 包:A2UIContent(或选定载体)。 2. ExtractMessageItems:当消息含 A2UI 部分时,追加 A2UIMessageItem(可与 AssistantMessageItem 同条消息共存或拆条,需产品决策)。 3. A2UIMessageItem 实现 MessageItemRawRenderID(建议 msg.ID + ":a2ui:" + surfaceId)。

验收: Mock 流式输出后,聊天列表中出现格式化 A2UI 块。

---

Phase 5:交互与焦点

步骤:

1. 实现 TextFieldCheckboxButton 的终端编辑状态(最小 viable)。 2. A2UIMessageItem 实现 KeyEventHandlerFocusable(与现有 list 焦点模型对齐)。 3. 在 UI.Update 中,当焦点在 main 且选中项为 A2UI 时转发按键。

验收: 用户可改字段并点击按钮,内存中 action 被捕获(日志或测试桩)。

---

Phase 6:回传 agent

步骤:

1. 定义 a2ui_action JSON schema(内部)。 2. tea.Cmd 发送:调用现有「发送消息」路径,注入合成用户内容或内部通道。 3. 更新 internal/agent 系统提示词模板:说明如何响应 A2UI 事件(文言文模板由项目规范决定)。

验收: 端到端:按钮 → 下一轮 assistant 可见事件并生成回复。

---

Phase 7:流式与性能

步骤:

1. 在 QueryEngine / 流式回调处挂载 A2UI 分帧器,边收边 Apply。 2. 渲染缓存与防抖:同一 tick 多帧合并一次 Invalidate。 3. 大 surface 懒渲染:仅计算可见区域(若 list 已虚拟化,单 item 高度估算需校准)。

验收: 大 JSONL 流下 UI 不卡顿(主观 + go test 基准可选)。

---

Phase 8:持久化

步骤:

1. DB migration:消息 parts 支持新类型。 2. 加载历史会话时重放 A2UI 帧或存储快照(二选一:快照简单,重放更省空间)。

验收: 重启 Crush 后同一 session 中 A2UI 状态一致。

---

Phase 9:v0.9 适配(可选)

步骤:

1. 在解码层根据 version 字段分发到 v0.8 / v0.9 规范化内部 IR(中间表示)。 2. 渲染与状态机仅依赖 IR,降低协议变动成本。

验收: v0.9 文档中的示例可渲染同等 TUI 效果(允许组件降级)。

---

Phase 10:文档与配置

步骤:

1. READMEdocs 中增加用户向说明:如何触发 A2UI、终端限制。 2. 配置项:a2ui.enableda2ui.max_components 等(走 internal/config 模式)。

验收: 新贡献者可按文档跑通 mock 场景。

---

13. 风险与缓解

风险缓解
模型输出非合法 JSONL强提示词 + 可选 tool 通道;解析失败降级为原始文本块
终端无真正「控件」导致 UX 差文档明确降级策略;重要表单可走 dialog 模式
焦点与 textarea 冲突严格沿用 uiFocusMain / uiFocusEditor 路由规则
协议快速迭代IR 层 + 版本字段分发(§Phase 9)
---

14. 小结

本方案以 A2UI 协议为输入Crush 自有 TUI catalog 为信任边界,分阶段落地:解析与状态机 → 只读渲染 → 聊天集成 → 交互与回传 → 流式与持久化。该路线与现有 Message / MessageItem / 中央 UI 路由模式兼容,且保留未来将同一 A2UI 流 旁路到桌面/Web 客户端 的可能性。

讨论回复 (0)