@lynker-desktop/electron-sdk 0.0.9-alpha.54 → 0.0.9-alpha.56

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.
@@ -2,44 +2,27 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import http from 'node:http';
4
4
  import https from 'node:https';
5
+ import { Transform } from 'node:stream';
5
6
  import md5 from 'md5';
6
- import ipc__default from '@lynker-desktop/electron-ipc/main';
7
+ import mime from 'mime-types';
8
+ import { ipcMain } from 'electron';
7
9
 
8
10
  /**
9
- * MIME 类型到文件扩展名的映射表
11
+ * HTTP 重定向状态码
10
12
  */
11
- const MIME_TO_EXT = {
12
- 'image/png': 'png',
13
- 'image/jpeg': 'jpeg',
14
- 'image/jpg': 'jpg',
15
- 'image/gif': 'gif',
16
- 'image/webp': 'webp',
17
- 'image/svg+xml': 'svg',
18
- 'image/x-icon': 'ico',
19
- 'image/bmp': 'bmp',
20
- 'image/avif': 'avif',
21
- 'image/heic': 'heic',
22
- 'image/heif': 'heif',
23
- 'image/tiff': 'tiff',
24
- 'font/woff': 'woff',
25
- 'font/woff2': 'woff2',
26
- 'font/ttf': 'ttf',
27
- 'font/otf': 'otf',
28
- 'application/font-woff': 'woff',
29
- 'application/font-woff2': 'woff2',
30
- 'video/mp4': 'mp4',
31
- 'video/webm': 'webm',
32
- 'video/ogg': 'ogg',
33
- 'audio/mpeg': 'mp3',
34
- 'audio/wav': 'wav',
35
- 'text/css': 'css',
36
- 'text/javascript': 'js',
37
- 'application/javascript': 'js',
38
- 'application/json': 'json',
39
- 'text/xml': 'xml',
40
- 'text/plain': 'txt',
41
- 'application/pdf': 'pdf',
42
- };
13
+ const REDIRECT_STATUS_CODES = [301, 302, 307, 308];
14
+ /**
15
+ * 默认文件扩展名
16
+ */
17
+ const DEFAULT_EXT = '.res';
18
+ /**
19
+ * 默认 MIME 类型
20
+ */
21
+ const DEFAULT_MIME_TYPE = 'application/octet-stream';
22
+ /**
23
+ * 最大重定向次数
24
+ */
25
+ const MAX_REDIRECTS = 5;
43
26
  /**
44
27
  * 默认配置
45
28
  */
@@ -68,6 +51,8 @@ class ResourceCache {
68
51
  */
69
52
  constructor(session, options) {
70
53
  this.cacheHost = `${ResourceCache.scheme}://-`;
54
+ /** 正在下载的 URL 集合(避免重复下载) */
55
+ this._downloadingUrls = new Set();
71
56
  if (!session)
72
57
  throw new Error('ResourceCache: session is required');
73
58
  this.session = session;
@@ -92,31 +77,39 @@ class ResourceCache {
92
77
  this._cleanOldCache().catch(err => {
93
78
  console.log('初始化时清理过期缓存失败:', err);
94
79
  });
95
- ipc__default.mainIPC.handleRenderer('core:cache', async (options) => {
80
+ ipcMain.handle('core:cache', async (event, options) => {
96
81
  try {
97
- if (options.method === 'clear') {
98
- return await this.clearCache();
99
- }
100
- if (options.method === 'add') {
101
- if (typeof options.urls === 'string') {
102
- const data = await this.addCacheUrls([options.urls], options.force ?? false, true);
103
- return data[0];
82
+ switch (options.method) {
83
+ case 'clear':
84
+ return await this.clearCache();
85
+ case 'add': {
86
+ const { id } = options;
87
+ const sender = event.sender;
88
+ const urls = Array.isArray(options.urls) ? options.urls : options.urls ? [options.urls] : [];
89
+ const data = await this.addCacheUrls(urls, options.force ?? false, true, (data) => {
90
+ try {
91
+ if (sender) {
92
+ if (sender) {
93
+ sender?.send?.(`core:cache:progress:${id}`, data);
94
+ }
95
+ }
96
+ }
97
+ catch (error) {
98
+ console.log('发送缓存进度回调失败:', error);
99
+ }
100
+ });
101
+ return Array.isArray(options.urls) ? data : data[0];
104
102
  }
105
- const data = await this.addCacheUrls(options.urls ?? [], options.force ?? false, true);
106
- return data;
107
- }
108
- if (options.method === 'delete') {
109
- if (typeof options.urls === 'string') {
110
- const data = await this.deleteCacheUrls([options.urls]);
111
- return data[0];
103
+ case 'delete': {
104
+ const urls = Array.isArray(options.urls) ? options.urls : options.urls ? [options.urls] : [];
105
+ const data = await this.deleteCacheUrls(urls);
106
+ return Array.isArray(options.urls) ? data : data[0];
112
107
  }
113
- const data = await this.deleteCacheUrls(options.urls ?? []);
114
- return data;
108
+ case 'stats':
109
+ return await this.getCacheStats();
110
+ default:
111
+ return undefined;
115
112
  }
116
- if (options.method === 'stats') {
117
- return await this.getCacheStats();
118
- }
119
- return undefined;
120
113
  }
121
114
  catch (error) {
122
115
  return { success: false, error: error instanceof Error ? error.message : '未知错误' };
@@ -219,11 +212,11 @@ class ResourceCache {
219
212
  // 尝试从 URL 中提取扩展名
220
213
  try {
221
214
  const urlObj = new URL(url);
222
- ext = path.extname(urlObj.pathname) || '.res';
215
+ ext = path.extname(urlObj.pathname) || DEFAULT_EXT;
223
216
  }
224
217
  catch {
225
218
  // 如果 URL 解析失败(可能是 base64 data URL),使用默认扩展名
226
- ext = '.res';
219
+ ext = DEFAULT_EXT;
227
220
  }
228
221
  }
229
222
  return {
@@ -263,45 +256,84 @@ class ResourceCache {
263
256
  * 下载资源到本地缓存(异步版本,返回 Promise)
264
257
  * @param url 资源URL
265
258
  * @param filePath 本地缓存路径
259
+ * @param redirectCount 当前重定向次数(内部使用)
260
+ * @param onProgress 下载进度回调函数(可选)
266
261
  * @returns Promise<void> 下载完成或失败
267
262
  */
268
- downloadResourceAsync(url, filePath) {
263
+ downloadResourceAsync(url, filePath, redirectCount = 0, onProgress) {
264
+ // 检查是否正在下载,避免重复下载
265
+ if (this._downloadingUrls.has(url)) {
266
+ return Promise.reject(new Error(`资源正在下载中: ${url}`));
267
+ }
268
+ // 检查重定向次数限制
269
+ if (redirectCount >= MAX_REDIRECTS) {
270
+ return Promise.reject(new Error(`重定向次数超过限制 (${MAX_REDIRECTS}): ${url}`));
271
+ }
272
+ this._downloadingUrls.add(url);
269
273
  return new Promise((resolve, reject) => {
270
274
  const tempFilePath = `${filePath}.cache`;
271
275
  const lib = url.startsWith('https') ? https : http;
272
276
  const file = fs.createWriteStream(tempFilePath);
273
277
  let request;
278
+ // 下载进度跟踪
279
+ let downloaded = 0;
280
+ let total = 0;
281
+ let lastProgressTime = Date.now();
282
+ let lastDownloaded = 0;
283
+ let progressInterval = null;
274
284
  const cleanupAndAbort = (errMsg, err) => {
275
- if (err) {
276
- console.log(errMsg, err);
277
- }
278
- else {
279
- console.log(errMsg);
285
+ this._downloadingUrls.delete(url);
286
+ if (progressInterval) {
287
+ clearInterval(progressInterval);
288
+ progressInterval = null;
280
289
  }
281
290
  if (request) {
282
291
  request.destroy();
283
292
  }
284
293
  file.close(() => {
285
- // 使用 existsSync 避免在文件不存在时 unlink 抛出错误
286
- if (fs.existsSync(tempFilePath)) {
287
- fs.unlink(tempFilePath, () => { });
288
- }
294
+ // 异步删除临时文件,不阻塞
295
+ fs.promises.unlink(tempFilePath).catch(() => {
296
+ // 忽略删除失败
297
+ });
289
298
  });
290
- reject(new Error(errMsg));
299
+ const error = err instanceof Error ? err : new Error(errMsg);
300
+ reject(error);
301
+ };
302
+ const reportProgress = () => {
303
+ if (!onProgress)
304
+ return;
305
+ const now = Date.now();
306
+ const timeDiff = (now - lastProgressTime) / 1000; // 秒
307
+ const downloadedDiff = downloaded - lastDownloaded;
308
+ const speed = timeDiff > 0 ? downloadedDiff / timeDiff : 0;
309
+ const percentage = total > 0
310
+ ? Math.round((downloaded / total) * 100)
311
+ : -1;
312
+ onProgress({
313
+ url,
314
+ downloaded,
315
+ total,
316
+ percentage,
317
+ speed
318
+ });
319
+ lastProgressTime = now;
320
+ lastDownloaded = downloaded;
291
321
  };
292
322
  request = lib.get(url, (res) => {
293
323
  // 处理重定向
294
- if (res.statusCode === 301 || res.statusCode === 302 || res.statusCode === 307 || res.statusCode === 308) {
324
+ if (res.statusCode && REDIRECT_STATUS_CODES.includes(res.statusCode)) {
295
325
  const location = res.headers.location;
296
326
  if (location) {
297
327
  request.destroy();
298
328
  file.close(() => {
299
- if (fs.existsSync(tempFilePath)) {
300
- fs.unlink(tempFilePath, () => { });
301
- }
329
+ // 异步删除临时文件,不阻塞
330
+ fs.promises.unlink(tempFilePath).catch(() => {
331
+ // 忽略删除失败
332
+ });
302
333
  });
303
- // 递归处理重定向
304
- this.downloadResourceAsync(location, filePath).then(resolve).catch(reject);
334
+ this._downloadingUrls.delete(url);
335
+ // 递归处理重定向,增加重定向计数,传递进度回调
336
+ this.downloadResourceAsync(location, filePath, redirectCount + 1, onProgress).then(resolve).catch(reject);
305
337
  return;
306
338
  }
307
339
  // 如果没有 location,继续处理为错误
@@ -311,14 +343,40 @@ class ResourceCache {
311
343
  cleanupAndAbort(`下载失败,状态码: ${res.statusCode} for ${url}`);
312
344
  return;
313
345
  }
314
- res.pipe(file);
346
+ // 获取文件总大小(如果服务器提供)
347
+ const contentLength = res.headers['content-length'];
348
+ if (contentLength) {
349
+ total = parseInt(contentLength, 10);
350
+ }
351
+ // 使用 Transform 流来跟踪下载进度,避免与 pipe 冲突
352
+ const progressStream = new Transform({
353
+ transform(chunk, encoding, callback) {
354
+ downloaded += chunk.length;
355
+ callback(null, chunk);
356
+ }
357
+ });
358
+ // 定期报告进度(每 200ms 一次)
359
+ if (onProgress) {
360
+ progressInterval = setInterval(reportProgress, 200);
361
+ }
362
+ // 将响应流通过进度跟踪流再写入文件
363
+ res.pipe(progressStream).pipe(file);
315
364
  });
316
365
  file.on('finish', () => {
366
+ if (progressInterval) {
367
+ clearInterval(progressInterval);
368
+ progressInterval = null;
369
+ }
370
+ // 最终进度报告
371
+ if (onProgress) {
372
+ reportProgress();
373
+ }
317
374
  file.close((err) => {
318
375
  if (err) {
319
376
  return cleanupAndAbort(`关闭临时文件流失败: ${tempFilePath}`, err);
320
377
  }
321
378
  fs.rename(tempFilePath, filePath, (renameErr) => {
379
+ this._downloadingUrls.delete(url);
322
380
  if (renameErr) {
323
381
  cleanupAndAbort(`缓存文件重命名失败 from ${tempFilePath} to ${filePath}`, renameErr);
324
382
  }
@@ -334,6 +392,13 @@ class ResourceCache {
334
392
  request.on('error', (err) => {
335
393
  cleanupAndAbort(`下载资源请求失败: ${url}`, err);
336
394
  });
395
+ // 添加超时处理(30秒)
396
+ const timeout = setTimeout(() => {
397
+ cleanupAndAbort(`下载超时: ${url}`);
398
+ }, 30000);
399
+ request.on('close', () => {
400
+ clearTimeout(timeout);
401
+ });
337
402
  });
338
403
  }
339
404
  /**
@@ -343,14 +408,28 @@ class ResourceCache {
343
408
  */
344
409
  downloadResource(url, filePath) {
345
410
  // 异步执行,不等待完成
346
- this.downloadResourceAsync(url, filePath).catch((err) => {
347
- console.log('后台下载资源失败:', err);
411
+ // 如果正在下载,跳过(避免重复下载)
412
+ if (this._downloadingUrls.has(url)) {
413
+ return;
414
+ }
415
+ this.downloadResourceAsync(url, filePath).catch(() => {
416
+ // 静默处理错误,避免日志过多
348
417
  });
349
418
  }
419
+ /**
420
+ * 从文件扩展名获取 MIME 类型
421
+ * @param ext 文件扩展名(带或不带点)
422
+ * @returns MIME 类型
423
+ */
424
+ _getMimeTypeFromExt(ext) {
425
+ const cleanExt = ext.replace(/^\./, '').toLowerCase();
426
+ const mimeType = mime.lookup(cleanExt);
427
+ return mimeType || DEFAULT_MIME_TYPE;
428
+ }
350
429
  /**
351
430
  * 检测并处理 base64 data URL
352
431
  * @param url 资源URL
353
- * @returns 如果是 base64 URL,返回 true 和文件扩展名;否则返回 false
432
+ * @returns 如果是 base64 URL,返回 true、文件扩展名、MIME 类型和数据;否则返回 false
354
433
  */
355
434
  _isBase64DataUrl(url) {
356
435
  if (!url.startsWith('data:')) {
@@ -368,16 +447,21 @@ class ResourceCache {
368
447
  if (!header.includes('base64')) {
369
448
  return { isBase64: false };
370
449
  }
371
- // 从 mediatype 中提取文件扩展名
372
- // 例如:data:image/png;base64 -> png
373
- // 例如:data:image/jpeg;base64 -> jpeg
374
- let ext = 'res';
450
+ // 从 mediatype 中提取文件扩展名和 MIME 类型
451
+ // 例如:data:image/png;base64 -> png, image/png
452
+ // 例如:data:image/jpeg;base64 -> jpeg, image/jpeg
453
+ let ext = DEFAULT_EXT.replace(/^\./, '');
454
+ let mimeType = DEFAULT_MIME_TYPE;
375
455
  const mimeMatch = header.match(/data:([^;]+)/);
376
456
  if (mimeMatch && mimeMatch[1]) {
377
- const mimeType = mimeMatch[1];
378
- ext = MIME_TO_EXT[mimeType] || ext;
457
+ mimeType = mimeMatch[1];
458
+ // 使用 mime-types 包从 MIME 类型获取扩展名
459
+ const extension = mime.extension(mimeType);
460
+ if (extension) {
461
+ ext = extension;
462
+ }
379
463
  }
380
- return { isBase64: true, ext, data };
464
+ return { isBase64: true, ext, mimeType, data };
381
465
  }
382
466
  catch (error) {
383
467
  return { isBase64: false };
@@ -404,10 +488,12 @@ class ResourceCache {
404
488
  * 手动缓存指定 URL 的资源
405
489
  * @param url 要缓存的资源 URL(支持普通 URL 和 base64 data URL)
406
490
  * @param force 是否强制重新下载,即使缓存有效(默认 false)
407
- * @returns Promise<{ filePath: string, hostPath: string }> 返回缓存文件路径和主机路径
491
+ * @param ignoreOrigin 是否忽略来源检查(默认 false)
492
+ * @param onDownloadProgress 下载进度回调函数(可选)
493
+ * @returns Promise<{ filePath: string, hostPath: string, mimeType: string, size: number }> 返回缓存文件路径、主机路径、MIME 类型和文件大小
408
494
  * @throws 如果 URL 不匹配缓存规则或来源不允许,会抛出错误
409
495
  */
410
- async cacheUrl(url, force = false, ignoreOrigin = false) {
496
+ async cacheUrl(url, force = false, ignoreOrigin = false, onDownloadProgress) {
411
497
  // 检查是否是 base64 data URL
412
498
  const base64Info = this._isBase64DataUrl(url);
413
499
  if (base64Info.isBase64) {
@@ -417,16 +503,28 @@ class ResourceCache {
417
503
  }
418
504
  // 获取缓存路径(使用检测到的扩展名)
419
505
  const cachePath = this.getCachedPath(url, base64Info.ext);
506
+ const mimeType = base64Info.mimeType || this._getMimeTypeFromExt(base64Info.ext);
420
507
  // 如果缓存有效且不强制重新下载,直接返回
421
508
  if (!force && await this.isCacheValidAsync(cachePath.filePath)) {
422
- return cachePath;
509
+ const stats = await fs.promises.stat(cachePath.filePath);
510
+ return {
511
+ ...cachePath,
512
+ mimeType,
513
+ size: stats.size
514
+ };
423
515
  }
424
516
  // 保存 base64 数据到文件
425
517
  await this._saveBase64ToFile(base64Info.data, cachePath.filePath);
426
- return cachePath;
518
+ // 获取文件大小
519
+ const stats = await fs.promises.stat(cachePath.filePath);
520
+ return {
521
+ ...cachePath,
522
+ mimeType,
523
+ size: stats.size
524
+ };
427
525
  }
428
526
  // 处理普通 URL
429
- const shouldCache = this._getMatchFunction();
527
+ const shouldCache = ignoreOrigin ? () => true : this._getMatchFunction();
430
528
  const isAllowedOrigin = ignoreOrigin ? () => true : this._getOriginAllowFunction();
431
529
  // 检查是否匹配缓存规则
432
530
  if (!shouldCache(url)) {
@@ -438,40 +536,135 @@ class ResourceCache {
438
536
  }
439
537
  // 获取缓存路径
440
538
  const cachePath = this.getCachedPath(url);
539
+ // 从文件扩展名获取 MIME 类型
540
+ const ext = path.extname(cachePath.filePath).replace(/^\./, '') || DEFAULT_EXT.replace(/^\./, '');
541
+ const mimeType = this._getMimeTypeFromExt(ext);
441
542
  // 如果缓存有效且不强制重新下载,直接返回
442
543
  if (!force && await this.isCacheValidAsync(cachePath.filePath)) {
443
- return cachePath;
544
+ const stats = await fs.promises.stat(cachePath.filePath);
545
+ return {
546
+ ...cachePath,
547
+ mimeType,
548
+ size: stats.size
549
+ };
444
550
  }
445
551
  // 下载资源
446
- await this.downloadResourceAsync(url, cachePath.filePath);
447
- return cachePath;
552
+ await this.downloadResourceAsync(url, cachePath.filePath, 0, onDownloadProgress);
553
+ // 获取文件大小
554
+ const stats = await fs.promises.stat(cachePath.filePath);
555
+ return {
556
+ ...cachePath,
557
+ mimeType,
558
+ size: stats.size
559
+ };
448
560
  }
449
561
  /**
450
562
  * 批量缓存多个 URL 的资源
451
563
  * @param urls 要缓存的资源 URL 数组
452
564
  * @param force 是否强制重新下载,即使缓存有效(默认 false)
453
- * @returns Promise<Array<{ url: string, success: boolean, filePath?: string, hostPath?: string, error?: string }>> 返回每个 URL 的缓存结果
565
+ * @param ignoreOrigin 是否忽略来源检查(默认 false)
566
+ * @param onProgress 进度回调函数(可选)
567
+ * @returns Promise<Array<{ url: string, success: boolean, filePath?: string, hostPath?: string, mimeType?: string, size?: number, error?: string }>> 返回每个 URL 的缓存结果
454
568
  */
455
- async addCacheUrls(urls, force = false, ignoreOrigin = false) {
456
- const results = await Promise.allSettled(urls.map(url => this.cacheUrl(url, force, ignoreOrigin)));
457
- return results.map((result, index) => {
458
- const url = urls[index] || '';
459
- if (result.status === 'fulfilled') {
460
- return {
569
+ async addCacheUrls(urls, force = false, ignoreOrigin = false, onProgress) {
570
+ const total = urls.length;
571
+ const results = [];
572
+ let completed = 0;
573
+ // 存储每个 URL 的下载进度
574
+ const downloadProgressMap = new Map();
575
+ // 进度回调的包装函数,确保线程安全
576
+ const reportProgress = (url, success, result) => {
577
+ if (onProgress) {
578
+ completed++;
579
+ const downloadProgress = downloadProgressMap.get(url);
580
+ onProgress({
581
+ current: completed,
582
+ total,
583
+ url,
584
+ success,
585
+ result,
586
+ percentage: Math.round((completed / total) * 100),
587
+ downloadProgress: downloadProgress ? {
588
+ downloaded: downloadProgress.downloaded,
589
+ total: downloadProgress.total,
590
+ percentage: downloadProgress.percentage,
591
+ speed: downloadProgress.speed
592
+ } : undefined
593
+ });
594
+ }
595
+ };
596
+ // 并行处理所有 URL,但跟踪每个的完成状态
597
+ const promises = urls.map(async (url, index) => {
598
+ // 为每个 URL 创建下载进度回调
599
+ const onDownloadProgress = onProgress
600
+ ? (progress) => {
601
+ // 更新下载进度
602
+ downloadProgressMap.set(url, {
603
+ downloaded: progress.downloaded,
604
+ total: progress.total,
605
+ percentage: progress.percentage,
606
+ speed: progress.speed
607
+ });
608
+ // 实时报告下载进度(不增加 completed 计数)
609
+ if (onProgress) {
610
+ onProgress({
611
+ current: completed,
612
+ total,
613
+ url,
614
+ success: true, // 下载中视为进行中
615
+ result: undefined,
616
+ percentage: Math.round((completed / total) * 100),
617
+ downloadProgress: {
618
+ downloaded: progress.downloaded,
619
+ total: progress.total,
620
+ percentage: progress.percentage,
621
+ speed: progress.speed
622
+ }
623
+ });
624
+ }
625
+ }
626
+ : undefined;
627
+ try {
628
+ const result = await this.cacheUrl(url, force, ignoreOrigin, onDownloadProgress);
629
+ const item = {
461
630
  url,
462
631
  success: true,
463
- filePath: result.value.filePath,
464
- hostPath: result.value.hostPath
632
+ filePath: result.filePath,
633
+ hostPath: result.hostPath,
634
+ mimeType: result.mimeType,
635
+ size: result.size
465
636
  };
637
+ results[index] = item;
638
+ // 清除下载进度(已完成)
639
+ downloadProgressMap.delete(url);
640
+ // 调用进度回调
641
+ reportProgress(url, true, {
642
+ filePath: result.filePath,
643
+ hostPath: result.hostPath,
644
+ mimeType: result.mimeType,
645
+ size: result.size
646
+ });
647
+ return item;
466
648
  }
467
- else {
468
- return {
649
+ catch (error) {
650
+ const item = {
469
651
  url,
470
652
  success: false,
471
- error: result.reason?.message || '未知错误'
653
+ error: error instanceof Error ? error.message : '未知错误'
472
654
  };
655
+ results[index] = item;
656
+ // 清除下载进度(失败)
657
+ downloadProgressMap.delete(url);
658
+ // 调用进度回调
659
+ reportProgress(url, false, {
660
+ error: item.error
661
+ });
662
+ return item;
473
663
  }
474
664
  });
665
+ // 等待所有请求完成
666
+ await Promise.allSettled(promises);
667
+ return results;
475
668
  }
476
669
  /**
477
670
  * 删除多个 URL 的资源
@@ -553,45 +746,44 @@ class ResourceCache {
553
746
  }
554
747
  }
555
748
  /**
556
- * 清理所有缓存文件
749
+ * 清理所有缓存文件(完全异步版本,性能更好)
557
750
  * @returns Promise<{ success: number, failed: number, totalSize: number }> 返回清理统计信息
558
751
  */
559
752
  async clearCache() {
560
753
  try {
561
- const files = fs.readdirSync(this.options.cacheDir);
754
+ const files = await fs.promises.readdir(this.options.cacheDir);
562
755
  if (files.length === 0) {
563
- console.log('缓存目录为空,无需清理');
564
756
  return { success: 0, failed: 0, totalSize: 0 };
565
757
  }
566
- // 先统计所有文件大小(避免并行时的竞态条件)
567
- const fileInfos = files.map((file) => {
758
+ // 并行获取所有文件信息(避免并行时的竞态条件)
759
+ const fileInfos = await Promise.allSettled(files.map(async (file) => {
568
760
  const filePath = path.join(this.options.cacheDir, file);
569
761
  try {
570
- const stats = fs.statSync(filePath);
762
+ const stats = await fs.promises.stat(filePath);
571
763
  return { file, filePath, size: stats.size };
572
764
  }
573
765
  catch (error) {
574
766
  return { file, filePath, size: 0, error };
575
767
  }
576
- });
768
+ }));
769
+ const validInfos = fileInfos
770
+ .filter((r) => r.status === 'fulfilled')
771
+ .map(r => r.value);
577
772
  // 计算总大小
578
- const totalSize = fileInfos.reduce((sum, info) => sum + info.size, 0);
773
+ const totalSize = validInfos.reduce((sum, info) => sum + info.size, 0);
579
774
  // 并行删除文件,提升性能
580
- const deleteResults = await Promise.allSettled(fileInfos.map(async (info) => {
775
+ const deleteResults = await Promise.allSettled(validInfos.map(async (info) => {
581
776
  try {
582
777
  await fs.promises.unlink(info.filePath);
583
778
  return { success: true, file: info.file };
584
779
  }
585
780
  catch (error) {
586
- console.log(`清理缓存文件失败: ${info.file}`, error);
587
781
  throw error;
588
782
  }
589
783
  }));
590
784
  // 统计成功和失败数量
591
785
  const success = deleteResults.filter(r => r.status === 'fulfilled').length;
592
786
  const failed = deleteResults.filter(r => r.status === 'rejected').length;
593
- const sizeMB = (totalSize / (1024 * 1024)).toFixed(2);
594
- console.log(`缓存清理完成: 成功 ${success} 个, 失败 ${failed} 个, 释放空间 ${sizeMB} MB`);
595
787
  return { success, failed, totalSize };
596
788
  }
597
789
  catch (error) {