openxiangda 1.0.270 → 1.0.271

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.
@@ -1062,6 +1062,22 @@ function withManagedReleaseForwardedFlags(stepId, args = [], flags = {}) {
1062
1062
  return forwarded;
1063
1063
  }
1064
1064
 
1065
+ function withLegacyReleaseForwardedFlags(stepId, args = [], flags = {}) {
1066
+ let forwarded = [...args];
1067
+ const isExactResourcePublish =
1068
+ forwarded[0] === 'resource' &&
1069
+ forwarded[1] === 'publish' &&
1070
+ (forwarded.includes('--only') || forwarded.includes('--code'));
1071
+ if (!isExactResourcePublish) return forwarded;
1072
+ if (stepId === 'backend-stage' && flags['replace-manifest']) {
1073
+ forwarded.push('--replace-manifest');
1074
+ const replacementReason = String(flags.reason || '').trim();
1075
+ if (replacementReason) forwarded.push('--reason', replacementReason);
1076
+ }
1077
+ forwarded = withOnlineBaselineAdoptionFlags(forwarded, flags);
1078
+ return forwarded;
1079
+ }
1080
+
1065
1081
  function withOnlineBaselineAdoptionFlags(args = [], flags = {}) {
1066
1082
  const forwarded = [...args];
1067
1083
  const isExactResourcePublish =
@@ -1141,6 +1157,7 @@ module.exports = {
1141
1157
  resolveEnvironmentTarget,
1142
1158
  saveCandidate,
1143
1159
  sha256Canonical,
1160
+ withLegacyReleaseForwardedFlags,
1144
1161
  withManagedReleaseForwardedFlags,
1145
1162
  withOnlineBaselineAdoptionFlags,
1146
1163
  withReleaseClientSessionArgs,
package/lib/cli.js CHANGED
@@ -164,6 +164,7 @@ const {
164
164
  resolveEnvironmentTarget,
165
165
  saveCandidate,
166
166
  sha256Canonical,
167
+ withLegacyReleaseForwardedFlags,
167
168
  withManagedReleaseForwardedFlags,
168
169
  withOnlineBaselineAdoptionFlags,
169
170
  withReleaseClientSessionArgs,
@@ -345,7 +346,7 @@ Usage:
345
346
  openxiangda status <runId> [--environment preproduction|production] [--json]
346
347
  openxiangda retry <runId> [--environment preproduction|production] [--json]
347
348
  openxiangda rollback <preproduction|production> --to <appReleaseId> [--json]
348
- openxiangda release publish|begin|status|integration-status|renew|end [--change id] [--profile name] [--adopt-online-baseline --adoption-reason text] [--watch] [--json]
349
+ openxiangda release publish|begin|status|integration-status|renew|end [--change id] [--profile name] [--replace-manifest --reason text] [--adopt-online-baseline --adoption-reason text] [--watch] [--json]
349
350
  openxiangda release ship|candidate|deploy|reconcile|test|fail|promote|rollback [--candidate id] [--deployment id] [--environment target] [--replace-manifest] [--allow-runtime-rollback --reason text] [--adopt-online-baseline --adoption-reason text] [--confirm-production] [--json]
350
351
  openxiangda task status --change <id> [--profile name] [--watch] [--json]
351
352
  openxiangda release backend-head|backend-list|backend-detail|backend-diff|backend-rollback|backend-abort|backend-retry [releaseId] [--profile name] [--json]
@@ -1635,6 +1636,16 @@ async function runOpenXiangdaInProcess(args, options = {}) {
1635
1636
  let stdout = '';
1636
1637
  const originalWrite = process.stdout.write;
1637
1638
  const previousExitCode = process.exitCode;
1639
+ const environment = Object.entries(options.env || {}).map(([key, value]) => ({
1640
+ key,
1641
+ existed: Object.prototype.hasOwnProperty.call(process.env, key),
1642
+ value: process.env[key],
1643
+ next: value,
1644
+ }));
1645
+ for (const entry of environment) {
1646
+ if (entry.next === undefined || entry.next === null) delete process.env[entry.key];
1647
+ else process.env[entry.key] = String(entry.next);
1648
+ }
1638
1649
  process.exitCode = undefined;
1639
1650
  process.stdout.write = function capture(chunk, encoding, callback) {
1640
1651
  stdout += Buffer.isBuffer(chunk) ? chunk.toString(encoding) : String(chunk);
@@ -1662,6 +1673,10 @@ async function runOpenXiangdaInProcess(args, options = {}) {
1662
1673
  }
1663
1674
  throw error;
1664
1675
  } finally {
1676
+ for (const entry of environment) {
1677
+ if (entry.existed) process.env[entry.key] = entry.value;
1678
+ else delete process.env[entry.key];
1679
+ }
1665
1680
  process.stdout.write = originalWrite;
1666
1681
  process.exitCode = previousExitCode;
1667
1682
  }
@@ -2111,7 +2126,7 @@ function decorateEnvironmentReleaseSteps(steps, target, flags = {}) {
2111
2126
  for (const step of steps) {
2112
2127
  step.args = managedEnvironment
2113
2128
  ? withManagedReleaseForwardedFlags(step.id, step.args, flags)
2114
- : withOnlineBaselineAdoptionFlags(step.args, flags);
2129
+ : withLegacyReleaseForwardedFlags(step.id, step.args, flags);
2115
2130
  if (!managedEnvironment) {
2116
2131
  step.command = commandFromArgs(step.args);
2117
2132
  continue;
@@ -2202,11 +2217,104 @@ function assertReleasePublishAdoptionScope(steps = [], flags = {}) {
2202
2217
  }
2203
2218
  }
2204
2219
 
2220
+ function assertReleasePublishManifestReplacementFlags(flags = {}) {
2221
+ const replacementRequested = Boolean(flags['replace-manifest']);
2222
+ const reasonSupplied = Object.prototype.hasOwnProperty.call(flags, 'reason');
2223
+ const reason = String(flags.reason || '').trim();
2224
+ if (!replacementRequested && reasonSupplied && !flags['allow-runtime-rollback']) {
2225
+ const error = new Error(
2226
+ 'RELEASE_PUBLISH_REPLACEMENT_FLAG_REQUIRED: --reason 必须与 --replace-manifest 一起使用;命令已在任何发布租约或资源写入前终止'
2227
+ );
2228
+ error.code = 'RELEASE_PUBLISH_REPLACEMENT_FLAG_REQUIRED';
2229
+ throw error;
2230
+ }
2231
+ if (replacementRequested && reason.length < 8) {
2232
+ const error = new Error(
2233
+ 'RELEASE_PUBLISH_REPLACEMENT_REASON_REQUIRED: --replace-manifest 必须提供至少 8 个字符的 --reason;命令已在任何发布租约或资源写入前终止'
2234
+ );
2235
+ error.code = 'RELEASE_PUBLISH_REPLACEMENT_REASON_REQUIRED';
2236
+ throw error;
2237
+ }
2238
+ }
2239
+
2240
+ function assertReleasePublishManifestReplacementScope(steps = [], flags = {}) {
2241
+ if (!flags['replace-manifest']) return;
2242
+ const replacementSteps = steps.filter(
2243
+ step =>
2244
+ step.id === 'backend-stage' &&
2245
+ step.args?.includes('--replace-manifest') &&
2246
+ (step.args.includes('--only') || step.args.includes('--code'))
2247
+ );
2248
+ if (replacementSteps.length !== 1) {
2249
+ const error = new Error(
2250
+ 'RELEASE_PUBLISH_REPLACEMENT_SCOPE_REQUIRED: 当前发布计划没有唯一的精确 Backend stage 可接收 --replace-manifest;命令已在任何发布租约或资源写入前终止'
2251
+ );
2252
+ error.code = 'RELEASE_PUBLISH_REPLACEMENT_SCOPE_REQUIRED';
2253
+ throw error;
2254
+ }
2255
+ }
2256
+
2205
2257
  function changeIdFromStep(step) {
2206
2258
  const index = step.args.indexOf('--change');
2207
2259
  return index >= 0 ? step.args[index + 1] : 'change';
2208
2260
  }
2209
2261
 
2262
+ function assertLegacyPageReleaseStaged(step, changeId, target) {
2263
+ if (step.stagedKind !== 'PageRelease') return;
2264
+ const file = stagedResourcesFileForChange(changeId, target.deploymentId);
2265
+ const resources = fs.existsSync(file)
2266
+ ? normalizeStagedAppReleaseResources(
2267
+ JSON.parse(fs.readFileSync(file, 'utf8'))
2268
+ )
2269
+ : [];
2270
+ const pageRelease = resources.find(resource => resource.kind === 'PageRelease');
2271
+ if (pageRelease) {
2272
+ assertLegacyPageReleaseCoverage(step, pageRelease);
2273
+ return;
2274
+ }
2275
+ const error = new Error(
2276
+ 'PAGE_RELEASE_STAGE_EVIDENCE_REQUIRED: legacy workspace publish 未记录 staged PageRelease;已阻止 Root finalize,工作区发布脚本必须遵守 OPENXIANGDA_PAGE_STAGE_ONLY=1 并通过当前 OpenXiangda CLI 登记 staged child'
2277
+ );
2278
+ error.code = 'PAGE_RELEASE_STAGE_EVIDENCE_REQUIRED';
2279
+ throw error;
2280
+ }
2281
+
2282
+ function legacyWorkspaceStepPageCodes(step = {}) {
2283
+ const onlyIndex = Array.isArray(step.args) ? step.args.indexOf('--only') : -1;
2284
+ if (onlyIndex < 0) return [];
2285
+ return unique(
2286
+ String(step.args[onlyIndex + 1] || '')
2287
+ .split(',')
2288
+ .map(value => value.trim())
2289
+ .filter(value => value.startsWith('pages/'))
2290
+ .map(value => value.slice('pages/'.length))
2291
+ .filter(Boolean)
2292
+ ).sort();
2293
+ }
2294
+
2295
+ function assertLegacyPageReleaseCoverage(step, pageRelease) {
2296
+ const expected = legacyWorkspaceStepPageCodes(step);
2297
+ const actual = unique(
2298
+ Array.isArray(pageRelease?.metadata?.selectedPageCodes)
2299
+ ? pageRelease.metadata.selectedPageCodes
2300
+ .map(value => String(value || '').trim())
2301
+ .filter(Boolean)
2302
+ : []
2303
+ ).sort();
2304
+ if (
2305
+ expected.length > 0 &&
2306
+ expected.length === actual.length &&
2307
+ expected.every((code, index) => code === actual[index])
2308
+ ) {
2309
+ return;
2310
+ }
2311
+ const error = new Error(
2312
+ `PAGE_RELEASE_STAGE_SCOPE_MISMATCH: staged PageRelease selectedPageCodes 与 workspace 计划不一致;expected=${expected.join(',') || '-'} actual=${actual.join(',') || '-'}`
2313
+ );
2314
+ error.code = 'PAGE_RELEASE_STAGE_SCOPE_MISMATCH';
2315
+ throw error;
2316
+ }
2317
+
2210
2318
  async function publishWorkspaceRelease(config, target, flags = {}) {
2211
2319
  const changeId = readStringFlag(flags, 'change');
2212
2320
  const deploymentId = readStringFlag(flags, 'deployment-id') || null;
@@ -2216,6 +2324,7 @@ async function publishWorkspaceRelease(config, target, flags = {}) {
2216
2324
  fail('用法: openxiangda release publish --change <id> --profile <name>');
2217
2325
  }
2218
2326
  assertReleasePublishAdoptionFlags(flags);
2327
+ assertReleasePublishManifestReplacementFlags(flags);
2219
2328
  assertManagedRuntimeRollbackFlags(flags);
2220
2329
  const releaseCandidate = flags.__verifiedCandidate
2221
2330
  ? assertLocalCandidate(flags.__verifiedCandidate)
@@ -2243,6 +2352,7 @@ async function publishWorkspaceRelease(config, target, flags = {}) {
2243
2352
  );
2244
2353
  decorateEnvironmentReleaseSteps(steps, target, flags);
2245
2354
  assertReleasePublishAdoptionScope(steps, flags);
2355
+ assertReleasePublishManifestReplacementScope(steps, flags);
2246
2356
  const releaseScope = getSddChangeScope({
2247
2357
  cwd: process.cwd(),
2248
2358
  configText: readWorkspaceConfigText(),
@@ -2264,6 +2374,12 @@ async function publishWorkspaceRelease(config, target, flags = {}) {
2264
2374
  runtimeMode: plan.workspace.runtimeMode,
2265
2375
  targets: plan.activationTargets,
2266
2376
  commands: steps.map(step => step.command),
2377
+ executionSteps: steps.map(step => ({
2378
+ id: step.id,
2379
+ command: step.command,
2380
+ env: step.env || {},
2381
+ stagedKind: step.stagedKind || null,
2382
+ })),
2267
2383
  candidateHash: releaseCandidate?.candidateHash || null,
2268
2384
  });
2269
2385
  if (flags['dry-run']) {
@@ -2379,6 +2495,7 @@ async function publishWorkspaceRelease(config, target, flags = {}) {
2379
2495
  ...steps.map(step => ({
2380
2496
  id: step.id,
2381
2497
  command: step.command,
2498
+ env: step.env || {},
2382
2499
  stagedKind: step.stagedKind,
2383
2500
  status: 'pending',
2384
2501
  })),
@@ -2478,7 +2595,10 @@ async function publishWorkspaceRelease(config, target, flags = {}) {
2478
2595
  execution
2479
2596
  );
2480
2597
  warn(`发布步骤开始: ${step.id}`);
2481
- const result = await runOpenXiangdaInProcess(step.args);
2598
+ const result = await runOpenXiangdaInProcess(step.args, {
2599
+ env: step.env,
2600
+ });
2601
+ assertLegacyPageReleaseStaged(step, changeId, target);
2482
2602
  journalStep.status = 'completed';
2483
2603
  journalStep.completedAt = new Date().toISOString();
2484
2604
  journalStep.result = summarizeReleaseStepResult(result);
@@ -5380,7 +5500,7 @@ async function release(args) {
5380
5500
  const { flags, positional } = parseArgs(rest);
5381
5501
  if (wantsSubcommandHelp(subcommand, flags)) {
5382
5502
  print([
5383
- '用法: openxiangda release publish|begin|status|explain|integration-status|renew|end [--change id] [--profile name] [--adopt-online-baseline --adoption-reason text] [--json]',
5503
+ '用法: openxiangda release publish|begin|status|explain|integration-status|renew|end [--change id] [--profile name] [--replace-manifest --reason text] [--adopt-online-baseline --adoption-reason text] [--json]',
5384
5504
  ' openxiangda release ship|candidate|deploy|reconcile|test|fail|promote|rollback [--candidate id] [--deployment id] [--environment target] [--replace-manifest] [--allow-runtime-rollback --reason text] [--adopt-online-baseline --adoption-reason text] [--confirm-production] [--json]',
5385
5505
  ' openxiangda release backend-head|backend-list|backend-detail|backend-diff|backend-rollback|backend-abort|backend-retry [releaseId] [--profile name] [--json]',
5386
5506
  ' openxiangda release app-capture|app-head|app-list|app-detail|app-diff|app-post-commit|app-retry|app-prepare|app-verify|app-activate|app-finalize|app-rollback|app-abort [releaseId] [--staged-resources-json <JSON|file>] [--activate-staged-children] [--break-glass-adopt-verified-root --reason text] [--force-activate-without-validation] [--profile name] [--json]',
@@ -5393,6 +5513,7 @@ async function release(args) {
5393
5513
  ' openxiangda release ship --change <id> --profile <name> --confirm-production',
5394
5514
  ' openxiangda release fail --deployment <id> --message "UAT 未通过" --code UAT_FAILED --details-json <JSON|file> --environment preproduction --profile <name>',
5395
5515
  ' openxiangda release publish --change <id> --profile <name>',
5516
+ ' openxiangda release publish --change <id> --profile <name> --replace-manifest --reason "已审查精确 Function manifest"',
5396
5517
  ' openxiangda release publish --change <id> --profile <name> --adopt-online-baseline --adoption-reason "已审计目标来自多次历史发布"',
5397
5518
  ' openxiangda release begin --change <id> --profile <name>',
5398
5519
  ' openxiangda resource publish function --only <code> --change <id> --profile <name>',
@@ -5413,6 +5534,7 @@ async function release(args) {
5413
5534
  ' - ship 当前不支持 --dry-run;任何未声明参数都会在 candidate/deployment 写入前 fail-closed。需要只读检查时使用 environment status/diff 与 release status。',
5414
5535
  ' - publish 默认等待租约并按私有执行日志恢复;上次写结果不确定时必须只读核对后显式 --resume-after-review。',
5415
5536
  ' - 旧式非环境托管 publish 也会把线上基线采纳意图仅透传给精确的 resource publish --only/--code 阶段;无精确资源阶段、原因不足或单独提供 reason 均在获取租约前 fail-closed。',
5537
+ ' - 旧式非环境托管 publish 的 --replace-manifest 与 --reason 必须成对,只透传给唯一的精确 Backend Function/Automation stage,不进入 Form、Page、Workflow 或 App finalize。',
5416
5538
  ' - app-head 默认只输出紧凑 head 摘要;需要完整 manifest 时显式追加 --full。',
5417
5539
  ' - begin 获取应用级单写者 promotion lease;不同工作区仍可并行开发和验证。',
5418
5540
  ' - 带 --change 的 resource/runtime 写命令在没有本地 lease 时会自动 begin,并在后续命令复用。',
@@ -11408,6 +11530,105 @@ function outputPageResult(data, flags = {}) {
11408
11530
  print(JSON.stringify(data, null, 2));
11409
11531
  }
11410
11532
 
11533
+ const PAGE_PUBLISH_MAX_DEFINITIONS = 200;
11534
+ const PAGE_PUBLISH_MAX_JSON_BYTES = 2 * 1024 * 1024;
11535
+
11536
+ function normalizePagePublishDefinitions(positional, flags = {}) {
11537
+ const pageCode = String(positional?.[0] || '').trim();
11538
+ const hasPagesJson = Object.prototype.hasOwnProperty.call(flags, 'pages-json');
11539
+ let definitions;
11540
+
11541
+ if (hasPagesJson) {
11542
+ if (pageCode) {
11543
+ fail('page publish 使用 --pages-json 时不能再提供单页 pageCode');
11544
+ }
11545
+ const input = readJsonArg(readStringFlag(flags, 'pages-json'), 'pages-json');
11546
+ definitions = Array.isArray(input) ? input : input?.pages;
11547
+ } else {
11548
+ if (!pageCode || !flags['entry-url']) {
11549
+ fail(
11550
+ '用法: openxiangda page publish <pageCode> --entry-url <url> --version <v> --build-id <id> [--activate]\n' +
11551
+ ' openxiangda page publish --pages-json <JSON|file> --version <v> --build-id <id> [--activate]'
11552
+ );
11553
+ }
11554
+ definitions = [
11555
+ {
11556
+ code: pageCode,
11557
+ name: flags.name || pageCode,
11558
+ description: flags.description || '',
11559
+ route: flags.route ? JSON.parse(flags.route) : {},
11560
+ props: flags.props ? JSON.parse(flags.props) : {},
11561
+ runtime: {
11562
+ entryUrl: flags['entry-url'],
11563
+ cssUrls: splitList(flags['css-urls']),
11564
+ jsUrls: splitList(flags['js-urls']),
11565
+ framework: flags.framework || 'react',
11566
+ frameworkVersion: flags['framework-version'] || '',
11567
+ cssIsolation: flags['css-isolation'] || 'none',
11568
+ format: flags.format || 'esm',
11569
+ },
11570
+ menu: flags.menu
11571
+ ? {
11572
+ enabled: true,
11573
+ name: flags['menu-name'] || flags.name || pageCode,
11574
+ parentId: flags['menu-parent-id'] || null,
11575
+ icon: flags['menu-icon'] || null,
11576
+ }
11577
+ : { enabled: false },
11578
+ },
11579
+ ];
11580
+ }
11581
+
11582
+ if (!Array.isArray(definitions) || definitions.length === 0) {
11583
+ fail('PAGE_PUBLISH_DEFINITIONS_REQUIRED: pages-json 必须包含非空页面定义数组');
11584
+ }
11585
+ if (definitions.length > PAGE_PUBLISH_MAX_DEFINITIONS) {
11586
+ fail(
11587
+ `PAGE_PUBLISH_BATCH_TOO_LARGE: 单次最多发布 ${PAGE_PUBLISH_MAX_DEFINITIONS} 个页面`
11588
+ );
11589
+ }
11590
+ const jsonBytes = Buffer.byteLength(JSON.stringify(definitions), 'utf8');
11591
+ if (jsonBytes > PAGE_PUBLISH_MAX_JSON_BYTES) {
11592
+ fail(
11593
+ `PAGE_PUBLISH_BATCH_TOO_LARGE: 页面定义 JSON 不得超过 ${PAGE_PUBLISH_MAX_JSON_BYTES} 字节`
11594
+ );
11595
+ }
11596
+
11597
+ const seen = new Set();
11598
+ return definitions.map((definition, index) => {
11599
+ if (!definition || typeof definition !== 'object' || Array.isArray(definition)) {
11600
+ fail(`PAGE_PUBLISH_DEFINITION_INVALID: pages-json[${index}] 必须是对象`);
11601
+ }
11602
+ const code = String(definition.code || '').trim();
11603
+ const runtime = definition.runtime;
11604
+ if (!code || code.length > 128) {
11605
+ fail(
11606
+ `PAGE_PUBLISH_DEFINITION_INVALID: pages-json[${index}].code 必须是 1-128 个字符`
11607
+ );
11608
+ }
11609
+ const stableCode = code.toLowerCase();
11610
+ if (seen.has(stableCode)) {
11611
+ fail(`PAGE_PUBLISH_DEFINITION_DUPLICATE: 页面 code 重复: ${code}`);
11612
+ }
11613
+ seen.add(stableCode);
11614
+ if (
11615
+ !runtime ||
11616
+ typeof runtime !== 'object' ||
11617
+ Array.isArray(runtime) ||
11618
+ !String(runtime.entryUrl || '').trim()
11619
+ ) {
11620
+ fail(
11621
+ `PAGE_PUBLISH_DEFINITION_INVALID: 页面 ${code} 缺少 runtime.entryUrl`
11622
+ );
11623
+ }
11624
+ return {
11625
+ ...clonePlainJson(definition),
11626
+ code,
11627
+ name: String(definition.name || code),
11628
+ };
11629
+ });
11630
+ }
11631
+
11411
11632
  async function page(args) {
11412
11633
  const [subcommand, ...rest] = args;
11413
11634
  const { flags, positional } = parseArgs(rest);
@@ -11529,78 +11750,128 @@ async function page(args) {
11529
11750
  }
11530
11751
 
11531
11752
  if (subcommand === 'publish') {
11532
- const [pageCode] = positional;
11533
- if (!pageCode || !flags['entry-url'] || !flags.version || !flags['build-id']) {
11753
+ if (!flags.version || !flags['build-id']) {
11534
11754
  fail(
11535
- '用法: openxiangda page publish <pageCode> --entry-url <url> --version <v> --build-id <id> [--activate]'
11755
+ '用法: openxiangda page publish <pageCode> --entry-url <url> --version <v> --build-id <id> [--activate]\n' +
11756
+ ' openxiangda page publish --pages-json <JSON|file> --version <v> --build-id <id> [--activate]'
11536
11757
  );
11537
11758
  }
11759
+ const pageDefinitions = normalizePagePublishDefinitions(positional, flags);
11760
+ const pageCodes = pageDefinitions.map(definition => definition.code);
11538
11761
  await ensureDirectMutationPublishContext(config, target, flags, 'page publish');
11539
11762
  const frozen = await fetchFrozenPageSnapshot(config, target);
11540
- const currentPage = frozen.pageByCode.get(pageCode) || null;
11541
- const expectedRevision = currentPage ? Number(currentPage.revision) : 0;
11542
11763
  const activate = flags.activate === true;
11543
- const definition = {
11544
- code: pageCode,
11545
- name: flags.name || pageCode,
11546
- description: flags.description || '',
11547
- route: flags.route ? JSON.parse(flags.route) : {},
11548
- props: flags.props ? JSON.parse(flags.props) : {},
11549
- runtime: {
11550
- entryUrl: flags['entry-url'],
11551
- cssUrls: splitList(flags['css-urls']),
11552
- jsUrls: splitList(flags['js-urls']),
11553
- framework: flags.framework || 'react',
11554
- frameworkVersion: flags['framework-version'] || '',
11555
- cssIsolation: flags['css-isolation'] || 'none',
11556
- format: flags.format || 'esm',
11557
- },
11558
- menu: flags.menu
11559
- ? {
11560
- enabled: true,
11561
- name: flags['menu-name'] || flags.name || pageCode,
11562
- parentId: flags['menu-parent-id'] || null,
11563
- icon: flags['menu-icon'] || null,
11564
- }
11565
- : { enabled: false },
11566
- expectedRevision,
11567
- };
11764
+ const definitions = pageDefinitions.map(definition => {
11765
+ const currentPage = frozen.pageByCode.get(definition.code) || null;
11766
+ return {
11767
+ ...definition,
11768
+ expectedRevision: currentPage ? Number(currentPage.revision) : 0,
11769
+ };
11770
+ });
11771
+ const expectedRevisions = { ...frozen.expectedRevisions };
11772
+ const expectedActiveAssets = { ...frozen.expectedActiveAssets };
11773
+ for (const definition of definitions) {
11774
+ expectedRevisions[definition.code] = definition.expectedRevision;
11775
+ if (!Object.prototype.hasOwnProperty.call(expectedActiveAssets, definition.code)) {
11776
+ expectedActiveAssets[definition.code] = null;
11777
+ }
11778
+ }
11779
+ const singleCurrentPage =
11780
+ definitions.length === 1
11781
+ ? frozen.pageByCode.get(definitions[0].code) || null
11782
+ : null;
11568
11783
  const data = await requestWithAuth(
11569
11784
  config,
11570
11785
  target.profileName,
11571
11786
  pageApiPath(target, '/publish'),
11572
11787
  {
11573
11788
  method: 'POST',
11574
- headers: { 'if-match': currentPage?.etag || '0' },
11789
+ headers:
11790
+ definitions.length === 1
11791
+ ? { 'if-match': singleCurrentPage?.etag || '0' }
11792
+ : {},
11575
11793
  body: {
11576
11794
  version: flags.version,
11577
11795
  buildId: flags['build-id'],
11578
- pages: [definition],
11579
- expectedRevision,
11580
- expectedRevisions: {
11581
- ...frozen.expectedRevisions,
11582
- [pageCode]: expectedRevision,
11583
- },
11584
- expectedActiveAssets: {
11585
- ...frozen.expectedActiveAssets,
11586
- [pageCode]: currentPage
11587
- ? frozen.expectedActiveAssets[pageCode]
11588
- : null,
11589
- },
11796
+ pages: definitions,
11797
+ ...(definitions.length === 1
11798
+ ? { expectedRevision: definitions[0].expectedRevision }
11799
+ : {}),
11800
+ expectedRevisions,
11801
+ expectedActiveAssets,
11590
11802
  expectedParent: frozen.expectedParent,
11591
11803
  stage: !activate,
11592
11804
  activate,
11593
11805
  },
11594
11806
  }
11595
11807
  );
11596
- const item = Array.isArray(data?.items) ? data.items[0] : null;
11597
- if (item?.pageId) {
11598
- savePageResource(target, pageCode, item.pageId, {
11599
- routeKey: item.routeKey,
11600
- legacyFormUuid: item.legacyFormUuid,
11601
- });
11808
+ const items = Array.isArray(data?.items) ? data.items : [];
11809
+ const itemByCode = new Map(
11810
+ items.map(item => [String(item?.code || '').trim(), item])
11811
+ );
11812
+ const missingPageCodes = pageCodes.filter(code => !itemByCode.has(code));
11813
+ if (missingPageCodes.length > 0) {
11814
+ fail(
11815
+ `PAGE_RELEASE_STAGE_SCOPE_MISMATCH: Page Release 响应缺少页面 ${missingPageCodes.join(',')}`
11816
+ );
11817
+ }
11818
+ for (const pageCode of pageCodes) {
11819
+ const item = itemByCode.get(pageCode);
11820
+ if (item?.pageId) {
11821
+ savePageResource(target, pageCode, item.pageId, {
11822
+ routeKey: item.routeKey,
11823
+ legacyFormUuid: item.legacyFormUuid,
11824
+ });
11825
+ }
11602
11826
  }
11603
11827
  if (!activate) {
11828
+ const releaseId = String(data?.release?.id || '').trim();
11829
+ const releaseHash = String(data?.release?.releaseHash || '').toLowerCase();
11830
+ if (
11831
+ !data?.staged ||
11832
+ data?.activation ||
11833
+ !releaseId ||
11834
+ !/^[a-f0-9]{64}$/.test(releaseHash) ||
11835
+ data?.release?.immutable !== true ||
11836
+ String(data?.release?.version || '') !== String(flags.version) ||
11837
+ String(data?.release?.buildId || '') !== String(flags['build-id'])
11838
+ ) {
11839
+ fail(
11840
+ `PAGE_RELEASE_STAGE_REQUIRED: ${pageCodes.join(',')} 的非激活发布必须返回匹配 version/buildId 的 immutable staged releaseId 和 64 位 releaseHash`
11841
+ );
11842
+ }
11843
+ const stagedChangeId =
11844
+ readStringFlag(flags, 'change') ||
11845
+ getStoredChangeBaseline(target)?.changeId;
11846
+ const stagedResource = {
11847
+ kind: 'PageRelease',
11848
+ identity: { releaseId },
11849
+ action: 'update',
11850
+ hash: releaseHash,
11851
+ revision: {
11852
+ releaseId,
11853
+ version: data.release.version,
11854
+ buildId: data.release.buildId,
11855
+ },
11856
+ metadata: {
11857
+ state: 'staged',
11858
+ immutable: true,
11859
+ parentReleaseId: data.release.parentReleaseId || null,
11860
+ pageCount: items.length,
11861
+ selectedPageCodes: [...pageCodes].sort(),
11862
+ releasePageCodes: unique(
11863
+ items
11864
+ .map(item => String(item?.code || '').trim())
11865
+ .filter(Boolean)
11866
+ ).sort(),
11867
+ },
11868
+ };
11869
+ data.stagedResource = stagedResource;
11870
+ data.stagedResourcesFile = recordStagedAppReleaseResources(
11871
+ stagedChangeId,
11872
+ [stagedResource],
11873
+ target
11874
+ );
11604
11875
  retainCommandScopedPublishLease('staged-page');
11605
11876
  }
11606
11877
  const leaseCompletion = await finalizeCommandScopedPublishLease({
@@ -14052,16 +14323,19 @@ async function notification(args) {
14052
14323
  const { flags, positional } = parseArgs(rest);
14053
14324
  if (wantsSubcommandHelp(subcommand, flags)) {
14054
14325
  print([
14055
- '用法: openxiangda notification template-list|template-get|template-upsert|template-delete|type-list|type-get|type-upsert|type-delete|preview|send|batch-send|capabilities|dingding-preview|dingding-send',
14326
+ '用法: openxiangda notification template-list|template-get|template-upsert|template-delete|type-list|type-get|type-upsert|type-delete|preview|capabilities|dingding-preview',
14056
14327
  ' preview <notificationType|templateCode> [--form-code code|--form-uuid FORM_XXX] --body-json \'{"payload":{"title":"测试"}}\'',
14057
- ' send <notificationType> --body-json \'{"recipientId":"USER_ID","payload":{"title":"测试"}}\' --force',
14058
- ' batch-send <notificationType> --body-json \'{"recipients":[{"recipientId":"USER_ID","payload":{}}]}\' --force',
14059
14328
  ' capabilities --json',
14060
14329
  ' dingding-preview <notificationType> [--template-code code|--template-id id] --body-json \'{"payload":{"title":"测试"}}\'',
14061
- ' dingding-send <notificationType> --body-json \'{"recipientId":"USER_ID","payload":{"title":"测试"}}\' --force',
14330
+ ' 页面和 CLI 不再直接发送通知;请使用 openxiangda function invoke <functionCode> --body-json \'{"input":{"businessId":"..."}}\'',
14062
14331
  ].join('\n'));
14063
14332
  return;
14064
14333
  }
14334
+ if (['send', 'batch-send', 'dingding-send'].includes(subcommand)) {
14335
+ fail(
14336
+ 'DIRECT_NOTIFICATION_SEND_REMOVED: CLI 不再直接发送通知,请使用具名 App Function,并通过 openxiangda function invoke <functionCode> 调用'
14337
+ );
14338
+ }
14065
14339
  const config = loadConfig();
14066
14340
  const target = getWorkspaceTarget(config, flags.profile || config.currentProfile, flags);
14067
14341
  const appPrefix = `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/notifications`;
@@ -14160,45 +14434,30 @@ async function notification(args) {
14160
14434
  strictEnvelope: true,
14161
14435
  });
14162
14436
  }
14163
- if (subcommand === 'dingding-preview' || subcommand === 'dingding-send') {
14164
- if (subcommand === 'dingding-send' && !flags.force) {
14165
- fail('openxiangda notification dingding-send 是发送动作,必须加 --force');
14166
- }
14437
+ if (subcommand === 'dingding-preview') {
14167
14438
  const code = positional[0] || flags.code;
14168
14439
  const rawBody = readDirectJsonBody(flags, `notification ${subcommand}`, { optional: true });
14169
14440
  const body = buildDingTalkNotificationActionBody(target, code, rawBody, flags);
14170
14441
  return runDirectRequest(config, target, flags, {
14171
14442
  method: 'POST',
14172
- path: `${appPrefix}/dingtalk/${subcommand === 'dingding-send' ? 'send' : 'preview'}`,
14443
+ path: `${appPrefix}/dingtalk/preview`,
14173
14444
  body,
14174
14445
  strictEnvelope: true,
14175
14446
  }, { controlPlaneMutation: false });
14176
14447
  }
14177
- if (['preview', 'send', 'batch-send'].includes(subcommand)) {
14178
- if ((subcommand === 'send' || subcommand === 'batch-send') && !flags.force) {
14179
- fail(`openxiangda notification ${subcommand} 是发送动作,必须加 --force`);
14180
- }
14448
+ if (subcommand === 'preview') {
14181
14449
  const code = positional[0] || flags.code;
14182
14450
  const rawBody = readDirectJsonBody(flags, `notification ${subcommand}`, { optional: true });
14183
- const body =
14184
- subcommand === 'preview'
14185
- ? await normalizeNotificationPreviewBody(config, target, code, rawBody, flags)
14186
- : normalizeNotificationActionBody(subcommand, code, rawBody);
14187
- const actionPath =
14188
- subcommand === 'preview'
14189
- ? 'templates/preview'
14190
- : subcommand === 'batch-send'
14191
- ? 'batch-send-by-type'
14192
- : 'send-by-type';
14451
+ const body = await normalizeNotificationPreviewBody(config, target, code, rawBody, flags);
14193
14452
  return runDirectRequest(config, target, flags, {
14194
14453
  method: 'POST',
14195
- path: `${appPrefix}/${actionPath}`,
14454
+ path: `${appPrefix}/templates/preview`,
14196
14455
  body,
14197
14456
  strictEnvelope: true,
14198
14457
  }, { controlPlaneMutation: false });
14199
14458
  }
14200
14459
 
14201
- fail('用法: openxiangda notification template-list|template-get|template-upsert|template-delete|type-list|type-get|type-upsert|type-delete|preview|send|batch-send|capabilities|dingding-preview|dingding-send');
14460
+ fail('用法: openxiangda notification template-list|template-get|template-upsert|template-delete|type-list|type-get|type-upsert|type-delete|preview|capabilities|dingding-preview');
14202
14461
  }
14203
14462
 
14204
14463
  async function organization(args) {
@@ -31512,6 +31771,9 @@ function buildWorkspacePublishEnv(
31512
31771
  env.OPENXIANGDA_BASE_URL = profile.baseUrl;
31513
31772
  env.OPENXIANGDA_ACCESS_TOKEN = profile.token.accessToken;
31514
31773
  env.OPENXIANGDA_APP_TYPE = appType;
31774
+ if (!env.OPENXIANGDA_CLI) {
31775
+ env.OPENXIANGDA_CLI = path.resolve(__dirname, '..', 'bin', 'openxiangda.js');
31776
+ }
31515
31777
  if (targetName) env.OPENXIANGDA_TARGET = targetName;
31516
31778
  else delete env.OPENXIANGDA_TARGET;
31517
31779
  env.OPENXIANGDA_GLOBAL_ENV_KEYS = injectedGlobalKeys.join(',');
@@ -31521,8 +31783,11 @@ function buildWorkspacePublishEnv(
31521
31783
  }
31522
31784
 
31523
31785
  module.exports = {
31786
+ assertLegacyPageReleaseCoverage,
31524
31787
  assertReleasePublishAdoptionFlags,
31525
31788
  assertReleasePublishAdoptionScope,
31789
+ assertReleasePublishManifestReplacementFlags,
31790
+ assertReleasePublishManifestReplacementScope,
31526
31791
  assertRuntimeReleaseReusable,
31527
31792
  buildOpenXiangdaUpdateInstallPlan,
31528
31793
  buildResourceManifestSddTargets,