Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
f1ba056
feat: pluginize native bridge capabilities
richerfu Aug 3, 2026
c3e61eb
merge: resolve conflicts with main (4dd2248)
richerfu Aug 3, 2026
184cf6d
feat: pluginize PR #65 native platform services (URL + file dialog)
richerfu Aug 3, 2026
8b2b927
refactor: ArkTS plugin type design, registration model and fixes
richerfu Aug 3, 2026
d4999d1
chore: format code
richerfu Aug 3, 2026
f62c138
chore: format code
richerfu Aug 3, 2026
9036ff3
feat(native-ability): add EagerPlugin and attach inbound event sink a…
richerfu Aug 4, 2026
a16cfc1
feat: pluginize the resource manager as ohos.resource
richerfu Aug 4, 2026
4250fa2
docs: add AGENTS.md workspace guide
richerfu Aug 4, 2026
bd66a48
fix(webview): release controller references safely
richerfu Aug 4, 2026
ee8e380
Merge branch 'fix/webview-object-ref-lifecycle' into feat/pluginized-…
richerfu Aug 4, 2026
b89001b
feat(bridge): support sync plugin calls from Rust workers via TSFN
richerfu Aug 5, 2026
799ec15
feat(demo): card-based demo UI with status feedback
richerfu Aug 5, 2026
cbe253a
Merge branch 'main' into feat/pluginized-bridge
richerfu Aug 5, 2026
b0d6fee
chore(packages): complete plugin metadata and changelogs
richerfu Aug 5, 2026
c7f4f63
chore(license): dual-license as MIT OR Apache-2.0 with root symlinks
richerfu Aug 5, 2026
8cd6fdf
refactor(packages): rename plugins to @ohos-rs/ability-plugin-*
richerfu Aug 5, 2026
877975b
docs(packages): complete plugin READMEs with install and versioned deps
richerfu Aug 5, 2026
6cdf498
chore(repo): retire legacy package/ staging dir, add ohpm workspace
richerfu Aug 5, 2026
0b5bad7
chore: pack script move to ohpm-rs
richerfu Aug 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
206 changes: 206 additions & 0 deletions .agents/skills/named-napi-contracts/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
---
name: named-napi-contracts
description: >-
openharmony-activity 仓库内置插件(crates/plugin-* 与 plugins/* 成对开发)的编码规范:
实现或修改插件 action 的 request/response 时,必须优先使用具名 N-API 类型
(#[napi(object)] + impl_bridge_napi_type!),不得使用 JSON 协议
(BridgeJson / call_json / bridgeJson / requireBridgeJson)。
ArkTS → Rust 的反向平台事件同样必须使用具名 N-API 类型;并给出 Rust 侧与 ArkTS 侧的
完整实现步骤与验证清单。
当任务涉及新增插件、新增 action、修改现有内置插件契约、或 review 插件代码时使用。
---

# 具名 N-API 契约优先(Named N-API Contracts)

本仓库(openharmony-ability)在 0.4.0-beta.7 → 1.0.0 重构后,所有内置插件的
request/response 已从 JSON envelope 迁移为**具名 N-API 类型**。新代码必须遵守同样规则。

## 1. 为什么不用 JSON

- JSON envelope 丢失类型信息:Rust 侧 `String`、`Vec<u8>`、`#[napi(object)]` 在桥上是
不同的 N-API value,JSON 会把它们全部压平成字符串,且无法区分。
- `typeName` 是稳定 ABI 契约:Rust 与 ArkTS 两端按名字校验,类型不匹配在边界上
**确定性报错**,而不是运行时悄悄丢字段。
- 零序列化开销:具名类型直接以真实 N-API value 传输(object / string / Uint8Array),
不经过 JSON.stringify / JSON.parse。
- 文档依据:`docs/plugin-development-standard.md` §3「请求与响应:具名 N-API 类型,不使用 JSON」。

## 2. 必须遵守的规则

### 规则 1:内置插件 request/response 一律具名 N-API

新建或修改任何内置插件 action(permission、app-control、window、webview 及未来插件),
请求与响应**都必须是** `#[napi(object)]` + `impl_bridge_napi_type!`,禁止:

- Rust 侧 `BridgeJson<T>` / `BridgeClient::call_json`
- ArkTS 侧 `bridgeJson` / `requireBridgeJson`
- 任何把 payload 包成 JSON 字符串再传输的做法

### 规则 2:没有 JSON 例外

当前 bridge 没有供插件使用的 JSON event port。request、response 以及 ArkTS → Rust 的平台回调
全部使用具名 N-API value;历史 `BridgeJson<T>`、`call_json`、`bridgeJson`、`requireBridgeJson` 和
`context.emit(..., JSON.stringify(...))` 一律不得在新代码中出现。反向回调使用
`context.invokeNativeSync(event, requestTypeName, responseTypeName, value)`,Rust 在
`BridgePlugin::on_main_thread_event` 中同步处理。

### 规则 3:typeName 命名约定

- 内置插件:以完整 plugin ID 为前缀,如 `ohos.webview.CreateRequest`、
`ohos.permission.PermissionResponse`、`ohos.window.AvoidAreaResponse`、
`ohos.app_control.TerminateRequest`。
- 业务类型:`<domain>.<TypeName>`,如 `account.LoginToken`、`demo.Profile`。
- 内置标量:`std.string`、`std.bytes`、`std.bool`、`std.i32`、`std.f64`。
- typeName 字符集:`^[A-Za-z0-9._-]+$`(`validate_identifier` 强制)。

### 规则 4:action 命名与版本

- action 使用 kebab-case 动词短语:`create`、`set-visible`、`load-url`、
`get-avoid-area`、`evaluate-script`、`clear-all-browsing-data`。
- 每个插件 `VERSION` 必须与 ArkTS 侧 `version` 完全一致(BridgeHost.lookup 强校验),
改契约时必须双端同步 bump。

## 3. Rust 侧实现步骤

以 `crates/plugin-permission/src/lib.rs` 为模板:

```rust
#[napi(object)]
#[derive(Clone, Debug)]
pub struct PermissionRequestPayload {
pub permissions: Vec<String>,
}

impl_bridge_napi_type!(PermissionRequestPayload, "ohos.permission.PermissionRequest");

#[napi(object)]
#[derive(Clone, Debug)]
pub struct PermissionResponsePayload {
pub codes: Vec<i32>,
}

impl_bridge_napi_type!(PermissionResponsePayload, "ohos.permission.PermissionResponse");
```

要点:

1. 字段用 **snake_case**(N-API 自动映射为 ArkTS camelCase)。
2. 调用入口是 `BridgeRuntime::call_async::<P, Req, Resp>`,`Req`/`Resp` 由类型系统
固定 typeName,Rust 侧无需运行时校验 typeName。
3. 业务校验放 Rust facade:非空、非法值、结果长度匹配
(参考 `validate_request` 与 `codes.len() != permissions.len()` 检查)。
4. 每个契约类型都要有单元测试断言 TYPE_NAME,防止意外改名:

```rust
#[test]
fn permission_uses_stable_named_napi_contracts() {
assert_eq!(
<PermissionRequestPayload as BridgeNapiType>::TYPE_NAME,
"ohos.permission.PermissionRequest"
);
}
```

## 4. ArkTS 侧实现步骤

以 `plugins/permission/src/main/ets/PermissionPlugin.ets` 为模板:

1. 顶部定义 typeName 常量:

```ts
const PERMISSION_REQUEST_TYPE = "ohos.permission.PermissionRequest";
const PERMISSION_RESPONSE_TYPE = "ohos.permission.PermissionResponse";
```

2. 定义 camelCase 接口与响应类:

```ts
interface PermissionRequestPayload {
permissions: string[];
}

class PermissionResponse {
readonly codes: number[];
constructor(codes: number[]) {
this.codes = codes;
}
}
```

3. 解析入口**必须校验 typeName**,不匹配直接 throw(不能静默接收):

```ts
function parsePermissions(payload: BridgeTypedValue): string[] {
if (payload.typeName !== PERMISSION_REQUEST_TYPE || typeof payload.value !== "object") {
throw new Error(`requires bridge type ${PERMISSION_REQUEST_TYPE}`);
}
// ...语义校验(非空数组、非空字符串)
}
```

4. 返回时**必须回填响应 typeName**,且响应类型名与 Rust 侧 `impl_bridge_napi_type!`
的第二个参数一字不差:

```ts
return { typeName: PERMISSION_RESPONSE_TYPE, value: new PermissionResponse(codes) };
```

5. `invokeAsync` / `invokeSync` 开头校验 `context.isActive()`,action 不支持时
`throw new Error(\`Unsupported <plugin-id> action '${action}'\`)`。

## 5. ArkTS → Rust 平台回调

平台回调若需要 Rust 立即决策,ArkTS 使用具名 N-API direct event:

```ts
const response = context.invokeNativeSync(
"navigation-request",
"ohos.webview.NavigationRequest",
"ohos.webview.NavigationResponse",
request,
);
```

Rust 在 `BridgePlugin::on_main_thread_event` 中立即解码 request 并返回对应 response。不得保存
ArkTS object/function,也不得将响应交给 worker 后再返回。完整线程与生命周期规则见
`docs/plugin-development-standard.md` §4、§5、§7。

## 6. 新增插件的完整落地清单

- [ ] Rust crate:`crates/plugin-<name>/src/lib.rs`
- [ ] `#[napi(object)]` 请求/响应结构体 + `impl_bridge_napi_type!`
- [ ] `impl BridgePlugin`:`type Mode`、`ID`、`VERSION`、`REQUIRED_CONTEXTS`
- [ ] 扩展 trait(如 `PermissionExt`)挂在 `OpenHarmonyApp` 上,core 不感知
- [ ] `validate_*` 业务校验
- [ ] 单测断言 typeName / 请求校验 / 响应字段完整
- [ ] ArkTS HAR:`plugins/<name>/src/main/ets/<Name>Plugin.ets`
- [ ] `create<Name>Plugin(): BridgePluginFactory`(id/version/execution/requires 与 Rust 一致)
- [ ] typeName 常量 + 解析校验 + 响应回填
- [ ] `execution` 与 Rust `type Mode` 一致:async → `invokeAsync`,sync → `invokeSync`
- [ ] `Cargo.toml` workspace 与 `native_ability/oh-package.json5` 登记
- [ ] demo 接入(如 `demo/entry/.../EntryAbility.ets` 的 `bridgePlugins` 数组)
- [ ] 文档:`docs/plugin-development-standard.md` §3.3 契约基线补充新行
- [ ] 验证:`cargo check --workspace`、`cargo clippy --workspace --all-targets`、
`cargo test -p openharmony-ability-plugin-<name> --lib`、`pnpm run format:check`

## 7. Review 时快速排查

```bash
# 新插件/新文件里出现 JSON 桥用法 = 违规
rg -n "BridgeJson|call_json|bridgeJson|requireBridgeJson" crates/plugin-* plugins/*/src

# 确认 request/response 都有 impl_bridge_napi_type
rg -n "impl_bridge_napi_type" crates/plugin-*/src

# 确认 ArkTS 侧解析入口校验了 typeName
rg -n "typeName !==|typeName ===" plugins/*/src
```

## 参考

- 现有契约表:见 [references/contract-table.md](references/contract-table.md)
- 开发规范:`docs/plugin-development-standard.md`(§3 命名契约、§4 线程模式)
- 核心实现:`crates/ability/src/bridge/mod.rs`(`impl_bridge_napi_type!`、
`BridgeNapiType`、`BridgePlugin`)
- 完整范例:`crates/plugin-permission` + `plugins/permission`(异步、ability 上下文)、
`crates/plugin-app-control` + `plugins/app-control`(同步、主线程 Env)
55 changes: 55 additions & 0 deletions .agents/skills/named-napi-contracts/references/contract-table.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# 内置插件具名 N-API 契约表

来源:`docs/plugin-development-standard.md` §3 与各插件源码。新增/修改 action 时必须与本表
保持命名一致(typeName 两端一字不差)。

## 通用约定

- 调用载荷不是 JSON envelope,而是 `{ typeName, value }` 的 `BridgeTypedValue`。
- `#[napi(object)]` 的 snake_case 字段按 N-API 规则映射为 ArkTS camelCase 字段。
- ArkTS → Rust 的反向事件使用 `context.invokeNativeSync(event, requestTypeName,
responseTypeName, value)`,同样传递具名 N-API value,不使用 JSON event port。
- 内置标量类型:`std.string`、`std.bytes`、`std.bool`、`std.i32`、`std.f64`。
- C++ N-API 插件必须使用相同的 `(pluginId, version, action, requestTypeName,
responseTypeName, value)` 边界,不得跨 worker 保存 `napi_env`/`napi_ref`/ArkTS 对象。

## 契约总表

| 插件 / action | request type → ArkTS value | response type → ArkTS value | 模式 / context |
|---|---|---|---|
| `ohos.app-control` / `terminate` | `ohos.app_control.TerminateRequest` → `{ code }` | `ohos.app_control.TerminateResponse` → `{ accepted }` | sync / `ability` |
| `ohos.permission` / `request` | `ohos.permission.PermissionRequest` → `{ permissions }` | `ohos.permission.PermissionResponse` → `{ codes }` | async / `ability` |
| `ohos.window` / `get-avoid-area` | `ohos.window.AvoidAreaRequest` → `{ areaType }` | `ohos.window.AvoidAreaResponse` → `{ area: { visible, leftRect, topRect, rightRect, bottomRect } }` | sync / `window-stage` |
| `ohos.webview` / `create` | `ohos.webview.CreateRequest` → `WebviewCreateRequest` | `ohos.webview.CreateResponse` → `{ id, slotId }` | async / `ui-context` |
| `ohos.webview` / `set-visible`、`set-background-color`、`remove`、`load-url`、`load-html`、`set-zoom`、`reload`、`focus`、`clear-all-browsing-data` | `ohos.webview.ControllerRequest` → `{ id, slotId, visible, color, url, html, headers, zoom }` | `ohos.webview.Acknowledgement` → `{ accepted }` | async / `ui-context` |
| `ohos.webview` / `get-url`、`cookies-with-url` | `ohos.webview.ControllerRequest` → `{ id, slotId, url }` | `ohos.webview.StringResponse` → `{ value }` | async / `ui-context` |
| `ohos.webview` / `evaluate-script` | `ohos.webview.ScriptRequest` → `{ id, slotId, script }` | `ohos.webview.ScriptResponse` → `{ result }` | async / `ui-context` |
| `ohos.resource` / `resource-manager-ready`(入站) | `ohos.resource.ResourceManagerRef`(ArkTS 直接传 `resourceManager` 对象) | `ohos.resource.ResourceManagerReadyResponse` → `{ accepted }` | 入站事件 / `ability` |

## 代码位置

| 插件 | Rust facade | ArkTS 实现 |
|---|---|---|
| permission | `crates/plugin-permission/src/lib.rs` | `plugins/permission/src/main/ets/PermissionPlugin.ets` |
| app-control | `crates/plugin-app-control/src/lib.rs` | `plugins/app-control/src/main/ets/AppControlPlugin.ets` |
| window | `crates/plugin-window/src/lib.rs` | `plugins/window/src/main/ets/WindowPlugin.ets` |
| webview | `crates/plugin-webview/src/lib.rs` | `plugins/webview/src/main/ets/WebviewPlugin.ets` |
| resource | `crates/plugin-resource/src/lib.rs` | `plugins/resource/src/main/ets/ResourcePlugin.ets` |
| webview 自定义协议 | `crates/plugin-webview/src/protocol.rs` | —(纯 native,ArkTS 只触发 `before-engine-init`/`engine-initialized` 事件) |
| webview JS proxy | `crates/plugin-webview/src/js_proxy.rs` | —(纯 native,依赖 `controller-attached` 事件) |

## WebView 反向事件(具名 N-API 类型)

| 事件 | request type → response type | 方向 |
|---|---|---|
| `before-engine-init` / `engine-initialized` | `ohos.webview.EngineLifecycleEvent` → `ohos.webview.EventAcknowledgement` | ArkTS → Rust |
| `controller-attached` / `controller-removed` | `ohos.webview.ControllerEvent` → `ohos.webview.EventAcknowledgement` | ArkTS → Rust |
| `navigation-request` | `ohos.webview.NavigationRequest` → `ohos.webview.NavigationResponse` | ArkTS → Rust |
| `download-start` | `ohos.webview.DownloadStartRequest` → `ohos.webview.DownloadStartResponse` | ArkTS → Rust |
| `download-end` | `ohos.webview.DownloadEndEvent` → `ohos.webview.EventAcknowledgement` | ArkTS → Rust |
| `title-change` | `ohos.webview.TitleChangeEvent` → `ohos.webview.EventAcknowledgement` | ArkTS → Rust |

## WebView 规则

下载、导航与标题回调已经使用上述具名 N-API 契约实现。新增 WebView callback 时必须在
`docs/plugin-development-standard.md` §7.1 记录 request/response 与失败策略,且不得回退到 JSON。
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@
package/src/main/ets
package/libs
dist
oh_modules
build/
2 changes: 2 additions & 0 deletions .oxlintrc.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
"$schema": "./node_modules/@ohos-rs/oxk/configuration_schema.json",
"plugins": ["unicorn", "typescript", "oxc", "arkts"],
"rules": {
// ArkTS compiler rejects `as const` (arkts-no-as-const); keep literal type annotations.
"typescript/prefer-as-const": "off",
"arkts/system-api-version": [
"error",
{
Expand Down
Loading
Loading