From 05d278f8f769b3c0edfc77d732ca27dabe72b44d Mon Sep 17 00:00:00 2001 From: kdletters Date: Sat, 20 Jun 2026 23:09:49 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=B6=E5=8F=A3=E7=A7=BB=E5=8A=A8=E5=A3=B3?= =?UTF-8?q?=E8=83=BD=E5=8A=9B=E8=AF=8A=E6=96=AD=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 移动壳 HostBridge 能力失败日志只记录稳定阶段标签 更新 badge notification haptics network share navigation 失败测试断言 扩展移动壳配置门禁禁止能力日志打印原生 error 对象 补充 Expo 与 Tauri 宿主壳方案中的移动诊断日志边界 --- apps/mobile-shell/scripts/check-config.mjs | 25 +++++++++++++++++++ .../src/host-bridge/badge.test.ts | 4 --- apps/mobile-shell/src/host-bridge/badge.ts | 4 +-- .../src/host-bridge/bridge.test.ts | 2 -- .../src/host-bridge/haptics.test.ts | 1 - apps/mobile-shell/src/host-bridge/haptics.ts | 4 +-- .../src/host-bridge/navigation.test.ts | 1 - .../src/host-bridge/navigation.ts | 4 +-- .../src/host-bridge/network.test.ts | 1 - apps/mobile-shell/src/host-bridge/network.ts | 4 +-- .../src/host-bridge/notifications.test.ts | 4 --- .../src/host-bridge/notifications.ts | 4 +-- .../src/host-bridge/share.test.ts | 1 - apps/mobile-shell/src/host-bridge/share.ts | 4 +-- ...ExpoReactNative与Tauri宿主壳方案-2026-06-17.md | 2 ++ 15 files changed, 39 insertions(+), 26 deletions(-) diff --git a/apps/mobile-shell/scripts/check-config.mjs b/apps/mobile-shell/scripts/check-config.mjs index a55063db7..63a072a96 100644 --- a/apps/mobile-shell/scripts/check-config.mjs +++ b/apps/mobile-shell/scripts/check-config.mjs @@ -161,6 +161,31 @@ const bridgeSourceFiles = fs const hostBridgeSource = bridgeSourceFiles .map((file) => fs.readFileSync(file, 'utf8')) .join('\n'); + +function assertLabelOnlyMobileHostBridgeDiagnostics(source, label, message) { + if (source.includes(`console.warn(\`${message} \${label}\`, error)`)) { + throw new Error(`${label} diagnostics must not log native error objects`); + } + if (!source.includes(`console.warn(\`${message} \${label}\`)`)) { + throw new Error(`${label} diagnostics must use stable label-only logging`); + } +} + +for (const [source, label, message] of [ + [badgeSource, 'mobile app badge', 'mobile app badge failed for'], + [clipboardSource, 'mobile clipboard', 'mobile clipboard failed for'], + [hapticsSource, 'mobile haptics', 'mobile haptics failed for'], + [ + hostBridgeNavigationSource, + 'mobile HostBridge navigation', + 'mobile HostBridge navigation failed for', + ], + [hostBridgeNetworkSource, 'mobile network', 'mobile network failed for'], + [notificationsSource, 'mobile notification', 'mobile notification failed for'], + [shareSource, 'mobile share', 'mobile share failed for'], +]) { + assertLabelOnlyMobileHostBridgeDiagnostics(source, label, message); +} const urlPath = new URL('../src/shell/url.ts', import.meta.url); const urlSource = fs.readFileSync(urlPath, 'utf8'); const urlTestPath = new URL('../src/shell/url.test.ts', import.meta.url); diff --git a/apps/mobile-shell/src/host-bridge/badge.test.ts b/apps/mobile-shell/src/host-bridge/badge.test.ts index 87b46b584..e76bd2a76 100644 --- a/apps/mobile-shell/src/host-bridge/badge.test.ts +++ b/apps/mobile-shell/src/host-bridge/badge.test.ts @@ -177,7 +177,6 @@ describe('mobile app badge helper', () => { expect(Notifications.setBadgeCountAsync).not.toHaveBeenCalled(); expect(warnSpy).toHaveBeenCalledWith( 'mobile app badge failed for permission.current', - error, ); warnSpy.mockRestore(); }); @@ -204,7 +203,6 @@ describe('mobile app badge helper', () => { expect(Notifications.setBadgeCountAsync).not.toHaveBeenCalled(); expect(warnSpy).toHaveBeenCalledWith( 'mobile app badge failed for permission.request', - error, ); warnSpy.mockRestore(); }); @@ -225,7 +223,6 @@ describe('mobile app badge helper', () => { }); expect(warnSpy).toHaveBeenCalledWith( 'mobile app badge failed for update.rejected', - expect.any(Error), ); warnSpy.mockRestore(); }); @@ -247,7 +244,6 @@ describe('mobile app badge helper', () => { }); expect(warnSpy).toHaveBeenCalledWith( 'mobile app badge failed for update.set_count', - error, ); warnSpy.mockRestore(); }); diff --git a/apps/mobile-shell/src/host-bridge/badge.ts b/apps/mobile-shell/src/host-bridge/badge.ts index 6e7d985f3..a535bbe2d 100644 --- a/apps/mobile-shell/src/host-bridge/badge.ts +++ b/apps/mobile-shell/src/host-bridge/badge.ts @@ -18,8 +18,8 @@ function hasBadgePermission(permission: NotificationPermissionStatus) { return permission.ios?.allowsBadge === true; } -function logMobileBadgeFailure(label: string, error: unknown) { - console.warn(`mobile app badge failed for ${label}`, error); +function logMobileBadgeFailure(label: string, _error: unknown) { + console.warn(`mobile app badge failed for ${label}`); } async function ensureMobileBadgePermission() { diff --git a/apps/mobile-shell/src/host-bridge/bridge.test.ts b/apps/mobile-shell/src/host-bridge/bridge.test.ts index a5d3b2515..9cbbd853f 100644 --- a/apps/mobile-shell/src/host-bridge/bridge.test.ts +++ b/apps/mobile-shell/src/host-bridge/bridge.test.ts @@ -618,7 +618,6 @@ describe('handleMobileHostBridgeMessage', () => { }); expect(warnSpy).toHaveBeenCalledWith( 'mobile app badge failed for update.set_count', - nativeError, ); } finally { warnSpy.mockRestore(); @@ -972,7 +971,6 @@ describe('handleMobileHostBridgeMessage', () => { }); expect(warnSpy).toHaveBeenCalledWith( 'mobile app badge failed for update.rejected', - expect.any(Error), ); } finally { warnSpy.mockRestore(); diff --git a/apps/mobile-shell/src/host-bridge/haptics.test.ts b/apps/mobile-shell/src/host-bridge/haptics.test.ts index 5f4814bf2..d4bc68663 100644 --- a/apps/mobile-shell/src/host-bridge/haptics.test.ts +++ b/apps/mobile-shell/src/host-bridge/haptics.test.ts @@ -80,7 +80,6 @@ describe('mobile haptics helpers', () => { }); expect(warnSpy).toHaveBeenCalledWith( 'mobile haptics failed for impact.dispatch', - nativeError, ); } finally { warnSpy.mockRestore(); diff --git a/apps/mobile-shell/src/host-bridge/haptics.ts b/apps/mobile-shell/src/host-bridge/haptics.ts index 26bbe0764..eb10ead4a 100644 --- a/apps/mobile-shell/src/host-bridge/haptics.ts +++ b/apps/mobile-shell/src/host-bridge/haptics.ts @@ -17,8 +17,8 @@ function toExpoImpactStyle(style: HostBridgeHapticsImpactStyle) { : Haptics.ImpactFeedbackStyle.Light; } -function logMobileHapticsFailure(label: string, error: unknown) { - console.warn(`mobile haptics failed for ${label}`, error); +function logMobileHapticsFailure(label: string, _error: unknown) { + console.warn(`mobile haptics failed for ${label}`); } export async function runMobileHapticsImpact(rawStyle: unknown) { diff --git a/apps/mobile-shell/src/host-bridge/navigation.test.ts b/apps/mobile-shell/src/host-bridge/navigation.test.ts index 9961d8ad0..4099b681e 100644 --- a/apps/mobile-shell/src/host-bridge/navigation.test.ts +++ b/apps/mobile-shell/src/host-bridge/navigation.test.ts @@ -127,7 +127,6 @@ describe('mobile HostBridge navigation helpers', () => { expect(Linking.openURL).not.toHaveBeenCalled(); expect(warnSpy).toHaveBeenCalledWith( 'mobile HostBridge navigation failed for external.open', - expect.any(Error), ); vi.mocked(Linking.canOpenURL).mockResolvedValueOnce(true); diff --git a/apps/mobile-shell/src/host-bridge/navigation.ts b/apps/mobile-shell/src/host-bridge/navigation.ts index 03dab43d5..482abb55f 100644 --- a/apps/mobile-shell/src/host-bridge/navigation.ts +++ b/apps/mobile-shell/src/host-bridge/navigation.ts @@ -20,8 +20,8 @@ import { unsupported, } from './protocol'; -function logMobileHostBridgeNavigationFailure(label: string, error: unknown) { - console.warn(`mobile HostBridge navigation failed for ${label}`, error); +function logMobileHostBridgeNavigationFailure(label: string, _error: unknown) { + console.warn(`mobile HostBridge navigation failed for ${label}`); } export async function openMobileHostBridgeExternalUrl( diff --git a/apps/mobile-shell/src/host-bridge/network.test.ts b/apps/mobile-shell/src/host-bridge/network.test.ts index 83dce7031..12d621d97 100644 --- a/apps/mobile-shell/src/host-bridge/network.test.ts +++ b/apps/mobile-shell/src/host-bridge/network.test.ts @@ -91,7 +91,6 @@ describe('mobile HostBridge network helper', () => { }); expect(warnSpy).toHaveBeenCalledWith( 'mobile network failed for status.query', - nativeError, ); } finally { warnSpy.mockRestore(); diff --git a/apps/mobile-shell/src/host-bridge/network.ts b/apps/mobile-shell/src/host-bridge/network.ts index bd75b27a1..c87f6f8f1 100644 --- a/apps/mobile-shell/src/host-bridge/network.ts +++ b/apps/mobile-shell/src/host-bridge/network.ts @@ -2,8 +2,8 @@ import { getMobileNetworkStatus } from '../shell/network'; import { type HostBridgeRequest } from '../../../../packages/shared/src/contracts/hostBridge'; import { failure, ok } from './protocol'; -function logMobileNetworkFailure(label: string, error: unknown) { - console.warn(`mobile network failed for ${label}`, error); +function logMobileNetworkFailure(label: string, _error: unknown) { + console.warn(`mobile network failed for ${label}`); } export async function getMobileHostBridgeNetworkStatus( diff --git a/apps/mobile-shell/src/host-bridge/notifications.test.ts b/apps/mobile-shell/src/host-bridge/notifications.test.ts index 8c1acc6bb..3d6f60812 100644 --- a/apps/mobile-shell/src/host-bridge/notifications.test.ts +++ b/apps/mobile-shell/src/host-bridge/notifications.test.ts @@ -169,7 +169,6 @@ describe('mobile local notification helpers', () => { expect(Notifications.scheduleNotificationAsync).not.toHaveBeenCalled(); expect(warnSpy).toHaveBeenCalledWith( 'mobile notification failed for permission.current', - error, ); warnSpy.mockRestore(); }); @@ -193,7 +192,6 @@ describe('mobile local notification helpers', () => { expect(Notifications.scheduleNotificationAsync).not.toHaveBeenCalled(); expect(warnSpy).toHaveBeenCalledWith( 'mobile notification failed for permission.request', - error, ); warnSpy.mockRestore(); }); @@ -252,7 +250,6 @@ describe('mobile local notification helpers', () => { expect(Notifications.scheduleNotificationAsync).not.toHaveBeenCalled(); expect(warnSpy).toHaveBeenCalledWith( 'mobile notification failed for delivery.schedule', - error, ); warnSpy.mockRestore(); }); @@ -270,7 +267,6 @@ describe('mobile local notification helpers', () => { }); expect(warnSpy).toHaveBeenCalledWith( 'mobile notification failed for delivery.schedule', - error, ); warnSpy.mockRestore(); }); diff --git a/apps/mobile-shell/src/host-bridge/notifications.ts b/apps/mobile-shell/src/host-bridge/notifications.ts index 182a5e107..0892dc184 100644 --- a/apps/mobile-shell/src/host-bridge/notifications.ts +++ b/apps/mobile-shell/src/host-bridge/notifications.ts @@ -20,8 +20,8 @@ Notifications.setNotificationHandler({ }), }); -function logMobileNotificationFailure(label: string, error: unknown) { - console.warn(`mobile notification failed for ${label}`, error); +function logMobileNotificationFailure(label: string, _error: unknown) { + console.warn(`mobile notification failed for ${label}`); } function hasNotificationPermission( diff --git a/apps/mobile-shell/src/host-bridge/share.test.ts b/apps/mobile-shell/src/host-bridge/share.test.ts index de0aae287..8f9dceeb8 100644 --- a/apps/mobile-shell/src/host-bridge/share.test.ts +++ b/apps/mobile-shell/src/host-bridge/share.test.ts @@ -205,7 +205,6 @@ describe('mobile share helpers', () => { }); expect(warnSpy).toHaveBeenCalledWith( 'mobile share failed for open.share', - nativeError, ); }); diff --git a/apps/mobile-shell/src/host-bridge/share.ts b/apps/mobile-shell/src/host-bridge/share.ts index 366674b58..291253934 100644 --- a/apps/mobile-shell/src/host-bridge/share.ts +++ b/apps/mobile-shell/src/host-bridge/share.ts @@ -10,8 +10,8 @@ import { invalidRequest, ok } from './protocol'; let currentShareTarget: ShareOpenPayload | null = null; -function logMobileShareFailure(label: string, error: unknown) { - console.warn(`mobile share failed for ${label}`, error); +function logMobileShareFailure(label: string, _error: unknown) { + console.warn(`mobile share failed for ${label}`); } export function setMobileHostBridgeShareTarget(request: HostBridgeRequest) { diff --git a/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md b/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md index be7c6ea91..0d10eba1f 100644 --- a/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md +++ b/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md @@ -521,6 +521,8 @@ GameBridge 禁止: 2026-06-18 追加:登录 / 支付能力伪声明进入门禁。`auth.requestLogin` 和 `payment.request` 保留在共享 HostBridge 契约中供未来真实接入,但 Expo 与 Tauri 壳在没有真实 SDK、渠道流程和后端契约前不得声明这些 capability,也不得把它们写入 `hostCapabilities`;请求到达壳层时必须返回明确 `unsupported_method`,让 H5 fallback 承接。两端壳测试会直接覆盖这两个 method,避免后续半接入时返回伪成功。 +2026-06-20 追加:移动壳 HostBridge 能力失败诊断统一只记录稳定标签。Expo 壳的 badge、clipboard、haptics、navigation、network、notification 和 share 等 HostBridge 能力在捕获原生 SDK / WebView / 系统 API 异常时,只能向控制台输出 `mobile ... failed for ` 这类固定阶段标签,不把原生 error 对象、native stack、平台错误码、文件路径或系统返回结构作为日志参数输出;HostBridge response 继续返回共享契约里的稳定 `host_error` / `cancelled` / `unsupported_capability` 等错误。`apps/mobile-shell/scripts/check-config.mjs` 会反查这些能力模块的 label-only 日志形式,避免后续为了排查方便把原生错误对象重新带进可分发移动壳日志。 + 2026-06-18 追加:微信 / Expo / Tauri 三端壳的生产源码和配置禁止出现 mock / fake / placeholder / stub / TODO / FIXME / 占位 / 模拟 / 伪造 / 未实现 / 临时 等脚手架或替身词;测试文件仍可使用 `vi.mock` 或等价测试替身。`apps/mobile-shell/scripts/check-config.mjs` 与 `apps/desktop-shell/scripts/check-config.mjs` 会扫描各自生产入口、配置和壳实现;移动壳 EAS / Expo config / Metro export smoke 脚本也属于可分发验收入口,必须纳入移动壳单端结构清单和替身词扫描。根级 `npm run check:native-shells` 会统一扫描 `miniprogram`、`apps/mobile-shell`、`apps/desktop-shell`、H5 HostBridge transport 和共享 HostBridge 契约生产源码,防止把临时替身或占位文案带进可分发壳。 2026-06-19 追加:H5 HostBridge 真实调用链扫描改为自动发现。根级 `npm run check:native-shells` 会从 `src/` 生产文件里收集直接导入并调用真实宿主能力 facade 的文件,以及 `useHostLifecycleActive`、`useHostNetworkOnline`、`platformProfileHostClipboard` 等薄 wrapper 的消费者;新增 `reloadHostWebView`、`showHostLocalNotification`、`setHostAppBadgeCount`、`importHostTextFile`、`importHostImageFile`、`captureHostImageFile`、`subscribeHostImageDrop`、`readHostClipboardText`、`openHostExternalUrl` 等 H5 调用点时,不再手工维护单个扫描文件清单。登录与支付外链属于敏感跳转路径,`src/services/authService.ts` 和 `src/services/payment/paymentRedirect.ts` 额外列入必扫清单,防止后续绕回裸浏览器跳转却脱离 HostBridge 调用链门禁。H5 业务文件允许正常表单 `placeholder` 属性、业务占位图文案和真实兼容 / 故障语义中的“未实现”“临时”表述,但仍会拒绝 mock / fake / stub / TODO / FIXME / 模拟 / 伪造等替身痕迹。