Files
proxycast/docs/design/a2ui-persistence.md
T
2026-03-18 10:39:02 +08:00

8.4 KiB
Raw Permalink Blame History

A2UI 表单数据持久化设计

问题背景

当前 A2UI 表单数据只存在于前端内存中,页面刷新或切换话题后会丢失。用户填写的表单数据需要持久化到数据库,以便重新进入时能够恢复。

数据分析

需要持久化的数据

  1. A2UI 响应结构 (A2UIResponse)

    • id: 响应 ID
    • components: 组件列表(包含表单字段定义)
    • root: 根组件 ID
    • data: 初始数据模型
    • submitAction: 提交动作配置
  2. 用户填写的表单数据 (A2UIFormData)

    • 键值对形式,key 是组件 ID,value 是用户输入的值
    • 例如:{ "scene": "我和同事说", "feeling": "对 go 很陌生" }
  3. 表单状态

    • submitted: 是否已提交
    • submittedAt: 提交时间
    • submittedData: 提交时的数据快照

设计方案

方案 A:扩展 agent_messages 表(推荐)

在现有 agent_messages 表中添加字段存储 A2UI 相关数据:

-- 添加 A2UI 相关字段
ALTER TABLE agent_messages ADD COLUMN a2ui_response_json TEXT;
ALTER TABLE agent_messages ADD COLUMN a2ui_form_data_json TEXT;
ALTER TABLE agent_messages ADD COLUMN a2ui_submitted INTEGER DEFAULT 0;
ALTER TABLE agent_messages ADD COLUMN a2ui_submitted_at TEXT;

优点:

  • 数据与消息紧密关联,查询简单
  • 不需要额外的表和外键
  • 迁移简单

缺点:

  • 消息表字段增多
  • 如果一条消息有多个 A2UI 表单,需要用 JSON 数组存储

方案 B:独立的 A2UI 表单表

创建独立的表存储 A2UI 表单数据:

CREATE TABLE IF NOT EXISTS a2ui_forms (
    id TEXT PRIMARY KEY,
    message_id INTEGER NOT NULL,
    session_id TEXT NOT NULL,
    a2ui_response_json TEXT NOT NULL,
    form_data_json TEXT DEFAULT '{}',
    submitted INTEGER DEFAULT 0,
    submitted_at TEXT,
    created_at INTEGER NOT NULL,
    updated_at INTEGER NOT NULL,
    FOREIGN KEY (message_id) REFERENCES agent_messages(id) ON DELETE CASCADE,
    FOREIGN KEY (session_id) REFERENCES agent_sessions(id) ON DELETE CASCADE
);

CREATE INDEX IF NOT EXISTS idx_a2ui_forms_message ON a2ui_forms(message_id);
CREATE INDEX IF NOT EXISTS idx_a2ui_forms_session ON a2ui_forms(session_id);

优点:

  • 数据结构清晰
  • 支持一条消息多个表单
  • 便于单独查询和管理表单数据

缺点:

  • 需要额外的表和外键
  • 查询时需要 JOIN

推荐方案:方案 B

考虑到:

  1. 一条 AI 消息可能包含多个 A2UI 表单
  2. 表单数据需要独立更新(用户填写时实时保存)
  3. 未来可能需要表单历史版本、表单模板等功能

数据流设计

┌─────────────────────────────────────────────────────────────────┐
│                        前端 (React)                              │
├─────────────────────────────────────────────────────────────────┤
│  StreamingRenderer                                               │
│       │                                                          │
│       ▼                                                          │
│  A2UIRenderer ──────► onFormChange() ──────► 防抖保存            │
│       │                                                          │
│       ▼                                                          │
│  onSubmit() ──────────────────────────────► 提交表单             │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│                     Tauri Commands                               │
├─────────────────────────────────────────────────────────────────┤
│  save_a2ui_form_data(form_id, form_data)                        │
│  submit_a2ui_form(form_id, form_data)                           │
│  get_a2ui_forms_by_session(session_id)                          │
│  get_a2ui_form_by_message(message_id)                           │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│                     Rust Backend                                 │
├─────────────────────────────────────────────────────────────────┤
│  A2UIFormService                                                 │
│    - save_form_data()                                           │
│    - submit_form()                                              │
│    - get_forms_by_session()                                     │
│    - get_form_by_message()                                      │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│                     SQLite Database                              │
├─────────────────────────────────────────────────────────────────┤
│  a2ui_forms 表                                                   │
└─────────────────────────────────────────────────────────────────┘

前端改动

  1. A2UIRenderer 组件

    • 添加 formId prop(从后端获取或生成)
    • 添加 initialFormData prop(从后端加载)
    • 添加 onFormChange 回调(防抖保存)
  2. useAgentChatUnified / useAsterAgentChat Hook

    • switchTopic 时加载该会话的所有 A2UI 表单数据
    • 将表单数据与消息关联
  3. StreamingRenderer 组件

    • 传递表单数据给 A2UIRenderer

后端改动

  1. 数据库 Schema

    • 添加 a2ui_forms 表
  2. Tauri Commands

    • save_a2ui_form_data: 保存表单数据(防抖调用)
    • submit_a2ui_form: 提交表单
    • get_a2ui_forms_by_session: 获取会话的所有表单
    • create_a2ui_form: 创建新表单记录
  3. 消息保存逻辑

    • 保存 AI 消息时,解析 A2UI 内容并创建表单记录

实现步骤

Phase 1: 数据库层

  1. 添加 a2ui_forms 表到 schema.rs
  2. 创建 A2UIFormDao

Phase 2: 后端服务

  1. 创建 A2UIFormService
  2. 添加 Tauri Commands

Phase 3: 前端集成

  1. 添加 API 调用函数
  2. 修改 A2UIRenderer 支持数据持久化
  3. 修改 useAgentChatUnified / useAsterAgentChat 加载表单数据

Phase 4: 测试和优化

  1. 测试表单数据保存和恢复
  2. 优化防抖保存策略
  3. 处理边界情况(网络错误、并发等)

注意事项

  1. 防抖保存:用户输入时不要每次都保存,使用 500ms 防抖
  2. 乐观更新:先更新 UI,后台异步保存
  3. 错误处理:保存失败时提示用户,但不阻塞操作
  4. 数据清理:删除会话时级联删除表单数据