openxiangda 1.0.15 → 1.0.17

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
@@ -1,5 +1,6 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
+ const crypto = require('crypto');
3
4
  const { spawnSync } = require('child_process');
4
5
  const {
5
6
  CONFIG_FILE,
@@ -44,6 +45,7 @@ async function main(argv) {
44
45
  if (command === 'automation') return automation(rest);
45
46
  if (command === 'permission') return permission(rest);
46
47
  if (command === 'settings') return settings(rest);
48
+ if (command === 'resource') return resource(rest);
47
49
  if (command === 'inspect') return inspect(rest);
48
50
  if (command === 'skill') return skill(rest);
49
51
  if (command === 'commands') return commands(rest);
@@ -63,7 +65,7 @@ Usage:
63
65
  openxiangda env [--profile name]
64
66
  openxiangda workspace init [dir] [--name package-name] [--install] [--profile name --app-type APP_XXX]
65
67
  openxiangda workspace bind --profile <name> --app-type <APP_XXX>
66
- openxiangda workspace publish --profile <name>
68
+ openxiangda workspace publish --profile <name> [--prune]
67
69
  openxiangda app list [--profile name] [--json]
68
70
  openxiangda app create <name> [--profile name] [--description text]
69
71
  openxiangda app snapshot <APP_XXX> [--profile name] [--json]
@@ -90,6 +92,7 @@ Usage:
90
92
  openxiangda permission page-group-list|page-group-create|page-group-bind
91
93
  openxiangda permission form-group-list|form-group-create|form-group-bind
92
94
  openxiangda settings get|save|indexes|indexes-save|data-management|data-management-save|public-access
95
+ openxiangda resource validate|plan|publish|pull [--profile name] [--json]
93
96
  openxiangda inspect app|form|workflow|automation|permissions
94
97
  openxiangda skill install [--agent codex] [--dest <skills-dir>] [--force] [--dry-run] [--json]
95
98
  openxiangda skill status [--agent codex] [--dest <skills-dir>] [--json]
@@ -407,6 +410,11 @@ async function workspace(args) {
407
410
  workflows: {},
408
411
  automations: {},
409
412
  menus: {},
413
+ roles: {},
414
+ connectors: {},
415
+ pagePermissionGroups: {},
416
+ formPermissionGroups: {},
417
+ formSettings: {},
410
418
  },
411
419
  updatedAt: new Date().toISOString(),
412
420
  };
@@ -429,6 +437,10 @@ async function workspace(args) {
429
437
  `/openxiangda-api/v1/apps/${encodeURIComponent(bound.appType)}/snapshot`
430
438
  );
431
439
  runWorkspacePublish(profileName, profile, bound.appType);
440
+ await publishResourcesForWorkspace(config, profileName, {
441
+ quiet: true,
442
+ prune: Boolean(flags.prune),
443
+ });
432
444
  return;
433
445
  }
434
446
 
@@ -1657,6 +1669,53 @@ async function settings(args) {
1657
1669
  );
1658
1670
  }
1659
1671
 
1672
+ async function resource(args) {
1673
+ const [subcommand, ...rest] = args;
1674
+ const { flags } = parseArgs(rest);
1675
+ const config = loadConfig();
1676
+ const profileName = flags.profile || config.currentProfile;
1677
+
1678
+ if (!['validate', 'plan', 'publish', 'pull'].includes(subcommand)) {
1679
+ fail('用法: openxiangda resource validate|plan|publish|pull [--profile name] [--json]');
1680
+ }
1681
+
1682
+ if (subcommand === 'pull') {
1683
+ const target = getWorkspaceTarget(config, profileName, flags);
1684
+ const result = await pullResources(config, target);
1685
+ if (flags.json) return writeJson(result);
1686
+ printResourceResult(result);
1687
+ return;
1688
+ }
1689
+
1690
+ const manifest = loadWorkspaceResources();
1691
+ const validation = validateWorkspaceResources(manifest);
1692
+ if (subcommand === 'validate') {
1693
+ if (flags.json) return writeJson(validation);
1694
+ printResourceValidation(validation);
1695
+ if (validation.errors.length > 0) {
1696
+ fail(`资源配置校验失败: ${validation.errors.length} 个错误`);
1697
+ }
1698
+ return;
1699
+ }
1700
+ if (validation.errors.length > 0) {
1701
+ fail(`资源配置校验失败: ${validation.errors[0]}`);
1702
+ }
1703
+
1704
+ const target = getWorkspaceTarget(config, profileName, flags);
1705
+ if (subcommand === 'plan') {
1706
+ const plan = await buildResourcePlan(config, target, manifest);
1707
+ if (flags.json) return writeJson(plan);
1708
+ printResourcePlan(plan);
1709
+ return;
1710
+ }
1711
+
1712
+ const result = await publishResourceManifest(config, target, manifest, {
1713
+ prune: Boolean(flags.prune),
1714
+ });
1715
+ if (flags.json) return writeJson(result);
1716
+ printResourceResult(result);
1717
+ }
1718
+
1660
1719
  async function inspect(args) {
1661
1720
  const [subcommand, ...rest] = args;
1662
1721
  const { flags, positional } = parseArgs(rest);
@@ -1744,7 +1803,7 @@ async function commands(args) {
1744
1803
  'platform add|list|use|remove',
1745
1804
  'auth status|refresh|logout',
1746
1805
  'env',
1747
- 'workspace init|bind|publish',
1806
+ 'workspace init|bind|publish [--prune]',
1748
1807
  'app list|create|snapshot',
1749
1808
  'form list|create|bind|pull|publish',
1750
1809
  'page list|publish|bind|releases|activate',
@@ -1755,6 +1814,7 @@ async function commands(args) {
1755
1814
  'permission page-group-list|page-group-create|page-group-bind',
1756
1815
  'permission form-group-list|form-group-create|form-group-bind|form-summary|menu-permissions',
1757
1816
  'settings get|save|indexes|indexes-save|data-management|data-management-save|public-access|public-access-save|public-access-delete',
1817
+ 'resource validate|plan|publish|pull',
1758
1818
  'inspect app|form|workflow|automation|permissions',
1759
1819
  'skill install|status',
1760
1820
  ],
@@ -1891,8 +1951,10 @@ function ensureResourceBuckets(bound) {
1891
1951
  bound.resources.automations = bound.resources.automations || {};
1892
1952
  bound.resources.menus = bound.resources.menus || {};
1893
1953
  bound.resources.roles = bound.resources.roles || {};
1954
+ bound.resources.connectors = bound.resources.connectors || {};
1894
1955
  bound.resources.pagePermissionGroups = bound.resources.pagePermissionGroups || {};
1895
1956
  bound.resources.formPermissionGroups = bound.resources.formPermissionGroups || {};
1957
+ bound.resources.formSettings = bound.resources.formSettings || {};
1896
1958
  }
1897
1959
 
1898
1960
  function resolveFormUuid(bound, formKey, flags = {}) {
@@ -2033,106 +2095,80 @@ function resolveSettingsFormUuid(bound, formKey, flags = {}) {
2033
2095
  }
2034
2096
 
2035
2097
  function saveFormResource(target, formCode, formUuid, extra = {}) {
2036
- target.state.profiles = target.state.profiles || {};
2037
- target.state.profiles[target.profileName] = {
2038
- ...target.bound,
2039
- baseUrl: target.profile.baseUrl,
2040
- appType: target.appType,
2041
- updatedAt: new Date().toISOString(),
2042
- };
2043
- const nextBound = target.state.profiles[target.profileName];
2044
- ensureResourceBuckets(nextBound);
2045
- nextBound.resources.forms[formCode] = {
2046
- ...(nextBound.resources.forms[formCode] || {}),
2047
- ...extra,
2098
+ saveStateResource(target, 'forms', formCode, {
2099
+ ...pickStateFields(extra, []),
2048
2100
  formUuid,
2049
- updatedAt: new Date().toISOString(),
2050
- };
2051
- saveProjectState(target.state);
2052
- target.bound = nextBound;
2101
+ }, ['formUuid']);
2053
2102
  }
2054
2103
 
2055
2104
  function savePageResource(target, pageCode, pageId, extra = {}) {
2056
- target.state.profiles = target.state.profiles || {};
2057
- target.state.profiles[target.profileName] = {
2058
- ...target.bound,
2059
- baseUrl: target.profile.baseUrl,
2060
- appType: target.appType,
2061
- updatedAt: new Date().toISOString(),
2062
- };
2063
- const nextBound = target.state.profiles[target.profileName];
2064
- ensureResourceBuckets(nextBound);
2065
- nextBound.resources.pages[pageCode] = {
2066
- ...(nextBound.resources.pages[pageCode] || {}),
2067
- ...extra,
2105
+ const keys = ['pageId', 'routeKey', 'legacyFormUuid', 'formUuid'];
2106
+ saveStateResource(target, 'pages', pageCode, {
2107
+ ...pickStateFields(extra, keys),
2068
2108
  pageId,
2069
- updatedAt: new Date().toISOString(),
2070
- };
2071
- saveProjectState(target.state);
2072
- target.bound = nextBound;
2109
+ }, keys);
2073
2110
  }
2074
2111
 
2075
2112
  function saveMenuResource(target, menuCode, menuId, extra = {}) {
2076
- target.state.profiles = target.state.profiles || {};
2077
- target.state.profiles[target.profileName] = {
2078
- ...target.bound,
2079
- baseUrl: target.profile.baseUrl,
2080
- appType: target.appType,
2081
- updatedAt: new Date().toISOString(),
2082
- };
2083
- const nextBound = target.state.profiles[target.profileName];
2084
- ensureResourceBuckets(nextBound);
2085
- nextBound.resources.menus[menuCode] = {
2086
- ...(nextBound.resources.menus[menuCode] || {}),
2087
- ...extra,
2113
+ const keys = [
2114
+ 'menuId',
2115
+ 'formUuid',
2116
+ 'pageId',
2117
+ 'pageCode',
2118
+ 'routeKey',
2119
+ 'legacyFormUuid',
2120
+ 'parentId',
2121
+ ];
2122
+ saveStateResource(target, 'menus', menuCode, {
2123
+ ...pickStateFields(extra, keys),
2088
2124
  menuId,
2089
- updatedAt: new Date().toISOString(),
2090
- };
2091
- saveProjectState(target.state);
2092
- target.bound = nextBound;
2125
+ }, keys);
2093
2126
  }
2094
2127
 
2095
2128
  function saveWorkflowResource(target, workflowCode, workflowId, extra = {}) {
2096
- target.state.profiles = target.state.profiles || {};
2097
- target.state.profiles[target.profileName] = {
2098
- ...target.bound,
2099
- baseUrl: target.profile.baseUrl,
2100
- appType: target.appType,
2101
- updatedAt: new Date().toISOString(),
2102
- };
2103
- const nextBound = target.state.profiles[target.profileName];
2104
- ensureResourceBuckets(nextBound);
2105
- nextBound.resources.workflows[workflowCode] = {
2106
- ...(nextBound.resources.workflows[workflowCode] || {}),
2107
- ...extra,
2129
+ const keys = ['workflowId', 'formUuid'];
2130
+ saveStateResource(target, 'workflows', workflowCode, {
2131
+ ...pickStateFields(extra, keys),
2108
2132
  workflowId,
2109
- updatedAt: new Date().toISOString(),
2110
- };
2111
- saveProjectState(target.state);
2112
- target.bound = nextBound;
2133
+ }, keys);
2113
2134
  }
2114
2135
 
2115
2136
  function saveAutomationResource(target, automationCode, automationId, extra = {}) {
2116
- target.state.profiles = target.state.profiles || {};
2117
- target.state.profiles[target.profileName] = {
2118
- ...target.bound,
2119
- baseUrl: target.profile.baseUrl,
2120
- appType: target.appType,
2121
- updatedAt: new Date().toISOString(),
2122
- };
2123
- const nextBound = target.state.profiles[target.profileName];
2124
- ensureResourceBuckets(nextBound);
2125
- nextBound.resources.automations[automationCode] = {
2126
- ...(nextBound.resources.automations[automationCode] || {}),
2127
- ...extra,
2137
+ const keys = ['automationId', 'formUuid'];
2138
+ saveStateResource(target, 'automations', automationCode, {
2139
+ ...pickStateFields(extra, keys),
2128
2140
  automationId,
2129
- updatedAt: new Date().toISOString(),
2130
- };
2131
- saveProjectState(target.state);
2132
- target.bound = nextBound;
2141
+ }, keys);
2142
+ }
2143
+
2144
+ function saveRoleResource(target, roleCode, roleId) {
2145
+ saveStateResource(target, 'roles', roleCode, { roleId }, ['roleId']);
2146
+ }
2147
+
2148
+ function saveConnectorResource(target, connectorCode, connectorId, extra = {}) {
2149
+ const apis = {};
2150
+ for (const [apiCode, value] of Object.entries(extra.apis || {})) {
2151
+ if (value?.apiId) apis[apiCode] = { apiId: value.apiId };
2152
+ }
2153
+ saveStateResource(target, 'connectors', connectorCode, {
2154
+ connectorId,
2155
+ ...(Object.keys(apis).length > 0 ? { apis } : {}),
2156
+ }, ['connectorId', 'apis']);
2157
+ }
2158
+
2159
+ function savePagePermissionGroupResource(target, groupCode, groupId) {
2160
+ saveStateResource(target, 'pagePermissionGroups', groupCode, { groupId }, ['groupId']);
2161
+ }
2162
+
2163
+ function saveFormPermissionGroupResource(target, groupCode, groupId, extra = {}) {
2164
+ const keys = ['groupId', 'formUuid'];
2165
+ saveStateResource(target, 'formPermissionGroups', groupCode, {
2166
+ ...pickStateFields(extra, keys),
2167
+ groupId,
2168
+ }, keys);
2133
2169
  }
2134
2170
 
2135
- function saveRoleResource(target, roleCode, roleId, extra = {}) {
2171
+ function saveStateResource(target, bucket, code, value = {}, keys = Object.keys(value)) {
2136
2172
  target.state.profiles = target.state.profiles || {};
2137
2173
  target.state.profiles[target.profileName] = {
2138
2174
  ...target.bound,
@@ -2142,54 +2178,1862 @@ function saveRoleResource(target, roleCode, roleId, extra = {}) {
2142
2178
  };
2143
2179
  const nextBound = target.state.profiles[target.profileName];
2144
2180
  ensureResourceBuckets(nextBound);
2145
- nextBound.resources.roles[roleCode] = {
2146
- ...(nextBound.resources.roles[roleCode] || {}),
2147
- ...extra,
2148
- roleId,
2181
+ const previous = nextBound.resources[bucket]?.[code] || {};
2182
+ nextBound.resources[bucket] = nextBound.resources[bucket] || {};
2183
+ nextBound.resources[bucket][code] = {
2184
+ ...pickStateFields(previous, keys),
2185
+ ...pickStateFields(value, keys),
2149
2186
  updatedAt: new Date().toISOString(),
2150
2187
  };
2151
2188
  saveProjectState(target.state);
2152
2189
  target.bound = nextBound;
2153
2190
  }
2154
2191
 
2155
- function savePagePermissionGroupResource(target, groupCode, groupId, extra = {}) {
2156
- target.state.profiles = target.state.profiles || {};
2157
- target.state.profiles[target.profileName] = {
2158
- ...target.bound,
2159
- baseUrl: target.profile.baseUrl,
2160
- appType: target.appType,
2161
- updatedAt: new Date().toISOString(),
2192
+ function pickStateFields(value, keys) {
2193
+ const result = {};
2194
+ for (const key of keys) {
2195
+ if (value?.[key] !== undefined && value[key] !== null && value[key] !== '') {
2196
+ result[key] = value[key];
2197
+ }
2198
+ }
2199
+ return result;
2200
+ }
2201
+
2202
+ const RESOURCE_SPECS = [
2203
+ { key: 'roles', dir: 'roles', topFiles: ['roles.json'], pluralKeys: ['roles'] },
2204
+ { key: 'connectors', dir: 'connectors', topFiles: ['connectors.json'], pluralKeys: ['connectors'] },
2205
+ { key: 'menus', dir: 'menus', topFiles: ['menus.json'], pluralKeys: ['menus'] },
2206
+ { key: 'workflows', dir: 'workflows', topFiles: ['workflows.json'], pluralKeys: ['workflows'] },
2207
+ { key: 'automations', dir: 'automations', topFiles: ['automations.json'], pluralKeys: ['automations'] },
2208
+ {
2209
+ key: 'pagePermissionGroups',
2210
+ dir: path.join('permissions', 'page-groups'),
2211
+ topFiles: [path.join('permissions', 'page-groups.json')],
2212
+ pluralKeys: ['pagePermissionGroups', 'pageGroups'],
2213
+ },
2214
+ {
2215
+ key: 'formPermissionGroups',
2216
+ dir: path.join('permissions', 'form-groups'),
2217
+ topFiles: [path.join('permissions', 'form-groups.json')],
2218
+ pluralKeys: ['formPermissionGroups', 'formGroups'],
2219
+ },
2220
+ {
2221
+ key: 'formSettings',
2222
+ dir: path.join('settings', 'forms'),
2223
+ topFiles: [path.join('settings', 'forms.json')],
2224
+ pluralKeys: ['formSettings', 'forms'],
2225
+ },
2226
+ ];
2227
+
2228
+ function loadWorkspaceResources() {
2229
+ const baseDir = path.join(process.cwd(), 'src', 'resources');
2230
+ const manifest = { baseDir };
2231
+ for (const spec of RESOURCE_SPECS) {
2232
+ manifest[spec.key] = readResourceItems(baseDir, spec);
2233
+ }
2234
+ return manifest;
2235
+ }
2236
+
2237
+ function readResourceItems(baseDir, spec) {
2238
+ const items = [];
2239
+ for (const relativeFile of spec.topFiles || []) {
2240
+ const filePath = path.join(baseDir, relativeFile);
2241
+ if (fs.existsSync(filePath)) {
2242
+ items.push(...readResourceItemsFromFile(filePath, spec));
2243
+ }
2244
+ }
2245
+
2246
+ const dirPath = path.join(baseDir, spec.dir);
2247
+ if (fs.existsSync(dirPath)) {
2248
+ for (const filePath of listJsonFiles(dirPath)) {
2249
+ items.push(...readResourceItemsFromFile(filePath, spec));
2250
+ }
2251
+ }
2252
+ return items;
2253
+ }
2254
+
2255
+ function readResourceItemsFromFile(filePath, spec) {
2256
+ let value;
2257
+ try {
2258
+ value = JSON.parse(fs.readFileSync(filePath, 'utf8'));
2259
+ } catch (error) {
2260
+ return [
2261
+ {
2262
+ __invalid: true,
2263
+ __source: filePath,
2264
+ __error: `JSON 解析失败: ${error.message}`,
2265
+ },
2266
+ ];
2267
+ }
2268
+
2269
+ const defaultCode = path.basename(filePath, '.json');
2270
+ const values = extractResourceValues(value, spec);
2271
+ return values.map((item, index) => {
2272
+ if (!item || typeof item !== 'object' || Array.isArray(item)) {
2273
+ return {
2274
+ __invalid: true,
2275
+ __source: filePath,
2276
+ __error: '资源项必须是 JSON object',
2277
+ };
2278
+ }
2279
+ const code =
2280
+ item.code ||
2281
+ item.resourceCode ||
2282
+ item.methodName ||
2283
+ item.formCode ||
2284
+ (values.length === 1 ? defaultCode : undefined);
2285
+ return {
2286
+ ...item,
2287
+ ...(code ? { code } : {}),
2288
+ __source: filePath,
2289
+ __dir: path.dirname(filePath),
2290
+ __index: index,
2291
+ };
2292
+ });
2293
+ }
2294
+
2295
+ function extractResourceValues(value, spec) {
2296
+ if (Array.isArray(value)) return value;
2297
+ if (!value || typeof value !== 'object') return [value];
2298
+ for (const key of spec.pluralKeys || []) {
2299
+ if (Array.isArray(value[key])) return value[key];
2300
+ }
2301
+ if (value.resource && typeof value.resource === 'object') return [value.resource];
2302
+ return [value];
2303
+ }
2304
+
2305
+ function listJsonFiles(dirPath) {
2306
+ if (!fs.existsSync(dirPath)) return [];
2307
+ return fs
2308
+ .readdirSync(dirPath, { withFileTypes: true })
2309
+ .flatMap(entry => {
2310
+ const nextPath = path.join(dirPath, entry.name);
2311
+ if (entry.isDirectory()) return listJsonFiles(nextPath);
2312
+ return entry.isFile() && entry.name.endsWith('.json') ? [nextPath] : [];
2313
+ })
2314
+ .sort();
2315
+ }
2316
+
2317
+ function validateWorkspaceResources(manifest) {
2318
+ const errors = [];
2319
+ const warnings = [];
2320
+ const counts = {};
2321
+ for (const spec of RESOURCE_SPECS) {
2322
+ const items = manifest[spec.key] || [];
2323
+ counts[spec.key] = items.length;
2324
+ for (const item of items) {
2325
+ validateResourceItem(spec.key, item, errors, warnings);
2326
+ }
2327
+ }
2328
+ return {
2329
+ valid: errors.length === 0,
2330
+ errors,
2331
+ warnings,
2332
+ counts,
2333
+ baseDir: manifest.baseDir,
2162
2334
  };
2163
- const nextBound = target.state.profiles[target.profileName];
2164
- ensureResourceBuckets(nextBound);
2165
- nextBound.resources.pagePermissionGroups[groupCode] = {
2166
- ...(nextBound.resources.pagePermissionGroups[groupCode] || {}),
2167
- ...extra,
2168
- groupId,
2169
- updatedAt: new Date().toISOString(),
2335
+ }
2336
+
2337
+ function validateResourceItem(kind, item, errors, warnings) {
2338
+ const label = resourceLabel(kind, item);
2339
+ if (item.__invalid) {
2340
+ errors.push(`${item.__source}: ${item.__error}`);
2341
+ return;
2342
+ }
2343
+ if (!item.code) errors.push(`${label}: 缺少 code`);
2344
+
2345
+ if (kind === 'connectors') {
2346
+ if (!item.name) errors.push(`${label}: 缺少 name`);
2347
+ if (!item.domain && !item.url) errors.push(`${label}: 缺少 domain 或 url`);
2348
+ if (!Array.isArray(item.apis) || item.apis.length === 0) {
2349
+ errors.push(`${label}: 缺少 apis`);
2350
+ } else {
2351
+ item.apis.forEach((api, index) => {
2352
+ if (!api.code && !api.methodName) errors.push(`${label}.apis[${index}]: 缺少 code`);
2353
+ if (!api.path) errors.push(`${label}.apis[${index}]: 缺少 path`);
2354
+ if (!api.method) errors.push(`${label}.apis[${index}]: 缺少 method`);
2355
+ });
2356
+ }
2357
+ return;
2358
+ }
2359
+
2360
+ if (kind === 'roles' && !item.name) errors.push(`${label}: 缺少 name`);
2361
+ if (kind === 'menus' && !item.name) errors.push(`${label}: 缺少 name`);
2362
+ if (kind === 'workflows') {
2363
+ if (!item.formCode && !item.formUuid) errors.push(`${label}: 缺少 formCode 或 formUuid`);
2364
+ if (!item.definitionJson && !item.definitionFile) {
2365
+ errors.push(`${label}: 缺少 definitionJson 或 definitionFile`);
2366
+ }
2367
+ }
2368
+ if (kind === 'automations') {
2369
+ if (!item.name) errors.push(`${label}: 缺少 name`);
2370
+ if (!item.triggerConfig) errors.push(`${label}: 缺少 triggerConfig`);
2371
+ if (!item.definitionJson && !item.definitionFile) {
2372
+ errors.push(`${label}: 缺少 definitionJson 或 definitionFile`);
2373
+ }
2374
+ }
2375
+ if (kind === 'pagePermissionGroups' && !item.name) errors.push(`${label}: 缺少 name`);
2376
+ if (kind === 'formPermissionGroups') {
2377
+ if (!item.name) errors.push(`${label}: 缺少 name`);
2378
+ if (!item.formCode && !item.formUuid) errors.push(`${label}: 缺少 formCode 或 formUuid`);
2379
+ }
2380
+ if (kind === 'formSettings') {
2381
+ if (!item.formCode && !item.formUuid && !item.code) {
2382
+ errors.push(`${label}: 缺少 formCode、formUuid 或 code`);
2383
+ }
2384
+ if (
2385
+ item.settings === undefined &&
2386
+ item.indexes === undefined &&
2387
+ item.dataManagement === undefined &&
2388
+ item.publicAccess === undefined
2389
+ ) {
2390
+ warnings.push(`${label}: 未声明 settings/indexes/dataManagement/publicAccess`);
2391
+ }
2392
+ }
2393
+ }
2394
+
2395
+ function resourceLabel(kind, item) {
2396
+ return `${kind}:${item.code || item.__source || item.__index || '?'}`;
2397
+ }
2398
+
2399
+ async function publishResourcesForWorkspace(config, profileName, options = {}) {
2400
+ const manifest = loadWorkspaceResources();
2401
+ const validation = validateWorkspaceResources(manifest);
2402
+ if (validation.errors.length > 0) {
2403
+ fail(`资源配置校验失败: ${validation.errors[0]}`);
2404
+ }
2405
+ const target = getWorkspaceTarget(config, profileName, {});
2406
+ return publishResourceManifest(config, target, manifest, options);
2407
+ }
2408
+
2409
+ async function buildResourcePlan(config, target, manifest) {
2410
+ const existing = await fetchExistingResourceMaps(config, target, manifest);
2411
+ const actions = [];
2412
+ addPlanActions(actions, 'role', manifest.roles, existing.roles, roleEquals);
2413
+ addPlanActions(actions, 'menu', manifest.menus, existing.menus, (item, current) => menuEquals(target.bound, item, current));
2414
+ addPlanActions(actions, 'connector', manifest.connectors, existing.connectors, connectorEquals);
2415
+ await addWorkflowPlanActions(config, target, actions, manifest.workflows, existing.workflows);
2416
+ await addAutomationPlanActions(config, target, actions, manifest.automations, existing.automations);
2417
+ addPlanActions(actions, 'pagePermissionGroup', manifest.pagePermissionGroups, existing.pagePermissionGroups, (item, current) => pagePermissionGroupEquals(target.bound, item, current));
2418
+ addPlanActions(actions, 'formPermissionGroup', manifest.formPermissionGroups, existing.formPermissionGroups, (item, current) => formPermissionGroupEquals(target.bound, item, current));
2419
+ for (const item of manifest.formSettings || []) {
2420
+ actions.push({ kind: 'formSetting', code: item.code || item.formCode || item.formUuid, action: 'update' });
2421
+ }
2422
+ return {
2423
+ appType: target.appType,
2424
+ profile: target.profileName,
2425
+ actions,
2426
+ summary: summarizeActions(actions),
2170
2427
  };
2171
- saveProjectState(target.state);
2172
- target.bound = nextBound;
2173
2428
  }
2174
2429
 
2175
- function saveFormPermissionGroupResource(target, groupCode, groupId, extra = {}) {
2176
- target.state.profiles = target.state.profiles || {};
2177
- target.state.profiles[target.profileName] = {
2178
- ...target.bound,
2179
- baseUrl: target.profile.baseUrl,
2430
+ async function publishResourceManifest(config, target, manifest, options = {}) {
2431
+ const result = {
2180
2432
  appType: target.appType,
2181
- updatedAt: new Date().toISOString(),
2433
+ profile: target.profileName,
2434
+ published: [],
2435
+ pruned: [],
2436
+ warnings: [],
2182
2437
  };
2183
- const nextBound = target.state.profiles[target.profileName];
2184
- ensureResourceBuckets(nextBound);
2185
- nextBound.resources.formPermissionGroups[groupCode] = {
2186
- ...(nextBound.resources.formPermissionGroups[groupCode] || {}),
2187
- ...extra,
2188
- groupId,
2189
- updatedAt: new Date().toISOString(),
2438
+ if (isManifestEmpty(manifest)) {
2439
+ if (!options.quiet) result.warnings.push('未发现 src/resources 资源配置');
2440
+ if (!options.prune) return result;
2441
+ }
2442
+
2443
+ await publishRoleResources(config, target, manifest.roles || [], result);
2444
+ await publishFormSettingsResources(config, target, manifest.formSettings || [], result);
2445
+ await publishMenuResources(config, target, manifest.menus || [], result);
2446
+ await publishConnectorResources(config, target, manifest.connectors || [], result);
2447
+ await publishWorkflowResources(config, target, manifest.workflows || [], result);
2448
+ await publishAutomationResources(config, target, manifest.automations || [], result);
2449
+ await publishPagePermissionGroupResources(config, target, manifest.pagePermissionGroups || [], result);
2450
+ await publishFormPermissionGroupResources(config, target, manifest.formPermissionGroups || [], result);
2451
+ if (options.prune) {
2452
+ await pruneResourceManifest(config, target, manifest, result);
2453
+ }
2454
+ return result;
2455
+ }
2456
+
2457
+ function isManifestEmpty(manifest) {
2458
+ return RESOURCE_SPECS.every(spec => (manifest[spec.key] || []).length === 0);
2459
+ }
2460
+
2461
+ async function fetchExistingResourceMaps(config, target, manifest) {
2462
+ const maps = {
2463
+ roles: new Map(),
2464
+ menus: new Map(),
2465
+ connectors: new Map(),
2466
+ workflows: new Map(),
2467
+ automations: new Map(),
2468
+ pagePermissionGroups: new Map(),
2469
+ formPermissionGroups: new Map(),
2190
2470
  };
2191
- saveProjectState(target.state);
2192
- target.bound = nextBound;
2471
+
2472
+ if ((manifest.roles || []).length > 0) {
2473
+ const data = await requestWithAuth(
2474
+ config,
2475
+ target.profileName,
2476
+ apiPathWithQuery(`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/roles`, {
2477
+ page: 1,
2478
+ limit: 1000,
2479
+ })
2480
+ );
2481
+ indexByCode(maps.roles, normalizeItems(data), item => item.code);
2482
+ }
2483
+ if ((manifest.menus || []).length > 0) {
2484
+ const data = await requestWithAuth(
2485
+ config,
2486
+ target.profileName,
2487
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/menus`
2488
+ );
2489
+ indexByCode(maps.menus, flattenItems(normalizeItems(data)), item => item.resourceCode);
2490
+ }
2491
+ if ((manifest.connectors || []).length > 0) {
2492
+ const data = await requestWithAuth(
2493
+ config,
2494
+ target.profileName,
2495
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/connectors`
2496
+ );
2497
+ indexByCode(maps.connectors, normalizeItems(data), item => item.code || item.methodName);
2498
+ }
2499
+ if ((manifest.workflows || []).length > 0) {
2500
+ const data = await requestWithAuth(
2501
+ config,
2502
+ target.profileName,
2503
+ apiPathWithQuery(`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/workflows`, {
2504
+ page: 1,
2505
+ pageSize: 1000,
2506
+ })
2507
+ );
2508
+ indexByCode(maps.workflows, normalizeItems(data), item => item.resourceCode);
2509
+ }
2510
+ if ((manifest.automations || []).length > 0) {
2511
+ const data = await requestWithAuth(
2512
+ config,
2513
+ target.profileName,
2514
+ apiPathWithQuery(`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/automations`, {
2515
+ page: 1,
2516
+ pageSize: 1000,
2517
+ })
2518
+ );
2519
+ indexByCode(maps.automations, normalizeItems(data), item => item.resourceCode);
2520
+ }
2521
+ if ((manifest.pagePermissionGroups || []).length > 0) {
2522
+ const data = await requestWithAuth(
2523
+ config,
2524
+ target.profileName,
2525
+ apiPathWithQuery(`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/page-permission-groups`, {
2526
+ page: 1,
2527
+ limit: 1000,
2528
+ })
2529
+ );
2530
+ indexByCode(maps.pagePermissionGroups, normalizeItems(data), item => item.resourceCode);
2531
+ }
2532
+ if ((manifest.formPermissionGroups || []).length > 0) {
2533
+ for (const formUuid of unique((manifest.formPermissionGroups || []).map(item => resolveManifestFormUuid(target.bound, item)).filter(Boolean))) {
2534
+ const data = await requestWithAuth(
2535
+ config,
2536
+ target.profileName,
2537
+ apiPathWithQuery(`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/forms/${encodeURIComponent(formUuid)}/permission-groups`, {
2538
+ page: 1,
2539
+ limit: 1000,
2540
+ })
2541
+ );
2542
+ indexByCode(maps.formPermissionGroups, normalizeItems(data), item => item.resourceCode);
2543
+ }
2544
+ }
2545
+ return maps;
2546
+ }
2547
+
2548
+ function addPlanActions(actions, kind, desiredItems = [], existingMap, equals) {
2549
+ for (const item of desiredItems) {
2550
+ const code = item.code || item.resourceCode || item.methodName;
2551
+ const existing = existingMap.get(code);
2552
+ actions.push({
2553
+ kind,
2554
+ code,
2555
+ action: existing ? (equals(item, existing) ? 'noop' : 'update') : 'create',
2556
+ platformId: existing?.id,
2557
+ });
2558
+ }
2559
+ }
2560
+
2561
+ async function addWorkflowPlanActions(config, target, actions, desiredItems = [], existingMap) {
2562
+ for (const item of desiredItems) {
2563
+ const code = item.code || item.resourceCode;
2564
+ const existing = existingMap.get(code);
2565
+ if (!existing) {
2566
+ actions.push({ kind: 'workflow', code, action: 'create' });
2567
+ continue;
2568
+ }
2569
+ const detail = await requestOptionalWithAuth(
2570
+ config,
2571
+ target.profileName,
2572
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/workflows/${encodeURIComponent(existing.id)}`
2573
+ );
2574
+ actions.push({
2575
+ kind: 'workflow',
2576
+ code,
2577
+ action: workflowEquals(target.bound, item, detail || existing) ? 'noop' : 'update',
2578
+ platformId: existing.id,
2579
+ });
2580
+ }
2581
+ }
2582
+
2583
+ async function addAutomationPlanActions(config, target, actions, desiredItems = [], existingMap) {
2584
+ for (const item of desiredItems) {
2585
+ const code = item.code || item.resourceCode;
2586
+ const existing = existingMap.get(code);
2587
+ if (!existing) {
2588
+ actions.push({ kind: 'automation', code, action: 'create' });
2589
+ continue;
2590
+ }
2591
+ const detail = await requestOptionalWithAuth(
2592
+ config,
2593
+ target.profileName,
2594
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/automations/${encodeURIComponent(existing.id)}`
2595
+ );
2596
+ actions.push({
2597
+ kind: 'automation',
2598
+ code,
2599
+ action: automationEquals(target, item, detail || existing) ? 'noop' : 'update',
2600
+ platformId: existing.id,
2601
+ });
2602
+ }
2603
+ }
2604
+
2605
+ function summarizeActions(actions) {
2606
+ return actions.reduce(
2607
+ (acc, item) => {
2608
+ acc[item.action] = (acc[item.action] || 0) + 1;
2609
+ return acc;
2610
+ },
2611
+ { create: 0, update: 0, noop: 0, delete: 0 }
2612
+ );
2613
+ }
2614
+
2615
+ function normalizeItems(data) {
2616
+ if (!data) return [];
2617
+ if (Array.isArray(data)) return data;
2618
+ if (Array.isArray(data.items)) return data.items;
2619
+ if (Array.isArray(data.data)) return data.data;
2620
+ if (Array.isArray(data.data?.items)) return data.data.items;
2621
+ return [];
2622
+ }
2623
+
2624
+ function flattenItems(items) {
2625
+ const result = [];
2626
+ const visit = item => {
2627
+ if (!item || typeof item !== 'object') return;
2628
+ result.push(item);
2629
+ for (const child of item.children || []) visit(child);
2630
+ };
2631
+ for (const item of items || []) visit(item);
2632
+ return result;
2633
+ }
2634
+
2635
+ function indexByCode(map, items, getCode) {
2636
+ for (const item of items || []) {
2637
+ const code = getCode(item);
2638
+ if (code) map.set(String(code), item);
2639
+ }
2640
+ }
2641
+
2642
+ async function requestOptionalWithAuth(config, profileName, apiPath, options = {}) {
2643
+ try {
2644
+ return await requestWithAuth(config, profileName, apiPath, options);
2645
+ } catch (error) {
2646
+ if (Number(error?.apiCode) === 404 || String(error?.message || '').includes('HTTP 404')) {
2647
+ return null;
2648
+ }
2649
+ throw error;
2650
+ }
2651
+ }
2652
+
2653
+ async function publishRoleResources(config, target, roles, result) {
2654
+ for (const role of roles) {
2655
+ const existing = await findExistingRole(config, target, role.code);
2656
+ const body = {
2657
+ code: role.code,
2658
+ name: role.name || role.code,
2659
+ description: role.description || '',
2660
+ };
2661
+ const data = existing
2662
+ ? await requestWithAuth(
2663
+ config,
2664
+ target.profileName,
2665
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/roles/${encodeURIComponent(existing.id)}`,
2666
+ { method: 'PUT', body }
2667
+ )
2668
+ : await requestWithAuth(
2669
+ config,
2670
+ target.profileName,
2671
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/roles`,
2672
+ { method: 'POST', body }
2673
+ );
2674
+ if (data?.id) {
2675
+ saveRoleResource(target, role.code, data.id, { code: data.code, name: data.name });
2676
+ }
2677
+ if (Array.isArray(role.userIds) && role.userIds.length > 0 && data?.id) {
2678
+ await requestWithAuth(
2679
+ config,
2680
+ target.profileName,
2681
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/roles/${encodeURIComponent(data.id)}/users`,
2682
+ { method: 'POST', body: { userIds: role.userIds } }
2683
+ );
2684
+ }
2685
+ result.published.push({ kind: 'role', code: role.code, action: existing ? 'update' : 'create', id: data?.id });
2686
+ }
2687
+ }
2688
+
2689
+ async function findExistingRole(config, target, code) {
2690
+ const data = await requestWithAuth(
2691
+ config,
2692
+ target.profileName,
2693
+ apiPathWithQuery(`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/roles`, {
2694
+ code,
2695
+ page: 1,
2696
+ limit: 1,
2697
+ })
2698
+ );
2699
+ return normalizeItems(data).find(item => item.code === code);
2700
+ }
2701
+
2702
+ async function publishFormSettingsResources(config, target, settingsItems, result) {
2703
+ for (const item of settingsItems) {
2704
+ const code = item.code || item.formCode || item.formUuid;
2705
+ const formUuid = resolveManifestFormUuid(target.bound, item, { fallbackToCode: true });
2706
+ if (!formUuid) fail(`表单设置 ${code} 无法解析 formUuid`);
2707
+ if (item.settings !== undefined) {
2708
+ await requestWithAuth(
2709
+ config,
2710
+ target.profileName,
2711
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/forms/${encodeURIComponent(formUuid)}/settings`,
2712
+ { method: 'PUT', body: { settings: item.settings } }
2713
+ );
2714
+ }
2715
+ if (item.indexes !== undefined) {
2716
+ await requestWithAuth(
2717
+ config,
2718
+ target.profileName,
2719
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/forms/${encodeURIComponent(formUuid)}/field-indexes`,
2720
+ { method: 'PUT', body: { indexes: item.indexes } }
2721
+ );
2722
+ }
2723
+ if (item.dataManagement !== undefined) {
2724
+ await requestWithAuth(
2725
+ config,
2726
+ target.profileName,
2727
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/forms/${encodeURIComponent(formUuid)}/data-management`,
2728
+ { method: 'PUT', body: { config: item.dataManagement } }
2729
+ );
2730
+ }
2731
+ if (item.publicAccess !== undefined) {
2732
+ const publicAccess =
2733
+ typeof item.publicAccess === 'object'
2734
+ ? item.publicAccess
2735
+ : { isPublic: Boolean(item.publicAccess) };
2736
+ await requestWithAuth(
2737
+ config,
2738
+ target.profileName,
2739
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/forms/${encodeURIComponent(formUuid)}/public-access`,
2740
+ {
2741
+ method: publicAccess.isPublic === false ? 'DELETE' : 'PUT',
2742
+ body: {
2743
+ isPublic: publicAccess.isPublic !== false,
2744
+ description: publicAccess.description || '',
2745
+ },
2746
+ }
2747
+ );
2748
+ }
2749
+ saveResourceEntry(target, 'formSettings', code, { formUuid });
2750
+ result.published.push({ kind: 'formSetting', code, action: 'update', formUuid });
2751
+ }
2752
+ }
2753
+
2754
+ async function publishMenuResources(config, target, menus, result) {
2755
+ for (const menuItem of menus) {
2756
+ const existing = await findExistingMenu(config, target, menuItem.code);
2757
+ const formUuid = resolveManifestFormUuid(target.bound, menuItem);
2758
+ const pageId = resolveManifestPageId(target.bound, menuItem);
2759
+ const parentId =
2760
+ resolveManifestMenuId(target.bound, menuItem.parentCode) ||
2761
+ menuItem.parentId ||
2762
+ null;
2763
+ const body = {
2764
+ resourceCode: menuItem.code,
2765
+ name: menuItem.name || menuItem.code,
2766
+ type: menuItem.type || 'nav',
2767
+ formUuid,
2768
+ pageId,
2769
+ parentId,
2770
+ sortOrder: menuItem.sortOrder,
2771
+ icon: menuItem.icon || null,
2772
+ isHidden: menuItem.isHidden,
2773
+ };
2774
+ const data = existing
2775
+ ? await requestWithAuth(
2776
+ config,
2777
+ target.profileName,
2778
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/menus/${encodeURIComponent(existing.id)}`,
2779
+ { method: 'PUT', body }
2780
+ )
2781
+ : await requestWithAuth(
2782
+ config,
2783
+ target.profileName,
2784
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/menus`,
2785
+ { method: 'POST', body }
2786
+ );
2787
+ if (data?.id) {
2788
+ saveMenuResource(target, menuItem.code, data.id, {
2789
+ resourceCode: menuItem.code,
2790
+ name: data.name,
2791
+ type: data.type,
2792
+ ...(formUuid ? { formUuid } : {}),
2793
+ ...(pageId ? { pageId } : {}),
2794
+ ...(menuItem.pageCode ? { pageCode: menuItem.pageCode } : {}),
2795
+ });
2796
+ }
2797
+ result.published.push({ kind: 'menu', code: menuItem.code, action: existing ? 'update' : 'create', id: data?.id });
2798
+ }
2799
+ }
2800
+
2801
+ async function findExistingMenu(config, target, code) {
2802
+ const data = await requestWithAuth(
2803
+ config,
2804
+ target.profileName,
2805
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/menus`
2806
+ );
2807
+ const items = flattenItems(normalizeItems(data));
2808
+ const stateId = target.bound.resources?.menus?.[code]?.menuId;
2809
+ if (stateId) {
2810
+ const byState = items.find(item => item.id === stateId);
2811
+ if (byState) return byState;
2812
+ }
2813
+ return items.find(item => item.resourceCode === code);
2814
+ }
2815
+
2816
+ async function publishConnectorResources(config, target, connectors, result) {
2817
+ if (connectors.length === 0) return;
2818
+ const body = { connectors: connectors.map(normalizeConnectorManifest) };
2819
+ const data = await requestWithAuth(
2820
+ config,
2821
+ target.profileName,
2822
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/connectors/actions/sync`,
2823
+ { method: 'POST', body }
2824
+ );
2825
+ for (const item of data.data || []) {
2826
+ const apiEntries = {};
2827
+ for (const api of item.apis || []) {
2828
+ const apiCode = api.code || api.methodName;
2829
+ if (apiCode) apiEntries[apiCode] = { apiId: api.id, name: api.name };
2830
+ }
2831
+ saveConnectorResource(target, item.code, item.connector?.id, {
2832
+ code: item.code,
2833
+ name: item.connector?.name,
2834
+ apis: apiEntries,
2835
+ });
2836
+ result.published.push({
2837
+ kind: 'connector',
2838
+ code: item.code,
2839
+ action: item.created ? 'create' : 'update',
2840
+ id: item.connector?.id,
2841
+ });
2842
+ }
2843
+ }
2844
+
2845
+ async function publishWorkflowResources(config, target, workflows, result) {
2846
+ for (const workflowItem of workflows) {
2847
+ const existing = await findExistingWorkflow(config, target, workflowItem.code);
2848
+ const definitionJson = await resolveManifestJson(
2849
+ config,
2850
+ target.profileName,
2851
+ workflowItem,
2852
+ 'definitionJson',
2853
+ 'definitionFile'
2854
+ );
2855
+ const viewJson = await resolveManifestJson(
2856
+ config,
2857
+ target.profileName,
2858
+ workflowItem,
2859
+ 'viewJson',
2860
+ 'viewFile',
2861
+ true
2862
+ );
2863
+ const formUuid = resolveManifestFormUuid(target.bound, workflowItem);
2864
+ const body = {
2865
+ resourceCode: workflowItem.code,
2866
+ formUuid,
2867
+ definitionJson,
2868
+ ...(viewJson !== undefined ? { viewJson } : {}),
2869
+ };
2870
+ const data = existing
2871
+ ? await requestWithAuth(
2872
+ config,
2873
+ target.profileName,
2874
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/workflows/${encodeURIComponent(existing.id)}`,
2875
+ { method: 'PUT', body }
2876
+ )
2877
+ : await requestWithAuth(
2878
+ config,
2879
+ target.profileName,
2880
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/workflows`,
2881
+ { method: 'POST', body }
2882
+ );
2883
+ if (workflowItem.publish && data?.id) {
2884
+ await requestWithAuth(
2885
+ config,
2886
+ target.profileName,
2887
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/workflows/${encodeURIComponent(data.id)}/publish`,
2888
+ { method: 'POST', body: { isPublished: true } }
2889
+ );
2890
+ }
2891
+ if (data?.id) saveWorkflowResource(target, workflowItem.code, data.id, { formUuid, resourceCode: workflowItem.code });
2892
+ result.published.push({ kind: 'workflow', code: workflowItem.code, action: existing ? 'update' : 'create', id: data?.id });
2893
+ }
2894
+ }
2895
+
2896
+ async function findExistingWorkflow(config, target, code) {
2897
+ const stateId = target.bound.resources?.workflows?.[code]?.workflowId;
2898
+ if (stateId) {
2899
+ const stateItem = await requestOptionalWithAuth(
2900
+ config,
2901
+ target.profileName,
2902
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/workflows/${encodeURIComponent(stateId)}`
2903
+ );
2904
+ if (stateItem?.id) return stateItem;
2905
+ }
2906
+ const data = await requestWithAuth(
2907
+ config,
2908
+ target.profileName,
2909
+ apiPathWithQuery(`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/workflows`, {
2910
+ resourceCode: code,
2911
+ page: 1,
2912
+ pageSize: 1000,
2913
+ })
2914
+ );
2915
+ return normalizeItems(data).find(item => item.resourceCode === code);
2916
+ }
2917
+
2918
+ async function publishAutomationResources(config, target, automations, result) {
2919
+ for (const automationItem of automations) {
2920
+ const existing = await findExistingAutomation(config, target, automationItem.code);
2921
+ const definitionJson = await resolveManifestJson(
2922
+ config,
2923
+ target.profileName,
2924
+ automationItem,
2925
+ 'definitionJson',
2926
+ 'definitionFile'
2927
+ );
2928
+ const viewJson = await resolveManifestJson(
2929
+ config,
2930
+ target.profileName,
2931
+ automationItem,
2932
+ 'viewJson',
2933
+ 'viewFile',
2934
+ true
2935
+ );
2936
+ const automationPayload = resolveAutomationManifestPayload(target, automationItem);
2937
+ const body = {
2938
+ resourceCode: automationItem.code,
2939
+ name: automationItem.name || automationItem.code,
2940
+ description: automationItem.description || '',
2941
+ formUuid: automationPayload.formUuid,
2942
+ triggerConfig: automationPayload.triggerConfig,
2943
+ definitionJson,
2944
+ ...(viewJson !== undefined ? { viewJson } : {}),
2945
+ ...(automationItem.tags !== undefined ? { tags: Array.isArray(automationItem.tags) ? automationItem.tags.join(',') : automationItem.tags } : {}),
2946
+ ...(automationItem.isEnabled !== undefined ? { isEnabled: Boolean(automationItem.isEnabled) } : {}),
2947
+ };
2948
+ const data = existing
2949
+ ? await requestWithAuth(
2950
+ config,
2951
+ target.profileName,
2952
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/automations/${encodeURIComponent(existing.id)}`,
2953
+ { method: 'PUT', body }
2954
+ )
2955
+ : await requestWithAuth(
2956
+ config,
2957
+ target.profileName,
2958
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/automations`,
2959
+ { method: 'POST', body }
2960
+ );
2961
+ if (automationItem.publish && data?.id) {
2962
+ await requestWithAuth(
2963
+ config,
2964
+ target.profileName,
2965
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/automations/${encodeURIComponent(data.id)}/publish`,
2966
+ { method: 'POST' }
2967
+ );
2968
+ }
2969
+ if (automationItem.enable !== undefined && data?.id) {
2970
+ await requestWithAuth(
2971
+ config,
2972
+ target.profileName,
2973
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/automations/${encodeURIComponent(data.id)}/${automationItem.enable ? 'enable' : 'disable'}`,
2974
+ { method: 'POST' }
2975
+ );
2976
+ }
2977
+ if (data?.id) saveAutomationResource(target, automationItem.code, data.id, { formUuid: automationPayload.formUuid, resourceCode: automationItem.code });
2978
+ result.published.push({ kind: 'automation', code: automationItem.code, action: existing ? 'update' : 'create', id: data?.id });
2979
+ }
2980
+ }
2981
+
2982
+ async function findExistingAutomation(config, target, code) {
2983
+ const stateId = target.bound.resources?.automations?.[code]?.automationId;
2984
+ if (stateId) {
2985
+ const stateItem = await requestOptionalWithAuth(
2986
+ config,
2987
+ target.profileName,
2988
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/automations/${encodeURIComponent(stateId)}`
2989
+ );
2990
+ if (stateItem?.id) return stateItem;
2991
+ }
2992
+ const data = await requestWithAuth(
2993
+ config,
2994
+ target.profileName,
2995
+ apiPathWithQuery(`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/automations`, {
2996
+ resourceCode: code,
2997
+ page: 1,
2998
+ pageSize: 1000,
2999
+ })
3000
+ );
3001
+ return normalizeItems(data).find(item => item.resourceCode === code);
3002
+ }
3003
+
3004
+ async function publishPagePermissionGroupResources(config, target, groups, result) {
3005
+ for (const group of groups) {
3006
+ const existing = await findExistingPagePermissionGroup(config, target, group.code);
3007
+ const body = {
3008
+ resourceCode: group.code,
3009
+ name: group.name || group.code,
3010
+ roles: group.roles || [],
3011
+ menuFormUuids: resolvePagePermissionGroupTargets(target.bound, group),
3012
+ };
3013
+ const data = existing
3014
+ ? await requestWithAuth(
3015
+ config,
3016
+ target.profileName,
3017
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/page-permission-groups/${encodeURIComponent(existing.id)}`,
3018
+ { method: 'PUT', body }
3019
+ )
3020
+ : await requestWithAuth(
3021
+ config,
3022
+ target.profileName,
3023
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/page-permission-groups`,
3024
+ { method: 'POST', body }
3025
+ );
3026
+ if (data?.id) savePagePermissionGroupResource(target, group.code, data.id, { name: data.name, resourceCode: group.code });
3027
+ result.published.push({ kind: 'pagePermissionGroup', code: group.code, action: existing ? 'update' : 'create', id: data?.id });
3028
+ }
3029
+ }
3030
+
3031
+ async function findExistingPagePermissionGroup(config, target, code) {
3032
+ const stateId = target.bound.resources?.pagePermissionGroups?.[code]?.groupId;
3033
+ if (stateId) {
3034
+ const stateItem = await requestOptionalWithAuth(
3035
+ config,
3036
+ target.profileName,
3037
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/page-permission-groups/${encodeURIComponent(stateId)}`
3038
+ );
3039
+ if (stateItem?.id) return stateItem;
3040
+ }
3041
+ const data = await requestWithAuth(
3042
+ config,
3043
+ target.profileName,
3044
+ apiPathWithQuery(`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/page-permission-groups`, {
3045
+ resourceCode: code,
3046
+ page: 1,
3047
+ limit: 1000,
3048
+ })
3049
+ );
3050
+ return normalizeItems(data).find(item => item.resourceCode === code);
3051
+ }
3052
+
3053
+ async function publishFormPermissionGroupResources(config, target, groups, result) {
3054
+ for (const group of groups) {
3055
+ const formUuid = resolveManifestFormUuid(target.bound, group);
3056
+ const existing = await findExistingFormPermissionGroup(config, target, group.code, formUuid);
3057
+ const body = {
3058
+ ...withoutResourceMeta(group),
3059
+ resourceCode: group.code,
3060
+ formUuid,
3061
+ name: group.name || group.code,
3062
+ type: group.type || 'view',
3063
+ roles: group.roles || [],
3064
+ };
3065
+ delete body.code;
3066
+ delete body.formCode;
3067
+ delete body.form;
3068
+ const data = existing
3069
+ ? await requestWithAuth(
3070
+ config,
3071
+ target.profileName,
3072
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/forms/${encodeURIComponent(formUuid)}/permission-groups/${encodeURIComponent(existing.id)}`,
3073
+ { method: 'PUT', body }
3074
+ )
3075
+ : await requestWithAuth(
3076
+ config,
3077
+ target.profileName,
3078
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/forms/${encodeURIComponent(formUuid)}/permission-groups`,
3079
+ { method: 'POST', body }
3080
+ );
3081
+ if (data?.id) saveFormPermissionGroupResource(target, group.code, data.id, { formUuid, name: data.name, resourceCode: group.code });
3082
+ result.published.push({ kind: 'formPermissionGroup', code: group.code, action: existing ? 'update' : 'create', id: data?.id });
3083
+ }
3084
+ }
3085
+
3086
+ async function findExistingFormPermissionGroup(config, target, code, formUuid) {
3087
+ const stateId = target.bound.resources?.formPermissionGroups?.[code]?.groupId;
3088
+ if (stateId) {
3089
+ const stateItem = await requestOptionalWithAuth(
3090
+ config,
3091
+ target.profileName,
3092
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/forms/${encodeURIComponent(formUuid)}/permission-groups/${encodeURIComponent(stateId)}`
3093
+ );
3094
+ if (stateItem?.id) return stateItem;
3095
+ }
3096
+ const data = await requestWithAuth(
3097
+ config,
3098
+ target.profileName,
3099
+ apiPathWithQuery(`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/forms/${encodeURIComponent(formUuid)}/permission-groups`, {
3100
+ resourceCode: code,
3101
+ page: 1,
3102
+ limit: 1000,
3103
+ })
3104
+ );
3105
+ return normalizeItems(data).find(item => item.resourceCode === code);
3106
+ }
3107
+
3108
+ async function pruneResourceManifest(config, target, manifest, result) {
3109
+ await pruneRoles(config, target, desiredCodes(manifest.roles), result);
3110
+ await pruneConnectors(config, target, desiredCodes(manifest.connectors), result);
3111
+ await pruneMenus(config, target, desiredCodes(manifest.menus), result);
3112
+ await pruneWorkflows(config, target, desiredCodes(manifest.workflows), result);
3113
+ await pruneAutomations(config, target, desiredCodes(manifest.automations), result);
3114
+ await prunePagePermissionGroups(
3115
+ config,
3116
+ target,
3117
+ desiredCodes(manifest.pagePermissionGroups),
3118
+ result
3119
+ );
3120
+ await pruneFormPermissionGroups(
3121
+ config,
3122
+ target,
3123
+ desiredCodes(manifest.formPermissionGroups),
3124
+ result
3125
+ );
3126
+ }
3127
+
3128
+ function desiredCodes(items = []) {
3129
+ return new Set(
3130
+ items
3131
+ .map(item => item.code || item.resourceCode || item.methodName)
3132
+ .filter(Boolean)
3133
+ .map(String)
3134
+ );
3135
+ }
3136
+
3137
+ async function pruneRoles(config, target, desired, result) {
3138
+ const data = await requestWithAuth(
3139
+ config,
3140
+ target.profileName,
3141
+ apiPathWithQuery(`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/roles`, {
3142
+ page: 1,
3143
+ limit: 1000,
3144
+ })
3145
+ );
3146
+ for (const role of normalizeItems(data)) {
3147
+ if (!role.code || desired.has(role.code) || role.isAppAdmin) continue;
3148
+ await pruneOne(config, target, result, 'role', role.code, role.id, async () => {
3149
+ await requestWithAuth(
3150
+ config,
3151
+ target.profileName,
3152
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/roles/${encodeURIComponent(role.id)}`,
3153
+ { method: 'DELETE' }
3154
+ );
3155
+ });
3156
+ }
3157
+ }
3158
+
3159
+ async function pruneConnectors(config, target, desired, result) {
3160
+ const data = await requestWithAuth(
3161
+ config,
3162
+ target.profileName,
3163
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/connectors`
3164
+ );
3165
+ for (const connector of normalizeItems(data)) {
3166
+ const code = connector.code || connector.methodName;
3167
+ if (!code || desired.has(code)) continue;
3168
+ await pruneOne(config, target, result, 'connector', code, connector.id, async () => {
3169
+ await requestWithAuth(
3170
+ config,
3171
+ target.profileName,
3172
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/connectors/${encodeURIComponent(code)}`,
3173
+ { method: 'DELETE' }
3174
+ );
3175
+ });
3176
+ }
3177
+ }
3178
+
3179
+ async function pruneMenus(config, target, desired, result) {
3180
+ const data = await requestWithAuth(
3181
+ config,
3182
+ target.profileName,
3183
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/menus`
3184
+ );
3185
+ const items = flattenItems(normalizeItems(data)).reverse();
3186
+ for (const menu of items) {
3187
+ const code = menu.resourceCode;
3188
+ if (!code || desired.has(code)) continue;
3189
+ await pruneOne(config, target, result, 'menu', code, menu.id, async () => {
3190
+ await requestWithAuth(
3191
+ config,
3192
+ target.profileName,
3193
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/menus/${encodeURIComponent(menu.id)}`,
3194
+ { method: 'DELETE' }
3195
+ );
3196
+ });
3197
+ }
3198
+ }
3199
+
3200
+ async function pruneWorkflows(config, target, desired, result) {
3201
+ const data = await requestWithAuth(
3202
+ config,
3203
+ target.profileName,
3204
+ apiPathWithQuery(`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/workflows`, {
3205
+ page: 1,
3206
+ pageSize: 1000,
3207
+ })
3208
+ );
3209
+ for (const workflow of normalizeItems(data)) {
3210
+ const code = workflow.resourceCode;
3211
+ if (!code || desired.has(code)) continue;
3212
+ await pruneOne(config, target, result, 'workflow', code, workflow.id, async () => {
3213
+ if (workflow.isPublished) {
3214
+ await requestWithAuth(
3215
+ config,
3216
+ target.profileName,
3217
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/workflows/${encodeURIComponent(workflow.id)}/publish`,
3218
+ { method: 'POST', body: { isPublished: false } }
3219
+ );
3220
+ }
3221
+ await requestWithAuth(
3222
+ config,
3223
+ target.profileName,
3224
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/workflows/${encodeURIComponent(workflow.id)}`,
3225
+ { method: 'DELETE' }
3226
+ );
3227
+ });
3228
+ }
3229
+ }
3230
+
3231
+ async function pruneAutomations(config, target, desired, result) {
3232
+ const data = await requestWithAuth(
3233
+ config,
3234
+ target.profileName,
3235
+ apiPathWithQuery(`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/automations`, {
3236
+ page: 1,
3237
+ pageSize: 1000,
3238
+ })
3239
+ );
3240
+ for (const automation of normalizeItems(data)) {
3241
+ const code = automation.resourceCode;
3242
+ if (!code || desired.has(code)) continue;
3243
+ await pruneOne(config, target, result, 'automation', code, automation.id, async () => {
3244
+ if (automation.isPublished) {
3245
+ await requestWithAuth(
3246
+ config,
3247
+ target.profileName,
3248
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/automations/${encodeURIComponent(automation.id)}/unpublish`,
3249
+ { method: 'POST' }
3250
+ );
3251
+ }
3252
+ await requestWithAuth(
3253
+ config,
3254
+ target.profileName,
3255
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/automations/${encodeURIComponent(automation.id)}`,
3256
+ { method: 'DELETE' }
3257
+ );
3258
+ });
3259
+ }
3260
+ }
3261
+
3262
+ async function prunePagePermissionGroups(config, target, desired, result) {
3263
+ const data = await requestWithAuth(
3264
+ config,
3265
+ target.profileName,
3266
+ apiPathWithQuery(`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/page-permission-groups`, {
3267
+ page: 1,
3268
+ limit: 1000,
3269
+ })
3270
+ );
3271
+ for (const group of normalizeItems(data)) {
3272
+ const code = group.resourceCode;
3273
+ if (!code || desired.has(code)) continue;
3274
+ await pruneOne(config, target, result, 'pagePermissionGroup', code, group.id, async () => {
3275
+ await requestWithAuth(
3276
+ config,
3277
+ target.profileName,
3278
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/page-permission-groups/${encodeURIComponent(group.id)}`,
3279
+ { method: 'DELETE' }
3280
+ );
3281
+ });
3282
+ }
3283
+ }
3284
+
3285
+ async function pruneFormPermissionGroups(config, target, desired, result) {
3286
+ const forms = await requestWithAuth(
3287
+ config,
3288
+ target.profileName,
3289
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/forms`
3290
+ );
3291
+ for (const form of normalizeItems(forms)) {
3292
+ const formUuid = form.formUuid;
3293
+ if (!formUuid) continue;
3294
+ const data = await requestWithAuth(
3295
+ config,
3296
+ target.profileName,
3297
+ apiPathWithQuery(`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/forms/${encodeURIComponent(formUuid)}/permission-groups`, {
3298
+ page: 1,
3299
+ limit: 1000,
3300
+ })
3301
+ );
3302
+ for (const group of normalizeItems(data)) {
3303
+ const code = group.resourceCode;
3304
+ if (!code || desired.has(code)) continue;
3305
+ await pruneOne(config, target, result, 'formPermissionGroup', code, group.id, async () => {
3306
+ await requestWithAuth(
3307
+ config,
3308
+ target.profileName,
3309
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/forms/${encodeURIComponent(formUuid)}/permission-groups/${encodeURIComponent(group.id)}`,
3310
+ { method: 'DELETE' }
3311
+ );
3312
+ });
3313
+ }
3314
+ }
3315
+ }
3316
+
3317
+ async function pruneOne(config, target, result, kind, code, id, deleter) {
3318
+ try {
3319
+ await deleter();
3320
+ removeStateResource(target, kind, code);
3321
+ result.pruned.push({ kind, code, id });
3322
+ } catch (error) {
3323
+ result.warnings.push(
3324
+ `prune ${kind}:${code} failed: ${error?.message || String(error)}`
3325
+ );
3326
+ }
3327
+ }
3328
+
3329
+ function removeStateResource(target, kind, code) {
3330
+ const bucketByKind = {
3331
+ role: 'roles',
3332
+ connector: 'connectors',
3333
+ menu: 'menus',
3334
+ workflow: 'workflows',
3335
+ automation: 'automations',
3336
+ pagePermissionGroup: 'pagePermissionGroups',
3337
+ formPermissionGroup: 'formPermissionGroups',
3338
+ };
3339
+ const bucket = bucketByKind[kind];
3340
+ if (!bucket || !target.bound.resources?.[bucket]?.[code]) return;
3341
+ delete target.bound.resources[bucket][code];
3342
+ target.bound.updatedAt = new Date().toISOString();
3343
+ if (target.state.profiles?.[target.profileName]) {
3344
+ target.state.profiles[target.profileName] = target.bound;
3345
+ }
3346
+ saveProjectState(target.state);
3347
+ }
3348
+
3349
+ async function pullResources(config, target) {
3350
+ const baseDir = path.join(process.cwd(), 'src', 'resources');
3351
+ const written = [];
3352
+ const pullLookups = buildPullResourceLookups(target.bound);
3353
+ const connectors = await requestWithAuth(
3354
+ config,
3355
+ target.profileName,
3356
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/connectors`
3357
+ );
3358
+ for (const connector of normalizeItems(connectors)) {
3359
+ const filePath = path.join(baseDir, 'connectors', `${connector.code || connector.methodName}.json`);
3360
+ writeResourceJsonFile(filePath, stripPulledResource(connector));
3361
+ written.push(path.relative(process.cwd(), filePath));
3362
+ }
3363
+
3364
+ const roles = await requestWithAuth(
3365
+ config,
3366
+ target.profileName,
3367
+ apiPathWithQuery(`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/roles`, {
3368
+ page: 1,
3369
+ limit: 1000,
3370
+ })
3371
+ );
3372
+ for (const role of normalizeItems(roles)) {
3373
+ const filePath = path.join(baseDir, 'roles', `${role.code}.json`);
3374
+ writeResourceJsonFile(filePath, {
3375
+ code: role.code,
3376
+ name: role.name,
3377
+ description: role.description || '',
3378
+ });
3379
+ written.push(path.relative(process.cwd(), filePath));
3380
+ }
3381
+
3382
+ const menus = await requestWithAuth(
3383
+ config,
3384
+ target.profileName,
3385
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/menus`
3386
+ );
3387
+ const flatMenus = flattenItems(normalizeItems(menus));
3388
+ for (const menu of flatMenus) {
3389
+ if (menu.id && menu.resourceCode) pullLookups.menuCodeById.set(menu.id, menu.resourceCode);
3390
+ }
3391
+ for (const menu of flatMenus) {
3392
+ const code = menu.resourceCode || menu.id;
3393
+ const filePath = path.join(baseDir, 'menus', `${code}.json`);
3394
+ const formCode = pullLookups.formCodeByUuid.get(menu.formUuid);
3395
+ const pageCode = pullLookups.pageCodeById.get(menu.pageId);
3396
+ const parentCode = pullLookups.menuCodeById.get(menu.parentId);
3397
+ writeResourceJsonFile(filePath, {
3398
+ code,
3399
+ name: menu.name,
3400
+ type: menu.type,
3401
+ ...(formCode ? { formCode } : menu.formUuid ? { formUuid: menu.formUuid } : {}),
3402
+ ...(pageCode ? { pageCode } : menu.pageId ? { pageId: menu.pageId } : {}),
3403
+ ...(parentCode ? { parentCode } : menu.parentId ? { parentId: menu.parentId } : {}),
3404
+ sortOrder: menu.sortOrder,
3405
+ icon: menu.icon || undefined,
3406
+ isHidden: menu.isHidden,
3407
+ });
3408
+ written.push(path.relative(process.cwd(), filePath));
3409
+ }
3410
+
3411
+ const workflows = await requestWithAuth(
3412
+ config,
3413
+ target.profileName,
3414
+ apiPathWithQuery(`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/workflows`, {
3415
+ page: 1,
3416
+ pageSize: 1000,
3417
+ })
3418
+ );
3419
+ for (const workflow of normalizeItems(workflows)) {
3420
+ const detail = await requestWithAuth(
3421
+ config,
3422
+ target.profileName,
3423
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/workflows/${encodeURIComponent(workflow.id)}`
3424
+ );
3425
+ const code = detail.resourceCode || workflow.resourceCode || workflow.id;
3426
+ const filePath = path.join(baseDir, 'workflows', `${code}.json`);
3427
+ const formCode = pullLookups.formCodeByUuid.get(detail.formUuid);
3428
+ writeResourceJsonFile(filePath, {
3429
+ code,
3430
+ ...(formCode ? { formCode } : detail.formUuid ? { formUuid: detail.formUuid } : {}),
3431
+ definitionJson: detail.definitionJson,
3432
+ viewJson: detail.viewJson || undefined,
3433
+ publish: Boolean(detail.isPublished),
3434
+ });
3435
+ written.push(path.relative(process.cwd(), filePath));
3436
+ }
3437
+
3438
+ const automations = await requestWithAuth(
3439
+ config,
3440
+ target.profileName,
3441
+ apiPathWithQuery(`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/automations`, {
3442
+ page: 1,
3443
+ pageSize: 1000,
3444
+ })
3445
+ );
3446
+ for (const automation of normalizeItems(automations)) {
3447
+ const detail = await requestWithAuth(
3448
+ config,
3449
+ target.profileName,
3450
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/automations/${encodeURIComponent(automation.id)}`
3451
+ );
3452
+ const code = detail.resourceCode || automation.resourceCode || automation.id;
3453
+ const filePath = path.join(baseDir, 'automations', `${code}.json`);
3454
+ const formCode = pullLookups.formCodeByUuid.get(detail.formUuid);
3455
+ writeResourceJsonFile(filePath, {
3456
+ code,
3457
+ name: detail.name,
3458
+ description: detail.description || '',
3459
+ ...(formCode ? { formCode } : detail.formUuid ? { formUuid: detail.formUuid } : {}),
3460
+ triggerConfig: detail.triggerConfig,
3461
+ definitionJson: detail.definitionJson,
3462
+ viewJson: detail.viewJson || undefined,
3463
+ tags: detail.tags || undefined,
3464
+ publish: Boolean(detail.isPublished),
3465
+ enable: Boolean(detail.isEnabled),
3466
+ });
3467
+ written.push(path.relative(process.cwd(), filePath));
3468
+ }
3469
+
3470
+ const pageGroups = await requestWithAuth(
3471
+ config,
3472
+ target.profileName,
3473
+ apiPathWithQuery(`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/page-permission-groups`, {
3474
+ page: 1,
3475
+ limit: 1000,
3476
+ })
3477
+ );
3478
+ for (const group of normalizeItems(pageGroups)) {
3479
+ const code = group.resourceCode || group.id;
3480
+ const filePath = path.join(baseDir, 'permissions', 'page-groups', `${code}.json`);
3481
+ const targets = splitPagePermissionTargetsForManifest(group.menuFormUuids || [], pullLookups);
3482
+ writeResourceJsonFile(filePath, {
3483
+ code,
3484
+ name: group.name,
3485
+ roles: group.roles || [],
3486
+ ...targets,
3487
+ });
3488
+ written.push(path.relative(process.cwd(), filePath));
3489
+ }
3490
+
3491
+ const forms = await requestWithAuth(
3492
+ config,
3493
+ target.profileName,
3494
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/forms`
3495
+ );
3496
+ const stateFormCodesByUuid = new Map(
3497
+ Object.entries(target.bound.resources?.forms || {})
3498
+ .filter(([, entry]) => entry?.formUuid)
3499
+ .map(([code, entry]) => [entry.formUuid, code])
3500
+ );
3501
+ const formEntries = normalizeItems(forms).map(form => [
3502
+ stateFormCodesByUuid.get(form.formUuid) || form.formUuid,
3503
+ { formUuid: form.formUuid },
3504
+ ]);
3505
+ for (const [formCode, formEntry] of formEntries) {
3506
+ const formUuid = formEntry?.formUuid;
3507
+ if (!formUuid) continue;
3508
+ const formGroups = await requestWithAuth(
3509
+ config,
3510
+ target.profileName,
3511
+ apiPathWithQuery(`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/forms/${encodeURIComponent(formUuid)}/permission-groups`, {
3512
+ page: 1,
3513
+ limit: 1000,
3514
+ })
3515
+ );
3516
+ for (const group of normalizeItems(formGroups)) {
3517
+ const code = group.resourceCode || group.id;
3518
+ const filePath = path.join(baseDir, 'permissions', 'form-groups', `${code}.json`);
3519
+ const mappedFormCode = pullLookups.formCodeByUuid.get(formUuid);
3520
+ const pulledGroup = stripPulledResource(group);
3521
+ if (mappedFormCode) delete pulledGroup.formUuid;
3522
+ writeResourceJsonFile(filePath, {
3523
+ ...pulledGroup,
3524
+ code,
3525
+ ...(mappedFormCode ? { formCode: mappedFormCode } : { formUuid }),
3526
+ });
3527
+ written.push(path.relative(process.cwd(), filePath));
3528
+ }
3529
+
3530
+ const mappedFormCode = pullLookups.formCodeByUuid.get(formUuid);
3531
+ const formSetting = {
3532
+ code: mappedFormCode || formCode,
3533
+ ...(mappedFormCode ? { formCode: mappedFormCode } : { formUuid }),
3534
+ };
3535
+ formSetting.settings = await requestWithAuth(
3536
+ config,
3537
+ target.profileName,
3538
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/forms/${encodeURIComponent(formUuid)}/settings`
3539
+ );
3540
+ formSetting.indexes = await requestWithAuth(
3541
+ config,
3542
+ target.profileName,
3543
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/forms/${encodeURIComponent(formUuid)}/field-indexes`
3544
+ );
3545
+ formSetting.dataManagement = await requestWithAuth(
3546
+ config,
3547
+ target.profileName,
3548
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/forms/${encodeURIComponent(formUuid)}/data-management`
3549
+ );
3550
+ formSetting.publicAccess = await requestWithAuth(
3551
+ config,
3552
+ target.profileName,
3553
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/forms/${encodeURIComponent(formUuid)}/public-access`
3554
+ );
3555
+ const filePath = path.join(baseDir, 'settings', 'forms', `${formCode}.json`);
3556
+ writeResourceJsonFile(filePath, formSetting);
3557
+ written.push(path.relative(process.cwd(), filePath));
3558
+ }
3559
+ return { appType: target.appType, profile: target.profileName, written };
3560
+ }
3561
+
3562
+ function buildPullResourceLookups(bound = {}) {
3563
+ const formCodeByUuid = new Map();
3564
+ for (const [code, entry] of Object.entries(bound.resources?.forms || {})) {
3565
+ if (entry?.formUuid) formCodeByUuid.set(entry.formUuid, code);
3566
+ }
3567
+
3568
+ const pageCodeById = new Map();
3569
+ for (const [code, entry] of Object.entries(bound.resources?.pages || {})) {
3570
+ if (entry?.pageId) pageCodeById.set(entry.pageId, code);
3571
+ }
3572
+
3573
+ const menuCodeById = new Map();
3574
+ for (const [code, entry] of Object.entries(bound.resources?.menus || {})) {
3575
+ if (entry?.menuId) menuCodeById.set(entry.menuId, code);
3576
+ }
3577
+
3578
+ return { formCodeByUuid, pageCodeById, menuCodeById };
3579
+ }
3580
+
3581
+ function splitPagePermissionTargetsForManifest(values = [], lookups) {
3582
+ const formCodes = [];
3583
+ const menuCodes = [];
3584
+ const menuFormUuids = [];
3585
+ for (const value of values || []) {
3586
+ if (lookups.formCodeByUuid.has(value)) {
3587
+ formCodes.push(lookups.formCodeByUuid.get(value));
3588
+ } else if (lookups.menuCodeById.has(value)) {
3589
+ menuCodes.push(lookups.menuCodeById.get(value));
3590
+ } else if (value) {
3591
+ menuFormUuids.push(value);
3592
+ }
3593
+ }
3594
+ return {
3595
+ ...(formCodes.length > 0 ? { formCodes: unique(formCodes) } : {}),
3596
+ ...(menuCodes.length > 0 ? { menuCodes: unique(menuCodes) } : {}),
3597
+ ...(menuFormUuids.length > 0 ? { menuFormUuids: unique(menuFormUuids) } : {}),
3598
+ };
3599
+ }
3600
+
3601
+ function writeResourceJsonFile(filePath, value) {
3602
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
3603
+ fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
3604
+ }
3605
+
3606
+ function stripPulledResource(value) {
3607
+ if (!value || typeof value !== 'object') return value;
3608
+ const next = { ...value };
3609
+ delete next.id;
3610
+ delete next.appType;
3611
+ delete next.createdAt;
3612
+ delete next.updatedAt;
3613
+ if (Array.isArray(next.apis)) {
3614
+ next.apis = next.apis.map(api => {
3615
+ const item = { ...api };
3616
+ delete item.id;
3617
+ delete item.connectorId;
3618
+ delete item.createdAt;
3619
+ delete item.updatedAt;
3620
+ return item;
3621
+ });
3622
+ }
3623
+ return next;
3624
+ }
3625
+
3626
+ function normalizeConnectorManifest(connector) {
3627
+ const parsedUrl = parseConnectorUrl(connector);
3628
+ return {
3629
+ code: connector.code || connector.methodName,
3630
+ name: connector.name || connector.code || connector.methodName,
3631
+ description: connector.description || '',
3632
+ protocol: connector.protocol || parsedUrl.protocol || 'https',
3633
+ domain: connector.domain || parsedUrl.domain,
3634
+ baseUrl: connector.baseUrl !== undefined ? connector.baseUrl : parsedUrl.baseUrl,
3635
+ authType: connector.authType || 'none',
3636
+ authConfig: connector.authConfig,
3637
+ defaultHeaders: connector.defaultHeaders || connector.headers,
3638
+ userContext: connector.userContext,
3639
+ sensitiveLog: connector.sensitiveLog,
3640
+ apis: (connector.apis || []).map(api => ({
3641
+ code: api.code || api.methodName,
3642
+ name: api.name || api.code || api.methodName,
3643
+ description: api.description || '',
3644
+ path: api.path,
3645
+ method: String(api.method || 'GET').toUpperCase(),
3646
+ timeout: api.timeout,
3647
+ requestBodyType: api.requestBodyType,
3648
+ responseType: api.responseType,
3649
+ pathParams: api.pathParams || [],
3650
+ queryParams: api.queryParams || [],
3651
+ headerParams: api.headerParams || [],
3652
+ bodyParams: api.bodyParams || [],
3653
+ responseDefinition: api.responseDefinition,
3654
+ })),
3655
+ };
3656
+ }
3657
+
3658
+ function parseConnectorUrl(connector) {
3659
+ const rawUrl = connector.url;
3660
+ if (!rawUrl) return {};
3661
+ try {
3662
+ const url = new URL(rawUrl);
3663
+ const baseUrl = url.pathname && url.pathname !== '/' ? url.pathname.replace(/^\/+|\/+$/g, '') : '';
3664
+ return {
3665
+ protocol: url.protocol.replace(':', ''),
3666
+ domain: url.host,
3667
+ baseUrl,
3668
+ };
3669
+ } catch {
3670
+ return {};
3671
+ }
3672
+ }
3673
+
3674
+ async function resolveManifestJson(config, profileName, item, objectKey, fileKey, optional = false) {
3675
+ let value = item[objectKey];
3676
+ if (value === undefined && item[fileKey]) {
3677
+ const filePath = path.resolve(item.__dir || process.cwd(), item[fileKey]);
3678
+ value = JSON.parse(fs.readFileSync(filePath, 'utf8'));
3679
+ }
3680
+ if (value === undefined) {
3681
+ if (optional) return undefined;
3682
+ fail(`${resourceLabel('resource', item)} 缺少 ${objectKey} 或 ${fileKey}`);
3683
+ }
3684
+ await resolveJsCodeSnapshotFiles(config, profileName, value, item.__dir || process.cwd(), new Map());
3685
+ return value;
3686
+ }
3687
+
3688
+ function resolveManifestFormUuid(bound, item, options = {}) {
3689
+ const formKey = item.formCode || item.form || (options.fallbackToCode ? item.code : undefined);
3690
+ if (formKey) {
3691
+ const mapped = bound.resources?.forms?.[formKey]?.formUuid;
3692
+ if (mapped) return mapped;
3693
+ }
3694
+ if (item.formUuid) return item.formUuid;
3695
+ return formKey;
3696
+ }
3697
+
3698
+ function resolveManifestPageId(bound, item) {
3699
+ if (item.pageCode) {
3700
+ const mapped = bound.resources?.pages?.[item.pageCode]?.pageId;
3701
+ if (mapped) return mapped;
3702
+ }
3703
+ return item.pageId || item.pageCode;
3704
+ }
3705
+
3706
+ function resolveManifestMenuId(bound, menuCode) {
3707
+ if (!menuCode) return undefined;
3708
+ return bound.resources?.menus?.[menuCode]?.menuId || menuCode;
3709
+ }
3710
+
3711
+ function resolveAutomationManifestPayload(target, automationItem) {
3712
+ const triggerConfig = {
3713
+ ...(automationItem.triggerConfig || {}),
3714
+ };
3715
+ if (!triggerConfig.appType) triggerConfig.appType = target.appType;
3716
+ const formUuid = resolveManifestFormUuid(target.bound, automationItem) || triggerConfig.formUuid;
3717
+ if (formUuid && !triggerConfig.formUuid) {
3718
+ triggerConfig.formUuid = formUuid;
3719
+ }
3720
+ return { formUuid, triggerConfig };
3721
+ }
3722
+
3723
+ function resolvePagePermissionGroupTargets(bound, group) {
3724
+ const direct = [
3725
+ ...(Array.isArray(group.menuFormUuids) ? group.menuFormUuids : []),
3726
+ ...(Array.isArray(group.menuIds) ? group.menuIds : []),
3727
+ ];
3728
+ const fromMenus = (group.menuCodes || []).flatMap(code => resolveMenuPermissionTargets(bound, code));
3729
+ const fromForms = (group.formCodes || []).map(code => resolveOptionalFormUuid(bound, code));
3730
+ const fromPages = (group.pageCodes || []).flatMap(code => resolvePagePermissionTargets(bound, code));
3731
+ return unique([...direct, ...fromMenus, ...fromForms, ...fromPages].filter(Boolean));
3732
+ }
3733
+
3734
+ function saveResourceEntry(target, bucket, code, extra = {}) {
3735
+ saveStateResource(target, bucket, code, pickStateFields(extra, ['formUuid']), [
3736
+ 'formUuid',
3737
+ ]);
3738
+ }
3739
+
3740
+ function withoutResourceMeta(value) {
3741
+ const next = { ...(value || {}) };
3742
+ for (const key of Object.keys(next)) {
3743
+ if (key.startsWith('__')) delete next[key];
3744
+ }
3745
+ return next;
3746
+ }
3747
+
3748
+ function roleEquals(desired, existing) {
3749
+ return (
3750
+ String(existing.name || '') === String(desired.name || desired.code || '') &&
3751
+ String(existing.description || '') === String(desired.description || '')
3752
+ );
3753
+ }
3754
+
3755
+ function menuEquals(bound, desired, existing) {
3756
+ if (!existing) return false;
3757
+ const desiredFormUuid = resolveManifestFormUuid(bound, desired);
3758
+ const desiredPageId = resolveManifestPageId(bound, desired);
3759
+ const desiredParentId =
3760
+ resolveManifestMenuId(bound, desired.parentCode) || desired.parentId || null;
3761
+ return (
3762
+ String(existing.name || '') === String(desired.name || desired.code || '') &&
3763
+ String(existing.type || '') === String(desired.type || 'nav') &&
3764
+ String(existing.resourceCode || '') === String(desired.code || '') &&
3765
+ optionalScalarEquals(existing.formUuid, desiredFormUuid, hasAnyKey(desired, ['formCode', 'formUuid', 'form'])) &&
3766
+ optionalScalarEquals(existing.pageId, desiredPageId, hasAnyKey(desired, ['pageCode', 'pageId'])) &&
3767
+ optionalScalarEquals(existing.parentId || null, desiredParentId, hasAnyKey(desired, ['parentCode', 'parentId'])) &&
3768
+ optionalScalarEquals(existing.sortOrder, desired.sortOrder, desired.sortOrder !== undefined) &&
3769
+ optionalScalarEquals(existing.icon || null, desired.icon || null, desired.icon !== undefined) &&
3770
+ optionalScalarEquals(Boolean(existing.isHidden), Boolean(desired.isHidden), desired.isHidden !== undefined)
3771
+ );
3772
+ }
3773
+
3774
+ function connectorEquals(desired, existing) {
3775
+ const normalizedDesired = normalizeConnectorManifest(desired);
3776
+ const normalizedExisting = normalizeConnectorManifest(existing);
3777
+ delete normalizedExisting.appType;
3778
+ return JSON.stringify(normalizedExisting) === JSON.stringify(normalizedDesired);
3779
+ }
3780
+
3781
+ function pagePermissionGroupEquals(bound, desired, existing) {
3782
+ if (!existing) return false;
3783
+ const desiredTargets = resolvePagePermissionGroupTargets(bound, desired);
3784
+ return (
3785
+ String(existing.name || '') === String(desired.name || desired.code || '') &&
3786
+ String(existing.resourceCode || '') === String(desired.code || '') &&
3787
+ stringSetEquals(existing.roles || [], desired.roles || []) &&
3788
+ stringSetEquals(existing.menuFormUuids || [], desiredTargets)
3789
+ );
3790
+ }
3791
+
3792
+ function formPermissionGroupEquals(bound, desired, existing) {
3793
+ if (!existing) return false;
3794
+ const expected = normalizeFormPermissionGroupManifest(bound, desired);
3795
+ const current = pickObjectKeys(existing, Object.keys(expected));
3796
+ return stableStringifyForPlan(current, process.cwd()) === stableStringifyForPlan(expected, desired.__dir || process.cwd());
3797
+ }
3798
+
3799
+ function normalizeFormPermissionGroupManifest(bound, group) {
3800
+ const formUuid = resolveManifestFormUuid(bound, group);
3801
+ const body = {
3802
+ ...withoutResourceMeta(group),
3803
+ resourceCode: group.code,
3804
+ formUuid,
3805
+ name: group.name || group.code,
3806
+ type: group.type || 'view',
3807
+ roles: group.roles || [],
3808
+ };
3809
+ delete body.code;
3810
+ delete body.formCode;
3811
+ delete body.form;
3812
+ return stripUndefinedValues(body);
3813
+ }
3814
+
3815
+ function optionalScalarEquals(existingValue, desiredValue, enabled) {
3816
+ if (!enabled) return true;
3817
+ return String(existingValue ?? '') === String(desiredValue ?? '');
3818
+ }
3819
+
3820
+ function hasAnyKey(value, keys) {
3821
+ return keys.some(key => value?.[key] !== undefined);
3822
+ }
3823
+
3824
+ function stringSetEquals(left = [], right = []) {
3825
+ return stableStringifyForPlan(sortStringValues(left), process.cwd()) ===
3826
+ stableStringifyForPlan(sortStringValues(right), process.cwd());
3827
+ }
3828
+
3829
+ function sortStringValues(value = []) {
3830
+ return [...(value || [])].map(item => String(item)).sort();
3831
+ }
3832
+
3833
+ function pickObjectKeys(source = {}, keys = []) {
3834
+ return keys.reduce((acc, key) => {
3835
+ acc[key] = source[key];
3836
+ return acc;
3837
+ }, {});
3838
+ }
3839
+
3840
+ function stripUndefinedValues(value) {
3841
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return value;
3842
+ return Object.entries(value).reduce((acc, [key, item]) => {
3843
+ if (item !== undefined) acc[key] = item;
3844
+ return acc;
3845
+ }, {});
3846
+ }
3847
+
3848
+ function workflowEquals(bound, desired, existing) {
3849
+ if (!existing) return false;
3850
+ const desiredDefinition = resolveManifestPlainJson(desired, 'definitionJson', 'definitionFile');
3851
+ const desiredView = resolveManifestPlainJson(desired, 'viewJson', 'viewFile', true);
3852
+ const desiredFormUuid = resolveManifestFormUuid(bound, desired);
3853
+ return (
3854
+ String(existing.resourceCode || '') === String(desired.code || desired.resourceCode || '') &&
3855
+ String(existing.formUuid || '') === String(desiredFormUuid || '') &&
3856
+ jsonEqualsForPlan(existing.definitionJson, desiredDefinition, desired.__dir) &&
3857
+ optionalJsonEqualsForPlan(existing.viewJson, desiredView, desired.__dir) &&
3858
+ (desired.publish ? Boolean(existing.isPublished) === true : true)
3859
+ );
3860
+ }
3861
+
3862
+ function automationEquals(target, desired, existing) {
3863
+ if (!existing) return false;
3864
+ const desiredDefinition = resolveManifestPlainJson(desired, 'definitionJson', 'definitionFile');
3865
+ const desiredView = resolveManifestPlainJson(desired, 'viewJson', 'viewFile', true);
3866
+ const automationPayload = resolveAutomationManifestPayload(target, desired);
3867
+ const desiredTags =
3868
+ desired.tags === undefined
3869
+ ? undefined
3870
+ : Array.isArray(desired.tags)
3871
+ ? desired.tags.join(',')
3872
+ : String(desired.tags);
3873
+ const desiredEnabled =
3874
+ desired.enable !== undefined
3875
+ ? Boolean(desired.enable)
3876
+ : desired.isEnabled !== undefined
3877
+ ? Boolean(desired.isEnabled)
3878
+ : undefined;
3879
+
3880
+ return (
3881
+ String(existing.resourceCode || '') === String(desired.code || desired.resourceCode || '') &&
3882
+ String(existing.name || '') === String(desired.name || desired.code || '') &&
3883
+ String(existing.description || '') === String(desired.description || '') &&
3884
+ String(existing.formUuid || '') === String(automationPayload.formUuid || '') &&
3885
+ jsonEqualsForPlan(existing.triggerConfig, automationPayload.triggerConfig, desired.__dir) &&
3886
+ jsonEqualsForPlan(existing.definitionJson, desiredDefinition, desired.__dir) &&
3887
+ optionalJsonEqualsForPlan(existing.viewJson, desiredView, desired.__dir) &&
3888
+ (desiredTags === undefined ? true : String(existing.tags || '') === desiredTags) &&
3889
+ (desired.publish ? Boolean(existing.isPublished) === true : true) &&
3890
+ (desiredEnabled === undefined ? true : Boolean(existing.isEnabled) === desiredEnabled)
3891
+ );
3892
+ }
3893
+
3894
+ function resolveManifestPlainJson(item, objectKey, fileKey, optional = false) {
3895
+ if (item[objectKey] !== undefined) return item[objectKey];
3896
+ if (item[fileKey]) {
3897
+ const filePath = path.resolve(item.__dir || process.cwd(), item[fileKey]);
3898
+ try {
3899
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
3900
+ } catch (error) {
3901
+ fail(`${resourceLabel('resource', item)} 无法读取 ${fileKey}: ${error.message}`);
3902
+ }
3903
+ }
3904
+ if (optional) return undefined;
3905
+ fail(`${resourceLabel('resource', item)} 缺少 ${objectKey} 或 ${fileKey}`);
3906
+ }
3907
+
3908
+ function optionalJsonEqualsForPlan(existingValue, desiredValue, desiredBaseDir) {
3909
+ const normalizedExisting =
3910
+ existingValue === null || existingValue === undefined ? undefined : existingValue;
3911
+ const normalizedDesired =
3912
+ desiredValue === null || desiredValue === undefined ? undefined : desiredValue;
3913
+ return jsonEqualsForPlan(normalizedExisting, normalizedDesired, desiredBaseDir);
3914
+ }
3915
+
3916
+ function jsonEqualsForPlan(existingValue, desiredValue, desiredBaseDir) {
3917
+ return (
3918
+ stableStringifyForPlan(existingValue, process.cwd()) ===
3919
+ stableStringifyForPlan(desiredValue, desiredBaseDir || process.cwd())
3920
+ );
3921
+ }
3922
+
3923
+ function stableStringifyForPlan(value, baseDir) {
3924
+ const normalized = normalizeJsonForPlan(value, baseDir, null);
3925
+ const result = JSON.stringify(normalized);
3926
+ return result === undefined ? 'undefined' : result;
3927
+ }
3928
+
3929
+ function normalizeJsonForPlan(value, baseDir, keyName) {
3930
+ if (value === undefined || value === null) return value;
3931
+ if (Array.isArray(value)) {
3932
+ return value.map(item => normalizeJsonForPlan(item, baseDir, null));
3933
+ }
3934
+ if (typeof value !== 'object') return value;
3935
+
3936
+ if (keyName === 'sourceFile') {
3937
+ if (value.localPath) return normalizeLocalSourceFileForPlan(value.localPath, baseDir);
3938
+ if (value.sha256) return { sha256: value.sha256 };
3939
+ }
3940
+
3941
+ const result = {};
3942
+ const hasLocalSourceFile =
3943
+ value.sourceFile && typeof value.sourceFile === 'object' && value.sourceFile.localPath;
3944
+ for (const key of Object.keys(value).sort()) {
3945
+ if (key.startsWith('__') || value[key] === undefined) continue;
3946
+ result[key] = normalizeJsonForPlan(value[key], baseDir, key);
3947
+ }
3948
+ if (hasLocalSourceFile) {
3949
+ result.code = result.code === undefined ? '' : result.code;
3950
+ result.runtimeMode = result.runtimeMode || 'trusted_node';
3951
+ result.sourceType = result.sourceType || 'file_snapshot';
3952
+ }
3953
+ return sortObjectForStableJson(result);
3954
+ }
3955
+
3956
+ function normalizeLocalSourceFileForPlan(localPath, baseDir) {
3957
+ const rawLocalPath = String(localPath);
3958
+ const baseResolvedPath = path.resolve(baseDir || process.cwd(), rawLocalPath);
3959
+ const cwdResolvedPath = path.resolve(process.cwd(), rawLocalPath);
3960
+ const resolvedLocalPath = fs.existsSync(baseResolvedPath)
3961
+ ? baseResolvedPath
3962
+ : cwdResolvedPath;
3963
+ const bundlePath = resolveJsCodeBundlePathForPlan(resolvedLocalPath);
3964
+ if (bundlePath && fs.existsSync(bundlePath)) {
3965
+ return {
3966
+ sha256: crypto
3967
+ .createHash('sha256')
3968
+ .update(fs.readFileSync(bundlePath))
3969
+ .digest('hex'),
3970
+ };
3971
+ }
3972
+ return {
3973
+ localPath: path.relative(process.cwd(), resolvedLocalPath).replace(/\\/g, '/'),
3974
+ };
3975
+ }
3976
+
3977
+ function resolveJsCodeBundlePathForPlan(localPath) {
3978
+ const extension = path.extname(localPath).toLowerCase();
3979
+ if (extension && extension !== '.ts' && extension !== '.tsx') return null;
3980
+ const normalized = localPath.replace(/\\/g, '/');
3981
+ const matched = normalized.match(/src\/js-code-nodes\/([^/]+)\/index\.tsx?$/);
3982
+ if (!matched) return null;
3983
+ const workspaceRoot = findWorkspaceRoot(localPath);
3984
+ return path.join(workspaceRoot, 'dist', 'js-code-nodes', matched[1], 'index.cjs');
3985
+ }
3986
+
3987
+ function sortObjectForStableJson(value) {
3988
+ return Object.keys(value)
3989
+ .sort()
3990
+ .reduce((acc, key) => {
3991
+ acc[key] = value[key];
3992
+ return acc;
3993
+ }, {});
3994
+ }
3995
+
3996
+ function printResourceValidation(validation) {
3997
+ const lines = [
3998
+ `资源目录: ${path.relative(process.cwd(), validation.baseDir) || validation.baseDir}`,
3999
+ `校验状态: ${validation.valid ? '通过' : '失败'}`,
4000
+ ];
4001
+ for (const [key, count] of Object.entries(validation.counts)) {
4002
+ lines.push(`- ${key}: ${count}`);
4003
+ }
4004
+ for (const warning of validation.warnings) lines.push(`Warning: ${warning}`);
4005
+ for (const error of validation.errors) lines.push(`Error: ${error}`);
4006
+ print(lines.join('\n'));
4007
+ }
4008
+
4009
+ function printResourcePlan(plan) {
4010
+ const lines = [
4011
+ `profile: ${plan.profile}`,
4012
+ `appType: ${plan.appType}`,
4013
+ `summary: create=${plan.summary.create} update=${plan.summary.update} noop=${plan.summary.noop} delete=${plan.summary.delete}`,
4014
+ ];
4015
+ for (const action of plan.actions) {
4016
+ lines.push(`- ${action.kind}:${action.code} ${action.action}${action.platformId ? ` (${action.platformId})` : ''}`);
4017
+ }
4018
+ print(lines.join('\n'));
4019
+ }
4020
+
4021
+ function printResourceResult(result) {
4022
+ const lines = [`profile: ${result.profile}`, `appType: ${result.appType}`];
4023
+ if (result.written) {
4024
+ lines.push(`written: ${result.written.length}`);
4025
+ result.written.forEach(file => lines.push(`- ${file}`));
4026
+ }
4027
+ if (result.published) {
4028
+ lines.push(`published: ${result.published.length}`);
4029
+ result.published.forEach(item => lines.push(`- ${item.kind}:${item.code} ${item.action}${item.id ? ` (${item.id})` : ''}`));
4030
+ }
4031
+ if (result.pruned) {
4032
+ lines.push(`pruned: ${result.pruned.length}`);
4033
+ result.pruned.forEach(item => lines.push(`- ${item.kind}:${item.code}${item.id ? ` (${item.id})` : ''}`));
4034
+ }
4035
+ for (const warning of result.warnings || []) lines.push(`Warning: ${warning}`);
4036
+ print(lines.join('\n'));
2193
4037
  }
2194
4038
 
2195
4039
  function readJsonArg(value, label) {