openxiangda 1.0.85 → 1.0.87

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.
Files changed (31) hide show
  1. package/README.md +28 -0
  2. package/lib/cli.js +482 -10
  3. package/openxiangda-skills/SKILL.md +5 -1
  4. package/openxiangda-skills/references/architecture-design.md +29 -0
  5. package/openxiangda-skills/references/openxiangda-api.md +105 -3
  6. package/openxiangda-skills/references/pages/page-sdk.md +35 -0
  7. package/openxiangda-skills/references/permissions-settings.md +39 -2
  8. package/openxiangda-skills/references/resource-manifest-cheatsheet.md +70 -4
  9. package/package.json +2 -1
  10. package/packages/sdk/dist/runtime/index.cjs +3590 -3388
  11. package/packages/sdk/dist/runtime/index.cjs.map +1 -1
  12. package/packages/sdk/dist/runtime/index.d.mts +4 -1001
  13. package/packages/sdk/dist/runtime/index.d.ts +4 -1001
  14. package/packages/sdk/dist/runtime/index.mjs +3049 -2849
  15. package/packages/sdk/dist/runtime/index.mjs.map +1 -1
  16. package/packages/sdk/dist/runtime/react.cjs +2793 -116
  17. package/packages/sdk/dist/runtime/react.cjs.map +1 -1
  18. package/packages/sdk/dist/runtime/react.d.mts +1394 -2
  19. package/packages/sdk/dist/runtime/react.d.ts +1394 -2
  20. package/packages/sdk/dist/runtime/react.mjs +2749 -84
  21. package/packages/sdk/dist/runtime/react.mjs.map +1 -1
  22. package/templates/openxiangda-react-spa/AGENTS.md +29 -0
  23. package/templates/openxiangda-react-spa/src/app/router.tsx +21 -1
  24. package/templates/openxiangda-react-spa/src/pages/public/PublicRegisterPage.tsx +15 -0
  25. package/templates/openxiangda-react-spa/src/resources/permissions/page-groups/public-visitor.json +8 -0
  26. package/templates/openxiangda-react-spa/src/resources/public-access/public-register.json +14 -0
  27. package/templates/openxiangda-react-spa/src/resources/routes/public-register.json +8 -0
  28. package/templates/openxiangda-react-spa/tsconfig.app.json +1 -1
  29. package/templates/openxiangda-react-spa/vite.config.ts +1 -1
  30. package/packages/sdk/dist/openxiangdaProvider-CaXMpsnK.d.mts +0 -328
  31. package/packages/sdk/dist/openxiangdaProvider-CaXMpsnK.d.ts +0 -328
package/lib/cli.js CHANGED
@@ -2,6 +2,7 @@ const fs = require('fs');
2
2
  const path = require('path');
3
3
  const crypto = require('crypto');
4
4
  const os = require('os');
5
+ const { builtinModules } = require('module');
5
6
  const { spawnSync } = require('child_process');
6
7
  const { pathToFileURL } = require('url');
7
8
  const esbuild = require('esbuild');
@@ -3375,6 +3376,8 @@ function ensureResourceBuckets(bound) {
3375
3376
  bound.resources.connectors = bound.resources.connectors || {};
3376
3377
  bound.resources.dataViews = bound.resources.dataViews || {};
3377
3378
  bound.resources.authConfigs = bound.resources.authConfigs || {};
3379
+ bound.resources.routes = bound.resources.routes || {};
3380
+ bound.resources.publicAccessPolicies = bound.resources.publicAccessPolicies || {};
3378
3381
  bound.resources.notifications = bound.resources.notifications || {};
3379
3382
  bound.resources.notifications.templates = bound.resources.notifications.templates || {};
3380
3383
  bound.resources.notifications.typeConfigs = bound.resources.notifications.typeConfigs || {};
@@ -3585,6 +3588,22 @@ function saveDataViewResource(target, dataViewCode, dataViewId, extra = {}) {
3585
3588
  }, keys);
3586
3589
  }
3587
3590
 
3591
+ function saveRouteResource(target, routeCode, routeId, extra = {}) {
3592
+ const keys = ['routeId', 'pathPattern', 'publicAccess', 'publicPolicyCode'];
3593
+ saveStateResource(target, 'routes', routeCode, {
3594
+ ...pickStateFields(extra, keys),
3595
+ routeId,
3596
+ }, keys);
3597
+ }
3598
+
3599
+ function savePublicAccessPolicyResource(target, policyCode, policyId, extra = {}) {
3600
+ const keys = ['policyId', 'mode', 'routeCode', 'pathPattern'];
3601
+ saveStateResource(target, 'publicAccessPolicies', policyCode, {
3602
+ ...pickStateFields(extra, keys),
3603
+ policyId,
3604
+ }, keys);
3605
+ }
3606
+
3588
3607
  function saveAuthConfigResource(target, authConfigCode, configId, extra = {}) {
3589
3608
  const keys = ['configId', 'status'];
3590
3609
  saveStateResource(target, 'authConfigs', authConfigCode, {
@@ -3701,6 +3720,13 @@ const RESOURCE_SPECS = [
3701
3720
  { key: 'functions', dir: 'functions', topFiles: ['functions.json'], pluralKeys: ['functions'] },
3702
3721
  { key: 'dataViews', dir: 'data-views', topFiles: ['data-views.json'], pluralKeys: ['dataViews', 'data-views'] },
3703
3722
  { key: 'authConfigs', dir: 'auth', topFiles: ['auth.json'], pluralKeys: ['authConfigs', 'auth'] },
3723
+ { key: 'routes', dir: 'routes', topFiles: ['routes.json'], pluralKeys: ['routes'] },
3724
+ {
3725
+ key: 'publicAccessPolicies',
3726
+ dir: 'public-access',
3727
+ topFiles: ['public-access.json'],
3728
+ pluralKeys: ['publicAccessPolicies', 'publicAccess', 'policies'],
3729
+ },
3704
3730
  {
3705
3731
  key: 'pagePermissionGroups',
3706
3732
  dir: path.join('permissions', 'page-groups'),
@@ -3882,6 +3908,12 @@ function generateResourceTypes(manifest, outputFile) {
3882
3908
  routeCode: item.routeCode || null,
3883
3909
  path: item.path || null,
3884
3910
  }));
3911
+ const routes = (manifest.routes || []).map(item => ({
3912
+ code: item.code,
3913
+ pathPattern: item.pathPattern || item.path || null,
3914
+ publicAccess: item.publicAccess || 'none',
3915
+ publicPolicyCode: item.publicPolicyCode || item.policyCode || null,
3916
+ }));
3885
3917
  const pagePermissionGroups = (manifest.pagePermissionGroups || []).map(item => ({
3886
3918
  code: item.code,
3887
3919
  menuCodes: normalizePermissionCodeArray(item.menuCodes),
@@ -3891,6 +3923,7 @@ function generateResourceTypes(manifest, outputFile) {
3891
3923
  const routeCodes = unique(
3892
3924
  [
3893
3925
  ...menus.map(item => item.routeCode),
3926
+ ...routes.map(item => item.code),
3894
3927
  ...pagePermissionGroups.flatMap(item => item.routeCodes),
3895
3928
  ].filter(Boolean)
3896
3929
  );
@@ -3901,6 +3934,7 @@ function generateResourceTypes(manifest, outputFile) {
3901
3934
  const dataViewCodes = unique((manifest.dataViews || []).map(item => item.code).filter(Boolean));
3902
3935
  const functionCodes = unique((manifest.functions || []).map(item => item.code).filter(Boolean));
3903
3936
  const authConfigCodes = unique((manifest.authConfigs || []).map(item => item.code).filter(Boolean));
3937
+ const publicAccessPolicyCodes = unique((manifest.publicAccessPolicies || []).map(item => item.code).filter(Boolean));
3904
3938
  const content = [
3905
3939
  '/* eslint-disable */',
3906
3940
  '// Generated by openxiangda resource typegen. Do not edit manually.',
@@ -3923,8 +3957,13 @@ function generateResourceTypes(manifest, outputFile) {
3923
3957
  `export const authConfigCodes = ${JSON.stringify(authConfigCodes, null, 2)} as const`,
3924
3958
  'export type AuthConfigCode = typeof authConfigCodes[number]',
3925
3959
  '',
3960
+ `export const publicAccessPolicyCodes = ${JSON.stringify(publicAccessPolicyCodes, null, 2)} as const`,
3961
+ 'export type PublicAccessPolicyCode = typeof publicAccessPolicyCodes[number]',
3962
+ '',
3926
3963
  `export const runtimeMenus = ${JSON.stringify(menus, null, 2)} as const`,
3927
3964
  '',
3965
+ `export const runtimeRoutes = ${JSON.stringify(routes, null, 2)} as const`,
3966
+ '',
3928
3967
  `export const pagePermissionGroups = ${JSON.stringify(pagePermissionGroups, null, 2)} as const`,
3929
3968
  '',
3930
3969
  ].join('\n');
@@ -3938,6 +3977,7 @@ function generateResourceTypes(manifest, outputFile) {
3938
3977
  dataViews: dataViewCodes.length,
3939
3978
  functions: functionCodes.length,
3940
3979
  authConfigs: authConfigCodes.length,
3980
+ publicAccessPolicies: publicAccessPolicyCodes.length,
3941
3981
  };
3942
3982
  }
3943
3983
 
@@ -4042,6 +4082,29 @@ function validateResourceItem(kind, item, errors, warnings) {
4042
4082
  warnings.push(`${label}: 已开启非默认注册策略 ${registrationMode},请确认身份匹配键、默认角色和审计字段`);
4043
4083
  }
4044
4084
  }
4085
+ if (kind === 'routes') {
4086
+ if (!item.pathPattern && !item.path) errors.push(`${label}: 缺少 pathPattern`);
4087
+ const mode = item.publicAccess === undefined ? 'none' : String(item.publicAccess);
4088
+ if (!['none', 'guest', 'ticket'].includes(mode)) {
4089
+ errors.push(`${label}: publicAccess 只能是 none、guest 或 ticket`);
4090
+ }
4091
+ }
4092
+ if (kind === 'publicAccessPolicies') {
4093
+ if (!item.name) warnings.push(`${label}: 未声明 name,将使用 code`);
4094
+ const mode = item.mode === undefined ? 'guest' : String(item.mode);
4095
+ if (!['guest', 'ticket'].includes(mode)) {
4096
+ errors.push(`${label}: mode 只能是 guest 或 ticket`);
4097
+ }
4098
+ if (!item.routeCode && !item.pathPattern && !item.path) {
4099
+ warnings.push(`${label}: 未声明 routeCode/pathPattern,建议绑定到 /view/:appType/public/* 路由`);
4100
+ }
4101
+ if (!item.grants || typeof item.grants !== 'object' || Array.isArray(item.grants)) {
4102
+ errors.push(`${label}: 缺少 grants object;未显式 grant 的 form/dataView/function/connector 默认拒绝`);
4103
+ }
4104
+ if (!Array.isArray(item.externalRoleCodes || item.externalRoles || item.roles || [])) {
4105
+ errors.push(`${label}: externalRoleCodes 必须是数组`);
4106
+ }
4107
+ }
4045
4108
  if (kind === 'pagePermissionGroups' && !item.name) errors.push(`${label}: 缺少 name`);
4046
4109
  if (kind === 'formPermissionGroups') {
4047
4110
  if (!item.name) errors.push(`${label}: 缺少 name`);
@@ -4444,6 +4507,8 @@ async function buildResourcePlan(config, target, manifest) {
4444
4507
  await addWorkflowPlanActions(config, target, actions, manifest.workflows, existing.workflows);
4445
4508
  addPlanActions(actions, 'function', manifest.functions, existing.functions, (item, current) => functionEquals(target, item, current));
4446
4509
  addPlanActions(actions, 'authConfig', manifest.authConfigs, existing.authConfigs, (item, current) => authConfigEquals(item, current));
4510
+ addPlanActions(actions, 'route', manifest.routes, existing.routes, (item, current) => routeEquals(item, current));
4511
+ addPlanActions(actions, 'publicAccessPolicy', manifest.publicAccessPolicies, existing.publicAccessPolicies, (item, current) => publicAccessPolicyEquals(target.bound, item, current));
4447
4512
  await addAutomationPlanActions(config, target, actions, manifest.automations, existing.automations);
4448
4513
  addPlanActions(actions, 'dataView', manifest.dataViews, existing.dataViews, (item, current) => dataViewEquals(target.bound, item, current));
4449
4514
  addPlanActions(actions, 'pagePermissionGroup', manifest.pagePermissionGroups, existing.pagePermissionGroups, (item, current) => pagePermissionGroupEquals(target.bound, item, current));
@@ -4480,8 +4545,10 @@ async function publishResourceManifest(config, target, manifest, options = {}) {
4480
4545
  await publishWorkflowResources(config, target, manifest.workflows || [], result);
4481
4546
  await publishFunctionResources(config, target, manifest.functions || [], result);
4482
4547
  await publishAuthConfigResources(config, target, manifest.authConfigs || [], result);
4548
+ await publishRouteResources(config, target, manifest.routes || [], result);
4483
4549
  await publishAutomationResources(config, target, manifest.automations || [], result);
4484
4550
  await publishDataViewResources(config, target, manifest.dataViews || [], result);
4551
+ await publishPublicAccessPolicyResources(config, target, manifest.publicAccessPolicies || [], result);
4485
4552
  await publishPagePermissionGroupResources(config, target, manifest.pagePermissionGroups || [], result);
4486
4553
  await publishFormPermissionGroupResources(config, target, manifest.formPermissionGroups || [], result);
4487
4554
  if (options.prune) {
@@ -4504,6 +4571,8 @@ async function fetchExistingResourceMaps(config, target, manifest) {
4504
4571
  workflows: new Map(),
4505
4572
  functions: new Map(),
4506
4573
  authConfigs: new Map(),
4574
+ routes: new Map(),
4575
+ publicAccessPolicies: new Map(),
4507
4576
  automations: new Map(),
4508
4577
  dataViews: new Map(),
4509
4578
  pagePermissionGroups: new Map(),
@@ -4634,6 +4703,28 @@ async function fetchExistingResourceMaps(config, target, manifest) {
4634
4703
  });
4635
4704
  }
4636
4705
  }
4706
+ if ((manifest.routes || []).length > 0) {
4707
+ const data = await requestWithAuth(
4708
+ config,
4709
+ target.profileName,
4710
+ apiPathWithQuery(`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/routes`, {
4711
+ page: 1,
4712
+ pageSize: 1000,
4713
+ })
4714
+ );
4715
+ indexByCode(maps.routes, normalizeItems(data), item => item.code || item.resourceCode);
4716
+ }
4717
+ if ((manifest.publicAccessPolicies || []).length > 0) {
4718
+ const data = await requestWithAuth(
4719
+ config,
4720
+ target.profileName,
4721
+ apiPathWithQuery(`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/public-access/policies`, {
4722
+ page: 1,
4723
+ pageSize: 1000,
4724
+ })
4725
+ );
4726
+ indexByCode(maps.publicAccessPolicies, normalizeItems(data), item => item.code || item.resourceCode);
4727
+ }
4637
4728
  if ((manifest.dataViews || []).length > 0) {
4638
4729
  const data = await requestWithAuth(
4639
4730
  config,
@@ -5361,6 +5452,74 @@ async function publishDataViewResources(config, target, dataViews, result) {
5361
5452
  }
5362
5453
  }
5363
5454
 
5455
+ async function publishRouteResources(config, target, routes, result) {
5456
+ for (const route of routes) {
5457
+ const code = route.code || route.resourceCode;
5458
+ const existing = await findExistingRoute(config, target, code);
5459
+ const body = normalizeRouteManifest(route);
5460
+ const data = existing
5461
+ ? await requestWithAuth(
5462
+ config,
5463
+ target.profileName,
5464
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/routes/${encodeURIComponent(code)}`,
5465
+ { method: 'PUT', body }
5466
+ )
5467
+ : await requestWithAuth(
5468
+ config,
5469
+ target.profileName,
5470
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/routes`,
5471
+ { method: 'POST', body }
5472
+ );
5473
+ if (data?.id) {
5474
+ saveRouteResource(target, code, data.id, {
5475
+ pathPattern: data.pathPattern,
5476
+ publicAccess: data.publicAccess,
5477
+ publicPolicyCode: data.publicPolicyCode,
5478
+ });
5479
+ }
5480
+ result.published.push({
5481
+ kind: 'route',
5482
+ code,
5483
+ action: existing ? 'update' : 'create',
5484
+ id: data?.id,
5485
+ });
5486
+ }
5487
+ }
5488
+
5489
+ async function publishPublicAccessPolicyResources(config, target, policies, result) {
5490
+ for (const policy of policies) {
5491
+ const code = policy.code || policy.resourceCode;
5492
+ const existing = await findExistingPublicAccessPolicy(config, target, code);
5493
+ const body = normalizePublicAccessPolicyManifest(target.bound, policy);
5494
+ const data = existing
5495
+ ? await requestWithAuth(
5496
+ config,
5497
+ target.profileName,
5498
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/public-access/policies/${encodeURIComponent(code)}`,
5499
+ { method: 'PUT', body }
5500
+ )
5501
+ : await requestWithAuth(
5502
+ config,
5503
+ target.profileName,
5504
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/public-access/policies`,
5505
+ { method: 'POST', body }
5506
+ );
5507
+ if (data?.id) {
5508
+ savePublicAccessPolicyResource(target, code, data.id, {
5509
+ mode: data.mode,
5510
+ routeCode: data.routeCode,
5511
+ pathPattern: data.pathPattern,
5512
+ });
5513
+ }
5514
+ result.published.push({
5515
+ kind: 'publicAccessPolicy',
5516
+ code,
5517
+ action: existing ? 'update' : 'create',
5518
+ id: data?.id,
5519
+ });
5520
+ }
5521
+ }
5522
+
5364
5523
  async function findExistingFunction(config, target, code) {
5365
5524
  const stateId = target.bound.resources?.functions?.[code]?.functionId;
5366
5525
  const byCode = await requestOptionalWithAuth(
@@ -5373,6 +5532,30 @@ async function findExistingFunction(config, target, code) {
5373
5532
  return null;
5374
5533
  }
5375
5534
 
5535
+ async function findExistingRoute(config, target, code) {
5536
+ const stateId = target.bound.resources?.routes?.[code]?.routeId;
5537
+ const byCode = await requestOptionalWithAuth(
5538
+ config,
5539
+ target.profileName,
5540
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/routes/${encodeURIComponent(code)}`
5541
+ );
5542
+ if (byCode?.id) return byCode;
5543
+ if (stateId) return { id: stateId, code };
5544
+ return null;
5545
+ }
5546
+
5547
+ async function findExistingPublicAccessPolicy(config, target, code) {
5548
+ const stateId = target.bound.resources?.publicAccessPolicies?.[code]?.policyId;
5549
+ const byCode = await requestOptionalWithAuth(
5550
+ config,
5551
+ target.profileName,
5552
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/public-access/policies/${encodeURIComponent(code)}`
5553
+ );
5554
+ if (byCode?.id) return byCode;
5555
+ if (stateId) return { id: stateId, code };
5556
+ return null;
5557
+ }
5558
+
5376
5559
  async function findExistingAuthConfig(config, target, code) {
5377
5560
  const stateId = target.bound.resources?.authConfigs?.[code]?.configId;
5378
5561
  const byCode = await requestOptionalWithAuth(
@@ -5536,7 +5719,14 @@ async function pruneResourceManifest(config, target, manifest, result) {
5536
5719
  await pruneAutomations(config, target, desiredCodes(manifest.automations), result);
5537
5720
  await pruneFunctions(config, target, desiredCodes(manifest.functions), result);
5538
5721
  await pruneAuthConfigs(config, target, desiredCodes(manifest.authConfigs), result);
5722
+ await pruneRoutes(config, target, desiredCodes(manifest.routes), result);
5539
5723
  await pruneDataViews(config, target, desiredCodes(manifest.dataViews), result);
5724
+ await prunePublicAccessPolicies(
5725
+ config,
5726
+ target,
5727
+ desiredCodes(manifest.publicAccessPolicies),
5728
+ result
5729
+ );
5540
5730
  await prunePagePermissionGroups(
5541
5731
  config,
5542
5732
  target,
@@ -5754,6 +5944,52 @@ async function pruneAuthConfigs(config, target, desired, result) {
5754
5944
  }
5755
5945
  }
5756
5946
 
5947
+ async function pruneRoutes(config, target, desired, result) {
5948
+ const data = await requestWithAuth(
5949
+ config,
5950
+ target.profileName,
5951
+ apiPathWithQuery(`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/routes`, {
5952
+ page: 1,
5953
+ pageSize: 1000,
5954
+ })
5955
+ );
5956
+ for (const route of normalizeItems(data)) {
5957
+ const code = route.code || route.resourceCode;
5958
+ if (!code || desired.has(code)) continue;
5959
+ await pruneOne(config, target, result, 'route', code, route.id, async () => {
5960
+ await requestWithAuth(
5961
+ config,
5962
+ target.profileName,
5963
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/routes/${encodeURIComponent(code)}`,
5964
+ { method: 'DELETE' }
5965
+ );
5966
+ });
5967
+ }
5968
+ }
5969
+
5970
+ async function prunePublicAccessPolicies(config, target, desired, result) {
5971
+ const data = await requestWithAuth(
5972
+ config,
5973
+ target.profileName,
5974
+ apiPathWithQuery(`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/public-access/policies`, {
5975
+ page: 1,
5976
+ pageSize: 1000,
5977
+ })
5978
+ );
5979
+ for (const policy of normalizeItems(data)) {
5980
+ const code = policy.code || policy.resourceCode;
5981
+ if (!code || desired.has(code)) continue;
5982
+ await pruneOne(config, target, result, 'publicAccessPolicy', code, policy.id, async () => {
5983
+ await requestWithAuth(
5984
+ config,
5985
+ target.profileName,
5986
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/public-access/policies/${encodeURIComponent(code)}`,
5987
+ { method: 'DELETE' }
5988
+ );
5989
+ });
5990
+ }
5991
+ }
5992
+
5757
5993
  async function prunePagePermissionGroups(config, target, desired, result) {
5758
5994
  const data = await requestWithAuth(
5759
5995
  config,
@@ -5830,6 +6066,8 @@ function removeStateResource(target, kind, code) {
5830
6066
  automation: 'automations',
5831
6067
  function: 'functions',
5832
6068
  authConfig: 'authConfigs',
6069
+ route: 'routes',
6070
+ publicAccessPolicy: 'publicAccessPolicies',
5833
6071
  dataView: 'dataViews',
5834
6072
  pagePermissionGroup: 'pagePermissionGroups',
5835
6073
  formPermissionGroup: 'formPermissionGroups',
@@ -6048,6 +6286,36 @@ async function pullResources(config, target) {
6048
6286
  written.push(path.relative(process.cwd(), filePath));
6049
6287
  }
6050
6288
 
6289
+ const routes = await requestWithAuth(
6290
+ config,
6291
+ target.profileName,
6292
+ apiPathWithQuery(`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/routes`, {
6293
+ page: 1,
6294
+ pageSize: 1000,
6295
+ })
6296
+ );
6297
+ for (const route of normalizeItems(routes)) {
6298
+ const code = route.code || route.resourceCode || route.id;
6299
+ const filePath = path.join(baseDir, 'routes', `${code}.json`);
6300
+ writeResourceJsonFile(filePath, toPulledRoute(route));
6301
+ written.push(path.relative(process.cwd(), filePath));
6302
+ }
6303
+
6304
+ const publicAccessPolicies = await requestWithAuth(
6305
+ config,
6306
+ target.profileName,
6307
+ apiPathWithQuery(`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/public-access/policies`, {
6308
+ page: 1,
6309
+ pageSize: 1000,
6310
+ })
6311
+ );
6312
+ for (const policy of normalizeItems(publicAccessPolicies)) {
6313
+ const code = policy.code || policy.resourceCode || policy.id;
6314
+ const filePath = path.join(baseDir, 'public-access', `${code}.json`);
6315
+ writeResourceJsonFile(filePath, toPulledPublicAccessPolicy(policy, pullLookups));
6316
+ written.push(path.relative(process.cwd(), filePath));
6317
+ }
6318
+
6051
6319
  const dataViews = await requestWithAuth(
6052
6320
  config,
6053
6321
  target.profileName,
@@ -6263,6 +6531,48 @@ function toPulledDataView(dataView, permissionGroups, lookups) {
6263
6531
  });
6264
6532
  }
6265
6533
 
6534
+ function toPulledRoute(route) {
6535
+ return stripUndefinedValues({
6536
+ code: route.code || route.resourceCode,
6537
+ title: route.title || route.name,
6538
+ kind: route.kind || 'page',
6539
+ pathPattern: route.pathPattern || route.path,
6540
+ menuCode: route.menuCode || undefined,
6541
+ publicAccess: route.publicAccess || 'none',
6542
+ publicPolicyCode: route.publicPolicyCode || route.policyCode || undefined,
6543
+ meta: route.metaJson || route.meta || undefined,
6544
+ });
6545
+ }
6546
+
6547
+ function toPulledPublicAccessPolicy(policy, lookups) {
6548
+ return stripUndefinedValues({
6549
+ code: policy.code || policy.resourceCode,
6550
+ name: policy.name,
6551
+ description: policy.description || '',
6552
+ enabled: policy.enabled !== false,
6553
+ mode: policy.mode || 'guest',
6554
+ routeCode: policy.routeCode || undefined,
6555
+ pathPattern: policy.pathPattern || policy.path || undefined,
6556
+ externalRoleCodes: policy.externalRoleCodes || policy.externalRoles || [],
6557
+ grants: rewritePublicAccessGrantsForManifest(policy.grantsJson || policy.grants || {}, lookups),
6558
+ ticketConfig: policy.ticketConfigJson || policy.ticketConfig || undefined,
6559
+ rateLimit: policy.rateLimitJson || policy.rateLimit || undefined,
6560
+ expiresAt: policy.expiresAt || undefined,
6561
+ });
6562
+ }
6563
+
6564
+ function rewritePublicAccessGrantsForManifest(grants, lookups) {
6565
+ const formCodes = normalizePermissionCodeArray(grants.forms).map(formUuid =>
6566
+ lookups.formCodeByUuid.get(formUuid) || formUuid
6567
+ );
6568
+ return stripUndefinedValues({
6569
+ formCodes,
6570
+ dataViews: normalizePermissionCodeArray(grants.dataViews),
6571
+ functions: normalizePermissionCodeArray(grants.functions),
6572
+ connectors: normalizePermissionCodeArray(grants.connectors),
6573
+ });
6574
+ }
6575
+
6266
6576
  function toPulledAuthConfig(authConfig) {
6267
6577
  return stripUndefinedValues({
6268
6578
  code: authConfig.code || authConfig.resourceCode,
@@ -6453,6 +6763,54 @@ function normalizeDataViewManifest(bound, dataView) {
6453
6763
  });
6454
6764
  }
6455
6765
 
6766
+ function normalizeRouteManifest(route) {
6767
+ const code = route.code || route.resourceCode;
6768
+ return stripUndefinedValues({
6769
+ code,
6770
+ title: route.title || route.name || code,
6771
+ kind: route.kind || 'page',
6772
+ pathPattern: route.pathPattern || route.path,
6773
+ menuCode: route.menuCode,
6774
+ publicAccess: route.publicAccess || 'none',
6775
+ publicPolicyCode: route.publicPolicyCode || route.policyCode,
6776
+ meta: route.meta || route.metaJson,
6777
+ });
6778
+ }
6779
+
6780
+ function normalizePublicAccessPolicyManifest(bound, policy) {
6781
+ const code = policy.code || policy.resourceCode;
6782
+ return stripUndefinedValues({
6783
+ code,
6784
+ name: policy.name || policy.title || code,
6785
+ description: policy.description || '',
6786
+ enabled: policy.enabled !== false,
6787
+ mode: policy.mode || 'guest',
6788
+ routeCode: policy.routeCode,
6789
+ pathPattern: policy.pathPattern || policy.path,
6790
+ externalRoleCodes: normalizePermissionCodeArray(
6791
+ policy.externalRoleCodes || policy.externalRoles || policy.roles
6792
+ ),
6793
+ grants: normalizePublicAccessGrants(bound, policy.grants || {}),
6794
+ ticketConfig: policy.ticketConfig,
6795
+ rateLimit: policy.rateLimit,
6796
+ expiresAt: policy.expiresAt,
6797
+ });
6798
+ }
6799
+
6800
+ function normalizePublicAccessGrants(bound, grants = {}) {
6801
+ const formEntries = [
6802
+ ...normalizePermissionCodeArray(grants.forms),
6803
+ ...normalizePermissionCodeArray(grants.formCodes),
6804
+ ...normalizePermissionCodeArray(grants.formUuids),
6805
+ ];
6806
+ return stripUndefinedValues({
6807
+ forms: unique(formEntries.map(code => resolveOptionalFormUuid(bound, code)).filter(Boolean)),
6808
+ dataViews: normalizePermissionCodeArray(grants.dataViews),
6809
+ functions: normalizePermissionCodeArray(grants.functions),
6810
+ connectors: normalizePermissionCodeArray(grants.connectors),
6811
+ });
6812
+ }
6813
+
6456
6814
  function normalizeAuthConfigManifest(authConfig) {
6457
6815
  const code = authConfig.code || authConfig.resourceCode || 'default';
6458
6816
  const configJson = clonePlainJson(
@@ -6968,6 +7326,40 @@ function authConfigEquals(desired, existing) {
6968
7326
  );
6969
7327
  }
6970
7328
 
7329
+ function routeEquals(desired, existing) {
7330
+ if (!existing) return false;
7331
+ const expected = normalizeRouteManifest(desired);
7332
+ return (
7333
+ String(existing.code || existing.resourceCode || '') === String(expected.code || '') &&
7334
+ String(existing.title || existing.name || '') === String(expected.title || '') &&
7335
+ String(existing.kind || 'page') === String(expected.kind || 'page') &&
7336
+ String(existing.pathPattern || existing.path || '') === String(expected.pathPattern || '') &&
7337
+ String(existing.menuCode || '') === String(expected.menuCode || '') &&
7338
+ String(existing.publicAccess || 'none') === String(expected.publicAccess || 'none') &&
7339
+ String(existing.publicPolicyCode || existing.policyCode || '') === String(expected.publicPolicyCode || '') &&
7340
+ jsonEqualsForPlan(existing.metaJson || existing.meta || {}, expected.meta || {}, desired.__dir)
7341
+ );
7342
+ }
7343
+
7344
+ function publicAccessPolicyEquals(bound, desired, existing) {
7345
+ if (!existing) return false;
7346
+ const expected = normalizePublicAccessPolicyManifest(bound, desired);
7347
+ return (
7348
+ String(existing.code || existing.resourceCode || '') === String(expected.code || '') &&
7349
+ String(existing.name || '') === String(expected.name || '') &&
7350
+ String(existing.description || '') === String(expected.description || '') &&
7351
+ Boolean(existing.enabled) === Boolean(expected.enabled) &&
7352
+ String(existing.mode || 'guest') === String(expected.mode || 'guest') &&
7353
+ String(existing.routeCode || '') === String(expected.routeCode || '') &&
7354
+ String(existing.pathPattern || '') === String(expected.pathPattern || '') &&
7355
+ stringSetEquals(existing.externalRoleCodes || [], expected.externalRoleCodes || []) &&
7356
+ jsonEqualsForPlan(existing.grantsJson || existing.grants || {}, expected.grants || {}, desired.__dir) &&
7357
+ jsonEqualsForPlan(existing.ticketConfigJson || existing.ticketConfig || {}, expected.ticketConfig || {}, desired.__dir) &&
7358
+ jsonEqualsForPlan(existing.rateLimitJson || existing.rateLimit || {}, expected.rateLimit || {}, desired.__dir) &&
7359
+ String(existing.expiresAt || '') === String(expected.expiresAt || '')
7360
+ );
7361
+ }
7362
+
6971
7363
  function dataViewEquals(bound, desired, existing) {
6972
7364
  if (!existing) return false;
6973
7365
  const expected = normalizeDataViewManifest(bound, desired);
@@ -7107,10 +7499,18 @@ function resolveJsCodeBundlePathForPlan(localPath) {
7107
7499
  const extension = path.extname(localPath).toLowerCase();
7108
7500
  if (extension && extension !== '.ts' && extension !== '.tsx') return null;
7109
7501
  const normalized = localPath.replace(/\\/g, '/');
7110
- const matched = normalized.match(/src\/js-code-nodes\/([^/]+)\/index\.tsx?$/);
7502
+ const jsCodeMatched = normalized.match(/src\/js-code-nodes\/([^/]+)\/index\.tsx?$/);
7503
+ const automationMatched = normalized.match(/src\/automations\/([^/]+)\/index\.tsx?$/);
7504
+ const functionMatched = normalized.match(/src\/functions\/([^/]+)\/index\.tsx?$/);
7505
+ const matched = jsCodeMatched || automationMatched || functionMatched;
7111
7506
  if (!matched) return null;
7507
+ const sourceKind = functionMatched
7508
+ ? 'functions'
7509
+ : automationMatched
7510
+ ? 'automations'
7511
+ : 'js-code-nodes';
7112
7512
  const workspaceRoot = findWorkspaceRoot(localPath);
7113
- return path.join(workspaceRoot, 'dist', 'js-code-nodes', matched[1], 'index.cjs');
7513
+ return path.join(workspaceRoot, 'dist', sourceKind, matched[1], 'index.cjs');
7114
7514
  }
7115
7515
 
7116
7516
  function sortObjectForStableJson(value) {
@@ -7272,7 +7672,7 @@ async function uploadJsCodeSnapshotFile(
7272
7672
  const resolvedLocalPath = fs.existsSync(baseResolvedPath)
7273
7673
  ? baseResolvedPath
7274
7674
  : cwdResolvedPath;
7275
- const bundlePath = resolveJsCodeBundlePath(resolvedLocalPath, scriptCode);
7675
+ const bundlePath = await resolveJsCodeBundlePath(resolvedLocalPath, scriptCode);
7276
7676
  if (!fs.existsSync(bundlePath)) {
7277
7677
  fail(`JS_CODE bundle不存在: ${bundlePath}`);
7278
7678
  }
@@ -7308,14 +7708,14 @@ async function uploadJsCodeSnapshotFile(
7308
7708
  return sourceFile;
7309
7709
  }
7310
7710
 
7311
- function resolveJsCodeBundlePath(localPath, scriptCode) {
7711
+ async function resolveJsCodeBundlePath(localPath, scriptCode) {
7312
7712
  const extension = path.extname(localPath).toLowerCase();
7313
7713
  if (['.js', '.cjs', '.mjs'].includes(extension)) {
7314
7714
  fail(
7315
7715
  `JS_CODE V2 要求 AI 源码使用 TypeScript,请把 sourceFile.localPath 指向 src/js-code-nodes/<scriptCode>/index.ts、src/automations/<scriptCode>/index.ts 或 src/functions/<functionCode>/index.ts,而不是已构建的 ${extension} 文件: ${localPath}`
7316
7716
  );
7317
7717
  }
7318
- if (extension !== '.ts') {
7718
+ if (!['.ts', '.tsx'].includes(extension)) {
7319
7719
  fail(
7320
7720
  `JS_CODE V2 sourceFile.localPath 必须指向 TypeScript 源文件 src/js-code-nodes/<scriptCode>/index.ts、src/automations/<scriptCode>/index.ts 或 src/functions/<functionCode>/index.ts: ${localPath}`
7321
7721
  );
@@ -7339,16 +7739,88 @@ function resolveJsCodeBundlePath(localPath, scriptCode) {
7339
7739
  : 'js-code-nodes';
7340
7740
 
7341
7741
  const workspaceRoot = findWorkspaceRoot(localPath);
7342
- const result = spawnSync('pnpm', ['build-js-code', '--script', resolvedScriptCode, '--source', sourceKind], {
7343
- cwd: workspaceRoot,
7344
- stdio: 'inherit',
7345
- });
7346
- if (result.status !== 0) {
7742
+ const result = runWorkspaceJsCodeBuild(workspaceRoot, resolvedScriptCode, sourceKind);
7743
+ if (result.status !== 0 && sourceKind === 'functions' && isUnsupportedFunctionsBuildScript(result)) {
7744
+ warn(
7745
+ `当前工作区 scripts/build-js-code.mjs 不支持 App Function source=functions,已使用 openxiangda 内置构建器兼容构建 ${resolvedScriptCode}`
7746
+ );
7747
+ await buildFunctionSourceWithBundledEsbuild(localPath, workspaceRoot, resolvedScriptCode);
7748
+ } else if (result.status !== 0) {
7749
+ if (result.stdout) process.stdout.write(result.stdout);
7750
+ if (result.stderr) process.stderr.write(result.stderr);
7347
7751
  fail(`JS_CODE bundle构建失败: ${resolvedScriptCode}`);
7752
+ } else {
7753
+ if (result.stdout) process.stdout.write(result.stdout);
7754
+ if (result.stderr) process.stderr.write(result.stderr);
7348
7755
  }
7349
7756
  return path.join(workspaceRoot, 'dist', sourceKind, resolvedScriptCode, 'index.cjs');
7350
7757
  }
7351
7758
 
7759
+ function runWorkspaceJsCodeBuild(workspaceRoot, resolvedScriptCode, sourceKind) {
7760
+ return spawnSync('pnpm', ['build-js-code', '--script', resolvedScriptCode, '--source', sourceKind], {
7761
+ cwd: workspaceRoot,
7762
+ encoding: 'utf8',
7763
+ });
7764
+ }
7765
+
7766
+ function isUnsupportedFunctionsBuildScript(result) {
7767
+ const output = `${result.stdout || ''}\n${result.stderr || ''}`;
7768
+ return (
7769
+ output.includes('unsupported source: functions') ||
7770
+ output.includes('Expected js-code-nodes or automations')
7771
+ );
7772
+ }
7773
+
7774
+ async function buildFunctionSourceWithBundledEsbuild(localPath, workspaceRoot, functionCode) {
7775
+ const outfile = path.join(workspaceRoot, 'dist', 'functions', functionCode, 'index.cjs');
7776
+ fs.mkdirSync(path.dirname(outfile), { recursive: true });
7777
+ const external = Array.from(
7778
+ new Set([...builtinModules, ...builtinModules.map(name => `node:${name}`)])
7779
+ );
7780
+ try {
7781
+ await esbuild.build({
7782
+ entryPoints: [localPath],
7783
+ outfile,
7784
+ bundle: true,
7785
+ platform: 'node',
7786
+ format: 'cjs',
7787
+ target: ['node20'],
7788
+ sourcemap: true,
7789
+ logLevel: 'silent',
7790
+ external,
7791
+ plugins: [
7792
+ {
7793
+ name: 'openxiangda-workspace-alias',
7794
+ setup(build) {
7795
+ build.onResolve({ filter: /^@\// }, args => ({
7796
+ path: resolveWorkspaceAliasPath(workspaceRoot, args.path),
7797
+ }));
7798
+ },
7799
+ },
7800
+ ],
7801
+ });
7802
+ } catch (error) {
7803
+ fail(`App Function 内置构建失败: ${functionCode}: ${error.message}`);
7804
+ }
7805
+ print(`built functions/${functionCode} -> ${path.relative(workspaceRoot, outfile)}`);
7806
+ }
7807
+
7808
+ function resolveWorkspaceAliasPath(workspaceRoot, importPath) {
7809
+ const raw = path.join(workspaceRoot, 'src', importPath.replace(/^@\//, ''));
7810
+ const candidates = [
7811
+ raw,
7812
+ `${raw}.ts`,
7813
+ `${raw}.tsx`,
7814
+ `${raw}.js`,
7815
+ `${raw}.mjs`,
7816
+ `${raw}.cjs`,
7817
+ path.join(raw, 'index.ts'),
7818
+ path.join(raw, 'index.tsx'),
7819
+ path.join(raw, 'index.js'),
7820
+ ];
7821
+ return candidates.find(candidate => fs.existsSync(candidate)) || raw;
7822
+ }
7823
+
7352
7824
  function findWorkspaceRoot(startPath) {
7353
7825
  if (!fs.existsSync(startPath)) {
7354
7826
  return process.cwd();