tanmi-dock 0.10.0-beta → 1.0.3

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 (45) hide show
  1. package/README.md +8 -5
  2. package/dist/commands/check.d.ts +36 -0
  3. package/dist/commands/check.d.ts.map +1 -1
  4. package/dist/commands/check.js +61 -7
  5. package/dist/commands/check.js.map +1 -1
  6. package/dist/commands/link.d.ts +3 -2
  7. package/dist/commands/link.d.ts.map +1 -1
  8. package/dist/commands/link.js +373 -98
  9. package/dist/commands/link.js.map +1 -1
  10. package/dist/commands/reset.d.ts.map +1 -1
  11. package/dist/commands/reset.js +46 -17
  12. package/dist/commands/reset.js.map +1 -1
  13. package/dist/commands/status.d.ts.map +1 -1
  14. package/dist/commands/status.js +145 -20
  15. package/dist/commands/status.js.map +1 -1
  16. package/dist/commands/unavailable.d.ts.map +1 -1
  17. package/dist/commands/unavailable.js +7 -2
  18. package/dist/commands/unavailable.js.map +1 -1
  19. package/dist/core/codepac.d.ts +44 -0
  20. package/dist/core/codepac.d.ts.map +1 -1
  21. package/dist/core/codepac.js +287 -5
  22. package/dist/core/codepac.js.map +1 -1
  23. package/dist/core/config.d.ts.map +1 -1
  24. package/dist/core/config.js +1 -0
  25. package/dist/core/config.js.map +1 -1
  26. package/dist/core/linker.d.ts +7 -0
  27. package/dist/core/linker.d.ts.map +1 -1
  28. package/dist/core/linker.js +47 -0
  29. package/dist/core/linker.js.map +1 -1
  30. package/dist/core/parser.d.ts +30 -5
  31. package/dist/core/parser.d.ts.map +1 -1
  32. package/dist/core/parser.js +375 -56
  33. package/dist/core/parser.js.map +1 -1
  34. package/dist/core/store.d.ts +2 -0
  35. package/dist/core/store.d.ts.map +1 -1
  36. package/dist/core/store.js +32 -6
  37. package/dist/core/store.js.map +1 -1
  38. package/dist/types/index.d.ts +61 -10
  39. package/dist/types/index.d.ts.map +1 -1
  40. package/dist/types/index.js.map +1 -1
  41. package/dist/utils/git.d.ts +20 -1
  42. package/dist/utils/git.d.ts.map +1 -1
  43. package/dist/utils/git.js +181 -4
  44. package/dist/utils/git.js.map +1 -1
  45. package/package.json +1 -1
@@ -7,13 +7,13 @@ import { Command } from 'commander';
7
7
  import { ensureInitialized } from '../core/guard.js';
8
8
  import * as config from '../core/config.js';
9
9
  import { setLogLevel } from '../utils/logger.js';
10
- import { parseProjectDependencies, getRelativeConfigPath, parseCodepacDep, extractActions, parseActionCommand, extractNestedDependencies, findAllCodepacConfigs, extractDependencies, resolveProjectRootPath, } from '../core/parser.js';
10
+ import { parseProjectDependencies, getRelativeConfigPath, parseCodepacDep, extractActions, buildActionExecutionPlan, extractNestedDependencies, findAllCodepacConfigs, extractDependencies, resolveProjectRootPath, } from '../core/parser.js';
11
11
  import { getRegistry } from '../core/registry.js';
12
12
  import * as store from '../core/store.js';
13
13
  import * as linker from '../core/linker.js';
14
14
  import * as codepac from '../core/codepac.js';
15
15
  import { setProxyConfig } from '../core/codepac.js';
16
- import { getPlatformHelpText, GENERAL_PLATFORM, SHARED_PLATFORM, pathsEqual, isSparseOnlyCommon, KNOWN_PLATFORM_VALUES, getRequestedPlatformTargets, } from '../core/platform.js';
16
+ import { getPlatformHelpText, GENERAL_PLATFORM, SHARED_PLATFORM, pathsEqual, isSparseOnlyCommon, KNOWN_PLATFORM_VALUES, getRequestedPlatformTargets, resolveSparseConfig, getBaseKeyForCodepac, } from '../core/platform.js';
17
17
  import { Transaction } from '../core/transaction.js';
18
18
  import { formatSize, checkDiskSpace } from '../utils/disk.js';
19
19
  import { getDirSize } from '../utils/fs-utils.js';
@@ -37,6 +37,7 @@ export function createLinkCommand() {
37
37
  .option('--no-download', '不自动下载缺失库')
38
38
  .option('--dry-run', '只显示将要执行的操作')
39
39
  .option('--config <configs...>', '指定可选配置文件 (如 codepac-dep-inner.json)')
40
+ .option('--skip-action, --skip_action, -sa <names...>', '按 action.name 跳过指定嵌套 action')
40
41
  .option('--no-submodules', '不检测 git submodule 依赖')
41
42
  .addHelpText('after', `${getPlatformHelpText()}
42
43
 
@@ -48,6 +49,7 @@ export function createLinkCommand() {
48
49
  td link --dry-run 预览操作,不实际执行
49
50
  td link -y 跳过确认,自动执行
50
51
  td link --config codepac-dep-inner.json 使用指定的可选配置
52
+ td link --skip-action prepareNested 跳过指定 action.name
51
53
  td link --no-submodules 跳过 git submodule 检测`)
52
54
  .action(async (projectPath, options) => {
53
55
  await ensureInitialized();
@@ -97,8 +99,20 @@ async function resolveRequestedStoreState(dependency, requestedPlatforms, vars)
97
99
  if (await store.exists(dependency.libName, dependency.commit, platform)) {
98
100
  resolvedExisting.push(platform);
99
101
  }
102
+ else {
103
+ const registry = getRegistry();
104
+ const storeKey = registry.getStoreKey(dependency.libName, dependency.commit, platform);
105
+ if (registry.getStore(storeKey)) {
106
+ registry.removeStore(storeKey);
107
+ const storePath = await store.getStorePath();
108
+ const platformPath = store.getLibraryPath(storePath, dependency.libName, dependency.commit, platform);
109
+ await fs.rm(platformPath, { recursive: true, force: true }).catch(() => { });
110
+ }
111
+ }
100
112
  }
101
- return resolveRequestedPlatformsByActual(requestedPlatforms, dependency.sparse, resolvedExisting, vars);
113
+ const result = resolveRequestedPlatformsByActual(requestedPlatforms, dependency.sparse, resolvedExisting, vars);
114
+ debug(`[link-store-state] ${dependency.libName}@${dependency.commit.slice(0, 7)} 请求平台=${requestedPlatforms.join(', ') || '空'}, Store候选=${candidatePlatforms.join(', ') || '空'}, 实际存在=${result.actualExisting.join(', ') || '无'}, 满足请求=${result.satisfiedRequested.join(', ') || '无'}, 缺失请求=${result.missingRequested.join(', ') || '无'}`);
115
+ return result;
102
116
  }
103
117
  function getBlockedRequestedPlatforms(registry, dependency, requestedPlatforms, vars) {
104
118
  const { manualPlatforms, autoPlatforms } = resolveUnavailablePlatforms(registry, dependency);
@@ -109,7 +123,9 @@ function getBlockedRequestedPlatforms(registry, dependency, requestedPlatforms,
109
123
  const targets = getRequestedPlatformTargets(platform, dependency.sparse, vars);
110
124
  return targets.length === 1 && targets[0] === platform;
111
125
  });
112
- return [...new Set([...manualBlocked, ...autoBlocked])];
126
+ const blocked = [...new Set([...manualBlocked, ...autoBlocked])];
127
+ debug(`[link-platforms] ${dependency.libName}@${dependency.commit.slice(0, 7)} 请求平台=${requestedPlatforms.join(', ') || '空'}, 手动跳过=${manualBlocked.join(', ') || '无'}, 自动跳过=${autoBlocked.join(', ') || '无'}, 最终跳过=${blocked.join(', ') || '无'}`);
128
+ return blocked;
113
129
  }
114
130
  function clearSatisfiedAutoUnavailablePlatforms(registry, dependency, satisfiedRequested) {
115
131
  if (satisfiedRequested.length === 0)
@@ -154,12 +170,93 @@ function upsertLibraryAvailability(dependency, updates) {
154
170
  lastAccess: new Date().toISOString(),
155
171
  });
156
172
  }
157
- export async function resolveLibraryGeneralState(dependency) {
173
+ export async function resolveLibraryGeneralState(dependency, requestedPlatforms = [], vars) {
158
174
  const registry = getRegistry();
159
175
  const libKey = registry.getLibraryKey(dependency.libName, dependency.commit);
160
176
  const library = registry.getLibrary(libKey);
177
+ const sparseConfig = resolveSparseConfig(dependency.sparse, vars);
178
+ const hasPlatformSparse = sparseConfig
179
+ ? Object.keys(sparseConfig).some((key) => key !== 'common')
180
+ : Boolean(dependency.sparse && !isSparseOnlyCommon(dependency.sparse));
181
+ const hasPlatformLibraryHistory = requestedPlatforms.length > 0
182
+ && library?.isGeneral === false
183
+ && library.platforms.some((platform) => platform !== GENERAL_PLATFORM);
184
+ if (requestedPlatforms.length > 0) {
185
+ const requestedState = await resolveRequestedStoreState(dependency, requestedPlatforms, vars);
186
+ if (requestedState.actualExisting.length > 0) {
187
+ if (library?.isGeneral === true) {
188
+ registry.updateLibrary(libKey, {
189
+ platforms: requestedState.actualExisting,
190
+ isGeneral: false,
191
+ lastAccess: new Date().toISOString(),
192
+ });
193
+ }
194
+ return false;
195
+ }
196
+ }
197
+ if (requestedPlatforms.length > 0 && library?.isGeneral === true) {
198
+ const requestedTargets = [...new Set(requestedPlatforms.flatMap((platform) => getRequestedPlatformTargets(platform, dependency.sparse, vars)))];
199
+ const storePath = await store.getStorePath();
200
+ const commitPath = path.join(storePath, dependency.libName, dependency.commit);
201
+ try {
202
+ const entries = await fs.readdir(commitPath, { withFileTypes: true });
203
+ let hasRequestedPlatformDir = false;
204
+ for (const entry of entries) {
205
+ if (!entry.isDirectory() || !requestedTargets.includes(entry.name))
206
+ continue;
207
+ if (await store.exists(dependency.libName, dependency.commit, entry.name)) {
208
+ hasRequestedPlatformDir = true;
209
+ break;
210
+ }
211
+ }
212
+ if (hasRequestedPlatformDir) {
213
+ return false;
214
+ }
215
+ }
216
+ catch {
217
+ // Store 目录不存在时继续使用历史记录或后续 General 检测
218
+ }
219
+ }
220
+ if (requestedPlatforms.length > 0 && hasPlatformSparse) {
221
+ if (library?.isGeneral === true) {
222
+ registry.updateLibrary(libKey, {
223
+ isGeneral: false,
224
+ lastAccess: new Date().toISOString(),
225
+ });
226
+ }
227
+ return false;
228
+ }
229
+ if (hasPlatformLibraryHistory) {
230
+ return false;
231
+ }
161
232
  if (typeof library?.isGeneral === 'boolean') {
162
- return library.isGeneral;
233
+ if (library.isGeneral) {
234
+ const hasGeneralContent = await store.exists(dependency.libName, dependency.commit, GENERAL_PLATFORM);
235
+ if (!hasGeneralContent) {
236
+ registry.updateLibrary(libKey, {
237
+ isGeneral: false,
238
+ lastAccess: new Date().toISOString(),
239
+ });
240
+ return false;
241
+ }
242
+ const actualGeneral = await store.isGeneralLib(dependency.libName, dependency.commit);
243
+ if (!actualGeneral) {
244
+ registry.updateLibrary(libKey, {
245
+ isGeneral: false,
246
+ lastAccess: new Date().toISOString(),
247
+ });
248
+ }
249
+ return actualGeneral;
250
+ }
251
+ const actualGeneral = await store.isGeneralLib(dependency.libName, dependency.commit);
252
+ if (actualGeneral) {
253
+ registry.updateLibrary(libKey, {
254
+ platforms: [GENERAL_PLATFORM],
255
+ isGeneral: true,
256
+ lastAccess: new Date().toISOString(),
257
+ });
258
+ }
259
+ return actualGeneral;
163
260
  }
164
261
  return store.isGeneralLib(dependency.libName, dependency.commit);
165
262
  }
@@ -173,7 +270,7 @@ export function resolveUnavailablePlatforms(registry, dependency) {
173
270
  autoPlatforms,
174
271
  };
175
272
  }
176
- export async function collectProjectDependencyGraph(projectRoot) {
273
+ export async function collectProjectDependencyGraph(projectRoot, codepacPlatforms = []) {
177
274
  const { normalizedPath, configPath } = await resolveProjectRootPath(projectRoot);
178
275
  if (!configPath) {
179
276
  throw new Error('只能在项目目录下使用');
@@ -182,11 +279,17 @@ export async function collectProjectDependencyGraph(projectRoot) {
182
279
  const seenDeps = new Set();
183
280
  const visitedConfigs = new Set();
184
281
  const queue = [
185
- { configPath, source: 'direct' },
282
+ { configPath, targetDir: path.dirname(configPath), codepacPlatforms, source: 'direct' },
186
283
  ];
187
- const submodules = await findSubmoduleConfigs(normalizedPath);
284
+ const submodules = await findSubmoduleConfigs(normalizedPath, '', 0, codepacPlatforms);
188
285
  for (const submodule of submodules) {
189
- queue.push({ configPath: submodule.configPath, scope: submodule.relativePath, source: 'submodule' });
286
+ queue.push({
287
+ configPath: submodule.configPath,
288
+ targetDir: path.dirname(submodule.configPath),
289
+ codepacPlatforms,
290
+ scope: submodule.relativePath,
291
+ source: 'submodule',
292
+ });
190
293
  }
191
294
  while (queue.length > 0) {
192
295
  const current = queue.shift();
@@ -200,25 +303,32 @@ export async function collectProjectDependencyGraph(projectRoot) {
200
303
  catch {
201
304
  continue;
202
305
  }
203
- for (const dependency of extractDependencies(parsedConfig)) {
306
+ const extractOptions = { platforms: current.codepacPlatforms, configPath: current.configPath };
307
+ for (const dependency of extractDependencies(parsedConfig, extractOptions)) {
204
308
  const key = `${dependency.libName}:${dependency.commit}:${current.scope ?? ''}`;
205
309
  if (!seenDeps.has(key)) {
206
310
  seenDeps.add(key);
207
311
  results.push({ ...dependency, scope: current.scope, source: current.source });
208
312
  }
209
313
  }
210
- for (const action of extractActions(parsedConfig)) {
211
- let parsedAction;
314
+ for (const action of extractActions(parsedConfig, extractOptions)) {
315
+ let plan;
212
316
  try {
213
- parsedAction = parseActionCommand(action.command);
317
+ plan = buildActionExecutionPlan(action, {
318
+ parentConfigPath: current.configPath,
319
+ parentTargetDir: current.targetDir,
320
+ inheritedCodepacPlatforms: current.codepacPlatforms,
321
+ });
214
322
  }
215
323
  catch {
216
324
  continue;
217
325
  }
218
- const nestedConfigPath = path.resolve(path.dirname(current.configPath), parsedAction.configDir, 'codepac-dep.json');
219
326
  let nested;
220
327
  try {
221
- nested = await extractNestedDependencies(nestedConfigPath, parsedAction.libraries);
328
+ nested = await extractNestedDependencies(plan.nestedConfigPath, plan.libraries, {
329
+ platforms: plan.effectiveCodepacPlatforms,
330
+ configPath: plan.nestedConfigPath,
331
+ });
222
332
  }
223
333
  catch {
224
334
  continue;
@@ -230,7 +340,15 @@ export async function collectProjectDependencyGraph(projectRoot) {
230
340
  results.push({ ...dependency, scope: current.scope, source: 'nested' });
231
341
  }
232
342
  }
233
- queue.push({ configPath: nestedConfigPath, scope: current.scope, source: 'nested' });
343
+ if (!plan.parsed.disableAction) {
344
+ queue.push({
345
+ configPath: plan.nestedConfigPath,
346
+ targetDir: plan.nestedTargetDir,
347
+ codepacPlatforms: plan.effectiveCodepacPlatforms,
348
+ scope: current.scope,
349
+ source: 'nested',
350
+ });
351
+ }
234
352
  }
235
353
  }
236
354
  return results;
@@ -276,6 +394,16 @@ async function selectSubmodules(configs, options, remembered) {
276
394
  }
277
395
  return selected.map(s => ({ ...s }));
278
396
  }
397
+ function buildSubmoduleOptionalConfigState(submodules) {
398
+ const state = {};
399
+ for (const submodule of submodules) {
400
+ const names = submodule.selectedOptionalConfigs?.map((config) => config.name) ?? [];
401
+ if (names.length > 0) {
402
+ state[submodule.relativePath] = names;
403
+ }
404
+ }
405
+ return Object.keys(state).length > 0 ? state : undefined;
406
+ }
279
407
  /**
280
408
  * 单个 scope(主项目或 submodule)的链接处理
281
409
  *
@@ -284,6 +412,7 @@ async function selectSubmodules(configs, options, remembered) {
284
412
  async function linkScope(params) {
285
413
  const { scopeName, configPath, platforms, scanExtraPlatforms, registry, tx, projectHash, projectRoot, storePath, download, dryRun, yes, concurrency, gitLightweightDownload, optionalConfigs, scope, } = params;
286
414
  let finalLinkPlatforms = [...params.finalLinkPlatforms];
415
+ const codepacPlatforms = [...new Set(platforms.map((platform) => getBaseKeyForCodepac(platform)))];
287
416
  // 记录 General 类型库
288
417
  const generalLibs = new Set();
289
418
  const downloadedLibs = [];
@@ -292,17 +421,32 @@ async function linkScope(params) {
292
421
  info(`分析 ${scopeName}: ${configPath}`);
293
422
  let dependencies;
294
423
  let configVars;
424
+ const selectedActions = [];
425
+ const cacheConfigPaths = [configPath];
295
426
  try {
296
- const result = await parseProjectDependencies(path.dirname(path.dirname(configPath)));
427
+ const result = await parseProjectDependencies(path.dirname(path.dirname(configPath)), {
428
+ platforms: codepacPlatforms,
429
+ });
297
430
  dependencies = result.dependencies;
298
431
  configVars = result.vars;
432
+ const topLevelConfig = await parseCodepacDep(configPath);
433
+ selectedActions.push(...extractActions(topLevelConfig, { platforms: codepacPlatforms, configPath }));
299
434
  // 合并可选配置依赖
300
435
  if (optionalConfigs && optionalConfigs.length > 0) {
301
436
  for (const optionalConfig of optionalConfigs) {
302
437
  try {
303
438
  const optionalResult = await parseCodepacDep(optionalConfig.path);
304
- const optionalDeps = extractDependencies(optionalResult);
439
+ const optionalDeps = extractDependencies(optionalResult, {
440
+ platforms: codepacPlatforms,
441
+ configPath: optionalConfig.path,
442
+ });
305
443
  dependencies = mergeDepLists(dependencies, optionalDeps);
444
+ configVars = { ...configVars, ...optionalResult.vars };
445
+ selectedActions.push(...extractActions(optionalResult, {
446
+ platforms: codepacPlatforms,
447
+ configPath: optionalConfig.path,
448
+ }));
449
+ cacheConfigPaths.push(optionalConfig.path);
306
450
  info(` + ${optionalConfig.name}: ${optionalDeps.length} 个依赖`);
307
451
  }
308
452
  catch (optErr) {
@@ -463,7 +607,7 @@ async function linkScope(params) {
463
607
  case DependencyStatus.LINKED: {
464
608
  const blockedPlatforms = getBlockedRequestedPlatforms(registry, dependency, platforms, configVars);
465
609
  const requestedPlatforms = platforms.filter((platform) => !blockedPlatforms.includes(platform));
466
- const isLinkedGeneral = await resolveLibraryGeneralState(dependency);
610
+ const isLinkedGeneral = await resolveLibraryGeneralState(dependency, requestedPlatforms, configVars);
467
611
  if (!isLinkedGeneral) {
468
612
  const supplementResult = await supplementMissingPlatforms(dependency, platforms, registry, tx, {
469
613
  vars: configVars,
@@ -489,7 +633,7 @@ async function linkScope(params) {
489
633
  const blockedPlatforms = getBlockedRequestedPlatforms(registry, dependency, platforms, configVars);
490
634
  const requestedPlatforms = platforms.filter((platform) => !blockedPlatforms.includes(platform));
491
635
  const relinkCommitPath = path.join(storePath, dependency.libName, dependency.commit);
492
- const isRelinkGeneral = await resolveLibraryGeneralState(dependency);
636
+ const isRelinkGeneral = await resolveLibraryGeneralState(dependency, requestedPlatforms, configVars);
493
637
  if (isRelinkGeneral) {
494
638
  tx.recordOp('unlink', localPath);
495
639
  await linker.unlink(localPath);
@@ -547,7 +691,7 @@ async function linkScope(params) {
547
691
  const requestedPlatforms = platforms.filter((platform) => !blockedPlatforms.includes(platform));
548
692
  const replaceSize = await getDirSize(localPath);
549
693
  const replaceCommitPath = path.join(storePath, dependency.libName, dependency.commit);
550
- const isReplaceGeneral = await resolveLibraryGeneralState(dependency);
694
+ const isReplaceGeneral = await resolveLibraryGeneralState(dependency, requestedPlatforms, configVars);
551
695
  if (isReplaceGeneral) {
552
696
  const sharedPath = path.join(replaceCommitPath, '_shared');
553
697
  tx.recordOp('replace', localPath, sharedPath);
@@ -776,7 +920,7 @@ async function linkScope(params) {
776
920
  }
777
921
  const { actualExisting: linkNewExisting, satisfiedRequested, } = await resolveRequestedStoreState(dependency, requestedPlatforms, configVars);
778
922
  clearSatisfiedAutoUnavailablePlatforms(registry, dependency, satisfiedRequested);
779
- const isLinkNewGeneral = await resolveLibraryGeneralState(dependency);
923
+ const isLinkNewGeneral = await resolveLibraryGeneralState(dependency, requestedPlatforms, configVars);
780
924
  if (isLinkNewGeneral) {
781
925
  const sharedPath = path.join(linkNewCommitPath, '_shared');
782
926
  tx.recordOp('link', localPath, sharedPath);
@@ -845,9 +989,34 @@ async function linkScope(params) {
845
989
  const storeCommitPath = path.join(storePath, dependency.libName, dependency.commit);
846
990
  const blockedPlatforms = getBlockedRequestedPlatforms(registry, dependency, platforms, configVars);
847
991
  const requestedPlatforms = platforms.filter((platform) => !blockedPlatforms.includes(platform));
992
+ if (requestedPlatforms.length === 0) {
993
+ pLog.warn(`${dependency.libName} 请求平台 [${platforms.join(', ')}] 均已按规则跳过,没有可链接产物`);
994
+ if (await linker.cleanupEmptyLocalDirectory(localPath)) {
995
+ pLog.warn(`${dependency.libName} 已清理历史遗留的空本地目录`);
996
+ }
997
+ return {
998
+ success: false,
999
+ name: dependency.libName,
1000
+ skipped: true,
1001
+ skippedPlatforms: blockedPlatforms,
1002
+ unsupported: true,
1003
+ };
1004
+ }
848
1005
  const { actualExisting: existing, missingRequested: missing, satisfiedRequested, } = await resolveRequestedStoreState(dependency, requestedPlatforms, configVars);
849
1006
  clearSatisfiedAutoUnavailablePlatforms(registry, dependency, satisfiedRequested);
850
1007
  if (missing.length === 0) {
1008
+ if (existing.length === 0) {
1009
+ pLog.warn(`${dependency.libName} 请求平台 [${requestedPlatforms.join(', ')}] 在 Store 中没有可链接产物,已拒绝创建空目录`);
1010
+ if (await linker.cleanupEmptyLocalDirectory(localPath)) {
1011
+ pLog.warn(`${dependency.libName} 已清理历史遗留的空本地目录`);
1012
+ }
1013
+ return {
1014
+ success: false,
1015
+ name: dependency.libName,
1016
+ skipped: true,
1017
+ skippedPlatforms: requestedPlatforms,
1018
+ };
1019
+ }
851
1020
  pLog.info(`${dependency.libName} 所有平台已存在,直接链接...`);
852
1021
  tx.recordOp('link', localPath, storeCommitPath);
853
1022
  await linker.linkLib(localPath, storeCommitPath, existing);
@@ -864,6 +1033,9 @@ async function linkScope(params) {
864
1033
  if (knownUnavailable.length > 0) {
865
1034
  pLog.warn(`${dependency.libName} 平台 [${knownUnavailable.join(', ')}] 已按规则跳过`);
866
1035
  }
1036
+ if (await linker.cleanupEmptyLocalDirectory(localPath)) {
1037
+ pLog.warn(`${dependency.libName} 已清理历史遗留的空本地目录`);
1038
+ }
867
1039
  return { success: false, name: dependency.libName, skipped: true, skippedPlatforms: missing, unsupported: true };
868
1040
  }
869
1041
  pLog.info(`下载 ${dependency.libName} [${dlToDownload.join(', ')}]...`);
@@ -924,6 +1096,10 @@ async function linkScope(params) {
924
1096
  }
925
1097
  const linkPlatforms = [...existing, ...filteredDownloaded];
926
1098
  if (linkPlatforms.length === 0) {
1099
+ pLog.warn(`${dependency.libName} 下载后没有生成请求平台 [${requestedPlatforms.join(', ')}] 的可链接产物,已拒绝创建空目录`);
1100
+ if (await linker.cleanupEmptyLocalDirectory(localPath)) {
1101
+ pLog.warn(`${dependency.libName} 已清理历史遗留的空本地目录`);
1102
+ }
927
1103
  return {
928
1104
  success: false,
929
1105
  name: dependency.libName,
@@ -978,26 +1154,32 @@ async function linkScope(params) {
978
1154
  }
979
1155
  }
980
1156
  // 9. 处理嵌套依赖 (actions)
981
- const topLevelConfig = await parseCodepacDep(configPath);
982
- const actions = extractActions(topLevelConfig);
1157
+ const actions = selectedActions;
983
1158
  const nestedLinkedDeps = [];
984
1159
  if (actions.length > 0) {
985
1160
  blank();
986
1161
  separator();
987
1162
  info(`发现 ${actions.length} 个嵌套依赖配置`);
988
1163
  const nestedContext = {
989
- depth: 0, processedConfigs: new Set([configPath]), platforms, vars: configVars,
1164
+ depth: 0,
1165
+ processedConfigs: new Set([buildActionVisitKey(configPath, path.dirname(configPath))]),
1166
+ platforms,
1167
+ codepacPlatforms,
1168
+ vars: configVars,
990
1169
  };
991
1170
  const thirdPartyDir = path.dirname(configPath);
992
1171
  for (const action of actions) {
993
- await processAction(action, nestedContext, thirdPartyDir, {
1172
+ await processAction(action, nestedContext, configPath, thirdPartyDir, {
994
1173
  tx, registry, projectHash, projectRoot, dryRun, download, yes,
995
- gitLightweightDownload, generalLibs, downloadedLibs, nestedLinkedDeps,
1174
+ gitLightweightDownload, skipActions: new Set(params.skipActions ?? []),
1175
+ generalLibs, downloadedLibs, nestedLinkedDeps,
996
1176
  });
997
1177
  }
998
1178
  }
999
1179
  // 10. 同步 cache 文件
1000
- await syncCacheFile(configPath);
1180
+ for (const cacheConfigPath of cacheConfigPaths) {
1181
+ await syncCacheFile(cacheConfigPath);
1182
+ }
1001
1183
  // 11. 构建返回结果
1002
1184
  const linkedDeps = [];
1003
1185
  for (const item of classified) {
@@ -1070,9 +1252,10 @@ export async function linkProject(projectPath, options) {
1070
1252
  error(`路径不存在: ${absolutePath}`);
1071
1253
  process.exit(EXIT_CODES.NOINPUT);
1072
1254
  }
1073
- // 发现可选配置文件
1074
- const thirdpartyDir = path.join(normalizedPath, '3rdparty');
1075
- const configDiscovery = await findAllCodepacConfigs(thirdpartyDir);
1255
+ // 发现可选配置文件,扫描位置跟随实际主配置所在目录。
1256
+ const configDiscovery = discoveredConfigPath
1257
+ ? await findAllCodepacConfigs(path.dirname(discoveredConfigPath))
1258
+ : null;
1076
1259
  let selectedOptionalConfigs = [];
1077
1260
  if (configDiscovery && configDiscovery.optionalConfigs.length > 0) {
1078
1261
  if (options.config && options.config.length > 0) {
@@ -1119,14 +1302,21 @@ export async function linkProject(projectPath, options) {
1119
1302
  // === 阶段 2: Submodule 检测 ===
1120
1303
  let selectedSubmodules = [];
1121
1304
  if (options.submodules !== false) {
1122
- const submoduleConfigs = await findSubmoduleConfigs(normalizedPath);
1305
+ const codepacPlatforms = [...new Set(platforms.map((platform) => getBaseKeyForCodepac(platform)))];
1306
+ const submoduleConfigs = await findSubmoduleConfigs(normalizedPath, '', 0, codepacPlatforms);
1123
1307
  if (submoduleConfigs.length > 0) {
1124
1308
  selectedSubmodules = await selectSubmodules(submoduleConfigs, options, existingProject?.submodules);
1125
1309
  // 为选中的 submodule 处理可选配置
1126
1310
  for (const sub of selectedSubmodules) {
1127
- if (sub.optionalConfigs.length > 0 && !options.yes && process.stdout.isTTY) {
1311
+ if (sub.optionalConfigs.length === 0)
1312
+ continue;
1313
+ const rememberedConfigs = existingProject?.submoduleOptionalConfigs?.[sub.relativePath] ?? [];
1314
+ if (options.yes) {
1315
+ sub.selectedOptionalConfigs = sub.optionalConfigs.filter((configInfo) => rememberedConfigs.includes(configInfo.name));
1316
+ }
1317
+ else if (process.stdout.isTTY) {
1128
1318
  info(`${sub.name} 发现可选配置:`);
1129
- const selected = await selectOptionalConfigs(sub.optionalConfigs, { isTTY: true, specifiedConfigs: [] });
1319
+ const selected = await selectOptionalConfigs(sub.optionalConfigs, { isTTY: true, specifiedConfigs: rememberedConfigs });
1130
1320
  if (selected !== PROMPT_CANCELLED) {
1131
1321
  sub.selectedOptionalConfigs = selected;
1132
1322
  }
@@ -1194,6 +1384,7 @@ export async function linkProject(projectPath, options) {
1194
1384
  concurrency,
1195
1385
  gitLightweightDownload,
1196
1386
  optionalConfigs: selectedOptionalConfigs,
1387
+ skipActions: options.skipAction ?? [],
1197
1388
  });
1198
1389
  const finalLinkPlatforms = mainResult.finalLinkPlatforms;
1199
1390
  // 如果 dry-run 模式,linkScope 内已显示信息
@@ -1219,6 +1410,7 @@ export async function linkProject(projectPath, options) {
1219
1410
  gitLightweightDownload,
1220
1411
  optionalConfigs: sub.selectedOptionalConfigs,
1221
1412
  scope: sub.relativePath,
1413
+ skipActions: options.skipAction ?? [],
1222
1414
  });
1223
1415
  }
1224
1416
  return;
@@ -1245,6 +1437,7 @@ export async function linkProject(projectPath, options) {
1245
1437
  gitLightweightDownload,
1246
1438
  optionalConfigs: sub.selectedOptionalConfigs,
1247
1439
  scope: sub.relativePath,
1440
+ skipActions: options.skipAction ?? [],
1248
1441
  });
1249
1442
  subResults.push(subResult);
1250
1443
  }
@@ -1269,6 +1462,7 @@ export async function linkProject(projectPath, options) {
1269
1462
  ? selectedOptionalConfigs.map(c => c.name) : undefined,
1270
1463
  submodules: selectedSubmodules.length > 0
1271
1464
  ? selectedSubmodules.map(s => s.relativePath) : undefined,
1465
+ submoduleOptionalConfigs: buildSubmoduleOptionalConfigState(selectedSubmodules),
1272
1466
  });
1273
1467
  // 更新 Store 引用关系
1274
1468
  const newStoreKeys = allLinkedDeps.map((d) => registry.getStoreKey(d.libName, d.commit, d.platform));
@@ -1419,10 +1613,7 @@ function hintStoreRepairCommand(err) {
1419
1613
  async function registerSharedStore(libName, commit, branch = '', url = '') {
1420
1614
  const registry = getRegistry();
1421
1615
  const storeKey = registry.getStoreKey(libName, commit, SHARED_PLATFORM);
1422
- // 如果已存在,不重复注册
1423
- if (registry.getStore(storeKey)) {
1424
- return true;
1425
- }
1616
+ const existingEntry = registry.getStore(storeKey);
1426
1617
  // 检查 _shared 目录是否存在
1427
1618
  const storePath = await config.getStorePath();
1428
1619
  if (!storePath)
@@ -1440,17 +1631,27 @@ async function registerSharedStore(libName, commit, branch = '', url = '') {
1440
1631
  }
1441
1632
  // 计算 _shared 完整性信息并注册
1442
1633
  const integrity = await store.captureIntegrity(libName, commit, SHARED_PLATFORM);
1443
- registry.addStore({
1444
- libName,
1445
- commit,
1446
- platform: SHARED_PLATFORM,
1447
- branch,
1448
- url,
1449
- ...integrity,
1450
- usedBy: [], // _shared 不追踪引用,生命周期跟随平台
1451
- createdAt: new Date().toISOString(),
1452
- lastAccess: new Date().toISOString(),
1453
- });
1634
+ if (existingEntry) {
1635
+ registry.updateStore(storeKey, {
1636
+ branch: existingEntry.branch || branch,
1637
+ url: existingEntry.url || url,
1638
+ ...integrity,
1639
+ lastAccess: new Date().toISOString(),
1640
+ });
1641
+ }
1642
+ else {
1643
+ registry.addStore({
1644
+ libName,
1645
+ commit,
1646
+ platform: SHARED_PLATFORM,
1647
+ branch,
1648
+ url,
1649
+ ...integrity,
1650
+ usedBy: [], // _shared 不追踪引用,生命周期跟随平台
1651
+ createdAt: new Date().toISOString(),
1652
+ lastAccess: new Date().toISOString(),
1653
+ });
1654
+ }
1454
1655
  debug(`注册 _shared: ${libName}:${commit.slice(0, 8)} (${formatSize(integrity.size)})`);
1455
1656
  return true;
1456
1657
  }
@@ -1476,6 +1677,13 @@ async function ensureStoreInfo(libName, commit, platform, branch, url, projectHa
1476
1677
  const integrity = await store.captureIntegrity(libName, commit, platform);
1477
1678
  registry.updateStore(storeKey, integrity);
1478
1679
  }
1680
+ if (existingEntry && ((!existingEntry.branch && branch) || (!existingEntry.url && url))) {
1681
+ registry.updateStore(storeKey, {
1682
+ branch: existingEntry.branch || branch,
1683
+ url: existingEntry.url || url,
1684
+ lastAccess: new Date().toISOString(),
1685
+ });
1686
+ }
1479
1687
  if (projectHash) {
1480
1688
  registry.addStoreReference(storeKey, projectHash);
1481
1689
  }
@@ -1716,7 +1924,7 @@ async function classifyDependencies(dependencies, projectPath, configPath, platf
1716
1924
  }
1717
1925
  const existing = verifiedExisting;
1718
1926
  // 也检查是否为 General 库(有 _shared 且有内容)
1719
- const isGeneral = await resolveLibraryGeneralState(dep);
1927
+ const isGeneral = await resolveLibraryGeneralState(dep, requestedPlatforms, vars);
1720
1928
  const inStore = existing.length > 0 || isGeneral;
1721
1929
  if (satisfiedRequested.length > 0) {
1722
1930
  clearSatisfiedAutoUnavailablePlatforms(getRegistry(), dep, satisfiedRequested);
@@ -1953,19 +2161,20 @@ function getStatusKey(status) {
1953
2161
  async function syncCacheFile(configPath) {
1954
2162
  const configDir = path.dirname(configPath);
1955
2163
  const cacheDir = path.join(configDir, '.cache');
1956
- const cachePath = path.join(cacheDir, 'codepac-dep.json');
2164
+ const configFileName = path.basename(configPath);
2165
+ const cachePath = path.join(cacheDir, configFileName);
1957
2166
  try {
1958
2167
  // 确保 .cache 目录存在
1959
2168
  await fs.mkdir(cacheDir, { recursive: true });
1960
2169
  // 复制配置文件到 cache
1961
2170
  await fs.copyFile(configPath, cachePath);
1962
2171
  if (process.env.VERBOSE) {
1963
- info(`已同步 cache: ${path.basename(configPath)}`);
2172
+ info(`已同步 cache: ${configFileName} -> ${path.relative(configDir, cachePath)}`);
1964
2173
  }
1965
2174
  }
1966
2175
  catch (err) {
1967
2176
  // cache 同步失败不应阻塞主流程,仅警告
1968
- warn(`cache 同步失败: ${err.message}`);
2177
+ warn(`cache 同步失败: ${configFileName} -> ${path.relative(configDir, cachePath)}: ${err.message}`);
1969
2178
  }
1970
2179
  }
1971
2180
  /**
@@ -1975,78 +2184,98 @@ async function syncCacheFile(configPath) {
1975
2184
  * @param thirdPartyDir 3rdparty 目录路径
1976
2185
  * @param options 处理选项
1977
2186
  */
1978
- async function processAction(action, context, thirdPartyDir, options) {
2187
+ async function processAction(action, context, parentConfigPath, parentTargetDir, options) {
1979
2188
  const indent = ' '.repeat(context.depth);
1980
- // 1. 解析 action 命令
1981
- let parsed;
2189
+ if (action.name && options.skipActions.has(action.name)) {
2190
+ info(`${indent}跳过嵌套 action: name=${action.name}, 原因=skip-action`);
2191
+ debug(`${indent}CodePac action 跳过: name=${action.name}, command=${action.command}, skipActions=${Array.from(options.skipActions).join(',')}`);
2192
+ return;
2193
+ }
2194
+ // 1. 解析 action 命令和执行计划
2195
+ let plan;
1982
2196
  try {
1983
- parsed = parseActionCommand(action.command);
2197
+ plan = buildActionExecutionPlan(action, {
2198
+ parentConfigPath,
2199
+ parentTargetDir,
2200
+ inheritedCodepacPlatforms: context.codepacPlatforms ?? context.platforms,
2201
+ });
1984
2202
  }
1985
2203
  catch (err) {
1986
- warn(`${indent}无法解析 action: ${err.message}`);
2204
+ warn(`${indent}跳过嵌套 action: name=${action.name ?? '(未命名)'}, 原因=无法解析命令, message=${err.message}`);
2205
+ debug(`${indent}CodePac action 解析失败: command=${action.command}`);
1987
2206
  return;
1988
2207
  }
1989
- const libsDisplay = parsed.libraries.length > 0 ? parsed.libraries.join(', ') : '全部依赖';
1990
- info(`${indent}处理嵌套依赖: ${parsed.configDir} [${libsDisplay}]`);
1991
- // 2. 构建嵌套配置路径
1992
- const nestedConfigPath = path.join(thirdPartyDir, parsed.configDir, 'codepac-dep.json');
1993
- // 3. 循环检测
1994
- if (context.processedConfigs.has(nestedConfigPath)) {
1995
- warn(`${indent} 检测到循环依赖,跳过: ${parsed.configDir}`);
2208
+ const libsDisplay = plan.libraries.length > 0 ? plan.libraries.join(', ') : '全部依赖';
2209
+ info(`${indent}处理嵌套 action: name=${plan.actionName ?? '(未命名)'}, config=${plan.nestedConfigPath}, target=${plan.nestedTargetDir}, libs=[${libsDisplay}]`);
2210
+ debug(`${indent}CodePac action 执行计划: command=${plan.command}, parentConfig=${parentConfigPath}, parentTarget=${parentTargetDir}, parsedConfigDir=${plan.configDir}, parsedTargetDir=${plan.targetDir}, resolvedConfig=${plan.nestedConfigPath}, resolvedTarget=${plan.nestedTargetDir}, inheritedPlatforms=${formatActionPlatforms(plan.inheritedPlatforms)}, effectivePlatforms=${formatActionPlatforms(plan.effectiveCodepacPlatforms)}, explicitPlatform=${plan.parsed.hasExplicitPlatform}, fullgit=${plan.parsed.hasExplicitFullGit}, unshallow=${plan.parsed.hasExplicitUnshallow}, disable_sparse=${plan.parsed.hasExplicitDisableSparse}`);
2211
+ // 2. 循环检测
2212
+ const visitKey = buildActionVisitKey(plan.nestedConfigPath, plan.nestedTargetDir);
2213
+ if (context.processedConfigs.has(visitKey)) {
2214
+ warn(`${indent} 跳过嵌套 action: name=${plan.actionName ?? '(未命名)'}, 原因=循环配置, config=${plan.nestedConfigPath}, target=${plan.nestedTargetDir}`);
1996
2215
  return;
1997
2216
  }
1998
- context.processedConfigs.add(nestedConfigPath);
1999
- // 4. 检查配置文件是否存在
2217
+ context.processedConfigs.add(visitKey);
2218
+ // 3. 检查配置文件是否存在
2000
2219
  try {
2001
- await fs.access(nestedConfigPath);
2220
+ await fs.access(plan.nestedConfigPath);
2002
2221
  }
2003
2222
  catch {
2004
- warn(`${indent} 嵌套配置文件不存在: ${nestedConfigPath}`);
2005
- hint(`${indent} 请确保 ${parsed.configDir} 库已被下载`);
2223
+ warn(`${indent} 跳过嵌套 action: name=${plan.actionName ?? '(未命名)'}, 原因=配置不存在, config=${plan.nestedConfigPath}`);
2224
+ hint(`${indent} 请确保 ${plan.configDir} 库已被下载`);
2006
2225
  return;
2007
2226
  }
2008
- // 5. 提取指定库的依赖
2227
+ // 4. 提取指定库的依赖
2009
2228
  let nestedResult;
2010
2229
  try {
2011
- nestedResult = await extractNestedDependencies(nestedConfigPath, parsed.libraries);
2230
+ nestedResult = await extractNestedDependencies(plan.nestedConfigPath, plan.libraries, {
2231
+ platforms: plan.effectiveCodepacPlatforms,
2232
+ configPath: plan.nestedConfigPath,
2233
+ });
2012
2234
  }
2013
2235
  catch (err) {
2014
- warn(`${indent} 解析嵌套配置失败: ${err.message}`);
2236
+ warn(`${indent} 跳过嵌套 action: name=${plan.actionName ?? '(未命名)'}, 原因=解析配置失败, message=${err.message}`);
2015
2237
  return;
2016
2238
  }
2017
2239
  const { dependencies, vars, nestedActions } = nestedResult;
2018
2240
  if (dependencies.length === 0) {
2019
- warn(`${indent} ${parsed.configDir} 中未找到指定的库`);
2241
+ warn(`${indent} 跳过嵌套 action: name=${plan.actionName ?? '(未命名)'}, 原因=未找到依赖, libs=[${libsDisplay}], config=${plan.nestedConfigPath}`);
2020
2242
  return;
2021
2243
  }
2022
- info(`${indent} 找到 ${dependencies.length} 个嵌套依赖`);
2023
- // 6. 合并变量
2244
+ info(`${indent} 嵌套 action 命中 ${dependencies.length} 个依赖,继承平台: ${formatActionPlatforms(plan.inheritedPlatforms)}, 生效平台: ${formatActionPlatforms(plan.effectiveCodepacPlatforms)}`);
2245
+ // 5. 合并变量
2024
2246
  const mergedVars = { ...context.vars, ...vars };
2025
- // 7. 处理这些依赖(targetDir 指定嵌套依赖的目标目录)
2247
+ // 6. 处理这些依赖(targetDir 指定嵌套依赖的目标目录)
2026
2248
  await linkNestedDependencies(dependencies, {
2027
- thirdPartyDir,
2028
- targetDir: parsed.targetDir,
2029
- nestedConfigPath,
2030
- context: { ...context, vars: mergedVars },
2249
+ targetDir: plan.nestedTargetDir,
2250
+ nestedConfigPath: plan.nestedConfigPath,
2251
+ context: { ...context, codepacPlatforms: plan.effectiveCodepacPlatforms, vars: mergedVars },
2031
2252
  options,
2032
2253
  indent,
2033
2254
  });
2034
2255
  // 同步嵌套配置文件的 cache(兼容 checkValid.js 检测)
2035
- await syncCacheFile(nestedConfigPath);
2036
- // 8. 递归处理嵌套 actions(如果没有 disable_action)
2037
- // 注意:递归时 thirdPartyDir 应该更新为当前嵌套依赖的目标目录
2038
- if (!parsed.disableAction && nestedActions.length > 0) {
2256
+ await syncCacheFile(plan.nestedConfigPath);
2257
+ // 7. 递归处理嵌套 actions(如果没有 disable_action)
2258
+ if (!plan.parsed.disableAction && nestedActions.length > 0) {
2039
2259
  const nestedContext = {
2040
2260
  depth: context.depth + 1,
2041
2261
  processedConfigs: context.processedConfigs,
2042
2262
  platforms: context.platforms,
2263
+ codepacPlatforms: plan.effectiveCodepacPlatforms,
2043
2264
  vars: mergedVars,
2044
2265
  };
2045
- const nestedThirdPartyDir = path.join(thirdPartyDir, parsed.targetDir);
2046
2266
  for (const nestedAction of nestedActions) {
2047
- await processAction(nestedAction, nestedContext, nestedThirdPartyDir, options);
2267
+ await processAction(nestedAction, nestedContext, plan.nestedConfigPath, plan.nestedTargetDir, options);
2048
2268
  }
2049
2269
  }
2270
+ else if (plan.parsed.disableAction && nestedActions.length > 0) {
2271
+ info(`${indent} 跳过后续嵌套 action: name=${plan.actionName ?? '(未命名)'}, 原因=disable_action`);
2272
+ }
2273
+ }
2274
+ function formatActionPlatforms(platforms) {
2275
+ return platforms && platforms.length > 0 ? platforms.join(',') : 'all';
2276
+ }
2277
+ function buildActionVisitKey(configPath, targetDir) {
2278
+ return `${configPath}::${targetDir}`;
2050
2279
  }
2051
2280
  /**
2052
2281
  * 统一的本地目录验证+吸收流程
@@ -2104,9 +2333,7 @@ async function resolveLocalAndAbsorb(localPath, libName, commit, options) {
2104
2333
  * 链接嵌套依赖
2105
2334
  */
2106
2335
  async function linkNestedDependencies(dependencies, params) {
2107
- const { thirdPartyDir, targetDir, context, options, indent } = params;
2108
- // 计算嵌套依赖的实际目标目录
2109
- const nestedTargetDir = path.join(thirdPartyDir, targetDir);
2336
+ const { targetDir: nestedTargetDir, context, options, indent } = params;
2110
2337
  const { tx, registry, projectHash, projectRoot, dryRun, download, generalLibs, downloadedLibs, nestedLinkedDeps } = options;
2111
2338
  const { platforms, vars } = context;
2112
2339
  for (const dep of dependencies) {
@@ -2130,7 +2357,7 @@ async function linkNestedDependencies(dependencies, params) {
2130
2357
  const availablePlatforms = platforms.filter(p => !unavailablePlatforms.includes(p));
2131
2358
  const knownUnavailable = platforms.filter(p => unavailablePlatforms.includes(p));
2132
2359
  let storeHas = false;
2133
- const existingPlatforms = [];
2360
+ let existingPlatforms = [];
2134
2361
  const initialStoreState = await resolveRequestedStoreState(dep, availablePlatforms, vars);
2135
2362
  existingPlatforms.push(...initialStoreState.actualExisting);
2136
2363
  if (initialStoreState.satisfiedRequested.length > 0) {
@@ -2140,6 +2367,7 @@ async function linkNestedDependencies(dependencies, params) {
2140
2367
  // 完整性校验:验证 existing 平台的文件完整性(与 classifyDependencies 同逻辑)
2141
2368
  if (existingPlatforms.length > 0) {
2142
2369
  const { valid: verifiedPlatforms, corrupted } = await verifyExistingPlatforms(dep.libName, dep.commit, existingPlatforms);
2370
+ existingPlatforms = verifiedPlatforms;
2143
2371
  if (corrupted.length > 0) {
2144
2372
  for (const p of corrupted) {
2145
2373
  const storeKey = registry.getStoreKey(dep.libName, dep.commit, p);
@@ -2147,10 +2375,10 @@ async function linkNestedDependencies(dependencies, params) {
2147
2375
  const platformPath = store.getLibraryPath(await store.getStorePath(), dep.libName, dep.commit, p);
2148
2376
  await fs.rm(platformPath, { recursive: true, force: true }).catch(() => { });
2149
2377
  }
2150
- storeHas = verifiedPlatforms.length > 0;
2151
2378
  }
2379
+ storeHas = existingPlatforms.length > 0;
2152
2380
  }
2153
- let isGeneral = await resolveLibraryGeneralState(dep);
2381
+ let isGeneral = await resolveLibraryGeneralState(dep, availablePlatforms, vars);
2154
2382
  // 如果所有平台都已知不可用,跳过
2155
2383
  if (availablePlatforms.length === 0 && knownUnavailable.length > 0 && !isGeneral) {
2156
2384
  warn(`${indent} ${dep.libName} - 平台 [${knownUnavailable.join(', ')}] 已按规则跳过`);
@@ -2171,10 +2399,21 @@ async function linkNestedDependencies(dependencies, params) {
2171
2399
  if (resolveResult.isGeneral) {
2172
2400
  isGeneral = true;
2173
2401
  }
2402
+ else if (resolveResult.absorbResult) {
2403
+ const absorbedPlatforms = resolveResult.absorbResult.platforms;
2404
+ const absorbedPlatformSet = new Set(absorbedPlatforms);
2405
+ const linkableAbsorbedPlatforms = [...new Set(availablePlatforms.flatMap((platform) => getRequestedPlatformTargets(platform, dep.sparse, vars)
2406
+ .filter((target) => absorbedPlatformSet.has(target))))];
2407
+ existingPlatforms = [...new Set([
2408
+ ...existingPlatforms,
2409
+ ...linkableAbsorbedPlatforms,
2410
+ ])];
2411
+ debug(`${indent} ${dep.libName} - 本地吸收后平台回填: requested=${availablePlatforms.join(', ') || '无'}, absorbed=${absorbedPlatforms.join(', ') || '无'}, linkable=${existingPlatforms.join(', ') || '无'}`);
2412
+ }
2174
2413
  if (resolveResult.absorbResult?.nestedLibraries.length) {
2175
2414
  await registerNestedLibraries(resolveResult.absorbResult.nestedLibraries, projectHash);
2176
2415
  }
2177
- storeHas = true;
2416
+ storeHas = isGeneral || existingPlatforms.length > 0;
2178
2417
  // 吸收为 General 后,检查是否实际需要平台内容
2179
2418
  // 本地可能只有部分文件(如只有 _shared 内容),被误分类为 General
2180
2419
  if (resolveResult.isGeneral && download) {
@@ -2481,26 +2720,62 @@ export async function verifyExistingPlatforms(libName, commit, existingPlatforms
2481
2720
  for (const platform of existingPlatforms) {
2482
2721
  const storeKey = registry.getStoreKey(libName, commit, platform);
2483
2722
  const entry = registry.getStore(storeKey);
2484
- // 没有 StoreEntry,跳过校验视为合法
2485
2723
  if (!entry) {
2724
+ try {
2725
+ const integrity = await store.captureIntegrity(libName, commit, platform);
2726
+ if (integrity.fileCount === 0) {
2727
+ corrupted.push(platform);
2728
+ warn(`${libName} (${commit.slice(0, 7)}) ${platform}: Store 目录为空`);
2729
+ continue;
2730
+ }
2731
+ registry.addStore({
2732
+ libName,
2733
+ commit,
2734
+ platform,
2735
+ branch: '',
2736
+ url: '',
2737
+ ...integrity,
2738
+ usedBy: [],
2739
+ createdAt: new Date().toISOString(),
2740
+ lastAccess: new Date().toISOString(),
2741
+ });
2742
+ }
2743
+ catch {
2744
+ corrupted.push(platform);
2745
+ warn(`${libName} (${commit.slice(0, 7)}) ${platform}: Store 完整性回填失败`);
2746
+ continue;
2747
+ }
2486
2748
  valid.push(platform);
2487
2749
  continue;
2488
2750
  }
2751
+ if (entry.fileCount === 0) {
2752
+ corrupted.push(platform);
2753
+ warn(`${libName} (${commit.slice(0, 7)}) ${platform}: Store 记录为空`);
2754
+ continue;
2755
+ }
2489
2756
  // 旧数据回填:升级后首次遇到无 fileCount 的条目,顺便记录完整性数据
2490
2757
  if (entry.fileCount == null) {
2491
2758
  try {
2492
2759
  const integrity = await store.captureIntegrity(libName, commit, platform);
2493
2760
  registry.updateStore(storeKey, integrity);
2761
+ if (integrity.fileCount === 0) {
2762
+ corrupted.push(platform);
2763
+ warn(`${libName} (${commit.slice(0, 7)}) ${platform}: Store 目录为空`);
2764
+ continue;
2765
+ }
2494
2766
  }
2495
2767
  catch {
2496
- // 回填失败不影响正常流程
2768
+ corrupted.push(platform);
2769
+ warn(`${libName} (${commit.slice(0, 7)}) ${platform}: Store 完整性回填失败`);
2770
+ continue;
2497
2771
  }
2498
2772
  valid.push(platform);
2499
2773
  continue;
2500
2774
  }
2501
- const result = await store.quickVerify(libName, commit, platform, {
2775
+ const result = await store.fullVerify(libName, commit, platform, {
2502
2776
  size: entry.size,
2503
2777
  fileCount: entry.fileCount,
2778
+ contentHash: entry.contentHash,
2504
2779
  });
2505
2780
  if (result.valid) {
2506
2781
  valid.push(platform);