@fieldwangai/agentflow 0.1.153 → 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 +16 -34
- 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 +165 -90
- 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 +8 -28
- package/bin/lib/html-escape.mjs +19 -0
- package/bin/lib/http-util.mjs +49 -0
- package/bin/lib/legacy-flow-execution.mjs +37 -0
- package/bin/lib/locales/en.json +1 -1
- package/bin/lib/locales/zh.json +1 -1
- package/bin/lib/main.mjs +152 -286
- 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 +1087 -18122
- 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-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-B9xslwI0.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 +1 -1
- package/skills/agentflow-cli/SKILL.md +139 -24
- package/skills/agentflow-cli/agents/openai.yaml +2 -2
- package/skills/agentflow-cli/scripts/agentflow-cli.mjs +654 -20
- package/skills/agentflow-cli/scripts/agentflow-runtime.mjs +97 -0
- package/skills/agentflow-flow-add-instances/SKILL.md +1 -1
- 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 +1 -1
- package/skills/agentflow-flow-recipes/SKILL.md +7 -4
- package/skills/agentflow-flow-recipes/references/recipes.md +97 -43
- package/skills/agentflow-flow-sync-ui/SKILL.md +1 -1
- 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 +2 -2
- package/skills/agentflow-node-reference/references/builtin-nodes.md +166 -115
- 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-DZ328oSo.css +0 -1
- package/builtin/web-ui/dist/assets/index-DmUV7ZCL.js +0 -888
- package/skills/agentflow-node-authoring/SKILL.md +0 -57
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace 图的磁盘表示。
|
|
3
|
+
*
|
|
4
|
+
* 设计态的**权威格式是代码**:
|
|
5
|
+
*
|
|
6
|
+
* ```
|
|
7
|
+
* workspace.flow.js 图结构——节点、连线、作者写的内容(受限 ESM,静态解析)
|
|
8
|
+
* workspace.layout.json 画布状态——坐标、尺寸、引脚显隐与顺序
|
|
9
|
+
* workspace.nodes.json 代码里说不清的——粘贴的图片、model、marketplaceRef、外置文件清单
|
|
10
|
+
* prompts/ docs/ scripts/ 超过 3 KB 的长文本
|
|
11
|
+
* workspace.state.json 运行态——由 workspace-state.mjs 负责,这里不碰
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* `workspace.graph.json` 降级为**只读的历史格式**:没有 `workspace.flow.js` 时按它读,
|
|
15
|
+
* 下一次写入自动迁移成代码。迁移不是无条件的——`writeWorkspaceDesign` 会先把生成的代码
|
|
16
|
+
* 解析回来跟原图逐字段比对,比不上就**退回写 JSON**。宁可不迁移,也不能因为 DSL 表达不了
|
|
17
|
+
* 某个字段就把它悄悄弄丢。
|
|
18
|
+
*/
|
|
19
|
+
import fs from "fs";
|
|
20
|
+
import path from "path";
|
|
21
|
+
|
|
22
|
+
import {
|
|
23
|
+
FLOW_LAYOUT_FILENAME,
|
|
24
|
+
FLOW_NODES_FILENAME,
|
|
25
|
+
FLOW_SOURCE_FILENAME,
|
|
26
|
+
flowFilesToGraph,
|
|
27
|
+
graphToFlowFiles,
|
|
28
|
+
} from "./flow-dsl/index.mjs";
|
|
29
|
+
import { definitionOf } from "./flow-dsl/defs.mjs";
|
|
30
|
+
import { packageBindingsForGraph, packageResolverFor, scanAvailableNodePackages } from "./flow-dsl/packages.mjs";
|
|
31
|
+
import { NODE_META_KEYS } from "./flow-dsl/ir.mjs";
|
|
32
|
+
import {
|
|
33
|
+
WORKSPACE_STATE_FILENAME,
|
|
34
|
+
isEmptyWorkspaceState,
|
|
35
|
+
mergeWorkspaceState,
|
|
36
|
+
splitWorkspaceGraph,
|
|
37
|
+
} from "./workspace-state.mjs";
|
|
38
|
+
|
|
39
|
+
export const WORKSPACE_GRAPH_FILENAME = "workspace.graph.json";
|
|
40
|
+
export { FLOW_SOURCE_FILENAME, FLOW_LAYOUT_FILENAME, FLOW_NODES_FILENAME };
|
|
41
|
+
|
|
42
|
+
/** 外置长文本只会落在这几个目录里;清理陈旧文件时不越界。 */
|
|
43
|
+
export const EXTERNAL_DIRS = ["prompts", "docs", "scripts"];
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 流程目录里由运行产生、不属于流程本身的文件(相对路径 glob)。
|
|
47
|
+
*
|
|
48
|
+
* 分享 / 发布一张流程图时必须排掉:这些文件每跑一次就变一次,而且装的是上一次运行的
|
|
49
|
+
* 真实产出——业务数据、接口返回、agent 写的正文。把它们打进 Hub 包等于顺手把内网内容
|
|
50
|
+
* 发出去。判断只看路径,不看内容,所以调用方不需要读文件。
|
|
51
|
+
*/
|
|
52
|
+
export const RUNTIME_ARTIFACT_GLOBS = [
|
|
53
|
+
WORKSPACE_STATE_FILENAME,
|
|
54
|
+
"nodes/*/history.md",
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
/** 相对路径是不是运行产物。`glob` 里只支持单层 `*`,够用且不会误伤。 */
|
|
58
|
+
export function isRuntimeArtifactPath(relPath) {
|
|
59
|
+
const rel = String(relPath || "").replace(/\\/g, "/").replace(/^\.\//, "");
|
|
60
|
+
return RUNTIME_ARTIFACT_GLOBS.some((glob) => {
|
|
61
|
+
const pattern = new RegExp(`^${glob.split("*").map((p) => p.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("[^/]*")}$`);
|
|
62
|
+
return pattern.test(rel);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** 三个结构文件——文件浏览器里当作机器产物处理。 */
|
|
67
|
+
export const WORKSPACE_DESIGN_FILENAMES = [
|
|
68
|
+
FLOW_SOURCE_FILENAME,
|
|
69
|
+
FLOW_LAYOUT_FILENAME,
|
|
70
|
+
FLOW_NODES_FILENAME,
|
|
71
|
+
WORKSPACE_GRAPH_FILENAME,
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
/** `workspace.flow.js` 解析失败。必须炸到调用方——静默当空图会在下一次保存时清空整张流程。 */
|
|
75
|
+
export class WorkspaceFlowParseError extends Error {
|
|
76
|
+
constructor(message, filePath) {
|
|
77
|
+
super(message);
|
|
78
|
+
this.name = "WorkspaceFlowParseError";
|
|
79
|
+
this.filePath = filePath;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function readJsonFile(file, fallback) {
|
|
84
|
+
if (!fs.existsSync(file)) return fallback;
|
|
85
|
+
try {
|
|
86
|
+
const raw = fs.readFileSync(file, "utf-8");
|
|
87
|
+
if (!raw.trim()) return fallback;
|
|
88
|
+
const parsed = JSON.parse(raw);
|
|
89
|
+
return parsed && typeof parsed === "object" ? parsed : fallback;
|
|
90
|
+
} catch {
|
|
91
|
+
return fallback;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function writeTextAtomic(file, text) {
|
|
96
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
97
|
+
const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
98
|
+
fs.writeFileSync(tmp, text, "utf-8");
|
|
99
|
+
fs.renameSync(tmp, file);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function readTextOrNull(file) {
|
|
103
|
+
try {
|
|
104
|
+
return fs.readFileSync(file, "utf-8");
|
|
105
|
+
} catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function emptyDesignGraph() {
|
|
111
|
+
return { version: 1, instances: {}, edges: [], ui: { nodePositions: {} } };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function normalizeDesignShape(raw) {
|
|
115
|
+
const graph = raw && typeof raw === "object" ? raw : {};
|
|
116
|
+
return {
|
|
117
|
+
version: Number(graph.version) || 1,
|
|
118
|
+
instances:
|
|
119
|
+
graph.instances && typeof graph.instances === "object" && !Array.isArray(graph.instances)
|
|
120
|
+
? graph.instances
|
|
121
|
+
: {},
|
|
122
|
+
edges: Array.isArray(graph.edges) ? graph.edges : [],
|
|
123
|
+
ui: graph.ui && typeof graph.ui === "object" ? graph.ui : { nodePositions: {} },
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** `file("prompts/x.md")` 引用的外置文本:把三个目录下的文件都读进来,解析时按引用取用。 */
|
|
128
|
+
function collectExternalFiles(dir) {
|
|
129
|
+
const files = {};
|
|
130
|
+
for (const sub of EXTERNAL_DIRS) {
|
|
131
|
+
const abs = path.join(dir, sub);
|
|
132
|
+
let entries;
|
|
133
|
+
try {
|
|
134
|
+
entries = fs.readdirSync(abs, { withFileTypes: true });
|
|
135
|
+
} catch {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
for (const entry of entries) {
|
|
139
|
+
if (!entry.isFile()) continue;
|
|
140
|
+
const text = readTextOrNull(path.join(abs, entry.name));
|
|
141
|
+
if (text !== null) files[`${sub}/${entry.name}`] = text;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return files;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ── 往返比对 ────────────────────────────────────────────────────────────────
|
|
148
|
+
// 逐字段规范化后深比。规范化只抹平「表示差异」(键顺序、`undefined` / `null` / `""`
|
|
149
|
+
// 三种缺省写法、role: normal 等价于没写),不抹平任何有意义的内容——否则这道闸门就白设了。
|
|
150
|
+
|
|
151
|
+
const asText = (value) => (value === undefined || value === null ? "" : String(value));
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* 槽的 showOnNode / required 缺省时继承定义表——`hydrateWorkspaceSlotMetaFromDefinitions`
|
|
155
|
+
* 在图进入运行时前就会把它们补齐,所以「没写」和「写了定义表里的那个值」是同一件事。
|
|
156
|
+
* 比对时先解析到同一基准,否则老 UI 存的、缺这两个键的图会被判成往返失败。
|
|
157
|
+
*/
|
|
158
|
+
function resolveSlotMeta(slot, defSlot, key) {
|
|
159
|
+
if (slot && key in slot) return slot[key] === true;
|
|
160
|
+
return defSlot ? defSlot[key] === true : false;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function normalizeSlot(slot, defSlot) {
|
|
164
|
+
const s = slot && typeof slot === "object" ? slot : {};
|
|
165
|
+
return {
|
|
166
|
+
name: asText(s.name),
|
|
167
|
+
type: asText(s.type),
|
|
168
|
+
value: asText(s.value ?? s.default ?? ""),
|
|
169
|
+
description: asText(s.description),
|
|
170
|
+
showOnNode: resolveSlotMeta(s, defSlot, "showOnNode"),
|
|
171
|
+
required: resolveSlotMeta(s, defSlot, "required"),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function normalizeInstance(instance) {
|
|
176
|
+
const inst = instance && typeof instance === "object" ? instance : {};
|
|
177
|
+
const def = definitionOf(asText(inst.definitionId));
|
|
178
|
+
const defSlot = (kind, name) => def[kind].find((s) => s.name === name) || null;
|
|
179
|
+
const out = {
|
|
180
|
+
definitionId: asText(inst.definitionId),
|
|
181
|
+
label: asText(inst.label),
|
|
182
|
+
role: asText(inst.role) === "normal" ? "" : asText(inst.role),
|
|
183
|
+
body: asText(inst.body),
|
|
184
|
+
script: asText(inst.script),
|
|
185
|
+
scriptRef: asText(inst.scriptRef),
|
|
186
|
+
globalContext: inst.globalContext === true,
|
|
187
|
+
images: inst.images === undefined || inst.images === null ? null : inst.images,
|
|
188
|
+
input: (Array.isArray(inst.input) ? inst.input : []).map((s) => normalizeSlot(s, defSlot("input", asText(s?.name)))),
|
|
189
|
+
output: (Array.isArray(inst.output) ? inst.output : []).map((s) => normalizeSlot(s, defSlot("output", asText(s?.name)))),
|
|
190
|
+
};
|
|
191
|
+
for (const key of NODE_META_KEYS) out[key] = asText(inst[key]);
|
|
192
|
+
return out;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function normalizeUi(ui) {
|
|
196
|
+
const src = ui && typeof ui === "object" ? ui : {};
|
|
197
|
+
const out = {};
|
|
198
|
+
for (const key of Object.keys(src).sort()) {
|
|
199
|
+
const value = src[key];
|
|
200
|
+
if (value === undefined || value === null) continue;
|
|
201
|
+
if ((key === "nodePositions" || key === "nodeSizes") && !Object.keys(value).length) continue;
|
|
202
|
+
out[key] = value;
|
|
203
|
+
}
|
|
204
|
+
return out;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function stableStringify(value) {
|
|
208
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
|
|
209
|
+
if (value && typeof value === "object") {
|
|
210
|
+
return `{${Object.keys(value)
|
|
211
|
+
.sort()
|
|
212
|
+
.map((k) => `${JSON.stringify(k)}:${stableStringify(value[k])}`)
|
|
213
|
+
.join(",")}}`;
|
|
214
|
+
}
|
|
215
|
+
return JSON.stringify(value === undefined ? null : value);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** 设计态图的规范化指纹。两张图指纹相同 = 语义上是同一张图。 */
|
|
219
|
+
export function designFingerprint(graph) {
|
|
220
|
+
const design = normalizeDesignShape(graph);
|
|
221
|
+
const instances = {};
|
|
222
|
+
for (const id of Object.keys(design.instances).sort()) {
|
|
223
|
+
instances[id] = normalizeInstance(design.instances[id]);
|
|
224
|
+
}
|
|
225
|
+
const edges = design.edges
|
|
226
|
+
.filter((e) => e && design.instances[e.source] && design.instances[e.target])
|
|
227
|
+
.map((e) => `${e.source}|${asText(e.sourceHandle)}|${e.target}|${asText(e.targetHandle)}`)
|
|
228
|
+
.sort();
|
|
229
|
+
return stableStringify({ instances, edges: [...new Set(edges)], ui: normalizeUi(design.ui) });
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ── 读 ──────────────────────────────────────────────────────────────────────
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* 读一个流程目录的设计态图。
|
|
236
|
+
*
|
|
237
|
+
* @param {string} flowDir
|
|
238
|
+
* @returns {{ format: "dsl"|"json"|"empty", path: string, graph: object, source: string|null }}
|
|
239
|
+
* @throws {WorkspaceFlowParseError} `workspace.flow.js` 存在但解析不出图
|
|
240
|
+
*/
|
|
241
|
+
export function readWorkspaceDesign(flowDir, opts = {}) {
|
|
242
|
+
const dir = path.resolve(flowDir);
|
|
243
|
+
const sourcePath = path.join(dir, FLOW_SOURCE_FILENAME);
|
|
244
|
+
const source = readTextOrNull(sourcePath);
|
|
245
|
+
if (source !== null && source.trim()) {
|
|
246
|
+
let graph;
|
|
247
|
+
try {
|
|
248
|
+
graph = flowFilesToGraph({
|
|
249
|
+
source,
|
|
250
|
+
layout: readJsonFile(path.join(dir, FLOW_LAYOUT_FILENAME), { nodes: {} }),
|
|
251
|
+
nodeMeta: readJsonFile(path.join(dir, FLOW_NODES_FILENAME), { nodes: {} }),
|
|
252
|
+
files: collectExternalFiles(dir),
|
|
253
|
+
// 和 lint 用同一份包扫描。不传的话 `import x from "./nodes/x"` 会读成一个
|
|
254
|
+
// 槽位表为空的节点,控制边跟着串位——lint 绿灯、画布是错图。
|
|
255
|
+
resolvePackage: packageResolverFor(scanAvailableNodePackages(dir, opts.marketplaceRoot || "")),
|
|
256
|
+
});
|
|
257
|
+
} catch (e) {
|
|
258
|
+
throw new WorkspaceFlowParseError(
|
|
259
|
+
`${FLOW_SOURCE_FILENAME} 解析失败:${(e && e.message) || String(e)}`,
|
|
260
|
+
sourcePath,
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
return { format: "dsl", path: sourcePath, graph: normalizeDesignShape(graph), source };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const graphPath = path.join(dir, WORKSPACE_GRAPH_FILENAME);
|
|
267
|
+
const rawJson = readTextOrNull(graphPath);
|
|
268
|
+
if (rawJson !== null && rawJson.trim()) {
|
|
269
|
+
return { format: "json", path: graphPath, graph: normalizeDesignShape(JSON.parse(rawJson)), source: null };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return { format: "empty", path: sourcePath, graph: emptyDesignGraph(), source: null };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// ── 写 ──────────────────────────────────────────────────────────────────────
|
|
276
|
+
|
|
277
|
+
function resolveInsideDir(dir, rel) {
|
|
278
|
+
const abs = path.resolve(dir, rel);
|
|
279
|
+
const prefix = dir.endsWith(path.sep) ? dir : dir + path.sep;
|
|
280
|
+
return abs.startsWith(prefix) ? abs : null;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* 删掉上一次生成、这一次不再需要的外置文本(body 缩短到阈值以下、节点被删)。
|
|
285
|
+
*
|
|
286
|
+
* 清单来自 `workspace.nodes.json` 的 `externals`,只删「我们自己写过的」——不靠文件名
|
|
287
|
+
* 猜测,免得误删 `scripts/` 下作者手写的脚本。清单本身可能被人改过,逐条做目录归属检查。
|
|
288
|
+
*/
|
|
289
|
+
function pruneStaleExternals(dir, previous, keep) {
|
|
290
|
+
if (!Array.isArray(previous)) return [];
|
|
291
|
+
const removed = [];
|
|
292
|
+
for (const rel of previous) {
|
|
293
|
+
if (typeof rel !== "string" || keep.has(rel)) continue;
|
|
294
|
+
if (!EXTERNAL_DIRS.includes(rel.split("/")[0])) continue;
|
|
295
|
+
const abs = resolveInsideDir(dir, rel);
|
|
296
|
+
if (!abs || !fs.existsSync(abs)) continue;
|
|
297
|
+
try {
|
|
298
|
+
fs.rmSync(abs, { force: true });
|
|
299
|
+
removed.push(rel);
|
|
300
|
+
} catch {
|
|
301
|
+
/* 删不掉就留着,是垃圾文件,不是错误 */
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return removed;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* 代码节点包实例上的 `script` 是**推导出来的**——`hydrateWorkspaceMarketplaceToolNodejsRuntime`
|
|
309
|
+
* 每次读图都会按 marketplaceRef 重新算一遍,内容是一串本机绝对路径(bootstrap 和
|
|
310
|
+
* index.mjs 的位置)。把它写进 flow.js 等于让流程文件带上某个人的主目录,发布出去、
|
|
311
|
+
* 换台机器就是错的。既然 import 已经指明了是哪个包,这里就不落盘。
|
|
312
|
+
*/
|
|
313
|
+
function stripDerivedPackageScripts(design, bindings) {
|
|
314
|
+
const ids = Object.keys(bindings || {});
|
|
315
|
+
if (!ids.length) return design;
|
|
316
|
+
const instances = { ...design.instances };
|
|
317
|
+
let changed = false;
|
|
318
|
+
for (const id of ids) {
|
|
319
|
+
const instance = instances[id];
|
|
320
|
+
if (!instance || !String(instance.script || "").trim()) continue;
|
|
321
|
+
const next = { ...instance };
|
|
322
|
+
delete next.script;
|
|
323
|
+
instances[id] = next;
|
|
324
|
+
changed = true;
|
|
325
|
+
}
|
|
326
|
+
return changed ? { ...design, instances } : design;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* 写设计态图。
|
|
331
|
+
*
|
|
332
|
+
* 顺序是有讲究的:**先落外置文本,再落 `workspace.flow.js`**。反过来的话,中途失败会留下
|
|
333
|
+
* 一个引用着不存在文件的流程文件,下次读直接炸;现在这个顺序最坏也只是多几个没人引用的文件。
|
|
334
|
+
*
|
|
335
|
+
* 返回的 `design` 是**落盘之后再读回来的那张图**,不是传进来的那张。代码化会做规范化
|
|
336
|
+
* (槽位补齐、`role: normal` 省掉、槽序归位),调用方拿它去算 revision 才和下一次 GET
|
|
337
|
+
* 对得上——不然客户端存完手里就攥着一个磁盘上根本不存在的版本号。
|
|
338
|
+
*
|
|
339
|
+
* @param {string} flowDir
|
|
340
|
+
* @param {object} designGraph 设计态图(运行态请先用 splitWorkspaceGraph 摘掉)
|
|
341
|
+
* @returns {{ format: "dsl"|"json", changed: boolean, degradedReason: string|null, externals: string[], design: object }}
|
|
342
|
+
*/
|
|
343
|
+
export function writeWorkspaceDesign(flowDir, designGraph, opts = {}) {
|
|
344
|
+
const dir = path.resolve(flowDir);
|
|
345
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
346
|
+
const design = normalizeDesignShape(designGraph);
|
|
347
|
+
|
|
348
|
+
const sourcePath = path.join(dir, FLOW_SOURCE_FILENAME);
|
|
349
|
+
const layoutPath = path.join(dir, FLOW_LAYOUT_FILENAME);
|
|
350
|
+
const nodesPath = path.join(dir, FLOW_NODES_FILENAME);
|
|
351
|
+
const graphPath = path.join(dir, WORKSPACE_GRAPH_FILENAME);
|
|
352
|
+
|
|
353
|
+
const packages = scanAvailableNodePackages(dir, opts.marketplaceRoot || "");
|
|
354
|
+
const bindings = packageBindingsForGraph(design, packages);
|
|
355
|
+
const written = stripDerivedPackageScripts(design, bindings);
|
|
356
|
+
|
|
357
|
+
let generated = null;
|
|
358
|
+
let persisted = null;
|
|
359
|
+
let degradedReason = null;
|
|
360
|
+
try {
|
|
361
|
+
generated = graphToFlowFiles(written, { packages: bindings });
|
|
362
|
+
} catch (e) {
|
|
363
|
+
degradedReason = `代码生成失败:${(e && e.message) || String(e)}`;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (generated) {
|
|
367
|
+
// 生成的代码必须能原样解析回同一张图,否则这次不迁移
|
|
368
|
+
try {
|
|
369
|
+
persisted = flowFilesToGraph({
|
|
370
|
+
source: generated.source,
|
|
371
|
+
layout: generated.layout,
|
|
372
|
+
nodeMeta: generated.nodeMeta,
|
|
373
|
+
files: Object.fromEntries(generated.files.map((f) => [f.path, f.text])),
|
|
374
|
+
resolvePackage: packageResolverFor(packages),
|
|
375
|
+
});
|
|
376
|
+
if (designFingerprint(persisted) !== designFingerprint(written)) {
|
|
377
|
+
degradedReason = "生成的代码解析回来与原图不一致";
|
|
378
|
+
}
|
|
379
|
+
} catch (e) {
|
|
380
|
+
degradedReason = `生成的代码解析不回来:${(e && e.message) || String(e)}`;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (degradedReason) {
|
|
385
|
+
// 退回历史格式。此时绝不能留下 workspace.flow.js——它的优先级更高,留着就等于让残缺的
|
|
386
|
+
// 那张图接管。
|
|
387
|
+
for (const file of [sourcePath, layoutPath, nodesPath]) fs.rmSync(file, { force: true });
|
|
388
|
+
writeTextAtomic(graphPath, `${JSON.stringify(design, null, 2)}\n`);
|
|
389
|
+
return { format: "json", changed: true, degradedReason, externals: [], design };
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const externals = generated.files.map((f) => f.path).sort();
|
|
393
|
+
const nodeMeta = { ...generated.nodeMeta };
|
|
394
|
+
if (externals.length) nodeMeta.externals = externals;
|
|
395
|
+
const layoutText = `${JSON.stringify(generated.layout, null, 2)}\n`;
|
|
396
|
+
const nodesText = `${JSON.stringify(nodeMeta, null, 2)}\n`;
|
|
397
|
+
const wantNodesFile = Object.keys(generated.nodeMeta.nodes || {}).length > 0 || externals.length > 0;
|
|
398
|
+
|
|
399
|
+
const unchanged =
|
|
400
|
+
readTextOrNull(sourcePath) === generated.source
|
|
401
|
+
&& readTextOrNull(layoutPath) === layoutText
|
|
402
|
+
&& readTextOrNull(nodesPath) === (wantNodesFile ? nodesText : null)
|
|
403
|
+
&& !fs.existsSync(graphPath)
|
|
404
|
+
&& generated.files.every((f) => readTextOrNull(path.join(dir, f.path)) === f.text);
|
|
405
|
+
if (unchanged) return { format: "dsl", changed: false, degradedReason: null, externals, design: persisted };
|
|
406
|
+
|
|
407
|
+
const previousExternals = readJsonFile(nodesPath, {}).externals;
|
|
408
|
+
|
|
409
|
+
for (const file of generated.files) {
|
|
410
|
+
const abs = resolveInsideDir(dir, file.path);
|
|
411
|
+
if (!abs) continue;
|
|
412
|
+
if (readTextOrNull(abs) !== file.text) writeTextAtomic(abs, file.text);
|
|
413
|
+
}
|
|
414
|
+
writeTextAtomic(sourcePath, generated.source);
|
|
415
|
+
writeTextAtomic(layoutPath, layoutText);
|
|
416
|
+
if (wantNodesFile) writeTextAtomic(nodesPath, nodesText);
|
|
417
|
+
else fs.rmSync(nodesPath, { force: true });
|
|
418
|
+
|
|
419
|
+
pruneStaleExternals(dir, previousExternals, new Set(externals));
|
|
420
|
+
// 迁移完成:历史格式退场。往返已经逐字段比对过,这里删的是一份可以再生成的副本。
|
|
421
|
+
fs.rmSync(graphPath, { force: true });
|
|
422
|
+
|
|
423
|
+
return { format: "dsl", changed: true, degradedReason: null, externals, design: persisted };
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// ── 完整图(设计态 + 运行态)────────────────────────────────────────────────
|
|
427
|
+
|
|
428
|
+
/** 运行态损坏不该让整张图打不开——产出重跑就有,设计态才是不可再生的。 */
|
|
429
|
+
function readWorkspaceStateFile(dir) {
|
|
430
|
+
const parsed = readJsonFile(path.join(dir, WORKSPACE_STATE_FILENAME), null);
|
|
431
|
+
return parsed && !Array.isArray(parsed) ? parsed : null;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* 只读运行态里的节点指纹,不解析设计态。
|
|
436
|
+
*
|
|
437
|
+
* 缓存判据必须以磁盘为准:客户端提交上来的图是可以随手改的,让它自带指纹等于把「这个值
|
|
438
|
+
* 是不是真跑出来的」交给调用方说了算。
|
|
439
|
+
*
|
|
440
|
+
* @returns {Record<string, string>} nodeId -> 上次成功执行时的输入指纹
|
|
441
|
+
*/
|
|
442
|
+
export function readWorkspaceRunFingerprints(flowDir) {
|
|
443
|
+
const state = readWorkspaceStateFile(path.resolve(flowDir));
|
|
444
|
+
const raw = state?.fingerprints;
|
|
445
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
|
|
446
|
+
const out = {};
|
|
447
|
+
for (const [nodeId, value] of Object.entries(raw)) {
|
|
448
|
+
if (typeof value === "string" && value) out[nodeId] = value;
|
|
449
|
+
}
|
|
450
|
+
return out;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/** 读回合并了运行态的完整图。 */
|
|
454
|
+
export function readWorkspaceGraphFiles(flowDir, opts = {}) {
|
|
455
|
+
const dir = path.resolve(flowDir);
|
|
456
|
+
const design = readWorkspaceDesign(dir, opts);
|
|
457
|
+
if (design.format === "empty") return { ...design, graph: emptyDesignGraph() };
|
|
458
|
+
return { ...design, graph: mergeWorkspaceState(design.graph, readWorkspaceStateFile(dir)) };
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* 写一张完整图:运行态进 `workspace.state.json`,设计态进代码。
|
|
463
|
+
*
|
|
464
|
+
* 先落运行态再落设计态——中途失败时设计态仍是上一版,不会出现「新设计 + 空运行态」
|
|
465
|
+
* 这种展示节点内容凭空消失的组合。
|
|
466
|
+
*/
|
|
467
|
+
export function writeWorkspaceGraphFiles(flowDir, graph, opts = {}) {
|
|
468
|
+
const dir = path.resolve(flowDir);
|
|
469
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
470
|
+
const statePath = path.join(dir, WORKSPACE_STATE_FILENAME);
|
|
471
|
+
const { design, state } = splitWorkspaceGraph(graph);
|
|
472
|
+
if (isEmptyWorkspaceState(state)) fs.rmSync(statePath, { force: true });
|
|
473
|
+
else writeTextAtomic(statePath, `${JSON.stringify(state, null, 2)}\n`);
|
|
474
|
+
const result = writeWorkspaceDesign(dir, design, opts);
|
|
475
|
+
// `graph` 是落盘之后读回来会拿到的那张完整图。调用方用它算 revision,客户端手里的
|
|
476
|
+
// 版本号才和磁盘一致。
|
|
477
|
+
return { ...result, graph: mergeWorkspaceState(result.design, state) };
|
|
478
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
|
|
3
|
+
import { splitWorkspaceGraph, workspaceRuntimeSurface } from "./workspace-state.mjs";
|
|
4
|
+
|
|
3
5
|
const MISSING = Symbol("missing");
|
|
4
6
|
|
|
5
7
|
function isPlainObject(value) {
|
|
@@ -36,31 +38,15 @@ function normalizedGraph(graph) {
|
|
|
36
38
|
};
|
|
37
39
|
}
|
|
38
40
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
delete next.displayReloadKey;
|
|
47
|
-
if (!isProvideInstance(next) && Array.isArray(next.output)) {
|
|
48
|
-
next.output = next.output.map((slot) => {
|
|
49
|
-
if (!isPlainObject(slot)) return slot;
|
|
50
|
-
const clean = { ...slot };
|
|
51
|
-
delete clean.value;
|
|
52
|
-
delete clean.default;
|
|
53
|
-
return clean;
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
return next;
|
|
57
|
-
}
|
|
58
|
-
|
|
41
|
+
/**
|
|
42
|
+
* 设计态视图 = 存储层拆出来的设计态,一个字不多一个字不少。
|
|
43
|
+
*
|
|
44
|
+
* 以前这里自己写了一遍「什么算运行态」,只覆盖 output 值和 displayReloadKey,漏掉了
|
|
45
|
+
* 接了入边的 input 值和展示节点正文。后果是跑一次流程 designRevision 就变,协作者手里
|
|
46
|
+
* 的基线全部作废——明明没人改过图。判断规则见 `workspace-state.mjs`。
|
|
47
|
+
*/
|
|
59
48
|
export function workspaceDesignGraph(graph) {
|
|
60
|
-
const next = normalizedGraph(graph);
|
|
61
|
-
next.instances = Object.fromEntries(
|
|
62
|
-
Object.entries(next.instances).map(([id, instance]) => [id, designInstance(instance)]),
|
|
63
|
-
);
|
|
49
|
+
const next = normalizedGraph(splitWorkspaceGraph(normalizedGraph(graph)).design);
|
|
64
50
|
delete next.ui.viewport;
|
|
65
51
|
return next;
|
|
66
52
|
}
|
|
@@ -125,17 +111,32 @@ function pathLabel(path) {
|
|
|
125
111
|
), "$");
|
|
126
112
|
}
|
|
127
113
|
|
|
128
|
-
|
|
114
|
+
/**
|
|
115
|
+
* 这个位置上的值是不是运行产出。是的话冲突不算冲突,直接取 incoming——重跑一次就有的
|
|
116
|
+
* 东西,没必要拦住用户让他手动选。
|
|
117
|
+
*
|
|
118
|
+
* 判断口径与 `workspaceRuntimeSurface` 完全一致;先在 incoming / current / base 里找到
|
|
119
|
+
* 第一个有这个节点的图,按那张图的槽位数组解析下标,避免三张图槽序不同时张冠李戴。
|
|
120
|
+
*/
|
|
121
|
+
function isRuntimePath(path, sides) {
|
|
129
122
|
if (path[0] === "ui" && path[1] === "viewport") return true;
|
|
130
123
|
if (path[0] !== "instances" || path.length < 3) return false;
|
|
131
|
-
|
|
132
|
-
if (path[2] !== "output" || !Number.isInteger(path[3])) return false;
|
|
133
|
-
if (path[4] !== "value" && path[4] !== "default") return false;
|
|
124
|
+
|
|
134
125
|
const nodeId = path[1];
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
126
|
+
const side = sides.find((s) => s.graph.instances?.[nodeId]);
|
|
127
|
+
if (!side) return false;
|
|
128
|
+
|
|
129
|
+
if (path[2] === "displayReloadKey" || path[2] === "runFingerprint") return true;
|
|
130
|
+
if (path[2] === "body") return side.runtime.displayBodies.has(nodeId);
|
|
131
|
+
|
|
132
|
+
if (path[2] !== "input" && path[2] !== "output") return false;
|
|
133
|
+
if (!Number.isInteger(path[3])) return false;
|
|
134
|
+
if (path[4] !== "value" && path[4] !== "default") return false;
|
|
135
|
+
|
|
136
|
+
if (path[2] === "output") return !side.runtime.isProvide(nodeId);
|
|
137
|
+
const slots = side.graph.instances[nodeId]?.input;
|
|
138
|
+
const slotName = String((Array.isArray(slots) ? slots : [])[path[3]]?.name ?? "");
|
|
139
|
+
return Boolean(slotName) && Boolean(side.runtime.inputs.get(nodeId)?.has(slotName));
|
|
139
140
|
}
|
|
140
141
|
|
|
141
142
|
function mergeValue(base, current, incoming, path, context) {
|
|
@@ -175,7 +176,7 @@ function mergeValue(base, current, incoming, path, context) {
|
|
|
175
176
|
));
|
|
176
177
|
}
|
|
177
178
|
|
|
178
|
-
if (isRuntimePath(path, context.
|
|
179
|
+
if (isRuntimePath(path, context.sides)) {
|
|
179
180
|
return incoming;
|
|
180
181
|
}
|
|
181
182
|
|
|
@@ -198,7 +199,13 @@ export function mergeWorkspaceGraphs({ baseGraph, currentGraph, incomingGraph })
|
|
|
198
199
|
const incoming = graphToMergeShape(incomingGraph);
|
|
199
200
|
const context = {
|
|
200
201
|
conflicts: [],
|
|
201
|
-
|
|
202
|
+
// 运行态判断要看边,而合并形态里 edges 已经变成 map 了,所以按原图先算好。
|
|
203
|
+
// 顺序即优先级:incoming 最先,和「哪张图有这个节点」的查找顺序一致。
|
|
204
|
+
sides: [
|
|
205
|
+
{ graph: incoming, runtime: workspaceRuntimeSurface(incomingGraph) },
|
|
206
|
+
{ graph: current, runtime: workspaceRuntimeSurface(currentGraph) },
|
|
207
|
+
{ graph: base, runtime: workspaceRuntimeSurface(baseGraph) },
|
|
208
|
+
],
|
|
202
209
|
};
|
|
203
210
|
const merged = mergeValue(base, current, incoming, [], context);
|
|
204
211
|
return {
|