统一原生扫码超时契约

将二维码扫码超时提升到共享 HostBridge 契约

让 H5 facade 和测试复用共享扫码超时常量

增加原生壳门禁拦截扫码超时字面量回退

补充宿主壳方案文档和共享决策记录
This commit is contained in:
2026-06-19 14:01:29 +08:00
parent fb8c9c2880
commit 14f4ae4a65
9 changed files with 28 additions and 4 deletions
@@ -1795,12 +1795,18 @@ if (!h5HostBridgeSource.includes('HOST_BRIDGE_RUNTIME_REFRESH_TIMEOUT_MS,')) {
if (!h5HostBridgeSource.includes('HOST_BRIDGE_USER_INTERACTION_TIMEOUT_MS,')) {
throw new Error('H5 HostBridge facade must import shared user interaction timeout');
}
if (!h5HostBridgeSource.includes('HOST_BRIDGE_SCANNER_TIMEOUT_MS,')) {
throw new Error('H5 HostBridge facade must import shared scanner timeout');
}
if (h5HostBridgeSource.includes('HOST_RUNTIME_REFRESH_TIMEOUT_MS')) {
throw new Error('H5 HostBridge facade must not redeclare runtime refresh timeout');
}
if (h5HostBridgeSource.includes('timeoutMs: 30000')) {
throw new Error('H5 HostBridge facade must not redeclare user interaction timeout');
}
if (h5HostBridgeSource.includes('timeoutMs: 60000')) {
throw new Error('H5 HostBridge facade must not redeclare scanner timeout');
}
for (const snippet of [
'HOST_BRIDGE_DEFAULT_REQUEST_TIMEOUT_MS',
'HOST_BRIDGE_MAX_REQUEST_TIMEOUT_MS',
@@ -2754,3 +2754,10 @@
- 决策:`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`
## 2026-06-19 原生宿主二维码扫码超时单一来源
- 背景:二维码扫码属于真实相机交互,等待时间应长于普通宿主请求;此前 H5 `scanHostQrCode()` 直接手写 `timeoutMs: 60000`,会让扫码等待边界和共享 HostBridge 契约漂移。
- 决策:`packages/shared/src/contracts/hostBridge.ts` 导出 `HOST_BRIDGE_SCANNER_TIMEOUT_MS`,作为 H5 facade 发起 `scanner.scanQrCode` 请求的唯一超时来源;`src/services/host-bridge/hostBridge.ts` 必须导入共享常量,不得继续手写 `timeoutMs: 60000`。根级原生壳门禁和桌面壳配置检查会拒绝回退到本地字面量。
- 影响范围:`packages/shared/src/contracts/hostBridge.ts``src/services/host-bridge/hostBridge.ts``scripts/check-native-shells.mjs`、Expo / Tauri HostBridge 方案文档和共享开发流程记忆。
- 验证方式:`npm run test -- packages/shared/src/contracts/hostBridge.test.ts src/services/host-bridge/hostBridge.test.ts``npm run check:native-shells``npm run check:encoding``git diff --check`
@@ -160,7 +160,7 @@ type HostBridgeEvent = {
| `haptics.impact` | 轻量触感反馈 | 支持 | 不声明 |
| `notification.showLocal` | 发送即时本地系统通知 | 支持 Expo Notifications | 支持 Rust 侧 Tauri notification |
每个 method 都必须有明确 payload schema、超时、错误码和能力开关;H5 看到不支持时回退到现有浏览器路径。
每个 method 都必须有明确 payload schema、超时、错误码和能力开关;H5 看到不支持时回退到现有浏览器路径。H5 发起原生壳请求的超时边界统一从 `packages/shared/src/contracts/hostBridge.ts` 读取:普通请求使用 `HOST_BRIDGE_DEFAULT_REQUEST_TIMEOUT_MS`,传入值最多裁剪到 `HOST_BRIDGE_MAX_REQUEST_TIMEOUT_MS`;宿主 runtime 回读使用 `HOST_BRIDGE_RUNTIME_REFRESH_TIMEOUT_MS`;文件导入 / 导出、图片选择 / 拍摄和音频导入 / 导出使用 `HOST_BRIDGE_USER_INTERACTION_TIMEOUT_MS`;二维码扫码使用 `HOST_BRIDGE_SCANNER_TIMEOUT_MS`。H5 facade 不得在调用点手写 `30000``60000` 等毫秒字面量,根级 `check:native-shells` 与桌面壳配置检查会拦截回退。
## Expo React Native 壳
File diff suppressed because one or more lines are too long
@@ -23,6 +23,7 @@ import {
HOST_BRIDGE_MAX_REQUEST_TIMEOUT_MS,
HOST_BRIDGE_RESPONSE_CACHE_MAX,
HOST_BRIDGE_RUNTIME_REFRESH_TIMEOUT_MS,
HOST_BRIDGE_SCANNER_TIMEOUT_MS,
HOST_BRIDGE_TEXT_MIME_TYPES,
HOST_BRIDGE_USER_INTERACTION_TIMEOUT_MS,
isHostBridgeMethod,
@@ -56,6 +57,7 @@ describe('HostBridge shared contract helpers', () => {
expect(HOST_BRIDGE_MAX_REQUEST_TIMEOUT_MS).toBe(60000);
expect(HOST_BRIDGE_RUNTIME_REFRESH_TIMEOUT_MS).toBe(3000);
expect(HOST_BRIDGE_USER_INTERACTION_TIMEOUT_MS).toBe(30000);
expect(HOST_BRIDGE_SCANNER_TIMEOUT_MS).toBe(60000);
});
test('固定宿主侧响应回放缓存边界', () => {
@@ -220,6 +220,7 @@ export const HOST_BRIDGE_DEFAULT_REQUEST_TIMEOUT_MS = 8000;
export const HOST_BRIDGE_MAX_REQUEST_TIMEOUT_MS = 60000;
export const HOST_BRIDGE_RUNTIME_REFRESH_TIMEOUT_MS = 3000;
export const HOST_BRIDGE_USER_INTERACTION_TIMEOUT_MS = 30000;
export const HOST_BRIDGE_SCANNER_TIMEOUT_MS = 60000;
export const HOST_BRIDGE_RESPONSE_CACHE_MAX = 128;
export const HOST_BRIDGE_REQUEST_ID_MAX_LENGTH = 120;
+6
View File
@@ -978,6 +978,7 @@ function assertH5HostBridgePayloadBoundaries() {
'HOST_BRIDGE_AUDIO_MIME_TYPES',
'HOST_BRIDGE_RUNTIME_REFRESH_TIMEOUT_MS',
'HOST_BRIDGE_USER_INTERACTION_TIMEOUT_MS',
'HOST_BRIDGE_SCANNER_TIMEOUT_MS',
]) {
if (!h5HostBridgeSource.includes(`${sharedBoundary},`)) {
throw new Error(
@@ -1022,6 +1023,11 @@ function assertH5HostBridgePayloadBoundaries() {
'H5 HostBridge facade must use shared HOST_BRIDGE_USER_INTERACTION_TIMEOUT_MS',
);
}
if (h5HostBridgeSource.includes('timeoutMs: 60000')) {
throw new Error(
'H5 HostBridge facade must use shared HOST_BRIDGE_SCANNER_TIMEOUT_MS',
);
}
}
function assertH5NativeAppTransportTimeoutBoundaries() {
+2 -1
View File
@@ -5,6 +5,7 @@ import { afterEach, describe, expect, test, vi } from 'vitest';
import type { HostBridgeCapability } from '../../../packages/shared/src/contracts/hostBridge';
import {
HOST_BRIDGE_RUNTIME_REFRESH_TIMEOUT_MS,
HOST_BRIDGE_SCANNER_TIMEOUT_MS,
HOST_BRIDGE_USER_INTERACTION_TIMEOUT_MS,
} from '../../../packages/shared/src/contracts/hostBridge';
import {
@@ -1209,7 +1210,7 @@ describe('hostBridge', () => {
expect(invoke).toHaveBeenCalledWith('host_bridge_request', {
request: expect.objectContaining({
method: 'scanner.scanQrCode',
timeoutMs: 60000,
timeoutMs: HOST_BRIDGE_SCANNER_TIMEOUT_MS,
}),
});
expect(invoke).toHaveBeenCalledWith('host_bridge_request', {
+2 -1
View File
@@ -31,6 +31,7 @@ import {
HOST_BRIDGE_NATIVE_APP_QUERY,
HOST_BRIDGE_RUNTIME_REFRESH_TIMEOUT_MS,
HOST_BRIDGE_RUNTIME_CONTEXT_QUERY_KEY,
HOST_BRIDGE_SCANNER_TIMEOUT_MS,
HOST_BRIDGE_TEXT_MIME_TYPES,
HOST_BRIDGE_USER_INTERACTION_TIMEOUT_MS,
HOST_BRIDGE_WECHAT_MINI_PROGRAM_SOURCE_QUERY,
@@ -931,7 +932,7 @@ export async function scanHostQrCode() {
await requestNativeAppHostBridge<ScannerScanQrCodeResult>(
'scanner.scanQrCode',
undefined,
{ timeoutMs: 60000 },
{ timeoutMs: HOST_BRIDGE_SCANNER_TIMEOUT_MS },
);
return normalizeHostBridgeQrCodeValue(result?.value) ?? false;
} catch (error) {