本文档描述在 Crush 终端界面中实现 **Google A2UI(Agent to UI)** 协议的完整技术路线:以 **声明式 JSON** 为输入,由 **受信任的 TUI 组件子集(catalog)** 渲染,并支持 **用户操作回传** 至 agent 循环。
- 协议参考:[A2UI 官网](https://a2ui.org/)、[google/A2UI](https://github.com/google/A2UI)。
- Crush 架构参考:`internal/ui/AGENTS.md`、`internal/message/content.go`、`internal/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 |
| `Checkbox` | Space 切换 | 绑定 bool |
| `RadioGroup` / `Select` | 数字键或 j/k 选择 + Enter 确认 | 枚举值 |
| `Button` | Enter 触发 `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.go` 的 `ExtractMessageItems` 扩展分支。
- 按键:`MessageItem` 的 `KeyEventHandler`(见 `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` 内使用约定围栏,例如:
````markdown
```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 拼接至 `\n` 再 `json.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 → 生成事件:
```json
{
"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`:**不在子组件内开独立 `Update`**。`A2UIMessageItem.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.md` 中 `config.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. 实现 `SurfaceStore`:`Apply(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. 仅支持 `Text`、`Divider`、简单 `Column` 式垂直栈。
3. 样式接入 `styles.Styles`(标题/正文/muted)。
**验收:** 在最小 demo 程序或临时测试中输出固定字符串快照。
---
### Phase 4:Chat 集成(只读)
**步骤:**
1. 扩展 `message` 包:`A2UIContent`(或选定载体)。
2. `ExtractMessageItems`:当消息含 A2UI 部分时,追加 `A2UIMessageItem`(可与 `AssistantMessageItem` 同条消息共存或拆条,需产品决策)。
3. `A2UIMessageItem` 实现 `MessageItem`、`RawRender`、`ID`(建议 `msg.ID + ":a2ui:" + surfaceId`)。
**验收:** Mock 流式输出后,聊天列表中出现格式化 A2UI 块。
---
### Phase 5:交互与焦点
**步骤:**
1. 实现 `TextField`、`Checkbox`、`Button` 的终端编辑状态(最小 viable)。
2. `A2UIMessageItem` 实现 `KeyEventHandler`、`Focusable`(与现有 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. `README` 或 `docs` 中增加用户向说明:如何触发 A2UI、终端限制。
2. 配置项:`a2ui.enabled`、`a2ui.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 条回复还没有人回复,快来发表你的看法吧!
推荐
推荐
智谱 GLM-5 已上线
我正在智谱大模型开放平台 BigModel.cn 上打造 AI 应用,智谱新一代旗舰模型 GLM-5 已上线,在推理、代码、智能体综合能力达到开源模型 SOTA 水平。
领取 2000万 Tokens
通过邀请链接注册即可获得大礼包,期待和你一起在 BigModel 上畅享卓越模型能力