收口移动剪贴板边界

将 Expo 剪贴板读写收口到 clipboard 模块

让 HostBridge dispatch 只负责剪贴板 payload 分发

同步原生壳结构门禁和架构文档清单
This commit is contained in:
2026-06-19 18:04:58 +08:00
parent 5db17d1a12
commit 2dcc2fa634
7 changed files with 66 additions and 30 deletions
+19 -3
View File
@@ -12,6 +12,8 @@ const qrScannerOverlayPath = new URL('../src/shell/QrScannerOverlay.tsx', import
const qrScannerOverlaySource = fs.readFileSync(qrScannerOverlayPath, 'utf8');
const bridgePath = new URL('../src/host-bridge/bridge.ts', import.meta.url);
const bridgeSource = fs.readFileSync(bridgePath, 'utf8');
const clipboardPath = new URL('../src/host-bridge/clipboard.ts', import.meta.url);
const clipboardSource = fs.readFileSync(clipboardPath, 'utf8');
const dispatchPath = new URL('../src/host-bridge/dispatch.ts', import.meta.url);
const dispatchSource = fs.readFileSync(dispatchPath, 'utf8');
const notificationsPath = new URL('../src/host-bridge/notifications.ts', import.meta.url);
@@ -73,6 +75,7 @@ const requiredMobileShellSourceModules = [
'env.d.ts',
'host-bridge/bridge.ts',
'host-bridge/capabilities.ts',
'host-bridge/clipboard.ts',
'host-bridge/dispatch.ts',
'host-bridge/files.ts',
'host-bridge/notifications.ts',
@@ -1619,13 +1622,26 @@ if (
);
}
if (
!hostBridgeSource.includes(
!clipboardSource.includes(
'const clipboardText = normalizeHostBridgeClipboardText(',
) ||
!hostBridgeSource.includes('Clipboard.setStringAsync(clipboardText.text)')
!clipboardSource.includes('Clipboard.setStringAsync(clipboardText.text)') ||
!clipboardSource.includes('await Clipboard.getStringAsync()')
) {
throw new Error(
'mobile shell clipboard.writeText must normalize text with the shared HostBridge clipboard boundary',
'mobile shell clipboard bridge must normalize text with the shared HostBridge clipboard boundary',
);
}
if (
dispatchSource.includes("from 'expo-clipboard'") ||
dispatchSource.includes('Clipboard.setStringAsync') ||
dispatchSource.includes('Clipboard.getStringAsync') ||
!dispatchSource.includes('writeMobileClipboardText(') ||
!dispatchSource.includes('readMobileClipboardText()')
) {
throw new Error(
'mobile shell dispatch must delegate clipboard IO to clipboard.ts',
);
}
@@ -0,0 +1,31 @@
import * as Clipboard from 'expo-clipboard';
import {
type ClipboardReadTextResult,
type HostBridgeError,
normalizeHostBridgeClipboardText,
} from '../../../../packages/shared/src/contracts/hostBridge';
export async function writeMobileClipboardText(rawText: unknown) {
const clipboardText = normalizeHostBridgeClipboardText(rawText);
if (!clipboardText) {
return false;
}
await Clipboard.setStringAsync(clipboardText.text);
return true;
}
export async function readMobileClipboardText(): Promise<ClipboardReadTextResult> {
const result = normalizeHostBridgeClipboardText(
await Clipboard.getStringAsync(),
);
if (!result) {
throw {
code: 'host_error',
message: 'clipboard text unavailable',
} satisfies HostBridgeError;
}
return result;
}
+10 -23
View File
@@ -1,4 +1,3 @@
import * as Clipboard from 'expo-clipboard';
import * as Haptics from 'expo-haptics';
import * as Linking from 'expo-linking';
import {
@@ -8,7 +7,6 @@ import {
} from 'react-native';
import {
type ClipboardReadTextResult,
type ClipboardWriteTextPayload,
type HapticsImpactPayload,
HOST_BRIDGE_BADGE_COUNT_MAX,
@@ -17,7 +15,6 @@ import {
type HostBridgeRequest,
type NavigateNativePagePayload,
normalizeHostBridgeBadgeCount,
normalizeHostBridgeClipboardText,
normalizeHostBridgeColorScheme,
normalizeHostBridgeExternalUrlPayload,
normalizeHostBridgeHapticsImpactStyle,
@@ -53,6 +50,10 @@ import { resetQrScannerForTest, scanQrCode } from './scanner';
import { openShare } from './share';
import { buildMobileShellUrl } from '../shell/url';
import { showMobileLocalNotification } from './notifications';
import {
readMobileClipboardText,
writeMobileClipboardText,
} from './clipboard';
let currentShareTarget: unknown = null;
let navigation: MobileHostBridgeNavigation | null = null;
@@ -82,31 +83,17 @@ async function openExternalUrl(payload: unknown) {
}
async function writeClipboard(payload: unknown) {
const clipboardText = normalizeHostBridgeClipboardText(
(payload as ClipboardWriteTextPayload | undefined)?.text,
);
if (!clipboardText) {
if (
!(await writeMobileClipboardText(
(payload as ClipboardWriteTextPayload | undefined)?.text,
))
) {
throw invalidRequest('text is required');
}
await Clipboard.setStringAsync(clipboardText.text);
return true;
}
async function readClipboard(): Promise<ClipboardReadTextResult> {
const result = normalizeHostBridgeClipboardText(
await Clipboard.getStringAsync(),
);
if (!result) {
throw {
code: 'host_error',
message: 'clipboard text unavailable',
} satisfies HostBridgeError;
}
return result;
}
async function runHaptics(payload: unknown) {
const style = normalizeHostBridgeHapticsImpactStyle(
(payload as HapticsImpactPayload | undefined)?.style,
@@ -227,7 +214,7 @@ export async function dispatchMobileHostBridgeRequest(
case 'clipboard.writeText':
return ok(request, await writeClipboard(request.payload));
case 'clipboard.readText':
return ok(request, await readClipboard());
return ok(request, await readMobileClipboardText());
case 'file.exportText':
return ok(request, await exportTextFile(request.payload));
case 'file.importText':