@mison/wecom-cleaner 1.1.0 → 1.2.0

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/src/config.js CHANGED
@@ -139,6 +139,8 @@ function normalizeSpaceGovernance(input, fallback) {
139
139
 
140
140
  export function parseCliArgs(argv) {
141
141
  const parsed = {
142
+ help: false,
143
+ version: false,
142
144
  rootDir: null,
143
145
  externalStorageRoots: null,
144
146
  externalStorageAutoDetect: null,
@@ -197,6 +199,14 @@ export function parseCliArgs(argv) {
197
199
 
198
200
  for (let i = 0; i < argv.length; i += 1) {
199
201
  const token = argv[i];
202
+ if (token === '-h' || token === '--help') {
203
+ parsed.help = true;
204
+ continue;
205
+ }
206
+ if (token === '-v' || token === '--version') {
207
+ parsed.version = true;
208
+ continue;
209
+ }
200
210
  if (ACTION_FLAG_MAP.has(token)) {
201
211
  parsed.action = ACTION_FLAG_MAP.get(token);
202
212
  parsed.actionFlagCount += 1;
@@ -218,9 +218,17 @@ export async function maintainRecycleBin({ indexPath, recycleRoot, policy, dryRu
218
218
  candidateCount: selected.candidates.length,
219
219
  selectedByAge: selected.candidates.filter((item) => item.selectedBy === 'age').length,
220
220
  selectedBySize: selected.candidates.filter((item) => item.selectedBy === 'size').length,
221
+ selectedCandidates: selected.candidates.map((item) => ({
222
+ batchId: item.batchId,
223
+ firstTime: item.firstTime,
224
+ ageDays: ageDays(item.firstTime, now),
225
+ totalBytes: Number(item.totalBytes || 0),
226
+ selectedBy: item.selectedBy || 'unknown',
227
+ })),
221
228
  deletedBatches: 0,
222
229
  deletedBytes: 0,
223
230
  failBatches: 0,
231
+ operations: [],
224
232
  errors: [],
225
233
  };
226
234
 
@@ -277,6 +285,13 @@ export async function maintainRecycleBin({ indexPath, recycleRoot, policy, dryRu
277
285
  const resolvedBatchRoot = resolveBatchRootFromEntries(recycleRoot, batch);
278
286
  if (!resolvedBatchRoot.ok) {
279
287
  summary.failBatches += 1;
288
+ summary.operations.push({
289
+ batchId: batch.batchId,
290
+ status: 'failed_invalid_path',
291
+ selectedBy: batch.selectedBy || 'unknown',
292
+ totalBytes: Number(batch.totalBytes || 0),
293
+ invalidReason: resolvedBatchRoot.invalidReason,
294
+ });
280
295
  summary.errors.push({
281
296
  batchId: batch.batchId,
282
297
  message: `批次路径校验失败: ${resolvedBatchRoot.invalidReason}`,
@@ -289,6 +304,13 @@ export async function maintainRecycleBin({ indexPath, recycleRoot, policy, dryRu
289
304
  if (dryRun) {
290
305
  summary.deletedBatches += 1;
291
306
  summary.deletedBytes += Number(batch.totalBytes || 0);
307
+ summary.operations.push({
308
+ batchId: batch.batchId,
309
+ status: 'dry_run',
310
+ selectedBy: batch.selectedBy || 'unknown',
311
+ totalBytes: Number(batch.totalBytes || 0),
312
+ batchRoot: resolvedBatchRoot.batchRoot,
313
+ });
292
314
  continue;
293
315
  }
294
316
 
@@ -296,9 +318,24 @@ export async function maintainRecycleBin({ indexPath, recycleRoot, policy, dryRu
296
318
  await fs.rm(resolvedBatchRoot.batchRoot, { recursive: true, force: true });
297
319
  summary.deletedBatches += 1;
298
320
  summary.deletedBytes += Number(batch.totalBytes || 0);
321
+ summary.operations.push({
322
+ batchId: batch.batchId,
323
+ status: 'deleted',
324
+ selectedBy: batch.selectedBy || 'unknown',
325
+ totalBytes: Number(batch.totalBytes || 0),
326
+ batchRoot: resolvedBatchRoot.batchRoot,
327
+ });
299
328
  } catch (error) {
300
329
  summary.failBatches += 1;
301
330
  const message = error instanceof Error ? error.message : String(error);
331
+ summary.operations.push({
332
+ batchId: batch.batchId,
333
+ status: 'failed',
334
+ selectedBy: batch.selectedBy || 'unknown',
335
+ totalBytes: Number(batch.totalBytes || 0),
336
+ batchRoot: resolvedBatchRoot.batchRoot,
337
+ errorType: classifyErrorType(message),
338
+ });
302
339
  summary.errors.push({
303
340
  batchId: batch.batchId,
304
341
  message,
package/src/restore.js CHANGED
@@ -277,6 +277,172 @@ function buildRestoreAuditMeta(entry = {}) {
277
277
  };
278
278
  }
279
279
 
280
+ function createRestoreBreakdownRow(seed = {}) {
281
+ return {
282
+ ...seed,
283
+ totalCount: 0,
284
+ totalBytes: 0,
285
+ successCount: 0,
286
+ successBytes: 0,
287
+ skippedCount: 0,
288
+ skippedBytes: 0,
289
+ failedCount: 0,
290
+ failedBytes: 0,
291
+ dryRunCount: 0,
292
+ dryRunBytes: 0,
293
+ };
294
+ }
295
+
296
+ function applyRestoreBreakdownStatus(row, statusKey, sizeBytes) {
297
+ const bytes = Number(sizeBytes || 0);
298
+ row.totalCount += 1;
299
+ row.totalBytes += bytes;
300
+ if (statusKey === 'success') {
301
+ row.successCount += 1;
302
+ row.successBytes += bytes;
303
+ return;
304
+ }
305
+ if (statusKey === 'skipped') {
306
+ row.skippedCount += 1;
307
+ row.skippedBytes += bytes;
308
+ return;
309
+ }
310
+ if (statusKey === 'failed') {
311
+ row.failedCount += 1;
312
+ row.failedBytes += bytes;
313
+ return;
314
+ }
315
+ row.dryRunCount += 1;
316
+ row.dryRunBytes += bytes;
317
+ }
318
+
319
+ function pushRestoreTopEntry(samples, sample, limit = 20) {
320
+ samples.push(sample);
321
+ samples.sort((a, b) => Number(b.sizeBytes || 0) - Number(a.sizeBytes || 0));
322
+ if (samples.length > limit) {
323
+ samples.length = limit;
324
+ }
325
+ }
326
+
327
+ function createRestoreBreakdownTracker(topEntryLimit = 20) {
328
+ return {
329
+ byScope: new Map(),
330
+ byCategory: new Map(),
331
+ byMonth: new Map(),
332
+ byRoot: new Map(),
333
+ status: {
334
+ success: { count: 0, bytes: 0 },
335
+ skipped: { count: 0, bytes: 0 },
336
+ failed: { count: 0, bytes: 0 },
337
+ dryRun: { count: 0, bytes: 0 },
338
+ },
339
+ topEntries: [],
340
+ topEntryLimit,
341
+ };
342
+ }
343
+
344
+ function updateRestoreBreakdown(tracker, entry, statusKey, statusLabel, restoredPath = null) {
345
+ const bytes = Number(entry?.sizeBytes || 0);
346
+ if (!tracker.status[statusKey]) {
347
+ tracker.status[statusKey] = { count: 0, bytes: 0 };
348
+ }
349
+ tracker.status[statusKey].count += 1;
350
+ tracker.status[statusKey].bytes += bytes;
351
+
352
+ const scopeKey = String(entry?.scope || 'cleanup_monthly');
353
+ if (!tracker.byScope.has(scopeKey)) {
354
+ tracker.byScope.set(scopeKey, createRestoreBreakdownRow({ scope: scopeKey }));
355
+ }
356
+ applyRestoreBreakdownStatus(tracker.byScope.get(scopeKey), statusKey, bytes);
357
+
358
+ const categoryKey = String(entry?.categoryKey || entry?.targetKey || 'unknown');
359
+ if (!tracker.byCategory.has(categoryKey)) {
360
+ tracker.byCategory.set(
361
+ categoryKey,
362
+ createRestoreBreakdownRow({
363
+ categoryKey,
364
+ categoryLabel: entry?.categoryLabel || categoryKey,
365
+ })
366
+ );
367
+ }
368
+ applyRestoreBreakdownStatus(tracker.byCategory.get(categoryKey), statusKey, bytes);
369
+
370
+ const monthKey = String(entry?.monthKey || '非月份目录');
371
+ if (!tracker.byMonth.has(monthKey)) {
372
+ tracker.byMonth.set(monthKey, createRestoreBreakdownRow({ monthKey }));
373
+ }
374
+ applyRestoreBreakdownStatus(tracker.byMonth.get(monthKey), statusKey, bytes);
375
+
376
+ const sourcePath = typeof entry?.sourcePath === 'string' ? entry.sourcePath : '';
377
+ const rootPath = sourcePath ? path.dirname(path.resolve(sourcePath)) : null;
378
+ const rootKey = rootPath || '(unknown)';
379
+ if (!tracker.byRoot.has(rootKey)) {
380
+ tracker.byRoot.set(
381
+ rootKey,
382
+ createRestoreBreakdownRow({
383
+ rootPath: rootPath || null,
384
+ })
385
+ );
386
+ }
387
+ applyRestoreBreakdownStatus(tracker.byRoot.get(rootKey), statusKey, bytes);
388
+
389
+ pushRestoreTopEntry(
390
+ tracker.topEntries,
391
+ {
392
+ sourcePath: entry?.sourcePath || null,
393
+ recyclePath: entry?.recyclePath || null,
394
+ restoredPath: restoredPath || null,
395
+ sizeBytes: bytes,
396
+ status: statusLabel,
397
+ scope: scopeKey,
398
+ categoryKey,
399
+ categoryLabel: entry?.categoryLabel || categoryKey,
400
+ monthKey: entry?.monthKey || null,
401
+ accountShortId: entry?.accountShortId || null,
402
+ },
403
+ tracker.topEntryLimit
404
+ );
405
+ }
406
+
407
+ function sortRestoreBreakdownRowsByBytes(rows = []) {
408
+ return [...rows].sort((a, b) => {
409
+ const bytesDiff = Number(b.totalBytes || 0) - Number(a.totalBytes || 0);
410
+ if (bytesDiff !== 0) {
411
+ return bytesDiff;
412
+ }
413
+ return Number(b.totalCount || 0) - Number(a.totalCount || 0);
414
+ });
415
+ }
416
+
417
+ function sortRestoreMonthRows(rows = []) {
418
+ const nonMonthKey = '非月份目录';
419
+ return [...rows].sort((a, b) => {
420
+ const aMonth = String(a.monthKey || nonMonthKey);
421
+ const bMonth = String(b.monthKey || nonMonthKey);
422
+ if (aMonth === nonMonthKey && bMonth !== nonMonthKey) {
423
+ return 1;
424
+ }
425
+ if (aMonth !== nonMonthKey && bMonth === nonMonthKey) {
426
+ return -1;
427
+ }
428
+ if (aMonth === bMonth) {
429
+ return Number(b.totalBytes || 0) - Number(a.totalBytes || 0);
430
+ }
431
+ return aMonth.localeCompare(bMonth);
432
+ });
433
+ }
434
+
435
+ function finalizeRestoreBreakdown(tracker) {
436
+ return {
437
+ byStatus: tracker.status,
438
+ byScope: sortRestoreBreakdownRowsByBytes([...tracker.byScope.values()]),
439
+ byCategory: sortRestoreBreakdownRowsByBytes([...tracker.byCategory.values()]),
440
+ byMonth: sortRestoreMonthRows([...tracker.byMonth.values()]),
441
+ byRoot: sortRestoreBreakdownRowsByBytes([...tracker.byRoot.values()]),
442
+ topEntries: [...tracker.topEntries],
443
+ };
444
+ }
445
+
280
446
  export async function listRestorableBatches(indexPath, options = {}) {
281
447
  const recycleRoot = typeof options.recycleRoot === 'string' ? options.recycleRoot : null;
282
448
  const restoredSet = new Set();
@@ -348,6 +514,7 @@ export async function restoreBatch({
348
514
  restoredBytes: 0,
349
515
  errors: [],
350
516
  };
517
+ const breakdownTracker = createRestoreBreakdownTracker();
351
518
 
352
519
  const validationState = await buildRestoreValidationState({
353
520
  profileRoot,
@@ -379,6 +546,7 @@ export async function restoreBatch({
379
546
 
380
547
  if (invalidPathReason) {
381
548
  summary.skipCount += 1;
549
+ updateRestoreBreakdown(breakdownTracker, entry, 'skipped', 'skipped_invalid_path');
382
550
  await appendJsonLine(indexPath, {
383
551
  action: 'restore',
384
552
  time: Date.now(),
@@ -401,6 +569,7 @@ export async function restoreBatch({
401
569
 
402
570
  if (!(await pathExists(recyclePath))) {
403
571
  summary.skipCount += 1;
572
+ updateRestoreBreakdown(breakdownTracker, entry, 'skipped', 'skipped_missing_recycle');
404
573
  await appendJsonLine(indexPath, {
405
574
  action: 'restore',
406
575
  time: Date.now(),
@@ -447,6 +616,7 @@ export async function restoreBatch({
447
616
 
448
617
  if (strategy === 'skip') {
449
618
  summary.skipCount += 1;
619
+ updateRestoreBreakdown(breakdownTracker, entry, 'skipped', 'skipped_conflict');
450
620
  await appendJsonLine(indexPath, {
451
621
  action: 'restore',
452
622
  time: Date.now(),
@@ -474,6 +644,7 @@ export async function restoreBatch({
474
644
  if (dryRun) {
475
645
  summary.successCount += 1;
476
646
  summary.restoredBytes += Number(entry.sizeBytes || 0);
647
+ updateRestoreBreakdown(breakdownTracker, entry, 'dryRun', 'dry_run', targetPath);
477
648
 
478
649
  await appendJsonLine(indexPath, {
479
650
  action: 'restore',
@@ -504,6 +675,7 @@ export async function restoreBatch({
504
675
  await movePath(recyclePath, targetPath);
505
676
  summary.successCount += 1;
506
677
  summary.restoredBytes += Number(entry.sizeBytes || 0);
678
+ updateRestoreBreakdown(breakdownTracker, entry, 'success', 'success', targetPath);
507
679
 
508
680
  await appendJsonLine(indexPath, {
509
681
  action: 'restore',
@@ -524,6 +696,7 @@ export async function restoreBatch({
524
696
  });
525
697
  } catch (error) {
526
698
  summary.failCount += 1;
699
+ updateRestoreBreakdown(breakdownTracker, entry, 'failed', 'failed', targetPath);
527
700
  summary.errors.push({
528
701
  recyclePath,
529
702
  sourcePath: originalPath,
@@ -551,5 +724,6 @@ export async function restoreBatch({
551
724
  }
552
725
  }
553
726
 
727
+ summary.breakdown = finalizeRestoreBreakdown(breakdownTracker);
554
728
  return summary;
555
729
  }