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.
- package/lib/application-environments.js +17 -0
- package/lib/cli.js +343 -78
- package/lib/release-plan.js +76 -63
- package/openxiangda-skills/SKILL.md +1 -1
- package/openxiangda-skills/references/notifications.md +28 -23
- package/openxiangda-skills/references/openxiangda-api.md +10 -30
- package/openxiangda-skills/references/pages/page-sdk.md +1 -1
- package/openxiangda-skills/references/resource-manifest-cheatsheet.md +5 -7
- package/openxiangda-skills/skills/openxiangda-core/SKILL.md +1 -1
- package/openxiangda-skills/skills/openxiangda-page/SKILL.md +2 -2
- package/package.json +22 -23
- package/packages/sdk/dist/runtime/index.cjs +39 -39
- package/packages/sdk/dist/runtime/index.mjs +39 -39
- package/packages/sdk/dist/runtime/react.cjs +39 -39
- package/packages/sdk/dist/runtime/react.d.mts +3 -0
- package/packages/sdk/dist/runtime/react.d.ts +3 -0
- package/packages/sdk/dist/runtime/react.mjs +46 -46
- package/packages/sdk/src/build-source/scripts/publish-all.mjs +33 -2
- package/packages/sdk/src/build-source/scripts/register.mjs +161 -1
- package/templates/openxiangda-react-spa/AGENTS.md +2 -0
- package/templates/sy-lowcode-app-workspace/AGENTS.md +1 -1
|
@@ -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
|
-
:
|
|
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
|
-
|
|
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
|
|
11544
|
-
code
|
|
11545
|
-
|
|
11546
|
-
|
|
11547
|
-
|
|
11548
|
-
|
|
11549
|
-
|
|
11550
|
-
|
|
11551
|
-
|
|
11552
|
-
|
|
11553
|
-
|
|
11554
|
-
|
|
11555
|
-
|
|
11556
|
-
|
|
11557
|
-
|
|
11558
|
-
|
|
11559
|
-
|
|
11560
|
-
|
|
11561
|
-
|
|
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:
|
|
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:
|
|
11579
|
-
|
|
11580
|
-
|
|
11581
|
-
|
|
11582
|
-
|
|
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
|
|
11597
|
-
|
|
11598
|
-
|
|
11599
|
-
|
|
11600
|
-
|
|
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|
|
|
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
|
-
'
|
|
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'
|
|
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
|
|
14443
|
+
path: `${appPrefix}/dingtalk/preview`,
|
|
14173
14444
|
body,
|
|
14174
14445
|
strictEnvelope: true,
|
|
14175
14446
|
}, { controlPlaneMutation: false });
|
|
14176
14447
|
}
|
|
14177
|
-
if (
|
|
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}
|
|
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|
|
|
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,
|