@lynker-desktop/electron-sdk 0.0.9-alpha.53 → 0.0.9-alpha.55

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.
@@ -25,6 +25,8 @@ export declare class ResourceCache {
25
25
  private _cachedMatchFunction?;
26
26
  /** 缓存的来源校验函数(避免重复创建) */
27
27
  private _cachedOriginFunction?;
28
+ /** 正在下载的 URL 集合(避免重复下载) */
29
+ private _downloadingUrls;
28
30
  /**
29
31
  * 构造函数
30
32
  * @param session Electron session
@@ -69,19 +71,26 @@ export declare class ResourceCache {
69
71
  * 下载资源到本地缓存(异步版本,返回 Promise)
70
72
  * @param url 资源URL
71
73
  * @param filePath 本地缓存路径
74
+ * @param redirectCount 当前重定向次数(内部使用)
72
75
  * @returns Promise<void> 下载完成或失败
73
76
  */
74
- downloadResourceAsync(url: string, filePath: string): Promise<void>;
77
+ downloadResourceAsync(url: string, filePath: string, redirectCount?: number): Promise<void>;
75
78
  /**
76
79
  * 下载资源到本地缓存(同步版本,不返回 Promise,用于拦截器)
77
80
  * @param url 资源URL
78
81
  * @param filePath 本地缓存路径
79
82
  */
80
83
  downloadResource(url: string, filePath: string): void;
84
+ /**
85
+ * 从文件扩展名获取 MIME 类型
86
+ * @param ext 文件扩展名(带或不带点)
87
+ * @returns MIME 类型
88
+ */
89
+ private _getMimeTypeFromExt;
81
90
  /**
82
91
  * 检测并处理 base64 data URL
83
92
  * @param url 资源URL
84
- * @returns 如果是 base64 URL,返回 true 和文件扩展名;否则返回 false
93
+ * @returns 如果是 base64 URL,返回 true、文件扩展名、MIME 类型和数据;否则返回 false
85
94
  */
86
95
  private _isBase64DataUrl;
87
96
  /**
@@ -94,24 +103,28 @@ export declare class ResourceCache {
94
103
  * 手动缓存指定 URL 的资源
95
104
  * @param url 要缓存的资源 URL(支持普通 URL 和 base64 data URL)
96
105
  * @param force 是否强制重新下载,即使缓存有效(默认 false)
97
- * @returns Promise<{ filePath: string, hostPath: string }> 返回缓存文件路径和主机路径
106
+ * @returns Promise<{ filePath: string, hostPath: string, mimeType: string, size: number }> 返回缓存文件路径、主机路径、MIME 类型和文件大小
98
107
  * @throws 如果 URL 不匹配缓存规则或来源不允许,会抛出错误
99
108
  */
100
109
  cacheUrl(url: string, force?: boolean, ignoreOrigin?: boolean): Promise<{
101
110
  filePath: string;
102
111
  hostPath: string;
112
+ mimeType: string;
113
+ size: number;
103
114
  }>;
104
115
  /**
105
116
  * 批量缓存多个 URL 的资源
106
117
  * @param urls 要缓存的资源 URL 数组
107
118
  * @param force 是否强制重新下载,即使缓存有效(默认 false)
108
- * @returns Promise<Array<{ url: string, success: boolean, filePath?: string, hostPath?: string, error?: string }>> 返回每个 URL 的缓存结果
119
+ * @returns Promise<Array<{ url: string, success: boolean, filePath?: string, hostPath?: string, mimeType?: string, size?: number, error?: string }>> 返回每个 URL 的缓存结果
109
120
  */
110
121
  addCacheUrls(urls: string[], force?: boolean, ignoreOrigin?: boolean): Promise<Array<{
111
122
  url: string;
112
123
  success: boolean;
113
124
  filePath?: string;
114
125
  hostPath?: string;
126
+ mimeType?: string;
127
+ size?: number;
115
128
  error?: string;
116
129
  }>>;
117
130
  /**
@@ -148,7 +161,7 @@ export declare class ResourceCache {
148
161
  */
149
162
  private _cleanOldCache;
150
163
  /**
151
- * 清理所有缓存文件
164
+ * 清理所有缓存文件(完全异步版本,性能更好)
152
165
  * @returns Promise<{ success: number, failed: number, totalSize: number }> 返回清理统计信息
153
166
  */
154
167
  clearCache(): Promise<{
@@ -1 +1 @@
1
- {"version":3,"file":"resource-cache.d.ts","sourceRoot":"","sources":["../../src/main/resource-cache.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"resource-cache.d.ts","sourceRoot":"","sources":["../../src/main/resource-cache.ts"],"names":[],"mappings":"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"}
@@ -4,42 +4,24 @@ import http from 'node:http';
4
4
  import https from 'node:https';
5
5
  import md5 from 'md5';
6
6
  import ipc__default from '@lynker-desktop/electron-ipc/main';
7
+ import mime from 'mime-types';
7
8
 
8
9
  /**
9
- * MIME 类型到文件扩展名的映射表
10
+ * HTTP 重定向状态码
10
11
  */
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
- };
12
+ const REDIRECT_STATUS_CODES = [301, 302, 307, 308];
13
+ /**
14
+ * 默认文件扩展名
15
+ */
16
+ const DEFAULT_EXT = '.res';
17
+ /**
18
+ * 默认 MIME 类型
19
+ */
20
+ const DEFAULT_MIME_TYPE = 'application/octet-stream';
21
+ /**
22
+ * 最大重定向次数
23
+ */
24
+ const MAX_REDIRECTS = 5;
43
25
  /**
44
26
  * 默认配置
45
27
  */
@@ -68,6 +50,8 @@ class ResourceCache {
68
50
  */
69
51
  constructor(session, options) {
70
52
  this.cacheHost = `${ResourceCache.scheme}://-`;
53
+ /** 正在下载的 URL 集合(避免重复下载) */
54
+ this._downloadingUrls = new Set();
71
55
  if (!session)
72
56
  throw new Error('ResourceCache: session is required');
73
57
  this.session = session;
@@ -93,19 +77,29 @@ class ResourceCache {
93
77
  console.log('初始化时清理过期缓存失败:', err);
94
78
  });
95
79
  ipc__default.mainIPC.handleRenderer('core:cache', async (options) => {
96
- if (options.method === 'clear') {
97
- return await this.clearCache();
98
- }
99
- if (options.method === 'add') {
100
- return await this.addCacheUrls(options.urls ?? [], options.force ?? false, true);
101
- }
102
- if (options.method === 'delete') {
103
- return await this.deleteCacheUrls(options.urls ?? []);
80
+ try {
81
+ switch (options.method) {
82
+ case 'clear':
83
+ return await this.clearCache();
84
+ case 'add': {
85
+ const urls = Array.isArray(options.urls) ? options.urls : options.urls ? [options.urls] : [];
86
+ const data = await this.addCacheUrls(urls, options.force ?? false, true);
87
+ return Array.isArray(options.urls) ? data : data[0];
88
+ }
89
+ case 'delete': {
90
+ const urls = Array.isArray(options.urls) ? options.urls : options.urls ? [options.urls] : [];
91
+ const data = await this.deleteCacheUrls(urls);
92
+ return Array.isArray(options.urls) ? data : data[0];
93
+ }
94
+ case 'stats':
95
+ return await this.getCacheStats();
96
+ default:
97
+ return undefined;
98
+ }
104
99
  }
105
- if (options.method === 'stats') {
106
- return await this.getCacheStats();
100
+ catch (error) {
101
+ return { success: false, error: error instanceof Error ? error.message : '未知错误' };
107
102
  }
108
- return undefined;
109
103
  });
110
104
  }
111
105
  /**
@@ -204,11 +198,11 @@ class ResourceCache {
204
198
  // 尝试从 URL 中提取扩展名
205
199
  try {
206
200
  const urlObj = new URL(url);
207
- ext = path.extname(urlObj.pathname) || '.res';
201
+ ext = path.extname(urlObj.pathname) || DEFAULT_EXT;
208
202
  }
209
203
  catch {
210
204
  // 如果 URL 解析失败(可能是 base64 data URL),使用默认扩展名
211
- ext = '.res';
205
+ ext = DEFAULT_EXT;
212
206
  }
213
207
  }
214
208
  return {
@@ -248,45 +242,53 @@ class ResourceCache {
248
242
  * 下载资源到本地缓存(异步版本,返回 Promise)
249
243
  * @param url 资源URL
250
244
  * @param filePath 本地缓存路径
245
+ * @param redirectCount 当前重定向次数(内部使用)
251
246
  * @returns Promise<void> 下载完成或失败
252
247
  */
253
- downloadResourceAsync(url, filePath) {
248
+ downloadResourceAsync(url, filePath, redirectCount = 0) {
249
+ // 检查是否正在下载,避免重复下载
250
+ if (this._downloadingUrls.has(url)) {
251
+ return Promise.reject(new Error(`资源正在下载中: ${url}`));
252
+ }
253
+ // 检查重定向次数限制
254
+ if (redirectCount >= MAX_REDIRECTS) {
255
+ return Promise.reject(new Error(`重定向次数超过限制 (${MAX_REDIRECTS}): ${url}`));
256
+ }
257
+ this._downloadingUrls.add(url);
254
258
  return new Promise((resolve, reject) => {
255
259
  const tempFilePath = `${filePath}.cache`;
256
260
  const lib = url.startsWith('https') ? https : http;
257
261
  const file = fs.createWriteStream(tempFilePath);
258
262
  let request;
259
263
  const cleanupAndAbort = (errMsg, err) => {
260
- if (err) {
261
- console.log(errMsg, err);
262
- }
263
- else {
264
- console.log(errMsg);
265
- }
264
+ this._downloadingUrls.delete(url);
266
265
  if (request) {
267
266
  request.destroy();
268
267
  }
269
268
  file.close(() => {
270
- // 使用 existsSync 避免在文件不存在时 unlink 抛出错误
271
- if (fs.existsSync(tempFilePath)) {
272
- fs.unlink(tempFilePath, () => { });
273
- }
269
+ // 异步删除临时文件,不阻塞
270
+ fs.promises.unlink(tempFilePath).catch(() => {
271
+ // 忽略删除失败
272
+ });
274
273
  });
275
- reject(new Error(errMsg));
274
+ const error = err instanceof Error ? err : new Error(errMsg);
275
+ reject(error);
276
276
  };
277
277
  request = lib.get(url, (res) => {
278
278
  // 处理重定向
279
- if (res.statusCode === 301 || res.statusCode === 302 || res.statusCode === 307 || res.statusCode === 308) {
279
+ if (res.statusCode && REDIRECT_STATUS_CODES.includes(res.statusCode)) {
280
280
  const location = res.headers.location;
281
281
  if (location) {
282
282
  request.destroy();
283
283
  file.close(() => {
284
- if (fs.existsSync(tempFilePath)) {
285
- fs.unlink(tempFilePath, () => { });
286
- }
284
+ // 异步删除临时文件,不阻塞
285
+ fs.promises.unlink(tempFilePath).catch(() => {
286
+ // 忽略删除失败
287
+ });
287
288
  });
288
- // 递归处理重定向
289
- this.downloadResourceAsync(location, filePath).then(resolve).catch(reject);
289
+ this._downloadingUrls.delete(url);
290
+ // 递归处理重定向,增加重定向计数
291
+ this.downloadResourceAsync(location, filePath, redirectCount + 1).then(resolve).catch(reject);
290
292
  return;
291
293
  }
292
294
  // 如果没有 location,继续处理为错误
@@ -304,6 +306,7 @@ class ResourceCache {
304
306
  return cleanupAndAbort(`关闭临时文件流失败: ${tempFilePath}`, err);
305
307
  }
306
308
  fs.rename(tempFilePath, filePath, (renameErr) => {
309
+ this._downloadingUrls.delete(url);
307
310
  if (renameErr) {
308
311
  cleanupAndAbort(`缓存文件重命名失败 from ${tempFilePath} to ${filePath}`, renameErr);
309
312
  }
@@ -319,6 +322,13 @@ class ResourceCache {
319
322
  request.on('error', (err) => {
320
323
  cleanupAndAbort(`下载资源请求失败: ${url}`, err);
321
324
  });
325
+ // 添加超时处理(30秒)
326
+ const timeout = setTimeout(() => {
327
+ cleanupAndAbort(`下载超时: ${url}`);
328
+ }, 30000);
329
+ request.on('close', () => {
330
+ clearTimeout(timeout);
331
+ });
322
332
  });
323
333
  }
324
334
  /**
@@ -328,14 +338,28 @@ class ResourceCache {
328
338
  */
329
339
  downloadResource(url, filePath) {
330
340
  // 异步执行,不等待完成
331
- this.downloadResourceAsync(url, filePath).catch((err) => {
332
- console.log('后台下载资源失败:', err);
341
+ // 如果正在下载,跳过(避免重复下载)
342
+ if (this._downloadingUrls.has(url)) {
343
+ return;
344
+ }
345
+ this.downloadResourceAsync(url, filePath).catch(() => {
346
+ // 静默处理错误,避免日志过多
333
347
  });
334
348
  }
349
+ /**
350
+ * 从文件扩展名获取 MIME 类型
351
+ * @param ext 文件扩展名(带或不带点)
352
+ * @returns MIME 类型
353
+ */
354
+ _getMimeTypeFromExt(ext) {
355
+ const cleanExt = ext.replace(/^\./, '').toLowerCase();
356
+ const mimeType = mime.lookup(cleanExt);
357
+ return mimeType || DEFAULT_MIME_TYPE;
358
+ }
335
359
  /**
336
360
  * 检测并处理 base64 data URL
337
361
  * @param url 资源URL
338
- * @returns 如果是 base64 URL,返回 true 和文件扩展名;否则返回 false
362
+ * @returns 如果是 base64 URL,返回 true、文件扩展名、MIME 类型和数据;否则返回 false
339
363
  */
340
364
  _isBase64DataUrl(url) {
341
365
  if (!url.startsWith('data:')) {
@@ -353,16 +377,21 @@ class ResourceCache {
353
377
  if (!header.includes('base64')) {
354
378
  return { isBase64: false };
355
379
  }
356
- // 从 mediatype 中提取文件扩展名
357
- // 例如:data:image/png;base64 -> png
358
- // 例如:data:image/jpeg;base64 -> jpeg
359
- let ext = 'res';
380
+ // 从 mediatype 中提取文件扩展名和 MIME 类型
381
+ // 例如:data:image/png;base64 -> png, image/png
382
+ // 例如:data:image/jpeg;base64 -> jpeg, image/jpeg
383
+ let ext = DEFAULT_EXT.replace(/^\./, '');
384
+ let mimeType = DEFAULT_MIME_TYPE;
360
385
  const mimeMatch = header.match(/data:([^;]+)/);
361
386
  if (mimeMatch && mimeMatch[1]) {
362
- const mimeType = mimeMatch[1];
363
- ext = MIME_TO_EXT[mimeType] || ext;
387
+ mimeType = mimeMatch[1];
388
+ // 使用 mime-types 包从 MIME 类型获取扩展名
389
+ const extension = mime.extension(mimeType);
390
+ if (extension) {
391
+ ext = extension;
392
+ }
364
393
  }
365
- return { isBase64: true, ext, data };
394
+ return { isBase64: true, ext, mimeType, data };
366
395
  }
367
396
  catch (error) {
368
397
  return { isBase64: false };
@@ -389,7 +418,7 @@ class ResourceCache {
389
418
  * 手动缓存指定 URL 的资源
390
419
  * @param url 要缓存的资源 URL(支持普通 URL 和 base64 data URL)
391
420
  * @param force 是否强制重新下载,即使缓存有效(默认 false)
392
- * @returns Promise<{ filePath: string, hostPath: string }> 返回缓存文件路径和主机路径
421
+ * @returns Promise<{ filePath: string, hostPath: string, mimeType: string, size: number }> 返回缓存文件路径、主机路径、MIME 类型和文件大小
393
422
  * @throws 如果 URL 不匹配缓存规则或来源不允许,会抛出错误
394
423
  */
395
424
  async cacheUrl(url, force = false, ignoreOrigin = false) {
@@ -402,13 +431,25 @@ class ResourceCache {
402
431
  }
403
432
  // 获取缓存路径(使用检测到的扩展名)
404
433
  const cachePath = this.getCachedPath(url, base64Info.ext);
434
+ const mimeType = base64Info.mimeType || this._getMimeTypeFromExt(base64Info.ext);
405
435
  // 如果缓存有效且不强制重新下载,直接返回
406
436
  if (!force && await this.isCacheValidAsync(cachePath.filePath)) {
407
- return cachePath;
437
+ const stats = await fs.promises.stat(cachePath.filePath);
438
+ return {
439
+ ...cachePath,
440
+ mimeType,
441
+ size: stats.size
442
+ };
408
443
  }
409
444
  // 保存 base64 数据到文件
410
445
  await this._saveBase64ToFile(base64Info.data, cachePath.filePath);
411
- return cachePath;
446
+ // 获取文件大小
447
+ const stats = await fs.promises.stat(cachePath.filePath);
448
+ return {
449
+ ...cachePath,
450
+ mimeType,
451
+ size: stats.size
452
+ };
412
453
  }
413
454
  // 处理普通 URL
414
455
  const shouldCache = this._getMatchFunction();
@@ -423,19 +464,33 @@ class ResourceCache {
423
464
  }
424
465
  // 获取缓存路径
425
466
  const cachePath = this.getCachedPath(url);
467
+ // 从文件扩展名获取 MIME 类型
468
+ const ext = path.extname(cachePath.filePath).replace(/^\./, '') || DEFAULT_EXT.replace(/^\./, '');
469
+ const mimeType = this._getMimeTypeFromExt(ext);
426
470
  // 如果缓存有效且不强制重新下载,直接返回
427
471
  if (!force && await this.isCacheValidAsync(cachePath.filePath)) {
428
- return cachePath;
472
+ const stats = await fs.promises.stat(cachePath.filePath);
473
+ return {
474
+ ...cachePath,
475
+ mimeType,
476
+ size: stats.size
477
+ };
429
478
  }
430
479
  // 下载资源
431
480
  await this.downloadResourceAsync(url, cachePath.filePath);
432
- return cachePath;
481
+ // 获取文件大小
482
+ const stats = await fs.promises.stat(cachePath.filePath);
483
+ return {
484
+ ...cachePath,
485
+ mimeType,
486
+ size: stats.size
487
+ };
433
488
  }
434
489
  /**
435
490
  * 批量缓存多个 URL 的资源
436
491
  * @param urls 要缓存的资源 URL 数组
437
492
  * @param force 是否强制重新下载,即使缓存有效(默认 false)
438
- * @returns Promise<Array<{ url: string, success: boolean, filePath?: string, hostPath?: string, error?: string }>> 返回每个 URL 的缓存结果
493
+ * @returns Promise<Array<{ url: string, success: boolean, filePath?: string, hostPath?: string, mimeType?: string, size?: number, error?: string }>> 返回每个 URL 的缓存结果
439
494
  */
440
495
  async addCacheUrls(urls, force = false, ignoreOrigin = false) {
441
496
  const results = await Promise.allSettled(urls.map(url => this.cacheUrl(url, force, ignoreOrigin)));
@@ -446,7 +501,9 @@ class ResourceCache {
446
501
  url,
447
502
  success: true,
448
503
  filePath: result.value.filePath,
449
- hostPath: result.value.hostPath
504
+ hostPath: result.value.hostPath,
505
+ mimeType: result.value.mimeType,
506
+ size: result.value.size
450
507
  };
451
508
  }
452
509
  else {
@@ -538,45 +595,44 @@ class ResourceCache {
538
595
  }
539
596
  }
540
597
  /**
541
- * 清理所有缓存文件
598
+ * 清理所有缓存文件(完全异步版本,性能更好)
542
599
  * @returns Promise<{ success: number, failed: number, totalSize: number }> 返回清理统计信息
543
600
  */
544
601
  async clearCache() {
545
602
  try {
546
- const files = fs.readdirSync(this.options.cacheDir);
603
+ const files = await fs.promises.readdir(this.options.cacheDir);
547
604
  if (files.length === 0) {
548
- console.log('缓存目录为空,无需清理');
549
605
  return { success: 0, failed: 0, totalSize: 0 };
550
606
  }
551
- // 先统计所有文件大小(避免并行时的竞态条件)
552
- const fileInfos = files.map((file) => {
607
+ // 并行获取所有文件信息(避免并行时的竞态条件)
608
+ const fileInfos = await Promise.allSettled(files.map(async (file) => {
553
609
  const filePath = path.join(this.options.cacheDir, file);
554
610
  try {
555
- const stats = fs.statSync(filePath);
611
+ const stats = await fs.promises.stat(filePath);
556
612
  return { file, filePath, size: stats.size };
557
613
  }
558
614
  catch (error) {
559
615
  return { file, filePath, size: 0, error };
560
616
  }
561
- });
617
+ }));
618
+ const validInfos = fileInfos
619
+ .filter((r) => r.status === 'fulfilled')
620
+ .map(r => r.value);
562
621
  // 计算总大小
563
- const totalSize = fileInfos.reduce((sum, info) => sum + info.size, 0);
622
+ const totalSize = validInfos.reduce((sum, info) => sum + info.size, 0);
564
623
  // 并行删除文件,提升性能
565
- const deleteResults = await Promise.allSettled(fileInfos.map(async (info) => {
624
+ const deleteResults = await Promise.allSettled(validInfos.map(async (info) => {
566
625
  try {
567
626
  await fs.promises.unlink(info.filePath);
568
627
  return { success: true, file: info.file };
569
628
  }
570
629
  catch (error) {
571
- console.log(`清理缓存文件失败: ${info.file}`, error);
572
630
  throw error;
573
631
  }
574
632
  }));
575
633
  // 统计成功和失败数量
576
634
  const success = deleteResults.filter(r => r.status === 'fulfilled').length;
577
635
  const failed = deleteResults.filter(r => r.status === 'rejected').length;
578
- const sizeMB = (totalSize / (1024 * 1024)).toFixed(2);
579
- console.log(`缓存清理完成: 成功 ${success} 个, 失败 ${failed} 个, 释放空间 ${sizeMB} MB`);
580
636
  return { success, failed, totalSize };
581
637
  }
582
638
  catch (error) {