diff --git a/apps/mobile-shell/scripts/check-config.mjs b/apps/mobile-shell/scripts/check-config.mjs index ca46f81e5..cdccb938f 100644 --- a/apps/mobile-shell/scripts/check-config.mjs +++ b/apps/mobile-shell/scripts/check-config.mjs @@ -1286,9 +1286,12 @@ if (webViewPolicySource.includes(`'${sharedPublicWebUrl}'`)) { for (const snippet of [ 'normalizeHostBridgeShareOpenPayload', - 'const explicitPayload = normalizeHostBridgeShareOpenPayload(payload);', + 'type HostBridgeRequest', + 'const explicitPayload = normalizeHostBridgeShareOpenPayload(request.payload);', 'normalizeHostBridgeShareOpenPayload(currentShareTarget)', 'setMobileHostBridgeShareTarget', + 'request: HostBridgeRequest', + 'ok(request, true)', 'resetMobileHostBridgeShareTargetForTest', ]) { if (!shareSource.includes(snippet)) { @@ -1297,8 +1300,10 @@ for (const snippet of [ } if ( - !dispatchSource.includes('setMobileHostBridgeShareTarget(request.payload)') || - !dispatchSource.includes('openShare(request.payload)') || + !dispatchSource.includes('setMobileHostBridgeShareTarget(request)') || + !dispatchSource.includes('openShare(request)') || + dispatchSource.includes('ok(request, await openShare') || + dispatchSource.includes('ok(request, setMobileHostBridgeShareTarget') || dispatchSource.includes('let currentShareTarget') || dispatchSource.includes('normalizeHostBridgeShareOpenPayload(currentShareTarget)') ) { diff --git a/apps/mobile-shell/src/host-bridge/dispatch.ts b/apps/mobile-shell/src/host-bridge/dispatch.ts index cbfade987..ab137b053 100644 --- a/apps/mobile-shell/src/host-bridge/dispatch.ts +++ b/apps/mobile-shell/src/host-bridge/dispatch.ts @@ -88,9 +88,9 @@ export async function dispatchMobileHostBridgeRequest( case 'app.setBadgeCount': return setMobileAppBadgeCount(request); case 'share.open': - return ok(request, await openShare(request.payload)); + return openShare(request); case 'share.setTarget': - return ok(request, setMobileHostBridgeShareTarget(request.payload)); + return setMobileHostBridgeShareTarget(request); case 'navigation.openNativePage': return ok( request, diff --git a/apps/mobile-shell/src/host-bridge/share.ts b/apps/mobile-shell/src/host-bridge/share.ts index 0cd872484..ea7a9806b 100644 --- a/apps/mobile-shell/src/host-bridge/share.ts +++ b/apps/mobile-shell/src/host-bridge/share.ts @@ -1,24 +1,28 @@ import { Share } from 'react-native'; -import { normalizeHostBridgeShareOpenPayload } from '../../../../packages/shared/src/contracts/hostBridge'; -import { invalidRequest } from './protocol'; +import { + type HostBridgeRequest, + normalizeHostBridgeShareOpenPayload, +} from '../../../../packages/shared/src/contracts/hostBridge'; +import { invalidRequest, ok } from './protocol'; let currentShareTarget: unknown = null; -export function setMobileHostBridgeShareTarget(payload: unknown) { +export function setMobileHostBridgeShareTarget(request: HostBridgeRequest) { + const payload = request.payload; currentShareTarget = payload && typeof payload === 'object' ? (payload as { target?: unknown }).target : null; - return true; + return ok(request, true); } export function resetMobileHostBridgeShareTargetForTest() { currentShareTarget = null; } -export async function openShare(payload: unknown) { - const explicitPayload = normalizeHostBridgeShareOpenPayload(payload); +export async function openShare(request: HostBridgeRequest) { + const explicitPayload = normalizeHostBridgeShareOpenPayload(request.payload); if (explicitPayload.status === 'invalid') { throw invalidRequest('share target is invalid'); } @@ -45,5 +49,5 @@ export async function openShare(payload: unknown) { message: message || url || sharePayload?.title || '', url, }); - return true; + return ok(request, true); } diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 11b8c2854..cdc32d777 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -29,7 +29,7 @@ - 2026-06-19 移动壳外链打开 helper 共用:Expo WebView 外域拦截和 HostBridge `app.openExternalUrl` 都必须复用 `openMobileShellExternalNavigation` 执行系统外链打开动作;HostBridge 分支仍先调用 `normalizeHostBridgeExternalUrlPayload` 保留 payload 错误语义,且 `apps/mobile-shell/src/host-bridge/navigation.ts` 自己承接 `expo-linking` 系统 API 调用,不再让 `dispatch.ts` 直接导入 `Linking` 或维护 `Linking.canOpenURL` / `Linking.openURL` 顺序。移动壳配置检查会拒绝 `app.openExternalUrl` 绕开该 helper 或分发层重新导入 `expo-linking`,避免两条离壳路径漂移。 - 2026-06-19 移动壳系统分享 URL 边界:Expo `share.open` 调用 React Native 系统分享面板前,只允许把 `url`、`href`、`path`、`targetPath` 和 `work` 归一为 `https://app.genarrative.world` 同源公开 URL;外域、协议相对 URL、`javascript:` 等危险目标必须返回 `invalid_request`,且显式非法 payload 不得回退到之前缓存的 `share.setTarget` 目标。分享实现复用移动壳入口 URL 的生产主站 origin,配置检查会拒绝重新声明同值 origin 或移除协议相对 URL 拦截。 - 2026-06-19 桌面壳系统分享 URL 边界:Tauri `share.open` 写入系统剪贴板前同样只允许把 `url`、`href`、`path`、`targetPath` 和 `work` 归一为 `https://app.genarrative.world` 同源公开 URL;外域、协议相对 URL、`javascript:` 等危险目标必须返回 `invalid_request`,且显式非法 payload 不得回退到之前缓存的 `share.setTarget` 目标。桌面壳配置检查会拒绝移除同源分享 URL 归一和协议相对 URL 拦截。 -- 2026-06-19 原生壳分享桥接边界:Expo `share.setTarget` / `share.open` 的缓存目标、分享 payload 归一和系统分享调用统一收口在 `apps/mobile-shell/src/host-bridge/share.ts`;Tauri `share.setTarget` / `share.open` 的缓存目标、分享文本生成、剪贴板 fallback 写入和 HostBridge 响应统一收口在 `apps/desktop-shell/src-tauri/src/host_bridge/share.rs`。两端 `dispatch` 只负责委托对应 share 模块,配置检查会拒绝分发层直接持有分享状态、生成分享文本或写入分享剪贴板结果。 +- 2026-06-19 原生壳分享桥接边界:Expo `share.setTarget` / `share.open` 的缓存目标、分享 payload 归一、系统分享调用和 HostBridge 响应统一收口在 `apps/mobile-shell/src/host-bridge/share.ts`;Tauri `share.setTarget` / `share.open` 的缓存目标、分享文本生成、剪贴板 fallback 写入和 HostBridge 响应统一收口在 `apps/desktop-shell/src-tauri/src/host_bridge/share.rs`。两端 `dispatch` 只负责委托对应 share 模块,配置检查会拒绝分发层直接持有分享状态、生成分享文本、写入分享剪贴板结果或包装分享成功响应。 - 2026-06-19 桌面壳窗口标题桥接边界:Tauri `app.setTitle` 的 payload 校验、非空 / 控制字符拒绝、80 字符截断和主窗口 `set_title` 调用统一收口在 `apps/desktop-shell/src-tauri/src/host_bridge/title.rs`;`dispatch.rs` 只负责委托 `set_desktop_host_bridge_window_title(...)`。桌面壳配置检查和根级结构门禁会覆盖 `title.rs` 文件清单、共享标题长度镜像和 dispatch 委托关系。 - 2026-06-19 桌面壳文件桥接执行边界:Tauri `file.exportText` / `file.importText` / `file.importDocument` / `file.exportImage` / `file.importImage` / `file.importAudio` / `file.exportAudio` 的系统文件对话框过滤器、用户取消语义、路径转换、异步读写编排和 HostBridge 响应统一收口在 `apps/desktop-shell/src-tauri/src/host_bridge/files.rs`;`dispatch.rs` 只负责按 method 委托 `export_desktop_host_bridge_*_file(...)` / `import_desktop_host_bridge_*_file(...)`。桌面壳配置检查会拒绝分发层直接调用 `.dialog()`、`blocking_save_file` / `blocking_pick_file`、文件 payload helper 或落盘 helper,避免文件访问边界重新散落。 - 2026-06-19 桌面壳外链打开 helper 共用:Tauri WebView 外域拦截和 HostBridge `app.openExternalUrl` 都必须复用 `open_normalized_desktop_external_url` 执行系统外链打开动作;HostBridge 分支仍先用 `normalize_external_url` 保留 payload 错误语义并把 opener 错误回传给 H5,WebView 拦截保持 best-effort 静默处理。桌面壳配置检查会拒绝 `dispatch.rs` 直接调用 `app.opener().open_url` 绕过该 helper,避免两条离壳路径漂移。