- name
- harmony-api
- description
- 在 harmony/ 子项目调用系统能力、配置权限、或新增原生(NAPI)API 前调用。覆盖 Stage 模型与 UIAbility 生命周期、module.json5 权限配置、本项目所用 12 个 Kit(剪贴板/加密/文件/网络/HUKS/生物认证/扫码等)的真实用法、NAPI 原生桥(libcryptocore.so)链路,以及 BusinessError/hilog 运行时约定。
# HarmonyOS 系统 API 开发
本 skill 是 ZPass `harmony/` 子项目调用系统能力的权威范式。所有 import 路径、API、字段
都取自项目真实代码与本地 SDK 声明,照抄优先于凭记忆。
## 一、何时用本 skill
满足任一条件即应先读本 skill:
- 要在 ArkTS 里调用系统能力:剪贴板、加密、文件读写、网络、HUKS、生物认证、扫码、振动等
- 要配置或申请权限(改 `module.json5` 的 `requestPermissions`,或运行时弹权限框)
- 要新增 / 修改原生 NAPI API(动 `cryptocore/src/harmony.rs`、`.d.ts` 声明、`oh-package.json5`)
- 要处理 UIAbility 生命周期、窗口沉浸式、系统栏、安全区
- 遇到 `BusinessError` 错误码、`hilog` 日志、`async` 与原生异步的映射问题
## 二、Stage 模型与工程结构
本项目是 **Stage 模型**(`entry/build-profile.json5` 的 `"apiType": "stageMode"`),
单 `entry` 模块(HAP),`module.json5` 里 `"type": "entry"`、`mainElement: "EntryAbility"`。
编译目标(以 `harmony/build-profile.json5` 的 `products[0]` 为准,不要写错):
- `targetSdkVersion`: **6.0.0(20)** — 即 API 20
- `compatibleSdkVersion`: **5.0.0(12)** — 即 API 12,最低兼容
- `runtimeOS`: `HarmonyOS`
注意:`harmony/.sdk-ref/sdk-api/` 下的 `.d.ts` 声明取自本地已装的 **API 24** SDK,**仅供查阅
签名与 import 路径,不是编译目标**。写代码时不要因为某 API 在 API 24 声明里存在就假设它在
API 12 可用;拿不准时以本项目已实际调用过的 API 为准。
关键工程文件:
- `harmony/build-profile.json5` — 应用级编译目标 / product
- `harmony/entry/build-profile.json5` — 模块级 `apiType` / `nativeLib`(把 `entry/libs/<arch>/*.so` 打入 HAP)
- `harmony/entry/oh-package.json5` — 模块依赖(含 `libcryptocore.so` 的 `file:` 依赖)
- `harmony/oh-package.json5` — 工程级 devDependencies(hvigor / hypium)
- `harmony/entry/src/main/module.json5` — abilities / pages / requestPermissions
- `harmony/entry/src/main/resources/base/profile/main_pages.json` — 路由页面列表
字体:本项目实际字体是 `'HarmonyOS Sans'` / `'HarmonyOS Sans Condensed'`。
(Geist 是 phone(RN) 端 token,**勿带入 harmony**。)
## 三、UIAbility 生命周期与 context
入口 `harmony/entry/src/main/ets/entryability/EntryAbility.ets`,`extends UIAbility`,
生命周期顺序:`onCreate` → `onWindowStageCreate` → `onForeground` ↔ `onBackground` →
`onWindowStageDestroy` → `onDestroy`。
import 行(真实):
```ts
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { display, mediaquery, window } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';
```
### context 与 filesDir 写入 AppStorage
`onCreate` 里把沙箱 `filesDir` 写进 AppStorage,供 VaultStorage / TrustedDeviceHuks 读取
(OHOS 5+ 沙箱路径可变,禁止硬编码):
```ts
onCreate(_want: Want, _launchParam: AbilityConstant.LaunchParam): void {
const ctx = this.context;
AppStorage.setOrCreate<string>('filesDir', ctx.filesDir);
}
```
下游用 `AppStorage.get<string>('filesDir')` 取回(见 VaultStorage.ets / TrustedDeviceHuks.ets)。
### 加载页面
`onWindowStageCreate` 里 `windowStage.loadContent('pages/Index', cb)`,`pages/Index` 必须
在 `main_pages.json` 的 `src` 数组里登记,否则路由失败。
### 沉浸式 / 系统栏 / 安全区(边到边)
四步(`applyImmersive`):
1. `mainWin.setWindowLayoutFullScreen(true)` — 窗口扩展到状态栏 / 导航栏区域
2. `mainWin.setWindowSystemBarProperties({...})` — 系统栏背景透明、按主题设前景色
3. `win.getWindowAvoidArea(type)` 取避让区,换算成 vp 写进 `safeAreaState`
4. `mainWin.on('avoidAreaChange', cb)` 订阅横竖屏 / 折叠态切换
真实片段:
```ts
const mainWin = windowStage.getMainWindowSync();
mainWin.setWindowLayoutFullScreen(true).catch((err: BusinessError) => {
hilog.error(DOMAIN, TAG, 'setWindowLayoutFullScreen failed: %{public}s', JSON.stringify(err));
});
mainWin.setWindowSystemBarProperties({
statusBarColor: '#00000000',
statusBarContentColor: fg, // 暗底白前景 / 亮底黑前景
navigationBarColor: '#00000000',
navigationBarContentColor: fg,
});
// avoidArea:window API 返回 px,ArkUI 单位是 vp,要用像素密度换算
const density = display.getDefaultDisplaySync().densityPixels || 1;
const area = mainWin.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM);
safeAreaState.top = area.topRect.height / density;
mainWin.on('avoidAreaChange', (data) => {
if (data.type === window.AvoidAreaType.TYPE_SYSTEM) {
safeAreaState.top = data.area.topRect.height / density;
} else if (data.type === window.AvoidAreaType.TYPE_KEYBOARD) {
safeAreaState.keyboard = data.area.bottomRect.height / density;
}
});
```
避让区类型用到:`TYPE_SYSTEM`(状态栏)、`TYPE_NAVIGATION_INDICATOR`(底部导航条)、
`TYPE_KEYBOARD`(软键盘)。
安全区状态走 `@ObservedV2` 单例(`state/SafeArea.ets`),不是 AppStorage:
```ts
@ObservedV2
export class SafeAreaState {
@Trace top: number = 0;
@Trace bottom: number = 0;
@Trace keyboard: number = 0; // 键盘占据高度(vp),不弹时 0
}
export const safeAreaState: SafeAreaState = new SafeAreaState();
```
页面里用 `@Local safeArea: SafeAreaState = safeAreaState;`,再 `.padding({ bottom: this.safeArea.keyboard })`。
原因:`@StorageProp` 是 ArkUI V1 装饰器,不能用在 `@ComponentV2` 里;本项目全量 V2。
### 跟随系统暗 / 亮模式
`mediaquery.matchMediaSync('(dark-mode: true)')` 拿初值 + `listener.on('change', cb)` 持续同步,
驱动 themeStore,再回调刷新系统栏前景色。`onDestroy` 里 `this.darkModeListener?.off('change')`。
## 四、module.json5 配置与权限
### 静态声明(已申请,照抄即可)
`module.json5` 的 `requestPermissions`(真实):
```json5
"requestPermissions": [
{
"name": "ohos.permission.ACCESS_BIOMETRIC",
"reason": "$string:permission_biometric_reason",
"usedScene": { "abilities": ["EntryAbility"], "when": "always" }
},
{ "name": "ohos.permission.VIBRATE" },
{ "name": "ohos.permission.INTERNET" }
]
```
权限用途映射:
| 权限 | 用途 | 触发场景 |
|---|---|---|
| `ohos.permission.ACCESS_BIOMETRIC` | 信任设备 / 生物解锁 | TrustedDeviceHuks(HUKS+UserAuth) |
| `ohos.permission.VIBRATE` | 剪贴板复制触感反馈 | Clipboard.lightHaptic |
| `ohos.permission.INTERNET` | HIBP 泄露检测 / LAN 同步 | Breach / SyncProtocol |
要点:
- `user_grant` 级权限(如 `ACCESS_BIOMETRIC`)必须带 `reason`(指向 `$string:` 资源)和 `usedScene`
- `system_grant` 级权限(`VIBRATE` / `INTERNET`)只写 `name` 即可,安装时自动授予
- abilities 里 `EntryAbility` 的 `skills` 含 `entity.system.home` + `action.system.home`(桌面图标入口)
### 运行时申请(user_grant 权限按需弹框)
声明在 `module.json5` 只是「可申请」;`user_grant` 权限实际授予要运行时弹框。
API(已用 `@kit.AbilityKit` 的 `abilityAccessCtrl`,声明 `@ohos.abilityAccessCtrl.d.ts`):
```ts
import { abilityAccessCtrl, common, Permissions } from '@kit.AbilityKit';
const atManager = abilityAccessCtrl.createAtManager();
const perms: Permissions[] = ['ohos.permission.ACCESS_BIOMETRIC'];
const result = await atManager.requestPermissionsFromUser(context, perms);
// result.authResults[i] === 0 表示已授予
```
注意:`ScanKit` 的相机权限由系统级扫码 UI 自己弹,**不需要**手动申请(见第五节 ScanKit)。
## 五、12 个 Kit 实战速查
每个 Kit 下:真实 import 行 + 关键 API + 所需权限 + 本项目最小片段 + 易踩坑。
(佐证文件见第九节。)
### 1. @kit.AbilityKit(UIAbility / context / 权限 / picker context)
```ts
import { AbilityConstant, UIAbility, Want, common } from '@kit.AbilityKit';
```
- 关键:`UIAbility` 生命周期、`this.context`(`UIAbilityContext`)、`common.Context`
- `common.Context` 作为参数传给 picker(见 Transfer.ets `exportVaultToFile(context: common.Context)`)
- 权限:无
- 坑:页面里拿 context 用 `getContext(this) as Context`;Ability 里用 `this.context`,二者不可混
### 2. @kit.ArkUI(window / display / mediaquery / router / promptAction)
```ts
import { display, mediaquery, window } from '@kit.ArkUI';
import { promptAction, router } from '@kit.ArkUI';
```
- 关键:见第三节(沉浸式 / 安全区 / 暗模式);页面跳转 `router.back()` / `router.pushUrl`
- `promptAction.showToast({ message, duration })`、`showDialog`、`showActionMenu`
- 权限:无
- 坑:`window` API 返回 px,UI 是 vp,必须 `display.getDefaultDisplaySync().densityPixels` 换算
### 3. @kit.BasicServicesKit(pasteboard / BusinessError)
```ts
import { pasteboard } from '@kit.BasicServicesKit';
import { BusinessError } from '@kit.BasicServicesKit';
```
- 关键:`pasteboard.getSystemPasteboard()`、`pasteboard.createData(MIMETYPE_TEXT_PLAIN, value)`、
`pb.setData(data)` / `pb.getData()` / `data.getPrimaryText()`
- 权限:无(剪贴板本身不要权限;触感反馈走另一个 Kit)
- 片段(Clipboard.ets `copyEphemeral`:30s 后剪贴板未变则清空):
```ts
const pb = pasteboard.getSystemPasteboard();
const data = pasteboard.createData(pasteboard.MIMETYPE_TEXT_PLAIN, value);
await pb.setData(data);
setTimeout(async () => {
const cur = (await pb.getData()).getPrimaryText();
if (cur === value) await pb.setData(pasteboard.createData(pasteboard.MIMETYPE_TEXT_PLAIN, ''));
}, 30_000);
```
- 坑:`BusinessError` 也从这个 Kit 导,不是从 `@ohos.base`(项目统一这么写)
### 4. @kit.SensorServiceKit(vibrator)
```ts
import { vibrator } from '@kit.SensorServiceKit';
```
- 关键:`vibrator.startVibration({ type: 'time', duration }, { id, usage })`
- 权限:`ohos.permission.VIBRATE`
- 片段(Clipboard.ets `lightHaptic`):
```ts
await vibrator.startVibration({ type: 'time', duration: 12 }, { id: 0, usage: 'touch' });
```
- 坑:模拟器 / 无马达设备会 reject,必须 try/catch 静默忽略,不要让它阻断主流程
### 5. @kit.CryptoArchitectureKit(cryptoFramework — 系统级哈希)
```ts
import { cryptoFramework } from '@kit.CryptoArchitectureKit';
```
- 关键:`cryptoFramework.createMd('SHA1' | 'SHA256')` → `md.updateSync({ data })` → `md.digestSync()`
- 用途:HIBP 的 SHA-1(Breach.ets)、sync 的 SHA-256(SyncProtocol.ets)
- 权限:无
- 片段(Breach.ets,SHA-1 大写 hex):
```ts
const md = cryptoFramework.createMd('SHA1');
md.updateSync({ data: utf8(password) });
const bytes = new Uint8Array(md.digestSync().data);
```
- 坑:本项目 **HMAC-SHA256 故意不走** `cryptoFramework.createMac`。原因:OHOS HMAC 要先
`createSymKeyGenerator('HMAC|SHA256')` 把字节 key 包成 SymKey 再 `convertKey`,调用栈过长;
SyncProtocol.ets 直接基于系统 SHA-256 手写 RFC 2104 HMAC(block size 64)。新增哈希优先复用此模式。
- 坑:Argon2id / XChaCha20 **不走系统加密**,走 cryptocore NAPI(见第六节)
### 6. @kit.CoreFileKit(fileIo / picker)
```ts
import { fileIo as fs } from '@kit.CoreFileKit';
import { fileIo as fs, picker } from '@kit.CoreFileKit';
```
- 关键:`fs.open(path, OpenMode)` → `fs.write(fd, buf)` / `fs.read(fd, buf)` → `fs.close(fd)`;
`fs.access(path)`(boolean Promise)、`fs.unlink`、`fs.moveFile(src, dst)`、`fs.readText`、`fs.stat(fd)`
- `OpenMode`:`READ_WRITE | CREATE | TRUNC`(写)、`READ_ONLY`(读)
- picker:`DocumentSaveOptions` / `DocumentSelectOptions` + `DocumentViewPicker(context)` 的 `.save()` / `.select()`
- 权限:沙箱 filesDir 读写无权限;picker 选文件由系统 UI 授权
- 片段(VaultStorage.ets 原子写:写 tmp + rename):
```ts
const handle = await fs.open(tmp, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE | fs.OpenMode.TRUNC);
try { await fs.write(handle.fd, text); } finally { await fs.close(handle.fd); }
await unlinkIfExists(dst); // moveFile 目标存在会失败,先 unlink
await fs.moveFile(tmp, dst);
```
- 坑:OHOS fs 是 open/write/close 三步,不是 Node 的 `writeFile` 一步;`moveFile` 目标存在会失败,
必须先 `unlink`。文件落 `filesDir`(卸载随之清除)。
### 7. @kit.NetworkKit(http)
```ts
import { http } from '@kit.NetworkKit';
```
- 关键:`http.createHttp()` → `request.request(url, options)` → `request.destroy()`(finally 必须调)
- options:`method`(`http.RequestMethod.GET/POST`)、`header`、`extraData`、`connectTimeout`、
`readTimeout`、`expectDataType`(`http.HttpDataType.ARRAY_BUFFER` 收二进制)
- 响应:`resp.responseCode`、`resp.result`(文本时 string,二进制时 ArrayBuffer)
- 权限:`ohos.permission.INTERNET`
- 片段(SyncProtocol.ets `postBinary`,二进制响应):
```ts
const request = http.createHttp();
try {
const resp = await request.request(url, {
method: http.RequestMethod.POST,
header: { 'content-type': 'application/octet-stream' },
extraData: ab, // ArrayBuffer
connectTimeout: 10_000, readTimeout: 60_000,
expectDataType: http.HttpDataType.ARRAY_BUFFER, // 否则默认按文本解析
});
if (resp.result instanceof ArrayBuffer) return new Uint8Array(resp.result);
} finally { request.destroy(); }
```
- 坑:默认按文本解析响应,收二进制**必须**设 `expectDataType: ARRAY_BUFFER`;
每次请求结束 `request.destroy()` 释放连接;catch 里 `e as BusinessError` 取 `code`/`message`
### 8. @kit.ArkTS(util — base64 / TextEncoder)
```ts
import { util } from '@kit.ArkTS';
Auf GitHub ansehen