diff --git a/apps/mobile-shell/scripts/check-config.mjs b/apps/mobile-shell/scripts/check-config.mjs index ff7e89476..482ba4c92 100644 --- a/apps/mobile-shell/scripts/check-config.mjs +++ b/apps/mobile-shell/scripts/check-config.mjs @@ -1520,12 +1520,20 @@ for (const snippet of [ 'genarrative.mobile.historyState', '__genarrativeMobileHistoryIndex', '__GENARRATIVE_MOBILE_POST_NAVIGATION_STATE__', + "console.warn('mobile navigation state sync failed', error)", ]) { if (!webViewPolicySource.includes(snippet)) { throw new Error(`mobile shell WebView policy missing ${snippet}`); } } +if ( + webViewPolicySource.includes('catch (error) {}') || + webViewPolicySource.includes('catch (_error) {}') +) { + throw new Error('mobile shell WebView policy must not hide injected script failures'); +} + if ( webViewPolicySource.includes("['blob:'") || webViewPolicySource.includes("'filesystem:'") diff --git a/apps/mobile-shell/src/shell/webViewPolicy.test.ts b/apps/mobile-shell/src/shell/webViewPolicy.test.ts index d21f9148a..0f0af8169 100644 --- a/apps/mobile-shell/src/shell/webViewPolicy.test.ts +++ b/apps/mobile-shell/src/shell/webViewPolicy.test.ts @@ -169,6 +169,32 @@ describe('TRACK_MOBILE_WEBVIEW_HISTORY_SCRIPT', () => { ); }); + test('路由栈状态写入失败时记录日志', () => { + const postMessage = vi.fn(); + const syncError = new Error('history state blocked'); + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + window.ReactNativeWebView = { + postMessage, + }; + window.history.replaceState = vi.fn(() => { + throw syncError; + }) as typeof window.history.replaceState; + + window.eval(TRACK_MOBILE_WEBVIEW_HISTORY_SCRIPT); + + expect(warnSpy).toHaveBeenCalledWith( + 'mobile navigation state sync failed', + syncError, + ); + expect(postMessage).toHaveBeenCalledWith( + JSON.stringify({ + type: 'genarrative.mobile.historyState', + canGoBack: false, + }), + ); + warnSpy.mockRestore(); + }); + test('重复注入时回放当前 H5 路由栈状态', () => { const postMessage = vi.fn(); window.ReactNativeWebView = { diff --git a/apps/mobile-shell/src/shell/webViewPolicy.ts b/apps/mobile-shell/src/shell/webViewPolicy.ts index a6fe58003..215d73396 100644 --- a/apps/mobile-shell/src/shell/webViewPolicy.ts +++ b/apps/mobile-shell/src/shell/webViewPolicy.ts @@ -176,7 +176,9 @@ export const TRACK_MOBILE_WEBVIEW_HISTORY_SCRIPT = ` '', window.location.href ); - } catch (error) {} + } catch (error) { + console.warn('mobile navigation state sync failed', error); + } } window.history.pushState = function(state, title, url) { diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index cf303baae..04068afa5 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -2958,6 +2958,13 @@ - 影响范围:`apps/mobile-shell/src/shell/ShellApp.tsx`、`apps/mobile-shell/src/shell/ShellApp.test.tsx`、`apps/mobile-shell/scripts/check-config.mjs`。 - 验证方式:`npm run mobile-shell:test -- src/shell/ShellApp.test.tsx`、`npm run mobile-shell:typecheck`、`npm run check:native-shells`、`npm run check:encoding`、`git diff --check`。 +## 2026-06-20 移动壳返回栈状态同步失败不可静默 + +- 背景:Expo 移动壳通过 WebView 注入脚本追踪当前 H5 文档 history index 并回传 `navigation.canGoBack`;如果 `replaceState(...)` 写入 index 失败后静默吞掉,Android 返回键和 H5 返回按钮状态可能漂移且难以排查。 +- 决策:`TRACK_MOBILE_WEBVIEW_HISTORY_SCRIPT` 的 `replaceCurrentState()` catch 路径必须输出 `mobile navigation state sync failed` 浏览器 console 警告,同时继续回传当前可返回状态;移动壳配置检查拒绝该注入脚本重新出现空 catch。 +- 影响范围:`apps/mobile-shell/src/shell/webViewPolicy.ts`、`apps/mobile-shell/src/shell/webViewPolicy.test.ts`、`apps/mobile-shell/scripts/check-config.mjs`。 +- 验证方式:`npm run mobile-shell:test -- src/shell/webViewPolicy.test.ts`、`npm run mobile-shell:typecheck`、`npm run check:native-shells`、`npm run check:encoding`、`git diff --check`。 + ## 2026-06-20 移动壳生命周期映射门禁 - 背景:Expo `AppState` 的 `active`、`background`、`inactive` 以及未知状态都会进入 `app.lifecycle` 事件;如果归一化映射漂移,H5 游戏循环、背景音乐和固定玩法音频会错误恢复或暂停。