openxiangda 1.0.269 → 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/README.md +2 -0
- package/lib/application-environments.js +17 -0
- package/lib/cli.js +720 -78
- package/lib/design-gates.js +18 -0
- package/lib/release-plan.js +78 -63
- package/openxiangda-skills/SKILL.md +3 -1
- package/openxiangda-skills/references/notifications.md +28 -23
- package/openxiangda-skills/references/openxiangda-api.md +22 -30
- package/openxiangda-skills/references/pages/page-sdk.md +1 -1
- package/openxiangda-skills/references/resource-manifest-cheatsheet.md +37 -9
- package/openxiangda-skills/references/webhooks.md +213 -0
- package/openxiangda-skills/skills/openxiangda-core/SKILL.md +1 -1
- package/openxiangda-skills/skills/openxiangda-page/SKILL.md +2 -2
- package/openxiangda-skills/skills/openxiangda-workflow-automation/SKILL.md +3 -0
- package/package.json +24 -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/.cursor/rules/openxiangda-resources.mdc +1 -0
- package/templates/openxiangda-react-spa/.qoder/rules/openxiangda-resources.md +1 -0
- package/templates/openxiangda-react-spa/AGENTS.md +4 -0
- package/templates/sy-lowcode-app-workspace/.cursor/rules/openxiangda-resources.mdc +2 -0
- package/templates/sy-lowcode-app-workspace/.qoder/rules/openxiangda-resources.md +1 -0
- package/templates/sy-lowcode-app-workspace/AGENTS.md +3 -1
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,
|
|
@@ -299,6 +300,7 @@ async function mainImpl(argv) {
|
|
|
299
300
|
if (command === 'workflow') return workflow(rest);
|
|
300
301
|
if (command === 'automation') return automation(rest);
|
|
301
302
|
if (command === 'data-view') return dataView(rest);
|
|
303
|
+
if (command === 'webhook') return webhook(rest);
|
|
302
304
|
if (command === 'scope') return scope(rest);
|
|
303
305
|
if (command === 'route') return route(rest);
|
|
304
306
|
if (command === 'public-access') return publicAccess(rest);
|
|
@@ -344,7 +346,7 @@ Usage:
|
|
|
344
346
|
openxiangda status <runId> [--environment preproduction|production] [--json]
|
|
345
347
|
openxiangda retry <runId> [--environment preproduction|production] [--json]
|
|
346
348
|
openxiangda rollback <preproduction|production> --to <appReleaseId> [--json]
|
|
347
|
-
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]
|
|
348
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]
|
|
349
351
|
openxiangda task status --change <id> [--profile name] [--watch] [--json]
|
|
350
352
|
openxiangda release backend-head|backend-list|backend-detail|backend-diff|backend-rollback|backend-abort|backend-retry [releaseId] [--profile name] [--json]
|
|
@@ -389,6 +391,7 @@ Usage:
|
|
|
389
391
|
openxiangda automation diagnose <automationCode|automationId> [--redact] [--json]
|
|
390
392
|
openxiangda automation publish|enable|disable <automationCode|automationId>
|
|
391
393
|
openxiangda data-view list|get|create|update|upsert|delete|status|refresh|query|stats <dataViewCode> [--profile name] [--json]
|
|
394
|
+
openxiangda webhook list|get|create|update|upsert|disable|deliveries|delivery [webhookCode] [deliveryId] [--json-file file] [--json]
|
|
392
395
|
openxiangda scope dimension-list|dimension-upsert|grant-source-list|grant-source-upsert|policy-list|policy-upsert|sync|explain [code] [--json-file file]
|
|
393
396
|
openxiangda route list|get|create|update|upsert|delete [--json-file file] [--dry-run] [--write-manifest]
|
|
394
397
|
openxiangda public-access list|get|create|update|upsert|delete|ticket-create|session-test|grant-check [--json-file file]
|
|
@@ -1633,6 +1636,16 @@ async function runOpenXiangdaInProcess(args, options = {}) {
|
|
|
1633
1636
|
let stdout = '';
|
|
1634
1637
|
const originalWrite = process.stdout.write;
|
|
1635
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
|
+
}
|
|
1636
1649
|
process.exitCode = undefined;
|
|
1637
1650
|
process.stdout.write = function capture(chunk, encoding, callback) {
|
|
1638
1651
|
stdout += Buffer.isBuffer(chunk) ? chunk.toString(encoding) : String(chunk);
|
|
@@ -1660,6 +1673,10 @@ async function runOpenXiangdaInProcess(args, options = {}) {
|
|
|
1660
1673
|
}
|
|
1661
1674
|
throw error;
|
|
1662
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
|
+
}
|
|
1663
1680
|
process.stdout.write = originalWrite;
|
|
1664
1681
|
process.exitCode = previousExitCode;
|
|
1665
1682
|
}
|
|
@@ -2109,7 +2126,7 @@ function decorateEnvironmentReleaseSteps(steps, target, flags = {}) {
|
|
|
2109
2126
|
for (const step of steps) {
|
|
2110
2127
|
step.args = managedEnvironment
|
|
2111
2128
|
? withManagedReleaseForwardedFlags(step.id, step.args, flags)
|
|
2112
|
-
:
|
|
2129
|
+
: withLegacyReleaseForwardedFlags(step.id, step.args, flags);
|
|
2113
2130
|
if (!managedEnvironment) {
|
|
2114
2131
|
step.command = commandFromArgs(step.args);
|
|
2115
2132
|
continue;
|
|
@@ -2200,11 +2217,104 @@ function assertReleasePublishAdoptionScope(steps = [], flags = {}) {
|
|
|
2200
2217
|
}
|
|
2201
2218
|
}
|
|
2202
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
|
+
|
|
2203
2257
|
function changeIdFromStep(step) {
|
|
2204
2258
|
const index = step.args.indexOf('--change');
|
|
2205
2259
|
return index >= 0 ? step.args[index + 1] : 'change';
|
|
2206
2260
|
}
|
|
2207
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
|
+
|
|
2208
2318
|
async function publishWorkspaceRelease(config, target, flags = {}) {
|
|
2209
2319
|
const changeId = readStringFlag(flags, 'change');
|
|
2210
2320
|
const deploymentId = readStringFlag(flags, 'deployment-id') || null;
|
|
@@ -2214,6 +2324,7 @@ async function publishWorkspaceRelease(config, target, flags = {}) {
|
|
|
2214
2324
|
fail('用法: openxiangda release publish --change <id> --profile <name>');
|
|
2215
2325
|
}
|
|
2216
2326
|
assertReleasePublishAdoptionFlags(flags);
|
|
2327
|
+
assertReleasePublishManifestReplacementFlags(flags);
|
|
2217
2328
|
assertManagedRuntimeRollbackFlags(flags);
|
|
2218
2329
|
const releaseCandidate = flags.__verifiedCandidate
|
|
2219
2330
|
? assertLocalCandidate(flags.__verifiedCandidate)
|
|
@@ -2241,6 +2352,7 @@ async function publishWorkspaceRelease(config, target, flags = {}) {
|
|
|
2241
2352
|
);
|
|
2242
2353
|
decorateEnvironmentReleaseSteps(steps, target, flags);
|
|
2243
2354
|
assertReleasePublishAdoptionScope(steps, flags);
|
|
2355
|
+
assertReleasePublishManifestReplacementScope(steps, flags);
|
|
2244
2356
|
const releaseScope = getSddChangeScope({
|
|
2245
2357
|
cwd: process.cwd(),
|
|
2246
2358
|
configText: readWorkspaceConfigText(),
|
|
@@ -2262,6 +2374,12 @@ async function publishWorkspaceRelease(config, target, flags = {}) {
|
|
|
2262
2374
|
runtimeMode: plan.workspace.runtimeMode,
|
|
2263
2375
|
targets: plan.activationTargets,
|
|
2264
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
|
+
})),
|
|
2265
2383
|
candidateHash: releaseCandidate?.candidateHash || null,
|
|
2266
2384
|
});
|
|
2267
2385
|
if (flags['dry-run']) {
|
|
@@ -2377,6 +2495,7 @@ async function publishWorkspaceRelease(config, target, flags = {}) {
|
|
|
2377
2495
|
...steps.map(step => ({
|
|
2378
2496
|
id: step.id,
|
|
2379
2497
|
command: step.command,
|
|
2498
|
+
env: step.env || {},
|
|
2380
2499
|
stagedKind: step.stagedKind,
|
|
2381
2500
|
status: 'pending',
|
|
2382
2501
|
})),
|
|
@@ -2476,7 +2595,10 @@ async function publishWorkspaceRelease(config, target, flags = {}) {
|
|
|
2476
2595
|
execution
|
|
2477
2596
|
);
|
|
2478
2597
|
warn(`发布步骤开始: ${step.id}`);
|
|
2479
|
-
const result = await runOpenXiangdaInProcess(step.args
|
|
2598
|
+
const result = await runOpenXiangdaInProcess(step.args, {
|
|
2599
|
+
env: step.env,
|
|
2600
|
+
});
|
|
2601
|
+
assertLegacyPageReleaseStaged(step, changeId, target);
|
|
2480
2602
|
journalStep.status = 'completed';
|
|
2481
2603
|
journalStep.completedAt = new Date().toISOString();
|
|
2482
2604
|
journalStep.result = summarizeReleaseStepResult(result);
|
|
@@ -5378,7 +5500,7 @@ async function release(args) {
|
|
|
5378
5500
|
const { flags, positional } = parseArgs(rest);
|
|
5379
5501
|
if (wantsSubcommandHelp(subcommand, flags)) {
|
|
5380
5502
|
print([
|
|
5381
|
-
'用法: 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]',
|
|
5382
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]',
|
|
5383
5505
|
' openxiangda release backend-head|backend-list|backend-detail|backend-diff|backend-rollback|backend-abort|backend-retry [releaseId] [--profile name] [--json]',
|
|
5384
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]',
|
|
@@ -5391,6 +5513,7 @@ async function release(args) {
|
|
|
5391
5513
|
' openxiangda release ship --change <id> --profile <name> --confirm-production',
|
|
5392
5514
|
' openxiangda release fail --deployment <id> --message "UAT 未通过" --code UAT_FAILED --details-json <JSON|file> --environment preproduction --profile <name>',
|
|
5393
5515
|
' openxiangda release publish --change <id> --profile <name>',
|
|
5516
|
+
' openxiangda release publish --change <id> --profile <name> --replace-manifest --reason "已审查精确 Function manifest"',
|
|
5394
5517
|
' openxiangda release publish --change <id> --profile <name> --adopt-online-baseline --adoption-reason "已审计目标来自多次历史发布"',
|
|
5395
5518
|
' openxiangda release begin --change <id> --profile <name>',
|
|
5396
5519
|
' openxiangda resource publish function --only <code> --change <id> --profile <name>',
|
|
@@ -5411,6 +5534,7 @@ async function release(args) {
|
|
|
5411
5534
|
' - ship 当前不支持 --dry-run;任何未声明参数都会在 candidate/deployment 写入前 fail-closed。需要只读检查时使用 environment status/diff 与 release status。',
|
|
5412
5535
|
' - publish 默认等待租约并按私有执行日志恢复;上次写结果不确定时必须只读核对后显式 --resume-after-review。',
|
|
5413
5536
|
' - 旧式非环境托管 publish 也会把线上基线采纳意图仅透传给精确的 resource publish --only/--code 阶段;无精确资源阶段、原因不足或单独提供 reason 均在获取租约前 fail-closed。',
|
|
5537
|
+
' - 旧式非环境托管 publish 的 --replace-manifest 与 --reason 必须成对,只透传给唯一的精确 Backend Function/Automation stage,不进入 Form、Page、Workflow 或 App finalize。',
|
|
5414
5538
|
' - app-head 默认只输出紧凑 head 摘要;需要完整 manifest 时显式追加 --full。',
|
|
5415
5539
|
' - begin 获取应用级单写者 promotion lease;不同工作区仍可并行开发和验证。',
|
|
5416
5540
|
' - 带 --change 的 resource/runtime 写命令在没有本地 lease 时会自动 begin,并在后续命令复用。',
|
|
@@ -10357,6 +10481,7 @@ async function workspace(args) {
|
|
|
10357
10481
|
connectors: {},
|
|
10358
10482
|
dataViews: {},
|
|
10359
10483
|
storageConfigs: {},
|
|
10484
|
+
webhooks: {},
|
|
10360
10485
|
scopeDimensions: {},
|
|
10361
10486
|
scopeGrantSources: {},
|
|
10362
10487
|
dataScopePolicies: {},
|
|
@@ -11405,6 +11530,105 @@ function outputPageResult(data, flags = {}) {
|
|
|
11405
11530
|
print(JSON.stringify(data, null, 2));
|
|
11406
11531
|
}
|
|
11407
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
|
+
|
|
11408
11632
|
async function page(args) {
|
|
11409
11633
|
const [subcommand, ...rest] = args;
|
|
11410
11634
|
const { flags, positional } = parseArgs(rest);
|
|
@@ -11526,78 +11750,128 @@ async function page(args) {
|
|
|
11526
11750
|
}
|
|
11527
11751
|
|
|
11528
11752
|
if (subcommand === 'publish') {
|
|
11529
|
-
|
|
11530
|
-
if (!pageCode || !flags['entry-url'] || !flags.version || !flags['build-id']) {
|
|
11753
|
+
if (!flags.version || !flags['build-id']) {
|
|
11531
11754
|
fail(
|
|
11532
|
-
'用法: 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]'
|
|
11533
11757
|
);
|
|
11534
11758
|
}
|
|
11759
|
+
const pageDefinitions = normalizePagePublishDefinitions(positional, flags);
|
|
11760
|
+
const pageCodes = pageDefinitions.map(definition => definition.code);
|
|
11535
11761
|
await ensureDirectMutationPublishContext(config, target, flags, 'page publish');
|
|
11536
11762
|
const frozen = await fetchFrozenPageSnapshot(config, target);
|
|
11537
|
-
const currentPage = frozen.pageByCode.get(pageCode) || null;
|
|
11538
|
-
const expectedRevision = currentPage ? Number(currentPage.revision) : 0;
|
|
11539
11763
|
const activate = flags.activate === true;
|
|
11540
|
-
const
|
|
11541
|
-
code
|
|
11542
|
-
|
|
11543
|
-
|
|
11544
|
-
|
|
11545
|
-
|
|
11546
|
-
|
|
11547
|
-
|
|
11548
|
-
|
|
11549
|
-
|
|
11550
|
-
|
|
11551
|
-
|
|
11552
|
-
|
|
11553
|
-
|
|
11554
|
-
|
|
11555
|
-
|
|
11556
|
-
|
|
11557
|
-
|
|
11558
|
-
|
|
11559
|
-
parentId: flags['menu-parent-id'] || null,
|
|
11560
|
-
icon: flags['menu-icon'] || null,
|
|
11561
|
-
}
|
|
11562
|
-
: { enabled: false },
|
|
11563
|
-
expectedRevision,
|
|
11564
|
-
};
|
|
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;
|
|
11565
11783
|
const data = await requestWithAuth(
|
|
11566
11784
|
config,
|
|
11567
11785
|
target.profileName,
|
|
11568
11786
|
pageApiPath(target, '/publish'),
|
|
11569
11787
|
{
|
|
11570
11788
|
method: 'POST',
|
|
11571
|
-
headers:
|
|
11789
|
+
headers:
|
|
11790
|
+
definitions.length === 1
|
|
11791
|
+
? { 'if-match': singleCurrentPage?.etag || '0' }
|
|
11792
|
+
: {},
|
|
11572
11793
|
body: {
|
|
11573
11794
|
version: flags.version,
|
|
11574
11795
|
buildId: flags['build-id'],
|
|
11575
|
-
pages:
|
|
11576
|
-
|
|
11577
|
-
|
|
11578
|
-
|
|
11579
|
-
|
|
11580
|
-
|
|
11581
|
-
expectedActiveAssets: {
|
|
11582
|
-
...frozen.expectedActiveAssets,
|
|
11583
|
-
[pageCode]: currentPage
|
|
11584
|
-
? frozen.expectedActiveAssets[pageCode]
|
|
11585
|
-
: null,
|
|
11586
|
-
},
|
|
11796
|
+
pages: definitions,
|
|
11797
|
+
...(definitions.length === 1
|
|
11798
|
+
? { expectedRevision: definitions[0].expectedRevision }
|
|
11799
|
+
: {}),
|
|
11800
|
+
expectedRevisions,
|
|
11801
|
+
expectedActiveAssets,
|
|
11587
11802
|
expectedParent: frozen.expectedParent,
|
|
11588
11803
|
stage: !activate,
|
|
11589
11804
|
activate,
|
|
11590
11805
|
},
|
|
11591
11806
|
}
|
|
11592
11807
|
);
|
|
11593
|
-
const
|
|
11594
|
-
|
|
11595
|
-
|
|
11596
|
-
|
|
11597
|
-
|
|
11598
|
-
|
|
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
|
+
}
|
|
11599
11826
|
}
|
|
11600
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
|
+
);
|
|
11601
11875
|
retainCommandScopedPublishLease('staged-page');
|
|
11602
11876
|
}
|
|
11603
11877
|
const leaseCompletion = await finalizeCommandScopedPublishLease({
|
|
@@ -12543,6 +12817,93 @@ async function dataView(args) {
|
|
|
12543
12817
|
fail('用法: openxiangda data-view list|get|create|update|upsert|delete|status|refresh|query|stats');
|
|
12544
12818
|
}
|
|
12545
12819
|
|
|
12820
|
+
async function webhook(args) {
|
|
12821
|
+
const { subcommand, rest } = parseSubcommandArgs(args);
|
|
12822
|
+
const { flags, positional } = parseArgs(rest);
|
|
12823
|
+
if (wantsSubcommandHelp(subcommand, flags)) {
|
|
12824
|
+
print('用法: openxiangda webhook list|get|create|update|upsert|disable|deliveries|delivery [webhookCode] [deliveryId] [--json-file file] [--json]');
|
|
12825
|
+
return;
|
|
12826
|
+
}
|
|
12827
|
+
const config = loadConfig();
|
|
12828
|
+
const target = getWorkspaceTarget(
|
|
12829
|
+
config,
|
|
12830
|
+
flags.profile || config.currentProfile,
|
|
12831
|
+
flags
|
|
12832
|
+
);
|
|
12833
|
+
const basePath = `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/webhooks`;
|
|
12834
|
+
|
|
12835
|
+
if (['list', 'get', 'create', 'update', 'upsert'].includes(subcommand)) {
|
|
12836
|
+
return directResourceCrud(config, target, {
|
|
12837
|
+
commandName: 'webhook',
|
|
12838
|
+
subcommand,
|
|
12839
|
+
flags,
|
|
12840
|
+
positional,
|
|
12841
|
+
resourceType: 'webhook',
|
|
12842
|
+
basePath,
|
|
12843
|
+
manifestDir: path.join('src', 'resources', 'webhooks'),
|
|
12844
|
+
normalizeBody: normalizeWebhookManifest,
|
|
12845
|
+
codeOf: body => body.code || body.resourceCode,
|
|
12846
|
+
saveState: (code, data) => saveWebhookResource(target, code, data),
|
|
12847
|
+
});
|
|
12848
|
+
}
|
|
12849
|
+
|
|
12850
|
+
if (subcommand === 'disable') {
|
|
12851
|
+
const code = positional[0] || flags.code;
|
|
12852
|
+
if (!code) fail('用法: openxiangda webhook disable <webhookCode>');
|
|
12853
|
+
const body = readDirectJsonBody(flags, 'webhook disable', {
|
|
12854
|
+
optional: true,
|
|
12855
|
+
});
|
|
12856
|
+
if (flags['expected-revision'] !== undefined) {
|
|
12857
|
+
body.expectedRevision = Number(flags['expected-revision']);
|
|
12858
|
+
}
|
|
12859
|
+
const data = await runDirectRequest(
|
|
12860
|
+
config,
|
|
12861
|
+
target,
|
|
12862
|
+
flags,
|
|
12863
|
+
{
|
|
12864
|
+
method: 'POST',
|
|
12865
|
+
path: apiPathWithPostDeleteAction(
|
|
12866
|
+
`${basePath}/${encodeURIComponent(code)}`
|
|
12867
|
+
),
|
|
12868
|
+
body,
|
|
12869
|
+
},
|
|
12870
|
+
{ returnData: true }
|
|
12871
|
+
);
|
|
12872
|
+
if (!flags['dry-run']) saveWebhookResource(target, code, data);
|
|
12873
|
+
return outputDirectResult({ action: 'disable', data }, flags);
|
|
12874
|
+
}
|
|
12875
|
+
|
|
12876
|
+
if (subcommand === 'deliveries') {
|
|
12877
|
+
const code = positional[0] || flags.code;
|
|
12878
|
+
if (!code) fail('用法: openxiangda webhook deliveries <webhookCode>');
|
|
12879
|
+
return runDirectRequest(config, target, flags, {
|
|
12880
|
+
method: 'GET',
|
|
12881
|
+
path: apiPathWithQuery(
|
|
12882
|
+
`${basePath}/${encodeURIComponent(code)}/deliveries`,
|
|
12883
|
+
{
|
|
12884
|
+
page: flags.page,
|
|
12885
|
+
pageSize: flags['page-size'] || flags.limit,
|
|
12886
|
+
status: flags.status,
|
|
12887
|
+
}
|
|
12888
|
+
),
|
|
12889
|
+
});
|
|
12890
|
+
}
|
|
12891
|
+
|
|
12892
|
+
if (subcommand === 'delivery') {
|
|
12893
|
+
const code = positional[0] || flags.code;
|
|
12894
|
+
const deliveryId = positional[1] || flags['delivery-id'];
|
|
12895
|
+
if (!code || !deliveryId) {
|
|
12896
|
+
fail('用法: openxiangda webhook delivery <webhookCode> <deliveryId>');
|
|
12897
|
+
}
|
|
12898
|
+
return runDirectRequest(config, target, flags, {
|
|
12899
|
+
method: 'GET',
|
|
12900
|
+
path: `${basePath}/${encodeURIComponent(code)}/deliveries/${encodeURIComponent(deliveryId)}`,
|
|
12901
|
+
});
|
|
12902
|
+
}
|
|
12903
|
+
|
|
12904
|
+
fail('用法: openxiangda webhook list|get|create|update|upsert|disable|deliveries|delivery');
|
|
12905
|
+
}
|
|
12906
|
+
|
|
12546
12907
|
async function scope(args) {
|
|
12547
12908
|
const { subcommand, rest } = parseSubcommandArgs(args);
|
|
12548
12909
|
const { flags, positional } = parseArgs(rest);
|
|
@@ -13962,16 +14323,19 @@ async function notification(args) {
|
|
|
13962
14323
|
const { flags, positional } = parseArgs(rest);
|
|
13963
14324
|
if (wantsSubcommandHelp(subcommand, flags)) {
|
|
13964
14325
|
print([
|
|
13965
|
-
'用法: 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',
|
|
13966
14327
|
' preview <notificationType|templateCode> [--form-code code|--form-uuid FORM_XXX] --body-json \'{"payload":{"title":"测试"}}\'',
|
|
13967
|
-
' send <notificationType> --body-json \'{"recipientId":"USER_ID","payload":{"title":"测试"}}\' --force',
|
|
13968
|
-
' batch-send <notificationType> --body-json \'{"recipients":[{"recipientId":"USER_ID","payload":{}}]}\' --force',
|
|
13969
14328
|
' capabilities --json',
|
|
13970
14329
|
' dingding-preview <notificationType> [--template-code code|--template-id id] --body-json \'{"payload":{"title":"测试"}}\'',
|
|
13971
|
-
'
|
|
14330
|
+
' 页面和 CLI 不再直接发送通知;请使用 openxiangda function invoke <functionCode> --body-json \'{"input":{"businessId":"..."}}\'',
|
|
13972
14331
|
].join('\n'));
|
|
13973
14332
|
return;
|
|
13974
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
|
+
}
|
|
13975
14339
|
const config = loadConfig();
|
|
13976
14340
|
const target = getWorkspaceTarget(config, flags.profile || config.currentProfile, flags);
|
|
13977
14341
|
const appPrefix = `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/notifications`;
|
|
@@ -14070,45 +14434,30 @@ async function notification(args) {
|
|
|
14070
14434
|
strictEnvelope: true,
|
|
14071
14435
|
});
|
|
14072
14436
|
}
|
|
14073
|
-
if (subcommand === 'dingding-preview'
|
|
14074
|
-
if (subcommand === 'dingding-send' && !flags.force) {
|
|
14075
|
-
fail('openxiangda notification dingding-send 是发送动作,必须加 --force');
|
|
14076
|
-
}
|
|
14437
|
+
if (subcommand === 'dingding-preview') {
|
|
14077
14438
|
const code = positional[0] || flags.code;
|
|
14078
14439
|
const rawBody = readDirectJsonBody(flags, `notification ${subcommand}`, { optional: true });
|
|
14079
14440
|
const body = buildDingTalkNotificationActionBody(target, code, rawBody, flags);
|
|
14080
14441
|
return runDirectRequest(config, target, flags, {
|
|
14081
14442
|
method: 'POST',
|
|
14082
|
-
path: `${appPrefix}/dingtalk
|
|
14443
|
+
path: `${appPrefix}/dingtalk/preview`,
|
|
14083
14444
|
body,
|
|
14084
14445
|
strictEnvelope: true,
|
|
14085
14446
|
}, { controlPlaneMutation: false });
|
|
14086
14447
|
}
|
|
14087
|
-
if (
|
|
14088
|
-
if ((subcommand === 'send' || subcommand === 'batch-send') && !flags.force) {
|
|
14089
|
-
fail(`openxiangda notification ${subcommand} 是发送动作,必须加 --force`);
|
|
14090
|
-
}
|
|
14448
|
+
if (subcommand === 'preview') {
|
|
14091
14449
|
const code = positional[0] || flags.code;
|
|
14092
14450
|
const rawBody = readDirectJsonBody(flags, `notification ${subcommand}`, { optional: true });
|
|
14093
|
-
const body =
|
|
14094
|
-
subcommand === 'preview'
|
|
14095
|
-
? await normalizeNotificationPreviewBody(config, target, code, rawBody, flags)
|
|
14096
|
-
: normalizeNotificationActionBody(subcommand, code, rawBody);
|
|
14097
|
-
const actionPath =
|
|
14098
|
-
subcommand === 'preview'
|
|
14099
|
-
? 'templates/preview'
|
|
14100
|
-
: subcommand === 'batch-send'
|
|
14101
|
-
? 'batch-send-by-type'
|
|
14102
|
-
: 'send-by-type';
|
|
14451
|
+
const body = await normalizeNotificationPreviewBody(config, target, code, rawBody, flags);
|
|
14103
14452
|
return runDirectRequest(config, target, flags, {
|
|
14104
14453
|
method: 'POST',
|
|
14105
|
-
path: `${appPrefix}
|
|
14454
|
+
path: `${appPrefix}/templates/preview`,
|
|
14106
14455
|
body,
|
|
14107
14456
|
strictEnvelope: true,
|
|
14108
14457
|
}, { controlPlaneMutation: false });
|
|
14109
14458
|
}
|
|
14110
14459
|
|
|
14111
|
-
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');
|
|
14112
14461
|
}
|
|
14113
14462
|
|
|
14114
14463
|
async function organization(args) {
|
|
@@ -17097,6 +17446,7 @@ async function commands(args) {
|
|
|
17097
17446
|
'workflow compile|list|create|bind|pull|publish|delete|validate',
|
|
17098
17447
|
'automation list|create|bind|pull|publish|unpublish|enable|disable|delete|validate|cron-validate',
|
|
17099
17448
|
'data-view list|get|create|update|upsert|delete|status|refresh|query|stats',
|
|
17449
|
+
'webhook list|get|create|update|upsert|disable|deliveries|delivery',
|
|
17100
17450
|
'route list|get|create|update|upsert|delete',
|
|
17101
17451
|
'public-access list|get|create|update|upsert|delete|ticket-create|session-test|grant-check',
|
|
17102
17452
|
'auth-config list|get|create|update|upsert|delete|methods',
|
|
@@ -17119,6 +17469,7 @@ async function commands(args) {
|
|
|
17119
17469
|
resourceNotes: [
|
|
17120
17470
|
'For formal multi-resource development, prefer src/resources/** + openxiangda resource validate|plan|publish.',
|
|
17121
17471
|
'Use first-class route/public-access/auth-config/function/connector/notification/data-view/menu/permission commands for discovery, diagnosis, dry-run, and small live fixes.',
|
|
17472
|
+
'Inbound Webhooks are declared in src/resources/webhooks and route only to a fixed App Function. Provider secrets belong in top-level Function secretRefs; signature verification must use input.rawBody before any data access.',
|
|
17122
17473
|
'public-access is the new React SPA public policy resource for /view/:appType/public/* routes; page code should use PublicAccessGate, and Page SDK hooks must stay inside OpenXiangdaProvider + OpenXiangdaPageProvider.',
|
|
17123
17474
|
'settings public-access is legacy form public-access compatibility/repair and should not be used for new React SPA apps.',
|
|
17124
17475
|
'Direct live mutation commands should use --dry-run first and --write-manifest when the repository should remain source of truth.',
|
|
@@ -17513,6 +17864,7 @@ function directManifestDir(resourceType) {
|
|
|
17513
17864
|
connector: path.join('src', 'resources', 'connectors'),
|
|
17514
17865
|
notification: path.join('src', 'resources', 'notifications'),
|
|
17515
17866
|
'data-view': path.join('src', 'resources', 'data-views'),
|
|
17867
|
+
webhook: path.join('src', 'resources', 'webhooks'),
|
|
17516
17868
|
menu: path.join('src', 'resources', 'menus'),
|
|
17517
17869
|
role: path.join('src', 'resources', 'roles'),
|
|
17518
17870
|
'page-permission-group': path.join('src', 'resources', 'permissions', 'page-groups'),
|
|
@@ -17532,6 +17884,7 @@ function removeDirectStateResource(target, resourceType, code) {
|
|
|
17532
17884
|
function: 'functions',
|
|
17533
17885
|
connector: 'connectors',
|
|
17534
17886
|
'data-view': 'dataViews',
|
|
17887
|
+
webhook: 'webhooks',
|
|
17535
17888
|
};
|
|
17536
17889
|
const bucket = buckets[resourceType];
|
|
17537
17890
|
if (!bucket || !target.bound.resources?.[bucket]?.[code]) return;
|
|
@@ -17854,6 +18207,7 @@ function ensureResourceBuckets(bound) {
|
|
|
17854
18207
|
bound.resources.connectors = bound.resources.connectors || {};
|
|
17855
18208
|
bound.resources.dataViews = bound.resources.dataViews || {};
|
|
17856
18209
|
bound.resources.storageConfigs = bound.resources.storageConfigs || {};
|
|
18210
|
+
bound.resources.webhooks = bound.resources.webhooks || {};
|
|
17857
18211
|
bound.resources.authConfigs = bound.resources.authConfigs || {};
|
|
17858
18212
|
bound.resources.routes = bound.resources.routes || {};
|
|
17859
18213
|
bound.resources.publicAccessPolicies = bound.resources.publicAccessPolicies || {};
|
|
@@ -18199,6 +18553,29 @@ function saveStorageConfigResource(target, storageCode, storageConfigId, extra =
|
|
|
18199
18553
|
}, keys);
|
|
18200
18554
|
}
|
|
18201
18555
|
|
|
18556
|
+
function saveWebhookResource(target, webhookCode, data = {}) {
|
|
18557
|
+
const keys = [
|
|
18558
|
+
'webhookId',
|
|
18559
|
+
'endpointId',
|
|
18560
|
+
'callbackPath',
|
|
18561
|
+
'callbackUrl',
|
|
18562
|
+
'targetFunctionCode',
|
|
18563
|
+
'idempotencyQueryParam',
|
|
18564
|
+
'maxBodyBytes',
|
|
18565
|
+
'status',
|
|
18566
|
+
'revision',
|
|
18567
|
+
];
|
|
18568
|
+
const callbackPath = String(data?.callbackPath || '').trim();
|
|
18569
|
+
const callbackUrl = callbackPath
|
|
18570
|
+
? `${String(target.profile?.baseUrl || '').replace(/\/+$/, '')}${callbackPath}`
|
|
18571
|
+
: data?.callbackUrl;
|
|
18572
|
+
saveStateResource(target, 'webhooks', webhookCode, {
|
|
18573
|
+
...pickStateFields(data, keys),
|
|
18574
|
+
webhookId: data?.id || data?.webhookId,
|
|
18575
|
+
callbackUrl,
|
|
18576
|
+
}, keys);
|
|
18577
|
+
}
|
|
18578
|
+
|
|
18202
18579
|
function saveScopeResource(target, bucket, code, data = {}) {
|
|
18203
18580
|
if (!bucket || !code) return;
|
|
18204
18581
|
const keys = ['id', 'resourceCode', 'name', 'updatedAt'];
|
|
@@ -18347,6 +18724,7 @@ const RESOURCE_SPECS = [
|
|
|
18347
18724
|
{ key: 'functions', dir: 'functions', topFiles: ['functions.json'], pluralKeys: ['functions'] },
|
|
18348
18725
|
{ key: 'dataViews', dir: 'data-views', topFiles: ['data-views.json'], pluralKeys: ['dataViews', 'data-views'] },
|
|
18349
18726
|
{ key: 'storageConfigs', dir: 'storage', topFiles: ['storage.json'], pluralKeys: ['storageConfigs', 'storage'] },
|
|
18727
|
+
{ key: 'webhooks', dir: 'webhooks', topFiles: ['webhooks.json'], pluralKeys: ['webhooks'] },
|
|
18350
18728
|
{ key: 'authConfigs', dir: 'auth', topFiles: ['auth.json'], pluralKeys: ['authConfigs', 'auth'] },
|
|
18351
18729
|
{ key: 'routes', dir: 'routes', topFiles: ['routes.json'], pluralKeys: ['routes'] },
|
|
18352
18730
|
{
|
|
@@ -18426,6 +18804,8 @@ const RESOURCE_TYPE_ALIASES = new Map([
|
|
|
18426
18804
|
['oss', 'storageConfigs'],
|
|
18427
18805
|
['oss-config', 'storageConfigs'],
|
|
18428
18806
|
['oss-configs', 'storageConfigs'],
|
|
18807
|
+
['webhook', 'webhooks'],
|
|
18808
|
+
['webhooks', 'webhooks'],
|
|
18429
18809
|
['auth', 'authConfigs'],
|
|
18430
18810
|
['auth-config', 'authConfigs'],
|
|
18431
18811
|
['auth-configs', 'authConfigs'],
|
|
@@ -19138,6 +19518,7 @@ function generateResourceTypes(manifest, outputFile) {
|
|
|
19138
19518
|
const dataViewCodes = unique((manifest.dataViews || []).map(item => item.code).filter(Boolean));
|
|
19139
19519
|
const functionCodes = unique((manifest.functions || []).map(item => item.code).filter(Boolean));
|
|
19140
19520
|
const storageConfigCodes = unique((manifest.storageConfigs || []).map(item => item.code).filter(Boolean));
|
|
19521
|
+
const webhookCodes = unique((manifest.webhooks || []).map(item => item.code).filter(Boolean));
|
|
19141
19522
|
const authConfigCodes = unique((manifest.authConfigs || []).map(item => item.code).filter(Boolean));
|
|
19142
19523
|
const publicAccessPolicyCodes = unique((manifest.publicAccessPolicies || []).map(item => item.code).filter(Boolean));
|
|
19143
19524
|
const scopeDimensionCodes = unique((manifest.scopeDimensions || []).map(item => item.code).filter(Boolean));
|
|
@@ -19162,6 +19543,9 @@ function generateResourceTypes(manifest, outputFile) {
|
|
|
19162
19543
|
`export const storageConfigCodes = ${JSON.stringify(storageConfigCodes, null, 2)} as const`,
|
|
19163
19544
|
'export type StorageConfigCode = typeof storageConfigCodes[number]',
|
|
19164
19545
|
'',
|
|
19546
|
+
`export const webhookCodes = ${JSON.stringify(webhookCodes, null, 2)} as const`,
|
|
19547
|
+
'export type WebhookCode = typeof webhookCodes[number]',
|
|
19548
|
+
'',
|
|
19165
19549
|
`export const functionCodes = ${JSON.stringify(functionCodes, null, 2)} as const`,
|
|
19166
19550
|
'export type FunctionCode = typeof functionCodes[number]',
|
|
19167
19551
|
'',
|
|
@@ -19196,6 +19580,7 @@ function generateResourceTypes(manifest, outputFile) {
|
|
|
19196
19580
|
pagePermissionGroups: pagePermissionGroupCodes.length,
|
|
19197
19581
|
dataViews: dataViewCodes.length,
|
|
19198
19582
|
storageConfigs: storageConfigCodes.length,
|
|
19583
|
+
webhooks: webhookCodes.length,
|
|
19199
19584
|
functions: functionCodes.length,
|
|
19200
19585
|
authConfigs: authConfigCodes.length,
|
|
19201
19586
|
publicAccessPolicies: publicAccessPolicyCodes.length,
|
|
@@ -19273,6 +19658,50 @@ function validateResourceItem(kind, item, errors, warnings) {
|
|
|
19273
19658
|
validateFunctionRuntimeInvoke(label, item, errors, warnings);
|
|
19274
19659
|
validateFunctionPerformancePolicies(label, item, errors);
|
|
19275
19660
|
}
|
|
19661
|
+
if (kind === 'webhooks') {
|
|
19662
|
+
const allowed = new Set([
|
|
19663
|
+
'code',
|
|
19664
|
+
'resourceCode',
|
|
19665
|
+
'name',
|
|
19666
|
+
'description',
|
|
19667
|
+
'targetFunctionCode',
|
|
19668
|
+
'idempotencyQueryParam',
|
|
19669
|
+
'maxBodyBytes',
|
|
19670
|
+
'status',
|
|
19671
|
+
]);
|
|
19672
|
+
const unsupported = Object.keys(item).filter(
|
|
19673
|
+
key => !key.startsWith('__') && !allowed.has(key)
|
|
19674
|
+
);
|
|
19675
|
+
if (unsupported.length > 0) {
|
|
19676
|
+
errors.push(`${label}: 包含不支持的字段 ${unsupported.join(', ')}`);
|
|
19677
|
+
}
|
|
19678
|
+
if (!item.name) errors.push(`${label}: 缺少 name`);
|
|
19679
|
+
if (!/^[A-Za-z][A-Za-z0-9_]{1,127}$/.test(String(item.targetFunctionCode || ''))) {
|
|
19680
|
+
errors.push(`${label}: targetFunctionCode 格式不正确`);
|
|
19681
|
+
}
|
|
19682
|
+
if (
|
|
19683
|
+
item.idempotencyQueryParam !== undefined &&
|
|
19684
|
+
!/^[A-Za-z][A-Za-z0-9_.-]{0,127}$/.test(
|
|
19685
|
+
String(item.idempotencyQueryParam)
|
|
19686
|
+
)
|
|
19687
|
+
) {
|
|
19688
|
+
errors.push(`${label}: idempotencyQueryParam 格式不正确`);
|
|
19689
|
+
}
|
|
19690
|
+
const maxBodyBytes = Number(item.maxBodyBytes || 262144);
|
|
19691
|
+
if (
|
|
19692
|
+
!Number.isInteger(maxBodyBytes) ||
|
|
19693
|
+
maxBodyBytes < 1024 ||
|
|
19694
|
+
maxBodyBytes > 1048576
|
|
19695
|
+
) {
|
|
19696
|
+
errors.push(`${label}: maxBodyBytes 必须在 1024 到 1048576 之间`);
|
|
19697
|
+
}
|
|
19698
|
+
if (!['active', 'disabled'].includes(String(item.status || 'active'))) {
|
|
19699
|
+
errors.push(`${label}: status 只能是 active 或 disabled`);
|
|
19700
|
+
}
|
|
19701
|
+
if (Object.keys(item).some(key => /secret|signature|hmac/i.test(key))) {
|
|
19702
|
+
errors.push(`${label}: Webhook manifest 不能包含 Secret 或供应商验签配置`);
|
|
19703
|
+
}
|
|
19704
|
+
}
|
|
19276
19705
|
if (kind === 'dataViews') {
|
|
19277
19706
|
const definition = item.definition || item;
|
|
19278
19707
|
const viewType = String(definition.viewType || 'row').toLowerCase();
|
|
@@ -20384,6 +20813,7 @@ async function buildResourcePlan(
|
|
|
20384
20813
|
await addAutomationPlanActions(config, target, actions, manifest.automations, existing.automations);
|
|
20385
20814
|
addPlanActions(actions, 'dataView', manifest.dataViews, existing.dataViews, (item, current) => dataViewEquals(target.bound, item, current));
|
|
20386
20815
|
addPlanActions(actions, 'storageConfig', manifest.storageConfigs, existing.storageConfigs, (item, current) => storageConfigEquals(item, current));
|
|
20816
|
+
addPlanActions(actions, 'webhook', manifest.webhooks, existing.webhooks, webhookEquals);
|
|
20387
20817
|
addPlanActions(actions, 'scopeDimension', manifest.scopeDimensions, existing.scopeDimensions, (item, current) => scopeDimensionEquals(target.bound, item, current));
|
|
20388
20818
|
addPlanActions(actions, 'scopeGrantSource', manifest.scopeGrantSources, existing.scopeGrantSources, (item, current) => scopeGrantSourceEquals(target.bound, item, current));
|
|
20389
20819
|
addPlanActions(actions, 'dataScopePolicy', manifest.dataScopePolicies, existing.dataScopePolicies, dataScopePolicyEquals);
|
|
@@ -22244,6 +22674,7 @@ async function publishResourceManifest(config, target, manifest, options = {}) {
|
|
|
22244
22674
|
await publishDataScopePolicyResources(config, target, manifest.dataScopePolicies || [], result, publishOptions);
|
|
22245
22675
|
await publishDataViewResources(config, target, manifest.dataViews || [], result, publishOptions);
|
|
22246
22676
|
await publishStorageConfigResources(config, target, manifest.storageConfigs || [], result, publishOptions);
|
|
22677
|
+
await publishWebhookResources(config, target, manifest.webhooks || [], result, publishOptions);
|
|
22247
22678
|
await publishPublicAccessPolicyResources(config, target, manifest.publicAccessPolicies || [], result, publishOptions);
|
|
22248
22679
|
await publishPagePermissionGroupResources(config, target, manifest.pagePermissionGroups || [], result, publishOptions);
|
|
22249
22680
|
await publishFormPermissionGroupResources(
|
|
@@ -22390,6 +22821,7 @@ const GENERIC_RESOURCE_KEY_BY_PLAN_KIND = Object.freeze({
|
|
|
22390
22821
|
publicAccessPolicy: 'publicAccessPolicies',
|
|
22391
22822
|
dataView: 'dataViews',
|
|
22392
22823
|
storageConfig: 'storageConfigs',
|
|
22824
|
+
webhook: 'webhooks',
|
|
22393
22825
|
scopeDimension: 'scopeDimensions',
|
|
22394
22826
|
scopeGrantSource: 'scopeGrantSources',
|
|
22395
22827
|
dataScopePolicy: 'dataScopePolicies',
|
|
@@ -22898,6 +23330,7 @@ async function fetchExistingResourceMaps(config, target, manifest) {
|
|
|
22898
23330
|
automations: new Map(),
|
|
22899
23331
|
dataViews: new Map(),
|
|
22900
23332
|
storageConfigs: new Map(),
|
|
23333
|
+
webhooks: new Map(),
|
|
22901
23334
|
scopeDimensions: new Map(),
|
|
22902
23335
|
scopeGrantSources: new Map(),
|
|
22903
23336
|
dataScopePolicies: new Map(),
|
|
@@ -23124,6 +23557,21 @@ async function fetchExistingResourceMaps(config, target, manifest) {
|
|
|
23124
23557
|
});
|
|
23125
23558
|
}
|
|
23126
23559
|
}
|
|
23560
|
+
if ((manifest.webhooks || []).length > 0) {
|
|
23561
|
+
const data = await requestWithAuth(
|
|
23562
|
+
config,
|
|
23563
|
+
target.profileName,
|
|
23564
|
+
apiPathWithQuery(
|
|
23565
|
+
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/webhooks`,
|
|
23566
|
+
{ page: 1, pageSize: 1000 }
|
|
23567
|
+
)
|
|
23568
|
+
);
|
|
23569
|
+
indexByCode(
|
|
23570
|
+
maps.webhooks,
|
|
23571
|
+
normalizeItems(data),
|
|
23572
|
+
item => item.code || item.resourceCode
|
|
23573
|
+
);
|
|
23574
|
+
}
|
|
23127
23575
|
if ((manifest.scopeDimensions || []).length > 0) {
|
|
23128
23576
|
const data = await requestWithAuth(
|
|
23129
23577
|
config,
|
|
@@ -25115,6 +25563,45 @@ async function publishStorageConfigResources(config, target, storageConfigs, res
|
|
|
25115
25563
|
}
|
|
25116
25564
|
}
|
|
25117
25565
|
|
|
25566
|
+
async function publishWebhookResources(config, target, webhooks, result, options = {}) {
|
|
25567
|
+
for (const webhookItem of webhooks) {
|
|
25568
|
+
const code = webhookItem.code || webhookItem.resourceCode;
|
|
25569
|
+
if (shouldSkipNoopResource(options, 'webhook', code)) {
|
|
25570
|
+
recordNoopResource(result, 'webhook', code, options);
|
|
25571
|
+
continue;
|
|
25572
|
+
}
|
|
25573
|
+
const existing = await findExistingWebhook(config, target, code);
|
|
25574
|
+
const existingRevision = Number(existing?.revision);
|
|
25575
|
+
const body = {
|
|
25576
|
+
...normalizeWebhookManifest(webhookItem),
|
|
25577
|
+
expectedRevision:
|
|
25578
|
+
existing && Number.isInteger(existingRevision) && existingRevision > 0
|
|
25579
|
+
? existingRevision
|
|
25580
|
+
: existing
|
|
25581
|
+
? undefined
|
|
25582
|
+
: 0,
|
|
25583
|
+
};
|
|
25584
|
+
const data = await requestWithAuth(
|
|
25585
|
+
config,
|
|
25586
|
+
target.profileName,
|
|
25587
|
+
existing
|
|
25588
|
+
? `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/webhooks/${encodeURIComponent(code)}`
|
|
25589
|
+
: `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/webhooks`,
|
|
25590
|
+
{ method: 'POST', body }
|
|
25591
|
+
);
|
|
25592
|
+
saveWebhookResource(target, code, data);
|
|
25593
|
+
result.published.push({
|
|
25594
|
+
kind: 'webhook',
|
|
25595
|
+
code,
|
|
25596
|
+
action: existing ? 'update' : 'create',
|
|
25597
|
+
id: data?.id,
|
|
25598
|
+
endpointId: data?.endpointId,
|
|
25599
|
+
callbackPath: data?.callbackPath,
|
|
25600
|
+
status: data?.status,
|
|
25601
|
+
});
|
|
25602
|
+
}
|
|
25603
|
+
}
|
|
25604
|
+
|
|
25118
25605
|
async function publishDataViewPermissionGroups(config, target, dataViewCode, permissionGroups) {
|
|
25119
25606
|
await requestWithAuth(
|
|
25120
25607
|
config,
|
|
@@ -25279,6 +25766,25 @@ async function findExistingStorageConfig(config, target, code) {
|
|
|
25279
25766
|
return null;
|
|
25280
25767
|
}
|
|
25281
25768
|
|
|
25769
|
+
async function findExistingWebhook(config, target, code) {
|
|
25770
|
+
const state = target.bound.resources?.webhooks?.[code];
|
|
25771
|
+
const byCode = await requestOptionalWithAuth(
|
|
25772
|
+
config,
|
|
25773
|
+
target.profileName,
|
|
25774
|
+
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/webhooks/${encodeURIComponent(code)}`
|
|
25775
|
+
);
|
|
25776
|
+
if (byCode?.id) return byCode;
|
|
25777
|
+
if (state?.webhookId) {
|
|
25778
|
+
return {
|
|
25779
|
+
id: state.webhookId,
|
|
25780
|
+
code,
|
|
25781
|
+
revision: state.revision,
|
|
25782
|
+
endpointId: state.endpointId,
|
|
25783
|
+
};
|
|
25784
|
+
}
|
|
25785
|
+
return null;
|
|
25786
|
+
}
|
|
25787
|
+
|
|
25282
25788
|
async function findExistingAutomation(config, target, code) {
|
|
25283
25789
|
const stateId = target.bound.resources?.automations?.[code]?.automationId;
|
|
25284
25790
|
if (stateId) {
|
|
@@ -25557,6 +26063,22 @@ async function collectPruneResourceCandidates(config, target, manifest) {
|
|
|
25557
26063
|
item => item.code || item.resourceCode
|
|
25558
26064
|
);
|
|
25559
26065
|
}
|
|
26066
|
+
if (enabled('webhooks')) {
|
|
26067
|
+
const data = await requestWithAuth(
|
|
26068
|
+
config,
|
|
26069
|
+
target.profileName,
|
|
26070
|
+
apiPathWithQuery(`${appPath}/webhooks`, {
|
|
26071
|
+
page: 1,
|
|
26072
|
+
pageSize: 1000,
|
|
26073
|
+
})
|
|
26074
|
+
);
|
|
26075
|
+
append(
|
|
26076
|
+
'webhook',
|
|
26077
|
+
desiredCodes(manifest.webhooks),
|
|
26078
|
+
normalizeItems(data),
|
|
26079
|
+
item => item.code || item.resourceCode
|
|
26080
|
+
);
|
|
26081
|
+
}
|
|
25560
26082
|
if (enabled('publicAccessPolicies')) {
|
|
25561
26083
|
const data = await requestWithAuth(
|
|
25562
26084
|
config,
|
|
@@ -25747,6 +26269,21 @@ async function pruneFrozenResourceCandidate(config, target, result, candidate) {
|
|
|
25747
26269
|
if (kind === 'storageConfig') {
|
|
25748
26270
|
return postDelete(`${appPath}/storage-configs/${encodeURIComponent(code)}`);
|
|
25749
26271
|
}
|
|
26272
|
+
if (kind === 'webhook') {
|
|
26273
|
+
return requestWithAuth(
|
|
26274
|
+
config,
|
|
26275
|
+
target.profileName,
|
|
26276
|
+
apiPathWithPostDeleteAction(
|
|
26277
|
+
`${appPath}/webhooks/${encodeURIComponent(code)}`
|
|
26278
|
+
),
|
|
26279
|
+
{
|
|
26280
|
+
method: 'POST',
|
|
26281
|
+
body: candidate.frozenRevision
|
|
26282
|
+
? { expectedRevision: candidate.frozenRevision }
|
|
26283
|
+
: {},
|
|
26284
|
+
}
|
|
26285
|
+
);
|
|
26286
|
+
}
|
|
25750
26287
|
if (kind === 'publicAccessPolicy') {
|
|
25751
26288
|
return postDelete(`${appPath}/public-access/policies/${encodeURIComponent(code)}`);
|
|
25752
26289
|
}
|
|
@@ -26147,6 +26684,7 @@ function removeStateResource(target, kind, code) {
|
|
|
26147
26684
|
publicAccessPolicy: 'publicAccessPolicies',
|
|
26148
26685
|
dataView: 'dataViews',
|
|
26149
26686
|
storageConfig: 'storageConfigs',
|
|
26687
|
+
webhook: 'webhooks',
|
|
26150
26688
|
pagePermissionGroup: 'pagePermissionGroups',
|
|
26151
26689
|
formPermissionGroup: 'formPermissionGroups',
|
|
26152
26690
|
};
|
|
@@ -26449,6 +26987,22 @@ async function pullResources(config, target) {
|
|
|
26449
26987
|
written.push(path.relative(process.cwd(), filePath));
|
|
26450
26988
|
}
|
|
26451
26989
|
|
|
26990
|
+
const webhooks = await requestWithAuth(
|
|
26991
|
+
config,
|
|
26992
|
+
target.profileName,
|
|
26993
|
+
apiPathWithQuery(
|
|
26994
|
+
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/webhooks`,
|
|
26995
|
+
{ page: 1, pageSize: 1000 }
|
|
26996
|
+
)
|
|
26997
|
+
);
|
|
26998
|
+
for (const webhookItem of normalizeItems(webhooks)) {
|
|
26999
|
+
const code = webhookItem.code || webhookItem.resourceCode || webhookItem.id;
|
|
27000
|
+
const filePath = path.join(baseDir, 'webhooks', `${code}.json`);
|
|
27001
|
+
writeResourceJsonFile(filePath, toPulledWebhook(webhookItem));
|
|
27002
|
+
written.push(path.relative(process.cwd(), filePath));
|
|
27003
|
+
saveWebhookResource(target, code, webhookItem);
|
|
27004
|
+
}
|
|
27005
|
+
|
|
26452
27006
|
const pageGroups = await requestWithAuth(
|
|
26453
27007
|
config,
|
|
26454
27008
|
target.profileName,
|
|
@@ -26722,6 +27276,18 @@ function toPulledStorageConfig(storageConfig) {
|
|
|
26722
27276
|
});
|
|
26723
27277
|
}
|
|
26724
27278
|
|
|
27279
|
+
function toPulledWebhook(webhookItem) {
|
|
27280
|
+
return stripUndefinedValues({
|
|
27281
|
+
code: webhookItem.code || webhookItem.resourceCode,
|
|
27282
|
+
name: webhookItem.name,
|
|
27283
|
+
description: webhookItem.description || '',
|
|
27284
|
+
targetFunctionCode: webhookItem.targetFunctionCode,
|
|
27285
|
+
idempotencyQueryParam: webhookItem.idempotencyQueryParam || 'nonce',
|
|
27286
|
+
maxBodyBytes: webhookItem.maxBodyBytes || 262144,
|
|
27287
|
+
status: webhookItem.status || 'active',
|
|
27288
|
+
});
|
|
27289
|
+
}
|
|
27290
|
+
|
|
26725
27291
|
function rewriteDataViewDefinitionForManifest(definition, lookups) {
|
|
26726
27292
|
rewriteDataViewSourceForManifest(definition.base, lookups);
|
|
26727
27293
|
for (const join of definition.joins || []) {
|
|
@@ -27174,6 +27740,61 @@ function normalizeStorageConfigManifest(storageConfig) {
|
|
|
27174
27740
|
});
|
|
27175
27741
|
}
|
|
27176
27742
|
|
|
27743
|
+
function normalizeWebhookManifest(webhook) {
|
|
27744
|
+
const code = String(webhook.code || webhook.resourceCode || '').trim();
|
|
27745
|
+
const allowed = new Set([
|
|
27746
|
+
'code',
|
|
27747
|
+
'resourceCode',
|
|
27748
|
+
'name',
|
|
27749
|
+
'description',
|
|
27750
|
+
'targetFunctionCode',
|
|
27751
|
+
'idempotencyQueryParam',
|
|
27752
|
+
'maxBodyBytes',
|
|
27753
|
+
'status',
|
|
27754
|
+
]);
|
|
27755
|
+
const unsupported = Object.keys(webhook).filter(
|
|
27756
|
+
key => !key.startsWith('__') && !allowed.has(key)
|
|
27757
|
+
);
|
|
27758
|
+
if (unsupported.length > 0) {
|
|
27759
|
+
fail(`webhook:${code || '<unknown>'} 包含不支持的字段 ${unsupported.join(', ')}`);
|
|
27760
|
+
}
|
|
27761
|
+
if (!/^[A-Za-z][A-Za-z0-9_]{1,127}$/.test(code)) {
|
|
27762
|
+
fail('Webhook code 格式不正确');
|
|
27763
|
+
}
|
|
27764
|
+
const targetFunctionCode = String(webhook.targetFunctionCode || '').trim();
|
|
27765
|
+
if (!/^[A-Za-z][A-Za-z0-9_]{1,127}$/.test(targetFunctionCode)) {
|
|
27766
|
+
fail(`webhook:${code} targetFunctionCode 格式不正确`);
|
|
27767
|
+
}
|
|
27768
|
+
const idempotencyQueryParam = String(
|
|
27769
|
+
webhook.idempotencyQueryParam || 'nonce'
|
|
27770
|
+
).trim();
|
|
27771
|
+
if (!/^[A-Za-z][A-Za-z0-9_.-]{0,127}$/.test(idempotencyQueryParam)) {
|
|
27772
|
+
fail(`webhook:${code} idempotencyQueryParam 格式不正确`);
|
|
27773
|
+
}
|
|
27774
|
+
const maxBodyBytes = Number(webhook.maxBodyBytes || 262144);
|
|
27775
|
+
if (
|
|
27776
|
+
!Number.isInteger(maxBodyBytes) ||
|
|
27777
|
+
maxBodyBytes < 1024 ||
|
|
27778
|
+
maxBodyBytes > 1048576
|
|
27779
|
+
) {
|
|
27780
|
+
fail(`webhook:${code} maxBodyBytes 必须在 1024 到 1048576 之间`);
|
|
27781
|
+
}
|
|
27782
|
+
const status = String(webhook.status || 'active');
|
|
27783
|
+
if (!['active', 'disabled'].includes(status)) {
|
|
27784
|
+
fail(`webhook:${code} status 只能是 active 或 disabled`);
|
|
27785
|
+
}
|
|
27786
|
+
return stripUndefinedValues({
|
|
27787
|
+
code,
|
|
27788
|
+
name: String(webhook.name || code).trim(),
|
|
27789
|
+
description:
|
|
27790
|
+
webhook.description === undefined ? '' : String(webhook.description),
|
|
27791
|
+
targetFunctionCode,
|
|
27792
|
+
idempotencyQueryParam,
|
|
27793
|
+
maxBodyBytes,
|
|
27794
|
+
status,
|
|
27795
|
+
});
|
|
27796
|
+
}
|
|
27797
|
+
|
|
27177
27798
|
function normalizeStorageCorsForPublish(cors) {
|
|
27178
27799
|
if (cors === undefined || cors === null || cors === '') return undefined;
|
|
27179
27800
|
const managed =
|
|
@@ -28321,6 +28942,21 @@ function storageConfigEquals(desired, existing) {
|
|
|
28321
28942
|
);
|
|
28322
28943
|
}
|
|
28323
28944
|
|
|
28945
|
+
function webhookEquals(desired, existing) {
|
|
28946
|
+
if (!existing) return false;
|
|
28947
|
+
const expected = normalizeWebhookManifest(desired);
|
|
28948
|
+
return (
|
|
28949
|
+
String(existing.code || existing.resourceCode || '') === expected.code &&
|
|
28950
|
+
String(existing.name || '') === expected.name &&
|
|
28951
|
+
String(existing.description || '') === expected.description &&
|
|
28952
|
+
String(existing.targetFunctionCode || '') === expected.targetFunctionCode &&
|
|
28953
|
+
String(existing.idempotencyQueryParam || 'nonce') ===
|
|
28954
|
+
expected.idempotencyQueryParam &&
|
|
28955
|
+
Number(existing.maxBodyBytes || 262144) === expected.maxBodyBytes &&
|
|
28956
|
+
String(existing.status || 'active') === expected.status
|
|
28957
|
+
);
|
|
28958
|
+
}
|
|
28959
|
+
|
|
28324
28960
|
function routeEquals(desired, existing) {
|
|
28325
28961
|
if (!existing) return false;
|
|
28326
28962
|
const expected = normalizeRouteManifest(desired);
|
|
@@ -31135,6 +31771,9 @@ function buildWorkspacePublishEnv(
|
|
|
31135
31771
|
env.OPENXIANGDA_BASE_URL = profile.baseUrl;
|
|
31136
31772
|
env.OPENXIANGDA_ACCESS_TOKEN = profile.token.accessToken;
|
|
31137
31773
|
env.OPENXIANGDA_APP_TYPE = appType;
|
|
31774
|
+
if (!env.OPENXIANGDA_CLI) {
|
|
31775
|
+
env.OPENXIANGDA_CLI = path.resolve(__dirname, '..', 'bin', 'openxiangda.js');
|
|
31776
|
+
}
|
|
31138
31777
|
if (targetName) env.OPENXIANGDA_TARGET = targetName;
|
|
31139
31778
|
else delete env.OPENXIANGDA_TARGET;
|
|
31140
31779
|
env.OPENXIANGDA_GLOBAL_ENV_KEYS = injectedGlobalKeys.join(',');
|
|
@@ -31144,8 +31783,11 @@ function buildWorkspacePublishEnv(
|
|
|
31144
31783
|
}
|
|
31145
31784
|
|
|
31146
31785
|
module.exports = {
|
|
31786
|
+
assertLegacyPageReleaseCoverage,
|
|
31147
31787
|
assertReleasePublishAdoptionFlags,
|
|
31148
31788
|
assertReleasePublishAdoptionScope,
|
|
31789
|
+
assertReleasePublishManifestReplacementFlags,
|
|
31790
|
+
assertReleasePublishManifestReplacementScope,
|
|
31149
31791
|
assertRuntimeReleaseReusable,
|
|
31150
31792
|
buildOpenXiangdaUpdateInstallPlan,
|
|
31151
31793
|
buildResourceManifestSddTargets,
|