收口移动剪贴板边界
将 Expo 剪贴板读写收口到 clipboard 模块 让 HostBridge dispatch 只负责剪贴板 payload 分发 同步原生壳结构门禁和架构文档清单
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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':
|
||||
|
||||
@@ -80,6 +80,7 @@
|
||||
- 2026-06-18 草稿生成完成 / 失败通知:平台壳层的 `markDraftReady` / `markDraftFailed` 统一收口会在原生壳声明 `notification.showLocal` 时请求即时本地通知;通知 payload 只包含生成完成 / 失败标题和草稿来源正文,按草稿来源去重,同一草稿重新进入生成中后才允许再次通知。该能力不替代现有完成 / 错误弹窗、作品架红点、队列概览或后端状态回读,通知失败不阻断主流程。根级原生壳门禁必须覆盖平台壳同步层通过真实 HostBridge transport 发出 `notification.showLocal`,避免只测模型文案。
|
||||
- 2026-06-19 草稿生成 HostBridge 消费门禁:`PlatformEntryFlowShellImpl` 只负责派生草稿通知和未读数量,实际宿主同步经 `platformHostBridgeSync.ts` 调用 `showHostLocalNotification` / `setHostAppBadgeCount`;`npm run check:native-shells` 必须运行该同步层的真实 Tauri transport 测试,并继续覆盖通知模型、未读计数模型、音频导入和文档导入等 H5 HostBridge 消费测试。
|
||||
- 2026-06-18 剪贴板读取能力:新增 `clipboard.readText` HostBridge capability,H5 只能读取纯文本结果,契约限制返回文本最多 100000 字符;Expo 壳通过 `expo-clipboard` 读取系统剪贴板文本,Tauri 壳通过 Rust 侧 `tauri-plugin-clipboard-manager` 读取文本且不开放插件 JS guest API。该能力不读取图片、HTML、文件列表或剪贴板监听事件,宿主未声明或读取失败时由 H5 视作失败并保留原流程。
|
||||
- 2026-06-19 移动壳剪贴板边界:Expo `clipboard.writeText` / `clipboard.readText` 的系统剪贴板读写与共享 100000 字符归一统一收口在 `apps/mobile-shell/src/host-bridge/clipboard.ts`;`dispatch.ts` 只负责从 HostBridge payload 取 `text` 并调用 `writeMobileClipboardText(...)` / `readMobileClipboardText()`,不得直接导入 `expo-clipboard` 或调用 `Clipboard.setStringAsync` / `Clipboard.getStringAsync`。移动壳配置检查会覆盖该模块结构、共享文本边界和 dispatch 委托关系,避免剪贴板能力散落到分发层。
|
||||
- 2026-06-18 文本文件导入能力:新增 `file.importText` HostBridge capability,H5 统一通过 `importHostTextFile()` 读取宿主返回的纯文本内容;Expo 壳通过 `expo-document-picker` 打开系统文档选择器,Tauri 壳通过系统文件选择框读取真实文本文件。两端只接受 `text/plain`、`text/markdown`、`text/csv`、`application/json` 或对应扩展名,单次不超过 5 MiB,成功只返回清洗后的文件名、MIME、UTF-8 文本内容和字节数,不暴露设备 URI / 本机绝对路径,也不开放通用文件系统。
|
||||
- 2026-06-19 文档文件导入能力:新增 `file.importDocument` HostBridge capability,作为创作 Agent 工作台优先导入路径;Expo 壳通过 DocumentPicker、Tauri 壳通过系统文件选择框读取文本类文档或 DOCX 副本。两端只接受文本 MIME / DOCX MIME 或对应扩展名,单次不超过 5 MiB,成功只返回清洗后的文件名、MIME、base64 内容和字节数,不暴露设备 URI、本机绝对路径,也不开放通用文件系统;H5 把返回内容转换成浏览器 `File` 后继续走后端 `/api/runtime/creation-agent/document-inputs/parse`,不在前端解析 DOCX。旧壳只声明 `file.importText` 时继续使用文本导入兜底。
|
||||
- 2026-06-18 Tauri 系统托盘:桌面壳启用真实 OS 托盘并复用品牌图标,托盘菜单只执行显示主窗口、刷新主窗口和退出应用,左键点击托盘图标恢复并聚焦主窗口;该能力归桌面壳自身,不进入 HostBridge capability,不向 H5 暴露托盘、菜单、shell 或任意窗口控制 API。托盘注册成功时主窗口关闭按钮只隐藏到托盘,必须通过托盘“退出”结束应用;托盘注册失败不得阻断主窗口启动,也不得拦截关闭,避免窗口消失后无法恢复。`check:native-shells` 和 Tauri cargo test 覆盖托盘配置、菜单动作映射和关闭策略。
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -252,6 +252,7 @@ const expectedMobileHostBridgeFiles = [
|
||||
'bridge.test.ts',
|
||||
'bridge.ts',
|
||||
'capabilities.ts',
|
||||
'clipboard.ts',
|
||||
'dispatch.ts',
|
||||
'files.ts',
|
||||
'notifications.ts',
|
||||
|
||||
Reference in New Issue
Block a user