From 25b140d63eb4df3aac3a13ae15daad136543c16c Mon Sep 17 00:00:00 2001 From: kdletters Date: Thu, 18 Jun 2026 11:39:05 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=B6=E7=B4=A7=E7=A7=BB=E5=8A=A8=E5=A3=B3?= =?UTF-8?q?=E8=A7=82=E6=B5=8B=E6=B8=A0=E9=81=93=E5=88=9D=E5=A7=8B=E5=8C=96?= =?UTF-8?q?=E8=BE=B9=E7=95=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 移动壳配置检查拒绝观测、渠道、更新 SDK 相关依赖。 移动壳配置检查拒绝生产入口和 HostBridge 提前初始化观测、渠道、更新 SDK。 更新 Expo React Native 与 Tauri 宿主壳方案中的 Phase 4 能力边界。 更新共享决策日志记录移动壳观测与渠道 SDK 初始化前置条件。 --- apps/mobile-shell/scripts/check-config.mjs | 88 +++++++++++++++---- .../shared-memory/decision-log.md | 1 + ...ExpoReactNative与Tauri宿主壳方案-2026-06-17.md | 4 +- 3 files changed, 75 insertions(+), 18 deletions(-) diff --git a/apps/mobile-shell/scripts/check-config.mjs b/apps/mobile-shell/scripts/check-config.mjs index 1027b3868..8f585ad9b 100644 --- a/apps/mobile-shell/scripts/check-config.mjs +++ b/apps/mobile-shell/scripts/check-config.mjs @@ -40,6 +40,37 @@ const devScaffoldTerms = [ '模' + '拟', '伪' + '造', ]; +const blockedMobileChannelDependencies = [ + '@react-native-firebase/analytics', + '@react-native-firebase/app', + '@segment/analytics-react-native', + '@sentry/react-native', + 'amplitude-react-native', + 'expo-application', + 'expo-updates', + 'posthog-react-native', + 'react-native-code-push', +]; +const blockedMobileChannelSnippets = [ + '@react-native-firebase/analytics', + '@react-native-firebase/app', + '@segment/analytics-react-native', + '@sentry/react-native', + 'Amplitude.getInstance', + 'Analytics.screen', + 'Analytics.track', + 'CodePush.sync', + 'PostHogProvider', + 'Sentry.init', + 'Updates.checkForUpdateAsync', + 'Updates.fetchUpdateAsync', + 'Updates.reloadAsync', + 'analytics().logEvent', + 'amplitude.init', + 'codePush(', + 'posthog.capture', + 'posthog.init', +]; const blockedAndroidPermissions = [ 'android.permission.RECORD_AUDIO', 'android.permission.RECEIVE_BOOT_COMPLETED', @@ -79,6 +110,45 @@ function extractStringConstExport(source, exportName) { return match[1]; } +function assertNoBlockedMobileChannelDependencies() { + const dependencySections = [ + 'dependencies', + 'devDependencies', + 'optionalDependencies', + 'peerDependencies', + ]; + + for (const dependency of blockedMobileChannelDependencies) { + for (const section of dependencySections) { + if (packageConfig[section]?.[dependency]) { + throw new Error( + `mobile shell must not depend on ${dependency} before the real channel contract exists`, + ); + } + } + } +} + +function assertNoBlockedMobileChannelSnippets() { + const sources = [ + ['app.json', JSON.stringify(appConfig)], + ['App.tsx', appSource], + ['mobileHostBridge.ts', bridgeSource], + ['mobileShellUrl.ts', mobileShellUrlSource], + ['mobileShellRuntime.ts', mobileShellRuntimeSource], + ]; + + for (const [sourceName, source] of sources) { + for (const snippet of blockedMobileChannelSnippets) { + if (source.includes(snippet)) { + throw new Error( + `mobile shell ${sourceName} must not initialize ${snippet} before the real channel contract exists`, + ); + } + } + } +} + function collectProductionSourceFiles(entry) { const stats = fs.statSync(entry); if (stats.isDirectory()) { @@ -148,6 +218,8 @@ function countAlphaPixels(png) { assertNoDevScaffoldTerms( productionSourceRoots.flatMap((root) => collectProductionSourceFiles(root)), ); +assertNoBlockedMobileChannelDependencies(); +assertNoBlockedMobileChannelSnippets(); const sharedCapabilities = extractStringArrayExport( sharedContractSource, @@ -405,22 +477,6 @@ for (const dependency of [ } } -for (const dependency of [ - 'expo-application', - 'expo-updates', - '@sentry/react-native', - '@react-native-firebase/app', - '@react-native-firebase/analytics', - 'react-native-code-push', - 'posthog-react-native', - 'amplitude-react-native', - '@segment/analytics-react-native', -]) { - if (packageConfig.dependencies?.[dependency] || packageConfig.devDependencies?.[dependency]) { - throw new Error(`mobile shell must not depend on ${dependency} before the real channel contract exists`); - } -} - const imagePickerPlugin = appConfig.plugins?.find((plugin) => Array.isArray(plugin) ? plugin[0] === 'expo-image-picker' : plugin === 'expo-image-picker', ); diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index ab1a2a5af..964fd993c 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -48,6 +48,7 @@ - 2026-06-18 移动壳安装包身份:Expo 移动壳的 iOS bundle identifier 与 Android package 统一固定为 `world.genarrative.mobile`,应用版本固定为 `0.1.0`,iOS `buildNumber` 从字符串 `"1"` 起步,Android `versionCode` 从整数 `1` 起步;后续分发安装包时递增构建号 / versionCode,产品版本号按发布节奏调整。移动壳配置检查会校验 `app.json` 与 `package.json` 版本一致,并拒绝缺失或漂移的包标识,当前不写入假商店元数据、假更新端点或占位渠道 SDK 配置。 - 2026-06-18 移动壳 HostBridge 版本单一来源:Expo 移动壳的 H5 入口 query 和 `host.getRuntime` 回包都读取 `MOBILE_SHELL_HOST_VERSION`,该常量必须与 `app.json` / `package.json` 版本一致;配置检查会拒绝 `App.tsx` 或 `mobileHostBridge.ts` 重新散落硬编码版本,避免安装包版本升级时 H5 首屏上下文与 runtime 回读分叉。 - 2026-06-18 移动壳发布通道边界:Expo 移动壳默认显式关闭 OTA 更新,只允许 `updates.enabled=false`;在真实发布通道、更新端点、签名 / 回滚策略和团队发布流程落地前,不得配置 `runtimeVersion`、release channel、EAS channel、`expo-updates` 插件或移动端 crash / analytics / CodePush 依赖。移动壳配置检查和 Expo public config smoke 会拒绝这些发布通道能力被提前打开。 +- 2026-06-18 移动壳观测与渠道 SDK 初始化边界:移动壳生产入口、HostBridge、启动 URL 和 runtime 配置不得提前初始化 Sentry、Firebase Analytics、PostHog、Amplitude、Segment、CodePush 或 Expo Updates;这些 SDK 必须等真实发布通道、采集字段、用户授权、隐私披露、签名 / 回滚策略和团队发布流程落地后逐项接入。配置检查会同时拒绝相关依赖、Expo 配置和源码初始化片段。 - 2026-06-18 桌面图片拖入接入主图槽位:`CreativeImageInputPanel` 在桌面壳声明 `file.imageDropped` 时订阅宿主拖入事件,只在拖入坐标命中当前主图卡片且未被上层元素遮挡时消费事件,避免窗口级拖入被多个创作面板同时接收;成功后仍转换为现有 `File` 上传回调。 - 2026-06-18 H5 背景音乐接入宿主生命周期:`useBackgroundMusic` 通过 `useHostLifecycleActive()` 消费 `subscribeHostAppLifecycle()` 的归一结果,宿主进入后台、inactive 或桌面窗口失焦时降低音量并暂停音频循环,同时 `suspend` WebAudio context;回到 `active + focused` 且用户原本开启音乐时再恢复播放,不改变用户音量设置。 - 2026-06-18 固定玩法音频接入宿主生命周期:前端新增 `useHostLifecycleActive()` 统一消费 `subscribeHostAppLifecycle()`,`useBackgroundMusic`、拼图运行态和抓大鹅运行态都只依赖该归一状态判断音频可播放性;宿主 inactive、background 或窗口失焦时暂停 `