Loading...
正在加载...
请稍候

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

✨步子哥 (steper) 2026年04月05日 00:37
本文档描述在 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 上畅享卓越模型能力
登录