change-image-suffix 2.1.3 → 2.1.4
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/CHANGELOG.md +17 -0
- package/README.md +15 -13
- package/dist/index.js +81 -27
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
|
4
4
|
|
|
5
|
+
### [2.1.4](https://github.com/GuoSirius/change-image-suffix/compare/v2.1.3...v2.1.4) (2026-05-23)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* output to <format>/ dir, command injection, recursive nesting, and other issues ([32da38a](https://github.com/GuoSirius/change-image-suffix/commit/32da38aef36a7d736e91e0375573a905d565d864))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Chores
|
|
14
|
+
|
|
15
|
+
* update local settings permissions ([1a86130](https://github.com/GuoSirius/change-image-suffix/commit/1a86130de6005348d6ba8aaba26500d7688be996))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Documentation
|
|
19
|
+
|
|
20
|
+
* update README output convention, add .claude/ commit policy memory ([b689bc6](https://github.com/GuoSirius/change-image-suffix/commit/b689bc60ed397eb826954e289c3f89488bd97449))
|
|
21
|
+
|
|
5
22
|
### [2.1.3](https://github.com/GuoSirius/change-image-suffix/compare/v2.1.2...v2.1.3) (2026-05-21)
|
|
6
23
|
|
|
7
24
|
|
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
- 📏 支持递归深度限制
|
|
10
10
|
- 🎯 支持指定源文件后缀(png, jpg, gif 等)
|
|
11
11
|
- 🎨 支持多种目标格式(webp, jpg, png, avif, tiff)
|
|
12
|
-
- 📤 输出到 `
|
|
12
|
+
- 📤 输出到 `<目标格式>/` 子目录(如 `webp/`, `jpg/`)
|
|
13
13
|
- 🔢 同名不同后缀文件自动编号(`_01`, `_02`)
|
|
14
14
|
- 🖱️ Windows 右键菜单集成
|
|
15
15
|
- ⚡ 基于 [sharp](https://sharp.pixel.glass/) 高性能图片处理
|
|
@@ -101,7 +101,7 @@ cis uninstall-menu
|
|
|
101
101
|
|
|
102
102
|
## 文件命名规范
|
|
103
103
|
|
|
104
|
-
转换后的文件输出到
|
|
104
|
+
转换后的文件输出到 **`<目标格式>/`** 子目录下(例如转换为 webp 则输出到 `webp/`,转换为 jpg 则输出到 `jpg/`),命名规则如下:
|
|
105
105
|
|
|
106
106
|
### 同名不同后缀 → 自动编号
|
|
107
107
|
|
|
@@ -109,34 +109,36 @@ cis uninstall-menu
|
|
|
109
109
|
|
|
110
110
|
```
|
|
111
111
|
源目录: photo.png + photo.jpg + photo.gif
|
|
112
|
-
输出:
|
|
113
|
-
|
|
114
|
-
|
|
112
|
+
输出: webp/photo_01.webp
|
|
113
|
+
webp/photo_02.webp
|
|
114
|
+
webp/photo_03.webp
|
|
115
115
|
```
|
|
116
116
|
|
|
117
117
|
### 不同名或不同文件 → 直接覆盖
|
|
118
118
|
|
|
119
119
|
```
|
|
120
120
|
源目录: banner.png + logo.jpg
|
|
121
|
-
输出:
|
|
122
|
-
|
|
121
|
+
输出: webp/banner.webp
|
|
122
|
+
webp/logo.webp
|
|
123
123
|
```
|
|
124
124
|
|
|
125
125
|
### 同格式转换 → 直接覆盖
|
|
126
126
|
|
|
127
127
|
```
|
|
128
128
|
源目录: photo.webp
|
|
129
|
-
输出:
|
|
129
|
+
输出: webp/photo.webp (直接复制,无双重后缀)
|
|
130
130
|
```
|
|
131
131
|
|
|
132
132
|
### 输出目录结构
|
|
133
133
|
|
|
134
134
|
```
|
|
135
135
|
📁 原目录/
|
|
136
|
-
├── 📁
|
|
136
|
+
├── 📁 webp/ ← 转换后的 webp 文件
|
|
137
137
|
│ ├── photo.webp
|
|
138
|
-
│ ├── banner.
|
|
139
|
-
│ └── photo_01.
|
|
138
|
+
│ ├── banner.webp
|
|
139
|
+
│ └── photo_01.webp
|
|
140
|
+
├── 📁 jpg/ ← 转换后的 jpg 文件
|
|
141
|
+
│ └── ...
|
|
140
142
|
├── photo.png
|
|
141
143
|
├── banner.jpg
|
|
142
144
|
└── logo.gif
|
|
@@ -193,14 +195,14 @@ cis -f ./avatar.png -t webp
|
|
|
193
195
|
### 示例 6:多选文件/目录批量转换
|
|
194
196
|
|
|
195
197
|
```bash
|
|
196
|
-
# 多选文件(空格分隔),每个文件的输出在其所在目录的
|
|
198
|
+
# 多选文件(空格分隔),每个文件的输出在其所在目录的 <目标格式>/
|
|
197
199
|
cis ./photo1.png ./photo2.jpg ./folder3
|
|
198
200
|
|
|
199
201
|
# 多选多个目录
|
|
200
202
|
cis ./images ./icons ./logos
|
|
201
203
|
```
|
|
202
204
|
|
|
203
|
-
> 💡 **多选时**:每个文件/目录的输出结果放在**各自所在目录的 `
|
|
205
|
+
> 💡 **多选时**:每个文件/目录的输出结果放在**各自所在目录的 `<目标格式>/` 子目录**中(如 `webp/`, `jpg/`),互不干扰。
|
|
204
206
|
|
|
205
207
|
---
|
|
206
208
|
|
package/dist/index.js
CHANGED
|
@@ -203,7 +203,7 @@ endlocal
|
|
|
203
203
|
console.log(' 📁 文件夹空白处/图标右键 → 悬停展开格式子菜单');
|
|
204
204
|
console.log(' 🖼 图片文件上右键 → 悬停展开格式子菜单');
|
|
205
205
|
console.log(' ⚠️ 非图片文件右键 → 菜单显示但不处理');
|
|
206
|
-
console.log(` 📂 输出目录:
|
|
206
|
+
console.log(` 📂 输出目录: <原目录>/<目标格式>/`);
|
|
207
207
|
console.log('\n💡 提示:如需卸载,执行 cis uninstall-menu');
|
|
208
208
|
}
|
|
209
209
|
function uninstallContextMenu() {
|
|
@@ -232,18 +232,28 @@ function uninstallContextMenu() {
|
|
|
232
232
|
}
|
|
233
233
|
catch { /* ignore */ }
|
|
234
234
|
}
|
|
235
|
-
//
|
|
235
|
+
// 删除批处理文件、图标和版本标记
|
|
236
236
|
const appDataDir = path.join(os.homedir(), 'AppData', 'Roaming', 'change-image-suffix');
|
|
237
237
|
const batPath = path.join(appDataDir, 'cis_file.bat');
|
|
238
238
|
try {
|
|
239
239
|
fs.unlinkSync(batPath);
|
|
240
240
|
}
|
|
241
241
|
catch { /* ignore */ }
|
|
242
|
+
const iconPath = path.join(appDataDir, 'icon.ico');
|
|
243
|
+
try {
|
|
244
|
+
fs.unlinkSync(iconPath);
|
|
245
|
+
}
|
|
246
|
+
catch { /* ignore */ }
|
|
242
247
|
const versionFile = path.join(appDataDir, 'version.json');
|
|
243
248
|
try {
|
|
244
249
|
fs.unlinkSync(versionFile);
|
|
245
250
|
}
|
|
246
251
|
catch { /* ignore */ }
|
|
252
|
+
// 尝试删除目录(仅当为空时),失败也不影响
|
|
253
|
+
try {
|
|
254
|
+
fs.rmdirSync(appDataDir);
|
|
255
|
+
}
|
|
256
|
+
catch { /* ignore */ }
|
|
247
257
|
console.log('✅ 右键菜单已卸载');
|
|
248
258
|
}
|
|
249
259
|
// 自动检测版本变化并更新右键菜单(解决 npm update 不触发 postinstall 的问题)
|
|
@@ -252,20 +262,23 @@ function autoUpdateContextMenu() {
|
|
|
252
262
|
return;
|
|
253
263
|
const appDataDir = path.join(os.homedir(), 'AppData', 'Roaming', 'change-image-suffix');
|
|
254
264
|
const versionFile = path.join(appDataDir, 'version.json');
|
|
265
|
+
// 如果从未安装过右键菜单,跳过自动更新(postinstall 负责首次安装)
|
|
266
|
+
if (!fs.existsSync(versionFile))
|
|
267
|
+
return;
|
|
255
268
|
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
|
|
256
269
|
const currentVersion = pkg.version;
|
|
257
270
|
let installedVersion = '';
|
|
258
271
|
try {
|
|
259
|
-
|
|
260
|
-
installedVersion = JSON.parse(fs.readFileSync(versionFile, 'utf8')).version || '';
|
|
261
|
-
}
|
|
272
|
+
installedVersion = JSON.parse(fs.readFileSync(versionFile, 'utf8')).version || '';
|
|
262
273
|
}
|
|
263
274
|
catch { /* ignore */ }
|
|
264
275
|
if (installedVersion !== currentVersion) {
|
|
265
276
|
try {
|
|
266
277
|
installContextMenu();
|
|
267
278
|
}
|
|
268
|
-
catch {
|
|
279
|
+
catch {
|
|
280
|
+
console.warn('⚠️ 右键菜单自动更新失败,请手动执行 cis install-menu');
|
|
281
|
+
}
|
|
269
282
|
}
|
|
270
283
|
}
|
|
271
284
|
// ─────────────────────────────────────────
|
|
@@ -294,27 +307,40 @@ function parseArgs() {
|
|
|
294
307
|
continue;
|
|
295
308
|
}
|
|
296
309
|
if (arg === '-d' || arg === '--depth') {
|
|
297
|
-
if (i + 1 < args.length) {
|
|
298
|
-
|
|
299
|
-
if (isNaN(
|
|
310
|
+
if (i + 1 < args.length && !args[i + 1].startsWith('-')) {
|
|
311
|
+
const val = parseInt(args[++i], 10);
|
|
312
|
+
if (isNaN(val) || val < 1) {
|
|
300
313
|
console.error('❌ 深度必须是正整数');
|
|
301
314
|
process.exit(1);
|
|
302
315
|
}
|
|
316
|
+
options.maxDepth = val;
|
|
317
|
+
}
|
|
318
|
+
else {
|
|
319
|
+
console.error('❌ -d/--depth 需要指定一个正整数参数');
|
|
320
|
+
process.exit(1);
|
|
303
321
|
}
|
|
304
322
|
i++;
|
|
305
323
|
continue;
|
|
306
324
|
}
|
|
307
325
|
if (arg === '-e' || arg === '--extensions') {
|
|
308
|
-
if (i + 1 < args.length) {
|
|
326
|
+
if (i + 1 < args.length && !args[i + 1].startsWith('-')) {
|
|
309
327
|
options.extensions = args[++i].split(',').map(e => e.trim().toLowerCase().replace(/^\./, ''));
|
|
310
328
|
}
|
|
329
|
+
else {
|
|
330
|
+
console.error('❌ -e/--extensions 需要指定后缀参数');
|
|
331
|
+
process.exit(1);
|
|
332
|
+
}
|
|
311
333
|
i++;
|
|
312
334
|
continue;
|
|
313
335
|
}
|
|
314
336
|
if (arg === '-t' || arg === '--to') {
|
|
315
|
-
if (i + 1 < args.length) {
|
|
337
|
+
if (i + 1 < args.length && !args[i + 1].startsWith('-')) {
|
|
316
338
|
options.targetFormat = args[++i].trim().toLowerCase().replace(/^\./, '');
|
|
317
339
|
}
|
|
340
|
+
else {
|
|
341
|
+
console.error('❌ -t/--to 需要指定目标格式');
|
|
342
|
+
process.exit(1);
|
|
343
|
+
}
|
|
318
344
|
i++;
|
|
319
345
|
continue;
|
|
320
346
|
}
|
|
@@ -343,10 +369,15 @@ function parseArgs() {
|
|
|
343
369
|
}
|
|
344
370
|
if (arg === '-f' || arg === '--file') {
|
|
345
371
|
// 收集 -f 后的所有文件
|
|
372
|
+
const start = i + 1;
|
|
346
373
|
while (i + 1 < args.length && !args[i + 1].startsWith('-')) {
|
|
347
374
|
i++;
|
|
348
375
|
filesFromFlag.push(path.resolve(args[i]));
|
|
349
376
|
}
|
|
377
|
+
if (start > i) {
|
|
378
|
+
console.error('❌ -f/--file 需要指定至少一个文件路径');
|
|
379
|
+
process.exit(1);
|
|
380
|
+
}
|
|
350
381
|
i++;
|
|
351
382
|
continue;
|
|
352
383
|
}
|
|
@@ -434,14 +465,16 @@ function printHelp() {
|
|
|
434
465
|
// ─────────────────────────────────────────
|
|
435
466
|
// 图片处理
|
|
436
467
|
// ─────────────────────────────────────────
|
|
437
|
-
function getAllFiles(dir, extensions, recursive, currentDepth, maxDepth) {
|
|
468
|
+
function getAllFiles(dir, extensions, recursive, currentDepth, maxDepth, excludeDirName) {
|
|
438
469
|
const files = [];
|
|
439
470
|
try {
|
|
440
471
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
441
472
|
for (const entry of entries) {
|
|
442
473
|
const fullPath = path.join(dir, entry.name);
|
|
443
474
|
if (entry.isDirectory() && recursive && currentDepth < maxDepth) {
|
|
444
|
-
|
|
475
|
+
if (excludeDirName && entry.name === excludeDirName)
|
|
476
|
+
continue;
|
|
477
|
+
files.push(...getAllFiles(fullPath, extensions, recursive, currentDepth + 1, maxDepth, excludeDirName));
|
|
445
478
|
}
|
|
446
479
|
else if (entry.isFile()) {
|
|
447
480
|
const ext = path.extname(entry.name).slice(1).toLowerCase();
|
|
@@ -462,7 +495,7 @@ function getOutputPath(inputPath, targetFormat, allInputFiles) {
|
|
|
462
495
|
const basename = path.basename(inputPath, ext);
|
|
463
496
|
const targetExt = targetFormat;
|
|
464
497
|
let coreName = basename;
|
|
465
|
-
const outputDir = path.join(dir,
|
|
498
|
+
const outputDir = path.join(dir, targetFormat);
|
|
466
499
|
// 检查输入目录中是否有同名(不含扩展名)但不同后缀的文件
|
|
467
500
|
// 如 photo.png 和 photo.jpg 会被判定为同名冲突
|
|
468
501
|
const hasNameConflict = allInputFiles.some(f => {
|
|
@@ -471,7 +504,7 @@ function getOutputPath(inputPath, targetFormat, allInputFiles) {
|
|
|
471
504
|
const fDir = path.dirname(f);
|
|
472
505
|
const fExt = path.extname(f);
|
|
473
506
|
const fBasename = path.basename(f, fExt);
|
|
474
|
-
return fDir === dir && fBasename === basename && fExt.toLowerCase() !== ext.toLowerCase();
|
|
507
|
+
return fDir === dir && fBasename.toLowerCase() === basename.toLowerCase() && fExt.toLowerCase() !== ext.toLowerCase();
|
|
475
508
|
});
|
|
476
509
|
if (hasNameConflict) {
|
|
477
510
|
// 找所有同basename的文件的序号
|
|
@@ -481,7 +514,7 @@ function getOutputPath(inputPath, targetFormat, allInputFiles) {
|
|
|
481
514
|
const fDir = path.dirname(f);
|
|
482
515
|
const fExt = path.extname(f);
|
|
483
516
|
const fBasename = path.basename(f, fExt);
|
|
484
|
-
return fDir === dir && fBasename === basename;
|
|
517
|
+
return fDir === dir && fBasename.toLowerCase() === basename.toLowerCase();
|
|
485
518
|
});
|
|
486
519
|
// 当前文件在所有同名文件中的索引(从1开始)
|
|
487
520
|
const sortedMatches = [...allBasenameMatches, inputPath].sort();
|
|
@@ -493,22 +526,23 @@ function getOutputPath(inputPath, targetFormat, allInputFiles) {
|
|
|
493
526
|
return path.join(outputDir, filename);
|
|
494
527
|
}
|
|
495
528
|
async function convertImage(inputPath, targetFormat, allInputFiles) {
|
|
529
|
+
const outputPath = getOutputPath(inputPath, targetFormat, allInputFiles);
|
|
530
|
+
const srcExt = path.extname(inputPath).slice(1).toLowerCase();
|
|
531
|
+
const fmt = targetFormat.toLowerCase();
|
|
532
|
+
// 先验证格式,避免在无效路径上创建目录
|
|
533
|
+
if (!SUPPORTED_OUTPUT_FORMATS.includes(fmt)) {
|
|
534
|
+
return { success: false, outputPath, error: `不支持的目标格式: ${targetFormat},支持: ${SUPPORTED_OUTPUT_FORMATS.join(', ')}` };
|
|
535
|
+
}
|
|
496
536
|
try {
|
|
497
|
-
const outputPath = getOutputPath(inputPath, targetFormat, allInputFiles);
|
|
498
537
|
const outputDir = path.dirname(outputPath);
|
|
499
538
|
if (!fs.existsSync(outputDir)) {
|
|
500
539
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
501
540
|
}
|
|
502
|
-
const srcExt = path.extname(inputPath).slice(1).toLowerCase();
|
|
503
|
-
const fmt = targetFormat.toLowerCase();
|
|
504
541
|
// 同格式直接复制,避免重新编码导致质量损失
|
|
505
542
|
if (srcExt === fmt || (srcExt === 'jpeg' && fmt === 'jpg') || (srcExt === 'jpg' && fmt === 'jpeg') || (srcExt === 'tif' && fmt === 'tiff') || (srcExt === 'tiff' && fmt === 'tif')) {
|
|
506
543
|
fs.copyFileSync(inputPath, outputPath);
|
|
507
544
|
return { success: true, outputPath };
|
|
508
545
|
}
|
|
509
|
-
if (!SUPPORTED_OUTPUT_FORMATS.includes(fmt)) {
|
|
510
|
-
return { success: false, outputPath: inputPath, error: `不支持的目标格式: ${targetFormat},支持: ${SUPPORTED_OUTPUT_FORMATS.join(', ')}` };
|
|
511
|
-
}
|
|
512
546
|
const image = (0, sharp_1.default)(inputPath);
|
|
513
547
|
switch (fmt) {
|
|
514
548
|
case 'webp':
|
|
@@ -534,7 +568,7 @@ async function convertImage(inputPath, targetFormat, allInputFiles) {
|
|
|
534
568
|
catch (err) {
|
|
535
569
|
return {
|
|
536
570
|
success: false,
|
|
537
|
-
outputPath
|
|
571
|
+
outputPath,
|
|
538
572
|
error: err instanceof Error ? err.message : '未知错误'
|
|
539
573
|
};
|
|
540
574
|
}
|
|
@@ -565,16 +599,18 @@ async function main() {
|
|
|
565
599
|
console.log('----------------------------------------\n');
|
|
566
600
|
let totalSuccess = 0;
|
|
567
601
|
let totalFail = 0;
|
|
602
|
+
const failures = [];
|
|
568
603
|
for (const filePath of files) {
|
|
569
604
|
const ext = path.extname(filePath).slice(1).toLowerCase();
|
|
570
605
|
if (!SUPPORTED_INPUT_EXTENSIONS.includes(ext)) {
|
|
571
606
|
console.log(` ⚠️ 跳过(不支持格式): ${filePath}`);
|
|
572
607
|
totalFail++;
|
|
608
|
+
failures.push(filePath);
|
|
573
609
|
continue;
|
|
574
610
|
}
|
|
575
611
|
console.log(` 📄 文件: ${filePath}`);
|
|
576
612
|
process.stdout.write(` 处理中: ${path.basename(filePath)} ... `);
|
|
577
|
-
const result = await convertImage(filePath, options.targetFormat,
|
|
613
|
+
const result = await convertImage(filePath, options.targetFormat, files);
|
|
578
614
|
if (result.success) {
|
|
579
615
|
console.log(`✅ -> ${path.relative(path.dirname(filePath), result.outputPath)}`);
|
|
580
616
|
totalSuccess++;
|
|
@@ -582,6 +618,13 @@ async function main() {
|
|
|
582
618
|
else {
|
|
583
619
|
console.log(`❌ 失败 (${result.error})`);
|
|
584
620
|
totalFail++;
|
|
621
|
+
failures.push(filePath);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
if (failures.length > 0) {
|
|
625
|
+
console.log('\n❌ 失败的文件:');
|
|
626
|
+
for (const f of failures) {
|
|
627
|
+
console.log(` - ${f}`);
|
|
585
628
|
}
|
|
586
629
|
}
|
|
587
630
|
return { success: totalSuccess, fail: totalFail };
|
|
@@ -593,11 +636,13 @@ async function main() {
|
|
|
593
636
|
console.log('----------------------------------------\n');
|
|
594
637
|
let totalSuccess = 0;
|
|
595
638
|
let totalFail = 0;
|
|
639
|
+
const failures = [];
|
|
596
640
|
for (const inputPath of dirs) {
|
|
597
641
|
const stat = fs.existsSync(inputPath) ? fs.statSync(inputPath) : null;
|
|
598
642
|
if (!stat) {
|
|
599
643
|
console.log(` ⚠️ 跳过(不存在): ${inputPath}`);
|
|
600
644
|
totalFail++;
|
|
645
|
+
failures.push(inputPath);
|
|
601
646
|
continue;
|
|
602
647
|
}
|
|
603
648
|
if (stat.isFile()) {
|
|
@@ -605,11 +650,12 @@ async function main() {
|
|
|
605
650
|
if (!SUPPORTED_INPUT_EXTENSIONS.includes(ext)) {
|
|
606
651
|
console.log(` ⚠️ 跳过(不支持格式): ${inputPath}`);
|
|
607
652
|
totalFail++;
|
|
653
|
+
failures.push(inputPath);
|
|
608
654
|
continue;
|
|
609
655
|
}
|
|
610
656
|
console.log(` 📄 文件: ${inputPath}`);
|
|
611
657
|
process.stdout.write(` 处理中: ${path.basename(inputPath)} ... `);
|
|
612
|
-
const result = await convertImage(inputPath, options.targetFormat,
|
|
658
|
+
const result = await convertImage(inputPath, options.targetFormat, dirs);
|
|
613
659
|
if (result.success) {
|
|
614
660
|
console.log(`✅ -> ${path.relative(path.dirname(inputPath), result.outputPath)}`);
|
|
615
661
|
totalSuccess++;
|
|
@@ -617,10 +663,11 @@ async function main() {
|
|
|
617
663
|
else {
|
|
618
664
|
console.log(`❌ 失败 (${result.error})`);
|
|
619
665
|
totalFail++;
|
|
666
|
+
failures.push(inputPath);
|
|
620
667
|
}
|
|
621
668
|
}
|
|
622
669
|
else {
|
|
623
|
-
const files = getAllFiles(inputPath, options.extensions, options.recursive, 0, options.maxDepth);
|
|
670
|
+
const files = getAllFiles(inputPath, options.extensions, options.recursive, 0, options.maxDepth, options.targetFormat);
|
|
624
671
|
console.log(` 📁 目录: ${inputPath} (${files.length} 个文件)`);
|
|
625
672
|
if (files.length === 0) {
|
|
626
673
|
console.log(' ✅ 没有找到图片文件');
|
|
@@ -636,10 +683,17 @@ async function main() {
|
|
|
636
683
|
else {
|
|
637
684
|
console.log(`❌ (${result.error})`);
|
|
638
685
|
totalFail++;
|
|
686
|
+
failures.push(file);
|
|
639
687
|
}
|
|
640
688
|
}
|
|
641
689
|
}
|
|
642
690
|
}
|
|
691
|
+
if (failures.length > 0) {
|
|
692
|
+
console.log('\n❌ 失败的文件:');
|
|
693
|
+
for (const f of failures) {
|
|
694
|
+
console.log(` - ${f}`);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
643
697
|
return { success: totalSuccess, fail: totalFail };
|
|
644
698
|
}
|
|
645
699
|
// ─── 混合模式:同时有文件和目录 ───
|
|
@@ -673,7 +727,7 @@ async function main() {
|
|
|
673
727
|
console.log(`📄 后缀: ${options.extensions.join(', ')}`);
|
|
674
728
|
console.log(`🎯 目标格式: ${options.targetFormat}`);
|
|
675
729
|
console.log('\n----------------------------------------\n');
|
|
676
|
-
const files = getAllFiles(options.directory, options.extensions, options.recursive, 0, options.maxDepth);
|
|
730
|
+
const files = getAllFiles(options.directory, options.extensions, options.recursive, 0, options.maxDepth, options.targetFormat);
|
|
677
731
|
if (files.length === 0) {
|
|
678
732
|
console.log('✅ 没有找到需要转换的图片文件。');
|
|
679
733
|
return;
|