diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index b26c59e72..40e728082 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -80,6 +80,7 @@ - 2026-06-20 桌面本地通知契约镜像:Tauri `notification.showLocal` 的 title / body 归一化、长度上限和成功结果 action 必须镜像共享 HostBridge 契约;Rust 侧常量使用 `HOST_BRIDGE_LOCAL_NOTIFICATION_TITLE_MAX_LENGTH`、`HOST_BRIDGE_LOCAL_NOTIFICATION_BODY_MAX_LENGTH` 和 `HOST_BRIDGE_LOCAL_NOTIFICATION_DELIVERED_TO_SYSTEM_ACTION` 命名,桌面单端配置检查会与 `packages/shared/src/contracts/hostBridge.ts` 比对数值并反查成功结果由该 action 常量组装,避免通知 payload 边界变成桌面壳本地规则。 - 2026-06-19 桌面壳外链打开 helper 共用:Tauri WebView 外域拦截和 HostBridge `app.openExternalUrl` 都必须复用 `open_normalized_desktop_external_url` 执行系统外链打开动作;HostBridge 分支仍先用 `normalize_external_url` 保留 payload 错误语义并把 opener 错误回传给 H5,WebView 拦截保持 best-effort 静默处理。桌面壳配置检查会拒绝 `dispatch.rs` 直接调用 `app.opener().open_url` 绕过该 helper,避免两条离壳路径漂移。 - 2026-06-20 H5 原生导航预校验:`navigateHostNativePage()` 在 `native_app` 下发送 `navigation.openNativePage` 前必须先拒绝空值、控制字符、协议相对 URL、外域绝对 URL 和非 `http:` / `https:` 协议目标;同源绝对 URL、`/path` 和保留给桌面壳兼容的相对 route 继续交给 Expo / Tauri 壳二次归一并补写宿主上下文。微信小程序分支仍按小程序页面 URL 语义走 `wx.miniProgram.navigateTo`,不套原生 App 同源 H5 预校验。根级 `npm run check:native-shells` 会反查 H5 facade 仍使用 `normalizeNativeAppPageUrl(...)` 且发送归一后的 URL,避免明显不安全目标触达原生壳。 +- 2026-06-20 微信受控原生页能力声明:微信小程序壳真实 capability profile 声明 `navigation.openNativePage`,用于承接已经登记并测试的小程序原生页 flow;当前订阅生成结果通知页通过 H5 `requestGenerationResultSubscribePermission()` 调用 `navigateHostNativePage()` 打开 `/pages/subscribe-message/index`,小程序页再调用真实 `wx.requestSubscribeMessage` 并按既有结果协议回灌。根级 `npm run check:native-shells` 必须把该能力反查到共享 profile、微信 `WECHAT_HOST_CAPABILITIES` 镜像、订阅页协议常量、H5 入口、小程序 host-bridge / shell / page 文件和相关测试;该能力不代表开放任意小程序页面跳转。 - 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 清单,移动壳配置检查必须反查 Android 角标请求失败测试仍存在。 diff --git a/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md b/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md index 9250c7ff7..9345276fb 100644 --- a/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md +++ b/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md @@ -291,7 +291,7 @@ GameBridge 禁止: 当前状态:已新增 `src/services/host-bridge/nativeAppHostBridge.ts`,支持 React Native WebView `postMessage` 和 Tauri `invoke('host_bridge_request')` 两种真实 transport。两条 transport 都会按 `timeoutMs` 在 H5 侧释放请求,超时统一抛出 `timeout / host_bridge_timeout`。登录、支付和原生页跳转如果宿主明确返回 `unsupported_method` / `unsupported_capability`,H5 回退到原有路径;生产代码不返回 mock 成功。 -2026-06-19 追加:微信小程序壳当前真实能力完整清单为 `auth.requestLogin`、`payment.request`、`share.setTarget` 和 `share.open`。 +2026-06-19 追加:微信小程序壳当前真实能力完整清单为 `auth.requestLogin`、`payment.request`、`share.setTarget`、`share.open` 和 `navigation.openNativePage`。 ### Phase 2:Expo 移动壳 MVP @@ -308,7 +308,7 @@ GameBridge 禁止: 2026-06-19 追加:移动壳声明并实现 `scanner.scanQrCode`,通过 `expo-camera` 请求系统相机权限并用 `CameraView` 扫描二维码。扫码控制器一次只允许一个进行中的请求,成功结果复用共享 `normalizeHostBridgeQrCodeValue` 边界,只回传清洗后的二维码文本和 `qr_code` 格式;用户关闭返回 `cancelled`,H5 个人中心扫码入口不会在取消后继续弹出浏览器摄像头权限。宿主缺能力、旧壳 unsupported 或返回非法结果时,H5 继续打开原浏览器扫码弹层。Tauri 桌面壳只保留 method 白名单并返回 `unsupported_method`,不声明桌面扫码 capability。 -2026-06-19 追加:微信小程序壳真实能力 profile 为 `HOST_BRIDGE_WECHAT_MINI_PROGRAM_CAPABILITIES`,当前只包含 `auth.requestLogin`、`payment.request`、`share.setTarget` 和 `share.open`;`miniprogram/host-bridge/protocol.js` 继续保留 `WECHAT_HOST_CAPABILITIES` 作为小程序运行时镜像,不直接 import TypeScript shared 包,`miniprogram/host-bridge/protocol.test.js` 和根级 `npm run check:native-shells` 必须反查它与共享微信 profile 完全一致。微信壳能力由 WebView 登录页、支付页、分享目标消息和九宫切图页等真实页面流程承接,不改造成统一 request dispatcher;根级门禁会把每个微信 capability 反查到对应 `miniprogram/host-bridge/*`、`miniprogram/shell/*`、`miniprogram/pages/*` 文件、关键 `wx.login` / `wx.requestPayment` / `wx.requestVirtualPayment` / `wx.saveImageToPhotosAlbum` 调用或页面工厂,以及已纳入 `check:native-shells` 的测试文件。移动壳 iOS 额外真实能力为 `app.setBadgeCount`。`packages/shared/src/contracts/hostBridge.ts` 中的 `HOST_BRIDGE_WECHAT_MINI_PROGRAM_CAPABILITIES`、`HOST_BRIDGE_EXPO_MOBILE_BASE_CAPABILITIES`、`HOST_BRIDGE_EXPO_MOBILE_IOS_CAPABILITIES` 和 `HOST_BRIDGE_TAURI_DESKTOP_CAPABILITIES` 是三端宿主能力 profile 来源;移动壳直接引用共享 profile,微信小程序壳和桌面壳分别以运行时镜像承接并由门禁反查同一共享 profile。`npm run check:native-shells` 会从共享 profile 反查能力清单文档、小程序协议镜像、微信 capability 真实流程映射和桌面 Rust 镜像,避免实现、入口 URL 和方案文档再次漂移。 +2026-06-19 追加:微信小程序壳真实能力 profile 为 `HOST_BRIDGE_WECHAT_MINI_PROGRAM_CAPABILITIES`,当前包含 `auth.requestLogin`、`payment.request`、`share.setTarget`、`share.open` 和 `navigation.openNativePage`;`miniprogram/host-bridge/protocol.js` 继续保留 `WECHAT_HOST_CAPABILITIES` 作为小程序运行时镜像,不直接 import TypeScript shared 包,`miniprogram/host-bridge/protocol.test.js` 和根级 `npm run check:native-shells` 必须反查它与共享微信 profile 完全一致。微信壳能力由 WebView 登录页、支付页、分享目标消息、九宫切图页和订阅生成结果通知页等真实页面流程承接,不改造成统一 request dispatcher;`navigation.openNativePage` 在微信壳中只表示受控打开已登记的小程序原生页,当前用于订阅授权等已有页面,不代表任意小程序页面跳转。根级门禁会把每个微信 capability 反查到对应 `miniprogram/host-bridge/*`、`miniprogram/shell/*`、`miniprogram/pages/*`、H5 入口文件、关键 `wx.login` / `wx.requestPayment` / `wx.requestVirtualPayment` / `wx.saveImageToPhotosAlbum` / `wx.requestSubscribeMessage` 调用或页面工厂,以及已纳入 `check:native-shells` 的测试文件。移动壳 iOS 额外真实能力为 `app.setBadgeCount`。`packages/shared/src/contracts/hostBridge.ts` 中的 `HOST_BRIDGE_WECHAT_MINI_PROGRAM_CAPABILITIES`、`HOST_BRIDGE_EXPO_MOBILE_BASE_CAPABILITIES`、`HOST_BRIDGE_EXPO_MOBILE_IOS_CAPABILITIES` 和 `HOST_BRIDGE_TAURI_DESKTOP_CAPABILITIES` 是三端宿主能力 profile 来源;移动壳直接引用共享 profile,微信小程序壳和桌面壳分别以运行时镜像承接并由门禁反查同一共享 profile。`npm run check:native-shells` 会从共享 profile 反查能力清单文档、小程序协议镜像、微信 capability 真实流程映射和桌面 Rust 镜像,避免实现、入口 URL 和方案文档再次漂移。 2026-06-18 追加:移动壳 `navigation.canGoBack` 不再只读取 `react-native-webview` 的原生跨文档导航状态。Expo 壳会在 WebView `injectedJavaScriptBeforeContentLoaded` 中注入固定脚本,追踪当前 H5 文档内 `pushState` / `replaceState` / `popstate` 写入的路由栈,并通过内部 `genarrative.mobile.historyState` 消息回传;壳层把原生 WebView back-forward 状态与 H5 当前文档路由栈状态合成为 HostBridge `navigation.canGoBack` 事件。Android 返回键优先执行固定 `window.history.back(); true;` 回退 H5 SPA 路由,只有 H5 当前文档不可回退时才调用 WebView 原生 `goBack()`;该内部消息不是 HostBridge request method,也不开放 H5 到原生的通用事件写入通道。 diff --git a/miniprogram/host-bridge/protocol.js b/miniprogram/host-bridge/protocol.js index c7003e4af..2786b378c 100644 --- a/miniprogram/host-bridge/protocol.js +++ b/miniprogram/host-bridge/protocol.js @@ -3,6 +3,7 @@ const WECHAT_HOST_CAPABILITIES = [ 'payment.request', 'share.setTarget', 'share.open', + 'navigation.openNativePage', ]; const WECHAT_WEB_VIEW_PAGE_URL = '/pages/web-view/index'; diff --git a/packages/shared/src/contracts/hostBridge.test.ts b/packages/shared/src/contracts/hostBridge.test.ts index f423b5582..f885a571d 100644 --- a/packages/shared/src/contracts/hostBridge.test.ts +++ b/packages/shared/src/contracts/hostBridge.test.ts @@ -244,6 +244,7 @@ describe('HostBridge shared contract helpers', () => { 'payment.request', 'share.setTarget', 'share.open', + 'navigation.openNativePage', ]); for (const nativeAppProfile of [ HOST_BRIDGE_EXPO_MOBILE_BASE_CAPABILITIES, diff --git a/packages/shared/src/contracts/hostBridge.ts b/packages/shared/src/contracts/hostBridge.ts index 0da7a1385..5a631f96f 100644 --- a/packages/shared/src/contracts/hostBridge.ts +++ b/packages/shared/src/contracts/hostBridge.ts @@ -132,6 +132,7 @@ export const HOST_BRIDGE_WECHAT_MINI_PROGRAM_CAPABILITIES: readonly HostBridgeCa 'payment.request', 'share.setTarget', 'share.open', + 'navigation.openNativePage', ]; export const HOST_BRIDGE_EXPO_MOBILE_BASE_CAPABILITIES: readonly HostBridgeCapability[] = [ diff --git a/scripts/check-native-shells.mjs b/scripts/check-native-shells.mjs index 44fc94e12..d53427c9b 100644 --- a/scripts/check-native-shells.mjs +++ b/scripts/check-native-shells.mjs @@ -212,6 +212,32 @@ const wechatCapabilityFlowContracts = [ 'miniprogram/shell/shareGrid.test.js', ], }, + { + capability: 'navigation.openNativePage', + files: [ + 'miniprogram/host-bridge/protocol.js', + 'miniprogram/host-bridge/subscribeMessage.js', + 'miniprogram/shell/subscribeMessage.js', + 'miniprogram/pages/subscribe-message/index.js', + 'src/services/wechatMiniProgramSubscribe.ts', + ], + snippets: [ + ['miniprogram/host-bridge/protocol.js', 'WECHAT_SUBSCRIBE_MESSAGE_PAGE_URL'], + ['miniprogram/host-bridge/subscribeMessage.js', 'wx.requestSubscribeMessage'], + ['miniprogram/host-bridge/subscribeMessage.js', 'createSubscribeMessagePageController'], + ['miniprogram/shell/subscribeMessage.js', 'createSubscribeMessagePage'], + ['miniprogram/pages/subscribe-message/index.js', 'GENERATION_RESULT_SUBSCRIBE_TEMPLATE_ID'], + ['src/services/wechatMiniProgramSubscribe.ts', 'requestGenerationResultSubscribePermission'], + ['src/services/wechatMiniProgramSubscribe.ts', 'navigateHostNativePage'], + ['src/services/wechatMiniProgramSubscribe.ts', 'MINI_PROGRAM_SUBSCRIBE_MESSAGE_PAGE_URL'], + ], + tests: [ + 'miniprogram/host-bridge/protocol.test.js', + 'miniprogram/host-bridge/subscribeMessage.test.js', + 'miniprogram/shell/subscribeMessage.test.js', + 'src/services/wechatMiniProgramSubscribe.test.ts', + ], + }, ]; const expectedWechatHostBridgeFiles = [ 'dispatch.js', @@ -495,6 +521,7 @@ const wechatShellTests = [ 'miniprogram/shell/shareGrid.test.js', 'miniprogram/shell/subscribeMessage.test.js', 'miniprogram/pages/web-view/index.style.test.js', + 'src/services/wechatMiniProgramSubscribe.test.ts', 'scripts/miniprogram-web-view-auth.test.ts', ];