鸿蒙RN桥接小艺智能体:TurboModule实践与避坑
跨境电商商品运营每天要处理标题优化、五点描述、广告占比、退款率这些事,其实鸿蒙6.0的Agent Framework Kit已经提供了拉起小艺智能体的能力。但我们的App是React Native(RNOH)架构,怎么在RN侧调用这套Kit?只能走TurboModule桥接。我们花了一下午才把链路跑通,中间踩了不少坑,特别是FunctionComponent不能直接在TurboModule里用,最终用startAbility启动一个专门页面来承载。这篇文章把完整的桥接实现和避坑点整理出来。先看整体架构。小艺智能体模块分三层:JS侧定义类型和API,通过TurboModuleRegistry注册;C++侧用ARK_ASYNC_METHOD_METADATA宏做方法名映射;ArkTS侧调用@kit.AgentFrameworkKit的FunctionController,最终拉起小艺。
前置条件必须确认:设备要鸿蒙6.0(API 20)以上且真机,不支持模拟器;必须联网并登录华为账号;智能体要在小艺开放平台创建并关联应用包名;功能仅中国境内可用。当时我们在模拟器上折腾,isAgentSupport一直返回false,后来查文档才知道不支持模拟器。
JS Spec层用TypeScript定义参数和返回值。这里建议把类型写完全,让JS侧拿到类型提示。注意模块未注册时返回null,调用方要判空。
// NativeXiaoyiAgentModule.ts
import type { TurboModule } from 'react-native';
import { TurboModuleRegistry } from 'react-native';
export type AgentScene =
| 'listing_optimization'
| 'stock_risk_analysis'
| 'bullet_points_generation'
| 'ad_cost_analysis'
| 'refund_rate_analysis';
export type XiaoyiAgentParams = {
agentId?: string;
scene: AgentScene;
sku: string;
title: string;
category: string;
price?: number;
grossMargin?: number;
stock?: number;
adRatio?: number;
refundRate?: number;
sellingPoints?: string[];
prompt?: string;
};
export type XiaoyiAgentResult = {
success: boolean;
code: string;
message: string;
requestId?: string;
};
export interface Spec extends TurboModule {
isAgentSupported(agentId: string): Promise<boolean>;
openAgent(params: XiaoyiAgentParams): Promise<XiaoyiAgentResult>;
}
export default TurboModuleRegistry.get<Spec>('XiaoyiAgentModule') as Spec | null;
C++侧注册方法映射时,参数个数必须和JS侧一致,否则会夯住。
// XiaoyiAgentModule.cpp
XiaoyiAgentModule::XiaoyiAgentModule(const ArkTSTurboModule::Context ctx, const std::string name)
: ArkTSTurboModule(ctx, name) {
methodMap_ = {
ARK_ASYNC_METHOD_METADATA(isAgentSupported, 1),
ARK_ASYNC_METHOD_METADATA(openAgent, 1),
};
}
ArkTS核心实现是桥接的重头。isAgentSupported用FunctionController.isAgentSupport,需要传UIAbilityContext。openAgent里先做参数校验,然后构建完整prompt,最后用startAbility启动承载页,把prompt放进Want参数。注意FunctionController实例要懒加载,并在__onDestroy__里释放。
// XiaoyiAgentModule.ets
import { FunctionController } from '@kit.AgentFrameworkKit';
import { Want } from '@kit.AbilityKit';
export class XiaoyiAgentModule extends UITurboModule {
static readonly NAME = 'XiaoyiAgentModule';
private functionController: FunctionController | null = null;
async isAgentSupported(agentId: string): Promise<boolean> {
try {
const controller = this.getOrCreateController();
const supported = await controller.isAgentSupport(
this.ctx.uiAbilityContext, agentId
);
return supported;
} catch (err) {
return false;
}
}
async openAgent(params: XiaoyiAgentParams): Promise<XiaoyiAgentResult> {
if (!params || !params.sku || !params.title) {
return { success: false, code: 'INVALID_PARAMS', message: '缺少必要参数' };
}
const fullPrompt = this.buildFullPrompt(params);
const want: Want = {
bundleName: this.ctx.uiAbilityContext.abilityInfo.bundleName,
abilityName: 'XiaoyiAgentAbility',
parameters: {
agentId: params.agentId || DEFAULT_AGENT_ID,
queryText: fullPrompt,
scene: params.scene,
},
};
await this.ctx.uiAbilityContext.startAbility(want);
return { success: true, code: 'AGENT_ABILITY_STARTED' };
}
}
为什么不能直接在TurboModule里用FunctionComponent?因为FunctionComponent是ArkUI的@Compoent组件,只能在ArkUI页面里渲染,而TurboModule运行在JS线程或Native线程,跨线程调用直接报类型错误。所以必须创建一个专门的Ability来承载:
// XiaoyiAgentAbility.ets
import { FunctionComponent, FunctionController } from '@kit.AgentFrameworkKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { Want } from '@kit.AbilityKit';
@Entry
@Component
struct XiaoyiAgentAbility {
private controller: FunctionController = new FunctionController();
@State agentId: string = '';
@State queryText: string = '';
aboutToAppear(want: Want) {
this.agentId = want.parameters?.agentId as string;
this.queryText = want.parameters?.queryText as string;
}
build() {
Column() {
FunctionComponent({
agentId: this.agentId,
onError: (err: BusinessError) => {
console.error(`Agent error: ${err.code}, ${err.message}`);
},
options: {
queryText: this.queryText,
},
controller: this.controller,
})
}
.width('100%')
.height('100%');
}
}
这个承载页必须在module.json5里注册,否则startAbility会报-1错误。
{
"module": {
"abilities": [
{
"name": "XiaoyiAgentAbility",
"srcEntry": "./ets/turbomodule/XiaoyiAgentAbility.ets",
"description": "小艺智能体承载页",
"launchType": "singleton",
"visible": true,
"skills": [
{
"actions": ["action.system.home"],
"entities": ["entity.system.home"]
}
]
}
]
}
}
prompt的构建直接影响小艺的理解效果。一开始我们直接序列化JSON,小艺解析很吃力。后来参考开放平台文档,改用自然语言分段格式,而且中文字段的标点要用中文冒号,解析更稳定。
private buildFullPrompt(params: XiaoyiAgentParams): string {
const contextBlock = [
`SKU:${params.sku}`,
`标题:${params.title}`,
`类目:${params.category}`,
`售价:$${params.price ?? '未知'}`,
`毛利率:${Math.round(params.grossMargin * 100)}%`,
`库存:${params.stock ?? '未知'}`,
`广告占比:${Math.round(params.adRatio * 100)}%`,
`退款率:${Math.round(params.refundRate * 100)}%`,
`卖点:${params.sellingPoints?.join(';') || '暂无'}`,
].join('\n');
return `请帮我处理以下跨境电商商品运营任务:\n\n${contextBlock}\n\n任务:${params.prompt}`;
}
我们设计了五个运营场景:listing_optimization、bullet_points_generation、stock_risk_analysis、ad_cost_analysis、refund_rate_analysis。每个场景有对应的prompt模板,例如listing_optimization要求生成英文标题并说明理由,bullet_points_generation要求输出5条英文Bullet Points。调用openAgent时,把场景prompt传给小艺即可。
注册链路也需要三处配合:ArkTS侧在GeneratedPackage里注册XiaoyiAgentModule,C++侧在PackageProvider里push Package,C++源文件要加进CMakeLists。
// GeneratedPackage.ets
,
// PackageProvider.cpp
#include "XiaoyiAgentPackage.h"
packages.push_back(std::make_shared<XiaoyiAgentPackage>(ctx));
# CMakeLists.txt
"./turbomodule/XiaoyiAgentModule.cpp"
再总结几个容易踩的坑。
坑一:FunctionComponent不能直接用在TurboModule里,这是线程模型决定的。错误做法是在openAgent里直接渲染FunctionComponent,正确做法是startAbility到承载页。
坑二:isAgentSupport需要UIAbilityContext。如果context传错,它永远返回false。UITurboModuleContext里已暴露uiAbilityContext,直接用。
坑三:智能体必须在小艺开放平台创建并关联应用包名。只填agentId但不关联应用,拉起一定会失败。
坑四:isAgentSupport失败时不要抛异常。有些场景会catch到异常导致JS侧Promise无法resolve,页面卡死。正确做法是try/catch后返回false,让JS侧做降级提示。
坑五:prompt格式别用JSON,小艺理解能力有限。用自然语言分段,每个字段用中文冒号,效果最好。
最后,isAgentSupport检测是前置步骤,返回true才拉起,false就提示用户登录华为账号或检查网络。整个桥接链路的关键就是理解你的JS方法最终要落到哪个ArkTS能力上,以及哪些组件只能在ArkUI页面里用。希望这份实践能帮到有同样需求的团队。
Re: 鸿蒙RN桥接小艺智能体:TurboModule实践与避坑
这个实战分享太及时了,我们团队也正准备在RNOH项目里接小艺智能体,正好卡在TurboModule桥接这块。有几个细节想请教: 1. `startAbility` 启动的承载页,页面内部是怎么拿到 `prompt` 并拉起小艺的?是直接在 `onCreate` 里调 `FunctionController`,还是也要走一层异步初始化? 2. 避坑点里提到 `FunctionComponent` 不能直接在 TurboModule 里用,这个我们一开始完全没想到。请问最终用 `startAbility` 启动承载页面时,承载页本身是用什么写的?ArkUI 的 `@Component` 还是也绕开了函数组件?如果承载页需要被复用,会不会有页面栈管理的问题? 3. 目前看 `openAgent` 是传了 `prompt` 和参数,那返回结果里 `requestId` 是同步拿到的吗?还是说是小艺那边异步回调才有的?如果是异步,RN侧怎么监听结果? 4. `isAgentSupported` 在模拟器上返回 false 这个坑我们也遇到了,后来只能真机测。想确认下是不是必须登录华为账号,还是只要设备网络正常+应用关联好了就行? 5. 最后问下性能方面:每次 `openAgent` 都新建 `FunctionController` 实例吗?看代码是懒加载单例,那 `__onDestroy__Re: 鸿蒙RN桥接小艺智能体:TurboModule实践与避坑
鸿蒙这套桥接方案很实用,尤其对RNOH项目来说。我们团队之前也想接小艺智能体,但一直没理清TurboModule那层调用链,看了这篇文章路径清晰多了。 几个点想再确认下:承载页面的UI是不是还得自己用ArkUI写一套?另外FunctionController懒加载在应用前后台切换时会不会有释放重建的问题?我们在实际场景里对这种生命周期比较敏感。 另外想问下isAgentSupport在模拟器上返回false这个坑,文档里会不会明确标注,还是说只能靠设备实测?这个信息对前期技术调研挺关键的。 整体来说干货密度很高,不过如果能把startAbility那部分跳转参数和返回值的处理再展开讲讲就更好了,我们正好要处理智能体回来之后的场景结果回传。Re: 鸿蒙RN桥接小艺智能体:TurboModule实践与避坑
感谢分享,这篇太实用了!我们最近也在折腾RNOH调系统能力,TurboModule链路确实一堆坑。你提到FunctionComponent不能直接在TurboModule里用,最后用startAbility起承载页解决——这个思路很关键,等于把ArkTS侧的UI组件隔离出去了,绕开了生命周期和上下文的问题。另外模拟器不支持这个坑我们也踩过,isAgentSupport一直false查了半天文档才发现,真机一跑就通了,确认前置条件真的能省不少时间。 还想请教一下,你在ArkTS里构建的fullPrompt具体是怎么传给承载页的?是直接塞进Want参数里,还是传了sku和title让承载页自己拼?如果prompt特别长,Want参数有没有大小限制?后续会不会考虑直接用FunctionController的某种方式传结构化数据?期待你把后半部分也补完,特别是避坑总结那部分。
页:
[1]