openxiangda 1.0.230 → 1.0.232

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/lib/cli.js CHANGED
@@ -166,6 +166,7 @@ const {
166
166
  withReleaseClientSessionArgs,
167
167
  } = require('./application-environments');
168
168
  const { startDeveloperCenter } = require('./developer-center');
169
+ const { createDeliveryV2Cli } = require('./delivery-v2-cli');
169
170
  const {
170
171
  describeOpenApiOperation,
171
172
  listOpenApiOperations,
@@ -219,6 +220,8 @@ let readOnlyHttpGuardLabel = null;
219
220
  let activePublishRequestContext = null;
220
221
  let activePublishLeaseHeartbeat = null;
221
222
  let activeCommandScopedPublishLease = null;
223
+ let activeDeliveryV2Context = null;
224
+ let deliveryV2Cli = null;
222
225
  const genericGitBasePlanCache = new Map();
223
226
 
224
227
  async function main(argv) {
@@ -270,6 +273,9 @@ async function mainImpl(argv) {
270
273
  if (command === 'design') return design(rest);
271
274
  if (command === 'sdd') return sdd(rest);
272
275
  if (command === 'environment') return environment(rest);
276
+ if (['check', 'deploy', 'status', 'retry', 'rollback'].includes(command)) {
277
+ return getDeliveryV2Cli().run(command, rest);
278
+ }
273
279
  if (command === 'release') return release(rest);
274
280
  if (command === 'studio') return studio(rest);
275
281
  if (command === 'task') return task(rest);
@@ -321,6 +327,11 @@ Usage:
321
327
  openxiangda design gates|template|review [--topic code] [--json]
322
328
  openxiangda sdd init|migrate|propose|quick|bundle|approve|status|context|verify|sync|archive [change] [--change id] [--json]
323
329
  openxiangda environment init|attach|bind|swap|policy update|list|status|diff|use [preproduction|production] [--logical-app code] [--environment target] [--json]
330
+ openxiangda check [--environment preproduction|production] [--json]
331
+ openxiangda deploy <preproduction|production> [--package <sha256>] [--profile name] [--json]
332
+ openxiangda status <runId> [--environment preproduction|production] [--json]
333
+ openxiangda retry <runId> [--environment preproduction|production] [--json]
334
+ openxiangda rollback <preproduction|production> --to <appReleaseId> [--json]
324
335
  openxiangda release publish|begin|status|integration-status|renew|end [--change id] [--profile name] [--watch] [--json]
325
336
  openxiangda release ship|candidate|deploy|test|promote|rollback [--candidate id] [--environment target] [--replace-manifest --reason text] [--adopt-online-baseline --adoption-reason text] [--json]
326
337
  openxiangda task status --change <id> [--profile name] [--watch] [--json]
@@ -1451,6 +1462,120 @@ async function runOpenXiangdaInProcess(args, options = {}) {
1451
1462
  }
1452
1463
  }
1453
1464
 
1465
+ function getDeliveryV2Cli() {
1466
+ if (deliveryV2Cli) return deliveryV2Cli;
1467
+ deliveryV2Cli = createDeliveryV2Cli({
1468
+ clearDeliveryContext: clearDeliveryV2Context,
1469
+ downloadWithAuth,
1470
+ getWorkspaceTarget,
1471
+ installDeliveryContext: installDeliveryV2Context,
1472
+ loadConfig,
1473
+ requestFormWithAuth,
1474
+ requestWithAuth,
1475
+ runOpenXiangdaInProcess,
1476
+ });
1477
+ return deliveryV2Cli;
1478
+ }
1479
+
1480
+ function deliveryV2SourceRevision(packageDigest) {
1481
+ return {
1482
+ repo: `sha256:${packageDigest}`,
1483
+ repositoryId: `sha256:${packageDigest}`,
1484
+ repoAliases: [`sha256:${packageDigest}`],
1485
+ baseCommit: packageDigest,
1486
+ treeHash: packageDigest,
1487
+ branch: 'delivery-v2',
1488
+ mainBranch: null,
1489
+ remoteName: null,
1490
+ remoteUrlHash: null,
1491
+ };
1492
+ }
1493
+
1494
+ function installDeliveryV2Context(target, run, targets = {}) {
1495
+ const control = run?.control || {};
1496
+ const publishLeaseId = String(control.publishLeaseId || '').trim();
1497
+ const baselineId = String(control.baselineId || '').trim();
1498
+ const changeId = String(control.changeId || '').trim();
1499
+ const clientSessionId = String(control.clientSessionId || '').trim();
1500
+ const packageDigest = String(run?.packageDigest || '').trim();
1501
+ if (
1502
+ !publishLeaseId ||
1503
+ !baselineId ||
1504
+ !changeId ||
1505
+ !clientSessionId ||
1506
+ !/^[a-f0-9]{64}$/.test(packageDigest)
1507
+ ) {
1508
+ fail(
1509
+ 'DELIVERY_CONTROL_INVALID: ReleaseRun 缺少 publishLease/baseline/change/session/package 控制信息'
1510
+ );
1511
+ }
1512
+ const sourceRevision = deliveryV2SourceRevision(packageDigest);
1513
+ saveChangeBaseline(target, {
1514
+ baselineId,
1515
+ appType: target.appType,
1516
+ profile: target.profileName,
1517
+ changeId,
1518
+ clientSessionId,
1519
+ sourceBase: {
1520
+ repo: sourceRevision.repo,
1521
+ baseCommit: packageDigest,
1522
+ treeHash: packageDigest,
1523
+ },
1524
+ releaseSourceRevision: sourceRevision,
1525
+ resourceHeads: {},
1526
+ lifecycleState: 'active',
1527
+ });
1528
+ savePublishLease(target, {
1529
+ leaseId: publishLeaseId,
1530
+ expiresAt:
1531
+ control.leaseExpiresAt ||
1532
+ new Date(Date.now() + 60 * 60 * 1000).toISOString(),
1533
+ appType: target.appType,
1534
+ profile: target.profileName,
1535
+ changeId,
1536
+ clientSessionId,
1537
+ baseRevision: packageDigest,
1538
+ holder: 'self',
1539
+ workspace: {
1540
+ branch: 'delivery-v2',
1541
+ commit: packageDigest,
1542
+ treeHash: packageDigest,
1543
+ },
1544
+ });
1545
+ activeDeliveryV2Context = {
1546
+ runId: run.id,
1547
+ appType: target.appType,
1548
+ profileName: target.profileName,
1549
+ packageDigest,
1550
+ publishLeaseId,
1551
+ baselineId,
1552
+ changeId,
1553
+ clientSessionId,
1554
+ leaseExpiresAt: control.leaseExpiresAt,
1555
+ sourceRevision,
1556
+ targets,
1557
+ };
1558
+ activePublishRequestContext = normalizePublishContext({
1559
+ appType: target.appType,
1560
+ profileName: target.profileName,
1561
+ publishLeaseId,
1562
+ changeBaselineId: baselineId,
1563
+ changeId,
1564
+ clientSessionId,
1565
+ });
1566
+ return activeDeliveryV2Context;
1567
+ }
1568
+
1569
+ function clearDeliveryV2Context(target, run) {
1570
+ const context = activeDeliveryV2Context;
1571
+ if (!context) return;
1572
+ if (run?.id && context.runId !== run.id) return;
1573
+ clearPublishLease(target, context.publishLeaseId);
1574
+ clearChangeBaseline(target, context.baselineId);
1575
+ activePublishRequestContext = null;
1576
+ activeDeliveryV2Context = null;
1577
+ }
1578
+
1454
1579
  function normalizeReleaseWaitSeconds(flags) {
1455
1580
  const raw =
1456
1581
  readStringFlag(flags, 'wait-seconds') ||
@@ -6478,22 +6603,35 @@ function assertStagedAppReleaseScope(target, flags, resources) {
6478
6603
  }
6479
6604
  const stagedContext = assertStagedResourcesContext(target, changeId);
6480
6605
  let scope;
6481
- try {
6482
- scope = getSddChangeScope({
6483
- cwd: process.cwd(),
6484
- configText: readWorkspaceConfigText(),
6606
+ if (activeDeliveryV2Context?.changeId === changeId) {
6607
+ scope = {
6485
6608
  changeId,
6486
- });
6487
- } catch (error) {
6488
- if (/SDD 未初始化|change 不存在|ENOENT/i.test(String(error?.message || ''))) {
6489
- return {
6609
+ targets: activeDeliveryV2Context.targets || {},
6610
+ changedFiles: [],
6611
+ deliveryRunId: activeDeliveryV2Context.runId,
6612
+ };
6613
+ } else {
6614
+ try {
6615
+ scope = getSddChangeScope({
6616
+ cwd: process.cwd(),
6617
+ configText: readWorkspaceConfigText(),
6490
6618
  changeId,
6491
- scope: null,
6492
- resources: normalizeStagedAppReleaseResources(resources),
6493
- compatibility: 'unmanaged_sdd_scope',
6494
- };
6619
+ });
6620
+ } catch (error) {
6621
+ if (
6622
+ /SDD 未初始化|change 不存在|ENOENT/i.test(
6623
+ String(error?.message || '')
6624
+ )
6625
+ ) {
6626
+ return {
6627
+ changeId,
6628
+ scope: null,
6629
+ resources: normalizeStagedAppReleaseResources(resources),
6630
+ compatibility: 'unmanaged_sdd_scope',
6631
+ };
6632
+ }
6633
+ throw error;
6495
6634
  }
6496
- throw error;
6497
6635
  }
6498
6636
  const runtimeMode = getWorkspaceRuntimeModeFromConfig(
6499
6637
  readWorkspaceConfigText()
@@ -6799,6 +6937,7 @@ function appReleaseEnvironmentControl(flags = {}) {
6799
6937
  environmentId: readStringFlag(flags, 'environment-id') || undefined,
6800
6938
  candidateId: readStringFlag(flags, 'candidate-id') || undefined,
6801
6939
  deploymentId: readStringFlag(flags, 'deployment-id') || undefined,
6940
+ deliveryRunId: readStringFlag(flags, 'delivery-run-id') || undefined,
6802
6941
  });
6803
6942
  }
6804
6943
 
@@ -7528,6 +7667,30 @@ async function renewPublishLease(config, target, lease, flags = {}) {
7528
7667
  }
7529
7668
 
7530
7669
  async function resolvePublishLeaseForWrite(config, target, flags = {}) {
7670
+ if (
7671
+ activeDeliveryV2Context?.appType === target.appType &&
7672
+ activeDeliveryV2Context?.profileName === target.profileName
7673
+ ) {
7674
+ activePublishRequestContext = normalizePublishContext({
7675
+ appType: target.appType,
7676
+ profileName: target.profileName,
7677
+ publishLeaseId: activeDeliveryV2Context.publishLeaseId,
7678
+ changeBaselineId: activeDeliveryV2Context.baselineId,
7679
+ changeId: activeDeliveryV2Context.changeId,
7680
+ clientSessionId: activeDeliveryV2Context.clientSessionId,
7681
+ });
7682
+ return {
7683
+ leaseId: activeDeliveryV2Context.publishLeaseId,
7684
+ expiresAt: activeDeliveryV2Context.leaseExpiresAt,
7685
+ appType: target.appType,
7686
+ profile: target.profileName,
7687
+ changeId: activeDeliveryV2Context.changeId,
7688
+ clientSessionId: activeDeliveryV2Context.clientSessionId,
7689
+ baseRevision: activeDeliveryV2Context.packageDigest,
7690
+ holder: 'self',
7691
+ explicit: true,
7692
+ };
7693
+ }
7531
7694
  const explicitLeaseId = readStringFlag(flags, 'publish-lease-id');
7532
7695
  const changeId = readStringFlag(flags, 'change');
7533
7696
  const storedLease = getStoredPublishLease(target);
@@ -14807,7 +14970,9 @@ async function runtime(args) {
14807
14970
  // Preserve Runtime's dedicated dirty-source contract before the generic
14808
14971
  // frozen-release revision check runs inside lease resolution. The release
14809
14972
  // freeze and clean-HEAD gates still run immediately afterwards.
14810
- assertRuntimeSourceClean(process.cwd());
14973
+ if (!activeDeliveryV2Context) {
14974
+ assertRuntimeSourceClean(process.cwd());
14975
+ }
14811
14976
  // Keep build after lease acquisition. In direct-storage modes the build
14812
14977
  // embeds the provider-specific assetBaseUrl returned by the protected
14813
14978
  // upload plan; substituting the gateway URL would change chunk/publicPath
@@ -14819,13 +14984,26 @@ async function runtime(args) {
14819
14984
  const runtimeHead = await telemetry.runPhase('head', async () =>
14820
14985
  loadRuntimeHeadApp(config, target)
14821
14986
  );
14822
- const runtimeLineage = prepareRuntimeDeployLineage({
14823
- cwd: process.cwd(),
14824
- app: runtimeHead,
14825
- activate: !flags['no-activate'],
14826
- allowRollback: flags['allow-runtime-rollback'],
14827
- reason: readStringFlag(flags, 'reason'),
14828
- });
14987
+ const runtimeLineage = activeDeliveryV2Context
14988
+ ? {
14989
+ sourceRevision: activeDeliveryV2Context.sourceRevision,
14990
+ parentReleaseId: runtimeHead?.activeRuntimeReleaseId || null,
14991
+ parentSourceRevision:
14992
+ runtimeHead?.activeRuntimeSourceRevision || null,
14993
+ lineageBypassReason: null,
14994
+ lineageStatus: 'delivery-v2-package',
14995
+ legacyBootstrap: Boolean(
14996
+ runtimeHead?.activeRuntimeReleaseId &&
14997
+ !runtimeHead?.activeRuntimeSourceRevision
14998
+ ),
14999
+ }
15000
+ : prepareRuntimeDeployLineage({
15001
+ cwd: process.cwd(),
15002
+ app: runtimeHead,
15003
+ activate: !flags['no-activate'],
15004
+ allowRollback: flags['allow-runtime-rollback'],
15005
+ reason: readStringFlag(flags, 'reason'),
15006
+ });
14829
15007
  if (runtimeLineage.lineageStatus === 'rollback-bypass') {
14830
15008
  warn(
14831
15009
  `[RUNTIME_LINEAGE_BYPASS] 已允许审计式 Runtime 源码回退: reason=${runtimeLineage.lineageBypassReason}`
@@ -14904,13 +15082,15 @@ async function runtime(args) {
14904
15082
  jsonOutput: Boolean(flags.json),
14905
15083
  })
14906
15084
  );
14907
- const postBuildLineage = prepareRuntimeDeployLineage({
14908
- cwd: process.cwd(),
14909
- app: runtimeHead,
14910
- activate: !flags['no-activate'],
14911
- allowRollback: flags['allow-runtime-rollback'],
14912
- reason: readStringFlag(flags, 'reason'),
14913
- });
15085
+ const postBuildLineage = activeDeliveryV2Context
15086
+ ? runtimeLineage
15087
+ : prepareRuntimeDeployLineage({
15088
+ cwd: process.cwd(),
15089
+ app: runtimeHead,
15090
+ activate: !flags['no-activate'],
15091
+ allowRollback: flags['allow-runtime-rollback'],
15092
+ reason: readStringFlag(flags, 'reason'),
15093
+ });
14914
15094
  assertRuntimeSourceRevisionStable(
14915
15095
  runtimeLineage.sourceRevision,
14916
15096
  postBuildLineage.sourceRevision
@@ -16309,7 +16489,12 @@ function removeDirectStateResource(target, resourceType, code) {
16309
16489
  function normalizeMenuDirectBody(bound, menuItem) {
16310
16490
  const formUuid = resolveManifestFormUuid(bound, menuItem);
16311
16491
  const pageId = resolveManifestPageId(bound, menuItem);
16312
- const parentId = resolveManifestMenuId(bound, menuItem.parentCode) || menuItem.parentId || null;
16492
+ const parentId = requireResolvedMenuParentId(
16493
+ bound,
16494
+ menuItem,
16495
+ new Map(),
16496
+ 'direct menu mutation'
16497
+ );
16313
16498
  return stripUndefinedValues({
16314
16499
  resourceCode: menuItem.code || menuItem.resourceCode,
16315
16500
  name: menuItem.name || menuItem.code,
@@ -19087,7 +19272,19 @@ async function buildResourcePlan(
19087
19272
  ]);
19088
19273
  const actions = [];
19089
19274
  addPlanActions(actions, 'role', manifest.roles, existing.roles, roleEquals);
19090
- addPlanActions(actions, 'menu', manifest.menus, existing.menus, (item, current) => menuEquals(target.bound, item, current));
19275
+ const orderedMenus = orderMenuResourcesForPublish(
19276
+ manifest.menus,
19277
+ target.bound,
19278
+ existing.menus
19279
+ );
19280
+ addPlanActions(
19281
+ actions,
19282
+ 'menu',
19283
+ orderedMenus,
19284
+ existing.menus,
19285
+ (item, current) =>
19286
+ menuEquals(target.bound, item, current, existing.menus)
19287
+ );
19091
19288
  addPlanActions(actions, 'connector', manifest.connectors, existing.connectors, connectorEquals);
19092
19289
  addNotificationPlanActions(target, actions, manifest.notifications || [], existing);
19093
19290
  await addWorkflowPlanActions(config, target, actions, manifest.workflows, existing.workflows);
@@ -21410,6 +21607,14 @@ async function runResourceChangeBaselinePreflight(
21410
21607
  flags = {},
21411
21608
  publishLease = null
21412
21609
  ) {
21610
+ if (activeDeliveryV2Context) {
21611
+ return {
21612
+ ok: true,
21613
+ baselineId: activeDeliveryV2Context.baselineId,
21614
+ checkedTargets: [],
21615
+ skipped: 'trusted-delivery-v2-package',
21616
+ };
21617
+ }
21413
21618
  const onlineBaselineAdoption = resolveOnlineBaselineAdoption(
21414
21619
  flags,
21415
21620
  planActions,
@@ -22594,21 +22799,61 @@ async function publishFormSettingsResources(
22594
22799
  }
22595
22800
 
22596
22801
  async function publishMenuResources(config, target, menus, result, options = {}) {
22597
- for (const menuItem of menus) {
22598
- if (shouldSkipNoopResource(options, 'menu', menuItem.code)) {
22599
- recordNoopResource(result, 'menu', menuItem.code, options);
22802
+ if (menus.length === 0) return;
22803
+ const existingMenus = await fetchExistingMenusByCode(config, target);
22804
+ const orderedMenus = orderMenuResourcesForPublish(
22805
+ menus,
22806
+ target.bound,
22807
+ existingMenus
22808
+ );
22809
+ for (const menuItem of orderedMenus) {
22810
+ const code = menuResourceCode(menuItem);
22811
+ const plannedAction = getPlannedResourceAction(options, 'menu', code);
22812
+ const existing = existingMenus.get(code);
22813
+ if (existing && !menuPlatformId(existing)) {
22814
+ const error = new Error(
22815
+ `MENU_ONLINE_ID_REQUIRED: 在线 menu ${code} 缺少 menuId,禁止执行恢复发布`
22816
+ );
22817
+ error.code = 'MENU_ONLINE_ID_REQUIRED';
22818
+ throw error;
22819
+ }
22820
+ if (
22821
+ ['noop', 'update'].includes(plannedAction?.action) &&
22822
+ !menuPlatformId(existing)
22823
+ ) {
22824
+ const error = new Error(
22825
+ `MENU_PLAN_TARGET_MISSING: menu ${code} 的冻结计划为 ${plannedAction.action},但写入前在线菜单已不存在`
22826
+ );
22827
+ error.code = 'MENU_PLAN_TARGET_MISSING';
22828
+ throw error;
22829
+ }
22830
+ }
22831
+ for (const menuItem of orderedMenus) {
22832
+ const code = menuResourceCode(menuItem);
22833
+ const existing = existingMenus.get(code);
22834
+ if (shouldSkipNoopResource(options, 'menu', code)) {
22835
+ const existingId = menuPlatformId(existing);
22836
+ saveMenuResource(target, code, existingId, {
22837
+ formUuid: existing.formUuid,
22838
+ pageId: existing.pageId,
22839
+ parentId: existing.parentId,
22840
+ routeCode: existing.routeCode,
22841
+ path: existing.path,
22842
+ });
22843
+ recordNoopResource(result, 'menu', code, options);
22600
22844
  continue;
22601
22845
  }
22602
- const existing = await findExistingMenu(config, target, menuItem.code);
22603
22846
  const formUuid = resolveManifestFormUuid(target.bound, menuItem);
22604
22847
  const pageId = resolveManifestPageId(target.bound, menuItem);
22605
- const parentId =
22606
- resolveManifestMenuId(target.bound, menuItem.parentCode) ||
22607
- menuItem.parentId ||
22608
- null;
22848
+ const parentId = requireResolvedMenuParentId(
22849
+ target.bound,
22850
+ menuItem,
22851
+ existingMenus,
22852
+ `resource publish menu ${code}`
22853
+ );
22609
22854
  const body = {
22610
- resourceCode: menuItem.code,
22611
- name: menuItem.name || menuItem.code,
22855
+ resourceCode: code,
22856
+ name: menuItem.name || code,
22612
22857
  type: menuItem.type || 'nav',
22613
22858
  formUuid,
22614
22859
  pageId,
@@ -22619,11 +22864,12 @@ async function publishMenuResources(config, target, menus, result, options = {})
22619
22864
  routeCode: menuItem.routeCode || null,
22620
22865
  path: menuItem.path || null,
22621
22866
  };
22867
+ const existingId = menuPlatformId(existing);
22622
22868
  let data = existing
22623
22869
  ? await requestWithAuth(
22624
22870
  config,
22625
22871
  target.profileName,
22626
- `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/menus/${encodeURIComponent(existing.id)}`,
22872
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/menus/${encodeURIComponent(existingId)}`,
22627
22873
  { method: 'POST', body }
22628
22874
  )
22629
22875
  : await requestWithAuth(
@@ -22632,37 +22878,54 @@ async function publishMenuResources(config, target, menus, result, options = {})
22632
22878
  `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/menus`,
22633
22879
  { method: 'POST', body }
22634
22880
  );
22635
- if (data?.id) {
22636
- saveMenuResource(target, menuItem.code, data.id, {
22637
- resourceCode: menuItem.code,
22638
- name: data.name,
22639
- type: data.type,
22640
- ...(formUuid ? { formUuid } : {}),
22641
- ...(pageId ? { pageId } : {}),
22642
- ...(menuItem.pageCode ? { pageCode: menuItem.pageCode } : {}),
22643
- ...(menuItem.routeCode || data.routeCode
22644
- ? { routeCode: data.routeCode || menuItem.routeCode }
22645
- : {}),
22646
- ...(menuItem.path || data.path ? { path: data.path || menuItem.path } : {}),
22647
- });
22881
+ if (!data?.id) {
22882
+ const error = new Error(
22883
+ `MENU_WRITE_ID_REQUIRED: menu ${code} 写入成功响应缺少 menuId,禁止继续发布子菜单`
22884
+ );
22885
+ error.code = 'MENU_WRITE_ID_REQUIRED';
22886
+ throw error;
22648
22887
  }
22649
- result.published.push({ kind: 'menu', code: menuItem.code, action: existing ? 'update' : 'create', id: data?.id });
22888
+ saveMenuResource(target, code, data.id, {
22889
+ resourceCode: code,
22890
+ name: data.name,
22891
+ type: data.type,
22892
+ ...(formUuid ? { formUuid } : {}),
22893
+ ...(pageId ? { pageId } : {}),
22894
+ ...(menuItem.pageCode ? { pageCode: menuItem.pageCode } : {}),
22895
+ ...(menuItem.routeCode || data.routeCode
22896
+ ? { routeCode: data.routeCode || menuItem.routeCode }
22897
+ : {}),
22898
+ ...(menuItem.path || data.path ? { path: data.path || menuItem.path } : {}),
22899
+ parentId: data.parentId || parentId,
22900
+ });
22901
+ existingMenus.set(code, {
22902
+ ...(existing || {}),
22903
+ ...body,
22904
+ ...data,
22905
+ id: data.id,
22906
+ });
22907
+ result.published.push({
22908
+ kind: 'menu',
22909
+ code,
22910
+ action: existing ? 'update' : 'create',
22911
+ id: data?.id,
22912
+ });
22650
22913
  }
22651
22914
  }
22652
22915
 
22653
- async function findExistingMenu(config, target, code) {
22916
+ async function fetchExistingMenusByCode(config, target) {
22654
22917
  const data = await requestWithAuth(
22655
22918
  config,
22656
22919
  target.profileName,
22657
22920
  `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/menus`
22658
22921
  );
22659
- const items = flattenItems(normalizeItems(data));
22660
- const stateId = target.bound.resources?.menus?.[code]?.menuId;
22661
- if (stateId) {
22662
- const byState = items.find(item => item.id === stateId);
22663
- if (byState) return byState;
22664
- }
22665
- return items.find(item => item.resourceCode === code);
22922
+ const menus = new Map();
22923
+ indexByCode(
22924
+ menus,
22925
+ flattenItems(normalizeItems(data)),
22926
+ item => item.resourceCode
22927
+ );
22928
+ return menus;
22666
22929
  }
22667
22930
 
22668
22931
  async function publishConnectorResources(config, target, connectors, result, options = {}) {
@@ -26026,7 +26289,123 @@ function resolveManifestPageId(bound, item) {
26026
26289
 
26027
26290
  function resolveManifestMenuId(bound, menuCode) {
26028
26291
  if (!menuCode) return undefined;
26029
- return bound.resources?.menus?.[menuCode]?.menuId || menuCode;
26292
+ return bound.resources?.menus?.[menuCode]?.menuId;
26293
+ }
26294
+
26295
+ function menuResourceCode(menuItem) {
26296
+ return String(menuItem?.code || menuItem?.resourceCode || '').trim();
26297
+ }
26298
+
26299
+ function menuPlatformId(menuItem) {
26300
+ const value = menuItem?.id || menuItem?.menuId;
26301
+ return value ? String(value) : undefined;
26302
+ }
26303
+
26304
+ function resolveMenuParentId(bound, menuItem, existingMenus = new Map()) {
26305
+ const parentCode = String(menuItem?.parentCode || '').trim();
26306
+ if (!parentCode) return menuItem?.parentId || null;
26307
+ const boundId = resolveManifestMenuId(bound, parentCode);
26308
+ const onlineId = menuPlatformId(existingMenus.get(parentCode));
26309
+ return onlineId || boundId || menuItem?.parentId;
26310
+ }
26311
+
26312
+ function menuParentResolutionError(code, parentCode, label) {
26313
+ const error = new Error(
26314
+ `MENU_PARENT_UNRESOLVED: ${label || `menu ${code}`} 无法将 parentCode ${parentCode} 解析为在线 menuId`
26315
+ );
26316
+ error.code = 'MENU_PARENT_UNRESOLVED';
26317
+ return error;
26318
+ }
26319
+
26320
+ function requireResolvedMenuParentId(
26321
+ bound,
26322
+ menuItem,
26323
+ existingMenus = new Map(),
26324
+ label
26325
+ ) {
26326
+ const parentId = resolveMenuParentId(bound, menuItem, existingMenus);
26327
+ if (menuItem?.parentCode && !parentId) {
26328
+ throw menuParentResolutionError(
26329
+ menuResourceCode(menuItem),
26330
+ menuItem.parentCode,
26331
+ label
26332
+ );
26333
+ }
26334
+ return parentId || null;
26335
+ }
26336
+
26337
+ function orderMenuResourcesForPublish(
26338
+ menus = [],
26339
+ bound = {},
26340
+ existingMenus = new Map()
26341
+ ) {
26342
+ const items = Array.isArray(menus) ? menus : [];
26343
+ const byCode = new Map();
26344
+ const sourceIndex = new Map();
26345
+ for (const [index, menuItem] of items.entries()) {
26346
+ const code = menuResourceCode(menuItem);
26347
+ if (!code) {
26348
+ const error = new Error('MENU_CODE_REQUIRED: menu resource 缺少 code');
26349
+ error.code = 'MENU_CODE_REQUIRED';
26350
+ throw error;
26351
+ }
26352
+ if (byCode.has(code)) {
26353
+ const error = new Error(`MENU_CODE_DUPLICATE: menu code ${code} 重复`);
26354
+ error.code = 'MENU_CODE_DUPLICATE';
26355
+ throw error;
26356
+ }
26357
+ byCode.set(code, menuItem);
26358
+ sourceIndex.set(code, index);
26359
+ }
26360
+
26361
+ const children = new Map();
26362
+ const indegree = new Map([...byCode.keys()].map(code => [code, 0]));
26363
+ for (const [code, menuItem] of byCode) {
26364
+ const parentCode = String(menuItem.parentCode || '').trim();
26365
+ if (!parentCode) continue;
26366
+ if (!byCode.has(parentCode)) {
26367
+ if (!resolveMenuParentId(bound, menuItem, existingMenus)) {
26368
+ throw menuParentResolutionError(code, parentCode, `menu ${code}`);
26369
+ }
26370
+ continue;
26371
+ }
26372
+ indegree.set(code, indegree.get(code) + 1);
26373
+ const siblings = children.get(parentCode) || [];
26374
+ siblings.push(code);
26375
+ children.set(parentCode, siblings);
26376
+ }
26377
+
26378
+ const bySourceOrder = (left, right) =>
26379
+ sourceIndex.get(left) - sourceIndex.get(right);
26380
+ const ready = [...indegree.entries()]
26381
+ .filter(([, count]) => count === 0)
26382
+ .map(([code]) => code)
26383
+ .sort(bySourceOrder);
26384
+ const ordered = [];
26385
+ while (ready.length > 0) {
26386
+ const code = ready.shift();
26387
+ ordered.push(byCode.get(code));
26388
+ for (const childCode of (children.get(code) || []).sort(bySourceOrder)) {
26389
+ const next = indegree.get(childCode) - 1;
26390
+ indegree.set(childCode, next);
26391
+ if (next === 0) {
26392
+ ready.push(childCode);
26393
+ ready.sort(bySourceOrder);
26394
+ }
26395
+ }
26396
+ }
26397
+ if (ordered.length !== items.length) {
26398
+ const cycleCodes = [...indegree.entries()]
26399
+ .filter(([, count]) => count > 0)
26400
+ .map(([code]) => code)
26401
+ .sort(bySourceOrder);
26402
+ const error = new Error(
26403
+ `MENU_PARENT_CYCLE: menu parentCode 存在循环依赖: ${cycleCodes.join(' -> ')}`
26404
+ );
26405
+ error.code = 'MENU_PARENT_CYCLE';
26406
+ throw error;
26407
+ }
26408
+ return ordered;
26030
26409
  }
26031
26410
 
26032
26411
  function resolveAutomationManifestPayload(target, automationItem) {
@@ -26178,12 +26557,16 @@ function unwrapFormSettingPlanValue(value, key) {
26178
26557
  return value;
26179
26558
  }
26180
26559
 
26181
- function menuEquals(bound, desired, existing) {
26560
+ function menuEquals(bound, desired, existing, existingMenus = new Map()) {
26182
26561
  if (!existing) return false;
26183
26562
  const desiredFormUuid = resolveManifestFormUuid(bound, desired);
26184
26563
  const desiredPageId = resolveManifestPageId(bound, desired);
26185
- const desiredParentId =
26186
- resolveManifestMenuId(bound, desired.parentCode) || desired.parentId || null;
26564
+ const desiredParentId = resolveMenuParentId(
26565
+ bound,
26566
+ desired,
26567
+ existingMenus
26568
+ );
26569
+ if (desired.parentCode && !desiredParentId) return false;
26187
26570
  return (
26188
26571
  String(existing.name || '') === String(desired.name || desired.code || '') &&
26189
26572
  String(existing.type || '') === String(desired.type || 'nav') &&
@@ -28495,6 +28878,7 @@ function buildExplicitSddReleasePlan(targets, options = {}) {
28495
28878
  }
28496
28879
 
28497
28880
  function assertSddReleaseGate(plan, flags, command) {
28881
+ if (activeDeliveryV2Context) return;
28498
28882
  if (!plan?.sdd || plan.sdd.errors.length === 0) return;
28499
28883
  const nonBypassable = new Set([
28500
28884
  'sdd-release-direct-form-schema-forbidden',
@@ -28785,7 +29169,8 @@ function isPublishMutationRequest(apiPath, options = {}) {
28785
29169
  if (
28786
29170
  normalizedPath.includes('/publish-lease/') ||
28787
29171
  normalizedPath.endsWith('/publish-lease/acquire') ||
28788
- normalizedPath.includes('/change-baselines')
29172
+ normalizedPath.includes('/change-baselines') ||
29173
+ normalizedPath.includes('/delivery/')
28789
29174
  ) {
28790
29175
  return false;
28791
29176
  }