From 2f37a143f10c81fc2fcfea0e29aa2f385f7ff8e1 Mon Sep 17 00:00:00 2001 From: kdletters Date: Fri, 19 Jun 2026 23:39:00 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=B6=E5=8F=A3=E7=A7=BB=E5=8A=A8=E5=A4=96?= =?UTF-8?q?=E9=93=BE=E6=A1=A5=E6=8E=A5=E7=B3=BB=E7=BB=9F=E8=B0=83=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将 Expo app.openExternalUrl 的 Linking 调用收口到 navigation 模块 让移动 dispatch 只委托外链请求载荷 同步移动壳配置门禁和共享记忆 --- apps/mobile-shell/scripts/check-config.mjs | 11 +++++++---- apps/mobile-shell/src/host-bridge/dispatch.ts | 7 +------ apps/mobile-shell/src/host-bridge/navigation.ts | 5 +++-- docs/project-memory/shared-memory/decision-log.md | 2 +- 4 files changed, 12 insertions(+), 13 deletions(-) diff --git a/apps/mobile-shell/scripts/check-config.mjs b/apps/mobile-shell/scripts/check-config.mjs index 62b75e182..40fc0fbd8 100644 --- a/apps/mobile-shell/scripts/check-config.mjs +++ b/apps/mobile-shell/scripts/check-config.mjs @@ -1668,7 +1668,7 @@ if ( 'const externalUrlPayload = normalizeHostBridgeExternalUrlPayload(', ) || !hostBridgeNavigationSource.includes( - 'openMobileShellExternalNavigation(navigator, externalUrlPayload.url)', + 'openMobileShellExternalNavigation(Linking, externalUrlPayload.url)', ) ) { throw new Error( @@ -1928,9 +1928,11 @@ for (const snippet of [ } if ( - !dispatchSource.includes('openMobileHostBridgeExternalUrl(Linking, request.payload)') || + !dispatchSource.includes('openMobileHostBridgeExternalUrl(request.payload)') || !dispatchSource.includes('openMobileHostBridgeNativePage(navigation, request.payload)') || !dispatchSource.includes('reloadMobileHostBridgeWebView(navigation)') || + dispatchSource.includes("from 'expo-linking'") || + dispatchSource.includes('Linking.') || dispatchSource.includes('openMobileShellExternalNavigation') || dispatchSource.includes('resolveMobileShellWebViewUrl') || dispatchSource.includes('normalizeHostBridgeExternalUrlPayload') || @@ -1940,7 +1942,8 @@ if ( } for (const snippet of [ 'openMobileHostBridgeExternalUrl', - 'openMobileShellExternalNavigation(navigator, externalUrlPayload.url)', + "import * as Linking from 'expo-linking'", + 'openMobileShellExternalNavigation(Linking, externalUrlPayload.url)', 'normalizeHostBridgeExternalUrlPayload', 'openMobileHostBridgeNativePage', 'resolveMobileShellWebViewUrl', @@ -1952,7 +1955,7 @@ for (const snippet of [ } } -if (!hostBridgeNavigationSource.includes('openMobileShellExternalNavigation(navigator, externalUrlPayload.url)')) { +if (!hostBridgeNavigationSource.includes('openMobileShellExternalNavigation(Linking, externalUrlPayload.url)')) { throw new Error( 'mobile shell HostBridge external URL flow must use the shared external navigation helper', ); diff --git a/apps/mobile-shell/src/host-bridge/dispatch.ts b/apps/mobile-shell/src/host-bridge/dispatch.ts index ea11b5e51..5f5c057be 100644 --- a/apps/mobile-shell/src/host-bridge/dispatch.ts +++ b/apps/mobile-shell/src/host-bridge/dispatch.ts @@ -1,5 +1,3 @@ -import * as Linking from 'expo-linking'; - import { type HostBridgeRequest } from '../../../../packages/shared/src/contracts/hostBridge'; import { captureImageFile, @@ -56,10 +54,7 @@ export async function dispatchMobileHostBridgeRequest( case 'appearance.getColorScheme': return ok(request, getMobileAppearanceColorScheme()); case 'app.openExternalUrl': - return ok( - request, - await openMobileHostBridgeExternalUrl(Linking, request.payload), - ); + return ok(request, await openMobileHostBridgeExternalUrl(request.payload)); case 'app.reloadWebView': return ok(request, reloadMobileHostBridgeWebView(navigation)); case 'network.status': diff --git a/apps/mobile-shell/src/host-bridge/navigation.ts b/apps/mobile-shell/src/host-bridge/navigation.ts index adb22d7de..597d6434f 100644 --- a/apps/mobile-shell/src/host-bridge/navigation.ts +++ b/apps/mobile-shell/src/host-bridge/navigation.ts @@ -1,3 +1,5 @@ +import * as Linking from 'expo-linking'; + import { type HostBridgeError, type NavigateNativePagePayload, @@ -17,7 +19,6 @@ import { } from './protocol'; export async function openMobileHostBridgeExternalUrl( - navigator: MobileShellExternalNavigator, payload: unknown, ) { const externalUrlPayload = normalizeHostBridgeExternalUrlPayload( @@ -28,7 +29,7 @@ export async function openMobileHostBridgeExternalUrl( } if ( - !(await openMobileShellExternalNavigation(navigator, externalUrlPayload.url)) + !(await openMobileShellExternalNavigation(Linking, externalUrlPayload.url)) ) { throw { code: 'host_error', diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index d0fb7418d..77ce26f4c 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -26,7 +26,7 @@ - 2026-06-18 移动壳 WebView 导航收紧:Expo WebView 自身拦截外域导航时复用 HostBridge 外链协议白名单,只把 `http:`、`https:`、`mailto:`、`tel:` 交给 `Linking.openURL`,`javascript:`、`file:`、相对异常路径等危险目标直接阻断,避免离开同源主站后仍保留完整 HostBridge。 - 2026-06-19 移动壳 WebView 外链协议共源:`apps/mobile-shell/src/shell/navigation.ts` 的 WebView 外链离壳判断必须调用共享 `normalizeHostBridgeExternalUrl`,不得在 shell 层另写协议判断;`apps/mobile-shell/scripts/check-config.mjs` 会拒绝重新硬编码 `mailto:` / `tel:` / `javascript:` 等协议分支,`navigation.test.ts` 用 `HOST_BRIDGE_EXTERNAL_URL_PROTOCOLS` 反查当前允许协议。 - 2026-06-19 移动壳 WebView 外链打开收口:Expo WebView 外链拦截统一调用 `openMobileShellExternalNavigation(Linking, request.url)`,该 helper 先复用共享外链协议 normalizer,再调用 `canOpenURL` 确认系统可处理,最后才 `openURL`;系统不能打开或 URL 被拒绝时只阻断留壳,不伪造成功也不把危险协议交给系统。`ShellApp` 不再内联 `Linking.canOpenURL` / `Linking.openURL` Promise 链,移动壳配置检查和 `navigation.test.ts` 会覆盖该顺序。 -- 2026-06-19 移动壳外链打开 helper 共用:Expo WebView 外域拦截和 HostBridge `app.openExternalUrl` 都必须复用 `openMobileShellExternalNavigation` 执行系统外链打开动作;HostBridge 分支仍先调用 `normalizeHostBridgeExternalUrlPayload` 保留 payload 错误语义,但不再单独维护 `Linking.canOpenURL` / `Linking.openURL` 顺序。移动壳配置检查会拒绝 `app.openExternalUrl` 绕开该 helper,避免两条离壳路径漂移。 +- 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 模块,配置检查会拒绝分发层直接持有分享状态、生成分享文本或写入分享剪贴板结果。