lan-low-code-cli 2.6.0 → 2.8.0
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 +78 -1
- package/dist/api/flow.js +119 -0
- package/dist/api/operation.js +8 -0
- package/dist/commands/flow.js +817 -0
- package/dist/commands/operation.js +123 -14
- package/dist/generator/flow/compiler.js +397 -0
- package/dist/generator/flow/index.js +22 -0
- package/dist/generator/flow/layout.js +136 -0
- package/dist/generator/flow/schema-resolver.js +133 -0
- package/dist/generator/flow/schema.js +96 -0
- package/dist/generator/flow/types.js +10 -0
- package/dist/generator/flow/validate.js +129 -0
- package/dist/index.js +12 -1
- package/package.json +1 -1
- package/skills/lan-low-code-cli/SKILL.md +14 -4
- package/skills/lan-low-code-cli/SKILL.zh-CN.md +13 -3
- package/skills/lan-low-code-cli/references/cli-contract.md +76 -5
- package/skills/lan-low-code-cli/references/cli-contract.zh-CN.md +72 -4
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.registerOperationCommand = registerOperationCommand;
|
|
4
4
|
const fs_1 = require("fs");
|
|
5
5
|
const path_1 = require("path");
|
|
6
|
+
const flow_1 = require("../api/flow");
|
|
6
7
|
const operation_1 = require("../api/operation");
|
|
7
8
|
const view_1 = require("../api/view");
|
|
8
9
|
const client_1 = require("../client");
|
|
@@ -18,12 +19,16 @@ const KIND_ALIAS = {
|
|
|
18
19
|
js: 'script',
|
|
19
20
|
dialog: 'dialog',
|
|
20
21
|
modal: 'dialog',
|
|
22
|
+
flow: 'flow',
|
|
21
23
|
};
|
|
22
24
|
const KIND_META = {
|
|
23
25
|
link: { fdType: 2, fdViewType: 4, label: '打开链接' },
|
|
24
26
|
script: { fdType: 3, fdViewType: 5, label: '执行脚本' },
|
|
25
27
|
dialog: { fdType: 3, fdViewType: 1, label: '执行业务操作(对话框)' },
|
|
28
|
+
flow: { fdType: 3, fdViewType: 1, label: '触发动作流' },
|
|
26
29
|
};
|
|
30
|
+
/** 动作流 fdTriggerModel.fdModelName(addOperationModal/content.tsx:981-985) */
|
|
31
|
+
const FLOW_MODEL_NAME = 'com.landray.sys.modeling.process.entity.SysModelingProcess';
|
|
27
32
|
/** 解析 fdConfig(后端实体不一定有顶层 fdViewType,视图类型存在 fdConfig 中) */
|
|
28
33
|
function readOperationConfig(op) {
|
|
29
34
|
if (!op.fdConfig)
|
|
@@ -43,7 +48,10 @@ function operationTypeLabel(op) {
|
|
|
43
48
|
return '对话框';
|
|
44
49
|
if (op.fdType === 6)
|
|
45
50
|
return 'AI 能力';
|
|
46
|
-
const
|
|
51
|
+
const config = readOperationConfig(op);
|
|
52
|
+
if (op.fdType === 3 && Number(config.fdTriggerType) === 3)
|
|
53
|
+
return '触发动作流';
|
|
54
|
+
const viewType = Number(config.fdViewType ?? op.fdViewType);
|
|
47
55
|
if (op.fdType === 3)
|
|
48
56
|
return viewType === 5 ? '脚本' : '无视图';
|
|
49
57
|
return '有视图';
|
|
@@ -212,19 +220,22 @@ function assertOperationCode(code) {
|
|
|
212
220
|
throw new Error('--code 需以字母开头,仅允许字母、数字、下划线、连字符');
|
|
213
221
|
}
|
|
214
222
|
}
|
|
215
|
-
/** 从已有操作推导类型(fdType=2
|
|
223
|
+
/** 从已有操作推导类型(fdType=2 链接;fdTriggerType=3 动作流;fdViewType=5 脚本;其余对话框) */
|
|
216
224
|
function detectKind(op) {
|
|
217
225
|
if (op.fdType === 2)
|
|
218
226
|
return 'link';
|
|
219
|
-
const
|
|
227
|
+
const config = readOperationConfig(op);
|
|
228
|
+
if (op.fdType === 3 && Number(config.fdTriggerType) === 3)
|
|
229
|
+
return 'flow';
|
|
230
|
+
const viewType = Number(config.fdViewType ?? op.fdViewType);
|
|
220
231
|
return op.fdType === 3 && viewType === 5 ? 'script' : 'dialog';
|
|
221
232
|
}
|
|
222
|
-
function buildCreatePayload(opts) {
|
|
233
|
+
function buildCreatePayload(opts, flow) {
|
|
223
234
|
const formId = String(opts.formId ?? '').trim();
|
|
224
235
|
const name = String(opts.name ?? '').trim();
|
|
225
236
|
const code = String(opts.code ?? '').trim();
|
|
226
237
|
const rawKind = String(opts.type ?? '').trim().toLowerCase();
|
|
227
|
-
const kind = KIND_ALIAS[rawKind];
|
|
238
|
+
const kind = opts.triggerFlow ? 'flow' : KIND_ALIAS[rawKind];
|
|
228
239
|
const positionRaw = String(opts.position ?? 'list').trim().toLowerCase();
|
|
229
240
|
const fdPosition = positionRaw === 'view' ? 2 : positionRaw === 'list' ? 1 : 0;
|
|
230
241
|
if (!formId)
|
|
@@ -232,7 +243,10 @@ function buildCreatePayload(opts) {
|
|
|
232
243
|
assertOperationName(name);
|
|
233
244
|
assertOperationCode(code);
|
|
234
245
|
if (!kind)
|
|
235
|
-
throw new Error(
|
|
246
|
+
throw new Error(`请指定 --type(link | script | dialog)或 --trigger-flow <动作流 fdId>`);
|
|
247
|
+
if (opts.triggerFlow && rawKind) {
|
|
248
|
+
throw new Error('--trigger-flow 与 --type 不能同时使用;触发动作流请省略 --type');
|
|
249
|
+
}
|
|
236
250
|
if (!fdPosition)
|
|
237
251
|
throw new Error(`未知 --position:${opts.position}(可选 list | view)`);
|
|
238
252
|
const desc = opts.desc === undefined ? '' : String(opts.desc);
|
|
@@ -264,6 +278,31 @@ function buildCreatePayload(opts) {
|
|
|
264
278
|
throw new Error('--type script 需指定 --script <代码|@文件>');
|
|
265
279
|
config.fdJavaScript = readScriptValue(script);
|
|
266
280
|
}
|
|
281
|
+
else if (kind === 'flow') {
|
|
282
|
+
if (!flow)
|
|
283
|
+
throw new Error('--trigger-flow 需要先加载动作流详情');
|
|
284
|
+
assertTriggerableFlow(flow);
|
|
285
|
+
config.fdTriggerType = 3;
|
|
286
|
+
config.fdTriggerId = flow.fdId;
|
|
287
|
+
config.fdTriggerCode = flow.fdCode;
|
|
288
|
+
config.fdTriggerModel = {
|
|
289
|
+
fdId: flow.fdId,
|
|
290
|
+
fdName: flow.fdName,
|
|
291
|
+
fdModelName: FLOW_MODEL_NAME,
|
|
292
|
+
fdCode: flow.fdCode,
|
|
293
|
+
};
|
|
294
|
+
const incParams = buildIncParams(opts.incParams);
|
|
295
|
+
if (incParams !== undefined)
|
|
296
|
+
config.incParams = incParams;
|
|
297
|
+
const active = flow.fdProcessActive ?? {};
|
|
298
|
+
if (active.fdFormId) {
|
|
299
|
+
// 动作流绑定了表单:按钮按当前文档 id/ids 传参
|
|
300
|
+
config.incParams = [{
|
|
301
|
+
field: { name: active.fdDataType === 2 ? 'fdIds' : 'fdId' },
|
|
302
|
+
value: { val: '${fdId}' },
|
|
303
|
+
}];
|
|
304
|
+
}
|
|
305
|
+
}
|
|
267
306
|
else {
|
|
268
307
|
const dialogId = String(opts.dialogId ?? '').trim();
|
|
269
308
|
if (!dialogId)
|
|
@@ -288,19 +327,54 @@ function buildCreatePayload(opts) {
|
|
|
288
327
|
payload.isModal = true;
|
|
289
328
|
return { payload, kind };
|
|
290
329
|
}
|
|
330
|
+
/**
|
|
331
|
+
* 只有「主动触发 + 已启用 + 允许操作触发」的动作流才能挂到按钮上
|
|
332
|
+
* (addOperationModal/content.tsx:332-335)。
|
|
333
|
+
*/
|
|
334
|
+
function assertTriggerableFlow(flow) {
|
|
335
|
+
if (flow.fdStartWay !== 'StartActiveNode') {
|
|
336
|
+
throw new Error(`动作流 ${flow.fdName ?? flow.fdId} 的启动方式是 ${flow.fdStartWay ?? '(空)'},只有 StartActiveNode 才能挂到按钮`);
|
|
337
|
+
}
|
|
338
|
+
if (flow.fdStatus !== 1) {
|
|
339
|
+
throw new Error(`动作流 ${flow.fdName ?? flow.fdId} 当前状态为 ${flow.fdStatus ?? '(空)'},请先发布并启用(llc flow enable --id ${flow.fdId})`);
|
|
340
|
+
}
|
|
341
|
+
const active = flow.fdProcessActive ?? {};
|
|
342
|
+
if (active.fdOperateTrigger !== true) {
|
|
343
|
+
throw new Error(`动作流 ${flow.fdName ?? flow.fdId} 未开启「操作触发」,请在 trigger.fdRunType 中包含 operate 后重新发布`);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
/** --inc-params 支持 @file 或内联 JSON;返回平台要求的 [{field:{name},value:{val}}] */
|
|
347
|
+
function buildIncParams(input) {
|
|
348
|
+
if (input === undefined || input === null || input === '')
|
|
349
|
+
return undefined;
|
|
350
|
+
const raw = typeof input === 'string' ? readScriptValue(input) : JSON.stringify(input);
|
|
351
|
+
let parsed;
|
|
352
|
+
try {
|
|
353
|
+
parsed = JSON.parse(raw);
|
|
354
|
+
}
|
|
355
|
+
catch (err) {
|
|
356
|
+
throw new Error(`--inc-params 不是合法 JSON:${err.message}`);
|
|
357
|
+
}
|
|
358
|
+
if (!Array.isArray(parsed))
|
|
359
|
+
throw new Error('--inc-params 需为数组:[{field:{name},value:{val}}]');
|
|
360
|
+
return parsed;
|
|
361
|
+
}
|
|
291
362
|
/**
|
|
292
363
|
* 基于现有操作构造 update 请求体。
|
|
293
364
|
* 显式切换类型(--type)时会重建 fdConfig 的类型相关字段,
|
|
294
365
|
* 但保留权限、默认值等与类型无关的配置。
|
|
295
366
|
*/
|
|
296
|
-
function buildUpdatePayload(current, opts) {
|
|
367
|
+
function buildUpdatePayload(current, opts, flow) {
|
|
297
368
|
if (current.fdType !== 2 && current.fdType !== 3) {
|
|
298
|
-
throw new Error(`该操作类型为「${operationTypeLabel(current)}」,仅支持修改自定义的链接 / 脚本 /
|
|
369
|
+
throw new Error(`该操作类型为「${operationTypeLabel(current)}」,仅支持修改自定义的链接 / 脚本 / 对话框 / 动作流操作`);
|
|
299
370
|
}
|
|
300
371
|
const currentKind = detectKind(current);
|
|
301
372
|
const rawKind = String(opts.type ?? '').trim().toLowerCase();
|
|
302
373
|
let kind = currentKind;
|
|
303
|
-
if (
|
|
374
|
+
if (opts.triggerFlow) {
|
|
375
|
+
kind = 'flow';
|
|
376
|
+
}
|
|
377
|
+
else if (rawKind) {
|
|
304
378
|
const resolved = KIND_ALIAS[rawKind];
|
|
305
379
|
if (!resolved)
|
|
306
380
|
throw new Error(`未知 --type:${opts.type}(可选 link | script | dialog)`);
|
|
@@ -353,6 +427,23 @@ function buildUpdatePayload(current, opts) {
|
|
|
353
427
|
throw new Error('脚本操作需指定 --script <代码|@文件>');
|
|
354
428
|
config.fdJavaScript = script;
|
|
355
429
|
}
|
|
430
|
+
else if (kind === 'flow') {
|
|
431
|
+
if (!flow)
|
|
432
|
+
throw new Error('--trigger-flow 需要先加载动作流详情');
|
|
433
|
+
assertTriggerableFlow(flow);
|
|
434
|
+
config.fdTriggerType = 3;
|
|
435
|
+
config.fdTriggerId = flow.fdId;
|
|
436
|
+
config.fdTriggerCode = flow.fdCode;
|
|
437
|
+
config.fdTriggerModel = {
|
|
438
|
+
fdId: flow.fdId,
|
|
439
|
+
fdName: flow.fdName,
|
|
440
|
+
fdModelName: FLOW_MODEL_NAME,
|
|
441
|
+
fdCode: flow.fdCode,
|
|
442
|
+
};
|
|
443
|
+
const incParams = buildIncParams(opts.incParams);
|
|
444
|
+
if (incParams !== undefined)
|
|
445
|
+
config.incParams = incParams;
|
|
446
|
+
}
|
|
356
447
|
else {
|
|
357
448
|
const dialogId = opts.dialogId !== undefined
|
|
358
449
|
? String(opts.dialogId).trim()
|
|
@@ -397,6 +488,14 @@ function buildUpdatePayload(current, opts) {
|
|
|
397
488
|
payload.isModal = true;
|
|
398
489
|
else
|
|
399
490
|
delete payload.isModal;
|
|
491
|
+
if (kind === 'flow') {
|
|
492
|
+
// 切换为动作流时清理对话框残留字段
|
|
493
|
+
delete config.fdModal;
|
|
494
|
+
delete config.fdModalType;
|
|
495
|
+
delete config.fdModalSizeType;
|
|
496
|
+
delete config.fdModalDetail;
|
|
497
|
+
payload.fdConfig = JSON.stringify(config);
|
|
498
|
+
}
|
|
400
499
|
if (opts.mobileIcon !== undefined)
|
|
401
500
|
payload.fdMobileIcon = String(opts.mobileIcon);
|
|
402
501
|
if (opts.enable === true)
|
|
@@ -504,16 +603,18 @@ function registerOperationCommand(program) {
|
|
|
504
603
|
// ---------------------------------------------------------------- create
|
|
505
604
|
operation
|
|
506
605
|
.command('create')
|
|
507
|
-
.description('
|
|
606
|
+
.description('新建业务操作(打开链接 / 执行脚本 / 执行对话框 / 触发动作流)')
|
|
508
607
|
.requiredOption('--form-id <fdId>', '表单 id')
|
|
509
608
|
.requiredOption('--name <name>', '操作名称(≤30)')
|
|
510
609
|
.requiredOption('--code <code>', '自定义操作编码(≤15,字母开头,可含数字/下划线/连字符)')
|
|
511
|
-
.
|
|
610
|
+
.option('--type <type>', '类型:link | script | dialog;触发动作流时改用 --trigger-flow')
|
|
512
611
|
.option('--position <position>', '使用位置:list(列表视图)| view(查看视图)', 'list')
|
|
513
612
|
.option('--need-select', '点击前需勾选数据(默认否;仅列表视图的自定义操作有效)')
|
|
514
613
|
.option('--url <url>', '打开链接地址(--type link 必填)')
|
|
515
614
|
.option('--script <code|@file>', '脚本内容或 @文件(--type script 必填)')
|
|
516
615
|
.option('--dialog-id <fdId>', '对话框 id(--type dialog 必填,可在平台对话框设计中获取)')
|
|
616
|
+
.option('--trigger-flow <fdId>', '绑定动作流(需 StartActiveNode + 已启用 + operateTrigger)')
|
|
617
|
+
.option('--inc-params <json|@file>', '动作流入参:[{field:{name},value:{val}}]')
|
|
517
618
|
.option('--desc <text>', '描述(≤500)')
|
|
518
619
|
.option('--mobile-icon <name>', '移动端图标名')
|
|
519
620
|
.option('--dry-run', '只打印请求体,不提交')
|
|
@@ -521,13 +622,16 @@ function registerOperationCommand(program) {
|
|
|
521
622
|
.option('--token <token>', '鉴权 Token(覆盖配置)')
|
|
522
623
|
.option('--json', '以 JSON 输出结果')
|
|
523
624
|
.action(async (opts) => {
|
|
524
|
-
const
|
|
625
|
+
const client = createClient(opts);
|
|
626
|
+
const flow = opts.triggerFlow
|
|
627
|
+
? await flow_1.flowApi.get(client, String(opts.triggerFlow))
|
|
628
|
+
: undefined;
|
|
629
|
+
const { payload, kind } = buildCreatePayload(opts, flow);
|
|
525
630
|
if (opts.dryRun) {
|
|
526
631
|
logger_1.logger.info(logger_1.color.gray(`# ${KIND_META[kind].label} · 表单 ${String(opts.formId)}(dry-run)`));
|
|
527
632
|
logger_1.logger.json(payload);
|
|
528
633
|
return;
|
|
529
634
|
}
|
|
530
|
-
const client = createClient(opts);
|
|
531
635
|
logger_1.logger.step(`新建业务操作:${String(opts.name)}(${KIND_META[kind].label})`);
|
|
532
636
|
const result = await operation_1.operationApi.add(client, payload);
|
|
533
637
|
const fdId = typeof result === 'string' ? result : result?.fdId;
|
|
@@ -551,6 +655,8 @@ function registerOperationCommand(program) {
|
|
|
551
655
|
.option('--url <url>', '打开链接地址(link)')
|
|
552
656
|
.option('--script <code|@file>', '脚本内容或 @文件(script)')
|
|
553
657
|
.option('--dialog-id <fdId>', '对话框 id(dialog)')
|
|
658
|
+
.option('--trigger-flow <fdId>', '切换为触发动作流(需 StartActiveNode + 已启用)')
|
|
659
|
+
.option('--inc-params <json|@file>', '动作流入参:[{field:{name},value:{val}}]')
|
|
554
660
|
.option('--need-select', '点击前需勾选数据')
|
|
555
661
|
.option('--no-need-select', '不需要勾选数据')
|
|
556
662
|
.option('--enable', '启用该操作')
|
|
@@ -570,7 +676,10 @@ function registerOperationCommand(program) {
|
|
|
570
676
|
const current = await operation_1.operationApi.get(client, fdId);
|
|
571
677
|
if (!current?.fdId)
|
|
572
678
|
throw new Error(`业务操作不存在:${fdId}`);
|
|
573
|
-
const
|
|
679
|
+
const flow = opts.triggerFlow
|
|
680
|
+
? await flow_1.flowApi.get(client, String(opts.triggerFlow))
|
|
681
|
+
: undefined;
|
|
682
|
+
const { payload, kind } = buildUpdatePayload(current, opts, flow);
|
|
574
683
|
if (opts.dryRun) {
|
|
575
684
|
logger_1.logger.info(logger_1.color.gray(`# ${KIND_META[kind].label} · ${current.fdName}(dry-run)`));
|
|
576
685
|
logger_1.logger.json(payload);
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* 动作流编译器:Flow Spec(声明式 JSON) → fdDesign(IDE 可回显的设计 JSON 字符串)
|
|
4
|
+
*
|
|
5
|
+
* 关键源码依据:
|
|
6
|
+
* - 节点结构 enter.tsx:367-393:{ id, code, name, type, nodeType, drawAttr:{bounds},
|
|
7
|
+
* extraAttr:{context_*}, ...generateNodeData 字段 }
|
|
8
|
+
* - 连线结构 enter.tsx:395-407:{ id, name, type:'line', sourceRef, targetRef,
|
|
9
|
+
* drawAttr:{style} },不写 wayPoints
|
|
10
|
+
* - 保存/发布 enter.tsx:532-583:params = { ...dataSource, ...basicInfo,
|
|
11
|
+
* fdDesign: JSON.stringify(...), fdStartWay }
|
|
12
|
+
* - 循环子画布 enter.tsx:357-365:subProcess = { nodes, lines, processVar, ... }
|
|
13
|
+
*
|
|
14
|
+
* 阶段 1 产出的是「结构 + 布局」合法的设计;extraAttr.context_* 由 spec.context
|
|
15
|
+
* 显式提供(或由后续阶段的 SchemaResolver 注入),不会凭空猜测。
|
|
16
|
+
*/
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.hasErrors = exports.NODE_SIZE = void 0;
|
|
19
|
+
exports.normalizeGraph = normalizeGraph;
|
|
20
|
+
exports.compileFlow = compileFlow;
|
|
21
|
+
const layout_1 = require("./layout");
|
|
22
|
+
Object.defineProperty(exports, "NODE_SIZE", { enumerable: true, get: function () { return layout_1.NODE_SIZE; } });
|
|
23
|
+
const schema_1 = require("./schema");
|
|
24
|
+
const validate_1 = require("./validate");
|
|
25
|
+
Object.defineProperty(exports, "hasErrors", { enumerable: true, get: function () { return validate_1.hasErrors; } });
|
|
26
|
+
function clone(value) {
|
|
27
|
+
return JSON.parse(JSON.stringify(value));
|
|
28
|
+
}
|
|
29
|
+
function allocateId(ids, preferred) {
|
|
30
|
+
if (!ids.has(preferred))
|
|
31
|
+
return preferred;
|
|
32
|
+
for (let i = 2; i < 100; i++) {
|
|
33
|
+
const candidate = `${preferred}_${i}`;
|
|
34
|
+
if (!ids.has(candidate))
|
|
35
|
+
return candidate;
|
|
36
|
+
}
|
|
37
|
+
return `${preferred}_${Date.now().toString(36)}`;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* 归一化图:自动补开始/结束节点、自动补首尾连线、分配 rank 与 lane。
|
|
41
|
+
*
|
|
42
|
+
* rank 语义:
|
|
43
|
+
* - ide 模式:BranchNode 的直接后继与分支节点同层(对应 IDE 的左右插入 R5)
|
|
44
|
+
* - layered 模式:所有边都推进一层
|
|
45
|
+
* lane 语义:lane 0 = 主链居中;分支目标取 ±1, ±2 … 左右对称。
|
|
46
|
+
*/
|
|
47
|
+
function normalizeGraph(spec, mode, hints = {}) {
|
|
48
|
+
const diagnostics = [];
|
|
49
|
+
const nodes = clone(spec.nodes);
|
|
50
|
+
const edges = [];
|
|
51
|
+
const seenEdge = new Set();
|
|
52
|
+
const addEdge = (edge) => {
|
|
53
|
+
const key = `${edge.from}->${edge.to}`;
|
|
54
|
+
if (seenEdge.has(key))
|
|
55
|
+
return;
|
|
56
|
+
seenEdge.add(key);
|
|
57
|
+
edges.push(edge);
|
|
58
|
+
};
|
|
59
|
+
for (const edge of clone(spec.edges))
|
|
60
|
+
addEdge(edge);
|
|
61
|
+
const ids = new Set(nodes.map((n) => n.id));
|
|
62
|
+
let start = nodes.find((n) => n.type === 'StartNode');
|
|
63
|
+
let end = nodes.find((n) => n.type === 'EndNode');
|
|
64
|
+
if (!start) {
|
|
65
|
+
const id = allocateId(ids, hints.startId ?? 'N1');
|
|
66
|
+
ids.add(id);
|
|
67
|
+
start = { id, type: 'StartNode', name: '开始' };
|
|
68
|
+
nodes.unshift(start);
|
|
69
|
+
}
|
|
70
|
+
if (!end) {
|
|
71
|
+
let maxN = 1;
|
|
72
|
+
for (const id of ids) {
|
|
73
|
+
const matched = /^N(\d+)$/.exec(id);
|
|
74
|
+
if (matched)
|
|
75
|
+
maxN = Math.max(maxN, Number(matched[1]));
|
|
76
|
+
}
|
|
77
|
+
const id = allocateId(ids, hints.endId ?? `N${maxN + 1}`);
|
|
78
|
+
ids.add(id);
|
|
79
|
+
end = { id, type: 'EndNode', name: '结束' };
|
|
80
|
+
nodes.push(end);
|
|
81
|
+
}
|
|
82
|
+
const startId = start.id;
|
|
83
|
+
const endId = end.id;
|
|
84
|
+
const degree = () => {
|
|
85
|
+
const incoming = new Map();
|
|
86
|
+
const outgoing = new Map();
|
|
87
|
+
for (const edge of edges) {
|
|
88
|
+
outgoing.set(edge.from, (outgoing.get(edge.from) ?? 0) + 1);
|
|
89
|
+
incoming.set(edge.to, (incoming.get(edge.to) ?? 0) + 1);
|
|
90
|
+
}
|
|
91
|
+
return { incoming, outgoing };
|
|
92
|
+
};
|
|
93
|
+
const business = nodes.filter((n) => n.id !== startId && n.id !== endId);
|
|
94
|
+
let deg = degree();
|
|
95
|
+
for (const node of business) {
|
|
96
|
+
if ((deg.incoming.get(node.id) ?? 0) === 0)
|
|
97
|
+
addEdge({ from: startId, to: node.id });
|
|
98
|
+
}
|
|
99
|
+
deg = degree();
|
|
100
|
+
for (const node of business) {
|
|
101
|
+
if ((deg.outgoing.get(node.id) ?? 0) === 0)
|
|
102
|
+
addEdge({ from: node.id, to: endId });
|
|
103
|
+
}
|
|
104
|
+
if (business.length === 0 && edges.length === 0)
|
|
105
|
+
addEdge({ from: startId, to: endId });
|
|
106
|
+
const byId = new Map(nodes.map((n) => [n.id, n]));
|
|
107
|
+
const ranks = { [startId]: 0 };
|
|
108
|
+
const branchTargets = new Set();
|
|
109
|
+
for (const edge of edges) {
|
|
110
|
+
if (byId.get(edge.from)?.type === 'BranchNode')
|
|
111
|
+
branchTargets.add(edge.to);
|
|
112
|
+
}
|
|
113
|
+
// 迭代松弛求最长路径层级;不收敛即存在环
|
|
114
|
+
const limit = nodes.length + 2;
|
|
115
|
+
let converged = false;
|
|
116
|
+
for (let pass = 0; pass < limit; pass++) {
|
|
117
|
+
let changed = false;
|
|
118
|
+
for (const edge of edges) {
|
|
119
|
+
const fromRank = ranks[edge.from];
|
|
120
|
+
if (fromRank === undefined)
|
|
121
|
+
continue;
|
|
122
|
+
const sameRank = mode === 'ide' && byId.get(edge.from)?.type === 'BranchNode';
|
|
123
|
+
const candidate = sameRank ? fromRank : fromRank + 1;
|
|
124
|
+
if (ranks[edge.to] === undefined || candidate > ranks[edge.to]) {
|
|
125
|
+
ranks[edge.to] = candidate;
|
|
126
|
+
changed = true;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (!changed) {
|
|
130
|
+
converged = true;
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (!converged) {
|
|
135
|
+
diagnostics.push({ level: 'error', code: 'GRAPH_CYCLE', message: '图中存在环,动作流必须是 DAG(IDE 禁止回环)' });
|
|
136
|
+
}
|
|
137
|
+
const maxRank = Math.max(0, ...Object.values(ranks));
|
|
138
|
+
for (const node of nodes) {
|
|
139
|
+
if (ranks[node.id] === undefined) {
|
|
140
|
+
diagnostics.push({
|
|
141
|
+
level: 'warn', code: 'GRAPH_ORPHAN',
|
|
142
|
+
message: `节点 ${node.id} 未连接到主流程,已放置在末层`, nodeId: node.id,
|
|
143
|
+
});
|
|
144
|
+
ranks[node.id] = maxRank + 1;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const normalized = nodes.map((node) => ({
|
|
148
|
+
...node,
|
|
149
|
+
_rank: node.rank ?? ranks[node.id] ?? 0,
|
|
150
|
+
_lane: node.lane ?? 0,
|
|
151
|
+
_isStart: node.id === startId,
|
|
152
|
+
_isEnd: node.id === endId,
|
|
153
|
+
_isBranchTarget: branchTargets.has(node.id),
|
|
154
|
+
}));
|
|
155
|
+
// 层内泳道分配:主链/分支节点优先占 lane 0,分支目标左右对称
|
|
156
|
+
const byRank = new Map();
|
|
157
|
+
for (const node of normalized) {
|
|
158
|
+
const group = byRank.get(node._rank) ?? [];
|
|
159
|
+
group.push(node);
|
|
160
|
+
byRank.set(node._rank, group);
|
|
161
|
+
}
|
|
162
|
+
for (const group of byRank.values()) {
|
|
163
|
+
const priority = (n) => n._isBranchTarget ? 2 : n._isStart || n._isEnd || n.type === 'BranchNode' ? 0 : 1;
|
|
164
|
+
group.sort((a, b) => priority(a) - priority(b));
|
|
165
|
+
const used = new Set();
|
|
166
|
+
for (const node of group) {
|
|
167
|
+
if (node.lane !== undefined) {
|
|
168
|
+
used.add(node.lane);
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
const candidates = node._isBranchTarget
|
|
172
|
+
? [-1, 1, -2, 2, -3, 3, -4, 4]
|
|
173
|
+
: [0, -1, 1, -2, 2, -3, 3, -4, 4];
|
|
174
|
+
let lane = candidates.find((value) => !used.has(value));
|
|
175
|
+
if (lane === undefined)
|
|
176
|
+
lane = used.size + 1;
|
|
177
|
+
used.add(lane);
|
|
178
|
+
node._lane = lane;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return { graph: { nodes: normalized, edges, startId, endId }, diagnostics };
|
|
182
|
+
}
|
|
183
|
+
/** 构造 startNodeDef(startNode.tsx:193-216 的 fdProcessActive) */
|
|
184
|
+
function buildStartNodeDef(node, ctx) {
|
|
185
|
+
const provided = node.config?.startNodeDef;
|
|
186
|
+
if (provided && typeof provided === 'object' && !Array.isArray(provided)) {
|
|
187
|
+
return provided;
|
|
188
|
+
}
|
|
189
|
+
const trigger = ctx.trigger ?? {};
|
|
190
|
+
const way = ctx.startWay;
|
|
191
|
+
// 平台落库的 inParam 是 JSON 字符串(真实 get 回包中 fdProcessActive.fdInParam 与
|
|
192
|
+
// startNodeDef.inParam 均为字符串);spec 里允许写成对象,这里统一序列化。
|
|
193
|
+
const inParam = trigger.inParam !== undefined && typeof trigger.inParam !== 'string'
|
|
194
|
+
? JSON.stringify(trigger.inParam)
|
|
195
|
+
: trigger.inParam;
|
|
196
|
+
if (way === 'StartActiveNode') {
|
|
197
|
+
const runTypes = Array.isArray(trigger.fdRunType) ? trigger.fdRunType : [];
|
|
198
|
+
const fdFormRelation = trigger.fdFormRelation ?? '0';
|
|
199
|
+
return {
|
|
200
|
+
type: way,
|
|
201
|
+
operateTrigger: runTypes.includes('operate') || trigger.operateTrigger === true,
|
|
202
|
+
bpmTrigger: runTypes.includes('bpm') || trigger.bpmTrigger === true,
|
|
203
|
+
apiTrigger: runTypes.includes('api') || trigger.apiTrigger === true,
|
|
204
|
+
inParamType: trigger.inParamType,
|
|
205
|
+
inParam,
|
|
206
|
+
fdFormRelation,
|
|
207
|
+
formId: fdFormRelation === '0' ? undefined : trigger.formId,
|
|
208
|
+
dataType: trigger.dataType ?? 1,
|
|
209
|
+
entityId: trigger.entityId,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
return { type: way, ...trigger, ...(inParam !== undefined ? { inParam } : {}) };
|
|
213
|
+
}
|
|
214
|
+
function buildNode(node, bounds, ctx, branchDefaults, branchFormulas, diagnostics) {
|
|
215
|
+
const config = { ...(schema_1.NODE_DEFAULTS[node.type] ?? {}), ...(node.config ?? {}) };
|
|
216
|
+
const picked = {};
|
|
217
|
+
const whitelist = schema_1.NODE_FIELD_WHITELIST[node.type];
|
|
218
|
+
if (whitelist) {
|
|
219
|
+
for (const field of whitelist) {
|
|
220
|
+
if (field in config && config[field] !== undefined)
|
|
221
|
+
picked[field] = config[field];
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
Object.assign(picked, config);
|
|
226
|
+
}
|
|
227
|
+
if (node._isStart && ctx.isRoot)
|
|
228
|
+
picked.startNodeDef = buildStartNodeDef(node, ctx);
|
|
229
|
+
if (node.type === 'BranchNode') {
|
|
230
|
+
const defaultTarget = branchDefaults.get(node.id);
|
|
231
|
+
if (defaultTarget !== undefined)
|
|
232
|
+
picked.defaultTargetRef = defaultTarget;
|
|
233
|
+
// 平台 IDE 的 branchFormulas[].line 引用连线 id;由编译器按实际出边自动生成。
|
|
234
|
+
const auto = branchFormulas.get(node.id);
|
|
235
|
+
if (picked.formulaConfig === undefined && auto !== undefined)
|
|
236
|
+
picked.formulaConfig = auto;
|
|
237
|
+
}
|
|
238
|
+
if (node.type === 'FilterDataNode' && picked.formula && typeof picked.formula === 'object') {
|
|
239
|
+
picked.formula = JSON.stringify(picked.formula);
|
|
240
|
+
}
|
|
241
|
+
// 循环节点子画布:独立坐标系、独立 ID 前缀(graphId = <父链>-<循环节点 id>)
|
|
242
|
+
if (node.subProcess) {
|
|
243
|
+
const subResult = compileGraph(node.subProcess, {
|
|
244
|
+
...ctx,
|
|
245
|
+
startId: `${node.id}_1`,
|
|
246
|
+
endId: `${node.id}_2`,
|
|
247
|
+
centerX: ctx.options.subCenterX ?? ctx.centerX,
|
|
248
|
+
isRoot: false,
|
|
249
|
+
});
|
|
250
|
+
diagnostics.push(...subResult.diagnostics);
|
|
251
|
+
picked.subProcess = {
|
|
252
|
+
nodes: subResult.design.nodes,
|
|
253
|
+
lines: subResult.design.lines,
|
|
254
|
+
...(node.subProcess.processVar ? { processVar: node.subProcess.processVar } : {}),
|
|
255
|
+
...(node.subProcess.processVarInitConfigs !== undefined
|
|
256
|
+
? { processVarInitConfigs: node.subProcess.processVarInitConfigs }
|
|
257
|
+
: {}),
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
const context = (node.context ?? {});
|
|
261
|
+
const extraAttr = {};
|
|
262
|
+
for (const key of schema_1.CONTEXT_KEYS) {
|
|
263
|
+
if (context[key] !== undefined)
|
|
264
|
+
extraAttr[key] = context[key];
|
|
265
|
+
}
|
|
266
|
+
return {
|
|
267
|
+
id: node.id,
|
|
268
|
+
// 真实平台保存时主画布节点通常不带 code(IDE 回显时以 id 兜底);
|
|
269
|
+
// 仅当 spec 显式声明 code 时才写出。
|
|
270
|
+
...(node.code ? { code: node.code } : {}),
|
|
271
|
+
name: node.name ?? node.type,
|
|
272
|
+
type: 'node',
|
|
273
|
+
nodeType: node.type,
|
|
274
|
+
...(node.dynamicProps ? { dynamicProps: node.dynamicProps } : {}),
|
|
275
|
+
drawAttr: { bounds: bounds[node.id] ?? { x: 0, y: 0, ...(0, layout_1.nodeSize)(node.type) } },
|
|
276
|
+
extraAttr,
|
|
277
|
+
...picked,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
function compileGraph(spec, ctx) {
|
|
281
|
+
const mode = ctx.options.mode ?? 'ide';
|
|
282
|
+
const normalized = normalizeGraph(spec, mode, { startId: ctx.startId, endId: ctx.endId });
|
|
283
|
+
const graphDiagnostics = (0, validate_1.validateGraph)(normalized.graph, spec, ctx.startWay);
|
|
284
|
+
const diagnostics = [
|
|
285
|
+
...normalized.diagnostics,
|
|
286
|
+
...(ctx.isRoot ? graphDiagnostics : graphDiagnostics.filter((d) => !d.code.startsWith('START_'))),
|
|
287
|
+
];
|
|
288
|
+
const layout = (0, layout_1.layoutGraph)(normalized.graph, { ...ctx.options, mode, centerX: ctx.centerX ?? ctx.options.centerX });
|
|
289
|
+
diagnostics.push(...layout.diagnostics);
|
|
290
|
+
const branchDefaults = new Map();
|
|
291
|
+
for (const edge of normalized.graph.edges) {
|
|
292
|
+
if (edge.default)
|
|
293
|
+
branchDefaults.set(edge.from, edge.to);
|
|
294
|
+
}
|
|
295
|
+
// 连线 id 先确定,formulaConfig.branchFormulas[].line 需要引用它们。
|
|
296
|
+
const nodeById = new Map(normalized.graph.nodes.map((n) => [n.id, n]));
|
|
297
|
+
const lineIdOf = new Map(normalized.graph.edges.map((e, i) => [e, `L${i + 1}`]));
|
|
298
|
+
const branchFormulas = new Map();
|
|
299
|
+
for (const node of normalized.graph.nodes) {
|
|
300
|
+
if (node.type !== 'BranchNode')
|
|
301
|
+
continue;
|
|
302
|
+
const outgoing = normalized.graph.edges.filter((e) => e.from === node.id);
|
|
303
|
+
if (outgoing.length === 0)
|
|
304
|
+
continue;
|
|
305
|
+
// 默认分支排 priority 1,其余按出边顺序;与真实 condition.tsx 的 branchFormulas 一致。
|
|
306
|
+
const ordered = [...outgoing].sort((a, b) => Number(Boolean(b.default)) - Number(Boolean(a.default)));
|
|
307
|
+
branchFormulas.set(node.id, {
|
|
308
|
+
branchFormulas: ordered.map((edge, index) => ({
|
|
309
|
+
line: lineIdOf.get(edge),
|
|
310
|
+
targetRef: edge.to,
|
|
311
|
+
priority: index + 1,
|
|
312
|
+
// 默认分支不配公式(与平台 condition.tsx 的 branchFormulas 一致)
|
|
313
|
+
...(edge.formula !== undefined && !edge.default
|
|
314
|
+
? { formula: typeof edge.formula === 'string' ? edge.formula : JSON.stringify(edge.formula) }
|
|
315
|
+
: {}),
|
|
316
|
+
})),
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
const nodes = normalized.graph.nodes.map((node) => buildNode(node, layout.bounds, ctx, branchDefaults, branchFormulas, diagnostics));
|
|
320
|
+
const lines = normalized.graph.edges.map((edge) => {
|
|
321
|
+
// 条件分支出边需要左右锚点,否则 IDE 里两条线会重叠(真实 style: exitX=0/1)。
|
|
322
|
+
const source = nodeById.get(edge.from);
|
|
323
|
+
const target = nodeById.get(edge.to);
|
|
324
|
+
const isBranchOut = source?.type === 'BranchNode';
|
|
325
|
+
const exitX = isBranchOut && target ? (target._lane < 0 ? 0 : target._lane > 0 ? 1 : 0.5) : 0.5;
|
|
326
|
+
const style = isBranchOut
|
|
327
|
+
? `line;exitX=${exitX};exitY=0.5;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;`
|
|
328
|
+
: '';
|
|
329
|
+
return {
|
|
330
|
+
id: lineIdOf.get(edge),
|
|
331
|
+
name: edge.name ?? '',
|
|
332
|
+
type: 'line',
|
|
333
|
+
...(edge.dynamicProps ? { dynamicProps: edge.dynamicProps } : {}),
|
|
334
|
+
sourceRef: edge.from,
|
|
335
|
+
targetRef: edge.to,
|
|
336
|
+
// 平台保存时 item.style 为空 → JSON.stringify 后 drawAttr 为 {};
|
|
337
|
+
// 仅 --emit-waypoints 时额外写入 wayPoints(非 IDE 原生行为)。
|
|
338
|
+
drawAttr: ctx.options.emitWayPoints
|
|
339
|
+
? { style, wayPoints: [] }
|
|
340
|
+
: isBranchOut ? { style } : {},
|
|
341
|
+
};
|
|
342
|
+
});
|
|
343
|
+
const design = {
|
|
344
|
+
nodes,
|
|
345
|
+
lines,
|
|
346
|
+
...(spec.processVar ? { processVar: spec.processVar } : {}),
|
|
347
|
+
...(spec.processVarInitConfigs !== undefined ? { processVarInitConfigs: spec.processVarInitConfigs } : {}),
|
|
348
|
+
};
|
|
349
|
+
const bounds = normalized.graph.nodes
|
|
350
|
+
.map((node) => ({
|
|
351
|
+
id: node.id,
|
|
352
|
+
type: node.type,
|
|
353
|
+
bounds: layout.bounds[node.id],
|
|
354
|
+
rank: node._rank,
|
|
355
|
+
lane: node._lane,
|
|
356
|
+
}))
|
|
357
|
+
.filter((item) => item.bounds !== undefined);
|
|
358
|
+
return { design, bounds, rankCenters: layout.rankCenters, diagnostics, graph: normalized.graph };
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* 编译入口:返回可直接与 basicInfo 一起提交给 addDraft / addPublish 的 fdDesign。
|
|
362
|
+
* 注意:调用方应在 hasErrors(result.diagnostics) 为真时阻止发包(除非 --force)。
|
|
363
|
+
*/
|
|
364
|
+
function compileFlow(spec, options = {}) {
|
|
365
|
+
const startWay = spec.startWay ?? 'StartActiveNode';
|
|
366
|
+
const diagnostics = (0, validate_1.validateSpec)(spec);
|
|
367
|
+
const graphResult = compileGraph(spec, {
|
|
368
|
+
startWay,
|
|
369
|
+
trigger: spec.trigger,
|
|
370
|
+
options,
|
|
371
|
+
centerX: options.centerX ?? spec.centerX,
|
|
372
|
+
isRoot: true,
|
|
373
|
+
});
|
|
374
|
+
diagnostics.push(...graphResult.diagnostics);
|
|
375
|
+
const basicInfo = {
|
|
376
|
+
fdName: spec.name,
|
|
377
|
+
fdCode: spec.code,
|
|
378
|
+
fdStartWay: startWay,
|
|
379
|
+
fdStatus: -1,
|
|
380
|
+
fdTransactionType: spec.fdTransactionType ?? 'multiTransaction',
|
|
381
|
+
fdLogDetail: spec.fdLogDetail ?? false,
|
|
382
|
+
...(spec.appCode ? { fdAppCode: spec.appCode } : {}),
|
|
383
|
+
...(spec.fdDesc !== undefined ? { fdDesc: spec.fdDesc } : {}),
|
|
384
|
+
...(spec.fdCategory ? { fdCategory: spec.fdCategory } : {}),
|
|
385
|
+
...(spec.fdIcon ? { fdIcon: spec.fdIcon } : {}),
|
|
386
|
+
...(spec.fdId ? { fdId: spec.fdId } : {}),
|
|
387
|
+
...(spec.basicInfo ?? {}),
|
|
388
|
+
};
|
|
389
|
+
return {
|
|
390
|
+
fdDesign: JSON.stringify(graphResult.design),
|
|
391
|
+
design: graphResult.design,
|
|
392
|
+
fdStartWay: startWay,
|
|
393
|
+
basicInfo,
|
|
394
|
+
bounds: graphResult.bounds,
|
|
395
|
+
diagnostics,
|
|
396
|
+
};
|
|
397
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./types"), exports);
|
|
18
|
+
__exportStar(require("./schema"), exports);
|
|
19
|
+
__exportStar(require("./layout"), exports);
|
|
20
|
+
__exportStar(require("./validate"), exports);
|
|
21
|
+
__exportStar(require("./schema-resolver"), exports);
|
|
22
|
+
__exportStar(require("./compiler"), exports);
|