From f8fe1235f1e9594dd053b2abb204abf0b16d8524 Mon Sep 17 00:00:00 2001 From: kdletters Date: Thu, 18 Jun 2026 14:30:46 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=B6=E5=8F=A3=E5=8E=9F=E7=94=9F=E5=A3=B3?= =?UTF-8?q?=E7=A6=81=E7=94=A8SDK=E9=94=81=E6=96=87=E4=BB=B6=E9=97=A8?= =?UTF-8?q?=E7=A6=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 移动壳配置检查拒绝发布与观测SDK锁文件解析 桌面壳配置检查拒绝观测更新与Tauri guest锁文件解析 宿主壳方案文档补充禁用SDK锁文件边界 共享决策日志记录原生壳禁用SDK解析约束 --- apps/desktop-shell/scripts/check-config.mjs | 39 +++++++++++++++++++ apps/mobile-shell/scripts/check-config.mjs | 20 ++++++++++ .../shared-memory/decision-log.md | 6 +-- ...€‘ExpoReactNative与Tauri宿主壳方案-2026-06-17.md | 6 +-- 4 files changed, 65 insertions(+), 6 deletions(-) diff --git a/apps/desktop-shell/scripts/check-config.mjs b/apps/desktop-shell/scripts/check-config.mjs index dfa7219ad..e7e100273 100644 --- a/apps/desktop-shell/scripts/check-config.mjs +++ b/apps/desktop-shell/scripts/check-config.mjs @@ -183,6 +183,30 @@ function assertNoBlockedNpmDependencies() { } } +function assertNoBlockedNpmLockPackages() { + const packageNames = Object.keys(rootPackageLock.packages ?? {}) + .filter((packagePath) => packagePath.startsWith('node_modules/')) + .map((packagePath) => packagePath.replace(/^node_modules\//, '')); + const dependencyNames = Object.keys(rootPackageLock.dependencies ?? {}); + const lockedPackageNames = new Set([...packageNames, ...dependencyNames]); + + for (const dependency of blockedDesktopNpmDependencies) { + if (lockedPackageNames.has(dependency)) { + throw new Error( + `root package-lock must not resolve ${dependency} before real observability/channel contracts exist`, + ); + } + } + + for (const dependency of lockedPackageNames) { + if (dependency === '@tauri-apps/api' || dependency.startsWith('@tauri-apps/plugin-')) { + throw new Error( + `root package-lock must not resolve ${dependency}; desktop H5 must use HostBridge through the injected Tauri global only`, + ); + } + } +} + function collectNpmDependencyNames(packageJson) { const dependencySections = [ 'dependencies', @@ -220,6 +244,19 @@ function assertNoBlockedCargoDependencies() { } } +function assertNoBlockedCargoLockPackages() { + const cargoPackageNames = new Set( + cargoLockPackages.map((entry) => entry.name).filter(Boolean), + ); + for (const dependency of blockedDesktopCargoDependencies) { + if (cargoPackageNames.has(dependency)) { + throw new Error( + `Cargo.lock must not resolve ${dependency} before real observability/channel contracts exist`, + ); + } + } +} + function assertNoBlockedDesktopSdkSnippets() { const sources = [ ['tauri.conf.json', JSON.stringify(config)], @@ -384,7 +421,9 @@ assertNoDevScaffoldTerms( assertNoBlockedNpmDependencies(); assertNoTauriGuestNpmDependencies(packageConfig, 'desktop shell package'); assertNoTauriGuestNpmDependencies(rootPackageConfig, 'root H5 package'); +assertNoBlockedNpmLockPackages(); assertNoBlockedCargoDependencies(); +assertNoBlockedCargoLockPackages(); assertNoBlockedDesktopSdkSnippets(); for (const [scriptName, expected] of Object.entries({ diff --git a/apps/mobile-shell/scripts/check-config.mjs b/apps/mobile-shell/scripts/check-config.mjs index 0ec0c08bc..7f55ffb2a 100644 --- a/apps/mobile-shell/scripts/check-config.mjs +++ b/apps/mobile-shell/scripts/check-config.mjs @@ -55,6 +55,9 @@ const blockedMobileChannelDependencies = [ 'posthog-react-native', 'react-native-code-push', ]; +const blockedMobileChannelLockDependencies = blockedMobileChannelDependencies.filter( + (dependency) => dependency !== 'expo-application', +); const blockedMobileChannelSnippets = [ '@react-native-firebase/analytics', '@react-native-firebase/app', @@ -170,6 +173,22 @@ function assertNoBlockedMobileChannelDependencies(packageJson, packageLabel) { } } +function assertNoBlockedMobileChannelLockPackages() { + const packageNames = Object.keys(rootPackageLock.packages ?? {}) + .filter((packagePath) => packagePath.startsWith('node_modules/')) + .map((packagePath) => packagePath.replace(/^node_modules\//, '')); + const dependencyNames = Object.keys(rootPackageLock.dependencies ?? {}); + const lockedPackageNames = new Set([...packageNames, ...dependencyNames]); + + for (const dependency of blockedMobileChannelLockDependencies) { + if (lockedPackageNames.has(dependency)) { + throw new Error( + `root package-lock must not resolve ${dependency} before the real mobile channel contract exists`, + ); + } + } +} + function assertNoBlockedMobileChannelSnippets() { const sources = [ ['app.json', JSON.stringify(appConfig)], @@ -305,6 +324,7 @@ assertNoDevScaffoldTerms( ); assertNoBlockedMobileChannelDependencies(packageConfig, 'mobile shell package'); assertNoBlockedMobileChannelDependencies(rootPackageConfig, 'root H5 package'); +assertNoBlockedMobileChannelLockPackages(); assertNoBlockedMobileChannelSnippets(); for (const [scriptName, expected] of Object.entries({ diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 150a1c45a..f983e26bf 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -47,8 +47,8 @@ - 2026-06-18 移动壳默认入口:Expo 壳默认 H5 地址固定为 `https://app.genarrative.world/`,开发联调本机 Vite 必须显式设置 `EXPO_PUBLIC_GENARRATIVE_WEB_URL=http://127.0.0.1:3000/`、`http://localhost:3000/` 或 `http://[::1]:3000/`;生产包不得在未配置环境变量时加载设备本机 localhost,也不得通过环境变量把第三方外域 H5 放入带完整 HostBridge 的 WebView。 - 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 移动壳发布通道边界:Expo 移动壳默认显式关闭 OTA 更新,只允许 `updates.enabled=false`;在真实发布通道、更新端点、签名 / 回滚策略和团队发布流程落地前,不得配置 `runtimeVersion`、release channel、EAS channel、`expo-updates` 插件或移动端 crash / analytics / CodePush 依赖。移动壳配置检查和 Expo public config smoke 会拒绝这些发布通道能力被提前打开,根 `package-lock.json` 也不得解析 `expo-updates`、Sentry、Firebase Analytics、PostHog、Amplitude、Segment、CodePush 等真实发布 / 观测 SDK。 +- 2026-06-18 移动壳观测与渠道 SDK 初始化边界:移动壳生产入口、HostBridge、启动 URL 和 runtime 配置不得提前初始化 Sentry、Firebase Analytics、PostHog、Amplitude、Segment、CodePush 或 Expo Updates;这些 SDK 必须等真实发布通道、采集字段、用户授权、隐私披露、签名 / 回滚策略和团队发布流程落地后逐项接入。配置检查会同时拒绝相关依赖、锁文件解析、Expo 配置和源码初始化片段;`expo-application` 可能由 Expo 自身传递解析,但项目不得主动 direct 依赖它实现渠道逻辑。 - 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 或窗口失焦时暂停 `