diff --git a/apps/mobile-shell/scripts/check-config.mjs b/apps/mobile-shell/scripts/check-config.mjs
index 6c15f81d8..f61ad27ad 100644
--- a/apps/mobile-shell/scripts/check-config.mjs
+++ b/apps/mobile-shell/scripts/check-config.mjs
@@ -42,6 +42,8 @@ const notificationsPath = new URL('../src/host-bridge/notifications.ts', import.
const notificationsSource = fs.readFileSync(notificationsPath, 'utf8');
const protocolPath = new URL('../src/host-bridge/protocol.ts', import.meta.url);
const protocolSource = fs.readFileSync(protocolPath, 'utf8');
+const scannerPath = new URL('../src/host-bridge/scanner.ts', import.meta.url);
+const scannerSource = fs.readFileSync(scannerPath, 'utf8');
const hostBridgeRuntimePath = new URL('../src/host-bridge/runtime.ts', import.meta.url);
const hostBridgeRuntimeSource = fs.readFileSync(hostBridgeRuntimePath, 'utf8');
const sharePath = new URL('../src/host-bridge/share.ts', import.meta.url);
@@ -1778,6 +1780,24 @@ if (!shellAppSource.includes('')) {
throw new Error('mobile shell ShellApp must render the QR scanner overlay');
}
+if (
+ !dispatchSource.includes('scanMobileHostBridgeQrCode(request)') ||
+ dispatchSource.includes('ok(request, await scanQrCode())')
+) {
+ throw new Error('mobile shell QR scanner HostBridge method must delegate to scanner module');
+}
+
+for (const scannerSnippet of [
+ 'scanMobileHostBridgeQrCode(request: HostBridgeRequest)',
+ 'ok(request, await scanQrCode())',
+ 'normalizeHostBridgeQrCodeValue',
+ "hostBridgeError('cancelled', 'qr scan cancelled')",
+]) {
+ if (!scannerSource.includes(scannerSnippet)) {
+ throw new Error(`mobile shell QR scanner module missing ${scannerSnippet}`);
+ }
+}
+
const capabilityQuerySnippet = "capabilities: MOBILE_HOST_CAPABILITIES";
if (shellAppSource.includes(capabilityQuerySnippet)) {
throw new Error('mobile shell URL must resolve platform-aware capabilities');
diff --git a/apps/mobile-shell/src/host-bridge/dispatch.ts b/apps/mobile-shell/src/host-bridge/dispatch.ts
index cec5a9b06..f35080033 100644
--- a/apps/mobile-shell/src/host-bridge/dispatch.ts
+++ b/apps/mobile-shell/src/host-bridge/dispatch.ts
@@ -10,12 +10,12 @@ import {
importTextFile,
} from './files';
import {
- type MobileHostBridgeNavigation,
failure,
+ type MobileHostBridgeNavigation,
ok,
unsupported,
} from './protocol';
-import { resetQrScannerForTest, scanQrCode } from './scanner';
+import { resetQrScannerForTest, scanMobileHostBridgeQrCode } from './scanner';
import {
openShare,
resetMobileHostBridgeShareTargetForTest,
@@ -76,7 +76,7 @@ export async function dispatchMobileHostBridgeRequest(
case 'file.captureImage':
return ok(request, await captureImageFile());
case 'scanner.scanQrCode':
- return ok(request, await scanQrCode());
+ return scanMobileHostBridgeQrCode(request);
case 'file.importAudio':
return ok(request, await importAudioFile());
case 'file.exportAudio':
diff --git a/apps/mobile-shell/src/host-bridge/scanner.ts b/apps/mobile-shell/src/host-bridge/scanner.ts
index 458016953..df3ed4ff8 100644
--- a/apps/mobile-shell/src/host-bridge/scanner.ts
+++ b/apps/mobile-shell/src/host-bridge/scanner.ts
@@ -1,8 +1,10 @@
import {
type HostBridgeError,
+ type HostBridgeRequest,
type ScannerScanQrCodeResult,
normalizeHostBridgeQrCodeValue,
} from '../../../../packages/shared/src/contracts/hostBridge';
+import { ok } from './protocol';
type QrScannerState = {
active: boolean;
@@ -75,6 +77,10 @@ export function scanQrCode(): Promise {
});
}
+export async function scanMobileHostBridgeQrCode(request: HostBridgeRequest) {
+ return ok(request, await scanQrCode());
+}
+
export function completeQrCodeScan(rawValue: unknown) {
const result = normalizeHostBridgeQrCodeValue(rawValue);
if (!result || !pendingQrScan) {
diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md
index 8e1f7b1d1..49af3391a 100644
--- a/docs/project-memory/shared-memory/decision-log.md
+++ b/docs/project-memory/shared-memory/decision-log.md
@@ -62,7 +62,7 @@
- 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;picker 调用必须固定为单选、禁用编辑、禁用 EXIF、请求 base64 且 `mediaTypes` 只允许 `images`,不得扩大到视频或任意媒体。成功只回传清洗后的文件名、MIME、base64 内容和字节数,不暴露设备本地 URI,用户取消返回 `cancelled` 并由 H5 facade 归为 `false`。
- 2026-06-18 移动图片拍摄导入:Expo 壳新增 `file.captureImage` HostBridge capability,通过 `expo-image-picker` 请求相机权限并打开系统相机拍摄图片,沿用 `file.importImage` 的 MIME、体积、base64 和文件名清洗规则;相机 picker 必须禁用编辑、禁用 EXIF、请求 base64 且 `mediaTypes` 只允许 `images`,成功回传 `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-19 移动二维码扫描:Expo 壳新增 `scanner.scanQrCode` HostBridge capability,通过 `expo-camera` 请求相机权限并打开真实扫码 overlay,成功只返回共享契约清洗后的二维码文本和 `qr_code` 格式,空值、控制字符和超长文本按 `normalizeHostBridgeQrCodeValue` 处理;HostBridge 成功响应包装统一收口在 `apps/mobile-shell/src/host-bridge/scanner.ts`,`dispatch.ts` 只委托扫码请求。用户关闭扫码返回 `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。
- 2026-06-18 移动壳方向策略:Expo 壳 `orientation` 固定为 `default`,不锁竖屏或横屏;后续固定玩法和 AI H5 sandbox 的方向需求由设备方向、H5 响应式布局和玩法自身画布适配承接,壳层只负责安全区、WebView 容器和 HostBridge。移动壳配置检查和 Expo public config smoke 会拒绝重新锁定 portrait / landscape。