@lynker-desktop/electron-sdk 0.0.9-alpha.60 → 0.0.9-alpha.62

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.
@@ -76,12 +76,20 @@ declare class Downloader {
76
76
  * @returns Promise<DownloadResult> 下载结果
77
77
  */
78
78
  download(url: string, options?: DownloadOptions, onProgress?: DownloadProgressCallback): Promise<DownloadResult>;
79
+ /**
80
+ * 生成下载任务的唯一标识
81
+ * @param url 下载 URL
82
+ * @param id 可选的任务 ID
83
+ * @returns 唯一标识
84
+ */
85
+ private _getDownloadKey;
79
86
  /**
80
87
  * 取消下载
81
88
  * @param url 要取消的 URL
89
+ * @param id 可选的任务 ID
82
90
  * @returns 是否成功取消
83
91
  */
84
- cancel(url: string): boolean;
92
+ cancel(url: string, id?: string): boolean;
85
93
  /**
86
94
  * 取消所有正在进行的下载
87
95
  */
@@ -89,14 +97,18 @@ declare class Downloader {
89
97
  /**
90
98
  * 检查指定 URL 是否正在下载
91
99
  * @param url 要检查的 URL
100
+ * @param id 可选的任务 ID
92
101
  * @returns 是否正在下载
93
102
  */
94
- isDownloading(url: string): boolean;
103
+ isDownloading(url: string, id?: string): boolean;
95
104
  /**
96
- * 获取正在下载的 URL 列表
97
- * @returns 正在下载的 URL 数组
105
+ * 获取正在下载的任务列表
106
+ * @returns 正在下载的任务信息数组,包含 URL 和 ID(如果有)
98
107
  */
99
- getDownloadingUrls(): string[];
108
+ getDownloadingUrls(): Array<{
109
+ url: string;
110
+ id?: string;
111
+ }>;
100
112
  /**
101
113
  * 显示保存对话框
102
114
  * @param url 下载 URL
@@ -1 +1 @@
1
- {"version":3,"file":"downloader.d.ts","sourceRoot":"","sources":["../../src/main/downloader.ts"],"names":[],"mappings":"AA4CA;;GAEG;AACH,MAAM,MAAM,wBAAwB,GAAG,CAAC,QAAQ,EAAE;IAChD,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;CACf,KAAK,IAAI,CAAC;AAEX;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,sBAAsB;IACtB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,mCAAmC;IACnC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,4BAA4B;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,+BAA+B;IAC/B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,0BAA0B;IAC1B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,2BAA2B;IAC3B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,sBAAsB;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,wBAAwB;IACxB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW;IACX,KAAK,CAAC,EAAE;QACN,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,CAAC;IACF,mBAAmB;IACnB,UAAU,CAAC,EAAE,wBAAwB,CAAC;IACtC,mBAAmB;IACnB,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,mBAAmB;IACnB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IACjC,gBAAgB;IAChB,kBAAkB,CAAC,EAAE;QACnB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QACjC,cAAc,CAAC,EAAE,OAAO,CAAC;QACzB,YAAY,CAAC,EAAE,MAAM,CAAC;KACvB,CAAC;CACH;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,WAAW;IACX,OAAO,EAAE,OAAO,CAAC;IACjB,cAAc;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW;IACX,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,eAAe;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;GAEG;AACH,cAAM,UAAU;IACd,2BAA2B;IAC3B,OAAO,CAAC,gBAAgB,CAAuC;IAC/D,oBAAoB;IACpB,OAAO,CAAC,gBAAgB,CAAuB;;IAiC/C;;;;;;OAMG;IACU,QAAQ,CACnB,GAAG,EAAE,MAAM,EACX,OAAO,GAAE,eAAoB,EAC7B,UAAU,CAAC,EAAE,wBAAwB,GACpC,OAAO,CAAC,cAAc,CAAC;IAsJ1B;;;;OAIG;IACI,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAUnC;;OAEG;IACI,SAAS,IAAI,IAAI;IAOxB;;;;OAIG;IACI,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAI1C;;;OAGG;IACI,kBAAkB,IAAI,MAAM,EAAE;IAIrC;;;;;;OAMG;YACW,eAAe;IAyE7B;;;;OAIG;IACH,OAAO,CAAC,gBAAgB;IAyCxB;;;;OAIG;YACW,iBAAiB;IAa/B;;;;;OAKG;YACW,kBAAkB;IAoBhC;;;;;OAKG;IACH,OAAO,CAAC,oBAAoB;IAQ5B;;;;OAIG;IACH,OAAO,CAAC,kBAAkB;IAO1B;;;;;;;;OAQG;YACW,qBAAqB;IA6CnC;;;;;OAKG;IACH,OAAO,CAAC,uBAAuB;IAyB/B;;;;OAIG;IACH,OAAO,CAAC,uBAAuB;IAU/B;;;;;;OAMG;IACH,OAAO,CAAC,kBAAkB;CA2B3B;AAOD;;;GAGG;AACH,wBAAgB,aAAa,IAAI,UAAU,CAK1C;AAED;;;;;;GAMG;AACH,wBAAsB,YAAY,CAChC,GAAG,EAAE,MAAM,EACX,OAAO,GAAE,eAAoB,EAC7B,UAAU,CAAC,EAAE,wBAAwB,GACpC,OAAO,CAAC,cAAc,CAAC,CAGzB"}
1
+ {"version":3,"file":"downloader.d.ts","sourceRoot":"","sources":["../../src/main/downloader.ts"],"names":[],"mappings":"AA4CA;;GAEG;AACH,MAAM,MAAM,wBAAwB,GAAG,CAAC,QAAQ,EAAE;IAChD,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;CACf,KAAK,IAAI,CAAC;AAEX;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,sBAAsB;IACtB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,mCAAmC;IACnC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,4BAA4B;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,+BAA+B;IAC/B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,0BAA0B;IAC1B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,2BAA2B;IAC3B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,sBAAsB;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,wBAAwB;IACxB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW;IACX,KAAK,CAAC,EAAE;QACN,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,CAAC;IACF,mBAAmB;IACnB,UAAU,CAAC,EAAE,wBAAwB,CAAC;IACtC,mBAAmB;IACnB,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,mBAAmB;IACnB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IACjC,gBAAgB;IAChB,kBAAkB,CAAC,EAAE;QACnB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QACjC,cAAc,CAAC,EAAE,OAAO,CAAC;QACzB,YAAY,CAAC,EAAE,MAAM,CAAC;KACvB,CAAC;CACH;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,WAAW;IACX,OAAO,EAAE,OAAO,CAAC;IACjB,cAAc;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW;IACX,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,eAAe;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;GAEG;AACH,cAAM,UAAU;IACd,2BAA2B;IAC3B,OAAO,CAAC,gBAAgB,CAAuC;IAC/D,oBAAoB;IACpB,OAAO,CAAC,gBAAgB,CAAuB;;IAiC/C;;;;;;OAMG;IACU,QAAQ,CACnB,GAAG,EAAE,MAAM,EACX,OAAO,GAAE,eAAoB,EAC7B,UAAU,CAAC,EAAE,wBAAwB,GACpC,OAAO,CAAC,cAAc,CAAC;IAyJ1B;;;;;OAKG;IACH,OAAO,CAAC,eAAe;IAIvB;;;;;OAKG;IACI,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO;IAWhD;;OAEG;IACI,SAAS,IAAI,IAAI;IAOxB;;;;;OAKG;IACI,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO;IAKvD;;;OAGG;IACI,kBAAkB,IAAI,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,EAAE,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAUhE;;;;;;OAMG;YACW,eAAe;IAyE7B;;;;OAIG;IACH,OAAO,CAAC,gBAAgB;IAyCxB;;;;OAIG;YACW,iBAAiB;IAa/B;;;;;OAKG;YACW,kBAAkB;IAoBhC;;;;;OAKG;IACH,OAAO,CAAC,oBAAoB;IAQ5B;;;;OAIG;IACH,OAAO,CAAC,kBAAkB;IAO1B;;;;;;;;OAQG;YACW,qBAAqB;IA6CnC;;;;;OAKG;IACH,OAAO,CAAC,uBAAuB;IAyB/B;;;;OAIG;IACH,OAAO,CAAC,uBAAuB;IAU/B;;;;;;OAMG;IACH,OAAO,CAAC,kBAAkB;CA2B3B;AAOD;;;GAGG;AACH,wBAAgB,aAAa,IAAI,UAAU,CAK1C;AAED;;;;;;GAMG;AACH,wBAAsB,YAAY,CAChC,GAAG,EAAE,MAAM,EACX,OAAO,GAAE,eAAoB,EAC7B,UAAU,CAAC,EAAE,wBAAwB,GACpC,OAAO,CAAC,cAAc,CAAC,CAGzB"}
@@ -81,9 +81,11 @@ class Downloader {
81
81
  * @returns Promise<DownloadResult> 下载结果
82
82
  */
83
83
  async download(url, options = {}, onProgress) {
84
+ // 生成唯一标识:URL + ID(如果存在)
85
+ const downloadKey = this._getDownloadKey(url, options.id);
84
86
  // 检查是否正在下载,避免重复下载
85
- if (this._downloadingUrls.has(url)) {
86
- return this._createErrorResult(`资源正在下载中: ${url}`);
87
+ if (this._downloadingUrls.has(downloadKey)) {
88
+ return this._createErrorResult(`资源正在下载中: ${url}${options.id ? ` (ID: ${options.id})` : ''}`);
87
89
  }
88
90
  // 合并进度回调:优先使用传入的 onProgress,其次使用 options.onProgress
89
91
  const progressCallback = onProgress || options.onProgress;
@@ -136,8 +138,8 @@ class Downloader {
136
138
  headers: options.httpRequestOptions?.headers
137
139
  }
138
140
  });
139
- // 记录正在下载的 URL
140
- this._downloadingUrls.set(url, dl);
141
+ // 记录正在下载的 URL(使用 URL + ID 作为 key)
142
+ this._downloadingUrls.set(downloadKey, dl);
141
143
  // 监听下载进度
142
144
  if (progressCallback) {
143
145
  dl.on('progress', (stats) => {
@@ -158,7 +160,7 @@ class Downloader {
158
160
  }
159
161
  // 监听下载完成
160
162
  dl.on('end', () => {
161
- this._downloadingUrls.delete(url);
163
+ this._downloadingUrls.delete(downloadKey);
162
164
  // 将临时文件重命名为最终文件
163
165
  fs.rename(tempFilePath, outputPath, (renameErr) => {
164
166
  if (renameErr) {
@@ -181,7 +183,7 @@ class Downloader {
181
183
  });
182
184
  // 统一的错误处理函数
183
185
  const handleError = (error) => {
184
- this._downloadingUrls.delete(url);
186
+ this._downloadingUrls.delete(downloadKey);
185
187
  // 清理临时文件
186
188
  fs.promises.unlink(tempFilePath).catch(() => {
187
189
  // 忽略删除失败
@@ -210,16 +212,27 @@ class Downloader {
210
212
  return this._createErrorResult(error instanceof Error ? error.message : '未知错误');
211
213
  });
212
214
  }
215
+ /**
216
+ * 生成下载任务的唯一标识
217
+ * @param url 下载 URL
218
+ * @param id 可选的任务 ID
219
+ * @returns 唯一标识
220
+ */
221
+ _getDownloadKey(url, id) {
222
+ return id ? `${url}::${id}` : url;
223
+ }
213
224
  /**
214
225
  * 取消下载
215
226
  * @param url 要取消的 URL
227
+ * @param id 可选的任务 ID
216
228
  * @returns 是否成功取消
217
229
  */
218
- cancel(url) {
219
- const dl = this._downloadingUrls.get(url);
230
+ cancel(url, id) {
231
+ const downloadKey = this._getDownloadKey(url, id);
232
+ const dl = this._downloadingUrls.get(downloadKey);
220
233
  if (dl) {
221
234
  dl.stop();
222
- this._downloadingUrls.delete(url);
235
+ this._downloadingUrls.delete(downloadKey);
223
236
  return true;
224
237
  }
225
238
  return false;
@@ -236,17 +249,25 @@ class Downloader {
236
249
  /**
237
250
  * 检查指定 URL 是否正在下载
238
251
  * @param url 要检查的 URL
252
+ * @param id 可选的任务 ID
239
253
  * @returns 是否正在下载
240
254
  */
241
- isDownloading(url) {
242
- return this._downloadingUrls.has(url);
255
+ isDownloading(url, id) {
256
+ const downloadKey = this._getDownloadKey(url, id);
257
+ return this._downloadingUrls.has(downloadKey);
243
258
  }
244
259
  /**
245
- * 获取正在下载的 URL 列表
246
- * @returns 正在下载的 URL 数组
260
+ * 获取正在下载的任务列表
261
+ * @returns 正在下载的任务信息数组,包含 URL 和 ID(如果有)
247
262
  */
248
263
  getDownloadingUrls() {
249
- return Array.from(this._downloadingUrls.keys());
264
+ return Array.from(this._downloadingUrls.keys()).map(key => {
265
+ const parts = key.split('::');
266
+ if (parts.length === 2 && parts[0] && parts[1]) {
267
+ return { url: parts[0], id: parts[1] };
268
+ }
269
+ return { url: key };
270
+ });
250
271
  }
251
272
  /**
252
273
  * 显示保存对话框
@@ -1 +1 @@
1
- {"version":3,"file":"downloader.js","sources":["../../src/main/downloader.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\nimport mime from 'mime-types';\nimport { DownloaderHelper } from 'node-downloader-helper';\nimport { ipcMain, dialog, BrowserWindow, app } from 'electron';\n\n/**\n * 最大重定向次数\n */\nconst MAX_REDIRECTS = 5;\n\n/**\n * 默认文件扩展名\n */\nconst DEFAULT_EXT = '.bin';\n\n/**\n * 默认 MIME 类型\n */\nconst DEFAULT_MIME_TYPE = 'application/octet-stream';\n\n/**\n * 默认超时时间(毫秒):10 分钟\n */\nconst DEFAULT_TIMEOUT = 600000;\n\n/**\n * 默认重试配置\n */\nconst DEFAULT_RETRY = {\n maxRetries: 3,\n delay: 1000\n};\n\n/**\n * 默认文件名\n */\nconst DEFAULT_FILENAME = 'download';\n\n/**\n * 最大文件名计数器(防止无限循环)\n */\nconst MAX_FILENAME_COUNTER = 10000;\n\n/**\n * 下载进度回调函数类型\n */\nexport type DownloadProgressCallback = (progress: {\n url: string; // 正在下载的 URL\n downloaded: number; // 已下载字节数\n total: number; // 总字节数(如果服务器提供 Content-Length)\n percentage: number; // 下载进度百分比 (0-100),如果 total 未知则为 -1\n speed: number; // 下载速度(字节/秒)\n}) => void;\n\n/**\n * 下载选项\n */\nexport interface DownloadOptions {\n /** 唯一标识(IPC 调用时必需) */\n id?: string;\n /** 输出文件路径(可选,如果不提供则使用 URL 的文件名) */\n outputPath?: string;\n /** 输出目录(可选,如果不提供则使用当前目录) */\n outputDir?: string;\n /** 输出文件名(可选,如果不提供则从 URL 提取) */\n fileName?: string;\n /** 是否覆盖已存在的文件(默认 true) */\n override?: boolean;\n /** 是否弹窗选择保存路径(默认 false) */\n showSaveDialog?: boolean;\n /** 保存对话框的默认文件名(可选) */\n defaultPath?: string;\n /** 超时时间(毫秒,默认 10 分钟) */\n timeout?: number;\n /** 重试配置 */\n retry?: {\n maxRetries?: number;\n delay?: number;\n };\n /** 下载进度回调函数(可选) */\n onProgress?: DownloadProgressCallback;\n /** 下载完成回调函数(可选) */\n onComplete?: (filePath: string) => void;\n /** 下载错误回调函数(可选) */\n onError?: (error: Error) => void;\n /** HTTP 请求选项 */\n httpRequestOptions?: {\n headers?: Record<string, string>;\n followRedirect?: boolean;\n maxRedirects?: number;\n };\n}\n\n/**\n * 下载结果\n */\nexport interface DownloadResult {\n /** 是否成功 */\n success: boolean;\n /** 下载的文件路径 */\n filePath?: string;\n /** 错误信息 */\n error?: string;\n /** 文件大小(字节) */\n size?: number;\n}\n\n/**\n * 下载器类:提供文件下载功能\n */\nclass Downloader {\n /** 正在下载的 URL 集合(避免重复下载) */\n private _downloadingUrls = new Map<string, DownloaderHelper>();\n /** 上一次选择的保存文件夹路径 */\n private _lastSelectedDir: string | null = null;\n\n constructor() {\n ipcMain.handle(`core:dowloader`, async (event: Electron.IpcMainInvokeEvent, options: DownloadOptions & { url: string; id: string }) => {\n try {\n const { id, url } = options;\n const sender = event.sender as Electron.WebContents;\n\n // 创建进度回调,通过 IPC 发送进度更新\n const progressCallback: DownloadProgressCallback = (progress) => {\n try {\n if (sender && typeof sender.send === 'function') {\n sender.send(`core:dowloader:progress`, {\n id,\n data: progress\n });\n }\n } catch (error) {\n console.error('发送下载进度失败:', error);\n }\n };\n\n const result = await this.download(url, options, progressCallback);\n return result;\n } catch (error) {\n return {\n success: false,\n error: error instanceof Error ? error.message : '未知错误'\n };\n }\n });\n }\n\n /**\n * 下载文件\n * @param url 要下载的 URL\n * @param options 下载选项\n * @param onProgress 进度回调函数(可选,优先级高于 options.onProgress)\n * @returns Promise<DownloadResult> 下载结果\n */\n public async download(\n url: string,\n options: DownloadOptions = {},\n onProgress?: DownloadProgressCallback\n ): Promise<DownloadResult> {\n // 检查是否正在下载,避免重复下载\n if (this._downloadingUrls.has(url)) {\n return this._createErrorResult(`资源正在下载中: ${url}`);\n }\n\n // 合并进度回调:优先使用传入的 onProgress,其次使用 options.onProgress\n const progressCallback = onProgress || options.onProgress;\n\n // 先检查是否是 base64 data URL(用于确定默认扩展名)\n const base64Info = this._isBase64DataUrl(url);\n const defaultExt = base64Info.isBase64 && base64Info.ext ? base64Info.ext : undefined;\n\n // 确定输出路径\n let outputPath: string;\n try {\n if (options.showSaveDialog) {\n const savePath = await this._showSaveDialog(url, options, defaultExt);\n if (!savePath) {\n return this._createErrorResult('用户取消了保存操作');\n }\n outputPath = savePath;\n } else {\n outputPath = this._resolveOutputPath(url, options, defaultExt);\n }\n } catch (error) {\n return this._createErrorResult(\n error instanceof Error ? error.message : '确定输出路径失败'\n );\n }\n\n // 准备输出路径(确保目录存在、检查文件是否已存在)\n const pathCheckResult = await this._prepareOutputPath(outputPath, options);\n if (pathCheckResult) {\n return pathCheckResult;\n }\n\n const downloadDir = path.dirname(outputPath);\n const downloadFileName = path.basename(outputPath);\n\n // 如果是 base64 data URL,使用已确定的输出路径进行处理\n if (base64Info.isBase64 && base64Info.ext && base64Info.mimeType && base64Info.data) {\n return await this._handleBase64Download(\n url,\n { ext: base64Info.ext, mimeType: base64Info.mimeType, data: base64Info.data },\n outputPath,\n options,\n progressCallback\n );\n }\n\n // 使用临时文件下载,避免下载中断时文件不完整\n const tempFilePath = `${outputPath}.cache`;\n\n return new Promise<DownloadResult>((resolve, reject) => {\n // 创建下载器实例\n const dl = new DownloaderHelper(url, downloadDir, {\n fileName: downloadFileName + '.cache',\n retry: {\n maxRetries: options.retry?.maxRetries ?? DEFAULT_RETRY.maxRetries,\n delay: options.retry?.delay ?? DEFAULT_RETRY.delay\n },\n timeout: options.timeout ?? DEFAULT_TIMEOUT,\n override: true, // 临时文件总是覆盖\n httpRequestOptions: {\n followRedirect: options.httpRequestOptions?.followRedirect ?? true,\n maxRedirects: options.httpRequestOptions?.maxRedirects ?? MAX_REDIRECTS,\n headers: options.httpRequestOptions?.headers\n }\n });\n\n // 记录正在下载的 URL\n this._downloadingUrls.set(url, dl);\n\n // 监听下载进度\n if (progressCallback) {\n dl.on('progress', (stats) => {\n try {\n progressCallback({\n url,\n downloaded: stats.downloaded || 0,\n total: stats.total || 0,\n percentage: stats.progress !== undefined ? Math.round(stats.progress) : -1,\n speed: stats.speed || 0\n });\n } catch (error) {\n // 忽略回调中的错误,避免影响下载\n console.error('进度回调执行失败:', error);\n }\n });\n }\n\n // 监听下载完成\n dl.on('end', () => {\n this._downloadingUrls.delete(url);\n // 将临时文件重命名为最终文件\n fs.rename(tempFilePath, outputPath, (renameErr) => {\n if (renameErr) {\n const error = new Error(`文件重命名失败 from ${tempFilePath} to ${outputPath}: ${renameErr.message}`);\n if (options.onError) {\n options.onError(error);\n }\n reject(error);\n } else {\n // 获取文件大小并返回结果\n const stats = fs.statSync(outputPath);\n const result = this._createSuccessResult(outputPath, stats.size);\n if (options.onComplete) {\n options.onComplete(outputPath);\n }\n resolve(result);\n }\n });\n });\n\n // 统一的错误处理函数\n const handleError = (error: Error) => {\n this._downloadingUrls.delete(url);\n // 清理临时文件\n fs.promises.unlink(tempFilePath).catch(() => {\n // 忽略删除失败\n });\n if (options.onError) {\n options.onError(error);\n }\n reject(error);\n };\n\n // 监听下载错误\n dl.on('error', (err) => {\n const error = err instanceof Error ? err : new Error(`下载失败: ${url}`);\n handleError(error);\n });\n\n // 监听下载停止(取消)\n dl.on('stop', () => {\n const error = new Error(`下载已停止: ${url}`);\n handleError(error);\n });\n\n // 开始下载\n dl.start().catch((err) => {\n const error = err instanceof Error ? err : new Error(`启动下载失败: ${url}`);\n handleError(error);\n });\n }).catch((error): DownloadResult => {\n return this._createErrorResult(error instanceof Error ? error.message : '未知错误');\n });\n }\n\n /**\n * 取消下载\n * @param url 要取消的 URL\n * @returns 是否成功取消\n */\n public cancel(url: string): boolean {\n const dl = this._downloadingUrls.get(url);\n if (dl) {\n dl.stop();\n this._downloadingUrls.delete(url);\n return true;\n }\n return false;\n }\n\n /**\n * 取消所有正在进行的下载\n */\n public cancelAll(): void {\n for (const [url, dl] of this._downloadingUrls.entries()) {\n dl.stop();\n }\n this._downloadingUrls.clear();\n }\n\n /**\n * 检查指定 URL 是否正在下载\n * @param url 要检查的 URL\n * @returns 是否正在下载\n */\n public isDownloading(url: string): boolean {\n return this._downloadingUrls.has(url);\n }\n\n /**\n * 获取正在下载的 URL 列表\n * @returns 正在下载的 URL 数组\n */\n public getDownloadingUrls(): string[] {\n return Array.from(this._downloadingUrls.keys());\n }\n\n /**\n * 显示保存对话框\n * @param url 下载 URL\n * @param options 下载选项\n * @param defaultExt 默认扩展名(可选,用于 base64 URL)\n * @returns Promise<string | null> 用户选择的保存路径,如果取消则返回 null\n */\n private async _showSaveDialog(url: string, options: DownloadOptions, defaultExt?: string): Promise<string | null> {\n // 获取默认文件名\n let defaultFileName: string;\n if (options.defaultPath) {\n defaultFileName = path.basename(options.defaultPath);\n } else if (options.fileName) {\n defaultFileName = options.fileName;\n } else {\n // 尝试从 URL 中提取文件名\n if (defaultExt) {\n // 如果提供了默认扩展名(base64 情况),使用它\n defaultFileName = `${DEFAULT_FILENAME}.${defaultExt}`;\n } else {\n defaultFileName = this._extractFileNameFromUrl(url);\n }\n }\n\n // 获取默认目录\n let defaultDir: string | undefined;\n if (options.defaultPath) {\n defaultDir = path.dirname(options.defaultPath);\n } else if (options.outputDir) {\n defaultDir = path.resolve(options.outputDir);\n } else {\n // 如果没有指定目录,使用上一次选择的文件夹,如果没有则使用下载目录\n defaultDir = this._lastSelectedDir || app.getPath('downloads');\n }\n\n // 如果指定了目录,检查文件是否存在并生成唯一文件名\n if (defaultDir) {\n defaultFileName = this._generateUniqueFileName(defaultDir, defaultFileName);\n }\n\n // 构建默认路径\n const defaultPath = defaultDir ? path.join(defaultDir, defaultFileName) : defaultFileName;\n\n // 获取当前活动的 BrowserWindow\n const focusedWindow = BrowserWindow.getFocusedWindow();\n const allWindows = BrowserWindow.getAllWindows();\n const parentWindow = focusedWindow || (allWindows.length > 0 ? allWindows[0] : undefined);\n\n // 显示保存对话框\n const dialogOptions = {\n defaultPath,\n title: '保存文件',\n buttonLabel: '保存',\n filters: [\n // 尝试从文件名推断文件类型\n ...(path.extname(defaultFileName) ? [{\n name: '所有文件',\n extensions: ['*']\n }] : [])\n ]\n };\n\n // 根据是否有父窗口调用不同的方法\n const result = parentWindow\n ? await dialog.showSaveDialog(parentWindow, dialogOptions)\n : await dialog.showSaveDialog(dialogOptions);\n\n if (result.canceled || !result.filePath) {\n return null;\n }\n\n // 记录用户选择的文件夹路径,用于下次默认使用\n const selectedDir = path.dirname(result.filePath);\n if (selectedDir) {\n this._lastSelectedDir = selectedDir;\n }\n\n return result.filePath;\n }\n\n /**\n * 检测并处理 base64 data URL\n * @param url 资源URL\n * @returns 如果是 base64 URL,返回 true、文件扩展名、MIME 类型和数据;否则返回 false\n */\n private _isBase64DataUrl(url: string): { isBase64: boolean, ext?: string, mimeType?: string, data?: string } {\n if (!url.startsWith('data:')) {\n return { isBase64: false };\n }\n\n try {\n // 解析 data URL 格式:data:[<mediatype>][;base64],<data>\n const commaIndex = url.indexOf(',');\n if (commaIndex === -1) {\n return { isBase64: false };\n }\n\n const header = url.substring(0, commaIndex);\n const data = url.substring(commaIndex + 1);\n\n // 检查是否包含 base64 标识\n if (!header.includes('base64')) {\n return { isBase64: false };\n }\n\n // 从 mediatype 中提取文件扩展名和 MIME 类型\n // 例如:data:image/png;base64 -> png, image/png\n // 例如:data:image/jpeg;base64 -> jpeg, image/jpeg\n let ext = DEFAULT_EXT.replace(/^\\./, '');\n let mimeType = DEFAULT_MIME_TYPE;\n const mimeMatch = header.match(/data:([^;]+)/);\n if (mimeMatch && mimeMatch[1]) {\n mimeType = mimeMatch[1];\n // 使用 mime-types 包从 MIME 类型获取扩展名\n const extension = mime.extension(mimeType);\n if (extension) {\n ext = extension;\n }\n }\n\n return { isBase64: true, ext, mimeType, data };\n } catch (error) {\n return { isBase64: false };\n }\n }\n\n /**\n * 保存 base64 数据到文件\n * @param base64Data base64 编码的数据\n * @param filePath 目标文件路径\n */\n private async _saveBase64ToFile(base64Data: string, filePath: string): Promise<void> {\n try {\n // 解码 base64 数据\n const buffer = Buffer.from(base64Data, 'base64');\n\n // 使用 Promise 版本的 writeFile\n // Buffer 继承自 Uint8Array,可以直接使用\n await fs.promises.writeFile(filePath, buffer as Uint8Array);\n } catch (error) {\n throw new Error(`保存 base64 文件失败: ${error instanceof Error ? error.message : '未知错误'}`);\n }\n }\n\n /**\n * 准备输出路径(确保目录存在、检查文件是否已存在)\n * @param outputPath 输出路径\n * @param options 下载选项\n * @returns 如果文件已存在且不允许覆盖,返回结果;否则返回 null\n */\n private async _prepareOutputPath(\n outputPath: string,\n options: DownloadOptions\n ): Promise<DownloadResult | null> {\n const downloadDir = path.dirname(outputPath);\n\n // 确保目录存在\n if (!fs.existsSync(downloadDir)) {\n fs.mkdirSync(downloadDir, { recursive: true });\n }\n\n // 如果文件已存在且不允许覆盖,直接返回\n if (fs.existsSync(outputPath) && !(options.override ?? true)) {\n const stats = fs.statSync(outputPath);\n return this._createSuccessResult(outputPath, stats.size);\n }\n\n return null;\n }\n\n /**\n * 创建成功结果\n * @param filePath 文件路径\n * @param size 文件大小\n * @returns DownloadResult\n */\n private _createSuccessResult(filePath: string, size: number): DownloadResult {\n return {\n success: true,\n filePath,\n size\n };\n }\n\n /**\n * 创建错误结果\n * @param error 错误信息\n * @returns DownloadResult\n */\n private _createErrorResult(error: string): DownloadResult {\n return {\n success: false,\n error\n };\n }\n\n /**\n * 处理 base64 data URL 的下载\n * @param url base64 data URL\n * @param base64Info base64 信息\n * @param outputPath 已确定的输出路径\n * @param options 下载选项\n * @param progressCallback 进度回调\n * @returns Promise<DownloadResult> 下载结果\n */\n private async _handleBase64Download(\n url: string,\n base64Info: { ext: string; mimeType: string; data: string },\n outputPath: string,\n options: DownloadOptions,\n progressCallback?: DownloadProgressCallback\n ): Promise<DownloadResult> {\n if (!base64Info.data || !base64Info.ext) {\n return this._createErrorResult(`无效的 base64 data URL: ${url}`);\n }\n\n try {\n // 模拟进度回调(base64 数据通常很小,立即完成)\n if (progressCallback) {\n const dataLength = Buffer.from(base64Info.data, 'base64').length;\n progressCallback({\n url,\n downloaded: dataLength,\n total: dataLength,\n percentage: 100,\n speed: 0\n });\n }\n\n // 保存 base64 数据到文件\n await this._saveBase64ToFile(base64Info.data, outputPath);\n\n // 获取文件大小\n const stats = fs.statSync(outputPath);\n const result = this._createSuccessResult(outputPath, stats.size);\n\n if (options.onComplete) {\n options.onComplete(outputPath);\n }\n\n return result;\n } catch (error) {\n const err = error instanceof Error ? error : new Error('保存 base64 文件失败');\n if (options.onError) {\n options.onError(err);\n }\n return this._createErrorResult(err.message);\n }\n }\n\n /**\n * 生成唯一的文件名(如果文件已存在,添加数字后缀)\n * @param dir 目录路径\n * @param fileName 原始文件名\n * @returns 唯一的文件名\n */\n private _generateUniqueFileName(dir: string, fileName: string): string {\n // 分离文件名和扩展名\n const ext = path.extname(fileName);\n const nameWithoutExt = path.basename(fileName, ext);\n\n // 检查原始文件名是否存在\n const originalPath = path.join(dir, fileName);\n if (!fs.existsSync(originalPath)) {\n return fileName;\n }\n\n // 如果存在,尝试添加数字后缀\n let counter = 1;\n let newFileName: string;\n let newPath: string;\n\n do {\n newFileName = `${nameWithoutExt}(${counter})${ext}`;\n newPath = path.join(dir, newFileName);\n counter++;\n } while (fs.existsSync(newPath) && counter < MAX_FILENAME_COUNTER);\n\n return newFileName;\n }\n\n /**\n * 从 URL 中提取文件名\n * @param url 下载 URL\n * @returns 文件名\n */\n private _extractFileNameFromUrl(url: string): string {\n try {\n const urlObj = new URL(url);\n const fileName = path.basename(urlObj.pathname) || DEFAULT_FILENAME;\n return path.extname(fileName) ? fileName : `${fileName}${DEFAULT_EXT}`;\n } catch {\n return `${DEFAULT_FILENAME}${DEFAULT_EXT}`;\n }\n }\n\n /**\n * 解析输出路径\n * @param url 下载 URL\n * @param options 下载选项\n * @param defaultExt 默认扩展名(可选,用于 base64 URL)\n * @returns 输出文件路径\n */\n private _resolveOutputPath(url: string, options: DownloadOptions, defaultExt?: string): string {\n // 如果提供了完整路径,直接使用\n if (options.outputPath) {\n return path.resolve(options.outputPath);\n }\n\n // 确定输出目录:优先使用指定的目录,否则使用 Electron 的下载目录\n const outputDir = options.outputDir\n ? path.resolve(options.outputDir)\n : app.getPath('downloads');\n\n // 确定文件名\n let fileName: string;\n if (options.fileName) {\n fileName = options.fileName;\n } else {\n // 如果提供了默认扩展名(base64 情况),使用它\n if (defaultExt) {\n fileName = `${DEFAULT_FILENAME}.${defaultExt}`;\n } else {\n // 尝试从 URL 中提取文件名\n fileName = this._extractFileNameFromUrl(url);\n }\n }\n\n return path.join(outputDir, fileName);\n }\n}\n\n/**\n * 默认下载器实例(单例)\n */\nlet defaultDownloader: Downloader | null = null;\n\n/**\n * 获取默认下载器实例\n * @returns Downloader 实例\n */\nexport function getDownloader(): Downloader {\n if (!defaultDownloader) {\n defaultDownloader = new Downloader();\n }\n return defaultDownloader;\n}\n\n/**\n * 下载文件的便捷函数\n * @param url 要下载的 URL\n * @param options 下载选项\n * @param onProgress 进度回调函数(可选)\n * @returns Promise<DownloadResult> 下载结果\n */\nexport async function downloadFile(\n url: string,\n options: DownloadOptions = {},\n onProgress?: DownloadProgressCallback\n): Promise<DownloadResult> {\n const downloader = getDownloader();\n return downloader.download(url, options, onProgress);\n}\n\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"downloader.js","sources":["../../src/main/downloader.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\nimport mime from 'mime-types';\nimport { DownloaderHelper } from 'node-downloader-helper';\nimport { ipcMain, dialog, BrowserWindow, app } from 'electron';\n\n/**\n * 最大重定向次数\n */\nconst MAX_REDIRECTS = 5;\n\n/**\n * 默认文件扩展名\n */\nconst DEFAULT_EXT = '.bin';\n\n/**\n * 默认 MIME 类型\n */\nconst DEFAULT_MIME_TYPE = 'application/octet-stream';\n\n/**\n * 默认超时时间(毫秒):10 分钟\n */\nconst DEFAULT_TIMEOUT = 600000;\n\n/**\n * 默认重试配置\n */\nconst DEFAULT_RETRY = {\n maxRetries: 3,\n delay: 1000\n};\n\n/**\n * 默认文件名\n */\nconst DEFAULT_FILENAME = 'download';\n\n/**\n * 最大文件名计数器(防止无限循环)\n */\nconst MAX_FILENAME_COUNTER = 10000;\n\n/**\n * 下载进度回调函数类型\n */\nexport type DownloadProgressCallback = (progress: {\n url: string; // 正在下载的 URL\n downloaded: number; // 已下载字节数\n total: number; // 总字节数(如果服务器提供 Content-Length)\n percentage: number; // 下载进度百分比 (0-100),如果 total 未知则为 -1\n speed: number; // 下载速度(字节/秒)\n}) => void;\n\n/**\n * 下载选项\n */\nexport interface DownloadOptions {\n /** 唯一标识(IPC 调用时必需) */\n id?: string;\n /** 输出文件路径(可选,如果不提供则使用 URL 的文件名) */\n outputPath?: string;\n /** 输出目录(可选,如果不提供则使用当前目录) */\n outputDir?: string;\n /** 输出文件名(可选,如果不提供则从 URL 提取) */\n fileName?: string;\n /** 是否覆盖已存在的文件(默认 true) */\n override?: boolean;\n /** 是否弹窗选择保存路径(默认 false) */\n showSaveDialog?: boolean;\n /** 保存对话框的默认文件名(可选) */\n defaultPath?: string;\n /** 超时时间(毫秒,默认 10 分钟) */\n timeout?: number;\n /** 重试配置 */\n retry?: {\n maxRetries?: number;\n delay?: number;\n };\n /** 下载进度回调函数(可选) */\n onProgress?: DownloadProgressCallback;\n /** 下载完成回调函数(可选) */\n onComplete?: (filePath: string) => void;\n /** 下载错误回调函数(可选) */\n onError?: (error: Error) => void;\n /** HTTP 请求选项 */\n httpRequestOptions?: {\n headers?: Record<string, string>;\n followRedirect?: boolean;\n maxRedirects?: number;\n };\n}\n\n/**\n * 下载结果\n */\nexport interface DownloadResult {\n /** 是否成功 */\n success: boolean;\n /** 下载的文件路径 */\n filePath?: string;\n /** 错误信息 */\n error?: string;\n /** 文件大小(字节) */\n size?: number;\n}\n\n/**\n * 下载器类:提供文件下载功能\n */\nclass Downloader {\n /** 正在下载的 URL 集合(避免重复下载) */\n private _downloadingUrls = new Map<string, DownloaderHelper>();\n /** 上一次选择的保存文件夹路径 */\n private _lastSelectedDir: string | null = null;\n\n constructor() {\n ipcMain.handle(`core:dowloader`, async (event: Electron.IpcMainInvokeEvent, options: DownloadOptions & { url: string; id: string }) => {\n try {\n const { id, url } = options;\n const sender = event.sender as Electron.WebContents;\n\n // 创建进度回调,通过 IPC 发送进度更新\n const progressCallback: DownloadProgressCallback = (progress) => {\n try {\n if (sender && typeof sender.send === 'function') {\n sender.send(`core:dowloader:progress`, {\n id,\n data: progress\n });\n }\n } catch (error) {\n console.error('发送下载进度失败:', error);\n }\n };\n\n const result = await this.download(url, options, progressCallback);\n return result;\n } catch (error) {\n return {\n success: false,\n error: error instanceof Error ? error.message : '未知错误'\n };\n }\n });\n }\n\n /**\n * 下载文件\n * @param url 要下载的 URL\n * @param options 下载选项\n * @param onProgress 进度回调函数(可选,优先级高于 options.onProgress)\n * @returns Promise<DownloadResult> 下载结果\n */\n public async download(\n url: string,\n options: DownloadOptions = {},\n onProgress?: DownloadProgressCallback\n ): Promise<DownloadResult> {\n // 生成唯一标识:URL + ID(如果存在)\n const downloadKey = this._getDownloadKey(url, options.id);\n\n // 检查是否正在下载,避免重复下载\n if (this._downloadingUrls.has(downloadKey)) {\n return this._createErrorResult(`资源正在下载中: ${url}${options.id ? ` (ID: ${options.id})` : ''}`);\n }\n\n // 合并进度回调:优先使用传入的 onProgress,其次使用 options.onProgress\n const progressCallback = onProgress || options.onProgress;\n\n // 先检查是否是 base64 data URL(用于确定默认扩展名)\n const base64Info = this._isBase64DataUrl(url);\n const defaultExt = base64Info.isBase64 && base64Info.ext ? base64Info.ext : undefined;\n\n // 确定输出路径\n let outputPath: string;\n try {\n if (options.showSaveDialog) {\n const savePath = await this._showSaveDialog(url, options, defaultExt);\n if (!savePath) {\n return this._createErrorResult('用户取消了保存操作');\n }\n outputPath = savePath;\n } else {\n outputPath = this._resolveOutputPath(url, options, defaultExt);\n }\n } catch (error) {\n return this._createErrorResult(\n error instanceof Error ? error.message : '确定输出路径失败'\n );\n }\n\n // 准备输出路径(确保目录存在、检查文件是否已存在)\n const pathCheckResult = await this._prepareOutputPath(outputPath, options);\n if (pathCheckResult) {\n return pathCheckResult;\n }\n\n const downloadDir = path.dirname(outputPath);\n const downloadFileName = path.basename(outputPath);\n\n // 如果是 base64 data URL,使用已确定的输出路径进行处理\n if (base64Info.isBase64 && base64Info.ext && base64Info.mimeType && base64Info.data) {\n return await this._handleBase64Download(\n url,\n { ext: base64Info.ext, mimeType: base64Info.mimeType, data: base64Info.data },\n outputPath,\n options,\n progressCallback\n );\n }\n\n // 使用临时文件下载,避免下载中断时文件不完整\n const tempFilePath = `${outputPath}.cache`;\n\n return new Promise<DownloadResult>((resolve, reject) => {\n // 创建下载器实例\n const dl = new DownloaderHelper(url, downloadDir, {\n fileName: downloadFileName + '.cache',\n retry: {\n maxRetries: options.retry?.maxRetries ?? DEFAULT_RETRY.maxRetries,\n delay: options.retry?.delay ?? DEFAULT_RETRY.delay\n },\n timeout: options.timeout ?? DEFAULT_TIMEOUT,\n override: true, // 临时文件总是覆盖\n httpRequestOptions: {\n followRedirect: options.httpRequestOptions?.followRedirect ?? true,\n maxRedirects: options.httpRequestOptions?.maxRedirects ?? MAX_REDIRECTS,\n headers: options.httpRequestOptions?.headers\n }\n });\n\n // 记录正在下载的 URL(使用 URL + ID 作为 key)\n this._downloadingUrls.set(downloadKey, dl);\n\n // 监听下载进度\n if (progressCallback) {\n dl.on('progress', (stats) => {\n try {\n progressCallback({\n url,\n downloaded: stats.downloaded || 0,\n total: stats.total || 0,\n percentage: stats.progress !== undefined ? Math.round(stats.progress) : -1,\n speed: stats.speed || 0\n });\n } catch (error) {\n // 忽略回调中的错误,避免影响下载\n console.error('进度回调执行失败:', error);\n }\n });\n }\n\n // 监听下载完成\n dl.on('end', () => {\n this._downloadingUrls.delete(downloadKey);\n // 将临时文件重命名为最终文件\n fs.rename(tempFilePath, outputPath, (renameErr) => {\n if (renameErr) {\n const error = new Error(`文件重命名失败 from ${tempFilePath} to ${outputPath}: ${renameErr.message}`);\n if (options.onError) {\n options.onError(error);\n }\n reject(error);\n } else {\n // 获取文件大小并返回结果\n const stats = fs.statSync(outputPath);\n const result = this._createSuccessResult(outputPath, stats.size);\n if (options.onComplete) {\n options.onComplete(outputPath);\n }\n resolve(result);\n }\n });\n });\n\n // 统一的错误处理函数\n const handleError = (error: Error) => {\n this._downloadingUrls.delete(downloadKey);\n // 清理临时文件\n fs.promises.unlink(tempFilePath).catch(() => {\n // 忽略删除失败\n });\n if (options.onError) {\n options.onError(error);\n }\n reject(error);\n };\n\n // 监听下载错误\n dl.on('error', (err) => {\n const error = err instanceof Error ? err : new Error(`下载失败: ${url}`);\n handleError(error);\n });\n\n // 监听下载停止(取消)\n dl.on('stop', () => {\n const error = new Error(`下载已停止: ${url}`);\n handleError(error);\n });\n\n // 开始下载\n dl.start().catch((err) => {\n const error = err instanceof Error ? err : new Error(`启动下载失败: ${url}`);\n handleError(error);\n });\n }).catch((error): DownloadResult => {\n return this._createErrorResult(error instanceof Error ? error.message : '未知错误');\n });\n }\n\n /**\n * 生成下载任务的唯一标识\n * @param url 下载 URL\n * @param id 可选的任务 ID\n * @returns 唯一标识\n */\n private _getDownloadKey(url: string, id?: string): string {\n return id ? `${url}::${id}` : url;\n }\n\n /**\n * 取消下载\n * @param url 要取消的 URL\n * @param id 可选的任务 ID\n * @returns 是否成功取消\n */\n public cancel(url: string, id?: string): boolean {\n const downloadKey = this._getDownloadKey(url, id);\n const dl = this._downloadingUrls.get(downloadKey);\n if (dl) {\n dl.stop();\n this._downloadingUrls.delete(downloadKey);\n return true;\n }\n return false;\n }\n\n /**\n * 取消所有正在进行的下载\n */\n public cancelAll(): void {\n for (const [url, dl] of this._downloadingUrls.entries()) {\n dl.stop();\n }\n this._downloadingUrls.clear();\n }\n\n /**\n * 检查指定 URL 是否正在下载\n * @param url 要检查的 URL\n * @param id 可选的任务 ID\n * @returns 是否正在下载\n */\n public isDownloading(url: string, id?: string): boolean {\n const downloadKey = this._getDownloadKey(url, id);\n return this._downloadingUrls.has(downloadKey);\n }\n\n /**\n * 获取正在下载的任务列表\n * @returns 正在下载的任务信息数组,包含 URL 和 ID(如果有)\n */\n public getDownloadingUrls(): Array<{ url: string; id?: string }> {\n return Array.from(this._downloadingUrls.keys()).map(key => {\n const parts = key.split('::');\n if (parts.length === 2 && parts[0] && parts[1]) {\n return { url: parts[0], id: parts[1] };\n }\n return { url: key };\n });\n }\n\n /**\n * 显示保存对话框\n * @param url 下载 URL\n * @param options 下载选项\n * @param defaultExt 默认扩展名(可选,用于 base64 URL)\n * @returns Promise<string | null> 用户选择的保存路径,如果取消则返回 null\n */\n private async _showSaveDialog(url: string, options: DownloadOptions, defaultExt?: string): Promise<string | null> {\n // 获取默认文件名\n let defaultFileName: string;\n if (options.defaultPath) {\n defaultFileName = path.basename(options.defaultPath);\n } else if (options.fileName) {\n defaultFileName = options.fileName;\n } else {\n // 尝试从 URL 中提取文件名\n if (defaultExt) {\n // 如果提供了默认扩展名(base64 情况),使用它\n defaultFileName = `${DEFAULT_FILENAME}.${defaultExt}`;\n } else {\n defaultFileName = this._extractFileNameFromUrl(url);\n }\n }\n\n // 获取默认目录\n let defaultDir: string | undefined;\n if (options.defaultPath) {\n defaultDir = path.dirname(options.defaultPath);\n } else if (options.outputDir) {\n defaultDir = path.resolve(options.outputDir);\n } else {\n // 如果没有指定目录,使用上一次选择的文件夹,如果没有则使用下载目录\n defaultDir = this._lastSelectedDir || app.getPath('downloads');\n }\n\n // 如果指定了目录,检查文件是否存在并生成唯一文件名\n if (defaultDir) {\n defaultFileName = this._generateUniqueFileName(defaultDir, defaultFileName);\n }\n\n // 构建默认路径\n const defaultPath = defaultDir ? path.join(defaultDir, defaultFileName) : defaultFileName;\n\n // 获取当前活动的 BrowserWindow\n const focusedWindow = BrowserWindow.getFocusedWindow();\n const allWindows = BrowserWindow.getAllWindows();\n const parentWindow = focusedWindow || (allWindows.length > 0 ? allWindows[0] : undefined);\n\n // 显示保存对话框\n const dialogOptions = {\n defaultPath,\n title: '保存文件',\n buttonLabel: '保存',\n filters: [\n // 尝试从文件名推断文件类型\n ...(path.extname(defaultFileName) ? [{\n name: '所有文件',\n extensions: ['*']\n }] : [])\n ]\n };\n\n // 根据是否有父窗口调用不同的方法\n const result = parentWindow\n ? await dialog.showSaveDialog(parentWindow, dialogOptions)\n : await dialog.showSaveDialog(dialogOptions);\n\n if (result.canceled || !result.filePath) {\n return null;\n }\n\n // 记录用户选择的文件夹路径,用于下次默认使用\n const selectedDir = path.dirname(result.filePath);\n if (selectedDir) {\n this._lastSelectedDir = selectedDir;\n }\n\n return result.filePath;\n }\n\n /**\n * 检测并处理 base64 data URL\n * @param url 资源URL\n * @returns 如果是 base64 URL,返回 true、文件扩展名、MIME 类型和数据;否则返回 false\n */\n private _isBase64DataUrl(url: string): { isBase64: boolean, ext?: string, mimeType?: string, data?: string } {\n if (!url.startsWith('data:')) {\n return { isBase64: false };\n }\n\n try {\n // 解析 data URL 格式:data:[<mediatype>][;base64],<data>\n const commaIndex = url.indexOf(',');\n if (commaIndex === -1) {\n return { isBase64: false };\n }\n\n const header = url.substring(0, commaIndex);\n const data = url.substring(commaIndex + 1);\n\n // 检查是否包含 base64 标识\n if (!header.includes('base64')) {\n return { isBase64: false };\n }\n\n // 从 mediatype 中提取文件扩展名和 MIME 类型\n // 例如:data:image/png;base64 -> png, image/png\n // 例如:data:image/jpeg;base64 -> jpeg, image/jpeg\n let ext = DEFAULT_EXT.replace(/^\\./, '');\n let mimeType = DEFAULT_MIME_TYPE;\n const mimeMatch = header.match(/data:([^;]+)/);\n if (mimeMatch && mimeMatch[1]) {\n mimeType = mimeMatch[1];\n // 使用 mime-types 包从 MIME 类型获取扩展名\n const extension = mime.extension(mimeType);\n if (extension) {\n ext = extension;\n }\n }\n\n return { isBase64: true, ext, mimeType, data };\n } catch (error) {\n return { isBase64: false };\n }\n }\n\n /**\n * 保存 base64 数据到文件\n * @param base64Data base64 编码的数据\n * @param filePath 目标文件路径\n */\n private async _saveBase64ToFile(base64Data: string, filePath: string): Promise<void> {\n try {\n // 解码 base64 数据\n const buffer = Buffer.from(base64Data, 'base64');\n\n // 使用 Promise 版本的 writeFile\n // Buffer 继承自 Uint8Array,可以直接使用\n await fs.promises.writeFile(filePath, buffer as Uint8Array);\n } catch (error) {\n throw new Error(`保存 base64 文件失败: ${error instanceof Error ? error.message : '未知错误'}`);\n }\n }\n\n /**\n * 准备输出路径(确保目录存在、检查文件是否已存在)\n * @param outputPath 输出路径\n * @param options 下载选项\n * @returns 如果文件已存在且不允许覆盖,返回结果;否则返回 null\n */\n private async _prepareOutputPath(\n outputPath: string,\n options: DownloadOptions\n ): Promise<DownloadResult | null> {\n const downloadDir = path.dirname(outputPath);\n\n // 确保目录存在\n if (!fs.existsSync(downloadDir)) {\n fs.mkdirSync(downloadDir, { recursive: true });\n }\n\n // 如果文件已存在且不允许覆盖,直接返回\n if (fs.existsSync(outputPath) && !(options.override ?? true)) {\n const stats = fs.statSync(outputPath);\n return this._createSuccessResult(outputPath, stats.size);\n }\n\n return null;\n }\n\n /**\n * 创建成功结果\n * @param filePath 文件路径\n * @param size 文件大小\n * @returns DownloadResult\n */\n private _createSuccessResult(filePath: string, size: number): DownloadResult {\n return {\n success: true,\n filePath,\n size\n };\n }\n\n /**\n * 创建错误结果\n * @param error 错误信息\n * @returns DownloadResult\n */\n private _createErrorResult(error: string): DownloadResult {\n return {\n success: false,\n error\n };\n }\n\n /**\n * 处理 base64 data URL 的下载\n * @param url base64 data URL\n * @param base64Info base64 信息\n * @param outputPath 已确定的输出路径\n * @param options 下载选项\n * @param progressCallback 进度回调\n * @returns Promise<DownloadResult> 下载结果\n */\n private async _handleBase64Download(\n url: string,\n base64Info: { ext: string; mimeType: string; data: string },\n outputPath: string,\n options: DownloadOptions,\n progressCallback?: DownloadProgressCallback\n ): Promise<DownloadResult> {\n if (!base64Info.data || !base64Info.ext) {\n return this._createErrorResult(`无效的 base64 data URL: ${url}`);\n }\n\n try {\n // 模拟进度回调(base64 数据通常很小,立即完成)\n if (progressCallback) {\n const dataLength = Buffer.from(base64Info.data, 'base64').length;\n progressCallback({\n url,\n downloaded: dataLength,\n total: dataLength,\n percentage: 100,\n speed: 0\n });\n }\n\n // 保存 base64 数据到文件\n await this._saveBase64ToFile(base64Info.data, outputPath);\n\n // 获取文件大小\n const stats = fs.statSync(outputPath);\n const result = this._createSuccessResult(outputPath, stats.size);\n\n if (options.onComplete) {\n options.onComplete(outputPath);\n }\n\n return result;\n } catch (error) {\n const err = error instanceof Error ? error : new Error('保存 base64 文件失败');\n if (options.onError) {\n options.onError(err);\n }\n return this._createErrorResult(err.message);\n }\n }\n\n /**\n * 生成唯一的文件名(如果文件已存在,添加数字后缀)\n * @param dir 目录路径\n * @param fileName 原始文件名\n * @returns 唯一的文件名\n */\n private _generateUniqueFileName(dir: string, fileName: string): string {\n // 分离文件名和扩展名\n const ext = path.extname(fileName);\n const nameWithoutExt = path.basename(fileName, ext);\n\n // 检查原始文件名是否存在\n const originalPath = path.join(dir, fileName);\n if (!fs.existsSync(originalPath)) {\n return fileName;\n }\n\n // 如果存在,尝试添加数字后缀\n let counter = 1;\n let newFileName: string;\n let newPath: string;\n\n do {\n newFileName = `${nameWithoutExt}(${counter})${ext}`;\n newPath = path.join(dir, newFileName);\n counter++;\n } while (fs.existsSync(newPath) && counter < MAX_FILENAME_COUNTER);\n\n return newFileName;\n }\n\n /**\n * 从 URL 中提取文件名\n * @param url 下载 URL\n * @returns 文件名\n */\n private _extractFileNameFromUrl(url: string): string {\n try {\n const urlObj = new URL(url);\n const fileName = path.basename(urlObj.pathname) || DEFAULT_FILENAME;\n return path.extname(fileName) ? fileName : `${fileName}${DEFAULT_EXT}`;\n } catch {\n return `${DEFAULT_FILENAME}${DEFAULT_EXT}`;\n }\n }\n\n /**\n * 解析输出路径\n * @param url 下载 URL\n * @param options 下载选项\n * @param defaultExt 默认扩展名(可选,用于 base64 URL)\n * @returns 输出文件路径\n */\n private _resolveOutputPath(url: string, options: DownloadOptions, defaultExt?: string): string {\n // 如果提供了完整路径,直接使用\n if (options.outputPath) {\n return path.resolve(options.outputPath);\n }\n\n // 确定输出目录:优先使用指定的目录,否则使用 Electron 的下载目录\n const outputDir = options.outputDir\n ? path.resolve(options.outputDir)\n : app.getPath('downloads');\n\n // 确定文件名\n let fileName: string;\n if (options.fileName) {\n fileName = options.fileName;\n } else {\n // 如果提供了默认扩展名(base64 情况),使用它\n if (defaultExt) {\n fileName = `${DEFAULT_FILENAME}.${defaultExt}`;\n } else {\n // 尝试从 URL 中提取文件名\n fileName = this._extractFileNameFromUrl(url);\n }\n }\n\n return path.join(outputDir, fileName);\n }\n}\n\n/**\n * 默认下载器实例(单例)\n */\nlet defaultDownloader: Downloader | null = null;\n\n/**\n * 获取默认下载器实例\n * @returns Downloader 实例\n */\nexport function getDownloader(): Downloader {\n if (!defaultDownloader) {\n defaultDownloader = new Downloader();\n }\n return defaultDownloader;\n}\n\n/**\n * 下载文件的便捷函数\n * @param url 要下载的 URL\n * @param options 下载选项\n * @param onProgress 进度回调函数(可选)\n * @returns Promise<DownloadResult> 下载结果\n */\nexport async function downloadFile(\n url: string,\n options: DownloadOptions = {},\n onProgress?: DownloadProgressCallback\n): Promise<DownloadResult> {\n const downloader = getDownloader();\n return downloader.download(url, options, onProgress);\n}\n\n"],"names":[],"mappings":"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"}
@@ -58,7 +58,7 @@ export declare class ResourceCache {
58
58
  private _cachedMatchFunction?;
59
59
  /** 缓存的来源校验函数(避免重复创建) */
60
60
  private _cachedOriginFunction?;
61
- /** 正在下载的 URL 集合(避免重复下载) */
61
+ /** 正在下载的 URL 和对应的下载信息(避免重复下载,允许多个请求等待同一个下载任务) */
62
62
  private _downloadingUrls;
63
63
  /**
64
64
  * 构造函数
@@ -175,6 +175,36 @@ export declare class ResourceCache {
175
175
  success: boolean;
176
176
  error?: string;
177
177
  }>>;
178
+ /**
179
+ * 获取单个 URL 的缓存信息
180
+ * @param url 资源 URL
181
+ * @returns Promise<{ url: string, exists: boolean, valid?: boolean, filePath?: string, hostPath?: string, mimeType?: string, size?: number, error?: string }> 返回缓存信息
182
+ */
183
+ getCacheUrl(url: string): Promise<{
184
+ url: string;
185
+ exists: boolean;
186
+ valid?: boolean;
187
+ filePath?: string;
188
+ hostPath?: string;
189
+ mimeType?: string;
190
+ size?: number;
191
+ error?: string;
192
+ }>;
193
+ /**
194
+ * 获取多个 URL 的缓存信息
195
+ * @param urls 资源 URL 数组
196
+ * @returns Promise<Array<{ url: string, exists: boolean, valid?: boolean, filePath?: string, hostPath?: string, mimeType?: string, size?: number, error?: string }>> 返回每个 URL 的缓存信息
197
+ */
198
+ getCacheUrls(urls: string[]): Promise<Array<{
199
+ url: string;
200
+ exists: boolean;
201
+ valid?: boolean;
202
+ filePath?: string;
203
+ hostPath?: string;
204
+ mimeType?: string;
205
+ size?: number;
206
+ error?: string;
207
+ }>>;
178
208
  /**
179
209
  * 删除单个 URL 的资源(异步版本,性能更好)
180
210
  * @param url 要删除的资源 URL
@@ -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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"}
@@ -45,8 +45,8 @@ class ResourceCache {
45
45
  */
46
46
  constructor(session, options) {
47
47
  this.cacheHost = `${ResourceCache.scheme}://-`;
48
- /** 正在下载的 URL 集合(避免重复下载) */
49
- this._downloadingUrls = new Set();
48
+ /** 正在下载的 URL 和对应的下载信息(避免重复下载,允许多个请求等待同一个下载任务) */
49
+ this._downloadingUrls = new Map();
50
50
  if (!session)
51
51
  throw new Error('ResourceCache: session is required');
52
52
  this.session = session;
@@ -102,6 +102,11 @@ class ResourceCache {
102
102
  const data = await this.deleteCacheUrls(urls);
103
103
  return Array.isArray(options.urls) ? data : data[0];
104
104
  }
105
+ case 'get': {
106
+ const urls = Array.isArray(options.urls) ? options.urls : options.urls ? [options.urls] : [];
107
+ const data = await this.getCacheUrls(urls);
108
+ return Array.isArray(options.urls) ? data : data[0];
109
+ }
105
110
  case 'stats':
106
111
  return await this.getCacheStats();
107
112
  default:
@@ -258,16 +263,27 @@ class ResourceCache {
258
263
  * @returns Promise<void> 下载完成或失败
259
264
  */
260
265
  downloadResourceAsync(url, filePath, redirectCount = 0, onProgress) {
261
- // 检查是否正在下载,避免重复下载
262
- if (this._downloadingUrls.has(url)) {
263
- return Promise.reject(new Error(`资源正在下载中: ${url}`));
266
+ // 检查是否正在下载,如果正在下载则返回已存在的 Promise(允许多个请求等待同一个下载任务)
267
+ const existingDownload = this._downloadingUrls.get(url);
268
+ if (existingDownload) {
269
+ // 如果提供了新的进度回调,添加到回调列表中
270
+ if (onProgress) {
271
+ existingDownload.progressCallbacks.add(onProgress);
272
+ }
273
+ return existingDownload.promise;
264
274
  }
265
275
  // 检查重定向次数限制
266
276
  if (redirectCount >= MAX_REDIRECTS) {
267
277
  return Promise.reject(new Error(`重定向次数超过限制 (${MAX_REDIRECTS}): ${url}`));
268
278
  }
269
- this._downloadingUrls.add(url);
270
- return new Promise((resolve, reject) => {
279
+ // 创建新的下载 Promise
280
+ let downloader;
281
+ // 创建进度回调集合(在 Promise 外部,以便后续可以访问)
282
+ const progressCallbacks = new Set();
283
+ if (onProgress) {
284
+ progressCallbacks.add(onProgress);
285
+ }
286
+ const downloadPromise = new Promise((resolve, reject) => {
271
287
  const tempFilePath = `${filePath}.cache`;
272
288
  const downloadDir = path.dirname(tempFilePath);
273
289
  const downloadFileName = path.basename(tempFilePath);
@@ -290,30 +306,35 @@ class ResourceCache {
290
306
  maxRedirects: MAX_REDIRECTS
291
307
  }
292
308
  });
293
- // 监听下载进度
294
- if (onProgress) {
295
- dl.on('progress', (stats) => {
309
+ // 将下载器实例赋值给外部变量(Promise 构造函数是同步执行的)
310
+ downloader = dl;
311
+ // 监听下载进度 - 必须在 start() 之前设置
312
+ // 注意:我们需要在 start() 之前设置事件监听器,这样后续的请求可以正确绑定进度回调
313
+ dl.on('progress', (stats) => {
314
+ // 调用所有注册的进度回调
315
+ const progressData = {
316
+ url,
317
+ downloaded: stats.downloaded || 0,
318
+ total: stats.total || 0,
319
+ percentage: stats.progress !== undefined ? Math.round(stats.progress) : -1,
320
+ speed: stats.speed || 0
321
+ };
322
+ progressCallbacks.forEach((callback) => {
296
323
  try {
297
- // stats 包含: progress (百分比), downloaded (已下载字节), total (总字节), speed (速度)
298
- onProgress({
299
- url,
300
- downloaded: stats.downloaded || 0,
301
- total: stats.total || 0,
302
- percentage: stats.progress !== undefined ? Math.round(stats.progress) : -1,
303
- speed: stats.speed || 0
304
- });
324
+ callback(progressData);
305
325
  }
306
326
  catch (error) {
307
327
  // 忽略回调中的错误,避免影响下载
308
328
  console.error('进度回调执行失败:', error);
309
329
  }
310
330
  });
311
- }
331
+ });
312
332
  // 监听下载完成
313
333
  dl.on('end', () => {
314
- this._downloadingUrls.delete(url);
315
334
  // 将临时文件重命名为最终文件
316
335
  fs.rename(tempFilePath, filePath, (renameErr) => {
336
+ // 无论成功还是失败,都要从下载列表中删除
337
+ this._downloadingUrls.delete(url);
317
338
  if (renameErr) {
318
339
  reject(new Error(`缓存文件重命名失败 from ${tempFilePath} to ${filePath}: ${renameErr.message}`));
319
340
  }
@@ -324,6 +345,7 @@ class ResourceCache {
324
345
  });
325
346
  // 监听下载错误
326
347
  dl.on('error', (err) => {
348
+ // 从下载列表中删除
327
349
  this._downloadingUrls.delete(url);
328
350
  // 清理临时文件
329
351
  fs.promises.unlink(tempFilePath).catch(() => {
@@ -333,6 +355,7 @@ class ResourceCache {
333
355
  });
334
356
  // 监听下载停止(取消)
335
357
  dl.on('stop', () => {
358
+ // 从下载列表中删除
336
359
  this._downloadingUrls.delete(url);
337
360
  // 清理临时文件
338
361
  fs.promises.unlink(tempFilePath).catch(() => {
@@ -340,12 +363,22 @@ class ResourceCache {
340
363
  });
341
364
  reject(new Error(`下载已停止: ${url}`));
342
365
  });
343
- // 开始下载
366
+ // 开始下载 - 必须在所有事件监听器设置完成后调用
344
367
  dl.start().catch((err) => {
368
+ // 从下载列表中删除
345
369
  this._downloadingUrls.delete(url);
346
370
  reject(err instanceof Error ? err : new Error(`启动下载失败: ${url}`));
347
371
  });
348
372
  });
373
+ // 立即将 Promise、下载器实例和进度回调集合存储到 Map 中,供后续相同 URL 的请求复用
374
+ // 注意:必须在 start() 调用之前存储,以便后续请求可以绑定进度回调
375
+ // 由于 Promise 构造函数是同步执行的,此时 downloader 已经被赋值,事件监听器也已设置完成
376
+ this._downloadingUrls.set(url, {
377
+ promise: downloadPromise,
378
+ downloader: downloader,
379
+ progressCallbacks
380
+ });
381
+ return downloadPromise;
349
382
  }
350
383
  /**
351
384
  * 下载资源到本地缓存(同步版本,不返回 Promise,用于拦截器)
@@ -358,6 +391,7 @@ class ResourceCache {
358
391
  if (this._downloadingUrls.has(url)) {
359
392
  return;
360
393
  }
394
+ // 调用 downloadResourceAsync,它会自动处理重复下载的情况
361
395
  this.downloadResourceAsync(url, filePath).catch(() => {
362
396
  // 静默处理错误,避免日志过多
363
397
  });
@@ -629,6 +663,64 @@ class ResourceCache {
629
663
  }
630
664
  });
631
665
  }
666
+ /**
667
+ * 获取单个 URL 的缓存信息
668
+ * @param url 资源 URL
669
+ * @returns Promise<{ url: string, exists: boolean, valid?: boolean, filePath?: string, hostPath?: string, mimeType?: string, size?: number, error?: string }> 返回缓存信息
670
+ */
671
+ async getCacheUrl(url) {
672
+ try {
673
+ const cachePath = this.getCachedPath(url);
674
+ const exists = fs.existsSync(cachePath.filePath);
675
+ if (!exists) {
676
+ return { url, exists: false };
677
+ }
678
+ // 检查缓存是否有效
679
+ const valid = await this.isCacheValidAsync(cachePath.filePath);
680
+ // 获取文件信息
681
+ const stats = await fs.promises.stat(cachePath.filePath);
682
+ // 从文件扩展名获取 MIME 类型
683
+ const ext = path.extname(cachePath.filePath).replace(/^\./, '') || DEFAULT_EXT.replace(/^\./, '');
684
+ const mimeType = this._getMimeTypeFromExt(ext);
685
+ return {
686
+ url,
687
+ exists: true,
688
+ valid,
689
+ filePath: cachePath.filePath,
690
+ hostPath: cachePath.hostPath,
691
+ mimeType,
692
+ size: stats.size
693
+ };
694
+ }
695
+ catch (error) {
696
+ return {
697
+ url,
698
+ exists: false,
699
+ error: error instanceof Error ? error.message : '未知错误'
700
+ };
701
+ }
702
+ }
703
+ /**
704
+ * 获取多个 URL 的缓存信息
705
+ * @param urls 资源 URL 数组
706
+ * @returns Promise<Array<{ url: string, exists: boolean, valid?: boolean, filePath?: string, hostPath?: string, mimeType?: string, size?: number, error?: string }>> 返回每个 URL 的缓存信息
707
+ */
708
+ async getCacheUrls(urls) {
709
+ const results = await Promise.allSettled(urls.map(url => this.getCacheUrl(url)));
710
+ return results.map((result, index) => {
711
+ const url = urls[index] || '';
712
+ if (result.status === 'fulfilled') {
713
+ return result.value;
714
+ }
715
+ else {
716
+ return {
717
+ url,
718
+ exists: false,
719
+ error: result.reason?.message || '未知错误'
720
+ };
721
+ }
722
+ });
723
+ }
632
724
  /**
633
725
  * 删除单个 URL 的资源(异步版本,性能更好)
634
726
  * @param url 要删除的资源 URL