@lynker-desktop/electron-sdk 0.0.9-alpha.7 → 0.0.9-alpha.72
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +160 -1
- package/common/index.d.ts +96 -0
- package/common/index.d.ts.map +1 -1
- package/common/index.js.map +1 -1
- package/esm/common/index.d.ts +96 -0
- package/esm/common/index.d.ts.map +1 -1
- package/esm/common/index.js.map +1 -1
- package/esm/main/clipboard.d.ts +32 -0
- package/esm/main/clipboard.d.ts.map +1 -0
- package/esm/main/clipboard.js +1208 -0
- package/esm/main/clipboard.js.map +1 -0
- package/esm/main/downloader.d.ts +212 -0
- package/esm/main/downloader.d.ts.map +1 -0
- package/esm/main/downloader.js +674 -0
- package/esm/main/downloader.js.map +1 -0
- package/esm/main/index.d.ts +20 -67
- package/esm/main/index.d.ts.map +1 -1
- package/esm/main/index.js +51 -202
- package/esm/main/index.js.map +1 -1
- package/esm/main/resource-cache.d.ts +245 -0
- package/esm/main/resource-cache.d.ts.map +1 -0
- package/esm/main/resource-cache.js +857 -0
- package/esm/main/resource-cache.js.map +1 -0
- package/esm/main/shortcut.d.ts +14 -0
- package/esm/main/shortcut.d.ts.map +1 -0
- package/esm/main/shortcut.js +173 -0
- package/esm/main/shortcut.js.map +1 -0
- package/esm/main/store.d.ts +10 -0
- package/esm/main/store.d.ts.map +1 -0
- package/esm/main/store.js +62 -0
- package/esm/main/store.js.map +1 -0
- package/esm/main/video-downloader.d.ts +39 -0
- package/esm/main/video-downloader.d.ts.map +1 -0
- package/esm/main/video-downloader.js +505 -0
- package/esm/main/video-downloader.js.map +1 -0
- package/esm/preload/index.js +19 -1
- package/esm/preload/index.js.map +1 -1
- package/esm/renderer/index.d.ts +8 -0
- package/esm/renderer/index.d.ts.map +1 -1
- package/esm/renderer/index.js +25 -0
- package/esm/renderer/index.js.map +1 -1
- package/main/clipboard.d.ts +32 -0
- package/main/clipboard.d.ts.map +1 -0
- package/main/clipboard.js +1208 -0
- package/main/clipboard.js.map +1 -0
- package/main/downloader.d.ts +212 -0
- package/main/downloader.d.ts.map +1 -0
- package/main/downloader.js +674 -0
- package/main/downloader.js.map +1 -0
- package/main/index.d.ts +20 -67
- package/main/index.d.ts.map +1 -1
- package/main/index.js +54 -205
- package/main/index.js.map +1 -1
- package/main/resource-cache.d.ts +245 -0
- package/main/resource-cache.d.ts.map +1 -0
- package/main/resource-cache.js +857 -0
- package/main/resource-cache.js.map +1 -0
- package/main/shortcut.d.ts +14 -0
- package/main/shortcut.d.ts.map +1 -0
- package/main/shortcut.js +173 -0
- package/main/shortcut.js.map +1 -0
- package/main/store.d.ts +10 -0
- package/main/store.d.ts.map +1 -0
- package/main/store.js +64 -0
- package/main/store.js.map +1 -0
- package/main/video-downloader.d.ts +39 -0
- package/main/video-downloader.d.ts.map +1 -0
- package/main/video-downloader.js +510 -0
- package/main/video-downloader.js.map +1 -0
- package/package.json +9 -5
- package/preload/index.js +19 -1
- package/preload/index.js.map +1 -1
- package/renderer/index.d.ts +8 -0
- package/renderer/index.d.ts.map +1 -1
- package/renderer/index.js +25 -0
- package/renderer/index.js.map +1 -1
|
@@ -0,0 +1,1208 @@
|
|
|
1
|
+
const electron = require('electron');
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const http = require('node:http');
|
|
5
|
+
const https = require('node:https');
|
|
6
|
+
const ipc = require('@lynker-desktop/electron-ipc/main');
|
|
7
|
+
|
|
8
|
+
// 常量定义
|
|
9
|
+
const MAX_FILE_SIZE = 100 * 1024 * 1024; // 100MB,超过此大小不读取 buffer
|
|
10
|
+
const FILE_URI_PREFIX = 'file://';
|
|
11
|
+
const PLATFORM = process.platform;
|
|
12
|
+
const DOWNLOAD_TIMEOUT = 30000; // 30秒下载超时
|
|
13
|
+
const UNIX_FILE_FORMATS = ['public.file-url', 'text/uri-list', 'x-special/gnome-copied-files'];
|
|
14
|
+
// 图片相关的剪贴板格式
|
|
15
|
+
const IMAGE_FORMATS = ['image/png', 'image/jpeg', 'image/jpg', 'image/gif', 'image/bmp', 'image/webp', 'image/tiff', 'image/x-png', 'image/x-icon'];
|
|
16
|
+
// Windows 文件格式
|
|
17
|
+
const WINDOWS_FILE_FORMAT = 'FileNameW';
|
|
18
|
+
// MIME 类型映射表
|
|
19
|
+
const MIME_TYPES = {
|
|
20
|
+
// 文本文件
|
|
21
|
+
'.txt': 'text/plain',
|
|
22
|
+
'.text': 'text/plain',
|
|
23
|
+
'.md': 'text/markdown',
|
|
24
|
+
'.markdown': 'text/markdown',
|
|
25
|
+
'.html': 'text/html',
|
|
26
|
+
'.htm': 'text/html',
|
|
27
|
+
'.css': 'text/css',
|
|
28
|
+
'.js': 'text/javascript',
|
|
29
|
+
'.jsx': 'text/javascript',
|
|
30
|
+
'.ts': 'text/typescript',
|
|
31
|
+
'.tsx': 'text/typescript',
|
|
32
|
+
'.json': 'application/json',
|
|
33
|
+
'.xml': 'application/xml',
|
|
34
|
+
'.yaml': 'text/yaml',
|
|
35
|
+
'.yml': 'text/yaml',
|
|
36
|
+
'.csv': 'text/csv',
|
|
37
|
+
'.rtf': 'application/rtf',
|
|
38
|
+
'.log': 'text/plain',
|
|
39
|
+
// 图片文件
|
|
40
|
+
'.jpg': 'image/jpeg',
|
|
41
|
+
'.jpeg': 'image/jpeg',
|
|
42
|
+
'.png': 'image/png',
|
|
43
|
+
'.gif': 'image/gif',
|
|
44
|
+
'.bmp': 'image/bmp',
|
|
45
|
+
'.webp': 'image/webp',
|
|
46
|
+
'.svg': 'image/svg+xml',
|
|
47
|
+
'.ico': 'image/x-icon',
|
|
48
|
+
'.tiff': 'image/tiff',
|
|
49
|
+
'.tif': 'image/tiff',
|
|
50
|
+
'.heic': 'image/heic',
|
|
51
|
+
'.heif': 'image/heif',
|
|
52
|
+
'.avif': 'image/avif',
|
|
53
|
+
// 音频文件
|
|
54
|
+
'.mp3': 'audio/mpeg',
|
|
55
|
+
'.wav': 'audio/wav',
|
|
56
|
+
'.ogg': 'audio/ogg',
|
|
57
|
+
'.oga': 'audio/ogg',
|
|
58
|
+
'.m4a': 'audio/mp4',
|
|
59
|
+
'.aac': 'audio/aac',
|
|
60
|
+
'.flac': 'audio/flac',
|
|
61
|
+
'.wma': 'audio/x-ms-wma',
|
|
62
|
+
'.opus': 'audio/opus',
|
|
63
|
+
'.amr': 'audio/amr',
|
|
64
|
+
// 视频文件
|
|
65
|
+
'.mp4': 'video/mp4',
|
|
66
|
+
'.avi': 'video/x-msvideo',
|
|
67
|
+
'.mov': 'video/quicktime',
|
|
68
|
+
'.wmv': 'video/x-ms-wmv',
|
|
69
|
+
'.flv': 'video/x-flv',
|
|
70
|
+
'.webm': 'video/webm',
|
|
71
|
+
'.mkv': 'video/x-matroska',
|
|
72
|
+
'.m4v': 'video/mp4',
|
|
73
|
+
'.3gp': 'video/3gpp',
|
|
74
|
+
'.ogv': 'video/ogg',
|
|
75
|
+
'.m2ts': 'video/mp2t',
|
|
76
|
+
'.mts': 'video/mp2t',
|
|
77
|
+
// 文档文件
|
|
78
|
+
'.pdf': 'application/pdf',
|
|
79
|
+
'.doc': 'application/msword',
|
|
80
|
+
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
81
|
+
'.xls': 'application/vnd.ms-excel',
|
|
82
|
+
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
83
|
+
'.ppt': 'application/vnd.ms-powerpoint',
|
|
84
|
+
'.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
85
|
+
'.odt': 'application/vnd.oasis.opendocument.text',
|
|
86
|
+
'.ods': 'application/vnd.oasis.opendocument.spreadsheet',
|
|
87
|
+
'.odp': 'application/vnd.oasis.opendocument.presentation',
|
|
88
|
+
// 压缩文件
|
|
89
|
+
'.zip': 'application/zip',
|
|
90
|
+
'.rar': 'application/x-rar-compressed',
|
|
91
|
+
'.7z': 'application/x-7z-compressed',
|
|
92
|
+
'.tar': 'application/x-tar',
|
|
93
|
+
'.gz': 'application/gzip',
|
|
94
|
+
'.bz2': 'application/x-bzip2',
|
|
95
|
+
'.xz': 'application/x-xz',
|
|
96
|
+
// 字体文件
|
|
97
|
+
'.ttf': 'font/ttf',
|
|
98
|
+
'.otf': 'font/otf',
|
|
99
|
+
'.woff': 'font/woff',
|
|
100
|
+
'.woff2': 'font/woff2',
|
|
101
|
+
'.eot': 'application/vnd.ms-fontobject',
|
|
102
|
+
// 代码文件
|
|
103
|
+
'.c': 'text/x-c',
|
|
104
|
+
'.cpp': 'text/x-c++',
|
|
105
|
+
'.h': 'text/x-c',
|
|
106
|
+
'.hpp': 'text/x-c++',
|
|
107
|
+
'.java': 'text/x-java-source',
|
|
108
|
+
'.py': 'text/x-python',
|
|
109
|
+
'.rb': 'text/x-ruby',
|
|
110
|
+
'.php': 'text/x-php',
|
|
111
|
+
'.go': 'text/x-go',
|
|
112
|
+
'.rs': 'text/x-rust',
|
|
113
|
+
'.swift': 'text/x-swift',
|
|
114
|
+
'.kt': 'text/x-kotlin',
|
|
115
|
+
'.scala': 'text/x-scala',
|
|
116
|
+
'.sh': 'application/x-sh',
|
|
117
|
+
'.bash': 'application/x-sh',
|
|
118
|
+
'.zsh': 'application/x-sh',
|
|
119
|
+
'.ps1': 'application/x-powershell',
|
|
120
|
+
'.bat': 'application/x-msdos-program',
|
|
121
|
+
'.cmd': 'application/x-msdos-program',
|
|
122
|
+
// 数据文件
|
|
123
|
+
'.db': 'application/x-sqlite3',
|
|
124
|
+
'.sqlite': 'application/x-sqlite3',
|
|
125
|
+
'.sql': 'application/sql',
|
|
126
|
+
// 其他
|
|
127
|
+
'.exe': 'application/x-msdownload',
|
|
128
|
+
'.dll': 'application/x-msdownload',
|
|
129
|
+
'.so': 'application/x-sharedlib',
|
|
130
|
+
'.dylib': 'application/x-mach-binary',
|
|
131
|
+
'.deb': 'application/vnd.debian.binary-package',
|
|
132
|
+
'.rpm': 'application/x-rpm',
|
|
133
|
+
'.apk': 'application/vnd.android.package-archive',
|
|
134
|
+
'.ipa': 'application/octet-stream',
|
|
135
|
+
'.dmg': 'application/x-apple-diskimage',
|
|
136
|
+
'.iso': 'application/x-iso9660-image',
|
|
137
|
+
};
|
|
138
|
+
/**
|
|
139
|
+
* 获取文件的 MIME 类型
|
|
140
|
+
*/
|
|
141
|
+
function getMimeType(filePath) {
|
|
142
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
143
|
+
return MIME_TYPES[ext] || '';
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* 类型守卫:判断是否为 ClipboardFileData
|
|
147
|
+
*/
|
|
148
|
+
function isClipboardFileData(value) {
|
|
149
|
+
return (typeof value === 'object' &&
|
|
150
|
+
value !== null &&
|
|
151
|
+
!Array.isArray(value) &&
|
|
152
|
+
'name' in value &&
|
|
153
|
+
'path' in value);
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* ArrayBuffer 转 Buffer
|
|
157
|
+
*/
|
|
158
|
+
function arrayBufferToBuffer(arrayBuffer) {
|
|
159
|
+
return Buffer.from(new Uint8Array(arrayBuffer));
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Buffer 转 ArrayBuffer
|
|
163
|
+
*/
|
|
164
|
+
function bufferToArrayBuffer(buffer) {
|
|
165
|
+
return new Uint8Array(buffer).buffer;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* 获取临时目录路径(缓存)
|
|
169
|
+
*/
|
|
170
|
+
let cachedTempDir = null;
|
|
171
|
+
function getTempDir() {
|
|
172
|
+
if (!cachedTempDir) {
|
|
173
|
+
cachedTempDir = electron.app.getPath('temp');
|
|
174
|
+
}
|
|
175
|
+
return cachedTempDir;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* 安全读取剪贴板操作(统一错误处理)
|
|
179
|
+
*/
|
|
180
|
+
function safeClipboardOperation(operation, defaultValue, errorMessage) {
|
|
181
|
+
try {
|
|
182
|
+
return operation();
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
console.error(errorMessage, error);
|
|
186
|
+
return defaultValue;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* 解析文件 URI 为本地路径
|
|
191
|
+
*/
|
|
192
|
+
function parseFileUri(uri) {
|
|
193
|
+
const cleanUri = uri.trim();
|
|
194
|
+
if (cleanUri.startsWith(FILE_URI_PREFIX)) {
|
|
195
|
+
try {
|
|
196
|
+
return decodeURIComponent(cleanUri.substring(FILE_URI_PREFIX.length));
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
console.error('URI 解码失败:', error);
|
|
200
|
+
return cleanUri.substring(FILE_URI_PREFIX.length);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
try {
|
|
204
|
+
return decodeURIComponent(cleanUri);
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
console.error('URI 解码失败:', error);
|
|
208
|
+
return cleanUri;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* 规范化文件路径(移除 file:// 前缀,处理相对路径等)
|
|
213
|
+
*/
|
|
214
|
+
function normalizeFilePath(filePath) {
|
|
215
|
+
const normalized = parseFileUri(filePath);
|
|
216
|
+
return path.isAbsolute(normalized) ? normalized : path.resolve(normalized);
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* 判断字符串是否为 URL(更严格的判断)
|
|
220
|
+
*/
|
|
221
|
+
function isUrl(str) {
|
|
222
|
+
if (!str || typeof str !== 'string') {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
const trimmed = str.trim();
|
|
226
|
+
if (trimmed.length === 0) {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
try {
|
|
230
|
+
const url = new URL(trimmed);
|
|
231
|
+
// 必须是 http 或 https 协议
|
|
232
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
// 必须有 hostname
|
|
236
|
+
if (!url.hostname || url.hostname.length === 0) {
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
// 避免将本地文件路径误判为 URL(如 C:\path\to\file)
|
|
240
|
+
if (url.hostname.includes('\\') || url.hostname.includes(':')) {
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* 检查剪贴板是否有图片格式
|
|
251
|
+
*/
|
|
252
|
+
function hasImageFormat() {
|
|
253
|
+
try {
|
|
254
|
+
const formats = electron.clipboard.availableFormats();
|
|
255
|
+
// 检查是否有图片格式
|
|
256
|
+
for (const format of formats) {
|
|
257
|
+
if (!format)
|
|
258
|
+
continue;
|
|
259
|
+
const lowerFormat = format.toLowerCase();
|
|
260
|
+
// 检查是否匹配图片格式
|
|
261
|
+
if (IMAGE_FORMATS.some(imgFormat => lowerFormat.includes(imgFormat.toLowerCase()))) {
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
264
|
+
// 检查常见的图片格式标识
|
|
265
|
+
if (lowerFormat.startsWith('image/')) {
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
// Electron 的 readImage 可能在没有明确格式时也能读取
|
|
270
|
+
// 尝试读取一次来验证(但不实际读取数据,只检查是否为空)
|
|
271
|
+
try {
|
|
272
|
+
const image = electron.clipboard.readImage();
|
|
273
|
+
if (!image.isEmpty()) {
|
|
274
|
+
return true;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
// 读取失败,说明不是图片
|
|
279
|
+
}
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* 检查剪贴板是否有文件格式
|
|
288
|
+
*/
|
|
289
|
+
function hasFileFormat() {
|
|
290
|
+
try {
|
|
291
|
+
const formats = electron.clipboard.availableFormats();
|
|
292
|
+
if (PLATFORM === 'win32') {
|
|
293
|
+
// Windows: 检查 FileNameW 格式
|
|
294
|
+
return formats.includes(WINDOWS_FILE_FORMAT);
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
// macOS/Linux: 检查文件 URI 格式
|
|
298
|
+
for (const format of UNIX_FILE_FORMATS) {
|
|
299
|
+
if (formats.includes(format)) {
|
|
300
|
+
return true;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
// 也检查文本中是否包含 file:// URI
|
|
304
|
+
try {
|
|
305
|
+
const text = electron.clipboard.readText();
|
|
306
|
+
if (text && text.includes(FILE_URI_PREFIX)) {
|
|
307
|
+
// 进一步验证:检查是否真的是文件路径
|
|
308
|
+
const lines = text.split('\n').filter(Boolean);
|
|
309
|
+
for (const line of lines) {
|
|
310
|
+
if (line.trim().startsWith(FILE_URI_PREFIX)) {
|
|
311
|
+
const filePath = parseFileUri(line.trim());
|
|
312
|
+
const normalizedPath = normalizeFilePath(filePath);
|
|
313
|
+
if (fs.existsSync(normalizedPath)) {
|
|
314
|
+
const stat = fs.statSync(normalizedPath);
|
|
315
|
+
if (stat.isFile()) {
|
|
316
|
+
return true;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
catch {
|
|
324
|
+
// 忽略错误
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
329
|
+
catch {
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* 检查剪贴板内容是否为 URL(基于格式判断,不读取内容)
|
|
335
|
+
*/
|
|
336
|
+
function hasUrlFormat() {
|
|
337
|
+
try {
|
|
338
|
+
const formats = electron.clipboard.availableFormats();
|
|
339
|
+
// 检查是否有 URL 相关的格式
|
|
340
|
+
if (formats.includes('text/uri-list') || formats.includes('text/x-moz-url')) {
|
|
341
|
+
return true;
|
|
342
|
+
}
|
|
343
|
+
// 注意:这里不读取文本内容来判断,因为读取会消耗剪贴板
|
|
344
|
+
// URL 检测将在 handleGetUrl 中进行
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
catch {
|
|
348
|
+
return false;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* 从 URL 下载文件到本地
|
|
353
|
+
*/
|
|
354
|
+
async function downloadFileFromUrl(url, outputPath) {
|
|
355
|
+
return new Promise((resolve, reject) => {
|
|
356
|
+
const lib = url.startsWith('https') ? https : http;
|
|
357
|
+
const file = fs.createWriteStream(outputPath);
|
|
358
|
+
let request;
|
|
359
|
+
const cleanup = () => {
|
|
360
|
+
if (file) {
|
|
361
|
+
file.close();
|
|
362
|
+
if (fs.existsSync(outputPath)) {
|
|
363
|
+
fs.unlinkSync(outputPath);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
if (request) {
|
|
367
|
+
request.destroy();
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
request = lib.get(url, (res) => {
|
|
371
|
+
if (res.statusCode && res.statusCode !== 200) {
|
|
372
|
+
res.resume(); // 消费响应数据以释放内存
|
|
373
|
+
cleanup();
|
|
374
|
+
reject(new Error(`下载失败,状态码: ${res.statusCode} for ${url}`));
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
res.pipe(file);
|
|
378
|
+
file.on('finish', () => {
|
|
379
|
+
file.close((err) => {
|
|
380
|
+
if (err) {
|
|
381
|
+
cleanup();
|
|
382
|
+
reject(new Error(`关闭文件流失败: ${err.message}`));
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
resolve();
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
file.on('error', (err) => {
|
|
389
|
+
cleanup();
|
|
390
|
+
reject(new Error(`写入文件失败: ${err.message}`));
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
request.on('error', (err) => {
|
|
394
|
+
cleanup();
|
|
395
|
+
reject(new Error(`下载请求失败: ${err.message}`));
|
|
396
|
+
});
|
|
397
|
+
request.setTimeout(DOWNLOAD_TIMEOUT, () => {
|
|
398
|
+
cleanup();
|
|
399
|
+
reject(new Error('下载超时'));
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* 从 Windows 剪贴板读取文件路径
|
|
405
|
+
*/
|
|
406
|
+
function readWindowsClipboardFiles() {
|
|
407
|
+
try {
|
|
408
|
+
const buffer = electron.clipboard.readBuffer('FileNameW');
|
|
409
|
+
if (buffer.length === 0) {
|
|
410
|
+
return [];
|
|
411
|
+
}
|
|
412
|
+
// Windows 使用 UCS-2 编码,以 \u0000 分隔多个文件路径
|
|
413
|
+
const content = buffer.toString('ucs2');
|
|
414
|
+
return content.split('\u0000').filter(Boolean).map(normalizeFilePath);
|
|
415
|
+
}
|
|
416
|
+
catch (error) {
|
|
417
|
+
return [];
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* 从 macOS/Linux 剪贴板读取文件路径
|
|
422
|
+
*/
|
|
423
|
+
function readUnixClipboardFiles() {
|
|
424
|
+
const filePaths = [];
|
|
425
|
+
// 方法1: 尝试从文本中解析 file:// URI
|
|
426
|
+
try {
|
|
427
|
+
const text = electron.clipboard.readText();
|
|
428
|
+
if (text && text.includes(FILE_URI_PREFIX)) {
|
|
429
|
+
const uris = text.split('\n').filter(Boolean);
|
|
430
|
+
filePaths.push(...uris.map(parseFileUri).map(normalizeFilePath));
|
|
431
|
+
if (filePaths.length > 0) {
|
|
432
|
+
return filePaths;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
catch (error) {
|
|
437
|
+
// 继续尝试其他方法
|
|
438
|
+
}
|
|
439
|
+
// 方法2: 尝试从剪贴板格式列表中读取文件格式
|
|
440
|
+
try {
|
|
441
|
+
const formats = electron.clipboard.availableFormats();
|
|
442
|
+
for (const format of UNIX_FILE_FORMATS) {
|
|
443
|
+
if (formats.includes(format)) {
|
|
444
|
+
try {
|
|
445
|
+
const buffer = electron.clipboard.readBuffer(format);
|
|
446
|
+
if (buffer.length > 0) {
|
|
447
|
+
const text = buffer.toString('utf8');
|
|
448
|
+
const uris = text.split('\n').filter(Boolean);
|
|
449
|
+
filePaths.push(...uris.map(parseFileUri).map(normalizeFilePath));
|
|
450
|
+
if (filePaths.length > 0) {
|
|
451
|
+
return filePaths;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
catch (error) {
|
|
456
|
+
// 继续尝试下一个格式
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
catch (error) {
|
|
462
|
+
// 忽略错误
|
|
463
|
+
}
|
|
464
|
+
return filePaths;
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* 从系统剪贴板读取文件路径
|
|
468
|
+
* 支持 Windows (FileNameW) 和 macOS/Linux (text/uri-list)
|
|
469
|
+
*/
|
|
470
|
+
function readClipboardFiles() {
|
|
471
|
+
try {
|
|
472
|
+
if (PLATFORM === 'win32') {
|
|
473
|
+
return readWindowsClipboardFiles();
|
|
474
|
+
}
|
|
475
|
+
else {
|
|
476
|
+
return readUnixClipboardFiles();
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
catch (error) {
|
|
480
|
+
console.error('读取剪贴板文件失败:', error);
|
|
481
|
+
return [];
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* 将文件路径转换为 URI 格式
|
|
486
|
+
*/
|
|
487
|
+
function filePathToUri(filePath) {
|
|
488
|
+
return filePath.startsWith(FILE_URI_PREFIX) ? filePath : `${FILE_URI_PREFIX}${filePath}`;
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* 将文件路径写入 Windows 剪贴板
|
|
492
|
+
*/
|
|
493
|
+
function writeWindowsClipboardFiles(filePaths) {
|
|
494
|
+
try {
|
|
495
|
+
// Windows 使用 UCS-2 编码,多个文件路径用 \u0000 分隔,最后以 \u0000\u0000 结尾
|
|
496
|
+
const content = filePaths.join('\u0000') + '\u0000\u0000';
|
|
497
|
+
electron.clipboard.writeBuffer('FileNameW', Buffer.from(content, 'ucs2'));
|
|
498
|
+
}
|
|
499
|
+
catch (error) {
|
|
500
|
+
console.error('写入文件到 Windows 剪贴板失败:', error);
|
|
501
|
+
throw error;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* 将文件路径写入 macOS/Linux 剪贴板
|
|
506
|
+
*/
|
|
507
|
+
function writeUnixClipboardFiles(filePaths) {
|
|
508
|
+
try {
|
|
509
|
+
// macOS/Linux 使用 URI 列表格式
|
|
510
|
+
const uriList = filePaths.map(filePathToUri).join('\n');
|
|
511
|
+
electron.clipboard.writeText(uriList);
|
|
512
|
+
// 注意:macOS 的文件剪贴板需要使用原生 API (NSFilenamesPboardType)
|
|
513
|
+
// 当前实现可能无法让系统完全识别为文件,需要进一步优化
|
|
514
|
+
}
|
|
515
|
+
catch (error) {
|
|
516
|
+
console.error('写入文件到 Unix 剪贴板失败:', error);
|
|
517
|
+
throw error;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* 将文件路径写入系统剪贴板
|
|
522
|
+
* 支持 Windows (FileNameW) 和 macOS/Linux (text/uri-list)
|
|
523
|
+
*/
|
|
524
|
+
function writeClipboardFiles(filePaths) {
|
|
525
|
+
if (filePaths.length === 0) {
|
|
526
|
+
return false;
|
|
527
|
+
}
|
|
528
|
+
try {
|
|
529
|
+
if (PLATFORM === 'win32') {
|
|
530
|
+
writeWindowsClipboardFiles(filePaths);
|
|
531
|
+
}
|
|
532
|
+
else {
|
|
533
|
+
writeUnixClipboardFiles(filePaths);
|
|
534
|
+
}
|
|
535
|
+
return true;
|
|
536
|
+
}
|
|
537
|
+
catch (error) {
|
|
538
|
+
console.error('写入文件到剪贴板失败:', error);
|
|
539
|
+
return false;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* 读取文件信息并返回 File 对象需要的数据
|
|
544
|
+
*/
|
|
545
|
+
function readFileData(filePath, options = {}) {
|
|
546
|
+
const { includeBuffer = true, maxFileSize = MAX_FILE_SIZE } = options;
|
|
547
|
+
try {
|
|
548
|
+
const normalizedPath = normalizeFilePath(filePath);
|
|
549
|
+
if (!fs.existsSync(normalizedPath)) {
|
|
550
|
+
console.warn('文件不存在:', normalizedPath);
|
|
551
|
+
return null;
|
|
552
|
+
}
|
|
553
|
+
const stat = fs.statSync(normalizedPath);
|
|
554
|
+
// 检查是否为文件(不是目录)
|
|
555
|
+
if (!stat.isFile()) {
|
|
556
|
+
console.warn('路径不是文件:', normalizedPath);
|
|
557
|
+
return null;
|
|
558
|
+
}
|
|
559
|
+
const fileData = {
|
|
560
|
+
name: path.basename(normalizedPath),
|
|
561
|
+
path: normalizedPath,
|
|
562
|
+
size: stat.size,
|
|
563
|
+
type: getMimeType(normalizedPath),
|
|
564
|
+
lastModified: stat.mtimeMs,
|
|
565
|
+
};
|
|
566
|
+
// 根据选项决定是否读取文件内容
|
|
567
|
+
if (includeBuffer && stat.size <= maxFileSize) {
|
|
568
|
+
try {
|
|
569
|
+
const buffer = fs.readFileSync(normalizedPath);
|
|
570
|
+
fileData.buffer = bufferToArrayBuffer(buffer);
|
|
571
|
+
}
|
|
572
|
+
catch (error) {
|
|
573
|
+
console.error('读取文件内容失败:', error);
|
|
574
|
+
// 即使读取失败,也返回文件基本信息
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
return fileData;
|
|
578
|
+
}
|
|
579
|
+
catch (error) {
|
|
580
|
+
console.error('读取文件失败:', error);
|
|
581
|
+
return null;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* 从文件名或 URL 中提取文件扩展名
|
|
586
|
+
*/
|
|
587
|
+
function extractExtension(fileNameOrUrl) {
|
|
588
|
+
try {
|
|
589
|
+
// 如果是 URL,尝试从 URL 路径中获取扩展名
|
|
590
|
+
if (isUrl(fileNameOrUrl)) {
|
|
591
|
+
const url = new URL(fileNameOrUrl);
|
|
592
|
+
const urlPath = url.pathname;
|
|
593
|
+
const ext = path.extname(urlPath);
|
|
594
|
+
if (ext) {
|
|
595
|
+
return ext;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
// 否则从文件名获取扩展名
|
|
599
|
+
return path.extname(fileNameOrUrl);
|
|
600
|
+
}
|
|
601
|
+
catch {
|
|
602
|
+
return '';
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* 生成临时文件名(保留原始扩展名)
|
|
607
|
+
*/
|
|
608
|
+
function generateTempFileName(originalName, url) {
|
|
609
|
+
const timestamp = Date.now();
|
|
610
|
+
const random = Math.random().toString(36).substring(7);
|
|
611
|
+
if (originalName) {
|
|
612
|
+
const ext = path.extname(originalName);
|
|
613
|
+
const nameWithoutExt = path.basename(originalName, ext) || 'file';
|
|
614
|
+
return `${nameWithoutExt}_${timestamp}_${random}${ext}`;
|
|
615
|
+
}
|
|
616
|
+
if (url) {
|
|
617
|
+
const ext = extractExtension(url);
|
|
618
|
+
return `clipboard_${timestamp}_${random}${ext}`;
|
|
619
|
+
}
|
|
620
|
+
return `clipboard_${timestamp}_${random}`;
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* 将 File 对象保存到临时目录并返回文件路径
|
|
624
|
+
* 支持本地路径、URL 和 buffer
|
|
625
|
+
*/
|
|
626
|
+
async function saveFileToTemp(fileData) {
|
|
627
|
+
try {
|
|
628
|
+
const tempDir = getTempDir();
|
|
629
|
+
// 如果已经有 buffer,直接保存到临时目录
|
|
630
|
+
if (fileData.buffer) {
|
|
631
|
+
const fileName = generateTempFileName(fileData.name, fileData.path);
|
|
632
|
+
const tempPath = path.join(tempDir, fileName);
|
|
633
|
+
const buffer = arrayBufferToBuffer(fileData.buffer);
|
|
634
|
+
fs.writeFileSync(tempPath, buffer);
|
|
635
|
+
return tempPath;
|
|
636
|
+
}
|
|
637
|
+
// 如果 path 是 URL,需要先下载
|
|
638
|
+
if (fileData.path && isUrl(fileData.path)) {
|
|
639
|
+
const fileName = generateTempFileName(fileData.name, fileData.path);
|
|
640
|
+
const tempPath = path.join(tempDir, fileName);
|
|
641
|
+
try {
|
|
642
|
+
await downloadFileFromUrl(fileData.path, tempPath);
|
|
643
|
+
return tempPath;
|
|
644
|
+
}
|
|
645
|
+
catch (error) {
|
|
646
|
+
console.error('从 URL 下载文件失败:', error);
|
|
647
|
+
return null;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
// 如果 path 是本地路径,检查文件是否存在
|
|
651
|
+
if (fileData.path) {
|
|
652
|
+
const normalizedPath = normalizeFilePath(fileData.path);
|
|
653
|
+
if (fs.existsSync(normalizedPath)) {
|
|
654
|
+
const stat = fs.statSync(normalizedPath);
|
|
655
|
+
if (stat.isFile()) {
|
|
656
|
+
return normalizedPath;
|
|
657
|
+
}
|
|
658
|
+
// 如果是目录,返回 null(目录不能复制到剪贴板)
|
|
659
|
+
console.warn('路径是目录,不是文件:', normalizedPath);
|
|
660
|
+
return null;
|
|
661
|
+
}
|
|
662
|
+
// 文件不存在
|
|
663
|
+
console.warn('文件不存在:', normalizedPath);
|
|
664
|
+
return null;
|
|
665
|
+
}
|
|
666
|
+
// 既没有 buffer 也没有有效路径
|
|
667
|
+
return null;
|
|
668
|
+
}
|
|
669
|
+
catch (error) {
|
|
670
|
+
console.error('保存文件到临时目录失败:', error);
|
|
671
|
+
return null;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* 读取文本类型
|
|
676
|
+
*/
|
|
677
|
+
function handleGetText() {
|
|
678
|
+
return safeClipboardOperation(() => electron.clipboard.readText(), '', '读取文本失败:');
|
|
679
|
+
}
|
|
680
|
+
/**
|
|
681
|
+
* 读取 HTML 类型
|
|
682
|
+
*/
|
|
683
|
+
function handleGetHtml() {
|
|
684
|
+
return safeClipboardOperation(() => {
|
|
685
|
+
const html = electron.clipboard.readHTML();
|
|
686
|
+
return { type: 'html', html: html || '' };
|
|
687
|
+
}, { type: 'html', html: '' }, '读取 HTML 失败:');
|
|
688
|
+
}
|
|
689
|
+
/**
|
|
690
|
+
* 从剪贴板图片创建文件数据(可重用逻辑)
|
|
691
|
+
*/
|
|
692
|
+
function createImageFileDataFromClipboard(options) {
|
|
693
|
+
try {
|
|
694
|
+
const image = electron.clipboard.readImage();
|
|
695
|
+
if (image.isEmpty()) {
|
|
696
|
+
return null;
|
|
697
|
+
}
|
|
698
|
+
const pngBuffer = image.toPNG();
|
|
699
|
+
const arrayBuffer = bufferToArrayBuffer(pngBuffer);
|
|
700
|
+
const tempDir = getTempDir();
|
|
701
|
+
const fileName = `clipboard_image_${Date.now()}.png`;
|
|
702
|
+
const tempPath = path.join(tempDir, fileName);
|
|
703
|
+
fs.writeFileSync(tempPath, pngBuffer);
|
|
704
|
+
const fileData = {
|
|
705
|
+
name: fileName,
|
|
706
|
+
path: tempPath,
|
|
707
|
+
size: pngBuffer.length,
|
|
708
|
+
type: 'image/png',
|
|
709
|
+
lastModified: Date.now(),
|
|
710
|
+
};
|
|
711
|
+
// 如果选项允许,包含 buffer
|
|
712
|
+
const maxSize = options?.maxFileSize || MAX_FILE_SIZE;
|
|
713
|
+
if (options?.includeBuffer !== false && arrayBuffer.byteLength <= maxSize) {
|
|
714
|
+
fileData.buffer = arrayBuffer;
|
|
715
|
+
}
|
|
716
|
+
return fileData;
|
|
717
|
+
}
|
|
718
|
+
catch (error) {
|
|
719
|
+
console.error('创建图片文件数据失败:', error);
|
|
720
|
+
return null;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* 读取图片类型
|
|
725
|
+
*/
|
|
726
|
+
function handleGetImage(options) {
|
|
727
|
+
// 先检查是否有图片格式,避免误判
|
|
728
|
+
if (!hasImageFormat()) {
|
|
729
|
+
// 即使没有明确的图片格式,也尝试读取(Electron 可能能读取)
|
|
730
|
+
// 但如果读取失败,返回空结果
|
|
731
|
+
const imageData = createImageFileDataFromClipboard(options);
|
|
732
|
+
if (imageData) {
|
|
733
|
+
return { type: 'image', image: imageData };
|
|
734
|
+
}
|
|
735
|
+
return { type: 'image' };
|
|
736
|
+
}
|
|
737
|
+
// 有图片格式,尝试读取
|
|
738
|
+
const imageData = createImageFileDataFromClipboard(options);
|
|
739
|
+
if (imageData) {
|
|
740
|
+
return { type: 'image', image: imageData };
|
|
741
|
+
}
|
|
742
|
+
return { type: 'image' };
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* 读取 URL 类型
|
|
746
|
+
*/
|
|
747
|
+
function handleGetUrl() {
|
|
748
|
+
try {
|
|
749
|
+
// 先检查是否有 URL 格式
|
|
750
|
+
if (!hasUrlFormat()) {
|
|
751
|
+
return { type: 'url' };
|
|
752
|
+
}
|
|
753
|
+
// 检查是否有 URL 相关的格式
|
|
754
|
+
const formats = electron.clipboard.availableFormats();
|
|
755
|
+
// 优先检查 text/uri-list 或 text/x-moz-url 格式
|
|
756
|
+
if (formats.includes('text/uri-list') || formats.includes('text/x-moz-url')) {
|
|
757
|
+
try {
|
|
758
|
+
const uriList = electron.clipboard.readText();
|
|
759
|
+
if (uriList) {
|
|
760
|
+
const uris = uriList.split('\n').filter(Boolean);
|
|
761
|
+
// 查找第一个有效的 HTTP/HTTPS URL
|
|
762
|
+
for (const uri of uris) {
|
|
763
|
+
const trimmed = uri.trim();
|
|
764
|
+
if (isUrl(trimmed)) {
|
|
765
|
+
return { type: 'url', url: trimmed };
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
catch {
|
|
771
|
+
// 忽略错误,继续尝试其他方法
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
// 尝试从普通文本中读取 URL
|
|
775
|
+
try {
|
|
776
|
+
const text = electron.clipboard.readText();
|
|
777
|
+
if (text) {
|
|
778
|
+
const trimmed = text.trim();
|
|
779
|
+
// 检查整段文本是否为 URL
|
|
780
|
+
if (isUrl(trimmed)) {
|
|
781
|
+
return { type: 'url', url: trimmed };
|
|
782
|
+
}
|
|
783
|
+
// 尝试从文本中提取 URL(简单模式:查找 http:// 或 https://)
|
|
784
|
+
const urlMatch = trimmed.match(/https?:\/\/[^\s]+/);
|
|
785
|
+
if (urlMatch && urlMatch[0] && isUrl(urlMatch[0])) {
|
|
786
|
+
return { type: 'url', url: urlMatch[0] };
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
catch {
|
|
791
|
+
// 忽略错误
|
|
792
|
+
}
|
|
793
|
+
return { type: 'url' };
|
|
794
|
+
}
|
|
795
|
+
catch (error) {
|
|
796
|
+
console.error('读取 URL 失败:', error);
|
|
797
|
+
return { type: 'url' };
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
/**
|
|
801
|
+
* 读取 Buffer 类型
|
|
802
|
+
*/
|
|
803
|
+
function handleGetBuffer() {
|
|
804
|
+
return safeClipboardOperation(() => {
|
|
805
|
+
const formats = electron.clipboard.availableFormats();
|
|
806
|
+
if (formats.length === 0 || !formats[0]) {
|
|
807
|
+
return { type: 'buffer' };
|
|
808
|
+
}
|
|
809
|
+
const buffer = electron.clipboard.readBuffer(formats[0]);
|
|
810
|
+
if (buffer.length > 0) {
|
|
811
|
+
const arrayBuffer = bufferToArrayBuffer(buffer);
|
|
812
|
+
return { type: 'buffer', buffer: arrayBuffer };
|
|
813
|
+
}
|
|
814
|
+
return { type: 'buffer' };
|
|
815
|
+
}, { type: 'buffer' }, '读取 Buffer 失败:');
|
|
816
|
+
}
|
|
817
|
+
/**
|
|
818
|
+
* 读取文件类型(单个)
|
|
819
|
+
*/
|
|
820
|
+
function handleGetFile(options) {
|
|
821
|
+
// 先检查是否有文件格式,避免误判
|
|
822
|
+
if (!hasFileFormat()) {
|
|
823
|
+
return { type: 'file' };
|
|
824
|
+
}
|
|
825
|
+
const filePaths = readClipboardFiles();
|
|
826
|
+
if (filePaths.length === 0 || !filePaths[0]) {
|
|
827
|
+
return { type: 'file' };
|
|
828
|
+
}
|
|
829
|
+
const fileData = readFileData(filePaths[0], options);
|
|
830
|
+
if (!fileData) {
|
|
831
|
+
return { type: 'file' };
|
|
832
|
+
}
|
|
833
|
+
return { type: 'file', file: fileData };
|
|
834
|
+
}
|
|
835
|
+
/**
|
|
836
|
+
* 自动检测类型(基于格式检测,优先文件,然后是图片、URL,最后是文本)
|
|
837
|
+
*/
|
|
838
|
+
function handleGetAuto(options) {
|
|
839
|
+
// 先获取剪贴板格式,避免重复调用
|
|
840
|
+
let formats = [];
|
|
841
|
+
try {
|
|
842
|
+
formats = electron.clipboard.availableFormats();
|
|
843
|
+
}
|
|
844
|
+
catch {
|
|
845
|
+
// 如果获取格式失败,降级到文本
|
|
846
|
+
const text = handleGetText();
|
|
847
|
+
return { type: 'text', text: text || '' };
|
|
848
|
+
}
|
|
849
|
+
// 1. 优先检测文件(基于格式检测)
|
|
850
|
+
if (hasFileFormat()) {
|
|
851
|
+
const filePaths = readClipboardFiles();
|
|
852
|
+
if (filePaths.length > 0) {
|
|
853
|
+
if (filePaths.length === 1) {
|
|
854
|
+
const firstPath = filePaths[0];
|
|
855
|
+
if (firstPath) {
|
|
856
|
+
const fileData = readFileData(firstPath, options);
|
|
857
|
+
if (fileData) {
|
|
858
|
+
return { type: 'file', file: fileData };
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
else {
|
|
863
|
+
// 多个文件
|
|
864
|
+
const files = filePaths
|
|
865
|
+
.map(filePath => readFileData(filePath, options))
|
|
866
|
+
.filter((file) => file !== null);
|
|
867
|
+
if (files.length > 0) {
|
|
868
|
+
return { type: 'files', files };
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
// 2. 检测图片(基于格式检测)
|
|
874
|
+
if (hasImageFormat()) {
|
|
875
|
+
const imageData = createImageFileDataFromClipboard(options);
|
|
876
|
+
if (imageData) {
|
|
877
|
+
return { type: 'image', image: imageData };
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
// 3. 检测 URL(基于格式检测,或文本内容判断)
|
|
881
|
+
// 优先检查是否有 URL 格式
|
|
882
|
+
if (hasUrlFormat()) {
|
|
883
|
+
const urlResult = handleGetUrl();
|
|
884
|
+
if (urlResult.url) {
|
|
885
|
+
return urlResult;
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
// 4. 检测 HTML(检查格式)
|
|
889
|
+
if (formats.includes('text/html') || formats.some(f => f && f.toLowerCase().includes('html'))) {
|
|
890
|
+
const htmlData = safeClipboardOperation(() => electron.clipboard.readHTML(), '', '读取 HTML 失败:');
|
|
891
|
+
if (htmlData && htmlData.trim().length > 0) {
|
|
892
|
+
return { type: 'html', html: htmlData };
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
// 5. 最后读取文本,并检查是否为 URL
|
|
896
|
+
const text = safeClipboardOperation(() => electron.clipboard.readText(), '', '读取文本失败:');
|
|
897
|
+
if (text && text.trim().length > 0) {
|
|
898
|
+
const trimmed = text.trim();
|
|
899
|
+
// 如果文本是纯 URL(单行且是有效的 URL),且之前没有检测到 URL 格式
|
|
900
|
+
// 则判断为 URL 类型
|
|
901
|
+
if (!trimmed.includes('\n') && isUrl(trimmed)) {
|
|
902
|
+
return { type: 'url', url: trimmed };
|
|
903
|
+
}
|
|
904
|
+
// 否则返回文本类型
|
|
905
|
+
return { type: 'text', text: trimmed };
|
|
906
|
+
}
|
|
907
|
+
return { type: 'text', text: '' };
|
|
908
|
+
}
|
|
909
|
+
/**
|
|
910
|
+
* 判断字符串是否为文件路径或 URL
|
|
911
|
+
*/
|
|
912
|
+
function isFilePathOrUrl(str) {
|
|
913
|
+
// 检查是否为 URL (http/https)
|
|
914
|
+
if (isUrl(str)) {
|
|
915
|
+
return true;
|
|
916
|
+
}
|
|
917
|
+
// 检查是否为 file:// URI
|
|
918
|
+
if (str.startsWith(FILE_URI_PREFIX)) {
|
|
919
|
+
return true;
|
|
920
|
+
}
|
|
921
|
+
// 检查是否为本地文件路径(绝对路径或相对路径)
|
|
922
|
+
try {
|
|
923
|
+
const normalizedPath = normalizeFilePath(str);
|
|
924
|
+
if (fs.existsSync(normalizedPath)) {
|
|
925
|
+
const stat = fs.statSync(normalizedPath);
|
|
926
|
+
return stat.isFile();
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
catch {
|
|
930
|
+
// 忽略错误
|
|
931
|
+
}
|
|
932
|
+
return false;
|
|
933
|
+
}
|
|
934
|
+
/**
|
|
935
|
+
* 处理字符串路径/URL:下载到临时文件后写入剪贴板
|
|
936
|
+
*/
|
|
937
|
+
async function handleSetStringAsFile(input) {
|
|
938
|
+
try {
|
|
939
|
+
let tempPath = null;
|
|
940
|
+
// 如果是 http/https URL,下载到临时文件
|
|
941
|
+
if (isUrl(input)) {
|
|
942
|
+
const tempDir = electron.app.getPath('temp');
|
|
943
|
+
const fileName = generateTempFileName(undefined, input);
|
|
944
|
+
tempPath = path.join(tempDir, fileName);
|
|
945
|
+
try {
|
|
946
|
+
await downloadFileFromUrl(input, tempPath);
|
|
947
|
+
}
|
|
948
|
+
catch (error) {
|
|
949
|
+
console.error('从 URL 下载文件失败:', error);
|
|
950
|
+
return false;
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
// 如果是 file:// URI 或本地路径
|
|
954
|
+
else {
|
|
955
|
+
console.log('error input', input);
|
|
956
|
+
const normalizedPath = normalizeFilePath(input);
|
|
957
|
+
if (!fs.existsSync(normalizedPath)) {
|
|
958
|
+
console.warn('文件不存在:', normalizedPath);
|
|
959
|
+
return false;
|
|
960
|
+
}
|
|
961
|
+
const stat = fs.statSync(normalizedPath);
|
|
962
|
+
if (!stat.isFile()) {
|
|
963
|
+
console.warn('路径不是文件:', normalizedPath);
|
|
964
|
+
return false;
|
|
965
|
+
}
|
|
966
|
+
tempPath = normalizedPath;
|
|
967
|
+
}
|
|
968
|
+
if (!tempPath) {
|
|
969
|
+
return false;
|
|
970
|
+
}
|
|
971
|
+
// 将临时文件路径写入剪贴板
|
|
972
|
+
return writeClipboardFiles([tempPath]);
|
|
973
|
+
}
|
|
974
|
+
catch (error) {
|
|
975
|
+
console.error('处理文件路径/URL 失败:', error);
|
|
976
|
+
return false;
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
/**
|
|
980
|
+
* 写入文本
|
|
981
|
+
*/
|
|
982
|
+
function handleSetText(text) {
|
|
983
|
+
return safeClipboardOperation(() => {
|
|
984
|
+
electron.clipboard.writeText(text);
|
|
985
|
+
return true;
|
|
986
|
+
}, false, '写入文本到剪贴板失败:');
|
|
987
|
+
}
|
|
988
|
+
/**
|
|
989
|
+
* 写入 HTML
|
|
990
|
+
*/
|
|
991
|
+
function handleSetHtml(html) {
|
|
992
|
+
return safeClipboardOperation(() => {
|
|
993
|
+
electron.clipboard.writeHTML(html);
|
|
994
|
+
return true;
|
|
995
|
+
}, false, '写入 HTML 到剪贴板失败:');
|
|
996
|
+
}
|
|
997
|
+
/**
|
|
998
|
+
* 写入图片
|
|
999
|
+
*/
|
|
1000
|
+
async function handleSetImage(imageData) {
|
|
1001
|
+
try {
|
|
1002
|
+
let image;
|
|
1003
|
+
if (typeof imageData === 'string') {
|
|
1004
|
+
// 检查是否是 HTTP/HTTPS URL
|
|
1005
|
+
if (imageData.startsWith('http://') || imageData.startsWith('https://')) {
|
|
1006
|
+
// 下载图片到临时文件
|
|
1007
|
+
const tempDir = getTempDir();
|
|
1008
|
+
const fileName = generateTempFileName(undefined, imageData);
|
|
1009
|
+
const tempPath = path.join(tempDir, fileName);
|
|
1010
|
+
await downloadFileFromUrl(imageData, tempPath);
|
|
1011
|
+
// 检查下载后的文件是否存在
|
|
1012
|
+
if (!fs.existsSync(tempPath)) {
|
|
1013
|
+
console.error('下载的图片文件不存在:', tempPath);
|
|
1014
|
+
return false;
|
|
1015
|
+
}
|
|
1016
|
+
image = electron.nativeImage.createFromPath(tempPath);
|
|
1017
|
+
}
|
|
1018
|
+
else if (imageData.startsWith('file://')) {
|
|
1019
|
+
// 处理 file:// 协议,去掉前缀
|
|
1020
|
+
const localPath = imageData.replace(/^file:\/\//, '');
|
|
1021
|
+
// 检查文件是否存在
|
|
1022
|
+
if (!fs.existsSync(localPath)) {
|
|
1023
|
+
console.error('图片文件不存在:', localPath);
|
|
1024
|
+
return false;
|
|
1025
|
+
}
|
|
1026
|
+
image = electron.nativeImage.createFromPath(localPath);
|
|
1027
|
+
}
|
|
1028
|
+
else if (imageData.startsWith('/')) {
|
|
1029
|
+
// 本地路径,直接使用
|
|
1030
|
+
// 检查文件是否存在
|
|
1031
|
+
if (!fs.existsSync(imageData)) {
|
|
1032
|
+
console.error('图片文件不存在:', imageData);
|
|
1033
|
+
return false;
|
|
1034
|
+
}
|
|
1035
|
+
image = electron.nativeImage.createFromPath(imageData);
|
|
1036
|
+
}
|
|
1037
|
+
else {
|
|
1038
|
+
// 其他情况,尝试直接使用
|
|
1039
|
+
// 检查文件是否存在
|
|
1040
|
+
if (!fs.existsSync(imageData)) {
|
|
1041
|
+
console.error('图片文件不存在:', imageData);
|
|
1042
|
+
return false;
|
|
1043
|
+
}
|
|
1044
|
+
image = electron.nativeImage.createFromPath(imageData);
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
else if (imageData instanceof ArrayBuffer) {
|
|
1048
|
+
const buffer = arrayBufferToBuffer(imageData);
|
|
1049
|
+
image = electron.nativeImage.createFromBuffer(buffer);
|
|
1050
|
+
}
|
|
1051
|
+
else {
|
|
1052
|
+
// 如果传入的是 ClipboardFileData
|
|
1053
|
+
const savedPath = await saveFileToTemp(imageData);
|
|
1054
|
+
if (!savedPath) {
|
|
1055
|
+
return false;
|
|
1056
|
+
}
|
|
1057
|
+
image = electron.nativeImage.createFromPath(savedPath);
|
|
1058
|
+
}
|
|
1059
|
+
if (!image.isEmpty()) {
|
|
1060
|
+
electron.clipboard.writeImage(image);
|
|
1061
|
+
return true;
|
|
1062
|
+
}
|
|
1063
|
+
return false;
|
|
1064
|
+
}
|
|
1065
|
+
catch (error) {
|
|
1066
|
+
console.error('写入图片到剪贴板失败:', error);
|
|
1067
|
+
return false;
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
/**
|
|
1071
|
+
* 写入 Buffer
|
|
1072
|
+
*/
|
|
1073
|
+
function handleSetBuffer(buffer) {
|
|
1074
|
+
return safeClipboardOperation(() => {
|
|
1075
|
+
const nodeBuffer = arrayBufferToBuffer(buffer);
|
|
1076
|
+
// Electron 的 clipboard.writeBuffer 需要指定格式
|
|
1077
|
+
// 使用 'application/octet-stream' 作为默认格式
|
|
1078
|
+
electron.clipboard.writeBuffer('application/octet-stream', nodeBuffer);
|
|
1079
|
+
return true;
|
|
1080
|
+
}, false, '写入 Buffer 到剪贴板失败:');
|
|
1081
|
+
}
|
|
1082
|
+
/**
|
|
1083
|
+
* 清空剪贴板
|
|
1084
|
+
*/
|
|
1085
|
+
function handleClear() {
|
|
1086
|
+
return safeClipboardOperation(() => {
|
|
1087
|
+
electron.clipboard.clear();
|
|
1088
|
+
return true;
|
|
1089
|
+
}, false, '清空剪贴板失败:');
|
|
1090
|
+
}
|
|
1091
|
+
/**
|
|
1092
|
+
* 写入单个文件
|
|
1093
|
+
*/
|
|
1094
|
+
async function handleSetFile(fileData) {
|
|
1095
|
+
const savedPath = await saveFileToTemp(fileData);
|
|
1096
|
+
if (!savedPath) {
|
|
1097
|
+
return false;
|
|
1098
|
+
}
|
|
1099
|
+
return writeClipboardFiles([savedPath]);
|
|
1100
|
+
}
|
|
1101
|
+
/**
|
|
1102
|
+
* 写入多个文件
|
|
1103
|
+
*/
|
|
1104
|
+
async function handleSetFiles(files) {
|
|
1105
|
+
if (files.length === 0) {
|
|
1106
|
+
return false;
|
|
1107
|
+
}
|
|
1108
|
+
const filePaths = [];
|
|
1109
|
+
for (const fileData of files) {
|
|
1110
|
+
const savedPath = await saveFileToTemp(fileData);
|
|
1111
|
+
if (savedPath) {
|
|
1112
|
+
filePaths.push(savedPath);
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
if (filePaths.length === 0) {
|
|
1116
|
+
return false;
|
|
1117
|
+
}
|
|
1118
|
+
return writeClipboardFiles(filePaths);
|
|
1119
|
+
}
|
|
1120
|
+
/**
|
|
1121
|
+
* 初始化剪贴板 IPC 处理器
|
|
1122
|
+
*/
|
|
1123
|
+
const initializeClipboard = () => {
|
|
1124
|
+
ipc.mainIPC.handleRenderer('core:clipboard', async (options) => {
|
|
1125
|
+
const { method, value, type = 'auto', options: fileOptions } = options;
|
|
1126
|
+
try {
|
|
1127
|
+
if (method === 'get') {
|
|
1128
|
+
// 从系统剪贴板读取内容
|
|
1129
|
+
switch (type) {
|
|
1130
|
+
case 'text':
|
|
1131
|
+
return handleGetText();
|
|
1132
|
+
case 'html':
|
|
1133
|
+
return handleGetHtml();
|
|
1134
|
+
case 'image':
|
|
1135
|
+
return handleGetImage();
|
|
1136
|
+
case 'file':
|
|
1137
|
+
return handleGetFile(fileOptions);
|
|
1138
|
+
case 'url':
|
|
1139
|
+
return handleGetUrl();
|
|
1140
|
+
case 'buffer':
|
|
1141
|
+
return handleGetBuffer();
|
|
1142
|
+
default: {
|
|
1143
|
+
return handleGetAuto(fileOptions);
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
if (method === 'set') {
|
|
1148
|
+
// 写入系统剪贴板
|
|
1149
|
+
if (value === undefined || value === null) {
|
|
1150
|
+
return false;
|
|
1151
|
+
}
|
|
1152
|
+
// 使用更清晰的类型检查和处理
|
|
1153
|
+
switch (type) {
|
|
1154
|
+
case 'text':
|
|
1155
|
+
if (typeof value === 'string') {
|
|
1156
|
+
return handleSetText(value);
|
|
1157
|
+
}
|
|
1158
|
+
break;
|
|
1159
|
+
case 'html':
|
|
1160
|
+
if (typeof value === 'string') {
|
|
1161
|
+
return handleSetHtml(value);
|
|
1162
|
+
}
|
|
1163
|
+
break;
|
|
1164
|
+
case 'image':
|
|
1165
|
+
if (typeof value === 'string') {
|
|
1166
|
+
return await handleSetImage(value);
|
|
1167
|
+
}
|
|
1168
|
+
if (value instanceof ArrayBuffer) {
|
|
1169
|
+
return await handleSetImage(value);
|
|
1170
|
+
}
|
|
1171
|
+
if (isClipboardFileData(value)) {
|
|
1172
|
+
return await handleSetImage(value);
|
|
1173
|
+
}
|
|
1174
|
+
break;
|
|
1175
|
+
case 'file':
|
|
1176
|
+
if (isClipboardFileData(value)) {
|
|
1177
|
+
return await handleSetFile(value);
|
|
1178
|
+
}
|
|
1179
|
+
if (Array.isArray(value) && value.every(isClipboardFileData)) {
|
|
1180
|
+
return await handleSetFiles(value);
|
|
1181
|
+
}
|
|
1182
|
+
break;
|
|
1183
|
+
case 'url':
|
|
1184
|
+
if (typeof value === 'string' && isFilePathOrUrl(value)) {
|
|
1185
|
+
return await handleSetStringAsFile(value);
|
|
1186
|
+
}
|
|
1187
|
+
break;
|
|
1188
|
+
case 'buffer':
|
|
1189
|
+
if (value instanceof ArrayBuffer) {
|
|
1190
|
+
return handleSetBuffer(value);
|
|
1191
|
+
}
|
|
1192
|
+
break;
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
if (method === 'clear') {
|
|
1196
|
+
return handleClear();
|
|
1197
|
+
}
|
|
1198
|
+
return false;
|
|
1199
|
+
}
|
|
1200
|
+
catch (error) {
|
|
1201
|
+
console.error('剪贴板操作失败:', error);
|
|
1202
|
+
return false;
|
|
1203
|
+
}
|
|
1204
|
+
});
|
|
1205
|
+
};
|
|
1206
|
+
|
|
1207
|
+
exports.initializeClipboard = initializeClipboard;
|
|
1208
|
+
//# sourceMappingURL=clipboard.js.map
|