跨境电商商品运营每天要处理标题优化、五点描述、广告占比、退款率这些事,其实鸿蒙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
- [XiaoyiAgentModule.NAME, (ctx) => new XiaoyiAgentModule(ctx)],
复制代码- // 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页面里用。希望这份实践能帮到有同样需求的团队。 |