digitalsee-ai-flow-cli 0.1.6 → 0.1.8

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.
Files changed (91) hide show
  1. package/.github/workflows/ci.yml +24 -0
  2. package/.github/workflows/publish.yml +21 -0
  3. package/CHANGELOG.md +72 -0
  4. package/dist/api/client.d.ts.map +1 -1
  5. package/dist/api/client.js +4 -4
  6. package/dist/api/client.js.map +1 -1
  7. package/dist/api/flow.d.ts.map +1 -1
  8. package/dist/api/flow.js +16 -13
  9. package/dist/api/flow.js.map +1 -1
  10. package/dist/api/knowledge.d.ts.map +1 -1
  11. package/dist/api/knowledge.js +3 -3
  12. package/dist/api/knowledge.js.map +1 -1
  13. package/dist/api/node.d.ts.map +1 -1
  14. package/dist/api/node.js +4 -4
  15. package/dist/api/node.js.map +1 -1
  16. package/dist/api/variables.d.ts.map +1 -1
  17. package/dist/api/variables.js +1 -1
  18. package/dist/api/variables.js.map +1 -1
  19. package/dist/commands/auth.d.ts.map +1 -1
  20. package/dist/commands/auth.js +44 -34
  21. package/dist/commands/auth.js.map +1 -1
  22. package/dist/commands/config.d.ts.map +1 -1
  23. package/dist/commands/config.js +24 -26
  24. package/dist/commands/config.js.map +1 -1
  25. package/dist/commands/flow/account.d.ts.map +1 -1
  26. package/dist/commands/flow/account.js +17 -16
  27. package/dist/commands/flow/account.js.map +1 -1
  28. package/dist/commands/flow/analyze.d.ts.map +1 -1
  29. package/dist/commands/flow/analyze.js +42 -42
  30. package/dist/commands/flow/analyze.js.map +1 -1
  31. package/dist/commands/flow/create.d.ts.map +1 -1
  32. package/dist/commands/flow/create.js +7 -6
  33. package/dist/commands/flow/create.js.map +1 -1
  34. package/dist/commands/flow/edge.d.ts.map +1 -1
  35. package/dist/commands/flow/edge.js +36 -35
  36. package/dist/commands/flow/edge.js.map +1 -1
  37. package/dist/commands/flow/helpers.d.ts.map +1 -1
  38. package/dist/commands/flow/helpers.js +84 -74
  39. package/dist/commands/flow/helpers.js.map +1 -1
  40. package/dist/commands/flow/list.d.ts.map +1 -1
  41. package/dist/commands/flow/list.js +7 -7
  42. package/dist/commands/flow/list.js.map +1 -1
  43. package/dist/commands/flow/node.d.ts.map +1 -1
  44. package/dist/commands/flow/node.js +164 -134
  45. package/dist/commands/flow/node.js.map +1 -1
  46. package/dist/commands/flow/test.d.ts.map +1 -1
  47. package/dist/commands/flow/test.js +20 -12
  48. package/dist/commands/flow/test.js.map +1 -1
  49. package/dist/commands/flow/update.js +5 -5
  50. package/dist/commands/flow/update.js.map +1 -1
  51. package/dist/commands/flow/variables.d.ts.map +1 -1
  52. package/dist/commands/flow/variables.js +45 -43
  53. package/dist/commands/flow/variables.js.map +1 -1
  54. package/dist/commands/flow.d.ts.map +1 -1
  55. package/dist/commands/flow.js +1 -3
  56. package/dist/commands/flow.js.map +1 -1
  57. package/dist/commands/knowledge.d.ts.map +1 -1
  58. package/dist/commands/knowledge.js +68 -70
  59. package/dist/commands/knowledge.js.map +1 -1
  60. package/dist/index.js +31 -20
  61. package/dist/index.js.map +1 -1
  62. package/dist/services/flowAnalyzer.d.ts.map +1 -1
  63. package/dist/services/flowAnalyzer.js +256 -203
  64. package/dist/services/flowAnalyzer.js.map +1 -1
  65. package/dist/services/skillGenerator.d.ts.map +1 -1
  66. package/dist/services/skillGenerator.js +31 -31
  67. package/dist/services/skillGenerator.js.map +1 -1
  68. package/dist/types/index.d.ts.map +1 -1
  69. package/dist/utils/config.d.ts.map +1 -1
  70. package/dist/utils/config.js +12 -10
  71. package/dist/utils/config.js.map +1 -1
  72. package/dist/utils/encoding.d.ts.map +1 -1
  73. package/dist/utils/encoding.js +3 -3
  74. package/dist/utils/encoding.js.map +1 -1
  75. package/dist/utils/format.d.ts.map +1 -1
  76. package/dist/utils/format.js +471 -126
  77. package/dist/utils/format.js.map +1 -1
  78. package/dist/utils/oauth.d.ts.map +1 -1
  79. package/dist/utils/oauth.js +54 -78
  80. package/dist/utils/oauth.js.map +1 -1
  81. package/dist/utils/output.d.ts.map +1 -1
  82. package/dist/utils/output.js +20 -16
  83. package/dist/utils/output.js.map +1 -1
  84. package/dist/utils/updateCheck.d.ts.map +1 -0
  85. package/dist/utils/updateCheck.js +138 -0
  86. package/dist/utils/updateCheck.js.map +1 -0
  87. package/package.json +15 -7
  88. package/tsconfig.json +1 -1
  89. package/dist/utils/auth.d.ts.map +0 -1
  90. package/dist/utils/auth.js +0 -213
  91. package/dist/utils/auth.js.map +0 -1
@@ -7,6 +7,7 @@ exports.fillConfigSchemaDefaults = fillConfigSchemaDefaults;
7
7
  exports.parseVariableRefs = parseVariableRefs;
8
8
  exports.mergeConfigValues = mergeConfigValues;
9
9
  exports.validateNodePropertyValues = validateNodePropertyValues;
10
+ exports.validateSpecialNodeType = validateSpecialNodeType;
10
11
  exports.deepMerge = deepMerge;
11
12
  function configSchemaToParams(subParams) {
12
13
  if (!subParams || !Array.isArray(subParams))
@@ -15,25 +16,25 @@ function configSchemaToParams(subParams) {
15
16
  name: p.name,
16
17
  type: inferFieldType(p),
17
18
  required: p.required ?? false,
18
- description: sanitizeDescription(p.description || ""),
19
+ description: sanitizeDescription(p.description || ''),
19
20
  display_name: p.display_name || p.name,
20
21
  options: normalizeOptionValues(p.option_value_objects ?? p.option_values),
21
22
  default: p.value,
22
23
  }));
23
24
  }
24
25
  function sanitizeDescription(desc) {
25
- return desc.replace(/\u0000/g, "\\u0000");
26
+ return desc.replace(/\u0000/g, '\\u0000');
26
27
  }
27
28
  function normalizeOptionValues(raw) {
28
29
  if (!raw || !Array.isArray(raw) || raw.length === 0)
29
30
  return undefined;
30
31
  return raw
31
32
  .map((item) => {
32
- if (typeof item === "string") {
33
+ if (typeof item === 'string') {
33
34
  const v = item.trim();
34
35
  return v ? { key: v, label: v } : null;
35
36
  }
36
- const value = String(item?.value ?? item?.label ?? "").trim();
37
+ const value = String(item?.value ?? item?.label ?? '').trim();
37
38
  if (!value)
38
39
  return null;
39
40
  const label = String(item?.label ?? value).trim() || value;
@@ -48,77 +49,79 @@ function inferFieldType(param) {
48
49
  if (param.type) {
49
50
  return param.type;
50
51
  }
51
- return "string";
52
+ return 'string';
52
53
  }
53
54
  function inferScenarios(actionName, description) {
54
55
  const scenarios = [];
55
- const text = `${actionName} ${description || ""}`.toLowerCase();
56
- if (text.includes("trigger") || text.includes("webhook")) {
57
- scenarios.push("作为流程触发器,在外部事件发生时启动工作流");
56
+ const text = `${actionName} ${description || ''}`.toLowerCase();
57
+ if (text.includes('trigger') || text.includes('webhook')) {
58
+ scenarios.push('作为流程触发器,在外部事件发生时启动工作流');
58
59
  }
59
- if (text.includes("http") || text.includes("api") || text.includes("rest")) {
60
- scenarios.push("调用外部 HTTP API 接口,实现系统间的数据交互");
60
+ if (text.includes('http') || text.includes('api') || text.includes('rest')) {
61
+ scenarios.push('调用外部 HTTP API 接口,实现系统间的数据交互');
61
62
  }
62
- if (text.includes("database") ||
63
- text.includes("sql") ||
64
- text.includes("mysql") ||
65
- text.includes("postgres") ||
66
- text.includes("数据")) {
67
- scenarios.push("连接数据库,执行数据查询或写入操作");
63
+ if (text.includes('database') ||
64
+ text.includes('sql') ||
65
+ text.includes('mysql') ||
66
+ text.includes('postgres') ||
67
+ text.includes('数据')) {
68
+ scenarios.push('连接数据库,执行数据查询或写入操作');
68
69
  }
69
- if (text.includes("email") || text.includes("mail") || text.includes("邮件")) {
70
- scenarios.push("发送或接收邮件通知");
70
+ if (text.includes('email') || text.includes('mail') || text.includes('邮件')) {
71
+ scenarios.push('发送或接收邮件通知');
71
72
  }
72
- if (text.includes("ai") || text.includes("llm") || text.includes("model")) {
73
- scenarios.push("调用 AI 大模型进行文本生成、分析或推理");
73
+ if (text.includes('ai') || text.includes('llm') || text.includes('model')) {
74
+ scenarios.push('调用 AI 大模型进行文本生成、分析或推理');
74
75
  }
75
- if (text.includes("condition") || text.includes("条件")) {
76
- scenarios.push("根据条件判断结果进行流程分支控制");
76
+ if (text.includes('condition') || text.includes('条件')) {
77
+ scenarios.push('根据条件判断结果进行流程分支控制');
77
78
  }
78
- if (text.includes("loop") || text.includes("循环")) {
79
- scenarios.push("对数据集进行批量循环处理");
79
+ if (text.includes('loop') || text.includes('循环')) {
80
+ scenarios.push('对数据集进行批量循环处理');
80
81
  }
81
- if (text.includes("transform") || text.includes("转换") || text.includes("map")) {
82
- scenarios.push("对数据进行格式转换或映射处理");
82
+ if (text.includes('transform') || text.includes('转换') || text.includes('map')) {
83
+ scenarios.push('对数据进行格式转换或映射处理');
83
84
  }
84
- if (text.includes("notification") || text.includes("通知") || text.includes("message")) {
85
- scenarios.push("发送消息通知或告警");
85
+ if (text.includes('notification') || text.includes('通知') || text.includes('message')) {
86
+ scenarios.push('发送消息通知或告警');
86
87
  }
87
88
  if (scenarios.length === 0) {
88
- scenarios.push("执行自定义业务逻辑操作");
89
+ scenarios.push('执行自定义业务逻辑操作');
89
90
  }
90
91
  return scenarios;
91
92
  }
92
93
  function sanitizeFileName(name) {
93
94
  return name
94
- .replace(/[<>:"/\\|?*]/g, "_")
95
- .replace(/\s+/g, "_")
96
- .replace(/^\.+/, "")
95
+ .replace(/[<>:"/\\|?*]/g, '_')
96
+ .replace(/\s+/g, '_')
97
+ .replace(/^\.+/, '')
97
98
  .substring(0, 200);
98
99
  }
99
100
  function fillConfigSchemaDefaults(schema) {
100
101
  const clone = JSON.parse(JSON.stringify(schema));
101
102
  function walk(node) {
102
- if (!node || typeof node !== "object")
103
+ if (!node || typeof node !== 'object')
103
104
  return;
104
- if (node.sub_params && Array.isArray(node.sub_params)) {
105
- for (const child of node.sub_params) {
106
- if (child.value === undefined || child.value === null) {
107
- child.value = inferDefaultValue(child);
105
+ const n = node;
106
+ if (n.sub_params && Array.isArray(n.sub_params)) {
107
+ for (const child of n.sub_params) {
108
+ const c = child;
109
+ if (c.value === undefined || c.value === null) {
110
+ c.value = inferDefaultValue(c);
108
111
  }
109
- child.update = true;
110
- walk(child);
112
+ c.update = true;
113
+ walk(c);
111
114
  }
112
115
  }
113
- if (node.value === undefined || node.value === null) {
114
- if (node.sub_params) {
115
- node.value = null;
116
+ if (n.value === undefined || n.value === null) {
117
+ if (n.sub_params) {
118
+ n.value = null;
116
119
  }
117
120
  else {
118
- node.value = inferDefaultValue(node);
121
+ n.value = inferDefaultValue(n);
119
122
  }
120
123
  }
121
- node.update = true;
124
+ n.update = true;
122
125
  }
123
126
  walk(clone);
124
127
  return clone;
@@ -126,43 +129,43 @@ function fillConfigSchemaDefaults(schema) {
126
129
  function inferDefaultValue(param) {
127
130
  if (param.value !== undefined && param.value !== null)
128
131
  return param.value;
129
- const type = (param.page_control || param.type || "").toUpperCase();
130
- if (type === "SELECT") {
132
+ const type = (param.page_control || param.type || '').toUpperCase();
133
+ if (type === 'SELECT') {
131
134
  if (param.multi_valued)
132
135
  return [];
133
136
  const options = param.option_value_objects ?? param.option_values;
134
137
  if (options && Array.isArray(options) && options.length > 0) {
135
138
  const first = options[0];
136
- if (typeof first === "string")
139
+ if (typeof first === 'string')
137
140
  return first;
138
- if (typeof first === "object" && first !== null) {
139
- return first.value ?? first.label ?? "";
141
+ if (typeof first === 'object' && first !== null) {
142
+ return first.value ?? first.label ?? '';
140
143
  }
141
144
  }
142
145
  return null;
143
146
  }
144
147
  switch (type) {
145
- case "NUMBER":
148
+ case 'NUMBER':
146
149
  return 0;
147
- case "BOOLEAN":
148
- case "SWITCH":
150
+ case 'BOOLEAN':
151
+ case 'SWITCH':
149
152
  return false;
150
- case "DATE":
151
- case "TIME":
152
- case "DATETIME":
153
+ case 'DATE':
154
+ case 'TIME':
155
+ case 'DATETIME':
153
156
  return null;
154
- case "JSON":
155
- return "{}";
156
- case "EXTEND_OBJ":
157
- case "FIXED_OBJ":
157
+ case 'JSON':
158
+ return '{}';
159
+ case 'EXTEND_OBJ':
160
+ case 'FIXED_OBJ':
158
161
  return param.sub_params ? null : {};
159
- case "TEXTAREA":
160
- case "PASSWORD":
161
- case "HIDDEN":
162
- case "LABEL":
163
- case "TEXT":
162
+ case 'TEXTAREA':
163
+ case 'PASSWORD':
164
+ case 'HIDDEN':
165
+ case 'LABEL':
166
+ case 'TEXT':
164
167
  default:
165
- return "";
168
+ return '';
166
169
  }
167
170
  }
168
171
  function parseVariableRefs(text) {
@@ -173,7 +176,7 @@ function parseVariableRefs(text) {
173
176
  refs.push({
174
177
  raw: match[0],
175
178
  nodeId: match[1],
176
- path: match[2] || "",
179
+ path: match[2] || '',
177
180
  });
178
181
  }
179
182
  return refs;
@@ -181,12 +184,14 @@ function parseVariableRefs(text) {
181
184
  function mergeConfigValues(schema, overrides) {
182
185
  const clone = JSON.parse(JSON.stringify(schema));
183
186
  function walk(node, overrideMap) {
184
- if (!node?.sub_params)
187
+ const n = node;
188
+ if (!n.sub_params)
185
189
  return;
186
- for (const child of node.sub_params) {
187
- if (child.name && child.name in overrideMap) {
188
- const val = overrideMap[child.name];
189
- if (child.sub_params && typeof val === "object" && !Array.isArray(val)) {
190
+ for (const child of n.sub_params) {
191
+ const childName = child.name;
192
+ if (childName && childName in overrideMap) {
193
+ const val = overrideMap[childName];
194
+ if (child.sub_params && typeof val === 'object' && !Array.isArray(val)) {
190
195
  walk(child, val);
191
196
  }
192
197
  else {
@@ -207,29 +212,30 @@ function mergeConfigValues(schema, overrides) {
207
212
  function validateNodePropertyValues(schema) {
208
213
  const errors = [];
209
214
  function walk(node, path) {
210
- if (!node || typeof node !== "object")
215
+ if (!node || typeof node !== 'object')
211
216
  return;
212
- if (node.sub_params && Array.isArray(node.sub_params)) {
213
- for (const child of node.sub_params) {
214
- const childPath = path
215
- ? `${path}.${child.name || "(anonymous)"}`
216
- : (child.name || "(anonymous)");
217
- checkField(child, childPath);
218
- walk(child, childPath);
217
+ const n = node;
218
+ if (n.sub_params && Array.isArray(n.sub_params)) {
219
+ for (const child of n.sub_params) {
220
+ const c = child;
221
+ const cName = String(c.name || '(anonymous)');
222
+ const childPath = path ? `${path}.${cName}` : cName;
223
+ checkField(c, childPath);
224
+ walk(c, childPath);
219
225
  }
220
226
  }
221
227
  }
222
228
  function checkField(field, fieldPath) {
223
229
  const val = field.value;
224
- const pageControl = (field.page_control || field.type || "").toUpperCase();
225
- const isEmpty = val === undefined || val === null || val === "";
230
+ const pageControl = String(field.page_control || field.type || '').toUpperCase();
231
+ const isEmpty = val === undefined || val === null || val === '';
226
232
  // 1. 检查必填字段
227
233
  if (field.required && isEmpty) {
228
234
  errors.push({
229
235
  field: fieldPath,
230
- rule: "required",
236
+ rule: 'required',
231
237
  message: `必填字段 "${field.display_name} : ${field.name}" 未提供值`,
232
- sample: "",
238
+ sample: '',
233
239
  });
234
240
  }
235
241
  // 2. 如果有值,检查值类型是否匹配 page_control
@@ -240,65 +246,65 @@ function validateNodePropertyValues(schema) {
240
246
  function checkTypeConstraint(field, fieldPath, pageControl, val) {
241
247
  const sample = JSON.stringify(val).substring(0, 80);
242
248
  switch (pageControl) {
243
- case "SELECT":
249
+ case 'SELECT':
244
250
  if (field.multi_valued) {
245
251
  if (!Array.isArray(val)) {
246
252
  errors.push({
247
253
  field: fieldPath,
248
- rule: "type",
254
+ rule: 'type',
249
255
  message: `SELECT(multi) 字段 "${field.display_name || field.name}" 期望 array 类型,实际为 ${typeof val}`,
250
256
  sample,
251
257
  });
252
258
  }
253
259
  }
254
260
  else {
255
- if (typeof val !== "string") {
261
+ if (typeof val !== 'string') {
256
262
  errors.push({
257
263
  field: fieldPath,
258
- rule: "type",
264
+ rule: 'type',
259
265
  message: `SELECT 字段 "${field.display_name || field.name}" 期望 string 类型,实际为 ${typeof val}`,
260
266
  sample,
261
267
  });
262
268
  }
263
269
  }
264
270
  break;
265
- case "NUMBER":
266
- if (typeof val !== "number") {
271
+ case 'NUMBER':
272
+ if (typeof val !== 'number') {
267
273
  errors.push({
268
274
  field: fieldPath,
269
- rule: "type",
275
+ rule: 'type',
270
276
  message: `NUMBER 字段 "${field.display_name || field.name}" 期望 number 类型,实际为 ${typeof val}`,
271
277
  sample,
272
278
  });
273
279
  }
274
280
  break;
275
- case "BOOLEAN":
276
- case "SWITCH":
277
- if (typeof val !== "boolean") {
281
+ case 'BOOLEAN':
282
+ case 'SWITCH':
283
+ if (typeof val !== 'boolean') {
278
284
  errors.push({
279
285
  field: fieldPath,
280
- rule: "type",
286
+ rule: 'type',
281
287
  message: `${pageControl} 字段 "${field.display_name || field.name}" 期望 boolean 类型,实际为 ${typeof val}`,
282
288
  sample,
283
289
  });
284
290
  }
285
291
  break;
286
- case "EXTEND_OBJ":
287
- case "FIXED_OBJ":
288
- if (typeof val !== "object" || Array.isArray(val)) {
292
+ case 'EXTEND_OBJ':
293
+ case 'FIXED_OBJ':
294
+ if (typeof val !== 'object' || Array.isArray(val)) {
289
295
  errors.push({
290
296
  field: fieldPath,
291
- rule: "type",
292
- message: `${pageControl} 字段 "${field.display_name || field.name}" 期望 object 类型,实际为 ${Array.isArray(val) ? "array" : typeof val}`,
297
+ rule: 'type',
298
+ message: `${pageControl} 字段 "${field.display_name || field.name}" 期望 object 类型,实际为 ${Array.isArray(val) ? 'array' : typeof val}`,
293
299
  sample,
294
300
  });
295
301
  }
296
302
  break;
297
- case "JSON":
298
- if (typeof val !== "string") {
303
+ case 'JSON':
304
+ if (typeof val !== 'string') {
299
305
  errors.push({
300
306
  field: fieldPath,
301
- rule: "type",
307
+ rule: 'type',
302
308
  message: `JSON 字段 "${field.display_name || field.name}" 期望 JSON 字符串,实际为 ${typeof val}`,
303
309
  sample,
304
310
  });
@@ -310,34 +316,34 @@ function validateNodePropertyValues(schema) {
310
316
  catch {
311
317
  errors.push({
312
318
  field: fieldPath,
313
- rule: "format",
319
+ rule: 'format',
314
320
  message: `JSON 字段 "${field.display_name || field.name}" 的值不是有效的 JSON`,
315
321
  sample: val.substring(0, 80),
316
322
  });
317
323
  }
318
324
  }
319
325
  break;
320
- case "DATE":
321
- case "TIME":
322
- case "DATETIME":
323
- if (typeof val !== "string") {
326
+ case 'DATE':
327
+ case 'TIME':
328
+ case 'DATETIME':
329
+ if (typeof val !== 'string') {
324
330
  errors.push({
325
331
  field: fieldPath,
326
- rule: "type",
332
+ rule: 'type',
327
333
  message: `${pageControl} 字段 "${field.display_name || field.name}" 期望日期/时间字符串,实际为 ${typeof val}`,
328
334
  sample,
329
335
  });
330
336
  }
331
337
  break;
332
- case "TEXT":
333
- case "TEXTAREA":
334
- case "PASSWORD":
335
- case "HIDDEN":
336
- case "LABEL":
337
- if (typeof val !== "string") {
338
+ case 'TEXT':
339
+ case 'TEXTAREA':
340
+ case 'PASSWORD':
341
+ case 'HIDDEN':
342
+ case 'LABEL':
343
+ if (typeof val !== 'string') {
338
344
  errors.push({
339
345
  field: fieldPath,
340
- rule: "type",
346
+ rule: 'type',
341
347
  message: `${pageControl} 字段 "${field.display_name || field.name}" 期望 string 类型,实际为 ${typeof val}`,
342
348
  sample,
343
349
  });
@@ -347,23 +353,338 @@ function validateNodePropertyValues(schema) {
347
353
  break;
348
354
  }
349
355
  }
350
- walk(schema, "");
356
+ walk(schema, '');
357
+ return errors;
358
+ }
359
+ const VALID_CONDITION_OPERATORS = new Set([
360
+ 'eq',
361
+ 'ne',
362
+ 'gt',
363
+ 'ge',
364
+ 'lt',
365
+ 'le',
366
+ 'sw',
367
+ 'ew',
368
+ 'co',
369
+ 'nco',
370
+ 'in',
371
+ 'nin',
372
+ 'is_null',
373
+ 'is_not_null',
374
+ ]);
375
+ const NULL_OPERATORS = new Set(['is_null', 'is_not_null']);
376
+ const LOOP_TYPES = new Set(['NUMBER', 'ARRAY', 'CONDITION']);
377
+ const ERROR_EXEC_OPTIONS = new Set(['END', 'CONTINUE', 'BREAK']);
378
+ const SPECIAL_NODE_TYPES = new Set(['ConditionNode', 'LoopNode', 'SelfLoopNode', 'SwitchNode']);
379
+ function safeSample(val) {
380
+ return (JSON.stringify(val) ?? '').substring(0, 80);
381
+ }
382
+ function validateOrList(orList, path) {
383
+ const errors = [];
384
+ if (!Array.isArray(orList)) {
385
+ errors.push({
386
+ field: path,
387
+ rule: 'type',
388
+ message: `${path} 必须是 array 类型`,
389
+ sample: safeSample(orList),
390
+ });
391
+ return errors;
392
+ }
393
+ if (orList.length === 0) {
394
+ errors.push({
395
+ field: path,
396
+ rule: 'required',
397
+ message: `${path} 至少需要 1 个条件组`,
398
+ sample: '',
399
+ });
400
+ return errors;
401
+ }
402
+ orList.forEach((orItem, i) => {
403
+ const orPath = `${path}[${i}]`;
404
+ if (!orItem || typeof orItem !== 'object') {
405
+ errors.push({
406
+ field: orPath,
407
+ rule: 'type',
408
+ message: `${orPath} 必须是 object 类型`,
409
+ sample: safeSample(orItem),
410
+ });
411
+ return;
412
+ }
413
+ const andList = orItem.and_list;
414
+ if (!Array.isArray(andList)) {
415
+ errors.push({
416
+ field: `${orPath}.and_list`,
417
+ rule: 'type',
418
+ message: `${orPath}.and_list 必须是 array 类型`,
419
+ sample: safeSample(andList),
420
+ });
421
+ return;
422
+ }
423
+ if (andList.length === 0) {
424
+ errors.push({
425
+ field: `${orPath}.and_list`,
426
+ rule: 'required',
427
+ message: `${orPath}.and_list 至少需要 1 个条件`,
428
+ sample: '',
429
+ });
430
+ return;
431
+ }
432
+ andList.forEach((item, j) => {
433
+ const itemPath = `${orPath}.and_list[${j}]`;
434
+ if (!item || typeof item !== 'object') {
435
+ errors.push({
436
+ field: itemPath,
437
+ rule: 'type',
438
+ message: `${itemPath} 必须是 object 类型`,
439
+ sample: safeSample(item),
440
+ });
441
+ return;
442
+ }
443
+ if (!item.query || typeof item.query !== 'string' || !item.query.trim()) {
444
+ errors.push({
445
+ field: `${itemPath}.query`,
446
+ rule: 'required',
447
+ message: `${itemPath}.query 是必填字段,须为变量引用`,
448
+ sample: '',
449
+ });
450
+ }
451
+ if (!item.operator || typeof item.operator !== 'string') {
452
+ errors.push({
453
+ field: `${itemPath}.operator`,
454
+ rule: 'required',
455
+ message: `${itemPath}.operator 是必填字段`,
456
+ sample: '',
457
+ });
458
+ }
459
+ else if (!VALID_CONDITION_OPERATORS.has(item.operator)) {
460
+ errors.push({
461
+ field: `${itemPath}.operator`,
462
+ rule: 'type',
463
+ message: `${itemPath}.operator 无效: "${item.operator}",有效值: ${[...VALID_CONDITION_OPERATORS].join(', ')}`,
464
+ sample: item.operator,
465
+ });
466
+ }
467
+ if (item.operator &&
468
+ !NULL_OPERATORS.has(item.operator) &&
469
+ (item.value === undefined || item.value === null)) {
470
+ errors.push({
471
+ field: `${itemPath}.value`,
472
+ rule: 'required',
473
+ message: `${itemPath}.value 在运算符为 "${item.operator}" 时必须提供`,
474
+ sample: '',
475
+ });
476
+ }
477
+ });
478
+ });
479
+ return errors;
480
+ }
481
+ /**
482
+ * 校验特殊节点类型(ConditionNode / LoopNode / SelfLoopNode / SwitchNode)的业务字段。
483
+ * 这些字段位于 data.properties 下,与 sub_params 同级。普通 ActionNode 直接返回空数组。
484
+ */
485
+ function validateSpecialNodeType(nodeType, data) {
486
+ if (!SPECIAL_NODE_TYPES.has(nodeType))
487
+ return [];
488
+ const props = data.properties || {};
489
+ const errors = [];
490
+ switch (nodeType) {
491
+ case 'ConditionNode': {
492
+ if (props.or_list === undefined || props.or_list === null) {
493
+ errors.push({
494
+ field: 'properties.or_list',
495
+ rule: 'required',
496
+ message: 'ConditionNode 缺少必填字段 properties.or_list',
497
+ sample: '',
498
+ });
499
+ }
500
+ else {
501
+ errors.push(...validateOrList(props.or_list, 'properties.or_list'));
502
+ }
503
+ break;
504
+ }
505
+ case 'LoopNode':
506
+ case 'SelfLoopNode': {
507
+ // LoopNode 字段在 properties 下双份存在:直接属性 + sub_params[].value
508
+ // CLI --data 只填充 sub_params,API 返回时两者都有,需要从两处查找
509
+ const loopField = (name) => {
510
+ if (props[name] !== undefined && props[name] !== null)
511
+ return props[name];
512
+ const subParams = props.sub_params;
513
+ if (Array.isArray(subParams)) {
514
+ const entry = subParams.find((s) => s.name === name);
515
+ if (entry && entry.value !== undefined && entry.value !== null)
516
+ return entry.value;
517
+ }
518
+ return undefined;
519
+ };
520
+ const loopType = loopField('type');
521
+ if (!loopType || !LOOP_TYPES.has(loopType)) {
522
+ errors.push({
523
+ field: 'properties.type',
524
+ rule: 'required',
525
+ message: `循环节点 properties.type 必须是 NUMBER / ARRAY / CONDITION 之一,当前为 "${loopType}"`,
526
+ sample: loopType || '',
527
+ });
528
+ }
529
+ if (loopType === 'NUMBER') {
530
+ const startVal = loopField('start');
531
+ const endVal = loopField('end');
532
+ if (typeof startVal !== 'number') {
533
+ errors.push({
534
+ field: 'properties.start',
535
+ rule: 'type',
536
+ message: `NUMBER 循环的 properties.start 必须是 number 类型,当前为 ${typeof startVal}`,
537
+ sample: String(startVal),
538
+ });
539
+ }
540
+ else if (startVal < 1) {
541
+ errors.push({
542
+ field: 'properties.start',
543
+ rule: 'type',
544
+ message: `NUMBER 循环的 properties.start 必须 >= 1,当前为 ${startVal}`,
545
+ sample: String(startVal),
546
+ });
547
+ }
548
+ if (typeof endVal !== 'number') {
549
+ errors.push({
550
+ field: 'properties.end',
551
+ rule: 'type',
552
+ message: `NUMBER 循环的 properties.end 必须是 number 类型,当前为 ${typeof endVal}`,
553
+ sample: String(endVal),
554
+ });
555
+ }
556
+ else if (typeof startVal === 'number' && endVal < startVal) {
557
+ errors.push({
558
+ field: 'properties.end',
559
+ rule: 'type',
560
+ message: `NUMBER 循环的 properties.end (${endVal}) 必须 >= properties.start (${startVal})`,
561
+ sample: String(endVal),
562
+ });
563
+ }
564
+ }
565
+ if (loopType === 'ARRAY' || loopType === 'CONDITION') {
566
+ const exprVal = loopField('expression');
567
+ if (!exprVal || typeof exprVal !== 'string' || !exprVal.trim()) {
568
+ errors.push({
569
+ field: 'properties.expression',
570
+ rule: 'required',
571
+ message: `${loopType} 循环的 properties.expression 是必填字段`,
572
+ sample: exprVal ? String(exprVal).substring(0, 80) : '',
573
+ });
574
+ }
575
+ }
576
+ const ascVal = loopField('asc');
577
+ if (typeof ascVal !== 'boolean') {
578
+ errors.push({
579
+ field: 'properties.asc',
580
+ rule: 'type',
581
+ message: `properties.asc 必须是 boolean 类型,当前为 ${typeof ascVal}`,
582
+ sample: String(ascVal),
583
+ });
584
+ }
585
+ const errExecVal = loopField('errorExec');
586
+ if (!errExecVal || !ERROR_EXEC_OPTIONS.has(errExecVal)) {
587
+ errors.push({
588
+ field: 'properties.errorExec',
589
+ rule: 'required',
590
+ message: `properties.errorExec 必须是 END / CONTINUE / BREAK 之一,当前为 "${errExecVal}"`,
591
+ sample: errExecVal ? String(errExecVal) : '',
592
+ });
593
+ }
594
+ break;
595
+ }
596
+ case 'SwitchNode': {
597
+ const branches = props.branches;
598
+ if (!Array.isArray(branches)) {
599
+ errors.push({
600
+ field: 'properties.branches',
601
+ rule: 'type',
602
+ message: `properties.branches 必须是 array 类型,当前为 ${typeof branches}`,
603
+ sample: safeSample(branches),
604
+ });
605
+ }
606
+ else if (branches.length < 2 || branches.length > 10) {
607
+ errors.push({
608
+ field: 'properties.branches',
609
+ rule: 'type',
610
+ message: `properties.branches 需要 2-10 个分支,当前为 ${branches.length} 个`,
611
+ sample: '',
612
+ });
613
+ }
614
+ else {
615
+ const seenIds = new Set();
616
+ branches.forEach((branch, i) => {
617
+ const brPath = `properties.branches[${i}]`;
618
+ if (!branch || typeof branch !== 'object') {
619
+ errors.push({
620
+ field: brPath,
621
+ rule: 'type',
622
+ message: `${brPath} 必须是 object 类型`,
623
+ sample: safeSample(branch),
624
+ });
625
+ return;
626
+ }
627
+ if (!branch.id || typeof branch.id !== 'string' || !branch.id.trim()) {
628
+ errors.push({
629
+ field: `${brPath}.id`,
630
+ rule: 'required',
631
+ message: `${brPath}.id 是必填字段`,
632
+ sample: '',
633
+ });
634
+ }
635
+ else if (seenIds.has(branch.id)) {
636
+ errors.push({
637
+ field: `${brPath}.id`,
638
+ rule: 'type',
639
+ message: `分支 id "${branch.id}" 重复`,
640
+ sample: branch.id,
641
+ });
642
+ }
643
+ else {
644
+ seenIds.add(branch.id);
645
+ }
646
+ if (!branch.name || typeof branch.name !== 'string' || !branch.name.trim()) {
647
+ errors.push({
648
+ field: `${brPath}.name`,
649
+ rule: 'required',
650
+ message: `${brPath}.name 是必填字段`,
651
+ sample: '',
652
+ });
653
+ }
654
+ if (!branch.or_list) {
655
+ errors.push({
656
+ field: `${brPath}.or_list`,
657
+ rule: 'required',
658
+ message: `${brPath}.or_list 是必填字段`,
659
+ sample: '',
660
+ });
661
+ }
662
+ else {
663
+ errors.push(...validateOrList(branch.or_list, `${brPath}.or_list`));
664
+ }
665
+ });
666
+ }
667
+ break;
668
+ }
669
+ }
351
670
  return errors;
352
671
  }
353
672
  function isPlainObject(value) {
354
673
  return (value !== null &&
355
- typeof value === "object" &&
674
+ typeof value === 'object' &&
356
675
  !Array.isArray(value) &&
357
- Object.prototype.toString.call(value) === "[object Object]");
676
+ Object.prototype.toString.call(value) === '[object Object]');
358
677
  }
359
678
  function deepMerge(target, ...sources) {
679
+ const tgt = target;
360
680
  for (const source of sources) {
361
- if (source == null || typeof source !== "object" || Array.isArray(source)) {
681
+ if (source == null || typeof source !== 'object' || Array.isArray(source)) {
362
682
  continue;
363
683
  }
364
- const subParams = target.sub_params;
365
- for (const key of Object.keys(source)) {
366
- const srcVal = source[key];
684
+ const src = source;
685
+ const subParams = tgt.sub_params;
686
+ for (const key of Object.keys(src)) {
687
+ const srcVal = src[key];
367
688
  // sub_params-aware merge: 匹配 name 并原地修改 target 的 sub_params 中的 value
368
689
  if (Array.isArray(subParams)) {
369
690
  const matched = subParams.find((item) => item.name === key);
@@ -377,12 +698,36 @@ function deepMerge(target, ...sources) {
377
698
  continue;
378
699
  }
379
700
  }
380
- const tgtVal = target[key];
381
- if (isPlainObject(tgtVal) && isPlainObject(srcVal)) {
382
- target[key] = deepMerge({ ...tgtVal }, srcVal);
701
+ const tgtVal = tgt[key];
702
+ // sub_params 数组按 name 合并,避免整体替换导致数据丢失
703
+ if (key === 'sub_params' && Array.isArray(tgtVal) && Array.isArray(srcVal)) {
704
+ const tgtArr = tgtVal;
705
+ for (const srcItem of srcVal) {
706
+ if (!srcItem || typeof srcItem.name !== 'string') {
707
+ tgtArr.push(srcItem);
708
+ continue;
709
+ }
710
+ const matched = tgtArr.find((t) => t.name === srcItem.name);
711
+ if (matched) {
712
+ if (matched.sub_params && isPlainObject(srcItem.value)) {
713
+ deepMerge(matched, srcItem.value);
714
+ }
715
+ else if (srcItem.value !== undefined) {
716
+ matched.value = srcItem.value;
717
+ }
718
+ if (srcItem.update !== undefined)
719
+ matched.update = srcItem.update;
720
+ }
721
+ else {
722
+ tgtArr.push(srcItem);
723
+ }
724
+ }
725
+ }
726
+ else if (isPlainObject(tgtVal) && isPlainObject(srcVal)) {
727
+ tgt[key] = deepMerge({ ...tgtVal }, srcVal);
383
728
  }
384
729
  else {
385
- target[key] = srcVal;
730
+ tgt[key] = srcVal;
386
731
  }
387
732
  }
388
733
  }