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.
- package/README.md +3 -0
- package/dist/api/client.js +36 -10
- package/dist/api/flowVersion.js +45 -0
- package/dist/api/knowledge.js +58 -17
- package/dist/api/node.js +76 -38
- package/dist/commands/flow/edge.js +71 -12
- package/dist/commands/flow/list.js +2 -1
- package/dist/commands/flow/node.js +147 -45
- package/dist/commands/flow/offline.js +37 -10
- package/dist/commands/flow/online.js +4 -4
- package/dist/commands/flow/publish.js +122 -17
- package/dist/commands/flow/validate.js +12 -0
- package/dist/commands/flow/version.js +303 -0
- package/dist/commands/flow.js +2 -0
- package/dist/commands/knowledge.js +101 -27
- package/dist/services/classicNodePorts.js +413 -0
- package/dist/services/portValidator.js +304 -0
- package/dist/services/skillGenerator.js +215 -29
- package/dist/utils/connectable.js +221 -0
- package/dist/utils/nodePackage.js +23 -1
- package/dist/utils/nodeSchemaCache.js +87 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1412,6 +1412,8 @@ ai-flow knowledge actions <linkId> --json # JSON 格式输出(含 outpu
|
|
|
1412
1412
|
|
|
1413
1413
|
遍历所有分类下的所有节点和动作,为每个动作生成 Markdown 文档和 JSON 结构化数据文件,按分类类型和分类名组织存储。支持 `node_schema` 模式(包含 input/output fields 详情)。
|
|
1414
1414
|
|
|
1415
|
+
每个 Markdown 文档包含 **连接 Handle (Input / Output)** 章节,列出该节点的输入/输出连接点(Handle、显示名、类型/协议、必填、最大连接数、连接约束)及 `flow edge connect` 示例命令;Handle 名称即 `--source-handle` / `--target-handle` 的取值。`node_schema` 动作直接取自 schema 的 `inputs` / `outputs`;无 `node_schema` 的内置节点回退到默认 `input` / `output`(触发器无输入 Handle),完整的内置端口声明见同目录 `builtin-nodes.md`。
|
|
1416
|
+
|
|
1415
1417
|
```bash
|
|
1416
1418
|
ai-flow knowledge generate-skills ./docs
|
|
1417
1419
|
ai-flow knowledge generate-skills ./docs --since "2026-01-01T00:00:00Z" # 增量更新
|
|
@@ -1422,6 +1424,7 @@ ai-flow knowledge generate-skills ./docs --json
|
|
|
1422
1424
|
输出:
|
|
1423
1425
|
```
|
|
1424
1426
|
docs/
|
|
1427
|
+
├── builtin-nodes.md
|
|
1425
1428
|
├── normal/
|
|
1426
1429
|
│ ├── HTTP/
|
|
1427
1430
|
│ │ ├── HTTP_发送请求.md
|
package/dist/api/client.js
CHANGED
|
@@ -15,6 +15,20 @@ const https_1 = __importDefault(require("https"));
|
|
|
15
15
|
const axios_1 = __importDefault(require("axios"));
|
|
16
16
|
const config_1 = require("@/utils/config");
|
|
17
17
|
const refresh_1 = require("@/api/refresh");
|
|
18
|
+
/** 业务错误信封成功码:error/code 为这些值时视为成功(如 {error:"0", error_description:"SUCCESS"}) */
|
|
19
|
+
const ENVELOPE_SUCCESS_CODES = new Set(['', '0', '200', 'SUCCESS']);
|
|
20
|
+
function isEnvelopeBusinessError(body) {
|
|
21
|
+
if (body.result === false)
|
|
22
|
+
return true;
|
|
23
|
+
const code = String(body.error ?? body.code ?? '');
|
|
24
|
+
return code !== '' && !ENVELOPE_SUCCESS_CODES.has(code.toUpperCase());
|
|
25
|
+
}
|
|
26
|
+
function envelopeErrorMessage(body) {
|
|
27
|
+
const code = body.error ?? body.code;
|
|
28
|
+
return (body.error_description ||
|
|
29
|
+
body.message ||
|
|
30
|
+
(code !== undefined && code !== '' ? String(code) : JSON.stringify(body)));
|
|
31
|
+
}
|
|
18
32
|
let clientInstance = null;
|
|
19
33
|
function classifyError(error) {
|
|
20
34
|
if (error && typeof error === 'object') {
|
|
@@ -61,9 +75,11 @@ function classifyError(error) {
|
|
|
61
75
|
}
|
|
62
76
|
if (status === 400 || status === 422) {
|
|
63
77
|
const body = response.data;
|
|
64
|
-
const msg = body?.
|
|
65
|
-
body?.data?.
|
|
78
|
+
const msg = body?.error_description ||
|
|
79
|
+
body?.data?.message ||
|
|
80
|
+
response.data?.error_description ||
|
|
66
81
|
response.data?.message ||
|
|
82
|
+
body?.error ||
|
|
67
83
|
response.data?.error ||
|
|
68
84
|
JSON.stringify(response.data);
|
|
69
85
|
return {
|
|
@@ -80,9 +96,11 @@ function classifyError(error) {
|
|
|
80
96
|
};
|
|
81
97
|
}
|
|
82
98
|
const body = response.data;
|
|
83
|
-
const msg = body?.
|
|
84
|
-
body?.data?.
|
|
99
|
+
const msg = body?.error_description ||
|
|
100
|
+
body?.data?.message ||
|
|
101
|
+
response.data?.error_description ||
|
|
85
102
|
response.data?.message ||
|
|
103
|
+
body?.error ||
|
|
86
104
|
response.data?.error ||
|
|
87
105
|
JSON.stringify(response.data);
|
|
88
106
|
return {
|
|
@@ -135,11 +153,16 @@ function buildClient() {
|
|
|
135
153
|
});
|
|
136
154
|
instance.interceptors.response.use((response) => {
|
|
137
155
|
const body = response.data;
|
|
138
|
-
if (body && typeof body === 'object' &&
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
156
|
+
if (body && typeof body === 'object' && !Array.isArray(body)) {
|
|
157
|
+
// HTTP 200 但业务失败的信封(如 {result:false, error:"1", error_description:"..."},
|
|
158
|
+
// 或无 data 键的 {code:"202", message:"..."}):抛错并优先透出 error_description,
|
|
159
|
+
// 避免错误被当成功放行(下游 undefined 崩溃 / "vnull" 之类的假成功)。
|
|
160
|
+
const looksLikeEnvelope = 'data' in body || 'result' in body || 'error' in body || 'code' in body;
|
|
161
|
+
if (looksLikeEnvelope && isEnvelopeBusinessError(body)) {
|
|
162
|
+
throw new Error(envelopeErrorMessage(body));
|
|
163
|
+
}
|
|
164
|
+
if ('data' in body) {
|
|
165
|
+
response.data = body.data;
|
|
143
166
|
}
|
|
144
167
|
}
|
|
145
168
|
return response;
|
|
@@ -160,7 +183,10 @@ function buildClient() {
|
|
|
160
183
|
}
|
|
161
184
|
}
|
|
162
185
|
}
|
|
163
|
-
|
|
186
|
+
const structured = classifyError(error);
|
|
187
|
+
const apiError = new Error(structured.message);
|
|
188
|
+
apiError.code = structured.code;
|
|
189
|
+
throw apiError;
|
|
164
190
|
});
|
|
165
191
|
return instance;
|
|
166
192
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.publishFlowVersion = publishFlowVersion;
|
|
4
|
+
exports.getFlowVersions = getFlowVersions;
|
|
5
|
+
exports.getFlowVersionDetail = getFlowVersionDetail;
|
|
6
|
+
exports.rollbackFlowVersion = rollbackFlowVersion;
|
|
7
|
+
exports.loadFlowVersionAsDraft = loadFlowVersionAsDraft;
|
|
8
|
+
exports.getFlowVersionStatus = getFlowVersionStatus;
|
|
9
|
+
exports.deleteFlowVersion = deleteFlowVersion;
|
|
10
|
+
exports.offlineFlowVersion = offlineFlowVersion;
|
|
11
|
+
const client_1 = require("@/api/client");
|
|
12
|
+
const BASE = (flowId) => `/acm/flows/v2/${flowId}/versions`;
|
|
13
|
+
/** 发布当前草稿为新的生效版本。 */
|
|
14
|
+
async function publishFlowVersion(flowId, data) {
|
|
15
|
+
return (0, client_1.apiPost)(`${BASE(flowId)}/publish`, data);
|
|
16
|
+
}
|
|
17
|
+
/** 查询发布版本历史。 */
|
|
18
|
+
async function getFlowVersions(flowId, params = { page: 1, size: 20 }) {
|
|
19
|
+
return (0, client_1.apiGet)(BASE(flowId), params);
|
|
20
|
+
}
|
|
21
|
+
/** 查询历史版本详情。 */
|
|
22
|
+
async function getFlowVersionDetail(flowId, versionFlowId) {
|
|
23
|
+
return (0, client_1.apiGet)(`${BASE(flowId)}/${encodeURIComponent(versionFlowId)}`);
|
|
24
|
+
}
|
|
25
|
+
/** 将线上生效版本切换到指定历史版本(成功即隐式发布)。 */
|
|
26
|
+
async function rollbackFlowVersion(flowId, versionFlowId) {
|
|
27
|
+
return (0, client_1.apiPost)(`${BASE(flowId)}/${encodeURIComponent(versionFlowId)}/rollback`, {});
|
|
28
|
+
}
|
|
29
|
+
/** 使用指定历史版本覆盖唯一草稿。 */
|
|
30
|
+
async function loadFlowVersionAsDraft(flowId, versionFlowId) {
|
|
31
|
+
return (0, client_1.apiPost)(`${BASE(flowId)}/${encodeURIComponent(versionFlowId)}/load-draft`, {});
|
|
32
|
+
}
|
|
33
|
+
/** 查询草稿与线上生效版本的关系。 */
|
|
34
|
+
async function getFlowVersionStatus(flowId) {
|
|
35
|
+
return (0, client_1.apiGet)(`${BASE(flowId)}/status`);
|
|
36
|
+
}
|
|
37
|
+
/** 删除发布版本。 */
|
|
38
|
+
async function deleteFlowVersion(flowId, versionFlowId) {
|
|
39
|
+
return (0, client_1.apiDelete)(`${BASE(flowId)}/${encodeURIComponent(versionFlowId)}`);
|
|
40
|
+
}
|
|
41
|
+
/** 版本化下线(替代废弃的 op/v2/:id/OFFLINE 下线场景)。 */
|
|
42
|
+
async function offlineFlowVersion(flowId) {
|
|
43
|
+
return (0, client_1.apiPost)(`${BASE(flowId)}/offline`);
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=flowVersion.js.map
|
package/dist/api/knowledge.js
CHANGED
|
@@ -1,29 +1,70 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildConnectableRequestBody = buildConnectableRequestBody;
|
|
3
4
|
exports.getNodeCategories = getNodeCategories;
|
|
4
5
|
exports.searchNodesByCategory = searchNodesByCategory;
|
|
5
6
|
exports.getConnectorDetail = getConnectorDetail;
|
|
6
7
|
exports.getAllConnectors = getAllConnectors;
|
|
7
8
|
const client_1 = require("@/api/client");
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
const connectable_1 = require("@/utils/connectable");
|
|
10
|
+
const trimOptionalText = (value) => {
|
|
11
|
+
const normalized = value?.trim();
|
|
12
|
+
return normalized ? normalized : undefined;
|
|
13
|
+
};
|
|
14
|
+
const parsePositiveOption = (value) => {
|
|
15
|
+
if (value === undefined)
|
|
16
|
+
return undefined;
|
|
17
|
+
return Number.isSafeInteger(value) && value > 0 ? value : undefined;
|
|
18
|
+
};
|
|
19
|
+
function buildConnectableRequestBody(query) {
|
|
15
20
|
const body = {
|
|
16
|
-
scene
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
trigger:
|
|
21
|
+
// scene 默认空(对齐 RCS config.scene ?? '';仅 MCP Server 画布场景传 'mcp')
|
|
22
|
+
scene: trimOptionalText(query.scene) ?? '',
|
|
23
|
+
// 服务端要求 trigger 必填;undefined 会被 JSON 序列化丢弃导致 1010002 错误
|
|
24
|
+
trigger: query.trigger ?? false,
|
|
20
25
|
};
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
const categoryName = trimOptionalText(query.categoryName);
|
|
27
|
+
if (categoryName !== undefined)
|
|
28
|
+
body.category_name = categoryName;
|
|
29
|
+
const linkId = trimOptionalText(query.linkId);
|
|
30
|
+
if (linkId !== undefined)
|
|
31
|
+
body.link_id = linkId;
|
|
32
|
+
const keyword = trimOptionalText(query.keyword);
|
|
33
|
+
if (keyword !== undefined)
|
|
34
|
+
body.keyword = keyword;
|
|
35
|
+
const flowId = trimOptionalText(query.flowId);
|
|
36
|
+
if (flowId !== undefined)
|
|
37
|
+
body.flow_id = flowId;
|
|
38
|
+
const page = parsePositiveOption(query.page);
|
|
39
|
+
if (page !== undefined)
|
|
40
|
+
body.page = page;
|
|
41
|
+
const size = parsePositiveOption(query.size);
|
|
42
|
+
if (size !== undefined)
|
|
43
|
+
body.size = size;
|
|
44
|
+
if (query.mode === 'source') {
|
|
45
|
+
body.source_id = query.sourceId;
|
|
46
|
+
body.source_handle = query.sourceHandle;
|
|
47
|
+
}
|
|
48
|
+
else if (query.mode === 'target') {
|
|
49
|
+
body.target_id = query.targetId;
|
|
50
|
+
body.target_handle = query.targetHandle;
|
|
51
|
+
}
|
|
52
|
+
return body;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* 节点分类只走 connectable API(POST /acm/node-package/categories/connectable),
|
|
56
|
+
* 本版本不兼容旧版接口与旧版入参(scene/trigger 裸参数形式已移除,统一 ConnectableQuery)。
|
|
57
|
+
*/
|
|
58
|
+
async function getNodeCategories(query) {
|
|
59
|
+
const data = await (0, client_1.apiPost)('/acm/node-package/categories/connectable', buildConnectableRequestBody(query));
|
|
60
|
+
return (0, connectable_1.normalizeConnectableCategories)(data);
|
|
61
|
+
}
|
|
62
|
+
async function searchNodesByCategory(query) {
|
|
63
|
+
const data = await (0, client_1.apiPost)('/acm/node-package/plugins/connectable', buildConnectableRequestBody(query));
|
|
64
|
+
if (data === null || data === undefined) {
|
|
65
|
+
return { items: [], total: 0, page: query.page || 1, size: query.size || 50 };
|
|
66
|
+
}
|
|
67
|
+
return (0, connectable_1.normalizeConnectablePluginPage)(data);
|
|
27
68
|
}
|
|
28
69
|
async function getConnectorDetail(connectorId) {
|
|
29
70
|
return (0, client_1.apiGet)(`/acm/links/${connectorId}`);
|
package/dist/api/node.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.getActionDetail = getActionDetail;
|
|
4
|
-
exports.
|
|
5
|
-
exports.searchActions = searchActions;
|
|
4
|
+
exports.getActionNodeSchema = getActionNodeSchema;
|
|
6
5
|
exports.isNodePackageSupported = isNodePackageSupported;
|
|
7
6
|
exports.getNodeDetail = getNodeDetail;
|
|
8
7
|
exports.createNode = createNode;
|
|
@@ -14,53 +13,92 @@ exports.cancelDebugNode = cancelDebugNode;
|
|
|
14
13
|
const client_1 = require("@/api/client");
|
|
15
14
|
const knowledge_1 = require("@/api/knowledge");
|
|
16
15
|
const encoding_1 = require("@/utils/encoding");
|
|
16
|
+
/**
|
|
17
|
+
* Legacy action detail lookup by numeric backend action id.
|
|
18
|
+
*
|
|
19
|
+
* Use `getActionNodeSchema(nodeKey, version)` instead for NodePackage nodes,
|
|
20
|
+
* whose persistent identity is `action_key + action_ver` — the path params
|
|
21
|
+
* there are the string action key and the numeric action version, not a
|
|
22
|
+
* backend id.
|
|
23
|
+
*/
|
|
17
24
|
async function getActionDetail(id, version) {
|
|
18
25
|
return await (0, client_1.apiGet)(`/acm/actions/${id}${version ? '/' + version : ''}`);
|
|
19
26
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
return
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
return [];
|
|
27
|
+
function parseMaybeJson(value) {
|
|
28
|
+
if (typeof value !== 'string') {
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
return JSON.parse(value);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
31
37
|
}
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
for (const item of data.items) {
|
|
37
|
-
if (item.matched_actions && Array.isArray(item.matched_actions)) {
|
|
38
|
-
actions.push(...item.matched_actions);
|
|
39
|
-
}
|
|
38
|
+
function pickFirstDefined(...values) {
|
|
39
|
+
for (const value of values) {
|
|
40
|
+
if (value !== undefined && value !== null) {
|
|
41
|
+
return value;
|
|
40
42
|
}
|
|
41
43
|
}
|
|
42
|
-
return
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
function normalizeActionNodeSchemaResponse(response, nodeKey, version) {
|
|
47
|
+
const parsedResponse = parseMaybeJson(response);
|
|
48
|
+
const rawSchema = parseMaybeJson(parsedResponse?.node_schema ?? parsedResponse?.nodeSchema ?? parsedResponse);
|
|
49
|
+
return {
|
|
50
|
+
...rawSchema,
|
|
51
|
+
key: (pickFirstDefined(rawSchema?.key, parsedResponse?.action_key, parsedResponse?.node_key, parsedResponse?.nodeKey, nodeKey) ?? nodeKey),
|
|
52
|
+
version: (pickFirstDefined(rawSchema?.version, parsedResponse?.action_ver, parsedResponse?.node_ver, parsedResponse?.nodeVersion, version) ?? version),
|
|
53
|
+
name: (pickFirstDefined(rawSchema?.name, parsedResponse?.name, parsedResponse?.action_name, nodeKey) ?? nodeKey),
|
|
54
|
+
description: pickFirstDefined(rawSchema?.description, parsedResponse?.description),
|
|
55
|
+
inputs: Array.isArray(rawSchema?.inputs) ? rawSchema.inputs : [],
|
|
56
|
+
outputs: Array.isArray(rawSchema?.outputs) ? rawSchema.outputs : [],
|
|
57
|
+
input_fields: Array.isArray(rawSchema?.input_fields)
|
|
58
|
+
? rawSchema.input_fields
|
|
59
|
+
: [],
|
|
60
|
+
output_fields: Array.isArray(rawSchema?.output_fields)
|
|
61
|
+
? rawSchema.output_fields
|
|
62
|
+
: [],
|
|
63
|
+
credentials: Array.isArray(rawSchema?.credentials)
|
|
64
|
+
? rawSchema.credentials
|
|
65
|
+
: [],
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Fetch the authoritative NodePackage node schema.
|
|
70
|
+
*
|
|
71
|
+
* GET `/acm/actions/:nodeKey/:version` where `nodeKey` is the action_key
|
|
72
|
+
* string (e.g. `slack.sendMessage`) and `version` is the numeric action_ver.
|
|
73
|
+
* The response is normalized: the body may be a JSON string, the schema may
|
|
74
|
+
* be nested under `node_schema`/`nodeSchema`, and missing key/version/name
|
|
75
|
+
* fields fall back through the outer envelope down to the call arguments.
|
|
76
|
+
*/
|
|
77
|
+
async function getActionNodeSchema(nodeKey, version) {
|
|
78
|
+
const response = await (0, client_1.apiGet)(`/acm/actions/${encodeURIComponent(nodeKey)}/${version}`);
|
|
79
|
+
return normalizeActionNodeSchemaResponse(response, nodeKey, version);
|
|
43
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* Single-request probe for NodePackage support.
|
|
83
|
+
*
|
|
84
|
+
* Calls `getNodeCategories()` once: a successful array response (including an
|
|
85
|
+
* empty array) means the connectable API exists → true. A 404/NOT_FOUND (or a
|
|
86
|
+
* non-array payload) means unsupported → false. Any other error (network,
|
|
87
|
+
* auth, 5xx) is re-thrown so callers can distinguish transient failures from
|
|
88
|
+
* a definitive "unsupported" conclusion.
|
|
89
|
+
*/
|
|
44
90
|
async function isNodePackageSupported() {
|
|
91
|
+
let categories;
|
|
45
92
|
try {
|
|
46
|
-
|
|
47
|
-
const firstName = categories[0]?.name;
|
|
48
|
-
if (!firstName)
|
|
49
|
-
return false;
|
|
50
|
-
const { items } = await (0, knowledge_1.searchNodesByCategory)({
|
|
51
|
-
category_name: firstName,
|
|
52
|
-
trigger: true,
|
|
53
|
-
size: 1,
|
|
54
|
-
});
|
|
55
|
-
const linkId = items?.[0]?.connector?.id;
|
|
56
|
-
if (!linkId)
|
|
57
|
-
return false;
|
|
58
|
-
const actions = await getActions({ linkId });
|
|
59
|
-
return actions.some((action) => action.hasOwnProperty('node_schema'));
|
|
93
|
+
categories = await (0, knowledge_1.getNodeCategories)({ mode: 'independent', scene: 'normal', trigger: false });
|
|
60
94
|
}
|
|
61
|
-
catch {
|
|
62
|
-
|
|
95
|
+
catch (error) {
|
|
96
|
+
if (error instanceof Error && error.code === 'NOT_FOUND') {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
throw error;
|
|
63
100
|
}
|
|
101
|
+
return Array.isArray(categories);
|
|
64
102
|
}
|
|
65
103
|
async function getNodeDetail(nodeId) {
|
|
66
104
|
return (0, client_1.apiGet)(`/acm/flows/nodes/${nodeId}`);
|
|
@@ -37,6 +37,8 @@ exports.registerFlowEdge = registerFlowEdge;
|
|
|
37
37
|
const fs = __importStar(require("fs"));
|
|
38
38
|
const flow_1 = require("@/api/flow");
|
|
39
39
|
const helpers_1 = require("@/commands/flow/helpers");
|
|
40
|
+
const portValidator_1 = require("@/services/portValidator");
|
|
41
|
+
const classicNodePorts_1 = require("@/services/classicNodePorts");
|
|
40
42
|
const output_1 = require("@/utils/output");
|
|
41
43
|
function registerFlowEdge(flowCmd) {
|
|
42
44
|
const edgeCmd = flowCmd.command('edge').description('边(连接)操作');
|
|
@@ -49,6 +51,7 @@ function registerFlowEdge(flowCmd) {
|
|
|
49
51
|
.option('--source-handle <handle>', '源节点输出句柄', 'output')
|
|
50
52
|
.option('--target-handle <handle>', '目标节点输入句柄', 'input')
|
|
51
53
|
.option('--edges-file <path>', '批量连边 JSON 文件路径')
|
|
54
|
+
.option('--force', '跳过 node_schema 端口校验,强制连接')
|
|
52
55
|
.option('--json', '以 JSON 格式输出')
|
|
53
56
|
.action(async (options) => {
|
|
54
57
|
try {
|
|
@@ -70,12 +73,14 @@ function registerFlowEdge(flowCmd) {
|
|
|
70
73
|
const flow = await (0, flow_1.getFlowDetail)(options.flowId);
|
|
71
74
|
if ((0, helpers_1.checkFlowNotOnline)(flow, !!options.json))
|
|
72
75
|
return;
|
|
76
|
+
// 存量边读取时做废弃 handle 名迁移(对齐 RCS useDataLoading REPLACE_HANDLE_MAP,
|
|
77
|
+
// 迁移后的新名随本次保存写回后端)
|
|
73
78
|
const existingEdges = (flow.edges || []).map((e) => ({
|
|
74
79
|
id: e.id,
|
|
75
80
|
source: e.source,
|
|
76
81
|
target: e.target,
|
|
77
|
-
sourceHandle: e.sourceHandle || 'output',
|
|
78
|
-
targetHandle: e.targetHandle || 'input',
|
|
82
|
+
sourceHandle: (0, classicNodePorts_1.normalizeLegacyHandleName)(e.sourceHandle) || 'output',
|
|
83
|
+
targetHandle: (0, classicNodePorts_1.normalizeLegacyHandleName)(e.targetHandle) || 'input',
|
|
79
84
|
type: e.type || 'main',
|
|
80
85
|
}));
|
|
81
86
|
let newEdges = [];
|
|
@@ -115,34 +120,87 @@ function registerFlowEdge(flowCmd) {
|
|
|
115
120
|
}
|
|
116
121
|
return;
|
|
117
122
|
}
|
|
118
|
-
newEdges = edgesData.map((e, i) => {
|
|
123
|
+
newEdges = await Promise.all(edgesData.map(async (e, i) => {
|
|
119
124
|
const edge = e;
|
|
120
125
|
if (!edge.source || !edge.target) {
|
|
121
126
|
throw new Error(`第 ${i + 1} 条边缺少 source 或 target`);
|
|
122
127
|
}
|
|
128
|
+
const sourceHandle = String(edge.sourceHandle || 'output');
|
|
129
|
+
const targetHandle = String(edge.targetHandle || 'input');
|
|
123
130
|
return {
|
|
124
131
|
id: `${String(edge.source)}-${String(edge.target)}-${Date.now() + i}`,
|
|
125
132
|
source: String(edge.source),
|
|
126
133
|
target: String(edge.target),
|
|
127
|
-
sourceHandle
|
|
128
|
-
targetHandle
|
|
129
|
-
|
|
134
|
+
sourceHandle,
|
|
135
|
+
targetHandle,
|
|
136
|
+
// 显式 type 优先(兼容手工构造的批量 JSON),否则按 NP schema 解析
|
|
137
|
+
type: edge.type !== undefined && edge.type !== null && String(edge.type).trim() !== ''
|
|
138
|
+
? String(edge.type)
|
|
139
|
+
: await (0, portValidator_1.resolveEdgeSaveType)(flow.nodes, {
|
|
140
|
+
source: String(edge.source),
|
|
141
|
+
target: String(edge.target),
|
|
142
|
+
sourceHandle,
|
|
143
|
+
targetHandle,
|
|
144
|
+
}),
|
|
130
145
|
};
|
|
131
|
-
});
|
|
146
|
+
}));
|
|
132
147
|
}
|
|
133
148
|
else {
|
|
149
|
+
// 连线保存 type 对齐 RCS EdgeFactory:NodePackage handle 项的 type,否则 main
|
|
150
|
+
const sourceHandle = options.sourceHandle || 'output';
|
|
151
|
+
const targetHandle = options.targetHandle || 'input';
|
|
134
152
|
newEdges = [
|
|
135
153
|
{
|
|
136
154
|
id: `${options.source}-${options.target}-${Date.now()}`,
|
|
137
155
|
source: options.source,
|
|
138
156
|
target: options.target,
|
|
139
|
-
sourceHandle
|
|
140
|
-
targetHandle
|
|
141
|
-
type:
|
|
157
|
+
sourceHandle,
|
|
158
|
+
targetHandle,
|
|
159
|
+
type: await (0, portValidator_1.resolveEdgeSaveType)(flow.nodes, {
|
|
160
|
+
source: options.source,
|
|
161
|
+
target: options.target,
|
|
162
|
+
sourceHandle,
|
|
163
|
+
targetHandle,
|
|
164
|
+
}),
|
|
142
165
|
},
|
|
143
166
|
];
|
|
144
167
|
}
|
|
145
168
|
const allEdges = [...existingEdges, ...newEdges];
|
|
169
|
+
// node_schema 端口级预检(legacy 节点自动跳过;--force 可跳过)
|
|
170
|
+
if (!options.force) {
|
|
171
|
+
const violations = [];
|
|
172
|
+
for (const edge of newEdges) {
|
|
173
|
+
violations.push(...(await (0, portValidator_1.validateSingleEdge)({
|
|
174
|
+
nodes: flow.nodes,
|
|
175
|
+
existingEdges,
|
|
176
|
+
newEdge: {
|
|
177
|
+
source: edge.source,
|
|
178
|
+
target: edge.target,
|
|
179
|
+
sourceHandle: edge.sourceHandle,
|
|
180
|
+
targetHandle: edge.targetHandle,
|
|
181
|
+
},
|
|
182
|
+
})));
|
|
183
|
+
}
|
|
184
|
+
if (violations.length > 0) {
|
|
185
|
+
if (options.json) {
|
|
186
|
+
(0, output_1.printStructuredResult)({
|
|
187
|
+
ok: false,
|
|
188
|
+
message: `端口校验未通过: ${violations.length} 个违规(可使用 --force 跳过)`,
|
|
189
|
+
portViolations: violations,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
(0, output_1.printError)(`端口校验未通过: ${violations.length} 个违规(可使用 --force 跳过):`);
|
|
194
|
+
for (const v of violations) {
|
|
195
|
+
(0, output_1.printError)(` [${v.code}] ${v.edgeId}: ${v.message}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
(0, output_1.printWarning)('已使用 --force 跳过端口校验');
|
|
203
|
+
}
|
|
146
204
|
const connections = (0, helpers_1.edgesToConnections)(allEdges);
|
|
147
205
|
(0, output_1.printInfo)(`正在更新连接流(新增 ${newEdges.length} 条边)...`);
|
|
148
206
|
await (0, flow_1.updateFlow)(options.flowId, {
|
|
@@ -188,14 +246,15 @@ function registerFlowEdge(flowCmd) {
|
|
|
188
246
|
const flow = await (0, flow_1.getFlowDetail)(options.flowId);
|
|
189
247
|
if ((0, helpers_1.checkFlowNotOnline)(flow, !!options.json))
|
|
190
248
|
return;
|
|
249
|
+
// 同 connect:存量边做废弃 handle 名迁移后写回
|
|
191
250
|
const remainingEdges = (flow.edges || [])
|
|
192
251
|
.filter((e) => !edgeIds.includes(e.id))
|
|
193
252
|
.map((e) => ({
|
|
194
253
|
id: e.id,
|
|
195
254
|
source: e.source,
|
|
196
255
|
target: e.target,
|
|
197
|
-
sourceHandle: e.sourceHandle || 'output',
|
|
198
|
-
targetHandle: e.targetHandle || 'input',
|
|
256
|
+
sourceHandle: (0, classicNodePorts_1.normalizeLegacyHandleName)(e.sourceHandle) || 'output',
|
|
257
|
+
targetHandle: (0, classicNodePorts_1.normalizeLegacyHandleName)(e.targetHandle) || 'input',
|
|
199
258
|
type: e.type || 'main',
|
|
200
259
|
}));
|
|
201
260
|
const connections = (0, helpers_1.edgesToConnections)(remainingEdges);
|
|
@@ -26,11 +26,12 @@ function registerFlowList(flowCmd) {
|
|
|
26
26
|
return;
|
|
27
27
|
}
|
|
28
28
|
(0, output_1.printHeader)(`连接流列表 (共 ${data.total} 条)`);
|
|
29
|
-
const headers = ['ID', '名称', '状态', '创建时间', '更新时间'];
|
|
29
|
+
const headers = ['ID', '名称', '状态', '生效版本', '创建时间', '更新时间'];
|
|
30
30
|
const rows = data.items.map((item) => [
|
|
31
31
|
item.id,
|
|
32
32
|
item.name,
|
|
33
33
|
(0, helpers_1.formatOpStatus)(item.op_status),
|
|
34
|
+
item.effective_version_no != null ? String(item.effective_version_no) : '-',
|
|
34
35
|
(0, helpers_1.formatDate)(item.create_time),
|
|
35
36
|
(0, helpers_1.formatDate)(item.update_time),
|
|
36
37
|
]);
|