From 9e9f8c8fcb09f79f3ad701a048ccaa9b32e46c58 Mon Sep 17 00:00:00 2001 From: kdletters Date: Sat, 20 Jun 2026 21:50:11 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E9=BD=90=E7=A7=BB=E5=8A=A8=E5=A4=96?= =?UTF-8?q?=E9=93=BE=E6=89=93=E5=BC=80=E5=A4=B1=E8=B4=A5=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 移动 HostBridge 外链打开原生异常记录稳定日志 配置门禁反查外链异常不可静默 同步原生壳方案文档和共享决策记录 --- apps/mobile-shell/scripts/check-config.mjs | 8 ++++++-- apps/mobile-shell/src/host-bridge/navigation.test.ts | 8 ++++++++ apps/mobile-shell/src/host-bridge/navigation.ts | 7 ++++++- docs/project-memory/shared-memory/decision-log.md | 7 +++++++ ...端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md | 2 ++ 5 files changed, 29 insertions(+), 3 deletions(-) diff --git a/apps/mobile-shell/scripts/check-config.mjs b/apps/mobile-shell/scripts/check-config.mjs index 39e906185..d63556caf 100644 --- a/apps/mobile-shell/scripts/check-config.mjs +++ b/apps/mobile-shell/scripts/check-config.mjs @@ -2382,7 +2382,8 @@ if ( 'const externalUrlPayload = normalizeHostBridgeExternalUrlPayload(', ) || !hostBridgeNavigationSource.includes('openMobileShellExternalNavigation(') || - !hostBridgeNavigationSource.includes('externalUrlPayload.url') + !hostBridgeNavigationSource.includes('externalUrlPayload.url') || + !hostBridgeNavigationSource.includes('logMobileHostBridgeNavigationFailure') ) { throw new Error( 'mobile shell app.openExternalUrl must normalize payloads and use the shared external navigation helper', @@ -2396,6 +2397,8 @@ for (const snippet of [ 'Linking.openURL', 'javascript:alert(1)', 'external URL cannot be opened', + 'mobile HostBridge navigation failed for external.open', + 'expect(warnSpy).toHaveBeenCalledTimes(2)', 'converts native external open exceptions to stable host_error', 'navigation.openNativePage unsupported in mobile shell', 'app.reloadWebView unsupported in mobile shell', @@ -2965,7 +2968,8 @@ for (const snippet of [ "import * as Linking from 'expo-linking'", 'openMobileShellExternalNavigation(', 'externalUrlPayload.url', - '} catch {', + "} catch (error) {", + "logMobileHostBridgeNavigationFailure('external.open', error)", 'opened = false;', '(request.payload as OpenExternalUrlPayload | undefined)?.url', 'normalizeHostBridgeExternalUrlPayload', diff --git a/apps/mobile-shell/src/host-bridge/navigation.test.ts b/apps/mobile-shell/src/host-bridge/navigation.test.ts index adbac1cbf..9961d8ad0 100644 --- a/apps/mobile-shell/src/host-bridge/navigation.test.ts +++ b/apps/mobile-shell/src/host-bridge/navigation.test.ts @@ -42,6 +42,7 @@ function navigation(): MobileHostBridgeNavigation { } beforeEach(() => { + vi.restoreAllMocks(); vi.mocked(Linking.canOpenURL).mockReset(); vi.mocked(Linking.openURL).mockReset(); }); @@ -107,6 +108,8 @@ describe('mobile HostBridge navigation helpers', () => { }); test('converts native external open exceptions to stable host_error', async () => { + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + vi.mocked(Linking.canOpenURL).mockRejectedValueOnce( new Error('native canOpenURL failed'), ); @@ -122,6 +125,10 @@ describe('mobile HostBridge navigation helpers', () => { message: 'external URL cannot be opened', }); expect(Linking.openURL).not.toHaveBeenCalled(); + expect(warnSpy).toHaveBeenCalledWith( + 'mobile HostBridge navigation failed for external.open', + expect.any(Error), + ); vi.mocked(Linking.canOpenURL).mockResolvedValueOnce(true); vi.mocked(Linking.openURL).mockRejectedValueOnce( @@ -138,6 +145,7 @@ describe('mobile HostBridge navigation helpers', () => { code: 'host_error', message: 'external URL cannot be opened', }); + expect(warnSpy).toHaveBeenCalledTimes(2); }); test('opens same-origin native page targets in the WebView with host context', () => { diff --git a/apps/mobile-shell/src/host-bridge/navigation.ts b/apps/mobile-shell/src/host-bridge/navigation.ts index 3533b7ee8..03dab43d5 100644 --- a/apps/mobile-shell/src/host-bridge/navigation.ts +++ b/apps/mobile-shell/src/host-bridge/navigation.ts @@ -20,6 +20,10 @@ import { unsupported, } from './protocol'; +function logMobileHostBridgeNavigationFailure(label: string, error: unknown) { + console.warn(`mobile HostBridge navigation failed for ${label}`, error); +} + export async function openMobileHostBridgeExternalUrl( request: HostBridgeRequest, ) { @@ -36,7 +40,8 @@ export async function openMobileHostBridgeExternalUrl( Linking, externalUrlPayload.url, ); - } catch { + } catch (error) { + logMobileHostBridgeNavigationFailure('external.open', error); opened = false; } if (!opened) { diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 85268c508..27336eea4 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -2965,6 +2965,13 @@ - 影响范围:`apps/desktop-shell/src-tauri/src/shell/deep_link.rs`、`apps/desktop-shell/scripts/check-config.mjs`、宿主壳方案文档。 - 验证方式:`cargo test --manifest-path apps/desktop-shell/src-tauri/Cargo.toml shell::deep_link`、`npm run desktop-shell:typecheck`、`npm run check:native-shells`、`npm run check:encoding`、`git diff --check`。 +## 2026-06-20 移动 HostBridge 外链打开异常不可静默 + +- 背景:Expo 移动壳的 `app.openExternalUrl` 会调用系统 `Linking.canOpenURL` / `openURL` 离开 WebView;如果系统 API reject 后只返回 H5 稳定失败,真机上外链无反应时缺少宿主侧排查线索。 +- 决策:`openMobileHostBridgeExternalUrl(...)` 捕获系统外链打开异常时必须记录 `mobile HostBridge navigation failed for external.open`,HostBridge 回包仍只暴露稳定 `host_error: external URL cannot be opened`,不透传系统异常细节。配置检查反查日志 helper、`catch (error)` 和对应测试断言。 +- 影响范围:`apps/mobile-shell/src/host-bridge/navigation.ts`、`apps/mobile-shell/src/host-bridge/navigation.test.ts`、`apps/mobile-shell/scripts/check-config.mjs`、宿主壳方案文档。 +- 验证方式:`npm run mobile-shell:test -- src/host-bridge/navigation.test.ts`、`npm run mobile-shell:typecheck`、`npm run check:native-shells`、`npm run check:encoding`、`git diff --check`。 + ## 2026-06-20 桌面壳生命周期窗口状态读取失败不可静默 - 背景:Tauri 桌面壳 `app.lifecycle` 事件会驱动 H5 游戏循环、背景音乐和固定玩法音频暂停 / 恢复;如果 `is_visible()`、`is_minimized()` 或 `is_focused()` 读取失败后静默使用默认值,生命周期状态可能错误且难以排查。 diff --git a/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md b/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md index 122d315b5..79b24cdb2 100644 --- a/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md +++ b/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md @@ -344,6 +344,8 @@ GameBridge 禁止: 2026-06-20 追加:移动壳剪贴板、触觉和网络查询的原生异常必须可观测。Expo Clipboard 写入 / 读取、Expo Haptics 触觉派发、Expo Network 状态查询失败时,移动壳必须分别记录 `mobile clipboard failed for ...`、`mobile haptics failed for ...` 或 `mobile network failed for ...` 日志;HostBridge 回包仍只暴露稳定 `clipboard write unavailable`、`clipboard read unavailable`、`haptics impact unavailable` 或 `network status unavailable` 语义。该约束不新增后台网络探测、任意系统能力或 H5 业务兜底路径。 +2026-06-20 追加:移动壳 HostBridge 外链打开异常必须可观测。Expo `app.openExternalUrl` 捕获 `Linking.canOpenURL` / `openURL` 原生异常时记录 `mobile HostBridge navigation failed for external.open`,HostBridge 回包仍只暴露稳定 `external URL cannot be opened` 语义,不透传系统异常细节。 + 2026-06-18 追加:移动壳 `haptics.impact` 只接受 `light`、`medium`、`heavy` 三档 impact style,缺省为 `light`;未知强度返回 `invalid_request`,不会静默降级成真实设备触觉反馈。桌面壳不声明该能力,H5 继续按 HostBridge fallback 处理。 2026-06-19 追加:移动壳 `haptics.impact` 的 HostBridge payload 解析、共享 style 归一、Expo style 映射和 `Haptics.impactAsync(...)` 真实调用统一收口在 `apps/mobile-shell/src/host-bridge/haptics.ts`;`dispatch.ts` 只按 method 委托 `runMobileHostBridgeHapticsImpact(request.payload)`。