diff --git a/src/main/api/renderer/settings.ts b/src/main/api/renderer/settings.ts index 0bfe794c..d09e560a 100644 --- a/src/main/api/renderer/settings.ts +++ b/src/main/api/renderer/settings.ts @@ -1,6 +1,6 @@ import { app, globalShortcut, ipcMain, nativeTheme } from 'electron' +import fs from 'fs' import type { PluginManager } from '../../managers/pluginManager' -import clipboardManager from '../../managers/clipboardManager.js' // 共享API(主程序和插件都能用) import { WindowManager as NativeWindowManager } from '../../core/native/index.js' @@ -13,10 +13,6 @@ import type { GlobalShortcutPreparation } from '../index' import api from '../index' import databaseAPI from '../shared/database' -const GLOBAL_SHORTCUT_COOLDOWN_MS = 180 -const KEY_RELEASE_WAIT_TIMEOUT_MS = 180 -const CLIPBOARD_COPY_WAIT_TIMEOUT_MS = 180 - /** * 快捷键触发时携带的文件输入 */ @@ -54,10 +50,9 @@ export class SettingsAPI { // 临时快捷键录制相关 private recordingShortcuts: string[] = [] - private lastGlobalShortcutTriggeredAt = new Map() - private globalShortcutKeyboardStateReleasers = new Map void>() // 全局快捷键配置映射(存储每个快捷键的 autoCopy 等配置) private globalShortcutConfigs: Map = new Map() + private globalShortcutKeyboardStateReleasers = new Map void>() private setupIPC(): void { // 主题 @@ -406,52 +401,38 @@ export class SettingsAPI { /** * 判断某个快捷键目标是否允许在阻断期内再次触发。 - * 同一 target 在 180ms 内只会放行一次。 + * 由于新的 native getSelectedContent() 方法不再需要等待,防抖逻辑已移除。 */ - private shouldTriggerGlobalShortcut(target: string): boolean { - const now = Date.now() - const lastTriggeredAt = this.lastGlobalShortcutTriggeredAt.get(target) ?? 0 - if (now - lastTriggeredAt < GLOBAL_SHORTCUT_COOLDOWN_MS) { - return false - } - - this.lastGlobalShortcutTriggeredAt.set(target, now) - for (const [cachedTarget, timestamp] of this.lastGlobalShortcutTriggeredAt.entries()) { - if (now - timestamp >= GLOBAL_SHORTCUT_COOLDOWN_MS) { - this.lastGlobalShortcutTriggeredAt.delete(cachedTarget) - } - } + private shouldTriggerGlobalShortcut(_target: string): boolean { return true } /** - * 获取当前选中文本并转换成快捷键启动上下文。 - * 会等待触发快捷键的按键全部弹起后再执行复制,避免修饰键残留改变复制组合键。 + * 获取当前选中内容并转换成快捷键启动上下文。 + * 使用 native getSelectedContent() 方法,自动处理按键释放和剪贴板暂停。 */ private async captureSelectedTextContext(): Promise { - console.log('[Settings] 开始捕获选中文本...') + console.log('[Settings] 开始捕获选中内容...') try { - console.log('[Settings] 等待按键释放...') - await doubleTapManager.waitForAllKeysReleased(KEY_RELEASE_WAIT_TIMEOUT_MS) - console.log('[Settings] 按键已释放') - - const lastSequence = clipboardManager.getLastCopiedSequence() - console.log('[Settings] 上次剪贴板序列号:', lastSequence) - - const modifier = process.platform === 'darwin' ? 'meta' : 'ctrl' - console.log('[Settings] 模拟按键: Ctrl/Cmd+C') - NativeWindowManager.simulateKeyboardTap('c', modifier) - - console.log('[Settings] 等待剪贴板更新,超时时间:', CLIPBOARD_COPY_WAIT_TIMEOUT_MS, 'ms') - const lastCopiedContent = await clipboardManager.waitForNextCopiedContent( - lastSequence, - CLIPBOARD_COPY_WAIT_TIMEOUT_MS - ) + const contents = NativeWindowManager.getSelectedContent() + + // 防御性检查:确保 contents 是有效数组 + if (!Array.isArray(contents)) { + console.log('[Settings] 未捕获到任何内容 (contents 不是数组)') + return { + searchQuery: '', + pastedImage: null, + pastedFiles: null, + pastedText: null + } + } - console.log('[Settings] 剪贴板内容:', lastCopiedContent) + console.log('[Settings] 捕获到内容数量:', contents.length) - if (lastCopiedContent?.type === 'text' && typeof lastCopiedContent.data === 'string') { - const text = lastCopiedContent.data + // 处理文本内容 + const textContent = contents.find((item) => item.type === 'text') + if (textContent && textContent.type === 'text') { + const text = textContent.data console.log('[Settings] 捕获到文本,长度:', text.length) if (text.trim()) { console.log('[Settings] 文本捕获成功') @@ -464,16 +445,50 @@ export class SettingsAPI { } else { console.log('[Settings] 文本为空') } - } else { - console.log('[Settings] 未捕获到文本内容') } - } catch (error) { - console.error('[Settings] 获取选中文本失败:', error) - if (error instanceof Error && error.message.includes('timeout')) { - console.error( - '[Settings] 等待复制超时!可能原因:1) 没有选中文本 2) 应用不支持复制 3) 网络延迟' - ) + + // 处理图片内容 + const imageContent = contents.find((item) => item.type === 'image') + if (imageContent && imageContent.type === 'image') { + console.log('[Settings] 捕获到图片') + return { + searchQuery: '', + pastedImage: imageContent.data, + pastedFiles: null, + pastedText: null + } + } + + // 处理文件内容 + const fileContent = contents.find((item) => item.type === 'file') + if (fileContent && fileContent.type === 'file') { + console.log('[Settings] 捕获到文件,数量:', fileContent.data.length) + const files = fileContent.data.map((filePath) => { + let isDirectory = false + try { + isDirectory = fs.statSync(filePath).isDirectory() + } catch (e) { + // 忽略读取失败的情况,默认设为 false + console.warn(`[Settings] 无法读取文件状态: ${filePath}`, e) + } + return { + path: filePath, + name: filePath.split(/[/\\]/).pop() || '', + isDirectory, + isFile: !isDirectory + } + }) + return { + searchQuery: '', + pastedImage: null, + pastedFiles: files, + pastedText: null + } } + + console.log('[Settings] 未捕获到任何内容') + } catch (error) { + console.error('[Settings] 获取选中内容失败:', error) } return { diff --git a/src/main/core/native/index.ts b/src/main/core/native/index.ts index 6960d649..fae968fa 100644 --- a/src/main/core/native/index.ts +++ b/src/main/core/native/index.ts @@ -66,6 +66,18 @@ interface NativeAddon { hwnd: number, callback: (url: string | null) => void ) => void + /** + * 获取当前选中的内容(支持文本、文件、图像) + * 实现方式: + * - Windows: 优先使用 UI Automation API,回退到剪贴板方法 + * - macOS: 使用模拟复制方法(Cmd+C) + * 自动暂停 clipboardMonitor,防止误触发监听 + */ + getSelectedContent: () => Array< + | { type: 'text'; data: string } + | { type: 'file'; data: string[] } + | { type: 'image'; data: string } + > } interface WindowInfo { @@ -526,6 +538,49 @@ export class WindowManager { } return (addon as NativeAddon).simulateMouseRightClick(x, y) } + + /** + * 获取当前选中的内容(支持文本、文件、图像) + * + * 实现方式: + * - Windows: 优先使用 UI Automation API,回退到剪贴板方法(适用于 Cursor/VS Code 等编辑器) + * - macOS: 使用模拟复制方法(Cmd+C) + * + * 在模拟复制时会自动暂停内部的 clipboardMonitor,防止误触发监听自身发起的事件 + * + * @returns {Array<{type: string, data: any}>} 选中内容数组 + * - type: 'text' | 'file' | 'image' + * - data: 根据类型不同: + * - text: 字符串 + * - file: 文件路径字符串数组 + * - image: base64 编码的 PNG 图像(带 format 和 encoding 字段) + * + * @example + * const contents = WindowManager.getSelectedContent(); + * contents.forEach(item => { + * switch (item.type) { + * case 'text': + * console.log('Selected text:', item.data); + * break; + * case 'file': + * console.log('Selected files:', item.data); + * break; + * case 'image': + * console.log('Selected image (base64):', item.data.substring(0, 50) + '...'); + * break; + * } + * }); + */ + static getSelectedContent(): Array< + | { type: 'text'; data: string } + | { type: 'file'; data: string[] } + | { type: 'image'; data: string } + > { + if (platform === 'linux') { + return [] + } + return (addon as NativeAddon).getSelectedContent() + } } /**