diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index cb4fb676c..c520d7db9 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -32,6 +32,7 @@ - 2026-06-18 原生壳统一验收门禁:根级 `npm run check:native-shells` 统一执行 H5 HostBridge 关键测试、Expo 壳 typecheck / test / config smoke / Metro export smoke、Tauri 壳 typecheck / cargo test、桌面 release `--no-bundle` 构建烟测,以及可分发壳与 H5 HostBridge 真实调用链的临时替身词扫描;根级 `npm run check` 会在 lint、主站测试、构建和内容检查后继续执行该门禁,避免 HostBridge、三端壳、Expo managed config、移动端 production bundle、桌面 release 入口和 H5 HostBridge 真实调用链禁替身验收散落成容易漏跑的单项命令。 - 2026-06-19 原生壳临时替身扫描范围:`npm run check:native-shells` 的生产替身词扫描必须覆盖微信小程序壳生产 `.js`、Expo / Tauri 壳源码与配置、共享 HostBridge 契约、H5 native transport,并自动覆盖已接入真实宿主能力 facade 的 H5 生产调用链文件;新增 H5 调用点接入 HostBridge 时,必须让自动扫描覆盖对应文件或在同等门禁中证明生产代码没有 mock / fake / stub / TODO / FIXME / 模拟 / 伪造。 - 2026-06-18 微信壳桥接层纳入统一验收:`npm run check:native-shells` 还会运行 `miniprogram/host-bridge/`、`miniprogram/shell/`、`pages/web-view` 样式和 `scripts/miniprogram-web-view-auth.test.ts` 的微信壳测试,覆盖 WebView 入口、登录触发、分享目标、支付结果、订阅消息结果和九宫切图行为;三端桥接层文件结构检查只证明目录边界,行为回归必须由同一门禁中的微信壳测试证明。 +- 2026-06-19 微信壳路由一致性门禁:`npm run check:native-shells` 必须反查 `miniprogram/app.json.pages`、`miniprogram/host-bridge/protocol.js`、H5 `src/services/host-bridge/hostBridge.ts` 小程序页面常量、H5 `src/services/wechatMiniProgramSubscribe.ts` 订阅授权页面常量、`miniprogram/host-bridge/webView.js` 分享入口 / 分享消息类型和 `miniprogram/config.js` source query / 域名格式。新增或调整小程序页面、登录派生 URL、支付页、九宫切图页、订阅页、WebView 来源标记、H5 入口域名或 API base URL 时,必须同步这几处常量并保持生产 / 开发域名都显式配置为纯 HTTPS domain;运行时开发域名回退生产域名只作为异常兜底。 - 2026-06-18 登录 / 支付能力禁伪声明:`auth.requestLogin` 和 `payment.request` 保留在共享 HostBridge 契约中供未来真实接入,但 Expo / Tauri 壳在真实 SDK、渠道流程和后端契约落地前不得声明这些 capability,也不得把它们写入入口 URL `hostCapabilities`;两端检查脚本会拒绝伪声明,请求实际到达壳层时必须返回明确 `unsupported_method` 并让 H5 fallback,两端壳测试直接覆盖这两个 method。 - 2026-06-18 移动壳触觉反馈边界:`haptics.impact` 只接受 `light`、`medium`、`heavy` 三档 impact style,缺省为 `light`;未知值必须返回 `invalid_request`,不得静默降级成真实设备触觉反馈。桌面壳不声明该 capability,H5 继续按 HostBridge fallback 处理。 - 2026-06-18 分享卡图片导出:新增 `file.exportImage` HostBridge capability,H5 分享卡下载在 native app 中优先把 canvas 生成的 base64 图片交给宿主导出;Expo 壳写缓存图片后交给系统分享 / 保存面板,Tauri 壳通过系统保存对话框写入图片字节。该能力只接受 `image/png` / `image/jpeg` / `image/webp`、单次 5 MiB 内图片数据,成功只返回文件名和字节数,不暴露本机绝对路径;宿主未声明时保留浏览器下载。 diff --git a/docs/project-memory/shared-memory/development-workflow.md b/docs/project-memory/shared-memory/development-workflow.md index c6f989b8f..d06fd6993 100644 --- a/docs/project-memory/shared-memory/development-workflow.md +++ b/docs/project-memory/shared-memory/development-workflow.md @@ -218,6 +218,7 @@ npm run check:native-shells 该命令会覆盖 H5 HostBridge 关键测试、微信 / Expo / Tauri 三端桥接层文件结构门禁、Expo 壳 typecheck / test / config smoke / Metro export smoke、Tauri 壳 typecheck / cargo test、桌面壳 release `--no-bundle` 构建烟测,以及可分发壳与 H5 HostBridge 真实调用链的临时替身词扫描,确认 Expo managed config、移动端 iOS / Android production bundle、打包 H5 资产、Tauri release 入口和 H5 HostBridge 真实调用链没有漂移;扫描范围包含微信小程序壳生产 `.js`、共享 HostBridge 契约、H5 native transport,并自动覆盖已接入真实宿主能力 facade 的 H5 生产调用链文件。壳源码和配置继续严格禁止 mock / fake / placeholder / stub / TODO / FIXME / 占位 / 模拟 / 伪造;H5 业务调用链允许正常表单 `placeholder` 属性和业务占位图文案,但仍禁止 mock / fake / stub / TODO / FIXME / 模拟 / 伪造等替身痕迹。 该命令同时会运行微信小程序 `miniprogram/host-bridge/`、`miniprogram/shell/`、`pages/web-view` 样式和 `scripts/miniprogram-web-view-auth.test.ts` 的壳层测试,保证微信桥接层拆分后的支付、订阅消息、九宫切图、分享目标和 WebView 登录 / 分享入口行为与 Expo、Tauri 壳一起验收。 +该命令还会反查微信小程序 `app.json.pages` 与 `host-bridge/protocol.js` 页面 URL、H5 小程序页面常量、H5 订阅授权页面常量、WebView 分享入口、分享目标消息类型、`WEB_VIEW_SOURCE_QUERY` 和 H5 / API base URL 格式,避免页面路由、来源标记或域名配置在微信壳、H5 HostBridge 与运行时配置之间分叉。生产 / 开发 H5 与 API 域名都必须显式配置为纯 HTTPS domain,运行时开发域名回退生产域名只作为异常兜底。 内容检查: diff --git a/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md b/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md index fb35ccbe8..d3884e207 100644 --- a/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md +++ b/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md @@ -256,7 +256,7 @@ GameBridge 禁止: - 每个请求必须有超时;H5 的 React Native WebView transport 和 Tauri `invoke` transport 都必须在前端侧按 `timeoutMs` 释放请求,宿主侧执行超时也只能返回标准 HostBridge 错误。重复 `id` 不得重复执行支付、登录、系统分享、文件导入导出、本地通知等宿主副作用;Expo 和 Tauri 壳都必须按 request id 回放首次完成结果。 - HostBridge 的 capability profile、文件 MIME 清单、导入 / 导出体积上限、文件名 fallback / 长度上限、request id 长度、角标上限、剪贴板文本长度、二维码文本长度和本地通知标题 / 正文长度都必须以 `packages/shared/src/contracts/hostBridge.ts` 为声明来源;Expo 移动壳直接导入共享 profile 和契约常量,Tauri 壳按 Rust 运行时代码镜像并由配置门禁反查共享契约。 - 能力按 `capabilities` / `hostCapabilities` 下发,H5 会过滤未知能力,并根据声明结果决定是否展示入口、发起宿主请求或走 fallback;进入 `native_app` 后主 App 会再通过真实 `host.getRuntime` 回读一次宿主 runtime 并缓存能力,用来补齐裁剪壳或旧入口 URL 缺少 `hostCapabilities` 的场景。不能只凭 `native_app` 宿主类型假设能力可用。 -- 壳能力声明与三端壳验收必须通过 `npm run check:native-shells` 统一校验;排查单端问题时可再分别运行微信壳测试集合、`npm run mobile-shell:typecheck`、`npm run mobile-shell:test`、`npm run mobile-shell:config`、`npm run mobile-shell:export`、`npm run desktop-shell:typecheck`、`npm run desktop-shell:test` 或 `npm run desktop-shell:build -- --no-bundle`。声明的 capability 必须来自共享 HostBridge profile 并存在于共享白名单,壳 runtime 回包、H5 URL `hostCapabilities`、壳实现、文件载荷边界、微信 WebView / 支付 / 订阅 / 分享桥接行为、Expo managed config、移动端 production bundle、桌面 release 构建入口和微信 / Expo / Tauri 三端生产源码临时替身词扫描不得漂移。 +- 壳能力声明与三端壳验收必须通过 `npm run check:native-shells` 统一校验;排查单端问题时可再分别运行微信壳测试集合、`npm run mobile-shell:typecheck`、`npm run mobile-shell:test`、`npm run mobile-shell:config`、`npm run mobile-shell:export`、`npm run desktop-shell:typecheck`、`npm run desktop-shell:test` 或 `npm run desktop-shell:build -- --no-bundle`。声明的 capability 必须来自共享 HostBridge profile 并存在于共享白名单,壳 runtime 回包、H5 URL `hostCapabilities`、壳实现、文件载荷边界、微信 WebView / 支付 / 订阅 / 分享桥接行为、微信小程序页面路由、WebView source query、微信壳 H5 / API HTTPS 域名格式、Expo managed config、移动端 production bundle、桌面 release 构建入口和微信 / Expo / Tauri 三端生产源码临时替身词扫描不得漂移。 - Expo SDK、React Native、`react-native-webview`、Tauri CLI、Tauri Rust crate 和桌面 Cargo 插件版本属于宿主壳行为边界。升级这些依赖前必须同步更新壳配置检查、`package-lock.json` / `Cargo.lock` 解析版本、本文档和对应验证结果,不能只改 package / Cargo 版本让生产壳行为静默漂移。 - 登录和支付能力在真实 SDK、渠道流程、后端契约和失败回退全部落地前不得进入 Expo / Tauri capabilities,也不得写进入口 URL `hostCapabilities`;两端配置检查会拒绝 `auth.requestLogin` 和 `payment.request` 的伪声明。 - 宿主壳不得把长期 token、支付密钥或用户敏感资料回传给 H5。 @@ -453,6 +453,8 @@ GameBridge 禁止: 2026-06-18 追加:微信壳行为测试进入原生壳统一验收。`npm run check:native-shells` 会运行 `miniprogram/host-bridge/`、`miniprogram/shell/`、`pages/web-view` 样式和 `scripts/miniprogram-web-view-auth.test.ts` 测试,覆盖微信 WebView 入口、登录触发、分享目标、支付结果、订阅消息结果和九宫切图桥接行为;三端桥接层文件结构检查只锁定职责边界,真实行为回归必须由同一门禁中的微信壳测试证明。 +2026-06-19 追加:微信小程序壳路由一致性进入原生壳统一验收。`npm run check:native-shells` 会反查 `miniprogram/app.json.pages` 与 `miniprogram/host-bridge/protocol.js` 页面 URL 常量一致,H5 `src/services/host-bridge/hostBridge.ts` 的小程序登录、支付、九宫切图页面常量、H5 `src/services/wechatMiniProgramSubscribe.ts` 的订阅授权页面常量与微信协议常量一致,`miniprogram/host-bridge/webView.js` 的 WebView 分享入口和分享目标消息类型不漂移;同时会校验 `miniprogram/config.js` 的生产 / 开发 H5 入口与 API base URL 都是显式配置的纯 HTTPS 域名,`WEB_VIEW_SOURCE_QUERY` 只包含 `clientType=mini_program` 和 `clientRuntime=wechat_mini_program`。运行时对开发域名的生产域名回退只作为异常兜底,不作为配置口径;新增小程序页面、改页面路径、调整来源 query 或切换域名格式时,必须同步协议常量、H5 HostBridge / 订阅服务常量、`app.json` 和这条门禁。 + 2026-06-18 追加:原生壳本地生成物不作为生产源码门禁输入。Expo `.expo/`、Expo export smoke 临时目录、Tauri `target/`、Tauri schema `gen/` 和 Tauri 自动生成权限目录都必须保持 gitignored;根级生产壳敏感词扫描只检查可提交的壳源码和配置,避免本机工具输出影响生产门禁。手写 capability / 权限配置仍需保留在扫描范围内。 2026-06-18 追加:移动壳启动页与 Android adaptive icon 复用现有真实品牌图标 `apps/mobile-shell/assets/icon.png`,背景色固定为 H5 壳根背景 `#fffdf9`。该 PNG 是 1024x1024 RGBA 透明前景品牌资产,不新增占位图;Expo `splash` 使用同一图标 `contain` 展示,Android `adaptiveIcon.foregroundImage` 使用同一透明前景图,`check-config.mjs` 会校验图标尺寸、透明像素、启动页和 adaptive icon 配置。 @@ -499,7 +501,7 @@ GameBridge 禁止: - AI sandbox 无法调用 HostBridge,也无法读取 H5 登录态。 - Tauri release 包不允许任意远端页面调用桌面命令。 - Expo WebView 外链离开主站后不保留完整 HostBridge。 -- 根级验收入口 `npm run check:native-shells` 必须同时覆盖 H5 HostBridge 关键路径、三端桥接层文件结构门禁、Expo 壳 typecheck / test / config smoke / Metro export smoke、Tauri 壳 typecheck / cargo test、桌面 release `--no-bundle` 构建烟测,以及可分发壳与 H5 HostBridge 真实调用链的临时替身词扫描。该扫描范围必须包含微信小程序壳生产 `.js`、共享 HostBridge 契约、H5 native transport,并自动覆盖已接入真实宿主能力 facade 的 H5 生产调用链文件。 +- 根级验收入口 `npm run check:native-shells` 必须同时覆盖 H5 HostBridge 关键路径、微信小程序页面路由与来源 query 反查、三端桥接层文件结构门禁、Expo 壳 typecheck / test / config smoke / Metro export smoke、Tauri 壳 typecheck / cargo test、桌面 release `--no-bundle` 构建烟测,以及可分发壳与 H5 HostBridge 真实调用链的临时替身词扫描。该扫描范围必须包含微信小程序壳生产 `.js`、共享 HostBridge 契约、H5 native transport,并自动覆盖已接入真实宿主能力 facade 的 H5 生产调用链文件。 ## 参考资料 diff --git a/docs/【前端架构】宿主壳能力统一协议-2026-06-17.md b/docs/【前端架构】宿主壳能力统一协议-2026-06-17.md index 83e35603d..722ec01e0 100644 --- a/docs/【前端架构】宿主壳能力统一协议-2026-06-17.md +++ b/docs/【前端架构】宿主壳能力统一协议-2026-06-17.md @@ -79,7 +79,7 @@ HostBridge 事件名以 `packages/shared/src/contracts/hostBridge.ts` 的 `HOST_ 2. `authService` 保留原导出,但内部委托 HostBridge,避免一次性改动 AuthGate。 3. 分享弹窗、分享目标同步、九宫切图、微信小程序支付和订阅授权改用 HostBridge 通用接口;旧微信命名服务只作为兼容导出。 4. 后续新增 `native_app` adapter 时只补桥接实现和测试,业务层不新增平台分叉;主 App 启动会触发一次 `host.getRuntime` 回读并订阅能力变化,避免裁剪壳或旧入口 URL 缺少 `hostCapabilities` 时长期隐藏真实可用能力。 -5. 每次新增或调整 native capability / HostBridge event 后,必须先更新 `packages/shared/src/contracts/hostBridge.ts` 中对应 Expo / Tauri capability profile 和事件白名单,再运行 `npm run check:native-shells`,统一覆盖 H5 HostBridge 关键测试、三端桥接层文件结构门禁、Expo 壳 typecheck / test / config smoke / Metro export smoke、Tauri 壳 typecheck / cargo test、桌面 release `--no-bundle` 构建烟测,以及可分发壳与 H5 HostBridge 真实调用链的临时替身词扫描;排查单端问题时再单独运行 `npm run mobile-shell:typecheck`、`npm run mobile-shell:test`、`npm run mobile-shell:config`、`npm run mobile-shell:export`、`npm run desktop-shell:typecheck`、`npm run desktop-shell:test` 或 `npm run desktop-shell:build -- --no-bundle`。 +5. 每次新增或调整 native capability / HostBridge event 后,必须先更新 `packages/shared/src/contracts/hostBridge.ts` 中对应 Expo / Tauri capability profile 和事件白名单,再运行 `npm run check:native-shells`,统一覆盖 H5 HostBridge 关键测试、三端桥接层文件结构门禁、微信小程序页面路由与 H5 常量反查、Expo 壳 typecheck / test / config smoke / Metro export smoke、Tauri 壳 typecheck / cargo test、桌面 release `--no-bundle` 构建烟测,以及可分发壳与 H5 HostBridge 真实调用链的临时替身词扫描;排查单端问题时再单独运行 `npm run mobile-shell:typecheck`、`npm run mobile-shell:test`、`npm run mobile-shell:config`、`npm run mobile-shell:export`、`npm run desktop-shell:typecheck`、`npm run desktop-shell:test` 或 `npm run desktop-shell:build -- --no-bundle`。 ## 验收 @@ -88,7 +88,7 @@ HostBridge 事件名以 `packages/shared/src/contracts/hostBridge.ts` 的 `HOST_ - 小程序支付仍跳转 `/pages/wechat-pay/index` 并保留支付结果 hash 回灌确认。 - 小程序订阅授权仍跳转 `/pages/subscribe-message/index`,且返回不阻断生成主链路。 - 普通浏览器分享、H5 支付和 Native 二维码支付不受影响。 -- 原生壳统一验收入口 `npm run check:native-shells` 通过,能力白名单、共享 capability profile、HostBridge event 白名单、壳 runtime 回包、URL `hostCapabilities`、H5 fallback、三端桥接层结构、两端壳实现、Expo managed config、移动端 production bundle、桌面 release 构建入口,以及可分发壳与 H5 HostBridge 真实调用链的临时替身词扫描没有漂移;扫描范围包含微信小程序壳生产 `.js`、共享 HostBridge 契约、H5 native transport,并自动覆盖已接入真实宿主能力 facade 的 H5 生产调用链文件。H5 业务文件允许正常表单 `placeholder` 属性和业务占位图文案,但不得出现 mock / fake / stub / TODO / FIXME / 模拟 / 伪造等替身痕迹。 +- 原生壳统一验收入口 `npm run check:native-shells` 通过,能力白名单、共享 capability profile、HostBridge event 白名单、壳 runtime 回包、URL `hostCapabilities`、H5 fallback、微信小程序 `app.json.pages` 与 `protocol.js` 页面 URL、H5 小程序页面常量、H5 订阅授权页面常量、WebView 分享入口、分享目标消息类型、WebView source query、生产 / 开发 H5 与 API HTTPS 域名格式、三端桥接层结构、两端壳实现、Expo managed config、移动端 production bundle、桌面 release 构建入口,以及可分发壳与 H5 HostBridge 真实调用链的临时替身词扫描没有漂移;扫描范围包含微信小程序壳生产 `.js`、共享 HostBridge 契约、H5 native transport,并自动覆盖已接入真实宿主能力 facade 的 H5 生产调用链文件。H5 业务文件允许正常表单 `placeholder` 属性和业务占位图文案,但不得出现 mock / fake / stub / TODO / FIXME / 模拟 / 伪造等替身痕迹。 ## 后续 diff --git a/scripts/check-native-shells.mjs b/scripts/check-native-shells.mjs index 0e4b10ba9..2e7add264 100644 --- a/scripts/check-native-shells.mjs +++ b/scripts/check-native-shells.mjs @@ -3,6 +3,7 @@ import {spawnSync} from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; +import vm from 'node:vm'; const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm'; const nativeShellPlanPath = @@ -634,6 +635,17 @@ function extractRustStringArray(source, constName) { return [...match[1].matchAll(/"([^"]+)"/g)].map((entry) => entry[1]); } +function extractStringConst(source, constName) { + const match = source.match( + new RegExp(`const ${constName}\\s*=\\s*['"]([^'"]+)['"];`), + ); + if (!match) { + throw new Error(`unable to read string const ${constName}`); + } + + return match[1]; +} + function extractDocumentCapabilityList(source, marker) { const markerIndex = source.indexOf(marker); if (markerIndex === -1) { @@ -815,6 +827,141 @@ function assertExternalUrlProtocolParity() { ); } +function requireCommonJsModule(modulePath) { + const absolutePath = path.resolve(modulePath); + const module = { exports: {} }; + const sandbox = { + module, + exports: module.exports, + URL, + URLSearchParams, + }; + + vm.runInNewContext(fs.readFileSync(absolutePath, 'utf8'), sandbox, { + filename: absolutePath, + }); + + return module.exports; +} + +function pageRouteFromMiniProgramUrl(url) { + return String(url).split('?')[0].replace(/^\//, ''); +} + +function assertHttpsDomain(value, label) { + const trimmed = String(value ?? '').trim(); + let url; + try { + url = new URL(trimmed); + } catch { + throw new Error(`${label} must be a valid HTTPS domain`); + } + + if ( + url.protocol !== 'https:' || + url.username || + url.password || + url.port || + url.pathname !== '/' || + url.search || + url.hash || + (trimmed !== url.origin && trimmed !== `${url.origin}/`) || + url.hostname === 'localhost' || + /^\d+\.\d+\.\d+\.\d+$/u.test(url.hostname) || + url.hostname.includes(':') || + !url.hostname.includes('.') + ) { + throw new Error(`${label} must be an HTTPS domain without path or query`); + } +} + +function assertWechatMiniProgramRouteParity() { + const appConfig = JSON.parse(fs.readFileSync('miniprogram/app.json', 'utf8')); + const runtimeConfig = requireCommonJsModule('miniprogram/config.js'); + const protocol = requireCommonJsModule('miniprogram/host-bridge/protocol.js'); + const webViewBridgeSource = fs.readFileSync( + 'miniprogram/host-bridge/webView.js', + 'utf8', + ); + const h5HostBridgeSource = fs.readFileSync( + 'src/services/host-bridge/hostBridge.ts', + 'utf8', + ); + const h5SubscribeSource = fs.readFileSync( + 'src/services/wechatMiniProgramSubscribe.ts', + 'utf8', + ); + + assertSameList( + appConfig.pages ?? [], + [ + protocol.WECHAT_WEB_VIEW_PAGE_URL, + protocol.WECHAT_SHARE_GRID_PAGE_URL, + protocol.WECHAT_PAY_PAGE_URL, + protocol.WECHAT_SUBSCRIBE_MESSAGE_PAGE_URL, + ].map(pageRouteFromMiniProgramUrl), + 'wechat mini program app pages', + ); + + const h5RoutePairs = [ + ['MINI_PROGRAM_AUTH_PAGE_URL', protocol.WECHAT_AUTH_PAGE_URL], + ['MINI_PROGRAM_PAY_PAGE_URL', protocol.WECHAT_PAY_PAGE_URL], + ['MINI_PROGRAM_SHARE_GRID_PAGE_URL', protocol.WECHAT_SHARE_GRID_PAGE_URL], + ]; + for (const [constName, expectedValue] of h5RoutePairs) { + const actualValue = extractStringConst(h5HostBridgeSource, constName); + if (actualValue !== expectedValue) { + throw new Error( + `H5 HostBridge ${constName} drifted: expected ${expectedValue} but got ${actualValue}`, + ); + } + } + + const h5SubscribePageUrl = extractStringConst( + h5SubscribeSource, + 'MINI_PROGRAM_SUBSCRIBE_MESSAGE_PAGE_URL', + ); + if (h5SubscribePageUrl !== protocol.WECHAT_SUBSCRIBE_MESSAGE_PAGE_URL) { + throw new Error( + `H5 subscribe page URL drifted: expected ${protocol.WECHAT_SUBSCRIBE_MESSAGE_PAGE_URL} but got ${h5SubscribePageUrl}`, + ); + } + + if ( + extractStringConst(webViewBridgeSource, 'WEB_VIEW_SHARE_PATH') !== + protocol.WECHAT_WEB_VIEW_PAGE_URL + ) { + throw new Error('wechat mini program share path must use the web-view page route'); + } + + if ( + extractStringConst(webViewBridgeSource, 'SHARE_TARGET_MESSAGE_TYPE') !== + protocol.WECHAT_SHARE_TARGET_MESSAGE_TYPE + ) { + throw new Error('wechat mini program share target message type drifted'); + } + + for (const [configKey, label] of [ + ['WEB_VIEW_ENTRY_URL', 'wechat release web-view entry URL'], + ['DEV_WEB_VIEW_ENTRY_URL', 'wechat dev web-view entry URL'], + ['API_BASE_URL', 'wechat release API base URL'], + ['DEV_API_BASE_URL', 'wechat dev API base URL'], + ]) { + assertHttpsDomain(runtimeConfig[configKey], label); + } + + const sourceQuery = runtimeConfig.WEB_VIEW_SOURCE_QUERY ?? {}; + if ( + sourceQuery.clientType !== 'mini_program' || + sourceQuery.clientRuntime !== 'wechat_mini_program' || + Object.keys(sourceQuery).some( + (key) => key !== 'clientType' && key !== 'clientRuntime', + ) + ) { + throw new Error('wechat web-view source query drifted from HostBridge runtime markers'); + } +} + function assertHostBridgeLayerLayout() { const wechatBridgeFiles = fs .readdirSync('miniprogram/host-bridge', { withFileTypes: true }) @@ -980,6 +1127,9 @@ assertNativeShellCapabilityPlan(); console.log('[check:native-shells] external-url-protocol-parity'); assertExternalUrlProtocolParity(); +console.log('[check:native-shells] wechat-mini-program-route-parity'); +assertWechatMiniProgramRouteParity(); + console.log('[check:native-shells] production-shell-dev-scaffold-scan'); assertNoProductionShellDevScaffoldTerms(); diff --git a/src/services/wechatMiniProgramSubscribe.ts b/src/services/wechatMiniProgramSubscribe.ts index a4915889d..1b7808160 100644 --- a/src/services/wechatMiniProgramSubscribe.ts +++ b/src/services/wechatMiniProgramSubscribe.ts @@ -4,6 +4,7 @@ import { } from './host-bridge/hostBridge'; const SUBSCRIBE_RESULT_HASH_KEY = 'wx_subscribe_result'; +const MINI_PROGRAM_SUBSCRIBE_MESSAGE_PAGE_URL = '/pages/subscribe-message/index'; const SUBSCRIBE_RESULT_TIMEOUT_MS = 2_500; const SUBSCRIBE_RESULT_RETURN_FALLBACK_MS = 800; @@ -110,7 +111,7 @@ export async function requestGenerationResultSubscribePermission() { let resultPromise: Promise | null = null; try { const navigated = await navigateHostNativePage( - `/pages/subscribe-message/index?requestId=${encodeURIComponent(requestId)}&scene=generation-result`, + `${MINI_PROGRAM_SUBSCRIBE_MESSAGE_PAGE_URL}?requestId=${encodeURIComponent(requestId)}&scene=generation-result`, { errorMessage: 'wechat_js_sdk_unavailable', beforeNavigate() {