From 15c160e45d27823191e4e569b20af33d6c76e37a Mon Sep 17 00:00:00 2001 From: lanxiuyun Date: Thu, 27 Nov 2025 14:47:47 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=A4=8D=E5=88=B6=E6=96=87?= =?UTF-8?q?=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/common/api.ts | 222 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 217 insertions(+), 5 deletions(-) diff --git a/src/main/common/api.ts b/src/main/common/api.ts index acefcf7..a7feb1f 100644 --- a/src/main/common/api.ts +++ b/src/main/common/api.ts @@ -28,6 +28,120 @@ import getWinPosition from './getWinPosition'; import path from 'path'; import commonConst from '@/common/utils/commonConst'; +const logCopyFile = (...messages: unknown[]) => + console.log('[copyFile]', ...messages); + +const DROPFILES_HEADER_SIZE = 20; + +type ClipboardExModule = typeof import('electron-clipboard-ex'); + +let clipboardExModule: ClipboardExModule | null = null; + +const ensureClipboardEx = (): ClipboardExModule | null => { + if (process.platform !== 'win32') return null; + if (clipboardExModule) return clipboardExModule; + try { + // eslint-disable-next-line global-require, @typescript-eslint/no-var-requires + clipboardExModule = require('electron-clipboard-ex'); + logCopyFile('Loaded electron-clipboard-ex successfully'); + } catch (error) { + logCopyFile('Failed to load electron-clipboard-ex', error); + clipboardExModule = null; + } + return clipboardExModule; +}; + +const buildWindowsFileListPayload = (files: string[]): Buffer => { + return Buffer.from(`${files.join('\0')}\0\0`, 'utf16le'); +}; + +const parseWindowsFileListPayload = (payload: Buffer): string[] => { + if (!payload?.length) return []; + const trimmed = payload.toString('utf16le').replace(/\0+$/, ''); + if (!trimmed) return []; + return trimmed.split('\0').filter(Boolean); +}; + +const buildWindowsFileDropBuffer = (files: string[]): Buffer => { + const payload = buildWindowsFileListPayload(files); + const header = Buffer.alloc(DROPFILES_HEADER_SIZE); + header.writeUInt32LE(DROPFILES_HEADER_SIZE, 0); // pFiles points to data offset + header.writeInt32LE(0, 4); // pt.x + header.writeInt32LE(0, 8); // pt.y + header.writeUInt32LE(0, 12); // fNC + header.writeUInt32LE(1, 16); // fWide => Unicode paths + + const result = Buffer.alloc(header.length + payload.length); + for (let i = 0; i < header.length; i += 1) { + result[i] = header[i]; + } + for (let i = 0; i < payload.length; i += 1) { + result[header.length + i] = payload[i]; + } + return result; +}; + +const parseWindowsDropBuffer = (buffer: Buffer): string[] => { + if (!buffer?.length || buffer.length < DROPFILES_HEADER_SIZE) { + return []; + } + const offset = buffer.readUInt32LE(0); + if (!offset || offset >= buffer.length) { + return []; + } + const payload = buffer.subarray(offset); + return parseWindowsFileListPayload(payload); +}; + +const buildDropEffectBuffer = (effect: 'copy' | 'move' | 'link' = 'copy') => { + const effectMap = { + copy: 1, + move: 2, + link: 4, + } as const; + const buffer = Buffer.alloc(4); + buffer.writeUInt32LE(effectMap[effect], 0); + return buffer; +}; + +const logAvailableFormats = (type: 'clipboard' | 'selection') => { + try { + logCopyFile(`availableFormats(${type})`, clipboard.availableFormats(type)); + } catch (error) { + logCopyFile(`availableFormats(${type}) read failed`, error); + } +}; + +const debugWindowsClipboard = (phase = 'afterWrite') => { + logCopyFile('---- clipboard debug phase ----', phase); + logAvailableFormats('clipboard'); + logAvailableFormats('selection'); + try { + const drop = clipboard.readBuffer('CF_HDROP'); + logCopyFile('CF_HDROP length', drop.length); + logCopyFile('CF_HDROP parsed', parseWindowsDropBuffer(drop)); + } catch (error) { + logCopyFile('read CF_HDROP failed', error); + } + try { + const fileNameW = clipboard.readBuffer('FileNameW'); + logCopyFile('FileNameW length', fileNameW.length); + logCopyFile('FileNameW parsed', parseWindowsFileListPayload(fileNameW)); + } catch (error) { + logCopyFile('read FileNameW failed', error); + } + try { + const dropEffect = clipboard.readBuffer('Preferred DropEffect'); + logCopyFile('DropEffect length', dropEffect.length); + logCopyFile( + 'DropEffect value', + dropEffect.length ? dropEffect.readUInt32LE(0) : undefined + ); + } catch (error) { + logCopyFile('read Preferred DropEffect failed', error); + } +}; + const runnerInstance = runner(); const detachInstance = detach(); @@ -230,13 +344,111 @@ class API extends DBInstance { } public copyFile({ data }) { - if (data.file && fs.existsSync(data.file)) { - clipboard.writeBuffer( - 'NSFilenamesPboardType', - Buffer.from(plist.build([data.file])) - ); + const input = data?.file; + logCopyFile('Input received', { input, platform: process.platform }); + const candidateFiles = Array.isArray(input) + ? input + : typeof input === 'string' + ? [input] + : []; + logCopyFile('Candidate files', candidateFiles); + + const targetFiles = candidateFiles + .map((filePath) => + typeof filePath === 'string' ? filePath.trim() : '' + ) + .filter((filePath) => { + if (!filePath) return false; + try { + return fs.existsSync(filePath); + } catch { + return false; + } + }); + + if (!targetFiles.length) { + logCopyFile('No valid files detected, abort copy'); + return false; + } + + if (process.platform === 'darwin') { + try { + clipboard.writeBuffer( + 'NSFilenamesPboardType', + Buffer.from(plist.build(targetFiles)) + ); + logCopyFile('macOS clipboard write succeeded', targetFiles); + } catch (error) { + logCopyFile('macOS clipboard write failed', error); + return false; + } return true; } + + if (process.platform === 'win32') { + const normalizedFiles = targetFiles.map((filePath) => { + const normalized = path.normalize(filePath); + logCopyFile('Normalized path', { original: filePath, normalized }); + return normalized; + }); + + const clipboardExInstance = ensureClipboardEx(); + if (clipboardExInstance) { + try { + const writeResult = clipboardExInstance.writeFilePaths(normalizedFiles); + logCopyFile('clipboard-ex write result', writeResult); + const readBack = clipboardExInstance.readFilePaths(); + logCopyFile('clipboard-ex readback', readBack); + if (writeResult.length === normalizedFiles.length) { + logCopyFile('clipboard-ex copied all files, skipping Electron fallback'); + return true; + } + logCopyFile( + 'clipboard-ex copied fewer files than requested, continue with Electron fallback' + ); + } catch (error) { + logCopyFile('clipboard-ex write failed, continue with Electron fallback', error); + } + } else { + logCopyFile('clipboard-ex not available, continue with Electron fallback'); + } + + try { + clipboard.writeBuffer( + 'CF_HDROP', + buildWindowsFileDropBuffer(normalizedFiles) + ); + clipboard.writeBuffer( + 'FileNameW', + buildWindowsFileListPayload(normalizedFiles) + ); + clipboard.writeBuffer( + 'Preferred DropEffect', + buildDropEffectBuffer('copy') + ); + logCopyFile('Electron clipboard write finished', normalizedFiles); + } catch (error) { + logCopyFile('Electron clipboard write failed', error); + } + + debugWindowsClipboard('afterElectronWrite'); + + let dropBufferLength = 0; + try { + dropBufferLength = clipboard.readBuffer('CF_HDROP').length; + } catch (error) { + logCopyFile('Failed to read CF_HDROP after Electron write', error); + return false; + } + if (!dropBufferLength) { + logCopyFile('CF_HDROP buffer is empty after Electron write, copy failed'); + return false; + } + logCopyFile('CF_HDROP buffer detected after Electron write, copy succeeded'); + return true; + } + + logCopyFile('Current platform is not supported yet'); return false; }