更新记录

1.0.0(2026-10-19)

首次发布:三端每日步数读取与授权生命周期

  • 对外 API 共 7 个:configure 配置隐私同意与远程通道、getEnvInfo 同步查询通道与能力位、requestAuthorization 拉起授权、getAuthorizationStatus 查询授权状态、cancelAuthorization 取消授权、getDailySteps 读取区间日步数、resolveRemote 远程通道回填。全部是 uni 风格 options + success / fail / complete,返回 void,不返回 Promise。
  • 通道模型:Android 按 Build.MANUFACTURER 命中小米 MIUI ContentProvider / 华为 HMS Health Kit / 荣耀 Health Kit + HonorId / vivo 远程通道;iOS 走 HealthKit(原生调用落在混编 BearHealthKitNative.swift);HarmonyOS NEXT 走 @kit.HealthServiceKit;Web 与小程序走 utssdk/index.uts 兜底,除 getEnvInfo 外回 9040009。
  • 通道判定、时间对齐、日步数聚合与补零、华为长区间分段重试、厂商原始码归类、授权状态归一都放在跨端纯逻辑层(core.uts / core-state.uts),三端共用同一份实现,同一机型不会出现分端行为差异。
  • 时间口径:timestamp 为本地时区当天 0 点、单位毫秒,查询区间左闭右开。
  • vivo 与荣耀的服务端链路走注入式远程通道(RemoteChannel 协议函数):插件包内零域名、零密钥,client_secret 与 access_token 都不进 App;未注入实现时数据类接口回 9040008。实现方拿到结果后按请求里的 requestId 调 resolveRemote({ requestId, data }) 回填,失败时也走它、把 errCode 写成非 0——入参对象里没有 success / fail 回调(uni-app 的 JS↔原生桥只传数据,原生侧构造的闭包过不到 JS)。errCode 只认 9040 段位内的码,段位外的数一律归 9040008,原值仍写进 errMsg。
  • 荣耀链路(静默登录、授权页结果解析、换 token、采样查询)每一跳都挂 20s 看门狗:厂商 SDK 或接入方服务端不回调时按 9040005(授权 / 查状态 / 取消)或 9040006(取步数)如实返回一次,errMsg 点名停在链条哪一跳并给出该跳的排查方向,不会让业务侧无限等待。同一份链路摘要拼在失败结果末尾,凭证只记「有没有取到 + 长度」。
  • 取消授权按各端真实能力返回:华为 / 荣耀 / 鸿蒙走 SDK 撤销(mode: 'sdk'),vivo 与远程通道走服务端解绑(mode: 'remote');Apple 与小米在系统层面没有撤销接口,统一回 9040007 并随结果给出用户自助撤销路径,不做假成功。
  • 隐私合规前置:未 configure({ privacyAgreed: true }) 时任何数据类接口都回 9040004,插件不会静默初始化厂商 SDK;privacyAgreed: false 会连同既有配置与已登记的远程实现一起清空,作为用户在应用内撤回同意的落点。Android 端只声明小米通道需要的 ACTIVITY_RECOGNITION 与 miui.permission.READ_STEPS(v1 只读不写,不声明 WRITE_STEPS),不申请定位等无关权限。
  • 错误码占 9040 段位,序号 001–009 全部登记文案;厂商原始码(HMS 50065、鸿蒙 1002703001、HealthKit 错误域)不占段位,统一拼进 errMsg。
  • 真机验收:小米(2026-10-16)、华为 P30 Pro(2026-10-16)、HUAWEI nova 14 / HarmonyOS 6.1.0.135(2026-10-16)、iPhone 6s / iOS 15.8.8(2026-10-17)、荣耀 RMO-AN00 / MagicOS 7.1(2026-10-19,配合接入方自己的 code2token 服务端)五条通道都在云端自定义调试基座上跑通四步链路并取到按天步数。vivo 通道尚未真机验收(无设备、平台资质未申请):这条通道不碰任何原生 SDK,实现全在你的远程代码里,所以案例页内置了一套可直接跑的 vivo 实现 + 零依赖模拟服务端,秒级时间戳 / 裸数组 / 未绑定 50001401 三种回包形状在任意机型上都能复现。
  • iOS 读取授权口径:HKAuthorizationStatus 按 Apple 文档只表示写入授权,用它当读取门禁会把已允许的用户也挡在外面;本插件改用 getRequestStatusForAuthorization,authorized 在 iOS 端表示「授权流程已完成」而不是「一定能读到」。用户在授权页里拒绝时 Apple 不给信号,查询如实回全 0 并在 detail 里说明「未授权或区间内无记录不可区分」。
  • 案例页覆盖环境检测 / 正常路径(配置 → 授权 → 读步数)/ 失败路径(9040004、9040001、9040007、9040008 可视化)/ 各厂商取消授权 / 生命周期五类场景,每个按钮下方直接显示本次结果;命中荣耀机型时展开「荣耀通道参数」区(App ID 与远程接口地址填进去就能跑通,不必改代码),vivo 远程实现与模拟服务端内置在案例里,onUnload 停止回调写入。
  • 服务端两跳的官方口径已抄录进 readme:荣耀 Code 换取 Access Token(POST https://hnoauth-login.cloud.honor.com/oauth2/v3/token、application/x-www-form-urlencoded、code 使用前去掉 \ 转义并 urlencode、expires_in 3600 秒不要长期缓存、失败只判 sub_error);vivo Health Kit 云云接入接口文档(/cloud/steps 无区间入参、有 /cloud/oauth/unAuthorize 解绑、客户端不可直连云服务)。
  • 华为 Android 通道的完整操作说明(scope 申请项、agconnect-services.json 放哪、证书与基座怎么打、打完包的自检命令、故障对照表)见同目录 huawei-android-setup.md。厂商资质申请材料清单不随插件包发布,需向作者索取。
  • 已知限制:v1 只读取步数、只读不写(wantWrite: true 回 9040001);Android 仅适配华为 / 荣耀 / vivo / 小米,其余厂商回 9040002;华为通道依赖 utssdk/app-android/config.json 的 project 里 apply 的 com.huawei.agconnect(agcp:1.9.1.301)——AGC 的应用身份由该插件在构建期写进安装包资源,只放 agconnect-services.json 而不 apply 插件时 HMS 取不到身份、授权会回原始码 907135000;不用华为通道的接入方可以删掉 project.plugins / project.dependencies 这两项,其余通道照常工作,只是华为机型回 9040002;小米通道要求宿主 manifest.json 的 app-plus.distribute.android.targetSdkVersion ≥ 29(uni-app 云打包默认 28,低于它系统不允许申请 ACTIVITY_RECOGNITION)。心率、睡眠与运动记录待后续版本(各厂商为独立 scope 与独立资质审批)。

平台兼容性

uni-app(4.75)

Vue2 Vue3 Chrome Safari app-vue app-nvue Android iOS 鸿蒙
√ √ × × √ - 6.0 12 12
微信小程序 支付宝小程序 抖音小程序 百度小程序 快手小程序 京东小程序 鸿蒙元服务 QQ小程序 飞书小程序 小红书小程序 快应用-华为 快应用-联盟
× × × × × × × × × × × ×

uni-app x(4.75)

Chrome Safari Android iOS 鸿蒙 微信小程序
- - 6.0 12 12 ×

其他

多语言 暗黑模式 宽屏模式 蒸汽模式
× × × √

bear-health-data

统一读取设备侧健康平台的步数,并配套完整的授权生命周期(拉起授权 → 查授权状态 → 取消授权)。支持 Android(小米 / 华为 / 荣耀 / vivo)/ iOS(HealthKit)/ HarmonyOS NEXT(Health Service Kit) 三端,一份代码跑多厂商:业务侧不需要按厂商写分支,但可以随时查到「这台机器命中了哪条通道、这条通道能不能撤销」。

与官方 API 的关系:本插件不替换 uni.getWeRunData(那只服务微信小程序),而是直接驱动各端原生健康平台;对上层给出 uni 风格的 options + success / fail / complete 接口,不返回 Promise。

v1 边界(如实声明,避免按宣传语去找不存在的能力):

  • 只有步数,且只读:心率、睡眠、运动记录在各厂商是独立 scope 与独立资质审批,未纳入 v1;requestAuthorization({ wantWrite: true }) 会直接回 9040001。
  • Android 只适配华为 / 荣耀 / vivo / 小米四类机型,其余厂商回 9040002;v1 刻意不接 Google Health Connect,误判成任一厂商只会让用户看到一个弹不出来的授权页。
  • vivo 与荣耀的服务端链路由接入方后端承担:插件零域名、零密钥,client_secret 与 access_token 都不进 App。
  • 厂商资质申请材料清单(各开放平台要提交什么、多久过审、产出物放哪里)不在插件包内,需向作者索取。
  • 授权档位(普通授权 / 源码授权)与权利边界见 license.txt;厂商申请材料清单只随源码授权档发送。

兼容性

平台 支持 实现方式 最低版本 说明
Android √ 按 Build.MANUFACTURER 命中通道:小米 MIUI 步数 ContentProvider / 华为 HMS Health Kit / 荣耀 Health Kit + HonorId / vivo 走注入式远程通道 minSdk 23;小米通道另需宿主 targetSdkVersion ≥ 29 小米、华为、荣耀三条通道已真机实测通过(小米 / 华为 2026-10-16,荣耀 2026-10-19);华为侧还要求宿主配 agconnect-services.json 并在 config.json 里 apply AGC 插件(见「使用前提」);荣耀 SDK 由 utssdk/app-android/config.json 依赖引入,需云端打包;荣耀通道还需 configure({ honorAppId }) + 注入带 exchangeAuthorizationCode 的远程实现(回填走 resolveRemote),并在开放平台登记包名与签名证书 SHA256 指纹,四项缺一取步数就失败
iOS √ Apple HealthKit(原生层为混编 Swift BearHealthKitNative.swift) deploymentTarget 12 已真机实测通过(2026-10-17,iPhone 6s / iOS 15.8.8);需要宿主 App ID 开启 HealthKit capability,模拟器不可用;status 口径与别的端不同,见「通道与能力」下的 iOS 专段
HarmonyOS √ @kit.HealthServiceKit healthStore,只申请 dailyActivities 读取 API 12(HarmonyOS NEXT) 已真机实测通过(2026-10-16,HUAWEI nova 14 / HarmonyOS 6.1.0.135);资质在 AppGallery Connect 侧申请(dailyActivities 数据类型需逐项开通,未开通时授权或取数会失败),另需设备已安装「华为运动健康」并同意隐私政策
Web / 小程序 × 走 utssdk/index.uts 兜底 - getEnvInfo() 照常返回且 channel 恒为 none,其余接口一律回 9040009

运行环境要求:HBuilderX 3.6.8+,uni-app / uni-app-x 4.75+,vue3。

Android 小米通道的硬性前置:宿主 manifest.json 的 app-plus.distribute.android.targetSdkVersion MUST ≥ 29(uni-app 云打包不写时默认 28)。低于 29 时系统不允许申请 ACTIVITY_RECOGNITION,requestAuthorization 回 9040002,errMsg 里会带出要改的这段配置。

真机实测状态(截至 1.0.0)。下表里的通道都在 HBuilderX 云端打包的自定义调试基座上跑通了 configure → requestAuthorization → getAuthorizationStatus → getDailySteps 四步,并能取到按天聚合的步数:

通道 实测机型与系统 验收日期
小米 / Redmi(MIUI 步数 Provider) 小米真机,宿主 targetSdkVersion: 30 2026-10-16
华为(HMS Health Kit) 华为 P30 Pro(VOG-AL10,Android 10,HMS Core 6.16.4.352,华为运动健康 17.0.7.310) 2026-10-16
荣耀(HonorId + Health Kit) 荣耀 RMO-AN00(MagicOS 7.1),配接入方自己的 code2token 服务端 2026-10-19
HarmonyOS NEXT(Health Service Kit) HUAWEI nova 14(TLR-AL00,HarmonyOS 6.1.0.135),已装「华为运动健康」 2026-10-16
iOS(HealthKit) iPhone 6s(iOS 15.8.8),宿主 App ID 已开 HealthKit capability 2026-10-17
vivo(云云接入) — —

每条通道要准备的资质与宿主配置见下面的「使用前提」。vivo 这条不碰任何原生 SDK,实现全在你注入的远程代码里,所以案例页内置了一套可直接跑的 vivo 实现 + 零依赖模拟服务端:四个协议函数、web-view 承接授权页、用户返回后回查状态再 resolveRemote 结算的时序,以及秒级时间戳 / 裸数组 / 未绑定 50001401 三种回包形状都能在任意机型上复现(非 vivo 机型时插件不会调用这套实现,案例改用自检按钮直接调它并打印回填入参)。口径见「vivo 通道怎么接」。

表格数值与 package.json 的平台矩阵、各端 config.json 逐项一致;uni-app-x 与 uni-app 共用同一份原生实现,最低版本相同。


使用前提

  1. 必须真机 + 自定义调试基座:UTS 插件不参与标准基座;Android 端还依赖 HMS / Honor 的 Maven 依赖,只能走云端打包。没走云端打包时的典型表现是本地编译 UTS 在 utssdk/app-android/index.kt 报 找不到名称“hihonor”(或 huawei)并附官方 known-issue error18 链接——那是本地编译不解析 config.json 依赖,不是代码或依赖声明写错。
  2. Android 宿主 targetSdkVersion 必须 ≥ 29(小米通道):ACTIVITY_RECOGNITION 是 Android 10(API 29)才有的运行时权限,而 uni-app 云打包不显式配置时 默认是 28,此时系统权限申请接口直接抛 IllegalStateException。在宿主 manifest.json 写:
    { "app-plus": { "distribute": { "android": { "targetSdkVersion": 30 } } } }

    改完 MUST 重新云端打自定义调试基座才生效。本插件在申请前会校验宿主 targetSdk,低于 29 时如实回 9040002 并把上面这段配置写进 errMsg,不会让原生异常逸出到 JS。 MUST NOT 把它写进插件的 utssdk/app-android/config.json:该文件支持的顶层字段只有 abis / dependencies / minSdkVersion / project(见官方 UTS 插件文档「Android平台原生配置」一节:https://uniapp.dcloud.net.cn/plugin/uts-plugin.html#androidconfigjson),targetSdkVersion 写进去不报错但完全无效——targetSdk 是整个 App 的打包参数,不属于插件。宿主侧配置项说明见 https://uniapp.dcloud.io/tutorial/app-android-targetsdkversion。

  3. 宿主侧配置由接入方提供,插件一律不内置:

    • 华为:在 AGC 为宿主包名创建应用并开通「运动健康」服务、申请步数读 scope;下载 agconnect-services.json 放到宿主工程的 nativeResources/android/(uni-app 对这类配置文件的官方口径见 https://doc.dcloud.net.cn/uni-app-x/plugin/uts-for-android.html「其他配置文件」一节),并在 AGC「项目设置 > 常规」登记该包实际打包用的签名证书 SHA256 指纹——包名或签名指纹与基座/正式包不一致时,HMS 侧会直接拒绝授权。还差一步:AGC 的配置是华为的 com.huawei.agconnect Gradle 插件在构建期写进安装包资源的,本插件已在 utssdk/app-android/config.json 的 project 里 apply 它(见「实现要点」);不用华为通道的接入方可以把 project.plugins / project.dependencies 这两项删掉,删掉后本插件其余通道照常工作,只是华为机型会回 9040002。没合入 json 或没 apply 插件时 HMS 取不到应用身份,表现为授权页拉不起来并带出 HMS 原始码 907135000,或直接回 9040002 并在 errMsg 里说明要配 agconnect-services.json。另外需在「华为运动健康」里同意开发者隐私政策;没同意时 HMS 会在读数据时报错,本插件翻成 9040006 并把 HMS 原始码带在 errMsg 里(与插件侧的 privacyAgreed 合规开关是两件事)。华为 Android 通道的完整操作说明(scope 申请项、配置文件放哪、证书与基座怎么打、打完包的 2 分钟自检、故障对照表)见同目录 huawei-android-setup.md。
    • 荣耀:这条通道是「原生授权 + 接入方服务端」两段式,两段都配齐才能取到步数:

      1. 在荣耀开放平台申请应用后拿到 App ID,通过 configure({ honorAppId }) 传入(只留在内存,不落盘、不打日志)。没传时任何荣耀接口都回 9040002。 同一个应用里还要登记三件事:应用包名,该包实际打包用的签名证书 SHA256 指纹(调试基座与正式包是两套指纹,都要登记),以及为该应用开通「荣耀账号服务」与「运动健康 Health Kit」并申请 step.read scope。这三项与 App ID 不一致时 HonorId 校验不会通过;荣耀 SDK 这些内部分支既不打日志也不回调业务,所以真机上不容易看出卡在哪,插件用 20s 看门狗把这种静默转成 9040005 / 9040006 并在 errMsg 里列出这三项。但「点了没反应」不一定出在登记项——远程实现没按 resolveRemote 契约回填时表现一模一样,见下面的第 2 条与 resolveRemote 一节。
      2. 通过 configure({ remote }) 注入的远程实现里必须带 exchangeAuthorizationCode:插件把 HonorId 的一次性 authorizationCode 交给你自己的服务端,服务端用 client_secret 换回 { accessToken }(HTTP 非 200 或缺 accessToken 都算失败)。缺这个函数、或 remote.channel 写成了别家(只有空串或 'honor' 才算接管本通道),getDailySteps 都会回 9040008。 openId 不从远程接口取:荣耀构造查询账号用的 openId 只来自 HonorId 的 SignInAccountInfo.openId(真机实测 2026-10-17 RMO-AN00 上这份有值),服务端只需要回 accessToken。本地这份为空时插件直接回 9040003 并提示重新授权。 authorizationCode 是一次性的:交出去换过一次 token 就作废,所以插件在每次需要换 token 前会重新静默登录取新码,不会拿旧码去撞服务端。 请求体字段名与响应包法由接入方服务端自定:案例页同时送 code 与 authCode(同值),响应里 accessToken / access_token 在顶层或包一层 data 都能取到,服务端回的 msg 会原样显示在案例页上。 回填用 resolveRemote({ requestId: req.requestId, data: { accessToken } }),不是 req.success(...)——远程入参对象里没有回调函数(原生闭包过不了 JS↔原生桥),失败时同样调它,把 errCode 写成非 0。

      授权、查状态、取消授权三步只走原生 HonorId,不需要远程实现,所以「授权成功、读步数失败」是这条通道最常见的形态;9040008 是可修复的配置问题,不是机型或资质问题。案例页在命中荣耀机型时会展开「荣耀通道参数」区,App ID 与服务端地址填进去就能直接跑通取数,不必改代码。 荣耀链路的每一步(静默登录、授权页结果解析、换 token、采样查询)都挂 20s 看门狗:某一步没回调时按 9040005(授权/查状态/取消)或 9040006(取步数)如实回一次,errMsg 里写明停在具体哪一跳以及那一跳该查什么(停在「远程换 token」先查服务端与网络,停在「采样查询」先查荣耀运动健康有没有当天数据),不会让业务侧无限等待。 这条链路同时会逐跳打关键日志(logcat 前缀 [bear-health-data]),并把同一份链路摘要拼在失败结果的 errMsg 末尾(形如 [荣耀链路]读步数发起 账号可复用=false → 静默登录通过 openId=有(36) authorizationCode=有(284) → 换 token 发起 … → 换 token 回来 accessToken=有(64) 线程=main → 采样查询失败 errorCode:xxx)。凭证一律只记「有没有取到 + 长度」,accessToken / authorizationCode / openId 的明文既不进日志也不落盘。部分厂商定制系统(如 MagicOS)关闭了第三方 App 的 logcat 读取,此时链路摘要就是唯一的现场证据。

    • vivo:整条链路(授权页 URL、取数、解绑)都在接入方服务端,通过 configure({ remote }) 注入;授权页由宿主的 web-view 页面承接。案例页内置了这套实现与一个零依赖模拟服务端,见「vivo 通道怎么接」。
  4. iOS:宿主 App ID 开启 HealthKit capability;插件随附 Info.plist 的 NSHealthShareUsageDescription 与 HealthKit entitlement 文案,宿主可在自己的证书里保留同名项。HealthKit 在模拟器上不可用,验收 MUST 用 iOS 15+ 真机 + 云端自定义调试基座;模拟器上 getDailySteps 会如实回 9040002。已实测机型:iPhone 6s(iOS 15.8.8),四步链路走通并能取到按天步数(2026-10-17);这是目前实测的最低系统版本,deploymentTarget 声明的 iOS 12–14 尚未实测。
  5. HarmonyOS:在 AGC 申请 Health Service Kit 的数据类型权限;用户在「华为运动健康」内未同意隐私政策时回 9040004,插件会同时引导跳到运动健康 App。已实测机型:HUAWEI nova 14(TLR-AL00,HarmonyOS 6.1.0.135,包内只声明 ohos.permission.INTERNET,不申请任何系统权限)。
  6. 调用顺序固定:configure({ privacyAgreed: true }) → requestAuthorization → getAuthorizationStatus / getDailySteps。未 configure 或隐私未同意时任何数据类接口都回 9040004,插件不会静默初始化厂商 SDK;用户在应用内撤回同意后,再调一次 configure({ privacyAgreed: false }) 就让插件回到未配置态(连同注入的远程实现一起注销)。
  7. 权限最小化:Android 端只声明小米通道读步数需要的 ACTIVITY_RECOGNITION 与 miui.permission.READ_STEPS(v1 只读不写,不声明 WRITE_STEPS);ACTIVITY_RECOGNITION 由 requestAuthorization 现申请,其余端与其余厂商都不申请任何系统权限,授权发生在厂商账号与运动健康 App 内。

快速开始

<script setup>
import { configure, getEnvInfo, requestAuthorization, getDailySteps } from '@/uni_modules/bear-health-data'

const env = getEnvInfo()                       // 同步,configure 之前也能查:channel / 三个能力位
console.log(env.channel, env.channelReason)    // 'huawei' / '华为设备,走 HMS Health Kit'

configure({
  privacyAgreed: true,                         // 必须先向用户说明用途并取得同意
  honorAppId: '',                              // 荣耀设备必填
  remote: null,                                // vivo / 荣耀取数时注入接入方实现
  success: (res) => {
    requestAuthorization({                     // 用户拒绝也算正常返回,走 success 按 status 降级
      success: (auth) => {
        if (auth.status != 'authorized') return
        getDailySteps({
          days: 7,
          success: (res) => console.log(res.totalStep, res.steps),  // timestamp 为当天 0 点,单位 ms
          fail: (err) => console.log(err.errCode, err.errMsg)
        })
      }
    })
  }
})
</script>

import 只允许写到插件根目录,MUST NOT 写 @/uni_modules/bear-health-data/utssdk/index.uts。

小米/红米机型先检查宿主 manifest.json 里有 "app-plus": { "distribute": { "android": { "targetSdkVersion": 30 } } }(缺省是 28,走不通 ACTIVITY_RECOGNITION 的动态申请),改完需重新云端打自定义调试基座。详见「使用前提」第 2 条。


API

所有异步接口都是 uni 风格:一个 options 入参携带 success / fail / complete,返回 void;fail 收到的对象继承自 UniError,含 errCode / errMsg / errSubject(errSubject 恒为 bear-health-data)。

configure(options)

记录隐私同意、注入远程通道、配置荣耀 App ID 与回溯上限。未命中可用通道时 configure 依然成功,channel 会是 none 并在 channelReason 里说明原因——这样业务能先把「本机不支持」显示出来,而不是被一个异常打断。

参数 类型 默认值 说明
privacyAgreed boolean - 必填。为 false 时回 9040004 并清空此前的配置与已登记的远程实现——用户在应用内撤回同意,就是再调一次 configure 并把本参数置 false
honorAppId string '' 荣耀开放平台应用 ID,仅命中 honor 通道时必需
remote RemoteChannel | null null 自定义远程通道实现(vivo / 荣耀);传 null 即无远程通道
maxBackfillDays number 90 步数查询最大回溯天数,超过 365 会被截断
success / fail / complete function - uni 风格回调

ConfigureResult(字段均为实际生效值,用于核对命中了哪条链路):

字段 类型 说明
platform string android / ios / harmony
channel string 本次判定命中的通道,取值见下方「通道与状态取值」
channelNeedsRemote boolean 该通道是否依赖接入方远程实现(vivo / 荣耀 / remote 为 true)
remoteConfigured boolean 远程通道是否已登记(登记不等于可用)
remoteHandlers Array\<string> 已登记的函数名列表,用来核对漏注了哪个
honorAppIdSet boolean 荣耀 App ID 是否已配置
maxBackfillDays number 实际生效的回溯上限

getEnvInfo() : EnvResult

同步返回环境与能力,不需要权限、configure 之前也可调用,用于案例页环境检测块与业务运行时自检。

字段 类型 说明
osName / osVersion string 端标识与系统版本原文,如 14 / 17.5 / 5.0.1
manufacturer string 设备厂商(Android 取 Build.MANUFACTURER,iOS / 鸿蒙为空串)
brand string 设备品牌
channel string 判定命中的通道;无可用通道为 none
channelReason string 判定说明:无通道 / 需要远程而未注册 / 缺荣耀 App ID
stepsSupported boolean 该通道能否读取步数
cancelSupported boolean 该通道能否在插件内取消授权(小米、iOS 为 false)
authSupported boolean 该通道能否在插件内拉起授权
configured boolean 是否已成功 configure
remoteConfigured boolean 远程通道是否已注册
healthPlatform string 本端依赖的原生健康平台名称,未实现端为 not-support
supportedPlatforms Array\<string> 插件声明支持的端,取自 package.json 平台矩阵

requestAuthorization(options)

拉起本端通道的授权。小米走系统 ACTIVITY_RECOGNITION 弹窗,华为 / 荣耀走厂商授权页(结果由 UTSAndroid.onAppActivityResult 回收),vivo / remote 交给注入实现,iOS 走 HKHealthStore.requestAuthorization,鸿蒙走 healthStore.requestAuthorizations。

参数 类型 默认值 说明
wantWrite boolean false v1 只读;传 true 回 9040001,避免业务误以为已拿到写入权限
success / fail / complete function - 用户拒绝属正常返回(success,按 status 降级);仅链路本身失败才走 fail,如小米通道宿主 targetSdkVersion 低于 29 时回 9040002 并在 errMsg 给出要改的 manifest 配置

AuthStatusResult:

字段 类型 说明
channel string 判定所依据的通道
status string authorized / denied / undetermined / unsupported
canCancel boolean 该通道能否在插件内取消授权
rawStatus string 厂商 SDK / 系统返回的原始状态文本,便于排查
detail string 面向用户的说明,如「请先在华为运动健康 App 内同意隐私政策」

getAuthorizationStatus(options)

不弹任何界面地查询当前授权状态,适合进入页面时先判定再决定是否引导授权。入参只有 success / fail / complete,返回结构同上为 AuthStatusResult。查询本身失败(厂商 SDK 报错、取不到 Activity / Context)回 9040005。

cancelAuthorization(options)

撤销授权。这是各端能力差异最大的一项:Apple 与小米在系统层面就不给第三方撤销接口,插件不会假装成功。

参数 类型 默认值 说明
clearLocalData boolean false 是否同时删除厂商侧已同步的数据。置 true 后用户历史步数不可恢复,业务 MUST 先二次确认
success / fail / complete function - 该端 / 厂商无法在插件内撤销时回 9040007,errMsg 里带用户自助撤销路径

CancelResult:

字段 类型 说明
channel string 操作所依据的通道
cancelled boolean 授权是否确实已撤销
mode string sdk(厂商 SDK 撤销:华为 / 鸿蒙 / 荣耀原生)/ remote(服务端解绑)/ local-only(只清插件内存态)/ unsupported
guide string 用户自助撤销指引,mode 为 local-only / unsupported 时必然非空
detail string 厂商返回的原始信息

getDailySteps(options)

读取区间内的日步数。

参数 类型 默认值 说明
startTime number - 区间起点(ms),按整天取;与 endTime 都不传时按 days 取最近 N 天
endTime number 当前时刻 区间终点(ms),含所在日期整天
days number 30 未传 startTime 时生效的回溯天数(含今天),上限取 configure.maxBackfillDays
includeEmptyDays boolean true 无记录的日期是否补 0 返回;关闭后区间内的空洞要业务自己识别
success / fail / complete function - 未授权回 9040003(小米在读取前自检系统权限;iOS 只在「还没拉起过授权页」时回,见下面的 iOS 口径说明),厂商侧读取失败回 9040006;华为的「未授权」由 HMS 自身报错,落到 9040006 并在 errMsg 带出原始码

DailyStepsResult:

字段 类型 说明
channel string 实际使用的通道
startTime / endTime number 生效区间(ms),起点已对齐当天 0 点,终点为末日次日 0 点(左闭右开)
unit string 恒为 ms,作为契约留位,后续版本改单位时业务可感知
steps Array\<DailyStep> { timestamp, step },按 timestamp 升序;timestamp 是本地当天 0 点
totalStep / dayCount number 区间合计 / 返回天数(恒等于 steps.length)
rawSampleCount number 厂商返回的原始采样条数,用来核对聚合是否丢数据
chunked boolean 是否命中过厂商区间限制而分段重读合并(华为长区间实测报 50065)
detail string 补充说明

resolveRemote(options)

远程通道的回填入口,只有注入了 remote 实现(vivo / 荣耀 / remote 通道)的业务需要调它:注入的函数请求完自己的服务端后,用请求入参里的 requestId 调本接口把结果交回插件。

参数 类型 默认值 说明
requestId string - 必填,取 req.requestId 原样带回。插件用它认领那次请求;不认识或已结算回 9040008
data RemoteReplyData | null null 成功时必填,按请求种类填对应字段(见下表)
errCode number 0 失败时给非 0(通常 9040008),插件把它转成业务收到的错误。段位内的码(9040001–9040009)原样透传;填段位外的数(如厂商自定义码)一律归 9040008,你原填的数仍会写进业务收到的 errMsg 里,不会丢
errMsg string '' 失败说明
success / fail / complete function - 只表示这次回填有没有被插件接收,与那次数据接口的结果无关

RemoteReplyData(按需填其中一个组合,字段全为可选):

字段 类型 用于哪个请求
steps Array\<DailyStep> getDailySteps
status string getAuthorizationStatus / requestAuthorization,取值同 AuthStatusResult.status
cancelled / mode / guide boolean / string / string cancelAuthorization;mode 一般写 remote
accessToken string exchangeAuthorizationCode。荣耀只需要这一个字段,openId 由 HonorId 账号对象给出,不由服务端下发
detail string 全部,实现方自带的排查说明

为什么回填要单独调一个接口,而不是 req.success(...):uni-app 的 JS↔原生桥只传数据,插件在原生侧构造的回调函数过不到 JS 里。真机(荣耀 RMO-AN00,2026-10-19)在 JS 侧实测注入的实现拿到的 req.success 是 undefined,一调用就抛 req.success is not a function,异常留在 webview、插件毫无感知,于是整条远程通道表现为「20s 内没有任何回调」。所以现在请求只带编号,回填走本接口,一个 requestId 只结算一次(重复回填后续调用会收到「requestId 不存在」)。

通道与状态取值

channel 端 取步数方式 插件内可撤销 需要接入方后端
xiaomi Android MIUI 步数 ContentProvider × ×
huawei Android HMS Health Kit readDailySummation √ ×
honor Android HonorId 授权 + 荣耀 Health Kit √ √(换 access_token)
vivo Android 全部由注入的远程实现完成 √(远程解绑) √
remote Android 接入方自定义远程通道 √(远程解绑) √
healthkit iOS HealthKit HKStatisticsQuery × ×
harmony-health HarmonyOS Health Service Kit readData √ ×
none 任意 无可用通道,数据类接口回 9040002 × ×

status 取值:authorized(已授予)/ denied(拒绝或已撤销)/ undetermined(从未申请)/ unsupported(该通道无法表达授权状态)。

iOS(healthkit)的 status 口径与别的端不同,必读:Apple 明确不向 App 暴露「读取是否被允许」——HKAuthorizationStatus 只表示写入授权,对只申请读取的步数类型恒为 denied(真机表现为「用户已在授权页点了允许,仍回未授权」)。本插件因此改用 getRequestStatusForAuthorization,authorized 在 iOS 端的含义是授权流程已完成(系统不再弹授权页),而不是「已确认允许」;undetermined 表示还没拉起过授权页。用户真的在授权页里拒绝时,HealthKit 让查询返回空而不是报错,此时 getDailySteps 成功回全 0、rawSampleCount 为 0,并在 detail 里写明「未授权或区间内无记录,Apple 不区分这两者」。业务侧 MUST NOT 把 iOS 的 authorized 当成「一定能读到数据」,正确做法是按查询结果降级。

远程通道注入协议(RemoteChannel)

插件对 vivo / 荣耀只定义协议、不带实现,五个函数按需注入,缺哪个即视为该能力不可用(回 9040008)。每个请求入参都带 channel 与 requestId,结果一律用 requestId 调 resolveRemote 回填,入参对象里没有也不该有 success / fail:

函数 入参 回填 data 字段
getDailySteps RemoteStepsRequest{ channel, requestId, startTime, endTime } steps(+ 可选 detail)
getAuthorizationStatus RemoteStatusRequest{ channel, requestId } status(+ 可选 detail)
requestAuthorization RemoteAuthRequest{ channel, requestId } status,用户中途放弃回填 denied
cancelAuthorization RemoteCancelRequest{ channel, requestId, clearRemoteData } cancelled / mode / guide / detail
exchangeAuthorizationCode RemoteTokenRequest{ channel, requestId, authCode } accessToken(荣耀链路服务端只需要回这一个)

失败同样走 resolveRemote,把 errCode 写成非 0、errMsg 写清楚即可;errCode 只认 9040 段位内的 9 个码,段位外的数会被归成 9040008(原数仍拼在 errMsg 里),业务侧因此不需要为厂商自定义码写分支。忘记回填时插件不会让业务无限等待,荣耀通道由 20s 看门狗按阶段回 9040005 / 9040006 并点名卡在哪一跳。

exchangeAuthorizationCode 背后对接的是荣耀官方的 Code 换取 Access Token 接口(文档),你那跳服务端替 App 做的正是它,几个会影响成败的细节:

  • 端点 POST https://hnoauth-login.cloud.honor.com/oauth2/v3/token,Content-Type: application/x-www-form-urlencoded(不是 JSON);表单体 grant_type=authorization_code、code、redirect_uri、client_id、client_secret。
  • 插件交出来的 authCode 是 HonorId silentSignIn 那份一次性授权码(官方口径:SDK 静默授权换 token 依赖 silentSignIn 拿到的 code)。官方明确要求使用前去掉 \ 转义字符并做 urlencode;redirect_uri 在 SDK 接入下填平台上配置的回调地址,没配回调地址时填固定值 honorid://redirect_url。
  • 成功回 access_token / expires_in / token_type,scope 含 openid 时另有 id_token,authorize 带 access_type=offline 时才有 refresh_token。expires_in 是 3600 秒,所以别把 token 缓存成长期状态——本插件每次取步数都会要一份新码来换。响应里没有 openId,插件用的是 HonorId 账号对象里那份,服务端不需要下发。
  • 失败回 { "error": 主码, "sub_error": 子码, "error_description": … },官方建议只判子码。和这条链路最相关的三个:20155 code 过期、20156 code 已被使用过(授权码是一次性的,这两种都要重新取码而不是重试)、12303 / 12304(client_id 不存在 / client_secret 不对,属平台登记问题)。入参类子码(20002、20022、20024、20152、20154、20172、20182 等)官方说明是不要重试,改代码。
  • 这一跳失败请回填 resolveRemote({ requestId, errCode: 9040008, errMsg: <带上 sub_error 与 error_description> }),插件会把原文拼进 errMsg 末尾的 [荣耀链路] 摘要,用户侧就能区分「卡在厂商 SDK」与「卡在换 token」。

impl.channel 传空串表示接管本端默认远程通道,写 'vivo' / 'honor' 则只服务该厂商。一份实现同时接两家时按入参的 req.channel 分派,别为了区分厂商丢掉另一家的函数。

vivo 通道怎么接(案例已内置演示)

vivo 是本插件里唯一完全不碰原生 SDK 的 Android 通道:设备命中 vivo 后,四个数据类接口全部转调注入的远程实现,exchangeAuthorizationCode 用不到(vivo 的授权码在 vivo→你的服务端回跳里就地换掉了,App 不经手,所以漏注这一个函数不影响 vivo)。

vivo 侧走的是官方 vivo Health Kit 云云接入接口文档——「云云接入」的意思就是服务端到服务端,官方 FAQ 明确写了客户端不可以直连其云服务,所以这条通道必须配你自己的后端。签名(HmacSHA256 + URL-Safe Base64)、appId/appSecret、accessToken(默认 30 天)与 refreshToken(默认 180 天,过期要用户重新授权)、服务端出口 IP 登记这些都在后端那一层,插件侧一概不经手。

协议函数 你要提供的服务端接口 回填
requestAuthorization 下发 vivo 侧 H5 授权页 URL(/auth?appId=&redirectUri=&state=,redirectUri 需 UrlEncode 且要先让 vivo 配置) 打开 URL 让用户同意,返回后回查绑定状态再回填 status
getAuthorizationStatus 查绑定关系(代理官方 /cloud/oauth/queryStatus,未授权回 4005) status:已绑定 authorized / 未绑定 undetermined
getDailySteps 按区间取日步数(代理官方 /cloud/steps,官方接口没有区间入参,区间过滤得你后端自己做) steps(timestamp 毫秒);未绑定 / 授权失效回填 errCode: 9040003
cancelAuthorization 服务端解绑(代理官方 /cloud/oauth/unAuthorize,解绑后官方其他接口会回 5032) cancelled / mode: 'remote',自家后端另需清掉存的 token 与绑定关系

四个必须注意的点:

  • 授权结果没有同步回调。流程是「服务端下发 URL → webview 承接 → 用户在页内同意 → vivo 回跳你的服务端换 token」,App 侧只能等用户返回后回查状态再结算这一次 requestId。案例用一个 web-view 页面承接(example/vivo-auth.vue,本地模拟页在 hybrid/html/vivo-auth-mock.html),并在本页 onShow 里回查后回填;用户长时间不返回时由案例的 120s 定时器把这次请求收口成 9040008,因为插件对远程请求不设超时。授权页路径属于宿主,插件 MUST NOT 代你 navigateTo。

  • 时间戳单位要自己核对。官方 StepInfo 是 {t: 13 位毫秒, v: double},与本插件 DailyStep.timestamp 的毫秒契约天然对得上,MUST NOT 除 1000;一旦回秒级值,会被折成 1970 年的某天、再被区间过滤掉,现象是「服务端明明有数据、插件回空数组」而不是报错。历史素材里踩过(原生分支除过 1000、vivo 分支没除),案例按位数补齐并留了开关复现。步数同理:官方 v 是小数,Math.round 是实现方的活。

  • 回包形状不唯一。历史素材的服务端既可能回 {data:{stepInfoList:[…]}} 也可能直接回数组,两种都要能吃;业务码 50001401(未绑定 / 授权失效)要转成 9040003 而不是给空数组,否则用户以为自己步数是 0。该码不在官方码表内(官方对应语义是 4005 / 5032),新接入的后端建议直接透官方码。

  • 当天查不到步数不一定是坏了。官方给的时延口径是「距上次上报 > 半小时 或 累计 > 1000 步才会上报」,云端同步另有延迟;非 vivo 手机与安卓版本过低都不支持 vivo Health Kit。

  • 授权页路径属于宿主。案例把承接页放在 example/vivo-auth.vue(整页只有一个 web-view,这是 App 端对 web-view 必须是页面根元素的限制),复制到宿主工程后 MUST 在宿主 pages.json 里登记这一页,插件 MUST NOT 假定路径存在就 navigateTo。

案例页的「vivo 通道」卡片可以在任何机型上把这套实现跑一遍:勾上「内置模拟服务端」就是零依赖的假数据(四个开关分别复现秒级时间戳、裸数组、未绑定),自检按钮用案例自编的 requestId 直接调这四个函数并打印会回填的入参。本机 channel 不是 vivo 时插件不会调用这套实现——通道由「端 + Build.MANUFACTURER」判定,插件刻意不提供覆盖入口,vivo 真机上勾上「注入远程通道」再按正常路径的按钮走完整链路。vivo 通道至今未做真机验收,验收前请按上面的官方文档把服务申请、出口 IP 与步数权限(permissionId 1)先就位。

注意两级检查不同粒度:configure 的 remoteConfigured 与 getEnvInfo 的 remoteConfigured 只看有没有登记并接管该通道,getDailySteps 才会逐个函数检查——登记了 remote 却漏注 exchangeAuthorizationCode,configure 仍显示 remoteConfigured=true,取数时回 9040008。ConfigureResult.remoteHandlers 列出本次真正登记上的函数名,接进来先核对这一项最省事。

configure 只需在 App 启动或进入功能页时调一次,注入的 remote 实现会被插件保留到后续接口使用。插件侧已给 configure 加 @UTSJS.keepAlive(官方 UTS 插件文档「keepAlive」一节):UTS 默认在「收到回调的那次调用」返回后就释放 JS 传进来的函数,若不加这个装饰器,第一次取步数之后再调就会在原生侧报 exchangeAuthorizationCode回调函数已释放,不能再次执行(荣耀真机实测踩过,现象是远程通道整体失效、取步数必然失败)。重复 configure 以最后一次注入的实现为准,MUST NOT 每次取数都重新 configure。

JS→原生方向靠 @UTSJS.keepAlive 保活,原生→JS 方向的回调却根本传不过去(见上面 resolveRemote 的说明),两件事容易混为一谈:前者是「函数被回收」,后者是「函数压根没出现在入参对象里」。远程通道要跑通,两者都得按现在的实现走。


实现要点与注意事项

  • 分层与唯一答案:interface.uts 只有声明;unierror.uts 是错误码表 + UniError 子类;core.uts 放与平台无关的纯逻辑(通道判定、能力表、时间对齐、区间归一、日步数聚合、厂商原始码归类、结果装配);core-state.uts 放有生命周期的配置中心 ConfigHub 与远程通道中心 RemoteHub(与 core.uts 拆开只因单文件行数上限);各端 index.uts 只做「读设备侧事实 → 守卫 → 驱动厂商 SDK → 用 core 装配函数回结果」。通道判定与日步数聚合在三端只有一份实现,这两件事最容易写出行为差异,分端 MUST NOT 自己算一遍。
  • 时间口径统一:timestamp 恒为本地时区当天 0 点、单位 ms,区间左闭右开。历史素材里 iOS / Android 返回毫秒而业务再除 1000、秒与毫秒混用的坑在本插件收口;dayStartOf 只在一处实现,跨时区与夏令时的逐日推进用「日 0 点 + 整天数」而不是给原时刻加天。
  • 厂商差异的处理方式:六套授权状态写法(1/0/yes/authorized,鸿蒙与华为含义还相反)统一折叠到四个 status 值,原始值放 rawStatus 带出;厂商原始码(HMS 50065、鸿蒙 1002703001、HealthKit 错误域)不占段位序号,一律拼进 errMsg 的 detail——为每家单开一码会逼业务按厂商写分支,与「一份代码跑多端」相悖。
  • 读取前的授权自检:小米通道在 getDailySteps 里先查 ACTIVITY_RECOGNITION 是否已授予,未授予直接回 9040003,不让 ContentProvider 抛错被翻成查询失败;荣耀(HonorId 未登录 / 无授权码)、iOS(getRequestStatusForAuthorization 回 shouldRequest,即还没拉起过授权页)、鸿蒙(getAuthorizations 未含 DAILY_ACTIVITIES)同样回 9040003。两处例外:华为——HMS 对未授权只报查询失败,本插件如实落 9040006 并带出原始码,MUST NOT 为了口径统一去猜它是不是没授权;iOS——用户拒绝读取时 Apple 不给任何信号(查询回空),所以自检只能拦「没弹过授权页」,拦不了「弹了但点了不允许」,见上面的 iOS 口径说明。
  • 华为长区间自动分段:readDailySummation 跨天区间实测报 50065,插件按 7 天切段串行重读合并(并发读会互相触发同一个码),并如实把 chunked=true 告知业务;其余端不做重试,失败就直接回码。
  • 荣耀判定必须在华为之前:荣耀独立后新机型仍可能上报 huawei,resolveChannel 里先匹配 honor / hihonor。
  • iOS 混编层的原因:HealthKit 的授权与统计接口大量使用 Swift 值类型(Set、Date、DateComponents、OptionSet),UTS 直接桥接不稳定,所以实际调用落在 BearHealthKitNative.swift,app-ios/index.uts 里没有任何 HealthKit import,只传毫秒数与布尔 / 字符串回调。跨这一层的数值入参与回调签名 MUST 是 NSNumber 而不是 Double(UTS 的 number 在 iOS 端就编译成 NSNumber,闭包类型也跟着是 (NSNumber, NSNumber) -> Void)。
  • iOS 读取授权只能「异步问系统要不要弹页」:HKHealthStore.authorizationStatus(for:) 看名字像授权状态,实际按 Apple 文档只表示「是否授权保存该类型」,读取被拒与被允许都看不出区别(真机实测:用户在授权页点了允许,它仍回原始值 1)。所以本端判定走 getRequestStatusForAuthorization(toShare:read:)(iOS 11+,异步),MUST NOT 在 getDailySteps 里同步读 authorizationStatus 当门禁——那会让所有 iOS 用户都拿不到步数。
  • 撤销不假成功:cancelSupported 由通道能力表给出,不由分端自述;iOS / 小米回 9040007 并随 guide 给出系统设置或厂商 App 内的自助路径。
  • 第三方原生依赖(版本与来源):Android 端全部由 utssdk/app-android/config.json 声明并在云端打包时从厂商官方 Maven 仓库解析——华为 com.huawei.hms:health:6.15.0.300、com.huawei.hms:hmscoreinstaller:6.6.0.300(https://developer.huawei.com/repo/)、荣耀 com.hihonor.mcs:fitness-health:1.1.0.300、com.hihonor.mcs:honorid-opensdk:8.0.1.351(https://developer.hihonor.com/repo/);iOS 端只链接系统框架 HealthKit.framework,鸿蒙端只用 @kit.HealthServiceKit,两端都不引入第三方二进制。插件包内 libs/ 与 Frameworks/ 为空,也 MUST NOT 通过 npm 依赖或运行时下载的方式引入原生代码。
  • config.json 的 project 为什么要 apply 华为的 Gradle 插件(动这段前必读):project.plugins: ["com.huawei.agconnect"] + project.dependencies: ["com.huawei.agconnect:agcp:1.9.1.301"],这是 DCloud 官方给的唯一注入口(plugins 进云端工程 app 模块的 plugins 块,dependencies 进项目级 buildscript,见 https://uniapp.dcloud.net.cn/plugin/uts-plugin.html#androidconfigjson)。华为侧的应用身份不是运行时读 res/raw 里的 json,而是由这个插件在构建期把它加密成 string/agc_* 资源写进安装包,agconnect-core 运行时读的是这些资源——所以宿主只把 agconnect-services.json 放进 nativeResources/android/ 而不 apply 插件,HMS 依然取不到应用身份(实测表现为授权页拉不起来、HMS 回原始码 907135000)。版本 MUST 写死:官方文档示例给的是 agcp:1.6.0.300,云端 AGP 8 上 apply 它会报 API 'android.registerTransform' is removed.;1.9.1.301 已实测可在同一云端链路正常出包(基座里 string/agc_* 66 条)。荣耀的 com.hihonor.mcs.asplugin 仍不注入——它的 classpath 坐标要挂在项目级 buildscript,本插件不持有这段配置;荣耀通道的 App ID 由 configure({ honorAppId }) 在运行时传入。两个 SDK 的运行时依赖都是普通 Maven 制品,去掉 Gradle 插件后 HuaweiHiHealth.getXxxController / HealthKit.getDataStoreClient 照常可用,插件包内零厂商 App ID、零密钥。
  • 合规红线(发布审核关注点):插件不采集、不会上传任何健康数据,数据只在用户本机与其系统健康平台之间读取;access_token / authorizationCode / openId 只在内存传递,MUST NOT 落盘、MUST NOT 打日志;包内零域名、零密钥、不内置厂商 App ID;未 configure 或隐私未同意时不调用任何厂商 SDK。
  • 回调语义:每个接口只触发一次 success 或 fail,并成对触发 complete;远程实现重复回填同一个 requestId 时,RemoteHub 在第一次结算就把该号摘掉,后续回填只回一句「requestId 不存在或已结算」,业务不会收到重复结果。等回填的请求最多保留 32 条,超限时淘汰最早那条,避免接入方忘记回填导致内存挂账。
  • 申请系统权限前先验宿主 targetSdk:UTSAndroid.requestSystemPermission 对 ACTIVITY_RECOGNITION 有硬性前置——宿主 targetSdkVersion 必须 ≥ 29,低于它(uni-app 云打包默认 28)时接口同步抛 IllegalStateException,异常穿过原生桥变成未捕获的 JS 错误,success/fail 一个都不回。小米通道因此在申请前先 checkSystemPermissionGranted(已授予直接成功返回,且查询本身包了 try/catch),再读 applicationInfo.targetSdkVersion 决定是否放行,宿主配置不当时回 9040002 并把要改的 manifest.json 路径写进 errMsg。任何原生接口调用都要预置「它会抛」的收口,MUST NOT 让原生异常裸奔到 JS。
  • 设备事实只用各端原生 API:getEnvInfo 的 osName/osVersion/brand 在 Android 取 Build.BRAND/Build.VERSION.RELEASE(端标识就是常量 'android',本文件只会被编译进这一端),iOS 取 UIDevice.current.systemVersion(混编层出一个 osVersion() 函数,品牌恒为 apple),鸿蒙取 deviceInfo.brand/deviceInfo.osFullName。MUST NOT 换回 uni.getSystemInfoSync():它是 uni ext-api 实现的 API,官方文档写明暂不支持在 UTS 插件里调用,uni-app(非 x)基座里没有 io.dcloud.uniapp.extapi.* 运行时类——UTS→Kotlin 编译能过,真机一点 getEnvInfo/configure 就抛 NoClassDefFoundError: UniGetSystemInfoKt,且同步接口异常不会走 success/fail 回调,现象是「点了没反应」。只有 utssdk/index.uts 的 JS 兜底层可以用 uni 取(那一层跑在 JS 里)。
  • Activity / Context 现取现用:Android 端每次调用取 UTSAndroid.getUniActivity(),不缓存成成员字段跨页面生命周期;取不到时回 9040002 而不是崩溃。鸿蒙端同理取 UIAbilityContext。
  • UTS 类型写法:数值判非法用 <= 0 或 != 判 NaN,不用 null 表达;Map 只用于按天累加,遍历顺序不依赖迭代行为(改为「命中的天先落数组再 sort」),因为三端 Map 迭代语义不一致。
  • Android 原生桥接的六条写法(UTS→Kotlin 编译实测,改代码前先照着做,别靠猜):
    1. Java 泛型回调接口 MUST 写成 implements OnSuccessListener<SampleSet> 这种带实参的形式,实参取 addOnSuccessListener 期望的类型(HMS 的 SampleSet / boolean / Void,荣耀账号 SDK 的 SignInAccountInfo,荣耀取数的 QueryResponse<SampleData>);写裸接口报「One type argument expected」,写 any 报类型不匹配。onSuccess 的形参跟着写成可空。
    2. 异常对象取信息用 .message,Kotlin 侧 java.lang.Exception 没有 getMessage() 方法。
    3. 入参是 Java String[] 的接口(HMS requestAuthorizationIntent、MIUI ContentResolver.query)MUST 传 arr.toTypedArray();UTS 的 Array<string> 直接传会报「actual type is 'UTSArray'」。UTSAndroid.requestSystemPermission / checkSystemPermissionGranted 收的就是 UTSArray,不要多转一层。
    4. java.util.List 只能按下标搬:list.size 判长度、list.get(i.toInt()) 取值,循环变量传给 get 前 MUST 显式 .toInt();厂商返回的 List 也不直接交给 UTS 侧的 Array<T> 形参,先在监听类里搬成 UTS 数组。
    5. UTS 的 number 编译成 Kotlin 的 Number,直接 .toString() 可能得到科学计数法串(1.76E12)。凡是要塞进 SQL 选择参数、ContentProvider 查询条件或厂商日期接口的数值,MUST 先 .toLong().toString();小米通道实测这样才拿得到 _begin_time 的行。
    6. 从采集线程发回 JS 的回调 MUST 用 Handler(Looper.getMainLooper()).post(() => {...}),MUST NOT 用 UTSAndroid.getDispatcher('main'):后者编译成 CoroutineScopeKt.MainScope() + launch,协程内的异常没有 handler 会被静默吞掉,表现为接口调用「既不成功也不失败、日志也没有」。 另:荣耀取数链路的 Task 监听在 com.hihonor.mcs.fitness.health.task 包下,和账号 SDK 的 com.hihonor.cloudservice.tasks 不是同一套接口,混用编译期即报类型不匹配。

错误码

错误码以 90 开头,本插件占 9040 段位,序号 009 固定表示平台不支持。与 utssdk/unierror.uts 逐项一致。

errCode 说明
9040001 参数错误(如 wantWrite: true,v1 只实现只读)
9040002 当前设备或厂商没有可用的健康数据通道(含取不到 Activity / Context、荣耀通道缺 honorAppId、宿主 targetSdkVersion 低于 29 导致小米通道无法申请系统权限)
9040003 未授权或授权被用户拒绝(iOS 端只表示「还没拉起过授权页」,用户在授权页里拒绝读取时 Apple 不给任何信号)
9040004 隐私合规未通过,请先向用户说明数据用途并调用 configure(含厂商侧运动健康隐私政策未同意)
9040005 授权状态查询失败(含荣耀 HonorId 在 20s 内没有任何回调,errMsg 会列出要核对的开放平台登记项)
9040006 步数数据读取失败(含荣耀换 token 后采样查询在 20s 内无回调)
9040007 当前端或厂商不支持在插件内取消授权(iOS / 小米;errMsg 带自助撤销路径)
9040008 远程通道未注册或调用失败(vivo / 荣耀未注入 remote 实现;荣耀取步数还要求注入的实现带 exchangeAuthorizationCode,且换回的响应里有非空 accessToken——服务端超时、HTTP 非 200、包里没有 token 都算这一码,errMsg 里带远程接口回的原文与荣耀链路摘要)
9040009 当前平台不支持(Web / 小程序等无原生实现的端)

目录结构

bear-health-data/                      # 独立可运行的 uni-app(vue3)工程,HBuilderX 导入即到案例页
├── pages.json                         # 首屏登记为案例页
├── manifest.json / main.js / App.vue / uni.scss
├── vendor-access-checklist.md         # 厂商资质申请材料清单(不随插件包发布,需向作者索取)
├── deviation.md                       # 对仓库规则的偏离记录(§9),不随插件包发布
└── uni_modules/bear-health-data/      # 发布时只右键上传这个目录
    ├── package.json                   # 元数据、平台矩阵与各端最低版本
    ├── readme.md / changelog.md / license.txt
    ├── huawei-android-setup.md        # 华为 Android 通道接入与自检说明
    ├── example/index.vue              # 案例页:环境检测 / 正常路径 / 失败路径 / 各厂商取消授权 / 生命周期;每个按钮下方直接显示本次结果,荣耀机型额外给出 App ID 与远程接口地址两个输入项
    ├── example/vivo-auth.vue          # vivo 授权页承接页(整页一个 web-view,复制到宿主后需在宿主 pages.json 登记)
    └── utssdk/
        ├── interface.uts              # 类型与错误码契约(只有声明)
        ├── unierror.uts               # 9040 段位错误码表 + HealthDataFailImpl
        ├── core.uts                   # 通道判定与能力表、时间与区间、步数聚合、状态归一、结果装配
        ├── core-state.uts             # ConfigHub / RemoteHub(有生命周期的跨端状态中心)
        ├── index.uts                  # Web / 小程序等无原生实现端的兜底导出
        ├── app-android/               # index.uts 主类 + steps-xiaomi / steps-huawei / steps-honor
        │   ├── config.json            # minSdk 23 + HMS / Honor Maven 依赖与仓库 + 华为 AGC Gradle 插件
        │   └── AndroidManifest.xml    # 只声明小米通道需要的 ACTIVITY_RECOGNITION 与 READ_STEPS
        ├── app-ios/                   # index.uts(无 HealthKit import)
        │   ├── BearHealthKitNative.swift  # HealthKit 混编实现
        │   ├── Info.plist             # NSHealthShareUsageDescription
        │   ├── UTS.entitlements       # HealthKit capability
        │   └── config.json            # deploymentTarget 12 + HealthKit.framework
        └── app-harmony/               # index.uts 驱动 @kit.HealthServiceKit + config.json(API 12)

隐私、权限声明

1. 本插件需要申请的系统权限列表:

健康数据读取授权:小米通道为系统运行时权限 ACTIVITY_RECOGNITION;华为/荣耀为各自开放平台的步数 scope;iOS 为 HealthKit 步数读取授权;HarmonyOS 为 Health Service Kit 的 dailyActivities 数据类型授权。均由用户在系统或厂商授权页同意后才读取,v1 不申请任何写入权限

2. 本插件采集的数据、发送的服务器地址、以及数据用途说明:

插件不采集、不会上传任何健康数据:每日步数仅在用户本机与其系统健康平台(华为/荣耀/vivo 运动健康、Apple HealthKit、HarmonyOS 运动健康)之间读取,读取结果只在内存交给应用界面;vivo 与荣耀的服务端链路由接入方自己的后端承担,插件包内零域名零密钥,授权码与 access_token 不落盘、不打日志

3. 本插件是否包含广告,如包含需详细说明广告表达方式、展示频率:

无