收口原生剪贴板桥接边界
将 Expo 剪贴板 HostBridge 读写收口到 clipboard 模块 将 Tauri 剪贴板 HostBridge 响应收口到 clipboard 模块 同步原生壳结构门禁、架构文档和共享记忆
This commit is contained in:
@@ -1719,6 +1719,30 @@ for (const snippet of [
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
!desktopHostBridgeDispatchSource.includes(
|
||||
'write_desktop_host_bridge_clipboard_text(&app, &request)',
|
||||
) ||
|
||||
!desktopHostBridgeDispatchSource.includes(
|
||||
'read_desktop_host_bridge_clipboard_text(&app, &request)',
|
||||
) ||
|
||||
desktopHostBridgeDispatchSource.includes('required_string_payload(&request, "text")') ||
|
||||
desktopHostBridgeDispatchSource.includes('read_desktop_clipboard_text(&app)')
|
||||
) {
|
||||
throw new Error('desktop shell clipboard HostBridge methods must delegate to clipboard module');
|
||||
}
|
||||
for (const snippet of [
|
||||
'write_desktop_clipboard_text(app, text)',
|
||||
'read_desktop_clipboard_text(app)',
|
||||
'normalize_clipboard_text(&text)',
|
||||
'required_string_payload(request, "text")',
|
||||
'"text": text',
|
||||
]) {
|
||||
if (!desktopHostBridgeClipboardSource.includes(snippet)) {
|
||||
throw new Error(`desktop shell clipboard module is missing ${snippet}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (config.build?.frontendDist !== '../../../dist') {
|
||||
throw new Error('desktop shell must package the root H5 dist');
|
||||
}
|
||||
@@ -1986,9 +2010,8 @@ const requiredRustHostSnippets = [
|
||||
'"file.exportAudio"',
|
||||
'"file.imageDropped"',
|
||||
'"notification.showLocal"',
|
||||
'write_desktop_clipboard_text(&app, text)',
|
||||
'read_desktop_clipboard_text(&app)',
|
||||
'normalize_clipboard_text(&text)',
|
||||
'write_desktop_host_bridge_clipboard_text(&app, &request)',
|
||||
'read_desktop_host_bridge_clipboard_text(&app, &request)',
|
||||
'tauri_plugin_dialog::init()',
|
||||
'tauri_plugin_notification::init()',
|
||||
'tauri_plugin_notification::{NotificationExt, PermissionState}',
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
use tauri_plugin_clipboard_manager::ClipboardExt;
|
||||
|
||||
use crate::host_bridge::protocol::{
|
||||
failed, ok, required_string_payload, HostBridgeRequest, HostBridgeResponse,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
pub(crate) const CLIPBOARD_TEXT_MAX_LENGTH: usize = 100000;
|
||||
|
||||
pub(crate) fn normalize_clipboard_text(text: &str) -> String {
|
||||
@@ -22,6 +27,36 @@ pub(crate) fn read_desktop_clipboard_text(app: &tauri::AppHandle) -> Result<Stri
|
||||
.map_err(|error| error.to_string())
|
||||
}
|
||||
|
||||
pub(crate) fn write_desktop_host_bridge_clipboard_text(
|
||||
app: &tauri::AppHandle,
|
||||
request: &HostBridgeRequest,
|
||||
) -> HostBridgeResponse {
|
||||
let text = match required_string_payload(request, "text") {
|
||||
Ok(text) => text,
|
||||
Err(response) => return response,
|
||||
};
|
||||
|
||||
match write_desktop_clipboard_text(app, text) {
|
||||
Ok(()) => ok(request.id.clone(), json!(true)),
|
||||
Err(error) => failed(request.id.clone(), "host_error", error),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn read_desktop_host_bridge_clipboard_text(
|
||||
app: &tauri::AppHandle,
|
||||
request: &HostBridgeRequest,
|
||||
) -> HostBridgeResponse {
|
||||
match read_desktop_clipboard_text(app) {
|
||||
Ok(text) => ok(
|
||||
request.id.clone(),
|
||||
json!({
|
||||
"text": text,
|
||||
}),
|
||||
),
|
||||
Err(error) => failed(request.id.clone(), "host_error", error),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -34,4 +69,22 @@ mod tests {
|
||||
CLIPBOARD_TEXT_MAX_LENGTH
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clipboard_read_response_reports_contract_shape() {
|
||||
let response = ok(
|
||||
"clipboard-read".to_string(),
|
||||
json!({
|
||||
"text": normalize_clipboard_text("邀请码 GEN-1")
|
||||
}),
|
||||
);
|
||||
|
||||
assert!(response.ok);
|
||||
assert_eq!(
|
||||
response.result.expect("clipboard result"),
|
||||
json!({
|
||||
"text": "邀请码 GEN-1"
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
use crate::host_bridge::appearance::desktop_appearance_color_scheme;
|
||||
use crate::host_bridge::badge::set_desktop_app_badge_count;
|
||||
use crate::host_bridge::capabilities::capabilities;
|
||||
use crate::host_bridge::clipboard::{read_desktop_clipboard_text, write_desktop_clipboard_text};
|
||||
use crate::host_bridge::clipboard::{
|
||||
read_desktop_host_bridge_clipboard_text, write_desktop_clipboard_text,
|
||||
write_desktop_host_bridge_clipboard_text,
|
||||
};
|
||||
use crate::host_bridge::files::{
|
||||
export_audio_payload, export_image_payload, export_text_payload, import_audio_file_payload,
|
||||
import_document_file_payload, import_image_file_payload, import_text_file_payload,
|
||||
@@ -68,32 +71,12 @@ pub(super) async fn execute_host_bridge_request(
|
||||
}
|
||||
|
||||
match request.method.as_str() {
|
||||
"app.openExternalUrl" => {
|
||||
open_desktop_host_bridge_external_url(&app, &request)
|
||||
}
|
||||
"app.openExternalUrl" => open_desktop_host_bridge_external_url(&app, &request),
|
||||
"appearance.getColorScheme" => desktop_appearance_color_scheme(&app, &request),
|
||||
"navigation.openNativePage" => open_desktop_host_bridge_native_page(&app, &request),
|
||||
"app.reloadWebView" => reload_desktop_host_bridge_webview(&app, &request),
|
||||
"clipboard.writeText" => {
|
||||
let text = match required_string_payload(&request, "text") {
|
||||
Ok(text) => text,
|
||||
Err(response) => return response,
|
||||
};
|
||||
|
||||
match write_desktop_clipboard_text(&app, text) {
|
||||
Ok(()) => ok(request.id, json!(true)),
|
||||
Err(error) => failed(request.id, "host_error", error),
|
||||
}
|
||||
}
|
||||
"clipboard.readText" => match read_desktop_clipboard_text(&app) {
|
||||
Ok(text) => ok(
|
||||
request.id,
|
||||
json!({
|
||||
"text": text,
|
||||
}),
|
||||
),
|
||||
Err(error) => failed(request.id, "host_error", error),
|
||||
},
|
||||
"clipboard.writeText" => write_desktop_host_bridge_clipboard_text(&app, &request),
|
||||
"clipboard.readText" => read_desktop_host_bridge_clipboard_text(&app, &request),
|
||||
"file.exportText" => {
|
||||
let (file_name, content) = match export_text_payload(&request) {
|
||||
Ok(payload) => payload,
|
||||
@@ -311,12 +294,10 @@ pub(super) async fn execute_host_bridge_request(
|
||||
Ok(()) => ok(request.id, json!(true)),
|
||||
Err(response) => response,
|
||||
},
|
||||
"network.status" => {
|
||||
match resolve_desktop_host_bridge_network_status().await {
|
||||
Ok(status) => ok(request.id, status),
|
||||
Err(error) => failed(request.id, "host_error", error.to_string()),
|
||||
}
|
||||
}
|
||||
"network.status" => match resolve_desktop_host_bridge_network_status().await {
|
||||
Ok(status) => ok(request.id, status),
|
||||
Err(error) => failed(request.id, "host_error", error.to_string()),
|
||||
},
|
||||
"notification.showLocal" => match show_desktop_local_notification(&app, &request) {
|
||||
Ok(()) => ok(request.id, json!(true)),
|
||||
Err(response) => response,
|
||||
@@ -491,5 +472,4 @@ mod tests {
|
||||
80
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1666,13 +1666,25 @@ if (
|
||||
dispatchSource.includes("from 'expo-clipboard'") ||
|
||||
dispatchSource.includes('Clipboard.setStringAsync') ||
|
||||
dispatchSource.includes('Clipboard.getStringAsync') ||
|
||||
!dispatchSource.includes('writeMobileClipboardText(') ||
|
||||
!dispatchSource.includes('readMobileClipboardText()')
|
||||
dispatchSource.includes('ClipboardWriteTextPayload') ||
|
||||
!dispatchSource.includes('writeMobileHostBridgeClipboardText(request.payload)') ||
|
||||
!dispatchSource.includes('readMobileHostBridgeClipboardText()')
|
||||
) {
|
||||
throw new Error(
|
||||
'mobile shell dispatch must delegate clipboard IO to clipboard.ts',
|
||||
);
|
||||
}
|
||||
for (const snippet of [
|
||||
'writeMobileHostBridgeClipboardText',
|
||||
'type ClipboardWriteTextPayload',
|
||||
'writeMobileClipboardText(text)',
|
||||
'readMobileHostBridgeClipboardText',
|
||||
'readMobileClipboardText()',
|
||||
]) {
|
||||
if (!clipboardSource.includes(snippet)) {
|
||||
throw new Error(`mobile shell clipboard module is missing ${snippet}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
!hapticsSource.includes('normalizeHostBridgeHapticsImpactStyle(rawStyle)') ||
|
||||
|
||||
@@ -2,9 +2,11 @@ import * as Clipboard from 'expo-clipboard';
|
||||
|
||||
import {
|
||||
type ClipboardReadTextResult,
|
||||
type ClipboardWriteTextPayload,
|
||||
type HostBridgeError,
|
||||
normalizeHostBridgeClipboardText,
|
||||
} from '../../../../packages/shared/src/contracts/hostBridge';
|
||||
import { invalidRequest } from './protocol';
|
||||
|
||||
export async function writeMobileClipboardText(rawText: unknown) {
|
||||
const clipboardText = normalizeHostBridgeClipboardText(rawText);
|
||||
@@ -16,6 +18,19 @@ export async function writeMobileClipboardText(rawText: unknown) {
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function writeMobileHostBridgeClipboardText(payload: unknown) {
|
||||
const text = (payload as ClipboardWriteTextPayload | undefined)?.text;
|
||||
if (!(await writeMobileClipboardText(text))) {
|
||||
throw invalidRequest('text is required');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function readMobileHostBridgeClipboardText() {
|
||||
return readMobileClipboardText();
|
||||
}
|
||||
|
||||
export async function readMobileClipboardText(): Promise<ClipboardReadTextResult> {
|
||||
const result = normalizeHostBridgeClipboardText(
|
||||
await Clipboard.getStringAsync(),
|
||||
|
||||
@@ -2,7 +2,6 @@ import * as Linking from 'expo-linking';
|
||||
import { Platform } from 'react-native';
|
||||
|
||||
import {
|
||||
type ClipboardWriteTextPayload,
|
||||
type HapticsImpactPayload,
|
||||
HOST_BRIDGE_VERSION,
|
||||
type HostBridgeRequest,
|
||||
@@ -31,8 +30,8 @@ import { resetQrScannerForTest, scanQrCode } from './scanner';
|
||||
import { openShare } from './share';
|
||||
import { showMobileLocalNotification } from './notifications';
|
||||
import {
|
||||
readMobileClipboardText,
|
||||
writeMobileClipboardText,
|
||||
readMobileHostBridgeClipboardText,
|
||||
writeMobileHostBridgeClipboardText,
|
||||
} from './clipboard';
|
||||
import { runMobileHapticsImpact } from './haptics';
|
||||
import { setMobileAppBadgeCount } from './badge';
|
||||
@@ -53,18 +52,6 @@ export function configureMobileHostBridgeNavigation(
|
||||
navigation = nextNavigation;
|
||||
}
|
||||
|
||||
async function writeClipboard(payload: unknown) {
|
||||
if (
|
||||
!(await writeMobileClipboardText(
|
||||
(payload as ClipboardWriteTextPayload | undefined)?.text,
|
||||
))
|
||||
) {
|
||||
throw invalidRequest('text is required');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async function runHaptics(payload: unknown) {
|
||||
if (
|
||||
!(await runMobileHapticsImpact(
|
||||
@@ -119,9 +106,9 @@ export async function dispatchMobileHostBridgeRequest(
|
||||
case 'network.status':
|
||||
return ok(request, await getMobileHostBridgeNetworkStatus());
|
||||
case 'clipboard.writeText':
|
||||
return ok(request, await writeClipboard(request.payload));
|
||||
return ok(request, await writeMobileHostBridgeClipboardText(request.payload));
|
||||
case 'clipboard.readText':
|
||||
return ok(request, await readMobileClipboardText());
|
||||
return ok(request, await readMobileHostBridgeClipboardText());
|
||||
case 'file.exportText':
|
||||
return ok(request, await exportTextFile(request.payload));
|
||||
case 'file.importText':
|
||||
|
||||
Reference in New Issue
Block a user