收口移动壳外层诊断日志
移动壳 shell 层注入 外链 深链 下载和扫码失败日志只记录稳定标签 移除 WebView 进程失败日志中的 H5 URL 输出 更新移动壳 shell 测试断言和配置门禁防止原生对象回流 补充宿主壳方案中的移动 shell 诊断边界
This commit is contained in:
@@ -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(() => {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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 契约生产源码,防止把临时替身或占位文案带进可分发壳。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user