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 | 创建 Surface | ui:create_surface |
surfaceUpdate | 更新组件定义 | ui:surface_update |
dataModelUpdate | 更新数据模型 | ui:data_update |
beginRendering | 开始渲染 | ui:surface_update |
deleteSurface | 删除 Surface | ui: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 的值
}
支持三种操作类型:
// 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 风格的简化格式:
// 标准格式
{ literalString: "Hello" }
{ literalNumber: 42 }
{ literalBoolean: true }
{ path: "/user/name" }
// A2UI 简化格式(同样支持)
"Hello" // 等价于 { literalString: "Hello" }
42 // 等价于 { literalNumber: 42 }
true // 等价于 { literalBoolean: true }
{ path: "/user/name" } // 路径引用格式相同
按钮支持 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("请输入姓名")},
}
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",
})
// 扩展的 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>
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')
消息验证失败时返回标准格式的错误,便于 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"
}
客户端发送用户交互事件:
// 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'
]
所有文本内容自动清理:
// 自动转义危险字符
sanitizeText('<script>alert("xss")</script>')
// 输出: <script>alert("xss")</script>
只允许安全的 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",
})
}
// ✅ 推荐:扁平列表 + ID 引用
components := []ComponentDefinition{
{ID: "root", Component: /* children: ["child1", "child2"] */},
{ID: "child1", Component: /* ... */},
{ID: "child2", Component: /* ... */},
}
// ❌ 不推荐:深度嵌套
// UI Protocol 不支持嵌套结构
// ✅ 推荐:数据模型独立
emitter.Emit(&types.ProgressUIDataUpdateEvent{
SurfaceID: "surface",
Contents: userData,
})
// 组件通过路径绑定
TextProps{Text: PropertyValue{Path: ptr("/user/name")}}
// ✅ 推荐:只更新变化的部分
emitter.Emit(&types.ProgressUIDataUpdateEvent{
SurfaceID: "surface",
Path: "/user/name", // 只更新 name
Contents: "Bob",
})
// ❌ 不推荐:每次都替换整个数据模型
// 前端处理 action 事件
function handleAction(event: UIActionEvent) {
switch (event.action) {
case 'submit-form':
// 获取表单数据
const formData = processor.getData(event.surfaceId, '/form')
// 发送到后端
submitForm(formData)
break
}
}