lan-low-code-cli 2.6.0 → 2.7.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.
@@ -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);
@@ -0,0 +1,136 @@
1
+ "use strict";
2
+ /**
3
+ * 动作流编排布局引擎
4
+ *
5
+ * 源码依据(均来自 action-flow-ide):
6
+ * - 尺寸表 styleSheet.ts:339-347:BranchNode 56×56,其余 180×38;回显时宽高被强制覆盖
7
+ * - 种子 mxgraph/utils.tsx:93-135:N1 在 (C-90, 80)、N2 在 (C-90, 166),L1 直连
8
+ * - 向下插入 R4(mxgraph/utils.tsx:422-465,1408-1428):distance = h_prev + 52,
9
+ * cy_new = cy_prev + distance,下游整体 y += distance
10
+ * - 左右插入 R5(mxgraph/utils.tsx:442-458,568-584):与源节点同层中心线,
11
+ * x = C - w/2 ± 2*distance
12
+ * - 无整图自动布局、无后端布局服务;坐标是持久化契约,IDE 回显原样使用
13
+ *
14
+ * 因此这里按「rank(逻辑层)+ lane(层内泳道)+ 中心线」复刻编排习惯:
15
+ * 主链居中,分支的直接后继与分支节点同层、左右对称,汇聚节点下移一层。
16
+ */
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.DEFAULT_CENTER_X = exports.MIN_GAP = exports.RANK_PADDING = exports.START_Y = exports.NODE_SIZE = void 0;
19
+ exports.nodeSize = nodeSize;
20
+ exports.layoutGraph = layoutGraph;
21
+ /** 与 styleSheet.ts nodeBounds 一致 */
22
+ exports.NODE_SIZE = {
23
+ BranchNode: { width: 56, height: 56 },
24
+ general: { width: 180, height: 38 },
25
+ };
26
+ /** 主画布开始节点的 y(mxgraph/utils.tsx:130) */
27
+ exports.START_Y = 80;
28
+ /** R4 的层间距增量:distance = h_prev + 52 */
29
+ exports.RANK_PADDING = 52;
30
+ /**
31
+ * 最小边缘间隙,默认 40。
32
+ *
33
+ * IDE 常规编排的垂直间隙由 R4 推导:gap = 52 + (h_prev - h_new)/2;
34
+ * 38 高节点后紧跟 56 高 BranchNode 时最小为 52 - (56-38)/2 = 43px。
35
+ * 因此不能照搬 mxHierarchicalLayout 的 interRankCellSpacing=50(该值只在
36
+ * 折叠/展开时生效),否则正常分支流程会被误判为布局重叠。
37
+ */
38
+ exports.MIN_GAP = 40;
39
+ /** W=1920 时 (1920-562)/2;CLI 无浏览器宽度,可用 --center-x 覆盖 */
40
+ exports.DEFAULT_CENTER_X = 679;
41
+ function nodeSize(nodeType) {
42
+ return exports.NODE_SIZE[nodeType] ?? exports.NODE_SIZE.general;
43
+ }
44
+ const round = (v) => Math.round(v * 100) / 100;
45
+ function layoutGraph(graph, options = {}) {
46
+ const mode = options.mode ?? 'ide';
47
+ const centerX = options.centerX ?? exports.DEFAULT_CENTER_X;
48
+ const padding = options.rankPadding ?? exports.RANK_PADDING;
49
+ const minGap = options.minGap ?? exports.MIN_GAP;
50
+ const diagnostics = [];
51
+ const bounds = {};
52
+ // passthrough:原样沿用已有坐标(P1 克隆 / UI 导出流程回写)
53
+ if (mode === 'passthrough') {
54
+ for (const node of graph.nodes) {
55
+ const size = nodeSize(node.type);
56
+ const given = node.bounds;
57
+ if (!given || typeof given.x !== 'number' || typeof given.y !== 'number') {
58
+ diagnostics.push({
59
+ level: 'error',
60
+ code: 'LAYOUT_MISSING_BOUNDS',
61
+ message: `passthrough 模式要求节点提供 bounds.{x,y}:${node.id}`,
62
+ nodeId: node.id,
63
+ });
64
+ continue;
65
+ }
66
+ bounds[node.id] = {
67
+ x: given.x,
68
+ y: given.y,
69
+ width: size.width,
70
+ height: size.height,
71
+ relative: given.relative ?? false,
72
+ };
73
+ }
74
+ return { bounds, rankCenters: [], diagnostics };
75
+ }
76
+ const maxRank = graph.nodes.reduce((max, n) => Math.max(max, n._rank), 0);
77
+ // 每层取最高节点,决定「该层到下一层」的间距:38→90、56→108
78
+ const rankHeight = [];
79
+ for (let r = 0; r <= maxRank; r++) {
80
+ const heights = graph.nodes.filter((n) => n._rank === r).map((n) => nodeSize(n.type).height);
81
+ rankHeight[r] = heights.length > 0 ? Math.max(...heights) : exports.NODE_SIZE.general.height;
82
+ }
83
+ const gapOf = (r) => (rankHeight[r] ?? exports.NODE_SIZE.general.height) + padding;
84
+ // 中心线:cy(0) = 80 + h_start/2 = 99;cy(r) = cy(r-1) + gap(r-1)
85
+ const rankCenters = [];
86
+ const startNode = graph.nodes.find((n) => n._isStart);
87
+ rankCenters[0] = exports.START_Y + (startNode ? nodeSize(startNode.type).height : exports.NODE_SIZE.general.height) / 2;
88
+ for (let r = 1; r <= maxRank; r++) {
89
+ rankCenters[r] = rankCenters[r - 1] + gapOf(r - 1);
90
+ }
91
+ for (const node of graph.nodes) {
92
+ const size = nodeSize(node.type);
93
+ const r = node._rank;
94
+ // R5 的 ±2*distance:distance 取当前层最高节点对应的层间距
95
+ const laneGap = options.laneGap ?? 2 * gapOf(r);
96
+ const x = centerX - size.width / 2 + node._lane * laneGap;
97
+ const y = (rankCenters[r] ?? rankCenters[rankCenters.length - 1]) - size.height / 2;
98
+ bounds[node.id] = {
99
+ x: round(x),
100
+ y: round(y),
101
+ width: size.width,
102
+ height: size.height,
103
+ relative: false,
104
+ };
105
+ }
106
+ // 矩形重叠检查(含最小间隙)
107
+ const placed = graph.nodes.filter((n) => bounds[n.id]);
108
+ for (let i = 0; i < placed.length; i++) {
109
+ for (let j = i + 1; j < placed.length; j++) {
110
+ const a = bounds[placed[i].id];
111
+ const b = bounds[placed[j].id];
112
+ const horizontal = a.x < b.x + b.width + minGap && b.x < a.x + a.width + minGap;
113
+ const vertical = a.y < b.y + b.height + minGap && b.y < a.y + a.height + minGap;
114
+ if (horizontal && vertical) {
115
+ diagnostics.push({
116
+ level: 'error',
117
+ code: 'LAYOUT_OVERLAP',
118
+ message: `节点 ${placed[i].id} 与 ${placed[j].id} 的布局重叠或间距不足 ${minGap}px`,
119
+ nodeId: placed[j].id,
120
+ });
121
+ }
122
+ }
123
+ }
124
+ for (const node of placed) {
125
+ const b = bounds[node.id];
126
+ if (b.x < 0 || b.y < 0) {
127
+ diagnostics.push({
128
+ level: 'warn',
129
+ code: 'LAYOUT_NEGATIVE',
130
+ message: `节点 ${node.id} 坐标为负 (${b.x}, ${b.y}),IDE 中可能显示不全;可增大 --center-x / --lane-gap`,
131
+ nodeId: node.id,
132
+ });
133
+ }
134
+ }
135
+ return { bounds, rankCenters, diagnostics };
136
+ }
@@ -0,0 +1,133 @@
1
+ "use strict";
2
+ /**
3
+ * SchemaResolver:联网补齐动作流节点的运行时上下文(extraAttr.context_*)
4
+ *
5
+ * 真实环境实测结论(2026-11-10,mkylxt):
6
+ * - `getSourceJsonSchema` / `getProcessDataOutJsonSchema` 的 entityId 必须带
7
+ * `sys-modeling#` 前缀,裸 id 返回 HTTP 500;返回值即 JSON Schema。
8
+ * - `getSourceFields` / `getConditionFields` / `loadDataModel` 用裸 id 正常。
9
+ * - 平台保存的 `extraAttr.context_bindFormId` 形如 `sys-modeling#<fdId>`。
10
+ * - 前端 `edit/content.tsx:55-87` 的 tempFormatFn 也会在打开编辑器时自动补
11
+ * extraAttr,因此这里采用 best-effort:调用失败只告警,不阻断发布。
12
+ *
13
+ * 不做的事:不臆造 `fields` 的公式映射。真实 fields 是
14
+ * `{ field:{name,label,tableName,tableType,type}, value:{format:'formula'|'fixed',valueJson:'...'} }`
15
+ * 数组,公式引用形如 `${data.node.N1.fd_xxx}`,必须由 spec 原样提供或从
16
+ * golden 模板复制;本模块只校验字段名是否在目标表 Schema 中存在。
17
+ */
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.resolveFlowSchema = resolveFlowSchema;
20
+ const flow_1 = require("../../api/flow");
21
+ function diagnose(out, level, code, message, nodeId) {
22
+ out.push({ level, code, message, nodeId });
23
+ }
24
+ async function tryCall(fn, label, out, strict, nodeId) {
25
+ try {
26
+ return await fn();
27
+ }
28
+ catch (err) {
29
+ diagnose(out, strict ? 'error' : 'warn', 'SCHEMA_FETCH_FAILED', `${label} 调用失败:${err.message.split('\n')[0]}`, nodeId);
30
+ return undefined;
31
+ }
32
+ }
33
+ /** 从节点扁平字段里取出目标实体 id(真实结构见 startNodeDef / dataSourceVO / target) */
34
+ function entityIdOf(node) {
35
+ const candidates = [
36
+ node.startNodeDef,
37
+ node.dataSourceVO,
38
+ node.target,
39
+ ];
40
+ for (const candidate of candidates) {
41
+ const value = candidate?.entityId;
42
+ if (typeof value === 'string' && value.trim())
43
+ return value.trim();
44
+ }
45
+ return undefined;
46
+ }
47
+ function parseInParam(raw, out, nodeId) {
48
+ if (typeof raw !== 'string')
49
+ return raw;
50
+ try {
51
+ return JSON.parse(raw);
52
+ }
53
+ catch {
54
+ diagnose(out, 'warn', 'SCHEMA_INPARAM_INVALID', 'startNodeDef.inParam 不是合法 JSON 字符串,已跳过注入', nodeId);
55
+ return undefined;
56
+ }
57
+ }
58
+ /** 校验 fields[].field.name 是否存在于目标表 Schema(不改写字段结构) */
59
+ function validateFieldNames(node, schema, out, nodeId) {
60
+ if (!schema || !Array.isArray(node.fields))
61
+ return;
62
+ const properties = schema.properties;
63
+ if (!properties)
64
+ return;
65
+ for (const item of node.fields) {
66
+ const field = item?.field;
67
+ const name = field?.name;
68
+ if (typeof name === 'string' && !(name in properties)) {
69
+ diagnose(out, 'warn', 'SCHEMA_FIELD_MISSING', `字段 ${name} 不在目标表 Schema 中,请核对 fdName`, nodeId);
70
+ }
71
+ }
72
+ }
73
+ async function resolveFlowSchema(client, spec, result, options = {}) {
74
+ const strict = Boolean(options.strict);
75
+ const diagnostics = [...result.diagnostics];
76
+ const startWay = spec.startWay ?? 'StartActiveNode';
77
+ const resolveNode = async (node, isRootCanvas) => {
78
+ const nodeId = String(node.id ?? '?');
79
+ const extra = (node.extraAttr ?? {});
80
+ node.extraAttr = extra;
81
+ const entityId = entityIdOf(node);
82
+ const isStart = node.nodeType === 'StartNode' && isRootCanvas;
83
+ let schema;
84
+ if (entityId) {
85
+ const qualified = (0, flow_1.qualifyEntityId)(entityId);
86
+ if (isStart && startWay === 'StartDataNode') {
87
+ const fetched = await tryCall(() => flow_1.flowApi.getDataOutJsonSchema(client, entityId), 'getProcessDataOutJsonSchema', diagnostics, strict, nodeId);
88
+ schema = fetched;
89
+ if (fetched) {
90
+ const sourceForm = fetched.properties?.sourceForm;
91
+ if (extra.context_fdInParam === undefined)
92
+ extra.context_fdInParam = fetched;
93
+ if (extra.context_nodeParam === undefined)
94
+ extra.context_nodeParam = sourceForm ?? null;
95
+ }
96
+ }
97
+ else if (!isStart) {
98
+ const fetched = await tryCall(() => flow_1.flowApi.getJsonSchema(client, entityId), 'getSourceJsonSchema', diagnostics, strict, nodeId);
99
+ schema = fetched;
100
+ if (fetched && extra.context_outputParam === undefined) {
101
+ extra.context_outputParam = fetched;
102
+ }
103
+ }
104
+ if (extra.context_bindFormId === undefined)
105
+ extra.context_bindFormId = qualified;
106
+ }
107
+ if (isStart) {
108
+ const startDef = node.startNodeDef;
109
+ if (extra.context_fdInParam === undefined && startDef?.inParam !== undefined) {
110
+ const parsed = parseInParam(startDef.inParam, diagnostics, nodeId);
111
+ if (parsed !== undefined)
112
+ extra.context_fdInParam = parsed;
113
+ }
114
+ if (extra.context_isMulti === undefined) {
115
+ extra.context_isMulti = startDef?.dataType === 2;
116
+ }
117
+ }
118
+ validateFieldNames(node, schema, diagnostics, nodeId);
119
+ const subProcess = node.subProcess;
120
+ if (subProcess && Array.isArray(subProcess.nodes)) {
121
+ for (const child of subProcess.nodes)
122
+ await resolveNode(child, false);
123
+ }
124
+ };
125
+ for (const node of result.design.nodes) {
126
+ await resolveNode(node, true);
127
+ }
128
+ return {
129
+ ...result,
130
+ fdDesign: JSON.stringify(result.design),
131
+ diagnostics,
132
+ };
133
+ }