openxiangda 1.0.268 → 1.0.270
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 +20 -1
- package/lib/cli.js +417 -24
- package/lib/config.js +60 -56
- package/lib/design-gates.js +18 -0
- package/lib/migration-advice.js +19 -0
- package/lib/release-plan.js +2 -0
- package/lib/workspace-init.js +11 -4
- package/openxiangda-skills/SKILL.md +8 -0
- package/openxiangda-skills/references/architecture-patterns.md +218 -218
- package/openxiangda-skills/references/component-guide.md +122 -122
- package/openxiangda-skills/references/openxiangda-api.md +12 -0
- package/openxiangda-skills/references/platform-data-model.md +285 -285
- package/openxiangda-skills/references/resource-manifest-cheatsheet.md +32 -2
- package/openxiangda-skills/references/troubleshooting.md +224 -224
- package/openxiangda-skills/references/webhooks.md +213 -0
- package/openxiangda-skills/references/workspace-state.md +1 -1
- package/openxiangda-skills/skills/openxiangda-core/SKILL.md +1 -1
- package/openxiangda-skills/skills/openxiangda-workflow-automation/SKILL.md +3 -0
- package/package.json +26 -23
- package/packages/sdk/dist/{ProcessPreview-Cyk6uv-w.d.mts → ProcessPreview-DSUIJi5V.d.mts} +2 -3
- package/packages/sdk/dist/{ProcessPreview-Cyk6uv-w.d.ts → ProcessPreview-DSUIJi5V.d.ts} +2 -3
- package/packages/sdk/dist/components/index.d.mts +41 -42
- package/packages/sdk/dist/components/index.d.ts +41 -42
- package/packages/sdk/dist/{dataManagementApi-CE8Zyj3a.d.ts → dataManagementApi-Ro9itwm8.d.ts} +2 -2
- package/packages/sdk/dist/{dataManagementApi-4fSaCA5t.d.mts → dataManagementApi-p_HOhQnA.d.mts} +2 -2
- package/packages/sdk/dist/runtime/index.d.mts +3 -4
- package/packages/sdk/dist/runtime/index.d.ts +3 -4
- package/packages/sdk/dist/runtime/react.d.mts +5 -5
- package/packages/sdk/dist/runtime/react.d.ts +5 -5
- package/packages/sdk/src/build-source/scripts/utils/mime-types.mjs +70 -70
- package/packages/sdk/src/build-source/scripts/utils/oss-client.mjs +128 -128
- package/packages/sdk/src/build-source/scripts/utils/progress.mjs +57 -57
- package/templates/openxiangda-react-spa/.cursor/rules/openxiangda-resources.mdc +1 -0
- package/templates/openxiangda-react-spa/.cursor/rules/openxiangda.mdc +6 -0
- package/templates/openxiangda-react-spa/.qoder/rules/openxiangda-resources.md +1 -0
- package/templates/openxiangda-react-spa/.qoder/rules/openxiangda.md +6 -0
- package/templates/openxiangda-react-spa/AGENTS.md +8 -0
- package/templates/sy-lowcode-app-workspace/.cursor/rules/openxiangda-resources.mdc +2 -0
- package/templates/sy-lowcode-app-workspace/.cursor/rules/openxiangda.mdc +7 -1
- package/templates/sy-lowcode-app-workspace/.qoder/rules/openxiangda-resources.md +1 -0
- package/templates/sy-lowcode-app-workspace/.qoder/rules/openxiangda.md +7 -1
- package/templates/sy-lowcode-app-workspace/AGENTS.md +9 -1
package/lib/cli.js
CHANGED
|
@@ -7,7 +7,7 @@ const { spawn, spawnSync } = require('child_process');
|
|
|
7
7
|
const { pathToFileURL } = require('url');
|
|
8
8
|
const esbuild = require('esbuild');
|
|
9
9
|
const {
|
|
10
|
-
|
|
10
|
+
getConfigFile,
|
|
11
11
|
GLOBAL_ENV_FILE,
|
|
12
12
|
PROJECT_STATE_FILE,
|
|
13
13
|
getProfile,
|
|
@@ -115,6 +115,7 @@ const {
|
|
|
115
115
|
} = require('./resource-binding-contract');
|
|
116
116
|
const { getSkillStatusReport, installSkills } = require('./skills');
|
|
117
117
|
const { resolveUpdateTarget, buildGenerationUpdateCommand } = require('./generation-update');
|
|
118
|
+
const { migrationAdvice, printMigrationAdvice } = require('./migration-advice');
|
|
118
119
|
const {
|
|
119
120
|
assertOrClaimWorktreeOwner,
|
|
120
121
|
claimWorktreeOwner,
|
|
@@ -233,7 +234,12 @@ const genericGitBasePlanCache = new Map();
|
|
|
233
234
|
async function main(argv) {
|
|
234
235
|
let commandError = null;
|
|
235
236
|
try {
|
|
236
|
-
|
|
237
|
+
const result = await mainImpl(argv);
|
|
238
|
+
const [command, action] = argv;
|
|
239
|
+
if (!argv.some(value => value === '--json' || value.startsWith('--json=')) && (!command || ['version', '--version', '-v', 'help', '--help', '-h', 'update'].includes(command) || command === 'skill' && ['install', 'bootstrap'].includes(action))) {
|
|
240
|
+
printMigrationAdvice();
|
|
241
|
+
}
|
|
242
|
+
return result;
|
|
237
243
|
} catch (error) {
|
|
238
244
|
commandError = error;
|
|
239
245
|
throw error;
|
|
@@ -293,6 +299,7 @@ async function mainImpl(argv) {
|
|
|
293
299
|
if (command === 'workflow') return workflow(rest);
|
|
294
300
|
if (command === 'automation') return automation(rest);
|
|
295
301
|
if (command === 'data-view') return dataView(rest);
|
|
302
|
+
if (command === 'webhook') return webhook(rest);
|
|
296
303
|
if (command === 'scope') return scope(rest);
|
|
297
304
|
if (command === 'route') return route(rest);
|
|
298
305
|
if (command === 'public-access') return publicAccess(rest);
|
|
@@ -319,12 +326,12 @@ function printHelp() {
|
|
|
319
326
|
|
|
320
327
|
Usage:
|
|
321
328
|
openxiangda version [--json]
|
|
322
|
-
openxiangda login <platform-url> [--profile name]
|
|
323
|
-
openxiangda update check|install [--json] [--registry https://registry.npmjs.org]
|
|
329
|
+
openxiangda login <platform-url> [--profile name] [--cwd directory]
|
|
330
|
+
openxiangda update check|install [--target workspace|launcher] [--json] [--registry https://registry.npmjs.org]
|
|
324
331
|
openxiangda platform add <name> <platform-url>
|
|
325
332
|
openxiangda platform list
|
|
326
333
|
openxiangda platform use <name>
|
|
327
|
-
openxiangda auth status|refresh|logout [--profile name]
|
|
334
|
+
openxiangda auth status|refresh|logout [--profile name] [--cwd directory]
|
|
328
335
|
openxiangda doctor [--profile name] [--app-type APP_XXX] [--release] [--json]
|
|
329
336
|
openxiangda contract list|explain <contract-name> [--json]
|
|
330
337
|
openxiangda policy show|check|render [--json]
|
|
@@ -383,6 +390,7 @@ Usage:
|
|
|
383
390
|
openxiangda automation diagnose <automationCode|automationId> [--redact] [--json]
|
|
384
391
|
openxiangda automation publish|enable|disable <automationCode|automationId>
|
|
385
392
|
openxiangda data-view list|get|create|update|upsert|delete|status|refresh|query|stats <dataViewCode> [--profile name] [--json]
|
|
393
|
+
openxiangda webhook list|get|create|update|upsert|disable|deliveries|delivery [webhookCode] [deliveryId] [--json-file file] [--json]
|
|
386
394
|
openxiangda scope dimension-list|dimension-upsert|grant-source-list|grant-source-upsert|policy-list|policy-upsert|sync|explain [code] [--json-file file]
|
|
387
395
|
openxiangda route list|get|create|update|upsert|delete [--json-file file] [--dry-run] [--write-manifest]
|
|
388
396
|
openxiangda public-access list|get|create|update|upsert|delete|ticket-create|session-test|grant-check [--json-file file]
|
|
@@ -519,6 +527,7 @@ async function version(args) {
|
|
|
519
527
|
atomicActivation: APP_FUNCTION_SECRET_CAPABILITIES.atomicActivation,
|
|
520
528
|
},
|
|
521
529
|
updateCheckCommand: 'openxiangda update check --json',
|
|
530
|
+
migrationAdvice: migrationAdvice(),
|
|
522
531
|
};
|
|
523
532
|
if (flags.json) return writeJson(result);
|
|
524
533
|
print(CURRENT_VERSION);
|
|
@@ -529,7 +538,7 @@ async function update(args) {
|
|
|
529
538
|
const parsedArgs = requestedSubcommand === args[0] ? args.slice(1) : args;
|
|
530
539
|
const { flags } = parseArgs(parsedArgs);
|
|
531
540
|
if (wantsSubcommandHelp(requestedSubcommand, flags)) {
|
|
532
|
-
print('用法: openxiangda update check|install [--json] [--registry https://registry.npmjs.org] [--timeout-seconds 300] [--no-skills]');
|
|
541
|
+
print('用法: openxiangda update check|install [--target workspace|launcher] [--json] [--registry https://registry.npmjs.org] [--timeout-seconds 300] [--no-skills]');
|
|
533
542
|
return;
|
|
534
543
|
}
|
|
535
544
|
const registry = normalizeNpmRegistry(flags.registry || OFFICIAL_NPM_REGISTRY);
|
|
@@ -557,7 +566,7 @@ async function update(args) {
|
|
|
557
566
|
return;
|
|
558
567
|
}
|
|
559
568
|
|
|
560
|
-
fail('用法: openxiangda update check|install [--json] [--registry https://registry.npmjs.org] [--timeout-seconds 300] [--no-skills]');
|
|
569
|
+
fail('用法: openxiangda update check|install [--target workspace|launcher] [--json] [--registry https://registry.npmjs.org] [--timeout-seconds 300] [--no-skills]');
|
|
561
570
|
}
|
|
562
571
|
|
|
563
572
|
function normalizeNpmRegistry(value) {
|
|
@@ -586,6 +595,7 @@ function checkOpenXiangdaUpdate(registry, selection = resolveUpdateTarget()) {
|
|
|
586
595
|
skillInstallCommand: 'openxiangda skill install',
|
|
587
596
|
compatibilityCheckCommand: 'openxiangda commands --json',
|
|
588
597
|
checkedAt: new Date().toISOString(),
|
|
598
|
+
migrationAdvice: migrationAdvice(),
|
|
589
599
|
notes: [
|
|
590
600
|
'Use the official npm registry for OpenXiangda updates; domestic mirrors may lag.',
|
|
591
601
|
'After updating the package, refresh OpenXiangda skills so AI guidance matches the CLI version.',
|
|
@@ -766,6 +776,7 @@ async function installOpenXiangdaUpdate(registry, options = {}) {
|
|
|
766
776
|
registry: plan.registry,
|
|
767
777
|
installCommand: `npm ${npmArgs.join(' ')}`,
|
|
768
778
|
installed: true,
|
|
779
|
+
migrationAdvice: migrationAdvice(),
|
|
769
780
|
durationMs: installResult.durationMs,
|
|
770
781
|
cacheMode: 'prefer-offline',
|
|
771
782
|
timeoutSeconds: plan.timeoutSeconds,
|
|
@@ -9444,7 +9455,7 @@ async function platform(args) {
|
|
|
9444
9455
|
print('用法: openxiangda platform add|list|use|remove');
|
|
9445
9456
|
return;
|
|
9446
9457
|
}
|
|
9447
|
-
const config = loadConfig();
|
|
9458
|
+
const config = loadConfig(flags.cwd || process.cwd(), { exact: Boolean(flags.cwd) });
|
|
9448
9459
|
|
|
9449
9460
|
if (subcommand === 'prepare') {
|
|
9450
9461
|
const result = prepareDependencyCapsule({
|
|
@@ -9549,10 +9560,10 @@ async function login(args) {
|
|
|
9549
9560
|
positional[0] === '--help' ||
|
|
9550
9561
|
positional[0] === '-h'
|
|
9551
9562
|
) {
|
|
9552
|
-
print('用法: openxiangda login <platform-url> [--profile name] [--no-open] [--json]');
|
|
9563
|
+
print('用法: openxiangda login <platform-url> [--profile name] [--cwd directory] [--no-open] [--json]');
|
|
9553
9564
|
return;
|
|
9554
9565
|
}
|
|
9555
|
-
const config = loadConfig();
|
|
9566
|
+
const config = loadConfig(flags.cwd || process.cwd(), { exact: Boolean(flags.cwd) });
|
|
9556
9567
|
const rawUrl = positional[0];
|
|
9557
9568
|
const profileName = flags.profile || config.currentProfile || 'default';
|
|
9558
9569
|
let profile = config.profiles[profileName] || { name: profileName };
|
|
@@ -9567,7 +9578,7 @@ async function login(args) {
|
|
|
9567
9578
|
}
|
|
9568
9579
|
|
|
9569
9580
|
if (!profile.baseUrl) {
|
|
9570
|
-
fail('缺少平台地址。用法: openxiangda login <platform-url> [--profile name]');
|
|
9581
|
+
fail('缺少平台地址。用法: openxiangda login <platform-url> [--profile name] [--cwd directory]');
|
|
9571
9582
|
}
|
|
9572
9583
|
|
|
9573
9584
|
const sessionPayload = await requestJson(
|
|
@@ -9666,12 +9677,12 @@ async function auth(args) {
|
|
|
9666
9677
|
const [subcommand, ...rest] = args;
|
|
9667
9678
|
const { flags } = parseArgs(rest);
|
|
9668
9679
|
if (wantsSubcommandHelp(subcommand, flags)) {
|
|
9669
|
-
print('用法: openxiangda auth status|refresh|logout [--profile name] [--json]');
|
|
9680
|
+
print('用法: openxiangda auth status|refresh|logout [--profile name] [--cwd directory] [--json]');
|
|
9670
9681
|
return;
|
|
9671
9682
|
}
|
|
9672
9683
|
|
|
9673
9684
|
if (subcommand === 'status') {
|
|
9674
|
-
const config = loadConfig();
|
|
9685
|
+
const config = loadConfig(flags.cwd || process.cwd(), { exact: Boolean(flags.cwd) });
|
|
9675
9686
|
const { profileName, profile } = getProfile(config, flags.profile);
|
|
9676
9687
|
if (!profile.token?.accessToken) {
|
|
9677
9688
|
print(`未登录: ${profileName} (${profile.baseUrl})`);
|
|
@@ -9689,7 +9700,7 @@ async function auth(args) {
|
|
|
9689
9700
|
}
|
|
9690
9701
|
|
|
9691
9702
|
if (subcommand === 'refresh') {
|
|
9692
|
-
const config = loadConfig();
|
|
9703
|
+
const config = loadConfig(flags.cwd || process.cwd(), { exact: Boolean(flags.cwd) });
|
|
9693
9704
|
const profileName = flags.profile || config.currentProfile;
|
|
9694
9705
|
if (!profileName) fail('未选择平台 profile');
|
|
9695
9706
|
const data = await refreshProfile(config, profileName);
|
|
@@ -9700,7 +9711,7 @@ async function auth(args) {
|
|
|
9700
9711
|
}
|
|
9701
9712
|
|
|
9702
9713
|
if (subcommand === 'logout') {
|
|
9703
|
-
const config = loadConfig();
|
|
9714
|
+
const config = loadConfig(flags.cwd || process.cwd(), { exact: Boolean(flags.cwd) });
|
|
9704
9715
|
const { profileName, profile } = getProfile(config, flags.profile);
|
|
9705
9716
|
if (profile.token?.accessToken) {
|
|
9706
9717
|
try {
|
|
@@ -9718,7 +9729,7 @@ async function auth(args) {
|
|
|
9718
9729
|
return;
|
|
9719
9730
|
}
|
|
9720
9731
|
|
|
9721
|
-
fail('用法: openxiangda auth status|refresh|logout [--profile name]');
|
|
9732
|
+
fail('用法: openxiangda auth status|refresh|logout [--profile name] [--cwd directory]');
|
|
9722
9733
|
}
|
|
9723
9734
|
|
|
9724
9735
|
async function env(args) {
|
|
@@ -9735,7 +9746,8 @@ async function env(args) {
|
|
|
9735
9746
|
const currentState = profileName ? state.profiles?.[profileName] : null;
|
|
9736
9747
|
const oss = summarizeOssEnv(globalEnv);
|
|
9737
9748
|
const data = {
|
|
9738
|
-
configFile:
|
|
9749
|
+
configFile: getConfigFile(config),
|
|
9750
|
+
sessionScope: 'workspace',
|
|
9739
9751
|
globalEnvFile: GLOBAL_ENV_FILE,
|
|
9740
9752
|
globalEnvExists: fs.existsSync(GLOBAL_ENV_FILE),
|
|
9741
9753
|
projectStateFile: path.join(process.cwd(), PROJECT_STATE_FILE),
|
|
@@ -10198,7 +10210,9 @@ async function workspace(args) {
|
|
|
10198
10210
|
printWorkspaceHelp();
|
|
10199
10211
|
return;
|
|
10200
10212
|
}
|
|
10201
|
-
const config =
|
|
10213
|
+
const config = subcommand === 'init'
|
|
10214
|
+
? loadConfig(path.resolve(positional[0] || process.cwd()), { exact: true })
|
|
10215
|
+
: loadConfig();
|
|
10202
10216
|
|
|
10203
10217
|
if (subcommand === 'ownership' || subcommand === 'owner') {
|
|
10204
10218
|
const action = positional[0] || 'status';
|
|
@@ -10345,6 +10359,7 @@ async function workspace(args) {
|
|
|
10345
10359
|
connectors: {},
|
|
10346
10360
|
dataViews: {},
|
|
10347
10361
|
storageConfigs: {},
|
|
10362
|
+
webhooks: {},
|
|
10348
10363
|
scopeDimensions: {},
|
|
10349
10364
|
scopeGrantSources: {},
|
|
10350
10365
|
dataScopePolicies: {},
|
|
@@ -12531,6 +12546,93 @@ async function dataView(args) {
|
|
|
12531
12546
|
fail('用法: openxiangda data-view list|get|create|update|upsert|delete|status|refresh|query|stats');
|
|
12532
12547
|
}
|
|
12533
12548
|
|
|
12549
|
+
async function webhook(args) {
|
|
12550
|
+
const { subcommand, rest } = parseSubcommandArgs(args);
|
|
12551
|
+
const { flags, positional } = parseArgs(rest);
|
|
12552
|
+
if (wantsSubcommandHelp(subcommand, flags)) {
|
|
12553
|
+
print('用法: openxiangda webhook list|get|create|update|upsert|disable|deliveries|delivery [webhookCode] [deliveryId] [--json-file file] [--json]');
|
|
12554
|
+
return;
|
|
12555
|
+
}
|
|
12556
|
+
const config = loadConfig();
|
|
12557
|
+
const target = getWorkspaceTarget(
|
|
12558
|
+
config,
|
|
12559
|
+
flags.profile || config.currentProfile,
|
|
12560
|
+
flags
|
|
12561
|
+
);
|
|
12562
|
+
const basePath = `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/webhooks`;
|
|
12563
|
+
|
|
12564
|
+
if (['list', 'get', 'create', 'update', 'upsert'].includes(subcommand)) {
|
|
12565
|
+
return directResourceCrud(config, target, {
|
|
12566
|
+
commandName: 'webhook',
|
|
12567
|
+
subcommand,
|
|
12568
|
+
flags,
|
|
12569
|
+
positional,
|
|
12570
|
+
resourceType: 'webhook',
|
|
12571
|
+
basePath,
|
|
12572
|
+
manifestDir: path.join('src', 'resources', 'webhooks'),
|
|
12573
|
+
normalizeBody: normalizeWebhookManifest,
|
|
12574
|
+
codeOf: body => body.code || body.resourceCode,
|
|
12575
|
+
saveState: (code, data) => saveWebhookResource(target, code, data),
|
|
12576
|
+
});
|
|
12577
|
+
}
|
|
12578
|
+
|
|
12579
|
+
if (subcommand === 'disable') {
|
|
12580
|
+
const code = positional[0] || flags.code;
|
|
12581
|
+
if (!code) fail('用法: openxiangda webhook disable <webhookCode>');
|
|
12582
|
+
const body = readDirectJsonBody(flags, 'webhook disable', {
|
|
12583
|
+
optional: true,
|
|
12584
|
+
});
|
|
12585
|
+
if (flags['expected-revision'] !== undefined) {
|
|
12586
|
+
body.expectedRevision = Number(flags['expected-revision']);
|
|
12587
|
+
}
|
|
12588
|
+
const data = await runDirectRequest(
|
|
12589
|
+
config,
|
|
12590
|
+
target,
|
|
12591
|
+
flags,
|
|
12592
|
+
{
|
|
12593
|
+
method: 'POST',
|
|
12594
|
+
path: apiPathWithPostDeleteAction(
|
|
12595
|
+
`${basePath}/${encodeURIComponent(code)}`
|
|
12596
|
+
),
|
|
12597
|
+
body,
|
|
12598
|
+
},
|
|
12599
|
+
{ returnData: true }
|
|
12600
|
+
);
|
|
12601
|
+
if (!flags['dry-run']) saveWebhookResource(target, code, data);
|
|
12602
|
+
return outputDirectResult({ action: 'disable', data }, flags);
|
|
12603
|
+
}
|
|
12604
|
+
|
|
12605
|
+
if (subcommand === 'deliveries') {
|
|
12606
|
+
const code = positional[0] || flags.code;
|
|
12607
|
+
if (!code) fail('用法: openxiangda webhook deliveries <webhookCode>');
|
|
12608
|
+
return runDirectRequest(config, target, flags, {
|
|
12609
|
+
method: 'GET',
|
|
12610
|
+
path: apiPathWithQuery(
|
|
12611
|
+
`${basePath}/${encodeURIComponent(code)}/deliveries`,
|
|
12612
|
+
{
|
|
12613
|
+
page: flags.page,
|
|
12614
|
+
pageSize: flags['page-size'] || flags.limit,
|
|
12615
|
+
status: flags.status,
|
|
12616
|
+
}
|
|
12617
|
+
),
|
|
12618
|
+
});
|
|
12619
|
+
}
|
|
12620
|
+
|
|
12621
|
+
if (subcommand === 'delivery') {
|
|
12622
|
+
const code = positional[0] || flags.code;
|
|
12623
|
+
const deliveryId = positional[1] || flags['delivery-id'];
|
|
12624
|
+
if (!code || !deliveryId) {
|
|
12625
|
+
fail('用法: openxiangda webhook delivery <webhookCode> <deliveryId>');
|
|
12626
|
+
}
|
|
12627
|
+
return runDirectRequest(config, target, flags, {
|
|
12628
|
+
method: 'GET',
|
|
12629
|
+
path: `${basePath}/${encodeURIComponent(code)}/deliveries/${encodeURIComponent(deliveryId)}`,
|
|
12630
|
+
});
|
|
12631
|
+
}
|
|
12632
|
+
|
|
12633
|
+
fail('用法: openxiangda webhook list|get|create|update|upsert|disable|deliveries|delivery');
|
|
12634
|
+
}
|
|
12635
|
+
|
|
12534
12636
|
async function scope(args) {
|
|
12535
12637
|
const { subcommand, rest } = parseSubcommandArgs(args);
|
|
12536
12638
|
const { flags, positional } = parseArgs(rest);
|
|
@@ -17064,7 +17166,7 @@ async function commands(args) {
|
|
|
17064
17166
|
commands: [
|
|
17065
17167
|
'version [--json]',
|
|
17066
17168
|
'login <platform-url> [--profile name]',
|
|
17067
|
-
'update check|install',
|
|
17169
|
+
'update check|install [--target workspace|launcher] [--json]',
|
|
17068
17170
|
'platform add|list|use|remove',
|
|
17069
17171
|
'auth status|refresh|logout',
|
|
17070
17172
|
'doctor [--profile name] [--app-type APP_XXX] [--release]',
|
|
@@ -17085,6 +17187,7 @@ async function commands(args) {
|
|
|
17085
17187
|
'workflow compile|list|create|bind|pull|publish|delete|validate',
|
|
17086
17188
|
'automation list|create|bind|pull|publish|unpublish|enable|disable|delete|validate|cron-validate',
|
|
17087
17189
|
'data-view list|get|create|update|upsert|delete|status|refresh|query|stats',
|
|
17190
|
+
'webhook list|get|create|update|upsert|disable|deliveries|delivery',
|
|
17088
17191
|
'route list|get|create|update|upsert|delete',
|
|
17089
17192
|
'public-access list|get|create|update|upsert|delete|ticket-create|session-test|grant-check',
|
|
17090
17193
|
'auth-config list|get|create|update|upsert|delete|methods',
|
|
@@ -17107,6 +17210,7 @@ async function commands(args) {
|
|
|
17107
17210
|
resourceNotes: [
|
|
17108
17211
|
'For formal multi-resource development, prefer src/resources/** + openxiangda resource validate|plan|publish.',
|
|
17109
17212
|
'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.',
|
|
17213
|
+
'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.',
|
|
17110
17214
|
'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.',
|
|
17111
17215
|
'settings public-access is legacy form public-access compatibility/repair and should not be used for new React SPA apps.',
|
|
17112
17216
|
'Direct live mutation commands should use --dry-run first and --write-manifest when the repository should remain source of truth.',
|
|
@@ -17160,6 +17264,7 @@ async function skill(args) {
|
|
|
17160
17264
|
|
|
17161
17265
|
if (subcommand === 'install') {
|
|
17162
17266
|
const result = installSkills(options);
|
|
17267
|
+
result.migrationAdvice = migrationAdvice();
|
|
17163
17268
|
if (flags.json) return writeJson(result);
|
|
17164
17269
|
printSkillInstallReport(result);
|
|
17165
17270
|
return;
|
|
@@ -17178,6 +17283,7 @@ async function skill(args) {
|
|
|
17178
17283
|
force: Boolean(flags.force),
|
|
17179
17284
|
dryRun: Boolean(flags['dry-run']),
|
|
17180
17285
|
});
|
|
17286
|
+
result.migrationAdvice = migrationAdvice();
|
|
17181
17287
|
if (flags.json) return writeJson(result);
|
|
17182
17288
|
printSkillBootstrapReport(result);
|
|
17183
17289
|
return;
|
|
@@ -17499,6 +17605,7 @@ function directManifestDir(resourceType) {
|
|
|
17499
17605
|
connector: path.join('src', 'resources', 'connectors'),
|
|
17500
17606
|
notification: path.join('src', 'resources', 'notifications'),
|
|
17501
17607
|
'data-view': path.join('src', 'resources', 'data-views'),
|
|
17608
|
+
webhook: path.join('src', 'resources', 'webhooks'),
|
|
17502
17609
|
menu: path.join('src', 'resources', 'menus'),
|
|
17503
17610
|
role: path.join('src', 'resources', 'roles'),
|
|
17504
17611
|
'page-permission-group': path.join('src', 'resources', 'permissions', 'page-groups'),
|
|
@@ -17518,6 +17625,7 @@ function removeDirectStateResource(target, resourceType, code) {
|
|
|
17518
17625
|
function: 'functions',
|
|
17519
17626
|
connector: 'connectors',
|
|
17520
17627
|
'data-view': 'dataViews',
|
|
17628
|
+
webhook: 'webhooks',
|
|
17521
17629
|
};
|
|
17522
17630
|
const bucket = buckets[resourceType];
|
|
17523
17631
|
if (!bucket || !target.bound.resources?.[bucket]?.[code]) return;
|
|
@@ -17840,6 +17948,7 @@ function ensureResourceBuckets(bound) {
|
|
|
17840
17948
|
bound.resources.connectors = bound.resources.connectors || {};
|
|
17841
17949
|
bound.resources.dataViews = bound.resources.dataViews || {};
|
|
17842
17950
|
bound.resources.storageConfigs = bound.resources.storageConfigs || {};
|
|
17951
|
+
bound.resources.webhooks = bound.resources.webhooks || {};
|
|
17843
17952
|
bound.resources.authConfigs = bound.resources.authConfigs || {};
|
|
17844
17953
|
bound.resources.routes = bound.resources.routes || {};
|
|
17845
17954
|
bound.resources.publicAccessPolicies = bound.resources.publicAccessPolicies || {};
|
|
@@ -18185,6 +18294,29 @@ function saveStorageConfigResource(target, storageCode, storageConfigId, extra =
|
|
|
18185
18294
|
}, keys);
|
|
18186
18295
|
}
|
|
18187
18296
|
|
|
18297
|
+
function saveWebhookResource(target, webhookCode, data = {}) {
|
|
18298
|
+
const keys = [
|
|
18299
|
+
'webhookId',
|
|
18300
|
+
'endpointId',
|
|
18301
|
+
'callbackPath',
|
|
18302
|
+
'callbackUrl',
|
|
18303
|
+
'targetFunctionCode',
|
|
18304
|
+
'idempotencyQueryParam',
|
|
18305
|
+
'maxBodyBytes',
|
|
18306
|
+
'status',
|
|
18307
|
+
'revision',
|
|
18308
|
+
];
|
|
18309
|
+
const callbackPath = String(data?.callbackPath || '').trim();
|
|
18310
|
+
const callbackUrl = callbackPath
|
|
18311
|
+
? `${String(target.profile?.baseUrl || '').replace(/\/+$/, '')}${callbackPath}`
|
|
18312
|
+
: data?.callbackUrl;
|
|
18313
|
+
saveStateResource(target, 'webhooks', webhookCode, {
|
|
18314
|
+
...pickStateFields(data, keys),
|
|
18315
|
+
webhookId: data?.id || data?.webhookId,
|
|
18316
|
+
callbackUrl,
|
|
18317
|
+
}, keys);
|
|
18318
|
+
}
|
|
18319
|
+
|
|
18188
18320
|
function saveScopeResource(target, bucket, code, data = {}) {
|
|
18189
18321
|
if (!bucket || !code) return;
|
|
18190
18322
|
const keys = ['id', 'resourceCode', 'name', 'updatedAt'];
|
|
@@ -18333,6 +18465,7 @@ const RESOURCE_SPECS = [
|
|
|
18333
18465
|
{ key: 'functions', dir: 'functions', topFiles: ['functions.json'], pluralKeys: ['functions'] },
|
|
18334
18466
|
{ key: 'dataViews', dir: 'data-views', topFiles: ['data-views.json'], pluralKeys: ['dataViews', 'data-views'] },
|
|
18335
18467
|
{ key: 'storageConfigs', dir: 'storage', topFiles: ['storage.json'], pluralKeys: ['storageConfigs', 'storage'] },
|
|
18468
|
+
{ key: 'webhooks', dir: 'webhooks', topFiles: ['webhooks.json'], pluralKeys: ['webhooks'] },
|
|
18336
18469
|
{ key: 'authConfigs', dir: 'auth', topFiles: ['auth.json'], pluralKeys: ['authConfigs', 'auth'] },
|
|
18337
18470
|
{ key: 'routes', dir: 'routes', topFiles: ['routes.json'], pluralKeys: ['routes'] },
|
|
18338
18471
|
{
|
|
@@ -18412,6 +18545,8 @@ const RESOURCE_TYPE_ALIASES = new Map([
|
|
|
18412
18545
|
['oss', 'storageConfigs'],
|
|
18413
18546
|
['oss-config', 'storageConfigs'],
|
|
18414
18547
|
['oss-configs', 'storageConfigs'],
|
|
18548
|
+
['webhook', 'webhooks'],
|
|
18549
|
+
['webhooks', 'webhooks'],
|
|
18415
18550
|
['auth', 'authConfigs'],
|
|
18416
18551
|
['auth-config', 'authConfigs'],
|
|
18417
18552
|
['auth-configs', 'authConfigs'],
|
|
@@ -19124,6 +19259,7 @@ function generateResourceTypes(manifest, outputFile) {
|
|
|
19124
19259
|
const dataViewCodes = unique((manifest.dataViews || []).map(item => item.code).filter(Boolean));
|
|
19125
19260
|
const functionCodes = unique((manifest.functions || []).map(item => item.code).filter(Boolean));
|
|
19126
19261
|
const storageConfigCodes = unique((manifest.storageConfigs || []).map(item => item.code).filter(Boolean));
|
|
19262
|
+
const webhookCodes = unique((manifest.webhooks || []).map(item => item.code).filter(Boolean));
|
|
19127
19263
|
const authConfigCodes = unique((manifest.authConfigs || []).map(item => item.code).filter(Boolean));
|
|
19128
19264
|
const publicAccessPolicyCodes = unique((manifest.publicAccessPolicies || []).map(item => item.code).filter(Boolean));
|
|
19129
19265
|
const scopeDimensionCodes = unique((manifest.scopeDimensions || []).map(item => item.code).filter(Boolean));
|
|
@@ -19148,6 +19284,9 @@ function generateResourceTypes(manifest, outputFile) {
|
|
|
19148
19284
|
`export const storageConfigCodes = ${JSON.stringify(storageConfigCodes, null, 2)} as const`,
|
|
19149
19285
|
'export type StorageConfigCode = typeof storageConfigCodes[number]',
|
|
19150
19286
|
'',
|
|
19287
|
+
`export const webhookCodes = ${JSON.stringify(webhookCodes, null, 2)} as const`,
|
|
19288
|
+
'export type WebhookCode = typeof webhookCodes[number]',
|
|
19289
|
+
'',
|
|
19151
19290
|
`export const functionCodes = ${JSON.stringify(functionCodes, null, 2)} as const`,
|
|
19152
19291
|
'export type FunctionCode = typeof functionCodes[number]',
|
|
19153
19292
|
'',
|
|
@@ -19182,6 +19321,7 @@ function generateResourceTypes(manifest, outputFile) {
|
|
|
19182
19321
|
pagePermissionGroups: pagePermissionGroupCodes.length,
|
|
19183
19322
|
dataViews: dataViewCodes.length,
|
|
19184
19323
|
storageConfigs: storageConfigCodes.length,
|
|
19324
|
+
webhooks: webhookCodes.length,
|
|
19185
19325
|
functions: functionCodes.length,
|
|
19186
19326
|
authConfigs: authConfigCodes.length,
|
|
19187
19327
|
publicAccessPolicies: publicAccessPolicyCodes.length,
|
|
@@ -19259,6 +19399,50 @@ function validateResourceItem(kind, item, errors, warnings) {
|
|
|
19259
19399
|
validateFunctionRuntimeInvoke(label, item, errors, warnings);
|
|
19260
19400
|
validateFunctionPerformancePolicies(label, item, errors);
|
|
19261
19401
|
}
|
|
19402
|
+
if (kind === 'webhooks') {
|
|
19403
|
+
const allowed = new Set([
|
|
19404
|
+
'code',
|
|
19405
|
+
'resourceCode',
|
|
19406
|
+
'name',
|
|
19407
|
+
'description',
|
|
19408
|
+
'targetFunctionCode',
|
|
19409
|
+
'idempotencyQueryParam',
|
|
19410
|
+
'maxBodyBytes',
|
|
19411
|
+
'status',
|
|
19412
|
+
]);
|
|
19413
|
+
const unsupported = Object.keys(item).filter(
|
|
19414
|
+
key => !key.startsWith('__') && !allowed.has(key)
|
|
19415
|
+
);
|
|
19416
|
+
if (unsupported.length > 0) {
|
|
19417
|
+
errors.push(`${label}: 包含不支持的字段 ${unsupported.join(', ')}`);
|
|
19418
|
+
}
|
|
19419
|
+
if (!item.name) errors.push(`${label}: 缺少 name`);
|
|
19420
|
+
if (!/^[A-Za-z][A-Za-z0-9_]{1,127}$/.test(String(item.targetFunctionCode || ''))) {
|
|
19421
|
+
errors.push(`${label}: targetFunctionCode 格式不正确`);
|
|
19422
|
+
}
|
|
19423
|
+
if (
|
|
19424
|
+
item.idempotencyQueryParam !== undefined &&
|
|
19425
|
+
!/^[A-Za-z][A-Za-z0-9_.-]{0,127}$/.test(
|
|
19426
|
+
String(item.idempotencyQueryParam)
|
|
19427
|
+
)
|
|
19428
|
+
) {
|
|
19429
|
+
errors.push(`${label}: idempotencyQueryParam 格式不正确`);
|
|
19430
|
+
}
|
|
19431
|
+
const maxBodyBytes = Number(item.maxBodyBytes || 262144);
|
|
19432
|
+
if (
|
|
19433
|
+
!Number.isInteger(maxBodyBytes) ||
|
|
19434
|
+
maxBodyBytes < 1024 ||
|
|
19435
|
+
maxBodyBytes > 1048576
|
|
19436
|
+
) {
|
|
19437
|
+
errors.push(`${label}: maxBodyBytes 必须在 1024 到 1048576 之间`);
|
|
19438
|
+
}
|
|
19439
|
+
if (!['active', 'disabled'].includes(String(item.status || 'active'))) {
|
|
19440
|
+
errors.push(`${label}: status 只能是 active 或 disabled`);
|
|
19441
|
+
}
|
|
19442
|
+
if (Object.keys(item).some(key => /secret|signature|hmac/i.test(key))) {
|
|
19443
|
+
errors.push(`${label}: Webhook manifest 不能包含 Secret 或供应商验签配置`);
|
|
19444
|
+
}
|
|
19445
|
+
}
|
|
19262
19446
|
if (kind === 'dataViews') {
|
|
19263
19447
|
const definition = item.definition || item;
|
|
19264
19448
|
const viewType = String(definition.viewType || 'row').toLowerCase();
|
|
@@ -20370,6 +20554,7 @@ async function buildResourcePlan(
|
|
|
20370
20554
|
await addAutomationPlanActions(config, target, actions, manifest.automations, existing.automations);
|
|
20371
20555
|
addPlanActions(actions, 'dataView', manifest.dataViews, existing.dataViews, (item, current) => dataViewEquals(target.bound, item, current));
|
|
20372
20556
|
addPlanActions(actions, 'storageConfig', manifest.storageConfigs, existing.storageConfigs, (item, current) => storageConfigEquals(item, current));
|
|
20557
|
+
addPlanActions(actions, 'webhook', manifest.webhooks, existing.webhooks, webhookEquals);
|
|
20373
20558
|
addPlanActions(actions, 'scopeDimension', manifest.scopeDimensions, existing.scopeDimensions, (item, current) => scopeDimensionEquals(target.bound, item, current));
|
|
20374
20559
|
addPlanActions(actions, 'scopeGrantSource', manifest.scopeGrantSources, existing.scopeGrantSources, (item, current) => scopeGrantSourceEquals(target.bound, item, current));
|
|
20375
20560
|
addPlanActions(actions, 'dataScopePolicy', manifest.dataScopePolicies, existing.dataScopePolicies, dataScopePolicyEquals);
|
|
@@ -22230,6 +22415,7 @@ async function publishResourceManifest(config, target, manifest, options = {}) {
|
|
|
22230
22415
|
await publishDataScopePolicyResources(config, target, manifest.dataScopePolicies || [], result, publishOptions);
|
|
22231
22416
|
await publishDataViewResources(config, target, manifest.dataViews || [], result, publishOptions);
|
|
22232
22417
|
await publishStorageConfigResources(config, target, manifest.storageConfigs || [], result, publishOptions);
|
|
22418
|
+
await publishWebhookResources(config, target, manifest.webhooks || [], result, publishOptions);
|
|
22233
22419
|
await publishPublicAccessPolicyResources(config, target, manifest.publicAccessPolicies || [], result, publishOptions);
|
|
22234
22420
|
await publishPagePermissionGroupResources(config, target, manifest.pagePermissionGroups || [], result, publishOptions);
|
|
22235
22421
|
await publishFormPermissionGroupResources(
|
|
@@ -22376,6 +22562,7 @@ const GENERIC_RESOURCE_KEY_BY_PLAN_KIND = Object.freeze({
|
|
|
22376
22562
|
publicAccessPolicy: 'publicAccessPolicies',
|
|
22377
22563
|
dataView: 'dataViews',
|
|
22378
22564
|
storageConfig: 'storageConfigs',
|
|
22565
|
+
webhook: 'webhooks',
|
|
22379
22566
|
scopeDimension: 'scopeDimensions',
|
|
22380
22567
|
scopeGrantSource: 'scopeGrantSources',
|
|
22381
22568
|
dataScopePolicy: 'dataScopePolicies',
|
|
@@ -22492,7 +22679,7 @@ function runGenericResourceGitBasePreflight({
|
|
|
22492
22679
|
copyProjectState: true,
|
|
22493
22680
|
},
|
|
22494
22681
|
({ buildRoot, buildEnv, tempRoot }) => {
|
|
22495
|
-
const isolatedHome = prepareReadonlyGitBasePlanHome(tempRoot);
|
|
22682
|
+
const isolatedHome = prepareReadonlyGitBasePlanHome(tempRoot, buildRoot);
|
|
22496
22683
|
const childEnv = {
|
|
22497
22684
|
...buildEnv,
|
|
22498
22685
|
HOME: isolatedHome,
|
|
@@ -22628,8 +22815,9 @@ function runGenericResourceGitBasePreflight({
|
|
|
22628
22815
|
};
|
|
22629
22816
|
}
|
|
22630
22817
|
|
|
22631
|
-
function prepareReadonlyGitBasePlanHome(tempRoot) {
|
|
22632
|
-
|
|
22818
|
+
function prepareReadonlyGitBasePlanHome(tempRoot, buildRoot) {
|
|
22819
|
+
const configSource = getConfigFile();
|
|
22820
|
+
if (!fs.existsSync(configSource)) {
|
|
22633
22821
|
const error = new Error(
|
|
22634
22822
|
'SOURCE_BASE_PLAN_FAILED: 当前 profile 配置不存在,无法执行隔离 Git 基线 plan'
|
|
22635
22823
|
);
|
|
@@ -22637,10 +22825,11 @@ function prepareReadonlyGitBasePlanHome(tempRoot) {
|
|
|
22637
22825
|
throw error;
|
|
22638
22826
|
}
|
|
22639
22827
|
const home = path.join(tempRoot, 'plan-home');
|
|
22640
|
-
const configDir = path.join(
|
|
22828
|
+
const configDir = path.join(buildRoot, '.openxiangda');
|
|
22829
|
+
fs.mkdirSync(home, { recursive: true, mode: 0o700 });
|
|
22641
22830
|
const configFile = path.join(configDir, 'profiles.json');
|
|
22642
22831
|
fs.mkdirSync(configDir, { recursive: true, mode: 0o700 });
|
|
22643
|
-
fs.copyFileSync(
|
|
22832
|
+
fs.copyFileSync(configSource, configFile);
|
|
22644
22833
|
try {
|
|
22645
22834
|
fs.chmodSync(configFile, 0o400);
|
|
22646
22835
|
} catch {
|
|
@@ -22882,6 +23071,7 @@ async function fetchExistingResourceMaps(config, target, manifest) {
|
|
|
22882
23071
|
automations: new Map(),
|
|
22883
23072
|
dataViews: new Map(),
|
|
22884
23073
|
storageConfigs: new Map(),
|
|
23074
|
+
webhooks: new Map(),
|
|
22885
23075
|
scopeDimensions: new Map(),
|
|
22886
23076
|
scopeGrantSources: new Map(),
|
|
22887
23077
|
dataScopePolicies: new Map(),
|
|
@@ -23108,6 +23298,21 @@ async function fetchExistingResourceMaps(config, target, manifest) {
|
|
|
23108
23298
|
});
|
|
23109
23299
|
}
|
|
23110
23300
|
}
|
|
23301
|
+
if ((manifest.webhooks || []).length > 0) {
|
|
23302
|
+
const data = await requestWithAuth(
|
|
23303
|
+
config,
|
|
23304
|
+
target.profileName,
|
|
23305
|
+
apiPathWithQuery(
|
|
23306
|
+
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/webhooks`,
|
|
23307
|
+
{ page: 1, pageSize: 1000 }
|
|
23308
|
+
)
|
|
23309
|
+
);
|
|
23310
|
+
indexByCode(
|
|
23311
|
+
maps.webhooks,
|
|
23312
|
+
normalizeItems(data),
|
|
23313
|
+
item => item.code || item.resourceCode
|
|
23314
|
+
);
|
|
23315
|
+
}
|
|
23111
23316
|
if ((manifest.scopeDimensions || []).length > 0) {
|
|
23112
23317
|
const data = await requestWithAuth(
|
|
23113
23318
|
config,
|
|
@@ -25099,6 +25304,45 @@ async function publishStorageConfigResources(config, target, storageConfigs, res
|
|
|
25099
25304
|
}
|
|
25100
25305
|
}
|
|
25101
25306
|
|
|
25307
|
+
async function publishWebhookResources(config, target, webhooks, result, options = {}) {
|
|
25308
|
+
for (const webhookItem of webhooks) {
|
|
25309
|
+
const code = webhookItem.code || webhookItem.resourceCode;
|
|
25310
|
+
if (shouldSkipNoopResource(options, 'webhook', code)) {
|
|
25311
|
+
recordNoopResource(result, 'webhook', code, options);
|
|
25312
|
+
continue;
|
|
25313
|
+
}
|
|
25314
|
+
const existing = await findExistingWebhook(config, target, code);
|
|
25315
|
+
const existingRevision = Number(existing?.revision);
|
|
25316
|
+
const body = {
|
|
25317
|
+
...normalizeWebhookManifest(webhookItem),
|
|
25318
|
+
expectedRevision:
|
|
25319
|
+
existing && Number.isInteger(existingRevision) && existingRevision > 0
|
|
25320
|
+
? existingRevision
|
|
25321
|
+
: existing
|
|
25322
|
+
? undefined
|
|
25323
|
+
: 0,
|
|
25324
|
+
};
|
|
25325
|
+
const data = await requestWithAuth(
|
|
25326
|
+
config,
|
|
25327
|
+
target.profileName,
|
|
25328
|
+
existing
|
|
25329
|
+
? `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/webhooks/${encodeURIComponent(code)}`
|
|
25330
|
+
: `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/webhooks`,
|
|
25331
|
+
{ method: 'POST', body }
|
|
25332
|
+
);
|
|
25333
|
+
saveWebhookResource(target, code, data);
|
|
25334
|
+
result.published.push({
|
|
25335
|
+
kind: 'webhook',
|
|
25336
|
+
code,
|
|
25337
|
+
action: existing ? 'update' : 'create',
|
|
25338
|
+
id: data?.id,
|
|
25339
|
+
endpointId: data?.endpointId,
|
|
25340
|
+
callbackPath: data?.callbackPath,
|
|
25341
|
+
status: data?.status,
|
|
25342
|
+
});
|
|
25343
|
+
}
|
|
25344
|
+
}
|
|
25345
|
+
|
|
25102
25346
|
async function publishDataViewPermissionGroups(config, target, dataViewCode, permissionGroups) {
|
|
25103
25347
|
await requestWithAuth(
|
|
25104
25348
|
config,
|
|
@@ -25263,6 +25507,25 @@ async function findExistingStorageConfig(config, target, code) {
|
|
|
25263
25507
|
return null;
|
|
25264
25508
|
}
|
|
25265
25509
|
|
|
25510
|
+
async function findExistingWebhook(config, target, code) {
|
|
25511
|
+
const state = target.bound.resources?.webhooks?.[code];
|
|
25512
|
+
const byCode = await requestOptionalWithAuth(
|
|
25513
|
+
config,
|
|
25514
|
+
target.profileName,
|
|
25515
|
+
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/webhooks/${encodeURIComponent(code)}`
|
|
25516
|
+
);
|
|
25517
|
+
if (byCode?.id) return byCode;
|
|
25518
|
+
if (state?.webhookId) {
|
|
25519
|
+
return {
|
|
25520
|
+
id: state.webhookId,
|
|
25521
|
+
code,
|
|
25522
|
+
revision: state.revision,
|
|
25523
|
+
endpointId: state.endpointId,
|
|
25524
|
+
};
|
|
25525
|
+
}
|
|
25526
|
+
return null;
|
|
25527
|
+
}
|
|
25528
|
+
|
|
25266
25529
|
async function findExistingAutomation(config, target, code) {
|
|
25267
25530
|
const stateId = target.bound.resources?.automations?.[code]?.automationId;
|
|
25268
25531
|
if (stateId) {
|
|
@@ -25541,6 +25804,22 @@ async function collectPruneResourceCandidates(config, target, manifest) {
|
|
|
25541
25804
|
item => item.code || item.resourceCode
|
|
25542
25805
|
);
|
|
25543
25806
|
}
|
|
25807
|
+
if (enabled('webhooks')) {
|
|
25808
|
+
const data = await requestWithAuth(
|
|
25809
|
+
config,
|
|
25810
|
+
target.profileName,
|
|
25811
|
+
apiPathWithQuery(`${appPath}/webhooks`, {
|
|
25812
|
+
page: 1,
|
|
25813
|
+
pageSize: 1000,
|
|
25814
|
+
})
|
|
25815
|
+
);
|
|
25816
|
+
append(
|
|
25817
|
+
'webhook',
|
|
25818
|
+
desiredCodes(manifest.webhooks),
|
|
25819
|
+
normalizeItems(data),
|
|
25820
|
+
item => item.code || item.resourceCode
|
|
25821
|
+
);
|
|
25822
|
+
}
|
|
25544
25823
|
if (enabled('publicAccessPolicies')) {
|
|
25545
25824
|
const data = await requestWithAuth(
|
|
25546
25825
|
config,
|
|
@@ -25731,6 +26010,21 @@ async function pruneFrozenResourceCandidate(config, target, result, candidate) {
|
|
|
25731
26010
|
if (kind === 'storageConfig') {
|
|
25732
26011
|
return postDelete(`${appPath}/storage-configs/${encodeURIComponent(code)}`);
|
|
25733
26012
|
}
|
|
26013
|
+
if (kind === 'webhook') {
|
|
26014
|
+
return requestWithAuth(
|
|
26015
|
+
config,
|
|
26016
|
+
target.profileName,
|
|
26017
|
+
apiPathWithPostDeleteAction(
|
|
26018
|
+
`${appPath}/webhooks/${encodeURIComponent(code)}`
|
|
26019
|
+
),
|
|
26020
|
+
{
|
|
26021
|
+
method: 'POST',
|
|
26022
|
+
body: candidate.frozenRevision
|
|
26023
|
+
? { expectedRevision: candidate.frozenRevision }
|
|
26024
|
+
: {},
|
|
26025
|
+
}
|
|
26026
|
+
);
|
|
26027
|
+
}
|
|
25734
26028
|
if (kind === 'publicAccessPolicy') {
|
|
25735
26029
|
return postDelete(`${appPath}/public-access/policies/${encodeURIComponent(code)}`);
|
|
25736
26030
|
}
|
|
@@ -26131,6 +26425,7 @@ function removeStateResource(target, kind, code) {
|
|
|
26131
26425
|
publicAccessPolicy: 'publicAccessPolicies',
|
|
26132
26426
|
dataView: 'dataViews',
|
|
26133
26427
|
storageConfig: 'storageConfigs',
|
|
26428
|
+
webhook: 'webhooks',
|
|
26134
26429
|
pagePermissionGroup: 'pagePermissionGroups',
|
|
26135
26430
|
formPermissionGroup: 'formPermissionGroups',
|
|
26136
26431
|
};
|
|
@@ -26433,6 +26728,22 @@ async function pullResources(config, target) {
|
|
|
26433
26728
|
written.push(path.relative(process.cwd(), filePath));
|
|
26434
26729
|
}
|
|
26435
26730
|
|
|
26731
|
+
const webhooks = await requestWithAuth(
|
|
26732
|
+
config,
|
|
26733
|
+
target.profileName,
|
|
26734
|
+
apiPathWithQuery(
|
|
26735
|
+
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/webhooks`,
|
|
26736
|
+
{ page: 1, pageSize: 1000 }
|
|
26737
|
+
)
|
|
26738
|
+
);
|
|
26739
|
+
for (const webhookItem of normalizeItems(webhooks)) {
|
|
26740
|
+
const code = webhookItem.code || webhookItem.resourceCode || webhookItem.id;
|
|
26741
|
+
const filePath = path.join(baseDir, 'webhooks', `${code}.json`);
|
|
26742
|
+
writeResourceJsonFile(filePath, toPulledWebhook(webhookItem));
|
|
26743
|
+
written.push(path.relative(process.cwd(), filePath));
|
|
26744
|
+
saveWebhookResource(target, code, webhookItem);
|
|
26745
|
+
}
|
|
26746
|
+
|
|
26436
26747
|
const pageGroups = await requestWithAuth(
|
|
26437
26748
|
config,
|
|
26438
26749
|
target.profileName,
|
|
@@ -26706,6 +27017,18 @@ function toPulledStorageConfig(storageConfig) {
|
|
|
26706
27017
|
});
|
|
26707
27018
|
}
|
|
26708
27019
|
|
|
27020
|
+
function toPulledWebhook(webhookItem) {
|
|
27021
|
+
return stripUndefinedValues({
|
|
27022
|
+
code: webhookItem.code || webhookItem.resourceCode,
|
|
27023
|
+
name: webhookItem.name,
|
|
27024
|
+
description: webhookItem.description || '',
|
|
27025
|
+
targetFunctionCode: webhookItem.targetFunctionCode,
|
|
27026
|
+
idempotencyQueryParam: webhookItem.idempotencyQueryParam || 'nonce',
|
|
27027
|
+
maxBodyBytes: webhookItem.maxBodyBytes || 262144,
|
|
27028
|
+
status: webhookItem.status || 'active',
|
|
27029
|
+
});
|
|
27030
|
+
}
|
|
27031
|
+
|
|
26709
27032
|
function rewriteDataViewDefinitionForManifest(definition, lookups) {
|
|
26710
27033
|
rewriteDataViewSourceForManifest(definition.base, lookups);
|
|
26711
27034
|
for (const join of definition.joins || []) {
|
|
@@ -27158,6 +27481,61 @@ function normalizeStorageConfigManifest(storageConfig) {
|
|
|
27158
27481
|
});
|
|
27159
27482
|
}
|
|
27160
27483
|
|
|
27484
|
+
function normalizeWebhookManifest(webhook) {
|
|
27485
|
+
const code = String(webhook.code || webhook.resourceCode || '').trim();
|
|
27486
|
+
const allowed = new Set([
|
|
27487
|
+
'code',
|
|
27488
|
+
'resourceCode',
|
|
27489
|
+
'name',
|
|
27490
|
+
'description',
|
|
27491
|
+
'targetFunctionCode',
|
|
27492
|
+
'idempotencyQueryParam',
|
|
27493
|
+
'maxBodyBytes',
|
|
27494
|
+
'status',
|
|
27495
|
+
]);
|
|
27496
|
+
const unsupported = Object.keys(webhook).filter(
|
|
27497
|
+
key => !key.startsWith('__') && !allowed.has(key)
|
|
27498
|
+
);
|
|
27499
|
+
if (unsupported.length > 0) {
|
|
27500
|
+
fail(`webhook:${code || '<unknown>'} 包含不支持的字段 ${unsupported.join(', ')}`);
|
|
27501
|
+
}
|
|
27502
|
+
if (!/^[A-Za-z][A-Za-z0-9_]{1,127}$/.test(code)) {
|
|
27503
|
+
fail('Webhook code 格式不正确');
|
|
27504
|
+
}
|
|
27505
|
+
const targetFunctionCode = String(webhook.targetFunctionCode || '').trim();
|
|
27506
|
+
if (!/^[A-Za-z][A-Za-z0-9_]{1,127}$/.test(targetFunctionCode)) {
|
|
27507
|
+
fail(`webhook:${code} targetFunctionCode 格式不正确`);
|
|
27508
|
+
}
|
|
27509
|
+
const idempotencyQueryParam = String(
|
|
27510
|
+
webhook.idempotencyQueryParam || 'nonce'
|
|
27511
|
+
).trim();
|
|
27512
|
+
if (!/^[A-Za-z][A-Za-z0-9_.-]{0,127}$/.test(idempotencyQueryParam)) {
|
|
27513
|
+
fail(`webhook:${code} idempotencyQueryParam 格式不正确`);
|
|
27514
|
+
}
|
|
27515
|
+
const maxBodyBytes = Number(webhook.maxBodyBytes || 262144);
|
|
27516
|
+
if (
|
|
27517
|
+
!Number.isInteger(maxBodyBytes) ||
|
|
27518
|
+
maxBodyBytes < 1024 ||
|
|
27519
|
+
maxBodyBytes > 1048576
|
|
27520
|
+
) {
|
|
27521
|
+
fail(`webhook:${code} maxBodyBytes 必须在 1024 到 1048576 之间`);
|
|
27522
|
+
}
|
|
27523
|
+
const status = String(webhook.status || 'active');
|
|
27524
|
+
if (!['active', 'disabled'].includes(status)) {
|
|
27525
|
+
fail(`webhook:${code} status 只能是 active 或 disabled`);
|
|
27526
|
+
}
|
|
27527
|
+
return stripUndefinedValues({
|
|
27528
|
+
code,
|
|
27529
|
+
name: String(webhook.name || code).trim(),
|
|
27530
|
+
description:
|
|
27531
|
+
webhook.description === undefined ? '' : String(webhook.description),
|
|
27532
|
+
targetFunctionCode,
|
|
27533
|
+
idempotencyQueryParam,
|
|
27534
|
+
maxBodyBytes,
|
|
27535
|
+
status,
|
|
27536
|
+
});
|
|
27537
|
+
}
|
|
27538
|
+
|
|
27161
27539
|
function normalizeStorageCorsForPublish(cors) {
|
|
27162
27540
|
if (cors === undefined || cors === null || cors === '') return undefined;
|
|
27163
27541
|
const managed =
|
|
@@ -28305,6 +28683,21 @@ function storageConfigEquals(desired, existing) {
|
|
|
28305
28683
|
);
|
|
28306
28684
|
}
|
|
28307
28685
|
|
|
28686
|
+
function webhookEquals(desired, existing) {
|
|
28687
|
+
if (!existing) return false;
|
|
28688
|
+
const expected = normalizeWebhookManifest(desired);
|
|
28689
|
+
return (
|
|
28690
|
+
String(existing.code || existing.resourceCode || '') === expected.code &&
|
|
28691
|
+
String(existing.name || '') === expected.name &&
|
|
28692
|
+
String(existing.description || '') === expected.description &&
|
|
28693
|
+
String(existing.targetFunctionCode || '') === expected.targetFunctionCode &&
|
|
28694
|
+
String(existing.idempotencyQueryParam || 'nonce') ===
|
|
28695
|
+
expected.idempotencyQueryParam &&
|
|
28696
|
+
Number(existing.maxBodyBytes || 262144) === expected.maxBodyBytes &&
|
|
28697
|
+
String(existing.status || 'active') === expected.status
|
|
28698
|
+
);
|
|
28699
|
+
}
|
|
28700
|
+
|
|
28308
28701
|
function routeEquals(desired, existing) {
|
|
28309
28702
|
if (!existing) return false;
|
|
28310
28703
|
const expected = normalizeRouteManifest(desired);
|