查看: 10700|回复: 3

鸿蒙RN桥接小艺智能体:TurboModule实践与避坑

[复制链接]
发表于 2026-9-1 16:00:00 | 显示全部楼层 |阅读模式
跨境电商商品运营每天要处理标题优化、五点描述、广告占比、退款率这些事,其实鸿蒙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,调用方要判空。
  1. // NativeXiaoyiAgentModule.ts
  2. import type { TurboModule } from 'react-native';
  3. import { TurboModuleRegistry } from 'react-native';
  4. export type AgentScene =
  5.   | 'listing_optimization'
  6.   | 'stock_risk_analysis'
  7.   | 'bullet_points_generation'
  8.   | 'ad_cost_analysis'
  9.   | 'refund_rate_analysis';
  10. export type XiaoyiAgentParams = {
  11.   agentId?: string;
  12.   scene: AgentScene;
  13.   sku: string;
  14.   title: string;
  15.   category: string;
  16.   price?: number;
  17.   grossMargin?: number;
  18.   stock?: number;
  19.   adRatio?: number;
  20.   refundRate?: number;
  21.   sellingPoints?: string[];
  22.   prompt?: string;
  23. };
  24. export type XiaoyiAgentResult = {
  25.   success: boolean;
  26.   code: string;
  27.   message: string;
  28.   requestId?: string;
  29. };
  30. export interface Spec extends TurboModule {
  31.   isAgentSupported(agentId: string): Promise<boolean>;
  32.   openAgent(params: XiaoyiAgentParams): Promise<XiaoyiAgentResult>;
  33. }
  34. export default TurboModuleRegistry.get<Spec>('XiaoyiAgentModule') as Spec | null;
复制代码

C++侧注册方法映射时,参数个数必须和JS侧一致,否则会夯住。
  1. // XiaoyiAgentModule.cpp
  2. XiaoyiAgentModule::XiaoyiAgentModule(const ArkTSTurboModule::Context ctx, const std::string name)
  3.   : ArkTSTurboModule(ctx, name) {
  4.   methodMap_ = {
  5.     ARK_ASYNC_METHOD_METADATA(isAgentSupported, 1),
  6.     ARK_ASYNC_METHOD_METADATA(openAgent, 1),
  7.   };
  8. }
复制代码

ArkTS核心实现是桥接的重头。isAgentSupported用FunctionController.isAgentSupport,需要传UIAbilityContext。openAgent里先做参数校验,然后构建完整prompt,最后用startAbility启动承载页,把prompt放进Want参数。注意FunctionController实例要懒加载,并在__onDestroy__里释放。
  1. // XiaoyiAgentModule.ets
  2. import { FunctionController } from '@kit.AgentFrameworkKit';
  3. import { Want } from '@kit.AbilityKit';
  4. export class XiaoyiAgentModule extends UITurboModule {
  5.   static readonly NAME = 'XiaoyiAgentModule';
  6.   private functionController: FunctionController | null = null;
  7.   async isAgentSupported(agentId: string): Promise<boolean> {
  8.     try {
  9.       const controller = this.getOrCreateController();
  10.       const supported = await controller.isAgentSupport(
  11.         this.ctx.uiAbilityContext, agentId
  12.       );
  13.       return supported;
  14.     } catch (err) {
  15.       return false;
  16.     }
  17.   }
  18.   async openAgent(params: XiaoyiAgentParams): Promise<XiaoyiAgentResult> {
  19.     if (!params || !params.sku || !params.title) {
  20.       return { success: false, code: 'INVALID_PARAMS', message: '缺少必要参数' };
  21.     }
  22.     const fullPrompt = this.buildFullPrompt(params);
  23.     const want: Want = {
  24.       bundleName: this.ctx.uiAbilityContext.abilityInfo.bundleName,
  25.       abilityName: 'XiaoyiAgentAbility',
  26.       parameters: {
  27.         agentId: params.agentId || DEFAULT_AGENT_ID,
  28.         queryText: fullPrompt,
  29.         scene: params.scene,
  30.       },
  31.     };
  32.     await this.ctx.uiAbilityContext.startAbility(want);
  33.     return { success: true, code: 'AGENT_ABILITY_STARTED' };
  34.   }
  35. }
复制代码

为什么不能直接在TurboModule里用FunctionComponent?因为FunctionComponent是ArkUI的@Compoent组件,只能在ArkUI页面里渲染,而TurboModule运行在JS线程或Native线程,跨线程调用直接报类型错误。所以必须创建一个专门的Ability来承载:
  1. // XiaoyiAgentAbility.ets
  2. import { FunctionComponent, FunctionController } from '@kit.AgentFrameworkKit';
  3. import { BusinessError } from '@kit.BasicServicesKit';
  4. import { Want } from '@kit.AbilityKit';
  5. @Entry
  6. @Component
  7. struct XiaoyiAgentAbility {
  8.   private controller: FunctionController = new FunctionController();
  9.   @State agentId: string = '';
  10.   @State queryText: string = '';
  11.   aboutToAppear(want: Want) {
  12.     this.agentId = want.parameters?.agentId as string;
  13.     this.queryText = want.parameters?.queryText as string;
  14.   }
  15.   build() {
  16.     Column() {
  17.       FunctionComponent({
  18.         agentId: this.agentId,
  19.         onError: (err: BusinessError) => {
  20.           console.error(`Agent error: ${err.code}, ${err.message}`);
  21.         },
  22.         options: {
  23.           queryText: this.queryText,
  24.         },
  25.         controller: this.controller,
  26.       })
  27.     }
  28.     .width('100%')
  29.     .height('100%');
  30.   }
  31. }
复制代码

这个承载页必须在module.json5里注册,否则startAbility会报-1错误。
  1. {
  2.   "module": {
  3.     "abilities": [
  4.       {
  5.         "name": "XiaoyiAgentAbility",
  6.         "srcEntry": "./ets/turbomodule/XiaoyiAgentAbility.ets",
  7.         "description": "小艺智能体承载页",
  8.         "launchType": "singleton",
  9.         "visible": true,
  10.         "skills": [
  11.           {
  12.             "actions": ["action.system.home"],
  13.             "entities": ["entity.system.home"]
  14.           }
  15.         ]
  16.       }
  17.     ]
  18.   }
  19. }
复制代码

prompt的构建直接影响小艺的理解效果。一开始我们直接序列化JSON,小艺解析很吃力。后来参考开放平台文档,改用自然语言分段格式,而且中文字段的标点要用中文冒号,解析更稳定。
  1. private buildFullPrompt(params: XiaoyiAgentParams): string {
  2.   const contextBlock = [
  3.     `SKU:${params.sku}`,
  4.     `标题:${params.title}`,
  5.     `类目:${params.category}`,
  6.     `售价:$${params.price ?? '未知'}`,
  7.     `毛利率:${Math.round(params.grossMargin * 100)}%`,
  8.     `库存:${params.stock ?? '未知'}`,
  9.     `广告占比:${Math.round(params.adRatio * 100)}%`,
  10.     `退款率:${Math.round(params.refundRate * 100)}%`,
  11.     `卖点:${params.sellingPoints?.join(';') || '暂无'}`,
  12.   ].join('\n');
  13.   return `请帮我处理以下跨境电商商品运营任务:\n\n${contextBlock}\n\n任务:${params.prompt}`;
  14. }
复制代码

我们设计了五个运营场景: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。
  1. // GeneratedPackage.ets
  2. [XiaoyiAgentModule.NAME, (ctx) => new XiaoyiAgentModule(ctx)],
复制代码
  1. // PackageProvider.cpp
  2. #include "XiaoyiAgentPackage.h"
  3. packages.push_back(std::make_shared<XiaoyiAgentPackage>(ctx));
复制代码
  1. # CMakeLists.txt
  2. "./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页面里用。希望这份实践能帮到有同样需求的团队。
回复

使用道具 举报

发表于 2026-9-1 19:00:00 | 显示全部楼层

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__
回复 支持 反对

使用道具 举报

发表于 2026-9-1 19:05:00 | 显示全部楼层

Re: 鸿蒙RN桥接小艺智能体:TurboModule实践与避坑

鸿蒙这套桥接方案很实用,尤其对RNOH项目来说。我们团队之前也想接小艺智能体,但一直没理清TurboModule那层调用链,看了这篇文章路径清晰多了。 几个点想再确认下:承载页面的UI是不是还得自己用ArkUI写一套?另外FunctionController懒加载在应用前后台切换时会不会有释放重建的问题?我们在实际场景里对这种生命周期比较敏感。 另外想问下isAgentSupport在模拟器上返回false这个坑,文档里会不会明确标注,还是说只能靠设备实测?这个信息对前期技术调研挺关键的。 整体来说干货密度很高,不过如果能把startAbility那部分跳转参数和返回值的处理再展开讲讲就更好了,我们正好要处理智能体回来之后的场景结果回传。
回复 支持 反对

使用道具 举报

发表于 2026-9-1 19:10:00 | 显示全部楼层

Re: 鸿蒙RN桥接小艺智能体:TurboModule实践与避坑

感谢分享,这篇太实用了!我们最近也在折腾RNOH调系统能力,TurboModule链路确实一堆坑。你提到FunctionComponent不能直接在TurboModule里用,最后用startAbility起承载页解决——这个思路很关键,等于把ArkTS侧的UI组件隔离出去了,绕开了生命周期和上下文的问题。另外模拟器不支持这个坑我们也踩过,isAgentSupport一直false查了半天文档才发现,真机一跑就通了,确认前置条件真的能省不少时间。 还想请教一下,你在ArkTS里构建的fullPrompt具体是怎么传给承载页的?是直接塞进Want参数里,还是传了sku和title让承载页自己拼?如果prompt特别长,Want参数有没有大小限制?后续会不会考虑直接用FunctionController的某种方式传结构化数据?期待你把后半部分也补完,特别是避坑总结那部分。
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-9-12 13:17 , Processed in 0.022947 second(s), 17 queries , Gzip On, Redis On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部