收口移动壳外层诊断日志

移动壳 shell 层注入 外链 深链 下载和扫码失败日志只记录稳定标签

移除 WebView 进程失败日志中的 H5 URL 输出

更新移动壳 shell 测试断言和配置门禁防止原生对象回流

补充宿主壳方案中的移动 shell 诊断边界
This commit is contained in:
2026-06-20 23:19:07 +08:00
parent 05d278f8f7
commit 636880d4df
8 changed files with 76 additions and 31 deletions
+62 -5
View File
@@ -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}`);
@@ -149,7 +149,6 @@ describe('QrScannerOverlay', () => {
});
expect(warnSpy).toHaveBeenCalledWith(
'mobile QR scanner permission request failed',
error,
);
expect(overlayHarness.cameraViewProps.current).toBeNull();
});
@@ -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() {
@@ -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(() => {
+10 -11
View File
@@ -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) {
@@ -184,7 +184,6 @@ describe('TRACK_MOBILE_WEBVIEW_HISTORY_SCRIPT', () => {
expect(warnSpy).toHaveBeenCalledWith(
'mobile navigation state sync failed',
syncError,
);
expect(postMessage).toHaveBeenCalledWith(
JSON.stringify({
+1 -1
View File
@@ -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');
}
}
@@ -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 <stage>` 这类固定阶段标签,不把原生 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 <stage>` 这类固定阶段标签,不把原生 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 契约生产源码,防止把临时替身或占位文案带进可分发壳。