Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 34 additions & 69 deletions src/main/core/superPanelManager.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,21 @@
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'
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'

// 超级面板窗口尺寸
const SUPER_PANEL_WIDTH = 250
const SUPER_PANEL_HEIGHT = 400

// 模拟复制后等待剪贴板更新的时间
const CLIPBOARD_WAIT_MS = 50
// 模拟复制后等待剪贴板监听更新的时间窗口
const CLIPBOARD_WAIT_MS = 180

// 剪贴板内容类型
interface ClipboardContent {
Expand Down Expand Up @@ -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
}

/**
Expand Down Expand Up @@ -261,47 +246,27 @@ class SuperPanelManager {

private async onMouseTriggerAsync(cursorPoint: { x: number; y: number }): Promise<void> {
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)
Expand Down
27 changes: 19 additions & 8 deletions src/main/managers/clipboardManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

// 文件项
Expand Down Expand Up @@ -94,6 +95,7 @@ class ClipboardManager {

// 记录最后一次复制的内容(统一管理)
private lastCopiedContent: LastCopiedContent | null = null
private lastCopiedSequence = 0

// 临时取消剪贴板监听的计时器(防止 paste API 写入剪贴板时自我触发)
private cancelWatchTimeout: ReturnType<typeof setTimeout> | null = null
Expand Down Expand Up @@ -262,7 +264,8 @@ class ClipboardManager {
this.lastCopiedContent = {
type: 'file',
data: files, // 存储完整的 FileItem 对象
timestamp: Date.now()
timestamp: Date.now(),
sequence: ++this.lastCopiedSequence
}

// 生成 hash(基于所有文件路径)
Expand Down Expand Up @@ -308,7 +311,8 @@ class ClipboardManager {
this.lastCopiedContent = {
type: 'image',
data: base64,
timestamp: Date.now()
timestamp: Date.now(),
sequence: ++this.lastCopiedSequence
}

// 检查图片大小
Expand Down Expand Up @@ -366,7 +370,8 @@ class ClipboardManager {
this.lastCopiedContent = {
type: 'text',
data: text,
timestamp: Date.now()
timestamp: Date.now(),
sequence: ++this.lastCopiedSequence
}

return {
Expand Down Expand Up @@ -792,6 +797,11 @@ class ClipboardManager {
}
}

// 获取最后一次复制内容的序号
public getLastCopiedSequence(): number {
return this.lastCopiedContent?.sequence ?? 0
}

// 获取最后一次复制的文本(在指定时间内)- 兼容旧 API
public async getLastCopiedText(timeLimit: number): Promise<string | null> {
const content = await this.getLastCopiedContent(timeLimit)
Expand All @@ -806,14 +816,15 @@ class ClipboardManager {

// 获取最后复制的内容(统一接口)
public async getLastCopiedContent(
timeLimit?: number // 可选:时间限制(毫秒),不传或传 0 表示无时间限制
timeLimit?: number, // 可选:时间限制(毫秒),不传或传 0 表示无时间限制
minSequence?: number // 可选:仅接受晚于该序号的新复制内容
): Promise<LastCopiedContent | null> {
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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

此处使用 Math.max 可能会引入竞态条件(Race Condition)。如果在第 823 行的检查之后到第 827 行的赋值之前,剪贴板内容发生了更新(导致 sequence 增加),initialSequence 将会被设置为更新后的序号。这会导致随后的循环开始等待一个更晚的更新,从而错过刚刚发生的这次更新,最终可能导致超时。建议在提供 minSequence 时直接使用它作为基准,或者在未提供时使用当前序号。

Suggested change
const initialSequence = Math.max(this.lastCopiedContent?.sequence ?? 0, minSequence ?? 0)
const initialSequence = minSequence !== undefined ? minSequence : (this.lastCopiedContent?.sequence ?? 0)

const waitMs =
timeLimit && timeLimit > 0
? Math.min(timeLimit, CLIPBOARD_READY_WAIT_MS)
Expand All @@ -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
}
}
Expand Down