smart-image-scraper-mcp 2.12.4 → 2.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/index.js +24 -7
- package/src/services/fileManager.js +12 -2
- package/src/services/orchestrator.js +248 -11
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -46,12 +46,16 @@ const SMART_SCRAPER_TOOL = {
|
|
|
46
46
|
【核心功能】
|
|
47
47
|
1. 搜索图片链接 (mode=link) - 返回验证过的图片URL列表
|
|
48
48
|
2. 下载图片 (mode=download) - 下载到本地,自动按质量排序优先高清
|
|
49
|
-
3.
|
|
50
|
-
4.
|
|
49
|
+
3. 搜索+下载 (mode=both) - 同时返回链接列表和下载文件,适合需要链接备份的场景
|
|
50
|
+
4. 尺寸统一 (targetSize) - 下载后自动裁剪/缩放到指定尺寸
|
|
51
|
+
5. 宽高比过滤 (aspect) - 横向/竖向/正方形
|
|
52
|
+
6. 自定义下载路径 (savePath) - 指定图片保存位置,不填则保存到MCP服务器项目下images目录
|
|
51
53
|
|
|
52
54
|
【参数选择指南】
|
|
53
55
|
- 用户要"找/搜索/查找图片" → mode="link"
|
|
54
56
|
- 用户要"下载/保存/获取图片" → mode="download"
|
|
57
|
+
- 用户要"搜索并下载/链接和下载都要" → mode="both"
|
|
58
|
+
- 用户要"保存到指定目录" → savePath="D:/my/path"
|
|
55
59
|
- 用户要"高清/大图/壁纸" → size="large" 或 "wallpaper",quality="high"
|
|
56
60
|
- 用户要"高质量/精选/优质" → quality="high"
|
|
57
61
|
- 用户要"电脑壁纸/横屏/横向" → aspect="wide"
|
|
@@ -75,7 +79,9 @@ const SMART_SCRAPER_TOOL = {
|
|
|
75
79
|
3. 下载电脑壁纸并统一为1080p: {"query":"风景","mode":"download","count":10,"aspect":"wide","targetSize":"desktop_1080p"}
|
|
76
80
|
4. 下载手机壁纸: {"query":"动漫","mode":"download","count":10,"aspect":"tall","targetSize":"mobile_hd"}
|
|
77
81
|
5. 批量下载多类图片: {"query":"猫,狗,兔子","mode":"download","count":5}
|
|
78
|
-
6. 获取高质量图片: {"query":"风景","mode":"link","count":5,"size":"large","quality":"high"}
|
|
82
|
+
6. 获取高质量图片: {"query":"风景","mode":"link","count":5,"size":"large","quality":"high"}
|
|
83
|
+
7. 搜索并下载到指定目录: {"query":"风景","mode":"both","count":5,"savePath":"D:/photos"}
|
|
84
|
+
8. 下载到自定义目录: {"query":"猫","mode":"download","count":5,"savePath":"D:/my/images"}`,
|
|
79
85
|
inputSchema: {
|
|
80
86
|
type: 'object',
|
|
81
87
|
properties: {
|
|
@@ -85,8 +91,8 @@ const SMART_SCRAPER_TOOL = {
|
|
|
85
91
|
},
|
|
86
92
|
mode: {
|
|
87
93
|
type: 'string',
|
|
88
|
-
enum: ['link', 'download'],
|
|
89
|
-
description: "运行模式。link=仅返回验证过的图片URL
|
|
94
|
+
enum: ['link', 'download', 'both'],
|
|
95
|
+
description: "运行模式。link=仅返回验证过的图片URL列表;download=下载图片到本地;both=同时返回链接和下载文件(用户需要链接备份时使用)",
|
|
90
96
|
},
|
|
91
97
|
count: {
|
|
92
98
|
type: 'number',
|
|
@@ -139,6 +145,15 @@ const SMART_SCRAPER_TOOL = {
|
|
|
139
145
|
description: '最小文件大小过滤。文件越大通常质量越高。any=不限制;建议高清图片用100kb以上',
|
|
140
146
|
default: 'any',
|
|
141
147
|
},
|
|
148
|
+
savePath: {
|
|
149
|
+
type: 'string',
|
|
150
|
+
description: '自定义图片保存路径(绝对路径)。仅对download和both模式有效。不填则默认保存到MCP服务器项目下images目录。示例: "D:/my/photos" 或 "C:/Users/xxx/Pictures"',
|
|
151
|
+
},
|
|
152
|
+
filterHotlink: {
|
|
153
|
+
type: 'boolean',
|
|
154
|
+
description: '是否过滤防盗链图片。默认true(开启过滤)。设为false可获取更多结果但部分链接可能无法直接在浏览器打开',
|
|
155
|
+
default: true,
|
|
156
|
+
},
|
|
142
157
|
},
|
|
143
158
|
required: ['query', 'mode'],
|
|
144
159
|
},
|
|
@@ -168,9 +183,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
168
183
|
};
|
|
169
184
|
}
|
|
170
185
|
|
|
171
|
-
if (!args.mode || !['link', 'download'].includes(args.mode)) {
|
|
186
|
+
if (!args.mode || !['link', 'download', 'both'].includes(args.mode)) {
|
|
172
187
|
return {
|
|
173
|
-
content: [{ type: 'text', text: "错误: 请指定有效的运行模式 (mode): 'link' 或 '
|
|
188
|
+
content: [{ type: 'text', text: "错误: 请指定有效的运行模式 (mode): 'link', 'download' 或 'both'" }],
|
|
174
189
|
isError: true,
|
|
175
190
|
};
|
|
176
191
|
}
|
|
@@ -196,6 +211,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
196
211
|
safeSearch: args.safeSearch || 'moderate',
|
|
197
212
|
quality: ['fast', 'balanced', 'high'].includes(args.quality) ? args.quality : 'balanced',
|
|
198
213
|
minFileSize: ['any', '50kb', '100kb', '200kb', '500kb', '1mb'].includes(args.minFileSize) ? args.minFileSize : 'any',
|
|
214
|
+
savePath: args.savePath && typeof args.savePath === 'string' ? args.savePath.trim() : null,
|
|
215
|
+
filterHotlink: args.filterHotlink !== false, // 默认 true
|
|
199
216
|
};
|
|
200
217
|
|
|
201
218
|
// 使用 Promise.race 确保一定会在超时内返回
|
|
@@ -15,11 +15,21 @@ import config from '../config/index.js';
|
|
|
15
15
|
const globalDownloadLimit = pLimit(config.MAX_DOWNLOAD_CONCURRENCY || 10);
|
|
16
16
|
|
|
17
17
|
export class FileManager {
|
|
18
|
-
constructor() {
|
|
19
|
-
this.saveRoot = config.SAVE_ROOT;
|
|
18
|
+
constructor(options = {}) {
|
|
19
|
+
this.saveRoot = options.saveRoot || config.SAVE_ROOT;
|
|
20
20
|
this.limit = globalDownloadLimit; // 使用全局共享限制器
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* 设置自定义保存根目录
|
|
25
|
+
* @param {string} savePath - 绝对路径
|
|
26
|
+
*/
|
|
27
|
+
setSaveRoot(savePath) {
|
|
28
|
+
if (savePath && typeof savePath === 'string') {
|
|
29
|
+
this.saveRoot = savePath;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
23
33
|
/**
|
|
24
34
|
* 清理文件名,移除非法字符
|
|
25
35
|
* @param {string} filename - 原始文件名
|
|
@@ -174,11 +174,16 @@ export class Orchestrator {
|
|
|
174
174
|
// 检查是否已中止
|
|
175
175
|
if (signal?.aborted) throw new Error('操作已取消');
|
|
176
176
|
|
|
177
|
-
//
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
177
|
+
// 根据 filterHotlink 参数决定是否过滤防盗链 URL
|
|
178
|
+
const shouldFilterHotlink = options.filterHotlink !== false;
|
|
179
|
+
let filteredRawUrls = rawUrls;
|
|
180
|
+
let hotlinkCount = 0;
|
|
181
|
+
if (shouldFilterHotlink) {
|
|
182
|
+
filteredRawUrls = rawUrls.filter(url => !this.linkValidator.isHotlinkProtected(url));
|
|
183
|
+
hotlinkCount = rawUrls.length - filteredRawUrls.length;
|
|
184
|
+
if (hotlinkCount > 0) {
|
|
185
|
+
logger.warn(`[HOTLINK] "${keyword}" - filtered ${hotlinkCount} hotlink-protected URLs`);
|
|
186
|
+
}
|
|
182
187
|
}
|
|
183
188
|
|
|
184
189
|
if (fastMode) {
|
|
@@ -394,6 +399,196 @@ export class Orchestrator {
|
|
|
394
399
|
}
|
|
395
400
|
}
|
|
396
401
|
|
|
402
|
+
/**
|
|
403
|
+
* 处理单个关键词 - Both 模式(同时返回链接和下载文件)
|
|
404
|
+
* @param {string} keyword - 关键词
|
|
405
|
+
* @param {number} count - 需要的图片数量
|
|
406
|
+
* @param {string} source - 搜索源
|
|
407
|
+
* @param {Object} options - 搜索选项
|
|
408
|
+
* @returns {Promise<Object>} - 处理结果
|
|
409
|
+
*/
|
|
410
|
+
async processKeywordBoth(keyword, count, source, options = {}) {
|
|
411
|
+
const startTime = Date.now();
|
|
412
|
+
const KEYWORD_TIMEOUT = 45000; // both 模式45秒超时(包含下载)
|
|
413
|
+
|
|
414
|
+
// 检查是否已被中止
|
|
415
|
+
if (this.abortController?.signal?.aborted) {
|
|
416
|
+
return { keyword, success: false, error: '操作已取消', duration: 0 };
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
let timeoutId;
|
|
420
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
421
|
+
timeoutId = setTimeout(() => reject(new Error(`关键词 "${keyword}" 处理超时(45秒)`)), KEYWORD_TIMEOUT);
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
try {
|
|
425
|
+
const result = await Promise.race([
|
|
426
|
+
this._processKeywordBothInternal(keyword, count, source, options, startTime),
|
|
427
|
+
timeoutPromise
|
|
428
|
+
]);
|
|
429
|
+
clearTimeout(timeoutId);
|
|
430
|
+
return result;
|
|
431
|
+
} catch (error) {
|
|
432
|
+
clearTimeout(timeoutId);
|
|
433
|
+
logger.error(`Process keyword both error: ${keyword}`, { error: error.message });
|
|
434
|
+
return {
|
|
435
|
+
keyword,
|
|
436
|
+
success: false,
|
|
437
|
+
error: error.message,
|
|
438
|
+
duration: Date.now() - startTime,
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
async _processKeywordBothInternal(keyword, count, source, options, startTime) {
|
|
444
|
+
const qualityMode = options.quality || 'balanced';
|
|
445
|
+
const fastMode = qualityMode === 'fast';
|
|
446
|
+
const prioritizeQuality = qualityMode === 'high';
|
|
447
|
+
const minFileSize = this._parseMinFileSize(options.minFileSize);
|
|
448
|
+
const signal = this.abortController?.signal;
|
|
449
|
+
const shouldFilterHotlink = options.filterHotlink !== false;
|
|
450
|
+
|
|
451
|
+
try {
|
|
452
|
+
const scraper = getScraper(source);
|
|
453
|
+
const searchCount = Math.max(count * 3, 10);
|
|
454
|
+
|
|
455
|
+
// 搜索
|
|
456
|
+
const cachedUrls = searchCache.getSearchResult(keyword, source, options);
|
|
457
|
+
let rawUrls;
|
|
458
|
+
|
|
459
|
+
if (cachedUrls && cachedUrls.length >= count) {
|
|
460
|
+
logger.info(`[CACHE] "${keyword}" - ${cachedUrls.length} URLs`);
|
|
461
|
+
rawUrls = cachedUrls;
|
|
462
|
+
metrics.recordCacheHit();
|
|
463
|
+
} else {
|
|
464
|
+
if (signal?.aborted) throw new Error('操作已取消');
|
|
465
|
+
logger.info(`[SEARCH] "${keyword}" (target: ${searchCount})...`);
|
|
466
|
+
rawUrls = await scraper.search(keyword, searchCount, options);
|
|
467
|
+
if (rawUrls.length > 0) {
|
|
468
|
+
searchCache.setSearchResult(keyword, source, options, rawUrls);
|
|
469
|
+
}
|
|
470
|
+
metrics.recordCacheMiss();
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if (rawUrls.length === 0) {
|
|
474
|
+
return {
|
|
475
|
+
keyword,
|
|
476
|
+
success: false,
|
|
477
|
+
error: '未找到任何图片',
|
|
478
|
+
duration: Date.now() - startTime,
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (signal?.aborted) throw new Error('操作已取消');
|
|
483
|
+
|
|
484
|
+
// both 模式:对所有原始 URL 统一验证一次,然后分别用于链接展示和下载
|
|
485
|
+
let allValidUrls; // 所有验证通过的 URL
|
|
486
|
+
let validatedUrls; // 链接列表(可能过滤防盗链)
|
|
487
|
+
let downloadUrls; // 下载列表(不过滤防盗链,下载时加 Referer)
|
|
488
|
+
let qualityModeLabel;
|
|
489
|
+
let hotlinkCount = 0;
|
|
490
|
+
|
|
491
|
+
if (fastMode) {
|
|
492
|
+
// fast 模式:不验证
|
|
493
|
+
allValidUrls = rawUrls.slice(0, count * 2 + 5);
|
|
494
|
+
qualityModeLabel = '快速模式(跳过验证)';
|
|
495
|
+
} else {
|
|
496
|
+
// balanced/high 模式:统一验证所有原始 URL
|
|
497
|
+
const maxValidate = Math.min(rawUrls.length, count * 2 + 5);
|
|
498
|
+
const urlsToValidate = rawUrls.slice(0, maxValidate);
|
|
499
|
+
const { valid } = await this.linkValidator.validateMany(urlsToValidate, {
|
|
500
|
+
fetchQuality: prioritizeQuality,
|
|
501
|
+
sortByQuality: prioritizeQuality,
|
|
502
|
+
minFileSize: minFileSize,
|
|
503
|
+
signal,
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
let filteredValid = valid;
|
|
507
|
+
if (minFileSize > 0) {
|
|
508
|
+
filteredValid = valid.filter(v => {
|
|
509
|
+
const size = v.quality?.contentLength || 0;
|
|
510
|
+
return size >= minFileSize || size === 0;
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
allValidUrls = filteredValid.map(v => v.url);
|
|
515
|
+
qualityModeLabel = prioritizeQuality ? '高质量模式(验证+排序)' : '平衡模式(验证)';
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// 链接列表:根据 filterHotlink 参数决定是否过滤防盗链
|
|
519
|
+
if (shouldFilterHotlink) {
|
|
520
|
+
const filtered = allValidUrls.filter(url => !this.linkValidator.isHotlinkProtected(url));
|
|
521
|
+
hotlinkCount = allValidUrls.length - filtered.length;
|
|
522
|
+
validatedUrls = filtered.slice(0, count);
|
|
523
|
+
if (hotlinkCount > 0) {
|
|
524
|
+
logger.warn(`[HOTLINK] "${keyword}" - filtered ${hotlinkCount} hotlink-protected URLs from links`);
|
|
525
|
+
}
|
|
526
|
+
} else {
|
|
527
|
+
validatedUrls = allValidUrls.slice(0, count);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// 下载列表:使用所有验证通过的 URL(不过滤防盗链,下载时加 Referer 绕过)
|
|
531
|
+
downloadUrls = allValidUrls.slice(0, count * 2);
|
|
532
|
+
|
|
533
|
+
if (signal?.aborted) throw new Error('操作已取消');
|
|
534
|
+
|
|
535
|
+
const { success, failed } = await this.fileManager.downloadMany(downloadUrls, keyword);
|
|
536
|
+
|
|
537
|
+
let resultDownloads = success.slice(0, count);
|
|
538
|
+
|
|
539
|
+
// 如果指定了目标尺寸,进行后处理
|
|
540
|
+
let processedCount = 0;
|
|
541
|
+
let processFailedCount = 0;
|
|
542
|
+
if (options.targetSize && resultDownloads.length > 0) {
|
|
543
|
+
const targetSize = this.imageProcessor.parseTargetSize(options.targetSize);
|
|
544
|
+
if (targetSize) {
|
|
545
|
+
logger.info(`Processing images to ${targetSize.width}x${targetSize.height}`);
|
|
546
|
+
const processResult = await this.imageProcessor.processMany(resultDownloads, {
|
|
547
|
+
width: targetSize.width,
|
|
548
|
+
height: targetSize.height,
|
|
549
|
+
fit: options.fit || 'cover',
|
|
550
|
+
position: options.position || 'center',
|
|
551
|
+
});
|
|
552
|
+
resultDownloads = processResult.success;
|
|
553
|
+
processedCount = processResult.success.length;
|
|
554
|
+
processFailedCount = processResult.failed.length;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// 保存元数据
|
|
559
|
+
let metadataPath = null;
|
|
560
|
+
if (resultDownloads.length > 0) {
|
|
561
|
+
metadataPath = await this.fileManager.saveMetadata(keyword, resultDownloads);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
return {
|
|
565
|
+
keyword,
|
|
566
|
+
success: true,
|
|
567
|
+
mode: 'both',
|
|
568
|
+
totalSearched: rawUrls.length,
|
|
569
|
+
hotlinkFiltered: hotlinkCount,
|
|
570
|
+
// 链接部分
|
|
571
|
+
urls: validatedUrls,
|
|
572
|
+
urlCount: validatedUrls.length,
|
|
573
|
+
qualityMode,
|
|
574
|
+
qualityModeLabel,
|
|
575
|
+
// 下载部分
|
|
576
|
+
totalDownloaded: success.length,
|
|
577
|
+
totalFailed: failed.length,
|
|
578
|
+
totalProcessed: processedCount,
|
|
579
|
+
totalProcessFailed: processFailedCount,
|
|
580
|
+
files: resultDownloads,
|
|
581
|
+
count: resultDownloads.length,
|
|
582
|
+
saveDir: this.fileManager.getKeywordDir(keyword),
|
|
583
|
+
metadataPath,
|
|
584
|
+
targetSize: options.targetSize || null,
|
|
585
|
+
duration: Date.now() - startTime,
|
|
586
|
+
};
|
|
587
|
+
} catch (error) {
|
|
588
|
+
throw error;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
397
592
|
/**
|
|
398
593
|
* 执行任务 - 直接执行(不使用队列,避免 MCP 兼容性问题)
|
|
399
594
|
* @param {Object} params - 任务参数
|
|
@@ -468,8 +663,14 @@ export class Orchestrator {
|
|
|
468
663
|
* 内部执行逻辑
|
|
469
664
|
*/
|
|
470
665
|
async _executeInternal(params) {
|
|
471
|
-
const { query, mode, count = config.DEFAULT_COUNT, source = config.DEFAULT_SOURCE, size = 'all', safeSearch = 'moderate', aspect = 'all', targetSize = null, fit = 'cover', position = 'center', quality = 'balanced', minFileSize = 'any' } = params;
|
|
472
|
-
const options = { size, safeSearch, aspect, targetSize, fit, position, quality, minFileSize };
|
|
666
|
+
const { query, mode, count = config.DEFAULT_COUNT, source = config.DEFAULT_SOURCE, size = 'all', safeSearch = 'moderate', aspect = 'all', targetSize = null, fit = 'cover', position = 'center', quality = 'balanced', minFileSize = 'any', savePath = null, filterHotlink = true } = params;
|
|
667
|
+
const options = { size, safeSearch, aspect, targetSize, fit, position, quality, minFileSize, filterHotlink };
|
|
668
|
+
|
|
669
|
+
// 如果指定了自定义保存路径,更新 fileManager
|
|
670
|
+
if (savePath && (mode === 'download' || mode === 'both')) {
|
|
671
|
+
this.fileManager.setSaveRoot(savePath);
|
|
672
|
+
logger.info(`[Orchestrator] Custom save path: ${savePath}`);
|
|
673
|
+
}
|
|
473
674
|
|
|
474
675
|
const startTime = Date.now();
|
|
475
676
|
let keywords = this.parseKeywords(query);
|
|
@@ -491,9 +692,14 @@ export class Orchestrator {
|
|
|
491
692
|
logger.info(`Starting task: mode=${mode}, keywords=${keywords.join(', ')}, count=${count}, source=${source}`);
|
|
492
693
|
|
|
493
694
|
// 根据模式选择处理函数
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
695
|
+
let processFunc;
|
|
696
|
+
if (mode === 'link') {
|
|
697
|
+
processFunc = this.processKeywordLink.bind(this);
|
|
698
|
+
} else if (mode === 'both') {
|
|
699
|
+
processFunc = this.processKeywordBoth.bind(this);
|
|
700
|
+
} else {
|
|
701
|
+
processFunc = this.processKeywordDownload.bind(this);
|
|
702
|
+
}
|
|
497
703
|
|
|
498
704
|
// 串行处理关键词,避免阻塞事件循环
|
|
499
705
|
const results = [];
|
|
@@ -552,7 +758,8 @@ export class Orchestrator {
|
|
|
552
758
|
const lines = [];
|
|
553
759
|
lines.push(`# 📷 图片抓取报告`);
|
|
554
760
|
lines.push('');
|
|
555
|
-
|
|
761
|
+
const modeLabels = { link: '链接提取', download: '本地下载', both: '链接提取+本地下载' };
|
|
762
|
+
lines.push(`- **模式**: ${modeLabels[result.mode] || result.mode}`);
|
|
556
763
|
lines.push(`- **搜索源**: ${result.source}`);
|
|
557
764
|
lines.push(`- **关键词数量**: ${result.totalKeywords}`);
|
|
558
765
|
lines.push(`- **成功**: ${result.successCount} | **失败**: ${result.failedCount}`);
|
|
@@ -583,7 +790,37 @@ export class Orchestrator {
|
|
|
583
790
|
(r.urls || []).forEach((url, i) => {
|
|
584
791
|
lines.push(`${i + 1}. ${url}`);
|
|
585
792
|
});
|
|
793
|
+
} else if (r.mode === 'both') {
|
|
794
|
+
// both 模式:同时显示链接和下载文件
|
|
795
|
+
lines.push(`- 搜索到: ${r.totalSearched || 0} 张`);
|
|
796
|
+
if (r.hotlinkFiltered > 0) {
|
|
797
|
+
lines.push(`- 防盗链过滤: ${r.hotlinkFiltered} 张`);
|
|
798
|
+
}
|
|
799
|
+
lines.push(`- 质量模式: ${r.qualityModeLabel || '快速模式'}`);
|
|
800
|
+
lines.push(`- 有效链接: ${r.urlCount || 0} 张`);
|
|
801
|
+
lines.push(`- 下载成功: ${r.totalDownloaded} 张`);
|
|
802
|
+
lines.push(`- 下载失败: ${r.totalFailed} 张`);
|
|
803
|
+
if (r.targetSize) {
|
|
804
|
+
lines.push(`- 尺寸处理: ${r.totalProcessed} 成功, ${r.totalProcessFailed} 失败`);
|
|
805
|
+
lines.push(`- 目标尺寸: ${r.targetSize}`);
|
|
806
|
+
}
|
|
807
|
+
lines.push(`- 最终保存: ${r.count} 张`);
|
|
808
|
+
lines.push(`- 存储目录: \`${r.saveDir}\``);
|
|
809
|
+
lines.push(`- 耗时: ${(r.duration / 1000).toFixed(2)}秒`);
|
|
810
|
+
lines.push('');
|
|
811
|
+
lines.push('### 有效链接');
|
|
812
|
+
lines.push('');
|
|
813
|
+
(r.urls || []).forEach((url, i) => {
|
|
814
|
+
lines.push(`${i + 1}. ${url}`);
|
|
815
|
+
});
|
|
816
|
+
lines.push('');
|
|
817
|
+
lines.push('### 已下载文件');
|
|
818
|
+
lines.push('');
|
|
819
|
+
(r.files || []).forEach((file, i) => {
|
|
820
|
+
lines.push(`${i + 1}. \`${file.path}\``);
|
|
821
|
+
});
|
|
586
822
|
} else {
|
|
823
|
+
// download 模式
|
|
587
824
|
lines.push(`- 搜索到: ${r.totalSearched} 张`);
|
|
588
825
|
lines.push(`- 下载成功: ${r.totalDownloaded} 张`);
|
|
589
826
|
lines.push(`- 下载失败: ${r.totalFailed} 张`);
|