查看: 302|回复: 0

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

[复制链接]
发表于 2 小时前 | 显示全部楼层 |阅读模式
跨境电商商品运营每天要处理标题优化、五点描述、广告占比、退款率这些事,其实鸿蒙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页面里用。希望这份实践能帮到有同样需求的团队。
回复

使用道具 举报

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

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

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

官方核心成员

关注微信公众号

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

GMT+8, 2026-9-1 18:09 , Processed in 0.021905 second(s), 18 queries , Gzip On, Redis On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部