From 17c1dac76d72e69b39d21b2a841002c834c4d3b3 Mon Sep 17 00:00:00 2001 From: kdletters Date: Sat, 20 Jun 2026 04:55:34 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=B6=E5=8F=A3=E5=8E=9F=E7=94=9F=E5=A3=B3?= =?UTF-8?q?=E5=89=AA=E8=B4=B4=E6=9D=BF=E8=AF=BB=E5=8F=96=E8=BE=B9=E7=95=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit H5 HostBridge 读取剪贴板时将畸形原生返回统一收口为 false 补充原生 App 剪贴板读取归一化回归测试 原生壳门禁反查 clipboard.readText 返回值必须经过共享剪贴板边界 --- scripts/check-native-shells.mjs | 15 +++++++++ src/services/host-bridge/hostBridge.test.ts | 37 +++++++++++++++++++++ src/services/host-bridge/hostBridge.ts | 3 +- 3 files changed, 54 insertions(+), 1 deletion(-) diff --git a/scripts/check-native-shells.mjs b/scripts/check-native-shells.mjs index 70dab7944..dfab2a1aa 100644 --- a/scripts/check-native-shells.mjs +++ b/scripts/check-native-shells.mjs @@ -1165,6 +1165,21 @@ function assertH5HostBridgePayloadBoundaries() { 'H5 HostBridge facade must normalize clipboard.writeText payloads with the shared clipboard boundary', ); } + if ( + !h5HostBridgeSource.includes( + 'const clipboardText = normalizeHostBridgeClipboardText(', + ) || + !h5HostBridgeSource.includes( + "await requestNativeAppHostBridge(\n 'clipboard.readText',", + ) || + !h5HostBridgeSource.includes( + 'return clipboardText ?? false;', + ) + ) { + throw new Error( + 'H5 HostBridge facade must normalize clipboard.readText results with the shared clipboard boundary', + ); + } if ( !h5HostBridgeSource.includes( 'const style = normalizeHostBridgeHapticsImpactStyle(params.style);', diff --git a/src/services/host-bridge/hostBridge.test.ts b/src/services/host-bridge/hostBridge.test.ts index fb1ef6a8f..1a2527138 100644 --- a/src/services/host-bridge/hostBridge.test.ts +++ b/src/services/host-bridge/hostBridge.test.ts @@ -4,6 +4,7 @@ import { afterEach, describe, expect, test, vi } from 'vitest'; import type { HostBridgeCapability } from '../../../packages/shared/src/contracts/hostBridge'; import { + HOST_BRIDGE_CLIPBOARD_TEXT_MAX_LENGTH, HOST_BRIDGE_RUNTIME_REFRESH_TIMEOUT_MS, HOST_BRIDGE_SCANNER_TIMEOUT_MS, HOST_BRIDGE_USER_INTERACTION_TIMEOUT_MS, @@ -1485,6 +1486,42 @@ describe('hostBridge', () => { }); }); + test('原生 App HostBridge 读取剪贴板结果按共享清单归一化', async () => { + const oversizedText = 'a'.repeat(HOST_BRIDGE_CLIPBOARD_TEXT_MAX_LENGTH + 5); + const invoke = vi.fn( + async (_command: string, args?: Record) => { + const request = (args as { request: { id: string } }).request; + return { + bridge: 'GenarrativeHostBridge', + version: 1, + id: request.id, + ok: true, + result: { + text: + invoke.mock.calls.length === 1 + ? 42 + : oversizedText, + }, + }; + }, + ); + window.__TAURI__ = { + core: { invoke: asTauriInvoke(invoke) }, + }; + window.history.replaceState( + null, + '', + nativeAppPath(['clipboard.readText']), + ); + + await expect(readHostClipboardText()).resolves.toBe(false); + await expect(readHostClipboardText()).resolves.toEqual({ + text: oversizedText.slice(0, HOST_BRIDGE_CLIPBOARD_TEXT_MAX_LENGTH), + }); + + expect(invoke).toHaveBeenCalledTimes(2); + }); + test('原生 App 宿主不支持能力时回退到 H5 路径', async () => { window.history.replaceState(null, '', nativeAppPath()); window.__TAURI__ = { diff --git a/src/services/host-bridge/hostBridge.ts b/src/services/host-bridge/hostBridge.ts index 59c9fc2d8..ab698d07d 100644 --- a/src/services/host-bridge/hostBridge.ts +++ b/src/services/host-bridge/hostBridge.ts @@ -889,13 +889,14 @@ export async function readHostClipboardText() { } try { - return normalizeHostBridgeClipboardText( + const clipboardText = normalizeHostBridgeClipboardText( ( await requestNativeAppHostBridge( 'clipboard.readText', ) )?.text, ); + return clipboardText ?? false; } catch { return false; }