openxiangda 1.0.21 → 1.0.22
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 +452 -0
- package/openxiangda-skills/SKILL.md +1 -0
- package/openxiangda-skills/references/automation-v3.md +2 -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/skills/openxiangda-page/SKILL.md +2 -0
- package/openxiangda-skills/skills/openxiangda-workflow-automation/SKILL.md +2 -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/lib/cli.js
CHANGED
|
@@ -1952,6 +1952,9 @@ function ensureResourceBuckets(bound) {
|
|
|
1952
1952
|
bound.resources.menus = bound.resources.menus || {};
|
|
1953
1953
|
bound.resources.roles = bound.resources.roles || {};
|
|
1954
1954
|
bound.resources.connectors = bound.resources.connectors || {};
|
|
1955
|
+
bound.resources.notifications = bound.resources.notifications || {};
|
|
1956
|
+
bound.resources.notifications.templates = bound.resources.notifications.templates || {};
|
|
1957
|
+
bound.resources.notifications.typeConfigs = bound.resources.notifications.typeConfigs || {};
|
|
1955
1958
|
bound.resources.pagePermissionGroups = bound.resources.pagePermissionGroups || {};
|
|
1956
1959
|
bound.resources.formPermissionGroups = bound.resources.formPermissionGroups || {};
|
|
1957
1960
|
bound.resources.formSettings = bound.resources.formSettings || {};
|
|
@@ -2156,6 +2159,46 @@ function saveConnectorResource(target, connectorCode, connectorId, extra = {}) {
|
|
|
2156
2159
|
}, ['connectorId', 'apis']);
|
|
2157
2160
|
}
|
|
2158
2161
|
|
|
2162
|
+
function saveNotificationTemplateResource(target, templateCode, templateId, extra = {}) {
|
|
2163
|
+
const nextBound = prepareStateResourceWrite(target);
|
|
2164
|
+
nextBound.resources.notifications.templates[templateCode] = {
|
|
2165
|
+
templateId,
|
|
2166
|
+
...pickStateFields(extra, ['level', 'formUuid']),
|
|
2167
|
+
updatedAt: new Date().toISOString(),
|
|
2168
|
+
};
|
|
2169
|
+
saveProjectState(target.state);
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2172
|
+
function saveNotificationTypeConfigResource(target, configCode, configId, extra = {}) {
|
|
2173
|
+
const nextBound = prepareStateResourceWrite(target);
|
|
2174
|
+
nextBound.resources.notifications.typeConfigs[configCode] = {
|
|
2175
|
+
configId,
|
|
2176
|
+
...pickStateFields(extra, [
|
|
2177
|
+
'notificationType',
|
|
2178
|
+
'level',
|
|
2179
|
+
'formUuid',
|
|
2180
|
+
'templateId',
|
|
2181
|
+
'templateCode',
|
|
2182
|
+
]),
|
|
2183
|
+
updatedAt: new Date().toISOString(),
|
|
2184
|
+
};
|
|
2185
|
+
saveProjectState(target.state);
|
|
2186
|
+
}
|
|
2187
|
+
|
|
2188
|
+
function prepareStateResourceWrite(target) {
|
|
2189
|
+
target.state.profiles = target.state.profiles || {};
|
|
2190
|
+
target.state.profiles[target.profileName] = {
|
|
2191
|
+
...target.bound,
|
|
2192
|
+
baseUrl: target.profile.baseUrl,
|
|
2193
|
+
appType: target.appType,
|
|
2194
|
+
updatedAt: new Date().toISOString(),
|
|
2195
|
+
};
|
|
2196
|
+
const nextBound = target.state.profiles[target.profileName];
|
|
2197
|
+
ensureResourceBuckets(nextBound);
|
|
2198
|
+
target.bound = nextBound;
|
|
2199
|
+
return nextBound;
|
|
2200
|
+
}
|
|
2201
|
+
|
|
2159
2202
|
function savePagePermissionGroupResource(target, groupCode, groupId) {
|
|
2160
2203
|
saveStateResource(target, 'pagePermissionGroups', groupCode, { groupId }, ['groupId']);
|
|
2161
2204
|
}
|
|
@@ -2202,6 +2245,7 @@ function pickStateFields(value, keys) {
|
|
|
2202
2245
|
const RESOURCE_SPECS = [
|
|
2203
2246
|
{ key: 'roles', dir: 'roles', topFiles: ['roles.json'], pluralKeys: ['roles'] },
|
|
2204
2247
|
{ key: 'connectors', dir: 'connectors', topFiles: ['connectors.json'], pluralKeys: ['connectors'] },
|
|
2248
|
+
{ key: 'notifications', dir: 'notifications', topFiles: ['notifications.json'], pluralKeys: ['notifications'] },
|
|
2205
2249
|
{ key: 'menus', dir: 'menus', topFiles: ['menus.json'], pluralKeys: ['menus'] },
|
|
2206
2250
|
{ key: 'workflows', dir: 'workflows', topFiles: ['workflows.json'], pluralKeys: ['workflows'] },
|
|
2207
2251
|
{ key: 'automations', dir: 'automations', topFiles: ['automations.json'], pluralKeys: ['automations'] },
|
|
@@ -2280,6 +2324,7 @@ function readResourceItemsFromFile(filePath, spec) {
|
|
|
2280
2324
|
item.code ||
|
|
2281
2325
|
item.resourceCode ||
|
|
2282
2326
|
item.methodName ||
|
|
2327
|
+
(spec.key === 'notifications' ? inferNotificationResourceCode(item) : undefined) ||
|
|
2283
2328
|
item.formCode ||
|
|
2284
2329
|
(values.length === 1 ? defaultCode : undefined);
|
|
2285
2330
|
return {
|
|
@@ -2292,7 +2337,20 @@ function readResourceItemsFromFile(filePath, spec) {
|
|
|
2292
2337
|
});
|
|
2293
2338
|
}
|
|
2294
2339
|
|
|
2340
|
+
function inferNotificationResourceCode(item) {
|
|
2341
|
+
const resourceType = normalizeNotificationResourceType(item);
|
|
2342
|
+
if (resourceType === 'template') return item.code || item.templateCode;
|
|
2343
|
+
if (resourceType !== 'typeConfig' || !item.notificationType) {
|
|
2344
|
+
return item.notificationType || item.templateCode;
|
|
2345
|
+
}
|
|
2346
|
+
const level = item.level || (item.formCode || item.formUuid ? 'form' : 'app');
|
|
2347
|
+
if (level === 'app') return item.notificationType;
|
|
2348
|
+
const formKey = item.formCode || item.formUuid || 'form';
|
|
2349
|
+
return `${level}:${formKey}:${item.notificationType}`;
|
|
2350
|
+
}
|
|
2351
|
+
|
|
2295
2352
|
function extractResourceValues(value, spec) {
|
|
2353
|
+
if (spec.key === 'notifications') return extractNotificationResourceValues(value);
|
|
2296
2354
|
if (Array.isArray(value)) return value;
|
|
2297
2355
|
if (!value || typeof value !== 'object') return [value];
|
|
2298
2356
|
for (const key of spec.pluralKeys || []) {
|
|
@@ -2302,6 +2360,29 @@ function extractResourceValues(value, spec) {
|
|
|
2302
2360
|
return [value];
|
|
2303
2361
|
}
|
|
2304
2362
|
|
|
2363
|
+
function extractNotificationResourceValues(value) {
|
|
2364
|
+
if (Array.isArray(value)) return value;
|
|
2365
|
+
if (!value || typeof value !== 'object') return [value];
|
|
2366
|
+
const result = [];
|
|
2367
|
+
for (const template of value.templates || []) {
|
|
2368
|
+
result.push({ ...template, resourceType: 'template' });
|
|
2369
|
+
}
|
|
2370
|
+
for (const config of value.typeConfigs || value.notificationTypeConfigs || []) {
|
|
2371
|
+
result.push({ ...config, resourceType: 'typeConfig' });
|
|
2372
|
+
}
|
|
2373
|
+
if (Array.isArray(value.notifications)) {
|
|
2374
|
+
result.push(...value.notifications);
|
|
2375
|
+
}
|
|
2376
|
+
if (result.length > 0) return result;
|
|
2377
|
+
if (value.template || value.typeConfig) {
|
|
2378
|
+
return [
|
|
2379
|
+
value.template ? { ...value.template, resourceType: 'template' } : null,
|
|
2380
|
+
value.typeConfig ? { ...value.typeConfig, resourceType: 'typeConfig' } : null,
|
|
2381
|
+
].filter(Boolean);
|
|
2382
|
+
}
|
|
2383
|
+
return [value];
|
|
2384
|
+
}
|
|
2385
|
+
|
|
2305
2386
|
function listJsonFiles(dirPath) {
|
|
2306
2387
|
if (!fs.existsSync(dirPath)) return [];
|
|
2307
2388
|
return fs
|
|
@@ -2325,6 +2406,7 @@ function validateWorkspaceResources(manifest) {
|
|
|
2325
2406
|
validateResourceItem(spec.key, item, errors, warnings);
|
|
2326
2407
|
}
|
|
2327
2408
|
}
|
|
2409
|
+
validateNotificationReferences(manifest.notifications || [], errors);
|
|
2328
2410
|
return {
|
|
2329
2411
|
valid: errors.length === 0,
|
|
2330
2412
|
errors,
|
|
@@ -2357,6 +2439,11 @@ function validateResourceItem(kind, item, errors, warnings) {
|
|
|
2357
2439
|
return;
|
|
2358
2440
|
}
|
|
2359
2441
|
|
|
2442
|
+
if (kind === 'notifications') {
|
|
2443
|
+
validateNotificationResourceItem(item, errors, warnings);
|
|
2444
|
+
return;
|
|
2445
|
+
}
|
|
2446
|
+
|
|
2360
2447
|
if (kind === 'roles' && !item.name) errors.push(`${label}: 缺少 name`);
|
|
2361
2448
|
if (kind === 'menus' && !item.name) errors.push(`${label}: 缺少 name`);
|
|
2362
2449
|
if (kind === 'workflows') {
|
|
@@ -2392,6 +2479,99 @@ function validateResourceItem(kind, item, errors, warnings) {
|
|
|
2392
2479
|
}
|
|
2393
2480
|
}
|
|
2394
2481
|
|
|
2482
|
+
const NOTIFICATION_CHANNELS = new Set([
|
|
2483
|
+
'inapp',
|
|
2484
|
+
'email',
|
|
2485
|
+
'dingding',
|
|
2486
|
+
'wechat',
|
|
2487
|
+
'thirdparty_todo',
|
|
2488
|
+
]);
|
|
2489
|
+
|
|
2490
|
+
function validateNotificationResourceItem(item, errors, warnings) {
|
|
2491
|
+
const label = resourceLabel('notifications', item);
|
|
2492
|
+
const resourceType = normalizeNotificationResourceType(item);
|
|
2493
|
+
if (!resourceType) {
|
|
2494
|
+
errors.push(`${label}: resourceType 必须是 template 或 typeConfig`);
|
|
2495
|
+
return;
|
|
2496
|
+
}
|
|
2497
|
+
|
|
2498
|
+
if (resourceType === 'template') {
|
|
2499
|
+
if (!item.code) errors.push(`${label}: 通知模板缺少 code`);
|
|
2500
|
+
if (!item.name) errors.push(`${label}: 通知模板缺少 name`);
|
|
2501
|
+
if (item.level && !['app', 'form'].includes(item.level)) {
|
|
2502
|
+
errors.push(`${label}: OpenXiangda 通知模板 level 只能是 app 或 form`);
|
|
2503
|
+
}
|
|
2504
|
+
if ((item.level === 'form' || item.formCode || item.formUuid) && !item.formCode && !item.formUuid) {
|
|
2505
|
+
errors.push(`${label}: 表单级通知模板缺少 formCode 或 formUuid`);
|
|
2506
|
+
}
|
|
2507
|
+
if (!item.content && !item.channelsConfig) {
|
|
2508
|
+
warnings.push(`${label}: 未声明 content 或 channelsConfig`);
|
|
2509
|
+
}
|
|
2510
|
+
validateNotificationChannels(label, item.channelsConfig, errors);
|
|
2511
|
+
return;
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2514
|
+
if (!item.notificationType) errors.push(`${label}: 通知类型配置缺少 notificationType`);
|
|
2515
|
+
if (!item.templateCode && !item.templateId) {
|
|
2516
|
+
errors.push(`${label}: 通知类型配置缺少 templateCode 或 templateId`);
|
|
2517
|
+
}
|
|
2518
|
+
if (item.level && !['app', 'form'].includes(item.level)) {
|
|
2519
|
+
errors.push(`${label}: OpenXiangda 通知类型配置 level 只能是 app 或 form`);
|
|
2520
|
+
}
|
|
2521
|
+
if ((item.level === 'form' || item.formCode || item.formUuid) && !item.formCode && !item.formUuid) {
|
|
2522
|
+
errors.push(`${label}: 表单级通知类型配置缺少 formCode 或 formUuid`);
|
|
2523
|
+
}
|
|
2524
|
+
}
|
|
2525
|
+
|
|
2526
|
+
function normalizeNotificationResourceType(item) {
|
|
2527
|
+
const value = item.resourceType || item.kind || item.type;
|
|
2528
|
+
if (value === 'template' || value === 'notificationTemplate') return 'template';
|
|
2529
|
+
if (value === 'typeConfig' || value === 'notificationTypeConfig') return 'typeConfig';
|
|
2530
|
+
if (item.notificationType || item.templateCode || item.templateId) return 'typeConfig';
|
|
2531
|
+
if (item.channelsConfig || item.content || item.variables) return 'template';
|
|
2532
|
+
return undefined;
|
|
2533
|
+
}
|
|
2534
|
+
|
|
2535
|
+
function validateNotificationReferences(items, errors) {
|
|
2536
|
+
const templates = new Set(
|
|
2537
|
+
(items || [])
|
|
2538
|
+
.filter(item => normalizeNotificationResourceType(item) === 'template')
|
|
2539
|
+
.map(item => item.code)
|
|
2540
|
+
.filter(Boolean)
|
|
2541
|
+
);
|
|
2542
|
+
for (const item of items || []) {
|
|
2543
|
+
if (normalizeNotificationResourceType(item) !== 'typeConfig') continue;
|
|
2544
|
+
if (item.templateCode && !templates.has(item.templateCode) && !item.templateId) {
|
|
2545
|
+
errors.push(
|
|
2546
|
+
`${resourceLabel('notifications', item)}: templateCode ${item.templateCode} 未在 src/resources/notifications 中声明`
|
|
2547
|
+
);
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
|
|
2552
|
+
function validateNotificationChannels(label, channelsConfig, errors) {
|
|
2553
|
+
if (!channelsConfig || typeof channelsConfig !== 'object' || Array.isArray(channelsConfig)) {
|
|
2554
|
+
return;
|
|
2555
|
+
}
|
|
2556
|
+
for (const [channel, config] of Object.entries(channelsConfig)) {
|
|
2557
|
+
if (!NOTIFICATION_CHANNELS.has(channel)) {
|
|
2558
|
+
errors.push(`${label}: 不支持的通知渠道 ${channel}`);
|
|
2559
|
+
}
|
|
2560
|
+
validateNoSensitiveNotificationConfig(`${label}.channelsConfig.${channel}`, config, errors);
|
|
2561
|
+
}
|
|
2562
|
+
}
|
|
2563
|
+
|
|
2564
|
+
function validateNoSensitiveNotificationConfig(label, value, errors) {
|
|
2565
|
+
if (!value || typeof value !== 'object') return;
|
|
2566
|
+
for (const [key, item] of Object.entries(value)) {
|
|
2567
|
+
if (/token|secret|password|authorization|auth|credential|headers/i.test(key)) {
|
|
2568
|
+
errors.push(`${label}.${key}: 通知资源不允许配置通道密钥或鉴权字段`);
|
|
2569
|
+
continue;
|
|
2570
|
+
}
|
|
2571
|
+
validateNoSensitiveNotificationConfig(`${label}.${key}`, item, errors);
|
|
2572
|
+
}
|
|
2573
|
+
}
|
|
2574
|
+
|
|
2395
2575
|
function resourceLabel(kind, item) {
|
|
2396
2576
|
return `${kind}:${item.code || item.__source || item.__index || '?'}`;
|
|
2397
2577
|
}
|
|
@@ -2412,6 +2592,7 @@ async function buildResourcePlan(config, target, manifest) {
|
|
|
2412
2592
|
addPlanActions(actions, 'role', manifest.roles, existing.roles, roleEquals);
|
|
2413
2593
|
addPlanActions(actions, 'menu', manifest.menus, existing.menus, (item, current) => menuEquals(target.bound, item, current));
|
|
2414
2594
|
addPlanActions(actions, 'connector', manifest.connectors, existing.connectors, connectorEquals);
|
|
2595
|
+
addNotificationPlanActions(target, actions, manifest.notifications || [], existing);
|
|
2415
2596
|
await addWorkflowPlanActions(config, target, actions, manifest.workflows, existing.workflows);
|
|
2416
2597
|
await addAutomationPlanActions(config, target, actions, manifest.automations, existing.automations);
|
|
2417
2598
|
addPlanActions(actions, 'pagePermissionGroup', manifest.pagePermissionGroups, existing.pagePermissionGroups, (item, current) => pagePermissionGroupEquals(target.bound, item, current));
|
|
@@ -2444,6 +2625,7 @@ async function publishResourceManifest(config, target, manifest, options = {}) {
|
|
|
2444
2625
|
await publishFormSettingsResources(config, target, manifest.formSettings || [], result);
|
|
2445
2626
|
await publishMenuResources(config, target, manifest.menus || [], result);
|
|
2446
2627
|
await publishConnectorResources(config, target, manifest.connectors || [], result);
|
|
2628
|
+
await publishNotificationResources(config, target, manifest.notifications || [], result);
|
|
2447
2629
|
await publishWorkflowResources(config, target, manifest.workflows || [], result);
|
|
2448
2630
|
await publishAutomationResources(config, target, manifest.automations || [], result);
|
|
2449
2631
|
await publishPagePermissionGroupResources(config, target, manifest.pagePermissionGroups || [], result);
|
|
@@ -2463,6 +2645,8 @@ async function fetchExistingResourceMaps(config, target, manifest) {
|
|
|
2463
2645
|
roles: new Map(),
|
|
2464
2646
|
menus: new Map(),
|
|
2465
2647
|
connectors: new Map(),
|
|
2648
|
+
notificationTemplates: new Map(),
|
|
2649
|
+
notificationTypeConfigs: new Map(),
|
|
2466
2650
|
workflows: new Map(),
|
|
2467
2651
|
automations: new Map(),
|
|
2468
2652
|
pagePermissionGroups: new Map(),
|
|
@@ -2496,6 +2680,31 @@ async function fetchExistingResourceMaps(config, target, manifest) {
|
|
|
2496
2680
|
);
|
|
2497
2681
|
indexByCode(maps.connectors, normalizeItems(data), item => item.code || item.methodName);
|
|
2498
2682
|
}
|
|
2683
|
+
if ((manifest.notifications || []).length > 0) {
|
|
2684
|
+
const templates = await requestWithAuth(
|
|
2685
|
+
config,
|
|
2686
|
+
target.profileName,
|
|
2687
|
+
apiPathWithQuery(`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/notifications/templates`, {
|
|
2688
|
+
page: 1,
|
|
2689
|
+
limit: 1000,
|
|
2690
|
+
})
|
|
2691
|
+
);
|
|
2692
|
+
indexByCode(maps.notificationTemplates, normalizeItems(templates), item => item.code);
|
|
2693
|
+
|
|
2694
|
+
const typeConfigs = await requestWithAuth(
|
|
2695
|
+
config,
|
|
2696
|
+
target.profileName,
|
|
2697
|
+
apiPathWithQuery(`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/notifications/type-configs`, {
|
|
2698
|
+
page: 1,
|
|
2699
|
+
limit: 1000,
|
|
2700
|
+
})
|
|
2701
|
+
);
|
|
2702
|
+
indexByCode(
|
|
2703
|
+
maps.notificationTypeConfigs,
|
|
2704
|
+
normalizeItems(typeConfigs),
|
|
2705
|
+
item => notificationTypeConfigExistingKey(item)
|
|
2706
|
+
);
|
|
2707
|
+
}
|
|
2499
2708
|
if ((manifest.workflows || []).length > 0) {
|
|
2500
2709
|
const data = await requestWithAuth(
|
|
2501
2710
|
config,
|
|
@@ -2558,6 +2767,37 @@ function addPlanActions(actions, kind, desiredItems = [], existingMap, equals) {
|
|
|
2558
2767
|
}
|
|
2559
2768
|
}
|
|
2560
2769
|
|
|
2770
|
+
function addNotificationPlanActions(target, actions, notificationItems = [], existing) {
|
|
2771
|
+
const { templates, typeConfigs } = splitNotificationResources(notificationItems);
|
|
2772
|
+
for (const template of templates) {
|
|
2773
|
+
const existingTemplate = existing.notificationTemplates.get(template.code);
|
|
2774
|
+
actions.push({
|
|
2775
|
+
kind: 'notificationTemplate',
|
|
2776
|
+
code: template.code,
|
|
2777
|
+
action: existingTemplate
|
|
2778
|
+
? notificationTemplateEquals(target.bound, template, existingTemplate)
|
|
2779
|
+
? 'noop'
|
|
2780
|
+
: 'update'
|
|
2781
|
+
: 'create',
|
|
2782
|
+
platformId: existingTemplate?.id,
|
|
2783
|
+
});
|
|
2784
|
+
}
|
|
2785
|
+
for (const config of typeConfigs) {
|
|
2786
|
+
const key = notificationTypeConfigDesiredKey(target.bound, config);
|
|
2787
|
+
const existingConfig = existing.notificationTypeConfigs.get(key);
|
|
2788
|
+
actions.push({
|
|
2789
|
+
kind: 'notificationTypeConfig',
|
|
2790
|
+
code: config.code || config.notificationType,
|
|
2791
|
+
action: existingConfig
|
|
2792
|
+
? notificationTypeConfigEquals(target.bound, config, existingConfig)
|
|
2793
|
+
? 'noop'
|
|
2794
|
+
: 'update'
|
|
2795
|
+
: 'create',
|
|
2796
|
+
platformId: existingConfig?.id,
|
|
2797
|
+
});
|
|
2798
|
+
}
|
|
2799
|
+
}
|
|
2800
|
+
|
|
2561
2801
|
async function addWorkflowPlanActions(config, target, actions, desiredItems = [], existingMap) {
|
|
2562
2802
|
for (const item of desiredItems) {
|
|
2563
2803
|
const code = item.code || item.resourceCode;
|
|
@@ -2842,6 +3082,57 @@ async function publishConnectorResources(config, target, connectors, result) {
|
|
|
2842
3082
|
}
|
|
2843
3083
|
}
|
|
2844
3084
|
|
|
3085
|
+
async function publishNotificationResources(config, target, notifications, result) {
|
|
3086
|
+
const { templates, typeConfigs } = splitNotificationResources(notifications);
|
|
3087
|
+
for (const template of templates) {
|
|
3088
|
+
const body = normalizeNotificationTemplateManifest(target.bound, template);
|
|
3089
|
+
const data = await requestWithAuth(
|
|
3090
|
+
config,
|
|
3091
|
+
target.profileName,
|
|
3092
|
+
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/notifications/templates/${encodeURIComponent(template.code)}`,
|
|
3093
|
+
{ method: 'PUT', body }
|
|
3094
|
+
);
|
|
3095
|
+
if (data?.id) {
|
|
3096
|
+
saveNotificationTemplateResource(target, template.code, data.id, {
|
|
3097
|
+
level: data.level || body.level,
|
|
3098
|
+
formUuid: data.formUuid || body.formUuid,
|
|
3099
|
+
});
|
|
3100
|
+
}
|
|
3101
|
+
result.published.push({
|
|
3102
|
+
kind: 'notificationTemplate',
|
|
3103
|
+
code: template.code,
|
|
3104
|
+
action: data?.created ? 'create' : 'update',
|
|
3105
|
+
id: data?.id,
|
|
3106
|
+
});
|
|
3107
|
+
}
|
|
3108
|
+
|
|
3109
|
+
for (const configItem of typeConfigs) {
|
|
3110
|
+
const body = normalizeNotificationTypeConfigManifest(target.bound, configItem);
|
|
3111
|
+
const data = await requestWithAuth(
|
|
3112
|
+
config,
|
|
3113
|
+
target.profileName,
|
|
3114
|
+
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/notifications/type-configs/${encodeURIComponent(configItem.notificationType)}`,
|
|
3115
|
+
{ method: 'PUT', body }
|
|
3116
|
+
);
|
|
3117
|
+
const stateCode = configItem.code || notificationTypeConfigDesiredKey(target.bound, configItem);
|
|
3118
|
+
if (data?.id) {
|
|
3119
|
+
saveNotificationTypeConfigResource(target, stateCode, data.id, {
|
|
3120
|
+
notificationType: data.notificationType || configItem.notificationType,
|
|
3121
|
+
level: data.level || body.level,
|
|
3122
|
+
formUuid: data.formUuid || body.formUuid,
|
|
3123
|
+
templateId: data.templateId,
|
|
3124
|
+
templateCode: data.template?.code || configItem.templateCode,
|
|
3125
|
+
});
|
|
3126
|
+
}
|
|
3127
|
+
result.published.push({
|
|
3128
|
+
kind: 'notificationTypeConfig',
|
|
3129
|
+
code: configItem.code || configItem.notificationType,
|
|
3130
|
+
action: data?.created ? 'create' : 'update',
|
|
3131
|
+
id: data?.id,
|
|
3132
|
+
});
|
|
3133
|
+
}
|
|
3134
|
+
}
|
|
3135
|
+
|
|
2845
3136
|
async function publishWorkflowResources(config, target, workflows, result) {
|
|
2846
3137
|
for (const workflowItem of workflows) {
|
|
2847
3138
|
const existing = await findExistingWorkflow(config, target, workflowItem.code);
|
|
@@ -3379,6 +3670,40 @@ async function pullResources(config, target) {
|
|
|
3379
3670
|
written.push(path.relative(process.cwd(), filePath));
|
|
3380
3671
|
}
|
|
3381
3672
|
|
|
3673
|
+
const notificationTemplates = await requestWithAuth(
|
|
3674
|
+
config,
|
|
3675
|
+
target.profileName,
|
|
3676
|
+
apiPathWithQuery(`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/notifications/templates`, {
|
|
3677
|
+
page: 1,
|
|
3678
|
+
limit: 1000,
|
|
3679
|
+
})
|
|
3680
|
+
);
|
|
3681
|
+
for (const template of normalizeItems(notificationTemplates)) {
|
|
3682
|
+
const code = template.code || template.id;
|
|
3683
|
+
const filePath = path.join(baseDir, 'notifications', 'templates', `${code}.json`);
|
|
3684
|
+
writeResourceJsonFile(filePath, {
|
|
3685
|
+
templates: [toPulledNotificationTemplate(template, pullLookups)],
|
|
3686
|
+
});
|
|
3687
|
+
written.push(path.relative(process.cwd(), filePath));
|
|
3688
|
+
}
|
|
3689
|
+
|
|
3690
|
+
const notificationTypeConfigs = await requestWithAuth(
|
|
3691
|
+
config,
|
|
3692
|
+
target.profileName,
|
|
3693
|
+
apiPathWithQuery(`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/notifications/type-configs`, {
|
|
3694
|
+
page: 1,
|
|
3695
|
+
limit: 1000,
|
|
3696
|
+
})
|
|
3697
|
+
);
|
|
3698
|
+
for (const configItem of normalizeItems(notificationTypeConfigs)) {
|
|
3699
|
+
const code = notificationTypeConfigExistingKey(configItem).replace(/[^a-zA-Z0-9_.-]+/g, '_');
|
|
3700
|
+
const filePath = path.join(baseDir, 'notifications', 'type-configs', `${code}.json`);
|
|
3701
|
+
writeResourceJsonFile(filePath, {
|
|
3702
|
+
typeConfigs: [toPulledNotificationTypeConfig(configItem, pullLookups)],
|
|
3703
|
+
});
|
|
3704
|
+
written.push(path.relative(process.cwd(), filePath));
|
|
3705
|
+
}
|
|
3706
|
+
|
|
3382
3707
|
const menus = await requestWithAuth(
|
|
3383
3708
|
config,
|
|
3384
3709
|
target.profileName,
|
|
@@ -3598,6 +3923,47 @@ function splitPagePermissionTargetsForManifest(values = [], lookups) {
|
|
|
3598
3923
|
};
|
|
3599
3924
|
}
|
|
3600
3925
|
|
|
3926
|
+
function toPulledNotificationTemplate(template, lookups) {
|
|
3927
|
+
const formCode = lookups.formCodeByUuid.get(template.formUuid);
|
|
3928
|
+
return stripUndefinedValues({
|
|
3929
|
+
code: template.code,
|
|
3930
|
+
name: template.name,
|
|
3931
|
+
content: template.content || '',
|
|
3932
|
+
description: template.description || '',
|
|
3933
|
+
level: template.level === 'form' ? 'form' : 'app',
|
|
3934
|
+
...(formCode ? { formCode } : template.formUuid ? { formUuid: template.formUuid } : {}),
|
|
3935
|
+
priority: template.priority,
|
|
3936
|
+
enabled: template.enabled,
|
|
3937
|
+
variables: template.variables || undefined,
|
|
3938
|
+
channelsConfig: stripNotificationSecrets(template.channelsConfig),
|
|
3939
|
+
});
|
|
3940
|
+
}
|
|
3941
|
+
|
|
3942
|
+
function toPulledNotificationTypeConfig(configItem, lookups) {
|
|
3943
|
+
const formCode = lookups.formCodeByUuid.get(configItem.formUuid);
|
|
3944
|
+
return stripUndefinedValues({
|
|
3945
|
+
code: notificationTypeConfigExistingKey(configItem),
|
|
3946
|
+
notificationType: configItem.notificationType,
|
|
3947
|
+
level: configItem.level === 'form' ? 'form' : 'app',
|
|
3948
|
+
...(formCode ? { formCode } : configItem.formUuid ? { formUuid: configItem.formUuid } : {}),
|
|
3949
|
+
templateCode: configItem.template?.code,
|
|
3950
|
+
enabled: configItem.enabled,
|
|
3951
|
+
priority: configItem.priority,
|
|
3952
|
+
description: configItem.description || '',
|
|
3953
|
+
});
|
|
3954
|
+
}
|
|
3955
|
+
|
|
3956
|
+
function stripNotificationSecrets(value) {
|
|
3957
|
+
if (!value || typeof value !== 'object') return value;
|
|
3958
|
+
if (Array.isArray(value)) return value.map(stripNotificationSecrets);
|
|
3959
|
+
const next = {};
|
|
3960
|
+
for (const [key, item] of Object.entries(value)) {
|
|
3961
|
+
if (/token|secret|password|authorization|auth|credential|headers/i.test(key)) continue;
|
|
3962
|
+
next[key] = stripNotificationSecrets(item);
|
|
3963
|
+
}
|
|
3964
|
+
return next;
|
|
3965
|
+
}
|
|
3966
|
+
|
|
3601
3967
|
function writeResourceJsonFile(filePath, value) {
|
|
3602
3968
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
3603
3969
|
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
@@ -3671,6 +4037,58 @@ function parseConnectorUrl(connector) {
|
|
|
3671
4037
|
}
|
|
3672
4038
|
}
|
|
3673
4039
|
|
|
4040
|
+
function splitNotificationResources(items = []) {
|
|
4041
|
+
const templates = [];
|
|
4042
|
+
const typeConfigs = [];
|
|
4043
|
+
for (const item of items || []) {
|
|
4044
|
+
const resourceType = normalizeNotificationResourceType(item);
|
|
4045
|
+
if (resourceType === 'template') templates.push(item);
|
|
4046
|
+
if (resourceType === 'typeConfig') typeConfigs.push(item);
|
|
4047
|
+
}
|
|
4048
|
+
return { templates, typeConfigs };
|
|
4049
|
+
}
|
|
4050
|
+
|
|
4051
|
+
function normalizeNotificationTemplateManifest(bound, template) {
|
|
4052
|
+
const level = template.level || (template.formCode || template.formUuid ? 'form' : 'app');
|
|
4053
|
+
const formUuid = level === 'form' ? resolveManifestFormUuid(bound, template) : undefined;
|
|
4054
|
+
return stripUndefinedValues({
|
|
4055
|
+
name: template.name || template.code,
|
|
4056
|
+
content: template.content || '',
|
|
4057
|
+
description: template.description || '',
|
|
4058
|
+
channelsConfig: template.channelsConfig,
|
|
4059
|
+
level,
|
|
4060
|
+
formUuid,
|
|
4061
|
+
priority: template.priority,
|
|
4062
|
+
enabled: template.enabled,
|
|
4063
|
+
variables: template.variables,
|
|
4064
|
+
});
|
|
4065
|
+
}
|
|
4066
|
+
|
|
4067
|
+
function normalizeNotificationTypeConfigManifest(bound, config) {
|
|
4068
|
+
const level = config.level || (config.formCode || config.formUuid ? 'form' : 'app');
|
|
4069
|
+
const formUuid = level === 'form' ? resolveManifestFormUuid(bound, config) : undefined;
|
|
4070
|
+
return stripUndefinedValues({
|
|
4071
|
+
level,
|
|
4072
|
+
formUuid,
|
|
4073
|
+
templateId: config.templateId,
|
|
4074
|
+
templateCode: config.templateCode,
|
|
4075
|
+
enabled: config.enabled,
|
|
4076
|
+
priority: config.priority,
|
|
4077
|
+
description: config.description || '',
|
|
4078
|
+
});
|
|
4079
|
+
}
|
|
4080
|
+
|
|
4081
|
+
function notificationTypeConfigDesiredKey(bound, config) {
|
|
4082
|
+
const level = config.level || (config.formCode || config.formUuid ? 'form' : 'app');
|
|
4083
|
+
const formUuid = level === 'form' ? resolveManifestFormUuid(bound, config) : '';
|
|
4084
|
+
return `${level}:${formUuid || ''}:${config.notificationType}`;
|
|
4085
|
+
}
|
|
4086
|
+
|
|
4087
|
+
function notificationTypeConfigExistingKey(config) {
|
|
4088
|
+
const level = config.level || 'app';
|
|
4089
|
+
return `${level}:${config.formUuid || ''}:${config.notificationType}`;
|
|
4090
|
+
}
|
|
4091
|
+
|
|
3674
4092
|
async function resolveManifestJson(config, profileName, item, objectKey, fileKey, optional = false) {
|
|
3675
4093
|
let value = item[objectKey];
|
|
3676
4094
|
if (value === undefined && item[fileKey]) {
|
|
@@ -3778,6 +4196,40 @@ function connectorEquals(desired, existing) {
|
|
|
3778
4196
|
return JSON.stringify(normalizedExisting) === JSON.stringify(normalizedDesired);
|
|
3779
4197
|
}
|
|
3780
4198
|
|
|
4199
|
+
function notificationTemplateEquals(bound, desired, existing) {
|
|
4200
|
+
if (!existing) return false;
|
|
4201
|
+
const expected = normalizeNotificationTemplateManifest(bound, desired);
|
|
4202
|
+
return (
|
|
4203
|
+
String(existing.code || '') === String(desired.code || '') &&
|
|
4204
|
+
String(existing.name || '') === String(expected.name || '') &&
|
|
4205
|
+
String(existing.content || '') === String(expected.content || '') &&
|
|
4206
|
+
String(existing.description || '') === String(expected.description || '') &&
|
|
4207
|
+
String(existing.level || 'app') === String(expected.level || 'app') &&
|
|
4208
|
+
String(existing.formUuid || '') === String(expected.formUuid || '') &&
|
|
4209
|
+
optionalScalarEquals(existing.priority, expected.priority, expected.priority !== undefined) &&
|
|
4210
|
+
optionalScalarEquals(Boolean(existing.enabled), Boolean(expected.enabled), expected.enabled !== undefined) &&
|
|
4211
|
+
jsonEqualsForPlan(existing.variables || [], expected.variables || [], desired.__dir) &&
|
|
4212
|
+
jsonEqualsForPlan(existing.channelsConfig || {}, expected.channelsConfig || {}, desired.__dir)
|
|
4213
|
+
);
|
|
4214
|
+
}
|
|
4215
|
+
|
|
4216
|
+
function notificationTypeConfigEquals(bound, desired, existing) {
|
|
4217
|
+
if (!existing) return false;
|
|
4218
|
+
const expected = normalizeNotificationTypeConfigManifest(bound, desired);
|
|
4219
|
+
const existingTemplateCode = existing.template?.code;
|
|
4220
|
+
return (
|
|
4221
|
+
String(existing.notificationType || '') === String(desired.notificationType || '') &&
|
|
4222
|
+
String(existing.level || 'app') === String(expected.level || 'app') &&
|
|
4223
|
+
String(existing.formUuid || '') === String(expected.formUuid || '') &&
|
|
4224
|
+
(expected.templateId
|
|
4225
|
+
? String(existing.templateId || '') === String(expected.templateId)
|
|
4226
|
+
: String(existingTemplateCode || '') === String(expected.templateCode || '')) &&
|
|
4227
|
+
optionalScalarEquals(Boolean(existing.enabled), Boolean(expected.enabled), expected.enabled !== undefined) &&
|
|
4228
|
+
optionalScalarEquals(existing.priority, expected.priority, expected.priority !== undefined) &&
|
|
4229
|
+
String(existing.description || '') === String(expected.description || '')
|
|
4230
|
+
);
|
|
4231
|
+
}
|
|
4232
|
+
|
|
3781
4233
|
function pagePermissionGroupEquals(bound, desired, existing) {
|
|
3782
4234
|
if (!existing) return false;
|
|
3783
4235
|
const desiredTargets = resolvePagePermissionGroupTargets(bound, desired);
|
|
@@ -105,6 +105,7 @@ Core CLI / state:
|
|
|
105
105
|
- `references/openxiangda-api.md` — `/openxiangda-api/v1` request and response fields.
|
|
106
106
|
- `references/workspace-state.md` — `.openxiangda/state.json` shape and profile isolation rules.
|
|
107
107
|
- `references/connector-resources.md` — `src/resources` manifests, connector schema, and SDK connector calls.
|
|
108
|
+
- `references/notifications.md` — `src/resources/notifications`, notification templates/type bindings, and `sdk.notification` / `ctx.notification`.
|
|
108
109
|
|
|
109
110
|
Form authoring:
|
|
110
111
|
|
|
@@ -123,6 +123,8 @@ The backend runs the snapshot in the trusted Node runtime, applies the node time
|
|
|
123
123
|
|
|
124
124
|
Runtime context includes `ctx.triggerEvent`, `ctx.formData`, `ctx.workflowData`, `ctx.operator`, `ctx.app`, `ctx.variables`, and `ctx.node`. Data/process bridge methods include `ctx.methods.queryOneData`, `queryManyData`, `updateOneData`, `updateDataByFormInstanceId`, `updateManyData`, `createOneData`, `terminateProcess`, and `getAllParentDepartments`.
|
|
125
125
|
|
|
126
|
+
Notification bridge methods include `ctx.notification.sendByType`, `batchSendByType`, `findConfig`, and `previewTemplate`. For custom business messages, create `src/resources/notifications/` first and use its `notificationType`; do not call legacy `/api/notification-config/*` endpoints directly.
|
|
127
|
+
|
|
126
128
|
Example `src/js-code-nodes/scheduled_reconcile/index.ts`:
|
|
127
129
|
|
|
128
130
|
```ts
|
|
@@ -5,6 +5,7 @@ OpenXiangda engineering resources live under `src/resources/`.
|
|
|
5
5
|
Common folders:
|
|
6
6
|
|
|
7
7
|
- `connectors`
|
|
8
|
+
- `notifications`
|
|
8
9
|
- `roles`
|
|
9
10
|
- `menus`
|
|
10
11
|
- `workflows`
|
|
@@ -24,6 +25,8 @@ openxiangda resource pull --profile dev
|
|
|
24
25
|
|
|
25
26
|
Connector manifests use stable `code` values. The platform maps connector `code` to the existing connector `methodName`, and API `code` to the existing connector API `methodName`.
|
|
26
27
|
|
|
28
|
+
Notification manifests live under `src/resources/notifications/` and contain `templates` plus `typeConfigs`. See `notifications.md` before generating reminders or message templates.
|
|
29
|
+
|
|
27
30
|
```json
|
|
28
31
|
{
|
|
29
32
|
"code": "crm",
|