Shiply Harmony 应用内升级 SDK 集成
一、隐私安全说明
SDK 名称:Shiply Harmony SDK(应用内升级)
版本:2.5.7
更新时间:2026-09-21
SDK 介绍:为移动开发者提供专业的应用内灰度升级能力,复用 Shiply 配置下发协议拉取升级策略(systemId=10016)。
服务提供方:深圳市腾讯计算机系统有限公司
接入指引:《Shiply Harmony 应用内升级 SDK 接入指引》
隐私保护规则:《Shiply 灰度升级SDK隐私保护规则》
前置概念:接入前请先阅读 通用概念,了解各平台共用的
appId/appKey/userId/deviceId/custom_properties等核心概念,避免跨平台混淆。
二、接入 SDK
应用内升级能力集成在 @shiply/shiply HAR 包中,无需额外依赖:
ohpm install @shiply/shiply
// entry/oh-package.json5
{
"dependencies": {
"@shiply/shiply": "^2.5.7"
}
}
三、版本相关参数(必读)
升级策略按「应用版本」匹配,因此版本参数填错是最常见的接入问题:轻则匹配不到策略,重则升级完成后仍反复弹窗、激活事件不上报。
3.1 需要宿主传入的字段
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
appVersion | string | 是 | 应用版本名(versionName) |
versionCode | string | 是 | 应用版本号(versionCode),必须为真实值 |
buildNumber | string | 是 | 应用构建号(buildNo),必须为真实值 |
三个版本字段均为宿主应用自身的版本信息,缺一不可。
「已更新」判定会拿本机三元组(versionName + versionCode + buildNo)与策略下发值逐字段比较,任一字段不匹配即判不出「已更新」→ 升级完成后仍会持续弹窗,且激活事件不会上报。
versionCode不传时 SDK 会回退读本机(bundleManager),但这仅是兑底,不应依赖;buildNumber运行时无法自动获取:鸿蒙构建号配置在build-profile.json5的buildVersion,由 hvigor 编译期注入,运行时系统 API 读不到(Android 的PackageInfo同样无此字段)。宿主需在编译期把注入值写入常量(如BuildProfile)后传入;buildNumber未传(恒为"0")时与策略下发的构建号不等,「已更新」判定失效 + 激活上报不触发。SDK 初始化时会对未传buildNumber打告警日志,接入后请检查日志。
versionCode 可从 bundleManager 获取:
import { bundleManager } from '@kit.AbilityKit';
const bundleInfo = bundleManager.getBundleInfoForSelfSync(
bundleManager.BundleFlag.GET_BUNDLE_INFO_DEFAULT);
let versionName: string = bundleInfo.versionName; // 如 "6.4.50"
let versionCode: string = bundleInfo.versionCode.toString(); // 如 "6450"
3.2 参数取值示例
import { ShiplyUpgradeConfig } from '@shiply/shiply';
let config: ShiplyUpgradeConfig = new ShiplyUpgradeConfig();
// versionName:对应 BundleInfo.versionName
config.appVersion = "6.4.50";
// versionCode:必填真实值,对应 BundleInfo.versionCode
config.versionCode = "6450";
// buildNo:必填真实值。鸿蒙构建号在 build-profile.json5 的 buildVersion(编译期注入),
// 运行时读不到,需在编译期写入常量后传入
config.buildNumber = "634";
3.3 三个版本字段的作用与影响
appVersion(versionName):决定能否拉到对应版本的策略,并参与「是否已更新」判定。若与策略登记的版本不一致,策略匹配不到,或升级装完新包后仍被判为「未更新」而继续弹窗。versionCode(版本号):同一版本名下的不同版本靠它区分,是「已更新」判定的必比对字段。buildNumber(构建号):同一版本名、同一版本号下的不同构建靠它区分。与策略侧构建号不一致时,升级完成后的「已更新」判定失效——装完新包仍会继续弹窗,且激活事件不上报。
三个字段都会与策略下发的 apkBasicInfo 对应字段逐一比对,任一不匹配即判定为「未更新」。
四、初始化 SDK
import { ShiplyUpgradeConfig, ShiplyUpgrade } from '@shiply/shiply';
import { common } from '@kit.AbilityKit';
import { bundleManager } from '@kit.AbilityKit';
import hilog from '@ohos.hilog';
// 版本参数(见第三节)
const bundleInfo = bundleManager.getBundleInfoForSelfSync(
bundleManager.BundleFlag.GET_BUNDLE_INFO_DEFAULT);
let config: ShiplyUpgradeConfig = new ShiplyUpgradeConfig();
// 从 Shiply 平台应用信息获取
config.appId = "your_app_id";
config.appKey = "your_app_key";
config.bundleId = "com.your.app";
config.userId = "your_user_id"; // 用户 ID(协议中的 guid)
config.deviceId = "your_device_id"; // 设备 ID(协议中的 qimei)
config.appVersion = bundleInfo.versionName; // versionName
config.versionCode = bundleInfo.versionCode.toString(); // versionCode(必填真实值)
// buildNumber:必填真实值,从编译期注入的构建号常量取(见 3.1,运行时读不到)
config.buildNumber = "634";
config.osVersion = "6.1.0"; // 系统版本
config.custom_properties = { channel: "appstore", user_tag: "vip" };
config.ignoreFrequencyLimit = false; // 是否忽略限频,默认 false
// 独立部署场景(需联系 Shiply 团队开通)
config.customServerUrl = "";
config.businessSetName = "";
// 多环境联调(独立部署场景):切逻辑环境,不改包
config.envId = "";
// debug 包可拉取未正式发布的策略(提测阶段自测灰度升级用)
config.isDebugPackage = false;
let logger = (level: number, log: string) => {
hilog.info(0x0000, 'ShiplyLog', '%{public}s', log);
};
let context = getContext(this) as common.UIAbilityContext;
let dbPath = context.filesDir;
let upgradeSDK = new ShiplyUpgrade(dbPath, config, logger);
构造即完成初始化,并自动执行一次激活检测上报(每次冷启动一次)。
custom_properties 的类型ShiplyUpgradeConfig.custom_properties 类型为 Record<string, string>。ArkTS 严格模式下不能直接用对象字面量赋值,需先声明接口:
interface CustomProperties extends Record<string, string> {
channel: string;
user_tag: string;
}
const props: CustomProperties = { channel: 'appstore', user_tag: 'vip' };
config.custom_properties = props;
ShiplyUpgradeConfig 参数
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
appId / appKey | string | 是 | 应用凭证 |
userId | string | 是 | 用户 ID(协议中的 guid) |
deviceId | string | 是 | 设备 ID(协议中的 qimei) |
bundleId | string | 是 | 包名,与策略登记值比对 |
appVersion | string | 是 | versionName |
versionCode | string | 是 | versionCode,必须为真实值(见第三节) |
buildNumber | string | 是 | buildNo(构建号),必须为真实值(见第三节;编译期从 buildVersion 取) |
osVersion | string | 否 | 系统版本,用于匹配下发条件 |
custom_properties | Record\<string, string> | 否 | 自定义属性,用于匹配下发条件 |
ignoreFrequencyLimit | boolean | 否 | 忽略限频,默认 false |
customServerUrl | string | 否 | 独立部署域名,传裸域名(/business 由编译宏统一补) |
businessSetName | string | 否 | 独立部署子业务集名 |
envId | string | 否 | 环境 ID(独立部署/多环境联调),默认空串走正式环境 |
isDebugPackage | boolean | 否 | 是否 debug 包;true 时可拉取未正式发布的策略(提测自测用),默认 false |
设备型号、系统版本等由业务注入,SDK 不主动采集。详见 合规使用指南。
五、检查更新
初始化后,在业务时机调用 checkGrayUpgrade(拉取 + 判定一体):
import {
ShiplyGrayTacticsEvent,
ShiplyGrayTacticsErrorCode,
ShiplyGrayTacticsRsp
} from '@shiply/shiply';
let onResult = (event: ShiplyGrayTacticsEvent,
errorCode: ShiplyGrayTacticsErrorCode,
grayTactics: ShiplyGrayTacticsRsp | null) => {
if (!grayTactics) {
return;
}
switch (event) {
case ShiplyGrayTacticsEvent.NewTactic: {
// 有可生效的新策略,业务自行弹窗
const title = grayTactics.clientInfo.title;
const desc = grayTactics.clientInfo.theDescription;
const downloadUrl = grayTactics.apkBasicInfo.downloadUrl; // 商店/更新页链接,业务打开它跳转更新
// 弹窗前建议上报一次曝光
upgradeSDK.reportEventPopupExposure();
// 业务打开链接跳转应用商店更新(示例)
let want: Want = { uri: downloadUrl, action: 'ohos.want.action.viewData' };
getContext(this).startAbility(want);
break;
}
case ShiplyGrayTacticsEvent.NoTactic:
// 服务端未下发策略
break;
case ShiplyGrayTacticsEvent.Error:
// errorCode 见 ShiplyGrayTacticsErrorCode
break;
}
};
upgradeSDK.checkGrayUpgrade(onResult);
SDK 不下载、不安装,只把 apkBasicInfo.downloadUrl(商店/更新页链接)交给业务。业务打开该链接跳转应用商店更新;装完新包后,下次冷启动 SDK 会自动检测到「本机版本 == 上次交付的目标版本」并上报激活事件。
因此同一个策略在宿主实际完成更新前会继续按限频策略弹窗,更新成功后自动停止。
downloadUrl 链接格式与打开方式
downloadUrl 由平台在策略中配置,业务直接打开即可:
| 场景 | downloadUrl 格式 | 打开方式 |
|---|---|---|
| 正式版(AppGallery) | store://appgallery.huawei.com/app/detail?id=<bundleId> | openLink 跳转应用市场详情页 |
接入方通常只需用 context.openLink(downloadUrl) 打开链接;不要自行拼接或改写 downloadUrl。
拉取失败与空策略的区分
拉取结果有三种不同形态,错误码已分开:
| 形态 | 回调 | 含义 |
|---|---|---|
| 本地有可用策略 | NewTactic | 即使本次拉取失败,也优先交付本地缓存策略,不会因网络抖动丢弃 |
| 服务端未下发策略 | NoTactic(7) | 平台本来就没配策略,检查平台配置 |
| 拉取失败且本地无缓存 | FetchFailed(8) | 网络/服务端不可达,宿主可据此做重试 |
事件与错误码
| ShiplyGrayTacticsEvent | 值 | 说明 |
|---|---|---|
| Error | -1 | 策略下发出错 / 弹窗条件不满足 |
| NoTactic | 0 | 没有拉到策略 |
| NewTactic | 1 | 有可生效的新策略 |
| ShiplyGrayTacticsErrorCode | 值 | 说明 |
|---|---|---|
| None | 0 | 无错误 |
| AlreadyUpdated | 1 | 已经是策略的版本 |
| InvalidType | 2 | 策略类型不正确(非鸿蒙灰度包) |
| BundleId | 3 | bundleId 与策略不匹配 |
| InvalidURL | 4 | downloadUrl 为空 |
| PopTimes | 5 | 弹窗次数超过上限 |
| TimeInterval | 6 | 弹窗时间小于最小间隔 |
| NoTactic | 7 | 没有拉到策略(服务端未下发) |
| FetchFailed | 8 | 拉取失败且本地无可用策略(网络/服务端不可达,可重试) |
| 100 及以上 | — | 缓存/网络相关错误码(对齐 GARErrorCode) |
弹窗判定链
判定顺序(任一不通过即拦截):
downloadUrl为空 →InvalidURLbundleId与宿主不匹配 →BundleIdpkgType不是鸿蒙包(6/7/8)→InvalidType- 策略版本 == 本机版本 →
AlreadyUpdated remindType == 红点(2)→ 放行(不受限频)- 限频:
popTimes/popInterval(ignoreFrequencyLimit = true时豁免)
「已更新」判定用逐字段比较(versionName + versionCode + buildNo),不做字符串拼接。策略侧 buildNoStr 优先,为空时回退整型 buildNo;versionCode 缺省归一到 "0"。
这也正是 versionCode 必填的原因——少传一个字段就判不出「已更新」。
六、事件上报
// 弹窗曝光
upgradeSDK.reportEventPopupExposure();
// 通用上报
upgradeSDK.reportEvent(ShiplyUpgradeEventType.Active, ShiplyUpgradeEventResult.Success);
| ShiplyUpgradeEventType | 说明 |
|---|---|
Active | 激活事件(SDK 自动上报) |
PopupExposure | 弹窗曝光 |
| ShiplyUpgradeEventResult | 值 | 说明 |
|---|---|---|
| Unknown | -1 | 触发 |
| Success | 0 | 成功 |
| Fail | 1 | 失败 |
业务无需手动上报激活事件。SDK 在冷启动时比对「本机版本」与「上次交付的目标版本」,命中即上报并清除记录。
激活判定不依赖本地缓存策略:即使服务端已撤回策略(本地缓存被清),已装完新包的用户仍会正常上报激活。激活上报的归因(tacticsId / updateType)取交付时刻的策略快照,不受后续账号切换、策略重发影响。待激活记录在上报成功后才清除:HTTP 失败时保留,下次冷启动自动重试(代价是可能重复上报,对漏斗统计比漏报更友好)。
七、获取策略信息
策略数据由 checkGrayUpgrade 的回调返回(ShiplyGrayTacticsRsp):
| 字段 | 类型 | 说明 |
|---|---|---|
title / newFeature | string | 更新标题 / 版本描述 |
publishTime / updateTime | number | 策略发布 / 更新时间(毫秒) |
updateStrategy | ShiplyGrayUpdateStrategy | 建议(1) / 强制(2) |
grayType | ShiplyGrayUpgradeType | 灰度(1) / 全量(2) / 热更新(3) |
popTimes / popInterval | number | 最大弹窗次数 / 最小弹窗间隔(毫秒) |
remindType | ShiplyGrayRemindType | 弹窗(1) / 红点(2) / 全部(3) |
tacticsId | string | 策略 ID |
status | number | 策略状态 |
apkBasicInfo | ShiplyApkBasicInfo | 安装包信息 |
clientInfo | ShiplyClientInfo | 弹窗文案(title / theDescription) |
extra | Record\<string, any> | 自定义下发字段 |
安装包信息(ShiplyApkBasicInfo):pkgName、md5、version(versionName)、versionCode、buildNo、buildNoStr、downloadUrl、pkgSize、bundleId、plistUrl、pkgType。
pkgType 取值:1=APK、2=TestFlight、3=Plist、4=AppStore、5=补丁包、6=鸿蒙灰度包、7=鸿蒙企业包、8=鸿蒙应用尝鲜灰度包(其余值为其他平台包类型)。
八、其他接口
// 设置自定义属性(下次拉取生效)
upgradeSDK.setCustomProperties(props);
// 修改用户 / 设备标识(真切换,异步完成)
upgradeSDK.setUserIdentifier("new_user_id");
upgradeSDK.setDeviceId("new_device_id");
upgradeSDK.setAppId("new_app_id");
// 重新读取本地策略(不触发网络)
upgradeSDK.reloadLocalGrayStrategy();
// 清空所有缓存
upgradeSDK.removeLocalGrayStrategy();
// SDK 版本号
let version = upgradeSDK.sdkVersion();
调用 setUserIdentifier / setDeviceId / setAppId 后,请再调一次 checkGrayUpgrade 拉取配置。
切换是异步完成的(内核走线程池切换存储),需等切换动作完成后再拉取;连续同步调用 setUserIdentifier(...) 后立即 checkGrayUpgrade(...) 可能仍用旧身份的本地策略。建议在切换后稍作延时或监听完成时机后再拉取。
九、注意事项
- 版本参数是接入第一坑:
appVersion/versionCode/buildNumber需与策略登记值一致,任一不匹配都会导致匹配不到策略或装完新包仍弹窗。详见第三节。 versionCode/buildNumber必填真实值:任一字段不匹配策略下发值都会导致「已更新」判不出,升级完成后持续弹窗且激活上报不触发。buildNumber运行时读不到(buildVersion编译期注入),需编译期写入常量后传入,详见第三节。- SDK 不下载不安装:只下发
downloadUrl(商店/更新页链接),业务打开该链接跳转应用商店完成更新;激活状态在下次冷启动时判定。 dbPath必填且需持久目录:new ShiplyUpgrade(dbPath, config, logger)第一个参数是数据目录,传context.filesDir。传空串或 cache 类可清理目录会导致限频状态丢失、用户被反复弹窗。customServerUrl传裸域名:不要自行拼/business,该前缀由 native 编译宏统一补,两侧各拼一次会产出/business/business而 404。- ArkTS 严格模式:
custom_properties需用extends Record<string, string>的接口声明后再赋值,字符串字面量索引签名会编译报错。 - 限频状态落盘在
dbPath目录:按tacticsId维度,30 天过期;同一tacticsId且publishTime变化视为服务端重发策略,会清零计数。 ignoreFrequencyLimit仅影响弹窗:不影响拉取,也不绕过版本/bundleId/pkgType 校验。
十、常见问题
Q:升级完成后还在反复弹窗,且激活事件没上报?
检查三个版本字段是否都传了真实值:appVersion 应对应策略侧 apkBasicInfo.version,versionCode 应对应 apkBasicInfo.versionCode,buildNumber 应对应 apkBasicInfo.buildNoStr(或 buildNo)。任一字段为默认值("0"/空串)都会同时导致弹窗不止 + 激活不上报。尤其确认 buildNumber 不是默认空串——它运行时读不到,需编译期从 buildVersion 取,这是鸿蒙侧最常见的原因。
Q:checkGrayUpgrade 一直回调 NoTactic?
依次检查:① 平台是否已创建并发布灰度任务;② 策略状态是否为「灰度中」;③ bundleId 是否与策略登记值一致;④ pkgType 是否为鸿蒙包类型(6/7/8);⑤ 下发条件(版本号、自定义属性)是否命中。
Q:拉不到策略?
依次检查:① 平台是否已创建并发布灰度任务;② 策略状态是否为「灰度中」;③ bundleId 是否与策略登记值一致;④ pkgType 是否为鸿蒙包类型(6/7/8);⑤ 下发条件(版本号、自定义属性)是否命中。
Q:回调 errorCode=8(FetchFailed)?
拉取请求失败(网络不通/服务端不可达)且本地无可用缓存策略。与 NoTactic(7) 的区别:7 是服务端正常返回但未下发策略;8 是请求本身失败。可据此做重试或提示网络异常。
Q:切换账号后配置不更新?
切换类接口只改标识,不会自动拉取,需自行再调一次 checkGrayUpgrade。
更多问题见 常见问题。