收口移动壳角标平台差异门禁

移动壳配置检查反查 Android 角标请求返回 unsupported_capability 的测试覆盖

HostBridge 协议文档明确 Android 不声明角标且不得伪造成功

项目记忆记录移动壳角标平台差异测试门禁
This commit is contained in:
2026-06-20 03:53:55 +08:00
parent 38acd4755f
commit 2f39794471
3 changed files with 15 additions and 2 deletions
@@ -2015,6 +2015,19 @@ for (const snippet of [
throw new Error(`mobile shell badge module is missing ${snippet}`);
}
}
for (const snippet of [
"test('app.setBadgeCount 拒绝非法数量并在 Android 返回 unsupported'",
"setPlatformOS('android')",
"request('app.setBadgeCount',",
"expect(expectFailed(unsupported).error.code).toBe('unsupported_capability')",
'expect(PushNotificationIOS.setApplicationIconBadgeNumber).not.toHaveBeenCalled()',
]) {
if (!bridgeTestSource.includes(snippet)) {
throw new Error(
`mobile shell bridge tests must cover Android app.setBadgeCount unsupported semantics: ${snippet}`,
);
}
}
if (
!dispatchSource.includes('getMobileHostBridgeAppearanceColorScheme(request)') ||
@@ -42,7 +42,7 @@
- 2026-06-19 桌面壳外链打开 helper 共用:Tauri WebView 外域拦截和 HostBridge `app.openExternalUrl` 都必须复用 `open_normalized_desktop_external_url` 执行系统外链打开动作;HostBridge 分支仍先用 `normalize_external_url` 保留 payload 错误语义并把 opener 错误回传给 H5WebView 拦截保持 best-effort 静默处理。桌面壳配置检查会拒绝 `dispatch.rs` 直接调用 `app.opener().open_url` 绕过该 helper,避免两条离壳路径漂移。
- 2026-06-18 能力声明收紧:`packages/shared/src/contracts/hostBridge.ts` 提供 HostBridge method / capability 白名单,H5 的 `getHostRuntime()` 会解析并过滤 `hostCapabilities``openHostShare``writeHostClipboardText``requestHostHapticsImpact``setHostAppTitle``exportHostTextFile` 等 native 能力只在宿主声明对应 capability 后调用。发布分享弹窗只有声明 `share.open` 时才显示“系统分享”,避免旧壳或裁剪壳露出不可用入口。
- 2026-06-20 H5 原生能力门控收口:除 `host.getRuntime` 为了支持旧入口 URL 缺少 capability 时回读真实 runtime 可保留特殊判断外,H5 facade 中所有 native_app request 能力都必须通过 `canUseNativeHostCapability(...)` 统一门控,不得在业务能力函数内直接读取 `runtime.hostCapabilities.includes(...)`,避免各能力复制门控规则;根级 `npm run check:native-shells` 会从共享 `HOST_BRIDGE_METHODS` 自动派生需门控的 request capability 清单,新增 method 时必须同步补齐 H5 facade 门控。
- 2026-06-20 移动壳未声明 method 覆盖:Expo 移动壳对未进入 `HOST_BRIDGE_EXPO_MOBILE_IOS_CAPABILITIES` 的共享 request method 必须由测试从 `HOST_BRIDGE_METHODS` 自动派生覆盖,并在请求到达时返回明确 `unsupported_method`;平台差异能力如 Android 不声明的 `app.setBadgeCount` 保持独立 `unsupported_capability` 语义,不混入未声明 method 清单。
- 2026-06-20 移动壳未声明 method 覆盖:Expo 移动壳对未进入 `HOST_BRIDGE_EXPO_MOBILE_IOS_CAPABILITIES` 的共享 request method 必须由测试从 `HOST_BRIDGE_METHODS` 自动派生覆盖,并在请求到达时返回明确 `unsupported_method`;平台差异能力如 Android 不声明的 `app.setBadgeCount` 保持独立 `unsupported_capability` 语义,不混入未声明 method 清单,移动壳配置检查必须反查 Android 角标请求失败测试仍存在
- 2026-06-20 壳文档能力清单反查:`npm run check:native-shells` 会同时反查移动壳 / 桌面壳主状态段落能力清单和后续完整能力清单;主状态段落按集合检查,允许按叙述需要调整顺序,但不得漏写或多写 capability,完整能力清单继续按共享 profile 顺序检查。
- 2026-06-18 宿主 runtime 回读:主 App 启动时会通过真实 `host.getRuntime` 回读 Expo / Tauri runtime 并缓存过滤后的能力清单,能力来源为 URL `hostCapabilities` 与宿主真实回包的并集;裁剪壳或旧入口 URL 缺少 `hostCapabilities` 时也能启用真实声明能力,但仍不会仅凭 `native_app` 或 transport 存在推断能力可用。该回读请求的短超时由共享契约 `HOST_BRIDGE_RUNTIME_REFRESH_TIMEOUT_MS` 声明,H5 facade 不得本地重声明。
- 2026-06-18 壳能力防漂移:`npm run mobile-shell:typecheck``npm run desktop-shell:typecheck` 会校验 Expo / Tauri 壳声明的 capability 均来自共享 HostBridge 白名单,并校验壳 runtime 回包、H5 URL `hostCapabilities` 和实现分支保持一致;微信小程序 `WECHAT_HOST_CAPABILITIES``miniprogram/host-bridge/protocol.test.js` 和根级 `npm run check:native-shells` 反查共享 `HOST_BRIDGE_WECHAT_MINI_PROGRAM_CAPABILITIES`。新增能力必须先更新契约和真实壳实现,再通过这些检查。
@@ -69,7 +69,7 @@ Tauri 桌面壳启动时必须按 `label="main"` 解析 `tauri.conf.json` 主窗
移动壳的触觉反馈 payload 解析、style 归一、Expo style 映射和真实设备反馈调用都必须留在 `apps/mobile-shell/src/host-bridge/haptics.ts``dispatch.ts` 只把 `request.payload` 委托给该模块。
- `showHostLocalNotification()`:原生 App 宿主的受控即时本地通知入口。H5 只能传必填 `title` 和可选 `body`,两者都会去除首尾空白、折叠普通空白、限制长度并拒绝控制字符;Expo 移动壳通过 `expo-notifications` 请求通知权限、创建 Android 本地通知 channel 并立刻调度本地通知,Android channel id 固定为共享契约 `HOST_BRIDGE_MOBILE_LOCAL_NOTIFICATION_CHANNEL_ID`Tauri 桌面壳通过 Rust 侧 `tauri-plugin-notification` 先检查系统通知权限,处于 prompt 状态时只在 Rust 侧请求一次权限,最终授权后才发送系统通知。成功结果统一为共享契约 `HOST_BRIDGE_LOCAL_NOTIFICATION_DELIVERED_TO_SYSTEM_RESULT`,只表示通知已交给系统通知层,不承诺用户一定看见或点击。该能力不包含远程推送、token 注册、定时提醒、后台远程通知或任意通知插件透传,宿主未声明、权限拒绝或系统失败时由 H5 视作失败并继续主流程。当前 H5 只在现有草稿生成任务收口为完成或失败时请求即时本地通知;通知按草稿来源去重,同一草稿重新进入生成中后才允许再次通知,不改变队列状态、弹窗、作品架或后端裁决。平台壳同步层必须通过真实 `host_bridge_request` transport 测到 `notification.showLocal` 请求,不能只测模型文案或替换 facade。
- `setHostAppTitle()`:原生 App 宿主的受控窗口标题入口。H5 主站会按当前平台阶段先同步 `document.title`,再通过 `app.setTitle` 请求宿主窗口标题同步;H5 facade 和 Tauri 桌面壳都必须按共享契约 `HOST_BRIDGE_APP_TITLE_MAX_LENGTH` 清洗标题,拒绝空值和控制字符,最多保留 80 个字符。Tauri 桌面壳支持该能力,Expo 移动壳不声明时静默忽略。
- `setHostAppBadgeCount()`:原生 App 宿主的受控应用角标入口。H5 只传 `0` 到共享契约 `HOST_BRIDGE_BADGE_COUNT_MAX` 之间的整数,`0` 表示清除角标;Expo 移动壳只在 iOS 声明 `app.setBadgeCount` 并通过 React Native `PushNotificationIOS` 设置应用图标角标,Android 不声明该能力;Tauri 桌面壳通过主窗口 `set_badge_count` 设置任务栏角标,底层平台不支持时返回明确错误,由 H5 视作失败并继续主流程。当前 H5 只把“可见作品架里未读的草稿生成完成更新”同步为角标数,同一个草稿有多个恢复 ID 时只计 1,已读、失败、生成中和不可见草稿不计入;宿主不支持或设置失败不改变 H5 红点、作品架或后端状态。平台壳同步层必须通过真实 `host_bridge_request` transport 测到 `app.setBadgeCount` 请求,保证未读计数模型和原生壳消费链路同时被门禁覆盖。
- `setHostAppBadgeCount()`:原生 App 宿主的受控应用角标入口。H5 只传 `0` 到共享契约 `HOST_BRIDGE_BADGE_COUNT_MAX` 之间的整数,`0` 表示清除角标;Expo 移动壳只在 iOS 声明 `app.setBadgeCount` 并通过 React Native `PushNotificationIOS` 设置应用图标角标,Android 不声明该能力,且请求实际到达 Android 壳时必须返回 `unsupported_capability`,不得伪造成功Tauri 桌面壳通过主窗口 `set_badge_count` 设置任务栏角标,底层平台不支持时返回明确错误,由 H5 视作失败并继续主流程。当前 H5 只把“可见作品架里未读的草稿生成完成更新”同步为角标数,同一个草稿有多个恢复 ID 时只计 1,已读、失败、生成中和不可见草稿不计入;宿主不支持或设置失败不改变 H5 红点、作品架或后端状态。平台壳同步层必须通过真实 `host_bridge_request` transport 测到 `app.setBadgeCount` 请求,保证未读计数模型和原生壳消费链路同时被门禁覆盖。
- `reloadHostWebView()`:原生 App 宿主的受控 WebView 刷新入口。H5 只能请求刷新当前承载主站的宿主 WebView;Expo 移动壳调用当前 `react-native-webview``reload()`Tauri 桌面壳调用主 `WebviewWindow.reload()`。该能力不接受 payload,不开放任意 URL 导航、脚本执行、Tauri guest API 或 RN WebView ref;成功只表示宿主已发起刷新,刷新后当前 H5 上下文会卸载。`AuthGate` 在登录态从未登录变为已登录、或从已登录变为未登录时优先调用该能力刷新当前容器;宿主未声明、返回失败或不可用时再回退浏览器 `window.location.reload()`
- `openHostExternalUrl()`:原生 App 宿主的受控外链入口。H5 中需要离开主站的外链在 `native_app` 下先通过 `app.openExternalUrl` 请求宿主系统浏览器打开;只允许 `http:``https:``mailto:``tel:`,相对路径会先归一化到当前站点绝对 URL,再通过共享契约 `normalizeHostBridgeExternalUrlPayload()` 清洗为 `{ url }` 载荷。Expo 移动壳消费该共享 payload normalizerTauri 桌面壳在 Rust 侧用 URL parser 镜像同一协议清单。宿主不可用或拒绝时回退浏览器外链行为,普通浏览器和小程序保持原有 `<a>` 语义。H5 支付链接和微信 OAuth 登录授权 URL 也走该入口:原生壳未声明真实 `payment.request` / `auth.requestLogin` 前,微信 H5 支付 URL 和后端返回的微信登录授权 URL 优先交给宿主系统浏览器,宿主未处理时才回退当前 WebView 跳转;不得把 H5 支付或网页登录伪装成已完成的原生支付 / 原生登录。
- `navigateHostNativePage()`:受控跳转宿主页,供订阅授权、支付、登录和内置独立 H5 体验入口等 adapter 复用。Expo 移动壳首版只接受同源 H5 route 并切换 WebView URLTauri 桌面壳同样只接受 `https://app.genarrative.world` 同源 H5 route 并在主窗口内跳转。平台首页的儿童动作热身 Demo 入口在 `native_app` 且宿主声明 `navigation.openNativePage` 时必须优先走该 facade 跳转 `/child-motion-demo`,普通浏览器、小程序和未声明能力的裁剪壳才回退浏览器跳转。真正原生页面、登录和支付能力必须等对应 SDK / 页面接入后再声明支持。