@boteteam/a2ui-render
v0.0.39
Published
博特-A2UI 协议渲染引擎
Readme
A2UI 业务接入指南
本文档面向业务方,说明如何使用 @boteteam/a2ui-render 渲染 A2UI 页面,并覆盖常见接入场景:
- 基础渲染
- 协议版本切换
- 消息更新与局部刷新
- 事件回调与自定义动作
- 样式覆盖(细则见
styleVars.md) - 内置文案国际化
- 支持 customComponents 自定义组件注册
1. 推荐接入方式
1.1 安装
npm install @boteteam/a2ui-render1.2 基础使用
import React from 'react';
import { BaseRenderer, normalizeToLitProtocolMessages, type A2UIMessage } from '@boteteam/a2ui-render';
const rawMessages: A2UIMessage[] = [
{
beginRendering: {
surfaceId: '@default',
root: 'root',
},
},
{
surfaceUpdate: {
surfaceId: '@default',
components: [
{
id: 'root',
component: 'Text',
text: 'Hello A2UI',
variant: 'h2',
},
],
},
},
];
export default function A2uiPage() {
const messages = normalizeToLitProtocolMessages(rawMessages);
return <BaseRenderer messages={messages} protocolVersion="0.8" />;
}2. 消息格式与渲染原则
2.1 运行时标准
BaseRenderer 运行时遵循 @a2ui/lit 和 @a2ui/web_core 消息协议:
beginRenderingsurfaceUpdatedataModelUpdate
2.2 扁平写法兼容
业务常见的扁平结构可以直接传入,先经 normalizeToLitProtocolMessages 转换再渲染。
转换器已内置如下兼容能力:
component: "Text"转component: { Text: {...} }children: []转children: { explicitList: [] }text: "abc"转text: { literalString: "abc" }variant映射为usageHintalign映射为alignmentjustify映射为distributionaction.event展平为actiontext.call formatDate执行后转literalStringIcon.icon自动映射为协议要求的Icon.name(literalString)
2.3 协议版本选择
BaseRenderer 支持通过 protocolVersion 明确指定运行时分支:
protocolVersion="0.8"使用 0.8 兼容分支protocolVersion="0.9"使用 0.9 分支- 不传时默认使用
0.8
2.4 v0.9 消息与 version 字段
- v0.9 传输形态(
createSurface/updateComponents/updateDataModel/deleteSurface)不要求每条消息带version: "v0.9";由BaseRenderer的protocolVersion决定如何解析。 BaseRenderer会在内部对传入的messages调用normalizeToLitProtocolMessages(messages, protocolVersion);与protocolVersion不一致时会报错并展示解析面板(如「A2UI 消息协议不匹配」)。- 业务侧若仍手动调用
normalizeToLitProtocolMessages:建议第二参数与protocolVersion一致。不传第二参数时:若整批均为 v0.9 信封则按0.9推断,否则按0.8推断。 - v0.9 下
@a2ui/web_core对纯字符串的DynamicString不做${...}插值;与官方 Playground 示例一致时,本包在normalizeToLitProtocolMessages的 0.9 路径中会把含真实插值片段的字符串自动包成formatString调用(已写\\${的转义文案不会误包)。 action:协议要求{ "event": { "name", "context?" } }或{ "functionCall": … }。若误写为顶层name/context,SurfaceModel.dispatchAction不会emit,按钮点击无回调。normalizeToLitProtocolMessages会把该扁平形态自动包成event;Button.primary会映射为variant:primary|default,以通过 strict schema。
示例:
<BaseRenderer
messages={messages}
protocolVersion="0.9"
/>注意事项:
- 单个页面会话内不要来回切换
0.8与0.9 - 若需要切换版本,建议刷新页面后再渲染
- 推荐通过 URL 参数或页面级配置固定当前页面协议版本
3. 如何更新消息与局部刷新
3.1 建议模式
建议拆分为两类消息:
- 结构消息(
beginRendering和surfaceUpdate)通常固定 - 数据消息(
dataModelUpdate)按业务变化更新
3.2 局部数据刷新示例
import React, { useMemo, useState } from 'react';
import { BaseRenderer, normalizeToLitProtocolMessages, type A2UIMessage } from '@boteteam/a2ui-render';
const baseMessages: A2UIMessage[] = [
{ beginRendering: { surfaceId: '@default', root: 'root' } },
{
surfaceUpdate: {
surfaceId: '@default',
components: [
{ id: 'root', component: 'Text', text: { path: '/status' }, variant: 'h2' },
],
},
},
];
export default function A2uiStatusPage() {
const [status, setStatus] = useState('On Time');
const rawMessages = useMemo<A2UIMessage[]>(() => ([
...baseMessages,
{
dataModelUpdate: {
surfaceId: '@default',
path: '/',
contents: [{ key: 'status', valueString: status }],
},
},
]), [status]);
const messages = useMemo(() => normalizeToLitProtocolMessages(rawMessages), [rawMessages]);
return (
<>
<button type="button" onClick={() => setStatus('Delayed')}>Set Delayed</button>
<BaseRenderer messages={messages} />
</>
);
}3.3 注意事项
- 每次更新
messages时,需包含完整上下文消息集合 - 同一页面内保持
surfaceId一致 - 数据更新优先使用
dataModelUpdate,避免频繁重建组件树
4. 如何处理自定义事件
4.1 事件定义
在组件里定义 action,例如按钮点击:
{
"id": "refresh-btn",
"component": "Button",
"child": "refresh-text",
"action": {
"name": "refresh",
"context": {
"source": "flight-card"
}
}
}4.2 事件回调
BaseRenderer 通过 onAction 统一回调业务层:
<BaseRenderer
messages={messages}
onAction={(action) => {
if (action.name === 'refresh') {
// 业务刷新逻辑
}
}}
/>其中 action 结构:
name动作名context上下文对象
5. 如何覆盖样式
5.1 推荐入口
当前包提供两种样式入口:
className:业务命名空间,便于在 Less 中写局部选择器覆盖。styleVars:CSS 自定义属性字典,写入 渲染根 与a2ui-surface宿主,并在各组件 shadow 内注入本包维护的覆盖样式,使var(--a2ui-*)能稳定解析。
<BaseRenderer
messages={messages}
className="biz-a2ui-theme"
styleVars={{
'--a2ui-text-h2-size': '24px',
'a2ui-color-primary': '#1677ff',
}}
/>styleVars 的 key 支持 --token 与 token(会自动补成 --token)。不建议把普通 CSS 属性如 color 塞进 styleVars,非变量样式放在业务 Less 即可。
5.2 生效范围:0.8 与 v0.9 的共同点与差异
共同点
DEFAULT_A2UI_LIT_STYLE_VARS中的键及你在styleVars里追加的a2ui-*变量名,两种协议下都会参与合并并注入宿主。a2ui-color-primary会同步到--p-50,与内置回退链一致。- 本包对 Text、Row、Column、Card、Button、TextField、CheckBox、DateTimeInput、Slider、MultipleChoice 等做的 shadow 样式覆盖,在 0.8 与 v0.9 下同一套实现都会执行;选择器同时匹配
a2ui-*与a2ui-basic-*,不因协议换 tag 而失效。
仅 v0.9 额外生效的路径
- 在
a2ui-surface的:host上注入 字号与圆角桥接,把a2ui-text-h1-size等映射为官方 Lit 使用的--a2ui-font-size-2xl~--a2ui-font-size-xs及--a2ui-border-radius,因此同一套「规范字号变量」会同时影响 本包 shadow 与 官方 basic 组件内部 typography。 - 官方主题大量使用
--a2ui-color-on-*等语义色,可通过styleVars在宿主上覆盖并继承进深层 shadow;0.8 侧 Lit 包中一般不出现这组命名,多为 v0.9 对齐明暗时使用。
不建议依赖
- 上游生成样式中的
--_a2ui-*内部槽位,仅作临时调试,勿当作对外契约。
完整清单
- 哪些变量在两种协议下均由本包消费、按组件列出每个 shadow 里出现的
var(--a2ui-*)、v0.8 与 v0.9 分节说明、以及 未做 shadow 覆盖的内置组件,见同目录styleVars.md(与shadow/*.css、defaultA2uiLitStyleVars.ts同步维护)。
6. 自定义组件能力
当前版本以官方 @a2ui/lit 标准组件为主,同时支持在渲染入口通过 customComponents 注入业务自定义组件。
自定义组件按当前这套机制,必须是 Web Component 构造器,本质上就是 HTMLElement 子类(或等价的自定义元素类)。
6.1 业务侧约定
- 将自定义组件名称统一前缀,例如
BizChart、BizTag - 组件配置保持纯 JSON 可序列化
- 交互通过
action回传,不在组件配置内嵌函数
6.2 接入方式
在渲染入口传入 customComponents,key 为协议组件名,value 支持两种写法:
- 直接传自定义元素构造器
- 传配置对象:
{ elementCtor, tagName, schema }
示例:
import { BaseRenderer, type A2UICustomComponentRegistry } from '@boteteam/a2ui-render';
const customComponents: A2UICustomComponentRegistry = {
// 写法一:直接传构造器
BizChart: BizChartElement,
// 写法二:传完整配置
BizTag: {
elementCtor: BizTagElement,
tagName: 'biz-tag',
schema: {
type: 'object',
},
},
};
<BaseRenderer
messages={messages}
customComponents={customComponents}
/>6.2.1 接入步骤小结
- 实现类:自定义元素类继承
HTMLElement(或等价),在类上完成展示与交互逻辑。 - 注册表:构造
A2UICustomComponentRegistry,key 与协议 JSON 里的component字符串完全一致(如"BizTag")。 - 挂载渲染:
<BaseRenderer messages={…} protocolVersion="0.8" | "0.9" customComponents={customComponents} onAction={…} />。 - 消息体:在
surfaceUpdate/updateComponents的components数组里为对应id声明component及业务字段;保持可 JSON 序列化。 - 动作:业务侧在
onAction中统一处理;自定义元素内通过a2uiaction上报(见下文)。
6.3 标准实现约定
是的,业务侧自定义组件建议按下面约定实现,这也是当前 BaseRenderer 的标准接入方式。
A 参数读取
- 推荐优先读取
this.componentProps,它是渲染引擎统一归一化后的参数对象 - 归一化会自动处理
literalString等协议值包装,减少业务组件的重复转换逻辑 - 仍可读取同名属性(如
this.subtitle),但建议统一走componentProps提升稳定性
B 事件上报
- 通过
dispatchEvent(new CustomEvent('a2uiaction', ...))向引擎抛出业务动作 - 事件建议设置
bubbles: true、composed: true,确保能穿过 Shadow DOM 被外层a2ui-surface监听 detail推荐扁平结构:{ name: string, context?: Record<string, unknown> }(与 README 示例一致)。mergeLitActionPayload同时兼容 v0.9 官方消息里常见的detail.action包裹形态;业务自定义组件优先使用扁平结构即可。
C 最小示例
class BizPromoCardElement extends HTMLElement {
componentProps: any;
connectedCallback() {
this.renderCard();
this.onclick = (ev: MouseEvent) => {
const target = ev.target as HTMLElement | null;
const isTitle = Boolean(target?.closest?.('[data-biz-title-action="1"]'));
if (!isTitle) return;
this.dispatchEvent(new CustomEvent('a2uiaction', {
detail: {
name: this.componentProps?.actionName || 'biz_promo_click',
context: {
source: 'BizPromoCard',
title: this.componentProps?.title,
badge: this.componentProps?.badge,
},
},
bubbles: true,
composed: true,
}));
};
}
private renderCard() {
const title = this.componentProps?.title ?? 'Custom Card';
const subtitle = this.componentProps?.subtitle ?? '';
this.innerHTML = `
<div>
<strong data-biz-title-action="1">${title}</strong>
<div>${subtitle}</div>
</div>
`;
}
}6.4 协议 v0.8 与 v0.9 下的注册与属性
| 项目 | v0.8 | v0.9 |
|---|---|---|
| 注册入口 | 写入官方 ComponentRegistry,register(typeName, ctor, tagName?, schema?) | 将自定义项与 basic catalog 合并为同一 catalogId 再交给 MessageProcessor;仍通过同一 customComponents 触发注册逻辑 |
| 消息里组件名 | 常见为嵌套对象 { "BizX": { …props } },引擎侧会归一成 component: "BizX" + 平铺 props | "component": "BizX" 与标准内置组件相同风格 |
| tagName 省略时 | 由运行时与 Lit 规则决定 | 默认生成为 a2ui- + 驼峰转短横线(如 BizPromoCard → a2ui-biz-promo-card),并 customElements.define(已存在则跳过) |
| schema 省略或非 Zod | 按运行时约定 | 使用宽松 Zod 占位(passthrough),便于通过校验;生产环境建议为自定义类型提供明确 Zod schema 与协议字段对齐 |
| componentProps 来源 | 来自旧链路中的 component.properties 等 | 主要来自 context.componentModel.properties;引擎在 connectedCallback 与模型 onUpdated 时会再次归一化,复杂 UI 可在收到更新后自行重绘 |
createSurface.catalogId 推荐使用官方 basic catalog 的 URL(即 basicCatalog.id)。为兼容 Playground 等来源,也支持 standard / default / basic(不区分大小写)或空字符串,包内在交给 MessageProcessor 前会解析为 basicCatalog.id;其它值仍按字面匹配已注册目录,未注册则抛出 Catalog not found。自定义类型名只要不与其他内置组件冲突即可。
6.5 onAction 与双通道(v0.9)
- 官方 Lit 组件:动作多走
@a2ui/web_core的processor.model.onAction流。 - 业务自定义原生组件:使用
a2uiaction自定义事件。 - v0.9 下
BaseRenderer同时订阅上述通道与a2uiaction,保证内置表单与自定义元素都能触发同一onAction回调。 - 回调入参经
mergeLitActionPayload与 dataModel 等合并;自定义组件请使用detail.name+detail.context,避免被误判为默认的name: 'action'。
6.6 使用 React 编写自定义块
本包 不支持 在 customComponents 里直接注册 React 函数组件;协议与 Lit 渲染树均为 原生自定义元素。
若业务必须用 React:
- 仍注册一个
HTMLElement子类。 - 在元素内部创建 挂载用
div,使用 React 18 的createRoot或 React 17 的ReactDOM.render将 React 子树挂到该节点。 - 在
disconnectedCallback中root.unmount()或unmountComponentAtNode,避免泄漏。 componentProps或协议更新后:引擎会更新归一化属性,元素内需 主动再次调用 render(例如在归一化完成后、componentModel.onUpdated回调中),否则 React 仍显示旧 props。
自定义组件如何更新数据(v0.9)
与内置 TextField / CheckBox 一样,推荐把可编辑状态放在 Surface 的 DataModel 上,而不是只放在 React 本地 useState 里(除非你不打算让同 surface 上的 Button action.context 路径绑定 或 mergeLitActionPayload 兜底读到该字段)。
Lit 在渲染自定义节点时会设置 this.context(@a2ui/web_core 的 ComponentContext)。其中 this.context.dataContext 提供与官方组件相同的读写能力:
| 能力 | 说明 |
|------|------|
| 读绑定值 | this.context.dataContext.resolveDynamicValue(dynamicValue),dynamicValue 与协议一致,例如组件 JSON 里的 { path: "/form/memo" }(与 this.componentProps.value 中归一化后的形态一致即可)。 |
| 写绑定值 | this.context.dataContext.set(path, value):path 为 JSON Pointer 风格字符串(与协议里 path 相同,一般以 / 开头);value 为写入模型的标量或对象。相对路径会按当前组件数据作用域解析,表单根路径下通常直接使用 /form/字段名。 |
| 何时重绘 React | 模型或其它组件改写同一路径后,订阅 this.context.componentModel.onUpdated,在回调里再次执行 ReactDOM.render / root.render,否则子树仍显示旧 UI。 |
在 React 子树里更新数据:在自定义元素的 renderReact 里把 dataContext.set 与 resolveDynamicValue 封装成闭包,以 props 形式传给 React 组件(例如 onChange 写路径、value 为解析后的当前值),避免在 .tsx 里直接依赖 this 生命周期错位。
与「只上报 a2uiaction」的区别:
dataContext.set:写入当前 surface 的 客户端数据模型,官方Button的action.event.context里若带有path,提交时会在服务端可见的载荷中体现为已解析的字面量;mergeLitActionPayload在部分形态下也会用整段form等做兜底合并。适合 表单字段、与内置控件混排 的场景。a2uiaction:不经过上述模型路径,仅在onAction中收到detail.context(经mergeLitActionPayload处理)。适合 一次性操作、无需进 DataModel 的场景。
应用侧可参考 monorepo 中 chatBot 的 A2UITest 页面:
custElement/BizReactCounterElement.tsx:内嵌 antd、本地计数与a2uiaction提交。custElement/BizFormCustomField.tsx:内嵌 antdInput,通过dataContext.set/resolveDynamicValue与/form/customMemo等路径绑定,并与componentModel.onUpdated联动重渲染。
工程提示:若使用 Umi 等构建工具,将实现文件命名为与 BizFormCustomFieldElement 类名不同的模块名(例如 BizFormCustomField.tsx)再 export class BizFormCustomFieldElement,再于 barrel index.ts 中 export { BizFormCustomFieldElement } from './BizFormCustomField',可避免删除同名 .ts 后解析仍优先命中 .ts 导致的 ENOENT。
6.7 注意事项
customComponents在每次 Lit 重渲染流程前会 先清空再按表注册;请保证同一渲染周期内注册表完整。- 重复注册同名 tag 时依赖浏览器
customElements.get跳过;若更换 构造器 实现,需避免与已定义 tag 冲突。 - 自定义组件名称建议带业务前缀,例如
BizChart、BizTag。 - 组件配置保持 纯 JSON 可序列化;回传业务优先用
a2uiaction/ 官方 action。需要与同 surface 内置表单、提交按钮共享状态时,使用context.dataContext写回模型(见 6.6),不在消息里嵌入函数。 - 使用
innerHTML拼接协议字符串时,注意 XSS:展示前做转义或仅用 文本节点 / React 默认转义。
7. 解析失败可视化
BaseRenderer 在解析失败时会显示错误面板,避免页面空白,支持展示:
- 错误摘要信息
- 可展开的堆栈详情
常见触发场景:
surfaceId不一致- 消息结构不符合协议
customComponents注册参数不合法
8. 内置文案国际化
渲染引擎内置了部分默认文案,可通过全局字典覆盖:
Please enter a valueNo options foundSelect itemsFilter options...
默认已提供中文回退文案。业务可在页面初始化时注入 window.A2UI_I18N 覆盖:
window.A2UI_I18N = {
'Please enter a value': '请输入内容',
'No options found': '未找到可选项',
'Select items': '请选择',
'Filter options...': '筛选选项...',
};9. 推荐最佳实践
- 传入
messages时可直接交给BaseRenderer,内部会按protocolVersion做normalizeToLitProtocolMessages与校验;若业务侧已预处理,也可继续手动调用(须与protocolVersion一致) - 页面级固定
protocolVersion,避免运行时混用版本 - 消息结构与数据更新分离管理
- 用
dataModelUpdate做局部刷新 - 用
onAction承接所有业务交互 - 用
className和styleVars管理样式覆盖 - 自定义组件优先通过
customComponents统一注册
