lan-low-code-cli 2.8.0 → 2.9.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 CHANGED
@@ -363,6 +363,46 @@ llc flow delete --id <flowId> --yes
363
363
  这两个端点要求 `entityId` 带 `sys-modeling#` 前缀(CLI 自动补),裸 id 会返回 500。
364
364
  失败默认只告警,`--strict-schema` 升级为错误。
365
365
 
366
+ **节点必填校验**(复刻 IDE 规则表与豁免逻辑):
367
+
368
+ - `TransformNode`:必填 `outParam` 与 `outputParamMapping`(`valueFormula` 或非空 `pointMappings`);
369
+ `outParam` 为空时平台会豁免映射校验,但 `outParam` 本身仍必填
370
+ - `ExecuteNode` / `InterfaceNode` / `SubActionNode` / `FlowBotsNode`:必填各自编码字段,存在入参 Schema 时必填参数映射
371
+ - `BranchNode`:必填 `conditionType` / `flowWay` 及匹配的 `formulaConfig` / `ruleConfig`(有分支出边时自动生成)
372
+ - `LoopNode`:必填 `loopDefine.type`;`MessageNode`:按 `subjectType` / `messageLink.linkType` 校验内容与链接
373
+ - `AddDataNode` / `AddOrUpdateNode`:分类表单或未填 `commitType` 时豁免 `flow`
374
+ - 校验失败时输出 `field` 与修复建议,`flow validate` / `flow compile` 退出码为 `1`,不会写入环境
375
+
376
+ **参数映射(`TransformNode` / `ExecuteNode` 等)**:
377
+
378
+ ```json
379
+ {
380
+ "id": "N4",
381
+ "type": "TransformNode",
382
+ "name": "转换",
383
+ "config": {
384
+ "outParam": {
385
+ "fd_title": { "type": "string", "description": "标题" },
386
+ "fd_amount": { "type": "number" }
387
+ },
388
+ "mapping": {
389
+ "fd_title": "$N2.fd_title",
390
+ "fd_amount": "$N2.fd_amount"
391
+ }
392
+ }
393
+ }
394
+ ```
395
+
396
+ - `outParam` 可写字段映射对象(自动转成 `{id:'responseBody',type:'object',properties:{...}}` 的 JSON Schema 字符串),
397
+ 也可直接写完整 Schema 或 Schema 字符串;编译器写入 `outParamType=1`
398
+ - `mapping` 的 `$<节点id>.<路径>` 自动转成
399
+ `{sourceType:'node',sourcePath:'node.N2.fd_title',targetPath:'fd_title'}`;`$context.<路径>` 转为上下文映射
400
+ - 编译器同时把出参 Schema 写入 `extraAttr.context_outputParam`,供下游映射与 IDE 显示
401
+ - 未写 `mapping` 时,若上游节点已提供 `context.context_outputParam`,按同名字段自动推断,并给出
402
+ `MAPPING_AUTO_INFERRED` 提示
403
+ - 常量或表达式请用 `{ "paramMappingWay": "formula", "valueFormula": "..." }`;
404
+ `ExecuteNode` / `InterfaceNode` / `SubActionNode` / `FlowBotsNode` 用同一 `mapping` 生成 `inputParamMapping`
405
+
366
406
  **边界与已知限制**:
367
407
 
368
408
  - `AddDataNode` / `AddOrUpdateNode` 的 `fields` 是平台真实结构
@@ -57,12 +57,20 @@ function buildOptions(opts) {
57
57
  function printDiagnostics(result, force) {
58
58
  const errors = result.diagnostics.filter((d) => d.level === 'error');
59
59
  const warnings = result.diagnostics.filter((d) => d.level === 'warn');
60
- warnings.forEach((d) => logger_1.logger.warn(`[${d.code}] ${d.message}`));
60
+ warnings.forEach((d) => {
61
+ logger_1.logger.warn(`[${d.code}] ${d.message}`);
62
+ if (d.hint)
63
+ logger_1.logger.info(logger_1.color.gray(` ↳ ${d.hint}`));
64
+ });
61
65
  if (errors.length === 0) {
62
66
  logger_1.logger.success(`离线校验通过(${result.diagnostics.length - errors.length} 条提示)`);
63
67
  return;
64
68
  }
65
- errors.forEach((d) => logger_1.logger.error(`[${d.code}] ${d.message}`));
69
+ errors.forEach((d) => {
70
+ logger_1.logger.error(`[${d.code}] ${d.message}`);
71
+ if (d.hint)
72
+ logger_1.logger.info(logger_1.color.gray(` ↳ ${d.hint}`));
73
+ });
66
74
  if (!force) {
67
75
  throw new Error(`离线校验未通过(${errors.length} 项错误);修正 spec 后重试,或追加 --force 仅用于查看输出`);
68
76
  }
@@ -594,6 +602,12 @@ function registerFlowCommand(program) {
594
602
  printDiagnostics(result, Boolean(opts.force));
595
603
  }
596
604
  const isUpdate = Boolean(spec.fdId);
605
+ let current;
606
+ if (isUpdate) {
607
+ current = await flow_1.flowApi.get(client, String(spec.fdId));
608
+ if (!current?.fdId)
609
+ throw new Error(`动作流不存在:${spec.fdId}`);
610
+ }
597
611
  if (!isUpdate) {
598
612
  let unique;
599
613
  try {
@@ -606,7 +620,15 @@ function registerFlowCommand(program) {
606
620
  throw new Error(`流程编码 ${spec.code} 在应用 ${spec.appCode} 中已存在;更换 code,或更新已有流程时在 spec 中写 fdId`);
607
621
  }
608
622
  }
609
- const payload = buildPublishPayload(result);
623
+ // 更新时合并现有 VO,避免把 fdStatus / dynamicProps / fdCreator 等服务端字段覆盖掉
624
+ const payload = isUpdate
625
+ ? {
626
+ ...current,
627
+ ...result.basicInfo,
628
+ fdStatus: current?.fdStatus ?? result.basicInfo.fdStatus,
629
+ fdDesign: result.fdDesign,
630
+ }
631
+ : buildPublishPayload(result);
610
632
  if (!opts.yes) {
611
633
  logger_1.logger.warn(`预览模式:未写入。目标应用 ${spec.appCode};确认 payload 后追加 --yes`);
612
634
  logger_1.logger.json(payload);
@@ -291,9 +291,11 @@ function buildCreatePayload(opts, flow) {
291
291
  fdModelName: FLOW_MODEL_NAME,
292
292
  fdCode: flow.fdCode,
293
293
  };
294
- const incParams = buildIncParams(opts.incParams);
295
- if (incParams !== undefined)
296
- config.incParams = incParams;
294
+ const builtInc = buildIncParams(opts.incParams);
295
+ if (builtInc) {
296
+ config.incParams = builtInc.incParams;
297
+ config.formIncParams = builtInc.formIncParams;
298
+ }
297
299
  const active = flow.fdProcessActive ?? {};
298
300
  if (active.fdFormId) {
299
301
  // 动作流绑定了表单:按钮按当前文档 id/ids 传参
@@ -343,7 +345,17 @@ function assertTriggerableFlow(flow) {
343
345
  throw new Error(`动作流 ${flow.fdName ?? flow.fdId} 未开启「操作触发」,请在 trigger.fdRunType 中包含 operate 后重新发布`);
344
346
  }
345
347
  }
346
- /** --inc-params 支持 @file 或内联 JSON;返回平台要求的 [{field:{name},value:{val}}] */
348
+ /**
349
+ * 编译动作流入参。
350
+ *
351
+ * 平台落库结构(addOperationModal/content.tsx:533-576):
352
+ * fdConfig.incParams = [{ batchNo: 'B001' }, { repeat: 2 }]
353
+ * fdConfig.formIncParams = [{ field: { name: 'batchNo' } }, ...]
354
+ * 入参支持三种写法:
355
+ * 对象 { "batchNo": "B001", "repeat": 2 }
356
+ * 数组 [{ "batchNo": "B001" }]
357
+ * 数组 [{ "field": { "name": "batchNo" }, "value": { "val": "B001" } }]
358
+ */
347
359
  function buildIncParams(input) {
348
360
  if (input === undefined || input === null || input === '')
349
361
  return undefined;
@@ -355,9 +367,33 @@ function buildIncParams(input) {
355
367
  catch (err) {
356
368
  throw new Error(`--inc-params 不是合法 JSON:${err.message}`);
357
369
  }
358
- if (!Array.isArray(parsed))
359
- throw new Error('--inc-params 需为数组:[{field:{name},value:{val}}]');
360
- return parsed;
370
+ const entries = [];
371
+ const pushEntry = (name, value) => {
372
+ entries.push({ [name]: value });
373
+ };
374
+ if (Array.isArray(parsed)) {
375
+ for (const item of parsed) {
376
+ if (!item || typeof item !== 'object')
377
+ continue;
378
+ const record = item;
379
+ const field = record.field;
380
+ if (field && typeof field.name === 'string') {
381
+ const value = record.value;
382
+ pushEntry(field.name, value && 'val' in value ? value.val : record.value);
383
+ continue;
384
+ }
385
+ Object.entries(record).forEach(([name, value]) => pushEntry(name, value));
386
+ }
387
+ }
388
+ else if (parsed && typeof parsed === 'object') {
389
+ Object.entries(parsed).forEach(([name, value]) => pushEntry(name, value));
390
+ }
391
+ if (entries.length === 0)
392
+ return undefined;
393
+ return {
394
+ incParams: entries,
395
+ formIncParams: entries.map((entry) => ({ field: { name: Object.keys(entry)[0] } })),
396
+ };
361
397
  }
362
398
  /**
363
399
  * 基于现有操作构造 update 请求体。
@@ -440,9 +476,11 @@ function buildUpdatePayload(current, opts, flow) {
440
476
  fdModelName: FLOW_MODEL_NAME,
441
477
  fdCode: flow.fdCode,
442
478
  };
443
- const incParams = buildIncParams(opts.incParams);
444
- if (incParams !== undefined)
445
- config.incParams = incParams;
479
+ const builtInc = buildIncParams(opts.incParams);
480
+ if (builtInc) {
481
+ config.incParams = builtInc.incParams;
482
+ config.formIncParams = builtInc.formIncParams;
483
+ }
446
484
  }
447
485
  else {
448
486
  const dialogId = opts.dialogId !== undefined
@@ -20,6 +20,7 @@ exports.normalizeGraph = normalizeGraph;
20
20
  exports.compileFlow = compileFlow;
21
21
  const layout_1 = require("./layout");
22
22
  Object.defineProperty(exports, "NODE_SIZE", { enumerable: true, get: function () { return layout_1.NODE_SIZE; } });
23
+ const mapping_1 = require("./mapping");
23
24
  const schema_1 = require("./schema");
24
25
  const validate_1 = require("./validate");
25
26
  Object.defineProperty(exports, "hasErrors", { enumerable: true, get: function () { return validate_1.hasErrors; } });
@@ -104,6 +105,10 @@ function normalizeGraph(spec, mode, hints = {}) {
104
105
  if (business.length === 0 && edges.length === 0)
105
106
  addEdge({ from: startId, to: endId });
106
107
  const byId = new Map(nodes.map((n) => [n.id, n]));
108
+ // 在必填校验与节点实例化之前统一补齐参数映射(TransformNode 等)
109
+ for (const node of nodes) {
110
+ (0, mapping_1.prepareNodeConfig)(node, diagnostics, { edges, byId });
111
+ }
107
112
  const ranks = { [startId]: 0 };
108
113
  const branchTargets = new Set();
109
114
  for (const edge of edges) {
@@ -0,0 +1,258 @@
1
+ "use strict";
2
+ /**
3
+ * 参数映射编译器
4
+ *
5
+ * 平台里映射的持久化结构只有两种(`configuration/controls/paramMapping/index.tsx:100-135`):
6
+ * point: { paramMappingWay:'point', pointMappings:[{ sourceType, sourcePath, targetPath }] }
7
+ * formula: { paramMappingWay:'formula', valueFormula:'<JSON 字符串>' }
8
+ *
9
+ * spec 里允许更友好的写法(编译后会被删除,不落库):
10
+ * "config": {
11
+ * "outParam": { "fd_title": { "type": "string", "description": "标题" } },
12
+ * "mapping": { "fd_title": "$N2.fd_title", "fd_amount": "$N2.fd_amount" }
13
+ * }
14
+ *
15
+ * - `outParam` 也接受完整 JSON Schema(含 type/properties)或 JSON 字符串;
16
+ * - `mapping` 的值支持 `$<节点id>.<路径>` 与 `$context.<路径>`;
17
+ * - 也接受 `{ paramMappingWay:'formula', valueFormula:'...' }` 或 pointMappings 数组直接透传。
18
+ */
19
+ Object.defineProperty(exports, "__esModule", { value: true });
20
+ exports.prepareNodeConfig = prepareNodeConfig;
21
+ const JSON_TYPES = new Set(['string', 'number', 'integer', 'boolean', 'array', 'object']);
22
+ const isRecord = (value) => Boolean(value) && typeof value === 'object' && !Array.isArray(value);
23
+ const NODE_REF = /^\$([0-9a-zA-Z_]{2,32})\.(.+)$/;
24
+ const CONTEXT_REF = /^\$context\.(.+)$/;
25
+ function targetPathOf(key) {
26
+ const index = key.indexOf('.');
27
+ return index >= 0 ? key.slice(index + 1) : key;
28
+ }
29
+ /** 字段映射 → JSON Schema(`{id:'responseBody', type:'object', properties:{...}}`) */
30
+ function buildSchemaFromFields(fields) {
31
+ const properties = {};
32
+ for (const [name, raw] of Object.entries(fields)) {
33
+ if (isRecord(raw)) {
34
+ properties[name] = { id: name, required: false, ...raw };
35
+ continue;
36
+ }
37
+ if (typeof raw === 'string' && JSON_TYPES.has(raw)) {
38
+ properties[name] = { id: name, type: raw, required: false, description: '' };
39
+ continue;
40
+ }
41
+ properties[name] = {
42
+ id: name,
43
+ type: 'string',
44
+ required: false,
45
+ ...(typeof raw === 'string' && raw ? { description: raw } : {}),
46
+ };
47
+ }
48
+ return { id: 'responseBody', type: 'object', description: '', properties };
49
+ }
50
+ /** 统一 outParam:字符串原样保留,对象转成 JSON Schema 字符串 */
51
+ function normalizeOutParam(value) {
52
+ if (typeof value === 'string') {
53
+ const trimmed = value.trim();
54
+ if (!trimmed)
55
+ return {};
56
+ try {
57
+ const parsed = JSON.parse(trimmed);
58
+ return isRecord(parsed) ? { text: trimmed, schema: parsed } : { text: value };
59
+ }
60
+ catch {
61
+ return { text: value };
62
+ }
63
+ }
64
+ if (isRecord(value)) {
65
+ const isSchema = 'type' in value || 'properties' in value || '$ref' in value;
66
+ const schema = isSchema
67
+ ? { id: 'responseBody', type: 'object', description: '', ...value }
68
+ : buildSchemaFromFields(value);
69
+ return { text: JSON.stringify(schema), schema };
70
+ }
71
+ return {};
72
+ }
73
+ function parseMappingSource(expr) {
74
+ if (typeof expr !== 'string')
75
+ return undefined;
76
+ const value = expr.trim();
77
+ const contextMatch = CONTEXT_REF.exec(value);
78
+ if (contextMatch) {
79
+ return { sourceType: 'context', sourcePath: `context.${contextMatch[1]}` };
80
+ }
81
+ const nodeMatch = NODE_REF.exec(value);
82
+ if (nodeMatch) {
83
+ return { sourceType: 'node', sourcePath: `node.${nodeMatch[1]}.${nodeMatch[2]}` };
84
+ }
85
+ return undefined;
86
+ }
87
+ /** `{ 目标字段: 表达式 }` → pointMappings */
88
+ function buildPointMapping(mapping, diagnostics, nodeId, field) {
89
+ const pointMappings = [];
90
+ for (const [target, expr] of Object.entries(mapping)) {
91
+ if (target === 'paramMappingWay' || target === 'pointMappings' || target === 'valueFormula')
92
+ continue;
93
+ const source = parseMappingSource(expr);
94
+ if (!source) {
95
+ diagnostics.push({
96
+ level: 'warn',
97
+ code: 'MAPPING_VALUE_UNSUPPORTED',
98
+ message: `节点 ${nodeId} 的映射 ${target} 不是 $节点.路径 / $context.路径 形式,已跳过`,
99
+ nodeId,
100
+ field,
101
+ hint: '常量或表达式请改用 { "paramMappingWay": "formula", "valueFormula": "..." }',
102
+ });
103
+ continue;
104
+ }
105
+ pointMappings.push({ sourceType: source.sourceType, sourcePath: source.sourcePath, targetPath: targetPathOf(target) });
106
+ }
107
+ if (pointMappings.length === 0)
108
+ return undefined;
109
+ return { paramMappingWay: 'point', pointMappings };
110
+ }
111
+ /** 把 spec 的 mapping 统一成平台结构 */
112
+ function buildMapping(input, diagnostics, nodeId, field) {
113
+ if (typeof input === 'string') {
114
+ const value = input.trim();
115
+ if (!value)
116
+ return undefined;
117
+ if (value.startsWith('{'))
118
+ return { paramMappingWay: 'formula', valueFormula: value };
119
+ diagnostics.push({
120
+ level: 'error', code: 'MAPPING_FORMULA_INVALID',
121
+ message: `节点 ${nodeId} 的 ${field} 字符串必须是 valueFormula 的 JSON`, nodeId, field,
122
+ });
123
+ return undefined;
124
+ }
125
+ if (Array.isArray(input)) {
126
+ return input.length > 0 ? { paramMappingWay: 'point', pointMappings: input } : undefined;
127
+ }
128
+ if (!isRecord(input))
129
+ return undefined;
130
+ if (input.paramMappingWay === 'formula') {
131
+ if (typeof input.valueFormula === 'string' && input.valueFormula.trim()) {
132
+ return { paramMappingWay: 'formula', valueFormula: input.valueFormula };
133
+ }
134
+ if (input.valueFormula !== undefined) {
135
+ return { paramMappingWay: 'formula', valueFormula: JSON.stringify(input.valueFormula) };
136
+ }
137
+ diagnostics.push({
138
+ level: 'error', code: 'MAPPING_FORMULA_REQUIRED',
139
+ message: `节点 ${nodeId} 的 ${field} 声明 paramMappingWay=formula 但缺少 valueFormula`, nodeId, field,
140
+ });
141
+ return undefined;
142
+ }
143
+ return buildPointMapping(input, diagnostics, nodeId, field);
144
+ }
145
+ /** 平台要求 valueFormula 是字符串;对象写法统一序列化 */
146
+ function normalizeMappingValue(value) {
147
+ if (!isRecord(value))
148
+ return value;
149
+ if (value.valueFormula !== undefined && typeof value.valueFormula !== 'string') {
150
+ return { ...value, valueFormula: JSON.stringify(value.valueFormula) };
151
+ }
152
+ return value;
153
+ }
154
+ /** 上游已有 `context_outputParam` 时,按同名字段自动补齐 point 映射 */
155
+ function inferFromUpstream(node, outputNames, graph) {
156
+ for (const edge of graph.edges.filter((item) => item.to === node.id)) {
157
+ const upstream = graph.byId.get(edge.from);
158
+ const upstreamContext = upstream?.context;
159
+ const schema = upstreamContext?.context_outputParam;
160
+ if (!isRecord(schema))
161
+ continue;
162
+ const properties = isRecord(schema.properties) ? schema.properties : undefined;
163
+ if (!properties)
164
+ continue;
165
+ const pointMappings = outputNames
166
+ .filter((name) => name in properties)
167
+ .map((name) => ({
168
+ sourceType: 'node',
169
+ sourcePath: `node.${edge.from}.${name}`,
170
+ targetPath: name,
171
+ }));
172
+ if (pointMappings.length > 0)
173
+ return { pointMappings, upstreamId: edge.from };
174
+ }
175
+ return undefined;
176
+ }
177
+ /**
178
+ * 在归一化阶段就地补齐节点配置(校验与实例化都会读取同一份 config)。
179
+ * 只处理参数映射相关的 nodeType,其余节点原样返回。
180
+ */
181
+ function prepareNodeConfig(node, diagnostics, graph) {
182
+ const config = { ...(node.config ?? {}) };
183
+ node.config = config;
184
+ if (node.type === 'TransformNode') {
185
+ const normalized = normalizeOutParam(config.outParam);
186
+ if (normalized.text) {
187
+ config.outParam = normalized.text;
188
+ if (config.outParamType === undefined)
189
+ config.outParamType = 1;
190
+ if (normalized.schema) {
191
+ // IDE 的 onParamChange 会把出参 Schema 写入 context_outputParam(transformNode.tsx:64-67)
192
+ node.context = { ...(node.context ?? {}), context_outputParam: normalized.schema };
193
+ }
194
+ }
195
+ else if (config.outParam !== undefined) {
196
+ diagnostics.push({
197
+ level: 'error', code: 'OUT_PARAM_INVALID',
198
+ message: `节点 ${node.id} 的 config.outParam 需为 JSON Schema 字符串或字段映射对象`,
199
+ nodeId: node.id, field: 'outParam',
200
+ });
201
+ }
202
+ if (config.outputParamMapping !== undefined) {
203
+ config.outputParamMapping = normalizeMappingValue(config.outputParamMapping);
204
+ }
205
+ else if (config.mapping !== undefined) {
206
+ const generated = buildMapping(config.mapping, diagnostics, node.id, 'mapping');
207
+ if (generated)
208
+ config.outputParamMapping = generated;
209
+ }
210
+ if (config.outputParamMapping === undefined && normalized.schema && isRecord(normalized.schema.properties) && graph) {
211
+ const names = Object.keys(normalized.schema.properties);
212
+ const inferred = inferFromUpstream(node, names, graph);
213
+ if (inferred) {
214
+ config.outputParamMapping = { paramMappingWay: 'point', pointMappings: inferred.pointMappings };
215
+ diagnostics.push({
216
+ level: 'warn', code: 'MAPPING_AUTO_INFERRED',
217
+ message: `节点 ${node.id} 未提供 mapping,已按上游 ${inferred.upstreamId} 的同名字段自动生成 ${inferred.pointMappings.length} 条映射`,
218
+ nodeId: node.id, field: 'outputParamMapping',
219
+ });
220
+ }
221
+ }
222
+ delete config.mapping;
223
+ return;
224
+ }
225
+ if (node.type === 'EndNode') {
226
+ const normalized = normalizeOutParam(config.outParam);
227
+ if (normalized.text) {
228
+ config.outParam = normalized.text;
229
+ // 主动触发的结束节点 outParamType=0(endNode.tsx:46)
230
+ if (config.outParamType === undefined)
231
+ config.outParamType = 0;
232
+ if (normalized.schema) {
233
+ node.context = { ...(node.context ?? {}), context_outputParam: normalized.schema };
234
+ }
235
+ }
236
+ if (config.outputParamMapping !== undefined) {
237
+ config.outputParamMapping = normalizeMappingValue(config.outputParamMapping);
238
+ }
239
+ else if (config.mapping !== undefined) {
240
+ const generated = buildMapping(config.mapping, diagnostics, node.id, 'mapping');
241
+ if (generated)
242
+ config.outputParamMapping = generated;
243
+ }
244
+ delete config.mapping;
245
+ return;
246
+ }
247
+ if (['ExecuteNode', 'InterfaceNode', 'SubActionNode', 'FlowBotsNode'].includes(node.type)) {
248
+ if (config.inputParamMapping !== undefined) {
249
+ config.inputParamMapping = normalizeMappingValue(config.inputParamMapping);
250
+ }
251
+ else if (config.mapping !== undefined) {
252
+ const generated = buildMapping(config.mapping, diagnostics, node.id, 'mapping');
253
+ if (generated)
254
+ config.inputParamMapping = generated;
255
+ }
256
+ delete config.mapping;
257
+ }
258
+ }
@@ -7,7 +7,7 @@
7
7
  * 必填规则来自 `configuration/nodes/utils.tsx:37-288` 与 `validate.ts:13-17`。
8
8
  */
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
- exports.CONTEXT_KEYS = exports.FLOW_CODE_PATTERN = exports.NODE_ID_PATTERN = exports.KNOWN_START_WAYS = exports.MULTI_OUT_NODE_TYPES = exports.NODE_REQUIRED = exports.NODE_DEFAULTS = exports.NODE_FIELD_WHITELIST = void 0;
10
+ exports.CONTEXT_KEYS = exports.FLOW_CODE_PATTERN = exports.NODE_ID_PATTERN = exports.KNOWN_START_WAYS = exports.MULTI_OUT_NODE_TYPES = exports.NODE_REQUIRED_RULES = exports.NODE_DEFAULTS = exports.NODE_FIELD_WHITELIST = void 0;
11
11
  /** 节点专属字段白名单;未列出的 nodeType 视为未知类型(透传 config 并告警) */
12
12
  exports.NODE_FIELD_WHITELIST = {
13
13
  StartNode: ['startNodeDef'],
@@ -53,25 +53,166 @@ exports.NODE_DEFAULTS = {
53
53
  LoopNode: { loopDefine: { type: 'LoopArray' } },
54
54
  MessageNode: { grantPermission: true },
55
55
  };
56
- /** 发布前 IDE 强校验的必填字段(configuration/nodes/utils.tsx:37-288) */
57
- exports.NODE_REQUIRED = {
58
- AddDataNode: ['dataSourceVO', 'flow', 'fields'],
59
- QueryDataNode: ['target'],
60
- UpdateDataNode: ['bindNode'],
61
- DeleteDataNode: ['bindNode'],
62
- FilterDataNode: ['bindNode'],
63
- BranchNode: ['conditionType', 'flowWay'],
64
- SubActionNode: ['actionFlowCode'],
65
- InterfaceNode: ['apiType', 'apiMethodPath'],
66
- ExecuteNode: ['executeCode'],
67
- QueryStatisticsNode: ['target'],
68
- AddOrUpdateNode: ['dataSourceVO', 'flow', 'fields'],
69
- LoopNode: ['loopDefine'],
70
- AssignValueNode: ['assignValueRuleConfigs'],
71
- CreateKmReviewNode: ['kmReviewTemplate', 'fdSubject'],
72
- FlowBotsNode: ['flowBotsCode'],
73
- MessageNode: ['subject', 'messageTarget'],
74
- MessageEliminateNode: ['eliminateRange', 'eliminateType'],
56
+ const record = (value) => value && typeof value === 'object' && !Array.isArray(value)
57
+ ? value
58
+ : {};
59
+ /** 节点是否有入参 Schema(决定参数映射是否必填) */
60
+ const hasInParam = (ctx) => {
61
+ const inParam = record(record(ctx.node.context).context_fdInParam);
62
+ return Object.keys(record(inParam.properties)).length > 0;
63
+ };
64
+ /** Execute/Interface/SubAction/FlowBots 共用的「code + 参数映射」规则 */
65
+ const mappingRules = (codeField, codeMessage) => [
66
+ { field: codeField, message: codeMessage },
67
+ {
68
+ field: 'paramMappingWay',
69
+ message: '参数映射不能为空',
70
+ read: (config) => record(config.inputParamMapping).paramMappingWay ?? 'point',
71
+ skipIf: (ctx) => !hasInParam(ctx),
72
+ },
73
+ {
74
+ field: 'fdParam',
75
+ message: '参数映射不能为空',
76
+ hint: 'inputParamMapping 需提供 valueFormula(非 point)或非空 pointMappings',
77
+ read: (config) => {
78
+ const mapping = record(config.inputParamMapping);
79
+ if (Object.keys(mapping).length === 0)
80
+ return undefined;
81
+ if ((mapping.paramMappingWay ?? 'point') !== 'point')
82
+ return mapping.valueFormula;
83
+ const points = mapping.pointMappings;
84
+ return Array.isArray(points) && points.length > 0 ? points : undefined;
85
+ },
86
+ skipIf: (ctx) => !hasInParam(ctx),
87
+ },
88
+ ];
89
+ /** 分类表单 / 未选提交方式时,AddData 不要求选流程 */
90
+ const skipFlowRequired = (ctx) => {
91
+ if (!ctx.config.commitType)
92
+ return true;
93
+ return String(record(ctx.config.dataSourceVO).entityId ?? '').includes('sys-modeling-main');
94
+ };
95
+ exports.NODE_REQUIRED_RULES = {
96
+ AddDataNode: [
97
+ { field: 'dataSourceVO', message: '目标表单不能为空' },
98
+ { field: 'flow', message: '选择流程不能为空', skipIf: skipFlowRequired },
99
+ { field: 'fields', message: '表单字段不能为空' },
100
+ ],
101
+ AddOrUpdateNode: [
102
+ { field: 'dataSourceVO', message: '选择表单不能为空' },
103
+ { field: 'flow', message: '选择流程不能为空', skipIf: skipFlowRequired },
104
+ ],
105
+ QueryDataNode: [{ field: 'target', message: '查询对象不能为空' }],
106
+ QueryLoopNode: [{ field: 'target', message: '查询对象不能为空' }],
107
+ UpdateDataNode: [{ field: 'bindNode', message: '请选择节点' }],
108
+ DeleteDataNode: [{ field: 'bindNode', message: '请选择删除节点' }],
109
+ FilterDataNode: [{ field: 'bindNode', message: '过滤节点不能为空' }],
110
+ BranchNode: [
111
+ { field: 'conditionType', message: '请设置条件' },
112
+ { field: 'flowWay', message: '请设置流转方式' },
113
+ {
114
+ field: 'formulaConfig',
115
+ message: '请设置公式定义',
116
+ // rule 模式免公式;有出边时编译器会按 branchFormulas 自动生成
117
+ skipIf: (ctx) => ctx.config.conditionType === 'rule' || ctx.outgoing > 0,
118
+ },
119
+ { field: 'ruleConfig', message: '请设置匹配规则', skipIf: (ctx) => ctx.config.conditionType === 'formula' },
120
+ ],
121
+ LoopNode: [
122
+ { field: 'loopType', message: '请设置循环方式', read: (config) => record(config.loopDefine).type },
123
+ ],
124
+ TransformNode: [
125
+ {
126
+ field: 'outParam',
127
+ message: '出参设置不能为空',
128
+ hint: 'config.outParam 需为输出参数 JSON Schema 字符串;可用 llc flow templates 从真实流程抽取',
129
+ },
130
+ {
131
+ field: 'paramMappingWay',
132
+ message: '参数映射不能为空',
133
+ read: (config) => record(config.outputParamMapping).paramMappingWay ?? 'point',
134
+ skipIf: (ctx) => !ctx.config.outParam,
135
+ },
136
+ {
137
+ field: 'fdParam',
138
+ message: '参数映射不能为空',
139
+ hint: 'outputParamMapping 需提供 valueFormula(非 point)或非空 pointMappings',
140
+ read: (config) => {
141
+ const mapping = record(config.outputParamMapping);
142
+ if (Object.keys(mapping).length === 0)
143
+ return undefined;
144
+ if ((mapping.paramMappingWay ?? 'point') !== 'point')
145
+ return mapping.valueFormula;
146
+ const points = mapping.pointMappings;
147
+ return Array.isArray(points) && points.length > 0 ? points : undefined;
148
+ },
149
+ skipIf: (ctx) => !ctx.config.outParam,
150
+ },
151
+ ],
152
+ AssignValueNode: [{ field: 'assignValueRuleConfigs', message: '赋值规则不能为空' }],
153
+ ExecuteNode: mappingRules('executeCode', '集成函数不能为空'),
154
+ InterfaceNode: mappingRules('apiMethodPath', '内部接口不能为空'),
155
+ SubActionNode: mappingRules('actionFlowCode', '子动作不能为空'),
156
+ FlowBotsNode: mappingRules('flowBotsCode', 'flowBots不能为空'),
157
+ MessageNode: [
158
+ { field: 'notifyType', message: '通知方式不能为空' },
159
+ { field: 'subject', message: '消息内容不能为空', skipIf: (ctx) => ctx.config.subjectType === 'TEMPLATE' },
160
+ {
161
+ field: 'templateCode',
162
+ message: '请选择通知模板',
163
+ skipIf: (ctx) => {
164
+ const subjectType = ctx.config.subjectType;
165
+ return subjectType === undefined || subjectType === '' || subjectType === 'CUSTOM';
166
+ },
167
+ },
168
+ {
169
+ field: 'linkPage',
170
+ message: '视图不能为空',
171
+ skipIf: (ctx) => {
172
+ const linkType = record(ctx.config.messageLink).linkType;
173
+ return linkType === undefined || linkType === 'default';
174
+ },
175
+ },
176
+ {
177
+ field: 'linkUrl',
178
+ message: '链接不能为空',
179
+ read: (config) => record(config.messageLink).linkUrl,
180
+ skipIf: (ctx) => {
181
+ const linkType = record(ctx.config.messageLink).linkType;
182
+ return linkType === undefined || linkType === 'view' || linkType === 'edit';
183
+ },
184
+ },
185
+ ],
186
+ MessageEliminateNode: [{ field: 'eventCode', message: '事件编码不允许为空' }],
187
+ QueryStatisticsNode: [
188
+ { field: 'target', message: '查询目标不能为空' },
189
+ {
190
+ field: 'statisticsConfig',
191
+ message: '统计维度和统计项不能全为空',
192
+ read: (config) => {
193
+ const dimensions = Array.isArray(config.statisticsDimensions) ? config.statisticsDimensions : [];
194
+ const items = Array.isArray(config.statisticsItems) ? config.statisticsItems : [];
195
+ return dimensions.length + items.length > 0 ? { dimensions, items } : undefined;
196
+ },
197
+ },
198
+ ],
199
+ CreateKmReviewNode: [
200
+ { field: 'kmReviewTemplate', message: '流程模板不能为空' },
201
+ { field: 'fdSubject', message: '流程标题不能为空' },
202
+ {
203
+ field: 'orgElement',
204
+ message: '创建人不能为空',
205
+ read: (config) => record(config.fdCreator).orgElement,
206
+ skipIf: (ctx) => (record(ctx.config.fdCreator).valueRule ?? 'Fixed') === 'Var',
207
+ },
208
+ {
209
+ field: 'formula',
210
+ message: '创建人不能为空',
211
+ read: (config) => record(config.fdCreator).formula,
212
+ skipIf: (ctx) => (record(ctx.config.fdCreator).valueRule ?? 'Fixed') === 'Fixed',
213
+ },
214
+ { field: 'fields', message: '表单字段不能为空' },
215
+ ],
75
216
  };
76
217
  /** 允许多条出边的节点(validateCommonVertex 的 getwayMap,另有 StartNode) */
77
218
  exports.MULTI_OUT_NODE_TYPES = new Set(['BranchNode', 'parallelGateway', 'StartNode']);
@@ -17,6 +17,16 @@ const err = (code, message, nodeId) => ({
17
17
  const warn = (code, message, nodeId) => ({
18
18
  level: 'warn', code, message, nodeId,
19
19
  });
20
+ /** 与平台 validateRequired 一致:空串、空数组、null/undefined 视为未填 */
21
+ function isBlank(value) {
22
+ if (value === undefined || value === null)
23
+ return true;
24
+ if (typeof value === 'string')
25
+ return value.trim() === '';
26
+ if (Array.isArray(value))
27
+ return value.length === 0;
28
+ return false;
29
+ }
20
30
  /** 基础信息校验 */
21
31
  function validateSpec(spec) {
22
32
  const out = [];
@@ -80,9 +90,21 @@ function validateGraph(graph, spec, startWay = 'StartActiveNode') {
80
90
  }
81
91
  // 与编译器一致:默认值(如 BranchNode.conditionType / flowWay)也算已填
82
92
  const config = { ...(schema_1.NODE_DEFAULTS[node.type] ?? {}), ...(node.config ?? {}) };
83
- for (const field of schema_1.NODE_REQUIRED[node.type] ?? []) {
84
- if (config[field] === undefined || config[field] === null || config[field] === '') {
85
- out.push(err('NODE_FIELD_REQUIRED', `节点 ${node.id}(${node.type})缺少必填字段 ${field}`, node.id));
93
+ const outgoing = graph.edges.filter((edge) => edge.from === node.id).length;
94
+ const ruleContext = { config, node: { context: node.context }, outgoing };
95
+ for (const rule of schema_1.NODE_REQUIRED_RULES[node.type] ?? []) {
96
+ if (rule.skipIf?.(ruleContext))
97
+ continue;
98
+ const value = rule.read ? rule.read(config) : config[rule.field];
99
+ if (isBlank(value)) {
100
+ out.push({
101
+ level: 'error',
102
+ code: 'NODE_FIELD_REQUIRED',
103
+ message: `节点 ${node.id}(${node.type})缺少必填字段 ${rule.field}:${rule.message}`,
104
+ nodeId: node.id,
105
+ field: rule.field,
106
+ ...(rule.hint ? { hint: rule.hint } : {}),
107
+ });
86
108
  }
87
109
  }
88
110
  }
package/dist/index.js CHANGED
@@ -12,7 +12,7 @@ const locale_1 = require("./commands/locale");
12
12
  const operation_1 = require("./commands/operation");
13
13
  const template_1 = require("./commands/template");
14
14
  const logger_1 = require("./utils/logger");
15
- const VERSION = '2.8.0';
15
+ const VERSION = '2.9.0';
16
16
  const program = new commander_1.Command();
17
17
  exports.program = program;
18
18
  program
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lan-low-code-cli",
3
- "version": "2.8.0",
3
+ "version": "2.9.0",
4
4
  "description": "lan-low-code cli:低代码表单引擎命令行工具(创建表单、获取可用字段、自动完善表单字段)",
5
5
  "keywords": [
6
6
  "low-code",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: lan-low-code-cli
3
- version: 2.8.0
3
+ version: 2.9.0
4
4
  author: Pig George
5
5
  description: Operate the Landray low-code form engine through the `llc` CLI. Use when the user asks to create, inspect, modify, publish, or delete low-code apps and forms; generate field configs; configure layouts (grid/tabs), field permissions, options, i18n, or control actions (onChange/onLoad); or run pre-publish validation. Also use for troubleshooting form loading/rendering errors.
6
6
  tags:
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: lan-low-code-cli
3
- version: 2.8.0
3
+ version: 2.9.0
4
4
  author: Pig George
5
5
  description: 通过 `llc` CLI 操作蓝凌低代码表单引擎。适用于:创建、查看、修改、发布或删除低代码应用与表单;生成字段配置;配置布局(栅格/页签)、字段权限、选项、多语言、控件动作(onChange/onLoad);或执行发布前校验、排查表单加载/渲染报错。
6
6
  tags:
@@ -285,6 +285,25 @@ Application-scoped action flows. Every write command needs `--yes` and an explic
285
285
  - `fields` of `AddDataNode` / `AddOrUpdateNode` uses the platform's real shape
286
286
  (`{field:{...},value:{format,valueJson}}`); the CLI passes it through and only validates field
287
287
  names — it does **not** synthesise formulas. Use `llc flow templates` / `flow get` for golden shapes.
288
+ - **Per-node required fields** mirror the IDE rules table (`configuration/nodes/utils.tsx`) plus its
289
+ exemptions (`validate.ts`):
290
+ `TransformNode` requires `outParam` **and** `outputParamMapping` (`valueFormula`, or non-empty
291
+ `pointMappings`; mapping is exempt only when `outParam` is empty);
292
+ `ExecuteNode`/`InterfaceNode`/`SubActionNode`/`FlowBotsNode` require their code field
293
+ (`executeCode`/`apiMethodPath`/`actionFlowCode`/`flowBotsCode`) and a mapping when an inbound
294
+ Schema exists; `BranchNode` requires `conditionType`/`flowWay` plus the matching
295
+ `formulaConfig`/`ruleConfig` (auto-generated when branch edges exist); `LoopNode` requires
296
+ `loopDefine.type`; `MessageNode` validates `notifyType`/`subject`/`templateCode`/`linkPage`/`linkUrl`
297
+ by `subjectType` and `messageLink.linkType`; `AddDataNode`/`AddOrUpdateNode` skip `flow` for
298
+ category forms or when `commitType` is absent. Diagnostics carry `field` and `hint`; errors exit `1`.
299
+ - **Parameter mapping shorthand**: `TransformNode` accepts `config.outParam` as a field map
300
+ (`{"fd_title":{"type":"string","description":"标题"}}`) or a full JSON Schema / Schema string, plus
301
+ `config.mapping` as `{"targetPath":"$<nodeId>.<path>"}` (also `$context.<path>`). The compiler emits
302
+ `outParamType=1`, `outParam` (Schema string), `outputParamMapping` (`pointMappings`) and copies the
303
+ Schema into `extraAttr.context_outputParam`. When `mapping` is absent it is inferred from an upstream
304
+ `context.context_outputParam` by matching field names (`MAPPING_AUTO_INFERRED` warning). Constants or
305
+ expressions require `{"paramMappingWay":"formula","valueFormula":"..."}`. `ExecuteNode` /
306
+ `InterfaceNode` / `SubActionNode` / `FlowBotsNode` reuse `config.mapping` for `inputParamMapping`.
288
307
  - Publishing derives `fdProcessActive` on the backend from `startNodeDef`; `fdStatus=1` means enabled.
289
308
  `flow logs` endpoints vary by deployment (`ModelingProcessLog/findAll` vs
290
309
  `sysModelingLog/findAllWithAppId`); use `--endpoint` if the defaults fail.
@@ -272,6 +272,24 @@ llc operation detach --form-id <fdId> --operation-id <operationId> #
272
272
  - `AddDataNode` / `AddOrUpdateNode` 的 `fields` 使用平台真实结构
273
273
  (`{field:{...},value:{format,valueJson}}`);CLI 原样透传并只校验字段名,**不臆造公式**。
274
274
  需要真实结构请用 `llc flow templates` / `flow get` 抽取 golden 骨架。
275
+ - **节点级必填校验**复刻 IDE 规则表(`configuration/nodes/utils.tsx`)及其豁免逻辑(`validate.ts`):
276
+ `TransformNode` 必填 `outParam` **和** `outputParamMapping`(`valueFormula` 或非空
277
+ `pointMappings`;仅当 `outParam` 为空时豁免映射);`ExecuteNode` / `InterfaceNode` /
278
+ `SubActionNode` / `FlowBotsNode` 必填各自的编码字段(`executeCode` / `apiMethodPath` /
279
+ `actionFlowCode` / `flowBotsCode`),且存在入参 Schema 时必填参数映射;`BranchNode` 必填
280
+ `conditionType` / `flowWay` 及对应的 `formulaConfig` / `ruleConfig`(有分支出边时自动生成);
281
+ `LoopNode` 必填 `loopDefine.type`;`MessageNode` 按 `subjectType` 与 `messageLink.linkType`
282
+ 校验 `notifyType` / `subject` / `templateCode` / `linkPage` / `linkUrl`;
283
+ `AddDataNode` / `AddOrUpdateNode` 在分类表单或未填 `commitType` 时豁免 `flow`。
284
+ 诊断会带 `field` 与 `hint`,有错误时退出码为 `1`。
285
+ - **参数映射简写**:`TransformNode` 的 `config.outParam` 可写字段映射对象
286
+ (`{"fd_title":{"type":"string","description":"标题"}}`),也可写完整 JSON Schema 或 Schema 字符串;
287
+ `config.mapping` 支持 `{"目标字段":"$<节点id>.<路径>"}`(也支持 `$context.<路径>`)。编译器会输出
288
+ `outParamType=1`、`outParam`(Schema 字符串)、`outputParamMapping`(`pointMappings`),并把 Schema
289
+ 写入 `extraAttr.context_outputParam`。未写 `mapping` 时,会按上游 `context.context_outputParam`
290
+ 的同名字段自动推断,并给出 `MAPPING_AUTO_INFERRED` 提示。常量或表达式需改用
291
+ `{"paramMappingWay":"formula","valueFormula":"..."}`。`ExecuteNode` / `InterfaceNode` /
292
+ `SubActionNode` / `FlowBotsNode` 复用 `config.mapping` 生成 `inputParamMapping`。
275
293
  - 发布时后端从 `startNodeDef` 派生 `fdProcessActive`;`fdStatus=1` 表示已启用。
276
294
  `flow logs` 端点因部署而异(`ModelingProcessLog/findAll` 与
277
295
  `sysModelingLog/findAllWithAppId`);默认都失败时用 `--endpoint` 指定。