digitalsee-ai-flow-cli 0.9.24-beta → 0.9.29-beta

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.
@@ -36,10 +36,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.OP_STATUS_MAP = exports.NODE_UPDATE_DATA_SCHEMA = exports.NODE_CREATE_DATA_SCHEMA = exports.NODE_CONFIG_DATA_SCHEMA = exports.FLOW_CREATE_DATA_SCHEMA = void 0;
39
+ exports.VALID_CREATE_NODE_TYPES = exports.OP_STATUS_MAP = exports.NODE_UPDATE_DATA_SCHEMA = exports.NODE_CREATE_DATA_SCHEMA = exports.NODE_CONFIG_DATA_SCHEMA = exports.FLOW_CREATE_DATA_SCHEMA = void 0;
40
40
  exports.validateDataStructure = validateDataStructure;
41
41
  exports.resolveDataOption = resolveDataOption;
42
42
  exports.checkFlowNotOnline = checkFlowNotOnline;
43
+ exports.deriveNodeTypeFromAction = deriveNodeTypeFromAction;
44
+ exports.validateNodeTypeValue = validateNodeTypeValue;
43
45
  exports.formatDate = formatDate;
44
46
  exports.calcDefaultPosition = calcDefaultPosition;
45
47
  exports.edgesToConnections = edgesToConnections;
@@ -51,13 +53,20 @@ exports.resolveErrorHandlerFile = resolveErrorHandlerFile;
51
53
  exports.checkPropertyValidation = checkPropertyValidation;
52
54
  exports.checkSpecialNodeValidation = checkSpecialNodeValidation;
53
55
  exports.validateVariableRefs = validateVariableRefs;
54
- exports.validateNodeSchemaProperties = validateNodeSchemaProperties;
55
56
  exports.printNodeSchemaFields = printNodeSchemaFields;
57
+ exports.buildNodeSchemaDefaults = buildNodeSchemaDefaults;
58
+ exports.buildClassicConfigWithDefaults = buildClassicConfigWithDefaults;
59
+ exports.findProxyFieldMisuse = findProxyFieldMisuse;
60
+ exports.validateNodeSchemaFieldMetadata = validateNodeSchemaFieldMetadata;
61
+ exports.validateNodeSchemaProperties = validateNodeSchemaProperties;
56
62
  const fs = __importStar(require("fs"));
57
63
  const chalk_1 = __importDefault(require("chalk"));
58
64
  const output_1 = require("@/utils/output");
59
65
  const flow_1 = require("@/api/flow");
66
+ const format_1 = require("@/utils/format");
67
+ const fieldTypeRegistry_1 = require("@/services/fieldTypeRegistry");
60
68
  const flowAnalyzer_1 = require("@/services/flowAnalyzer");
69
+ const nodeEnvelope_1 = require("@/utils/nodeEnvelope");
61
70
  function validateDataStructure(data, schema, path = '--data') {
62
71
  if (data === null || data === undefined) {
63
72
  throw new Error(`${path} 不能为 null`);
@@ -153,15 +162,33 @@ exports.NODE_CONFIG_DATA_SCHEMA = {
153
162
  properties: { type: 'object', required: true },
154
163
  },
155
164
  };
156
- /** flow node create 的 --data 结构:可选 properties 和 position,其余字段自动转发 */
165
+ /**
166
+ * flow node create 的 --data 结构。
167
+ *
168
+ * 顶层为 NodePackage 提交信封(未声明的额外字段自动转发):
169
+ * - `properties` — 业务配置字段
170
+ * - `position` — 节点坐标
171
+ * - `proxy_id` — 代理网关 ID(字符串,'0'=不使用)。PROXY 类型字段由宿主
172
+ * 处理,必须写在信封顶层,写在 properties 中会被拦截
173
+ * - `mcp_tool_description` — MCP 工具描述
174
+ * - `account_id` — 账号实例 ID(建议用 --account-id 选项)
175
+ */
157
176
  exports.NODE_CREATE_DATA_SCHEMA = {
158
177
  type: 'object',
159
178
  properties: {
160
179
  properties: { type: 'object', required: false },
161
180
  position: { type: 'object', required: false },
181
+ proxy_id: { type: 'string', required: false },
182
+ mcp_tool_description: { type: 'string', required: false },
183
+ account_id: { type: 'string', required: false },
162
184
  },
163
185
  };
164
- /** flow node update 的 --data 结构:可选 properties 和元数据字段,其余字段自动转发 */
186
+ /**
187
+ * flow node update 的 --data/--patch 结构。
188
+ *
189
+ * 顶层为 NodePackage 提交信封:`properties` 必填,其余元数据字段可选,
190
+ * 未声明的额外字段自动转发。
191
+ */
165
192
  exports.NODE_UPDATE_DATA_SCHEMA = {
166
193
  type: 'object',
167
194
  properties: {
@@ -172,6 +199,8 @@ exports.NODE_UPDATE_DATA_SCHEMA = {
172
199
  account_id: { type: 'string', required: false },
173
200
  note: { type: 'string', required: false },
174
201
  flow_id: { type: 'string', required: false },
202
+ proxy_id: { type: 'string', required: false },
203
+ mcp_tool_description: { type: 'string', required: false },
175
204
  },
176
205
  };
177
206
  exports.OP_STATUS_MAP = {
@@ -195,6 +224,122 @@ function checkFlowNotOnline(flow, json) {
195
224
  }
196
225
  return false;
197
226
  }
227
+ /**
228
+ * 服务端 NodeEnum(uep-service acm/acm-api NodeEnum.java)中创建节点可用的合法值,
229
+ * 顺序与枚举声明一致。START / dynamic / custom 三个前端画布渲染别名不在其中
230
+ * (见 INVALID_NODE_TYPE_HINTS),仅用于读路径兼容,不是节点类型。
231
+ */
232
+ exports.VALID_CREATE_NODE_TYPES = [
233
+ 'StartEventNode',
234
+ 'ActionNode',
235
+ 'ConditionNode',
236
+ 'LoopNode',
237
+ 'SwitchNode',
238
+ 'ParallelNode',
239
+ 'EndNode',
240
+ 'AgentNode',
241
+ 'VectorStoreNode',
242
+ 'AISubNode',
243
+ 'MCPServerNode',
244
+ 'BuiltinMCPNode',
245
+ 'MCPToolNode',
246
+ 'SelfLoopNode',
247
+ 'BrowserNode',
248
+ 'StickyNoteNode',
249
+ 'UserSyncAiNode',
250
+ 'UserPushAiNode',
251
+ 'MCP_TRIGGER_NODE',
252
+ 'MCP_TOOLS_NODE',
253
+ ];
254
+ /**
255
+ * 【仅老节点】按服务端完整换算链推算 NodeType。
256
+ *
257
+ * 适用范围:legacy 详情接口 /acm/actions/:id 返回的 action_type 是 DB 原值,
258
+ * 不能直接当 nodeType(曾致 'StartEvent' / 脏数据直进创建 payload)。
259
+ * connectable / v2 列表的 action_type 已是推算终值,经 R1 原样保留,幂等。
260
+ *
261
+ * 完整换算链(三个阶段、四条规则,对齐 AppActionDBOImpl / AppActionServiceImpl):
262
+ *
263
+ * Stage 0 — DB 层 queryApiActions:
264
+ * R0「MCP 整包改写」:请求 actionType == "MCPToolNode" 时 ① 跳过 actionType
265
+ * 等值过滤(库里 MCP 动作存的也是 ActionNode,按类型查会查空),② 查完后把
266
+ * 结果集每一行的 actionType 统一改写为 "MCPToolNode"。
267
+ * ⚠️ 副作用:actionType=MCPToolNode 的 v2 请求根本不过滤类型,返回该连接器
268
+ * 下所有动作再全部标成 MCPToolNode——拿 v2 的 MCP 结果统计"有多少个 MCP
269
+ * 工具"是错的。CLI 未用该查询形态,仅备此警示。
270
+ *
271
+ * Stage 1 — Service 层 queryActionsV2 循环体(每行依次):
272
+ * R1「枚举归一化」:actionType 精确匹配(区分大小写)NodeEnum 任一名字 →
273
+ * 保留;否则("StartEvent"、脏数据、大小写不符)→ ActionNode。
274
+ * R2「触发翻转」:仅当 trigger 参数显式 true 且结果为 ActionNode →
275
+ * StartEventNode。按真值表此翻转只命中 DB 原值就是 ActionNode 的行;
276
+ * 脏数据行(R1 兜成的 ActionNode)任意 trigger 均保持 ActionNode。
277
+ * trigger=false 或不传 → 不翻转(不传时 DB 层已强制 isTrigger=0)。
278
+ * R3「LOOP 终裁」:nodeStruct(DB 原值)== "LOOP" → 强制 SelfLoopNode,
279
+ * 最后执行,覆盖前面一切结果。
280
+ *
281
+ * 同循环副作用(非 actionType,影响对 v2 响应的断言):icon 重写为默认图标;
282
+ * status 字段被置 null(v2 响应 status 恒空);configSchema 中 {{server}}/
283
+ * {{flowId}} 占位符替换为实际 host/flowId(webhook URL 断言以替换后为准)。
284
+ *
285
+ * 最终值真值表:
286
+ *
287
+ * | DB actionType | nodeStruct | trigger 参数 | v2 最终返回 |
288
+ * |-----------------------------|------------|--------------|------------------------|
289
+ * | 合法 NodeEnum 名 | SINGLE | 任意 | 原值(R1) |
290
+ * | "StartEvent"/脏数据/大小写不符 | SINGLE | 任意 | ActionNode |
291
+ * | ActionNode | SINGLE | true | StartEventNode(R2) |
292
+ * | ActionNode | SINGLE | false/不传 | ActionNode |
293
+ * | 任何值 | LOOP | 任意 | SelfLoopNode(R3 终裁) |
294
+ *
295
+ * DB 未存 actionType(undefined/null/空串)不在规则表内:维持 CLI 既有默认
296
+ * (trigger → StartEventNode,否则 ActionNode)。
297
+ *
298
+ * 推论:v2 结果一定是合法 NodeEnum 名;ActionNode 与 StartEventNode 不可信为
299
+ * 存储值(受请求参数影响);SelfLoopNode 有两种来路(存储原值 / R3 终裁),
300
+ * 不能反推存储值;R1 大小写敏感(如 "selfloopnode" 兜成 ActionNode 而非纠正)。
301
+ */
302
+ function deriveNodeTypeFromAction(actionType, nodeStruct, trigger) {
303
+ // DB 未存 actionType(表外):CLI 既有默认(trigger → StartEventNode)
304
+ let nodeType;
305
+ if (!actionType) {
306
+ nodeType = trigger ? 'StartEventNode' : 'ActionNode';
307
+ }
308
+ else {
309
+ // R1 枚举归一化(精确匹配,区分大小写)
310
+ nodeType = exports.VALID_CREATE_NODE_TYPES.includes(actionType) ? actionType : 'ActionNode';
311
+ // R2 触发翻转:仅对 DB 原值就是 ActionNode 的行(真值表行 3;R1 兜成的
312
+ // ActionNode 不翻转,见真值表行 2 "任意")
313
+ if (trigger && actionType === 'ActionNode') {
314
+ nodeType = 'StartEventNode';
315
+ }
316
+ }
317
+ // R3 LOOP 终裁:最后执行,覆盖前两步("任何值"含 DB 未存 actionType 的表外缺省)
318
+ if (nodeStruct === 'LOOP') {
319
+ nodeType = 'SelfLoopNode';
320
+ }
321
+ return nodeType;
322
+ }
323
+ /**
324
+ * 前端画布渲染别名(读路径 classicNodePorts / isTriggerNode 兼容用)→ 提示消息。
325
+ * 这些别名不在服务端 NodeEnum 中,实际不是节点类型,校验报错时据此附加提示。
326
+ */
327
+ const INVALID_NODE_TYPE_HINTS = {
328
+ START: '"START" 为前端画布渲染别名,不是节点类型,请使用规范类型 StartEventNode',
329
+ dynamic: '"dynamic" 为前端画布渲染别名,不是节点类型,请使用规范类型 ActionNode',
330
+ custom: '"custom" 为前端画布渲染别名,不是节点类型,请使用规范类型 ActionNode',
331
+ };
332
+ /**
333
+ * 校验节点类型值合法性,合法返回 null,否则返回含合法值列表的错误消息。
334
+ * source 描述取值来源(如 '--node-type 参数'),用于错误定位。
335
+ */
336
+ function validateNodeTypeValue(nodeType, source) {
337
+ if (exports.VALID_CREATE_NODE_TYPES.includes(nodeType))
338
+ return null;
339
+ const hint = INVALID_NODE_TYPE_HINTS[nodeType];
340
+ const hintSuffix = hint ? `;${hint}` : '';
341
+ return `节点类型 "${nodeType}" 不合法(来源: ${source})${hintSuffix},合法值: ${exports.VALID_CREATE_NODE_TYPES.join(', ')}`;
342
+ }
198
343
  function formatDate(d, isSeconds = true) {
199
344
  if (!d)
200
345
  return '-';
@@ -392,69 +537,246 @@ async function validateVariableRefs(configValues, flowId, nodeId, json) {
392
537
  const refs = parseVariableRefs(configStr);
393
538
  if (refs.length === 0)
394
539
  return false;
540
+ const nodeRefs = refs.filter((r) => !r.kind || r.kind === 'node');
541
+ const globalRefs = refs.filter((r) => r.kind === 'global');
542
+ const builtinRefs = refs.filter((r) => r.kind === 'builtin');
395
543
  (0, output_1.printInfo)(`发现 ${refs.length} 个变量引用,正在验证...`);
396
544
  const flow = await (0, flow_1.getFlowDetail)(flowId);
397
- const { getFlowOutputTree } = await Promise.resolve().then(() => __importStar(require('@/api/variables')));
545
+ const { getFlowOutputTree, getGlobalVariables } = await Promise.resolve().then(() => __importStar(require('@/api/variables')));
398
546
  const outputTree = await getFlowOutputTree(flowId, {
399
547
  nodeId,
400
548
  node_id: nodeId,
401
549
  edges: flow.edges || [],
402
550
  });
403
551
  const availableNodes = new Set(outputTree.map((o) => o.node_id));
404
- const unresolvedRefs = refs.filter((r) => !availableNodes.has(r.nodeId));
405
- if (unresolvedRefs.length > 0) {
406
- const msg = `变量引用验证失败: ${unresolvedRefs.map((r) => r.raw).join(', ')} 无法在上游节点找到`;
552
+ const unresolvedRefs = nodeRefs.filter((r) => !availableNodes.has(r.nodeId));
553
+ // global 类引用做全局变量表存在性检查(复用 /acm/api/globalVariables/list)
554
+ const globalKeys = new Set();
555
+ if (globalRefs.length > 0) {
556
+ try {
557
+ const globalResult = await getGlobalVariables();
558
+ for (const v of globalResult.items || []) {
559
+ if (v.key)
560
+ globalKeys.add(v.key);
561
+ }
562
+ }
563
+ catch (error) {
564
+ (0, output_1.printWarning)(`获取全局变量列表失败,跳过存在性检查: ${error.message}`);
565
+ }
566
+ }
567
+ const unresolvedGlobalRefs = globalRefs.filter((r) => globalKeys.size > 0 && !globalKeys.has(r.name || ''));
568
+ const allUnresolved = [...unresolvedRefs, ...unresolvedGlobalRefs];
569
+ if (allUnresolved.length > 0) {
570
+ const parts = [];
571
+ if (unresolvedRefs.length > 0) {
572
+ parts.push(`${unresolvedRefs.map((r) => r.raw).join(', ')} 无法在上游节点找到`);
573
+ }
574
+ if (unresolvedGlobalRefs.length > 0) {
575
+ parts.push(`${unresolvedGlobalRefs.map((r) => r.raw).join(', ')} 在全局变量表中不存在`);
576
+ }
577
+ const msg = `变量引用验证失败: ${parts.join('; ')}`;
407
578
  if (json)
408
579
  (0, output_1.printStructuredResult)({ ok: false, message: msg });
409
580
  else
410
581
  (0, output_1.printError)(msg);
411
582
  return true;
412
583
  }
413
- (0, output_1.printInfo)('变量引用验证通过');
584
+ const refSummary = [
585
+ nodeRefs.length > 0 ? `节点引用 ${nodeRefs.length} 个` : '',
586
+ globalRefs.length > 0 ? `全局变量引用 ${globalRefs.length} 个` : '',
587
+ builtinRefs.length > 0 ? `内置变量引用 ${builtinRefs.length} 个` : '',
588
+ ]
589
+ .filter(Boolean)
590
+ .join(', ');
591
+ (0, output_1.printInfo)(`变量引用验证通过${refSummary ? `(${refSummary})` : ''}`);
414
592
  return false;
415
593
  }
416
594
  /**
417
- * 校验 properties 是否符合 node_schema.input_fields 定义
595
+ * 打印 node_schema.input_fields 字段列表
418
596
  */
419
- function validateNodeSchemaProperties(properties, inputFields) {
420
- const errors = [];
597
+ function printNodeSchemaFields(inputFields) {
598
+ if (!inputFields || inputFields.length === 0)
599
+ return;
600
+ console.log(chalk_1.default.gray(` 配置字段 (${inputFields.length}):`));
421
601
  for (const field of inputFields) {
422
- const value = properties[field.key];
423
- const hasValue = value !== undefined && value !== null;
424
- if (field.required && !hasValue) {
425
- errors.push({
426
- field: field.key,
427
- rule: 'required',
428
- message: `${field.label} (${field.key}) 是必填字段`,
429
- sample: '',
430
- });
602
+ const required = field.required ? chalk_1.default.red('*') : ' ';
603
+ const typeLabel = field.type.toLowerCase();
604
+ const defaultStr = field.default_value !== undefined && field.default_value !== null
605
+ ? chalk_1.default.dim(` [默认: ${field.default_value}]`)
606
+ : '';
607
+ const helpStr = field.help_text ? chalk_1.default.gray(` - ${field.help_text}`) : '';
608
+ console.log(` ${required} ${chalk_1.default.cyan(field.key)} ${chalk_1.default.gray(`[${typeLabel}]`)} ${field.label}${defaultStr}${helpStr}`);
609
+ }
610
+ }
611
+ /**
612
+ * 递归推断 node_schema input_field 的默认值(OBJECT 递归 properties 嵌套)。
613
+ * 返回 undefined 表示该字段无默认值可填。
614
+ */
615
+ function inferSchemaFieldDefault(field) {
616
+ const type = String(field.type || '').toUpperCase();
617
+ if (type === 'OBJECT' && field.properties && field.properties.length > 0) {
618
+ const nested = {};
619
+ for (const child of field.properties) {
620
+ const childDefault = inferSchemaFieldDefault(child);
621
+ if (childDefault !== undefined) {
622
+ nested[child.key] = childDefault;
623
+ }
431
624
  }
432
- if (hasValue) {
433
- const type = field.type.toUpperCase();
434
- if ((type === 'NUMBER' || type === 'INTEGER') &&
435
- typeof value !== 'number' &&
436
- (typeof value !== 'string' || isNaN(Number(value)))) {
437
- errors.push({
625
+ return nested;
626
+ }
627
+ const value = (0, fieldTypeRegistry_1.getDefaultValueForSchemaField)(field);
628
+ if (value !== undefined && value !== null)
629
+ return value;
630
+ // 内置基础类型兜底(对齐画布初次打开表单的默认值)
631
+ if (type === 'STRING' || type === 'TEXT' || type === 'TEXTAREA')
632
+ return '';
633
+ if (type === 'NUMBER' || type === 'INTEGER' || type === 'FLOAT')
634
+ return 0;
635
+ if (type === 'BOOLEAN')
636
+ return false;
637
+ return undefined;
638
+ }
639
+ /**
640
+ * 基于 node_schema.input_fields 递归生成默认 properties。
641
+ * 仅填充有默认值的字段;用户 override 由调用方浅合并覆盖。
642
+ */
643
+ function buildNodeSchemaDefaults(inputFields) {
644
+ const properties = {};
645
+ const applied = [];
646
+ function fill(fields, target, pathPrefix) {
647
+ for (const field of fields) {
648
+ const type = String(field.type || '').toUpperCase();
649
+ if (type === 'OBJECT' && field.properties && field.properties.length > 0) {
650
+ const nested = {};
651
+ fill(field.properties, nested, pathPrefix ? `${pathPrefix}.${field.key}` : field.key);
652
+ target[field.key] = nested;
653
+ continue;
654
+ }
655
+ const value = inferSchemaFieldDefault(field);
656
+ if (value !== undefined) {
657
+ target[field.key] = value;
658
+ applied.push({
659
+ path: pathPrefix ? `${pathPrefix}.${field.key}` : field.key,
438
660
  field: field.key,
439
- rule: 'type',
440
- message: `${field.label} (${field.key}) 应为数字类型,实际为 ${typeof value}`,
441
- sample: '',
661
+ value,
442
662
  });
443
663
  }
444
- else if (type === 'BOOLEAN' && typeof value !== 'boolean') {
664
+ }
665
+ }
666
+ fill(inputFields, properties, '');
667
+ return { properties, applied };
668
+ }
669
+ /**
670
+ * classic 路径默认值填充:先 fillConfigSchemaDefaults,再 deepMerge 用户 override。
671
+ * 返回填充后的 schema 与实际生效的默认值记录(用户 override 覆盖的字段已剔除)。
672
+ */
673
+ function buildClassicConfigWithDefaults(configSchema, overrideValues) {
674
+ const applied = [];
675
+ const filled = (0, format_1.fillConfigSchemaDefaults)(configSchema, applied);
676
+ const config = (0, format_1.deepMerge)({ properties: filled }, overrideValues ?? {});
677
+ const overrideProps = overrideValues?.properties ?? {};
678
+ const effective = applied.filter((e) => !(e.field in overrideProps));
679
+ return { config, applied: effective };
680
+ }
681
+ /**
682
+ * 校验 --data.properties 中是否误写 PROXY 类型字段(对照 node_schema.input_fields)。
683
+ *
684
+ * PROXY 是工作流宿主能力:提交时会从 properties 中提升为信封顶层 proxy_id,
685
+ * 写在 properties 中服务端不会读取。返回错误消息数组(空数组表示通过)。
686
+ */
687
+ function findProxyFieldMisuse(properties, inputFields) {
688
+ const proxyFieldKey = (0, nodeEnvelope_1.getProxyFieldKey)(inputFields);
689
+ if (!proxyFieldKey || !properties || !(proxyFieldKey in properties)) {
690
+ return [];
691
+ }
692
+ return [
693
+ `--data.properties 中包含 PROXY 类型字段 "${proxyFieldKey}"(代理网关选择),` +
694
+ `该字段由宿主处理、不属于业务参数,写在 properties 中不会生效。` +
695
+ `请改用信封顶层字段:{"proxy_id": "<代理ID>"}` +
696
+ `(不使用代理时传 "0",或省略该字段)`,
697
+ ];
698
+ }
699
+ /**
700
+ * 校验 node_schema input_fields 声明的元数据:validation.pattern/min/max、
701
+ * validation.min_length/max_length、min_items/max_items。
702
+ */
703
+ function validateNodeSchemaFieldMetadata(value, field, fieldPath) {
704
+ const errors = [];
705
+ const label = `${field.label || field.key} (${fieldPath})`;
706
+ const isEmpty = value === undefined || value === null;
707
+ if (!isEmpty) {
708
+ const v = field.validation;
709
+ if (v) {
710
+ if ((typeof value === 'string' || typeof value === 'number') &&
711
+ v.pattern &&
712
+ typeof v.pattern === 'string') {
713
+ const strValue = String(value);
714
+ try {
715
+ if (!new RegExp(v.pattern).test(strValue)) {
716
+ errors.push({
717
+ field: fieldPath,
718
+ rule: 'pattern',
719
+ message: `${label} 的值 "${strValue.substring(0, 40)}" 不符合格式要求 /${v.pattern}/`,
720
+ sample: strValue.substring(0, 80),
721
+ });
722
+ }
723
+ }
724
+ catch {
725
+ // pattern 本身非法时跳过该校验,避免误报
726
+ }
727
+ }
728
+ if (typeof value === 'number') {
729
+ if (typeof v.min === 'number' && value < v.min) {
730
+ errors.push({
731
+ field: fieldPath,
732
+ rule: 'min',
733
+ message: `${label} 的值 ${value} 小于最小值 ${v.min}`,
734
+ sample: String(value),
735
+ });
736
+ }
737
+ if (typeof v.max === 'number' && value > v.max) {
738
+ errors.push({
739
+ field: fieldPath,
740
+ rule: 'max',
741
+ message: `${label} 的值 ${value} 大于最大值 ${v.max}`,
742
+ sample: String(value),
743
+ });
744
+ }
745
+ }
746
+ if (typeof value === 'string') {
747
+ if (typeof v.min_length === 'number' && value.length < v.min_length) {
748
+ errors.push({
749
+ field: fieldPath,
750
+ rule: 'min_length',
751
+ message: `${label} 的长度 ${value.length} 小于最小长度 ${v.min_length}`,
752
+ sample: value.substring(0, 80),
753
+ });
754
+ }
755
+ if (typeof v.max_length === 'number' && value.length > v.max_length) {
756
+ errors.push({
757
+ field: fieldPath,
758
+ rule: 'max_length',
759
+ message: `${label} 的长度 ${value.length} 大于最大长度 ${v.max_length}`,
760
+ sample: value.substring(0, 80),
761
+ });
762
+ }
763
+ }
764
+ }
765
+ if (Array.isArray(value)) {
766
+ if (typeof field.min_items === 'number' && value.length < field.min_items) {
445
767
  errors.push({
446
- field: field.key,
447
- rule: 'type',
448
- message: `${field.label} (${field.key}) 应为布尔类型,实际为 ${typeof value}`,
449
- sample: '',
768
+ field: fieldPath,
769
+ rule: 'min_items',
770
+ message: `${label} 的元素个数 ${value.length} 小于最小个数 ${field.min_items}`,
771
+ sample: JSON.stringify(value).substring(0, 80),
450
772
  });
451
773
  }
452
- else if (type === 'SELECT' && typeof value !== 'string') {
774
+ if (typeof field.max_items === 'number' && value.length > field.max_items) {
453
775
  errors.push({
454
- field: field.key,
455
- rule: 'type',
456
- message: `${field.label} (${field.key}) 应为字符串类型 (SELECT),实际为 ${typeof value}`,
457
- sample: '',
776
+ field: fieldPath,
777
+ rule: 'max_items',
778
+ message: `${label} 的元素个数 ${value.length} 大于最大个数 ${field.max_items}`,
779
+ sample: JSON.stringify(value).substring(0, 80),
458
780
  });
459
781
  }
460
782
  }
@@ -462,20 +784,84 @@ function validateNodeSchemaProperties(properties, inputFields) {
462
784
  return errors;
463
785
  }
464
786
  /**
465
- * 打印 node_schema.input_fields 字段列表
787
+ * 校验 properties 是否符合 node_schema.input_fields 定义
788
+ * (含 validation.pattern/min/max、min_items/max_items 元数据与嵌套 properties 递归)
466
789
  */
467
- function printNodeSchemaFields(inputFields) {
468
- if (!inputFields || inputFields.length === 0)
469
- return;
470
- console.log(chalk_1.default.gray(` 配置字段 (${inputFields.length}):`));
471
- for (const field of inputFields) {
472
- const required = field.required ? chalk_1.default.red('*') : ' ';
473
- const typeLabel = field.type.toLowerCase();
474
- const defaultStr = field.default_value !== undefined && field.default_value !== null
475
- ? chalk_1.default.dim(` [默认: ${field.default_value}]`)
476
- : '';
477
- const helpStr = field.help_text ? chalk_1.default.gray(` - ${field.help_text}`) : '';
478
- console.log(` ${required} ${chalk_1.default.cyan(field.key)} ${chalk_1.default.gray(`[${typeLabel}]`)} ${field.label}${defaultStr}${helpStr}`);
790
+ function validateNodeSchemaProperties(properties, inputFields) {
791
+ const errors = [];
792
+ function validateFields(props, fields, pathPrefix) {
793
+ for (const field of fields) {
794
+ const value = props[field.key];
795
+ const fieldPath = pathPrefix ? `${pathPrefix}.${field.key}` : field.key;
796
+ const hasValue = value !== undefined && value !== null;
797
+ // 字符串空串视为未填(默认值填充 '' 后 required 校验不放宽)
798
+ const isFilled = hasValue && value !== '';
799
+ if (field.required && !isFilled) {
800
+ errors.push({
801
+ field: fieldPath,
802
+ rule: 'required',
803
+ message: `${field.label} (${fieldPath}) 是必填字段`,
804
+ sample: '',
805
+ });
806
+ }
807
+ // 嵌套 properties 递归校验(OBJECT 字段的子字段)
808
+ if (hasValue &&
809
+ field.properties &&
810
+ field.properties.length > 0 &&
811
+ typeof value === 'object' &&
812
+ !Array.isArray(value)) {
813
+ validateFields(value, field.properties, fieldPath);
814
+ }
815
+ if (hasValue) {
816
+ const type = field.type.toUpperCase();
817
+ // 值形态豁免:变量引用 / {{$fromAI}} 哨兵 / ValueMode 编码(${expr}、\$ 转义)跳过类型断言
818
+ if (typeof value === 'string' && (0, format_1.isExemptValueForm)(value, type)) {
819
+ // 豁免类型断言,仅保留元数据校验
820
+ }
821
+ else if ((type === 'NUMBER' || type === 'INTEGER') &&
822
+ typeof value !== 'number' &&
823
+ (typeof value !== 'string' || isNaN(Number(value)))) {
824
+ errors.push({
825
+ field: fieldPath,
826
+ rule: 'type',
827
+ message: `${field.label} (${fieldPath}) 应为数字类型,实际为 ${typeof value}`,
828
+ sample: '',
829
+ });
830
+ }
831
+ else if (type === 'BOOLEAN' && typeof value !== 'boolean') {
832
+ errors.push({
833
+ field: fieldPath,
834
+ rule: 'type',
835
+ message: `${field.label} (${fieldPath}) 应为布尔类型,实际为 ${typeof value}`,
836
+ sample: '',
837
+ });
838
+ }
839
+ else if (type === 'SELECT' && typeof value !== 'string') {
840
+ errors.push({
841
+ field: fieldPath,
842
+ rule: 'type',
843
+ message: `${field.label} (${fieldPath}) 应为字符串类型 (SELECT),实际为 ${typeof value}`,
844
+ sample: '',
845
+ });
846
+ }
847
+ else {
848
+ // 注册表全量校验:DIP 复杂类型 / rcs 特殊类型 / 宿主注册类型
849
+ // 未注册类型返回 null 静默放行(宿主自定义类型)
850
+ const contractErrors = (0, fieldTypeRegistry_1.validateNodeSchemaValueByContract)(value, field);
851
+ if (contractErrors && contractErrors.length > 0) {
852
+ errors.push(...contractErrors.map((e) => ({
853
+ ...e,
854
+ field: e.field ? `${fieldPath}.${e.field}` : fieldPath,
855
+ message: `${field.label} (${fieldPath}) ${e.message}`,
856
+ })));
857
+ }
858
+ }
859
+ // 元数据校验:validation.pattern/min/max/min_length/max_length、min_items/max_items
860
+ errors.push(...validateNodeSchemaFieldMetadata(value, field, fieldPath));
861
+ }
862
+ }
479
863
  }
864
+ validateFields(properties, inputFields, '');
865
+ return errors;
480
866
  }
481
867
  //# sourceMappingURL=helpers.js.map