From 2db23c9b11008f38a44ba83e88d57330630fdf7b Mon Sep 17 00:00:00 2001 From: pantao <980141374@qq.com> Date: Tue, 21 Apr 2026 14:12:57 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BD=BF=E7=94=A8=E5=BA=8F=E5=8F=B7?= =?UTF-8?q?=E6=9C=BA=E5=88=B6=E4=BF=AE=E5=A4=8D=E8=B6=85=E7=BA=A7=E9=9D=A2?= =?UTF-8?q?=E6=9D=BF=E5=89=AA=E8=B4=B4=E6=9D=BF=E8=AF=86=E5=88=AB=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/core/superPanelManager.ts | 103 +++++++++----------------- src/main/managers/clipboardManager.ts | 27 +++++-- 2 files changed, 53 insertions(+), 77 deletions(-) diff --git a/src/main/core/superPanelManager.ts b/src/main/core/superPanelManager.ts index 0131b6f2..5a3ead8a 100644 --- a/src/main/core/superPanelManager.ts +++ b/src/main/core/superPanelManager.ts @@ -1,5 +1,4 @@ -import { BrowserWindow, clipboard, ipcMain, screen } from 'electron' -import os from 'os' +import { BrowserWindow, ipcMain, screen } from 'electron' import path from 'path' import { is } from '@electron-toolkit/utils' import { MouseMonitor, WindowManager, type MouseMonitorResult } from './native/index.js' @@ -7,8 +6,7 @@ import { launchApp } from './commandLauncher/index.js' import databaseAPI from '../api/shared/database.js' import pluginsAPI from '../api/renderer/plugins.js' import windowManager from '../managers/windowManager.js' -import clipboardManager from '../managers/clipboardManager.js' -import { readClipboardFiles } from '../utils/clipboardFiles.js' +import clipboardManager, { type LastCopiedContent } from '../managers/clipboardManager.js' import { applyWindowMaterial, getDefaultWindowMaterial } from '../utils/windowUtils.js' import translationManager from './translationManager.js' @@ -16,8 +14,8 @@ import translationManager from './translationManager.js' const SUPER_PANEL_WIDTH = 250 const SUPER_PANEL_HEIGHT = 400 -// 模拟复制后等待剪贴板更新的时间 -const CLIPBOARD_WAIT_MS = 50 +// 模拟复制后等待剪贴板监听更新的时间窗口 +const CLIPBOARD_WAIT_MS = 180 // 剪贴板内容类型 interface ClipboardContent { @@ -190,41 +188,28 @@ class SuperPanelManager { } | null = null /** - * 读取剪贴板内容(支持文件、图片、文字三种类型) + * 将剪贴板管理器返回的数据转换为超级面板使用的结构 */ - private readClipboardContent(): ClipboardContent | null { - try { - // 优先检测文件 - if (os.platform() === 'darwin' || os.platform() === 'win32') { - try { - const files = readClipboardFiles() - if (Array.isArray(files) && files.length > 0) { - return { type: 'file', files } - } - } catch (error) { - console.error('[SuperPanel] 读取文件剪贴板失败:', error) - } - } - - // 检测图片 - const image = clipboard.readImage() - if (!image.isEmpty()) { - const buffer = image.toPNG() - const base64 = `data:image/png;base64,${buffer.toString('base64')}` - return { type: 'image', image: base64 } - } + private convertLastCopiedContent(content: LastCopiedContent | null): ClipboardContent | null { + if (!content) { + return null + } - // 检测文本 - const text = clipboard.readText() - if (text && text.trim() !== '') { - return { type: 'text', text } - } + if (content.type === 'text') { + return typeof content.data === 'string' && content.data.trim() !== '' + ? { type: 'text', text: content.data } + : null + } - return null - } catch (error) { - console.error('[SuperPanel] 读取剪贴板失败:', error) - return null + if (content.type === 'image') { + return typeof content.data === 'string' && content.data + ? { type: 'image', image: content.data } + : null } + + return Array.isArray(content.data) && content.data.length > 0 + ? { type: 'file', files: content.data } + : null } /** @@ -261,47 +246,27 @@ class SuperPanelManager { private async onMouseTriggerAsync(cursorPoint: { x: number; y: number }): Promise { try { - // 2. 记录当前剪贴板内容快照(用于对比是否有新内容) - const oldContent = this.readClipboardContent() - const oldClipboardText = clipboard.readText() + const lastSequence = clipboardManager.getLastCopiedSequence() - // 3. 等待鼠标按键释放 - // await new Promise((resolve) => setTimeout(resolve, 100)) - - // 4. 模拟复制(Cmd+C on macOS, Ctrl+C on Windows) + // 2. 模拟复制(Cmd+C on macOS, Ctrl+C on Windows) const modifier = process.platform === 'darwin' ? 'meta' : 'ctrl' WindowManager.simulateKeyboardTap('c', modifier) - // 5. 等待剪贴板更新 - await new Promise((resolve) => setTimeout(resolve, CLIPBOARD_WAIT_MS)) - - // 6. 读取新的剪贴板内容(支持文件/图片/文字) - const newContent = this.readClipboardContent() - const newClipboardText = clipboard.readText() - - // 7. 判断是否有新的复制内容 - let hasNewContent = false - if (newContent) { - if (newContent.type === 'text') { - hasNewContent = newClipboardText !== oldClipboardText && newClipboardText.trim() !== '' - } else if (newContent.type === 'file') { - // 文件:对比文件路径列表 - const oldPaths = oldContent?.files?.map((f) => f.path).join('|') || '' - const newPaths = newContent.files?.map((f) => f.path).join('|') || '' - hasNewContent = newPaths !== oldPaths && newPaths !== '' - } else if (newContent.type === 'image') { - // 图片:只要有图片就认为有新内容(无法精确对比) - hasNewContent = !oldContent || oldContent.type !== 'image' - } - } + // 3. 等待剪贴板监听捕获本次复制事件 + const lastCopiedContent = await clipboardManager.getLastCopiedContent( + CLIPBOARD_WAIT_MS, + lastSequence + ) + const newContent = this.convertLastCopiedContent(lastCopiedContent) + const hasNewContent = !!newContent - // 8. 保存当前剪贴板内容 + // 4. 保存当前剪贴板内容 this.currentClipboardContent = hasNewContent ? newContent : null - // 9. 显示超级面板窗口 + // 5. 显示超级面板窗口 this.showWindow(cursorPoint.x, cursorPoint.y) - // 10. 根据剪贴板内容决定模式 + // 6. 根据剪贴板内容决定模式 if (hasNewContent && newContent) { // 有新内容:发送搜索请求到主窗口(携带剪贴板类型和数据) this.requestSearch(newContent) diff --git a/src/main/managers/clipboardManager.ts b/src/main/managers/clipboardManager.ts index 8e73813b..4b72ce4a 100644 --- a/src/main/managers/clipboardManager.ts +++ b/src/main/managers/clipboardManager.ts @@ -19,10 +19,11 @@ import ClipboardMonitor, { WindowMonitor, WindowManager } from '../core/native' // 剪贴板类型 type ClipboardType = 'text' | 'image' | 'file' -type LastCopiedContent = { +export type LastCopiedContent = { type: 'text' | 'image' | 'file' data: string | FileItem[] timestamp: number + sequence: number } // 文件项 @@ -94,6 +95,7 @@ class ClipboardManager { // 记录最后一次复制的内容(统一管理) private lastCopiedContent: LastCopiedContent | null = null + private lastCopiedSequence = 0 // 临时取消剪贴板监听的计时器(防止 paste API 写入剪贴板时自我触发) private cancelWatchTimeout: ReturnType | null = null @@ -262,7 +264,8 @@ class ClipboardManager { this.lastCopiedContent = { type: 'file', data: files, // 存储完整的 FileItem 对象 - timestamp: Date.now() + timestamp: Date.now(), + sequence: ++this.lastCopiedSequence } // 生成 hash(基于所有文件路径) @@ -308,7 +311,8 @@ class ClipboardManager { this.lastCopiedContent = { type: 'image', data: base64, - timestamp: Date.now() + timestamp: Date.now(), + sequence: ++this.lastCopiedSequence } // 检查图片大小 @@ -366,7 +370,8 @@ class ClipboardManager { this.lastCopiedContent = { type: 'text', data: text, - timestamp: Date.now() + timestamp: Date.now(), + sequence: ++this.lastCopiedSequence } return { @@ -792,6 +797,11 @@ class ClipboardManager { } } + // 获取最后一次复制内容的序号 + public getLastCopiedSequence(): number { + return this.lastCopiedContent?.sequence ?? 0 + } + // 获取最后一次复制的文本(在指定时间内)- 兼容旧 API public async getLastCopiedText(timeLimit: number): Promise { const content = await this.getLastCopiedContent(timeLimit) @@ -806,14 +816,15 @@ class ClipboardManager { // 获取最后复制的内容(统一接口) public async getLastCopiedContent( - timeLimit?: number // 可选:时间限制(毫秒),不传或传 0 表示无时间限制 + timeLimit?: number, // 可选:时间限制(毫秒),不传或传 0 表示无时间限制 + minSequence?: number // 可选:仅接受晚于该序号的新复制内容 ): Promise { const cachedContent = this.getValidLastCopiedContent(timeLimit) - if (cachedContent) { + if (cachedContent && (!minSequence || cachedContent.sequence > minSequence)) { return cachedContent } - const initialTimestamp = this.lastCopiedContent?.timestamp ?? 0 + const initialSequence = Math.max(this.lastCopiedContent?.sequence ?? 0, minSequence ?? 0) const waitMs = timeLimit && timeLimit > 0 ? Math.min(timeLimit, CLIPBOARD_READY_WAIT_MS) @@ -824,7 +835,7 @@ class ClipboardManager { await sleep(CLIPBOARD_RETRY_INTERVAL_MS) const latestContent = this.getValidLastCopiedContent(timeLimit) - if (latestContent && latestContent.timestamp !== initialTimestamp) { + if (latestContent && latestContent.sequence > initialSequence) { return latestContent } }