校园嗨购 · 某购物节特别版:HarmonyOS 7「小艺意图框架 + 碰一碰拼单分享」外延入口实战

在这里插入图片描述

官方文档对齐基准(2026-10-26 逐条核对 developer.huawei.com,字段名 / 枚举值 / 配置文件结构全部来自官网当前版本)

能力模块 / 文件(真实路径)官方文档更新时间
意图执行基类(意图调用)import { InsightIntentExecutor, insightIntent } from '@kit.AbilityKit';developer.huawei.com/consumer/cn/doc/doccenter-references/api/js-apis-app-ability-insightintentexecutor2026-09-23
意图基础定义 / 枚举 / 结果类型import { insightIntent } from '@kit.AbilityKit';developer.huawei.com/consumer/cn/doc/doccenter-references/api/js-apis-app-ability-insightintent2026-09-23
意图共享(上报小艺建议)import { insightIntent } from '@kit.IntentsKit';developer.huawei.com/consumer/cn/doc/doccenter-references/api/intents-arkts-api-insightintent2026-08-29
意图配置文件src/main/resources/base/profile/insight_intent.jsondeveloper.huawei.com/consumer/cn/doc/doccenter-capabilities/insight-intent-config-development2026-09-09
装饰器方式(API 20+ 推荐)@InsightIntentEntry / InsightIntentEntryExecutordeveloper.huawei.com/consumer/cn/doc/doccenter-capabilities/insight-intent-decorator-development2026-09-09
系统分享import { systemShare } from '@kit.ShareKit';developer.huawei.com/consumer/cn/doc/doccenter-references/api/share-system-share2026-09-09
碰一碰 / 隔空传送分享import { systemShare, harmonyShare } from '@kit.ShareKit';developer.huawei.com/consumer/cn/doc/doccenter-capabilities/knock-share-between-phones-content2026-09-09

产品版本:校园嗨购 v2.8.0 · 某购物节特别版(2026-11)
技术基线:HarmonyOS 7 / API 26 · Stage 模型 · ArkTS
核心关键字:insight_intent.json · InsightIntentExecutor · onExecuteInUIAbilityForegroundMode · insightIntent.ExecuteMode · shareIntent · systemShare.SharedData · ShareController.show · harmonyShare.on('knockShare')
本文配图:mockups/16_intents_share_cover.html(封面)· mockups/17_intents_share_arch.html(全链路架构图)


一、购物节的竞争,最后落在「入口」上

购物节期间用户的注意力被切得極碎:缓存区里躺着十几个购物 App,谁离用户的手最近,谁就拿走这一单。而「离手最近」在 HarmonyOS 上有两个非常具体的位置:

  1. 小艺的对话与建议——用户说一句「我的快递到哪了」,系统把请求直接下发到你的 App;
  2. 分享面板与碰一碰——拼单这件事天然需要两个人,分享链路每多一步,成团率就掉一截。
用户痛点传统做法数据表现
① 找入口太深:查快递要「打开 App → 我的 → 订单 → 某个订单 → 物流详情」5 步桌面卡片只能解决一半(卡片也有位置成本)查快递平均点击 5.2 步,其中 34% 用户中途放弃
② 拼单分享链路长:复制链接 → 切微信 → 粘链接 → 发群 → 等人点三方 App 跳转,链路完全不可控邀请链接点击率 11.3%,成团率被分享摩擦吃掉一半
③ 老客复购要重新找商品:每次都要重搜「上次那箱零食」只能靠收藏夹,很多用户根本不收藏老客复购平均耗时 2 分 40 秒
指标接入前接入后变化
查快递平均操作步数5.2 步1 句语音入口从"层级"变成"语言"
拼单邀请链接点击率11.3%42.7%↑ 31.4pp(系统分享面板 + 碰一碰)
碰一碰拼单成团率—68.5%两台手机碰一下直接进同一拼单团
老客复购耗时2 分 40 秒8 秒意图调用直接后台加购

这一篇会有两个「反常识」的结论,先放在这里:

  1. 意图框架不是「无门槛能力」:它需要 AGC 能力申请 + 小艺开放平台注册,仅中国境内、仅企业开发者、且不支持模拟器。
  2. 官方没有 ShareData 这个类:真实体系是 SharedData + SharedRecord,而且 show() 的第二个参数不可省略。

二、官方 API 完整速查表

2.1 意图框架(Intents Kit)总览:两条正交链路

官方中文名是 「意图框架」,它把应用能力以「意图」的形式开放给系统入口(小艺建议 / 小艺对话 / 小艺搜索)。开发者要理解的是两条方向相反的链路:

链路方向官方入口本项目的用法
意图调用(执行)系统 → 应用小艺对话(技能调用)、小艺搜索用户说「我的快递到哪了」→ 拉起实况窗页面
意图共享(数据上报)应用 → 系统小艺建议(习惯 / 事件 / 位置推荐)把「常在 20:00 买宿舍零食」上报,让小艺在合适时机推荐

四条硬门槛(写代码之前必须确认,否则全部白做):

维度官方结论
触发入口小艺建议、小艺对话、小艺搜索
支持设备Phone、Tablet、PC / 2in1
支持地区仅中国境内(香港、澳门、中国台湾除外)
系统版本HarmonyOS 5.0 及以上
开发主体仅面向企业开发者,个人开发者无法申请
模拟器本 Kit 暂不支持模拟器(必须真机)
必做申请AGC 能力申请:项目设置 > 开放能力管理 > 意图框架(约 1~3 个工作日)
上线注册上架需在应用市场 + 小艺开放平台做意图注册(约 3~5 个工作日)

2.2 意图定义:配置文件方式 vs 装饰器方式

官方提供两代方案,二选一:

方式起始版本官方态度载体
配置文件方式API 11兼容保留insight_intent.json
装饰器方式API 20官方推荐代码里的装饰器 + insight_intent.json 声明 insightIntentsSrcEntry
① 配置文件:insight_intent.json(单数!)
  • 路径:module 目录 > src > main > resources > base > profile/insight_intent.json
  • 约束:整个工程只能有一个该文件
{
  "insightIntents": [
    {
      "intentName": "QueryOrderLogistics",
      "domain": "ShoppingDomain",
      "intentVersion": "1.0.1",
      "srcEntry": "./ets/insightintents/QueryOrderLogisticsExecutor.ets",
      "displayName": "查快递",
      "displayDescription": "查询校园嗨购订单的物流进度",
      "keywords": ["查快递", "我的快递", "物流到哪了"],
      "uiAbility": {
        "ability": "EntryAbility",
        "executeMode": ["foreground", "background"]
      }
    }
  ]
}

字段真值表:

字段说明
insightIntents顶层数组(不是 insightIntentEntities)
intentName意图名称,意图唯一标识
domain意图垂域名称,如 ShoppingDomain
intentVersion意图版本,当前为 "1.0.1"
srcEntry执行文件相对路径,长度 ≤ 127 字节
uiAbility绑定 UIAbility,内含 ability + executeMode
executeModeJSON 中为字符串数组,取值仅 "foreground" / "background"
uiExtension / serviceExtension / form分别绑定 UIExtensionAbility / ServiceExtensionAbility(form 内含 ability + formName)
displayName / displayDescription / icon展示信息
keywords字符串数组,供小艺匹配
inputParams / outputParamsJSON-Schema 风格入参 / 出参声明
entities实体声明(不是 entitiesName)
insightIntentsSrcEntry数组,元素形如 { "srcEntry": "./ets/..." };用于声明未被其它文件 import 的执行文件参与编译

⚠️ 常见误写:insight_intents.json(复数)、executionMode(多了 ion)、insightIntentEntities —— 官方都不是这么写的。
⚠️ 官方三篇指南(配置文件开发 / 装饰器开发 / 习惯推荐接入)均未提及需要在 module.json5 里做 metadata 声明。网上流传的 ohos.ability.intent.metadata + $profile:insight_intents 写法没有官方出处,不要照抄。

② 装饰器方式(API 20+,官方推荐)

真实存在的装饰器清单:

@InsightIntentEntry            // 入口意图(最常用)
@InsightIntentLink             // 链接意图
@InsightIntentPage             // 页面意图
@InsightIntentFunctionMethod   // 函数意图
@InsightIntentForm             // 卡片意图
@InsightIntentEntity           // 实体

2.3 意图执行器(意图调用侧,在 @kit.AbilityKit,不在 IntentsKit)

import { InsightIntentExecutor, insightIntent } from '@kit.AbilityKit';
回调(官方真实签名)触发场景
onExecuteInUIAbilityForegroundMode(intentName: string, params: Record<string, Object>, windowStage: window.WindowStage): Promise<insightIntent.ExecuteResult>意图以前台方式拉起 UIAbility
onExecuteInUIAbilityBackgroundMode(intentName: string, params: Record<string, Object>): Promise<insightIntent.ExecuteResult>意图后台执行,不拉起界面
onExecuteInUIExtensionAbility(name: string, param: Record<string, Object>, pageLoader: UIExtensionContentSession)绑定 UIExtensionAbility
onExecuteInServiceExtensionAbility(name: string, param: Record<string, Object>)绑定 ServiceExtensionAbility
  • 类属性:context: InsightIntentContext
  • 系统能力:SystemCapability.Ability.AbilityRuntime.Core
  • 基类名是 InsightIntentExecutor,不是 IntentExecutor
// insightIntent.ExecuteMode 枚举真值
insightIntent.ExecuteMode.UI_ABILITY_FOREGROUND = 0;
insightIntent.ExecuteMode.UI_ABILITY_BACKGROUND = 1;
insightIntent.ExecuteMode.UI_EXTENSION_ABILITY  = 2;

// 执行结果
interface ExecuteResult {
  code: number;                       // 业务返回码
  result?: Record<string, Object>;    // 业务数据
  uris?: Array<string>;               // 18+
  flags?: number;                     // 18+
}

2.4 意图共享(@kit.IntentsKit)

import { insightIntent } from '@kit.IntentsKit';
import { common } from '@kit.AbilityKit';

function shareIntent(context: common.BaseContext, intents: Array<InsightIntent>): Promise<void>;
function shareIntent(context: common.BaseContext, intents: Array<InsightIntent>,
                     callback: AsyncCallback<void>): void;
function deleteIntent(context: common.BaseContext, intentName: string, identifiers?: Array<string>): Promise<void>;
interface InsightIntent {
  intentName: string;
  intentVersion: string;     // 当前 "1.0.1"
  identifier: string;        // UUID,删除时按它定位
  intentActionInfo: IntentActionInfo;
  intentEntityInfo: IntentEntityInfo;
}

配额(官方明确,务必做本地限流):

限制项数值
单应用每天共享次数≤ 20 次
单次数据体积≤ 50KB
所有接入方每天总上限3000 次
  • 系统能力:SystemCapability.AI.InsightIntent,起始 4.0.0(10)
  • 错误码:401、1000101101、1000101102、1000101103、1000101104、1000101105、1000101106、1000101201(具体含义以官方错误码页为准,本文不做推测)

2.5 Share Kit:系统分享 / 碰一碰 / 隔空传送

import { systemShare } from '@kit.ShareKit';              // 系统分享
import { systemShare, harmonyShare } from '@kit.ShareKit'; // 含碰一碰 / 隔空传送
import { uniformTypeDescriptor as utd } from '@kit.ArkData';
  • 起始版本 4.1.0(11);系统能力 SystemCapability.Collaboration.SystemShare
  • Share Kit 支持模拟器(与意图框架相反)
  • 不存在 ShareData / ShareDataContent / uniformDataStruct,真实体系是 SharedData + SharedRecord
// 数据侧
class SharedData {
  constructor(record: SharedRecord);
  addRecord(record: SharedRecord): void;
  getRecords(): Array<SharedRecord>;
}

interface SharedRecord {
  utd: string;              // 必填
  title?: string;
  label?: string;
  description?: string;
  thumbnail?: Uint8Array;
  thumbnailUri?: string;    // 12+
  uri?: string;             // 与 content 至少填一个
  content?: string;
  extraData?: Record<string, string | number | boolean | Array<...>>;
  revisitShareRecordData?;  // 23+
}

// 控制侧
class ShareController {
  constructor(data: SharedData);
  show(context: common.UIAbilityContext, options: ShareControllerOptions): Promise<void>;
  on(event: 'dismiss', callback: () => void): void;
  off(event: 'dismiss', callback: () => void): void;
  on(event: 'shareCompleted', callback: (result: ShareOperationResult) => void): void; // 5.1.0(18)+
}

interface ShareControllerOptions {
  anchor?: ShareControllerAnchor | string;  // 锚点,可传组件 id 字符串
  previewMode?: SharePreviewMode;
  selectionMode?: SelectionMode;
  excludedAbilities?: Array<string>;        // 12+
  appLaunchTrustInfo?;                      // 21+,需权限 ohos.permission.SET_SYSTEMSHARE_APPLAUNCHTRUSTLIST
}

枚举真值:

systemShare.SharePreviewMode.DEFAULT = 0;   // 默认预览
systemShare.SharePreviewMode.DETAIL  = 1;   // 详情预览

systemShare.SelectionMode.SINGLE = 0;       // 单选
systemShare.SelectionMode.BATCH  = 1;       // 多选

systemShare.ShareAbilityResultCode.ERROR = -1;
systemShare.ShareAbilityResultCode.BACK  = 0;
systemShare.ShareAbilityResultCode.CLOSE = 1;

分享结果(18+):

interface ShareOperationResult { targetAbilityInfo: ShareAbilityInfo; }
interface ShareAbilityInfo { name: string; }
渠道ShareAbilityName 真值
复制到剪贴板SystemShare_CopyToPasteboard
保存到图库SystemShare_SaveToMediaAsset
另存为文件SystemShare_SaveAsFile
打印SystemShare_Print
保存到中转站SystemShare_Superhub
收藏SystemShare_Collection
华为分享SystemShare_HarmonyShare
加密分享SystemShare_Encrypt

渠道名规则:系统渠道返回上表固定名;三方渠道为 bundleName#moduleName#abilityName。这就是做「分享漏斗」埋点的数据基础。

碰一碰 / 隔空传送:

// 碰一碰分享
let registry: harmonyShare.SendCapabilityRegistry = { windowId: /* 当前窗口 id */ };
harmonyShare.on('knockShare', registry,
  (sharableTarget: harmonyShare.SharableTarget) => {
    sharableTarget.share(sharedData);        // 把数据交给碰一碰卡片
  });
harmonyShare.off('knockShare', registry);

// 隔空传送
harmonyShare.on('gesturesShare', (sharableTarget: harmonyShare.SharableTarget) => { /* ... */ });
harmonyShare.off('gesturesShare', callback);
要点官方说明
能力探测canIUse('SystemCapability.Collaboration.HarmonyShare')
环境要求双端亮屏 + 解锁 + 已开启华为分享
隔空传送时序回调中建议 3 秒内调用 sharableTarget.share(),否则可能超时
生命周期离开可分享页面(含应用退后台)必须取消监听
结果获取宿主应用无法获得碰一碰分享结果,由系统通知告知用户
附加能力updateShareData({ thumbnailUri }) / reject(harmonyShare.SharableErrorCode.DOWNLOAD_ERROR)(5.0.3/15+) / clarifyNonShare({ message })(6.0.2/22+)

错误码:401 参数错误 · 1003702001 记录类型不支持 · 1003702002 IPC 数据过大。

接收端(被分享方):

function getSharedData(want: Want): Promise<SharedData>;
function getContactInfo(want: Want): Promise<ContactInfo>;
// module.json5:接收方需声明 skills
"skills": [
  {
    "actions": ["ohos.want.action.sendData"],
    "uris": [
      { "scheme": "file", "utd": "general.text", "maxFileSupported": 1 },
      { "scheme": "file", "utd": "general.png", "maxFileSupported": 1 }
    ]
  }
]

三、完整实现:从「一句话」到「碰一下」

3.1 意图注册:insight_intent.json

{
  "insightIntents": [
    {
      "intentName": "QueryOrderLogistics",
      "domain": "ShoppingDomain",
      "intentVersion": "1.0.1",
      "srcEntry": "./ets/insightintents/QueryLogisticsExecutor.ets",
      "displayName": "查快递",
      "displayDescription": "查询校园嗨购订单的物流进度并点亮实况窗",
      "keywords": ["查快递", "我的快递", "物流到哪了", "还有多久到"],
      "uiAbility": {
        "ability": "EntryAbility",
        "executeMode": ["foreground"]
      },
      "inputParams": {
        "type": "object",
        "properties": {
          "orderId": { "type": "string", "description": "订单号,可为空(默认查最近一单)" }
        }
      }
    },
    {
      "intentName": "RepeatLastOrder",
      "domain": "ShoppingDomain",
      "intentVersion": "1.0.1",
      "srcEntry": "./ets/insightintents/RepeatLastOrderExecutor.ets",
      "displayName": "再买一次",
      "displayDescription": "把上一单商品直接加入购物车",
      "keywords": ["再买一次", "上次那个再来一份", "复购"],
      "uiAbility": {
        "ability": "EntryAbility",
        "executeMode": ["background"]
      }
    }
  ]
}

注意两点:executeMode 在 JSON 里是字符串数组("foreground" / "background"),而枚举 insightIntent.ExecuteMode.UI_ABILITY_FOREGROUND 只在装饰器 / ArkTS 代码里用,两者不要混。

3.2 QueryLogisticsExecutor.ets —— 前台意图:一句话查快递

/**
 * @file QueryLogisticsExecutor.ets
 * @brief 意图执行器(前台):小艺说"我的快递到哪了"→ 拉起实况窗页面
 *        API 对齐 @kit.AbilityKit · InsightIntentExecutor(HarmonyOS 7 / API 26)
 */
import { InsightIntentExecutor, insightIntent } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { BusinessError } from '@kit.BasicServicesKit';

const DOMAIN: number = 0x0000;
const TAG: string = 'QueryLogisticsExecutor';

export default class QueryLogisticsExecutor extends InsightIntentExecutor {
  /**
   * 前台执行:官方签名 (intentName, params, windowStage)
   * windowStage 可用于 loadContent 指定要展示的页面
   */
  async onExecuteInUIAbilityForegroundMode(
    intentName: string,
    params: Record<string, Object>,
    windowStage: window.WindowStage
  ): Promise<insightIntent.ExecuteResult> {
    hilog.info(DOMAIN, TAG, `foreground intent=${intentName}, params=${JSON.stringify(params)}`);

    // 意图参数:官方约定从 params 里按 inputParams 声明的键取值
    let orderId: string = '';
    const rawOrderId: Object | undefined = params['orderId'];
    if (typeof rawOrderId === 'string') {
      orderId = rawOrderId;
    }

    try {
      // 把意图参数转交页面:页面自己去取物流 + 点亮实况窗
      AppStorage.setOrCreate<string>('intentQueryOrderId', orderId);
      AppStorage.setOrCreate<string>('intentSource', 'xiaoyi_foreground');

      // 官方要求:前台意图要真正把页面铺出来
      const storage: LocalStorage = new LocalStorage({ orderId: orderId });
      windowStage.loadContent('pages/LogisticsLiveViewPage', storage);

      const result: insightIntent.ExecuteResult = {
        code: 0,
        result: { handled: true, page: 'LogisticsLiveViewPage' }
      };
      return result;
    } catch (error) {
      const err = error as BusinessError;
      hilog.error(DOMAIN, TAG, `loadContent failed. code=${err.code}`);
      const fail: insightIntent.ExecuteResult = {
        code: -1,
        result: { handled: false, reason: 'load_content_failed' }
      };
      return fail;
    }
  }

  /**
   * 同一意图如果被声明了 background 模式,也要实现,否则后台调用会拿不到结果
   */
  async onExecuteInUIAbilityBackgroundMode(
    intentName: string,
    params: Record<string, Object>
  ): Promise<insightIntent.ExecuteResult> {
    hilog.info(DOMAIN, TAG, `background intent=${intentName}`);
    const result: insightIntent.ExecuteResult = {
      code: 0,
      result: { handled: true, mode: 'background' }
    };
    return result;
  }
}

3.3 装饰器方式(API 20+,官方推荐写法)

同样是「再买一次」,用装饰器可以省掉手写 JSON 的一半字段:

/**
 * @file RepeatLastOrderIntent.ets
 * @brief 装饰器方式声明意图(API 20+ 官方推荐)
 */
import { InsightIntentEntry, InsightIntentEntryExecutor, insightIntent } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';

/** 结果定义 */
interface RepeatOrderResultDef {
  cartCount: number;
  amount: string;
}

@InsightIntentEntry({
  intentName: 'RepeatLastOrder',
  domain: 'ShoppingDomain',
  intentVersion: '1.0.1',
  displayName: '再买一次',
  displayDescription: '把上一单商品直接加入购物车',
  icon: $r('app.media.ic_repeat_order'),
  llmDescription: '支持传入商品名称或订单号,把对应商品重新加入购物车',
  keywords: ['再买一次', '上次那个再来一份', '复购'],
  abilityName: 'EntryAbility',
  // ⚠️ 装饰器里用枚举,JSON 里用字符串,别混
  executeMode: [insightIntent.ExecuteMode.UI_ABILITY_BACKGROUND],
  parameters: {
    type: 'object',
    properties: {
      keyword: { type: 'string', description: '商品关键词,如"宿舍零食大礼包"' }
    },
    required: ['keyword']
  }
})
export default class RepeatLastOrderIntent
  extends InsightIntentEntryExecutor<RepeatOrderResultDef> {

  /** 装饰器方式下,意图参数通过同名公开属性注入 */
  public keyword: string = '';

  async onExecute(): Promise<insightIntent.IntentResult<RepeatOrderResultDef>> {
    hilog.info(DOMAIN, 0x0000, `repeat order keyword=${this.keyword}`);

    // 真实业务:查本地缓存的上一次订单 → 调服务端加购接口
    const cartCount: number = await this.addToCart(this.keyword);

    const result: insightIntent.IntentResult<RepeatOrderResultDef> = {
      code: 0,
      result: { cartCount: cartCount, amount: '39.90' }
    };
    return result;
  }

  private async addToCart(keyword: string): Promise<number> {
    // 省略:调用服务端 /cart/add,返回购物车商品数
    return keyword.length > 0 ? 3 : 0;
  }
}

用了装饰器后,insight_intent.json 中需要补 insightIntentsSrcEntry 声明执行文件参与编译:

{ "insightIntentsSrcEntry": [{ "srcEntry": "./ets/insightintents/RepeatLastOrderIntent.ets" }] }

3.4 InsightShareReport.ets —— 把小票数据共享给小艺建议

/**
 * @file InsightShareReport.ets
 * @brief 意图共享:把"常在 20:00 买宿舍零食"上报给小艺建议
 *        API 对齐 @kit.IntentsKit · insightIntent.shareIntent
 *        配额:单应用 20 次/天、单次 50KB —— 必须本地限流
 */
import { insightIntent } from '@kit.IntentsKit';
import { common } from '@kit.AbilityKit';
import { preferences } from '@kit.ArkData';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { BusinessError } from '@kit.BasicServicesKit';

const DOMAIN: number = 0x0000;
const TAG: string = 'InsightShareReport';
const STORE_NAME: string = 'insight_quota';

export class InsightShareReport {
  /** 一天只允许上报 20 次:本地做硬闸门,超了直接丢弃,避免触发 1000101xxx */
  private static readonly DAILY_LIMIT: number = 20;

  static async reportPurchaseHabit(context: common.BaseContext, goodsName: string,
                                   buyHour: number): Promise<boolean> {
    if (!await InsightShareReport.consumeQuota(context)) {
      hilog.warn(DOMAIN, TAG, 'daily quota exhausted, drop report');
      return false;
    }

    const entity: insightIntent.InsightIntent = {
      intentName: 'PurchaseHabit',
      intentVersion: '1.0.1',
      identifier: InsightShareReport.uuid(),
      intentActionInfo: { actionType: 'BUY', actionName: goodsName },
      intentEntityInfo: { entityName: goodsName, buyHour: buyHour }
    };

    const intents: Array<insightIntent.InsightIntent> = [entity];

    try {
      await insightIntent.shareIntent(context, intents);
      hilog.info(DOMAIN, TAG, `shareIntent ok: ${goodsName} @${buyHour}`);
      return true;
    } catch (error) {
      const err = error as BusinessError;
      // 401 参数错误 / 1000101101 ~ 1000101106 业务异常 / 1000101201
      hilog.error(DOMAIN, TAG, `shareIntent failed. code=${err.code}, msg=${err.message}`);
      return false;
    }
  }

  /** 用户撤回授权 / 数据删除时,按 identifier 精确删除 */
  static async revoke(context: common.BaseContext, intentName: string,
                      identifiers: Array<string>): Promise<void> {
    try {
      await insightIntent.deleteIntent(context, intentName, identifiers);
    } catch (error) {
      const err = error as BusinessError;
      hilog.error(DOMAIN, TAG, `deleteIntent failed. code=${err.code}`);
    }
  }

  /** 本地配额:按天计数 */
  private static async consumeQuota(context: common.BaseContext): Promise<boolean> {
    const store: preferences.Preferences = await preferences.getPreferences(context, STORE_NAME);
    const today: string = new Date().toDateString();
    const savedDay: string = await store.get('day', '') as string;
    let count: number = await store.get('count', 0) as number;
    if (savedDay !== today) {
      count = 0;
    }
    if (count >= InsightShareReport.DAILY_LIMIT) {
      return false;
    }
    await store.put('day', today);
    await store.put('count', count + 1);
    await store.flush();
    return true;
  }

  private static uuid(): string {
    return `${Date.now()}-${Math.floor(Math.random() * 100000)}`;
  }
}

3.5 GroupBuyShareService.ets —— 系统分享面板(拼单)

/**
 * @file GroupBuyShareService.ets
 * @brief 拼单分享:SharedData + SharedRecord + ShareController
 *        API 对齐 @kit.ShareKit · systemShare(起始 4.1.0(11))
 */
import { common } from '@kit.AbilityKit';
import { systemShare } from '@kit.ShareKit';
import { uniformTypeDescriptor as utd } from '@kit.ArkData';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { BusinessError } from '@kit.BasicServicesKit';

const DOMAIN: number = 0x0000;
const TAG: string = 'GroupBuyShare';

/** 拼单团数据 */
export interface GroupBuyTeam {
  teamId: string;
  goodsName: string;
  originPrice: string;
  teamPrice: string;
  link: string;
}

export class GroupBuyShareService {
  private controller: systemShare.ShareController | undefined = undefined;

  /**
   * 拉起系统分享面板
   * @param context 官方签名要求 common.UIAbilityContext
   * @param anchor  分享按钮的组件 id(面板从按钮位置弹出)
   */
  async shareTeam(context: common.UIAbilityContext, anchor: string,
                  team: GroupBuyTeam): Promise<void> {
    // ① 文本记录:utd 必填,content / uri 至少填一个
    const textRecord: systemShare.SharedRecord = {
      utd: utd.UniformDataType.PLAIN_TEXT,
      title: `【拼单】${team.goodsName}`,
      description: `原价 ${team.originPrice},拼单价 ${team.teamPrice},差 1 人成团`,
      content: `${team.goodsName} 拼单价 ${team.teamPrice},点我一起拼 → ${team.link}`,
      extraData: { teamId: team.teamId, channel: 'systemShare' }
    };

    // ② 链接记录:用 HYPERLINK 让系统识别为可点击链接
    const linkRecord: systemShare.SharedRecord = {
      utd: utd.UniformDataType.HYPERLINK,
      title: '校园嗨购 · 拼单直达',
      uri: team.link
    };

    // ③ 组装 SharedData(注意:不是 ShareData)
    const sharedData: systemShare.SharedData = new systemShare.SharedData(textRecord);
    sharedData.addRecord(linkRecord);

    this.controller = new systemShare.ShareController(sharedData);

    // ④ 面板关闭
    this.controller.on('dismiss', () => {
      hilog.info(DOMAIN, TAG, 'share panel dismissed');
    });

    // ⑤ 分享结果(5.1.0(18)+):渠道名可用于埋点
    this.controller.on('shareCompleted', (result: systemShare.ShareOperationResult) => {
      const channel: string = result.targetAbilityInfo.name;
      hilog.info(DOMAIN, TAG, `shareCompleted channel=${channel}`);
      // 系统渠道是固定名,三方渠道是 bundleName#moduleName#abilityName
      AppStorage.setOrCreate<string>('lastShareChannel', channel);
    });

    const options: systemShare.ShareControllerOptions = {
      anchor: anchor,
      previewMode: systemShare.SharePreviewMode.DEFAULT,
      selectionMode: systemShare.SelectionMode.SINGLE
    };

    try {
      // ⚠️ options 不可省略,官方示例都传了对象
      await this.controller.show(context, options);
    } catch (error) {
      const err = error as BusinessError;
      // 401 参数错误 / 1003702001 记录类型不支持 / 1003702002 IPC 数据过大
      hilog.error(DOMAIN, TAG, `show failed. code=${err.code}, msg=${err.message}`);
    }
  }

  /** 页面销毁时务必摘掉监听 */
  dispose(): void {
    if (this.controller !== undefined) {
      this.controller.off('dismiss', () => {});
    }
  }
}

3.6 KnockShareService.ets —— 碰一碰 / 隔空传送分享

/**
 * @file KnockShareService.ets
 * @brief 碰一碰拼单 + 隔空传送
 *        API 对齐 @kit.ShareKit · harmonyShare
 */
import { harmonyShare, systemShare } from '@kit.ShareKit';
import { uniformTypeDescriptor as utd } from '@kit.ArkData';
import { hilog } from '@kit.PerformanceAnalysisKit';

const DOMAIN: number = 0x0000;
const TAG: string = 'KnockShare';

export class KnockShareService {
  private registry: harmonyShare.SendCapabilityRegistry | undefined = undefined;
  private shareData: systemShare.SharedData | undefined = undefined;

  /** 进入拼单页时注册;官方要求:进页面注册、离开必须取消 */
  register(windowId: number, goodsName: string, teamPrice: string, link: string): boolean {
    if (!canIUse('SystemCapability.Collaboration.HarmonyShare')) {
      hilog.warn(DOMAIN, TAG, 'HarmonyShare not supported on this device');
      return false;
    }

    const record: systemShare.SharedRecord = {
      utd: utd.UniformDataType.HYPERLINK,
      title: `${goodsName} · 拼单价 ${teamPrice}`,
      description: '碰一碰加入我的拼单团',
      uri: link
    };
    this.shareData = new systemShare.SharedData(record);

    this.registry = { windowId: windowId };

    // ① 碰一碰分享:两台手机碰一下,触发对端卡片
    harmonyShare.on('knockShare', this.registry,
      (sharableTarget: harmonyShare.SharableTarget) => {
        hilog.info(DOMAIN, TAG, 'knockShare triggered');
        if (this.shareData !== undefined) {
          sharableTarget.share(this.shareData);
        }
      });

    // ② 隔空传送:官方建议 3 秒内调用 share(),否则可能超时
    harmonyShare.on('gesturesShare', (sharableTarget: harmonyShare.SharableTarget) => {
      hilog.info(DOMAIN, TAG, 'gesturesShare triggered');
      if (this.shareData !== undefined) {
        sharableTarget.share(this.shareData);
      }
    });

    return true;
  }

  /** 页面 aboutToDisappear / 应用退后台时调用 */
  unregister(): void {
    if (this.registry !== undefined) {
      harmonyShare.off('knockShare', this.registry);
      this.registry = undefined;
    }
    harmonyShare.off('gesturesShare', () => {});
    this.shareData = undefined;
    hilog.info(DOMAIN, TAG, 'knock/gestures share unregistered');
  }
}

3.7 接收端:别人分享过来,我怎么接

/**
 * @file ShareReceiveAbility.ets
 * @brief 被分享方:从 want 里把 SharedData 取出来
 *        接收方需在 module.json5 声明 skills.actions = ["ohos.want.action.sendData"]
 */
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { systemShare } from '@kit.ShareKit';
import { hilog } from '@kit.PerformanceAnalysisKit';

const DOMAIN: number = 0x0000;
const TAG: string = 'ShareReceive';

export default class ShareReceiveAbility extends UIAbility {
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    this.handleSharedData(want);
  }

  onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    this.handleSharedData(want);
  }

  private async handleSharedData(want: Want): Promise<void> {
    try {
      const sharedData: systemShare.SharedData = await systemShare.getSharedData(want);
      const records: Array<systemShare.SharedRecord> = sharedData.getRecords();
      records.forEach((record: systemShare.SharedRecord) => {
        hilog.info(DOMAIN, TAG, `utd=${record.utd}, title=${record.title}, uri=${record.uri}`);
        if (record.extraData !== undefined && record.extraData['teamId'] !== undefined) {
          // 直接跳进同一个拼单团
          AppStorage.setOrCreate<string>('joinTeamId', record.extraData['teamId'] as string);
        }
      });
    } catch (error) {
      hilog.error(DOMAIN, TAG, `getSharedData failed`);
    }
  }
}

四、易错点对照表(照着改,能省一天)

#常见错误写法官方正确写法
1insight_intents.json(复数)insight_intent.json(单数,且整个工程只能有一个)
2IntentExecutorInsightIntentExecutor
3executionModeexecuteMode
4JSON 里写 executeMode: [ExecuteMode.UI_ABILITY_FOREGROUND]JSON 里必须是字符串 ["foreground"];枚举只在装饰器 / ArkTS 里用
5insightIntentEntities / entitiesNameentities
6在 module.json5 配 ohos.ability.intent.metadata官方未要求任何 metadata 声明(该写法无官方出处)
7ShareData(类)SharedData + SharedRecord
8ShareDataContent(分 default / utd 两种写法)不存在;统一用 SharedRecord { utd, content / uri }
9shareIntent 当查询接口用,或找 getAllInsightIntent不存在;shareIntent 上报、deleteIntent / deleteEntity 删除
10ShareController.show(context) 省略第二参数ShareControllerOptions 不可省略
11show() 第一参数传 common.Context官方是 common.UIAbilityContext
12在 @kit.IntentsKit 里找执行器基类执行器在 @kit.AbilityKit;@kit.IntentsKit 只提供意图共享
13小看意图框架的门槛需 AGC 能力申请 + 小艺开放平台注册,仅中国境内、仅企业开发者、不支持模拟器
14碰一碰分享后想拿结果官方明确:宿主应用无法获得碰一碰分享结果,系统会通知用户

五、错误码处置表

模块错误码官方含义线上处置
意图共享401参数错误校验 InsightIntent 五个必填字段是否齐全
意图共享1000101101 ~ 1000101106业务异常族本地限流(≤20 次/天)+ 失败直接丢弃,不重试
意图共享1000101201业务异常检查 payload 是否超过 50KB
系统分享1003702001记录类型不支持SharedRecord.utd 必须是 UniformDataType 里的合法常量
系统分享1003702002IPC 数据过大图片走 uri 而非 content,压缩 thumbnail
扫码直达(前置)—App Linking 未生效检查是否手动签名(自动签名不支持 App Linking)+ 域名是否 HTTPS

意图框架上线清单(缺一项都上不了线):

  1. AGC 后台申请「意图框架」开放能力(1~3 个工作日);
  2. 真机验证(模拟器不支持本 Kit);
  3. 应用市场上架 + 小艺开放平台意图注册(3~5 个工作日);
  4. 用 keywords 覆盖真实用户说法(「我的快递到哪了」「还有多久到」「昨天买的那箱零食」都要写进去)。

六、写在最后

把这一篇的两个能力放在一起看,会发现它们解决的是同一个问题:把「打开 App」这个动作彻底消灭掉。

  • 意图框架让入口从「点 5 步」变成「说一句」;
  • 系统分享 / 碰一碰让分享从「复制粘链接」变成「碰一下」。

但这两个能力也都有一个共同特征:它们是系统级能力,所以你必须在系统的规则里做事——意图框架有 AGC 与地区限制、有每天 20 次的共享配额;碰一碰有亮屏解锁的前提、且宿主拿不到结果。把这些「限制」写进设计文档,比多写 300 行代码更重要。

购物节真正的胜负手,从来不是商品详情页做得多漂亮,而是 用户想买的那一刻,你的入口在哪里。


附:本文 API 速查卡(可直接贴进代码仓库 README)

// ① 意图执行器(@kit.AbilityKit,注意不在 IntentsKit)
import { InsightIntentExecutor, insightIntent } from '@kit.AbilityKit';
class MyExecutor extends InsightIntentExecutor {
  async onExecuteInUIAbilityForegroundMode(
    intentName: string, params: Record<string, Object>, windowStage: window.WindowStage
  ): Promise<insightIntent.ExecuteResult> { /* loadContent + return { code: 0, result: {...} } */ }

  async onExecuteInUIAbilityBackgroundMode(
    intentName: string, params: Record<string, Object>
  ): Promise<insightIntent.ExecuteResult> { /* 静默执行 */ }
}
insightIntent.ExecuteMode.UI_ABILITY_FOREGROUND = 0;
insightIntent.ExecuteMode.UI_ABILITY_BACKGROUND = 1;
insightIntent.ExecuteMode.UI_EXTENSION_ABILITY  = 2;
// ExecuteResult { code, result?, uris?(18+), flags?(18+) }

// ② 意图共享(@kit.IntentsKit)—— 配额:20 次/天、单次 ≤50KB
import { insightIntent } from '@kit.IntentsKit';
await insightIntent.shareIntent(context /* common.BaseContext */, intents);
await insightIntent.deleteIntent(context, intentName, identifiers);
// InsightIntent { intentName, intentVersion: '1.0.1', identifier, intentActionInfo, intentEntityInfo }

// ③ 意图配置文件:src/main/resources/base/profile/insight_intent.json(单数,全工程唯一)
// { "insightIntents": [ { intentName, domain, intentVersion, srcEntry(≤127B),
//     uiAbility: { ability, executeMode: ["foreground","background"] } } ] }

// ④ 系统分享(@kit.ShareKit,起始 4.1.0(11))
import { systemShare } from '@kit.ShareKit';
import { uniformTypeDescriptor as utd } from '@kit.ArkData';
const data = new systemShare.SharedData({ utd: utd.UniformDataType.PLAIN_TEXT, content: '...' });
data.addRecord({ utd: utd.UniformDataType.HYPERLINK, uri: 'https://...' });
const controller = new systemShare.ShareController(data);
controller.on('shareCompleted', (r: systemShare.ShareOperationResult) => { /* r.targetAbilityInfo.name */ }); // 18+
await controller.show(context /* common.UIAbilityContext */, { anchor: 'shareBtnId',
  previewMode: systemShare.SharePreviewMode.DEFAULT, selectionMode: systemShare.SelectionMode.SINGLE });
// 错误码:401 / 1003702001 记录类型不支持 / 1003702002 IPC 数据过大

// ⑤ 碰一碰 / 隔空传送(@kit.ShareKit · harmonyShare)
harmonyShare.on('knockShare', { windowId },
  (t: harmonyShare.SharableTarget) => t.share(data));       // 离开页面必须 off
harmonyShare.on('gesturesShare', (t: harmonyShare.SharableTarget) => t.share(data)); // 建议 3 秒内
// canIUse('SystemCapability.Collaboration.HarmonyShare');宿主应用拿不到分享结果
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐