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.
@@ -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 viewType = Number(readOperationConfig(op).fdViewType ?? op.fdViewType);
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 为链接;fdType=3 + fdViewType=5 为脚本,其余按对话框) */
223
+ /** 从已有操作推导类型(fdType=2 链接;fdTriggerType=3 动作流;fdViewType=5 脚本;其余对话框) */
216
224
  function detectKind(op) {
217
225
  if (op.fdType === 2)
218
226
  return 'link';
219
- const viewType = Number(readOperationConfig(op).fdViewType ?? op.fdViewType);
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(`未知 --type:${opts.type}(可选 link | script | dialog)`);
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 (rawKind) {
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
- .requiredOption('--type <type>', '类型:link | script | dialog')
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 { payload, kind } = buildCreatePayload(opts);
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 { payload, kind } = buildUpdatePayload(current, opts);
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);