diff --git a/apps/mobile-shell/scripts/check-config.mjs b/apps/mobile-shell/scripts/check-config.mjs index 1c4076d1d..e83931175 100644 --- a/apps/mobile-shell/scripts/check-config.mjs +++ b/apps/mobile-shell/scripts/check-config.mjs @@ -1708,10 +1708,13 @@ for (const snippet of [ 'let currentShareTarget: ShareOpenPayload | null = null;', 'currentShareTarget = normalizedTarget.payload;', 'setMobileHostBridgeShareTarget', + 'logMobileShareFailure', + 'mobile share failed for', 'request: HostBridgeRequest', "throw invalidRequest('target is required')", 'const normalizedTarget = normalizeHostBridgeShareOpenPayload(target);', "'share target is invalid'", + "logMobileShareFailure('open.share', error)", "message: 'share unavailable'", 'ok(request, true)', 'resetMobileHostBridgeShareTargetForTest', @@ -1738,6 +1741,8 @@ for (const snippet of [ 'stores only the normalized cached share payload', 'keeps the previous cached target when a new target is missing or invalid', 'maps native share sheet failures to stable host errors', + "vi.spyOn(console, 'warn')", + 'mobile share failed for open.share', 'does not fall back to cached target when explicit payload is unsafe', 'rejects empty share requests before opening native share sheet', "message: 'share unavailable'", diff --git a/apps/mobile-shell/src/host-bridge/share.test.ts b/apps/mobile-shell/src/host-bridge/share.test.ts index 33592f544..de0aae287 100644 --- a/apps/mobile-shell/src/host-bridge/share.test.ts +++ b/apps/mobile-shell/src/host-bridge/share.test.ts @@ -32,6 +32,7 @@ function request( } beforeEach(() => { + vi.restoreAllMocks(); vi.mocked(Share.share).mockReset(); resetMobileHostBridgeShareTargetForTest(); }); @@ -187,7 +188,9 @@ describe('mobile share helpers', () => { }); test('maps native share sheet failures to stable host errors', async () => { - vi.mocked(Share.share).mockRejectedValueOnce(new Error('native share failed')); + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const nativeError = new Error('native share failed'); + vi.mocked(Share.share).mockRejectedValueOnce(nativeError); await expect( openShare( @@ -200,6 +203,10 @@ describe('mobile share helpers', () => { code: 'host_error', message: 'share unavailable', }); + expect(warnSpy).toHaveBeenCalledWith( + 'mobile share failed for open.share', + nativeError, + ); }); test.each([ diff --git a/apps/mobile-shell/src/host-bridge/share.ts b/apps/mobile-shell/src/host-bridge/share.ts index 3a913bd38..366674b58 100644 --- a/apps/mobile-shell/src/host-bridge/share.ts +++ b/apps/mobile-shell/src/host-bridge/share.ts @@ -10,6 +10,10 @@ import { invalidRequest, ok } from './protocol'; let currentShareTarget: ShareOpenPayload | null = null; +function logMobileShareFailure(label: string, error: unknown) { + console.warn(`mobile share failed for ${label}`, error); +} + export function setMobileHostBridgeShareTarget(request: HostBridgeRequest) { const payload = request.payload; const target = @@ -65,7 +69,8 @@ export async function openShare(request: HostBridgeRequest) { message: message || url || sharePayload?.title || '', url, }); - } catch { + } catch (error) { + logMobileShareFailure('open.share', error); throw { code: 'host_error', message: 'share unavailable', diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index ddbe3aaee..1c8c398bb 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -3148,6 +3148,12 @@ - 决策:`apps/desktop-shell/src-tauri/src/host_bridge/network.rs` 必须在网络状态解析失败时记录 `desktop network failed for status.resolve` 日志。HostBridge 对 H5 仍只返回稳定 `network status unavailable`,不透传 resolver、线程或系统错误细节。 - 验证方式:`cargo test --manifest-path apps/desktop-shell/src-tauri/Cargo.toml host_bridge::network`、`npm run desktop-shell:typecheck`、`npm run check:native-shells`、`npm run check:encoding`、`git diff --check`。 +## 2026-06-20 移动壳系统分享异常必须可观测 + +- 背景:移动壳 `share.open` 调用 React Native 系统分享面板,失败时 H5 只需要知道分享不可用并保留复制链接等回退;但如果原生分享面板 reject 没有日志,开发侧无法区分分享面板不可用、系统取消异常或平台分享模块异常。 +- 决策:`apps/mobile-shell/src/host-bridge/share.ts` 必须在 `Share.share(...)` reject 时记录 `mobile share failed for open.share` 日志。HostBridge 对 H5 仍只返回稳定 `share unavailable`,不透传原生分享面板异常明细。 +- 验证方式:`npm run mobile-shell:test -- --run src/host-bridge/share.test.ts`、`npm run mobile-shell:typecheck`、`npm run check:native-shells`、`npm run check:encoding`、`git diff --check`。 + ## 2026-06-20 移动壳门禁脚本必须自登记自扫描 - 背景:Expo 移动壳单端检查已把 `apps/mobile-shell/scripts/` 纳入生产源码扫描入口,但 `check-config.mjs` 自身仍被排除在脚本清单和替身词扫描之外;这会让移动壳与桌面壳门禁结构不一致,也可能让后续门禁反查内容绕过生产替身词规则。 diff --git a/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md b/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md index b8aeecefe..31549ba51 100644 --- a/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md +++ b/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md @@ -320,6 +320,8 @@ GameBridge 禁止: 2026-06-20 追加:移动壳文件能力的原生系统异常必须可观测。Expo Sharing 可用性 / 分享面板、DocumentPicker、文本 / base64 文件读取、相册 / 相机权限请求和相册 / 相机打开失败时,`apps/mobile-shell/src/host-bridge/files.ts` 必须记录 `mobile HostBridge file failed for ...` 日志;HostBridge 回包仍只返回稳定的 `host_error` / `unsupported_capability` / `cancelled` / `invalid_request` 语义,不把原生异常明细透传给 H5。移动壳配置检查反查这些日志 label 和对应单测,避免用户可见文件导入导出动作失败后只剩静默兜底。 +2026-06-20 追加:移动壳系统分享失败必须可观测。React Native `Share.share(...)` reject 时,`apps/mobile-shell/src/host-bridge/share.ts` 必须记录 `mobile share failed for open.share` 日志;HostBridge 回包仍只暴露稳定 `share unavailable` 语义,不透传原生分享面板异常明细。 + 2026-06-19 追加:视觉小说结果页素材选择弹窗在移动壳声明 `file.importImage` / `file.importAudio` 时优先打开宿主系统图片或音频选择器,把 HostBridge 返回的 base64 副本转换成浏览器 `File` 后继续调用 `uploadVisualNovelAsset`,不绕过平台素材上传、历史素材、AI 图片生成或草稿写回链路;用户取消原生选择时停留在壳流程内,不连带弹出浏览器文件输入。普通浏览器、小程序和未声明能力的裁剪壳继续使用原隐藏文件输入。 2026-06-19 追加:帮助与反馈页在移动壳声明 `file.captureImage` 时展示“拍摄凭证”入口,调用 Expo 相机拍摄后把 HostBridge 返回的图片副本转换成浏览器 `File`,继续复用反馈页最多 4 张、单张 1 MiB、总 4 MiB、图片 MIME、data URL 预览和提交 payload 校验。拍摄取消时停留在壳流程内,不触发浏览器文件输入;Tauri 桌面壳不声明拍摄能力,普通浏览器、小程序和未声明能力的裁剪壳不显示该入口。