From 15c160e45d27823191e4e569b20af33d6c76e37a Mon Sep 17 00:00:00 2001 From: lanxiuyun Date: Thu, 27 Nov 2025 14:47:47 +0800 Subject: [PATCH 1/5] =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=A4=8D=E5=88=B6?= =?UTF-8?q?=E6=96=87=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; } From 9adfa84cab0c5f7a637e0f83dd136cd2a958e597 Mon Sep 17 00:00:00 2001 From: lanxiuyun Date: Thu, 27 Nov 2025 15:32:42 +0800 Subject: [PATCH 2/5] =?UTF-8?q?=E7=AE=80=E5=8C=96=E5=A4=8D=E5=88=B6?= =?UTF-8?q?=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/common/api.ts | 207 ++++++++++++----------------------------- 1 file changed, 58 insertions(+), 149 deletions(-) diff --git a/src/main/common/api.ts b/src/main/common/api.ts index a7feb1f..6478d33 100644 --- a/src/main/common/api.ts +++ b/src/main/common/api.ts @@ -28,9 +28,6 @@ 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'); @@ -43,24 +40,14 @@ const ensureClipboardEx = (): ClipboardExModule | null => { 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 buildWindowsFileListPayload = (files: string[]): Buffer => + Buffer.from(`${files.join('\0')}\0\0`, 'utf16le'); const buildWindowsFileDropBuffer = (files: string[]): Buffer => { const payload = buildWindowsFileListPayload(files); @@ -81,18 +68,6 @@ const buildWindowsFileDropBuffer = (files: string[]): Buffer => { 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, @@ -104,41 +79,61 @@ const buildDropEffectBuffer = (effect: 'copy' | 'move' | 'link' = 'copy') => { return buffer; }; -const logAvailableFormats = (type: 'clipboard' | 'selection') => { +const sanitizeInputFiles = (input: unknown): string[] => { + const candidates = Array.isArray(input) + ? input + : typeof input === 'string' + ? [input] + : []; + return candidates + .map((filePath) => + typeof filePath === 'string' ? filePath.trim() : '' + ) + .filter((filePath) => { + if (!filePath) return false; + try { + return fs.existsSync(filePath); + } catch { + return false; + } + }); +}; + +const writeMacClipboardFiles = (files: string[]): boolean => { try { - logCopyFile(`availableFormats(${type})`, clipboard.availableFormats(type)); - } catch (error) { - logCopyFile(`availableFormats(${type}) read failed`, error); + clipboard.writeBuffer( + 'NSFilenamesPboardType', + Buffer.from(plist.build(files)) + ); + return true; + } catch { + return false; } }; -const debugWindowsClipboard = (phase = 'afterWrite') => { - logCopyFile('---- clipboard debug phase ----', phase); - logAvailableFormats('clipboard'); - logAvailableFormats('selection'); +const writeWindowsClipboardFiles = (files: string[]): boolean => { 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); + clipboard.writeBuffer('CF_HDROP', buildWindowsFileDropBuffer(files)); + clipboard.writeBuffer('FileNameW', buildWindowsFileListPayload(files)); + clipboard.writeBuffer('Preferred DropEffect', buildDropEffectBuffer('copy')); + return clipboard.readBuffer('CF_HDROP').length > 0; + } catch { + return false; } +}; + +const writeWithClipboardEx = (files: string[]): boolean => { + const clipboardEx = ensureClipboardEx(); + if (!clipboardEx) return false; 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); + clipboardEx.writeFilePaths(files); + if (typeof clipboardEx.readFilePaths === 'function') { + const result = clipboardEx.readFilePaths(); + return Array.isArray(result) && result.length === files.length; + } + return true; + } catch { + return false; } }; @@ -344,111 +339,25 @@ class API extends DBInstance { } public copyFile({ data }) { - 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; - } - }); - + const targetFiles = sanitizeInputFiles(data?.file); 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; + return writeMacClipboardFiles(targetFiles); } 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'); + const normalizedFiles = targetFiles.map((filePath) => + path.normalize(filePath) + ); + if (writeWithClipboardEx(normalizedFiles)) { + return true; } - - 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; + return writeWindowsClipboardFiles(normalizedFiles); } - logCopyFile('Current platform is not supported yet'); return false; } From 9005d070aae4c2e5890b30d8df13ef61cd53624f Mon Sep 17 00:00:00 2001 From: lanxiuyun Date: Thu, 27 Nov 2025 16:01:58 +0800 Subject: [PATCH 3/5] =?UTF-8?q?windows=E5=A4=8D=E5=88=B6=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E5=88=B0=E5=89=AA=E5=88=87=E6=9D=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/common/api.ts | 86 +-------------------------- src/main/common/windowsClipboard.ts | 91 +++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 84 deletions(-) create mode 100644 src/main/common/windowsClipboard.ts diff --git a/src/main/common/api.ts b/src/main/common/api.ts index 6478d33..819fb5b 100644 --- a/src/main/common/api.ts +++ b/src/main/common/api.ts @@ -27,57 +27,7 @@ import DBInstance from './db'; import getWinPosition from './getWinPosition'; import path from 'path'; import commonConst from '@/common/utils/commonConst'; - -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'); - } catch (error) { - clipboardExModule = null; - } - return clipboardExModule; -}; - -const buildWindowsFileListPayload = (files: string[]): Buffer => - Buffer.from(`${files.join('\0')}\0\0`, 'utf16le'); - -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 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; -}; +import { copyFilesToWindowsClipboard } from './windowsClipboard'; const sanitizeInputFiles = (input: unknown): string[] => { const candidates = Array.isArray(input) @@ -111,32 +61,6 @@ const writeMacClipboardFiles = (files: string[]): boolean => { } }; -const writeWindowsClipboardFiles = (files: string[]): boolean => { - try { - clipboard.writeBuffer('CF_HDROP', buildWindowsFileDropBuffer(files)); - clipboard.writeBuffer('FileNameW', buildWindowsFileListPayload(files)); - clipboard.writeBuffer('Preferred DropEffect', buildDropEffectBuffer('copy')); - return clipboard.readBuffer('CF_HDROP').length > 0; - } catch { - return false; - } -}; - -const writeWithClipboardEx = (files: string[]): boolean => { - const clipboardEx = ensureClipboardEx(); - if (!clipboardEx) return false; - try { - clipboardEx.writeFilePaths(files); - if (typeof clipboardEx.readFilePaths === 'function') { - const result = clipboardEx.readFilePaths(); - return Array.isArray(result) && result.length === files.length; - } - return true; - } catch { - return false; - } -}; - const runnerInstance = runner(); const detachInstance = detach(); @@ -349,13 +273,7 @@ class API extends DBInstance { } if (process.platform === 'win32') { - const normalizedFiles = targetFiles.map((filePath) => - path.normalize(filePath) - ); - if (writeWithClipboardEx(normalizedFiles)) { - return true; - } - return writeWindowsClipboardFiles(normalizedFiles); + return copyFilesToWindowsClipboard(targetFiles); } return false; diff --git a/src/main/common/windowsClipboard.ts b/src/main/common/windowsClipboard.ts new file mode 100644 index 0000000..c10aac8 --- /dev/null +++ b/src/main/common/windowsClipboard.ts @@ -0,0 +1,91 @@ +import { clipboard } from 'electron'; +import path from 'path'; + +type ClipboardExModule = typeof import('electron-clipboard-ex'); + +const DROPFILES_HEADER_SIZE = 20; + +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'); + } catch { + clipboardExModule = null; + } + return clipboardExModule; +}; + +const buildWindowsFileListPayload = (files: string[]): Buffer => + Buffer.from(`${files.join('\0')}\0\0`, 'utf16le'); + +const buildWindowsFileDropBuffer = (files: string[]): Buffer => { + const payload = buildWindowsFileListPayload(files); + const header = Buffer.alloc(DROPFILES_HEADER_SIZE); + header.writeUInt32LE(DROPFILES_HEADER_SIZE, 0); + header.writeInt32LE(0, 4); + header.writeInt32LE(0, 8); + header.writeUInt32LE(0, 12); + header.writeUInt32LE(1, 16); + + 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 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 writeWindowsBuffers = (files: string[]): boolean => { + try { + clipboard.writeBuffer('CF_HDROP', buildWindowsFileDropBuffer(files)); + clipboard.writeBuffer('FileNameW', buildWindowsFileListPayload(files)); + clipboard.writeBuffer('Preferred DropEffect', buildDropEffectBuffer('copy')); + return clipboard.readBuffer('CF_HDROP').length > 0; + } catch { + return false; + } +}; + +const writeWithClipboardEx = (files: string[]): boolean => { + const clipboardEx = ensureClipboardEx(); + if (!clipboardEx) return false; + try { + clipboardEx.writeFilePaths(files); + if (typeof clipboardEx.readFilePaths === 'function') { + const result = clipboardEx.readFilePaths(); + return Array.isArray(result) && result.length === files.length; + } + return true; + } catch { + return false; + } +}; + +export const copyFilesToWindowsClipboard = (files: string[]): boolean => { + const normalizedFiles = files + .map((filePath) => path.normalize(filePath)) + .filter(Boolean); + if (!normalizedFiles.length) return false; + if (writeWithClipboardEx(normalizedFiles)) { + return true; + } + return writeWindowsBuffers(normalizedFiles); +}; + From 0bddb33fdeeadfcf70bf319cef373352e539e0e1 Mon Sep 17 00:00:00 2001 From: lanxiuyun Date: Thu, 27 Nov 2025 16:29:53 +0800 Subject: [PATCH 4/5] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=8F=AF=E8=AF=BB?= =?UTF-8?q?=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/common/api.ts | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/main/common/api.ts b/src/main/common/api.ts index 819fb5b..34453e8 100644 --- a/src/main/common/api.ts +++ b/src/main/common/api.ts @@ -29,6 +29,11 @@ import path from 'path'; import commonConst from '@/common/utils/commonConst'; import { copyFilesToWindowsClipboard } from './windowsClipboard'; +/** + * sanitize input files 剪贴板文件合法性校验 + * @param input + * @returns + */ const sanitizeInputFiles = (input: unknown): string[] => { const candidates = Array.isArray(input) ? input @@ -36,9 +41,7 @@ const sanitizeInputFiles = (input: unknown): string[] => { ? [input] : []; return candidates - .map((filePath) => - typeof filePath === 'string' ? filePath.trim() : '' - ) + .map((filePath) => (typeof filePath === 'string' ? filePath.trim() : '')) .filter((filePath) => { if (!filePath) return false; try { @@ -49,18 +52,6 @@ const sanitizeInputFiles = (input: unknown): string[] => { }); }; -const writeMacClipboardFiles = (files: string[]): boolean => { - try { - clipboard.writeBuffer( - 'NSFilenamesPboardType', - Buffer.from(plist.build(files)) - ); - return true; - } catch { - return false; - } -}; - const runnerInstance = runner(); const detachInstance = detach(); @@ -264,12 +255,21 @@ class API extends DBInstance { public copyFile({ data }) { const targetFiles = sanitizeInputFiles(data?.file); + if (!targetFiles.length) { return false; } if (process.platform === 'darwin') { - return writeMacClipboardFiles(targetFiles); + try { + clipboard.writeBuffer( + 'NSFilenamesPboardType', + Buffer.from(plist.build(targetFiles)) + ); + return true; + } catch { + return false; + } } if (process.platform === 'win32') { From f5f3f030ce47fc76e2de440913a2b3032721a7eb Mon Sep 17 00:00:00 2001 From: lanxiuyun Date: Thu, 27 Nov 2025 17:32:14 +0800 Subject: [PATCH 5/5] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/common/windowsClipboard.ts | 42 +++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/src/main/common/windowsClipboard.ts b/src/main/common/windowsClipboard.ts index c10aac8..bf495c1 100644 --- a/src/main/common/windowsClipboard.ts +++ b/src/main/common/windowsClipboard.ts @@ -1,12 +1,18 @@ import { clipboard } from 'electron'; import path from 'path'; +// 仅在 Windows 平台辅助操作剪贴板多文件格式。 type ClipboardExModule = typeof import('electron-clipboard-ex'); const DROPFILES_HEADER_SIZE = 20; let clipboardExModule: ClipboardExModule | null = null; +/** + * Windows 平台专用:尝试加载第三方库 electron-clipboard-ex。 + * 这个库能够调用系统底层接口写入“文件复制”数据,成功率更高。 + * 其他系统无需加载它,因此这里做了“按需加载”的处理。 + */ const ensureClipboardEx = (): ClipboardExModule | null => { if (process.platform !== 'win32') return null; if (clipboardExModule) return clipboardExModule; @@ -19,9 +25,20 @@ const ensureClipboardEx = (): ClipboardExModule | null => { return clipboardExModule; }; +/** + * 把一组文件路径变成 Windows 规定的文本格式。 + * 要求:每个路径之间用单个空字符分隔,最后再额外放两个空字符,表示列表结束。 + * Windows 资源管理器会按这个格式解析我们复制到剪贴板的文件。 + */ const buildWindowsFileListPayload = (files: string[]): Buffer => Buffer.from(`${files.join('\0')}\0\0`, 'utf16le'); +/** + * 构造 CF_HDROP 专用的二进制数据。 + * 这是 Windows 复制文件时的底层格式,前 20 字节是固定的结构头, + * 后面紧跟着具体的文件路径(由 buildWindowsFileListPayload 生成)。 + * 只要把这个内容写入剪贴板,任何支持粘贴文件的程序都能理解。 + */ const buildWindowsFileDropBuffer = (files: string[]): Buffer => { const payload = buildWindowsFileListPayload(files); const header = Buffer.alloc(DROPFILES_HEADER_SIZE); @@ -41,6 +58,11 @@ const buildWindowsFileDropBuffer = (files: string[]): Buffer => { return result; }; +/** + * 复制/移动/创建快捷方式 等不同操作在 Windows 中对应不同的“意图”值。 + * Preferred DropEffect 告诉系统:当前剪贴板数据应该以何种方式处理。 + * 我们默认写入“copy”,相当于普通的复制粘贴。 + */ const buildDropEffectBuffer = (effect: 'copy' | 'move' | 'link' = 'copy') => { const effectMap = { copy: 1, @@ -52,6 +74,14 @@ const buildDropEffectBuffer = (effect: 'copy' | 'move' | 'link' = 'copy') => { return buffer; }; +/** + * 直接使用 Electron 内置 API 写入多种剪贴板格式。 + * 步骤: + * 1. 写入二进制的 CF_HDROP(含头部与路径列表) + * 2. 写入纯文本形式的 FileNameW(备选格式) + * 3. 写入 Preferred DropEffect(告诉系统“这是复制”) + * 全部成功后,读取一次 CF_HDROP 的长度,确认剪贴板里确实有内容。 + */ const writeWindowsBuffers = (files: string[]): boolean => { try { clipboard.writeBuffer('CF_HDROP', buildWindowsFileDropBuffer(files)); @@ -63,6 +93,11 @@ const writeWindowsBuffers = (files: string[]): boolean => { } }; +/** + * 如果项目中安装了 electron-clipboard-ex,我们优先使用它。 + * 理由:该库通过原生方式与系统交互,兼容性往往优于 Electron 的 JS 层写入。 + * 调用成功后,必要时读回文件列表做一次数量校验,确保复制的文件数量正确。 + */ const writeWithClipboardEx = (files: string[]): boolean => { const clipboardEx = ensureClipboardEx(); if (!clipboardEx) return false; @@ -78,6 +113,13 @@ const writeWithClipboardEx = (files: string[]): boolean => { } }; +/** + * 对外暴露的唯一入口。 + * 1. 先把所有路径换成 Windows 可识别的标准形式(path.normalize)。 + * 2. 尝试使用 electron-clipboard-ex 写入,如果成功就结束。 + * 3. 若第三方库不可用或失败,再退回 Electron 原生写入流程。 + * 这一层屏蔽了所有细节,外部调用者只需传入字符串数组即可。 + */ export const copyFilesToWindowsClipboard = (files: string[]): boolean => { const normalizedFiles = files .map((filePath) => path.normalize(filePath))