From 636880d4df263774a4ae0c43c31200e827d4be17 Mon Sep 17 00:00:00 2001 From: kdletters Date: Sat, 20 Jun 2026 23:19:07 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=B6=E5=8F=A3=E7=A7=BB=E5=8A=A8=E5=A3=B3?= =?UTF-8?q?=E5=A4=96=E5=B1=82=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 移动壳 shell 层注入 外链 深链 下载和扫码失败日志只记录稳定标签 移除 WebView 进程失败日志中的 H5 URL 输出 更新移动壳 shell 测试断言和配置门禁防止原生对象回流 补充宿主壳方案中的移动 shell 诊断边界 --- apps/mobile-shell/scripts/check-config.mjs | 67 +++++++++++++++++-- .../src/shell/QrScannerOverlay.test.tsx | 1 - .../src/shell/QrScannerOverlay.tsx | 4 +- apps/mobile-shell/src/shell/ShellApp.test.tsx | 9 --- apps/mobile-shell/src/shell/ShellApp.tsx | 21 +++--- .../src/shell/webViewPolicy.test.ts | 1 - apps/mobile-shell/src/shell/webViewPolicy.ts | 2 +- ...ExpoReactNative与Tauri宿主壳方案-2026-06-17.md | 2 +- 8 files changed, 76 insertions(+), 31 deletions(-) diff --git a/apps/mobile-shell/scripts/check-config.mjs b/apps/mobile-shell/scripts/check-config.mjs index 63a072a96..35ebd81da 100644 --- a/apps/mobile-shell/scripts/check-config.mjs +++ b/apps/mobile-shell/scripts/check-config.mjs @@ -186,6 +186,13 @@ for (const [source, label, message] of [ ]) { assertLabelOnlyMobileHostBridgeDiagnostics(source, label, message); } + +function assertSourceDoesNotInclude(source, snippet, message) { + if (source.includes(snippet)) { + throw new Error(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); @@ -215,6 +222,47 @@ const lifecycleTestPath = new URL('../src/shell/lifecycle.test.ts', import.meta. const lifecycleTestSource = fs.readFileSync(lifecycleTestPath, 'utf8'); const safeAreaTestPath = new URL('../src/shell/safeArea.test.ts', import.meta.url); const safeAreaTestSource = fs.readFileSync(safeAreaTestPath, 'utf8'); + +for (const [source, snippet, message] of [ + [ + shellAppSource, + 'console.warn(`mobile host event failed for ${label}`, error)', + 'mobile shell host event diagnostics must not log native error objects', + ], + [ + shellAppSource, + "console.warn('mobile HostBridge message injection failed', error)", + 'mobile shell HostBridge injection diagnostics must not log native error objects', + ], + [ + shellAppSource, + 'console.warn(`mobile shell navigation failed for ${label}`, error)', + 'mobile shell navigation diagnostics must not log native error objects', + ], + [ + shellAppSource, + "console.warn('mobile shell blocked WebView file download', event)", + 'mobile shell blocked download diagnostics must not log WebView event objects', + ], + [ + shellAppSource, + 'console.warn(`mobile shell deep link failed for ${label}`, error)', + 'mobile shell deep link diagnostics must not log native error objects', + ], + [ + qrScannerOverlaySource, + "console.warn('mobile QR scanner permission request failed', error)", + 'mobile QR scanner diagnostics must not log native error objects', + ], + [ + webViewPolicySource, + "console.warn('mobile navigation state sync failed', error)", + 'mobile WebView history diagnostics must not log native error objects', + ], +]) { + assertSourceDoesNotInclude(source, snippet, message); +} + const sharedContractPath = new URL( '../../../packages/shared/src/contracts/hostBridge.ts', import.meta.url, @@ -1508,7 +1556,7 @@ for (const snippet of [ 'if (!isShellMountedRef.current)', 'logMobileHostEventFailure(event, error)', 'injectHostBridgeMessage(response, logMobileHostBridgeMessageFailure)', - "console.warn('mobile HostBridge message injection failed', error)", + "console.warn('mobile HostBridge message injection failed')", "logMobileHostEventFailure('network.statusChanged', error)", 'handleWebViewLoad', 'onLoad={handleWebViewLoad}', @@ -1523,7 +1571,7 @@ for (const snippet of [ 'MOBILE_WEBVIEW_BEFORE_CONTENT_SCRIPT', 'shouldBlockMobileWebViewNavigationRequest', 'logMobileShellDownloadBlocked', - "console.warn('mobile shell blocked WebView file download', event)", + "console.warn('mobile shell blocked WebView file download')", 'SafeAreaProvider', 'SafeAreaView', 'MOBILE_SHELL_SAFE_AREA_EDGES', @@ -1559,6 +1607,15 @@ if (shellAppSource.includes('catch(() => undefined)')) { throw new Error('mobile shell ShellApp must not hide async navigation failures'); } +if ( + shellAppSource.includes("console.warn('mobile WebView process failed', {") && + shellAppSource.includes( + "console.warn('mobile WebView process failed', {\n count: nextFailureWindow.count,\n label,\n url: webUrl,", + ) +) { + throw new Error('mobile shell WebView process diagnostics must not log H5 URLs'); +} + if (!shellAppTestSource.includes('blocked WebView file downloads are logged for host diagnostics')) { throw new Error('mobile shell tests must cover blocked WebView file download diagnostics'); } @@ -1703,7 +1760,7 @@ for (const snippet of [ 'genarrative.mobile.historyState', '__genarrativeMobileHistoryIndex', '__GENARRATIVE_MOBILE_POST_NAVIGATION_STATE__', - "console.warn('mobile navigation state sync failed', error)", + "console.warn('mobile navigation state sync failed')", ]) { if (!webViewPolicySource.includes(snippet)) { throw new Error(`mobile shell WebView policy missing ${snippet}`); @@ -2608,7 +2665,7 @@ for (const snippet of [ 'cancelQrCodeScan', 'subscribeQrScannerState', 'logQrScannerPermissionFailure(error)', - "console.warn('mobile QR scanner permission request failed', error)", + "console.warn('mobile QR scanner permission request failed')", ]) { if (!qrScannerOverlaySource.includes(snippet)) { throw new Error(`mobile shell QR scanner overlay missing ${snippet}`); @@ -2679,7 +2736,7 @@ for (const snippet of [ 'mobile WebView process failed', 'repeated WebView process failures show the load failure panel and retry clears the failure window', "expect(screen.getByText('页面已停止')).toBeTruthy()", - 'expect.any(Error)', + "expect(warnSpy).toHaveBeenCalledWith('mobile WebView process failed', {", ]) { if (!shellAppTestSource.includes(snippet)) { throw new Error(`mobile shell ShellApp HostBridge event test missing ${snippet}`); diff --git a/apps/mobile-shell/src/shell/QrScannerOverlay.test.tsx b/apps/mobile-shell/src/shell/QrScannerOverlay.test.tsx index f5ca3fcd1..32ae825be 100644 --- a/apps/mobile-shell/src/shell/QrScannerOverlay.test.tsx +++ b/apps/mobile-shell/src/shell/QrScannerOverlay.test.tsx @@ -149,7 +149,6 @@ describe('QrScannerOverlay', () => { }); expect(warnSpy).toHaveBeenCalledWith( 'mobile QR scanner permission request failed', - error, ); expect(overlayHarness.cameraViewProps.current).toBeNull(); }); diff --git a/apps/mobile-shell/src/shell/QrScannerOverlay.tsx b/apps/mobile-shell/src/shell/QrScannerOverlay.tsx index 74bee3bf6..0953ec255 100644 --- a/apps/mobile-shell/src/shell/QrScannerOverlay.tsx +++ b/apps/mobile-shell/src/shell/QrScannerOverlay.tsx @@ -13,8 +13,8 @@ import { subscribeQrScannerState, } from '../host-bridge/scanner'; -function logQrScannerPermissionFailure(error: unknown) { - console.warn('mobile QR scanner permission request failed', error); +function logQrScannerPermissionFailure(_error: unknown) { + console.warn('mobile QR scanner permission request failed'); } export function QrScannerOverlay() { diff --git a/apps/mobile-shell/src/shell/ShellApp.test.tsx b/apps/mobile-shell/src/shell/ShellApp.test.tsx index 5bf318e10..ffdc9f298 100644 --- a/apps/mobile-shell/src/shell/ShellApp.test.tsx +++ b/apps/mobile-shell/src/shell/ShellApp.test.tsx @@ -377,7 +377,6 @@ describe('ShellApp QR scanner HostBridge flow', () => { await waitFor(() => { expect(warnSpy).toHaveBeenCalledWith( 'mobile HostBridge message injection failed', - injectionError, ); }); @@ -525,7 +524,6 @@ describe('ShellApp HostBridge event injection', () => { expect(warnSpy).toHaveBeenCalledWith( 'mobile host event failed for network.statusChanged', - injectionError, ); warnSpy.mockRestore(); @@ -602,7 +600,6 @@ describe('ShellApp HostBridge event injection', () => { await waitFor(() => { expect(warnSpy).toHaveBeenCalledWith( 'mobile host event failed for network.statusChanged', - networkError, ); }); @@ -636,7 +633,6 @@ describe('ShellApp HostBridge event injection', () => { await waitFor(() => { expect(warnSpy).toHaveBeenCalledWith( 'mobile shell navigation failed for external_navigation.open', - expect.any(Error), ); }); @@ -661,7 +657,6 @@ describe('ShellApp HostBridge event injection', () => { expect(warnSpy).toHaveBeenCalledWith( 'mobile shell blocked WebView file download', - downloadEvent, ); warnSpy.mockRestore(); @@ -684,7 +679,6 @@ describe('ShellApp HostBridge event injection', () => { await waitFor(() => { expect(warnSpy).toHaveBeenCalledWith( 'mobile shell deep link failed for initial_url.read', - initialUrlError, ); }); expect(shellHarness.webViewProps.current?.source).toEqual({ @@ -711,7 +705,6 @@ describe('ShellApp HostBridge event injection', () => { await waitFor(() => { expect(warnSpy).toHaveBeenCalledWith( 'mobile shell deep link failed for runtime_url.rejected', - 'https://outside.example/works/detail?work=PZ-1', ); }); expect(shellHarness.webViewProps.current?.source).toEqual({ @@ -743,7 +736,6 @@ describe('ShellApp HostBridge event injection', () => { expect(warnSpy).toHaveBeenCalledWith('mobile WebView process failed', { count: 1, label: 'content_process_terminated', - url: expect.stringContaining('https://app.genarrative.world/'), }); warnSpy.mockRestore(); @@ -776,7 +768,6 @@ describe('ShellApp HostBridge event injection', () => { expect(warnSpy).toHaveBeenLastCalledWith('mobile WebView process failed', { count: 2, label: 'render_process_gone', - url: expect.stringContaining('https://app.genarrative.world/'), }); act(() => { diff --git a/apps/mobile-shell/src/shell/ShellApp.tsx b/apps/mobile-shell/src/shell/ShellApp.tsx index 5e0c9fb09..afae6a714 100644 --- a/apps/mobile-shell/src/shell/ShellApp.tsx +++ b/apps/mobile-shell/src/shell/ShellApp.tsx @@ -59,24 +59,24 @@ function buildHostBridgeMessageScript(message: unknown) { )}, origin: window.location.origin, source: window })); true;`; } -function logMobileHostEventFailure(label: HostBridgeEventName, error: unknown) { - console.warn(`mobile host event failed for ${label}`, error); +function logMobileHostEventFailure(label: HostBridgeEventName, _error: unknown) { + console.warn(`mobile host event failed for ${label}`); } -function logMobileHostBridgeMessageFailure(error: unknown) { - console.warn('mobile HostBridge message injection failed', error); +function logMobileHostBridgeMessageFailure(_error: unknown) { + console.warn('mobile HostBridge message injection failed'); } -function logMobileShellNavigationFailure(label: string, error: unknown) { - console.warn(`mobile shell navigation failed for ${label}`, error); +function logMobileShellNavigationFailure(label: string, _error: unknown) { + console.warn(`mobile shell navigation failed for ${label}`); } -function logMobileShellDownloadBlocked(event: unknown) { - console.warn('mobile shell blocked WebView file download', event); +function logMobileShellDownloadBlocked(_event: unknown) { + console.warn('mobile shell blocked WebView file download'); } -function logMobileShellDeepLinkFailure(label: string, error: unknown) { - console.warn(`mobile shell deep link failed for ${label}`, error); +function logMobileShellDeepLinkFailure(label: string, _error: unknown) { + console.warn(`mobile shell deep link failed for ${label}`); } const MOBILE_WEBVIEW_PROCESS_FAILURE_WINDOW_MS = 30_000; @@ -428,7 +428,6 @@ export default function ShellApp() { console.warn('mobile WebView process failed', { count: nextFailureWindow.count, label, - url: webUrl, }); if (nextFailureWindow.count <= MOBILE_WEBVIEW_PROCESS_FAILURE_RELOAD_LIMIT) { diff --git a/apps/mobile-shell/src/shell/webViewPolicy.test.ts b/apps/mobile-shell/src/shell/webViewPolicy.test.ts index 0f0af8169..97afc33fb 100644 --- a/apps/mobile-shell/src/shell/webViewPolicy.test.ts +++ b/apps/mobile-shell/src/shell/webViewPolicy.test.ts @@ -184,7 +184,6 @@ describe('TRACK_MOBILE_WEBVIEW_HISTORY_SCRIPT', () => { expect(warnSpy).toHaveBeenCalledWith( 'mobile navigation state sync failed', - syncError, ); expect(postMessage).toHaveBeenCalledWith( JSON.stringify({ diff --git a/apps/mobile-shell/src/shell/webViewPolicy.ts b/apps/mobile-shell/src/shell/webViewPolicy.ts index 215d73396..ee454e0e8 100644 --- a/apps/mobile-shell/src/shell/webViewPolicy.ts +++ b/apps/mobile-shell/src/shell/webViewPolicy.ts @@ -177,7 +177,7 @@ export const TRACK_MOBILE_WEBVIEW_HISTORY_SCRIPT = ` window.location.href ); } catch (error) { - console.warn('mobile navigation state sync failed', error); + console.warn('mobile navigation state sync failed'); } } diff --git a/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md b/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md index 0d10eba1f..14b51f7f3 100644 --- a/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md +++ b/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md @@ -521,7 +521,7 @@ 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-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` 等错误。移动壳 shell 层的 HostBridge message 注入、宿主事件注入、外链离壳、WebView 下载阻断、深链读取 / 拒绝、扫码权限请求和 H5 history 同步失败也按同一 label-only 口径记录;WebView 进程失败只记录失败窗口计数和固定阶段标签,不记录当前 H5 URL 或 query。`apps/mobile-shell/scripts/check-config.mjs` 会反查这些能力模块和 shell 层日志形式,避免后续为了排查方便把原生错误对象、WebView 事件或 H5 地址重新带进可分发移动壳日志。 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 契约生产源码,防止把临时替身或占位文案带进可分发壳。