关闭移动壳更新通道

移动壳 Expo 配置显式关闭 OTA 更新

移动壳配置检查拒绝更新通道和移动端观测依赖提前接入

宿主壳方案和共享决策记录发布通道边界
This commit is contained in:
2026-06-18 11:23:22 +08:00
parent 4e11ffb644
commit 5a03592f61
5 changed files with 60 additions and 0 deletions
+3
View File
@@ -15,6 +15,9 @@
"assetBundlePatterns": [
"**/*"
],
"updates": {
"enabled": false
},
"plugins": [
[
"expo-image-picker",
@@ -252,6 +252,22 @@ if (
throw new Error('mobile shell splash must use the real brand icon and brand background');
}
if (appConfig.updates?.enabled !== false) {
throw new Error('mobile shell OTA updates must stay disabled until a real release channel exists');
}
if (Object.keys(appConfig.updates ?? {}).some((key) => key !== 'enabled')) {
throw new Error('mobile shell must not configure OTA update metadata without a real release channel');
}
if ('runtimeVersion' in appConfig) {
throw new Error('mobile shell must not configure runtimeVersion without a real OTA release channel');
}
if ('releaseChannel' in appConfig || 'channel' in appConfig) {
throw new Error('mobile shell must not configure an app release channel without a real release process');
}
if (!appConfig.ios?.associatedDomains?.includes('applinks:app.genarrative.world')) {
throw new Error('mobile shell iOS associated domain is missing');
}
@@ -389,6 +405,22 @@ 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',
);
@@ -416,6 +448,14 @@ if (!notificationsPlugin) {
throw new Error('mobile shell notifications plugin is missing');
}
if (
appConfig.plugins?.some((plugin) =>
Array.isArray(plugin) ? plugin[0] === 'expo-updates' : plugin === 'expo-updates',
)
) {
throw new Error('mobile shell must not install expo-updates without a real release channel');
}
if (Array.isArray(notificationsPlugin)) {
const pluginOptions = notificationsPlugin[1] ?? {};
if (pluginOptions.enableBackgroundRemoteNotifications !== false) {
@@ -80,6 +80,16 @@ assertEqual(expoConfig.icon, './assets/icon.png', 'icon');
assertEqual(expoConfig.splash?.image, './assets/icon.png', 'splash image');
assertEqual(expoConfig.splash?.resizeMode, 'contain', 'splash resize mode');
assertEqual(expoConfig.splash?.backgroundColor, '#fffdf9', 'splash background');
assertEqual(expoConfig.updates?.enabled, false, 'OTA updates enabled flag');
if (Object.keys(expoConfig.updates ?? {}).some((key) => key !== 'enabled')) {
throw new Error('Expo config OTA update metadata must stay empty');
}
if ('runtimeVersion' in expoConfig) {
throw new Error('Expo config runtimeVersion must not be set without a real OTA release channel');
}
if ('releaseChannel' in expoConfig || 'channel' in expoConfig) {
throw new Error('Expo config release channel must not be set without a real release process');
}
assertEqual(
expoConfig.extra?.genarrativeHostBridgeVersion,
1,
@@ -192,4 +202,8 @@ assertEqual(
'background remote notifications',
);
if (findPlugin('expo-updates')) {
throw new Error('Expo config must not include expo-updates without a real release channel');
}
console.log('[mobile-shell:expo-config] OK');
@@ -47,6 +47,7 @@
- 2026-06-18 移动壳默认入口:Expo 壳默认 H5 地址固定为 `https://app.genarrative.world/`,开发联调本机 Vite 必须显式设置 `EXPO_PUBLIC_GENARRATIVE_WEB_URL=http://127.0.0.1:3000/` 或其它允许的 `http:` / `https:` 地址;生产包不得在未配置环境变量时加载设备本机 localhost。
- 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 桌面图片拖入接入主图槽位:`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 或窗口失焦时暂停 `<audio>` / WebAudio,回到 `active + focused` 后仅在运行态仍在播放、音源存在且用户音乐音量大于 0 时恢复,不改变用户音量设置。
@@ -294,6 +294,8 @@ GameBridge 禁止:
2026-06-18 追加:移动壳 H5 入口 query 和 `host.getRuntime` 回包统一读取 `MOBILE_SHELL_HOST_VERSION`,该常量必须与 Expo `app.json` / `package.json` 版本一致。配置检查会拒绝在 `App.tsx` 或 `mobileHostBridge.ts` 内重新散落硬编码版本,避免升级移动安装包时 H5 首屏上下文和宿主 runtime 回读版本不一致。
2026-06-18 追加:移动壳默认显式关闭 Expo OTA 更新,直到存在真实发布通道、更新端点、签名 / 回滚策略和团队发布流程后再接入。`app.json` 只允许 `updates.enabled=false`,不得配置 `runtimeVersion`、release channel、EAS channel、`expo-updates` 插件或移动端 crash / analytics / CodePush 依赖;`apps/mobile-shell/scripts/check-config.mjs` 和 Expo public config smoke 会共同拒绝这些发布通道能力被提前打开。
### Phase 3:Tauri 桌面壳 MVP
- 新增 `apps/desktop-shell/`。