diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 3d27968fe..4add90ed4 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -107,6 +107,7 @@ - 2026-06-21 微信九宫切图诊断日志收口:微信 `share-grid` 壳下载封面、读取图片、导出切图、保存相册或最终保存流程失败时,只记录 `[share-grid] ` 固定标签,不输出 `wx.downloadFile`、`wx.getImageInfo`、`wx.canvasToTempFilePath`、`wx.saveImageToPhotosAlbum` 或其它原生错误对象;页面仍只展示 `九宫切图保存失败。` 稳定文案。 - 2026-06-21 微信支付与订阅诊断日志收口:微信支付参数解析、虚拟支付失败和订阅消息请求失败只记录 `[wechat-pay] ` / `[subscribe-message] ` 固定标签,不输出微信原生错误对象;H5 回灌继续只使用稳定 `wechat payment unavailable` / `wechat subscribe unavailable` 语义。 - 2026-06-21 微信 WebView 认证诊断日志收口:微信 `web-view` 壳解析认证结果、`wx.login`、小程序登录请求、手机号绑定请求、认证流程和手机号授权拒绝失败时,只记录 `[web-view] ` 固定标签,不输出微信原生错误对象、HTTP response 或授权 detail;页面仍只展示稳定登录 / 绑手机号错误文案。 +- 2026-06-21 微信 WebView 页面事件诊断日志收口:微信 `web-view` 壳加载成功、加载失败和 H5 message 事件只记录 `[web-view] ` 固定标签,不输出 WebView `event.detail`;分享目标解析继续走结构化 message payload,但生产日志不得回吐原生事件体。 - 2026-06-19 微信壳路由一致性门禁:`npm run check:native-shells` 必须反查 `miniprogram/app.json.pages`、`miniprogram/host-bridge/protocol.js`、H5 `src/services/host-bridge/hostBridge.ts` 小程序页面常量、H5 `src/services/wechatMiniProgramSubscribe.ts` 订阅授权页面常量、`miniprogram/host-bridge/webView.js` 分享入口 / 分享消息类型、`miniprogram/config.js` source query / 域名格式、`miniprogram/shell/webView.js` 请求头来源标记、H5 runtime parser 和 H5 路由保留字段。新增或调整小程序页面、登录派生 URL、支付页、九宫切图页、订阅页、WebView 来源标记、H5 入口域名、API base URL 或宿主上下文 query 字段时,必须同步这几处常量并保持生产 / 开发域名都显式配置为纯 HTTPS domain;运行时开发域名回退生产域名只作为异常兜底。 - 2026-06-18 登录 / 支付能力禁伪声明:`auth.requestLogin` 和 `payment.request` 保留在共享 HostBridge 契约中供未来真实接入,但 Expo / Tauri 壳在真实 SDK、渠道流程和后端契约落地前不得声明这些 capability,也不得把它们写入入口 URL `hostCapabilities`;两端检查脚本会拒绝伪声明,请求实际到达壳层时必须返回明确 `unsupported_method` 并让 H5 fallback,两端壳测试直接覆盖这两个 method。 - 2026-06-20 桌面壳未声明 method 禁伪成功:Tauri 桌面壳只声明真实可用 capability;共享 HostBridge method 白名单中未进入桌面 capability profile 的 method,例如 `auth.requestLogin`、`payment.request`、`file.captureImage`、`scanner.scanQrCode` 和 `haptics.impact`,请求实际到达桌面壳时必须统一返回明确 `unsupported_method`,不得伪造成功或半接入。桌面 Rust 测试必须从 `HOST_BRIDGE_METHODS` 与 `capabilities()` 差集派生 unsupported 覆盖清单,桌面单端配置检查会反查该派生路径,后续共享契约新增 method 时必须同步声明真实桌面能力或补进 unsupported 语义。 diff --git a/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md b/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md index da79a96fd..4058b7295 100644 --- a/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md +++ b/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md @@ -544,6 +544,8 @@ GameBridge 禁止: 2026-06-21 追加:微信 `web-view` 认证链路诊断只允许记录 `[web-view] ` 固定标签,不把 `wx.login` 错误、登录 / 绑手机号 HTTP response、手机号授权 detail 或认证异常对象写入生产日志;页面仍只展示稳定登录 / 绑手机号错误文案。 +2026-06-21 追加:微信 `web-view` 页面事件诊断只允许记录 `[web-view] ` 固定标签,不把加载成功、加载失败或 H5 message 的 `event.detail` 写入生产日志;分享目标仍从结构化 message payload 解析,但日志不能输出原生事件体。 + 2026-06-19 追加:微信小程序壳路由一致性进入原生壳统一验收。`npm run check:native-shells` 会反查 `miniprogram/app.json.pages` 与 `miniprogram/host-bridge/protocol.js` 页面 URL 常量一致,H5 `src/services/host-bridge/hostBridge.ts` 的小程序登录、支付、九宫切图页面常量、H5 `src/services/wechatMiniProgramSubscribe.ts` 的订阅授权页面常量与微信协议常量一致,`miniprogram/host-bridge/webView.js` 的 WebView 分享入口和分享目标消息类型不漂移;同时会校验 `miniprogram/config.js` 的生产 / 开发 H5 入口与 API base URL 都是显式配置的纯 HTTPS 域名,`WEB_VIEW_SOURCE_QUERY` 与共享 `HOST_BRIDGE_WECHAT_MINI_PROGRAM_SOURCE_QUERY` 一致,`miniprogram/shell/webView.js` 请求头从 `WEB_VIEW_SOURCE_QUERY` 读取 `clientType` / `clientRuntime`,H5 runtime parser 读取共享 `HOST_BRIDGE_RUNTIME_CONTEXT_QUERY_KEY`,H5 路由保留字段读取共享 `HOST_BRIDGE_PRESERVED_RUNTIME_CONTEXT_QUERY_KEYS`。运行时对开发域名的生产域名回退只作为异常兜底,不作为配置口径;新增小程序页面、改页面路径、调整来源 query 或切换域名格式时,必须同步协议常量、H5 HostBridge / 订阅服务常量、`app.json`、共享 HostBridge query 契约和这条门禁。 2026-06-18 追加:原生壳本地生成物不作为生产源码门禁输入。Expo `.expo/`、Expo export smoke 临时目录、Tauri `target/`、Tauri schema `gen/` 和 Tauri 自动生成权限目录都必须保持 gitignored;根级生产壳敏感词扫描只检查可提交的壳源码和配置,避免本机工具输出影响生产门禁。手写 capability / 权限配置仍需保留在扫描范围内。 diff --git a/miniprogram/shell/webView.js b/miniprogram/shell/webView.js index f388973de..4d8957967 100644 --- a/miniprogram/shell/webView.js +++ b/miniprogram/shell/webView.js @@ -181,6 +181,14 @@ function logWebViewAuthFailure(label, _detail) { console.error(`[web-view] ${label}`); } +function logWebViewPageEvent(label, _detail) { + console.info(`[web-view] ${label}`); +} + +function logWebViewPageFailure(label, _detail) { + console.error(`[web-view] ${label}`); +} + function resolveMiniProgramRuntimeConfig() { const miniProgramEnv = normalizeMiniProgramEnv(readMiniProgramEnvVersion()) || @@ -678,11 +686,11 @@ function createWechatWebViewPage() { }, handleWebViewLoad(event) { - console.info('[web-view] loaded', event.detail); + logWebViewPageEvent('loaded', event.detail); }, handleWebViewError(event) { - console.error('[web-view] load failed', event.detail); + logWebViewPageFailure('load failed', event.detail); }, handleWebViewMessage(event) { @@ -690,8 +698,7 @@ function createWechatWebViewPage() { if (shareTarget) { this._currentShareTarget = shareTarget; } - // 中文注释:支付和订阅消息都由独立 native 页面承接,web-view 消息只保留调试输出。 - console.info('[web-view] message', event.detail); + logWebViewPageEvent('message', event.detail); }, onShareAppMessage() { diff --git a/miniprogram/shell/webView.test.js b/miniprogram/shell/webView.test.js index 242a220cb..77f5fd066 100644 --- a/miniprogram/shell/webView.test.js +++ b/miniprogram/shell/webView.test.js @@ -40,6 +40,9 @@ function createPage() { describe('wechat web-view shell page', () => { beforeEach(() => { + vi.clearAllMocks(); + vi.spyOn(console, 'error').mockImplementation(() => {}); + vi.spyOn(console, 'info').mockImplementation(() => {}); globalThis.wx = { getAccountInfoSync: vi.fn(() => ({ miniProgram: { envVersion: 'release' }, @@ -80,21 +83,23 @@ describe('wechat web-view shell page', () => { test('stores latest H5 share target for native share menu', () => { const page = createPage(); - page.handleWebViewMessage({ - detail: { - data: [ - { - data: { - type: 'genarrative:share-target', - payload: { - targetPath: '/works/detail', - work: 'BB-12345678', - title: '汪汪声浪', - }, + const webViewDetail = { + data: [ + { + data: { + type: 'genarrative:share-target', + payload: { + targetPath: '/works/detail', + work: 'BB-12345678', + title: '汪汪声浪', }, }, - ], - }, + }, + ], + }; + + page.handleWebViewMessage({ + detail: webViewDetail, }); expect(page.onShareAppMessage()).toEqual({ @@ -105,5 +110,21 @@ describe('wechat web-view shell page', () => { title: '陶泥儿', query: 'targetPath=%2Fworks%2Fdetail&work=BB-12345678', }); + expect(console.info).toHaveBeenCalledWith('[web-view] message'); + expect(console.info.mock.calls.flat()).not.toContain(webViewDetail); + }); + + test('logs web-view page events without native detail payloads', () => { + const page = createPage(); + const loadDetail = { src: 'https://www.genarrative.world/private' }; + const errorDetail = { errMsg: 'private native load failed' }; + + page.handleWebViewLoad({ detail: loadDetail }); + page.handleWebViewError({ detail: errorDetail }); + + expect(console.info).toHaveBeenCalledWith('[web-view] loaded'); + expect(console.error).toHaveBeenCalledWith('[web-view] load failed'); + expect(console.info.mock.calls.flat()).not.toContain(loadDetail); + expect(console.error.mock.calls.flat()).not.toContain(errorDetail); }); }); diff --git a/scripts/check-native-shells.mjs b/scripts/check-native-shells.mjs index d6dbe04ab..60c1d1d09 100644 --- a/scripts/check-native-shells.mjs +++ b/scripts/check-native-shells.mjs @@ -2817,6 +2817,55 @@ function assertWechatAuthFailureBoundaries() { } } +function assertWechatWebViewPageEventBoundaries() { + const webViewShellSource = fs.readFileSync( + 'miniprogram/shell/webView.js', + 'utf8', + ); + const webViewTestSource = fs.readFileSync( + 'miniprogram/shell/webView.test.js', + 'utf8', + ); + + for (const snippet of [ + 'function logWebViewPageEvent(label, _detail)', + 'function logWebViewPageFailure(label, _detail)', + 'console.info(`[web-view] ${label}`)', + 'console.error(`[web-view] ${label}`)', + "logWebViewPageEvent('loaded', event.detail)", + "logWebViewPageFailure('load failed', event.detail)", + "logWebViewPageEvent('message', event.detail)", + ]) { + if (!webViewShellSource.includes(snippet)) { + throw new Error(`wechat web-view page event diagnostics must include ${snippet}`); + } + } + + for (const forbiddenSnippet of [ + "console.info('[web-view] loaded', event.detail)", + "console.error('[web-view] load failed', event.detail)", + "console.info('[web-view] message', event.detail)", + ]) { + if (webViewShellSource.includes(forbiddenSnippet)) { + throw new Error(`wechat web-view page event diagnostics must not expose native detail via ${forbiddenSnippet}`); + } + } + + for (const snippet of [ + 'logs web-view page events without native detail payloads', + "expect(console.info).toHaveBeenCalledWith('[web-view] loaded')", + "expect(console.error).toHaveBeenCalledWith('[web-view] load failed')", + 'expect(console.info.mock.calls.flat()).not.toContain(loadDetail)', + 'expect(console.error.mock.calls.flat()).not.toContain(errorDetail)', + "expect(console.info).toHaveBeenCalledWith('[web-view] message')", + 'expect(console.info.mock.calls.flat()).not.toContain(webViewDetail)', + ]) { + if (!webViewTestSource.includes(snippet)) { + throw new Error(`wechat web-view page event boundary test must include ${snippet}`); + } + } +} + function assertWechatShareGridFailureBoundaries() { const shareGridSource = fs.readFileSync( 'miniprogram/shell/shareGrid.js', @@ -3131,6 +3180,9 @@ assertWechatSubscribeResultBoundaries(); console.log('[check:native-shells] wechat-auth-failure-boundaries'); assertWechatAuthFailureBoundaries(); +console.log('[check:native-shells] wechat-web-view-page-event-boundaries'); +assertWechatWebViewPageEventBoundaries(); + console.log('[check:native-shells] wechat-share-grid-failure-boundaries'); assertWechatShareGridFailureBoundaries();