A2UI Custom Component Development Guide

June 11, 2026 · View on GitHub

本文以 app/src/pages/a2ui-remote 下的四个示例组件为准,说明从 API 定义、Element 实现、注册合并,到远程 ESM 打包与运行时加载的完整开发流程。完整 kit API 见 @boteai/a2ui-custom-kit API 参考

示例组件名演示能力kit API
DemoNativeElementDemoNativeElement原生 DOM 渲染,无 React 运行时createNativeElement
DemoReactComponentDemoReactComponentJSX 写视图,对外仍是 CustomElementcreateReactComponent
DemoStyledPanelDemoStyledPanelLess 编译 CSS 注入 ShadowRootensureComponentStyles
DemoActionDispatchDemoActionDispatch协议声明 action vs 代码内派发dispatchDeclaredAction / dispatchA2UIAction

目录约定

app/src/pages/a2ui-remote 下按组件名新建目录:

app/src/pages/a2ui-remote/YourComponent
  api.ts          # 协议 API(defineComponentApi)
  element.ts(x)   # 组件实现
  index.ts        # 导出 a2uiRemoteRegistry
  index.less      # 可选,Shadow 内样式

禁止与官方 basic catalog 组件同名(TextButtonColumn 等)。

复杂 UI 的推荐分层(antd、表单、级联等)

当组件逻辑较重时,将 标准 React UIA2UI 桥接 拆开:

YourComponent/
  api.ts
  YourWidget.tsx      # 普通受控 React 组件(value / onChange)
  useYourBinding.ts   # path 读写、action 派发
  element.tsx         # 薄桥接:ensureComponentStyles + return <YourWidgetHost />
  index.ts
  index.less

要点:createReactComponent 的 render 回调 不是 React 函数组件,不能在回调里直接写 useState / useEffect;Hooks 应放在独立的 XxxHost 组件或 useXxxBinding 中。


四个示例组件详解

1. DemoNativeElement:原生 DOM

适用:轻量标签、徽章、简单展示,bundle 体积更小。

api.ts

import { z } from 'zod';
import { defineComponentApi, DynString } from '@boteai/a2ui-custom-kit';

export const DemoNativeElementApi = defineComponentApi({
  name: 'DemoNativeElement',
  shape: {
    label: DynString,
    tone: z.enum(['default', 'success', 'warning']).optional(),
  },
});

element.ts

import { createNativeElement, readComponentProps, readStringProp } from '@boteai/a2ui-custom-kit';

export const DemoNativeElementElement = createNativeElement('DemoNativeElementHost', {
  render(host) {
    const props = readComponentProps(host);
    const label = readStringProp(props, 'label', 'Native Tag');
    const tone = readStringProp(props, 'tone', 'default');

    host.replaceChildren();
    const tag = document.createElement('span');
    tag.textContent = label;
    // … 按 tone 设置样式
    host.appendChild(tag);
  },
});

要点

  • connectedCallback 与 props 更新由 kit 自动订阅,无需手写。
  • readComponentProps / readStringProp 读取协议字段。

源码:app/src/pages/a2ui-remote/DemoNativeElement/


2. DemoReactComponent:React 桥接

适用:复杂 JSX 布局、复用现有 React 组件。

api.ts

export const DemoReactComponentApi = defineComponentApi({
  name: 'DemoReactComponent',
  shape: {
    title: DynString,
    subtitle: DynString.optional(),
    align: z.enum(['left', 'center']).optional(),
  },
});

element.tsx

import { createReactComponent, ensureComponentStyles } from '@boteai/a2ui-custom-kit';
import { DemoReactComponentApi } from './api';
import styles from './index.less';

export const DemoReactComponentElement = createReactComponent(
  DemoReactComponentApi,
  ({ props, host }) => {
    ensureComponentStyles(host, 'demo-react-component', styles);

    const title = String(props.title ?? 'React 组件');
    const subtitle = props.subtitle != null ? String(props.subtitle) : '';

    return (
      <div className="demo-react-component">
        <strong>{title}</strong>
        {subtitle ? <p>{subtitle}</p> : null}
      </div>
    );
  },
);

要点

  • 第一个参数传入 ComponentApi,props 按 schema 自动推断。
  • kit 负责 React mount / unmount / props 同步。
  • render 回调内 不可直接使用 Hooks;需要 Hooks 时拆成独立 Host 组件。

源码:app/src/pages/a2ui-remote/DemoReactComponent/


3. DemoStyledPanel:Shadow 内样式

适用:需要 Less / CSS 类名样式的组件。

A2UI 自定义组件运行在 Shadow DOM 内,页面全局 CSS 无法穿透。须用 ensureComponentStyles(host, styleKey, css) 将 Less 编译结果注入当前 ShadowRoot;styleKey 保证同一 Shadow 内只注入一次。

element.ts

import { createNativeElement, ensureComponentStyles, readComponentProps, readStringProp } from '@boteai/a2ui-custom-kit';
import styles from './index.less';

export const DemoStyledPanelElement = createNativeElement('DemoStyledPanelHost', {
  render(host) {
    ensureComponentStyles(host, 'demo-styled-panel', styles);

    const props = readComponentProps(host);
    const title = readStringProp(props, 'title', 'Styled Panel');
    // … 使用 className,样式在 Shadow 内生效
  },
});

index.less 按 BEM 写法组织,构建时 Umi / esbuild-less-plugin 会编译为 CSS 字符串。

源码:app/src/pages/a2ui-remote/DemoStyledPanel/


4. DemoActionDispatch:Action 派发

适用:按钮点击、表单提交等需要回传业务层的交互。

两种派发方式:

API动作来源典型场景
dispatchDeclaredAction(host)协议 JSON 的 props.actionAgent 配置点击行为,与官方 Button 一致
dispatchA2UIAction(host, { name, context })组件代码写死固定业务逻辑、调试

api.ts

import { ActionSchema, defineComponentApi, DynString } from '@boteai/a2ui-custom-kit';

export const DemoActionDispatchApi = defineComponentApi({
  name: 'DemoActionDispatch',
  shape: {
    label: DynString,
    action: ActionSchema.optional(),
  },
});

element.tsx

<button
  type="button"
  disabled={!props.action}
  onClick={() => dispatchDeclaredAction(host)}
>
  协议 action
</button>

<button
  type="button"
  onClick={() =>
    dispatchA2UIAction(host, {
      name: 'demo_action_imperative',
      context: { source: 'DemoActionDispatch' },
    })
  }
>
  代码 action
</button>

协议 JSON 示例

{
  "component": "DemoActionDispatch",
  "label": "左键走协议 action,右键走代码派发",
  "action": {
    "event": {
      "name": "demo_declared_action",
      "context": {
        "source": "showcase",
        "via": "dispatchDeclaredAction"
      }
    }
  }
}

也支持扁平写法:{ "action": { "name": "...", "context": { ... } } }context 中的 { "path": "/xxx" } 会在派发前自动解析。

页面侧接收

<BaseRenderer
  onAction={({ name, context }) => {
    if (name === 'demo_declared_action') { /* … */ }
    if (name === 'demo_action_imperative') { /* … */ }
  }}
/>

注意dispatchDeclaredAction 只处理带 event 或扁平 name 的 action,不包含 functionCall 形态;若协议使用 functionCall,需在组件内自行读取 props.action 处理。

源码:app/src/pages/a2ui-remote/DemoActionDispatch/


@boteai/a2ui-custom-kit API 参考

所有 API 从主入口 @boteai/a2ui-custom-kit 导出。远程 ESM 打包见文末 子路径打包入口

API 总览

API分类说明四个 Demo 是否用到
defineComponentApiSchema声明组件名与 Zod props schema,产出 ComponentApi全部
componentApiToJsonSchema2019SchemaComponentApi 转为 JSON Schema 2019-09,供 Agent catalog间接(经 defineRegistryEntry
DynStringSchema动态字符串:字面量 / { path } / functionCall,与官方 Catalog 一致DemoNativeElement、DemoReactComponent、DemoStyledPanel、DemoActionDispatch
DynamicValueSchemaSchema动态值:string / number / boolean / array / path / functionCall可选,复杂 props
ActionSchemaSchema官方 Action 类型:eventfunctionCallDemoActionDispatch
ComponentIdSchemaSchema子组件 ID 字符串容器类组件可选
ChildListSchemaSchema静态子 ID 列表或模板 { componentId, path }容器类组件可选
defineRegistryEntry注册表ComponentApi + 元素构造器 → 带 schema 的注册项全部
defineSimpleRegistryEntry注册表无 Zod schema 的简易注册
mergeRegistryEntries注册表合并多份注册表片段全部
createNativeElement工厂原生 DOM 自定义元素,自动订阅 props 更新DemoNativeElement、DemoStyledPanel
createReactComponent工厂React 桥接自定义元素,自动 mount / 更新DemoReactComponent、DemoActionDispatch
ensureComponentStyles样式向 ShadowRoot 注入 CSS(同 key 只注入一次)DemoReactComponent、DemoStyledPanel、DemoActionDispatch
readComponentProps运行时读取引擎归一化后的 host.componentPropsDemoNativeElement、DemoStyledPanel
readStringProp运行时从 props 读字符串,带默认值DemoNativeElement、DemoStyledPanel
readNumberProp运行时从 props 读数字,带默认值
readBoundPath运行时{ path } 绑定对象提取 JSON Pointer表单类组件
resolveBoundValue运行时解析 DynamicString / path,读 DataModel 当前值表单类组件
writeBoundValue运行时写入 DataModel 指定 path表单类组件
dispatchDeclaredActionAction读取 props.action 并派发,与官方 Button 一致DemoActionDispatch
dispatchA2UIActionAction代码内手写 action 名与 contextDemoActionDispatch
subscribeV09ComponentUpdates生命周期订阅 v0.9 componentModel.onUpdated工厂内部使用,高级场景可手动调用
runAfterPropsReady生命周期connected 时立即 + microtask 再执行一次 render工厂内部使用,高级原生组件可手动调用

类型一览

类型说明
ComponentApi{ name: string; schema: ZodObject },由 defineComponentApi 产出
A2UICustomElementHost自定义元素实例:componentPropscontext(dataContext / dispatchAction 等)
A2UIV09ElementContext引擎注入的 v0.9 上下文:dataContextcomponentModeldispatchAction
A2UIDeclaredAction协议 action 结构:event.name 或扁平 name + context
A2UIActionDetaildispatchA2UIAction 入参:{ name, context? }
A2UIActionPayload页面 onAction 收到的扁平结构
A2UICustomElementDefinition注册表单项:构造器,或 { elementCtor, tagName?, schema? }
A2UICustomComponentRegistryRecord<string, A2UICustomElementDefinition>,传给 BaseRenderer.customComponents
ReactA2UICustomRenderProps<A>createReactComponent 回调参数:{ props, host }
NativeElementLifecyclecreateNativeElement 配置:render、可选 onConnect / onDisconnect

Schema 与 API 定义

API签名 / 用法说明
defineComponentApidefineComponentApi({ name, shape }) → ComponentApiname 与协议 "component" 一致;shape 为 Zod 字段对象,内置 .strict()
componentApiToJsonSchema2019componentApiToJsonSchema2019(api) → Record<string, unknown>单独导出 JSON Schema;defineRegistryEntry 已自动调用
DynStringZod schema,等价于 DynamicStringSchemaAgent 可绑 path 的字符串字段,协议写 { "path": "/foo" } 或字面量
DynamicValueSchemaZod schema任意动态值:字面量、path、functionCall;用于非字符串 props
ActionSchemaZod schema点击/提交等行为声明;配合 dispatchDeclaredAction 使用
ComponentIdSchemaZod schema单个子组件 ID
ChildListSchemaZod schema静态 string[] 或动态列表模板 { componentId, path }

示例

import { z } from 'zod';
import {
  defineComponentApi,
  DynString,
  DynamicValueSchema,
  ActionSchema,
  ChildListSchema,
} from '@boteai/a2ui-custom-kit';

export const YourComponentApi = defineComponentApi({
  name: 'YourComponent',
  shape: {
    title: DynString,
    count: DynamicValueSchema.optional(),
    action: ActionSchema.optional(),
    children: ChildListSchema.optional(),
    tone: z.enum(['a', 'b']).optional(),
  },
});

规则

  1. 需要 DataModel 绑定的字段用 DynStringDynamicValueSchema,协议 JSON 写 { "path": "/xxx" }
  2. 需要 Agent 配置点击行为时,增加 action: ActionSchema.optional(),交互处调用 dispatchDeclaredAction(host)
  3. name 与消息里 "component": "YourComponent" 完全一致
  4. 注册时 defineRegistryEntry 会自动附带 JSON Schema,供 Agent catalog 使用。

参考:A2UI Custom Catalog


注册表构建

API签名返回值说明
defineRegistryEntry(api, elementCtor, options?) → Record<string, A2UICustomElementDefinition>单组件注册片段,key 为 api.name自动附带 JSON Schema;options.tagName 可覆盖自定义标签名
defineSimpleRegistryEntry(name, elementCtor, options?) → Record<...>单组件注册片段无 Zod 时快速注册;可手动传 options.schema
mergeRegistryEntries(...entries) → A2UICustomComponentRegistry合并后的完整注册表本地多组件、本地 + 远程合并均用此函数
import { defineRegistryEntry, mergeRegistryEntries } from '@boteai/a2ui-custom-kit';

export const a2uiRemoteRegistry = mergeRegistryEntries(
  defineRegistryEntry(YourComponentApi, YourComponentElement),
  defineRegistryEntry(AnotherApi, AnotherElement),
);

元素工厂

API签名说明
createNativeElement(displayName, { render, onConnect?, onDisconnect? }) → CustomElementConstructor原生 DOM 实现;自动 runAfterPropsReady + subscribeV09ComponentUpdates
createReactComponent(api, ({ props, host }) => JSX) → CustomElementConstructorReact 桥接;render 回调不是 React 组件,不可直接写 Hooks

createNativeElement 生命周期

回调时机
onConnect(host)connectedCallback,可选
render(host)首次连接 + 每次 props 更新
onDisconnect(host)disconnectedCallback,可选

createReactComponent 回调参数 ReactA2UICustomRenderProps

字段类型说明
propsz.infer<api.schema>引擎归一化后的组件 props
hostA2UICustomElementHost自定义元素实例,用于样式注入、action、数据绑定

复杂 UI 需 Hooks 时:拆独立 XxxHost 函数组件,在 render 回调里 return <XxxHost host={host} apiProps={props} />


运行时 — Props 读取

createNativeElementrender(host)createReactComponent 回调中使用。

API签名说明
readComponentProps(host) → Record<string, unknown>读取 host.componentProps,无则 {}
readStringProp(props, key, fallback?) → string读字符串 prop,null/undefined 时返回 fallback(默认 ''
readNumberProp(props, key, fallback?) → number读数字 prop,非法时返回 fallback(默认 0
const props = readComponentProps(host);
const label = readStringProp(props, 'label', 'Default');
const size = readNumberProp(props, 'size', 12);

运行时 — 数据绑定

表单、级联等需读写 Surface DataModel 时使用。api.ts 中对应字段须为 DynString / DynamicValueSchema,协议写 { "path": "/xxx" }

API签名说明
readBoundPath(raw) → string | undefined从绑定描述提取 path;无 path 返回 undefined
resolveBoundValue(host, raw) → stringdataContext.resolveDynamicValue 解析当前值;失败返回 ''
writeBoundValue(host, raw, value) → void写入 dataContext.set(path, value);无 path 时静默跳过
场景推荐 API
读取 Agent 绑定的 path 当前值resolveBoundValue(host, apiProps.field)
用户输入写回模型writeBoundValue(host, apiProps.field, nextValue)
仅解析 path 字符串readBoundPath(apiProps.field)
writeBoundValue(host, apiProps.province, '11');
writeBoundValue(host, apiProps.city, '');

运行时 — Action 派发

API签名说明
dispatchDeclaredAction(host) → void读取 props.action,解析 context 中的 path,走 context.dispatchAction 或回退 a2uiaction;无 action 时静默返回
dispatchA2UIAction(host, { name, context? }) → void派发 a2uiaction 自定义事件,由 BaseRenderer 转给页面 onAction
场景推荐 API
动作由 Agent 在协议 JSON 配置api 声明 ActionSchema + dispatchDeclaredAction(host)
动作名固定或 context 依赖运行时 UI 状态dispatchA2UIAction(host, { name, context })
改模型并通知业务writeBoundValue + 上述二者之一

dispatchDeclaredAction 不处理 functionCall 形态;若协议使用 functionCall,需自行读取 props.action 并处理。


运行时 — 样式与生命周期

API签名说明
ensureComponentStyles(host, styleKey, css) → void向 ShadowRoot(或 host)注入 <style>;相同 styleKey 在同一 Shadow 内只注入一次
subscribeV09ComponentUpdates(host, onUpdate) → unsubscribe手动订阅 props 更新;createNativeElement / createReactComponent 已内置
runAfterPropsReady(run) → void立即执行 + queueMicrotask 再执行;等待引擎写入 componentProps
import styles from './index.less';

ensureComponentStyles(host, 'my-component', styles);

Less / CSS Modules 构建产物为 CSS 字符串,作为第三个参数传入。


子路径打包入口

远程 ESM 打包时按组件技术栈选择入口,避免把 React 打进纯原生 bundle。

入口包含不含适用
@boteai/a2ui-custom-kit全部 API业务应用内开发
@boteai/a2ui-custom-kit/remote-runtime原生工厂、runtime、registry、schemacreateReactComponent、React纯原生远程 .mjs
@boteai/a2ui-custom-kit/react-runtime上述 + createReactComponentReact 桥接远程 .mjs

esbuild 配置中通过 alias 指向对应子路径(见 app/scripts/a2ui-resolve-kit.mjs)。


导出与注册

每个组件目录的 index.ts 导出一个注册表片段:

import { defineRegistryEntry, mergeRegistryEntries } from '@boteai/a2ui-custom-kit';
import { DemoNativeElementApi } from './api';
import { DemoNativeElementElement } from './element';

export const a2uiRemoteRegistry = mergeRegistryEntries(
  defineRegistryEntry(DemoNativeElementApi, DemoNativeElementElement),
);

本地注册合并

多个组件注册表可合并后传给 BaseRenderer

import { defineRegistryEntry, mergeRegistryEntries } from '@boteai/a2ui-custom-kit';
import { DemoNativeElementApi } from './DemoNativeElement/api';
import { DemoNativeElementElement } from './DemoNativeElement/element';
// … 其余组件

export const customComponents = mergeRegistryEntries(
  defineRegistryEntry(DemoNativeElementApi, DemoNativeElementElement),
  defineRegistryEntry(DemoReactComponentApi, DemoReactComponentElement),
  defineRegistryEntry(DemoStyledPanelApi, DemoStyledPanelElement),
  defineRegistryEntry(DemoActionDispatchApi, DemoActionDispatchElement),
);
<BaseRenderer
  messages={messages}
  protocolVersion="0.9"
  customComponents={customComponents}
  onAction={handleAction}
/>

数据绑定(进阶)

表单类组件除 action 外,常需把用户输入写回 Surface DataModel

API作用
resolveBoundValue(host, raw){ path } 或已归一化值读取当前模型
writeBoundValue(host, raw, value)写入 DataModel,联动同 path 的其他组件
// api.ts 中 province 用 DynString
writeBoundValue(host, apiProps.province, '11');

协议片段:

{
  "component": "YourFormField",
  "province": { "path": "/region/province" }
}

典型组合:表单 onChange同时 writeBoundValue + dispatchA2UIAction 通知业务层。


编译为远程 ESM

配置

app/a2ui-esm.config.mjs — 一组件一目录、一 bundle,产出 public/{name}.mjs

export default {
  outdir: 'public',
  minify: true,
  sourcemap: true,
  schema: {
    outdir: 'public/schemas',
    registry: 'scripts/a2ui-schema-registry.ts',
  },
  entries: [
    {
      name: 'DemoNativeElement',
      input: 'src/pages/a2ui-remote/DemoNativeElement/index.ts',
    },
    {
      name: 'DemoReactComponent',
      input: 'src/pages/a2ui-remote/DemoReactComponent/index.ts',
    },
    {
      name: 'DemoStyledPanel',
      input: 'src/pages/a2ui-remote/DemoStyledPanel/index.ts',
    },
    {
      name: 'DemoActionDispatch',
      input: 'src/pages/a2ui-remote/DemoActionDispatch/index.ts',
    },
  ],
};

新增组件时同步更新:

  1. a2ui-esm.config.mjsentries
  2. app/scripts/a2ui-schema-registry.tsschemaRegistry

执行命令

cd app

# 打包远程 .mjs(同时生成 JSON Schema)
yarn build:a2ui

# 仅生成 Schema
yarn generate:a2ui-schema

成功后:

  • app/public/DemoNativeElement.mjs 等 — 可部署到 CDN
  • app/public/schemas/DemoNativeElement.schema.json 等 — 供 Agent catalog 引用

运行时加载与渲染

方式一:传 URL(推荐)

@boteai/a2ui-renderremoteComponentUrls 会在内部自动加载远程 .mjs 并与 customComponents 合并,无需手动 await,也不必安装 @boteai/a2ui-custom-kit

import { BaseRenderer } from '@boteai/a2ui-render';

<BaseRenderer
  messages={messages}
  protocolVersion="0.9"
  remoteComponentUrls={[
    'https://cdn.example.com/DemoNativeElement.mjs',
    'https://cdn.example.com/DemoActionDispatch.mjs',
  ]}
  customComponents={localRegistry}
  onAction={handleAction}
/>

方式二:手动加载(高级)

需要在渲染前自行控制加载时机时,使用 @boteai/a2ui-render 导出的 loader(类型同样从该包导入):

单个远程地址

import { loadRemoteA2UICustomRegistry, type A2UICustomComponentRegistry } from '@boteai/a2ui-render';

const remotePart = await loadRemoteA2UICustomRegistry(
  'https://cdn.example.com/DemoNativeElement.mjs',
);

const customComponents: A2UICustomComponentRegistry = { ...localRegistry, ...remotePart };

多个远程地址

import { loadRemoteA2UICustomRegistries, type A2UICustomComponentRegistry } from '@boteai/a2ui-render';

const remotePart = await loadRemoteA2UICustomRegistries([
  'https://cdn.example.com/DemoNativeElement.mjs',
  'https://cdn.example.com/DemoActionDispatch.mjs',
]);

const customComponents: A2UICustomComponentRegistry = { ...localRegistry, ...remotePart };

渲染

<BaseRenderer
  messages={messages}
  protocolVersion="0.9"
  customComponents={customComponents}
  onAction={handleAction}
/>

useEffect、路由 loader 等可 await 的地方先加载远程注册表,再传入 BaseRenderer

若远程包导出名不是默认的 a2uiRemoteRegistry,加载时可传 { exportName: '你的导出名' }

使用 antd 的远程组件

A2UI 在嵌套 ShadowRoot 中渲染,全局 antd.css 不会自动穿透。开启渲染器开关:

<BaseRenderer
  messages={messages}
  protocolVersion="0.9"
  customComponents={customComponents}
  injectAntdStylesInShadow
  onAction={handleAction}
/>

远程组件 无需 在 bundle 里 import 'antd/dist/antd.min.css'。使用 Select 等弹出层时,建议 ConfigProvider + getPopupContainer 指向 Shadow 内节点。


快速对照

需求做法主要 API
声明组件 propsapi.ts + ZoddefineComponentApiDynStringActionSchema
轻量展示、无 React原生 DOMcreateNativeElementreadComponentProps
JSX 复杂 UIReact 桥接createReactComponentensureComponentStyles
Shadow 内样式Less 注入ensureComponentStyles
Agent 配置点击协议 actionActionSchema + dispatchDeclaredAction
代码写死 action手写派发dispatchA2UIAction
读 DataModelpath 绑定resolveBoundValuereadBoundPath
写 DataModel表单 onChangewriteBoundValue
合并注册表多组件 / 本地+远程defineRegistryEntrymergeRegistryEntries
导出 Agent SchemacatalogcomponentApiToJsonSchema2019
容器动态子列表复杂 propsChildListSchemaComponentIdSchema
纯原生远程包esbuild@boteai/a2ui-custom-kit/remote-runtime
React 远程包esbuild@boteai/a2ui-custom-kit/react-runtime
独立部署CDN 加载yarn build:a2uiloadRemoteA2UICustomRegistry

相关文档