@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,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `flow.yaml` -> Workspace 设计态图。**只给迁移用**,读图那条路不走这里。
|
|
3
|
+
*
|
|
4
|
+
* Start/End Pipeline 退休之后 `flow.yaml` 变成了一块墓碑:目录哨兵认它,所以流程还挂在
|
|
5
|
+
* 列表里;`readWorkspaceGraphFiles` 不认它,所以画布上是空图、跑不了、也编辑不了。里面
|
|
6
|
+
* 却存着作者真写过的东西——body、prompt、script。没有这条路的话,摘掉哨兵等于让这些内容
|
|
7
|
+
* 从「坏的但看得见」变成「坏的且找不到」。
|
|
8
|
+
*
|
|
9
|
+
* 两种格式的**结构完全相同**(`instances` / `edges` / `ui`,`output-N` 索引式 handle),
|
|
10
|
+
* 差的是节点词汇表:yaml 用的一批节点在 Workspace 运行时里 `runtime: none`。所以这里做的
|
|
11
|
+
* 不是解析,是**改词**——外加一份诚实的损耗清单。
|
|
12
|
+
*
|
|
13
|
+
* 迁移不追求无损。追求的是:丢了什么,当场说清楚。
|
|
14
|
+
*/
|
|
15
|
+
import yaml from "js-yaml";
|
|
16
|
+
|
|
17
|
+
import { DEFINITIONS, definitionOf } from "./defs.mjs";
|
|
18
|
+
import { normalizeFlowYamlText } from "../flow-normalize.mjs";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 换词表:`runtime: none` 的节点里,有对等物的那些。
|
|
22
|
+
*
|
|
23
|
+
* 换的是 `definitionId`,边按**槽位名**重接——不是按索引。索引会错:`tool_print` 的
|
|
24
|
+
* `next` 在 0 号位,`display_markdown` 的 `next` 在 1 号位(0 号位是 `content` 输出)。
|
|
25
|
+
* 照索引搬会把控制边接到内容槽上,图还是连通的,跑起来才发现错。
|
|
26
|
+
*/
|
|
27
|
+
export const LEGACY_NODE_REMAP = {
|
|
28
|
+
// 老流程的入口 = Workspace 的运行节点。`next` 对 `next`。
|
|
29
|
+
control_start: "workspace_run",
|
|
30
|
+
// Workspace 没有「终点」这个概念,运行到没有后继就结束了。
|
|
31
|
+
control_end: null,
|
|
32
|
+
// 唯一的展示节点 -> Workspace 的展示节点。
|
|
33
|
+
tool_print: "display_markdown",
|
|
34
|
+
// 同名同形(prev/value -> next/prediction),但换过去之后语义靠 agent 兜,
|
|
35
|
+
// 而 `parse-bool.mjs` 只认 true/1/yes/on。所以要单独提醒。
|
|
36
|
+
control_toBool: "control_agent_toBool",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/** 换过去之后语义会松掉的,迁移时单独提醒一句。 */
|
|
40
|
+
const REMAP_CAVEATS = {
|
|
41
|
+
control_toBool: "判定从确定性解析变成 agent 判定;prediction 只认 true/1/yes/on,模型答「是」会被当成 false",
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 换词之后新节点装不下的实例字段。留着的话往返比对过不去,整张图会退回 JSON——
|
|
46
|
+
* 那等于因为一句文档说明放弃整次迁移。所以在这儿丢掉,并把原文报出去。
|
|
47
|
+
*/
|
|
48
|
+
const REMAP_DROPPED_FIELDS = {
|
|
49
|
+
// 运行节点生成的是 `flow("标签", ...)`,没有正文位置(只有排程流程用 body 存排程 JSON)
|
|
50
|
+
workspace_run: ["body"],
|
|
51
|
+
// 换成 agent 节点之后 `script` 不再执行——判定改由模型给出,脚本留着只会误导
|
|
52
|
+
control_agent_toBool: ["script"],
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/** `output-3` -> 3;不是这个形状就返回 -1。 */
|
|
56
|
+
function handleIndex(handle) {
|
|
57
|
+
const m = /^(?:input|output)-(\d+)$/.exec(String(handle || ""));
|
|
58
|
+
return m ? Number(m[1]) : -1;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** 实例自己声明的槽位数组;缺了就退回定义里的。 */
|
|
62
|
+
function slotsOf(instance, definitionId, kind) {
|
|
63
|
+
const own = Array.isArray(instance?.[kind]) ? instance[kind] : null;
|
|
64
|
+
if (own && own.length) return own;
|
|
65
|
+
return definitionOf(definitionId)[kind] || [];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const isNodeSlot = (slot) => String(slot?.type || "") === "node";
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 把老实例的槽位搬到新定义的槽位表上:先对名字,对不上的按位置兜底。
|
|
72
|
+
*
|
|
73
|
+
* 位置兜底不是图省事——老流程会改槽位名。LikeeProduce 的 `tool_print` 输入槽叫 `summary`
|
|
74
|
+
* 类型是 `file`,而展示节点的运行时按**名字**取内容(`workspaceWriteDisplayContent` 找
|
|
75
|
+
* `content`,或者退而求其次找第一个 `text`)。只按名字对,这条边接不上;照搬旧名字,接上
|
|
76
|
+
* 了运行时也读不到。所以要真的改名,并且把改名报出去。
|
|
77
|
+
*
|
|
78
|
+
* 兜底只在「都是控制槽」或「都不是控制槽」之间发生,不会把一条控制边挪到内容槽上。
|
|
79
|
+
* 名字对上的先占位,占过的不参与兜底——否则 `tool_print` 的 `next` 会被 `content` 抢走。
|
|
80
|
+
*
|
|
81
|
+
* @returns {{ slots: object[], renames: Map<string, string> }} renames: 老槽位名 -> 新槽位名
|
|
82
|
+
*/
|
|
83
|
+
function rebuildSlots(oldSlots, newDefSlots) {
|
|
84
|
+
const claimed = new Set();
|
|
85
|
+
const picked = new Array(newDefSlots.length).fill(null);
|
|
86
|
+
const renames = new Map();
|
|
87
|
+
|
|
88
|
+
newDefSlots.forEach((def, i) => {
|
|
89
|
+
const idx = oldSlots.findIndex((s, j) => !claimed.has(j) && String(s?.name || "") === String(def.name || ""));
|
|
90
|
+
if (idx < 0) return;
|
|
91
|
+
claimed.add(idx);
|
|
92
|
+
picked[i] = oldSlots[idx];
|
|
93
|
+
});
|
|
94
|
+
newDefSlots.forEach((def, i) => {
|
|
95
|
+
if (picked[i] || claimed.has(i)) return;
|
|
96
|
+
const old = oldSlots[i];
|
|
97
|
+
if (!old || isNodeSlot(old) !== isNodeSlot(def)) return;
|
|
98
|
+
claimed.add(i);
|
|
99
|
+
picked[i] = old;
|
|
100
|
+
renames.set(String(old.name || ""), String(def.name || ""));
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const slots = newDefSlots.map((def, i) => {
|
|
104
|
+
const old = picked[i];
|
|
105
|
+
// 类型跟新定义走。老槽位上的 `file` 在新节点上没有意义——运行时按新定义的类型读。
|
|
106
|
+
const slot = { type: def.type, name: def.name, value: old?.value ?? def.value ?? "" };
|
|
107
|
+
for (const key of ["required", "showOnNode", "description"]) {
|
|
108
|
+
if (def[key] !== undefined) slot[key] = def[key];
|
|
109
|
+
}
|
|
110
|
+
return slot;
|
|
111
|
+
});
|
|
112
|
+
return { slots, renames };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 把一份 `flow.yaml` 转成 Workspace 设计态图。
|
|
117
|
+
*
|
|
118
|
+
* @param {string} yamlText
|
|
119
|
+
* @returns {{
|
|
120
|
+
* graph: object,
|
|
121
|
+
* remapped: Array<{ id: string, from: string, to: string, caveat?: string }>,
|
|
122
|
+
* dropped: Array<{ id: string, definitionId: string, reason: string }>,
|
|
123
|
+
* droppedEdges: Array<{ source: string, target: string, reason: string }>,
|
|
124
|
+
* warnings: string[],
|
|
125
|
+
* }}
|
|
126
|
+
*/
|
|
127
|
+
export function legacyYamlToDesignGraph(yamlText) {
|
|
128
|
+
const parsed = yaml.load(normalizeFlowYamlText(String(yamlText ?? "")).text);
|
|
129
|
+
if (!parsed || typeof parsed !== "object") throw new Error("flow.yaml 解析不出对象");
|
|
130
|
+
const srcInstances = parsed.instances && typeof parsed.instances === "object" ? parsed.instances : {};
|
|
131
|
+
const srcEdges = Array.isArray(parsed.edges) ? parsed.edges : [];
|
|
132
|
+
|
|
133
|
+
const remapped = [];
|
|
134
|
+
const dropped = [];
|
|
135
|
+
const droppedEdges = [];
|
|
136
|
+
const warnings = [];
|
|
137
|
+
|
|
138
|
+
/** id -> { input, output, renameIn, renameOut },用来重接边。 */
|
|
139
|
+
const kept = new Map();
|
|
140
|
+
/** 丢了不算数的节点;指向它们的边跟着一起不算数。 */
|
|
141
|
+
const benignDrops = new Set();
|
|
142
|
+
const instances = {};
|
|
143
|
+
|
|
144
|
+
for (const [id, raw] of Object.entries(srcInstances)) {
|
|
145
|
+
const instance = raw && typeof raw === "object" ? raw : {};
|
|
146
|
+
const from = String(instance.definitionId || "");
|
|
147
|
+
const oldIn = slotsOf(instance, from, "input");
|
|
148
|
+
const oldOut = slotsOf(instance, from, "output");
|
|
149
|
+
|
|
150
|
+
const hasRemap = Object.prototype.hasOwnProperty.call(LEGACY_NODE_REMAP, from);
|
|
151
|
+
const to = hasRemap ? LEGACY_NODE_REMAP[from] : from;
|
|
152
|
+
|
|
153
|
+
// `control_end` 是唯一一个「丢了等于没丢」的:Workspace 里跑到没有后继就结束,
|
|
154
|
+
// 终点节点本来就没有对应物需要表达。所以标 benign——报出来,但不算有损。
|
|
155
|
+
if (hasRemap && to === null) {
|
|
156
|
+
dropped.push({
|
|
157
|
+
id,
|
|
158
|
+
definitionId: from,
|
|
159
|
+
benign: true,
|
|
160
|
+
reason: "Workspace 没有终点节点,跑到没有后继就结束",
|
|
161
|
+
});
|
|
162
|
+
benignDrops.add(id);
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (!hasRemap && definitionOf(from).runtime === "none") {
|
|
166
|
+
dropped.push({
|
|
167
|
+
id,
|
|
168
|
+
definitionId: from,
|
|
169
|
+
benign: false,
|
|
170
|
+
reason: DEFINITIONS[from]
|
|
171
|
+
? "这个节点类型在 Workspace 运行时里没有实现,也没有对等物"
|
|
172
|
+
: "未知节点类型",
|
|
173
|
+
});
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (to !== from) {
|
|
178
|
+
const def = definitionOf(to);
|
|
179
|
+
const rebuiltIn = rebuildSlots(oldIn, def.input || []);
|
|
180
|
+
const rebuiltOut = rebuildSlots(oldOut, def.output || []);
|
|
181
|
+
const next = { ...instance, definitionId: to, input: rebuiltIn.slots, output: rebuiltOut.slots };
|
|
182
|
+
instances[id] = next;
|
|
183
|
+
kept.set(id, {
|
|
184
|
+
input: next.input,
|
|
185
|
+
output: next.output,
|
|
186
|
+
renameIn: rebuiltIn.renames,
|
|
187
|
+
renameOut: rebuiltOut.renames,
|
|
188
|
+
});
|
|
189
|
+
const entry = { id, from, to };
|
|
190
|
+
const droppedFields = [];
|
|
191
|
+
for (const field of REMAP_DROPPED_FIELDS[to] || []) {
|
|
192
|
+
const text = String(next[field] ?? "").trim();
|
|
193
|
+
if (!text) continue;
|
|
194
|
+
delete next[field];
|
|
195
|
+
droppedFields.push({ field, text });
|
|
196
|
+
}
|
|
197
|
+
if (droppedFields.length) entry.droppedFields = droppedFields;
|
|
198
|
+
const renames = [...rebuiltIn.renames, ...rebuiltOut.renames].map(([a, b]) => `${a} -> ${b}`);
|
|
199
|
+
if (renames.length) entry.renamedSlots = renames;
|
|
200
|
+
if (REMAP_CAVEATS[from]) entry.caveat = REMAP_CAVEATS[from];
|
|
201
|
+
remapped.push(entry);
|
|
202
|
+
} else {
|
|
203
|
+
// 原样保留:`tool_nodejs` 这类带动态槽位的节点,实例上的槽位表就是权威,
|
|
204
|
+
// 拿定义去覆盖会把作者加的输入抹掉。
|
|
205
|
+
const next = { ...instance, input: [...oldIn], output: [...oldOut] };
|
|
206
|
+
instances[id] = next;
|
|
207
|
+
kept.set(id, { input: next.input, output: next.output, renameIn: new Map(), renameOut: new Map() });
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ── 重接边:索引 -> 老槽位名 -> 新索引 ─────────────────────────────────────
|
|
212
|
+
const edges = [];
|
|
213
|
+
const takenTargets = new Set();
|
|
214
|
+
for (const edge of srcEdges) {
|
|
215
|
+
const source = String(edge?.source || "");
|
|
216
|
+
const target = String(edge?.target || "");
|
|
217
|
+
const from = kept.get(source);
|
|
218
|
+
const to = kept.get(target);
|
|
219
|
+
if (!from || !to) {
|
|
220
|
+
const missing = !from ? source : target;
|
|
221
|
+
droppedEdges.push({ source, target, benign: benignDrops.has(missing), reason: `${missing} 已丢弃` });
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
const srcSlots = slotsOf(srcInstances[source], srcInstances[source]?.definitionId, "output");
|
|
225
|
+
const tgtSlots = slotsOf(srcInstances[target], srcInstances[target]?.definitionId, "input");
|
|
226
|
+
const srcName = srcSlots[handleIndex(edge.sourceHandle)]?.name;
|
|
227
|
+
const tgtName = tgtSlots[handleIndex(edge.targetHandle)]?.name;
|
|
228
|
+
// 换词时改过名的槽位,按新名字找
|
|
229
|
+
const srcFinal = from.renameOut.get(String(srcName)) ?? srcName;
|
|
230
|
+
const tgtFinal = to.renameIn.get(String(tgtName)) ?? tgtName;
|
|
231
|
+
const srcIdx = from.output.findIndex((s) => s.name === srcFinal);
|
|
232
|
+
const tgtIdx = to.input.findIndex((s) => s.name === tgtFinal);
|
|
233
|
+
if (srcIdx < 0 || tgtIdx < 0) {
|
|
234
|
+
droppedEdges.push({
|
|
235
|
+
source,
|
|
236
|
+
target,
|
|
237
|
+
benign: false,
|
|
238
|
+
reason: `新节点上找不到槽位 ${srcIdx < 0 ? `${source}.${srcFinal ?? edge.sourceHandle}` : `${target}.${tgtFinal ?? edge.targetHandle}`}`,
|
|
239
|
+
});
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
// Workspace 禁止 fan-in:同一个输入槽只能有一条入边。老图里靠 control_anyOne
|
|
243
|
+
// 汇合的分支,删掉汇合点之后会撞在同一个槽上。
|
|
244
|
+
const key = `${target} input-${tgtIdx}`;
|
|
245
|
+
if (takenTargets.has(key)) {
|
|
246
|
+
droppedEdges.push({ source, target, benign: false, reason: `${target}.${tgtFinal} 已经有入边了,Workspace 不允许 fan-in` });
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
takenTargets.add(key);
|
|
250
|
+
edges.push({ source, target, sourceHandle: `output-${srcIdx}`, targetHandle: `input-${tgtIdx}` });
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ── ui ────────────────────────────────────────────────────────────────────
|
|
254
|
+
const srcUi = parsed.ui && typeof parsed.ui === "object" ? parsed.ui : {};
|
|
255
|
+
const nodePositions = {};
|
|
256
|
+
for (const [id, pos] of Object.entries(srcUi.nodePositions || {})) {
|
|
257
|
+
if (kept.has(id)) nodePositions[id] = pos;
|
|
258
|
+
}
|
|
259
|
+
const ui = { nodePositions, nodeSizes: {} };
|
|
260
|
+
if (typeof srcUi.description === "string" && srcUi.description.trim()) ui.description = srcUi.description;
|
|
261
|
+
|
|
262
|
+
if (!Object.keys(instances).length) warnings.push("迁移之后一个节点都不剩");
|
|
263
|
+
else if (![...kept.keys()].some((id) => instances[id].definitionId === "workspace_run")) {
|
|
264
|
+
warnings.push("图里没有运行节点(老流程缺 control_start);补一个 workspace_run 才能跑");
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return { graph: { version: 1, instances, edges, ui }, remapped, dropped, droppedEdges, warnings };
|
|
268
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `workspace.flow.js` 的静态校验。
|
|
3
|
+
*
|
|
4
|
+
* 三类检查,对应三类真实会犯的错:
|
|
5
|
+
*
|
|
6
|
+
* 1. **语法层** —— 结构文件里不许出现控制流。写了 `for` / `if` / `await` 说明作者
|
|
7
|
+
* 把节点实现塞进了图结构文件;实现该放 `nodes/<name>/index.mjs`。这条也保证了
|
|
8
|
+
* 画布永远能靠静态解析渲染出来。
|
|
9
|
+
* 2. **语义层** —— 节点类型是否存在、运行时是否真的支持(读各节点 .md 的
|
|
10
|
+
* `runtime:` 分级)、槽位是否存在、fan-in、环。
|
|
11
|
+
* 3. **连通性** —— `control.if` 的 prediction 是否接了 bool、有没有孤立节点、
|
|
12
|
+
* 有没有 run 入口。
|
|
13
|
+
*
|
|
14
|
+
* 注意 lint 的对象只有图结构文件。`nodes/` 下的节点实现是普通 JS,`for`/`await`
|
|
15
|
+
* 随便写,不受这套约束。
|
|
16
|
+
*/
|
|
17
|
+
import fs from "fs";
|
|
18
|
+
import path from "path";
|
|
19
|
+
import { parse as acornParse } from "acorn";
|
|
20
|
+
|
|
21
|
+
import { CTRL_SLOTS, RUN_DEFINITIONS, DEFINITIONS, definitionOf } from "./defs.mjs";
|
|
22
|
+
import { packageResolverFor, scanAvailableNodePackages } from "./packages.mjs";
|
|
23
|
+
import { FLOW_SOURCE_FILENAME } from "./index.mjs";
|
|
24
|
+
import { parseFlowSource } from "./parser.mjs";
|
|
25
|
+
|
|
26
|
+
/** 图结构文件里禁用的语法;值是给人看的名字。 */
|
|
27
|
+
const BANNED_SYNTAX = {
|
|
28
|
+
ForStatement: "for",
|
|
29
|
+
ForOfStatement: "for-of",
|
|
30
|
+
ForInStatement: "for-in",
|
|
31
|
+
WhileStatement: "while",
|
|
32
|
+
DoWhileStatement: "do-while",
|
|
33
|
+
IfStatement: "if 语句",
|
|
34
|
+
ConditionalExpression: "三元表达式",
|
|
35
|
+
AwaitExpression: "await",
|
|
36
|
+
SpreadElement: "展开运算符",
|
|
37
|
+
FunctionDeclaration: "函数声明",
|
|
38
|
+
ArrowFunctionExpression: "箭头函数",
|
|
39
|
+
FunctionExpression: "函数表达式",
|
|
40
|
+
SwitchStatement: "switch",
|
|
41
|
+
TryStatement: "try",
|
|
42
|
+
NewExpression: "new",
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/** 这两类节点的槽位由实例自己定义,不受定义表约束。 */
|
|
46
|
+
const CUSTOM_SLOTS_ALLOWED = new Set(["agent_subAgent", "tool_nodejs"]);
|
|
47
|
+
|
|
48
|
+
function walk(node, visit) {
|
|
49
|
+
if (!node || typeof node !== "object") return;
|
|
50
|
+
if (node.type) visit(node);
|
|
51
|
+
for (const key of Object.keys(node)) {
|
|
52
|
+
const value = node[key];
|
|
53
|
+
if (Array.isArray(value)) value.forEach((v) => walk(v, visit));
|
|
54
|
+
else if (value && typeof value === "object" && value.type) walk(value, visit);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @param {string} flowDir 含 workspace.flow.js 的目录
|
|
60
|
+
* @returns {{ errors: string[], warnings: string[], ir?: object }}
|
|
61
|
+
*/
|
|
62
|
+
export function lintFlowDir(flowDir, opts = {}) {
|
|
63
|
+
const errors = [];
|
|
64
|
+
const warnings = [];
|
|
65
|
+
const sourcePath = path.join(flowDir, FLOW_SOURCE_FILENAME);
|
|
66
|
+
if (!fs.existsSync(sourcePath)) return { errors: [`缺少 ${sourcePath}`], warnings };
|
|
67
|
+
|
|
68
|
+
const source = fs.readFileSync(sourcePath, "utf-8");
|
|
69
|
+
let ast;
|
|
70
|
+
try {
|
|
71
|
+
ast = acornParse(source, { ecmaVersion: 2022, sourceType: "module", locations: true });
|
|
72
|
+
} catch (e) {
|
|
73
|
+
return { errors: [`语法错误: ${(e && e.message) || e}`], warnings };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
walk(ast, (node) => {
|
|
77
|
+
if (BANNED_SYNTAX[node.type]) {
|
|
78
|
+
errors.push(`结构文件禁用 ${BANNED_SYNTAX[node.type]} @L${node.loc?.start.line}(节点实现请放 nodes/<name>/index.mjs)`);
|
|
79
|
+
}
|
|
80
|
+
if (node.type === "MemberExpression" && node.computed) {
|
|
81
|
+
errors.push(`禁止动态属性访问 @L${node.loc?.start.line}`);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// file() 引用的外置文本
|
|
86
|
+
const files = {};
|
|
87
|
+
walk(ast, (node) => {
|
|
88
|
+
if (node.type !== "CallExpression" || node.callee?.name !== "file") return;
|
|
89
|
+
const rel = node.arguments[0]?.value;
|
|
90
|
+
if (typeof rel !== "string") {
|
|
91
|
+
errors.push(`file() 参数必须是字符串字面量 @L${node.loc?.start.line}`);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const abs = path.join(flowDir, rel);
|
|
95
|
+
if (!fs.existsSync(abs)) errors.push(`file(${JSON.stringify(rel)}) 指向的文件不存在`);
|
|
96
|
+
else files[rel] = fs.readFileSync(abs, "utf-8");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// 与存储层同一份扫描。分成两份就会出现「lint 绿灯、画布是错图」——包节点在图里
|
|
100
|
+
// 到底长什么样,两边必须给同一个答案。
|
|
101
|
+
const packages = scanAvailableNodePackages(flowDir, opts.workspaceRoot || "");
|
|
102
|
+
const lookupDef = (definitionId) => (DEFINITIONS[definitionId] ? definitionOf(definitionId) : null);
|
|
103
|
+
|
|
104
|
+
for (const stmt of ast.body) {
|
|
105
|
+
if (stmt.type !== "ImportDeclaration") continue;
|
|
106
|
+
const spec = String(stmt.source.value);
|
|
107
|
+
if (spec === "agentflow/flow") continue;
|
|
108
|
+
if (spec.startsWith("./nodes/")) {
|
|
109
|
+
if (!packages.bySpecifier[spec]) errors.push(`import ${JSON.stringify(spec)}:节点包不存在或缺 index.mjs / node.yaml`);
|
|
110
|
+
} else if (spec.startsWith("marketplace:")) {
|
|
111
|
+
if (!packages.bySpecifier[spec]) errors.push(`import ${JSON.stringify(spec)}:本地未安装该 marketplace 节点包`);
|
|
112
|
+
} else {
|
|
113
|
+
warnings.push(`import ${JSON.stringify(spec)}:来源不是 ./nodes/ 也不是 marketplace:`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
let ir;
|
|
118
|
+
try {
|
|
119
|
+
ir = parseFlowSource(source, {
|
|
120
|
+
files,
|
|
121
|
+
resolvePackage: packageResolverFor(packages),
|
|
122
|
+
});
|
|
123
|
+
} catch (e) {
|
|
124
|
+
errors.push(`解析成图失败: ${(e && e.message) || e}`);
|
|
125
|
+
return { errors, warnings };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// 解析器读不出图结构的地方。lint 是列全问题的地方,所以这里不抛,逐条报出来
|
|
129
|
+
for (const item of ir.unresolved) {
|
|
130
|
+
errors.push(`${FLOW_SOURCE_FILENAME}:${item.line} ${item.message}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const N = ir.nodes;
|
|
134
|
+
|
|
135
|
+
for (const [id, node] of Object.entries(N)) {
|
|
136
|
+
const definitionId = node.definitionId;
|
|
137
|
+
const def = lookupDef(definitionId);
|
|
138
|
+
if (!def) {
|
|
139
|
+
errors.push(`${id}: 未知节点类型 ${definitionId}`);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
// 运行时支持程度来自各节点 .md 的 runtime: 字段,不是这里的第二份清单
|
|
143
|
+
if (def.runtime === "degraded") {
|
|
144
|
+
warnings.push(`${id}: ${definitionId} 无专用 handler,靠通用 agent + 输出契约工作;结果必须恰好是 true/false`);
|
|
145
|
+
} else if (def.runtime === "none") {
|
|
146
|
+
errors.push(`${id}: ${definitionId} 没有 Workspace 运行时实现`);
|
|
147
|
+
}
|
|
148
|
+
if (CUSTOM_SLOTS_ALLOWED.has(definitionId)) {
|
|
149
|
+
const defOut = new Set(def.output.map((s) => s.name));
|
|
150
|
+
for (const slot of node.extraOut) {
|
|
151
|
+
if (defOut.has(slot) || (node.declaredOut || []).includes(slot)) continue;
|
|
152
|
+
errors.push(`${id}.${slot}: 自定义输出槽要用 const { ${slot} } = ${id} 声明,并在 body 里按 ---agentflow 信封回填`);
|
|
153
|
+
}
|
|
154
|
+
} else {
|
|
155
|
+
const defIn = new Set(def.input.map((s) => s.name));
|
|
156
|
+
const defOut = new Set(def.output.map((s) => s.name));
|
|
157
|
+
for (const slot of node.extraIn) if (!defIn.has(slot)) errors.push(`${id}[${definitionId}]: 不存在的输入槽 "${slot}"`);
|
|
158
|
+
for (const slot of node.extraOut) if (!defOut.has(slot)) errors.push(`${id}[${definitionId}]: 不存在的输出槽 "${slot}"`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const inputSeen = new Map();
|
|
163
|
+
const adjacency = new Map();
|
|
164
|
+
for (const key of ir.edges) {
|
|
165
|
+
const [src, fromSlot, dst, toSlot] = key.split("|");
|
|
166
|
+
if (!N[src]) {
|
|
167
|
+
errors.push(`边引用了未声明的节点 ${src}`);
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (!N[dst]) {
|
|
171
|
+
errors.push(`边引用了未声明的节点 ${dst}`);
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
const def = lookupDef(N[src].definitionId);
|
|
175
|
+
if (def && !CTRL_SLOTS.has(fromSlot)
|
|
176
|
+
&& !def.output.some((s) => s.name === fromSlot)
|
|
177
|
+
&& !(N[src].declaredOut || []).includes(fromSlot)) {
|
|
178
|
+
errors.push(`${src}.${fromSlot}: 输出槽不存在(只有 ${def.output.map((s) => s.name).join("/")})`);
|
|
179
|
+
}
|
|
180
|
+
// 控制槽也得真的存在。`provide.*` 这类纯数据源没有 prev/next,写进 flow(...) 链里
|
|
181
|
+
// 这条边落不下去——不报的话它会在往返时无声消失,链子断了还看不出原因。
|
|
182
|
+
const dstDef = lookupDef(N[dst].definitionId);
|
|
183
|
+
if (CTRL_SLOTS.has(toSlot) && dstDef && !dstDef.input.some((s) => s.name === toSlot)) {
|
|
184
|
+
errors.push(`${dst}[${N[dst].definitionId}] 没有 ${toSlot} 槽,接不进控制链(这类节点只能被别的节点引用值)`);
|
|
185
|
+
}
|
|
186
|
+
if (CTRL_SLOTS.has(fromSlot) && def && !def.output.some((s) => s.name === fromSlot)) {
|
|
187
|
+
errors.push(`${src}[${N[src].definitionId}] 没有 ${fromSlot} 槽,控制流串不下去`);
|
|
188
|
+
}
|
|
189
|
+
const target = `${dst}|${toSlot}`;
|
|
190
|
+
if (inputSeen.has(target)) {
|
|
191
|
+
errors.push(`fan-in 禁止: ${dst}.${toSlot} 被 ${inputSeen.get(target)} 和 ${src} 同时连入`);
|
|
192
|
+
} else {
|
|
193
|
+
inputSeen.set(target, src);
|
|
194
|
+
}
|
|
195
|
+
if (!adjacency.has(src)) adjacency.set(src, []);
|
|
196
|
+
adjacency.get(src).push(dst);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Workspace 运行计划是 DAG,有环会被运行时直接拒绝——在这里就报出来
|
|
200
|
+
const color = new Map();
|
|
201
|
+
const visit = (u) => {
|
|
202
|
+
color.set(u, 1);
|
|
203
|
+
for (const v of adjacency.get(u) || []) {
|
|
204
|
+
if (color.get(v) === 1) {
|
|
205
|
+
errors.push(`存在环: ${u} -> ${v}`);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (!color.has(v)) visit(v);
|
|
209
|
+
}
|
|
210
|
+
color.set(u, 2);
|
|
211
|
+
};
|
|
212
|
+
for (const id of Object.keys(N)) if (!color.has(id)) visit(id);
|
|
213
|
+
|
|
214
|
+
for (const [id, node] of Object.entries(N)) {
|
|
215
|
+
if (node.definitionId !== "control_if") continue;
|
|
216
|
+
const edge = ir.edges.find((e) => e.endsWith(`|${id}|prediction`));
|
|
217
|
+
if (!edge) {
|
|
218
|
+
errors.push(`${id}: control.if 的 prediction 未接线`);
|
|
219
|
+
} else {
|
|
220
|
+
const [src, slot] = edge.split("|");
|
|
221
|
+
const type = lookupDef(N[src]?.definitionId)?.output.find((s) => s.name === slot)?.type;
|
|
222
|
+
if (type && type !== "bool") errors.push(`${id}: prediction 只能接 bool,${src}.${slot} 是 ${type}`);
|
|
223
|
+
}
|
|
224
|
+
for (const [slot, label] of [["next1", "then"], ["next2", "else"]]) {
|
|
225
|
+
if (!ir.edges.some((e) => e.startsWith(`${id}|${slot}|`))) warnings.push(`${id}: control.if 缺 ${label} 分支`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const touched = new Set(ir.edges.flatMap((e) => {
|
|
230
|
+
const p = e.split("|");
|
|
231
|
+
return [p[0], p[2]];
|
|
232
|
+
}));
|
|
233
|
+
for (const id of Object.keys(N)) {
|
|
234
|
+
if (!touched.has(id) && !RUN_DEFINITIONS.has(N[id].definitionId)) warnings.push(`${id}: 未接任何线`);
|
|
235
|
+
}
|
|
236
|
+
if (!Object.values(N).some((n) => RUN_DEFINITIONS.has(n.definitionId))) {
|
|
237
|
+
warnings.push("没有任何 run 入口,这张图不会被执行");
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return { errors, warnings, ir };
|
|
241
|
+
}
|