openxiangda 1.0.92 → 1.0.93
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 +42 -0
- package/lib/cli.js +1129 -23
- package/lib/design-gates.js +449 -0
- package/openxiangda-skills/SKILL.md +23 -20
- package/openxiangda-skills/references/architecture-design.md +25 -0
- package/openxiangda-skills/references/resource-manifest-cheatsheet.md +36 -0
- package/openxiangda-skills/skills/openxiangda-architecture-design/SKILL.md +29 -0
- package/package.json +3 -1
- package/templates/openxiangda-react-spa/AGENTS.md +6 -0
- package/templates/sy-lowcode-app-workspace/AGENTS.md +7 -0
package/lib/cli.js
CHANGED
|
@@ -22,6 +22,13 @@ const { requestJson } = require('./http');
|
|
|
22
22
|
const { getSkillStatusReport, installSkills } = require('./skills');
|
|
23
23
|
const { assertCanInitializeWorkspace, initWorkspace } = require('./workspace-init');
|
|
24
24
|
const { bootstrapWorkspace } = require('./workspace-bootstrap');
|
|
25
|
+
const {
|
|
26
|
+
getDesignGates,
|
|
27
|
+
getResourceExplain,
|
|
28
|
+
renderDesignGatesText,
|
|
29
|
+
renderDesignTemplate,
|
|
30
|
+
renderResourceExplain,
|
|
31
|
+
} = require('./design-gates');
|
|
25
32
|
const {
|
|
26
33
|
fail,
|
|
27
34
|
formatFetchError,
|
|
@@ -52,6 +59,8 @@ async function main(argv) {
|
|
|
52
59
|
if (command === 'env') return env(rest);
|
|
53
60
|
if (command === 'auth') return auth(rest);
|
|
54
61
|
if (command === 'platform') return platform(rest);
|
|
62
|
+
if (command === 'doctor') return doctor(rest);
|
|
63
|
+
if (command === 'design') return design(rest);
|
|
55
64
|
if (command === 'workspace') return workspace(rest);
|
|
56
65
|
if (command === 'app') return app(rest);
|
|
57
66
|
if (command === 'form') return form(rest);
|
|
@@ -60,6 +69,12 @@ async function main(argv) {
|
|
|
60
69
|
if (command === 'workflow') return workflow(rest);
|
|
61
70
|
if (command === 'automation') return automation(rest);
|
|
62
71
|
if (command === 'data-view') return dataView(rest);
|
|
72
|
+
if (command === 'route') return route(rest);
|
|
73
|
+
if (command === 'public-access') return publicAccess(rest);
|
|
74
|
+
if (command === 'auth-config') return authConfig(rest);
|
|
75
|
+
if (command === 'function') return appFunction(rest);
|
|
76
|
+
if (command === 'connector') return connector(rest);
|
|
77
|
+
if (command === 'notification') return notification(rest);
|
|
63
78
|
if (command === 'permission') return permission(rest);
|
|
64
79
|
if (command === 'settings') return settings(rest);
|
|
65
80
|
if (command === 'resource') return resource(rest);
|
|
@@ -82,6 +97,8 @@ Usage:
|
|
|
82
97
|
openxiangda platform list
|
|
83
98
|
openxiangda platform use <name>
|
|
84
99
|
openxiangda auth status|refresh|logout [--profile name]
|
|
100
|
+
openxiangda doctor [--profile name] [--app-type APP_XXX] [--json]
|
|
101
|
+
openxiangda design gates|template [--topic code] [--json]
|
|
85
102
|
openxiangda env [--profile name]
|
|
86
103
|
openxiangda workspace init [dir] [--name package-name] [--runtime legacy|react-spa] [--install] [--profile name --app-type APP_XXX]
|
|
87
104
|
openxiangda workspace init [dir] --profile <name> --app-name <app-name> [--runtime legacy|react-spa] [--install]
|
|
@@ -100,6 +117,7 @@ Usage:
|
|
|
100
117
|
openxiangda page bind <pageCode> --page-id <id>
|
|
101
118
|
openxiangda menu list [--profile name] [--json]
|
|
102
119
|
openxiangda menu create <menuCode> --name <text> --type <nav|receipt|display>
|
|
120
|
+
openxiangda menu update|sort [--json-file file] [--write-manifest]
|
|
103
121
|
openxiangda menu bind <menuCode> --menu-id <id>
|
|
104
122
|
openxiangda workflow list [--profile name] [--form-code code] [--json]
|
|
105
123
|
openxiangda workflow create <workflowCode> --form-code <formCode> --definition-json <file>
|
|
@@ -112,12 +130,18 @@ Usage:
|
|
|
112
130
|
openxiangda automation logs <instanceId> [--automation <automationCode|automationId>] [--summary] [--redact] [--json]
|
|
113
131
|
openxiangda automation diagnose <automationCode|automationId> [--redact] [--json]
|
|
114
132
|
openxiangda automation publish|enable|disable <automationCode|automationId>
|
|
115
|
-
openxiangda data-view list|status|refresh|query|stats <dataViewCode> [--profile name] [--json]
|
|
116
|
-
openxiangda
|
|
117
|
-
openxiangda
|
|
118
|
-
openxiangda
|
|
133
|
+
openxiangda data-view list|get|create|update|upsert|delete|status|refresh|query|stats <dataViewCode> [--profile name] [--json]
|
|
134
|
+
openxiangda route list|get|create|update|upsert|delete [--json-file file] [--dry-run] [--write-manifest]
|
|
135
|
+
openxiangda public-access list|get|create|update|upsert|delete|ticket-create|session-test|grant-check [--json-file file]
|
|
136
|
+
openxiangda auth-config list|get|create|update|upsert|delete|methods [--json-file file]
|
|
137
|
+
openxiangda function list|get|create|update|upsert|delete|invoke [--json-file file]
|
|
138
|
+
openxiangda connector list|get|create|update|upsert|delete|invoke|download-test [--json-file file]
|
|
139
|
+
openxiangda notification template-list|template-get|template-upsert|template-delete|type-list|type-get|type-upsert|type-delete|preview|send|batch-send
|
|
140
|
+
openxiangda permission role-list|role-create|role-update|role-delete|role-bind|audit
|
|
141
|
+
openxiangda permission page-group-list|page-group-create|page-group-update|page-group-delete|page-group-bind
|
|
142
|
+
openxiangda permission form-group-list|form-group-create|form-group-update|form-group-delete|form-group-bind
|
|
119
143
|
openxiangda settings get|save|indexes|indexes-save|data-management|data-management-save|public-access
|
|
120
|
-
openxiangda resource validate|plan|publish|pull [--profile name] [--json]
|
|
144
|
+
openxiangda resource validate|plan|publish|pull|typegen|explain [--profile name] [--json]
|
|
121
145
|
openxiangda runtime deploy [--profile name] [--dist dist] [--build-id id] [--upload-mode staged|legacy-json] [--upload-timeout-ms ms] [--no-build] [--no-activate] [--json]
|
|
122
146
|
openxiangda runtime releases [--profile name] [--json]
|
|
123
147
|
openxiangda runtime activate <releaseId> [--profile name] [--json]
|
|
@@ -152,6 +176,7 @@ const SUBCOMMAND_BOOLEAN_FLAGS = new Set([
|
|
|
152
176
|
'--strict',
|
|
153
177
|
'--summary',
|
|
154
178
|
'--unpublish',
|
|
179
|
+
'--write-manifest',
|
|
155
180
|
'--yes',
|
|
156
181
|
'-h',
|
|
157
182
|
]);
|
|
@@ -401,6 +426,154 @@ function parseSemver(value) {
|
|
|
401
426
|
};
|
|
402
427
|
}
|
|
403
428
|
|
|
429
|
+
async function design(args) {
|
|
430
|
+
const { subcommand, rest } = parseSubcommandArgs(args);
|
|
431
|
+
const { flags, positional } = parseArgs(rest);
|
|
432
|
+
const topic = flags.topic || positional[0];
|
|
433
|
+
if (wantsSubcommandHelp(subcommand, flags)) {
|
|
434
|
+
print('用法: openxiangda design gates|template [--topic new-app,public-access] [--json]');
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (subcommand === 'gates') {
|
|
439
|
+
const result = getDesignGates(topic);
|
|
440
|
+
if (flags.json) return writeJson(result);
|
|
441
|
+
print(renderDesignGatesText(topic));
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (subcommand === 'template') {
|
|
446
|
+
const result = {
|
|
447
|
+
topic: topic || 'all',
|
|
448
|
+
content: renderDesignTemplate(topic),
|
|
449
|
+
};
|
|
450
|
+
if (flags.json) return writeJson(result);
|
|
451
|
+
print(result.content);
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
fail('用法: openxiangda design gates|template [--topic code] [--json]');
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
async function doctor(args) {
|
|
459
|
+
const { flags } = parseArgs(args);
|
|
460
|
+
const config = loadConfig();
|
|
461
|
+
const profileName = flags.profile || config.currentProfile;
|
|
462
|
+
const result = {
|
|
463
|
+
cli: {
|
|
464
|
+
version: CURRENT_VERSION,
|
|
465
|
+
commandDiscovery: 'openxiangda commands --json',
|
|
466
|
+
designGateCommand: 'openxiangda design gates --json',
|
|
467
|
+
},
|
|
468
|
+
profile: {
|
|
469
|
+
requested: profileName || null,
|
|
470
|
+
exists: false,
|
|
471
|
+
baseUrl: null,
|
|
472
|
+
loggedIn: false,
|
|
473
|
+
user: null,
|
|
474
|
+
},
|
|
475
|
+
workspace: {
|
|
476
|
+
cwd: process.cwd(),
|
|
477
|
+
stateFile: path.join(process.cwd(), PROJECT_STATE_FILE),
|
|
478
|
+
hasState: fs.existsSync(path.join(process.cwd(), PROJECT_STATE_FILE)),
|
|
479
|
+
appType: flags['app-type'] || null,
|
|
480
|
+
bound: false,
|
|
481
|
+
},
|
|
482
|
+
resources: {
|
|
483
|
+
hasDirectory: fs.existsSync(path.join(process.cwd(), 'src', 'resources')),
|
|
484
|
+
validation: null,
|
|
485
|
+
},
|
|
486
|
+
skills: null,
|
|
487
|
+
checks: [],
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
if (profileName && config.profiles?.[profileName]) {
|
|
491
|
+
const { profile } = getProfile(config, profileName);
|
|
492
|
+
result.profile.exists = true;
|
|
493
|
+
result.profile.baseUrl = profile.baseUrl || null;
|
|
494
|
+
result.profile.loggedIn = Boolean(profile.token?.accessToken);
|
|
495
|
+
result.profile.user = profile.user || null;
|
|
496
|
+
try {
|
|
497
|
+
const target = getWorkspaceTarget(config, profileName, flags);
|
|
498
|
+
result.workspace.appType = target.appType;
|
|
499
|
+
result.workspace.bound = true;
|
|
500
|
+
result.workspace.profile = target.profileName;
|
|
501
|
+
result.workspace.boundAt = target.bound.updatedAt || null;
|
|
502
|
+
result.checks.push({ name: 'workspace-binding', status: 'ok' });
|
|
503
|
+
} catch (error) {
|
|
504
|
+
result.checks.push({ name: 'workspace-binding', status: 'warn', message: error.message });
|
|
505
|
+
}
|
|
506
|
+
if (result.profile.loggedIn) {
|
|
507
|
+
try {
|
|
508
|
+
const authStatus = await requestWithAuth(config, profileName, '/openxiangda-api/v1/auth/whoami');
|
|
509
|
+
result.profile.authStatus = authStatus;
|
|
510
|
+
result.checks.push({ name: 'auth', status: 'ok' });
|
|
511
|
+
} catch (error) {
|
|
512
|
+
result.profile.authError = error.message;
|
|
513
|
+
result.checks.push({ name: 'auth', status: 'warn', message: error.message });
|
|
514
|
+
}
|
|
515
|
+
} else {
|
|
516
|
+
result.checks.push({ name: 'auth', status: 'warn', message: 'profile 未登录' });
|
|
517
|
+
}
|
|
518
|
+
} else {
|
|
519
|
+
result.checks.push({ name: 'profile', status: 'warn', message: '未选择或不存在 profile' });
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (result.resources.hasDirectory) {
|
|
523
|
+
const manifest = loadWorkspaceResources();
|
|
524
|
+
const validation = validateWorkspaceResources(manifest);
|
|
525
|
+
result.resources.validation = {
|
|
526
|
+
errors: validation.errors,
|
|
527
|
+
warnings: validation.warnings,
|
|
528
|
+
counts: Object.fromEntries(
|
|
529
|
+
RESOURCE_SPECS.map(spec => [spec.key, (manifest[spec.key] || []).length])
|
|
530
|
+
),
|
|
531
|
+
};
|
|
532
|
+
result.checks.push({
|
|
533
|
+
name: 'resource-validate',
|
|
534
|
+
status: validation.errors.length > 0 ? 'error' : 'ok',
|
|
535
|
+
errors: validation.errors.length,
|
|
536
|
+
warnings: validation.warnings.length,
|
|
537
|
+
});
|
|
538
|
+
} else {
|
|
539
|
+
result.checks.push({ name: 'resource-validate', status: 'skip', message: '未发现 src/resources' });
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
result.skills = getSkillStatusReport({ agent: flags.agent || 'codex' });
|
|
543
|
+
const outdatedSkills = result.skills.results
|
|
544
|
+
.flatMap(item => item.skills)
|
|
545
|
+
.filter(item => item.status !== 'installed');
|
|
546
|
+
result.checks.push({
|
|
547
|
+
name: 'skills',
|
|
548
|
+
status: outdatedSkills.length > 0 ? 'warn' : 'ok',
|
|
549
|
+
message: outdatedSkills.length > 0 ? `${outdatedSkills.length} 个 skill 未安装或过期` : undefined,
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
if (flags.json) return writeJson(result);
|
|
553
|
+
printDoctorReport(result);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function printDoctorReport(result) {
|
|
557
|
+
const lines = [
|
|
558
|
+
`OpenXiangda doctor v${result.cli.version}`,
|
|
559
|
+
`profile: ${result.profile.requested || '(none)'} ${result.profile.loggedIn ? '(logged in)' : '(not logged in)'}`,
|
|
560
|
+
`baseUrl: ${result.profile.baseUrl || '(none)'}`,
|
|
561
|
+
`workspace: ${result.workspace.bound ? `${result.workspace.profile}/${result.workspace.appType}` : 'not bound'}`,
|
|
562
|
+
`resources: ${result.resources.hasDirectory ? 'src/resources found' : 'missing'}`,
|
|
563
|
+
];
|
|
564
|
+
if (result.resources.validation) {
|
|
565
|
+
lines.push(
|
|
566
|
+
`resource validation: ${result.resources.validation.errors.length} errors, ${result.resources.validation.warnings.length} warnings`
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
for (const check of result.checks) {
|
|
570
|
+
const suffix = check.message ? ` - ${check.message}` : '';
|
|
571
|
+
lines.push(`- ${check.name}: ${check.status}${suffix}`);
|
|
572
|
+
}
|
|
573
|
+
lines.push(`design gate: ${result.cli.designGateCommand}`);
|
|
574
|
+
print(lines.join('\n'));
|
|
575
|
+
}
|
|
576
|
+
|
|
404
577
|
async function platform(args) {
|
|
405
578
|
const [subcommand, ...rest] = args;
|
|
406
579
|
const { flags, positional } = parseArgs(rest);
|
|
@@ -1626,7 +1799,46 @@ async function menu(args) {
|
|
|
1626
1799
|
return;
|
|
1627
1800
|
}
|
|
1628
1801
|
|
|
1629
|
-
|
|
1802
|
+
if (subcommand === 'update') {
|
|
1803
|
+
const [menuKey] = positional;
|
|
1804
|
+
const body = readDirectJsonBody(flags, 'menu update');
|
|
1805
|
+
const menuCode = menuKey || body.code || body.resourceCode;
|
|
1806
|
+
if (!menuCode) fail('用法: openxiangda menu update <menuCode|menuId> --json-file file');
|
|
1807
|
+
const target = getWorkspaceTarget(config, profileName, flags);
|
|
1808
|
+
const menuId = resolveMenuId(target.bound, menuCode, flags);
|
|
1809
|
+
const desired = normalizeMenuDirectBody(target.bound, { ...body, code: body.code || menuCode });
|
|
1810
|
+
const data = await runDirectRequest(config, target, flags, {
|
|
1811
|
+
method: 'PUT',
|
|
1812
|
+
path: `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/menus/${encodeURIComponent(menuId)}`,
|
|
1813
|
+
body: desired,
|
|
1814
|
+
}, { returnData: true });
|
|
1815
|
+
if (!flags['dry-run']) {
|
|
1816
|
+
const resultCode = body.code || body.resourceCode || menuCode;
|
|
1817
|
+
if (data?.id) {
|
|
1818
|
+
saveMenuResource(target, resultCode, data.id, {
|
|
1819
|
+
formUuid: data.formUuid || desired.formUuid,
|
|
1820
|
+
pageId: data.pageId || desired.pageId,
|
|
1821
|
+
parentId: data.parentId || desired.parentId,
|
|
1822
|
+
routeCode: data.routeCode || desired.routeCode,
|
|
1823
|
+
path: data.path || desired.path,
|
|
1824
|
+
});
|
|
1825
|
+
}
|
|
1826
|
+
if (flags['write-manifest']) writeDirectManifest('menu', resultCode, { ...body, code: resultCode });
|
|
1827
|
+
}
|
|
1828
|
+
return outputDirectResult(data, flags);
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
if (subcommand === 'sort') {
|
|
1832
|
+
const target = getWorkspaceTarget(config, profileName, flags);
|
|
1833
|
+
const body = readDirectJsonBody(flags, 'menu sort');
|
|
1834
|
+
return runDirectRequest(config, target, flags, {
|
|
1835
|
+
method: 'POST',
|
|
1836
|
+
path: `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/menus/sort`,
|
|
1837
|
+
body,
|
|
1838
|
+
});
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
fail('用法: openxiangda menu list|create|update|sort|bind|delete');
|
|
1630
1842
|
}
|
|
1631
1843
|
|
|
1632
1844
|
async function workflow(args) {
|
|
@@ -2093,7 +2305,7 @@ async function dataView(args) {
|
|
|
2093
2305
|
const { subcommand, rest } = parseSubcommandArgs(args);
|
|
2094
2306
|
const { flags, positional } = parseArgs(rest);
|
|
2095
2307
|
if (wantsSubcommandHelp(subcommand, flags)) {
|
|
2096
|
-
print('用法: openxiangda data-view list|status|refresh|query|stats <dataViewCode> [--profile name] [--json]');
|
|
2308
|
+
print('用法: openxiangda data-view list|get|create|update|upsert|delete|status|refresh|query|stats <dataViewCode> [--profile name] [--json]');
|
|
2097
2309
|
return;
|
|
2098
2310
|
}
|
|
2099
2311
|
const config = loadConfig();
|
|
@@ -2176,7 +2388,407 @@ async function dataView(args) {
|
|
|
2176
2388
|
return;
|
|
2177
2389
|
}
|
|
2178
2390
|
|
|
2179
|
-
|
|
2391
|
+
if (['get', 'create', 'update', 'upsert', 'delete'].includes(subcommand)) {
|
|
2392
|
+
return directResourceCrud(config, target, {
|
|
2393
|
+
commandName: 'data-view',
|
|
2394
|
+
subcommand,
|
|
2395
|
+
flags,
|
|
2396
|
+
positional,
|
|
2397
|
+
resourceType: 'data-view',
|
|
2398
|
+
basePath: `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/data-views`,
|
|
2399
|
+
manifestDir: path.join('src', 'resources', 'data-views'),
|
|
2400
|
+
normalizeBody: body => normalizeDataViewManifest(target.bound, body),
|
|
2401
|
+
codeOf: body => body.code || body.resourceCode || body.definition?.code,
|
|
2402
|
+
saveState: (code, data) => saveDataViewResource(target, code, data?.id, {
|
|
2403
|
+
materializedViewName: data?.materializedViewName,
|
|
2404
|
+
status: data?.status,
|
|
2405
|
+
storageMode: data?.storageMode,
|
|
2406
|
+
}),
|
|
2407
|
+
});
|
|
2408
|
+
}
|
|
2409
|
+
|
|
2410
|
+
fail('用法: openxiangda data-view list|get|create|update|upsert|delete|status|refresh|query|stats');
|
|
2411
|
+
}
|
|
2412
|
+
|
|
2413
|
+
async function route(args) {
|
|
2414
|
+
const { subcommand, rest } = parseSubcommandArgs(args);
|
|
2415
|
+
const { flags, positional } = parseArgs(rest);
|
|
2416
|
+
if (wantsSubcommandHelp(subcommand, flags)) {
|
|
2417
|
+
print('用法: openxiangda route list|get|create|update|upsert|delete [routeCode] [--json-file file] [--dry-run] [--write-manifest]');
|
|
2418
|
+
return;
|
|
2419
|
+
}
|
|
2420
|
+
const config = loadConfig();
|
|
2421
|
+
const target = getWorkspaceTarget(config, flags.profile || config.currentProfile, flags);
|
|
2422
|
+
return directResourceCrud(config, target, {
|
|
2423
|
+
commandName: 'route',
|
|
2424
|
+
subcommand,
|
|
2425
|
+
flags,
|
|
2426
|
+
positional,
|
|
2427
|
+
resourceType: 'route',
|
|
2428
|
+
basePath: `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/routes`,
|
|
2429
|
+
manifestDir: path.join('src', 'resources', 'routes'),
|
|
2430
|
+
normalizeBody: body => normalizeRouteManifest(body),
|
|
2431
|
+
codeOf: body => body.code || body.resourceCode,
|
|
2432
|
+
saveState: (code, data) => saveRouteResource(target, code, data?.id, {
|
|
2433
|
+
pathPattern: data?.pathPattern,
|
|
2434
|
+
publicAccess: data?.publicAccess,
|
|
2435
|
+
publicPolicyCode: data?.publicPolicyCode,
|
|
2436
|
+
}),
|
|
2437
|
+
});
|
|
2438
|
+
}
|
|
2439
|
+
|
|
2440
|
+
async function publicAccess(args) {
|
|
2441
|
+
const { subcommand, rest } = parseSubcommandArgs(args);
|
|
2442
|
+
const { flags, positional } = parseArgs(rest);
|
|
2443
|
+
if (wantsSubcommandHelp(subcommand, flags)) {
|
|
2444
|
+
print('用法: openxiangda public-access list|get|create|update|upsert|delete|ticket-create|session-test|grant-check [policyCode] [--json-file file]');
|
|
2445
|
+
return;
|
|
2446
|
+
}
|
|
2447
|
+
const config = loadConfig();
|
|
2448
|
+
const target = getWorkspaceTarget(config, flags.profile || config.currentProfile, flags);
|
|
2449
|
+
|
|
2450
|
+
if (['list', 'get', 'create', 'update', 'upsert', 'delete'].includes(subcommand)) {
|
|
2451
|
+
return directResourceCrud(config, target, {
|
|
2452
|
+
commandName: 'public-access',
|
|
2453
|
+
subcommand,
|
|
2454
|
+
flags,
|
|
2455
|
+
positional,
|
|
2456
|
+
resourceType: 'public-access',
|
|
2457
|
+
basePath: `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/public-access/policies`,
|
|
2458
|
+
manifestDir: path.join('src', 'resources', 'public-access'),
|
|
2459
|
+
normalizeBody: body => normalizePublicAccessPolicyManifest(target.bound, body),
|
|
2460
|
+
codeOf: body => body.code || body.resourceCode,
|
|
2461
|
+
saveState: (code, data) => savePublicAccessPolicyResource(target, code, data?.id, {
|
|
2462
|
+
mode: data?.mode,
|
|
2463
|
+
routeCode: data?.routeCode,
|
|
2464
|
+
pathPattern: data?.pathPattern,
|
|
2465
|
+
}),
|
|
2466
|
+
});
|
|
2467
|
+
}
|
|
2468
|
+
|
|
2469
|
+
if (subcommand === 'ticket-create') {
|
|
2470
|
+
const [policyCode] = positional;
|
|
2471
|
+
if (!policyCode) fail('用法: openxiangda public-access ticket-create <policyCode> [--json-file file]');
|
|
2472
|
+
const body = readDirectJsonBody(flags, 'ticket');
|
|
2473
|
+
return runDirectRequest(config, target, flags, {
|
|
2474
|
+
method: 'POST',
|
|
2475
|
+
path: `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/public-access/policies/${encodeURIComponent(policyCode)}/tickets`,
|
|
2476
|
+
body,
|
|
2477
|
+
strictEnvelope: true,
|
|
2478
|
+
});
|
|
2479
|
+
}
|
|
2480
|
+
|
|
2481
|
+
if (subcommand === 'session-test') {
|
|
2482
|
+
const [policyCode] = positional;
|
|
2483
|
+
const body = {
|
|
2484
|
+
...readDirectJsonBody(flags, 'public-session', { optional: true }),
|
|
2485
|
+
...(policyCode ? { policyCode } : {}),
|
|
2486
|
+
...(flags.path ? { path: flags.path } : {}),
|
|
2487
|
+
...(flags.ticket ? { ticket: flags.ticket } : {}),
|
|
2488
|
+
};
|
|
2489
|
+
return runDirectRequest(config, target, flags, {
|
|
2490
|
+
method: 'POST',
|
|
2491
|
+
path: `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/public/session`,
|
|
2492
|
+
body,
|
|
2493
|
+
auth: false,
|
|
2494
|
+
strictEnvelope: true,
|
|
2495
|
+
});
|
|
2496
|
+
}
|
|
2497
|
+
|
|
2498
|
+
if (subcommand === 'grant-check') {
|
|
2499
|
+
const [policyCode] = positional;
|
|
2500
|
+
if (!policyCode) fail('用法: openxiangda public-access grant-check <policyCode> [--form-code code] [--data-view code] [--function code] [--connector code]');
|
|
2501
|
+
const policy = flags['json-file']
|
|
2502
|
+
? readDirectJsonBody(flags, 'policy')
|
|
2503
|
+
: await requestWithAuth(
|
|
2504
|
+
config,
|
|
2505
|
+
target.profileName,
|
|
2506
|
+
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/public-access/policies/${encodeURIComponent(policyCode)}`
|
|
2507
|
+
);
|
|
2508
|
+
const normalized = normalizePublicAccessPolicyManifest(target.bound, policy);
|
|
2509
|
+
const grants = normalized.grants || {};
|
|
2510
|
+
const checks = buildPublicGrantChecks(target.bound, grants, flags);
|
|
2511
|
+
const result = { policyCode, grants, checks, allowed: checks.every(item => item.allowed) };
|
|
2512
|
+
if (flags.json) return writeJson(result);
|
|
2513
|
+
print(JSON.stringify(result, null, 2));
|
|
2514
|
+
if (!result.allowed) fail('公开访问 grant-check 失败');
|
|
2515
|
+
return;
|
|
2516
|
+
}
|
|
2517
|
+
|
|
2518
|
+
fail('用法: openxiangda public-access list|get|create|update|upsert|delete|ticket-create|session-test|grant-check');
|
|
2519
|
+
}
|
|
2520
|
+
|
|
2521
|
+
async function authConfig(args) {
|
|
2522
|
+
const { subcommand, rest } = parseSubcommandArgs(args);
|
|
2523
|
+
const { flags, positional } = parseArgs(rest);
|
|
2524
|
+
if (wantsSubcommandHelp(subcommand, flags)) {
|
|
2525
|
+
print('用法: openxiangda auth-config list|get|create|update|upsert|delete|methods [configCode] [--json-file file]');
|
|
2526
|
+
return;
|
|
2527
|
+
}
|
|
2528
|
+
if (subcommand === 'methods') {
|
|
2529
|
+
const result = {
|
|
2530
|
+
methods: [
|
|
2531
|
+
{ type: 'password', requiredFields: ['username', 'password'] },
|
|
2532
|
+
{ type: 'phone_code', requiredFields: ['phone', 'code'], provider: 'functionCode 或 connector' },
|
|
2533
|
+
{ type: 'sso', requiredFields: ['provider', 'callbackUrl'] },
|
|
2534
|
+
],
|
|
2535
|
+
defaultRegistration: { mode: 'reject' },
|
|
2536
|
+
defaultBinding: { mode: 'auto' },
|
|
2537
|
+
};
|
|
2538
|
+
if (flags.json) return writeJson(result);
|
|
2539
|
+
print(JSON.stringify(result, null, 2));
|
|
2540
|
+
return;
|
|
2541
|
+
}
|
|
2542
|
+
const config = loadConfig();
|
|
2543
|
+
const target = getWorkspaceTarget(config, flags.profile || config.currentProfile, flags);
|
|
2544
|
+
return directResourceCrud(config, target, {
|
|
2545
|
+
commandName: 'auth-config',
|
|
2546
|
+
subcommand,
|
|
2547
|
+
flags,
|
|
2548
|
+
positional,
|
|
2549
|
+
resourceType: 'auth-config',
|
|
2550
|
+
basePath: `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/auth/configs`,
|
|
2551
|
+
manifestDir: path.join('src', 'resources', 'auth'),
|
|
2552
|
+
normalizeBody: body => normalizeAuthConfigManifest(body),
|
|
2553
|
+
codeOf: body => body.code || body.resourceCode || 'default',
|
|
2554
|
+
saveState: (code, data) => saveAuthConfigResource(target, code, data?.id, {
|
|
2555
|
+
status: data?.status,
|
|
2556
|
+
}),
|
|
2557
|
+
});
|
|
2558
|
+
}
|
|
2559
|
+
|
|
2560
|
+
async function appFunction(args) {
|
|
2561
|
+
const { subcommand, rest } = parseSubcommandArgs(args);
|
|
2562
|
+
const { flags, positional } = parseArgs(rest);
|
|
2563
|
+
if (wantsSubcommandHelp(subcommand, flags)) {
|
|
2564
|
+
print('用法: openxiangda function list|get|create|update|upsert|delete|invoke [functionCode] [--json-file file]');
|
|
2565
|
+
return;
|
|
2566
|
+
}
|
|
2567
|
+
const config = loadConfig();
|
|
2568
|
+
const target = getWorkspaceTarget(config, flags.profile || config.currentProfile, flags);
|
|
2569
|
+
|
|
2570
|
+
if (subcommand === 'invoke') {
|
|
2571
|
+
const [functionCode] = positional;
|
|
2572
|
+
if (!functionCode) fail('用法: openxiangda function invoke <functionCode> [--body-json file|json]');
|
|
2573
|
+
const body = readDirectJsonBody(flags, 'function invoke', { optional: true });
|
|
2574
|
+
return runDirectRequest(config, target, flags, {
|
|
2575
|
+
method: 'POST',
|
|
2576
|
+
path: `/${encodeURIComponent(target.appType)}/v1/functions/${encodeURIComponent(functionCode)}/invoke.json`,
|
|
2577
|
+
body,
|
|
2578
|
+
strictEnvelope: true,
|
|
2579
|
+
});
|
|
2580
|
+
}
|
|
2581
|
+
|
|
2582
|
+
return directResourceCrud(config, target, {
|
|
2583
|
+
commandName: 'function',
|
|
2584
|
+
subcommand,
|
|
2585
|
+
flags,
|
|
2586
|
+
positional,
|
|
2587
|
+
resourceType: 'function',
|
|
2588
|
+
basePath: `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/functions`,
|
|
2589
|
+
manifestDir: path.join('src', 'resources', 'functions'),
|
|
2590
|
+
prepareBody: async body => {
|
|
2591
|
+
const definitionJson = await resolveManifestJson(
|
|
2592
|
+
config,
|
|
2593
|
+
target.profileName,
|
|
2594
|
+
body,
|
|
2595
|
+
'definitionJson',
|
|
2596
|
+
'definitionFile'
|
|
2597
|
+
);
|
|
2598
|
+
applyResourceBindingsToRuntimeDefinition(target.bound, body, definitionJson);
|
|
2599
|
+
const code = body.code || body.functionCode || body.resourceCode;
|
|
2600
|
+
return stripUndefinedValues({
|
|
2601
|
+
code,
|
|
2602
|
+
name: body.name || definitionJson.name || code,
|
|
2603
|
+
description: body.description !== undefined ? body.description : definitionJson.description || '',
|
|
2604
|
+
definitionJson,
|
|
2605
|
+
resourceBindings: definitionJson.resourceBindings,
|
|
2606
|
+
inputSchema: body.inputSchema || definitionJson.inputSchema,
|
|
2607
|
+
outputSchema: body.outputSchema || definitionJson.outputSchema,
|
|
2608
|
+
status: body.status,
|
|
2609
|
+
});
|
|
2610
|
+
},
|
|
2611
|
+
codeOf: body => body.code || body.functionCode || body.resourceCode,
|
|
2612
|
+
saveState: (code, data) => saveFunctionResource(target, code, data?.id, {
|
|
2613
|
+
status: data?.status,
|
|
2614
|
+
}),
|
|
2615
|
+
});
|
|
2616
|
+
}
|
|
2617
|
+
|
|
2618
|
+
async function connector(args) {
|
|
2619
|
+
const { subcommand, rest } = parseSubcommandArgs(args);
|
|
2620
|
+
const { flags, positional } = parseArgs(rest);
|
|
2621
|
+
if (wantsSubcommandHelp(subcommand, flags)) {
|
|
2622
|
+
print('用法: openxiangda connector list|get|create|update|upsert|delete|invoke|download-test [connectorCode[.apiCode]] [--json-file file]');
|
|
2623
|
+
return;
|
|
2624
|
+
}
|
|
2625
|
+
const config = loadConfig();
|
|
2626
|
+
const target = getWorkspaceTarget(config, flags.profile || config.currentProfile, flags);
|
|
2627
|
+
|
|
2628
|
+
if (subcommand === 'invoke' || subcommand === 'download-test') {
|
|
2629
|
+
const targetName = positional[0] || flags.connector;
|
|
2630
|
+
if (!targetName) fail(`用法: openxiangda connector ${subcommand} <connectorCode.apiCode> [--body-json file|json]`);
|
|
2631
|
+
const parsed = parseConnectorApiName(targetName, flags);
|
|
2632
|
+
const body = {
|
|
2633
|
+
connector: parsed.connector,
|
|
2634
|
+
api: parsed.api,
|
|
2635
|
+
...readDirectJsonBody(flags, 'connector invoke', { optional: true }),
|
|
2636
|
+
...(flags['path-params-json'] ? { pathParams: readJsonArg(flags['path-params-json'], 'path-params-json') } : {}),
|
|
2637
|
+
...(flags['query-json'] ? { query: readJsonArg(flags['query-json'], 'query-json') } : {}),
|
|
2638
|
+
...(flags['headers-json'] ? { headers: readJsonArg(flags['headers-json'], 'headers-json') } : {}),
|
|
2639
|
+
...(flags['request-body-type'] ? { requestBodyType: flags['request-body-type'] } : {}),
|
|
2640
|
+
...(flags['response-type'] ? { responseType: flags['response-type'] } : {}),
|
|
2641
|
+
};
|
|
2642
|
+
if (subcommand === 'download-test') body.responseType = 'binary';
|
|
2643
|
+
return runDirectRequest(config, target, flags, {
|
|
2644
|
+
method: 'POST',
|
|
2645
|
+
path: `/${encodeURIComponent(target.appType)}/v1/connectors/actions/${subcommand === 'download-test' ? 'download' : 'invoke'}`,
|
|
2646
|
+
body,
|
|
2647
|
+
strictEnvelope: subcommand !== 'download-test',
|
|
2648
|
+
});
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2651
|
+
return directResourceCrud(config, target, {
|
|
2652
|
+
commandName: 'connector',
|
|
2653
|
+
subcommand,
|
|
2654
|
+
flags,
|
|
2655
|
+
positional,
|
|
2656
|
+
resourceType: 'connector',
|
|
2657
|
+
basePath: `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/connectors`,
|
|
2658
|
+
manifestDir: path.join('src', 'resources', 'connectors'),
|
|
2659
|
+
createMethod: 'POST',
|
|
2660
|
+
createPath: `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/connectors/actions/sync`,
|
|
2661
|
+
createBody: body => ({ connectors: [normalizeConnectorManifest(body)] }),
|
|
2662
|
+
updateMethod: 'POST',
|
|
2663
|
+
updatePath: `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/connectors/actions/sync`,
|
|
2664
|
+
updateBody: body => ({ connectors: [normalizeConnectorManifest(body)] }),
|
|
2665
|
+
normalizeBody: body => normalizeConnectorManifest(body),
|
|
2666
|
+
codeOf: body => body.code || body.methodName,
|
|
2667
|
+
responseData: data => Array.isArray(data?.data) ? data.data[0] : data,
|
|
2668
|
+
saveState: (code, data) => saveConnectorResource(target, code, data?.connector?.id || data?.id, {
|
|
2669
|
+
apis: Object.fromEntries(
|
|
2670
|
+
(data?.apis || []).map(api => [api.code || api.methodName, { apiId: api.id, name: api.name }])
|
|
2671
|
+
),
|
|
2672
|
+
}),
|
|
2673
|
+
});
|
|
2674
|
+
}
|
|
2675
|
+
|
|
2676
|
+
async function notification(args) {
|
|
2677
|
+
const { subcommand, rest } = parseSubcommandArgs(args);
|
|
2678
|
+
const { flags, positional } = parseArgs(rest);
|
|
2679
|
+
if (wantsSubcommandHelp(subcommand, flags)) {
|
|
2680
|
+
print('用法: openxiangda notification template-list|template-get|template-upsert|template-delete|type-list|type-get|type-upsert|type-delete|preview|send|batch-send');
|
|
2681
|
+
return;
|
|
2682
|
+
}
|
|
2683
|
+
const config = loadConfig();
|
|
2684
|
+
const target = getWorkspaceTarget(config, flags.profile || config.currentProfile, flags);
|
|
2685
|
+
const appPrefix = `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/notifications`;
|
|
2686
|
+
|
|
2687
|
+
if (subcommand === 'template-list') {
|
|
2688
|
+
return runDirectRequest(config, target, flags, {
|
|
2689
|
+
method: 'GET',
|
|
2690
|
+
path: apiPathWithQuery(`${appPrefix}/templates`, { page: flags.page, pageSize: flags['page-size'] || flags.limit }),
|
|
2691
|
+
});
|
|
2692
|
+
}
|
|
2693
|
+
if (subcommand === 'template-get') {
|
|
2694
|
+
const code = positional[0] || flags.code;
|
|
2695
|
+
if (!code) fail('用法: openxiangda notification template-get <templateCode>');
|
|
2696
|
+
return runDirectRequest(config, target, flags, {
|
|
2697
|
+
method: 'GET',
|
|
2698
|
+
path: `${appPrefix}/templates/${encodeURIComponent(code)}`,
|
|
2699
|
+
});
|
|
2700
|
+
}
|
|
2701
|
+
if (subcommand === 'template-upsert') {
|
|
2702
|
+
const body = readDirectJsonBody(flags, 'notification template');
|
|
2703
|
+
const template = extractNotificationTemplateBody(target.bound, body);
|
|
2704
|
+
const code = positional[0] || body.code || body.templateCode || template.code;
|
|
2705
|
+
if (!code) fail('notification template-upsert 缺少 template code');
|
|
2706
|
+
const normalized = normalizeNotificationTemplateManifest(target.bound, { ...body, ...template, code });
|
|
2707
|
+
const data = await runDirectRequest(config, target, flags, {
|
|
2708
|
+
method: 'PUT',
|
|
2709
|
+
path: `${appPrefix}/templates/${encodeURIComponent(code)}`,
|
|
2710
|
+
body: normalized,
|
|
2711
|
+
}, { returnData: true });
|
|
2712
|
+
if (!flags['dry-run'] && data?.id) {
|
|
2713
|
+
saveNotificationTemplateResource(target, code, data.id, {
|
|
2714
|
+
level: data.level || normalized.level,
|
|
2715
|
+
formUuid: data.formUuid || normalized.formUuid,
|
|
2716
|
+
});
|
|
2717
|
+
if (flags['write-manifest']) writeDirectManifest('notification', code, { ...body, code, resourceType: 'template' });
|
|
2718
|
+
}
|
|
2719
|
+
return outputDirectResult(data, flags);
|
|
2720
|
+
}
|
|
2721
|
+
if (subcommand === 'template-delete') {
|
|
2722
|
+
const code = positional[0] || flags.code;
|
|
2723
|
+
if (!code || !flags.force) fail('用法: openxiangda notification template-delete <templateCode> --force');
|
|
2724
|
+
return runDirectRequest(config, target, flags, {
|
|
2725
|
+
method: 'DELETE',
|
|
2726
|
+
path: `${appPrefix}/templates/${encodeURIComponent(code)}`,
|
|
2727
|
+
});
|
|
2728
|
+
}
|
|
2729
|
+
if (subcommand === 'type-list') {
|
|
2730
|
+
return runDirectRequest(config, target, flags, {
|
|
2731
|
+
method: 'GET',
|
|
2732
|
+
path: apiPathWithQuery(`${appPrefix}/type-configs`, { page: flags.page, pageSize: flags['page-size'] || flags.limit }),
|
|
2733
|
+
});
|
|
2734
|
+
}
|
|
2735
|
+
if (subcommand === 'type-get') {
|
|
2736
|
+
const code = positional[0] || flags.code;
|
|
2737
|
+
if (!code) fail('用法: openxiangda notification type-get <notificationType>');
|
|
2738
|
+
return runDirectRequest(config, target, flags, {
|
|
2739
|
+
method: 'GET',
|
|
2740
|
+
path: `${appPrefix}/type-configs/${encodeURIComponent(code)}`,
|
|
2741
|
+
});
|
|
2742
|
+
}
|
|
2743
|
+
if (subcommand === 'type-upsert') {
|
|
2744
|
+
const body = readDirectJsonBody(flags, 'notification type-config');
|
|
2745
|
+
const configBody = extractNotificationTypeConfigBody(target.bound, body);
|
|
2746
|
+
const code = positional[0] || body.notificationType || configBody.notificationType;
|
|
2747
|
+
if (!code) fail('notification type-upsert 缺少 notificationType');
|
|
2748
|
+
const normalized = normalizeNotificationTypeConfigManifest(target.bound, { ...body, ...configBody, notificationType: code });
|
|
2749
|
+
const data = await runDirectRequest(config, target, flags, {
|
|
2750
|
+
method: 'PUT',
|
|
2751
|
+
path: `${appPrefix}/type-configs/${encodeURIComponent(code)}`,
|
|
2752
|
+
body: normalized,
|
|
2753
|
+
}, { returnData: true });
|
|
2754
|
+
if (!flags['dry-run'] && data?.id) {
|
|
2755
|
+
saveNotificationTypeConfigResource(target, body.code || code, data.id, {
|
|
2756
|
+
notificationType: data.notificationType || code,
|
|
2757
|
+
level: data.level || normalized.level,
|
|
2758
|
+
formUuid: data.formUuid || normalized.formUuid,
|
|
2759
|
+
templateId: data.templateId,
|
|
2760
|
+
templateCode: data.template?.code || body.templateCode,
|
|
2761
|
+
});
|
|
2762
|
+
if (flags['write-manifest']) writeDirectManifest('notification', body.code || code, { ...body, notificationType: code, resourceType: 'typeConfig' });
|
|
2763
|
+
}
|
|
2764
|
+
return outputDirectResult(data, flags);
|
|
2765
|
+
}
|
|
2766
|
+
if (subcommand === 'type-delete') {
|
|
2767
|
+
const code = positional[0] || flags.code;
|
|
2768
|
+
if (!code || !flags.force) fail('用法: openxiangda notification type-delete <notificationType> --force');
|
|
2769
|
+
return runDirectRequest(config, target, flags, {
|
|
2770
|
+
method: 'DELETE',
|
|
2771
|
+
path: `${appPrefix}/type-configs/${encodeURIComponent(code)}`,
|
|
2772
|
+
});
|
|
2773
|
+
}
|
|
2774
|
+
if (['preview', 'send', 'batch-send'].includes(subcommand)) {
|
|
2775
|
+
if ((subcommand === 'send' || subcommand === 'batch-send') && !flags.force) {
|
|
2776
|
+
fail(`openxiangda notification ${subcommand} 是发送动作,必须加 --force`);
|
|
2777
|
+
}
|
|
2778
|
+
const code = positional[0] || flags.code;
|
|
2779
|
+
const body = {
|
|
2780
|
+
...readDirectJsonBody(flags, `notification ${subcommand}`, { optional: true }),
|
|
2781
|
+
...(code ? { templateCode: code, notificationType: code } : {}),
|
|
2782
|
+
};
|
|
2783
|
+
return runDirectRequest(config, target, flags, {
|
|
2784
|
+
method: 'POST',
|
|
2785
|
+
path: `${appPrefix}/${subcommand === 'batch-send' ? 'batch-send' : subcommand}`,
|
|
2786
|
+
body,
|
|
2787
|
+
strictEnvelope: true,
|
|
2788
|
+
});
|
|
2789
|
+
}
|
|
2790
|
+
|
|
2791
|
+
fail('用法: openxiangda notification template-list|template-get|template-upsert|template-delete|type-list|type-get|type-upsert|type-delete|preview|send|batch-send');
|
|
2180
2792
|
}
|
|
2181
2793
|
|
|
2182
2794
|
async function permission(args) {
|
|
@@ -2243,6 +2855,46 @@ async function permission(args) {
|
|
|
2243
2855
|
return;
|
|
2244
2856
|
}
|
|
2245
2857
|
|
|
2858
|
+
if (subcommand === 'role-update') {
|
|
2859
|
+
const [roleKey] = positional;
|
|
2860
|
+
const body = readDirectJsonBody(flags, 'role update');
|
|
2861
|
+
const roleCode = roleKey || body.code || body.resourceCode;
|
|
2862
|
+
if (!roleCode) fail('用法: openxiangda permission role-update <roleCode|roleId> --json-file file');
|
|
2863
|
+
const target = getWorkspaceTarget(config, profileName, flags);
|
|
2864
|
+
const roleId = resolveRoleId(target.bound, roleCode, flags);
|
|
2865
|
+
const data = await runDirectRequest(config, target, flags, {
|
|
2866
|
+
method: 'PUT',
|
|
2867
|
+
path: `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/roles/${encodeURIComponent(roleId)}`,
|
|
2868
|
+
body: {
|
|
2869
|
+
code: body.code || roleCode,
|
|
2870
|
+
name: body.name || roleCode,
|
|
2871
|
+
description: body.description || '',
|
|
2872
|
+
},
|
|
2873
|
+
}, { returnData: true });
|
|
2874
|
+
if (!flags['dry-run']) {
|
|
2875
|
+
const resultCode = body.code || roleCode;
|
|
2876
|
+
if (data?.id) saveRoleResource(target, resultCode, data.id);
|
|
2877
|
+
if (flags['write-manifest']) writeDirectManifest('role', resultCode, { ...body, code: resultCode });
|
|
2878
|
+
}
|
|
2879
|
+
return outputDirectResult(data, flags);
|
|
2880
|
+
}
|
|
2881
|
+
|
|
2882
|
+
if (subcommand === 'role-delete') {
|
|
2883
|
+
const [roleKey] = positional;
|
|
2884
|
+
if (!roleKey || !flags.force) fail('用法: openxiangda permission role-delete <roleCode|roleId> --force');
|
|
2885
|
+
const target = getWorkspaceTarget(config, profileName, flags);
|
|
2886
|
+
const roleId = resolveRoleId(target.bound, roleKey, flags);
|
|
2887
|
+
const data = await runDirectRequest(config, target, flags, {
|
|
2888
|
+
method: 'DELETE',
|
|
2889
|
+
path: `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/roles/${encodeURIComponent(roleId)}`,
|
|
2890
|
+
}, { returnData: true });
|
|
2891
|
+
if (!flags['dry-run'] && target.bound.resources?.roles?.[roleKey]) {
|
|
2892
|
+
delete target.bound.resources.roles[roleKey];
|
|
2893
|
+
saveProjectState(target.state);
|
|
2894
|
+
}
|
|
2895
|
+
return outputDirectResult(data, flags);
|
|
2896
|
+
}
|
|
2897
|
+
|
|
2246
2898
|
if (subcommand === 'role-users') {
|
|
2247
2899
|
const [roleKey] = positional;
|
|
2248
2900
|
if (!roleKey) fail('用法: openxiangda permission role-users <roleCode|roleId>');
|
|
@@ -2380,6 +3032,42 @@ async function permission(args) {
|
|
|
2380
3032
|
return;
|
|
2381
3033
|
}
|
|
2382
3034
|
|
|
3035
|
+
if (subcommand === 'page-group-update') {
|
|
3036
|
+
const [groupKey] = positional;
|
|
3037
|
+
const body = readDirectJsonBody(flags, 'page permission group update');
|
|
3038
|
+
const groupCode = groupKey || body.code || body.resourceCode;
|
|
3039
|
+
if (!groupCode) fail('用法: openxiangda permission page-group-update <groupCode|groupId> --json-file file');
|
|
3040
|
+
const target = getWorkspaceTarget(config, profileName, flags);
|
|
3041
|
+
const groupId = flags['group-id'] || target.bound.resources?.pagePermissionGroups?.[groupCode]?.groupId || groupCode;
|
|
3042
|
+
const normalized = normalizePagePermissionGroupDirectBody(target.bound, { ...body, code: body.code || groupCode });
|
|
3043
|
+
const data = await runDirectRequest(config, target, flags, {
|
|
3044
|
+
method: 'PUT',
|
|
3045
|
+
path: `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/page-permission-groups/${encodeURIComponent(groupId)}`,
|
|
3046
|
+
body: normalized,
|
|
3047
|
+
}, { returnData: true });
|
|
3048
|
+
if (!flags['dry-run']) {
|
|
3049
|
+
if (data?.id) savePagePermissionGroupResource(target, body.code || groupCode, data.id, { name: data.name, resourceCode: body.code || groupCode });
|
|
3050
|
+
if (flags['write-manifest']) writeDirectManifest('page-permission-group', body.code || groupCode, { ...body, code: body.code || groupCode });
|
|
3051
|
+
}
|
|
3052
|
+
return outputDirectResult(data, flags);
|
|
3053
|
+
}
|
|
3054
|
+
|
|
3055
|
+
if (subcommand === 'page-group-delete') {
|
|
3056
|
+
const [groupKey] = positional;
|
|
3057
|
+
if (!groupKey || !flags.force) fail('用法: openxiangda permission page-group-delete <groupCode|groupId> --force');
|
|
3058
|
+
const target = getWorkspaceTarget(config, profileName, flags);
|
|
3059
|
+
const groupId = flags['group-id'] || target.bound.resources?.pagePermissionGroups?.[groupKey]?.groupId || groupKey;
|
|
3060
|
+
const data = await runDirectRequest(config, target, flags, {
|
|
3061
|
+
method: 'DELETE',
|
|
3062
|
+
path: `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/page-permission-groups/${encodeURIComponent(groupId)}`,
|
|
3063
|
+
}, { returnData: true });
|
|
3064
|
+
if (!flags['dry-run'] && target.bound.resources?.pagePermissionGroups?.[groupKey]) {
|
|
3065
|
+
delete target.bound.resources.pagePermissionGroups[groupKey];
|
|
3066
|
+
saveProjectState(target.state);
|
|
3067
|
+
}
|
|
3068
|
+
return outputDirectResult(data, flags);
|
|
3069
|
+
}
|
|
3070
|
+
|
|
2383
3071
|
if (subcommand === 'form-group-list') {
|
|
2384
3072
|
const target = getWorkspaceTarget(config, profileName, flags);
|
|
2385
3073
|
const formUuid = flags['form-uuid'] || resolveOptionalFormUuid(target.bound, flags['form-code']);
|
|
@@ -2477,6 +3165,44 @@ async function permission(args) {
|
|
|
2477
3165
|
return;
|
|
2478
3166
|
}
|
|
2479
3167
|
|
|
3168
|
+
if (subcommand === 'form-group-update') {
|
|
3169
|
+
const [groupKey] = positional;
|
|
3170
|
+
const body = readDirectJsonBody(flags, 'form permission group update');
|
|
3171
|
+
const groupCode = groupKey || body.code || body.resourceCode;
|
|
3172
|
+
const target = getWorkspaceTarget(config, profileName, flags);
|
|
3173
|
+
const formUuid = flags['form-uuid'] || resolveManifestFormUuid(target.bound, body);
|
|
3174
|
+
if (!groupCode || !formUuid) fail('用法: openxiangda permission form-group-update <groupCode|groupId> --form-code code|--form-uuid FORM --json-file file');
|
|
3175
|
+
const groupId = flags['group-id'] || target.bound.resources?.formPermissionGroups?.[groupCode]?.groupId || groupCode;
|
|
3176
|
+
const normalized = normalizeFormPermissionGroupManifest(target.bound, { ...body, code: body.code || groupCode, formUuid });
|
|
3177
|
+
const data = await runDirectRequest(config, target, flags, {
|
|
3178
|
+
method: 'PUT',
|
|
3179
|
+
path: `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/forms/${encodeURIComponent(formUuid)}/permission-groups/${encodeURIComponent(groupId)}`,
|
|
3180
|
+
body: normalized,
|
|
3181
|
+
}, { returnData: true });
|
|
3182
|
+
if (!flags['dry-run']) {
|
|
3183
|
+
if (data?.id) saveFormPermissionGroupResource(target, body.code || groupCode, data.id, { formUuid, name: data.name, resourceCode: body.code || groupCode });
|
|
3184
|
+
if (flags['write-manifest']) writeDirectManifest('form-permission-group', body.code || groupCode, { ...body, code: body.code || groupCode });
|
|
3185
|
+
}
|
|
3186
|
+
return outputDirectResult(data, flags);
|
|
3187
|
+
}
|
|
3188
|
+
|
|
3189
|
+
if (subcommand === 'form-group-delete') {
|
|
3190
|
+
const [groupKey] = positional;
|
|
3191
|
+
const target = getWorkspaceTarget(config, profileName, flags);
|
|
3192
|
+
const formUuid = flags['form-uuid'] || resolveOptionalFormUuid(target.bound, flags['form-code']);
|
|
3193
|
+
if (!groupKey || !formUuid || !flags.force) fail('用法: openxiangda permission form-group-delete <groupCode|groupId> --form-code code|--form-uuid FORM --force');
|
|
3194
|
+
const groupId = flags['group-id'] || target.bound.resources?.formPermissionGroups?.[groupKey]?.groupId || groupKey;
|
|
3195
|
+
const data = await runDirectRequest(config, target, flags, {
|
|
3196
|
+
method: 'DELETE',
|
|
3197
|
+
path: `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/forms/${encodeURIComponent(formUuid)}/permission-groups/${encodeURIComponent(groupId)}`,
|
|
3198
|
+
}, { returnData: true });
|
|
3199
|
+
if (!flags['dry-run'] && target.bound.resources?.formPermissionGroups?.[groupKey]) {
|
|
3200
|
+
delete target.bound.resources.formPermissionGroups[groupKey];
|
|
3201
|
+
saveProjectState(target.state);
|
|
3202
|
+
}
|
|
3203
|
+
return outputDirectResult(data, flags);
|
|
3204
|
+
}
|
|
3205
|
+
|
|
2480
3206
|
if (subcommand === 'form-summary') {
|
|
2481
3207
|
const target = getWorkspaceTarget(config, profileName, flags);
|
|
2482
3208
|
const formUuid = flags['form-uuid'] || resolveOptionalFormUuid(target.bound, flags['form-code'] || positional[0]);
|
|
@@ -2503,8 +3229,17 @@ async function permission(args) {
|
|
|
2503
3229
|
return;
|
|
2504
3230
|
}
|
|
2505
3231
|
|
|
3232
|
+
if (subcommand === 'audit') {
|
|
3233
|
+
const target = getWorkspaceTarget(config, profileName, flags);
|
|
3234
|
+
const result = await buildPermissionAudit(config, target, flags);
|
|
3235
|
+
if (flags.json) return writeJson(result);
|
|
3236
|
+
print(JSON.stringify(result, null, 2));
|
|
3237
|
+
if (result.errors.length > 0) fail(`permission audit 发现 ${result.errors.length} 个错误`);
|
|
3238
|
+
return;
|
|
3239
|
+
}
|
|
3240
|
+
|
|
2506
3241
|
fail(
|
|
2507
|
-
'用法: openxiangda permission role-list|role-create|role-bind|role-users|role-add-users|page-group-list|page-group-create|page-group-bind|form-group-list|form-group-create|form-group-bind|form-summary|menu-permissions'
|
|
3242
|
+
'用法: openxiangda permission role-list|role-create|role-update|role-delete|role-bind|role-users|role-add-users|page-group-list|page-group-create|page-group-update|page-group-delete|page-group-bind|form-group-list|form-group-create|form-group-update|form-group-delete|form-group-bind|form-summary|menu-permissions|audit'
|
|
2508
3243
|
);
|
|
2509
3244
|
}
|
|
2510
3245
|
|
|
@@ -2673,16 +3408,25 @@ async function settings(args) {
|
|
|
2673
3408
|
|
|
2674
3409
|
async function resource(args) {
|
|
2675
3410
|
const { subcommand, rest } = parseSubcommandArgs(args);
|
|
2676
|
-
const { flags } = parseArgs(rest);
|
|
3411
|
+
const { flags, positional } = parseArgs(rest);
|
|
2677
3412
|
if (wantsSubcommandHelp(subcommand, flags)) {
|
|
2678
|
-
print('用法: openxiangda resource validate|plan|publish|pull|typegen [--profile name] [--json]');
|
|
3413
|
+
print('用法: openxiangda resource validate|plan|publish|pull|typegen|explain [type] [--profile name] [--json]');
|
|
2679
3414
|
return;
|
|
2680
3415
|
}
|
|
2681
3416
|
const config = loadConfig();
|
|
2682
3417
|
const profileName = flags.profile || config.currentProfile;
|
|
2683
3418
|
|
|
2684
|
-
if (!['validate', 'plan', 'publish', 'pull', 'typegen'].includes(subcommand)) {
|
|
2685
|
-
fail('用法: openxiangda resource validate|plan|publish|pull|typegen [--profile name] [--json]');
|
|
3419
|
+
if (!['validate', 'plan', 'publish', 'pull', 'typegen', 'explain'].includes(subcommand)) {
|
|
3420
|
+
fail('用法: openxiangda resource validate|plan|publish|pull|typegen|explain [--profile name] [--json]');
|
|
3421
|
+
}
|
|
3422
|
+
|
|
3423
|
+
if (subcommand === 'explain') {
|
|
3424
|
+
const type = positional[0] || flags.type || 'route';
|
|
3425
|
+
const result = getResourceExplain(type);
|
|
3426
|
+
if (!result) fail(`未知资源类型: ${type}`);
|
|
3427
|
+
if (flags.json) return writeJson({ type, ...result });
|
|
3428
|
+
print(renderResourceExplain(type));
|
|
3429
|
+
return;
|
|
2686
3430
|
}
|
|
2687
3431
|
|
|
2688
3432
|
if (subcommand === 'pull') {
|
|
@@ -3181,20 +3925,28 @@ async function commands(args) {
|
|
|
3181
3925
|
'update check|install',
|
|
3182
3926
|
'platform add|list|use|remove',
|
|
3183
3927
|
'auth status|refresh|logout',
|
|
3928
|
+
'doctor [--profile name] [--app-type APP_XXX]',
|
|
3929
|
+
'design gates|template [--topic code]',
|
|
3184
3930
|
'env',
|
|
3185
3931
|
'workspace init|bind|publish [--app-name] [--changed|--since|--form|--page|--only|--dry-run|--force|--resources|--skip-resources|--prune]',
|
|
3186
3932
|
'app list|create|snapshot',
|
|
3187
3933
|
'form list|create|bind|pull|publish',
|
|
3188
3934
|
'page list|publish|bind|releases|activate',
|
|
3189
|
-
'menu list|create|bind|delete',
|
|
3935
|
+
'menu list|create|update|sort|bind|delete',
|
|
3190
3936
|
'workflow list|create|bind|pull|publish|delete|validate',
|
|
3191
3937
|
'automation list|create|bind|pull|publish|unpublish|enable|disable|delete|validate|cron-validate',
|
|
3192
|
-
'data-view list|status|refresh|query|stats',
|
|
3193
|
-
'
|
|
3194
|
-
'
|
|
3195
|
-
'
|
|
3938
|
+
'data-view list|get|create|update|upsert|delete|status|refresh|query|stats',
|
|
3939
|
+
'route list|get|create|update|upsert|delete',
|
|
3940
|
+
'public-access list|get|create|update|upsert|delete|ticket-create|session-test|grant-check',
|
|
3941
|
+
'auth-config list|get|create|update|upsert|delete|methods',
|
|
3942
|
+
'function list|get|create|update|upsert|delete|invoke',
|
|
3943
|
+
'connector list|get|create|update|upsert|delete|invoke|download-test',
|
|
3944
|
+
'notification template-list|template-get|template-upsert|template-delete|type-list|type-get|type-upsert|type-delete|preview|send|batch-send',
|
|
3945
|
+
'permission role-list|role-create|role-update|role-delete|role-bind|role-users|role-add-users|audit',
|
|
3946
|
+
'permission page-group-list|page-group-create|page-group-update|page-group-delete|page-group-bind',
|
|
3947
|
+
'permission form-group-list|form-group-create|form-group-update|form-group-delete|form-group-bind|form-summary|menu-permissions',
|
|
3196
3948
|
'settings get|save|indexes|indexes-save|data-management|data-management-save|public-access|public-access-save|public-access-delete',
|
|
3197
|
-
'resource validate|plan|publish|pull|typegen',
|
|
3949
|
+
'resource validate|plan|publish|pull|typegen|explain',
|
|
3198
3950
|
'inspect app|form|workflow|automation|permissions',
|
|
3199
3951
|
'feedback preview|submit',
|
|
3200
3952
|
'skill install|status|bootstrap',
|
|
@@ -3365,6 +4117,334 @@ function getWorkspaceTarget(config, profileName, flags = {}) {
|
|
|
3365
4117
|
};
|
|
3366
4118
|
}
|
|
3367
4119
|
|
|
4120
|
+
async function directResourceCrud(config, target, spec) {
|
|
4121
|
+
const { subcommand, flags, positional } = spec;
|
|
4122
|
+
const basePath = spec.basePath;
|
|
4123
|
+
|
|
4124
|
+
if (subcommand === 'list') {
|
|
4125
|
+
return runDirectRequest(config, target, flags, {
|
|
4126
|
+
method: 'GET',
|
|
4127
|
+
path: apiPathWithQuery(basePath, {
|
|
4128
|
+
page: flags.page,
|
|
4129
|
+
pageSize: flags['page-size'] || flags.limit,
|
|
4130
|
+
limit: flags.limit,
|
|
4131
|
+
code: flags.code,
|
|
4132
|
+
}),
|
|
4133
|
+
});
|
|
4134
|
+
}
|
|
4135
|
+
|
|
4136
|
+
if (subcommand === 'get') {
|
|
4137
|
+
const code = positional[0] || flags.code;
|
|
4138
|
+
if (!code) fail(`用法: openxiangda ${spec.commandName} get <code>`);
|
|
4139
|
+
return runDirectRequest(config, target, flags, {
|
|
4140
|
+
method: 'GET',
|
|
4141
|
+
path: `${basePath}/${encodeURIComponent(code)}`,
|
|
4142
|
+
});
|
|
4143
|
+
}
|
|
4144
|
+
|
|
4145
|
+
if (subcommand === 'delete') {
|
|
4146
|
+
const code = positional[0] || flags.code;
|
|
4147
|
+
if (!code || !flags.force) fail(`用法: openxiangda ${spec.commandName} delete <code> --force`);
|
|
4148
|
+
const data = await runDirectRequest(config, target, flags, {
|
|
4149
|
+
method: 'DELETE',
|
|
4150
|
+
path: `${basePath}/${encodeURIComponent(code)}`,
|
|
4151
|
+
}, { returnData: true });
|
|
4152
|
+
if (!flags['dry-run']) removeDirectStateResource(target, spec.resourceType, code);
|
|
4153
|
+
return outputDirectResult(data, flags);
|
|
4154
|
+
}
|
|
4155
|
+
|
|
4156
|
+
if (!['create', 'update', 'upsert'].includes(subcommand)) {
|
|
4157
|
+
fail(`用法: openxiangda ${spec.commandName} list|get|create|update|upsert|delete`);
|
|
4158
|
+
}
|
|
4159
|
+
|
|
4160
|
+
const rawBody = readDirectJsonBody(flags, spec.commandName);
|
|
4161
|
+
const explicitCode = positional[0] || flags.code;
|
|
4162
|
+
if (explicitCode && !rawBody.code && !rawBody.resourceCode && !rawBody.functionCode && !rawBody.methodName) {
|
|
4163
|
+
rawBody.code = explicitCode;
|
|
4164
|
+
}
|
|
4165
|
+
const code = explicitCode || spec.codeOf(rawBody);
|
|
4166
|
+
if (!code) fail(`openxiangda ${spec.commandName} ${subcommand} 缺少资源 code`);
|
|
4167
|
+
const normalizedBody = spec.prepareBody
|
|
4168
|
+
? await spec.prepareBody(rawBody)
|
|
4169
|
+
: spec.normalizeBody
|
|
4170
|
+
? spec.normalizeBody(rawBody)
|
|
4171
|
+
: rawBody;
|
|
4172
|
+
|
|
4173
|
+
let method = subcommand === 'create' ? (spec.createMethod || 'POST') : (spec.updateMethod || 'PUT');
|
|
4174
|
+
let requestPath = subcommand === 'create'
|
|
4175
|
+
? (spec.createPath || basePath)
|
|
4176
|
+
: (spec.updatePath || `${basePath}/${encodeURIComponent(code)}`);
|
|
4177
|
+
let requestBody = subcommand === 'create'
|
|
4178
|
+
? (spec.createBody ? spec.createBody(rawBody, normalizedBody) : normalizedBody)
|
|
4179
|
+
: (spec.updateBody ? spec.updateBody(rawBody, normalizedBody) : normalizedBody);
|
|
4180
|
+
let action = subcommand;
|
|
4181
|
+
|
|
4182
|
+
if (subcommand === 'upsert') {
|
|
4183
|
+
if (flags['dry-run']) {
|
|
4184
|
+
return outputDirectResult({
|
|
4185
|
+
dryRun: true,
|
|
4186
|
+
action: 'upsert',
|
|
4187
|
+
existsCheck: { method: 'GET', path: `${basePath}/${encodeURIComponent(code)}` },
|
|
4188
|
+
create: {
|
|
4189
|
+
method: spec.createMethod || 'POST',
|
|
4190
|
+
path: spec.createPath || basePath,
|
|
4191
|
+
body: spec.createBody ? spec.createBody(rawBody, normalizedBody) : normalizedBody,
|
|
4192
|
+
},
|
|
4193
|
+
update: {
|
|
4194
|
+
method: spec.updateMethod || 'PUT',
|
|
4195
|
+
path: spec.updatePath || `${basePath}/${encodeURIComponent(code)}`,
|
|
4196
|
+
body: spec.updateBody ? spec.updateBody(rawBody, normalizedBody) : normalizedBody,
|
|
4197
|
+
},
|
|
4198
|
+
}, flags);
|
|
4199
|
+
}
|
|
4200
|
+
const existing = await requestOptionalWithAuth(
|
|
4201
|
+
config,
|
|
4202
|
+
target.profileName,
|
|
4203
|
+
`${basePath}/${encodeURIComponent(code)}`
|
|
4204
|
+
);
|
|
4205
|
+
action = existing ? 'update' : 'create';
|
|
4206
|
+
method = existing ? (spec.updateMethod || 'PUT') : (spec.createMethod || 'POST');
|
|
4207
|
+
requestPath = existing
|
|
4208
|
+
? (spec.updatePath || `${basePath}/${encodeURIComponent(code)}`)
|
|
4209
|
+
: (spec.createPath || basePath);
|
|
4210
|
+
requestBody = existing
|
|
4211
|
+
? (spec.updateBody ? spec.updateBody(rawBody, normalizedBody) : normalizedBody)
|
|
4212
|
+
: (spec.createBody ? spec.createBody(rawBody, normalizedBody) : normalizedBody);
|
|
4213
|
+
}
|
|
4214
|
+
|
|
4215
|
+
const response = await runDirectRequest(config, target, flags, {
|
|
4216
|
+
method,
|
|
4217
|
+
path: requestPath,
|
|
4218
|
+
body: requestBody,
|
|
4219
|
+
}, { returnData: true });
|
|
4220
|
+
const data = spec.responseData ? spec.responseData(response) : response;
|
|
4221
|
+
if (!flags['dry-run']) {
|
|
4222
|
+
if (spec.saveState) spec.saveState(code, data);
|
|
4223
|
+
if (flags['write-manifest']) writeDirectManifest(spec.resourceType, code, { ...rawBody, code });
|
|
4224
|
+
}
|
|
4225
|
+
return outputDirectResult({ action, data }, flags);
|
|
4226
|
+
}
|
|
4227
|
+
|
|
4228
|
+
async function runDirectRequest(config, target, flags, request, options = {}) {
|
|
4229
|
+
const plan = {
|
|
4230
|
+
dryRun: true,
|
|
4231
|
+
method: request.method || 'GET',
|
|
4232
|
+
path: request.path,
|
|
4233
|
+
body: request.body,
|
|
4234
|
+
};
|
|
4235
|
+
if (flags['dry-run']) {
|
|
4236
|
+
if (options.returnData) return plan;
|
|
4237
|
+
return outputDirectResult(plan, flags);
|
|
4238
|
+
}
|
|
4239
|
+
|
|
4240
|
+
const requestOptions = {
|
|
4241
|
+
method: request.method || 'GET',
|
|
4242
|
+
body: request.body,
|
|
4243
|
+
strictEnvelope: request.strictEnvelope,
|
|
4244
|
+
};
|
|
4245
|
+
let data;
|
|
4246
|
+
if (request.auth === false) {
|
|
4247
|
+
const payload = await requestJson(target.profile.baseUrl, request.path, requestOptions);
|
|
4248
|
+
data = request.strictEnvelope ? unwrapStrictApi(payload) : unwrapApi(payload);
|
|
4249
|
+
} else {
|
|
4250
|
+
data = await requestWithAuth(config, target.profileName, request.path, requestOptions);
|
|
4251
|
+
}
|
|
4252
|
+
if (options.returnData) return data;
|
|
4253
|
+
return outputDirectResult(data, flags);
|
|
4254
|
+
}
|
|
4255
|
+
|
|
4256
|
+
function outputDirectResult(data, flags = {}) {
|
|
4257
|
+
if (flags.json) return writeJson(data);
|
|
4258
|
+
print(JSON.stringify(data, null, 2));
|
|
4259
|
+
}
|
|
4260
|
+
|
|
4261
|
+
function readDirectJsonBody(flags = {}, label, options = {}) {
|
|
4262
|
+
if (flags['json-file']) return readJsonArg(flags['json-file'], 'json-file');
|
|
4263
|
+
if (flags['body-json']) return readJsonArg(flags['body-json'], 'body-json');
|
|
4264
|
+
if (options.optional) return {};
|
|
4265
|
+
fail(`${label} 缺少 --json-file <file> 或 --body-json <json|file>`);
|
|
4266
|
+
}
|
|
4267
|
+
|
|
4268
|
+
function writeDirectManifest(resourceType, code, value) {
|
|
4269
|
+
const dir = directManifestDir(resourceType);
|
|
4270
|
+
if (!dir) return;
|
|
4271
|
+
const fileName = `${String(code).replace(/[^A-Za-z0-9_.-]+/g, '_')}.json`;
|
|
4272
|
+
writeResourceJsonFile(path.join(process.cwd(), dir, fileName), value);
|
|
4273
|
+
}
|
|
4274
|
+
|
|
4275
|
+
function directManifestDir(resourceType) {
|
|
4276
|
+
const dirs = {
|
|
4277
|
+
route: path.join('src', 'resources', 'routes'),
|
|
4278
|
+
'public-access': path.join('src', 'resources', 'public-access'),
|
|
4279
|
+
'auth-config': path.join('src', 'resources', 'auth'),
|
|
4280
|
+
function: path.join('src', 'resources', 'functions'),
|
|
4281
|
+
connector: path.join('src', 'resources', 'connectors'),
|
|
4282
|
+
notification: path.join('src', 'resources', 'notifications'),
|
|
4283
|
+
'data-view': path.join('src', 'resources', 'data-views'),
|
|
4284
|
+
menu: path.join('src', 'resources', 'menus'),
|
|
4285
|
+
role: path.join('src', 'resources', 'roles'),
|
|
4286
|
+
'page-permission-group': path.join('src', 'resources', 'permissions', 'page-groups'),
|
|
4287
|
+
'form-permission-group': path.join('src', 'resources', 'permissions', 'form-groups'),
|
|
4288
|
+
};
|
|
4289
|
+
return dirs[resourceType];
|
|
4290
|
+
}
|
|
4291
|
+
|
|
4292
|
+
function removeDirectStateResource(target, resourceType, code) {
|
|
4293
|
+
const buckets = {
|
|
4294
|
+
route: 'routes',
|
|
4295
|
+
'public-access': 'publicAccessPolicies',
|
|
4296
|
+
'auth-config': 'authConfigs',
|
|
4297
|
+
function: 'functions',
|
|
4298
|
+
connector: 'connectors',
|
|
4299
|
+
'data-view': 'dataViews',
|
|
4300
|
+
};
|
|
4301
|
+
const bucket = buckets[resourceType];
|
|
4302
|
+
if (!bucket || !target.bound.resources?.[bucket]?.[code]) return;
|
|
4303
|
+
delete target.bound.resources[bucket][code];
|
|
4304
|
+
saveProjectState(target.state);
|
|
4305
|
+
}
|
|
4306
|
+
|
|
4307
|
+
function normalizeMenuDirectBody(bound, menuItem) {
|
|
4308
|
+
const formUuid = resolveManifestFormUuid(bound, menuItem);
|
|
4309
|
+
const pageId = resolveManifestPageId(bound, menuItem);
|
|
4310
|
+
const parentId = resolveManifestMenuId(bound, menuItem.parentCode) || menuItem.parentId || null;
|
|
4311
|
+
return stripUndefinedValues({
|
|
4312
|
+
resourceCode: menuItem.code || menuItem.resourceCode,
|
|
4313
|
+
name: menuItem.name || menuItem.code,
|
|
4314
|
+
type: menuItem.type || 'nav',
|
|
4315
|
+
formUuid,
|
|
4316
|
+
pageId,
|
|
4317
|
+
parentId,
|
|
4318
|
+
sortOrder: menuItem.sortOrder,
|
|
4319
|
+
icon: menuItem.icon || null,
|
|
4320
|
+
isHidden: menuItem.isHidden,
|
|
4321
|
+
routeCode: menuItem.routeCode || null,
|
|
4322
|
+
path: menuItem.path || null,
|
|
4323
|
+
});
|
|
4324
|
+
}
|
|
4325
|
+
|
|
4326
|
+
function normalizePagePermissionGroupDirectBody(bound, group) {
|
|
4327
|
+
return stripUndefinedValues({
|
|
4328
|
+
resourceCode: group.code || group.resourceCode,
|
|
4329
|
+
name: group.name || group.code,
|
|
4330
|
+
roles: group.roles || [],
|
|
4331
|
+
menuFormUuids: resolvePagePermissionGroupTargets(bound, group),
|
|
4332
|
+
menuCodes: normalizePermissionCodeArray(group.menuCodes),
|
|
4333
|
+
routeCodes: normalizePermissionCodeArray(group.routeCodes),
|
|
4334
|
+
pathPatterns: normalizePermissionCodeArray(group.pathPatterns),
|
|
4335
|
+
});
|
|
4336
|
+
}
|
|
4337
|
+
|
|
4338
|
+
function parseConnectorApiName(targetName, flags = {}) {
|
|
4339
|
+
const raw = String(targetName || '');
|
|
4340
|
+
const separator = raw.indexOf('.');
|
|
4341
|
+
const connectorCode = separator >= 0 ? raw.slice(0, separator) : raw;
|
|
4342
|
+
const apiCode = flags.api || flags['api-code'] || (separator >= 0 ? raw.slice(separator + 1) : undefined);
|
|
4343
|
+
if (!connectorCode || !apiCode) fail('连接器调用需要 <connectorCode.apiCode> 或 --api <apiCode>');
|
|
4344
|
+
return { connector: connectorCode, api: apiCode };
|
|
4345
|
+
}
|
|
4346
|
+
|
|
4347
|
+
function extractNotificationTemplateBody(bound, body) {
|
|
4348
|
+
const template = Array.isArray(body.templates) ? body.templates[0] : body.template || body;
|
|
4349
|
+
return {
|
|
4350
|
+
...template,
|
|
4351
|
+
code: template.code || template.templateCode || body.code || body.templateCode,
|
|
4352
|
+
formCode: template.formCode || body.formCode,
|
|
4353
|
+
formUuid: template.formUuid || body.formUuid,
|
|
4354
|
+
level: template.level || body.level,
|
|
4355
|
+
};
|
|
4356
|
+
}
|
|
4357
|
+
|
|
4358
|
+
function extractNotificationTypeConfigBody(bound, body) {
|
|
4359
|
+
const config = Array.isArray(body.typeConfigs)
|
|
4360
|
+
? body.typeConfigs[0]
|
|
4361
|
+
: Array.isArray(body.notificationTypeConfigs)
|
|
4362
|
+
? body.notificationTypeConfigs[0]
|
|
4363
|
+
: body.typeConfig || body;
|
|
4364
|
+
return {
|
|
4365
|
+
...config,
|
|
4366
|
+
notificationType: config.notificationType || body.notificationType || body.code,
|
|
4367
|
+
formCode: config.formCode || body.formCode,
|
|
4368
|
+
formUuid: config.formUuid || body.formUuid,
|
|
4369
|
+
level: config.level || body.level,
|
|
4370
|
+
};
|
|
4371
|
+
}
|
|
4372
|
+
|
|
4373
|
+
function buildPublicGrantChecks(bound, grants, flags = {}) {
|
|
4374
|
+
const checks = [];
|
|
4375
|
+
const formTargets = [
|
|
4376
|
+
...splitList(flags['form-code']).map(code => resolveOptionalFormUuid(bound, code)),
|
|
4377
|
+
...splitList(flags['form-uuid']),
|
|
4378
|
+
].filter(Boolean);
|
|
4379
|
+
for (const formUuid of formTargets) {
|
|
4380
|
+
checks.push({ type: 'form', code: formUuid, allowed: (grants.forms || []).includes(formUuid) });
|
|
4381
|
+
}
|
|
4382
|
+
for (const code of splitList(flags['data-view'])) {
|
|
4383
|
+
checks.push({ type: 'dataView', code, allowed: (grants.dataViews || []).includes(code) });
|
|
4384
|
+
}
|
|
4385
|
+
for (const code of splitList(flags.function || flags['function-code'])) {
|
|
4386
|
+
checks.push({ type: 'function', code, allowed: (grants.functions || []).includes(code) });
|
|
4387
|
+
}
|
|
4388
|
+
for (const code of splitList(flags.connector || flags['connector-code'])) {
|
|
4389
|
+
checks.push({ type: 'connector', code, allowed: (grants.connectors || []).includes(code) });
|
|
4390
|
+
}
|
|
4391
|
+
if (checks.length === 0) {
|
|
4392
|
+
return [
|
|
4393
|
+
{ type: 'forms', granted: grants.forms || [], allowed: true },
|
|
4394
|
+
{ type: 'dataViews', granted: grants.dataViews || [], allowed: true },
|
|
4395
|
+
{ type: 'functions', granted: grants.functions || [], allowed: true },
|
|
4396
|
+
{ type: 'connectors', granted: grants.connectors || [], allowed: true },
|
|
4397
|
+
];
|
|
4398
|
+
}
|
|
4399
|
+
return checks;
|
|
4400
|
+
}
|
|
4401
|
+
|
|
4402
|
+
async function buildPermissionAudit(config, target, flags = {}) {
|
|
4403
|
+
const manifest = loadWorkspaceResources();
|
|
4404
|
+
const validation = validateWorkspaceResources(manifest);
|
|
4405
|
+
const roleCodes = new Set((manifest.roles || []).map(item => item.code || item.resourceCode).filter(Boolean));
|
|
4406
|
+
const warnings = [...validation.warnings];
|
|
4407
|
+
const errors = [...validation.errors];
|
|
4408
|
+
for (const group of manifest.pagePermissionGroups || []) {
|
|
4409
|
+
for (const roleCode of group.roles || []) {
|
|
4410
|
+
if (roleCodes.size > 0 && !roleCodes.has(roleCode)) warnings.push(`page permission group ${group.code} 引用未声明角色: ${roleCode}`);
|
|
4411
|
+
}
|
|
4412
|
+
}
|
|
4413
|
+
for (const group of manifest.formPermissionGroups || []) {
|
|
4414
|
+
for (const roleCode of group.roles || []) {
|
|
4415
|
+
if (roleCodes.size > 0 && !roleCodes.has(roleCode)) warnings.push(`form permission group ${group.code} 引用未声明角色: ${roleCode}`);
|
|
4416
|
+
}
|
|
4417
|
+
const formUuid = resolveManifestFormUuid(target.bound, group);
|
|
4418
|
+
if (!formUuid) errors.push(`form permission group ${group.code} 缺少 formCode/formUuid`);
|
|
4419
|
+
}
|
|
4420
|
+
for (const policy of manifest.publicAccessPolicies || []) {
|
|
4421
|
+
const grants = normalizePublicAccessPolicyManifest(target.bound, policy).grants || {};
|
|
4422
|
+
if (
|
|
4423
|
+
(grants.forms || []).length === 0 &&
|
|
4424
|
+
(grants.dataViews || []).length === 0 &&
|
|
4425
|
+
(grants.functions || []).length === 0 &&
|
|
4426
|
+
(grants.connectors || []).length === 0
|
|
4427
|
+
) {
|
|
4428
|
+
warnings.push(`public-access policy ${policy.code} 未声明任何 grants`);
|
|
4429
|
+
}
|
|
4430
|
+
}
|
|
4431
|
+
const result = {
|
|
4432
|
+
appType: target.appType,
|
|
4433
|
+
manifestCounts: Object.fromEntries(
|
|
4434
|
+
RESOURCE_SPECS.map(spec => [spec.key, (manifest[spec.key] || []).length])
|
|
4435
|
+
),
|
|
4436
|
+
errors,
|
|
4437
|
+
warnings,
|
|
4438
|
+
};
|
|
4439
|
+
if (flags.live) {
|
|
4440
|
+
result.live = {
|
|
4441
|
+
roles: await requestWithAuth(config, target.profileName, `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/roles`),
|
|
4442
|
+
pagePermissionGroups: await requestWithAuth(config, target.profileName, `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/page-permission-groups`),
|
|
4443
|
+
};
|
|
4444
|
+
}
|
|
4445
|
+
return result;
|
|
4446
|
+
}
|
|
4447
|
+
|
|
3368
4448
|
function ensureResourceBuckets(bound) {
|
|
3369
4449
|
bound.resources = bound.resources || {};
|
|
3370
4450
|
bound.resources.forms = bound.resources.forms || {};
|
|
@@ -7884,6 +8964,31 @@ function unwrapApi(payload) {
|
|
|
7884
8964
|
return 'data' in payload ? payload.data : payload;
|
|
7885
8965
|
}
|
|
7886
8966
|
|
|
8967
|
+
function unwrapStrictApi(payload) {
|
|
8968
|
+
if (!payload || typeof payload !== 'object') return payload;
|
|
8969
|
+
if (payload.success === false) {
|
|
8970
|
+
const error = new Error(payload.message || payload.errorMessage || 'OpenXiangda API request failed');
|
|
8971
|
+
error.apiCode = payload.code;
|
|
8972
|
+
throw error;
|
|
8973
|
+
}
|
|
8974
|
+
if ('code' in payload) {
|
|
8975
|
+
const code = payload.code;
|
|
8976
|
+
const numericCode = Number(code);
|
|
8977
|
+
const normalizedCode = String(code || '').toUpperCase();
|
|
8978
|
+
const isOk =
|
|
8979
|
+
numericCode === 0 ||
|
|
8980
|
+
(Number.isFinite(numericCode) && numericCode >= 200 && numericCode < 300) ||
|
|
8981
|
+
normalizedCode === 'OK' ||
|
|
8982
|
+
normalizedCode === 'SUCCESS';
|
|
8983
|
+
if (!isOk) {
|
|
8984
|
+
const error = new Error(payload.message || payload.errorMessage || String(code));
|
|
8985
|
+
error.apiCode = code;
|
|
8986
|
+
throw error;
|
|
8987
|
+
}
|
|
8988
|
+
}
|
|
8989
|
+
return 'data' in payload ? payload.data : payload;
|
|
8990
|
+
}
|
|
8991
|
+
|
|
7887
8992
|
function isUnauthorized(error) {
|
|
7888
8993
|
return (
|
|
7889
8994
|
Number(error?.apiCode) === 401 ||
|
|
@@ -7897,23 +9002,24 @@ async function requestWithAuth(config, profileName, apiPath, options = {}) {
|
|
|
7897
9002
|
if (!profile.token?.accessToken) {
|
|
7898
9003
|
fail(`profile ${resolved.profileName} 未登录,请先执行 openxiangda login --profile ${resolved.profileName}`);
|
|
7899
9004
|
}
|
|
9005
|
+
const { strictEnvelope, ...requestOptions } = options;
|
|
7900
9006
|
|
|
7901
9007
|
try {
|
|
7902
9008
|
const payload = await requestJson(profile.baseUrl, apiPath, {
|
|
7903
|
-
...
|
|
9009
|
+
...requestOptions,
|
|
7904
9010
|
accessToken: profile.token.accessToken,
|
|
7905
9011
|
});
|
|
7906
|
-
return unwrapApi(payload);
|
|
9012
|
+
return strictEnvelope ? unwrapStrictApi(payload) : unwrapApi(payload);
|
|
7907
9013
|
} catch (error) {
|
|
7908
9014
|
if (!isUnauthorized(error) || !profile.token?.refreshToken) {
|
|
7909
9015
|
throw error;
|
|
7910
9016
|
}
|
|
7911
9017
|
await refreshProfile(config, resolved.profileName);
|
|
7912
9018
|
const payload = await requestJson(profile.baseUrl, apiPath, {
|
|
7913
|
-
...
|
|
9019
|
+
...requestOptions,
|
|
7914
9020
|
accessToken: profile.token.accessToken,
|
|
7915
9021
|
});
|
|
7916
|
-
return unwrapApi(payload);
|
|
9022
|
+
return strictEnvelope ? unwrapStrictApi(payload) : unwrapApi(payload);
|
|
7917
9023
|
}
|
|
7918
9024
|
}
|
|
7919
9025
|
|