foliko 1.1.13 → 1.1.15

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.
Files changed (102) hide show
  1. package/.agent/data/plugins-state.json +1 -1
  2. package/.agent/data/weixin/images/file_1776188148383jpg +0 -0
  3. package/.agent/data/weixin/images/file_1776188458326.jpg +0 -0
  4. package/.agent/data/weixin/images/file_1776188689423.jpg +0 -0
  5. package/.agent/data/weixin/images/file_1776188813604.jpg +0 -0
  6. package/.agent/data/weixin/images/file_1776189097450.jpg +0 -0
  7. package/.agent/data/weixin/videos/file_1776188318431.mp4 +0 -0
  8. package/.agent/mcp_config.json +7 -0
  9. package/.agent/memory/feedback/mnxe0cxc-14l6q5.md +17 -0
  10. package/.agent/memory/feedback/mnxe11pa-nxf577.md +9 -0
  11. package/.agent/memory/feedback/mnxe1an2-84faff.md +9 -0
  12. package/.agent/memory/feedback/mnxgcfj0-qg3wjc.md +9 -0
  13. package/.agent/memory/feedback/mnxgcn3y-40mqss.md +9 -0
  14. package/.agent/memory/feedback/mnxgcxq9-jm7ydl.md +9 -0
  15. package/.agent/memory/feedback/mnxgdyfj-pzjvkb.md +9 -0
  16. package/.agent/memory/feedback/mnxge3z1-7vyit1.md +9 -0
  17. package/.agent/memory/feedback/mnxhrg28-41hhjr.md +9 -0
  18. package/.agent/memory/feedback/mnxhrx0e-yth94k.md +9 -0
  19. package/.agent/memory/feedback/mnxhs3jd-rvx8aq.md +9 -0
  20. package/.agent/memory/feedback/mnxhs7p7-g5rtn9.md +9 -0
  21. package/.agent/memory/feedback/mnxhslx5-oqwuhr.md +9 -0
  22. package/.agent/memory/feedback/mnxhsvd6-nuyvvc.md +9 -0
  23. package/.agent/memory/project/mnxegq6z-5fc64w.md +22 -0
  24. package/.agent/memory/project/mnxh2w4r-le9hur.md +17 -0
  25. package/.agent/memory/project/mnxhq2yv-9qa8ay.md +31 -0
  26. package/.agent/memory/project/mnxhql11-iaun2o.md +34 -0
  27. package/.agent/memory/project/mnxhr78p-jpg7eq.md +23 -0
  28. package/.agent/memory/reference/mnxe0oa9-p6wzk6.md +27 -0
  29. package/.agent/memory/reference/mnxehcll-kcrmpf.md +29 -0
  30. package/.agent/memory/reference/mnxei0ts-jw091y.md +18 -0
  31. package/.agent/memory/reference/mnxfnrr4-rski36.md +40 -0
  32. package/.agent/memory/reference/mnxfo6n5-af9zls.md +18 -0
  33. package/.agent/memory/reference/mnxh2ady-u6cmvk.md +61 -0
  34. package/.agent/memory/reference/mnxhqdqh-ucsbsk.md +31 -0
  35. package/.agent/memory/reference/mnxiixyp-rz2gvw.md +34 -0
  36. package/.agent/memory/user/mnxhqxk3-vjjhlf.md +23 -0
  37. package/.agent/sessions/cli_default.json +11 -639
  38. package/.agent/sessions/weixin_o9cq80zgZqKPA2-s59PN43GdDy1w@im.wechat.json +25 -0
  39. package/.claude/settings.local.json +23 -1
  40. package/cli/src/commands/chat.js +9 -15
  41. package/cli/src/ui/chat-ui.js +40 -71
  42. package/package.json +4 -2
  43. package/plugins/default-plugins.js +5 -5
  44. package/plugins/file-system-plugin.js +1 -1
  45. package/plugins/memory-plugin.js +12 -12
  46. package/plugins/plugin-manager-plugin.js +1 -0
  47. package/plugins/subagent-plugin.js +55 -1
  48. package/plugins/telegram-plugin.js +9 -6
  49. package/plugins/weixin-plugin.js +75 -78
  50. package/src/core/agent-chat.js +468 -1612
  51. package/src/core/agent.js +53 -134
  52. package/src/core/chat-session.js +423 -0
  53. package/src/core/context-compressor.js +473 -0
  54. package/src/core/context-manager.js +0 -48
  55. package/src/core/framework.js +95 -68
  56. package/src/core/index.js +11 -0
  57. package/src/core/notification-manager.js +125 -0
  58. package/src/core/subagent.js +295 -0
  59. package/src/core/token-counter.js +190 -0
  60. package/src/core/tool-executor.js +270 -0
  61. package/src/executors/mcp-executor.js +14 -1
  62. package/src/utils/download.js +596 -0
  63. package/system.md +312 -2373
  64. package/.agent/agents/code-assistant.json +0 -17
  65. package/.agent/agents/email-assistant.json +0 -14
  66. package/.agent/agents/file-assistant.json +0 -18
  67. package/.agent/agents/orchestrator-demo.md +0 -53
  68. package/.agent/agents/orchestrator.json +0 -7
  69. package/.agent/agents/poster-expert.md +0 -228
  70. package/.agent/agents/system-assistant.json +0 -15
  71. package/.agent/agents/web-assistant.json +0 -12
  72. package/.agent/memory/feedback/mnv3nu27-3o15pf.md +0 -9
  73. package/.agent/memory/feedback/mnv3o078-b959yj.md +0 -9
  74. package/.agent/memory/feedback/mnv3o6ej-u0fif5.md +0 -9
  75. package/.agent/memory/feedback/mnv3obgl-bkkjoj.md +0 -9
  76. package/.agent/memory/feedback/mnv4a3js-dv6onx.md +0 -9
  77. package/.agent/memory/feedback/mnv4aacm-sxxowp.md +0 -9
  78. package/.agent/memory/feedback/mnv4ahto-w40ffm.md +0 -9
  79. package/.agent/memory/feedback/mnv4anvp-3cs06y.md +0 -9
  80. package/.agent/memory/feedback/mnvzgvtd-0o2900.md +0 -9
  81. package/.agent/memory/feedback/mnvzhajn-swbx61.md +0 -15
  82. package/.agent/memory/feedback/mnvzhgsp-p5vog3.md +0 -9
  83. package/.agent/memory/feedback/mnvzho0c-fgql7q.md +0 -14
  84. package/.agent/memory/feedback/mnvzhtzq-ufr5at.md +0 -9
  85. package/.agent/memory/feedback/mnvzhyb3-9byq2z.md +0 -9
  86. package/.agent/memory/feedback/mnvzi7hp-hyeafp.md +0 -9
  87. package/.agent/memory/feedback/mnvzibph-z7rwp5.md +0 -9
  88. package/.agent/memory/feedback/mnvzilys-7h176w.md +0 -14
  89. package/.agent/memory/feedback/mnvziuh5-zjshci.md +0 -9
  90. package/.agent/memory/feedback/mnw07wde-6zqsc8.md +0 -9
  91. package/.agent/memory/feedback/mnw084bp-j0ba2a.md +0 -9
  92. package/.agent/memory/user/mnv3n62r-y0h79j.md +0 -21
  93. package/.agent/memory/user/mnv3n9yf-ead4g8.md +0 -13
  94. package/.agent/memory/user/mnv3ne3j-82tq1k.md +0 -19
  95. package/.agent/memory/user/mnv3nhgm-g2s2us.md +0 -11
  96. package/.agent/memory/user/mnv3nl9u-ejd998.md +0 -16
  97. package/.agent/memory/user/mnv3nofp-ya5szl.md +0 -10
  98. package/.agent/memory/user/mnv49qne-bhk0ki.md +0 -9
  99. package/.agent/memory/user/mnv49w3y-rzr8ju.md +0 -13
  100. package/.agent/sessions/test.json +0 -16
  101. package/plugins/python-plugin-loader.js.bak +0 -856
  102. package/src/core/agent-context.js +0 -188
@@ -0,0 +1,596 @@
1
+ const https = require('https');
2
+ const http = require('http');
3
+ const fs = require('fs');
4
+ const fsPromises = require('fs/promises');
5
+ const path = require('path');
6
+ const crypto = require('crypto');
7
+ const { fileTypeFromFile, fileTypeFromBuffer } = require('file-type');
8
+ const { downloadAndDecryptMedia } = require('@chnak/weixin-bot');
9
+ class FileDownloader {
10
+ constructor(options = {}) {
11
+ this.timeout = options.timeout || 30000;
12
+ this.retries = options.retries || 3;
13
+ this.headers = options.headers || {};
14
+ this.baseDir = options.baseDir || './downloads'; // 基础下载目录
15
+ }
16
+
17
+ /**
18
+ * 根据文件类型获取保存目录
19
+ */
20
+ _getTypeDir(mimeType, extension) {
21
+ const typeMap = {
22
+ // 图片
23
+ 'image/jpeg': 'images',
24
+ 'image/jpg': 'images',
25
+ 'image/png': 'images',
26
+ 'image/gif': 'images',
27
+ 'image/webp': 'images',
28
+ 'image/svg+xml': 'images',
29
+
30
+ // 视频
31
+ 'video/mp4': 'videos',
32
+ 'video/webm': 'videos',
33
+ 'video/avi': 'videos',
34
+ 'video/mov': 'videos',
35
+ 'video/mkv': 'videos',
36
+ 'video/x-msvideo': 'videos',
37
+ 'video/quicktime': 'videos',
38
+
39
+ // 音频
40
+ 'audio/mpeg': 'audio',
41
+ 'audio/mp3': 'audio',
42
+ 'audio/wav': 'audio',
43
+ 'audio/ogg': 'audio',
44
+ 'audio/m4a': 'audio',
45
+ 'audio/flac': 'audio',
46
+ 'audio/aac': 'audio',
47
+
48
+ // 文档
49
+ 'application/pdf': 'documents',
50
+ 'application/msword': 'documents',
51
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'documents',
52
+ 'application/vnd.ms-excel': 'documents',
53
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'documents',
54
+ 'application/vnd.ms-powerpoint': 'documents',
55
+ 'application/vnd.openxmlformats-officedocument.presentationml.presentation': 'documents',
56
+ 'text/plain': 'documents',
57
+ 'text/markdown': 'documents',
58
+ 'text/csv': 'documents',
59
+
60
+ // 压缩包
61
+ 'application/zip': 'archives',
62
+ 'application/x-rar-compressed': 'archives',
63
+ 'application/x-7z-compressed': 'archives',
64
+ 'application/x-tar': 'archives',
65
+ 'application/gzip': 'archives',
66
+ 'application/x-bzip2': 'archives',
67
+
68
+ // 可执行文件
69
+ 'application/x-msdownload': 'executables',
70
+ 'application/x-sh': 'executables',
71
+ 'application/x-executable': 'executables',
72
+
73
+ // 代码文件
74
+ 'text/html': 'code',
75
+ 'text/css': 'code',
76
+ 'text/javascript': 'code',
77
+ 'application/javascript': 'code',
78
+ 'application/json': 'code',
79
+ 'text/xml': 'code',
80
+ 'application/xml': 'code',
81
+ 'text/x-python': 'code',
82
+ 'text/x-java': 'code',
83
+ 'text/x-c': 'code',
84
+ 'text/x-c++': 'code',
85
+ };
86
+
87
+ // 先尝试根据 MIME 类型判断
88
+ if (mimeType && typeMap[mimeType]) {
89
+ return typeMap[mimeType];
90
+ }
91
+
92
+ // 如果 MIME 类型未匹配,根据扩展名判断
93
+ const extMap = {
94
+ // 图片
95
+ '.jpg': 'images',
96
+ '.jpeg': 'images',
97
+ '.png': 'images',
98
+ '.gif': 'images',
99
+ '.webp': 'images',
100
+ '.svg': 'images',
101
+ '.bmp': 'images',
102
+ '.ico': 'images',
103
+
104
+ // 视频
105
+ '.mp4': 'videos',
106
+ '.webm': 'videos',
107
+ '.avi': 'videos',
108
+ '.mov': 'videos',
109
+ '.mkv': 'videos',
110
+ '.flv': 'videos',
111
+ '.wmv': 'videos',
112
+ '.m4v': 'videos',
113
+ '.3gp': 'videos',
114
+
115
+ // 音频
116
+ '.mp3': 'audio',
117
+ '.wav': 'audio',
118
+ '.ogg': 'audio',
119
+ '.m4a': 'audio',
120
+ '.flac': 'audio',
121
+ '.aac': 'audio',
122
+ '.wma': 'audio',
123
+ '.opus': 'audio',
124
+ '.silk': 'audio',
125
+
126
+ // 文档
127
+ '.pdf': 'documents',
128
+ '.doc': 'documents',
129
+ '.docx': 'documents',
130
+ '.xls': 'documents',
131
+ '.xlsx': 'documents',
132
+ '.ppt': 'documents',
133
+ '.pptx': 'documents',
134
+ '.txt': 'documents',
135
+ '.md': 'documents',
136
+ '.csv': 'documents',
137
+ '.rtf': 'documents',
138
+ '.odt': 'documents',
139
+
140
+ // 压缩包
141
+ '.zip': 'archives',
142
+ '.rar': 'archives',
143
+ '.7z': 'archives',
144
+ '.tar': 'archives',
145
+ '.gz': 'archives',
146
+ '.bz2': 'archives',
147
+ '.xz': 'archives',
148
+ '.tgz': 'archives',
149
+
150
+ // 可执行文件
151
+ '.exe': 'executables',
152
+ '.msi': 'executables',
153
+ '.sh': 'executables',
154
+ '.bat': 'executables',
155
+ '.cmd': 'executables',
156
+ '.app': 'executables',
157
+ '.deb': 'executables',
158
+ '.rpm': 'executables',
159
+ '.dmg': 'executables',
160
+
161
+ // 代码
162
+ '.html': 'code',
163
+ '.htm': 'code',
164
+ '.css': 'code',
165
+ '.js': 'code',
166
+ '.json': 'code',
167
+ '.xml': 'code',
168
+ '.yaml': 'code',
169
+ '.yml': 'code',
170
+ '.py': 'code',
171
+ '.java': 'code',
172
+ '.c': 'code',
173
+ '.cpp': 'code',
174
+ '.h': 'code',
175
+ '.php': 'code',
176
+ '.rb': 'code',
177
+ '.go': 'code',
178
+ '.rs': 'code',
179
+ '.ts': 'code',
180
+ '.jsx': 'code',
181
+ '.tsx': 'code',
182
+ '.vue': 'code',
183
+ '.sql': 'code',
184
+ '.sh': 'code',
185
+ };
186
+
187
+ if (extension && extMap[extension.toLowerCase()]) {
188
+ return extMap[extension.toLowerCase()];
189
+ }
190
+
191
+ // 默认目录
192
+ return 'others';
193
+ }
194
+
195
+ /**
196
+ * 从 URL 或 Content-Type 提取文件信息
197
+ */
198
+ _extractFileInfo(url, contentType, fileExt) {
199
+ const urlObj = new URL(url);
200
+ let filename = path.basename(urlObj.pathname);
201
+ // 如果没有文件名,生成一个
202
+ if (!filename || !filename.includes('.')) {
203
+ const timestamp = Date.now();
204
+ let ext = fileExt || this._getExtensionFromMime(contentType);
205
+
206
+ // Content-Type 为空时,尝试从 URL 参数推断
207
+ if (!ext) {
208
+ const taskid = urlObj.searchParams.get('taskid');
209
+ if (taskid) {
210
+ ext = path.extname(taskid) || '';
211
+ }
212
+ }
213
+ // 仍然没有扩展名,默认 .jpg(微信图片通常是 jpeg)
214
+ if (!ext) {
215
+ ext = '.jpg';
216
+ }
217
+
218
+ filename = `file_${timestamp}.${ext}`;
219
+ }
220
+
221
+ // 清理文件名中的特殊字符
222
+ filename = this._sanitizeFilename(filename);
223
+
224
+ const extension = path.extname(filename);
225
+
226
+ return { filename, extension };
227
+ }
228
+
229
+ /**
230
+ * 根据 MIME 类型获取扩展名
231
+ */
232
+ _getExtensionFromMime(mimeType) {
233
+ if (!mimeType) return '';
234
+
235
+ const mimeMap = {
236
+ 'image/jpeg': '.jpg',
237
+ 'image/png': '.png',
238
+ 'image/gif': '.gif',
239
+ 'image/webp': '.webp',
240
+ 'image/svg+xml': '.svg',
241
+ 'video/mp4': '.mp4',
242
+ 'video/webm': '.webm',
243
+ 'video/quicktime': '.mov',
244
+ 'audio/mpeg': '.mp3',
245
+ 'audio/wav': '.wav',
246
+ 'audio/ogg': '.ogg',
247
+ 'audio/silk': '.silk',
248
+ 'application/pdf': '.pdf',
249
+ 'application/zip': '.zip',
250
+ 'text/html': '.html',
251
+ 'text/plain': '.txt',
252
+ 'application/json': '.json',
253
+ };
254
+
255
+ return mimeMap[mimeType] || '';
256
+ }
257
+
258
+ /**
259
+ * 清理文件名中的非法字符
260
+ */
261
+ _sanitizeFilename(filename) {
262
+ return filename
263
+ .replace(/[<>:"/\\|?*]/g, '_') // Windows 非法字符
264
+ .replace(/\s+/g, '_') // 空格转下划线
265
+ .replace(/_+/g, '_') // 多个下划线合并
266
+ .substring(0, 255); // 限制长度
267
+ }
268
+
269
+ /**
270
+ * 生成唯一文件名(避免重名)
271
+ */
272
+ async _getUniqueFilePath(dir, filename) {
273
+ const ext = path.extname(filename);
274
+ const baseName = path.basename(filename, ext);
275
+ let filePath = path.join(dir, filename);
276
+ let counter = 1;
277
+
278
+ while (true) {
279
+ try {
280
+ await fsPromises.access(filePath);
281
+ // 文件存在,添加序号
282
+ filePath = path.join(dir, `${baseName}_${counter}${ext}`);
283
+ counter++;
284
+ } catch {
285
+ // 文件不存在,可以使用
286
+ break;
287
+ }
288
+ }
289
+
290
+ return filePath;
291
+ }
292
+
293
+ /**
294
+ * 下载文件(自动分类保存)
295
+ */
296
+ async download(url, options = {}) {
297
+ const {
298
+ customFilename,
299
+ customDir,
300
+ fileExt,
301
+ aes_key,
302
+ onProgress,
303
+ retries = this.retries,
304
+ skipExisting = true,
305
+ } = options;
306
+
307
+ let lastError;
308
+
309
+ for (let i = 0; i < retries; i++) {
310
+ try {
311
+ return await this._doDownload(url, {
312
+ customFilename,
313
+ customDir,
314
+ fileExt,
315
+ aes_key,
316
+ onProgress,
317
+ skipExisting,
318
+ });
319
+ } catch (error) {
320
+ lastError = error;
321
+ console.log(`下载失败,第 ${i + 1}/${retries} 次重试...`);
322
+ await this._sleep(1000 * (i + 1));
323
+ }
324
+ }
325
+
326
+ throw lastError;
327
+ }
328
+
329
+ async fromWeXin(url, aesKey, { customFilename, customDir, fileExt } = {}) {
330
+ try {
331
+ // 1. 下载并解密
332
+ const buffer = await downloadAndDecryptMedia({ url, aesKey });
333
+
334
+ // 2. 检测文件类型
335
+ let ext, mime;
336
+ if (fileExt) {
337
+ // 如果手动指定了扩展名
338
+ ext = fileExt.startsWith('.') ? fileExt.slice(1) : fileExt;
339
+ mime = null;
340
+ } else {
341
+ const fileType = await fileTypeFromBuffer(buffer);
342
+ ext = fileType?.ext || 'bin';
343
+ mime = fileType?.mime || 'application/octet-stream';
344
+ }
345
+
346
+ // 3. 确定目标目录
347
+ let targetDir;
348
+ if (customDir) {
349
+ targetDir = customDir;
350
+ } else {
351
+ const typeDir = this._getTypeDir(mime, `.${ext}`);
352
+ targetDir = path.join(this.baseDir, typeDir);
353
+ }
354
+
355
+ // 4. 创建目录
356
+ await fsPromises.mkdir(targetDir, { recursive: true });
357
+
358
+ // 5. 生成文件名
359
+ let filename;
360
+ if (customFilename) {
361
+ // 确保自定义文件名有正确的扩展名
362
+ filename = customFilename.includes('.') ? customFilename : `${customFilename}.${ext}`;
363
+ } else {
364
+ // 自动生成
365
+ const { filename: autoFilename } = this._extractFileInfo(url, mime, ext);
366
+ filename = autoFilename;
367
+ }
368
+
369
+ // 6. 获取唯一文件路径(避免覆盖)
370
+ const destPath = await this._getUniqueFilePath(targetDir, filename);
371
+
372
+ // 7. 安全写入文件
373
+ const tempPath = `${destPath}.tmp`;
374
+ await fsPromises.writeFile(tempPath, buffer);
375
+ await fsPromises.rename(tempPath, destPath);
376
+
377
+ // 8. 返回结果
378
+ return {
379
+ filename: path.basename(destPath),
380
+ path: destPath,
381
+ dir: targetDir,
382
+ size: buffer.length,
383
+ ext: ext,
384
+ mime: mime,
385
+ };
386
+ } catch (err) {
387
+ throw new Error(`微信媒体下载失败: ${err.message}`);
388
+ }
389
+ }
390
+
391
+ /**
392
+ * 实际下载逻辑
393
+ */
394
+ async _doDownload(
395
+ url,
396
+ { customFilename, customDir, fileExt, onProgress, skipExisting, aes_key }
397
+ ) {
398
+ return new Promise((resolve, reject) => {
399
+ const protocol = url.startsWith('https') ? https : http;
400
+
401
+ const requestOptions = {
402
+ headers: {
403
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
404
+ Accept: '*/*',
405
+ 'Accept-Encoding': 'gzip, deflate, br',
406
+ ...this.headers,
407
+ },
408
+ timeout: this.timeout,
409
+ };
410
+
411
+ const req = protocol.get(url, requestOptions, async (response) => {
412
+ try {
413
+ // 处理重定向
414
+ if ([301, 302, 307, 308].includes(response.statusCode)) {
415
+ const redirectUrl = response.headers.location;
416
+ return this._doDownload(redirectUrl, {
417
+ customFilename,
418
+ customDir,
419
+ fileExt,
420
+ onProgress,
421
+ aes_key,
422
+ skipExisting,
423
+ })
424
+ .then(resolve)
425
+ .catch(reject);
426
+ }
427
+
428
+ if (response.statusCode !== 200) {
429
+ reject(new Error(`HTTP ${response.statusCode}: ${response.statusMessage}`));
430
+ return;
431
+ }
432
+
433
+ // 获取 Content-Type
434
+ const contentType = response.headers['content-type'] || '';
435
+ console.log(`[Download] URL: ${url}`);
436
+ console.log(`[Download] Content-Type: ${contentType}`);
437
+
438
+ // 提取文件信息
439
+ const { filename: autoFilename, extension } = this._extractFileInfo(
440
+ url,
441
+ contentType,
442
+ fileExt
443
+ );
444
+ const filename = customFilename || autoFilename;
445
+
446
+ // 确定保存目录
447
+ let targetDir;
448
+ if (customDir) {
449
+ targetDir = customDir;
450
+ } else {
451
+ const typeDir = this._getTypeDir(contentType, extension);
452
+ targetDir = path.join(this.baseDir, typeDir);
453
+ }
454
+
455
+ // 创建目录
456
+ await fsPromises.mkdir(targetDir, { recursive: true });
457
+
458
+ // 获取唯一文件路径
459
+ let destPath = path.join(targetDir, filename);
460
+ if (skipExisting) {
461
+ destPath = await this._getUniqueFilePath(targetDir, filename);
462
+ }
463
+
464
+ const tempPath = `${destPath}.tmp`;
465
+ const totalSize = parseInt(response.headers['content-length'], 10);
466
+ let downloadedSize = 0;
467
+
468
+ // 创建写入流
469
+ const fileStream = fs.createWriteStream(tempPath);
470
+
471
+ // 监听进度
472
+ response.on('data', (chunk) => {
473
+ downloadedSize += chunk.length;
474
+ if (onProgress) {
475
+ onProgress({
476
+ downloaded: downloadedSize,
477
+ total: totalSize,
478
+ percent: totalSize ? ((downloadedSize / totalSize) * 100).toFixed(2) : '未知',
479
+ filename,
480
+ contentType,
481
+ category: path.basename(targetDir),
482
+ });
483
+ }
484
+ });
485
+
486
+ req.on('error', (err) => {
487
+ console.log('下载请求错误:', err.message);
488
+ reject(err);
489
+ });
490
+
491
+ // 使用 pipe 下载
492
+ if (aes_key) {
493
+ // aes_key 是 base64 编码的
494
+ const keyBuffer = Buffer.from(aes_key, 'base64');
495
+ response.pipe(crypto.createDecipheriv('aes-256-ecb', keyBuffer, null)).pipe(fileStream);
496
+ } else {
497
+ response.pipe(fileStream);
498
+ }
499
+ await new Promise((resolve, reject) => {
500
+ fileStream.on('finish', resolve);
501
+ fileStream.on('error', reject);
502
+ });
503
+
504
+ // 如果没有 content-type,检测文件魔数来修正扩展名
505
+ let finalPath = destPath;
506
+ const detectedExt = fileExt || this._getExtensionFromMime(contentType);
507
+ if (!detectedExt) {
508
+ try {
509
+ const fileType = await fileTypeFromFile(tempPath);
510
+ if (fileType) {
511
+ const ext = `.${fileType.ext}`;
512
+ const dir = path.dirname(destPath);
513
+ const base = path.basename(destPath, path.extname(destPath));
514
+ const newFilename = `${base}${ext}`;
515
+ finalPath = path.join(dir, newFilename);
516
+
517
+ // 如果文件名不同,删除旧的重命名
518
+ if (newFilename !== path.basename(destPath)) {
519
+ await fsPromises.unlink(destPath).catch(() => {});
520
+ await fsPromises.rename(tempPath, finalPath);
521
+ }
522
+ } else {
523
+ await fsPromises.rename(tempPath, destPath);
524
+ }
525
+ } catch (e) {
526
+ // 魔数检测失败,直接用原文件名
527
+ await fsPromises.rename(tempPath, destPath);
528
+ }
529
+ } else {
530
+ await fsPromises.rename(tempPath, destPath);
531
+ }
532
+
533
+ // 返回下载信息
534
+ resolve({
535
+ url,
536
+ path: finalPath,
537
+ filename: path.basename(finalPath),
538
+ size: downloadedSize,
539
+ contentType,
540
+ category: path.basename(path.dirname(finalPath)),
541
+ downloadTime: new Date().toISOString(),
542
+ });
543
+ } catch (error) {
544
+ reject(error);
545
+ }
546
+ });
547
+
548
+ req.on('timeout', () => {
549
+ req.destroy();
550
+ reject(new Error('请求超时'));
551
+ });
552
+
553
+ req.on('error', reject);
554
+ });
555
+ }
556
+
557
+ /**
558
+ * 批量下载
559
+ */
560
+ async downloadBatch(urls, options = {}) {
561
+ const results = {
562
+ success: [],
563
+ failed: [],
564
+ total: urls.length,
565
+ };
566
+
567
+ const { concurrency = 3, ...downloadOptions } = options;
568
+
569
+ // 控制并发数
570
+ for (let i = 0; i < urls.length; i += concurrency) {
571
+ const batch = urls.slice(i, i + concurrency);
572
+ const promises = batch.map(async (url) => {
573
+ try {
574
+ const result = await this.download(url, downloadOptions);
575
+ results.success.push(result);
576
+ return result;
577
+ } catch (error) {
578
+ results.failed.push({ url, error: error.message });
579
+ return null;
580
+ }
581
+ });
582
+
583
+ await Promise.all(promises);
584
+ }
585
+
586
+ return results;
587
+ }
588
+
589
+ _sleep(ms) {
590
+ return new Promise((resolve) => setTimeout(resolve, ms));
591
+ }
592
+ }
593
+ module.exports = {
594
+ FileDownloader,
595
+ downloader: new FileDownloader({ retries: 3 }),
596
+ };