From 68d0208e2dfb70ed4807b9dee9b7e51b709b6b21 Mon Sep 17 00:00:00 2001 From: kdletters Date: Fri, 19 Jun 2026 13:01:16 +0800 Subject: [PATCH] =?UTF-8?q?=E5=BC=80=E6=94=BE=E7=A7=BB=E5=8A=A8=E5=A3=B3?= =?UTF-8?q?=E5=90=8C=E6=BA=90=E9=BA=A6=E5=85=8B=E9=A3=8E=E7=8E=A9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为 Expo 移动壳配置同源 H5 实时玩法所需麦克风权限 为 WebView 增加同源媒体捕获授权策略 更新移动壳配置门禁和 Expo public config 烟测 同步宿主壳方案文档和项目共享决策记录 --- apps/mobile-shell/app.json | 11 +++-- apps/mobile-shell/scripts/check-config.mjs | 37 ++++++++++++++--- .../scripts/check-expo-config.mjs | 41 +++++++++++++++---- apps/mobile-shell/src/shell/ShellApp.tsx | 1 + .../shared-memory/decision-log.md | 4 +- ...ExpoReactNative与Tauri宿主壳方案-2026-06-17.md | 4 +- 6 files changed, 78 insertions(+), 20 deletions(-) diff --git a/apps/mobile-shell/app.json b/apps/mobile-shell/app.json index 05c3b5d4c..0dbb9281c 100644 --- a/apps/mobile-shell/app.json +++ b/apps/mobile-shell/app.json @@ -22,7 +22,9 @@ [ "expo-camera", { - "cameraPermission": "允许 Genarrative 使用相机扫描二维码。" + "cameraPermission": "允许 Genarrative 使用相机扫描二维码。", + "microphonePermission": "允许 Genarrative 使用麦克风运行需要实时声音输入的玩法。", + "recordAudioAndroid": true } ], [ @@ -30,7 +32,7 @@ { "photosPermission": "允许 Genarrative 读取你选择的图片,用于导入创作素材和参考图。", "cameraPermission": "允许 Genarrative 使用相机拍摄创作素材和参考图。", - "microphonePermission": false + "microphonePermission": "允许 Genarrative 使用麦克风运行需要实时声音输入的玩法。" } ], [ @@ -46,6 +48,7 @@ "supportsTablet": true, "infoPlist": { "ITSAppUsesNonExemptEncryption": false, + "NSMicrophoneUsageDescription": "允许 Genarrative 使用麦克风运行需要实时声音输入的玩法。", "NSAppTransportSecurity": { "NSAllowsArbitraryLoads": false } @@ -60,10 +63,12 @@ "usesCleartextTraffic": false, "allowBackup": false, "softwareKeyboardLayoutMode": "resize", + "permissions": [ + "android.permission.RECORD_AUDIO" + ], "blockedPermissions": [ "android.permission.MANAGE_EXTERNAL_STORAGE", "android.permission.READ_EXTERNAL_STORAGE", - "android.permission.RECORD_AUDIO", "android.permission.RECEIVE_BOOT_COMPLETED", "android.permission.REQUEST_INSTALL_PACKAGES", "android.permission.SCHEDULE_EXACT_ALARM", diff --git a/apps/mobile-shell/scripts/check-config.mjs b/apps/mobile-shell/scripts/check-config.mjs index c30133111..17ee05079 100644 --- a/apps/mobile-shell/scripts/check-config.mjs +++ b/apps/mobile-shell/scripts/check-config.mjs @@ -153,7 +153,6 @@ const blockedScheduledNotificationSnippets = [ const blockedAndroidPermissions = [ 'android.permission.MANAGE_EXTERNAL_STORAGE', 'android.permission.READ_EXTERNAL_STORAGE', - 'android.permission.RECORD_AUDIO', 'android.permission.RECEIVE_BOOT_COMPLETED', 'android.permission.REQUEST_INSTALL_PACKAGES', 'android.permission.SCHEDULE_EXACT_ALARM', @@ -874,6 +873,13 @@ if ( throw new Error('mobile shell iOS ATS must not allow arbitrary network loads'); } +if ( + appConfig.ios?.infoPlist?.NSMicrophoneUsageDescription !== + '允许 Genarrative 使用麦克风运行需要实时声音输入的玩法。' +) { + throw new Error('mobile shell iOS microphone permission text must describe same-origin H5 gameplay input'); +} + if (appConfig.android?.package !== 'world.genarrative.mobile') { throw new Error('mobile shell Android package must be world.genarrative.mobile'); } @@ -894,6 +900,14 @@ if (appConfig.android?.softwareKeyboardLayoutMode !== 'resize') { throw new Error('mobile shell Android keyboard layout must resize the WebView'); } +if ( + !Array.isArray(appConfig.android?.permissions) || + appConfig.android.permissions.length !== 1 || + appConfig.android.permissions[0] !== 'android.permission.RECORD_AUDIO' +) { + throw new Error('mobile shell Android package must request only RECORD_AUDIO for same-origin H5 microphone gameplay'); +} + for (const permission of blockedAndroidPermissions) { if (!appConfig.android?.blockedPermissions?.includes(permission)) { throw new Error(`mobile shell Android package must block ${permission}`); @@ -906,8 +920,8 @@ for (const permission of blockedAndroidPermissions) { } } -if (appConfig.android?.permissions && appConfig.android.permissions.length > 0) { - throw new Error('mobile shell Android package must not request explicit permissions by default'); +if (appConfig.android?.blockedPermissions?.includes('android.permission.RECORD_AUDIO')) { + throw new Error('mobile shell Android package must not block RECORD_AUDIO needed by same-origin H5 microphone gameplay'); } if ( @@ -1007,6 +1021,7 @@ for (const snippet of [ 'allowUniversalAccessFromFileURLs={false}', 'allowsFullscreenVideo', 'allowsInlineMediaPlayback', + 'mediaCapturePermissionGrantType="grantIfSameHostElsePrompt"', 'mediaPlaybackRequiresUserAction', 'thirdPartyCookiesEnabled={false}', 'sharedCookiesEnabled={false}', @@ -1269,8 +1284,11 @@ if (Array.isArray(imagePickerPlugin)) { if (typeof pluginOptions.cameraPermission !== 'string') { throw new Error('mobile shell image picker camera permission text is missing'); } - if (pluginOptions.microphonePermission !== false) { - throw new Error('mobile shell image picker must not request microphone'); + if ( + pluginOptions.microphonePermission !== + '允许 Genarrative 使用麦克风运行需要实时声音输入的玩法。' + ) { + throw new Error('mobile shell image picker microphone text must describe same-origin H5 gameplay input'); } } @@ -1286,6 +1304,15 @@ if (Array.isArray(cameraPlugin)) { if (typeof pluginOptions.cameraPermission !== 'string') { throw new Error('mobile shell camera permission text is missing'); } + if ( + pluginOptions.microphonePermission !== + '允许 Genarrative 使用麦克风运行需要实时声音输入的玩法。' + ) { + throw new Error('mobile shell camera microphone text must describe same-origin H5 gameplay input'); + } + if (pluginOptions.recordAudioAndroid !== true) { + throw new Error('mobile shell camera plugin must enable Android record audio for same-origin H5 microphone gameplay'); + } } const notificationsPlugin = appConfig.plugins?.find((plugin) => diff --git a/apps/mobile-shell/scripts/check-expo-config.mjs b/apps/mobile-shell/scripts/check-expo-config.mjs index 13d362357..c26f033ac 100644 --- a/apps/mobile-shell/scripts/check-expo-config.mjs +++ b/apps/mobile-shell/scripts/check-expo-config.mjs @@ -80,6 +80,18 @@ function assertSameList(actual, expected, label) { } } +function assertSameSet(actual, expected, label) { + if ( + !Array.isArray(actual) || + actual.length !== expected.length || + expected.some((value) => !actual.includes(value)) + ) { + throw new Error( + `Expo config ${label} drifted: expected ${JSON.stringify(expected)} but got ${JSON.stringify(actual)}`, + ); + } +} + function findPlugin(name) { return expoConfig.plugins?.find((plugin) => Array.isArray(plugin) ? plugin[0] === name : plugin === name, @@ -172,6 +184,11 @@ assertEqual( false, 'iOS ATS arbitrary loads', ); +assertEqual( + expoConfig.ios?.infoPlist?.NSMicrophoneUsageDescription, + '允许 Genarrative 使用麦克风运行需要实时声音输入的玩法。', + 'iOS microphone permission text', +); assertEqual( expoConfig.android?.package, @@ -190,9 +207,9 @@ assertEqual( 'resize', 'Android software keyboard layout mode', ); -assertSameList( +assertSameSet( expoConfig.android?.permissions ?? [], - ['android.permission.CAMERA'], + ['android.permission.CAMERA', 'android.permission.RECORD_AUDIO'], 'Android explicit permissions', ); assertIncludes( @@ -205,11 +222,9 @@ assertIncludes( 'android.permission.READ_EXTERNAL_STORAGE', 'Android external download blocked permissions', ); -assertIncludes( - expoConfig.android?.blockedPermissions, - 'android.permission.RECORD_AUDIO', - 'Android blocked permissions', -); +if (expoConfig.android?.blockedPermissions?.includes('android.permission.RECORD_AUDIO')) { + throw new Error('Expo config Android permissions must not block RECORD_AUDIO needed by same-origin H5 microphone gameplay'); +} assertIncludes( expoConfig.android?.blockedPermissions, 'android.permission.REQUEST_INSTALL_PACKAGES', @@ -285,7 +300,7 @@ assertEqual( ); assertEqual( imagePickerPlugin[1]?.microphonePermission, - false, + '允许 Genarrative 使用麦克风运行需要实时声音输入的玩法。', 'image picker microphone permission', ); @@ -298,6 +313,16 @@ assertEqual( 'string', 'camera permission text type', ); +assertEqual( + cameraPlugin[1]?.microphonePermission, + '允许 Genarrative 使用麦克风运行需要实时声音输入的玩法。', + 'camera microphone permission', +); +assertEqual( + cameraPlugin[1]?.recordAudioAndroid, + true, + 'camera Android record audio flag', +); const notificationsPlugin = findPlugin('expo-notifications'); if (!Array.isArray(notificationsPlugin)) { diff --git a/apps/mobile-shell/src/shell/ShellApp.tsx b/apps/mobile-shell/src/shell/ShellApp.tsx index f96e9cd99..e35009e22 100644 --- a/apps/mobile-shell/src/shell/ShellApp.tsx +++ b/apps/mobile-shell/src/shell/ShellApp.tsx @@ -339,6 +339,7 @@ export default function ShellApp() { allowUniversalAccessFromFileURLs={false} allowsFullscreenVideo allowsInlineMediaPlayback + mediaCapturePermissionGrantType="grantIfSameHostElsePrompt" mediaPlaybackRequiresUserAction thirdPartyCookiesEnabled={false} sharedCookiesEnabled={false} diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 5fa04b90a..a3e4ad78b 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -49,7 +49,7 @@ - 2026-06-18 外部生成队列轮询接入宿主网络状态:H5 新增 `useHostNetworkOnline()`,宿主未声明网络能力时按在线处理以保持浏览器和旧壳行为;宿主明确 `isConnected=false` 或 `isInternetReachable=false` 时,平台外部生成队列概览暂停 HTTP 轮询,恢复在线后重新刷新。该能力只减少离线请求,不改变外部生成队列、作品架、弹窗或后端任务状态事实。 - 2026-06-18 桌面图片导入:新增 `file.importImage` 与 `file.imageDropped` HostBridge capability,Tauri 壳通过系统文件选择框和主窗口拖拽事件读取用户选择 / 拖入的真实图片,只允许 `image/png`、`image/jpeg`、`image/webp` 且单次不超过 10 MiB;H5 统一使用 `importHostImageFile()` / `subscribeHostImageDrop()`,宿主只回传文件名、MIME、base64 内容、字节数和可选坐标,不暴露本地绝对路径,也不开放通用文件系统。 - 2026-06-18 移动图片导入:Expo 壳开始声明并实现 `file.importImage`,通过 `expo-image-picker` 请求相册权限并打开系统相册选择器,只允许 `image/png`、`image/jpeg`、`image/webp` 且单次不超过 10 MiB;成功只回传清洗后的文件名、MIME、base64 内容和字节数,不暴露设备本地 URI,用户取消返回 `cancelled` 并由 H5 facade 归为 `false`。 -- 2026-06-18 移动图片拍摄导入:Expo 壳新增 `file.captureImage` HostBridge capability,通过 `expo-image-picker` 请求相机权限并打开系统相机拍摄图片,沿用 `file.importImage` 的 MIME、体积、base64 和文件名清洗规则,成功回传 `action=captured`,不暴露设备本地 URI,也不请求麦克风权限;Tauri 壳不声明该能力,不伪造桌面拍摄。 +- 2026-06-18 移动图片拍摄导入:Expo 壳新增 `file.captureImage` HostBridge capability,通过 `expo-image-picker` 请求相机权限并打开系统相机拍摄图片,沿用 `file.importImage` 的 MIME、体积、base64 和文件名清洗规则,成功回传 `action=captured`,不暴露设备本地 URI;该拍摄能力不使用麦克风权限,移动壳麦克风权限只服务同源 H5 实时玩法。Tauri 壳不声明该能力,不伪造桌面拍摄。 - 2026-06-19 移动二维码扫描:Expo 壳新增 `scanner.scanQrCode` HostBridge capability,通过 `expo-camera` 请求相机权限并打开真实扫码 overlay,成功只返回共享契约清洗后的二维码文本和 `qr_code` 格式,空值、控制字符和超长文本按 `normalizeHostBridgeQrCodeValue` 处理;用户关闭扫码返回 `cancelled`,H5 个人中心扫码入口不再连带弹出浏览器摄像头权限。Tauri 桌面壳只把 `scanner.scanQrCode` 保留在 method 白名单中返回 `unsupported_method`,不声明 capability、不伪造桌面扫码;宿主缺能力或非法结果时 H5 继续走原浏览器扫码 fallback。 - 2026-06-18 H5 图片上传接入宿主导入:`CreativeImageInputPanel` 在 `native_app` 且声明 `file.importImage` / `file.captureImage` 时,主图上传和描述参考图上传可分别调用 `importHostImageFile()` / `captureHostImageFile()`,并把宿主返回的 base64 图片转换为现有 `File` 回调;浏览器、小程序和未声明能力的裁剪壳继续走原生 `` 路径,不新增玩法侧上传分叉。 - 2026-06-18 移动壳安全区:Expo 壳根布局使用 `react-native-safe-area-context` 的 `SafeAreaProvider` 与四边 `SafeAreaView` 保护 WebView,避免 H5 主站内容贴进 iOS 刘海、底部 Home Indicator、Android 状态栏或横屏边缘;该能力属于宿主壳布局保护,不新增 H5 占位 UI,不改变玩法 runtime 或 HostBridge capability。 @@ -101,7 +101,7 @@ - 2026-06-18 移动壳启动页与 adaptive icon:Expo 移动壳启动页和 Android adaptive icon 复用现有真实品牌图标 `apps/mobile-shell/assets/icon.png`,背景色固定为 H5 壳根背景 `#fffdf9`。该 PNG 是 1024x1024 RGBA 透明前景品牌资产,不新增占位图;配置检查会校验图标尺寸、透明像素、splash 和 adaptive icon 指向,避免后续换成非品牌或占位素材。 - 2026-06-18 桌面壳 bundle 图标集:Tauri 桌面壳从现有真实品牌 PNG `apps/desktop-shell/src-tauri/icons/icon.png` 派生 `32x32.png`、`128x128.png`、`128x128@2x.png`、`icon.ico` 和 `icon.icns`,并在 `bundle.icon` 中同时声明这些平台图标。检查脚本会校验 PNG 尺寸、ICO 多尺寸头部、ICNS 容器长度和 bundle 图标列表,避免后续退回单图标或替换为非品牌 / 占位素材。 - 2026-06-18 移动壳网络安全元数据:Expo 移动壳默认包配置显式禁用 Android 明文流量 `usesCleartextTraffic=false`,iOS ATS 禁用任意加载 `NSAllowsArbitraryLoads=false`,并设置 `ITSAppUsesNonExemptEncryption=false` 作为当前未接入自定义加密能力的出口合规声明;本地 Vite 联调只通过 development build 显式环境变量进入,不把任意明文流量开关带进默认包配置。 -- 2026-06-18 移动壳麦克风权限禁用:Expo 移动壳的 `expo-image-picker` 插件必须保持 `microphonePermission=false`,Android 包配置必须通过 `android.blockedPermissions` 显式移除 `android.permission.RECORD_AUDIO`,源 `app.json` 不手写 `android.permissions`,最终 Expo public config 只允许 `expo-camera` 为 `scanner.scanQrCode` 带入 `android.permission.CAMERA`;移动壳现阶段没有录音、后台音频采集或远程语音 SDK,音频导入只走系统文档选择器。配置检查会拒绝麦克风权限拦截缺失、被反向加入或相机之外的显式权限漂移。 +- 2026-06-19 移动壳同源 H5 麦克风权限:Expo 移动壳允许 `RECORD_AUDIO` 和 iOS 麦克风用途文案,仅用于同源主站 H5 中需要实时声音输入的正式玩法,例如汪汪声浪 `published` runtime 的 `getUserMedia({ audio: true })` 音量采样;WebView 必须保持 `mediaCapturePermissionGrantType="grantIfSameHostElsePrompt"`,外域页面仍不能留在带 HostBridge 的 WebView 内。该权限不新增 HostBridge method,不代表后台录音、远程语音 SDK 或 AI H5 sandbox 能直接访问宿主能力;`expo-camera` 与 `expo-image-picker` 的麦克风用途文案、Android `RECORD_AUDIO`、Expo public config 和 WebView 媒体捕获策略由移动壳配置检查统一约束。 - 2026-06-18 移动壳 Android 自动备份关闭:Expo 移动壳必须保持 `android.allowBackup=false`,避免 WebView cookie、localStorage、缓存文件和宿主文件导入导出中间态进入 Google Drive 自动备份 / 恢复链路;正式业务事实仍以后端账号、作品、钱包和草稿状态为准。配置检查会拒绝恢复 Android 默认允许备份的包配置。 - 2026-06-18 移动壳 WebView 安全开关:Expo 移动壳 WebView 必须显式禁用 JS 自动开窗、多窗口、文件访问、file URL 跨源访问、HTTPS 混合内容、第三方 Cookie、共享 Cookie 和 WebView 远程调试;同源主站页面才能留在带 HostBridge 的 WebView 内,外链只通过受控协议离开容器交给系统。配置检查和移动壳导航测试会拒绝这些边界被放宽。 - 2026-06-18 移动壳 WebView 默认下载边界:Expo WebView 内网页自动下载和 `` 直接落盘默认关闭;壳层注入脚本阻断 download 链接,iOS `onFileDownload` 只丢弃不落盘,Android 包配置通过 `blockedPermissions` 移除外部存储读写、管理外部存储和请求安装包权限。移动端文本、图片、音频保存只能通过 `file.exportText`、`file.exportImage`、`file.exportAudio` 等 HostBridge 受控导出能力进入系统分享 / 保存面板。 diff --git a/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md b/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md index 55e0f9bf3..ca13010b9 100644 --- a/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md +++ b/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md @@ -467,7 +467,7 @@ GameBridge 禁止: 2026-06-18 追加:桌面壳 release 构建烟测进入统一验收。`npm run check:native-shells` 会在 H5 HostBridge、Expo 壳和 Tauri 单测通过后执行 `npm run desktop-shell:build -- --no-bundle`,确认根 `dist` H5 资产、Tauri release 入口、受控命令白名单、图标和 Rust release 编译可以共同产出桌面二进制;该烟测不生成平台安装包,避免把 Linux 本机缺少的系统打包器误判为 HostBridge 回归。 -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 追加:移动壳 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 打包。 @@ -489,7 +489,7 @@ GameBridge 禁止: 2026-06-18 追加:移动壳生产包网络安全元数据显式收紧。Android `usesCleartextTraffic=false`,iOS `NSAppTransportSecurity.NSAllowsArbitraryLoads=false`,并设置 `ITSAppUsesNonExemptEncryption=false` 作为当前未接入自定义加密能力的出口合规声明;开发联调本机 Vite 仍通过显式 `EXPO_PUBLIC_GENARRATIVE_WEB_URL=http://127.0.0.1:3000/` 进入 development build,不把任意明文流量开关带进默认包配置。`apps/mobile-shell/scripts/check-config.mjs` 会校验这些网络安全字段。 -2026-06-18 追加:移动壳 Android 包级权限显式移除麦克风。`expo-image-picker` 插件继续设置 `microphonePermission=false`,同时 Expo `android.blockedPermissions` 固定包含 `android.permission.RECORD_AUDIO`,防止后续插件或 manifest 合并把录音权限带入最终包;源 `app.json` 不手写 `android.permissions`,最终 public config 只允许 `expo-camera` 因 `scanner.scanQrCode` 带入 `android.permission.CAMERA`。移动壳拍照和扫码只请求相机,相册只请求媒体读取,本地通知只请求即时通知权限,音频导入走系统文档选择器,不申请麦克风或后台录音能力。`apps/mobile-shell/scripts/check-config.mjs` 会拒绝麦克风权限缺失拦截或被重新声明。 +2026-06-19 追加:移动壳麦克风权限只服务同源 H5 实时玩法。汪汪声浪正式 `published` runtime 必须通过浏览器标准 `getUserMedia({ audio: true })` 采样真实音量;Expo 壳因此显式配置 iOS 麦克风用途文案、Android `RECORD_AUDIO` 权限和 `react-native-webview` 的 `mediaCapturePermissionGrantType="grantIfSameHostElsePrompt"`。该权限不新增 HostBridge method,不开放后台录音、远程语音 SDK、任意文件系统或 AI H5 sandbox 直连宿主能力;只有通过同源主站 WebView 留壳规则的 H5 页面能触发系统授权。`expo-camera` 与 `expo-image-picker` 的麦克风用途文案必须保持同一条真实玩法说明,`android.blockedPermissions` 不得再移除 `RECORD_AUDIO`,最终 public config 只允许 `CAMERA` 和 `RECORD_AUDIO` 两类显式权限。移动拍照、扫码、相册、音频导入和本地通知仍按各自受控能力边界执行,不把麦克风权限当作通用录音入口。`apps/mobile-shell/scripts/check-config.mjs` 与 `check-expo-config.mjs` 会拒绝权限、文案或 WebView 媒体捕获策略漂移。 2026-06-18 追加:移动壳 Android 自动备份显式关闭。Expo `android.allowBackup=false`,避免 WebView cookie、localStorage、缓存文件、系统剪贴板导入中间文件等本地宿主状态进入 Google Drive 自动备份 / 恢复链路;正式业务事实仍以后端账号、作品、钱包和草稿状态为准,不依赖设备备份恢复。`apps/mobile-shell/scripts/check-config.mjs` 会拒绝恢复默认允许备份的 Android 包配置。