digitalsee-ai-flow-cli 0.9.15 → 0.9.24-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.
@@ -0,0 +1,304 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_PORT_TYPE = void 0;
4
+ exports.validateFlowPorts = validateFlowPorts;
5
+ exports.validateSingleEdge = validateSingleEdge;
6
+ exports.resolveEdgeSaveType = resolveEdgeSaveType;
7
+ const nodeSchemaCache_1 = require("@/utils/nodeSchemaCache");
8
+ const classicNodePorts_1 = require("@/services/classicNodePorts");
9
+ exports.DEFAULT_PORT_TYPE = 'main';
10
+ const normalizeNodeKeyList = (value) => Array.isArray(value)
11
+ ? value.filter((item) => typeof item === 'string' && Boolean(item.trim()))
12
+ : [];
13
+ const normalizePort = (port) => ({
14
+ name: port.name,
15
+ displayName: port.display_name || port.name,
16
+ type: typeof port.type === 'string' && port.type.trim() ? port.type.trim() : exports.DEFAULT_PORT_TYPE,
17
+ required: Boolean(port.required),
18
+ maxConnections: port.max_connections !== undefined &&
19
+ port.max_connections !== null &&
20
+ Number.isFinite(port.max_connections)
21
+ ? Math.max(0, Math.trunc(port.max_connections))
22
+ : null,
23
+ allowedSourceNodeKeys: normalizeNodeKeyList(port.allowed_source_node_keys),
24
+ allowedTargetNodeKeys: normalizeNodeKeyList(port.allowed_target_node_keys),
25
+ deniedSourceNodeKeys: normalizeNodeKeyList(port.denied_source_node_keys),
26
+ deniedTargetNodeKeys: normalizeNodeKeyList(port.denied_target_node_keys),
27
+ });
28
+ const displayHandle = (handle) => handle || '(未指定)';
29
+ const formatHandleList = (ports) => ports.length
30
+ ? ports
31
+ .map((p) => p.displayName && p.displayName !== p.name ? `${p.name}(${p.displayName})` : p.name)
32
+ .join('、')
33
+ : '(无)';
34
+ const includesKey = (keys, identity) => Boolean(identity && keys.includes(identity));
35
+ const rejectedByAllowlist = (keys, identity) => Boolean(keys.length && !includesKey(keys, identity));
36
+ /** 协议/type 列表交集匹配(大小写不敏感;对齐 RCS connectionCheck 多协议兼容逻辑) */
37
+ function protocolListsOverlap(sourceType, targetType) {
38
+ const sourceList = sourceType.split(',').map((p) => p.trim().toLowerCase());
39
+ const targetList = targetType.split(',').map((p) => p.trim().toLowerCase());
40
+ return sourceList.some((sp) => targetList.includes(sp));
41
+ }
42
+ /** 端口间协议/白黑名单规则校验(不含 max_connections 与 required,二者按端口聚合检查) */
43
+ function checkPortRules(sourcePort, targetPort, sourceIdentity, targetIdentity) {
44
+ // 协议/type 比较大小写不敏感,支持逗号分隔多协议列表交集(对齐 RCS connectionCheck)
45
+ if (sourcePort.type.toLowerCase() !== targetPort.type.toLowerCase() &&
46
+ !protocolListsOverlap(sourcePort.type, targetPort.type)) {
47
+ return 'PORT_TYPE_MISMATCH';
48
+ }
49
+ // 黑名单优先于白名单:同一个 node key 同时出现时必须拒绝
50
+ if (includesKey(targetPort.deniedSourceNodeKeys, sourceIdentity)) {
51
+ return 'SOURCE_NODE_DENIED';
52
+ }
53
+ if (includesKey(sourcePort.deniedTargetNodeKeys, targetIdentity)) {
54
+ return 'TARGET_NODE_DENIED';
55
+ }
56
+ if (rejectedByAllowlist(targetPort.allowedSourceNodeKeys, sourceIdentity)) {
57
+ return 'SOURCE_NODE_NOT_ALLOWED';
58
+ }
59
+ if (rejectedByAllowlist(sourcePort.allowedTargetNodeKeys, targetIdentity)) {
60
+ return 'TARGET_NODE_NOT_ALLOWED';
61
+ }
62
+ return null;
63
+ }
64
+ function describeRuleViolation(code, sourcePort, targetPort, sourceIdentity, targetIdentity) {
65
+ switch (code) {
66
+ case 'PORT_TYPE_MISMATCH':
67
+ return `端口类型不匹配:${sourcePort.type} 输出不能连接到 ${targetPort.type} 输入`;
68
+ case 'SOURCE_NODE_DENIED':
69
+ return `目标输入端口 ${targetPort.name} 禁止源节点 ${sourceIdentity}`;
70
+ case 'TARGET_NODE_DENIED':
71
+ return `源输出端口 ${sourcePort.name} 禁止目标节点 ${targetIdentity}`;
72
+ case 'SOURCE_NODE_NOT_ALLOWED':
73
+ return `源节点 ${sourceIdentity || '(未声明 node key)'} 不在目标输入端口 ${targetPort.name} 白名单中`;
74
+ case 'TARGET_NODE_NOT_ALLOWED':
75
+ return `目标节点 ${targetIdentity || '(未声明 node key)'} 不在源输出端口 ${sourcePort.name} 白名单中`;
76
+ default:
77
+ return code;
78
+ }
79
+ }
80
+ function countPortConnections(edges, nodeId, portName, side) {
81
+ return edges.filter((edge) => {
82
+ if (side === 'source') {
83
+ return edge.source === nodeId && (edge.sourceHandle || exports.DEFAULT_PORT_TYPE) === portName;
84
+ }
85
+ return (edge.target === nodeId &&
86
+ (edge.targetHandle || exports.DEFAULT_PORT_TYPE) === portName &&
87
+ // 自环边(source === target,如 auto_connections 声明的 retry -> main)
88
+ // 不占 target 端口 max_connections 名额
89
+ edge.source !== edge.target);
90
+ }).length;
91
+ }
92
+ async function resolveNodeSchemas(nodes) {
93
+ const entries = new Map();
94
+ const unresolved = [];
95
+ await Promise.all(nodes.map(async (node) => {
96
+ const nodeId = node.n_id || node.id;
97
+ const actionKey = node.action_key;
98
+ const actionVer = node.action_ver;
99
+ if (actionKey && actionVer !== undefined && actionVer !== null) {
100
+ // NodePackage 节点:远端 schema + switch 实例分支派生 + Loop/SelfLoop 自环兜底
101
+ let schema = await (0, nodeSchemaCache_1.getNodePackageSchema)(String(actionKey), Number(actionVer));
102
+ if (!schema) {
103
+ unresolved.push({ nodeId, actionKey: String(actionKey), actionVer: Number(actionVer) });
104
+ return;
105
+ }
106
+ schema = (0, classicNodePorts_1.applySwitchBranchesToSchema)(schema, node);
107
+ entries.set(nodeId, {
108
+ nodeId,
109
+ schemaKey: schema.key,
110
+ inputs: (schema.inputs || []).map(normalizePort),
111
+ outputs: (schema.outputs || []).map(normalizePort),
112
+ autoConnect: (0, classicNodePorts_1.getAutoConnectRulesForNodeType)(node.node_type),
113
+ });
114
+ return;
115
+ }
116
+ // classic/legacy 节点:内置端口表(未知 node_type 跳过,避免误报)
117
+ const classic = (0, classicNodePorts_1.resolveClassicNodeSchema)(node);
118
+ if (classic) {
119
+ entries.set(nodeId, {
120
+ nodeId,
121
+ schemaKey: classic.schemaKey,
122
+ inputs: classic.inputs.map(normalizePort),
123
+ outputs: classic.outputs.map(normalizePort),
124
+ autoConnect: classic.autoConnect,
125
+ });
126
+ }
127
+ }));
128
+ return { entries, unresolved };
129
+ }
130
+ function validateEdge(edge, sourceEntry, targetEntry, allEdges) {
131
+ const violations = [];
132
+ const base = {
133
+ edgeId: edge.id,
134
+ sourceNodeId: edge.source,
135
+ targetNodeId: edge.target,
136
+ sourceHandle: edge.sourceHandle,
137
+ targetHandle: edge.targetHandle,
138
+ };
139
+ if (edge.source === edge.target) {
140
+ // 自环边须命中节点声明的 autoConnect 配对(如 LOOP 的 loop → input)才放行
141
+ const allowed = sourceEntry.autoConnect.some((rule) => rule.source === edge.sourceHandle && rule.target === edge.targetHandle);
142
+ if (!allowed) {
143
+ const hint = sourceEntry.autoConnect.length
144
+ ? `;允许的自环端口对: ${sourceEntry.autoConnect
145
+ .map((rule) => `${rule.source} → ${rule.target}`)
146
+ .join('、')}`
147
+ : '';
148
+ violations.push({
149
+ ...base,
150
+ code: 'SELF_CONNECTION_NOT_ALLOWED',
151
+ message: `节点 ${edge.source} 不能连接到自身${hint}`,
152
+ });
153
+ return violations;
154
+ }
155
+ // 命中 autoConnect:放行并继续端口存在性 / type / max 校验
156
+ }
157
+ const sourcePort = sourceEntry.outputs.find((p) => p.name === (edge.sourceHandle || exports.DEFAULT_PORT_TYPE));
158
+ if (!sourcePort) {
159
+ violations.push({
160
+ ...base,
161
+ code: 'SOURCE_PORT_NOT_FOUND',
162
+ message: `源节点不存在输出端口 ${displayHandle(edge.sourceHandle)},可用输出端口: ${formatHandleList(sourceEntry.outputs)}`,
163
+ });
164
+ return violations;
165
+ }
166
+ const targetPort = targetEntry.inputs.find((p) => p.name === (edge.targetHandle || exports.DEFAULT_PORT_TYPE));
167
+ if (!targetPort) {
168
+ violations.push({
169
+ ...base,
170
+ code: 'TARGET_PORT_NOT_FOUND',
171
+ message: `目标节点不存在输入端口 ${displayHandle(edge.targetHandle)},可用输入端口: ${formatHandleList(targetEntry.inputs)}`,
172
+ });
173
+ return violations;
174
+ }
175
+ const ruleCode = checkPortRules(sourcePort, targetPort, sourceEntry.schemaKey, targetEntry.schemaKey);
176
+ if (ruleCode) {
177
+ violations.push({
178
+ ...base,
179
+ code: ruleCode,
180
+ message: describeRuleViolation(ruleCode, sourcePort, targetPort, sourceEntry.schemaKey, targetEntry.schemaKey),
181
+ });
182
+ }
183
+ // max_connections:按端口维度聚合已有边数量(校验当前 flow 全量边)
184
+ if (sourcePort.maxConnections !== null) {
185
+ const count = countPortConnections(allEdges, edge.source, sourcePort.name, 'source');
186
+ if (count > sourcePort.maxConnections) {
187
+ violations.push({
188
+ ...base,
189
+ code: 'SOURCE_MAX_CONNECTIONS_EXCEEDED',
190
+ message: `源输出端口 ${sourcePort.name} 连接数 ${count} 超过上限 ${sourcePort.maxConnections}`,
191
+ });
192
+ }
193
+ }
194
+ if (targetPort.maxConnections !== null) {
195
+ const count = countPortConnections(allEdges, edge.target, targetPort.name, 'target');
196
+ if (count > targetPort.maxConnections) {
197
+ violations.push({
198
+ ...base,
199
+ code: 'TARGET_MAX_CONNECTIONS_EXCEEDED',
200
+ message: `目标输入端口 ${targetPort.name} 连接数 ${count} 超过上限 ${targetPort.maxConnections}`,
201
+ });
202
+ }
203
+ }
204
+ return violations;
205
+ }
206
+ function validateRequiredInputs(entries, edges) {
207
+ const violations = [];
208
+ const connected = new Set(edges.map((e) => `${e.target}::${e.targetHandle || exports.DEFAULT_PORT_TYPE}`));
209
+ for (const entry of entries.values()) {
210
+ for (const port of entry.inputs) {
211
+ if (port.required && !connected.has(`${entry.nodeId}::${port.name}`)) {
212
+ violations.push({
213
+ code: 'REQUIRED_INPUT_NOT_CONNECTED',
214
+ message: `节点 ${entry.nodeId} 的必需输入端口 ${port.name} 没有连线`,
215
+ targetNodeId: entry.nodeId,
216
+ targetHandle: port.name,
217
+ });
218
+ }
219
+ }
220
+ }
221
+ return violations;
222
+ }
223
+ /** 边 handle 规范化:废弃 handle 名(chat_model/memory/tools 等)替换为新名(对齐 RCS 加载迁移) */
224
+ function normalizeEdgeHandles(edges) {
225
+ return edges.map((edge) => ({
226
+ ...edge,
227
+ sourceHandle: (0, classicNodePorts_1.normalizeLegacyHandleName)(edge.sourceHandle),
228
+ targetHandle: (0, classicNodePorts_1.normalizeLegacyHandleName)(edge.targetHandle),
229
+ }));
230
+ }
231
+ /**
232
+ * 对 flow 全量 nodes + edges 做 node_schema 端口级校验。
233
+ *
234
+ * classic 节点(无 action_key/action_ver)按内置端口表(classicNodePorts)参与校验,
235
+ * 未知 node_type 的 legacy 节点跳过;NodePackage schema 拉取失败记入 unresolvedNodes 并跳过。
236
+ */
237
+ async function validateFlowPorts(input) {
238
+ const { entries, unresolved } = await resolveNodeSchemas(input.nodes);
239
+ const edges = normalizeEdgeHandles(input.edges);
240
+ const violations = [];
241
+ for (const edge of edges) {
242
+ const sourceEntry = entries.get(edge.source);
243
+ const targetEntry = entries.get(edge.target);
244
+ // 只有两端都有 node_schema 时才做端口级校验(legacy 端跳过,避免误报)
245
+ if (sourceEntry && targetEntry) {
246
+ violations.push(...validateEdge(edge, sourceEntry, targetEntry, edges));
247
+ }
248
+ }
249
+ violations.push(...validateRequiredInputs(entries, edges));
250
+ return {
251
+ violations,
252
+ checkedNodeCount: entries.size,
253
+ unresolvedNodes: unresolved,
254
+ };
255
+ }
256
+ /**
257
+ * 单边预检:校验一条准备创建的边(尚未加入现有 edges)。
258
+ * 两端均可解析端口(NodePackage schema 或 classic 内置表)时才校验;
259
+ * 任一端为未知 node_type 的 legacy 节点返回空数组。
260
+ */
261
+ async function validateSingleEdge(input) {
262
+ const { entries } = await resolveNodeSchemas(input.nodes);
263
+ const sourceEntry = entries.get(input.newEdge.source);
264
+ const targetEntry = entries.get(input.newEdge.target);
265
+ if (!sourceEntry || !targetEntry) {
266
+ return [];
267
+ }
268
+ const edge = {
269
+ id: '(new-edge)',
270
+ source: input.newEdge.source,
271
+ target: input.newEdge.target,
272
+ sourceHandle: (0, classicNodePorts_1.normalizeLegacyHandleName)(input.newEdge.sourceHandle),
273
+ targetHandle: (0, classicNodePorts_1.normalizeLegacyHandleName)(input.newEdge.targetHandle),
274
+ };
275
+ return validateEdge(edge, sourceEntry, targetEntry, [
276
+ ...normalizeEdgeHandles(input.existingEdges),
277
+ edge,
278
+ ]);
279
+ }
280
+ /**
281
+ * 解析连线的保存 type(对齐 RCS EdgeFactory ef7558b 语义):
282
+ * 仅 NodePackage 节点的 handle 参与——源侧为 NP 时取其 schema 输出项的 type,
283
+ * 否则目标侧为 NP 时取其 schema 输入项的 type,都不是则 'main'。
284
+ * classic 节点的协议不写入连线 type。
285
+ */
286
+ async function resolveEdgeSaveType(nodes, edge) {
287
+ const findNode = (id) => nodes.find((n) => (n.n_id || n.id) === id);
288
+ const sourceNode = findNode(edge.source);
289
+ if (sourceNode?.action_key && sourceNode.action_ver !== undefined) {
290
+ const schema = await (0, nodeSchemaCache_1.getNodePackageSchema)(String(sourceNode.action_key), Number(sourceNode.action_ver));
291
+ const port = schema?.outputs?.find((p) => p.name === ((0, classicNodePorts_1.normalizeLegacyHandleName)(edge.sourceHandle) || exports.DEFAULT_PORT_TYPE));
292
+ if (port?.type)
293
+ return port.type;
294
+ }
295
+ const targetNode = findNode(edge.target);
296
+ if (targetNode?.action_key && targetNode.action_ver !== undefined) {
297
+ const schema = await (0, nodeSchemaCache_1.getNodePackageSchema)(String(targetNode.action_key), Number(targetNode.action_ver));
298
+ const port = schema?.inputs?.find((p) => p.name === ((0, classicNodePorts_1.normalizeLegacyHandleName)(edge.targetHandle) || exports.DEFAULT_PORT_TYPE));
299
+ if (port?.type)
300
+ return port.type;
301
+ }
302
+ return exports.DEFAULT_PORT_TYPE;
303
+ }
304
+ //# sourceMappingURL=portValidator.js.map
@@ -37,9 +37,10 @@ exports.generateAllSkills = generateAllSkills;
37
37
  const fs = __importStar(require("fs"));
38
38
  const path = __importStar(require("path"));
39
39
  const knowledge_1 = require("@/api/knowledge");
40
- const node_1 = require("@/api/node");
41
40
  const format_1 = require("@/utils/format");
42
41
  const output_1 = require("@/utils/output");
42
+ const nodePackage_1 = require("@/utils/nodePackage");
43
+ const classicNodePorts_1 = require("@/services/classicNodePorts");
43
44
  async function generateAllSkills(outputDir) {
44
45
  const progress = {
45
46
  totalCategories: 0,
@@ -49,17 +50,30 @@ async function generateAllSkills(outputDir) {
49
50
  totalActions: 0,
50
51
  processedActions: 0,
51
52
  skillsGenerated: 0,
53
+ builtinNodeTypes: 0,
52
54
  errors: [],
53
55
  };
56
+ // builtin-nodes.md:内置节点类型端口参考(纯本地数据,探针之前写入,
57
+ // 即使目标环境不支持 NodePackage 也能产出连线知识)
58
+ writeBuiltinNodesDoc(outputDir, progress);
54
59
  const categoryTypes = [
55
- { type: 'normal', trigger: false, label: '自动化' },
56
- { type: 'normal-trigger', trigger: true, label: '自动化触发器' },
57
- { type: 'mcp', trigger: true, label: 'MCP' },
58
- { type: 'mcp-trigger', trigger: true, label: 'MCP触发器' },
60
+ { type: 'normal', scene: 'normal', trigger: false, label: '自动化' },
61
+ { type: 'normal-trigger', scene: 'normal', trigger: true, label: '自动化触发器' },
62
+ { type: 'mcp', scene: 'mcp', trigger: false, label: 'MCP' },
63
+ { type: 'mcp-trigger', scene: 'mcp', trigger: true, label: 'MCP触发器' },
59
64
  ];
65
+ // 本版本统一使用 connectable API;服务端不支持时直接失败并给出降级建议,不再回退旧接口
66
+ const nodePackageSupported = await (0, nodePackage_1.checkAndCacheNodePackageSupport)();
67
+ if (!nodePackageSupported) {
68
+ throw new Error(`目标 baseUrl 不支持 NodePackage。${nodePackage_1.NODE_PACKAGE_DOWNGRADE_HINT}`);
69
+ }
60
70
  for (const ct of categoryTypes) {
61
71
  try {
62
- const categories = await (0, knowledge_1.getNodeCategories)({ trigger: ct.trigger });
72
+ const categories = await (0, knowledge_1.getNodeCategories)({
73
+ mode: 'independent',
74
+ scene: ct.scene,
75
+ trigger: ct.trigger,
76
+ });
63
77
  progress.totalCategories += categories.length;
64
78
  (0, output_1.printInfo)(`发现 ${categories.length} 个${ct.label}分类`);
65
79
  const mdDir = path.join(outputDir, ct.type);
@@ -67,7 +81,7 @@ async function generateAllSkills(outputDir) {
67
81
  for (const category of categories) {
68
82
  const categoryMdDir = path.join(mdDir, category.name);
69
83
  fs.mkdirSync(categoryMdDir, { recursive: true });
70
- await processCategory(category, categoryMdDir, progress, ct.trigger);
84
+ await processCategory(category, categoryMdDir, progress, ct.trigger, ct.scene);
71
85
  }
72
86
  }
73
87
  catch (error) {
@@ -77,17 +91,19 @@ async function generateAllSkills(outputDir) {
77
91
  }
78
92
  return progress;
79
93
  }
80
- async function processCategory(category, mdDir, progress, trigger = false) {
94
+ async function processCategory(category, mdDir, progress, trigger = false, scene = '') {
81
95
  const categoryKey = String(category.name || '');
82
96
  const categoryLabel = String(category.label || categoryKey);
83
97
  try {
84
98
  const batchSize = 50;
85
99
  let page = 1;
86
- let hasMore = true;
87
- while (hasMore) {
100
+ let processedCount = 0;
101
+ while (true) {
88
102
  const result = await (0, knowledge_1.searchNodesByCategory)({
89
- category_name: categoryKey,
103
+ mode: 'independent',
104
+ scene,
90
105
  trigger,
106
+ categoryName: categoryKey,
91
107
  page,
92
108
  size: batchSize,
93
109
  });
@@ -97,15 +113,23 @@ async function processCategory(category, mdDir, progress, trigger = false) {
97
113
  const connectorBatchSize = 3;
98
114
  for (let i = 0; i < items.length; i += connectorBatchSize) {
99
115
  const batch = items.slice(i, i + connectorBatchSize);
100
- await Promise.all(batch.map((item) => processConnector(item.connector, category, mdDir, progress, trigger)));
116
+ await Promise.all(batch.map((item) => processConnector(item.connector, category, mdDir, progress, trigger, scene)));
101
117
  progress.processedNodes += batch.length;
102
118
  }
103
- if (items.length < batchSize) {
104
- hasMore = false;
119
+ processedCount += items.length;
120
+ // items 为空即停的保护:防止 total 异常时死循环
121
+ if (items.length === 0) {
122
+ break;
123
+ }
124
+ // 健壮的终止条件:以 total 为准(后端返回不足一页但仍有下一页时也能继续翻页)
125
+ const total = Number(result.total) || 0;
126
+ if (total > 0 && processedCount >= total) {
127
+ break;
105
128
  }
106
- else {
107
- page++;
129
+ if (total <= 0 && items.length < batchSize) {
130
+ break;
108
131
  }
132
+ page++;
109
133
  }
110
134
  }
111
135
  catch (error) {
@@ -113,9 +137,17 @@ async function processCategory(category, mdDir, progress, trigger = false) {
113
137
  }
114
138
  progress.processedCategories++;
115
139
  }
116
- async function processConnector(connector, category, mdDir, progress, trigger = false) {
140
+ async function processConnector(connector, category, mdDir, progress, trigger = false, scene = '') {
117
141
  try {
118
- const actions = await (0, node_1.getActions)({ linkId: connector.id, trigger });
142
+ // connectable 按 link_id 重查,一次请求同时拿 connector 与 matched_actions(含 node_schema)
143
+ const result = await (0, knowledge_1.searchNodesByCategory)({
144
+ mode: 'independent',
145
+ scene,
146
+ trigger,
147
+ linkId: connector.id,
148
+ });
149
+ const exact = result.items.find((candidate) => candidate.connector.id === connector.id) ?? result.items[0];
150
+ const actions = (exact?.matched_actions ?? []) || [];
119
151
  progress.totalActions += actions.length;
120
152
  const actionBatchSize = 5;
121
153
  for (let i = 0; i < actions.length; i += actionBatchSize) {
@@ -145,6 +177,8 @@ async function generateSkillFile(action, category, connector, mdDir, progress) {
145
177
  },
146
178
  action_type: action.action_type || 'action',
147
179
  action_key: hasNodeSchema ? action.node_schema?.key || action.action_key : action.action_key,
180
+ action_ver: action.action_ver ?? action.node_schema?.version,
181
+ is_trigger: Boolean(action.is_trigger),
148
182
  connector_id: action.connector_id,
149
183
  link_id: connector.id,
150
184
  action_id: action.id,
@@ -164,6 +198,8 @@ async function generateSkillFile(action, category, connector, mdDir, progress) {
164
198
  version: action.node_schema.version,
165
199
  name: action.node_schema.name,
166
200
  description: action.node_schema.description,
201
+ inputs: action.node_schema.inputs ?? [],
202
+ outputs: action.node_schema.outputs ?? [],
167
203
  input_fields: (0, format_1.nodeSchemaToParams)(action.node_schema.input_fields),
168
204
  output_fields: (action.node_schema.output_fields || []).map((o) => ({
169
205
  key: o.key,
@@ -183,6 +219,152 @@ async function generateSkillFile(action, category, connector, mdDir, progress) {
183
219
  progress.errors.push(`写入 skill 文件 ${safeName} 失败: ${error.message}`);
184
220
  }
185
221
  }
222
+ function renderClassicPortRow(port) {
223
+ return `| \`${port.name}\` | ${port.displayName} | \`${port.type ?? 'main'}\` | ${port.maxConnections !== undefined && port.maxConnections !== null
224
+ ? String(port.maxConnections)
225
+ : '不限'} |`;
226
+ }
227
+ function renderClassicPortsTable(ports) {
228
+ if (ports.length === 0)
229
+ return ['(无端口)', ''];
230
+ return [
231
+ '| 端口 | 显示名 | 类型/协议 | 最大连接数 |',
232
+ '|------|--------|-----------|------------|',
233
+ ...ports.map(renderClassicPortRow),
234
+ '',
235
+ ];
236
+ }
237
+ function renderClassicExampleCommands(descriptor) {
238
+ const lines = ['### 连线示例', '', '```bash'];
239
+ const firstOutput = descriptor.outputs[0];
240
+ const firstInput = descriptor.inputs[0];
241
+ if (firstOutput) {
242
+ // 目标侧描述该类型自己的输入桩(若有),否则提示连接下游节点
243
+ const targetPart = firstInput
244
+ ? `--target <目标节点ID> --target-handle ${firstInput.name}`
245
+ : '--target <下游节点ID> --target-handle input';
246
+ lines.push(`ai-flow flow edge connect --flow-id <flowId> --source <${descriptor.nodeType}的节点ID> ${targetPart} --source-handle ${firstOutput.name}`);
247
+ }
248
+ else if (firstInput) {
249
+ lines.push(`ai-flow flow edge connect --flow-id <flowId> --source <上游节点ID> --target <${descriptor.nodeType}的节点ID> --source-handle output --target-handle ${firstInput.name}`);
250
+ }
251
+ for (const rule of descriptor.autoConnect ?? []) {
252
+ lines.push(`# 自环边(必需结构):`, `ai-flow flow edge connect --flow-id <flowId> --source <${descriptor.nodeType}的节点ID> --target <同节点ID> --source-handle ${rule.source} --target-handle ${rule.target}`);
253
+ }
254
+ // AI slave 桩(连线方向对齐 RCS 82b5976:slave 桩为源,指向 AI 子节点顶部 input 桩)
255
+ const slaveOutput = descriptor.outputs.find((p) => p.name.startsWith('ai_'));
256
+ if (slaveOutput) {
257
+ lines.push(`# AI 子节点接入(${descriptor.nodeType} 的 ${slaveOutput.name} 桩为连线源,AISubNode 顶部 input 桩为目标):`, `ai-flow flow edge connect --flow-id <flowId> --source <${descriptor.nodeType}的节点ID> --source-handle ${slaveOutput.name} --target <AISubNode节点ID> --target-handle input`);
258
+ }
259
+ // 仍作为目标的 slave 入桩(如 BrowserNode ai_model):协议匹配的源是 AgentNode 的 ai_model 出桩
260
+ const slaveInput = descriptor.inputs.find((p) => p.name.startsWith('ai_'));
261
+ if (slaveInput) {
262
+ lines.push(`# AI 子节点接入 ${slaveInput.name} 桩(源侧用 AgentNode 的同协议 ai_model 出桩):`, `ai-flow flow edge connect --flow-id <flowId> --source <AgentNode节点ID> --source-handle ai_model --target <${descriptor.nodeType}的节点ID> --target-handle ${slaveInput.name}`);
263
+ }
264
+ lines.push('```', '');
265
+ return lines;
266
+ }
267
+ function builtinNodesDocToMarkdown() {
268
+ const lines = [
269
+ '# 内置节点类型端口参考(classic/legacy)',
270
+ '',
271
+ '非 NodePackage 节点的内置 handle 声明(数据源:CLI 内置端口表,与 `flow edge connect` / `flow validate` 的端口校验同源)。',
272
+ '',
273
+ ];
274
+ for (const descriptor of Object.values(classicNodePorts_1.CLASSIC_NODE_DESCRIPTORS)) {
275
+ lines.push(`## ${descriptor.nodeType}(${descriptor.label})`, '', descriptor.description, '');
276
+ lines.push('### 输入端口', '', ...renderClassicPortsTable(descriptor.inputs));
277
+ lines.push('### 输出端口', '', ...renderClassicPortsTable(descriptor.outputs));
278
+ if (descriptor.nodeType === 'SwitchNode') {
279
+ lines.push('> 输出端口随 `properties.branches` 实例派生:端口名 = 分支 `id`(`branch` 前缀),显示名 = 分支 `name`;`default-branch` 固定兜底。', '');
280
+ }
281
+ if (descriptor.byProtocol && Object.keys(descriptor.byProtocol).length > 0) {
282
+ lines.push('### 协议变体', '');
283
+ for (const [protocol, override] of Object.entries(descriptor.byProtocol)) {
284
+ lines.push(`- 节点 protocol = \`${protocol}\` 时:`);
285
+ if (override.outputs) {
286
+ lines.push(` - 输出端口变为 ${override.outputs.length === 0 ? '(无)' : override.outputs.map((p) => `\`${p.name}\``).join('、')}`);
287
+ }
288
+ if (override.inputs) {
289
+ lines.push(` - 输入端口变为 ${override.inputs.length === 0 ? '(无)' : override.inputs.map((p) => `\`${p.name}\``).join('、')}`);
290
+ }
291
+ }
292
+ lines.push('');
293
+ }
294
+ if ((descriptor.autoConnect ?? []).length > 0) {
295
+ lines.push('### 自动连线', '', ...descriptor.autoConnect.map((rule) => `- \`${rule.source}\` → \`${rule.target}\`(自环边)`), '');
296
+ }
297
+ lines.push(...renderClassicExampleCommands(descriptor));
298
+ }
299
+ return lines.join('\n');
300
+ }
301
+ function writeBuiltinNodesDoc(outputDir, progress) {
302
+ try {
303
+ fs.mkdirSync(outputDir, { recursive: true });
304
+ fs.writeFileSync(path.join(outputDir, 'builtin-nodes.md'), builtinNodesDocToMarkdown(), 'utf-8');
305
+ progress.builtinNodeTypes = Object.keys(classicNodePorts_1.CLASSIC_NODE_DESCRIPTORS).length;
306
+ }
307
+ catch (error) {
308
+ progress.errors.push(`生成 builtin-nodes.md 失败: ${error.message}`);
309
+ }
310
+ }
311
+ const DEFAULT_INPUT_HANDLE = { name: 'input', display_name: '输入', type: 'main' };
312
+ const DEFAULT_OUTPUT_HANDLE = {
313
+ name: 'output',
314
+ display_name: '输出',
315
+ type: 'main',
316
+ };
317
+ function formatHandleConstraints(port, direction) {
318
+ const allowed = direction === 'input' ? port.allowed_source_node_keys : port.allowed_target_node_keys;
319
+ const denied = direction === 'input' ? port.denied_source_node_keys : port.denied_target_node_keys;
320
+ const parts = [];
321
+ if ((allowed ?? []).length > 0)
322
+ parts.push(`仅允许 ${allowed.join('、')}`);
323
+ if ((denied ?? []).length > 0)
324
+ parts.push(`禁止 ${denied.join('、')}`);
325
+ return parts.length > 0 ? parts.join(';') : '-';
326
+ }
327
+ function renderHandleTable(ports, direction) {
328
+ if (ports.length === 0) {
329
+ return [`(无${direction === 'input' ? '输入' : '输出'} Handle)`, ''];
330
+ }
331
+ const lines = [
332
+ '| Handle | 显示名 | 类型/协议 | 必填 | 最大连接数 | 连接约束 |',
333
+ '|--------|--------|-----------|------|------------|----------|',
334
+ ];
335
+ for (const port of ports) {
336
+ const maxConnections = port.max_connections === undefined || port.max_connections === null
337
+ ? '不限'
338
+ : String(port.max_connections);
339
+ lines.push(`| \`${port.name}\` | ${port.display_name || port.name} | \`${port.type || 'main'}\` | ${port.required ? '是' : '否'} | ${maxConnections} | ${formatHandleConstraints(port, direction)} |`);
340
+ }
341
+ lines.push('');
342
+ return lines;
343
+ }
344
+ /**
345
+ * 渲染连线的 input/output Handle 说明(连接点位的 wire 名称 + 连线命令示例)。
346
+ * Handle 名称即 `flow edge connect` 的 `--source-handle` / `--target-handle` 取值。
347
+ */
348
+ function renderHandleSection(inputs, outputs, note) {
349
+ const lines = ['## 连接 Handle (Input / Output)', ''];
350
+ if (note)
351
+ lines.push(`> ${note}`, '');
352
+ lines.push('### 输入 Handle', '', ...renderHandleTable(inputs, 'input'));
353
+ lines.push('### 输出 Handle', '', ...renderHandleTable(outputs, 'output'));
354
+ if (inputs.length > 0 || outputs.length > 0) {
355
+ lines.push('```bash');
356
+ if (inputs.length > 0) {
357
+ const names = inputs.map((p) => `\`${p.name}\``).join('、');
358
+ lines.push(`# 作为下游节点连入(本节点 --target-handle 可选:${names})`, `ai-flow flow edge connect --flow-id <flowId> --source <上游节点ID> --source-handle <上游输出Handle> --target <本节点ID> --target-handle ${inputs[0].name}`);
359
+ }
360
+ if (outputs.length > 0) {
361
+ const names = outputs.map((p) => `\`${p.name}\``).join('、');
362
+ lines.push(`# 作为上游节点连出(本节点 --source-handle 可选:${names})`, `ai-flow flow edge connect --flow-id <flowId> --source <本节点ID> --source-handle ${outputs[0].name} --target <下游节点ID> --target-handle <下游输入Handle>`);
363
+ }
364
+ lines.push('```', '');
365
+ }
366
+ return lines;
367
+ }
186
368
  function skillToMarkdown(skill) {
187
369
  const lines = [`# ${skill.name}`, ''];
188
370
  if (skill.description) {
@@ -192,12 +374,13 @@ function skillToMarkdown(skill) {
192
374
  const ns = skill.node_schema;
193
375
  lines.push('## Node Package 信息', '');
194
376
  lines.push(`- **Schema Key**: \`${ns.key}\``);
195
- lines.push(`- **Version**: ${ns.version}`);
377
+ lines.push(`- **Version**: ${skill.action_ver ?? ns.version}`);
196
378
  if (ns.name)
197
379
  lines.push(`- **Name**: ${ns.name}`);
198
380
  if (ns.description)
199
381
  lines.push(`- **Description**: ${ns.description}`);
200
382
  lines.push('');
383
+ lines.push(...renderHandleSection(ns.inputs, ns.outputs, '连线时通过 `--source-handle` 选择输出 Handle、`--target-handle` 选择输入 Handle;Handle 即端口在流程数据中的 wire 名称。'));
201
384
  if (ns.input_fields.length > 0) {
202
385
  lines.push('## 配置参数 (Input Fields)', '');
203
386
  lines.push('| 字段名 | 显示名 | 类型 | 必填 | 说明 | 默认值 | 可选值 |');
@@ -220,17 +403,20 @@ function skillToMarkdown(skill) {
220
403
  lines.push('');
221
404
  }
222
405
  }
223
- else if (skill.parameters.length > 0) {
224
- lines.push('## 配置参数', '');
225
- lines.push('| 字段名 | 显示名 | 类型 | 必填 | 说明 | 默认值 | 可选值 |');
226
- lines.push('|--------|--------|------|------|------|--------|--------|');
227
- for (const param of skill.parameters) {
228
- const options = (param.options || [])
229
- ?.map((o) => (o.label !== o.key ? `\`${o.key}\` (${o.label})` : `\`${o.key}\``))
230
- .join(', ') || '-';
231
- lines.push(`| \`${param.name}\` | ${param.display_name} | ${param.type} | ${param.required ? '是' : '否'} | ${param.description || '-'} | ${param.default ?? '-'} | ${options} |`);
406
+ else {
407
+ lines.push(...renderHandleSection(skill.is_trigger ? [] : [DEFAULT_INPUT_HANDLE], [DEFAULT_OUTPUT_HANDLE], '本 Skill 未提供 NodePackage schema,以下为内置节点默认 Handle;其他内置节点类型的端口声明见 `builtin-nodes.md`。'));
408
+ if (skill.parameters.length > 0) {
409
+ lines.push('## 配置参数', '');
410
+ lines.push('| 字段名 | 显示名 | 类型 | 必填 | 说明 | 默认值 | 可选值 |');
411
+ lines.push('|--------|--------|------|------|------|--------|--------|');
412
+ for (const param of skill.parameters) {
413
+ const options = (param.options || [])
414
+ ?.map((o) => (o.label !== o.key ? `\`${o.key}\` (${o.label})` : `\`${o.key}\``))
415
+ .join(', ') || '-';
416
+ lines.push(`| \`${param.name}\` | ${param.display_name} | ${param.type} | ${param.required ? '是' : '否'} | ${param.description || '-'} | ${param.default ?? '-'} | ${options} |`);
417
+ }
418
+ lines.push('');
232
419
  }
233
- lines.push('');
234
420
  }
235
421
  lines.push('## 原始数据', '', '```json', JSON.stringify(skill, null, 2), '```', '');
236
422
  return lines.join('\n');