openxiangda 1.0.21 → 1.0.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -10
- package/lib/cli.js +723 -11
- package/lib/workspace-init.js +13 -0
- package/openxiangda-skills/SKILL.md +26 -10
- package/openxiangda-skills/references/architecture-patterns.md +44 -22
- package/openxiangda-skills/references/automation-v3.md +2 -0
- package/openxiangda-skills/references/best-practices.md +163 -0
- package/openxiangda-skills/references/connector-resources.md +3 -0
- package/openxiangda-skills/references/notifications.md +80 -0
- package/openxiangda-skills/references/openxiangda-api.md +45 -0
- package/openxiangda-skills/references/pages/page-sdk.md +1 -0
- package/openxiangda-skills/references/pages/workspace-structure.md +5 -3
- package/openxiangda-skills/references/workspace-state.md +6 -0
- package/openxiangda-skills/skills/openxiangda-app/SKILL.md +11 -7
- package/openxiangda-skills/skills/openxiangda-core/SKILL.md +22 -4
- package/openxiangda-skills/skills/openxiangda-form/SKILL.md +6 -1
- package/openxiangda-skills/skills/openxiangda-page/SKILL.md +9 -1
- package/openxiangda-skills/skills/openxiangda-permission-settings/SKILL.md +3 -0
- package/openxiangda-skills/skills/openxiangda-workflow-automation/SKILL.md +9 -0
- package/package.json +1 -1
- package/packages/sdk/dist/runtime/index.cjs +34 -2
- package/packages/sdk/dist/runtime/index.cjs.map +1 -1
- package/packages/sdk/dist/runtime/index.d.mts +66 -1
- package/packages/sdk/dist/runtime/index.d.ts +66 -1
- package/packages/sdk/dist/runtime/index.mjs +34 -2
- package/packages/sdk/dist/runtime/index.mjs.map +1 -1
- package/templates/sy-lowcode-app-workspace/examples/best-practices/README.md +32 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/catalog.json +61 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/decision-guide.md +44 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/design-style.md +30 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/module-structure.md +48 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/role-governance/index.ts +2 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/role-governance/permissions.test.ts +35 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/role-governance/permissions.ts +24 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/role-governance/types.ts +17 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/service-ticket/index.ts +4 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/service-ticket/permissions.test.ts +42 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/service-ticket/permissions.ts +23 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/service-ticket/state-machine.test.ts +63 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/service-ticket/state-machine.ts +73 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/service-ticket/ticket-query.test.ts +34 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/service-ticket/ticket-query.ts +73 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/service-ticket/types.ts +64 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/forms/app-role/page.tsx +1 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/forms/app-role/schema.ts +57 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/forms/customer-profile/page.tsx +1 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/forms/customer-profile/schema.ts +83 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/forms/service-ticket/page.tsx +1 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/forms/service-ticket/schema.ts +97 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/forms/ticket-action-log/page.tsx +1 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/forms/ticket-action-log/schema.ts +65 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/js-code-nodes/daily_ticket_digest/index.ts +44 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/js-code-nodes/sync_roles_to_platform/index.ts +33 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/interactive-workbench/App.tsx +7 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/interactive-workbench/WorkbenchPage.tsx +36 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/interactive-workbench/components/ConfigPanel.tsx +34 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/interactive-workbench/components/PreviewPanel.tsx +17 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/interactive-workbench/index.tsx +10 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/interactive-workbench/page.config.ts +9 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/interactive-workbench/reducer.ts +29 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/interactive-workbench/styles.css +24 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/mobile-portal-shell/App.tsx +7 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/mobile-portal-shell/MobilePortalShell.tsx +31 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/mobile-portal-shell/index.tsx +10 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/mobile-portal-shell/modules/MobileHome.tsx +13 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/mobile-portal-shell/page.config.ts +14 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/mobile-portal-shell/routes.ts +13 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/mobile-portal-shell/styles.css +11 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/pc-portal-shell/App.tsx +7 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/pc-portal-shell/PcPortalShell.tsx +35 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/pc-portal-shell/components/PortalMetric.tsx +11 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/pc-portal-shell/index.tsx +10 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/pc-portal-shell/modules/HomeModule.tsx +25 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/pc-portal-shell/modules/TicketsModule.tsx +14 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/pc-portal-shell/page.config.ts +14 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/pc-portal-shell/routes.ts +19 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/pc-portal-shell/styles.css +35 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/service-ticket-ops/App.tsx +7 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/service-ticket-ops/TicketOpsPage.tsx +105 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/service-ticket-ops/components/TicketActionTimeline.tsx +22 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/service-ticket-ops/components/TicketDetailDrawer.tsx +41 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/service-ticket-ops/components/TicketTableActions.tsx +55 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/service-ticket-ops/index.tsx +10 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/service-ticket-ops/page.config.ts +9 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/service-ticket-ops/styles.css +35 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/resources/automations/daily-ticket-digest/automation.json +25 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/resources/automations/daily-ticket-digest/trigger.json +9 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/resources/notifications/daily-ticket-digest.json +24 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/resources/permissions/form-groups/service-ticket-college.json +21 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/resources/permissions/roles.json +17 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/resources/workflows/expense-approval-workflow.json +48 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/shared/components/ConfirmAction.tsx +22 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/shared/components/QueryState.tsx +37 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/shared/components/StatusTag.tsx +20 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/shared/hooks/useTicketOps.ts +96 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/shared/services/role-governance.ts +48 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/shared/services/service-ticket.ts +113 -0
- package/templates/sy-lowcode-app-workspace/package.json +1 -0
- package/templates/sy-lowcode-app-workspace/src/dev/App.tsx +11 -1
- package/templates/sy-lowcode-app-workspace/tsconfig.examples.json +24 -0
package/lib/cli.js
CHANGED
|
@@ -14,7 +14,7 @@ const {
|
|
|
14
14
|
} = require('./config');
|
|
15
15
|
const { requestJson } = require('./http');
|
|
16
16
|
const { getSkillStatusReport, installSkills } = require('./skills');
|
|
17
|
-
const { initWorkspace } = require('./workspace-init');
|
|
17
|
+
const { assertCanInitializeWorkspace, initWorkspace } = require('./workspace-init');
|
|
18
18
|
const {
|
|
19
19
|
fail,
|
|
20
20
|
openBrowser,
|
|
@@ -24,6 +24,10 @@ const {
|
|
|
24
24
|
warn,
|
|
25
25
|
writeJson,
|
|
26
26
|
} = require('./utils');
|
|
27
|
+
const { version: CURRENT_VERSION } = require('../package.json');
|
|
28
|
+
|
|
29
|
+
const NPM_PACKAGE_NAME = 'openxiangda';
|
|
30
|
+
const OFFICIAL_NPM_REGISTRY = 'https://registry.npmjs.org';
|
|
27
31
|
|
|
28
32
|
async function main(argv) {
|
|
29
33
|
const [command, ...rest] = argv;
|
|
@@ -33,6 +37,7 @@ async function main(argv) {
|
|
|
33
37
|
}
|
|
34
38
|
|
|
35
39
|
if (command === 'login') return login(rest);
|
|
40
|
+
if (command === 'update') return update(rest);
|
|
36
41
|
if (command === 'env') return env(rest);
|
|
37
42
|
if (command === 'auth') return auth(rest);
|
|
38
43
|
if (command === 'platform') return platform(rest);
|
|
@@ -58,12 +63,14 @@ function printHelp() {
|
|
|
58
63
|
|
|
59
64
|
Usage:
|
|
60
65
|
openxiangda login <platform-url> [--profile name]
|
|
66
|
+
openxiangda update check|install [--json] [--registry https://registry.npmjs.org]
|
|
61
67
|
openxiangda platform add <name> <platform-url>
|
|
62
68
|
openxiangda platform list
|
|
63
69
|
openxiangda platform use <name>
|
|
64
70
|
openxiangda auth status|refresh|logout [--profile name]
|
|
65
71
|
openxiangda env [--profile name]
|
|
66
72
|
openxiangda workspace init [dir] [--name package-name] [--install] [--profile name --app-type APP_XXX]
|
|
73
|
+
openxiangda workspace init [dir] --profile <name> --app-name <app-name> [--install]
|
|
67
74
|
openxiangda workspace bind --profile <name> --app-type <APP_XXX>
|
|
68
75
|
openxiangda workspace publish --profile <name> [--prune]
|
|
69
76
|
openxiangda app list [--profile name] [--json]
|
|
@@ -102,6 +109,199 @@ OpenXiangda 使用普通用户登录 token,不需要 AK/SK。
|
|
|
102
109
|
JS_CODE V2 使用 trusted_node;AI 源码必须写在 src/js-code-nodes/<scriptCode>/index.ts,definition-json 中的 sourceFile.localPath 会在 validate/create 时先 TS 校验、再构建并上传为快照。`);
|
|
103
110
|
}
|
|
104
111
|
|
|
112
|
+
async function update(args) {
|
|
113
|
+
const requestedSubcommand = args[0] && !args[0].startsWith('--') ? args[0] : 'check';
|
|
114
|
+
const parsedArgs = requestedSubcommand === args[0] ? args.slice(1) : args;
|
|
115
|
+
const { flags } = parseArgs(parsedArgs);
|
|
116
|
+
const registry = normalizeNpmRegistry(flags.registry || OFFICIAL_NPM_REGISTRY);
|
|
117
|
+
|
|
118
|
+
if (requestedSubcommand === 'check') {
|
|
119
|
+
const result = checkOpenXiangdaUpdate(registry);
|
|
120
|
+
if (flags.json) return writeJson(result);
|
|
121
|
+
printUpdateCheck(result);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (requestedSubcommand === 'install') {
|
|
126
|
+
const result = installOpenXiangdaUpdate(registry, {
|
|
127
|
+
json: Boolean(flags.json),
|
|
128
|
+
skipSkills: Boolean(flags['no-skills']),
|
|
129
|
+
});
|
|
130
|
+
if (flags.json) return writeJson(result);
|
|
131
|
+
print('OpenXiangda 已更新。');
|
|
132
|
+
if (result.skillInstall?.attempted && result.skillInstall.status !== 0) {
|
|
133
|
+
warn(`skill install 未完成: ${result.skillInstall.error}`);
|
|
134
|
+
}
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
fail('用法: openxiangda update check|install [--json] [--registry https://registry.npmjs.org] [--no-skills]');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function normalizeNpmRegistry(value) {
|
|
142
|
+
const registry = String(value || OFFICIAL_NPM_REGISTRY).trim();
|
|
143
|
+
return registry.replace(/\/+$/, '') || OFFICIAL_NPM_REGISTRY;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function checkOpenXiangdaUpdate(registry) {
|
|
147
|
+
const latestVersion = fetchLatestOpenXiangdaVersion(registry);
|
|
148
|
+
const versionComparison = compareVersions(latestVersion, CURRENT_VERSION);
|
|
149
|
+
const updateAvailable = versionComparison > 0;
|
|
150
|
+
return {
|
|
151
|
+
packageName: NPM_PACKAGE_NAME,
|
|
152
|
+
currentVersion: CURRENT_VERSION,
|
|
153
|
+
latestVersion,
|
|
154
|
+
registry,
|
|
155
|
+
updateAvailable,
|
|
156
|
+
status: updateAvailable
|
|
157
|
+
? 'update_available'
|
|
158
|
+
: versionComparison < 0
|
|
159
|
+
? 'local_newer_than_registry'
|
|
160
|
+
: 'latest',
|
|
161
|
+
installCommand: `npm install -g ${NPM_PACKAGE_NAME}@latest --registry=${registry}`,
|
|
162
|
+
skillInstallCommand: 'openxiangda skill install --force',
|
|
163
|
+
compatibilityCheckCommand: 'openxiangda commands --json',
|
|
164
|
+
checkedAt: new Date().toISOString(),
|
|
165
|
+
notes: [
|
|
166
|
+
'Use the official npm registry for OpenXiangda updates; domestic mirrors may lag.',
|
|
167
|
+
'After updating the package, refresh OpenXiangda skills so AI guidance matches the CLI version.',
|
|
168
|
+
],
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function fetchLatestOpenXiangdaVersion(registry) {
|
|
173
|
+
const result = spawnSync(
|
|
174
|
+
'npm',
|
|
175
|
+
['view', `${NPM_PACKAGE_NAME}@latest`, 'version', `--registry=${registry}`, '--json'],
|
|
176
|
+
{
|
|
177
|
+
encoding: 'utf8',
|
|
178
|
+
env: { ...process.env, npm_config_registry: registry },
|
|
179
|
+
}
|
|
180
|
+
);
|
|
181
|
+
if (result.error) {
|
|
182
|
+
fail(`无法执行 npm: ${result.error.message}`);
|
|
183
|
+
}
|
|
184
|
+
if (result.status !== 0) {
|
|
185
|
+
fail(`检查 OpenXiangda 更新失败: ${formatSpawnOutput(result)}`);
|
|
186
|
+
}
|
|
187
|
+
const raw = String(result.stdout || '').trim();
|
|
188
|
+
if (!raw) fail('检查 OpenXiangda 更新失败: npm 未返回版本号');
|
|
189
|
+
try {
|
|
190
|
+
const parsed = JSON.parse(raw);
|
|
191
|
+
return String(parsed).trim();
|
|
192
|
+
} catch (_error) {
|
|
193
|
+
return raw.replace(/^"|"$/g, '').trim();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function installOpenXiangdaUpdate(registry, options = {}) {
|
|
198
|
+
const npmArgs = [
|
|
199
|
+
'install',
|
|
200
|
+
'-g',
|
|
201
|
+
`${NPM_PACKAGE_NAME}@latest`,
|
|
202
|
+
`--registry=${registry}`,
|
|
203
|
+
];
|
|
204
|
+
const quiet = Boolean(options.json);
|
|
205
|
+
if (!quiet) {
|
|
206
|
+
print(`执行: npm ${npmArgs.join(' ')}`);
|
|
207
|
+
}
|
|
208
|
+
const installResult = spawnSync('npm', npmArgs, {
|
|
209
|
+
encoding: 'utf8',
|
|
210
|
+
stdio: quiet ? 'pipe' : 'inherit',
|
|
211
|
+
env: { ...process.env, npm_config_registry: registry },
|
|
212
|
+
});
|
|
213
|
+
if (installResult.error) {
|
|
214
|
+
fail(`无法执行 npm: ${installResult.error.message}`);
|
|
215
|
+
}
|
|
216
|
+
if (installResult.status !== 0) {
|
|
217
|
+
fail(`OpenXiangda 更新失败: ${formatSpawnOutput(installResult)}`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const result = {
|
|
221
|
+
packageName: NPM_PACKAGE_NAME,
|
|
222
|
+
registry,
|
|
223
|
+
installCommand: `npm ${npmArgs.join(' ')}`,
|
|
224
|
+
installed: true,
|
|
225
|
+
skillInstall: {
|
|
226
|
+
attempted: false,
|
|
227
|
+
status: null,
|
|
228
|
+
error: null,
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
if (!options.skipSkills) {
|
|
233
|
+
if (!quiet) {
|
|
234
|
+
print('刷新 OpenXiangda skills: openxiangda skill install --force');
|
|
235
|
+
}
|
|
236
|
+
const skillResult = spawnSync('openxiangda', ['skill', 'install', '--force'], {
|
|
237
|
+
encoding: 'utf8',
|
|
238
|
+
stdio: quiet ? 'pipe' : 'inherit',
|
|
239
|
+
env: process.env,
|
|
240
|
+
});
|
|
241
|
+
result.skillInstall.attempted = true;
|
|
242
|
+
result.skillInstall.status = skillResult.status;
|
|
243
|
+
if (skillResult.error || skillResult.status !== 0) {
|
|
244
|
+
result.skillInstall.error = skillResult.error?.message || formatSpawnOutput(skillResult);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return result;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function printUpdateCheck(result) {
|
|
252
|
+
const lines = [
|
|
253
|
+
'OpenXiangda update check',
|
|
254
|
+
`current: ${result.currentVersion}`,
|
|
255
|
+
`latest: ${result.latestVersion}`,
|
|
256
|
+
`registry: ${result.registry}`,
|
|
257
|
+
];
|
|
258
|
+
if (result.updateAvailable) {
|
|
259
|
+
lines.push('status: update available');
|
|
260
|
+
lines.push('recommended:');
|
|
261
|
+
lines.push(` ${result.installCommand}`);
|
|
262
|
+
lines.push(` ${result.skillInstallCommand}`);
|
|
263
|
+
} else if (result.status === 'local_newer_than_registry') {
|
|
264
|
+
lines.push('status: local version is newer than registry');
|
|
265
|
+
} else {
|
|
266
|
+
lines.push('status: already latest');
|
|
267
|
+
}
|
|
268
|
+
lines.push(`compatibility: ${result.compatibilityCheckCommand}`);
|
|
269
|
+
print(lines.join('\n'));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function formatSpawnOutput(result) {
|
|
273
|
+
return (
|
|
274
|
+
String(result.stderr || '').trim() ||
|
|
275
|
+
String(result.stdout || '').trim() ||
|
|
276
|
+
`exit status ${result.status}`
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function compareVersions(a, b) {
|
|
281
|
+
const left = parseSemver(a);
|
|
282
|
+
const right = parseSemver(b);
|
|
283
|
+
for (let index = 0; index < 3; index += 1) {
|
|
284
|
+
if (left.numbers[index] !== right.numbers[index]) {
|
|
285
|
+
return left.numbers[index] > right.numbers[index] ? 1 : -1;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
if (left.prerelease === right.prerelease) return 0;
|
|
289
|
+
if (!left.prerelease) return 1;
|
|
290
|
+
if (!right.prerelease) return -1;
|
|
291
|
+
return left.prerelease > right.prerelease ? 1 : -1;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function parseSemver(value) {
|
|
295
|
+
const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/.exec(String(value || ''));
|
|
296
|
+
if (!match) {
|
|
297
|
+
return { numbers: [0, 0, 0], prerelease: String(value || '') };
|
|
298
|
+
}
|
|
299
|
+
return {
|
|
300
|
+
numbers: [Number(match[1]), Number(match[2]), Number(match[3])],
|
|
301
|
+
prerelease: match[4] || '',
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
105
305
|
async function platform(args) {
|
|
106
306
|
const [subcommand, ...rest] = args;
|
|
107
307
|
const { flags, positional } = parseArgs(rest);
|
|
@@ -378,14 +578,49 @@ async function workspace(args) {
|
|
|
378
578
|
const config = loadConfig();
|
|
379
579
|
|
|
380
580
|
if (subcommand === 'init') {
|
|
581
|
+
let appType = flags['app-type'];
|
|
582
|
+
let profileName = flags.profile || null;
|
|
583
|
+
let createdApp = null;
|
|
584
|
+
|
|
585
|
+
if (flags['app-name']) {
|
|
586
|
+
if (appType) {
|
|
587
|
+
fail('workspace init 不能同时使用 --app-name 和 --app-type');
|
|
588
|
+
}
|
|
589
|
+
profileName = profileName || config.currentProfile;
|
|
590
|
+
if (!profileName) {
|
|
591
|
+
fail('workspace init --app-name 需要先选择 profile,或显式传 --profile <name>');
|
|
592
|
+
}
|
|
593
|
+
assertCanInitializeWorkspace({
|
|
594
|
+
dir: positional[0],
|
|
595
|
+
force: flags.force,
|
|
596
|
+
});
|
|
597
|
+
const data = await createPlatformApp(config, profileName, {
|
|
598
|
+
name: flags['app-name'],
|
|
599
|
+
description: flags.description || '',
|
|
600
|
+
iconfontCss: flags['iconfont-css'] || '',
|
|
601
|
+
});
|
|
602
|
+
appType = extractCreatedAppType(data);
|
|
603
|
+
if (!appType) {
|
|
604
|
+
fail('应用已创建,但平台响应缺少 appType,无法写入 workspace 绑定');
|
|
605
|
+
}
|
|
606
|
+
createdApp = {
|
|
607
|
+
name: flags['app-name'],
|
|
608
|
+
appType,
|
|
609
|
+
data,
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
|
|
381
613
|
const result = initWorkspace({
|
|
382
614
|
dir: positional[0],
|
|
383
615
|
name: flags.name,
|
|
384
616
|
install: flags.install,
|
|
385
617
|
force: flags.force,
|
|
386
|
-
profile:
|
|
387
|
-
appType
|
|
618
|
+
profile: profileName,
|
|
619
|
+
appType,
|
|
388
620
|
});
|
|
621
|
+
if (createdApp) {
|
|
622
|
+
result.createdApp = createdApp;
|
|
623
|
+
}
|
|
389
624
|
if (flags.json) return writeJson(result);
|
|
390
625
|
printWorkspaceInitReport(result);
|
|
391
626
|
return;
|
|
@@ -418,6 +653,7 @@ async function workspace(args) {
|
|
|
418
653
|
},
|
|
419
654
|
updatedAt: new Date().toISOString(),
|
|
420
655
|
};
|
|
656
|
+
ensureResourceBuckets(state.profiles[profileName]);
|
|
421
657
|
saveProjectState(state);
|
|
422
658
|
print(`当前工作区已绑定 ${profileName}: ${appType}`);
|
|
423
659
|
return;
|
|
@@ -463,13 +699,10 @@ async function app(args) {
|
|
|
463
699
|
if (subcommand === 'create') {
|
|
464
700
|
const name = flags.name || positional.join(' ');
|
|
465
701
|
if (!name) fail('用法: openxiangda app create <name> [--profile name]');
|
|
466
|
-
const data = await
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
description: flags.description || '',
|
|
471
|
-
iconfontCss: flags['iconfont-css'] || '',
|
|
472
|
-
},
|
|
702
|
+
const data = await createPlatformApp(config, profileName, {
|
|
703
|
+
name,
|
|
704
|
+
description: flags.description || '',
|
|
705
|
+
iconfontCss: flags['iconfont-css'] || '',
|
|
473
706
|
});
|
|
474
707
|
if (flags.json) return writeJson(data);
|
|
475
708
|
print(JSON.stringify(data, null, 2));
|
|
@@ -490,6 +723,29 @@ async function app(args) {
|
|
|
490
723
|
print(JSON.stringify(data, null, 2));
|
|
491
724
|
}
|
|
492
725
|
|
|
726
|
+
async function createPlatformApp(config, profileName, payload) {
|
|
727
|
+
return requestWithAuth(config, profileName, '/openxiangda-api/v1/apps/', {
|
|
728
|
+
method: 'POST',
|
|
729
|
+
body: {
|
|
730
|
+
name: payload.name,
|
|
731
|
+
description: payload.description || '',
|
|
732
|
+
iconfontCss: payload.iconfontCss || '',
|
|
733
|
+
},
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function extractCreatedAppType(data) {
|
|
738
|
+
const candidates = [
|
|
739
|
+
data?.appType,
|
|
740
|
+
data?.app?.appType,
|
|
741
|
+
data?.result?.appType,
|
|
742
|
+
data?.application?.appType,
|
|
743
|
+
data?.type,
|
|
744
|
+
];
|
|
745
|
+
const value = candidates.find(item => typeof item === 'string' && item.trim());
|
|
746
|
+
return value ? value.trim() : null;
|
|
747
|
+
}
|
|
748
|
+
|
|
493
749
|
async function form(args) {
|
|
494
750
|
const [subcommand, ...rest] = args;
|
|
495
751
|
const { flags, positional } = parseArgs(rest);
|
|
@@ -1800,10 +2056,11 @@ async function commands(args) {
|
|
|
1800
2056
|
name: 'openxiangda',
|
|
1801
2057
|
commands: [
|
|
1802
2058
|
'login <platform-url> [--profile name]',
|
|
2059
|
+
'update check|install',
|
|
1803
2060
|
'platform add|list|use|remove',
|
|
1804
2061
|
'auth status|refresh|logout',
|
|
1805
2062
|
'env',
|
|
1806
|
-
'workspace init|bind|publish [--prune]',
|
|
2063
|
+
'workspace init|bind|publish [--app-name] [--prune]',
|
|
1807
2064
|
'app list|create|snapshot',
|
|
1808
2065
|
'form list|create|bind|pull|publish',
|
|
1809
2066
|
'page list|publish|bind|releases|activate',
|
|
@@ -1828,6 +2085,9 @@ function printWorkspaceInitReport(result) {
|
|
|
1828
2085
|
`已创建 OpenXiangda workspace: ${result.targetDir}`,
|
|
1829
2086
|
`package: ${result.packageName}`,
|
|
1830
2087
|
];
|
|
2088
|
+
if (result.createdApp) {
|
|
2089
|
+
lines.push(`已创建平台应用: ${result.createdApp.name} (${result.createdApp.appType})`);
|
|
2090
|
+
}
|
|
1831
2091
|
if (result.bound) {
|
|
1832
2092
|
lines.push(`已绑定 ${result.bound.profile}: ${result.bound.appType}`);
|
|
1833
2093
|
}
|
|
@@ -1952,6 +2212,9 @@ function ensureResourceBuckets(bound) {
|
|
|
1952
2212
|
bound.resources.menus = bound.resources.menus || {};
|
|
1953
2213
|
bound.resources.roles = bound.resources.roles || {};
|
|
1954
2214
|
bound.resources.connectors = bound.resources.connectors || {};
|
|
2215
|
+
bound.resources.notifications = bound.resources.notifications || {};
|
|
2216
|
+
bound.resources.notifications.templates = bound.resources.notifications.templates || {};
|
|
2217
|
+
bound.resources.notifications.typeConfigs = bound.resources.notifications.typeConfigs || {};
|
|
1955
2218
|
bound.resources.pagePermissionGroups = bound.resources.pagePermissionGroups || {};
|
|
1956
2219
|
bound.resources.formPermissionGroups = bound.resources.formPermissionGroups || {};
|
|
1957
2220
|
bound.resources.formSettings = bound.resources.formSettings || {};
|
|
@@ -2156,6 +2419,46 @@ function saveConnectorResource(target, connectorCode, connectorId, extra = {}) {
|
|
|
2156
2419
|
}, ['connectorId', 'apis']);
|
|
2157
2420
|
}
|
|
2158
2421
|
|
|
2422
|
+
function saveNotificationTemplateResource(target, templateCode, templateId, extra = {}) {
|
|
2423
|
+
const nextBound = prepareStateResourceWrite(target);
|
|
2424
|
+
nextBound.resources.notifications.templates[templateCode] = {
|
|
2425
|
+
templateId,
|
|
2426
|
+
...pickStateFields(extra, ['level', 'formUuid']),
|
|
2427
|
+
updatedAt: new Date().toISOString(),
|
|
2428
|
+
};
|
|
2429
|
+
saveProjectState(target.state);
|
|
2430
|
+
}
|
|
2431
|
+
|
|
2432
|
+
function saveNotificationTypeConfigResource(target, configCode, configId, extra = {}) {
|
|
2433
|
+
const nextBound = prepareStateResourceWrite(target);
|
|
2434
|
+
nextBound.resources.notifications.typeConfigs[configCode] = {
|
|
2435
|
+
configId,
|
|
2436
|
+
...pickStateFields(extra, [
|
|
2437
|
+
'notificationType',
|
|
2438
|
+
'level',
|
|
2439
|
+
'formUuid',
|
|
2440
|
+
'templateId',
|
|
2441
|
+
'templateCode',
|
|
2442
|
+
]),
|
|
2443
|
+
updatedAt: new Date().toISOString(),
|
|
2444
|
+
};
|
|
2445
|
+
saveProjectState(target.state);
|
|
2446
|
+
}
|
|
2447
|
+
|
|
2448
|
+
function prepareStateResourceWrite(target) {
|
|
2449
|
+
target.state.profiles = target.state.profiles || {};
|
|
2450
|
+
target.state.profiles[target.profileName] = {
|
|
2451
|
+
...target.bound,
|
|
2452
|
+
baseUrl: target.profile.baseUrl,
|
|
2453
|
+
appType: target.appType,
|
|
2454
|
+
updatedAt: new Date().toISOString(),
|
|
2455
|
+
};
|
|
2456
|
+
const nextBound = target.state.profiles[target.profileName];
|
|
2457
|
+
ensureResourceBuckets(nextBound);
|
|
2458
|
+
target.bound = nextBound;
|
|
2459
|
+
return nextBound;
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2159
2462
|
function savePagePermissionGroupResource(target, groupCode, groupId) {
|
|
2160
2463
|
saveStateResource(target, 'pagePermissionGroups', groupCode, { groupId }, ['groupId']);
|
|
2161
2464
|
}
|
|
@@ -2202,6 +2505,7 @@ function pickStateFields(value, keys) {
|
|
|
2202
2505
|
const RESOURCE_SPECS = [
|
|
2203
2506
|
{ key: 'roles', dir: 'roles', topFiles: ['roles.json'], pluralKeys: ['roles'] },
|
|
2204
2507
|
{ key: 'connectors', dir: 'connectors', topFiles: ['connectors.json'], pluralKeys: ['connectors'] },
|
|
2508
|
+
{ key: 'notifications', dir: 'notifications', topFiles: ['notifications.json'], pluralKeys: ['notifications'] },
|
|
2205
2509
|
{ key: 'menus', dir: 'menus', topFiles: ['menus.json'], pluralKeys: ['menus'] },
|
|
2206
2510
|
{ key: 'workflows', dir: 'workflows', topFiles: ['workflows.json'], pluralKeys: ['workflows'] },
|
|
2207
2511
|
{ key: 'automations', dir: 'automations', topFiles: ['automations.json'], pluralKeys: ['automations'] },
|
|
@@ -2280,6 +2584,7 @@ function readResourceItemsFromFile(filePath, spec) {
|
|
|
2280
2584
|
item.code ||
|
|
2281
2585
|
item.resourceCode ||
|
|
2282
2586
|
item.methodName ||
|
|
2587
|
+
(spec.key === 'notifications' ? inferNotificationResourceCode(item) : undefined) ||
|
|
2283
2588
|
item.formCode ||
|
|
2284
2589
|
(values.length === 1 ? defaultCode : undefined);
|
|
2285
2590
|
return {
|
|
@@ -2292,7 +2597,20 @@ function readResourceItemsFromFile(filePath, spec) {
|
|
|
2292
2597
|
});
|
|
2293
2598
|
}
|
|
2294
2599
|
|
|
2600
|
+
function inferNotificationResourceCode(item) {
|
|
2601
|
+
const resourceType = normalizeNotificationResourceType(item);
|
|
2602
|
+
if (resourceType === 'template') return item.code || item.templateCode;
|
|
2603
|
+
if (resourceType !== 'typeConfig' || !item.notificationType) {
|
|
2604
|
+
return item.notificationType || item.templateCode;
|
|
2605
|
+
}
|
|
2606
|
+
const level = item.level || (item.formCode || item.formUuid ? 'form' : 'app');
|
|
2607
|
+
if (level === 'app') return item.notificationType;
|
|
2608
|
+
const formKey = item.formCode || item.formUuid || 'form';
|
|
2609
|
+
return `${level}:${formKey}:${item.notificationType}`;
|
|
2610
|
+
}
|
|
2611
|
+
|
|
2295
2612
|
function extractResourceValues(value, spec) {
|
|
2613
|
+
if (spec.key === 'notifications') return extractNotificationResourceValues(value);
|
|
2296
2614
|
if (Array.isArray(value)) return value;
|
|
2297
2615
|
if (!value || typeof value !== 'object') return [value];
|
|
2298
2616
|
for (const key of spec.pluralKeys || []) {
|
|
@@ -2302,6 +2620,29 @@ function extractResourceValues(value, spec) {
|
|
|
2302
2620
|
return [value];
|
|
2303
2621
|
}
|
|
2304
2622
|
|
|
2623
|
+
function extractNotificationResourceValues(value) {
|
|
2624
|
+
if (Array.isArray(value)) return value;
|
|
2625
|
+
if (!value || typeof value !== 'object') return [value];
|
|
2626
|
+
const result = [];
|
|
2627
|
+
for (const template of value.templates || []) {
|
|
2628
|
+
result.push({ ...template, resourceType: 'template' });
|
|
2629
|
+
}
|
|
2630
|
+
for (const config of value.typeConfigs || value.notificationTypeConfigs || []) {
|
|
2631
|
+
result.push({ ...config, resourceType: 'typeConfig' });
|
|
2632
|
+
}
|
|
2633
|
+
if (Array.isArray(value.notifications)) {
|
|
2634
|
+
result.push(...value.notifications);
|
|
2635
|
+
}
|
|
2636
|
+
if (result.length > 0) return result;
|
|
2637
|
+
if (value.template || value.typeConfig) {
|
|
2638
|
+
return [
|
|
2639
|
+
value.template ? { ...value.template, resourceType: 'template' } : null,
|
|
2640
|
+
value.typeConfig ? { ...value.typeConfig, resourceType: 'typeConfig' } : null,
|
|
2641
|
+
].filter(Boolean);
|
|
2642
|
+
}
|
|
2643
|
+
return [value];
|
|
2644
|
+
}
|
|
2645
|
+
|
|
2305
2646
|
function listJsonFiles(dirPath) {
|
|
2306
2647
|
if (!fs.existsSync(dirPath)) return [];
|
|
2307
2648
|
return fs
|
|
@@ -2325,6 +2666,7 @@ function validateWorkspaceResources(manifest) {
|
|
|
2325
2666
|
validateResourceItem(spec.key, item, errors, warnings);
|
|
2326
2667
|
}
|
|
2327
2668
|
}
|
|
2669
|
+
validateNotificationReferences(manifest.notifications || [], errors);
|
|
2328
2670
|
return {
|
|
2329
2671
|
valid: errors.length === 0,
|
|
2330
2672
|
errors,
|
|
@@ -2357,6 +2699,11 @@ function validateResourceItem(kind, item, errors, warnings) {
|
|
|
2357
2699
|
return;
|
|
2358
2700
|
}
|
|
2359
2701
|
|
|
2702
|
+
if (kind === 'notifications') {
|
|
2703
|
+
validateNotificationResourceItem(item, errors, warnings);
|
|
2704
|
+
return;
|
|
2705
|
+
}
|
|
2706
|
+
|
|
2360
2707
|
if (kind === 'roles' && !item.name) errors.push(`${label}: 缺少 name`);
|
|
2361
2708
|
if (kind === 'menus' && !item.name) errors.push(`${label}: 缺少 name`);
|
|
2362
2709
|
if (kind === 'workflows') {
|
|
@@ -2392,6 +2739,99 @@ function validateResourceItem(kind, item, errors, warnings) {
|
|
|
2392
2739
|
}
|
|
2393
2740
|
}
|
|
2394
2741
|
|
|
2742
|
+
const NOTIFICATION_CHANNELS = new Set([
|
|
2743
|
+
'inapp',
|
|
2744
|
+
'email',
|
|
2745
|
+
'dingding',
|
|
2746
|
+
'wechat',
|
|
2747
|
+
'thirdparty_todo',
|
|
2748
|
+
]);
|
|
2749
|
+
|
|
2750
|
+
function validateNotificationResourceItem(item, errors, warnings) {
|
|
2751
|
+
const label = resourceLabel('notifications', item);
|
|
2752
|
+
const resourceType = normalizeNotificationResourceType(item);
|
|
2753
|
+
if (!resourceType) {
|
|
2754
|
+
errors.push(`${label}: resourceType 必须是 template 或 typeConfig`);
|
|
2755
|
+
return;
|
|
2756
|
+
}
|
|
2757
|
+
|
|
2758
|
+
if (resourceType === 'template') {
|
|
2759
|
+
if (!item.code) errors.push(`${label}: 通知模板缺少 code`);
|
|
2760
|
+
if (!item.name) errors.push(`${label}: 通知模板缺少 name`);
|
|
2761
|
+
if (item.level && !['app', 'form'].includes(item.level)) {
|
|
2762
|
+
errors.push(`${label}: OpenXiangda 通知模板 level 只能是 app 或 form`);
|
|
2763
|
+
}
|
|
2764
|
+
if ((item.level === 'form' || item.formCode || item.formUuid) && !item.formCode && !item.formUuid) {
|
|
2765
|
+
errors.push(`${label}: 表单级通知模板缺少 formCode 或 formUuid`);
|
|
2766
|
+
}
|
|
2767
|
+
if (!item.content && !item.channelsConfig) {
|
|
2768
|
+
warnings.push(`${label}: 未声明 content 或 channelsConfig`);
|
|
2769
|
+
}
|
|
2770
|
+
validateNotificationChannels(label, item.channelsConfig, errors);
|
|
2771
|
+
return;
|
|
2772
|
+
}
|
|
2773
|
+
|
|
2774
|
+
if (!item.notificationType) errors.push(`${label}: 通知类型配置缺少 notificationType`);
|
|
2775
|
+
if (!item.templateCode && !item.templateId) {
|
|
2776
|
+
errors.push(`${label}: 通知类型配置缺少 templateCode 或 templateId`);
|
|
2777
|
+
}
|
|
2778
|
+
if (item.level && !['app', 'form'].includes(item.level)) {
|
|
2779
|
+
errors.push(`${label}: OpenXiangda 通知类型配置 level 只能是 app 或 form`);
|
|
2780
|
+
}
|
|
2781
|
+
if ((item.level === 'form' || item.formCode || item.formUuid) && !item.formCode && !item.formUuid) {
|
|
2782
|
+
errors.push(`${label}: 表单级通知类型配置缺少 formCode 或 formUuid`);
|
|
2783
|
+
}
|
|
2784
|
+
}
|
|
2785
|
+
|
|
2786
|
+
function normalizeNotificationResourceType(item) {
|
|
2787
|
+
const value = item.resourceType || item.kind || item.type;
|
|
2788
|
+
if (value === 'template' || value === 'notificationTemplate') return 'template';
|
|
2789
|
+
if (value === 'typeConfig' || value === 'notificationTypeConfig') return 'typeConfig';
|
|
2790
|
+
if (item.notificationType || item.templateCode || item.templateId) return 'typeConfig';
|
|
2791
|
+
if (item.channelsConfig || item.content || item.variables) return 'template';
|
|
2792
|
+
return undefined;
|
|
2793
|
+
}
|
|
2794
|
+
|
|
2795
|
+
function validateNotificationReferences(items, errors) {
|
|
2796
|
+
const templates = new Set(
|
|
2797
|
+
(items || [])
|
|
2798
|
+
.filter(item => normalizeNotificationResourceType(item) === 'template')
|
|
2799
|
+
.map(item => item.code)
|
|
2800
|
+
.filter(Boolean)
|
|
2801
|
+
);
|
|
2802
|
+
for (const item of items || []) {
|
|
2803
|
+
if (normalizeNotificationResourceType(item) !== 'typeConfig') continue;
|
|
2804
|
+
if (item.templateCode && !templates.has(item.templateCode) && !item.templateId) {
|
|
2805
|
+
errors.push(
|
|
2806
|
+
`${resourceLabel('notifications', item)}: templateCode ${item.templateCode} 未在 src/resources/notifications 中声明`
|
|
2807
|
+
);
|
|
2808
|
+
}
|
|
2809
|
+
}
|
|
2810
|
+
}
|
|
2811
|
+
|
|
2812
|
+
function validateNotificationChannels(label, channelsConfig, errors) {
|
|
2813
|
+
if (!channelsConfig || typeof channelsConfig !== 'object' || Array.isArray(channelsConfig)) {
|
|
2814
|
+
return;
|
|
2815
|
+
}
|
|
2816
|
+
for (const [channel, config] of Object.entries(channelsConfig)) {
|
|
2817
|
+
if (!NOTIFICATION_CHANNELS.has(channel)) {
|
|
2818
|
+
errors.push(`${label}: 不支持的通知渠道 ${channel}`);
|
|
2819
|
+
}
|
|
2820
|
+
validateNoSensitiveNotificationConfig(`${label}.channelsConfig.${channel}`, config, errors);
|
|
2821
|
+
}
|
|
2822
|
+
}
|
|
2823
|
+
|
|
2824
|
+
function validateNoSensitiveNotificationConfig(label, value, errors) {
|
|
2825
|
+
if (!value || typeof value !== 'object') return;
|
|
2826
|
+
for (const [key, item] of Object.entries(value)) {
|
|
2827
|
+
if (/token|secret|password|authorization|auth|credential|headers/i.test(key)) {
|
|
2828
|
+
errors.push(`${label}.${key}: 通知资源不允许配置通道密钥或鉴权字段`);
|
|
2829
|
+
continue;
|
|
2830
|
+
}
|
|
2831
|
+
validateNoSensitiveNotificationConfig(`${label}.${key}`, item, errors);
|
|
2832
|
+
}
|
|
2833
|
+
}
|
|
2834
|
+
|
|
2395
2835
|
function resourceLabel(kind, item) {
|
|
2396
2836
|
return `${kind}:${item.code || item.__source || item.__index || '?'}`;
|
|
2397
2837
|
}
|
|
@@ -2412,6 +2852,7 @@ async function buildResourcePlan(config, target, manifest) {
|
|
|
2412
2852
|
addPlanActions(actions, 'role', manifest.roles, existing.roles, roleEquals);
|
|
2413
2853
|
addPlanActions(actions, 'menu', manifest.menus, existing.menus, (item, current) => menuEquals(target.bound, item, current));
|
|
2414
2854
|
addPlanActions(actions, 'connector', manifest.connectors, existing.connectors, connectorEquals);
|
|
2855
|
+
addNotificationPlanActions(target, actions, manifest.notifications || [], existing);
|
|
2415
2856
|
await addWorkflowPlanActions(config, target, actions, manifest.workflows, existing.workflows);
|
|
2416
2857
|
await addAutomationPlanActions(config, target, actions, manifest.automations, existing.automations);
|
|
2417
2858
|
addPlanActions(actions, 'pagePermissionGroup', manifest.pagePermissionGroups, existing.pagePermissionGroups, (item, current) => pagePermissionGroupEquals(target.bound, item, current));
|
|
@@ -2444,6 +2885,7 @@ async function publishResourceManifest(config, target, manifest, options = {}) {
|
|
|
2444
2885
|
await publishFormSettingsResources(config, target, manifest.formSettings || [], result);
|
|
2445
2886
|
await publishMenuResources(config, target, manifest.menus || [], result);
|
|
2446
2887
|
await publishConnectorResources(config, target, manifest.connectors || [], result);
|
|
2888
|
+
await publishNotificationResources(config, target, manifest.notifications || [], result);
|
|
2447
2889
|
await publishWorkflowResources(config, target, manifest.workflows || [], result);
|
|
2448
2890
|
await publishAutomationResources(config, target, manifest.automations || [], result);
|
|
2449
2891
|
await publishPagePermissionGroupResources(config, target, manifest.pagePermissionGroups || [], result);
|
|
@@ -2463,6 +2905,8 @@ async function fetchExistingResourceMaps(config, target, manifest) {
|
|
|
2463
2905
|
roles: new Map(),
|
|
2464
2906
|
menus: new Map(),
|
|
2465
2907
|
connectors: new Map(),
|
|
2908
|
+
notificationTemplates: new Map(),
|
|
2909
|
+
notificationTypeConfigs: new Map(),
|
|
2466
2910
|
workflows: new Map(),
|
|
2467
2911
|
automations: new Map(),
|
|
2468
2912
|
pagePermissionGroups: new Map(),
|
|
@@ -2496,6 +2940,31 @@ async function fetchExistingResourceMaps(config, target, manifest) {
|
|
|
2496
2940
|
);
|
|
2497
2941
|
indexByCode(maps.connectors, normalizeItems(data), item => item.code || item.methodName);
|
|
2498
2942
|
}
|
|
2943
|
+
if ((manifest.notifications || []).length > 0) {
|
|
2944
|
+
const templates = await requestWithAuth(
|
|
2945
|
+
config,
|
|
2946
|
+
target.profileName,
|
|
2947
|
+
apiPathWithQuery(`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/notifications/templates`, {
|
|
2948
|
+
page: 1,
|
|
2949
|
+
limit: 1000,
|
|
2950
|
+
})
|
|
2951
|
+
);
|
|
2952
|
+
indexByCode(maps.notificationTemplates, normalizeItems(templates), item => item.code);
|
|
2953
|
+
|
|
2954
|
+
const typeConfigs = await requestWithAuth(
|
|
2955
|
+
config,
|
|
2956
|
+
target.profileName,
|
|
2957
|
+
apiPathWithQuery(`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/notifications/type-configs`, {
|
|
2958
|
+
page: 1,
|
|
2959
|
+
limit: 1000,
|
|
2960
|
+
})
|
|
2961
|
+
);
|
|
2962
|
+
indexByCode(
|
|
2963
|
+
maps.notificationTypeConfigs,
|
|
2964
|
+
normalizeItems(typeConfigs),
|
|
2965
|
+
item => notificationTypeConfigExistingKey(item)
|
|
2966
|
+
);
|
|
2967
|
+
}
|
|
2499
2968
|
if ((manifest.workflows || []).length > 0) {
|
|
2500
2969
|
const data = await requestWithAuth(
|
|
2501
2970
|
config,
|
|
@@ -2558,6 +3027,37 @@ function addPlanActions(actions, kind, desiredItems = [], existingMap, equals) {
|
|
|
2558
3027
|
}
|
|
2559
3028
|
}
|
|
2560
3029
|
|
|
3030
|
+
function addNotificationPlanActions(target, actions, notificationItems = [], existing) {
|
|
3031
|
+
const { templates, typeConfigs } = splitNotificationResources(notificationItems);
|
|
3032
|
+
for (const template of templates) {
|
|
3033
|
+
const existingTemplate = existing.notificationTemplates.get(template.code);
|
|
3034
|
+
actions.push({
|
|
3035
|
+
kind: 'notificationTemplate',
|
|
3036
|
+
code: template.code,
|
|
3037
|
+
action: existingTemplate
|
|
3038
|
+
? notificationTemplateEquals(target.bound, template, existingTemplate)
|
|
3039
|
+
? 'noop'
|
|
3040
|
+
: 'update'
|
|
3041
|
+
: 'create',
|
|
3042
|
+
platformId: existingTemplate?.id,
|
|
3043
|
+
});
|
|
3044
|
+
}
|
|
3045
|
+
for (const config of typeConfigs) {
|
|
3046
|
+
const key = notificationTypeConfigDesiredKey(target.bound, config);
|
|
3047
|
+
const existingConfig = existing.notificationTypeConfigs.get(key);
|
|
3048
|
+
actions.push({
|
|
3049
|
+
kind: 'notificationTypeConfig',
|
|
3050
|
+
code: config.code || config.notificationType,
|
|
3051
|
+
action: existingConfig
|
|
3052
|
+
? notificationTypeConfigEquals(target.bound, config, existingConfig)
|
|
3053
|
+
? 'noop'
|
|
3054
|
+
: 'update'
|
|
3055
|
+
: 'create',
|
|
3056
|
+
platformId: existingConfig?.id,
|
|
3057
|
+
});
|
|
3058
|
+
}
|
|
3059
|
+
}
|
|
3060
|
+
|
|
2561
3061
|
async function addWorkflowPlanActions(config, target, actions, desiredItems = [], existingMap) {
|
|
2562
3062
|
for (const item of desiredItems) {
|
|
2563
3063
|
const code = item.code || item.resourceCode;
|
|
@@ -2842,6 +3342,57 @@ async function publishConnectorResources(config, target, connectors, result) {
|
|
|
2842
3342
|
}
|
|
2843
3343
|
}
|
|
2844
3344
|
|
|
3345
|
+
async function publishNotificationResources(config, target, notifications, result) {
|
|
3346
|
+
const { templates, typeConfigs } = splitNotificationResources(notifications);
|
|
3347
|
+
for (const template of templates) {
|
|
3348
|
+
const body = normalizeNotificationTemplateManifest(target.bound, template);
|
|
3349
|
+
const data = await requestWithAuth(
|
|
3350
|
+
config,
|
|
3351
|
+
target.profileName,
|
|
3352
|
+
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/notifications/templates/${encodeURIComponent(template.code)}`,
|
|
3353
|
+
{ method: 'PUT', body }
|
|
3354
|
+
);
|
|
3355
|
+
if (data?.id) {
|
|
3356
|
+
saveNotificationTemplateResource(target, template.code, data.id, {
|
|
3357
|
+
level: data.level || body.level,
|
|
3358
|
+
formUuid: data.formUuid || body.formUuid,
|
|
3359
|
+
});
|
|
3360
|
+
}
|
|
3361
|
+
result.published.push({
|
|
3362
|
+
kind: 'notificationTemplate',
|
|
3363
|
+
code: template.code,
|
|
3364
|
+
action: data?.created ? 'create' : 'update',
|
|
3365
|
+
id: data?.id,
|
|
3366
|
+
});
|
|
3367
|
+
}
|
|
3368
|
+
|
|
3369
|
+
for (const configItem of typeConfigs) {
|
|
3370
|
+
const body = normalizeNotificationTypeConfigManifest(target.bound, configItem);
|
|
3371
|
+
const data = await requestWithAuth(
|
|
3372
|
+
config,
|
|
3373
|
+
target.profileName,
|
|
3374
|
+
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/notifications/type-configs/${encodeURIComponent(configItem.notificationType)}`,
|
|
3375
|
+
{ method: 'PUT', body }
|
|
3376
|
+
);
|
|
3377
|
+
const stateCode = configItem.code || notificationTypeConfigDesiredKey(target.bound, configItem);
|
|
3378
|
+
if (data?.id) {
|
|
3379
|
+
saveNotificationTypeConfigResource(target, stateCode, data.id, {
|
|
3380
|
+
notificationType: data.notificationType || configItem.notificationType,
|
|
3381
|
+
level: data.level || body.level,
|
|
3382
|
+
formUuid: data.formUuid || body.formUuid,
|
|
3383
|
+
templateId: data.templateId,
|
|
3384
|
+
templateCode: data.template?.code || configItem.templateCode,
|
|
3385
|
+
});
|
|
3386
|
+
}
|
|
3387
|
+
result.published.push({
|
|
3388
|
+
kind: 'notificationTypeConfig',
|
|
3389
|
+
code: configItem.code || configItem.notificationType,
|
|
3390
|
+
action: data?.created ? 'create' : 'update',
|
|
3391
|
+
id: data?.id,
|
|
3392
|
+
});
|
|
3393
|
+
}
|
|
3394
|
+
}
|
|
3395
|
+
|
|
2845
3396
|
async function publishWorkflowResources(config, target, workflows, result) {
|
|
2846
3397
|
for (const workflowItem of workflows) {
|
|
2847
3398
|
const existing = await findExistingWorkflow(config, target, workflowItem.code);
|
|
@@ -3379,6 +3930,40 @@ async function pullResources(config, target) {
|
|
|
3379
3930
|
written.push(path.relative(process.cwd(), filePath));
|
|
3380
3931
|
}
|
|
3381
3932
|
|
|
3933
|
+
const notificationTemplates = await requestWithAuth(
|
|
3934
|
+
config,
|
|
3935
|
+
target.profileName,
|
|
3936
|
+
apiPathWithQuery(`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/notifications/templates`, {
|
|
3937
|
+
page: 1,
|
|
3938
|
+
limit: 1000,
|
|
3939
|
+
})
|
|
3940
|
+
);
|
|
3941
|
+
for (const template of normalizeItems(notificationTemplates)) {
|
|
3942
|
+
const code = template.code || template.id;
|
|
3943
|
+
const filePath = path.join(baseDir, 'notifications', 'templates', `${code}.json`);
|
|
3944
|
+
writeResourceJsonFile(filePath, {
|
|
3945
|
+
templates: [toPulledNotificationTemplate(template, pullLookups)],
|
|
3946
|
+
});
|
|
3947
|
+
written.push(path.relative(process.cwd(), filePath));
|
|
3948
|
+
}
|
|
3949
|
+
|
|
3950
|
+
const notificationTypeConfigs = await requestWithAuth(
|
|
3951
|
+
config,
|
|
3952
|
+
target.profileName,
|
|
3953
|
+
apiPathWithQuery(`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/notifications/type-configs`, {
|
|
3954
|
+
page: 1,
|
|
3955
|
+
limit: 1000,
|
|
3956
|
+
})
|
|
3957
|
+
);
|
|
3958
|
+
for (const configItem of normalizeItems(notificationTypeConfigs)) {
|
|
3959
|
+
const code = notificationTypeConfigExistingKey(configItem).replace(/[^a-zA-Z0-9_.-]+/g, '_');
|
|
3960
|
+
const filePath = path.join(baseDir, 'notifications', 'type-configs', `${code}.json`);
|
|
3961
|
+
writeResourceJsonFile(filePath, {
|
|
3962
|
+
typeConfigs: [toPulledNotificationTypeConfig(configItem, pullLookups)],
|
|
3963
|
+
});
|
|
3964
|
+
written.push(path.relative(process.cwd(), filePath));
|
|
3965
|
+
}
|
|
3966
|
+
|
|
3382
3967
|
const menus = await requestWithAuth(
|
|
3383
3968
|
config,
|
|
3384
3969
|
target.profileName,
|
|
@@ -3598,6 +4183,47 @@ function splitPagePermissionTargetsForManifest(values = [], lookups) {
|
|
|
3598
4183
|
};
|
|
3599
4184
|
}
|
|
3600
4185
|
|
|
4186
|
+
function toPulledNotificationTemplate(template, lookups) {
|
|
4187
|
+
const formCode = lookups.formCodeByUuid.get(template.formUuid);
|
|
4188
|
+
return stripUndefinedValues({
|
|
4189
|
+
code: template.code,
|
|
4190
|
+
name: template.name,
|
|
4191
|
+
content: template.content || '',
|
|
4192
|
+
description: template.description || '',
|
|
4193
|
+
level: template.level === 'form' ? 'form' : 'app',
|
|
4194
|
+
...(formCode ? { formCode } : template.formUuid ? { formUuid: template.formUuid } : {}),
|
|
4195
|
+
priority: template.priority,
|
|
4196
|
+
enabled: template.enabled,
|
|
4197
|
+
variables: template.variables || undefined,
|
|
4198
|
+
channelsConfig: stripNotificationSecrets(template.channelsConfig),
|
|
4199
|
+
});
|
|
4200
|
+
}
|
|
4201
|
+
|
|
4202
|
+
function toPulledNotificationTypeConfig(configItem, lookups) {
|
|
4203
|
+
const formCode = lookups.formCodeByUuid.get(configItem.formUuid);
|
|
4204
|
+
return stripUndefinedValues({
|
|
4205
|
+
code: notificationTypeConfigExistingKey(configItem),
|
|
4206
|
+
notificationType: configItem.notificationType,
|
|
4207
|
+
level: configItem.level === 'form' ? 'form' : 'app',
|
|
4208
|
+
...(formCode ? { formCode } : configItem.formUuid ? { formUuid: configItem.formUuid } : {}),
|
|
4209
|
+
templateCode: configItem.template?.code,
|
|
4210
|
+
enabled: configItem.enabled,
|
|
4211
|
+
priority: configItem.priority,
|
|
4212
|
+
description: configItem.description || '',
|
|
4213
|
+
});
|
|
4214
|
+
}
|
|
4215
|
+
|
|
4216
|
+
function stripNotificationSecrets(value) {
|
|
4217
|
+
if (!value || typeof value !== 'object') return value;
|
|
4218
|
+
if (Array.isArray(value)) return value.map(stripNotificationSecrets);
|
|
4219
|
+
const next = {};
|
|
4220
|
+
for (const [key, item] of Object.entries(value)) {
|
|
4221
|
+
if (/token|secret|password|authorization|auth|credential|headers/i.test(key)) continue;
|
|
4222
|
+
next[key] = stripNotificationSecrets(item);
|
|
4223
|
+
}
|
|
4224
|
+
return next;
|
|
4225
|
+
}
|
|
4226
|
+
|
|
3601
4227
|
function writeResourceJsonFile(filePath, value) {
|
|
3602
4228
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
3603
4229
|
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
@@ -3671,6 +4297,58 @@ function parseConnectorUrl(connector) {
|
|
|
3671
4297
|
}
|
|
3672
4298
|
}
|
|
3673
4299
|
|
|
4300
|
+
function splitNotificationResources(items = []) {
|
|
4301
|
+
const templates = [];
|
|
4302
|
+
const typeConfigs = [];
|
|
4303
|
+
for (const item of items || []) {
|
|
4304
|
+
const resourceType = normalizeNotificationResourceType(item);
|
|
4305
|
+
if (resourceType === 'template') templates.push(item);
|
|
4306
|
+
if (resourceType === 'typeConfig') typeConfigs.push(item);
|
|
4307
|
+
}
|
|
4308
|
+
return { templates, typeConfigs };
|
|
4309
|
+
}
|
|
4310
|
+
|
|
4311
|
+
function normalizeNotificationTemplateManifest(bound, template) {
|
|
4312
|
+
const level = template.level || (template.formCode || template.formUuid ? 'form' : 'app');
|
|
4313
|
+
const formUuid = level === 'form' ? resolveManifestFormUuid(bound, template) : undefined;
|
|
4314
|
+
return stripUndefinedValues({
|
|
4315
|
+
name: template.name || template.code,
|
|
4316
|
+
content: template.content || '',
|
|
4317
|
+
description: template.description || '',
|
|
4318
|
+
channelsConfig: template.channelsConfig,
|
|
4319
|
+
level,
|
|
4320
|
+
formUuid,
|
|
4321
|
+
priority: template.priority,
|
|
4322
|
+
enabled: template.enabled,
|
|
4323
|
+
variables: template.variables,
|
|
4324
|
+
});
|
|
4325
|
+
}
|
|
4326
|
+
|
|
4327
|
+
function normalizeNotificationTypeConfigManifest(bound, config) {
|
|
4328
|
+
const level = config.level || (config.formCode || config.formUuid ? 'form' : 'app');
|
|
4329
|
+
const formUuid = level === 'form' ? resolveManifestFormUuid(bound, config) : undefined;
|
|
4330
|
+
return stripUndefinedValues({
|
|
4331
|
+
level,
|
|
4332
|
+
formUuid,
|
|
4333
|
+
templateId: config.templateId,
|
|
4334
|
+
templateCode: config.templateCode,
|
|
4335
|
+
enabled: config.enabled,
|
|
4336
|
+
priority: config.priority,
|
|
4337
|
+
description: config.description || '',
|
|
4338
|
+
});
|
|
4339
|
+
}
|
|
4340
|
+
|
|
4341
|
+
function notificationTypeConfigDesiredKey(bound, config) {
|
|
4342
|
+
const level = config.level || (config.formCode || config.formUuid ? 'form' : 'app');
|
|
4343
|
+
const formUuid = level === 'form' ? resolveManifestFormUuid(bound, config) : '';
|
|
4344
|
+
return `${level}:${formUuid || ''}:${config.notificationType}`;
|
|
4345
|
+
}
|
|
4346
|
+
|
|
4347
|
+
function notificationTypeConfigExistingKey(config) {
|
|
4348
|
+
const level = config.level || 'app';
|
|
4349
|
+
return `${level}:${config.formUuid || ''}:${config.notificationType}`;
|
|
4350
|
+
}
|
|
4351
|
+
|
|
3674
4352
|
async function resolveManifestJson(config, profileName, item, objectKey, fileKey, optional = false) {
|
|
3675
4353
|
let value = item[objectKey];
|
|
3676
4354
|
if (value === undefined && item[fileKey]) {
|
|
@@ -3778,6 +4456,40 @@ function connectorEquals(desired, existing) {
|
|
|
3778
4456
|
return JSON.stringify(normalizedExisting) === JSON.stringify(normalizedDesired);
|
|
3779
4457
|
}
|
|
3780
4458
|
|
|
4459
|
+
function notificationTemplateEquals(bound, desired, existing) {
|
|
4460
|
+
if (!existing) return false;
|
|
4461
|
+
const expected = normalizeNotificationTemplateManifest(bound, desired);
|
|
4462
|
+
return (
|
|
4463
|
+
String(existing.code || '') === String(desired.code || '') &&
|
|
4464
|
+
String(existing.name || '') === String(expected.name || '') &&
|
|
4465
|
+
String(existing.content || '') === String(expected.content || '') &&
|
|
4466
|
+
String(existing.description || '') === String(expected.description || '') &&
|
|
4467
|
+
String(existing.level || 'app') === String(expected.level || 'app') &&
|
|
4468
|
+
String(existing.formUuid || '') === String(expected.formUuid || '') &&
|
|
4469
|
+
optionalScalarEquals(existing.priority, expected.priority, expected.priority !== undefined) &&
|
|
4470
|
+
optionalScalarEquals(Boolean(existing.enabled), Boolean(expected.enabled), expected.enabled !== undefined) &&
|
|
4471
|
+
jsonEqualsForPlan(existing.variables || [], expected.variables || [], desired.__dir) &&
|
|
4472
|
+
jsonEqualsForPlan(existing.channelsConfig || {}, expected.channelsConfig || {}, desired.__dir)
|
|
4473
|
+
);
|
|
4474
|
+
}
|
|
4475
|
+
|
|
4476
|
+
function notificationTypeConfigEquals(bound, desired, existing) {
|
|
4477
|
+
if (!existing) return false;
|
|
4478
|
+
const expected = normalizeNotificationTypeConfigManifest(bound, desired);
|
|
4479
|
+
const existingTemplateCode = existing.template?.code;
|
|
4480
|
+
return (
|
|
4481
|
+
String(existing.notificationType || '') === String(desired.notificationType || '') &&
|
|
4482
|
+
String(existing.level || 'app') === String(expected.level || 'app') &&
|
|
4483
|
+
String(existing.formUuid || '') === String(expected.formUuid || '') &&
|
|
4484
|
+
(expected.templateId
|
|
4485
|
+
? String(existing.templateId || '') === String(expected.templateId)
|
|
4486
|
+
: String(existingTemplateCode || '') === String(expected.templateCode || '')) &&
|
|
4487
|
+
optionalScalarEquals(Boolean(existing.enabled), Boolean(expected.enabled), expected.enabled !== undefined) &&
|
|
4488
|
+
optionalScalarEquals(existing.priority, expected.priority, expected.priority !== undefined) &&
|
|
4489
|
+
String(existing.description || '') === String(expected.description || '')
|
|
4490
|
+
);
|
|
4491
|
+
}
|
|
4492
|
+
|
|
3781
4493
|
function pagePermissionGroupEquals(bound, desired, existing) {
|
|
3782
4494
|
if (!existing) return false;
|
|
3783
4495
|
const desiredTargets = resolvePagePermissionGroupTargets(bound, desired);
|