前沿特性实践 #01

把 A2UI 接到鸿蒙,这次改了哪些代码

CPF KMP 2.2.21-1.2.0 / CMP 1.9.2-1.2.0

这次给 a2ui-kmp 补了 OHOS 支持。Demo 能显示预约表单,修改姓名和人数后,按钮事件能拿到新值;继续发送消息,还能更新订单状态、插入取餐按钮。

适配库放在独立的 a2ui-kmp-ohos仓库,编译器和渲染后端直接复用 CPF 公版产物。

01 什么是 A2UI

A2UI(Agent-to-User Interface)是一套面向 AI Agent 的开源界面描述协议及配套渲染工具。它让 Agent 在对话中提供表单、卡片和按钮,用户可以直接选择、填写和确认。

官方 README 中的 A2UI 界面示例:卡片、表单和按钮可以组合成不同界面。

图 1|官方 README 中的 A2UI 界面示例:卡片、表单和按钮可以组合成不同界面

来源:A2UI 官方 README

Agent 发送 JSON 格式的消息,描述界面的组件、布局、数据和交互动作。客户端读取消息,再用本地已注册的组件渲染。界面的样式、可用能力和执行逻辑由应用掌握。

用户修改输入或点击按钮后,客户端把操作及相关数据作为事件返回。Agent 或业务服务据此继续处理任务,也可以发送新消息,更新界面中的内容或组件。

这套协议将界面描述与具体 UI 框架分开。同一份描述可以交给不同平台的渲染器,各端仍需提供相应的组件和交互支持。它适合对话中的信息收集、条件选择和多步确认,让用户在合适的时候直接操作界面。

02 a2ui-kmp 怎样完成跨端移植

在 Android Studio 用 File → Open 打开项目根目录,主要看 source、target和 scripts。上游源码放在 source,真正的多平台构建放在 target。

本次消息使用 v0.9,1.0.0-alpha02 是客户端库版本,协议格式和代码发布分别管理。

2.1 把原始源码和补丁一起编译

source/UPSTREAM 固定了 AndroidX 提交 e0a272dc。同步脚本按这个提交取代码,再生成需要替换的文件。

源码装配由 A2uiModule.kt 的 upstreamSources 完成:原文件进入 commonMain;同一路径存在 overlay 时,排除原文件,编译覆盖版本。

kotlin · A2uiModule.kt
srcDir(main)
srcDir(generated)
srcDir(manual)
exclude { it.file.startsWith(main) && it.relativePath.pathString in overlaid }

自动覆盖处理 import 等机械改动,手工覆盖处理 API 和行为差异。升级上游时,manifest 中的源文件 hash 用来检查旧补丁是否需要重做。

上游源码与两种 overlay 进入 commonMain

图 2|上游源码与两种 overlay 进入 commonMain

来源:A2uiModule.kt

2.2 把平台 API 收进 compat

上游还会用到 Locale、日期格式化、锁和 android.util.JsonReader。公共代码通过 a2ui-compat 访问这些能力。

以 Locale 为例,commonMain 定义 expect,desktopMain 可以委托 JDK:

kotlin · Locale.kt
// commonMain
public expect class Locale
// desktopMain
public actual typealias Locale = java.util.Locale

iOS actual 使用 Foundation。Android 专有接口在桌面端同样需要兼容实现;其中 JSON Reader 的状态机已经写在公共 Kotlin 代码里,Native 平台继续复用。

generate-overlays.py 负责把上游导入改到 compat 包。调用点继续使用原来的协议逻辑,平台差异留在各自的实现中。

commonMain 中的 Locale 契约

图 3|commonMain 中的 Locale 契约

来源:Locale.kt

2.3 让状态变化进入 Compose

这几个模块各管一段:model 定义消息和数据模型,engine 处理 Surface、组件与动作,compose-runtime 提供 Compose 状态,compose-ui 查找并绘制组件。

Runtime 给核心处理器注入 Compose 版数据模型和组件注册表,用 mutableStateOf保存状态,在 Snapshot.withMutableSnapshot中更新。组件通过 bind 读取,状态变化触发重组;OHOS 复用 CPF Compose Runtime。

渲染入口的关键调用是:

kotlin · A2uiComponent.kt
val a2uiComponent = catalog.components[component.type]
    ?: throw IllegalStateException("Component with type '${component.type}' is not registered")
with(a2uiComponent) {
    component.scope.Content(properties = component.properties, modifier = modifier)
}

Catalog 把协议中的组件名接到本地 Content。Material 3 Catalog 和应用自定义目录,都通过这套接口提供控件。

Catalog 查找与 Content 调用

图 4|Catalog 查找与 Content 调用

来源:A2uiComponent.kt

2.4 发布成库,再接进应用

原项目在 Android 端解析到官方构件,Desktop 和 iOS 使用源码编译的产物。公共 Maven 元数据描述平台变体,消费者据此选择 JVM 构件或 Native KLIB。

应用接入时还要接好处理循环:collectMessages 持续消费消息,processInput 负责解析并入队,activeSurfaces 提供界面状态,outboundEvents 输出交互事件。

上游 FormSample 中的 parser、消息输入和事件收集就在这里:

上游示例的消息输入和事件收集

图 5|上游示例的消息输入和事件收集

来源:FormSample.kt

当前 OHOS fork 默认构建 ARM64。原 Desktop/iOS 源码仍保留,发布配置已调整为本次使用的 OHOS 产物。

03 基于 CPF 公版补 OHOS 支持

3.1 固定依赖,再注册目标

先固定这一组版本:

toml · libs.versions.tomlkotlin = "2.2.21-1.2.0"composeRuntime = "1.9.2-1.2.0"composeMultiplatform = "1.9.2-1.2.0"kotlinxSerialization = "1.9.1-1.0.0"kotlinxCoroutines = "1.10.2-1.0.0"

依赖要带 OHOS 变体。这里 Runtime、UI、Collection、Annotation 使用 CPF 发布的坐标;源码仍然导入 androidx 包。包名和 Maven 坐标要分别看。

固定 KMP、Runtime 和 UI 版本

图 6|固定 KMP、Runtime 和 UI 版本

来源:libs.versions.toml

接着在共用的 a2uiTargets 中注册 ohosArm64,并连接源码层级:

gradle · A2uiModule.kt
commonMain → ohosMain → ohosArm64Main
commonTest → ohosTest → ohosArm64Test

新增 actual 放在 ohosMain,协议和引擎继续从 commonMain 编译。仅创建目录还不够,source set 之间的 dependsOn 必须接上。

OHOS 目标与源码层级

图 7|OHOS 目标与源码层级

来源:A2uiModule.kt

3.2 补上 OHOS actual

主要代码在 a2ui-compat/src/ohosMain。地区、日期、数字、货币和 Calendar 调用公开 ICU API;锁和容器使用 pthread;IO 补齐异常等平台类型,JSON Reader 继续用公共实现。

ICU 通过 cinterop 接入,定义文件很短:

def · a2ui_icu.defheaders = a2ui_icu.hheaderFilter = a2ui_icu.hpackage = ai.littlepress.a2ui.compat.iculinkerOpts = -licu

Kotlin actual 调用 C 包装,包装层处理字符串转换、错误码和资源释放。下图的 Locale.forLanguageTag 会检查已解析长度,拒绝不完整的标签。

Locale 的 OHOS actual

图 8|Locale 的 OHOS actual

来源:Locale.kt

锁的实现保留线程所有权和重入计数,完全释放后再唤醒等待者。打开链接、地区和时制偏好则留给 OhosA2uiPlatform,由宿主注入。

这部分除了签名,还要检查行为。比如 Java 日期模式 u 在 ICU 中含义不同,当前解析入口明确拒绝;格式化结果也受设备 ICU 数据版本影响。

3.3 回补 Compose 1.9 的 SideEffect

固定的 CMP 1.9.2 没有上游使用的 keyed SideEffect。相关调用用于错误上报,需要保留「同一组 key,成功提交后执行一次」的语义。

兼容函数用 remember(*keys) 保存提交标记,在普通 SideEffect 中执行并置位。标记不是 Compose 可观察状态,写入它不会再触发重组。

keyed SideEffect 的兼容实现

图 9|keyed SideEffect 的兼容实现

来源:CompatKeyedSideEffect.kt

四处调用通过 manual overlay 替换,原始 source 快照保留。回归测试检查首次执行、无关重组、key 改变和改回原值时的次数。

3.4 把组件的读、写和点击接回数据模型

这次的 KaiCatalog 包含 Panel、Heading、Label、Input、Party、Action 六种组件。输入框的接法是:

kotlin · KaiCatalog.kt
val update = properties.bindUpdater(value)
BasicTextField(
    value = properties.bind(value).orEmpty(),
    onValueChange = { update?.invoke(it) },
    readOnly = update == null,
)

bind 读取当前值,bindUpdater写回同一个属性。输入框没有再用 remember 保存另一份姓名,提交事件才能取到编辑后的值。

Input 的双向绑定

图 10|Input 的双向绑定

来源:KaiCatalog.kt

人数选择同样写回 /party,本例统一使用字符串。Action 点击后调用 dispatchAction,引擎解析 context,再从 outboundEvents 发出事件。示例先订阅事件,再输入消息,避免漏掉初次处理时的错误。

3.5 从本地 Maven 到 HAP

五个库先发布到 build/repo:compat、model、engine、compose-runtime、compose-ui。发布物需要包含公共元数据、ARM64 KLIB,以及 ICU cinterop。

ohos-sample 通过 exclusiveContent限定本地 A2UI 仓库,再按 Maven 坐标重新消费这些库。这样能一起检查平台变体和传递依赖。

后面的构建沿用 CPF 的承载方式:

 

pipeline
A2UI 库与 App
  → libkn.so / libkn_api.h
  → CMake、NAPI、ArkTS 壳
  → HAP

Kotlin 入口先初始化主线程,再创建 ComposeArkUIViewController;ArkTS 将返回的 controller 交给 Compose 组件。HAR 也固定为 1.9.2-1.2.0。

so 和头文件同步到鸿蒙壳

图 11|so 和头文件同步到鸿蒙壳

来源:build.gradle.kts

准备 JDK 17+、CPF Native 2.2.21-1.2.0、DevEco 工具链和 OpenHarmony SDK 20,在项目根目录执行:

shell
./scripts/build-ohos-demo.sh --checks

脚本使用 Gradle 8.14,完成库发布、测试链接、示例二进制和 HAP 打包。默认 SDK 根目录是 ~/Library/OpenHarmony/Sdk,可用 A2UI_OHOS_SDK 覆盖。

直接在 Android Studio 打开 ohos-sample 时,还要给原始 harmonyApp 配本机路径:

properties · local.properties
# ohos-sample/harmonyApp/local.properties,不提交到 Git
sdk.dir=<本机 OpenHarmony SDK 根目录>

首次同步缺少 sdk.dir,补齐后,IDE 自带 Hvigor 的 --sync 通过。Native 静态缓存也出现过未解析符号,当前配置使用 cacheKind=none。

HAP 默认输出到 /private/tmp/kai-a2ui-harmony/entry/build/default/outputs/default/entry-default-unsigned.hap。连接模拟器后安装、启动:

shell
hdc list targets
hdc -t <设备> install -r <HAP路径>
hdc -t <设备> shell aa start -a EntryAbility -b com.kai.a2ui

04 运行结果

测试环境是 OpenHarmony 6.0.0.47 ARM64 模拟器。预约初值为 Ada、2 人,改成 Lin、3 人后提交,出站事件拿到的也是新值:

output
book_table from submit on booking: {guest=Lin, party=3}

Lin、3 人,以及提交事件

图 12|Lin、3 人,以及提交事件

来源:KaiCatalog.kt

订单页最初只有标题和状态。后续消息先把 /status 改为「可以取餐了」,再给 root.children 加入 pickup 按钮。点击后收到:

output
confirm_pickup from pickup on order: {status=可以取餐了}

动态新增的取餐按钮与出站事件

图 13|动态新增的取餐按钮与出站事件

来源:AgentMessages.kt

HAP 内的 ICU 探测 3/3、五项选定回归 5/5通过;完整脚本也已在 fork 目录构建成功。独立 test.kexe 在模拟器 shell 缺少运行环境,回归方法实际是在调试 HAP 中执行的。

当前示例使用预置 JSON 和六组件目录,真实 Agent、业务服务以及完整 Material 3 Catalog 留待后续接入。已有的 processInput和 outboundEvents就是消息入口和事件出口。


源码与链接

源码与构建说明https://github.com/cesar-blog/a2ui-kmp-ohos

上游项目https://github.com/BinaryTape/a2ui-kmp

协议说明https://a2ui.org/specification/v0.9.1-a2ui/

即刻体验

CPF-KMP-CMP 鸿蒙平台首个正式版本已正式发布。欢迎上手验证、在社区提交 Issue/Discussion 反馈问题与需求,共同打磨 KMP&CMP 在鸿蒙平台上的生态与产品能力。

👉快速开始:(可复制链接至网页打开)

https://atomgit.com/CPF-KMP-CMP/docs

👉发布公告:(可复制链接至网页打开)

https://atomgit.com/org/CPF-KMP-CMP/discussions/15

参与社区共建

加入 CPF-KMP-CMP 社区,参与 KMP&CMP 鸿蒙版本规划和技术共建。

图片

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐