openxiangda 1.0.91 → 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/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
  ]);
@@ -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
- fail('用法: openxiangda menu list|create|bind|delete');
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
- fail('用法: openxiangda data-view list|status|refresh|query|stats');
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
- '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',
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
- ...options,
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
- ...options,
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