@fieldwangai/agentflow 0.1.149 → 0.1.154
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 +20 -33
- package/README.zh-CN.md +14 -34
- package/agents/agentflow-node-executor-code.md +13 -3
- package/agents/agentflow-node-executor-planning.md +13 -3
- package/agents/agentflow-node-executor-requirement.md +13 -3
- package/agents/agentflow-node-executor-test.md +13 -3
- package/agents/agentflow-node-executor-ui.md +13 -3
- package/agents/agentflow-node-executor.md +13 -3
- package/agents/en/agentflow-node-executor.md +13 -3
- package/agents/zh/agentflow-node-executor.md +13 -3
- package/bin/agentflow.mjs +3 -3
- package/bin/lib/admin-builtin-pipelines.mjs +3 -2
- package/bin/lib/agent-runners.mjs +0 -929
- package/bin/lib/auth.mjs +2 -1
- package/bin/lib/catalog-agents.mjs +2 -1
- package/bin/lib/catalog-flows.mjs +167 -88
- package/bin/lib/composer-agent.mjs +3 -793
- package/bin/lib/composer-skill-router.mjs +3 -323
- package/bin/lib/exec-buffered.mjs +32 -0
- package/bin/lib/flow-dsl/cli.mjs +276 -0
- package/bin/lib/flow-dsl/codegen.mjs +358 -0
- package/bin/lib/flow-dsl/defs.mjs +103 -0
- package/bin/lib/flow-dsl/index.mjs +77 -0
- package/bin/lib/flow-dsl/ir.mjs +305 -0
- package/bin/lib/flow-dsl/layout.mjs +115 -0
- package/bin/lib/flow-dsl/legacy-yaml.mjs +268 -0
- package/bin/lib/flow-dsl/lint.mjs +241 -0
- package/bin/lib/flow-dsl/packages.mjs +251 -0
- package/bin/lib/flow-dsl/parser.mjs +444 -0
- package/bin/lib/flow-import.mjs +96 -26
- package/bin/lib/flow-write.mjs +48 -12
- package/bin/lib/help.mjs +10 -30
- package/bin/lib/html-escape.mjs +19 -0
- package/bin/lib/http-util.mjs +49 -0
- package/bin/lib/legacy-flow-execution.mjs +43 -0
- package/bin/lib/locales/en.json +1 -1
- package/bin/lib/locales/zh.json +1 -1
- package/bin/lib/main.mjs +153 -283
- package/bin/lib/marketplace.mjs +206 -61
- package/bin/lib/model-config.mjs +10 -0
- package/bin/lib/node-package-archive.mjs +240 -0
- package/bin/lib/node-package-bootstrap.mjs +72 -0
- package/bin/lib/node-package-manifest.mjs +174 -0
- package/bin/lib/paths.mjs +42 -19
- package/bin/lib/prd-workflow-routes.mjs +2949 -0
- package/bin/lib/prd-workflow-server.mjs +4884 -0
- package/bin/lib/run-events.mjs +0 -66
- package/bin/lib/schedule-config.mjs +4 -4
- package/bin/lib/skill-runtime.mjs +35 -0
- package/bin/lib/startup-storage-migrations.mjs +274 -0
- package/bin/lib/ui-server.mjs +1106 -18013
- package/bin/lib/user-env.mjs +16 -0
- package/bin/lib/workspace-auto-layout.mjs +234 -0
- package/bin/lib/workspace-flow-store.mjs +478 -0
- package/bin/lib/workspace-graph-merge.mjs +41 -34
- package/bin/lib/workspace-preview.mjs +74 -0
- package/bin/lib/workspace-routes.mjs +3088 -0
- package/bin/lib/workspace-server.mjs +6199 -0
- package/bin/lib/workspace-state.mjs +331 -0
- package/bin/lib/workspace.mjs +2 -1
- package/builtin/nodes/agent_subAgent.md +1 -0
- package/builtin/nodes/control_agent_toBool.md +2 -0
- package/builtin/nodes/control_anyOne.md +2 -0
- package/builtin/nodes/control_cancelled.md +2 -0
- package/builtin/nodes/control_cd_workspace.md +5 -11
- package/builtin/nodes/control_delay.md +2 -0
- package/builtin/nodes/control_end.md +2 -0
- package/builtin/nodes/control_if.md +1 -0
- package/builtin/nodes/control_interval_loop.md +2 -0
- package/builtin/nodes/control_load_mcp.md +24 -0
- package/builtin/nodes/control_load_skills.md +8 -23
- package/builtin/nodes/control_start.md +2 -0
- package/builtin/nodes/control_toBool.md +2 -0
- package/builtin/nodes/control_user_workspace.md +2 -0
- package/builtin/nodes/control_wait_until.md +2 -0
- package/builtin/nodes/display_ascii.md +1 -0
- package/builtin/nodes/display_chart.md +1 -0
- package/builtin/nodes/display_html.md +1 -0
- package/builtin/nodes/display_image.md +1 -0
- package/builtin/nodes/display_markdown.md +1 -0
- package/builtin/nodes/display_mermaid.md +1 -0
- package/builtin/nodes/display_react_app.md +1 -0
- package/builtin/nodes/display_table.md +1 -0
- package/builtin/nodes/provide_bool.md +1 -0
- package/builtin/nodes/provide_file.md +1 -0
- package/builtin/nodes/provide_password.md +1 -0
- package/builtin/nodes/provide_str.md +1 -0
- package/builtin/nodes/tool_display_share_link.md +1 -0
- package/builtin/nodes/tool_get_env.md +2 -0
- package/builtin/nodes/tool_git_checkout.md +1 -0
- package/builtin/nodes/tool_git_worktree_load.md +1 -0
- package/builtin/nodes/tool_git_worktree_unload.md +1 -0
- package/builtin/nodes/tool_gitlab_create_mr.md +1 -0
- package/builtin/nodes/tool_jenkins_build.md +2 -0
- package/builtin/nodes/tool_load_key.md +2 -0
- package/builtin/nodes/tool_nodejs.md +17 -19
- package/builtin/nodes/tool_print.md +2 -0
- package/builtin/nodes/tool_save_key.md +2 -0
- package/builtin/nodes/tool_set_run_env.md +1 -0
- package/builtin/nodes/tool_user_ask.md +2 -0
- package/builtin/nodes/tool_user_check.md +2 -0
- package/builtin/nodes/tool_wecom_send_app_markdown.md +1 -0
- package/builtin/nodes/tool_wecom_send_group_markdown.md +1 -0
- package/builtin/nodes/workspace_one_click_task.md +44 -0
- package/builtin/nodes/workspace_run.md +16 -0
- package/builtin/nodes/workspace_scheduled_run.md +16 -0
- package/builtin/pipelines/module-migrate/scripts/gate.mjs +37 -0
- package/builtin/pipelines/module-migrate/scripts/static-check.mjs +82 -0
- package/builtin/pipelines/module-migrate/workspace.flow.js +172 -0
- package/builtin/pipelines/module-migrate/workspace.layout.json +134 -0
- package/builtin/pipelines/new/scripts/lint-flow.mjs +38 -0
- package/builtin/pipelines/new/workspace.flow.js +91 -0
- package/builtin/pipelines/new/workspace.layout.json +70 -0
- package/builtin/web-ui/dist/assets/{WorkflowAssistantThread-DPbR2UaN.js → WorkflowAssistantThread-CKClwj96.js} +1 -1
- package/builtin/web-ui/dist/assets/index-BZ5KqLur.js +870 -0
- package/builtin/web-ui/dist/assets/index-CEXmmwM2.css +1 -0
- package/builtin/web-ui/dist/index.html +2 -2
- package/package.json +2 -1
- package/reference/flow-control-capabilities.md +77 -158
- package/reference/flow-layout.md +1 -1
- package/reference/flow-prompt-handler-check.md +2 -2
- package/skills/agentflow-author-flow/SKILL.md +8 -47
- package/skills/agentflow-cli/SKILL.md +158 -25
- package/skills/agentflow-cli/agents/openai.yaml +2 -2
- package/skills/agentflow-cli/scripts/agentflow-cli.mjs +668 -18
- package/skills/agentflow-cli/scripts/agentflow-runtime.mjs +97 -0
- package/skills/agentflow-flow-add-instances/SKILL.md +7 -248
- package/skills/agentflow-flow-dsl/SKILL.md +206 -0
- package/skills/agentflow-flow-dsl/agents/openai.yaml +4 -0
- package/skills/agentflow-flow-dsl/references/node-calls.md +39 -0
- package/skills/agentflow-flow-edit-node-fields/SKILL.md +4 -73
- package/skills/agentflow-flow-recipes/SKILL.md +7 -18
- package/skills/agentflow-flow-recipes/references/recipes.md +97 -43
- package/skills/agentflow-flow-sync-ui/SKILL.md +3 -54
- package/skills/agentflow-node-dsl/SKILL.md +212 -0
- package/skills/agentflow-node-dsl/agents/openai.yaml +4 -0
- package/skills/agentflow-node-reference/SKILL.md +5 -4
- package/skills/agentflow-node-reference/references/builtin-nodes.md +166 -115
- package/skills/agentflow-placeholder-reference/SKILL.md +2 -2
- package/skills/agentflow-runtime-reference/SKILL.md +2 -2
- package/skills/agentflow-runtime-reference/references/runtime.md +1 -1
- package/skills/agentflow-workspace-ascii/SKILL.md +9 -16
- package/skills/agentflow-workspace-graph/SKILL.md +62 -48
- package/skills/agentflow-workspace-html/SKILL.md +7 -2
- package/skills/agentflow-workspace-image/SKILL.md +6 -2
- package/skills/agentflow-workspace-markdown/SKILL.md +14 -21
- package/skills/agentflow-workspace-mermaid/SKILL.md +8 -16
- package/bin/lib/api-runner.mjs +0 -387
- package/bin/lib/apply.mjs +0 -903
- package/bin/lib/composer-flow-instances.mjs +0 -68
- package/bin/lib/composer-flow-skeleton.mjs +0 -334
- package/bin/lib/composer-flow-validate.mjs +0 -47
- package/bin/lib/composer-model-router.mjs +0 -185
- package/bin/lib/composer-node-schema.mjs +0 -303
- package/bin/lib/composer-planner.mjs +0 -751
- package/bin/lib/composer-script-ops.mjs +0 -233
- package/bin/lib/flow-static-preview.mjs +0 -104
- package/bin/lib/hub-login.mjs +0 -54
- package/bin/lib/hub-publish.mjs +0 -159
- package/bin/lib/hub-remote.mjs +0 -189
- package/bin/lib/hub.mjs +0 -299
- package/bin/lib/jenkins.mjs +0 -380
- package/bin/lib/node-execute.mjs +0 -539
- package/bin/lib/normalize-node-tool-command.mjs +0 -97
- package/bin/lib/runtime-context.mjs +0 -243
- package/bin/lib/scheduler.mjs +0 -601
- package/bin/lib/ui-print.mjs +0 -94
- package/bin/pipeline/build-node-prompt.mjs +0 -271
- package/bin/pipeline/check-cache.mjs +0 -191
- package/bin/pipeline/check-flow.mjs +0 -543
- package/bin/pipeline/collect-nodes.mjs +0 -212
- package/bin/pipeline/compute-cache-md5.mjs +0 -177
- package/bin/pipeline/ensure-run-dir.mjs +0 -71
- package/bin/pipeline/gc.mjs +0 -129
- package/bin/pipeline/get-env.mjs +0 -59
- package/bin/pipeline/get-resolved-values.mjs +0 -344
- package/bin/pipeline/load-key.mjs +0 -62
- package/bin/pipeline/parse-flow.mjs +0 -708
- package/bin/pipeline/post-process-control-if.mjs +0 -23
- package/bin/pipeline/post-process-node.mjs +0 -490
- package/bin/pipeline/pre-process-node.mjs +0 -1430
- package/bin/pipeline/resolve-inputs.mjs +0 -201
- package/bin/pipeline/run-tool-nodejs.mjs +0 -167
- package/bin/pipeline/save-key.mjs +0 -93
- package/bin/pipeline/snapshot-prior-round.mjs +0 -70
- package/bin/pipeline/validate-for-ui.mjs +0 -234
- package/bin/pipeline/validate-script-output.mjs +0 -130
- package/bin/pipeline/write-result.mjs +0 -182
- package/builtin/pipelines/module-migrate/flow.yaml +0 -819
- package/builtin/pipelines/new/flow.yaml +0 -545
- package/builtin/pipelines/new/scripts/check-flow.mjs +0 -9
- package/builtin/pipelines/new/scripts/collect-nodes.mjs +0 -211
- package/builtin/web-ui/dist/assets/index-BSCpd5la.js +0 -889
- package/builtin/web-ui/dist/assets/index-DZ328oSo.css +0 -1
- package/skills/agentflow-node-authoring/SKILL.md +0 -57
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 流程目录里的代码节点包(`<flowDir>/nodes/<name>/index.mjs`)。
|
|
3
|
+
*
|
|
4
|
+
* **lint 和存储层必须用同一份扫描。** 分成两份的后果实际发生过:lint 自己解析包、看到
|
|
5
|
+
* 的是对的图,存储层不解析、把 `import x from "./nodes/x"` 读成一个槽位表为空的
|
|
6
|
+
* `pkg:./nodes/x`,于是控制边落到第一个数据槽上、两条边撞同一个句柄。AI 照文档写完
|
|
7
|
+
* lint 绿灯,画布上却是一张错图。
|
|
8
|
+
*
|
|
9
|
+
* 一个包在图里的形态与画布从面板拖出来的**完全一致**:`definitionId` 是基础类型
|
|
10
|
+
* (`tool_nodejs` 等),包的身份放 `marketplaceRef`。代码里则渲染成 import + 调用,
|
|
11
|
+
* 那只是同一件事的可读写法。
|
|
12
|
+
*/
|
|
13
|
+
import fs from "fs";
|
|
14
|
+
import path from "path";
|
|
15
|
+
import { parse as acornParse } from "acorn";
|
|
16
|
+
|
|
17
|
+
import { isNodePackageDir, readNodePackageManifest, slotMapToList } from "../node-package-manifest.mjs";
|
|
18
|
+
import { listMarketplaceNodes, parseMarketplaceDefinitionId } from "../marketplace.mjs";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 从结构代码里提取远端节点依赖。import 本身就是依赖声明,不能再维护第二份 lock 清单。
|
|
22
|
+
* 这里只看静态 ImportDeclaration,和 DSL 解析器的边界一致。
|
|
23
|
+
*
|
|
24
|
+
* @returns {{ dependencies: Array<{id:string,version:string,specifier:string,line:number}>, errors: string[] }}
|
|
25
|
+
*/
|
|
26
|
+
export function marketplaceDependenciesFromSource(source) {
|
|
27
|
+
let ast;
|
|
28
|
+
try {
|
|
29
|
+
ast = acornParse(String(source || ""), {
|
|
30
|
+
ecmaVersion: 2022,
|
|
31
|
+
sourceType: "module",
|
|
32
|
+
locations: true,
|
|
33
|
+
});
|
|
34
|
+
} catch (error) {
|
|
35
|
+
return { dependencies: [], errors: [`workspace.flow.js 语法错误:${error?.message || String(error)}`] };
|
|
36
|
+
}
|
|
37
|
+
const bySpecifier = new Map();
|
|
38
|
+
const errors = [];
|
|
39
|
+
for (const statement of ast.body) {
|
|
40
|
+
if (statement.type !== "ImportDeclaration") continue;
|
|
41
|
+
const specifier = String(statement.source?.value || "").trim();
|
|
42
|
+
if (!specifier.startsWith("marketplace:")) continue;
|
|
43
|
+
const parsed = parseMarketplaceDefinitionId(specifier);
|
|
44
|
+
const line = Number(statement.loc?.start?.line) || 0;
|
|
45
|
+
if (!parsed?.id) {
|
|
46
|
+
errors.push(`第 ${line} 行:无效的 marketplace 节点引用 ${JSON.stringify(specifier)}`);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (!parsed.version) {
|
|
50
|
+
errors.push(`第 ${line} 行:${specifier} 没有固定版本;请使用 marketplace:<id>@<version>`);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
bySpecifier.set(specifier, { id: parsed.id, version: parsed.version, specifier, line });
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
dependencies: [...bySpecifier.values()].sort((a, b) => a.specifier.localeCompare(b.specifier)),
|
|
57
|
+
errors,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 把流程目录里的相对节点包 import 改成可分发的固定版本 import。
|
|
63
|
+
*
|
|
64
|
+
* 只替换 ImportDeclaration 的字符串字面量,不碰用户文件,也不做正则替换;因此注释、
|
|
65
|
+
* 普通字符串和动态 import 不会被误伤。调用方可以把返回的 source 作为一次性的发布产物。
|
|
66
|
+
*
|
|
67
|
+
* @param {string} source
|
|
68
|
+
* @param {{bySpecifier?: Record<string, object>}} packages scanFlowLocalPackages 的结果
|
|
69
|
+
* @returns {{source:string, dependencies:object[], rewritten:object[]}}
|
|
70
|
+
*/
|
|
71
|
+
export function rewriteFlowLocalPackageImports(source, packages) {
|
|
72
|
+
const text = String(source || "");
|
|
73
|
+
const ast = acornParse(text, {
|
|
74
|
+
ecmaVersion: 2022,
|
|
75
|
+
sourceType: "module",
|
|
76
|
+
locations: true,
|
|
77
|
+
});
|
|
78
|
+
const replacements = [];
|
|
79
|
+
const byRef = new Map();
|
|
80
|
+
for (const statement of ast.body) {
|
|
81
|
+
if (statement.type !== "ImportDeclaration") continue;
|
|
82
|
+
const specifier = String(statement.source?.value || "");
|
|
83
|
+
const record = packages?.bySpecifier?.[specifier];
|
|
84
|
+
if (specifier.startsWith("./nodes/") && !record) {
|
|
85
|
+
throw new Error(`第 ${Number(statement.loc?.start?.line) || 0} 行:节点包不存在或声明无效 ${specifier}`);
|
|
86
|
+
}
|
|
87
|
+
if (!record) continue;
|
|
88
|
+
const marketplaceRef = String(record.marketplaceRef || "").trim();
|
|
89
|
+
const parsed = parseMarketplaceDefinitionId(marketplaceRef);
|
|
90
|
+
if (!parsed?.id || !parsed.version) {
|
|
91
|
+
throw new Error(`${specifier} 没有可发布的固定版本 marketplaceRef`);
|
|
92
|
+
}
|
|
93
|
+
replacements.push({
|
|
94
|
+
start: statement.source.start,
|
|
95
|
+
end: statement.source.end,
|
|
96
|
+
value: JSON.stringify(marketplaceRef),
|
|
97
|
+
specifier,
|
|
98
|
+
marketplaceRef,
|
|
99
|
+
line: Number(statement.loc?.start?.line) || 0,
|
|
100
|
+
});
|
|
101
|
+
byRef.set(marketplaceRef, {
|
|
102
|
+
id: parsed.id,
|
|
103
|
+
version: parsed.version,
|
|
104
|
+
specifier: marketplaceRef,
|
|
105
|
+
line: Number(statement.loc?.start?.line) || 0,
|
|
106
|
+
packageDir: record.packageDir || "",
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
let portable = text;
|
|
110
|
+
for (const replacement of replacements.sort((a, b) => b.start - a.start)) {
|
|
111
|
+
portable = `${portable.slice(0, replacement.start)}${replacement.value}${portable.slice(replacement.end)}`;
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
source: portable,
|
|
115
|
+
dependencies: [...byRef.values()].sort((a, b) => a.specifier.localeCompare(b.specifier)),
|
|
116
|
+
rewritten: replacements
|
|
117
|
+
.sort((a, b) => a.start - b.start)
|
|
118
|
+
.map(({ specifier, marketplaceRef, line }) => ({ specifier, marketplaceRef, line })),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** 包没写 baseDefinitionId 时按什么算——和 catalog 面板那边同一套回落。 */
|
|
123
|
+
function baseDefinitionIdOf(manifest) {
|
|
124
|
+
return String(manifest.baseDefinitionId || manifest.runtime?.type || "tool_nodejs").trim();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* 扫 `<flowDir>/nodes/*`。
|
|
129
|
+
*
|
|
130
|
+
* @param {string} flowDir
|
|
131
|
+
* @returns {{ bySpecifier: Record<string, object>, byRef: Record<string, object>, list: object[] }}
|
|
132
|
+
*/
|
|
133
|
+
export function scanFlowLocalPackages(flowDir) {
|
|
134
|
+
const bySpecifier = {};
|
|
135
|
+
const byRef = {};
|
|
136
|
+
const list = [];
|
|
137
|
+
const root = path.join(String(flowDir || ""), "nodes");
|
|
138
|
+
let entries;
|
|
139
|
+
try {
|
|
140
|
+
entries = fs.readdirSync(root, { withFileTypes: true });
|
|
141
|
+
} catch {
|
|
142
|
+
return { bySpecifier, byRef, list };
|
|
143
|
+
}
|
|
144
|
+
for (const entry of entries) {
|
|
145
|
+
if (!entry.isDirectory()) continue;
|
|
146
|
+
const dir = path.join(root, entry.name);
|
|
147
|
+
if (!isNodePackageDir(dir)) continue;
|
|
148
|
+
let manifest = null;
|
|
149
|
+
try {
|
|
150
|
+
// 只认 index.mjs 的静态声明;node.yaml 的老包由 marketplace 那条路负责
|
|
151
|
+
manifest = readNodePackageManifest(dir, () => null);
|
|
152
|
+
} catch {
|
|
153
|
+
manifest = null;
|
|
154
|
+
}
|
|
155
|
+
if (!manifest?.id) continue;
|
|
156
|
+
const record = {
|
|
157
|
+
id: manifest.id,
|
|
158
|
+
version: String(manifest.version || ""),
|
|
159
|
+
dirName: entry.name,
|
|
160
|
+
packageDir: dir,
|
|
161
|
+
specifier: `./nodes/${entry.name}`,
|
|
162
|
+
marketplaceRef: manifest.definitionId || `marketplace:${manifest.id}@${manifest.version}`,
|
|
163
|
+
baseDefinitionId: baseDefinitionIdOf(manifest),
|
|
164
|
+
input: manifest.input || slotMapToList({}, "input"),
|
|
165
|
+
output: manifest.output || slotMapToList({}, "output"),
|
|
166
|
+
};
|
|
167
|
+
bySpecifier[record.specifier] = record;
|
|
168
|
+
bySpecifier[`${record.specifier}/index.mjs`] = record;
|
|
169
|
+
byRef[record.marketplaceRef] = record;
|
|
170
|
+
list.push(record);
|
|
171
|
+
}
|
|
172
|
+
return { bySpecifier, byRef, list };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Resolve both flow-local packages and packages installed in the workspace
|
|
177
|
+
* marketplace. The latter use `marketplace:<id>@<version>` as their DSL import
|
|
178
|
+
* specifier, so package identity is present in workspace.flow.js itself.
|
|
179
|
+
*/
|
|
180
|
+
export function scanAvailableNodePackages(flowDir, workspaceRoot = "") {
|
|
181
|
+
const local = scanFlowLocalPackages(flowDir);
|
|
182
|
+
if (!workspaceRoot) return local;
|
|
183
|
+
const bySpecifier = {};
|
|
184
|
+
const byRef = {};
|
|
185
|
+
const list = [];
|
|
186
|
+
for (const manifest of listMarketplaceNodes(workspaceRoot)) {
|
|
187
|
+
const marketplaceRef = manifest.definitionId || `marketplace:${manifest.id}@${manifest.version}`;
|
|
188
|
+
const record = {
|
|
189
|
+
id: manifest.id,
|
|
190
|
+
version: String(manifest.version || ""),
|
|
191
|
+
dirName: manifest.id,
|
|
192
|
+
specifier: marketplaceRef,
|
|
193
|
+
marketplaceRef,
|
|
194
|
+
baseDefinitionId: baseDefinitionIdOf(manifest),
|
|
195
|
+
input: manifest.input || slotMapToList({}, "input"),
|
|
196
|
+
output: manifest.output || slotMapToList({}, "output"),
|
|
197
|
+
source: manifest.source || "marketplace",
|
|
198
|
+
};
|
|
199
|
+
bySpecifier[record.specifier] = record;
|
|
200
|
+
byRef[record.marketplaceRef] = record;
|
|
201
|
+
list.push(record);
|
|
202
|
+
}
|
|
203
|
+
// A flow-local package intentionally wins over an installed package with the
|
|
204
|
+
// same id/version; runtime resolution follows the same order.
|
|
205
|
+
Object.assign(bySpecifier, local.bySpecifier);
|
|
206
|
+
Object.assign(byRef, local.byRef);
|
|
207
|
+
const localRefs = new Set(local.list.map((item) => item.marketplaceRef));
|
|
208
|
+
return {
|
|
209
|
+
bySpecifier,
|
|
210
|
+
byRef,
|
|
211
|
+
list: [...list.filter((item) => !localRefs.has(item.marketplaceRef)), ...local.list],
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** 给 `parseFlowSource` 用的 import 解析器。 */
|
|
216
|
+
export function packageResolverFor(packages) {
|
|
217
|
+
return (specifier) => packages.bySpecifier[String(specifier || "")] || null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* 给 `generateFlowSource` 用的 `opts.packages`:nodeId -> 包。
|
|
222
|
+
* 本地包写相对 import,已安装包写 `marketplace:<id>@<version>` import。
|
|
223
|
+
*/
|
|
224
|
+
export function packageBindingsForGraph(graph, packages) {
|
|
225
|
+
const out = {};
|
|
226
|
+
const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
|
|
227
|
+
const used = new Set();
|
|
228
|
+
for (const [nodeId, instance] of Object.entries(instances)) {
|
|
229
|
+
const ref = String(instance?.marketplaceRef || "").trim();
|
|
230
|
+
const record = ref ? packages.byRef[ref] : null;
|
|
231
|
+
if (!record) continue;
|
|
232
|
+
let binding = safeBindingName(record.dirName);
|
|
233
|
+
while (used.has(binding)) binding = `${binding}_`;
|
|
234
|
+
used.add(binding);
|
|
235
|
+
out[nodeId] = { ...record, binding };
|
|
236
|
+
}
|
|
237
|
+
return out;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** `count-lines` -> `countLines`;保证是合法且不与 API 命名空间撞车的标识符。 */
|
|
241
|
+
const RESERVED_BINDINGS = new Set([
|
|
242
|
+
"agent", "control", "display", "file", "flow", "provide", "tool", "workspace",
|
|
243
|
+
]);
|
|
244
|
+
|
|
245
|
+
export function safeBindingName(dirName) {
|
|
246
|
+
const camel = String(dirName || "node")
|
|
247
|
+
.replace(/[^A-Za-z0-9]+(.)?/g, (_, ch) => (ch ? ch.toUpperCase() : ""))
|
|
248
|
+
.replace(/^[^A-Za-z_$]+/, "");
|
|
249
|
+
const base = camel || "node";
|
|
250
|
+
return RESERVED_BINDINGS.has(base) ? `${base}Node` : base;
|
|
251
|
+
}
|
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `workspace.flow.js` -> IR。
|
|
3
|
+
*
|
|
4
|
+
* **绝不执行代码**——用 acorn 取 AST 后静态读出来。画布渲染、lint、导入都走这条路径,
|
|
5
|
+
* 执行第三方流程文件去画一张图既慢又不安全。
|
|
6
|
+
*
|
|
7
|
+
* 代价是能识别的写法有限:import、`const x = call(...)`、解构声明、以及
|
|
8
|
+
* `flow.resume` / `flow.detached` 这两个表达式语句。
|
|
9
|
+
*
|
|
10
|
+
* 认不出来的东西**一律记进 `unresolved`**,绝不静默跳过。这个文件是流程的权威表示,
|
|
11
|
+
* 「解析器看不懂就当它不存在」等于:画布少画一个节点,用户随手一保存,那个节点就从
|
|
12
|
+
* 磁盘上消失了。调用方(`flowFilesToGraph` 默认、以及 lint)据此决定是报错还是列出来。
|
|
13
|
+
*/
|
|
14
|
+
import { parse as acornParse } from "acorn";
|
|
15
|
+
|
|
16
|
+
import { STD_SLOTS, definitionOf, definitionIdFromApi } from "./defs.mjs";
|
|
17
|
+
|
|
18
|
+
const calleePath = (n) => (
|
|
19
|
+
n.type === "Identifier"
|
|
20
|
+
? n.name
|
|
21
|
+
: (n.type === "MemberExpression" && !n.computed ? `${calleePath(n.object)}.${n.property.name}` : null)
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* `tool_nodejs` 脚本里除槽名之外还能用的占位符常量(见运行时的 `constants` 表)。
|
|
26
|
+
* 它们不是引脚,`${flowDir}` 这种写法必须原样留在脚本里,不能被当成 JS 插值。
|
|
27
|
+
*/
|
|
28
|
+
const SCRIPT_CONSTANTS = new Set([
|
|
29
|
+
"workspaceRoot",
|
|
30
|
+
"pipelineWorkspace",
|
|
31
|
+
"flowDir",
|
|
32
|
+
"cwd",
|
|
33
|
+
"nodeRunDir",
|
|
34
|
+
"nodeTmpDir",
|
|
35
|
+
"outputsDir",
|
|
36
|
+
"scriptRef",
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
const memberPath = (n) => (
|
|
40
|
+
n.type === "MemberExpression" && !n.computed
|
|
41
|
+
&& n.object.type === "Identifier" && n.property.type === "Identifier"
|
|
42
|
+
? [n.object.name, n.property.name]
|
|
43
|
+
: null
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @param {string} source workspace.flow.js
|
|
48
|
+
* @param {{ files?: Record<string,string>, resolvePackage?: (specifier: string) => {definitionId?: string} }} [opts]
|
|
49
|
+
* files:`file("...")` 引用的外置文本,键是相对路径
|
|
50
|
+
* @returns {{ nodes: object, edges: string[] }}
|
|
51
|
+
*/
|
|
52
|
+
export function parseFlowSource(source, opts = {}) {
|
|
53
|
+
const files = opts.files || {};
|
|
54
|
+
const ast = acornParse(source, { ecmaVersion: 2022, sourceType: "module", locations: true });
|
|
55
|
+
|
|
56
|
+
/** 解析器认不出、因此没有进入图的东西。 */
|
|
57
|
+
const unresolved = [];
|
|
58
|
+
const unresolvedAt = (node, message) => {
|
|
59
|
+
unresolved.push({ line: node?.loc?.start?.line || 0, message });
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
function stringOf(node) {
|
|
63
|
+
if (!node) return null;
|
|
64
|
+
if (node.type === "Literal" && typeof node.value === "string") return node.value;
|
|
65
|
+
// 槽值在图里统一是字符串。`true` / `42` 是 JS 里写这类值的自然写法,收下并规范化,
|
|
66
|
+
// 不要逼作者写 `"true"`;写回时由槽的 type 决定加不加引号。
|
|
67
|
+
if (node.type === "Literal" && (typeof node.value === "boolean" || typeof node.value === "number")) {
|
|
68
|
+
return String(node.value);
|
|
69
|
+
}
|
|
70
|
+
if (node.type === "UnaryExpression" && (node.operator === "-" || node.operator === "+")
|
|
71
|
+
&& node.argument.type === "Literal" && typeof node.argument.value === "number") {
|
|
72
|
+
return String(node.operator === "-" ? -node.argument.value : node.argument.value);
|
|
73
|
+
}
|
|
74
|
+
if (node.type === "TemplateLiteral" && node.expressions.length === 0) return node.quasis[0].value.cooked;
|
|
75
|
+
if (node.type === "CallExpression" && calleePath(node.callee) === "file") {
|
|
76
|
+
const rel = stringOf(node.arguments[0]);
|
|
77
|
+
if (rel == null) return null;
|
|
78
|
+
if (!(rel in files)) throw new Error(`file(${JSON.stringify(rel)}) 找不到对应文件`);
|
|
79
|
+
return files[rel];
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// import 绑定 -> 代码节点包
|
|
85
|
+
const packageOf = new Map();
|
|
86
|
+
for (const stmt of ast.body) {
|
|
87
|
+
if (stmt.type !== "ImportDeclaration") continue;
|
|
88
|
+
const specifier = String(stmt.source.value);
|
|
89
|
+
if (specifier === "agentflow/flow") continue;
|
|
90
|
+
const def = stmt.specifiers.find((s) => s.type === "ImportDefaultSpecifier");
|
|
91
|
+
if (def) {
|
|
92
|
+
packageOf.set(def.local.name, {
|
|
93
|
+
specifier,
|
|
94
|
+
...(opts.resolvePackage ? opts.resolvePackage(specifier) || {} : {}),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const nodes = {};
|
|
100
|
+
const edges = [];
|
|
101
|
+
const varOf = new Map();
|
|
102
|
+
const pendingIf = [];
|
|
103
|
+
const runDecls = [];
|
|
104
|
+
const destructures = [];
|
|
105
|
+
|
|
106
|
+
// 先扫一遍解构声明:`const { storyId } = node;` 让后面引用 storyId 时能还原成边
|
|
107
|
+
for (const stmt of ast.body) {
|
|
108
|
+
const decl = stmt.type === "ExportNamedDeclaration" ? stmt.declaration : stmt;
|
|
109
|
+
if (decl?.type !== "VariableDeclaration") continue;
|
|
110
|
+
const d = decl.declarations[0];
|
|
111
|
+
if (d?.id.type === "ObjectPattern" && d.init?.type === "Identifier") {
|
|
112
|
+
for (const prop of d.id.properties) {
|
|
113
|
+
const slot = prop.key.name ?? prop.key.value;
|
|
114
|
+
varOf.set(prop.value.type === "Identifier" ? prop.value.name : slot, [d.init.name, slot]);
|
|
115
|
+
}
|
|
116
|
+
destructures.push({ node: d.init.name, slots: d.id.properties.map((p) => p.key.name ?? p.key.value) });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 带插值的模板字符串 -> 正文文本 + 数据边。
|
|
122
|
+
*
|
|
123
|
+
* 运行时的正文占位符只认槽名(`${slotName}`,见 `workspaceBodyPlaceholderNames` 的正则,
|
|
124
|
+
* 里面没有 `.`),所以 `` `分析 ${date.value} 的数据` `` 不能原样落进正文——它编译成
|
|
125
|
+
* 正文 `分析 ${date} 的数据` 加一条 `date.value -> 本节点.date` 的边。槽名取引用表达式
|
|
126
|
+
* 的**根标识符**,这样写回时(codegen)能原样还原成同一段代码。
|
|
127
|
+
*
|
|
128
|
+
* @returns {{ text: string, refs: Array<{slot,src,srcSlot}> } | { error: string, node } | null}
|
|
129
|
+
*/
|
|
130
|
+
function interpolatedOf(id, tpl, definitionId) {
|
|
131
|
+
if (tpl?.type !== "TemplateLiteral" || !tpl.expressions.length) return null;
|
|
132
|
+
// 本节点自己就有这个名字 -> `${x}` 是运行时占位符,原样留在正文里,不是 JS 插值
|
|
133
|
+
const def = definitionOf(definitionId);
|
|
134
|
+
const isScript = definitionId === "tool_nodejs";
|
|
135
|
+
const ownSlot = (name) => def.input.some((s) => s.name === name)
|
|
136
|
+
|| nodes[id].extraIn.includes(name)
|
|
137
|
+
|| nodes[id].inputs[name] !== undefined
|
|
138
|
+
|| edges.some((e) => e.endsWith(`|${id}|${name}`))
|
|
139
|
+
// 脚本里还能引用常量和自己的输出槽(运行时会把它们填成可写文件路径)
|
|
140
|
+
|| (isScript && (
|
|
141
|
+
SCRIPT_CONSTANTS.has(name)
|
|
142
|
+
|| def.output.some((s) => s.name === name)
|
|
143
|
+
|| destructures.some((d) => d.node === id && d.slots.includes(name))
|
|
144
|
+
));
|
|
145
|
+
|
|
146
|
+
const refs = [];
|
|
147
|
+
let text = tpl.quasis[0].value.cooked;
|
|
148
|
+
for (let i = 0; i < tpl.expressions.length; i += 1) {
|
|
149
|
+
const expr = tpl.expressions[i];
|
|
150
|
+
if (expr.type === "Identifier" && ownSlot(expr.name)) {
|
|
151
|
+
text += `\${${expr.name}}${tpl.quasis[i + 1].value.cooked}`;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
const member = memberPath(expr);
|
|
155
|
+
let slot;
|
|
156
|
+
let src;
|
|
157
|
+
let srcSlot;
|
|
158
|
+
if (member) {
|
|
159
|
+
[src, srcSlot] = member;
|
|
160
|
+
slot = src;
|
|
161
|
+
} else if (expr.type === "Identifier" && varOf.has(expr.name)) {
|
|
162
|
+
[src, srcSlot] = varOf.get(expr.name);
|
|
163
|
+
slot = expr.name;
|
|
164
|
+
} else {
|
|
165
|
+
return { error: `${id}: 模板插值只能引用上游节点的输出(\`\${节点.槽}\` 或解构出来的变量)`, node: expr };
|
|
166
|
+
}
|
|
167
|
+
refs.push({ slot, src, srcSlot });
|
|
168
|
+
text += `\${${slot}}${tpl.quasis[i + 1].value.cooked}`;
|
|
169
|
+
}
|
|
170
|
+
return { text, refs };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function readPins(id, definitionId, obj) {
|
|
174
|
+
const def = definitionOf(definitionId);
|
|
175
|
+
const defInputs = new Set(def.input.map((s) => s.name));
|
|
176
|
+
const defOutputs = new Set(def.output.map((s) => s.name));
|
|
177
|
+
const node = nodes[id];
|
|
178
|
+
if (!obj || obj.type !== "ObjectExpression") return;
|
|
179
|
+
for (const prop of obj.properties) {
|
|
180
|
+
if (prop.type !== "Property") {
|
|
181
|
+
unresolvedAt(prop, `${id}: 引脚对象里出现了展开运算,静态解析读不出引脚`);
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (prop.computed) {
|
|
185
|
+
unresolvedAt(prop, `${id}: 引脚名不能是动态表达式`);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
const key = prop.key.name ?? prop.key.value;
|
|
189
|
+
const isCustom = !STD_SLOTS.has(key) && !defInputs.has(key);
|
|
190
|
+
|
|
191
|
+
const member = memberPath(prop.value);
|
|
192
|
+
if (member) {
|
|
193
|
+
edges.push(`${member[0]}|${member[1]}|${id}|${key}`);
|
|
194
|
+
if (isCustom) node.extraIn.push(key);
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
if (prop.value.type === "Identifier" && varOf.has(prop.value.name)) {
|
|
198
|
+
const [src, slot] = varOf.get(prop.value.name);
|
|
199
|
+
edges.push(`${src}|${slot}|${id}|${key}`);
|
|
200
|
+
if (isCustom) node.extraIn.push(key);
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
// `{ url: null }` = 声明了槽但没接线,槽要存在、值为空
|
|
204
|
+
if (prop.value.type === "Literal" && prop.value.value === null) {
|
|
205
|
+
if (isCustom) node.extraIn.push(key);
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
const text = stringOf(prop.value);
|
|
209
|
+
if (text === null) {
|
|
210
|
+
unresolvedAt(prop, `${id}.${key}: 引脚值既不是上游引用也不是字面量`);
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
// provide_* 的值写在引脚对象里,但它其实是输出槽
|
|
214
|
+
if (defOutputs.has(key) && !defInputs.has(key)) {
|
|
215
|
+
node.outputs[key] = text;
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
node.inputs[key] = text;
|
|
219
|
+
// 自定义槽的类型代码里没别处写,只能从字面量的种类看出来。记下来,`irToGraph`
|
|
220
|
+
// 才能把槽建成 bool,写回时也才知道该写 `true` 而不是 `"true"`。
|
|
221
|
+
if (isCustom && prop.value.type === "Literal" && typeof prop.value.value === "boolean") {
|
|
222
|
+
node.inputTypes[key] = "bool";
|
|
223
|
+
}
|
|
224
|
+
if (isCustom) node.extraIn.push(key);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const itemsOf = (call) => {
|
|
229
|
+
const out = [];
|
|
230
|
+
for (const arg of call.arguments) {
|
|
231
|
+
if (arg.type === "Identifier") out.push(arg.name);
|
|
232
|
+
else if (arg.type === "CallExpression" && calleePath(arg.callee) === "flow.fork") {
|
|
233
|
+
out.push({ fork: arg.arguments.map(itemsOf) });
|
|
234
|
+
} else {
|
|
235
|
+
unresolvedAt(arg, "控制流参数只能是节点变量名或 flow.fork(...)");
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return out;
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
function linkChain(head, items) {
|
|
242
|
+
let prev = head;
|
|
243
|
+
let slot = "next";
|
|
244
|
+
for (const item of items) {
|
|
245
|
+
if (typeof item === "object" && item.fork) {
|
|
246
|
+
for (const seq of item.fork) linkChain(prev, seq);
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
if (prev) edges.push(`${prev}|${slot}|${item}|prev`);
|
|
250
|
+
prev = item;
|
|
251
|
+
slot = "next";
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
for (const stmt of ast.body) {
|
|
256
|
+
if (stmt.type === "ImportDeclaration") continue;
|
|
257
|
+
const decl = stmt.type === "ExportNamedDeclaration" ? stmt.declaration : stmt;
|
|
258
|
+
|
|
259
|
+
if (decl?.type === "VariableDeclaration") {
|
|
260
|
+
if (decl.declarations.length !== 1) {
|
|
261
|
+
unresolvedAt(decl, "一条 const 只能声明一个节点");
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
const d = decl.declarations[0];
|
|
265
|
+
if (d.id.type === "ObjectPattern") {
|
|
266
|
+
// 前面那趟已经处理过;只有 `const {a} = notAnIdentifier` 会漏下来
|
|
267
|
+
if (d.init?.type !== "Identifier") unresolvedAt(d, "解构只能来自一个节点变量");
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
if (d.id.type !== "Identifier") {
|
|
271
|
+
unresolvedAt(d, "节点声明左边必须是一个变量名");
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
const id = d.id.name;
|
|
275
|
+
const init = d.init;
|
|
276
|
+
if (init?.type !== "CallExpression") {
|
|
277
|
+
unresolvedAt(d, `${id}: 节点声明右边必须是一次节点调用`);
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
const path = calleePath(init.callee);
|
|
281
|
+
const args = [...init.arguments];
|
|
282
|
+
|
|
283
|
+
const first = args[0];
|
|
284
|
+
const hasLabel = first
|
|
285
|
+
&& ((first.type === "Literal" && typeof first.value === "string") || first.type === "TemplateLiteral");
|
|
286
|
+
const label = hasLabel ? stringOf(args.shift()) : null;
|
|
287
|
+
|
|
288
|
+
if (path === "flow" || path === "flow.schedule") {
|
|
289
|
+
const body = path === "flow.schedule" ? stringOf(args.shift()) : null;
|
|
290
|
+
runDecls.push({
|
|
291
|
+
id,
|
|
292
|
+
definitionId: path === "flow.schedule" ? "workspace_scheduled_run" : "workspace_run",
|
|
293
|
+
items: itemsOf({ arguments: args }),
|
|
294
|
+
label,
|
|
295
|
+
body,
|
|
296
|
+
});
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const pkg = packageOf.get(path);
|
|
301
|
+
// 代码节点包在图里就是「基础类型 + marketplaceRef」,和画布从面板拖出来的一模一样。
|
|
302
|
+
// 解析不出包时退回 `pkg:<specifier>`——那是一张读不出槽位的图,交给上层报错,
|
|
303
|
+
// 绝不能假装它是个正常节点。
|
|
304
|
+
const definitionId = pkg
|
|
305
|
+
? (pkg.baseDefinitionId || pkg.definitionId || `pkg:${pkg.specifier}`)
|
|
306
|
+
: definitionIdFromApi(path);
|
|
307
|
+
nodes[id] = {
|
|
308
|
+
definitionId,
|
|
309
|
+
inputs: {},
|
|
310
|
+
inputTypes: {},
|
|
311
|
+
outputs: {},
|
|
312
|
+
extraIn: [],
|
|
313
|
+
extraOut: [],
|
|
314
|
+
declaredOut: [],
|
|
315
|
+
attrs: {},
|
|
316
|
+
};
|
|
317
|
+
if (label) nodes[id].label = label;
|
|
318
|
+
if (pkg) {
|
|
319
|
+
nodes[id].package = pkg.specifier;
|
|
320
|
+
nodes[id].packageBinding = path;
|
|
321
|
+
// 包声明的槽位相对基础类型是「自定义槽」,得先建出来,否则 `x.total` 这条边
|
|
322
|
+
// 找不到落点,句柄下标会串到别的槽上
|
|
323
|
+
// 包自己就是这个节点的定义表——槽位以它为准,不能拿基础类型的
|
|
324
|
+
// (`tool_nodejs` 带着 workspaceContext / skillsContext 这些上下文槽,代码节点没有)
|
|
325
|
+
if (pkg.input || pkg.output) {
|
|
326
|
+
nodes[id].packageDef = { input: pkg.input || [], output: pkg.output || [] };
|
|
327
|
+
}
|
|
328
|
+
if (pkg.marketplaceRef) nodes[id].attrs.marketplaceRef = pkg.marketplaceRef;
|
|
329
|
+
if (pkg.id) nodes[id].attrs.marketplacePackageId = pkg.id;
|
|
330
|
+
if (pkg.version) nodes[id].attrs.marketplaceVersion = pkg.version;
|
|
331
|
+
const def = definitionOf(definitionId);
|
|
332
|
+
for (const [kind, declared, defSlots] of [
|
|
333
|
+
["extraIn", pkg.input || [], def.input],
|
|
334
|
+
["extraOut", pkg.output || [], def.output],
|
|
335
|
+
]) {
|
|
336
|
+
for (const slot of declared) {
|
|
337
|
+
const name = String(slot?.name || "").trim();
|
|
338
|
+
if (!name || STD_SLOTS.has(name) || defSlots.some((s) => s.name === name)) continue;
|
|
339
|
+
if (!nodes[id][kind].includes(name)) nodes[id][kind].push(name);
|
|
340
|
+
// 包已经声明过了,不必再 `const { total } = x` 解构一遍
|
|
341
|
+
if (kind === "extraOut" && !nodes[id].declaredOut.includes(name)) nodes[id].declaredOut.push(name);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
readPins(id, definitionId, args[0]);
|
|
346
|
+
|
|
347
|
+
if (definitionId === "control_if") {
|
|
348
|
+
if (args[1]) pendingIf.push({ id, slot: "next1", call: args[1] });
|
|
349
|
+
if (args[2]) pendingIf.push({ id, slot: "next2", call: args[2] });
|
|
350
|
+
} else if (args[1]) {
|
|
351
|
+
let body = stringOf(args[1]);
|
|
352
|
+
if (body === null) {
|
|
353
|
+
const interp = interpolatedOf(id, args[1], definitionId);
|
|
354
|
+
if (interp?.error) {
|
|
355
|
+
unresolvedAt(interp.node, interp.error);
|
|
356
|
+
} else if (interp) {
|
|
357
|
+
body = interp.text;
|
|
358
|
+
for (const ref of interp.refs) {
|
|
359
|
+
// 插值引用的槽和显式写在引脚对象里的槽撞了:两者会争同一个槽,谁赢取决于
|
|
360
|
+
// 解析顺序。不猜,报出来让作者改名。
|
|
361
|
+
const clash = nodes[id].inputs[ref.slot] !== undefined
|
|
362
|
+
|| edges.some((e) => e.endsWith(`|${id}|${ref.slot}`) && !e.startsWith(`${ref.src}|${ref.srcSlot}|`));
|
|
363
|
+
if (clash) {
|
|
364
|
+
unresolvedAt(args[1], `${id}.${ref.slot}: 模板插值要占用的槽已经在引脚对象里写过了`);
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
edges.push(`${ref.src}|${ref.srcSlot}|${id}|${ref.slot}`);
|
|
368
|
+
if (!STD_SLOTS.has(ref.slot) && !definitionOf(definitionId).input.some((s) => s.name === ref.slot)) {
|
|
369
|
+
nodes[id].extraIn.push(ref.slot);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
} else {
|
|
373
|
+
// 以前这里是 `if (body !== null)` 静默跳过:正文读不出来就整段消失,
|
|
374
|
+
// 保存一次磁盘上就真没了。读不懂必须记账。
|
|
375
|
+
unresolvedAt(args[1], `${id}: 正文既不是字符串字面量、file(...) 也不是模板插值`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
if (body !== null) {
|
|
379
|
+
if (definitionId === "tool_nodejs") nodes[id].script = body;
|
|
380
|
+
else nodes[id].body = body;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (decl?.type === "ExpressionStatement" && decl.expression.type === "CallExpression") {
|
|
387
|
+
const path = calleePath(decl.expression.callee);
|
|
388
|
+
if (path === "flow.resume") {
|
|
389
|
+
const [a, b] = decl.expression.arguments.map((x) => x.name);
|
|
390
|
+
if (a && b) edges.push(`${a}|next|${b}|prev`);
|
|
391
|
+
else unresolvedAt(decl, "flow.resume 的两个参数都必须是节点变量名");
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
if (path === "flow.detached") {
|
|
395
|
+
linkChain(null, itemsOf(decl.expression));
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
unresolvedAt(stmt, `顶层出现了流程图表达不了的语句(${stmt.type})`);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
for (const run of runDecls) {
|
|
404
|
+
nodes[run.id] = {
|
|
405
|
+
definitionId: run.definitionId,
|
|
406
|
+
inputs: {},
|
|
407
|
+
inputTypes: {},
|
|
408
|
+
outputs: {},
|
|
409
|
+
extraIn: [],
|
|
410
|
+
extraOut: [],
|
|
411
|
+
declaredOut: [],
|
|
412
|
+
attrs: {},
|
|
413
|
+
};
|
|
414
|
+
if (run.label) nodes[run.id].label = run.label;
|
|
415
|
+
if (run.body) nodes[run.id].body = run.body;
|
|
416
|
+
linkChain(run.id, run.items);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
for (const branch of pendingIf) {
|
|
420
|
+
const items = itemsOf(branch.call);
|
|
421
|
+
if (!items.length) continue;
|
|
422
|
+
const first = typeof items[0] === "string" ? items[0] : null;
|
|
423
|
+
if (!first) continue;
|
|
424
|
+
edges.push(`${branch.id}|${branch.slot}|${first}|prev`);
|
|
425
|
+
linkChain(null, items);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
for (const ds of destructures) {
|
|
429
|
+
const node = nodes[ds.node];
|
|
430
|
+
if (!node) continue;
|
|
431
|
+
for (const slot of ds.slots) {
|
|
432
|
+
if (!node.extraOut.includes(slot)) node.extraOut.push(slot);
|
|
433
|
+
// 记下「代码里显式解构声明过」——lint 据此区分自定义输出槽是声明的还是猜的
|
|
434
|
+
if (!node.declaredOut.includes(slot)) node.declaredOut.push(slot);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
for (const node of Object.values(nodes)) {
|
|
439
|
+
node.extraIn = [...new Set(node.extraIn)];
|
|
440
|
+
node.extraOut = [...new Set(node.extraOut)];
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return { nodes, edges: [...new Set(edges)].sort(), unresolved };
|
|
444
|
+
}
|