From 84156e7d86be54142270bade08929602b77b9b2e Mon Sep 17 00:00:00 2001 From: kdletters Date: Thu, 18 Jun 2026 09:55:49 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E9=BD=90=E7=A7=BB=E5=8A=A8=E5=A3=B3Me?= =?UTF-8?q?tro=E5=AF=BC=E5=87=BA=E7=83=9F=E6=B5=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 Expo iOS 和 Android production bundle 导出检查 将 mobile-shell:export 纳入原生壳统一验收 同步宿主壳方案、能力协议和共享记忆验收口径 --- .gitignore | 1 + apps/mobile-shell/package.json | 1 + .../scripts/check-expo-export.mjs | 96 +++++++++++++++++++ docs/README.md | 2 +- .../shared-memory/decision-log.md | 2 +- .../shared-memory/development-workflow.md | 2 +- ...ExpoReactNative与Tauri宿主壳方案-2026-06-17.md | 6 +- ...前端架构】宿主壳能力统一协议-2026-06-17.md | 4 +- package.json | 1 + scripts/check-native-shells.mjs | 5 + 10 files changed, 113 insertions(+), 7 deletions(-) create mode 100644 apps/mobile-shell/scripts/check-expo-export.mjs diff --git a/.gitignore b/.gitignore index b90d61ceb..8de32f4a9 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,7 @@ temp*build*/ /apps/desktop-shell/src-tauri/target/ /apps/desktop-shell/src-tauri/gen/ /apps/desktop-shell/src-tauri/permissions/autogenerated/ +/apps/mobile-shell/.expo-export-smoke/ /server-rs/.spacetimedb/ /server-rs/.data/ /public/generated-animations diff --git a/apps/mobile-shell/package.json b/apps/mobile-shell/package.json index b4108788e..03070965a 100644 --- a/apps/mobile-shell/package.json +++ b/apps/mobile-shell/package.json @@ -10,6 +10,7 @@ "ios": "expo run:ios", "test": "vitest run -c vitest.config.ts", "config:smoke": "node scripts/check-expo-config.mjs", + "export:smoke": "node scripts/check-expo-export.mjs", "typecheck": "tsc -p tsconfig.json --noEmit && node scripts/check-config.mjs" }, "dependencies": { diff --git a/apps/mobile-shell/scripts/check-expo-export.mjs b/apps/mobile-shell/scripts/check-expo-export.mjs new file mode 100644 index 000000000..d377af9db --- /dev/null +++ b/apps/mobile-shell/scripts/check-expo-export.mjs @@ -0,0 +1,96 @@ +import {spawnSync} from 'node:child_process'; + +import fs from 'node:fs'; + +const shellRoot = new URL('../', import.meta.url); +const outputRoot = new URL('../.expo-export-smoke/', import.meta.url); +const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm'; +const platforms = ['android', 'ios']; + +function runExpoExport(platform) { + const outputDir = `.expo-export-smoke/${platform}`; + const result = spawnSync( + npmCommand, + [ + 'exec', + 'expo', + 'export', + '--', + '--platform', + platform, + '--output-dir', + outputDir, + ], + { + cwd: shellRoot, + encoding: 'utf8', + stdio: 'pipe', + }, + ); + + if (result.error) { + throw new Error( + `failed to start Expo ${platform} export smoke: ${result.error.message}`, + ); + } + + if (result.signal) { + throw new Error( + `Expo ${platform} export smoke was terminated by signal ${result.signal}`, + ); + } + + if ((result.status ?? 0) !== 0) { + process.stdout.write(result.stdout ?? ''); + process.stderr.write(result.stderr ?? ''); + process.exit(result.status ?? 1); + } +} + +function readMetadata(platform) { + const metadataPath = new URL(`${platform}/metadata.json`, outputRoot); + if (!fs.existsSync(metadataPath)) { + throw new Error(`Expo ${platform} export did not produce metadata.json`); + } + + const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf8')); + if (metadata.bundler !== 'metro') { + throw new Error(`Expo ${platform} export must use the Metro bundler`); + } + + const bundlePath = metadata.fileMetadata?.[platform]?.bundle; + if (typeof bundlePath !== 'string' || bundlePath.length === 0) { + throw new Error(`Expo ${platform} export metadata is missing bundle path`); + } + + return bundlePath; +} + +function assertBundle(platform, bundlePath) { + const bundleFile = new URL(`${platform}/${bundlePath}`, outputRoot); + if (!fs.existsSync(bundleFile)) { + throw new Error(`Expo ${platform} bundle is missing: ${bundlePath}`); + } + + const stats = fs.statSync(bundleFile); + if (stats.size < 100_000) { + throw new Error(`Expo ${platform} bundle is unexpectedly small`); + } + + if (!bundlePath.includes(`/static/js/${platform}/AppEntry-`)) { + throw new Error(`Expo ${platform} bundle path does not target AppEntry`); + } +} + +fs.rmSync(outputRoot, {recursive: true, force: true}); + +try { + for (const platform of platforms) { + runExpoExport(platform); + assertBundle(platform, readMetadata(platform)); + } + + console.log('[mobile-shell:expo-export] OK'); +} finally { + fs.rmSync(outputRoot, {recursive: true, force: true}); +} diff --git a/docs/README.md b/docs/README.md index 3f661d1c9..9ee5e7cfb 100644 --- a/docs/README.md +++ b/docs/README.md @@ -24,7 +24,7 @@ 微信小程序壳、未来原生 App 壳、固定内置玩法与 AI H5 沙箱之间的宿主能力边界见 [【前端架构】宿主壳能力统一协议-2026-06-17.md](./【前端架构】宿主壳能力统一协议-2026-06-17.md)。 移动端壳采用 Expo + React Native、桌面端壳采用 Tauri,并统一作为 HostBridge adapter 的分阶段方案见 [【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md](./【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md)。 -当前首轮工程入口:`npm run mobile-shell:dev`、`npm run desktop-shell:dev`;统一验收入口:`npm run check:native-shells`。排查单端问题时再分别运行 `npm run mobile-shell:typecheck`、`npm run mobile-shell:test`、`npm run mobile-shell:config`、`npm run desktop-shell:typecheck`、`npm run desktop-shell:test`;排查桌面 release 打包入口时运行 `npm run desktop-shell:build -- --no-bundle`。 +当前首轮工程入口:`npm run mobile-shell:dev`、`npm run desktop-shell:dev`;统一验收入口:`npm run check:native-shells`。排查单端问题时再分别运行 `npm run mobile-shell:typecheck`、`npm run mobile-shell:test`、`npm run mobile-shell:config`、`npm run mobile-shell:export`、`npm run desktop-shell:typecheck`、`npm run desktop-shell:test`;排查桌面 release 打包入口时运行 `npm run desktop-shell:build -- --no-bundle`。 本地通过 SSH alias 管理多台服务器、查看硬件 / systemd / HTTP 健康状态并执行受控服务启停的 egui 桌面工具见 [【开发运维】本地SSH服务器管理面板技术方案-2026-06-11.md](./technical/【开发运维】本地SSH服务器管理面板技术方案-2026-06-11.md)。 diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 7007a8977..5005939fb 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -26,7 +26,7 @@ - 2026-06-18 能力声明收紧:`packages/shared/src/contracts/hostBridge.ts` 提供 HostBridge method / capability 白名单,H5 的 `getHostRuntime()` 会解析并过滤 `hostCapabilities`;`openHostShare`、`writeHostClipboardText`、`requestHostHapticsImpact`、`setHostAppTitle`、`exportHostTextFile` 等 native 能力只在宿主声明对应 capability 后调用。发布分享弹窗只有声明 `share.open` 时才显示“系统分享”,避免旧壳或裁剪壳露出不可用入口。 - 2026-06-18 宿主 runtime 回读:主 App 启动时会通过真实 `host.getRuntime` 回读 Expo / Tauri runtime 并缓存过滤后的能力清单,能力来源为 URL `hostCapabilities` 与宿主真实回包的并集;裁剪壳或旧入口 URL 缺少 `hostCapabilities` 时也能启用真实声明能力,但仍不会仅凭 `native_app` 或 transport 存在推断能力可用。 - 2026-06-18 壳能力防漂移:`npm run mobile-shell:typecheck` 与 `npm run desktop-shell:typecheck` 会校验 Expo / Tauri 壳声明的 capability 均来自共享 HostBridge 白名单,并校验壳 runtime 回包、H5 URL `hostCapabilities` 和实现分支保持一致;新增能力必须先更新契约和真实壳实现,再通过这些检查。 -- 2026-06-18 原生壳统一验收门禁:根级 `npm run check:native-shells` 统一执行 H5 HostBridge 关键测试、Expo 壳 typecheck / test / config smoke、Tauri 壳 typecheck / cargo test 和桌面 release `--no-bundle` 构建烟测;根级 `npm run check` 会在 lint、主站测试、构建和内容检查后继续执行该门禁,避免 HostBridge、两端壳、Expo managed config 和桌面 release 入口验收散落成容易漏跑的单项命令。 +- 2026-06-18 原生壳统一验收门禁:根级 `npm run check:native-shells` 统一执行 H5 HostBridge 关键测试、Expo 壳 typecheck / test / config smoke / Metro export smoke、Tauri 壳 typecheck / cargo test 和桌面 release `--no-bundle` 构建烟测;根级 `npm run check` 会在 lint、主站测试、构建和内容检查后继续执行该门禁,避免 HostBridge、两端壳、Expo managed config、移动端 production bundle 和桌面 release 入口验收散落成容易漏跑的单项命令。 - 2026-06-18 分享卡图片导出:新增 `file.exportImage` HostBridge capability,H5 分享卡下载在 native app 中优先把 canvas 生成的 base64 图片交给宿主导出;Expo 壳写缓存图片后交给系统分享 / 保存面板,Tauri 壳通过系统保存对话框写入图片字节。该能力只接受 `image/png` / `image/jpeg` / `image/webp`、单次 5 MiB 内图片数据,成功只返回文件名和字节数,不暴露本机绝对路径;宿主未声明时保留浏览器下载。 - 2026-06-18 应用角标能力:新增 `app.setBadgeCount` HostBridge capability,H5 只传 `0-99999` 整数并在宿主未声明时静默 fallback;Expo 壳只在 iOS 声明并通过 React Native `PushNotificationIOS` 设置应用图标角标,Android 不声明、不伪造成功;Tauri 壳通过主窗口 `set_badge_count` 设置任务栏角标,底层平台不支持时返回真实错误。 - 2026-06-18 草稿生成未读角标:平台壳层把“可见作品架里未读的草稿生成完成更新”同步到 `app.setBadgeCount`;同一草稿的 work/profile/session 等多个恢复 ID 只计 1,已读、失败、生成中和不可见草稿不计入。该角标只消费已有 HostBridge 能力,宿主不支持或设置失败不影响 H5 红点、作品架或后端状态。 diff --git a/docs/project-memory/shared-memory/development-workflow.md b/docs/project-memory/shared-memory/development-workflow.md index dbc82d174..e275b283f 100644 --- a/docs/project-memory/shared-memory/development-workflow.md +++ b/docs/project-memory/shared-memory/development-workflow.md @@ -216,7 +216,7 @@ npm run build npm run check:native-shells ``` -该命令会覆盖 H5 HostBridge 关键测试、Expo 壳 typecheck / test / config smoke、Tauri 壳 typecheck / cargo test,并执行桌面壳 release `--no-bundle` 构建烟测,确认 Expo managed config、打包 H5 资产和 Tauri release 入口可编译。 +该命令会覆盖 H5 HostBridge 关键测试、Expo 壳 typecheck / test / config smoke / Metro export smoke、Tauri 壳 typecheck / cargo test,并执行桌面壳 release `--no-bundle` 构建烟测,确认 Expo managed config、移动端 iOS / Android production bundle、打包 H5 资产和 Tauri release 入口可编译。 内容检查: diff --git a/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md b/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md index 027a7b5c6..1974c0999 100644 --- a/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md +++ b/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md @@ -229,7 +229,7 @@ GameBridge 禁止: - 壳层只接受来自允许 origin / packaged asset 的消息。 - 每个请求必须有超时,重复 `id` 不得重复执行支付、登录等非幂等动作。 - 能力按 `capabilities` / `hostCapabilities` 下发,H5 会过滤未知能力,并根据声明结果决定是否展示入口、发起宿主请求或走 fallback;进入 `native_app` 后主 App 会再通过真实 `host.getRuntime` 回读一次宿主 runtime 并缓存能力,用来补齐裁剪壳或旧入口 URL 缺少 `hostCapabilities` 的场景。不能只凭 `native_app` 宿主类型假设能力可用。 -- 壳能力声明与两端壳测试必须通过 `npm run check:native-shells` 统一校验;排查单端问题时可再分别运行 `npm run mobile-shell:typecheck`、`npm run mobile-shell:test`、`npm run mobile-shell:config`、`npm run desktop-shell:typecheck`、`npm run desktop-shell:test` 或 `npm run desktop-shell:build -- --no-bundle`。声明的 capability 必须存在于共享 HostBridge 白名单,壳 runtime 回包、H5 URL `hostCapabilities`、壳实现、Expo managed config 和桌面 release 构建入口不得漂移。 +- 壳能力声明与两端壳测试必须通过 `npm run check:native-shells` 统一校验;排查单端问题时可再分别运行 `npm run mobile-shell:typecheck`、`npm run mobile-shell:test`、`npm run mobile-shell:config`、`npm run mobile-shell:export`、`npm run desktop-shell:typecheck`、`npm run desktop-shell:test` 或 `npm run desktop-shell:build -- --no-bundle`。声明的 capability 必须存在于共享 HostBridge 白名单,壳 runtime 回包、H5 URL `hostCapabilities`、壳实现、Expo managed config、移动端 production bundle 和桌面 release 构建入口不得漂移。 - 宿主壳不得把长期 token、支付密钥或用户敏感资料回传给 H5。 - Tauri 禁止把 shell / fs 等高危插件作为默认能力暴露给主 WebView。 - RN WebView 禁止打开任意 URL 后仍保留完整 HostBridge;跳外链只允许 `http:`、`https:`、`mailto:`、`tel:`,并使用系统浏览器或降级能力,危险协议直接阻断。 @@ -315,6 +315,8 @@ GameBridge 禁止: 2026-06-18 追加:移动壳 Expo managed config 烟测进入统一验收。`npm run check:native-shells` 会执行 `npm run mobile-shell:config`,在 `apps/mobile-shell` 目录内调用 `expo config --type public --json`,校验 Expo CLI 实际解析结果中的包名、scheme、深链、ATS / cleartext / backup / 麦克风权限、启动页、adaptive icon、插件配置和 HostBridge 版本没有漂移。 +2026-06-18 追加:移动壳 Metro export 烟测进入统一验收。`npm run check:native-shells` 会执行 `npm run mobile-shell:export`,用 Expo CLI 分别为 Android 和 iOS 生成 production bundle,校验 Metro metadata 与 `AppEntry` bundle 已产出后清理临时 `.expo-export-smoke/` 目录。该烟测不生成 APK、AAB 或 IPA,也不写商店资料,只证明移动壳入口、Expo/RN 依赖和平台差异代码能被真实生产 bundler 打包。 + 2026-06-18 追加:两端壳的生产源码和配置禁止出现 mock / fake / placeholder / stub / TODO / FIXME 以及对应中文脚手架词;测试文件仍可使用 `vi.mock` 或等价测试替身。`apps/mobile-shell/scripts/check-config.mjs` 与 `apps/desktop-shell/scripts/check-config.mjs` 会扫描各自生产入口、配置和壳实现,防止把临时替身或占位文案带进可分发壳。 2026-06-18 追加:移动壳启动页与 Android adaptive icon 复用现有真实品牌图标 `apps/mobile-shell/assets/icon.png`,背景色固定为 H5 壳根背景 `#fffdf9`。该 PNG 是 1024x1024 RGBA 透明前景品牌资产,不新增占位图;Expo `splash` 使用同一图标 `contain` 展示,Android `adaptiveIcon.foregroundImage` 使用同一透明前景图,`check-config.mjs` 会校验图标尺寸、透明像素、启动页和 adaptive icon 配置。 @@ -353,7 +355,7 @@ GameBridge 禁止: - AI sandbox 无法调用 HostBridge,也无法读取 H5 登录态。 - Tauri release 包不允许任意远端页面调用桌面命令。 - Expo WebView 外链离开主站后不保留完整 HostBridge。 -- 根级验收入口 `npm run check:native-shells` 必须同时覆盖 H5 HostBridge 关键路径、Expo 壳 typecheck / test / config smoke、Tauri 壳 typecheck / cargo test 和桌面 release `--no-bundle` 构建烟测。 +- 根级验收入口 `npm run check:native-shells` 必须同时覆盖 H5 HostBridge 关键路径、Expo 壳 typecheck / test / config smoke / Metro export smoke、Tauri 壳 typecheck / cargo test 和桌面 release `--no-bundle` 构建烟测。 ## 参考资料 diff --git a/docs/【前端架构】宿主壳能力统一协议-2026-06-17.md b/docs/【前端架构】宿主壳能力统一协议-2026-06-17.md index 221544cf4..a7ced4128 100644 --- a/docs/【前端架构】宿主壳能力统一协议-2026-06-17.md +++ b/docs/【前端架构】宿主壳能力统一协议-2026-06-17.md @@ -70,7 +70,7 @@ AI H5 sandbox 2. `authService` 保留原导出,但内部委托 HostBridge,避免一次性改动 AuthGate。 3. 分享弹窗、分享目标同步、九宫切图、微信小程序支付和订阅授权改用 HostBridge 通用接口;旧微信命名服务只作为兼容导出。 4. 后续新增 `native_app` adapter 时只补桥接实现和测试,业务层不新增平台分叉;主 App 启动会触发一次 `host.getRuntime` 回读并订阅能力变化,避免裁剪壳或旧入口 URL 缺少 `hostCapabilities` 时长期隐藏真实可用能力。 -5. 每次新增或调整 native capability 后,必须运行 `npm run check:native-shells`,统一覆盖 H5 HostBridge 关键测试、Expo 壳 typecheck / test / config smoke、Tauri 壳 typecheck / cargo test 和桌面 release `--no-bundle` 构建烟测;排查单端问题时再单独运行 `npm run mobile-shell:typecheck`、`npm run mobile-shell:test`、`npm run mobile-shell:config`、`npm run desktop-shell:typecheck`、`npm run desktop-shell:test` 或 `npm run desktop-shell:build -- --no-bundle`。 +5. 每次新增或调整 native capability 后,必须运行 `npm run check:native-shells`,统一覆盖 H5 HostBridge 关键测试、Expo 壳 typecheck / test / config smoke / Metro export smoke、Tauri 壳 typecheck / cargo test 和桌面 release `--no-bundle` 构建烟测;排查单端问题时再单独运行 `npm run mobile-shell:typecheck`、`npm run mobile-shell:test`、`npm run mobile-shell:config`、`npm run mobile-shell:export`、`npm run desktop-shell:typecheck`、`npm run desktop-shell:test` 或 `npm run desktop-shell:build -- --no-bundle`。 ## 验收 @@ -79,7 +79,7 @@ AI H5 sandbox - 小程序支付仍跳转 `/pages/wechat-pay/index` 并保留支付结果 hash 回灌确认。 - 小程序订阅授权仍跳转 `/pages/subscribe-message/index`,且返回不阻断生成主链路。 - 普通浏览器分享、H5 支付和 Native 二维码支付不受影响。 -- 原生壳统一验收入口 `npm run check:native-shells` 通过,能力白名单、壳 runtime 回包、URL `hostCapabilities`、H5 fallback、两端壳实现、Expo managed config 和桌面 release 构建入口没有漂移。 +- 原生壳统一验收入口 `npm run check:native-shells` 通过,能力白名单、壳 runtime 回包、URL `hostCapabilities`、H5 fallback、两端壳实现、Expo managed config、移动端 production bundle 和桌面 release 构建入口没有漂移。 ## 后续 diff --git a/package.json b/package.json index c95bf0af5..6aa13dd49 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "mobile-shell:typecheck": "npm --prefix apps/mobile-shell run typecheck", "mobile-shell:test": "npm --prefix apps/mobile-shell run test", "mobile-shell:config": "npm --prefix apps/mobile-shell run config:smoke", + "mobile-shell:export": "npm --prefix apps/mobile-shell run export:smoke", "desktop-shell:dev": "npm --prefix apps/desktop-shell run dev", "desktop-shell:build": "npm --prefix apps/desktop-shell run build --", "desktop-shell:typecheck": "npm --prefix apps/desktop-shell run typecheck", diff --git a/scripts/check-native-shells.mjs b/scripts/check-native-shells.mjs index d5e01be65..11d8c3c8b 100644 --- a/scripts/check-native-shells.mjs +++ b/scripts/check-native-shells.mjs @@ -36,6 +36,11 @@ const steps = [ command: npmCommand, args: ['run', 'mobile-shell:config'], }, + { + label: 'mobile-shell-expo-export-smoke', + command: npmCommand, + args: ['run', 'mobile-shell:export'], + }, { label: 'desktop-shell-typecheck', command: npmCommand,