openxiangda 1.0.92 → 1.0.94

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/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 permission role-list|role-create|role-bind
117
- openxiangda permission page-group-list|page-group-create|page-group-bind
118
- openxiangda permission form-group-list|form-group-create|form-group-bind
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
  ]);
@@ -212,6 +237,10 @@ async function update(args) {
212
237
  const requestedSubcommand = args[0] && !args[0].startsWith('--') ? args[0] : 'check';
213
238
  const parsedArgs = requestedSubcommand === args[0] ? args.slice(1) : args;
214
239
  const { flags } = parseArgs(parsedArgs);
240
+ if (wantsSubcommandHelp(requestedSubcommand, flags)) {
241
+ print('用法: openxiangda update check|install [--json] [--registry https://registry.npmjs.org] [--no-skills]');
242
+ return;
243
+ }
215
244
  const registry = normalizeNpmRegistry(flags.registry || OFFICIAL_NPM_REGISTRY);
216
245
 
217
246
  if (requestedSubcommand === 'check') {
@@ -401,9 +430,165 @@ function parseSemver(value) {
401
430
  };
402
431
  }
403
432
 
433
+ async function design(args) {
434
+ const { subcommand, rest } = parseSubcommandArgs(args);
435
+ const { flags, positional } = parseArgs(rest);
436
+ const topic = flags.topic || positional[0];
437
+ if (wantsSubcommandHelp(subcommand, flags)) {
438
+ print('用法: openxiangda design gates|template [--topic new-app,public-access] [--json]');
439
+ return;
440
+ }
441
+
442
+ if (subcommand === 'gates') {
443
+ const result = getDesignGates(topic);
444
+ if (flags.json) return writeJson(result);
445
+ print(renderDesignGatesText(topic));
446
+ return;
447
+ }
448
+
449
+ if (subcommand === 'template') {
450
+ const result = {
451
+ topic: topic || 'all',
452
+ content: renderDesignTemplate(topic),
453
+ };
454
+ if (flags.json) return writeJson(result);
455
+ print(result.content);
456
+ return;
457
+ }
458
+
459
+ fail('用法: openxiangda design gates|template [--topic code] [--json]');
460
+ }
461
+
462
+ async function doctor(args) {
463
+ const { flags } = parseArgs(args);
464
+ if (flags.help || flags.h) {
465
+ print('用法: openxiangda doctor [--profile name] [--app-type APP_XXX] [--agent codex] [--json]');
466
+ return;
467
+ }
468
+ const config = loadConfig();
469
+ const profileName = flags.profile || config.currentProfile;
470
+ const result = {
471
+ cli: {
472
+ version: CURRENT_VERSION,
473
+ commandDiscovery: 'openxiangda commands --json',
474
+ designGateCommand: 'openxiangda design gates --json',
475
+ },
476
+ profile: {
477
+ requested: profileName || null,
478
+ exists: false,
479
+ baseUrl: null,
480
+ loggedIn: false,
481
+ user: null,
482
+ },
483
+ workspace: {
484
+ cwd: process.cwd(),
485
+ stateFile: path.join(process.cwd(), PROJECT_STATE_FILE),
486
+ hasState: fs.existsSync(path.join(process.cwd(), PROJECT_STATE_FILE)),
487
+ appType: flags['app-type'] || null,
488
+ bound: false,
489
+ },
490
+ resources: {
491
+ hasDirectory: fs.existsSync(path.join(process.cwd(), 'src', 'resources')),
492
+ validation: null,
493
+ },
494
+ skills: null,
495
+ checks: [],
496
+ };
497
+
498
+ if (profileName && config.profiles?.[profileName]) {
499
+ const { profile } = getProfile(config, profileName);
500
+ result.profile.exists = true;
501
+ result.profile.baseUrl = profile.baseUrl || null;
502
+ result.profile.loggedIn = Boolean(profile.token?.accessToken);
503
+ result.profile.user = profile.user || null;
504
+ try {
505
+ const target = getWorkspaceTarget(config, profileName, flags);
506
+ result.workspace.appType = target.appType;
507
+ result.workspace.bound = true;
508
+ result.workspace.profile = target.profileName;
509
+ result.workspace.boundAt = target.bound.updatedAt || null;
510
+ result.checks.push({ name: 'workspace-binding', status: 'ok' });
511
+ } catch (error) {
512
+ result.checks.push({ name: 'workspace-binding', status: 'warn', message: error.message });
513
+ }
514
+ if (result.profile.loggedIn) {
515
+ try {
516
+ const authStatus = await requestWithAuth(config, profileName, '/openxiangda-api/v1/auth/whoami');
517
+ result.profile.authStatus = authStatus;
518
+ result.checks.push({ name: 'auth', status: 'ok' });
519
+ } catch (error) {
520
+ result.profile.authError = error.message;
521
+ result.checks.push({ name: 'auth', status: 'warn', message: error.message });
522
+ }
523
+ } else {
524
+ result.checks.push({ name: 'auth', status: 'warn', message: 'profile 未登录' });
525
+ }
526
+ } else {
527
+ result.checks.push({ name: 'profile', status: 'warn', message: '未选择或不存在 profile' });
528
+ }
529
+
530
+ if (result.resources.hasDirectory) {
531
+ const manifest = loadWorkspaceResources();
532
+ const validation = validateWorkspaceResources(manifest);
533
+ result.resources.validation = {
534
+ errors: validation.errors,
535
+ warnings: validation.warnings,
536
+ counts: Object.fromEntries(
537
+ RESOURCE_SPECS.map(spec => [spec.key, (manifest[spec.key] || []).length])
538
+ ),
539
+ };
540
+ result.checks.push({
541
+ name: 'resource-validate',
542
+ status: validation.errors.length > 0 ? 'error' : 'ok',
543
+ errors: validation.errors.length,
544
+ warnings: validation.warnings.length,
545
+ });
546
+ } else {
547
+ result.checks.push({ name: 'resource-validate', status: 'skip', message: '未发现 src/resources' });
548
+ }
549
+
550
+ result.skills = getSkillStatusReport({ agent: flags.agent || 'codex' });
551
+ const outdatedSkills = result.skills.results
552
+ .flatMap(item => item.skills)
553
+ .filter(item => item.status !== 'installed');
554
+ result.checks.push({
555
+ name: 'skills',
556
+ status: outdatedSkills.length > 0 ? 'warn' : 'ok',
557
+ message: outdatedSkills.length > 0 ? `${outdatedSkills.length} 个 skill 未安装或过期` : undefined,
558
+ });
559
+
560
+ if (flags.json) return writeJson(result);
561
+ printDoctorReport(result);
562
+ }
563
+
564
+ function printDoctorReport(result) {
565
+ const lines = [
566
+ `OpenXiangda doctor v${result.cli.version}`,
567
+ `profile: ${result.profile.requested || '(none)'} ${result.profile.loggedIn ? '(logged in)' : '(not logged in)'}`,
568
+ `baseUrl: ${result.profile.baseUrl || '(none)'}`,
569
+ `workspace: ${result.workspace.bound ? `${result.workspace.profile}/${result.workspace.appType}` : 'not bound'}`,
570
+ `resources: ${result.resources.hasDirectory ? 'src/resources found' : 'missing'}`,
571
+ ];
572
+ if (result.resources.validation) {
573
+ lines.push(
574
+ `resource validation: ${result.resources.validation.errors.length} errors, ${result.resources.validation.warnings.length} warnings`
575
+ );
576
+ }
577
+ for (const check of result.checks) {
578
+ const suffix = check.message ? ` - ${check.message}` : '';
579
+ lines.push(`- ${check.name}: ${check.status}${suffix}`);
580
+ }
581
+ lines.push(`design gate: ${result.cli.designGateCommand}`);
582
+ print(lines.join('\n'));
583
+ }
584
+
404
585
  async function platform(args) {
405
586
  const [subcommand, ...rest] = args;
406
587
  const { flags, positional } = parseArgs(rest);
588
+ if (wantsSubcommandHelp(subcommand, flags)) {
589
+ print('用法: openxiangda platform add|list|use|remove');
590
+ return;
591
+ }
407
592
  const config = loadConfig();
408
593
 
409
594
  if (subcommand === 'add') {
@@ -479,6 +664,16 @@ async function platform(args) {
479
664
 
480
665
  async function login(args) {
481
666
  const { flags, positional } = parseArgs(args);
667
+ if (
668
+ flags.help ||
669
+ flags.h ||
670
+ positional[0] === 'help' ||
671
+ positional[0] === '--help' ||
672
+ positional[0] === '-h'
673
+ ) {
674
+ print('用法: openxiangda login <platform-url> [--profile name] [--no-open] [--json]');
675
+ return;
676
+ }
482
677
  const config = loadConfig();
483
678
  const rawUrl = positional[0];
484
679
  const profileName = flags.profile || config.currentProfile || 'default';
@@ -592,6 +787,10 @@ function normalizePlatformSessionUrl(value, baseUrl) {
592
787
  async function auth(args) {
593
788
  const [subcommand, ...rest] = args;
594
789
  const { flags } = parseArgs(rest);
790
+ if (wantsSubcommandHelp(subcommand, flags)) {
791
+ print('用法: openxiangda auth status|refresh|logout [--profile name] [--json]');
792
+ return;
793
+ }
595
794
 
596
795
  if (subcommand === 'status') {
597
796
  const config = loadConfig();
@@ -646,6 +845,10 @@ async function auth(args) {
646
845
 
647
846
  async function env(args) {
648
847
  const { flags } = parseArgs(args);
848
+ if (flags.help || flags.h) {
849
+ print('用法: openxiangda env [--profile name] [--json]');
850
+ return;
851
+ }
649
852
  const config = loadConfig();
650
853
  const globalEnv = loadGlobalEnv();
651
854
  const profileName = flags.profile || config.currentProfile;
@@ -683,6 +886,12 @@ async function env(args) {
683
886
  async function feedback(args) {
684
887
  const [subcommand, ...rest] = args;
685
888
  const { flags, positional } = parseArgs(rest);
889
+ if (wantsSubcommandHelp(subcommand, flags)) {
890
+ print(
891
+ '用法: openxiangda feedback preview|submit --summary <text> [--type bug] [--severity medium] [--profile name] [--yes] [--json]'
892
+ );
893
+ return;
894
+ }
686
895
 
687
896
  if (subcommand !== 'preview' && subcommand !== 'submit') {
688
897
  fail(
@@ -1106,6 +1315,10 @@ function summarizeOssEnv(globalEnv) {
1106
1315
  async function workspace(args) {
1107
1316
  const [subcommand, ...rest] = args;
1108
1317
  const { flags, positional } = parseArgs(rest);
1318
+ if (wantsSubcommandHelp(subcommand, flags)) {
1319
+ print('用法: openxiangda workspace init|bind|publish [--changed [--since ref]|--form code|--page code|--only list] [--dry-run] [--force] [--resources|--skip-resources]');
1320
+ return;
1321
+ }
1109
1322
  const config = loadConfig();
1110
1323
 
1111
1324
  if (subcommand === 'init') {
@@ -1238,6 +1451,10 @@ async function workspace(args) {
1238
1451
  async function app(args) {
1239
1452
  const [subcommand, ...rest] = args;
1240
1453
  const { flags, positional } = parseArgs(rest);
1454
+ if (wantsSubcommandHelp(subcommand, flags)) {
1455
+ print('用法: openxiangda app list|create|snapshot [--profile name] [--json]');
1456
+ return;
1457
+ }
1241
1458
  const config = loadConfig();
1242
1459
  const profileName = flags.profile || config.currentProfile;
1243
1460
 
@@ -1308,6 +1525,10 @@ function extractCreatedAppType(data) {
1308
1525
  async function form(args) {
1309
1526
  const [subcommand, ...rest] = args;
1310
1527
  const { flags, positional } = parseArgs(rest);
1528
+ if (wantsSubcommandHelp(subcommand, flags)) {
1529
+ print('用法: openxiangda form list|create|bind|pull|publish [--profile name] [--json]');
1530
+ return;
1531
+ }
1311
1532
  const config = loadConfig();
1312
1533
  const profileName = flags.profile || config.currentProfile;
1313
1534
 
@@ -1416,6 +1637,10 @@ async function form(args) {
1416
1637
  async function page(args) {
1417
1638
  const [subcommand, ...rest] = args;
1418
1639
  const { flags, positional } = parseArgs(rest);
1640
+ if (wantsSubcommandHelp(subcommand, flags)) {
1641
+ print('用法: openxiangda page list|publish|bind|releases|activate [--profile name] [--json]');
1642
+ return;
1643
+ }
1419
1644
  const config = loadConfig();
1420
1645
  const profileName = flags.profile || config.currentProfile;
1421
1646
 
@@ -1536,6 +1761,10 @@ async function page(args) {
1536
1761
  async function menu(args) {
1537
1762
  const [subcommand, ...rest] = args;
1538
1763
  const { flags, positional } = parseArgs(rest);
1764
+ if (wantsSubcommandHelp(subcommand, flags)) {
1765
+ print('用法: openxiangda menu list|create|update|sort|bind|delete [--profile name] [--json]');
1766
+ return;
1767
+ }
1539
1768
  const config = loadConfig();
1540
1769
  const profileName = flags.profile || config.currentProfile;
1541
1770
 
@@ -1626,7 +1855,46 @@ async function menu(args) {
1626
1855
  return;
1627
1856
  }
1628
1857
 
1629
- fail('用法: openxiangda menu list|create|bind|delete');
1858
+ if (subcommand === 'update') {
1859
+ const [menuKey] = positional;
1860
+ const body = readDirectJsonBody(flags, 'menu update');
1861
+ const menuCode = menuKey || body.code || body.resourceCode;
1862
+ if (!menuCode) fail('用法: openxiangda menu update <menuCode|menuId> --json-file file');
1863
+ const target = getWorkspaceTarget(config, profileName, flags);
1864
+ const menuId = resolveMenuId(target.bound, menuCode, flags);
1865
+ const desired = normalizeMenuDirectBody(target.bound, { ...body, code: body.code || menuCode });
1866
+ const data = await runDirectRequest(config, target, flags, {
1867
+ method: 'PUT',
1868
+ path: `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/menus/${encodeURIComponent(menuId)}`,
1869
+ body: desired,
1870
+ }, { returnData: true });
1871
+ if (!flags['dry-run']) {
1872
+ const resultCode = body.code || body.resourceCode || menuCode;
1873
+ if (data?.id) {
1874
+ saveMenuResource(target, resultCode, data.id, {
1875
+ formUuid: data.formUuid || desired.formUuid,
1876
+ pageId: data.pageId || desired.pageId,
1877
+ parentId: data.parentId || desired.parentId,
1878
+ routeCode: data.routeCode || desired.routeCode,
1879
+ path: data.path || desired.path,
1880
+ });
1881
+ }
1882
+ if (flags['write-manifest']) writeDirectManifest('menu', resultCode, { ...body, code: resultCode });
1883
+ }
1884
+ return outputDirectResult(data, flags);
1885
+ }
1886
+
1887
+ if (subcommand === 'sort') {
1888
+ const target = getWorkspaceTarget(config, profileName, flags);
1889
+ const body = readDirectJsonBody(flags, 'menu sort');
1890
+ return runDirectRequest(config, target, flags, {
1891
+ method: 'POST',
1892
+ path: `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/menus/sort`,
1893
+ body,
1894
+ });
1895
+ }
1896
+
1897
+ fail('用法: openxiangda menu list|create|update|sort|bind|delete');
1630
1898
  }
1631
1899
 
1632
1900
  async function workflow(args) {
@@ -2093,7 +2361,7 @@ async function dataView(args) {
2093
2361
  const { subcommand, rest } = parseSubcommandArgs(args);
2094
2362
  const { flags, positional } = parseArgs(rest);
2095
2363
  if (wantsSubcommandHelp(subcommand, flags)) {
2096
- print('用法: openxiangda data-view list|status|refresh|query|stats <dataViewCode> [--profile name] [--json]');
2364
+ print('用法: openxiangda data-view list|get|create|update|upsert|delete|status|refresh|query|stats <dataViewCode> [--profile name] [--json]');
2097
2365
  return;
2098
2366
  }
2099
2367
  const config = loadConfig();
@@ -2176,12 +2444,418 @@ async function dataView(args) {
2176
2444
  return;
2177
2445
  }
2178
2446
 
2179
- fail('用法: openxiangda data-view list|status|refresh|query|stats');
2447
+ if (['get', 'create', 'update', 'upsert', 'delete'].includes(subcommand)) {
2448
+ return directResourceCrud(config, target, {
2449
+ commandName: 'data-view',
2450
+ subcommand,
2451
+ flags,
2452
+ positional,
2453
+ resourceType: 'data-view',
2454
+ basePath: `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/data-views`,
2455
+ manifestDir: path.join('src', 'resources', 'data-views'),
2456
+ normalizeBody: body => normalizeDataViewManifest(target.bound, body),
2457
+ codeOf: body => body.code || body.resourceCode || body.definition?.code,
2458
+ saveState: (code, data) => saveDataViewResource(target, code, data?.id, {
2459
+ materializedViewName: data?.materializedViewName,
2460
+ status: data?.status,
2461
+ storageMode: data?.storageMode,
2462
+ }),
2463
+ });
2464
+ }
2465
+
2466
+ fail('用法: openxiangda data-view list|get|create|update|upsert|delete|status|refresh|query|stats');
2467
+ }
2468
+
2469
+ async function route(args) {
2470
+ const { subcommand, rest } = parseSubcommandArgs(args);
2471
+ const { flags, positional } = parseArgs(rest);
2472
+ if (wantsSubcommandHelp(subcommand, flags)) {
2473
+ print('用法: openxiangda route list|get|create|update|upsert|delete [routeCode] [--json-file file] [--dry-run] [--write-manifest]');
2474
+ return;
2475
+ }
2476
+ const config = loadConfig();
2477
+ const target = getWorkspaceTarget(config, flags.profile || config.currentProfile, flags);
2478
+ return directResourceCrud(config, target, {
2479
+ commandName: 'route',
2480
+ subcommand,
2481
+ flags,
2482
+ positional,
2483
+ resourceType: 'route',
2484
+ basePath: `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/routes`,
2485
+ manifestDir: path.join('src', 'resources', 'routes'),
2486
+ normalizeBody: body => normalizeRouteManifest(body),
2487
+ codeOf: body => body.code || body.resourceCode,
2488
+ saveState: (code, data) => saveRouteResource(target, code, data?.id, {
2489
+ pathPattern: data?.pathPattern,
2490
+ publicAccess: data?.publicAccess,
2491
+ publicPolicyCode: data?.publicPolicyCode,
2492
+ }),
2493
+ });
2494
+ }
2495
+
2496
+ async function publicAccess(args) {
2497
+ const { subcommand, rest } = parseSubcommandArgs(args);
2498
+ const { flags, positional } = parseArgs(rest);
2499
+ if (wantsSubcommandHelp(subcommand, flags)) {
2500
+ print('用法: openxiangda public-access list|get|create|update|upsert|delete|ticket-create|session-test|grant-check [policyCode] [--json-file file]');
2501
+ return;
2502
+ }
2503
+ const config = loadConfig();
2504
+ const target = getWorkspaceTarget(config, flags.profile || config.currentProfile, flags);
2505
+
2506
+ if (['list', 'get', 'create', 'update', 'upsert', 'delete'].includes(subcommand)) {
2507
+ return directResourceCrud(config, target, {
2508
+ commandName: 'public-access',
2509
+ subcommand,
2510
+ flags,
2511
+ positional,
2512
+ resourceType: 'public-access',
2513
+ basePath: `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/public-access/policies`,
2514
+ manifestDir: path.join('src', 'resources', 'public-access'),
2515
+ normalizeBody: body => normalizePublicAccessPolicyManifest(target.bound, body),
2516
+ codeOf: body => body.code || body.resourceCode,
2517
+ saveState: (code, data) => savePublicAccessPolicyResource(target, code, data?.id, {
2518
+ mode: data?.mode,
2519
+ routeCode: data?.routeCode,
2520
+ pathPattern: data?.pathPattern,
2521
+ }),
2522
+ });
2523
+ }
2524
+
2525
+ if (subcommand === 'ticket-create') {
2526
+ const [policyCode] = positional;
2527
+ if (!policyCode) fail('用法: openxiangda public-access ticket-create <policyCode> [--json-file file]');
2528
+ const body = readDirectJsonBody(flags, 'ticket');
2529
+ return runDirectRequest(config, target, flags, {
2530
+ method: 'POST',
2531
+ path: `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/public-access/policies/${encodeURIComponent(policyCode)}/tickets`,
2532
+ body,
2533
+ strictEnvelope: true,
2534
+ });
2535
+ }
2536
+
2537
+ if (subcommand === 'session-test') {
2538
+ const [policyCode] = positional;
2539
+ const body = {
2540
+ ...readDirectJsonBody(flags, 'public-session', { optional: true }),
2541
+ ...(policyCode ? { policyCode } : {}),
2542
+ ...(flags.path ? { path: flags.path } : {}),
2543
+ ...(flags.ticket ? { ticket: flags.ticket } : {}),
2544
+ };
2545
+ return runDirectRequest(config, target, flags, {
2546
+ method: 'POST',
2547
+ path: `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/public/session`,
2548
+ body,
2549
+ auth: false,
2550
+ strictEnvelope: true,
2551
+ });
2552
+ }
2553
+
2554
+ if (subcommand === 'grant-check') {
2555
+ const [policyCode] = positional;
2556
+ if (!policyCode) fail('用法: openxiangda public-access grant-check <policyCode> [--form-code code] [--data-view code] [--function code] [--connector code]');
2557
+ const policy = flags['json-file']
2558
+ ? readDirectJsonBody(flags, 'policy')
2559
+ : await requestWithAuth(
2560
+ config,
2561
+ target.profileName,
2562
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/public-access/policies/${encodeURIComponent(policyCode)}`
2563
+ );
2564
+ const normalized = normalizePublicAccessPolicyManifest(target.bound, policy);
2565
+ const grants = normalized.grants || {};
2566
+ const checks = buildPublicGrantChecks(target.bound, grants, flags);
2567
+ const result = { policyCode, grants, checks, allowed: checks.every(item => item.allowed) };
2568
+ if (flags.json) return writeJson(result);
2569
+ print(JSON.stringify(result, null, 2));
2570
+ if (!result.allowed) fail('公开访问 grant-check 失败');
2571
+ return;
2572
+ }
2573
+
2574
+ fail('用法: openxiangda public-access list|get|create|update|upsert|delete|ticket-create|session-test|grant-check');
2575
+ }
2576
+
2577
+ async function authConfig(args) {
2578
+ const { subcommand, rest } = parseSubcommandArgs(args);
2579
+ const { flags, positional } = parseArgs(rest);
2580
+ if (wantsSubcommandHelp(subcommand, flags)) {
2581
+ print('用法: openxiangda auth-config list|get|create|update|upsert|delete|methods [configCode] [--json-file file]');
2582
+ return;
2583
+ }
2584
+ if (subcommand === 'methods') {
2585
+ const result = {
2586
+ methods: [
2587
+ { type: 'password', requiredFields: ['username', 'password'] },
2588
+ { type: 'phone_code', requiredFields: ['phone', 'code'], provider: 'functionCode 或 connector' },
2589
+ { type: 'sso', requiredFields: ['provider', 'callbackUrl'] },
2590
+ ],
2591
+ defaultRegistration: { mode: 'reject' },
2592
+ defaultBinding: { mode: 'auto' },
2593
+ };
2594
+ if (flags.json) return writeJson(result);
2595
+ print(JSON.stringify(result, null, 2));
2596
+ return;
2597
+ }
2598
+ const config = loadConfig();
2599
+ const target = getWorkspaceTarget(config, flags.profile || config.currentProfile, flags);
2600
+ return directResourceCrud(config, target, {
2601
+ commandName: 'auth-config',
2602
+ subcommand,
2603
+ flags,
2604
+ positional,
2605
+ resourceType: 'auth-config',
2606
+ basePath: `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/auth/configs`,
2607
+ manifestDir: path.join('src', 'resources', 'auth'),
2608
+ normalizeBody: body => normalizeAuthConfigManifest(body),
2609
+ codeOf: body => body.code || body.resourceCode || 'default',
2610
+ saveState: (code, data) => saveAuthConfigResource(target, code, data?.id, {
2611
+ status: data?.status,
2612
+ }),
2613
+ });
2614
+ }
2615
+
2616
+ async function appFunction(args) {
2617
+ const { subcommand, rest } = parseSubcommandArgs(args);
2618
+ const { flags, positional } = parseArgs(rest);
2619
+ if (wantsSubcommandHelp(subcommand, flags)) {
2620
+ print('用法: openxiangda function list|get|create|update|upsert|delete|invoke [functionCode] [--json-file file]');
2621
+ return;
2622
+ }
2623
+ const config = loadConfig();
2624
+ const target = getWorkspaceTarget(config, flags.profile || config.currentProfile, flags);
2625
+
2626
+ if (subcommand === 'invoke') {
2627
+ const [functionCode] = positional;
2628
+ if (!functionCode) fail('用法: openxiangda function invoke <functionCode> [--body-json file|json]');
2629
+ const body = readDirectJsonBody(flags, 'function invoke', { optional: true });
2630
+ return runDirectRequest(config, target, flags, {
2631
+ method: 'POST',
2632
+ path: `/${encodeURIComponent(target.appType)}/v1/functions/${encodeURIComponent(functionCode)}/invoke.json`,
2633
+ body,
2634
+ strictEnvelope: true,
2635
+ });
2636
+ }
2637
+
2638
+ return directResourceCrud(config, target, {
2639
+ commandName: 'function',
2640
+ subcommand,
2641
+ flags,
2642
+ positional,
2643
+ resourceType: 'function',
2644
+ basePath: `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/functions`,
2645
+ manifestDir: path.join('src', 'resources', 'functions'),
2646
+ prepareBody: async body => {
2647
+ const definitionJson = await resolveManifestJson(
2648
+ config,
2649
+ target.profileName,
2650
+ body,
2651
+ 'definitionJson',
2652
+ 'definitionFile'
2653
+ );
2654
+ applyResourceBindingsToRuntimeDefinition(target.bound, body, definitionJson);
2655
+ const code = body.code || body.functionCode || body.resourceCode;
2656
+ return stripUndefinedValues({
2657
+ code,
2658
+ name: body.name || definitionJson.name || code,
2659
+ description: body.description !== undefined ? body.description : definitionJson.description || '',
2660
+ definitionJson,
2661
+ resourceBindings: definitionJson.resourceBindings,
2662
+ inputSchema: body.inputSchema || definitionJson.inputSchema,
2663
+ outputSchema: body.outputSchema || definitionJson.outputSchema,
2664
+ status: body.status,
2665
+ });
2666
+ },
2667
+ codeOf: body => body.code || body.functionCode || body.resourceCode,
2668
+ saveState: (code, data) => saveFunctionResource(target, code, data?.id, {
2669
+ status: data?.status,
2670
+ }),
2671
+ });
2672
+ }
2673
+
2674
+ async function connector(args) {
2675
+ const { subcommand, rest } = parseSubcommandArgs(args);
2676
+ const { flags, positional } = parseArgs(rest);
2677
+ if (wantsSubcommandHelp(subcommand, flags)) {
2678
+ print('用法: openxiangda connector list|get|create|update|upsert|delete|invoke|download-test [connectorCode[.apiCode]] [--json-file file]');
2679
+ return;
2680
+ }
2681
+ const config = loadConfig();
2682
+ const target = getWorkspaceTarget(config, flags.profile || config.currentProfile, flags);
2683
+
2684
+ if (subcommand === 'invoke' || subcommand === 'download-test') {
2685
+ const targetName = positional[0] || flags.connector;
2686
+ if (!targetName) fail(`用法: openxiangda connector ${subcommand} <connectorCode.apiCode> [--body-json file|json]`);
2687
+ const parsed = parseConnectorApiName(targetName, flags);
2688
+ const body = {
2689
+ connector: parsed.connector,
2690
+ api: parsed.api,
2691
+ ...readDirectJsonBody(flags, 'connector invoke', { optional: true }),
2692
+ ...(flags['path-params-json'] ? { pathParams: readJsonArg(flags['path-params-json'], 'path-params-json') } : {}),
2693
+ ...(flags['query-json'] ? { query: readJsonArg(flags['query-json'], 'query-json') } : {}),
2694
+ ...(flags['headers-json'] ? { headers: readJsonArg(flags['headers-json'], 'headers-json') } : {}),
2695
+ ...(flags['request-body-type'] ? { requestBodyType: flags['request-body-type'] } : {}),
2696
+ ...(flags['response-type'] ? { responseType: flags['response-type'] } : {}),
2697
+ };
2698
+ if (subcommand === 'download-test') body.responseType = 'binary';
2699
+ return runDirectRequest(config, target, flags, {
2700
+ method: 'POST',
2701
+ path: `/${encodeURIComponent(target.appType)}/v1/connectors/actions/${subcommand === 'download-test' ? 'download' : 'invoke'}`,
2702
+ body,
2703
+ strictEnvelope: subcommand !== 'download-test',
2704
+ });
2705
+ }
2706
+
2707
+ return directResourceCrud(config, target, {
2708
+ commandName: 'connector',
2709
+ subcommand,
2710
+ flags,
2711
+ positional,
2712
+ resourceType: 'connector',
2713
+ basePath: `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/connectors`,
2714
+ manifestDir: path.join('src', 'resources', 'connectors'),
2715
+ createMethod: 'POST',
2716
+ createPath: `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/connectors/actions/sync`,
2717
+ createBody: body => ({ connectors: [normalizeConnectorManifest(body)] }),
2718
+ updateMethod: 'POST',
2719
+ updatePath: `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/connectors/actions/sync`,
2720
+ updateBody: body => ({ connectors: [normalizeConnectorManifest(body)] }),
2721
+ normalizeBody: body => normalizeConnectorManifest(body),
2722
+ codeOf: body => body.code || body.methodName,
2723
+ responseData: data => Array.isArray(data?.data) ? data.data[0] : data,
2724
+ saveState: (code, data) => saveConnectorResource(target, code, data?.connector?.id || data?.id, {
2725
+ apis: Object.fromEntries(
2726
+ (data?.apis || []).map(api => [api.code || api.methodName, { apiId: api.id, name: api.name }])
2727
+ ),
2728
+ }),
2729
+ });
2730
+ }
2731
+
2732
+ async function notification(args) {
2733
+ const { subcommand, rest } = parseSubcommandArgs(args);
2734
+ const { flags, positional } = parseArgs(rest);
2735
+ if (wantsSubcommandHelp(subcommand, flags)) {
2736
+ print('用法: openxiangda notification template-list|template-get|template-upsert|template-delete|type-list|type-get|type-upsert|type-delete|preview|send|batch-send');
2737
+ return;
2738
+ }
2739
+ const config = loadConfig();
2740
+ const target = getWorkspaceTarget(config, flags.profile || config.currentProfile, flags);
2741
+ const appPrefix = `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/notifications`;
2742
+
2743
+ if (subcommand === 'template-list') {
2744
+ return runDirectRequest(config, target, flags, {
2745
+ method: 'GET',
2746
+ path: apiPathWithQuery(`${appPrefix}/templates`, { page: flags.page, pageSize: flags['page-size'] || flags.limit }),
2747
+ });
2748
+ }
2749
+ if (subcommand === 'template-get') {
2750
+ const code = positional[0] || flags.code;
2751
+ if (!code) fail('用法: openxiangda notification template-get <templateCode>');
2752
+ return runDirectRequest(config, target, flags, {
2753
+ method: 'GET',
2754
+ path: `${appPrefix}/templates/${encodeURIComponent(code)}`,
2755
+ });
2756
+ }
2757
+ if (subcommand === 'template-upsert') {
2758
+ const body = readDirectJsonBody(flags, 'notification template');
2759
+ const template = extractNotificationTemplateBody(target.bound, body);
2760
+ const code = positional[0] || body.code || body.templateCode || template.code;
2761
+ if (!code) fail('notification template-upsert 缺少 template code');
2762
+ const normalized = normalizeNotificationTemplateManifest(target.bound, { ...body, ...template, code });
2763
+ const data = await runDirectRequest(config, target, flags, {
2764
+ method: 'PUT',
2765
+ path: `${appPrefix}/templates/${encodeURIComponent(code)}`,
2766
+ body: normalized,
2767
+ }, { returnData: true });
2768
+ if (!flags['dry-run'] && data?.id) {
2769
+ saveNotificationTemplateResource(target, code, data.id, {
2770
+ level: data.level || normalized.level,
2771
+ formUuid: data.formUuid || normalized.formUuid,
2772
+ });
2773
+ if (flags['write-manifest']) writeDirectManifest('notification', code, { ...body, code, resourceType: 'template' });
2774
+ }
2775
+ return outputDirectResult(data, flags);
2776
+ }
2777
+ if (subcommand === 'template-delete') {
2778
+ const code = positional[0] || flags.code;
2779
+ if (!code || !flags.force) fail('用法: openxiangda notification template-delete <templateCode> --force');
2780
+ return runDirectRequest(config, target, flags, {
2781
+ method: 'DELETE',
2782
+ path: `${appPrefix}/templates/${encodeURIComponent(code)}`,
2783
+ });
2784
+ }
2785
+ if (subcommand === 'type-list') {
2786
+ return runDirectRequest(config, target, flags, {
2787
+ method: 'GET',
2788
+ path: apiPathWithQuery(`${appPrefix}/type-configs`, { page: flags.page, pageSize: flags['page-size'] || flags.limit }),
2789
+ });
2790
+ }
2791
+ if (subcommand === 'type-get') {
2792
+ const code = positional[0] || flags.code;
2793
+ if (!code) fail('用法: openxiangda notification type-get <notificationType>');
2794
+ return runDirectRequest(config, target, flags, {
2795
+ method: 'GET',
2796
+ path: `${appPrefix}/type-configs/${encodeURIComponent(code)}`,
2797
+ });
2798
+ }
2799
+ if (subcommand === 'type-upsert') {
2800
+ const body = readDirectJsonBody(flags, 'notification type-config');
2801
+ const configBody = extractNotificationTypeConfigBody(target.bound, body);
2802
+ const code = positional[0] || body.notificationType || configBody.notificationType;
2803
+ if (!code) fail('notification type-upsert 缺少 notificationType');
2804
+ const normalized = normalizeNotificationTypeConfigManifest(target.bound, { ...body, ...configBody, notificationType: code });
2805
+ const data = await runDirectRequest(config, target, flags, {
2806
+ method: 'PUT',
2807
+ path: `${appPrefix}/type-configs/${encodeURIComponent(code)}`,
2808
+ body: normalized,
2809
+ }, { returnData: true });
2810
+ if (!flags['dry-run'] && data?.id) {
2811
+ saveNotificationTypeConfigResource(target, body.code || code, data.id, {
2812
+ notificationType: data.notificationType || code,
2813
+ level: data.level || normalized.level,
2814
+ formUuid: data.formUuid || normalized.formUuid,
2815
+ templateId: data.templateId,
2816
+ templateCode: data.template?.code || body.templateCode,
2817
+ });
2818
+ if (flags['write-manifest']) writeDirectManifest('notification', body.code || code, { ...body, notificationType: code, resourceType: 'typeConfig' });
2819
+ }
2820
+ return outputDirectResult(data, flags);
2821
+ }
2822
+ if (subcommand === 'type-delete') {
2823
+ const code = positional[0] || flags.code;
2824
+ if (!code || !flags.force) fail('用法: openxiangda notification type-delete <notificationType> --force');
2825
+ return runDirectRequest(config, target, flags, {
2826
+ method: 'DELETE',
2827
+ path: `${appPrefix}/type-configs/${encodeURIComponent(code)}`,
2828
+ });
2829
+ }
2830
+ if (['preview', 'send', 'batch-send'].includes(subcommand)) {
2831
+ if ((subcommand === 'send' || subcommand === 'batch-send') && !flags.force) {
2832
+ fail(`openxiangda notification ${subcommand} 是发送动作,必须加 --force`);
2833
+ }
2834
+ const code = positional[0] || flags.code;
2835
+ const body = {
2836
+ ...readDirectJsonBody(flags, `notification ${subcommand}`, { optional: true }),
2837
+ ...(code ? { templateCode: code, notificationType: code } : {}),
2838
+ };
2839
+ return runDirectRequest(config, target, flags, {
2840
+ method: 'POST',
2841
+ path: `${appPrefix}/${subcommand === 'batch-send' ? 'batch-send' : subcommand}`,
2842
+ body,
2843
+ strictEnvelope: true,
2844
+ });
2845
+ }
2846
+
2847
+ fail('用法: openxiangda notification template-list|template-get|template-upsert|template-delete|type-list|type-get|type-upsert|type-delete|preview|send|batch-send');
2180
2848
  }
2181
2849
 
2182
2850
  async function permission(args) {
2183
2851
  const [subcommand, ...rest] = args;
2184
2852
  const { flags, positional } = parseArgs(rest);
2853
+ if (wantsSubcommandHelp(subcommand, flags)) {
2854
+ print(
2855
+ '用法: 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'
2856
+ );
2857
+ return;
2858
+ }
2185
2859
  const config = loadConfig();
2186
2860
  const profileName = flags.profile || config.currentProfile;
2187
2861
 
@@ -2243,6 +2917,46 @@ async function permission(args) {
2243
2917
  return;
2244
2918
  }
2245
2919
 
2920
+ if (subcommand === 'role-update') {
2921
+ const [roleKey] = positional;
2922
+ const body = readDirectJsonBody(flags, 'role update');
2923
+ const roleCode = roleKey || body.code || body.resourceCode;
2924
+ if (!roleCode) fail('用法: openxiangda permission role-update <roleCode|roleId> --json-file file');
2925
+ const target = getWorkspaceTarget(config, profileName, flags);
2926
+ const roleId = resolveRoleId(target.bound, roleCode, flags);
2927
+ const data = await runDirectRequest(config, target, flags, {
2928
+ method: 'PUT',
2929
+ path: `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/roles/${encodeURIComponent(roleId)}`,
2930
+ body: {
2931
+ code: body.code || roleCode,
2932
+ name: body.name || roleCode,
2933
+ description: body.description || '',
2934
+ },
2935
+ }, { returnData: true });
2936
+ if (!flags['dry-run']) {
2937
+ const resultCode = body.code || roleCode;
2938
+ if (data?.id) saveRoleResource(target, resultCode, data.id);
2939
+ if (flags['write-manifest']) writeDirectManifest('role', resultCode, { ...body, code: resultCode });
2940
+ }
2941
+ return outputDirectResult(data, flags);
2942
+ }
2943
+
2944
+ if (subcommand === 'role-delete') {
2945
+ const [roleKey] = positional;
2946
+ if (!roleKey || !flags.force) fail('用法: openxiangda permission role-delete <roleCode|roleId> --force');
2947
+ const target = getWorkspaceTarget(config, profileName, flags);
2948
+ const roleId = resolveRoleId(target.bound, roleKey, flags);
2949
+ const data = await runDirectRequest(config, target, flags, {
2950
+ method: 'DELETE',
2951
+ path: `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/roles/${encodeURIComponent(roleId)}`,
2952
+ }, { returnData: true });
2953
+ if (!flags['dry-run'] && target.bound.resources?.roles?.[roleKey]) {
2954
+ delete target.bound.resources.roles[roleKey];
2955
+ saveProjectState(target.state);
2956
+ }
2957
+ return outputDirectResult(data, flags);
2958
+ }
2959
+
2246
2960
  if (subcommand === 'role-users') {
2247
2961
  const [roleKey] = positional;
2248
2962
  if (!roleKey) fail('用法: openxiangda permission role-users <roleCode|roleId>');
@@ -2380,6 +3094,42 @@ async function permission(args) {
2380
3094
  return;
2381
3095
  }
2382
3096
 
3097
+ if (subcommand === 'page-group-update') {
3098
+ const [groupKey] = positional;
3099
+ const body = readDirectJsonBody(flags, 'page permission group update');
3100
+ const groupCode = groupKey || body.code || body.resourceCode;
3101
+ if (!groupCode) fail('用法: openxiangda permission page-group-update <groupCode|groupId> --json-file file');
3102
+ const target = getWorkspaceTarget(config, profileName, flags);
3103
+ const groupId = flags['group-id'] || target.bound.resources?.pagePermissionGroups?.[groupCode]?.groupId || groupCode;
3104
+ const normalized = normalizePagePermissionGroupDirectBody(target.bound, { ...body, code: body.code || groupCode });
3105
+ const data = await runDirectRequest(config, target, flags, {
3106
+ method: 'PUT',
3107
+ path: `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/page-permission-groups/${encodeURIComponent(groupId)}`,
3108
+ body: normalized,
3109
+ }, { returnData: true });
3110
+ if (!flags['dry-run']) {
3111
+ if (data?.id) savePagePermissionGroupResource(target, body.code || groupCode, data.id, { name: data.name, resourceCode: body.code || groupCode });
3112
+ if (flags['write-manifest']) writeDirectManifest('page-permission-group', body.code || groupCode, { ...body, code: body.code || groupCode });
3113
+ }
3114
+ return outputDirectResult(data, flags);
3115
+ }
3116
+
3117
+ if (subcommand === 'page-group-delete') {
3118
+ const [groupKey] = positional;
3119
+ if (!groupKey || !flags.force) fail('用法: openxiangda permission page-group-delete <groupCode|groupId> --force');
3120
+ const target = getWorkspaceTarget(config, profileName, flags);
3121
+ const groupId = flags['group-id'] || target.bound.resources?.pagePermissionGroups?.[groupKey]?.groupId || groupKey;
3122
+ const data = await runDirectRequest(config, target, flags, {
3123
+ method: 'DELETE',
3124
+ path: `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/page-permission-groups/${encodeURIComponent(groupId)}`,
3125
+ }, { returnData: true });
3126
+ if (!flags['dry-run'] && target.bound.resources?.pagePermissionGroups?.[groupKey]) {
3127
+ delete target.bound.resources.pagePermissionGroups[groupKey];
3128
+ saveProjectState(target.state);
3129
+ }
3130
+ return outputDirectResult(data, flags);
3131
+ }
3132
+
2383
3133
  if (subcommand === 'form-group-list') {
2384
3134
  const target = getWorkspaceTarget(config, profileName, flags);
2385
3135
  const formUuid = flags['form-uuid'] || resolveOptionalFormUuid(target.bound, flags['form-code']);
@@ -2477,6 +3227,44 @@ async function permission(args) {
2477
3227
  return;
2478
3228
  }
2479
3229
 
3230
+ if (subcommand === 'form-group-update') {
3231
+ const [groupKey] = positional;
3232
+ const body = readDirectJsonBody(flags, 'form permission group update');
3233
+ const groupCode = groupKey || body.code || body.resourceCode;
3234
+ const target = getWorkspaceTarget(config, profileName, flags);
3235
+ const formUuid = flags['form-uuid'] || resolveManifestFormUuid(target.bound, body);
3236
+ if (!groupCode || !formUuid) fail('用法: openxiangda permission form-group-update <groupCode|groupId> --form-code code|--form-uuid FORM --json-file file');
3237
+ const groupId = flags['group-id'] || target.bound.resources?.formPermissionGroups?.[groupCode]?.groupId || groupCode;
3238
+ const normalized = normalizeFormPermissionGroupManifest(target.bound, { ...body, code: body.code || groupCode, formUuid });
3239
+ const data = await runDirectRequest(config, target, flags, {
3240
+ method: 'PUT',
3241
+ path: `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/forms/${encodeURIComponent(formUuid)}/permission-groups/${encodeURIComponent(groupId)}`,
3242
+ body: normalized,
3243
+ }, { returnData: true });
3244
+ if (!flags['dry-run']) {
3245
+ if (data?.id) saveFormPermissionGroupResource(target, body.code || groupCode, data.id, { formUuid, name: data.name, resourceCode: body.code || groupCode });
3246
+ if (flags['write-manifest']) writeDirectManifest('form-permission-group', body.code || groupCode, { ...body, code: body.code || groupCode });
3247
+ }
3248
+ return outputDirectResult(data, flags);
3249
+ }
3250
+
3251
+ if (subcommand === 'form-group-delete') {
3252
+ const [groupKey] = positional;
3253
+ const target = getWorkspaceTarget(config, profileName, flags);
3254
+ const formUuid = flags['form-uuid'] || resolveOptionalFormUuid(target.bound, flags['form-code']);
3255
+ if (!groupKey || !formUuid || !flags.force) fail('用法: openxiangda permission form-group-delete <groupCode|groupId> --form-code code|--form-uuid FORM --force');
3256
+ const groupId = flags['group-id'] || target.bound.resources?.formPermissionGroups?.[groupKey]?.groupId || groupKey;
3257
+ const data = await runDirectRequest(config, target, flags, {
3258
+ method: 'DELETE',
3259
+ path: `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/forms/${encodeURIComponent(formUuid)}/permission-groups/${encodeURIComponent(groupId)}`,
3260
+ }, { returnData: true });
3261
+ if (!flags['dry-run'] && target.bound.resources?.formPermissionGroups?.[groupKey]) {
3262
+ delete target.bound.resources.formPermissionGroups[groupKey];
3263
+ saveProjectState(target.state);
3264
+ }
3265
+ return outputDirectResult(data, flags);
3266
+ }
3267
+
2480
3268
  if (subcommand === 'form-summary') {
2481
3269
  const target = getWorkspaceTarget(config, profileName, flags);
2482
3270
  const formUuid = flags['form-uuid'] || resolveOptionalFormUuid(target.bound, flags['form-code'] || positional[0]);
@@ -2503,14 +3291,29 @@ async function permission(args) {
2503
3291
  return;
2504
3292
  }
2505
3293
 
3294
+ if (subcommand === 'audit') {
3295
+ const target = getWorkspaceTarget(config, profileName, flags);
3296
+ const result = await buildPermissionAudit(config, target, flags);
3297
+ if (flags.json) return writeJson(result);
3298
+ print(JSON.stringify(result, null, 2));
3299
+ if (result.errors.length > 0) fail(`permission audit 发现 ${result.errors.length} 个错误`);
3300
+ return;
3301
+ }
3302
+
2506
3303
  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'
3304
+ '用法: 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
3305
  );
2509
3306
  }
2510
3307
 
2511
3308
  async function settings(args) {
2512
3309
  const [subcommand, ...rest] = args;
2513
3310
  const { flags, positional } = parseArgs(rest);
3311
+ if (wantsSubcommandHelp(subcommand, flags)) {
3312
+ print(
3313
+ '用法: openxiangda settings get|save|indexes|indexes-save|data-management|data-management-save|public-access|public-access-save|public-access-delete'
3314
+ );
3315
+ return;
3316
+ }
2514
3317
  const config = loadConfig();
2515
3318
  const profileName = flags.profile || config.currentProfile;
2516
3319
 
@@ -2673,16 +3476,25 @@ async function settings(args) {
2673
3476
 
2674
3477
  async function resource(args) {
2675
3478
  const { subcommand, rest } = parseSubcommandArgs(args);
2676
- const { flags } = parseArgs(rest);
3479
+ const { flags, positional } = parseArgs(rest);
2677
3480
  if (wantsSubcommandHelp(subcommand, flags)) {
2678
- print('用法: openxiangda resource validate|plan|publish|pull|typegen [--profile name] [--json]');
3481
+ print('用法: openxiangda resource validate|plan|publish|pull|typegen|explain [type] [--profile name] [--json]');
2679
3482
  return;
2680
3483
  }
2681
3484
  const config = loadConfig();
2682
3485
  const profileName = flags.profile || config.currentProfile;
2683
3486
 
2684
- if (!['validate', 'plan', 'publish', 'pull', 'typegen'].includes(subcommand)) {
2685
- fail('用法: openxiangda resource validate|plan|publish|pull|typegen [--profile name] [--json]');
3487
+ if (!['validate', 'plan', 'publish', 'pull', 'typegen', 'explain'].includes(subcommand)) {
3488
+ fail('用法: openxiangda resource validate|plan|publish|pull|typegen|explain [--profile name] [--json]');
3489
+ }
3490
+
3491
+ if (subcommand === 'explain') {
3492
+ const type = positional[0] || flags.type || 'route';
3493
+ const result = getResourceExplain(type);
3494
+ if (!result) fail(`未知资源类型: ${type}`);
3495
+ if (flags.json) return writeJson({ type, ...result });
3496
+ print(renderResourceExplain(type));
3497
+ return;
2686
3498
  }
2687
3499
 
2688
3500
  if (subcommand === 'pull') {
@@ -3097,6 +3909,16 @@ function formatBytes(value) {
3097
3909
  async function inspect(args) {
3098
3910
  const [subcommand, ...rest] = args;
3099
3911
  const { flags, positional } = parseArgs(rest);
3912
+ if (
3913
+ subcommand === 'help' ||
3914
+ subcommand === '--help' ||
3915
+ subcommand === '-h' ||
3916
+ flags.help ||
3917
+ flags.h
3918
+ ) {
3919
+ print('用法: openxiangda inspect app|form|workflow|automation|permissions [--profile name] [--json]');
3920
+ return;
3921
+ }
3100
3922
  const config = loadConfig();
3101
3923
  const profileName = flags.profile || config.currentProfile;
3102
3924
 
@@ -3181,20 +4003,28 @@ async function commands(args) {
3181
4003
  'update check|install',
3182
4004
  'platform add|list|use|remove',
3183
4005
  'auth status|refresh|logout',
4006
+ 'doctor [--profile name] [--app-type APP_XXX]',
4007
+ 'design gates|template [--topic code]',
3184
4008
  'env',
3185
4009
  'workspace init|bind|publish [--app-name] [--changed|--since|--form|--page|--only|--dry-run|--force|--resources|--skip-resources|--prune]',
3186
4010
  'app list|create|snapshot',
3187
4011
  'form list|create|bind|pull|publish',
3188
4012
  'page list|publish|bind|releases|activate',
3189
- 'menu list|create|bind|delete',
4013
+ 'menu list|create|update|sort|bind|delete',
3190
4014
  'workflow list|create|bind|pull|publish|delete|validate',
3191
4015
  'automation list|create|bind|pull|publish|unpublish|enable|disable|delete|validate|cron-validate',
3192
- 'data-view list|status|refresh|query|stats',
3193
- 'permission role-list|role-create|role-bind|role-users|role-add-users',
3194
- 'permission page-group-list|page-group-create|page-group-bind',
3195
- 'permission form-group-list|form-group-create|form-group-bind|form-summary|menu-permissions',
4016
+ 'data-view list|get|create|update|upsert|delete|status|refresh|query|stats',
4017
+ 'route list|get|create|update|upsert|delete',
4018
+ 'public-access list|get|create|update|upsert|delete|ticket-create|session-test|grant-check',
4019
+ 'auth-config list|get|create|update|upsert|delete|methods',
4020
+ 'function list|get|create|update|upsert|delete|invoke',
4021
+ 'connector list|get|create|update|upsert|delete|invoke|download-test',
4022
+ 'notification template-list|template-get|template-upsert|template-delete|type-list|type-get|type-upsert|type-delete|preview|send|batch-send',
4023
+ 'permission role-list|role-create|role-update|role-delete|role-bind|role-users|role-add-users|audit',
4024
+ 'permission page-group-list|page-group-create|page-group-update|page-group-delete|page-group-bind',
4025
+ 'permission form-group-list|form-group-create|form-group-update|form-group-delete|form-group-bind|form-summary|menu-permissions',
3196
4026
  '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',
4027
+ 'resource validate|plan|publish|pull|typegen|explain',
3198
4028
  'inspect app|form|workflow|automation|permissions',
3199
4029
  'feedback preview|submit',
3200
4030
  'skill install|status|bootstrap',
@@ -3228,6 +4058,10 @@ function printWorkspaceInitReport(result) {
3228
4058
  async function skill(args) {
3229
4059
  const [subcommand, ...rest] = args;
3230
4060
  const { flags, positional } = parseArgs(rest);
4061
+ if (wantsSubcommandHelp(subcommand, flags)) {
4062
+ print('用法: openxiangda skill install|status [--agent codex|claude|qoder|dual] [--dest <skills-dir>] [--force] [--dry-run] [--json]\n openxiangda skill bootstrap [<dir>] [--force] [--dry-run] [--json]');
4063
+ return;
4064
+ }
3231
4065
  const options = {
3232
4066
  agent: flags.agent || 'codex',
3233
4067
  dest: flags.dest,
@@ -3365,6 +4199,334 @@ function getWorkspaceTarget(config, profileName, flags = {}) {
3365
4199
  };
3366
4200
  }
3367
4201
 
4202
+ async function directResourceCrud(config, target, spec) {
4203
+ const { subcommand, flags, positional } = spec;
4204
+ const basePath = spec.basePath;
4205
+
4206
+ if (subcommand === 'list') {
4207
+ return runDirectRequest(config, target, flags, {
4208
+ method: 'GET',
4209
+ path: apiPathWithQuery(basePath, {
4210
+ page: flags.page,
4211
+ pageSize: flags['page-size'] || flags.limit,
4212
+ limit: flags.limit,
4213
+ code: flags.code,
4214
+ }),
4215
+ });
4216
+ }
4217
+
4218
+ if (subcommand === 'get') {
4219
+ const code = positional[0] || flags.code;
4220
+ if (!code) fail(`用法: openxiangda ${spec.commandName} get <code>`);
4221
+ return runDirectRequest(config, target, flags, {
4222
+ method: 'GET',
4223
+ path: `${basePath}/${encodeURIComponent(code)}`,
4224
+ });
4225
+ }
4226
+
4227
+ if (subcommand === 'delete') {
4228
+ const code = positional[0] || flags.code;
4229
+ if (!code || !flags.force) fail(`用法: openxiangda ${spec.commandName} delete <code> --force`);
4230
+ const data = await runDirectRequest(config, target, flags, {
4231
+ method: 'DELETE',
4232
+ path: `${basePath}/${encodeURIComponent(code)}`,
4233
+ }, { returnData: true });
4234
+ if (!flags['dry-run']) removeDirectStateResource(target, spec.resourceType, code);
4235
+ return outputDirectResult(data, flags);
4236
+ }
4237
+
4238
+ if (!['create', 'update', 'upsert'].includes(subcommand)) {
4239
+ fail(`用法: openxiangda ${spec.commandName} list|get|create|update|upsert|delete`);
4240
+ }
4241
+
4242
+ const rawBody = readDirectJsonBody(flags, spec.commandName);
4243
+ const explicitCode = positional[0] || flags.code;
4244
+ if (explicitCode && !rawBody.code && !rawBody.resourceCode && !rawBody.functionCode && !rawBody.methodName) {
4245
+ rawBody.code = explicitCode;
4246
+ }
4247
+ const code = explicitCode || spec.codeOf(rawBody);
4248
+ if (!code) fail(`openxiangda ${spec.commandName} ${subcommand} 缺少资源 code`);
4249
+ const normalizedBody = spec.prepareBody
4250
+ ? await spec.prepareBody(rawBody)
4251
+ : spec.normalizeBody
4252
+ ? spec.normalizeBody(rawBody)
4253
+ : rawBody;
4254
+
4255
+ let method = subcommand === 'create' ? (spec.createMethod || 'POST') : (spec.updateMethod || 'PUT');
4256
+ let requestPath = subcommand === 'create'
4257
+ ? (spec.createPath || basePath)
4258
+ : (spec.updatePath || `${basePath}/${encodeURIComponent(code)}`);
4259
+ let requestBody = subcommand === 'create'
4260
+ ? (spec.createBody ? spec.createBody(rawBody, normalizedBody) : normalizedBody)
4261
+ : (spec.updateBody ? spec.updateBody(rawBody, normalizedBody) : normalizedBody);
4262
+ let action = subcommand;
4263
+
4264
+ if (subcommand === 'upsert') {
4265
+ if (flags['dry-run']) {
4266
+ return outputDirectResult({
4267
+ dryRun: true,
4268
+ action: 'upsert',
4269
+ existsCheck: { method: 'GET', path: `${basePath}/${encodeURIComponent(code)}` },
4270
+ create: {
4271
+ method: spec.createMethod || 'POST',
4272
+ path: spec.createPath || basePath,
4273
+ body: spec.createBody ? spec.createBody(rawBody, normalizedBody) : normalizedBody,
4274
+ },
4275
+ update: {
4276
+ method: spec.updateMethod || 'PUT',
4277
+ path: spec.updatePath || `${basePath}/${encodeURIComponent(code)}`,
4278
+ body: spec.updateBody ? spec.updateBody(rawBody, normalizedBody) : normalizedBody,
4279
+ },
4280
+ }, flags);
4281
+ }
4282
+ const existing = await requestOptionalWithAuth(
4283
+ config,
4284
+ target.profileName,
4285
+ `${basePath}/${encodeURIComponent(code)}`
4286
+ );
4287
+ action = existing ? 'update' : 'create';
4288
+ method = existing ? (spec.updateMethod || 'PUT') : (spec.createMethod || 'POST');
4289
+ requestPath = existing
4290
+ ? (spec.updatePath || `${basePath}/${encodeURIComponent(code)}`)
4291
+ : (spec.createPath || basePath);
4292
+ requestBody = existing
4293
+ ? (spec.updateBody ? spec.updateBody(rawBody, normalizedBody) : normalizedBody)
4294
+ : (spec.createBody ? spec.createBody(rawBody, normalizedBody) : normalizedBody);
4295
+ }
4296
+
4297
+ const response = await runDirectRequest(config, target, flags, {
4298
+ method,
4299
+ path: requestPath,
4300
+ body: requestBody,
4301
+ }, { returnData: true });
4302
+ const data = spec.responseData ? spec.responseData(response) : response;
4303
+ if (!flags['dry-run']) {
4304
+ if (spec.saveState) spec.saveState(code, data);
4305
+ if (flags['write-manifest']) writeDirectManifest(spec.resourceType, code, { ...rawBody, code });
4306
+ }
4307
+ return outputDirectResult({ action, data }, flags);
4308
+ }
4309
+
4310
+ async function runDirectRequest(config, target, flags, request, options = {}) {
4311
+ const plan = {
4312
+ dryRun: true,
4313
+ method: request.method || 'GET',
4314
+ path: request.path,
4315
+ body: request.body,
4316
+ };
4317
+ if (flags['dry-run']) {
4318
+ if (options.returnData) return plan;
4319
+ return outputDirectResult(plan, flags);
4320
+ }
4321
+
4322
+ const requestOptions = {
4323
+ method: request.method || 'GET',
4324
+ body: request.body,
4325
+ strictEnvelope: request.strictEnvelope,
4326
+ };
4327
+ let data;
4328
+ if (request.auth === false) {
4329
+ const payload = await requestJson(target.profile.baseUrl, request.path, requestOptions);
4330
+ data = request.strictEnvelope ? unwrapStrictApi(payload) : unwrapApi(payload);
4331
+ } else {
4332
+ data = await requestWithAuth(config, target.profileName, request.path, requestOptions);
4333
+ }
4334
+ if (options.returnData) return data;
4335
+ return outputDirectResult(data, flags);
4336
+ }
4337
+
4338
+ function outputDirectResult(data, flags = {}) {
4339
+ if (flags.json) return writeJson(data);
4340
+ print(JSON.stringify(data, null, 2));
4341
+ }
4342
+
4343
+ function readDirectJsonBody(flags = {}, label, options = {}) {
4344
+ if (flags['json-file']) return readJsonArg(flags['json-file'], 'json-file');
4345
+ if (flags['body-json']) return readJsonArg(flags['body-json'], 'body-json');
4346
+ if (options.optional) return {};
4347
+ fail(`${label} 缺少 --json-file <file> 或 --body-json <json|file>`);
4348
+ }
4349
+
4350
+ function writeDirectManifest(resourceType, code, value) {
4351
+ const dir = directManifestDir(resourceType);
4352
+ if (!dir) return;
4353
+ const fileName = `${String(code).replace(/[^A-Za-z0-9_.-]+/g, '_')}.json`;
4354
+ writeResourceJsonFile(path.join(process.cwd(), dir, fileName), value);
4355
+ }
4356
+
4357
+ function directManifestDir(resourceType) {
4358
+ const dirs = {
4359
+ route: path.join('src', 'resources', 'routes'),
4360
+ 'public-access': path.join('src', 'resources', 'public-access'),
4361
+ 'auth-config': path.join('src', 'resources', 'auth'),
4362
+ function: path.join('src', 'resources', 'functions'),
4363
+ connector: path.join('src', 'resources', 'connectors'),
4364
+ notification: path.join('src', 'resources', 'notifications'),
4365
+ 'data-view': path.join('src', 'resources', 'data-views'),
4366
+ menu: path.join('src', 'resources', 'menus'),
4367
+ role: path.join('src', 'resources', 'roles'),
4368
+ 'page-permission-group': path.join('src', 'resources', 'permissions', 'page-groups'),
4369
+ 'form-permission-group': path.join('src', 'resources', 'permissions', 'form-groups'),
4370
+ };
4371
+ return dirs[resourceType];
4372
+ }
4373
+
4374
+ function removeDirectStateResource(target, resourceType, code) {
4375
+ const buckets = {
4376
+ route: 'routes',
4377
+ 'public-access': 'publicAccessPolicies',
4378
+ 'auth-config': 'authConfigs',
4379
+ function: 'functions',
4380
+ connector: 'connectors',
4381
+ 'data-view': 'dataViews',
4382
+ };
4383
+ const bucket = buckets[resourceType];
4384
+ if (!bucket || !target.bound.resources?.[bucket]?.[code]) return;
4385
+ delete target.bound.resources[bucket][code];
4386
+ saveProjectState(target.state);
4387
+ }
4388
+
4389
+ function normalizeMenuDirectBody(bound, menuItem) {
4390
+ const formUuid = resolveManifestFormUuid(bound, menuItem);
4391
+ const pageId = resolveManifestPageId(bound, menuItem);
4392
+ const parentId = resolveManifestMenuId(bound, menuItem.parentCode) || menuItem.parentId || null;
4393
+ return stripUndefinedValues({
4394
+ resourceCode: menuItem.code || menuItem.resourceCode,
4395
+ name: menuItem.name || menuItem.code,
4396
+ type: menuItem.type || 'nav',
4397
+ formUuid,
4398
+ pageId,
4399
+ parentId,
4400
+ sortOrder: menuItem.sortOrder,
4401
+ icon: menuItem.icon || null,
4402
+ isHidden: menuItem.isHidden,
4403
+ routeCode: menuItem.routeCode || null,
4404
+ path: menuItem.path || null,
4405
+ });
4406
+ }
4407
+
4408
+ function normalizePagePermissionGroupDirectBody(bound, group) {
4409
+ return stripUndefinedValues({
4410
+ resourceCode: group.code || group.resourceCode,
4411
+ name: group.name || group.code,
4412
+ roles: group.roles || [],
4413
+ menuFormUuids: resolvePagePermissionGroupTargets(bound, group),
4414
+ menuCodes: normalizePermissionCodeArray(group.menuCodes),
4415
+ routeCodes: normalizePermissionCodeArray(group.routeCodes),
4416
+ pathPatterns: normalizePermissionCodeArray(group.pathPatterns),
4417
+ });
4418
+ }
4419
+
4420
+ function parseConnectorApiName(targetName, flags = {}) {
4421
+ const raw = String(targetName || '');
4422
+ const separator = raw.indexOf('.');
4423
+ const connectorCode = separator >= 0 ? raw.slice(0, separator) : raw;
4424
+ const apiCode = flags.api || flags['api-code'] || (separator >= 0 ? raw.slice(separator + 1) : undefined);
4425
+ if (!connectorCode || !apiCode) fail('连接器调用需要 <connectorCode.apiCode> 或 --api <apiCode>');
4426
+ return { connector: connectorCode, api: apiCode };
4427
+ }
4428
+
4429
+ function extractNotificationTemplateBody(bound, body) {
4430
+ const template = Array.isArray(body.templates) ? body.templates[0] : body.template || body;
4431
+ return {
4432
+ ...template,
4433
+ code: template.code || template.templateCode || body.code || body.templateCode,
4434
+ formCode: template.formCode || body.formCode,
4435
+ formUuid: template.formUuid || body.formUuid,
4436
+ level: template.level || body.level,
4437
+ };
4438
+ }
4439
+
4440
+ function extractNotificationTypeConfigBody(bound, body) {
4441
+ const config = Array.isArray(body.typeConfigs)
4442
+ ? body.typeConfigs[0]
4443
+ : Array.isArray(body.notificationTypeConfigs)
4444
+ ? body.notificationTypeConfigs[0]
4445
+ : body.typeConfig || body;
4446
+ return {
4447
+ ...config,
4448
+ notificationType: config.notificationType || body.notificationType || body.code,
4449
+ formCode: config.formCode || body.formCode,
4450
+ formUuid: config.formUuid || body.formUuid,
4451
+ level: config.level || body.level,
4452
+ };
4453
+ }
4454
+
4455
+ function buildPublicGrantChecks(bound, grants, flags = {}) {
4456
+ const checks = [];
4457
+ const formTargets = [
4458
+ ...splitList(flags['form-code']).map(code => resolveOptionalFormUuid(bound, code)),
4459
+ ...splitList(flags['form-uuid']),
4460
+ ].filter(Boolean);
4461
+ for (const formUuid of formTargets) {
4462
+ checks.push({ type: 'form', code: formUuid, allowed: (grants.forms || []).includes(formUuid) });
4463
+ }
4464
+ for (const code of splitList(flags['data-view'])) {
4465
+ checks.push({ type: 'dataView', code, allowed: (grants.dataViews || []).includes(code) });
4466
+ }
4467
+ for (const code of splitList(flags.function || flags['function-code'])) {
4468
+ checks.push({ type: 'function', code, allowed: (grants.functions || []).includes(code) });
4469
+ }
4470
+ for (const code of splitList(flags.connector || flags['connector-code'])) {
4471
+ checks.push({ type: 'connector', code, allowed: (grants.connectors || []).includes(code) });
4472
+ }
4473
+ if (checks.length === 0) {
4474
+ return [
4475
+ { type: 'forms', granted: grants.forms || [], allowed: true },
4476
+ { type: 'dataViews', granted: grants.dataViews || [], allowed: true },
4477
+ { type: 'functions', granted: grants.functions || [], allowed: true },
4478
+ { type: 'connectors', granted: grants.connectors || [], allowed: true },
4479
+ ];
4480
+ }
4481
+ return checks;
4482
+ }
4483
+
4484
+ async function buildPermissionAudit(config, target, flags = {}) {
4485
+ const manifest = loadWorkspaceResources();
4486
+ const validation = validateWorkspaceResources(manifest);
4487
+ const roleCodes = new Set((manifest.roles || []).map(item => item.code || item.resourceCode).filter(Boolean));
4488
+ const warnings = [...validation.warnings];
4489
+ const errors = [...validation.errors];
4490
+ for (const group of manifest.pagePermissionGroups || []) {
4491
+ for (const roleCode of group.roles || []) {
4492
+ if (roleCodes.size > 0 && !roleCodes.has(roleCode)) warnings.push(`page permission group ${group.code} 引用未声明角色: ${roleCode}`);
4493
+ }
4494
+ }
4495
+ for (const group of manifest.formPermissionGroups || []) {
4496
+ for (const roleCode of group.roles || []) {
4497
+ if (roleCodes.size > 0 && !roleCodes.has(roleCode)) warnings.push(`form permission group ${group.code} 引用未声明角色: ${roleCode}`);
4498
+ }
4499
+ const formUuid = resolveManifestFormUuid(target.bound, group);
4500
+ if (!formUuid) errors.push(`form permission group ${group.code} 缺少 formCode/formUuid`);
4501
+ }
4502
+ for (const policy of manifest.publicAccessPolicies || []) {
4503
+ const grants = normalizePublicAccessPolicyManifest(target.bound, policy).grants || {};
4504
+ if (
4505
+ (grants.forms || []).length === 0 &&
4506
+ (grants.dataViews || []).length === 0 &&
4507
+ (grants.functions || []).length === 0 &&
4508
+ (grants.connectors || []).length === 0
4509
+ ) {
4510
+ warnings.push(`public-access policy ${policy.code} 未声明任何 grants`);
4511
+ }
4512
+ }
4513
+ const result = {
4514
+ appType: target.appType,
4515
+ manifestCounts: Object.fromEntries(
4516
+ RESOURCE_SPECS.map(spec => [spec.key, (manifest[spec.key] || []).length])
4517
+ ),
4518
+ errors,
4519
+ warnings,
4520
+ };
4521
+ if (flags.live) {
4522
+ result.live = {
4523
+ roles: await requestWithAuth(config, target.profileName, `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/roles`),
4524
+ pagePermissionGroups: await requestWithAuth(config, target.profileName, `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/page-permission-groups`),
4525
+ };
4526
+ }
4527
+ return result;
4528
+ }
4529
+
3368
4530
  function ensureResourceBuckets(bound) {
3369
4531
  bound.resources = bound.resources || {};
3370
4532
  bound.resources.forms = bound.resources.forms || {};
@@ -7884,6 +9046,31 @@ function unwrapApi(payload) {
7884
9046
  return 'data' in payload ? payload.data : payload;
7885
9047
  }
7886
9048
 
9049
+ function unwrapStrictApi(payload) {
9050
+ if (!payload || typeof payload !== 'object') return payload;
9051
+ if (payload.success === false) {
9052
+ const error = new Error(payload.message || payload.errorMessage || 'OpenXiangda API request failed');
9053
+ error.apiCode = payload.code;
9054
+ throw error;
9055
+ }
9056
+ if ('code' in payload) {
9057
+ const code = payload.code;
9058
+ const numericCode = Number(code);
9059
+ const normalizedCode = String(code || '').toUpperCase();
9060
+ const isOk =
9061
+ numericCode === 0 ||
9062
+ (Number.isFinite(numericCode) && numericCode >= 200 && numericCode < 300) ||
9063
+ normalizedCode === 'OK' ||
9064
+ normalizedCode === 'SUCCESS';
9065
+ if (!isOk) {
9066
+ const error = new Error(payload.message || payload.errorMessage || String(code));
9067
+ error.apiCode = code;
9068
+ throw error;
9069
+ }
9070
+ }
9071
+ return 'data' in payload ? payload.data : payload;
9072
+ }
9073
+
7887
9074
  function isUnauthorized(error) {
7888
9075
  return (
7889
9076
  Number(error?.apiCode) === 401 ||
@@ -7897,23 +9084,24 @@ async function requestWithAuth(config, profileName, apiPath, options = {}) {
7897
9084
  if (!profile.token?.accessToken) {
7898
9085
  fail(`profile ${resolved.profileName} 未登录,请先执行 openxiangda login --profile ${resolved.profileName}`);
7899
9086
  }
9087
+ const { strictEnvelope, ...requestOptions } = options;
7900
9088
 
7901
9089
  try {
7902
9090
  const payload = await requestJson(profile.baseUrl, apiPath, {
7903
- ...options,
9091
+ ...requestOptions,
7904
9092
  accessToken: profile.token.accessToken,
7905
9093
  });
7906
- return unwrapApi(payload);
9094
+ return strictEnvelope ? unwrapStrictApi(payload) : unwrapApi(payload);
7907
9095
  } catch (error) {
7908
9096
  if (!isUnauthorized(error) || !profile.token?.refreshToken) {
7909
9097
  throw error;
7910
9098
  }
7911
9099
  await refreshProfile(config, resolved.profileName);
7912
9100
  const payload = await requestJson(profile.baseUrl, apiPath, {
7913
- ...options,
9101
+ ...requestOptions,
7914
9102
  accessToken: profile.token.accessToken,
7915
9103
  });
7916
- return unwrapApi(payload);
9104
+ return strictEnvelope ? unwrapStrictApi(payload) : unwrapApi(payload);
7917
9105
  }
7918
9106
  }
7919
9107