核心概念

UI 协议

使用 Aster UI Protocol 让 AI Agent 生成富交互界面

UI 协议

Aster UI Protocol 是一套声明式 UI 协议,借鉴 Google A2UI 的设计理念,让 AI Agent 能够安全地生成和更新富交互界面。

🎯 核心理念

Safe like data, but expressive like code - 安全如数据,表达如代码。

┌─────────────────────────────────────────────────────────────┐
│                      AI Agent (Go)                          │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  工具执行 → 生成 AsterUIMessage → 发送到 Progress   │   │
│  └─────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                    Progress Channel                          │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  ui:surface_update  │  ui:data_update  │  ui:action │   │
│  └─────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                    Vue Renderer                              │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  MessageProcessor → ComponentRegistry → AsterSurface │   │
│  └─────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘

📦 消息类型

UI Protocol 支持五种消息操作:

操作类型用途事件类型
createSurface创建 Surfaceui:create_surface
surfaceUpdate更新组件定义ui:surface_update
dataModelUpdate更新数据模型ui:data_update
beginRendering开始渲染ui:surface_update
deleteSurface删除 Surfaceui:delete_surface

消息结构

// AsterUIMessage 主消息结构
type AsterUIMessage struct {
    CreateSurface   *CreateSurfaceMessage   `json:"createSurface,omitempty"`
    SurfaceUpdate   *SurfaceUpdateMessage   `json:"surfaceUpdate,omitempty"`
    DataModelUpdate *DataModelUpdateMessage `json:"dataModelUpdate,omitempty"`
    BeginRendering  *BeginRenderingMessage  `json:"beginRendering,omitempty"`
    DeleteSurface   *DeleteSurfaceMessage   `json:"deleteSurface,omitempty"`
}

// CreateSurfaceMessage 创建 Surface(A2UI 对齐)
type CreateSurfaceMessage struct {
    SurfaceID string `json:"surfaceId"`
    CatalogID string `json:"catalogId,omitempty"`  // 组件目录标识符
}

// SurfaceUpdateMessage 更新组件
type SurfaceUpdateMessage struct {
    SurfaceID  string                `json:"surfaceId"`
    Components []ComponentDefinition `json:"components"`
}

// DataModelUpdateMessage 更新数据
type DataModelUpdateMessage struct {
    SurfaceID string             `json:"surfaceId"`
    Path      string             `json:"path,omitempty"`  // JSON Pointer 路径
    Op        DataModelOperation `json:"op,omitempty"`    // add/replace/remove
    Contents  any                `json:"contents,omitempty"`
}

// DataModelOperation 数据操作类型(A2UI 对齐)
type DataModelOperation string
const (
    DataModelOperationAdd     DataModelOperation = "add"
    DataModelOperationReplace DataModelOperation = "replace"
    DataModelOperationRemove  DataModelOperation = "remove"
)

// BeginRenderingMessage 开始渲染
type BeginRenderingMessage struct {
    SurfaceID string            `json:"surfaceId"`
    Root      string            `json:"root"`              // 根组件 ID
    Styles    map[string]string `json:"styles,omitempty"`  // CSS 自定义属性
    CatalogID string            `json:"catalogId,omitempty"` // 可覆盖 createSurface 的值
}

数据模型操作(A2UI 对齐)

支持三种操作类型:

// add - 追加到数组或合并到对象
emitter.Emit(&types.ProgressUIDataUpdateEvent{
    SurfaceID: "surface",
    Path:      "/items",
    Op:        types.DataModelOperationAdd,
    Contents:  "new-item",  // 追加到数组
})

// replace - 替换值(默认行为)
emitter.Emit(&types.ProgressUIDataUpdateEvent{
    SurfaceID: "surface",
    Path:      "/user/name",
    Op:        types.DataModelOperationReplace,
    Contents:  "Bob",
})

// remove - 删除值
emitter.Emit(&types.ProgressUIDataUpdateEvent{
    SurfaceID: "surface",
    Path:      "/items/0",
    Op:        types.DataModelOperationRemove,
})

🧩 组件系统

邻接表模型

组件使用扁平列表 + ID 引用,而非嵌套树结构:

// ComponentDefinition 组件定义
type ComponentDefinition struct {
    ID        string        `json:"id"`
    Weight    string        `json:"weight,omitempty"`  // "initial" | "final"
    Component ComponentSpec `json:"component"`
}

// ComponentSpec 组件规格(联合类型)
type ComponentSpec struct {
    Text      *TextProps      `json:"Text,omitempty"`
    Button    *ButtonProps    `json:"Button,omitempty"`
    Row       *RowProps       `json:"Row,omitempty"`
    Column    *ColumnProps    `json:"Column,omitempty"`
    Card      *CardProps      `json:"Card,omitempty"`
    // ... 更多组件类型
}

标准组件目录

类别组件
布局Row, Column, Card, List, Tabs, Modal, Divider
内容Text, Image, Icon, Video, AudioPlayer
输入Button, TextField, Checkbox, Select, DateTimeInput, Slider, MultipleChoice

属性值类型

// PropertyValue 支持字面值和数据绑定
type PropertyValue struct {
    LiteralString  *string  `json:"literalString,omitempty"`
    LiteralNumber  *float64 `json:"literalNumber,omitempty"`
    LiteralBoolean *bool    `json:"literalBoolean,omitempty"`
    Path           *string  `json:"path,omitempty"`  // JSON Pointer 数据绑定
}

简化属性值格式(A2UI 兼容)

除了标准格式,还支持 A2UI 风格的简化格式:

// 标准格式
{ literalString: "Hello" }
{ literalNumber: 42 }
{ literalBoolean: true }
{ path: "/user/name" }

// A2UI 简化格式(同样支持)
"Hello"           // 等价于 { literalString: "Hello" }
42                // 等价于 { literalNumber: 42 }
true              // 等价于 { literalBoolean: true }
{ path: "/user/name" }  // 路径引用格式相同

Button Action Context(A2UI 对齐)

按钮支持 actionContext 属性,点击时自动收集表单数据:

ButtonProps{
    Label:  PropertyValue{LiteralString: ptr("提交")},
    Action: "submit-form",
    ActionContext: map[string]PropertyValue{
        "userName": {Path: ptr("/form/name")},      // 自动解析路径
        "formId":   {LiteralString: ptr("form-1")}, // 字面值
    },
}

点击时生成的 UIActionEvent 会包含解析后的 context

interface UIActionEvent {
    surfaceId: string;
    componentId: string;
    action: string;
    timestamp: string;  // ISO 8601 格式
    context: {          // 解析后的 actionContext
        userName: "Alice",
        formId: "form-1"
    };
}

🔗 数据绑定

使用 JSON Pointer 语法绑定数据:

// 数据模型
dataModel := map[string]any{
    "user": map[string]any{
        "name":  "Alice",
        "email": "alice@example.com",
    },
    "items": []string{"苹果", "香蕉", "橙子"},
}

// 组件绑定到数据
TextProps{
    Text: PropertyValue{Path: ptr("/user/name")},  // 显示 "Alice"
}

双向绑定

输入组件支持双向数据绑定:

TextFieldProps{
    Value:       PropertyValue{Path: ptr("/form/name")},
    Label:       PropertyValue{LiteralString: ptr("姓名")},
    Placeholder: PropertyValue{LiteralString: ptr("请输入姓名")},
}

🚀 Go 端使用

发送 UI 更新

import "github.com/astercloud/aster/pkg/types"

// 在工具执行中发送 UI 更新
func (t *MyTool) Execute(ctx context.Context, input map[string]any) (any, error) {
    // 获取事件发射器
    emitter := agent.GetEventEmitter(ctx)

    // 发送 Surface 更新
    emitter.Emit(&types.ProgressUISurfaceUpdateEvent{
        SurfaceID: "tool-output",
        Components: []types.ComponentDefinition{
            {
                ID: "result",
                Component: types.ComponentSpec{
                    Card: &types.CardProps{
                        Title: types.PropertyValue{LiteralString: ptr("执行结果")},
                        Children: types.ComponentArrayReference{
                            ExplicitList: []string{"content"},
                        },
                    },
                },
            },
            {
                ID: "content",
                Component: types.ComponentSpec{
                    Text: &types.TextProps{
                        Text: types.PropertyValue{LiteralString: ptr("操作成功!")},
                    },
                },
            },
        },
    })

    // 开始渲染
    emitter.Emit(&types.ProgressUISurfaceUpdateEvent{
        SurfaceID: "tool-output",
        Root:      "result",
    })

    return nil, nil
}

func ptr[T any](v T) *T { return &v }

更新数据模型

// 发送数据更新
emitter.Emit(&types.ProgressUIDataUpdateEvent{
    SurfaceID: "tool-output",
    Path:      "/status",
    Contents:  "completed",
})

在工具中间结果中包含 UI

// 扩展的 ProgressToolIntermediateEvent
type ProgressToolIntermediateEvent struct {
    Call  ToolCallSnapshot `json:"call"`
    Label string           `json:"label,omitempty"`
    Data  any              `json:"data,omitempty"`
    UI    *AsterUIMessage  `json:"ui,omitempty"`  // 可选的 UI 描述
}

🖥️ 前端使用

安装

npm install @aster/ui

基础使用

<template>
  <AsterSurface
    :surface-id="surfaceId"
    :processor="processor"
    @action="handleAction"
  />
</template>

<script setup>
import { AsterSurface } from '@aster/ui'
import { createMessageProcessor } from '@aster/ui/protocol'
import { createStandardRegistry } from '@aster/ui/protocol'

const surfaceId = 'my-surface'
const registry = createStandardRegistry()
const processor = createMessageProcessor(registry)

// 处理用户交互
function handleAction(event) {
  console.log('User action:', event)
  // 发送到 Control 通道
}
</script>

处理 WebSocket 消息

import { createMessageProcessor } from '@aster/ui/protocol'
import { createStandardRegistry } from '@aster/ui/protocol'

const processor = createMessageProcessor(createStandardRegistry())

// 连接 WebSocket
const ws = new WebSocket('ws://localhost:8080/events')

ws.onmessage = (event) => {
  const data = JSON.parse(event.data)
  
  // 处理 UI 事件
  if (data.type === 'ui:surface_update') {
    processor.processMessage({
      surfaceUpdate: data.payload
    })
  } else if (data.type === 'ui:data_update') {
    processor.processMessage({
      dataModelUpdate: data.payload
    })
  }
}

自定义组件

import { ComponentRegistry } from '@aster/ui/protocol'

const registry = createStandardRegistry()

// 注册自定义组件
registry.register('MyChart', MyChartComponent, 'my-chart')

🔒 安全性

验证错误格式(A2UI 对齐)

消息验证失败时返回标准格式的错误,便于 LLM 自我纠正:

// ValidationError 验证错误
type ProtocolError struct {
    Code      string         `json:"code"`       // "VALIDATION_FAILED"
    SurfaceID string         `json:"surfaceId"`
    Path      string         `json:"path"`       // JSON Pointer 指向错误位置
    Message   string         `json:"message"`
    Details   map[string]any `json:"details,omitempty"`
}

// 示例错误
{
    "code": "VALIDATION_FAILED",
    "surfaceId": "form-surface",
    "path": "/dataModelUpdate/contents",
    "message": "contents is required for add operation"
}

Client-to-Server 消息(A2UI 对齐)

客户端发送用户交互事件:

// ClientMessage 客户端消息
type ClientMessage struct {
    UserAction *UserActionMessage `json:"userAction,omitempty"`
    Error      *ProtocolError     `json:"error,omitempty"`
}

// UserActionMessage 用户动作
type UserActionMessage struct {
    Name              string         `json:"name"`
    SurfaceID         string         `json:"surfaceId"`
    SourceComponentID string         `json:"sourceComponentId"`
    Timestamp         string         `json:"timestamp"`  // ISO 8601
    Context           map[string]any `json:"context"`
}

白名单机制

只有注册的组件类型才能被渲染:

// 标准组件白名单
const STANDARD_COMPONENTS = [
  'Text', 'Image', 'Icon', 'Video', 'AudioPlayer',
  'Row', 'Column', 'Card', 'List', 'Tabs', 'Modal', 'Divider',
  'Button', 'TextField', 'Checkbox', 'Select', 'DateTimeInput',
  'Slider', 'MultipleChoice', 'Custom'
]

XSS 防护

所有文本内容自动清理:

// 自动转义危险字符
sanitizeText('<script>alert("xss")</script>')
// 输出: &lt;script&gt;alert("xss")&lt;/script&gt;

URL 验证

只允许安全的 URL 方案:

const ALLOWED_URL_SCHEMES = ['https:', 'http:', 'data:']
// javascript: 等危险方案会被拒绝

注册表冻结

生产模式下注册表不可变:

registry.freeze()  // 冻结后无法注册新组件

📡 流式渲染

支持在组件定义完成前开始渲染:

// 1. 先开始渲染(组件还未定义)
emitter.Emit(&types.ProgressUISurfaceUpdateEvent{
    SurfaceID: "stream-surface",
    Root:      "root",
})

// 2. 逐步添加组件
time.Sleep(500 * time.Millisecond)
emitter.Emit(&types.ProgressUISurfaceUpdateEvent{
    SurfaceID: "stream-surface",
    Components: []types.ComponentDefinition{
        {ID: "root", Component: /* ... */},
    },
})

// 3. 继续添加更多组件
time.Sleep(500 * time.Millisecond)
emitter.Emit(&types.ProgressUISurfaceUpdateEvent{
    SurfaceID: "stream-surface",
    Components: []types.ComponentDefinition{
        {ID: "content", Component: /* ... */},
    },
})

状态保持

增量更新期间自动保持:

  • 滚动位置
  • 输入焦点
  • 表单状态

🎨 主题化

使用 CSS 自定义属性:

emitter.Emit(&types.ProgressUISurfaceUpdateEvent{
    SurfaceID: "themed-surface",
    Root:      "root",
    Styles: map[string]string{
        "--aster-primary":    "#3b82f6",
        "--aster-background": "#f8fafc",
        "--aster-text":       "#1e293b",
    },
})

📊 完整示例

表单生成

func generateForm(emitter EventEmitter) {
    // 设置数据模型
    emitter.Emit(&types.ProgressUIDataUpdateEvent{
        SurfaceID: "form-surface",
        Path:      "/",
        Contents: map[string]any{
            "form": map[string]any{
                "name":    "",
                "email":   "",
                "agree":   false,
                "country": "cn",
            },
            "countries": []map[string]any{
                {"value": "cn", "label": "中国"},
                {"value": "us", "label": "美国"},
                {"value": "jp", "label": "日本"},
            },
        },
    })

    // 定义组件
    emitter.Emit(&types.ProgressUISurfaceUpdateEvent{
        SurfaceID: "form-surface",
        Components: []types.ComponentDefinition{
            {
                ID: "root",
                Component: types.ComponentSpec{
                    Card: &types.CardProps{
                        Title: types.PropertyValue{LiteralString: ptr("用户注册")},
                        Children: types.ComponentArrayReference{
                            ExplicitList: []string{"name", "email", "country", "agree", "submit"},
                        },
                    },
                },
            },
            {
                ID: "name",
                Component: types.ComponentSpec{
                    TextField: &types.TextFieldProps{
                        Value:       types.PropertyValue{Path: ptr("/form/name")},
                        Label:       types.PropertyValue{LiteralString: ptr("姓名")},
                        Placeholder: types.PropertyValue{LiteralString: ptr("请输入姓名")},
                    },
                },
            },
            {
                ID: "email",
                Component: types.ComponentSpec{
                    TextField: &types.TextFieldProps{
                        Value:       types.PropertyValue{Path: ptr("/form/email")},
                        Label:       types.PropertyValue{LiteralString: ptr("邮箱")},
                        Placeholder: types.PropertyValue{LiteralString: ptr("请输入邮箱")},
                    },
                },
            },
            {
                ID: "country",
                Component: types.ComponentSpec{
                    Select: &types.SelectProps{
                        Value:   types.PropertyValue{Path: ptr("/form/country")},
                        Options: types.PropertyValue{Path: ptr("/countries")},
                        Label:   types.PropertyValue{LiteralString: ptr("国家")},
                    },
                },
            },
            {
                ID: "agree",
                Component: types.ComponentSpec{
                    Checkbox: &types.CheckboxProps{
                        Checked: types.PropertyValue{Path: ptr("/form/agree")},
                        Label:   types.PropertyValue{LiteralString: ptr("我同意服务条款")},
                    },
                },
            },
            {
                ID: "submit",
                Component: types.ComponentSpec{
                    Button: &types.ButtonProps{
                        Label:   types.PropertyValue{LiteralString: ptr("提交")},
                        Action:  "submit-form",
                        Variant: "primary",
                    },
                },
            },
        },
    })

    // 开始渲染
    emitter.Emit(&types.ProgressUISurfaceUpdateEvent{
        SurfaceID: "form-surface",
        Root:      "root",
    })
}

🎯 最佳实践

1. 使用邻接表模型

// ✅ 推荐:扁平列表 + ID 引用
components := []ComponentDefinition{
    {ID: "root", Component: /* children: ["child1", "child2"] */},
    {ID: "child1", Component: /* ... */},
    {ID: "child2", Component: /* ... */},
}

// ❌ 不推荐:深度嵌套
// UI Protocol 不支持嵌套结构

2. 分离数据和 UI

// ✅ 推荐:数据模型独立
emitter.Emit(&types.ProgressUIDataUpdateEvent{
    SurfaceID: "surface",
    Contents:  userData,
})

// 组件通过路径绑定
TextProps{Text: PropertyValue{Path: ptr("/user/name")}}

3. 增量更新

// ✅ 推荐:只更新变化的部分
emitter.Emit(&types.ProgressUIDataUpdateEvent{
    SurfaceID: "surface",
    Path:      "/user/name",  // 只更新 name
    Contents:  "Bob",
})

// ❌ 不推荐:每次都替换整个数据模型

4. 处理用户交互

// 前端处理 action 事件
function handleAction(event: UIActionEvent) {
  switch (event.action) {
    case 'submit-form':
      // 获取表单数据
      const formData = processor.getData(event.surfaceId, '/form')
      // 发送到后端
      submitForm(formData)
      break
  }
}

📚 下一步

🔗 相关资源