绑定微信壳能力到真实流程

微信小程序 capability 增加真实流程文件和测试清单门禁

反查登录支付分享能力对应微信页面工厂和 wx 宿主调用

同步宿主壳方案和共享开发记忆

验证 native-shells、编码检查和 diff 检查通过
This commit is contained in:
2026-06-19 12:49:55 +08:00
parent 44ed59a90f
commit aad54b55b4
4 changed files with 163 additions and 3 deletions
@@ -2724,3 +2724,10 @@
- 决策:登录状态异常页重试复用 `reloadCurrentPageForAuthStateChange()`,先请求原生宿主刷新当前 WebView,宿主未声明、失败或不可用时再回退浏览器刷新。`AuthGate.test.tsx` 进入 `check:native-shells` 的 H5 HostBridge 测试清单,避免认证页刷新路径再次分叉。
- 影响范围:`src/components/auth/AuthGate.tsx`、`src/components/auth/AuthGate.test.tsx`、`scripts/check-native-shells.mjs`、Expo / Tauri HostBridge 方案文档和共享开发流程记忆。
- 验证方式:`npm run test -- src/components/auth/AuthGate.test.tsx`、`npm run check:native-shells`、`npm run check:encoding`、`git diff --check`。
## 2026-06-19 微信壳 capability 绑定真实流程门禁
- 背景:Expo / Tauri 壳已有单端配置检查,能反查声明的 request capability 是否有真实 handler;微信小程序壳不使用统一 request dispatcher,而是通过 WebView 登录页、支付页、分享目标消息和九宫切图页承接 `auth.requestLogin`、`payment.request`、`share.setTarget` 和 `share.open`,此前根级门禁只确认 capability profile 与页面路由一致,未显式绑定每个 capability 的真实流程和测试。
- 决策:`scripts/check-native-shells.mjs` 新增微信 capability flow contract。每个微信 capability 必须对应真实 `miniprogram/host-bridge/*`、`miniprogram/shell/*`、`miniprogram/pages/*` 文件,源码中必须保留关键页面工厂或 `wx.login` / `wx.requestPayment` / `wx.requestVirtualPayment` / `wx.saveImageToPhotosAlbum` 等真实宿主调用,并且对应测试必须在 `check:native-shells` 的微信壳测试清单内。
- 影响范围:`scripts/check-native-shells.mjs`、Expo / Tauri HostBridge 方案文档、共享开发流程记忆。
- 验证方式:`npm run check:native-shells`、`npm run check:encoding`、`git diff --check`。
@@ -216,7 +216,7 @@ npm run build
npm run check:native-shells
```
该命令会覆盖 H5 HostBridge 关键测试、微信 / Expo / Tauri 三端桥接层文件结构门禁、完整相对路径文档反查、H5 HostBridge 事件订阅双能力门控反查、H5 `navigation.canGoBack` 消费 hook 与直达二级页返回锚点测试、移动端和桌面端单端源码清单门禁、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 页面内导航保留完整原生宿主上下文和 H5 HostBridge 真实调用链没有漂移;扫描范围包含微信小程序壳生产 `.js`、共享 HostBridge 契约、H5 native transport,并自动覆盖已接入真实宿主能力 facade 的 H5 生产调用链文件。登录与支付外链跳转必须保持在该调用链扫描内,`src/services/authService.ts` 和 `src/services/payment/paymentRedirect.ts` 是必扫文件;`AuthGate` 的登录成功、退出登录、身份边界刷新和登录状态异常重试都必须通过 `app.reloadWebView` 优先路径,并由 `src/components/auth/AuthGate.test.tsx` 进入该门禁。壳源码和配置继续严格禁止 mock / fake / placeholder / stub / TODO / FIXME / 占位 / 模拟 / 伪造;H5 业务调用链允许正常表单 `placeholder` 属性和业务占位图文案,但仍禁止 mock / fake / stub / TODO / FIXME / 模拟 / 伪造等替身痕迹。
该命令会覆盖 H5 HostBridge 关键测试、微信 / Expo / Tauri 三端桥接层文件结构门禁、完整相对路径文档反查、微信 capability 到真实 WebView / 支付 / 分享页面流程和测试清单的映射门禁、H5 HostBridge 事件订阅双能力门控反查、H5 `navigation.canGoBack` 消费 hook 与直达二级页返回锚点测试、移动端和桌面端单端源码清单门禁、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 页面内导航保留完整原生宿主上下文和 H5 HostBridge 真实调用链没有漂移;扫描范围包含微信小程序壳生产 `.js`、共享 HostBridge 契约、H5 native transport,并自动覆盖已接入真实宿主能力 facade 的 H5 生产调用链文件。登录与支付外链跳转必须保持在该调用链扫描内,`src/services/authService.ts` 和 `src/services/payment/paymentRedirect.ts` 是必扫文件;`AuthGate` 的登录成功、退出登录、身份边界刷新和登录状态异常重试都必须通过 `app.reloadWebView` 优先路径,并由 `src/components/auth/AuthGate.test.tsx` 进入该门禁。壳源码和配置继续严格禁止 mock / fake / placeholder / stub / TODO / FIXME / 占位 / 模拟 / 伪造;H5 业务调用链允许正常表单 `placeholder` 属性和业务占位图文案,但仍禁止 mock / fake / stub / TODO / FIXME / 模拟 / 伪造等替身痕迹。
创作 Agent 原生壳文档导入优先走 `file.importDocument`,旧壳只声明 `file.importText` 时才回退文本导入;相关变更必须让根级和单端门禁覆盖共享 method、capability profile、文档 MIME / 5 MiB 上限、读取前 size 校验,以及 H5 base64 转 `File` 后继续走后端文档解析的链路。
创作 Agent 参考图上传在原生壳声明 `file.importImage` 时必须优先走宿主图片导入,并把 H5 base64 转 `File` 后继续交给既有 `onReferenceImageChange` 校验链路;用户取消原生选择不应再连带弹出浏览器文件输入,普通浏览器、小程序和未声明能力的裁剪壳才使用原隐藏文件输入。
创作 Agent 轻输入 composer 的参考图按钮在原生壳声明 `file.importImage` 时必须优先走宿主图片导入;移动壳声明 `file.captureImage` 时才显示拍摄参考图入口,并把宿主图片同样转为 `File` 后复用 `readPuzzleReferenceImageAsDataUrl` 的类型、大小、压缩和预览链路。
@@ -262,7 +262,7 @@ GameBridge 禁止:
- 每个请求必须有超时;H5 的 React Native WebView transport 和 Tauri `invoke` transport 都必须在前端侧按 `timeoutMs` 释放请求,宿主侧执行超时也只能返回标准 HostBridge 错误。重复 `id` 不得重复执行支付、登录、系统分享、文件导入导出、本地通知等宿主副作用;Expo 和 Tauri 壳都必须按 request id 回放首次完成结果。
- HostBridge 的 capability profile、宿主上下文 query 字段和值、文件 MIME 清单、导入 / 导出体积上限、文件名 fallback / 长度上限、request id 长度、角标上限、剪贴板文本长度、二维码文本长度和本地通知标题 / 正文长度都必须以 `packages/shared/src/contracts/hostBridge.ts` 为声明来源;Expo 移动壳直接导入共享 profile 和契约常量,微信小程序壳和 Tauri 壳分别保留小程序 CommonJS / 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 / 支付 / 订阅 / 分享桥接行为、微信小程序页面路由、WebView source query、微信请求头运行时标记、H5 runtime parser、H5 路由保留字段、微信壳 H5 / API HTTPS 域名格式、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 runtime parser、H5 路由保留字段、微信壳 H5 / API HTTPS 域名格式、Expo managed config、移动端 production bundle、桌面 release 构建入口和微信 / Expo / Tauri 三端生产源码临时替身词扫描不得漂移。微信小程序壳不使用 Expo / Tauri 式统一 request dispatcher,但每个声明 capability 都必须在根级门禁中映射到真实流程文件、关键 `wx.*` 或页面工厂调用和对应测试清单。
- 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。
@@ -304,7 +304,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 完全一致。移动壳 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 反查能力清单文档、小程序协议镜像和桌面 Rust 镜像,避免实现、入口 URL 和方案文档再次漂移。
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-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 到原生的通用事件写入通道。
+153
View File
@@ -117,6 +117,101 @@ const h5HostBridgeEventSubscriptionFacades = [
eventName: 'file.imageDropped',
},
];
const wechatCapabilityFlowContracts = [
{
capability: 'auth.requestLogin',
files: [
'miniprogram/host-bridge/protocol.js',
'miniprogram/host-bridge/webView.js',
'miniprogram/shell/webView.js',
'miniprogram/pages/web-view/index.js',
],
snippets: [
['miniprogram/host-bridge/protocol.js', 'WECHAT_AUTH_PAGE_URL'],
['miniprogram/host-bridge/webView.js', 'resolveWebViewUrlFromRuntimeConfig'],
['miniprogram/shell/webView.js', 'shouldStartAuthFromQuery'],
['miniprogram/shell/webView.js', 'wx.login'],
['miniprogram/shell/webView.js', '/api/auth/wechat/miniprogram-login'],
['miniprogram/pages/web-view/index.js', 'createWechatWebViewPage'],
],
tests: [
'miniprogram/host-bridge/protocol.test.js',
'miniprogram/host-bridge/webView.test.js',
'miniprogram/shell/webView.test.js',
'scripts/miniprogram-web-view-auth.test.ts',
],
},
{
capability: 'payment.request',
files: [
'miniprogram/host-bridge/protocol.js',
'miniprogram/host-bridge/payment.js',
'miniprogram/shell/payment.js',
'miniprogram/pages/wechat-pay/index.js',
],
snippets: [
['miniprogram/host-bridge/protocol.js', 'WECHAT_PAY_PAGE_URL'],
['miniprogram/host-bridge/payment.js', 'requestWechatPayment'],
['miniprogram/host-bridge/payment.js', 'wx.requestPayment'],
['miniprogram/host-bridge/payment.js', 'wx.requestVirtualPayment'],
['miniprogram/shell/payment.js', 'createWechatPayPage'],
['miniprogram/shell/payment.js', 'notifyPreviousWebView'],
['miniprogram/pages/wechat-pay/index.js', 'createWechatPayPage'],
],
tests: [
'miniprogram/host-bridge/protocol.test.js',
'miniprogram/host-bridge/payment.test.js',
'miniprogram/shell/payment.test.js',
'scripts/miniprogram-web-view-auth.test.ts',
],
},
{
capability: 'share.setTarget',
files: [
'miniprogram/host-bridge/protocol.js',
'miniprogram/host-bridge/webView.js',
'miniprogram/shell/webView.js',
],
snippets: [
['miniprogram/host-bridge/protocol.js', 'WECHAT_SHARE_TARGET_MESSAGE_TYPE'],
['miniprogram/host-bridge/webView.js', 'resolveShareTargetFromWebViewMessage'],
['miniprogram/shell/webView.js', 'handleWebViewMessage'],
['miniprogram/shell/webView.js', '_currentShareTarget'],
],
tests: [
'miniprogram/host-bridge/protocol.test.js',
'miniprogram/host-bridge/webView.test.js',
'miniprogram/shell/webView.test.js',
],
},
{
capability: 'share.open',
files: [
'miniprogram/host-bridge/protocol.js',
'miniprogram/host-bridge/webView.js',
'miniprogram/host-bridge/shareGrid.js',
'miniprogram/shell/webView.js',
'miniprogram/shell/shareGrid.js',
'miniprogram/pages/share-grid/index.js',
],
snippets: [
['miniprogram/host-bridge/protocol.js', 'WECHAT_SHARE_GRID_PAGE_URL'],
['miniprogram/host-bridge/webView.js', 'buildWebViewSharePath'],
['miniprogram/host-bridge/shareGrid.js', 'buildShareGridTilePlan'],
['miniprogram/shell/webView.js', 'onShareAppMessage'],
['miniprogram/shell/webView.js', 'onShareTimeline'],
['miniprogram/shell/shareGrid.js', 'wx.saveImageToPhotosAlbum'],
['miniprogram/pages/share-grid/index.js', 'createWechatShareGridPage'],
],
tests: [
'miniprogram/host-bridge/protocol.test.js',
'miniprogram/host-bridge/webView.test.js',
'miniprogram/host-bridge/shareGrid.test.js',
'miniprogram/shell/webView.test.js',
'miniprogram/shell/shareGrid.test.js',
],
},
];
const expectedWechatHostBridgeFiles = [
'dispatch.js',
'payment.js',
@@ -1278,6 +1373,61 @@ function assertWechatMiniProgramRouteParity() {
}
}
function assertFileIncludesSnippet(filePath, snippet, label) {
const source = fs.readFileSync(filePath, 'utf8');
if (!source.includes(snippet)) {
throw new Error(`${label} must include ${snippet} in ${filePath}`);
}
}
function assertWechatMiniProgramCapabilityFlows() {
const protocol = requireCommonJsModule('miniprogram/host-bridge/protocol.js');
const declaredCapabilities = protocol.WECHAT_HOST_CAPABILITIES ?? [];
const declaredCapabilitySet = new Set(declaredCapabilities);
const contractedCapabilities = wechatCapabilityFlowContracts.map(
({ capability }) => capability,
);
assertSameList(
contractedCapabilities,
declaredCapabilities,
'wechat mini program capability flow contracts',
);
const wechatShellTestSet = new Set(wechatShellTests);
for (const contract of wechatCapabilityFlowContracts) {
if (!declaredCapabilitySet.has(contract.capability)) {
throw new Error(
`wechat capability flow contract declared for missing capability: ${contract.capability}`,
);
}
for (const filePath of contract.files) {
if (!fs.existsSync(filePath)) {
throw new Error(
`wechat ${contract.capability} flow file is missing: ${filePath}`,
);
}
}
for (const [filePath, snippet] of contract.snippets) {
assertFileIncludesSnippet(
filePath,
snippet,
`wechat ${contract.capability} flow`,
);
}
for (const testPath of contract.tests) {
if (!wechatShellTestSet.has(testPath)) {
throw new Error(
`wechat ${contract.capability} flow test is not part of check:native-shells: ${testPath}`,
);
}
}
}
}
function assertHostBridgeLayerLayout() {
assertSameList(
readDirectoryFileList(
@@ -1443,6 +1593,9 @@ assertExternalUrlProtocolParity();
console.log('[check:native-shells] wechat-mini-program-route-parity');
assertWechatMiniProgramRouteParity();
console.log('[check:native-shells] wechat-mini-program-capability-flows');
assertWechatMiniProgramCapabilityFlows();
console.log('[check:native-shells] h5-host-bridge-event-subscription-gates');
assertH5HostBridgeEventSubscriptionGates();