tanmi-dock 1.0.3 → 1.0.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.
Files changed (114) hide show
  1. package/README.md +18 -0
  2. package/dist/commands/calibrate.d.ts +10 -0
  3. package/dist/commands/calibrate.d.ts.map +1 -0
  4. package/dist/commands/calibrate.js +158 -0
  5. package/dist/commands/calibrate.js.map +1 -0
  6. package/dist/commands/check.d.ts +9 -1
  7. package/dist/commands/check.d.ts.map +1 -1
  8. package/dist/commands/check.js +261 -94
  9. package/dist/commands/check.js.map +1 -1
  10. package/dist/commands/clean.d.ts +2 -6
  11. package/dist/commands/clean.d.ts.map +1 -1
  12. package/dist/commands/clean.js +224 -209
  13. package/dist/commands/clean.js.map +1 -1
  14. package/dist/commands/dashboard.d.ts.map +1 -1
  15. package/dist/commands/dashboard.js +56 -28
  16. package/dist/commands/dashboard.js.map +1 -1
  17. package/dist/commands/link.d.ts +6 -6
  18. package/dist/commands/link.d.ts.map +1 -1
  19. package/dist/commands/link.js +417 -196
  20. package/dist/commands/link.js.map +1 -1
  21. package/dist/commands/migrate.d.ts +11 -0
  22. package/dist/commands/migrate.d.ts.map +1 -1
  23. package/dist/commands/migrate.js +277 -57
  24. package/dist/commands/migrate.js.map +1 -1
  25. package/dist/commands/reset.d.ts.map +1 -1
  26. package/dist/commands/reset.js +19 -24
  27. package/dist/commands/reset.js.map +1 -1
  28. package/dist/commands/status.d.ts +4 -0
  29. package/dist/commands/status.d.ts.map +1 -1
  30. package/dist/commands/status.js +37 -20
  31. package/dist/commands/status.js.map +1 -1
  32. package/dist/commands/unavailable.js.map +1 -1
  33. package/dist/commands/unlink.d.ts.map +1 -1
  34. package/dist/commands/unlink.js +43 -39
  35. package/dist/commands/unlink.js.map +1 -1
  36. package/dist/core/config.d.ts +3 -1
  37. package/dist/core/config.d.ts.map +1 -1
  38. package/dist/core/config.js +6 -3
  39. package/dist/core/config.js.map +1 -1
  40. package/dist/core/parser.js +1 -1
  41. package/dist/core/parser.js.map +1 -1
  42. package/dist/core/registry.d.ts +11 -29
  43. package/dist/core/registry.d.ts.map +1 -1
  44. package/dist/core/registry.js +46 -113
  45. package/dist/core/registry.js.map +1 -1
  46. package/dist/core/space/calibration-executor.d.ts +16 -0
  47. package/dist/core/space/calibration-executor.d.ts.map +1 -0
  48. package/dist/core/space/calibration-executor.js +142 -0
  49. package/dist/core/space/calibration-executor.js.map +1 -0
  50. package/dist/core/space/calibration-planner.d.ts +5 -0
  51. package/dist/core/space/calibration-planner.d.ts.map +1 -0
  52. package/dist/core/space/calibration-planner.js +157 -0
  53. package/dist/core/space/calibration-planner.js.map +1 -0
  54. package/dist/core/space/cleanup-executor.d.ts +15 -0
  55. package/dist/core/space/cleanup-executor.d.ts.map +1 -0
  56. package/dist/core/space/cleanup-executor.js +188 -0
  57. package/dist/core/space/cleanup-executor.js.map +1 -0
  58. package/dist/core/space/cleanup-journal.d.ts +43 -0
  59. package/dist/core/space/cleanup-journal.d.ts.map +1 -0
  60. package/dist/core/space/cleanup-journal.js +96 -0
  61. package/dist/core/space/cleanup-journal.js.map +1 -0
  62. package/dist/core/space/cleanup-planner.d.ts +14 -0
  63. package/dist/core/space/cleanup-planner.d.ts.map +1 -0
  64. package/dist/core/space/cleanup-planner.js +166 -0
  65. package/dist/core/space/cleanup-planner.js.map +1 -0
  66. package/dist/core/space/cleanup-selection.d.ts +10 -0
  67. package/dist/core/space/cleanup-selection.d.ts.map +1 -0
  68. package/dist/core/space/cleanup-selection.js +84 -0
  69. package/dist/core/space/cleanup-selection.js.map +1 -0
  70. package/dist/core/space/estimator.d.ts +11 -0
  71. package/dist/core/space/estimator.d.ts.map +1 -0
  72. package/dist/core/space/estimator.js +142 -0
  73. package/dist/core/space/estimator.js.map +1 -0
  74. package/dist/core/space/inventory.d.ts +15 -0
  75. package/dist/core/space/inventory.d.ts.map +1 -0
  76. package/dist/core/space/inventory.js +587 -0
  77. package/dist/core/space/inventory.js.map +1 -0
  78. package/dist/core/space/meter.d.ts +5 -0
  79. package/dist/core/space/meter.d.ts.map +1 -0
  80. package/dist/core/space/meter.js +87 -0
  81. package/dist/core/space/meter.js.map +1 -0
  82. package/dist/core/space/migration-journal.d.ts +31 -0
  83. package/dist/core/space/migration-journal.d.ts.map +1 -0
  84. package/dist/core/space/migration-journal.js +121 -0
  85. package/dist/core/space/migration-journal.js.map +1 -0
  86. package/dist/core/space/service.d.ts +26 -0
  87. package/dist/core/space/service.d.ts.map +1 -0
  88. package/dist/core/space/service.js +54 -0
  89. package/dist/core/space/service.js.map +1 -0
  90. package/dist/core/space/types.d.ts +241 -0
  91. package/dist/core/space/types.d.ts.map +1 -0
  92. package/dist/core/space/types.js +13 -0
  93. package/dist/core/space/types.js.map +1 -0
  94. package/dist/core/store.d.ts.map +1 -1
  95. package/dist/core/store.js +12 -6
  96. package/dist/core/store.js.map +1 -1
  97. package/dist/index.js +2 -0
  98. package/dist/index.js.map +1 -1
  99. package/dist/types/index.d.ts +2 -0
  100. package/dist/types/index.d.ts.map +1 -1
  101. package/dist/utils/atomic-file.d.ts +6 -0
  102. package/dist/utils/atomic-file.d.ts.map +1 -0
  103. package/dist/utils/atomic-file.js +37 -0
  104. package/dist/utils/atomic-file.js.map +1 -0
  105. package/dist/utils/fs-utils.d.ts +18 -1
  106. package/dist/utils/fs-utils.d.ts.map +1 -1
  107. package/dist/utils/fs-utils.js +150 -2
  108. package/dist/utils/fs-utils.js.map +1 -1
  109. package/dist/utils/logger.d.ts +1 -1
  110. package/dist/utils/logger.js +2 -2
  111. package/dist/utils/prompt.d.ts.map +1 -1
  112. package/dist/utils/prompt.js +4 -4
  113. package/dist/utils/prompt.js.map +1 -1
  114. package/package.json +1 -1
@@ -9,7 +9,11 @@ import { getDiskInfo, formatSize } from '../utils/disk.js';
9
9
  import * as config from '../core/config.js';
10
10
  import { getRegistry } from '../core/registry.js';
11
11
  import * as store from '../core/store.js';
12
- import { KNOWN_PLATFORM_VALUES } from '../core/platform.js';
12
+ import { KNOWN_PLATFORM_VALUES, SHARED_PLATFORM } from '../core/platform.js';
13
+ import { SpaceService } from '../core/space/service.js';
14
+ import { CleanupJournal } from '../core/space/cleanup-journal.js';
15
+ import { MigrationJournal } from '../core/space/migration-journal.js';
16
+ import { withGlobalLock } from '../utils/global-lock.js';
13
17
  import { info, warn, success, error, hint, blank, title, separator, colorize, debug, } from '../utils/logger.js';
14
18
  import { selectWithCancel, checkboxWithCancel, confirmWithCancel, PROMPT_CANCELLED, } from '../utils/prompt.js';
15
19
  // ============ 命令创建 ============
@@ -27,7 +31,17 @@ export function createCheckCommand() {
27
31
  }
28
32
  // ============ 主流程 ============
29
33
  async function runCheck(options) {
30
- const result = await collectAllIssues({ integrity: options.integrity });
34
+ if (options.dryRun) {
35
+ const cfg = await config.load({ migrate: false });
36
+ if (cfg && config.checkConfigVersion(cfg) === 'migrate') {
37
+ error('配置需要迁移;dry-run 不会修改配置,请先运行一次普通命令完成迁移');
38
+ return;
39
+ }
40
+ }
41
+ const result = await collectAllIssues({
42
+ integrity: options.integrity,
43
+ readOnly: options.dryRun,
44
+ });
31
45
  // JSON 输出
32
46
  if (options.json) {
33
47
  console.log(JSON.stringify(result, null, 2));
@@ -35,7 +49,8 @@ async function runCheck(options) {
35
49
  }
36
50
  // 渲染报告
37
51
  renderReport(result);
38
- const hasIssues = result.summary.integrityIssues > 0;
52
+ const hasIntegrityIssues = result.summary.integrityIssues > 0;
53
+ const hasIssues = hasIntegrityIssues || result.summary.spaceIssues > 0;
39
54
  const hasEnvErrors = result.summary.envErrors > 0;
40
55
  // dry-run 模式
41
56
  if (options.dryRun) {
@@ -55,6 +70,10 @@ async function runCheck(options) {
55
70
  if (!hasIssues) {
56
71
  return;
57
72
  }
73
+ if (!hasIntegrityIssues) {
74
+ hint('空间差异需要通过详细检查确认,本次没有自动修改注册记录');
75
+ return;
76
+ }
58
77
  // --fix 模式:直接修复
59
78
  if (options.fix) {
60
79
  await fixAllIssues(result.integrity, { force: options.force });
@@ -65,8 +84,58 @@ async function runCheck(options) {
65
84
  }
66
85
  // ============ 数据收集 ============
67
86
  async function collectAllIssues(options) {
68
- const environment = await checkEnvironment();
87
+ const environment = await checkEnvironment(options?.readOnly);
69
88
  const integrity = await checkIntegrity(options);
89
+ const space = { issues: [] };
90
+ try {
91
+ const registry = getRegistry();
92
+ await registry.load({ migrate: !options?.readOnly });
93
+ const storePath = await store.getStorePath();
94
+ debug(`[space] audit 开始: store=${storePath}, source=td-check`);
95
+ const snapshot = await new SpaceService(registry, storePath).buildSnapshot('audit');
96
+ space.snapshotId = snapshot.id;
97
+ space.totals = snapshot.totals;
98
+ space.issues = snapshot.issues;
99
+ const cleanupLogs = await CleanupJournal.inspectPending();
100
+ for (const journal of cleanupLogs.journals) {
101
+ const data = journal.getData();
102
+ space.issues.push({
103
+ kind: 'recovery-log',
104
+ id: { libName: 'cleanup-journal', commit: data.planId },
105
+ message: `存在待恢复清理日志,组状态: ${data.groups.map((group) => `${group.index}:${group.state}`).join(', ')}`,
106
+ });
107
+ }
108
+ for (const filePath of cleanupLogs.corruptedFiles) {
109
+ space.issues.push({
110
+ kind: 'recovery-log',
111
+ id: { libName: 'cleanup-journal', commit: path.basename(filePath) },
112
+ message: `清理日志损坏,已保留原文件: ${filePath}`,
113
+ });
114
+ }
115
+ try {
116
+ const migrations = await MigrationJournal.listPending();
117
+ for (const migration of migrations) {
118
+ const data = migration.getData();
119
+ space.issues.push({
120
+ kind: 'recovery-log',
121
+ id: { libName: 'migration-journal', commit: data.id },
122
+ message: `存在待恢复迁移: ${data.sourcePath} -> ${data.destinationPath},阶段 ${data.phase}`,
123
+ });
124
+ }
125
+ }
126
+ catch (migrationError) {
127
+ space.issues.push({
128
+ kind: 'recovery-log',
129
+ id: { libName: 'migration-journal', commit: 'corrupted' },
130
+ message: migrationError.message,
131
+ });
132
+ }
133
+ debug(`[space] audit 完成: snapshot=${snapshot.id}, physical=${snapshot.totals.physical.bytes}, registered=${snapshot.totals.registered.bytes}, difference=${snapshot.totals.differenceBytes}, issues=${space.issues.length}, pendingCleanupLogs=${cleanupLogs.journals.length}, corruptedCleanupLogs=${cleanupLogs.corruptedFiles.length}`);
134
+ }
135
+ catch (err) {
136
+ space.error = err.message;
137
+ debug(`[space] audit 失败: error=${space.error}`);
138
+ }
70
139
  // 计算汇总
71
140
  // 错误:检查项失败(ok === false),排除仅作为警告的项
72
141
  // 警告:目前只有 disk 有 warn 字段(空间 < 5GB 但不影响功能)
@@ -83,19 +152,22 @@ async function collectAllIssues(options) {
83
152
  integrity.missingLibraries.length +
84
153
  integrity.staleReferences.length +
85
154
  integrity.corruptedStores.length;
86
- const reclaimableSize = integrity.orphanLibraries.reduce((sum, lib) => sum + lib.size, 0);
155
+ const reclaimableSize = space.totals?.reclaimable.bytes
156
+ ?? integrity.orphanLibraries.reduce((sum, lib) => sum + lib.size, 0);
87
157
  return {
88
158
  environment,
89
159
  integrity,
160
+ space,
90
161
  summary: {
91
162
  envErrors,
92
163
  envWarnings,
93
164
  integrityIssues,
94
165
  reclaimableSize,
166
+ spaceIssues: space.issues.length,
95
167
  },
96
168
  };
97
169
  }
98
- async function checkEnvironment() {
170
+ async function checkEnvironment(readOnly = false) {
99
171
  const codepacEnvironment = await checkCodepacEnvironment();
100
172
  const result = {
101
173
  codepac: { ok: false, message: '', details: codepacEnvironment },
@@ -111,7 +183,7 @@ async function checkEnvironment() {
111
183
  details: codepacEnvironment,
112
184
  };
113
185
  // 2. 配置
114
- const cfg = await config.load();
186
+ const cfg = await config.load({ migrate: !readOnly });
115
187
  result.config = {
116
188
  ok: !!cfg,
117
189
  message: cfg ? '已初始化' : '未初始化,运行 td init',
@@ -179,7 +251,7 @@ async function checkIntegrity(options) {
179
251
  let storePath;
180
252
  try {
181
253
  registry = getRegistry();
182
- await registry.load();
254
+ await registry.load({ migrate: !options?.readOnly });
183
255
  storePath = await store.getStorePath();
184
256
  }
185
257
  catch (err) {
@@ -348,7 +420,7 @@ async function checkIntegrity(options) {
348
420
  if (options?.integrity) {
349
421
  const stores = registry.listStores();
350
422
  for (const entry of stores) {
351
- if (entry.fileCount != null) {
423
+ if (entry.fileCount != null && entry.contentHash) {
352
424
  // 有 fileCount 的做 fullVerify
353
425
  const expectedData = {
354
426
  size: entry.size,
@@ -368,23 +440,17 @@ async function checkIntegrity(options) {
368
440
  }
369
441
  }
370
442
  else {
371
- // 无 fileCount 的做回填(captureIntegrity)
372
- try {
373
- const integrity = await store.captureIntegrity(entry.libName, entry.commit, entry.platform);
374
- const storeKey = registry.getStoreKey(entry.libName, entry.commit, entry.platform);
375
- registry.updateStore(storeKey, {
376
- size: integrity.size,
377
- fileCount: integrity.fileCount,
378
- contentHash: integrity.contentHash,
379
- });
380
- }
381
- catch {
382
- debug(`[check] 回填完整性数据失败: ${entry.libName}:${entry.commit}:${entry.platform}`);
383
- }
443
+ result.corruptedStores.push({
444
+ libName: entry.libName,
445
+ commit: entry.commit,
446
+ platform: entry.platform,
447
+ kind: 'missing_integrity_record',
448
+ reason: 'missing integrity record',
449
+ expected: { size: entry.size },
450
+ actual: { fileCount: 0, size: 0 },
451
+ });
384
452
  }
385
453
  }
386
- // 保存回填的数据
387
- await registry.save();
388
454
  }
389
455
  return result;
390
456
  }
@@ -426,10 +492,28 @@ function renderReport(result) {
426
492
  ? '完整'
427
493
  : `${integ.corruptedStores.length} 个损坏的 Store 条目`, false);
428
494
  blank();
495
+ info('空间一致性');
496
+ separator();
497
+ if (result.space.error) {
498
+ renderCheck('空间审计', false, result.space.error, false);
499
+ }
500
+ else if (result.space.totals) {
501
+ const totals = result.space.totals;
502
+ renderCheck('空间审计', result.space.issues.length === 0, result.space.issues.length === 0
503
+ ? `实际占用 ${formatSize(totals.physical.bytes)},登记容量一致`
504
+ : `${result.space.issues.length} 个差异,实际 ${formatSize(totals.physical.bytes)},登记 ${formatSize(totals.registered.bytes)}`, false);
505
+ for (const issue of result.space.issues.slice(0, 20)) {
506
+ debug(`[space] audit 差异: kind=${issue.kind}, lib=${issue.id.libName}, commit=${issue.id.commit}, platform=${issue.platform ?? 'all'}, registered=${issue.registeredBytes ?? 'unknown'}, measured=${issue.measuredBytes ?? 'unknown'}, message=${issue.message}`);
507
+ }
508
+ if (result.space.issues.length > 20) {
509
+ debug(`[space] audit 差异省略: remaining=${result.space.issues.length - 20}`);
510
+ }
511
+ }
512
+ blank();
429
513
  separator();
430
514
  // 汇总
431
- const { envErrors, envWarnings, integrityIssues, reclaimableSize } = result.summary;
432
- if (envErrors === 0 && envWarnings === 0 && integrityIssues === 0) {
515
+ const { envErrors, envWarnings, integrityIssues, reclaimableSize, spaceIssues } = result.summary;
516
+ if (envErrors === 0 && envWarnings === 0 && integrityIssues === 0 && spaceIssues === 0) {
433
517
  success('系统健康,无问题');
434
518
  }
435
519
  else {
@@ -440,6 +524,8 @@ function renderReport(result) {
440
524
  parts.push(`${envWarnings} 个警告`);
441
525
  if (integrityIssues > 0)
442
526
  parts.push(`${integrityIssues} 个数据问题`);
527
+ if (spaceIssues > 0)
528
+ parts.push(`${spaceIssues} 个空间差异`);
443
529
  if (reclaimableSize > 0)
444
530
  parts.push(`可回收 ${formatSize(reclaimableSize)}`);
445
531
  warn(`发现问题: ${parts.join(', ')}`);
@@ -696,15 +782,68 @@ async function fixAllIssues(issues, options) {
696
782
  separator();
697
783
  info('正在修复...');
698
784
  blank();
785
+ await withGlobalLock(() => applyAllIssueFixes(issues));
786
+ }
787
+ function commitKey(libName, commit) {
788
+ return JSON.stringify([libName, commit]);
789
+ }
790
+ function buildCorruptedSelections(corrupted, includeShared) {
791
+ const grouped = new Map();
792
+ for (const item of corrupted) {
793
+ const key = commitKey(item.libName, item.commit);
794
+ const group = grouped.get(key) ?? {
795
+ id: { libName: item.libName, commit: item.commit },
796
+ platforms: new Set(),
797
+ };
798
+ if (!includeShared)
799
+ group.platforms.add(item.platform);
800
+ grouped.set(key, group);
801
+ }
802
+ return [...grouped.values()].map((group) => ({
803
+ id: group.id,
804
+ platforms: includeShared ? 'all' : [...group.platforms],
805
+ ...(includeShared ? { includeShared: true } : {}),
806
+ }));
807
+ }
808
+ async function executeRepairPlan(spaceService, executor, selections, options) {
809
+ if (selections.length === 0)
810
+ return null;
811
+ const plan = await spaceService.buildCleanupPlan({
812
+ reason: 'repair',
813
+ selections,
814
+ requireUnreferenced: options.requireUnreferenced,
815
+ allowUnknownContent: options.allowUnknownContent,
816
+ });
817
+ for (const warningMessage of plan.warnings)
818
+ warn(`[space] ${warningMessage}`);
819
+ debug(`[check] 调用统一清理执行器: category=${options.category}, plan=${plan.id}, selections=${selections.length}, groups=${plan.groups.length}, expected=${plan.expectedRelease.bytes}`);
820
+ if (plan.groups.length === 0)
821
+ return null;
822
+ const result = await executor.execute(plan);
823
+ debug(`[check] 清理执行返回: category=${options.category}, plan=${plan.id}, status=${result.status}, succeeded=${result.succeeded.length}, failed=${result.failed.length}, released=${result.released.bytes}`);
824
+ return result;
825
+ }
826
+ function resolvedIssueCount(result, issues) {
827
+ if (!result)
828
+ return 0;
829
+ return issues.filter((issue) => result.succeeded.some((group) => group.id.libName === issue.libName
830
+ && group.id.commit === issue.commit
831
+ && (group.platform == null || issue.platform == null || group.platform === issue.platform))).length;
832
+ }
833
+ async function applyAllIssueFixes(issues) {
699
834
  const registry = getRegistry();
700
835
  await registry.load();
701
836
  const storePath = await store.getStorePath();
837
+ const spaceService = new SpaceService(registry, storePath);
838
+ const executor = spaceService.createCleanupExecutor();
839
+ const recovered = await executor.recoverPending();
840
+ debug(`[check] 修复开始: invalidProjects=${issues.invalidProjects.length}, danglingLinks=${issues.danglingLinks.length}, orphanLibraries=${issues.orphanLibraries.length}, staleReferences=${issues.staleReferences.length}, corruptedStores=${issues.corruptedStores.length}, recovered=${recovered}`);
702
841
  let fixed = 0;
703
842
  // 1. 清理无效项目
704
843
  for (const p of issues.invalidProjects) {
705
844
  try {
706
845
  registry.removeProject(p.hash);
707
- success(`[ok] 清理项目: ${p.path}`);
846
+ success(`清理项目: ${p.path}`);
708
847
  fixed++;
709
848
  }
710
849
  catch (err) {
@@ -729,23 +868,28 @@ async function fixAllIssues(issues, options) {
729
868
  error(`[err] 移除链接失败: ${link.path} - ${err.message}`);
730
869
  }
731
870
  }
732
- // 3. 删除孤立库
733
- for (const lib of issues.orphanLibraries) {
734
- try {
735
- await fs.rm(lib.path, { recursive: true, force: true });
736
- // 如果删除后 libName 目录为空,也一并删除
737
- const libDir = path.dirname(lib.path);
738
- const remaining = await fs.readdir(libDir).catch(() => ['placeholder']);
739
- if (remaining.length === 0) {
740
- await fs.rm(libDir, { recursive: true, force: true }).catch(() => { });
741
- }
742
- success(`[ok] 删除孤立库: ${lib.libName}/${lib.commit.slice(0, 7)}`);
743
- fixed++;
871
+ // 3. 删除孤立库。显式修复允许清理该版本中的未登记内容。
872
+ const orphanSelections = [...new Map(issues.orphanLibraries.map((lib) => [
873
+ commitKey(lib.libName, lib.commit),
874
+ { id: { libName: lib.libName, commit: lib.commit }, platforms: 'all' },
875
+ ])).values()];
876
+ try {
877
+ const result = await executeRepairPlan(spaceService, executor, orphanSelections, {
878
+ requireUnreferenced: true,
879
+ allowUnknownContent: true,
880
+ category: 'orphan-library',
881
+ });
882
+ fixed += resolvedIssueCount(result, issues.orphanLibraries);
883
+ for (const group of result?.succeeded ?? []) {
884
+ success(`[ok] 删除孤立库: ${group.id.libName}/${group.id.commit.slice(0, 7)}`);
744
885
  }
745
- catch (err) {
746
- error(`[err] 删除孤立库失败: ${lib.libName}/${lib.commit.slice(0, 7)} - ${err.message}`);
886
+ for (const failure of result?.failed ?? []) {
887
+ error(`[err] 删除孤立库失败: ${failure.id.libName}/${failure.id.commit.slice(0, 7)} - ${failure.message}`);
747
888
  }
748
889
  }
890
+ catch (err) {
891
+ error(`[err] 删除孤立库失败: ${err.message}`);
892
+ }
749
893
  // 4. 移除失效引用
750
894
  for (const ref of issues.staleReferences) {
751
895
  try {
@@ -769,35 +913,51 @@ async function fixAllIssues(issues, options) {
769
913
  error(`[err] 移除引用失败: ${ref.libKey} - ${err.message}`);
770
914
  }
771
915
  }
772
- // 5. 清理损坏的 Store 条目
773
- for (const item of issues.corruptedStores) {
916
+ // 5. 清理损坏的 Store 条目。共享冲突整版本处理,其他问题按平台处理。
917
+ const orphanKeys = new Set(issues.orphanLibraries.map((item) => commitKey(item.libName, item.commit)));
918
+ const sharedCorruptions = issues.corruptedStores.filter((item) => (item.kind === 'shared_platform_conflict' || item.platform === SHARED_PLATFORM)
919
+ && !orphanKeys.has(commitKey(item.libName, item.commit)));
920
+ const sharedCorruptionKeys = new Set(sharedCorruptions.map((item) => commitKey(item.libName, item.commit)));
921
+ const platformCorruptions = issues.corruptedStores.filter((item) => !orphanKeys.has(commitKey(item.libName, item.commit))
922
+ && !sharedCorruptionKeys.has(commitKey(item.libName, item.commit)));
923
+ for (const cleanup of [
924
+ {
925
+ items: issues.corruptedStores.filter((item) => sharedCorruptionKeys.has(commitKey(item.libName, item.commit))),
926
+ selections: buildCorruptedSelections(sharedCorruptions, true),
927
+ allowUnknownContent: false,
928
+ category: 'shared-corruption',
929
+ },
930
+ {
931
+ items: platformCorruptions,
932
+ selections: buildCorruptedSelections(platformCorruptions, false),
933
+ allowUnknownContent: false,
934
+ category: 'corrupted-platform',
935
+ },
936
+ ]) {
774
937
  try {
775
- if (item.kind === 'shared_platform_conflict') {
776
- const storeKeys = registry.getLibraryStoreKeys(item.libName, item.commit);
777
- for (const storeKey of storeKeys) {
778
- registry.removeStore(storeKey);
779
- }
780
- const commitPath = path.join(storePath, item.libName, item.commit);
781
- await fs.rm(commitPath, { recursive: true, force: true });
782
- success(`[ok] 清理损坏 Store: ${item.libName}/${item.commit.slice(0, 7)} (移除整个 commit 缓存)`);
938
+ const result = await executeRepairPlan(spaceService, executor, cleanup.selections, {
939
+ requireUnreferenced: false,
940
+ allowUnknownContent: cleanup.allowUnknownContent,
941
+ category: cleanup.category,
942
+ });
943
+ fixed += resolvedIssueCount(result, cleanup.items);
944
+ for (const group of result?.succeeded ?? []) {
945
+ const suffix = group.platform ? `/${group.platform}` : ' (移除整个 commit 缓存)';
946
+ success(`清理损坏 Store: ${group.id.libName}/${group.id.commit.slice(0, 7)}${suffix}`);
783
947
  }
784
- else {
785
- const storeKey = registry.getStoreKey(item.libName, item.commit, item.platform);
786
- registry.removeStore(storeKey);
787
- const platformPath = path.join(storePath, item.libName, item.commit, item.platform);
788
- await fs.rm(platformPath, { recursive: true, force: true });
789
- success(`[ok] 清理损坏 Store: ${item.libName}/${item.commit.slice(0, 7)}/${item.platform}`);
948
+ for (const failure of result?.failed ?? []) {
949
+ error(`清理损坏 Store 失败: ${failure.id.libName}/${failure.id.commit.slice(0, 7)}/${failure.platform ?? 'all'} - ${failure.message}`);
790
950
  }
791
- fixed++;
792
951
  }
793
952
  catch (err) {
794
- error(`[err] 清理损坏 Store 失败: ${item.libName}/${item.commit.slice(0, 7)}/${item.platform} - ${err.message}`);
953
+ error(`清理损坏 Store 失败: category=${cleanup.category} - ${err.message}`);
795
954
  }
796
955
  }
797
956
  await registry.save();
798
957
  blank();
799
958
  separator();
800
959
  success(`修复完成: ${fixed} 个问题已解决`);
960
+ debug(`[check] 修复完成: fixed=${fixed}`);
801
961
  }
802
962
  // ============ 工具函数 ============
803
963
  async function getDirSizeRecursive(dirPath) {
@@ -902,7 +1062,7 @@ export async function checkStoreIntegrity() {
902
1062
  });
903
1063
  }
904
1064
  }
905
- if (entry.fileCount != null) {
1065
+ if (entry.fileCount != null && entry.contentHash) {
906
1066
  const expectedData = {
907
1067
  size: entry.size,
908
1068
  fileCount: entry.fileCount,
@@ -921,22 +1081,17 @@ export async function checkStoreIntegrity() {
921
1081
  }
922
1082
  }
923
1083
  else {
924
- // 无 fileCount 的做回填
925
- try {
926
- const integrity = await store.captureIntegrity(entry.libName, entry.commit, entry.platform);
927
- const storeKey = registry.getStoreKey(entry.libName, entry.commit, entry.platform);
928
- registry.updateStore(storeKey, {
929
- size: integrity.size,
930
- fileCount: integrity.fileCount,
931
- contentHash: integrity.contentHash,
932
- });
933
- }
934
- catch {
935
- debug(`[check] 回填完整性数据失败: ${entry.libName}:${entry.commit}:${entry.platform}`);
936
- }
1084
+ corrupted.push({
1085
+ libName: entry.libName,
1086
+ commit: entry.commit,
1087
+ platform: entry.platform,
1088
+ kind: 'missing_integrity_record',
1089
+ reason: 'missing integrity record',
1090
+ expected: { size: entry.size },
1091
+ actual: { fileCount: 0, size: 0 },
1092
+ });
937
1093
  }
938
1094
  }
939
- await registry.save();
940
1095
  return corrupted;
941
1096
  }
942
1097
  /**
@@ -948,30 +1103,42 @@ export async function checkStoreIntegrity() {
948
1103
  export async function fixCorruptedStores(corrupted) {
949
1104
  if (corrupted.length === 0)
950
1105
  return;
1106
+ await withGlobalLock(() => applyCorruptedStoreFixes(corrupted));
1107
+ }
1108
+ async function applyCorruptedStoreFixes(corrupted) {
951
1109
  const registry = getRegistry();
952
1110
  await registry.load();
953
1111
  const storePath = await store.getStorePath();
954
- for (const item of corrupted) {
955
- try {
956
- if (item.kind === 'shared_platform_conflict') {
957
- const storeKeys = registry.getLibraryStoreKeys(item.libName, item.commit);
958
- for (const storeKey of storeKeys) {
959
- registry.removeStore(storeKey);
960
- }
961
- const commitPath = path.join(storePath, item.libName, item.commit);
962
- await fs.rm(commitPath, { recursive: true, force: true });
963
- success(`[ok] 清理损坏 Store: ${item.libName}/${item.commit.slice(0, 7)} (移除整个 commit 缓存)`);
964
- }
965
- else {
966
- const storeKey = registry.getStoreKey(item.libName, item.commit, item.platform);
967
- registry.removeStore(storeKey);
968
- const platformPath = path.join(storePath, item.libName, item.commit, item.platform);
969
- await fs.rm(platformPath, { recursive: true, force: true });
970
- success(`[ok] 清理损坏 Store: ${item.libName}/${item.commit.slice(0, 7)}/${item.platform}`);
971
- }
1112
+ const spaceService = new SpaceService(registry, storePath);
1113
+ const executor = spaceService.createCleanupExecutor();
1114
+ const recovered = await executor.recoverPending();
1115
+ debug(`[check] 兼容修复入口开始: corruptedStores=${corrupted.length}, recovered=${recovered}`);
1116
+ const sharedCorruptions = corrupted.filter((item) => item.kind === 'shared_platform_conflict' || item.platform === SHARED_PLATFORM);
1117
+ const sharedCorruptionKeys = new Set(sharedCorruptions.map((item) => commitKey(item.libName, item.commit)));
1118
+ const platformCorruptions = corrupted.filter((item) => !sharedCorruptionKeys.has(commitKey(item.libName, item.commit)));
1119
+ for (const cleanup of [
1120
+ {
1121
+ selections: buildCorruptedSelections(sharedCorruptions, true),
1122
+ allowUnknownContent: false,
1123
+ category: 'shared-corruption',
1124
+ },
1125
+ {
1126
+ selections: buildCorruptedSelections(platformCorruptions, false),
1127
+ allowUnknownContent: false,
1128
+ category: 'corrupted-platform',
1129
+ },
1130
+ ]) {
1131
+ const result = await executeRepairPlan(spaceService, executor, cleanup.selections, {
1132
+ requireUnreferenced: false,
1133
+ allowUnknownContent: cleanup.allowUnknownContent,
1134
+ category: cleanup.category,
1135
+ });
1136
+ for (const group of result?.succeeded ?? []) {
1137
+ const suffix = group.platform ? `/${group.platform}` : ' (移除整个 commit 缓存)';
1138
+ success(`清理损坏 Store: ${group.id.libName}/${group.id.commit.slice(0, 7)}${suffix}`);
972
1139
  }
973
- catch (err) {
974
- error(`[err] 清理损坏 Store 失败: ${item.libName}/${item.commit.slice(0, 7)}/${item.platform} - ${err.message}`);
1140
+ for (const failure of result?.failed ?? []) {
1141
+ error(`清理损坏 Store 失败: ${failure.id.libName}/${failure.id.commit.slice(0, 7)}/${failure.platform ?? 'all'} - ${failure.message}`);
975
1142
  }
976
1143
  }
977
1144
  await registry.save();