@fieldwangai/agentflow 0.1.101 → 0.1.102
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/bin/lib/ui-server.mjs +917 -46
- package/bin/lib/workspace-collaboration.mjs +336 -0
- package/bin/lib/workspace-graph-merge.mjs +208 -0
- package/builtin/web-ui/dist/assets/index-BMmmoIiB.js +348 -0
- package/builtin/web-ui/dist/assets/index-DhNqbtUT.css +1 -0
- package/builtin/web-ui/dist/index.html +2 -2
- package/package.json +1 -1
- package/builtin/web-ui/dist/assets/index-DBiy5eMk.js +0 -346
- package/builtin/web-ui/dist/assets/index-DKaacU6h.css +0 -1
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import crypto from "crypto";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { getAgentflowDataRoot } from "./paths.mjs";
|
|
5
|
+
|
|
6
|
+
const REGISTRY_VERSION = 1;
|
|
7
|
+
const DEFAULT_INVITE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
8
|
+
|
|
9
|
+
function collaborationRoot() {
|
|
10
|
+
return path.join(getAgentflowDataRoot(), "collaboration");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function registryPath() {
|
|
14
|
+
return path.join(collaborationRoot(), "workspaces.json");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function readRegistry() {
|
|
18
|
+
try {
|
|
19
|
+
const filePath = registryPath();
|
|
20
|
+
if (!fs.existsSync(filePath)) return { version: REGISTRY_VERSION, workspaces: {} };
|
|
21
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
22
|
+
return {
|
|
23
|
+
version: REGISTRY_VERSION,
|
|
24
|
+
workspaces: parsed?.workspaces && typeof parsed.workspaces === "object" && !Array.isArray(parsed.workspaces)
|
|
25
|
+
? parsed.workspaces
|
|
26
|
+
: {},
|
|
27
|
+
};
|
|
28
|
+
} catch {
|
|
29
|
+
return { version: REGISTRY_VERSION, workspaces: {} };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function writeRegistry(registry) {
|
|
34
|
+
const filePath = registryPath();
|
|
35
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
36
|
+
const tmp = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
37
|
+
fs.writeFileSync(tmp, JSON.stringify({
|
|
38
|
+
version: REGISTRY_VERSION,
|
|
39
|
+
workspaces: registry?.workspaces || {},
|
|
40
|
+
}, null, 2) + "\n", "utf-8");
|
|
41
|
+
fs.renameSync(tmp, filePath);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function normalizeUserId(value) {
|
|
45
|
+
return String(value || "").trim().toLowerCase();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function normalizeFlowId(value) {
|
|
49
|
+
return String(value || "").trim();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function flowKey(flowId, archived = false, flowSource = "workspace", ownerId = "") {
|
|
53
|
+
return [
|
|
54
|
+
"project",
|
|
55
|
+
normalizeUserId(ownerId),
|
|
56
|
+
String(flowSource || "workspace"),
|
|
57
|
+
archived ? "archived" : "active",
|
|
58
|
+
normalizeFlowId(flowId),
|
|
59
|
+
].join(":");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function tokenHash(token) {
|
|
63
|
+
return crypto.createHash("sha256").update(String(token || "")).digest("hex");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function publicWorkspace(record, userId = "") {
|
|
67
|
+
if (!record) return null;
|
|
68
|
+
const actorId = normalizeUserId(userId);
|
|
69
|
+
const members = record.members && typeof record.members === "object" ? record.members : {};
|
|
70
|
+
return {
|
|
71
|
+
id: record.id,
|
|
72
|
+
flowId: record.flowId,
|
|
73
|
+
flowSource: record.projectSource || record.flowSource || "workspace",
|
|
74
|
+
archived: record.archived === true,
|
|
75
|
+
ownerId: record.ownerId,
|
|
76
|
+
role: actorId === record.ownerId ? "owner" : members[actorId] || "",
|
|
77
|
+
memberCount: Object.keys(members).length,
|
|
78
|
+
members: Object.entries(members).map(([id, role]) => ({ userId: id, role })),
|
|
79
|
+
createdAt: record.createdAt || "",
|
|
80
|
+
updatedAt: record.updatedAt || "",
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function getWorkspaceCollaborationByFlow(flowId, archived = false) {
|
|
85
|
+
return Object.values(readRegistry().workspaces)
|
|
86
|
+
.find((record) => (
|
|
87
|
+
record
|
|
88
|
+
&& record.flowId === normalizeFlowId(flowId)
|
|
89
|
+
&& record.archived === (archived === true)
|
|
90
|
+
&& (record.projectSource || record.flowSource || "workspace") === "workspace"
|
|
91
|
+
)) || null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function getWorkspaceCollaborationById(workspaceId) {
|
|
95
|
+
return readRegistry().workspaces[String(workspaceId || "").trim()] || null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function workspaceCollaborationAccess(record, userId) {
|
|
99
|
+
if (!record) return { allowed: true, role: "" };
|
|
100
|
+
const id = normalizeUserId(userId);
|
|
101
|
+
const role = id === record.ownerId ? "owner" : String(record.members?.[id] || "");
|
|
102
|
+
return {
|
|
103
|
+
allowed: role === "owner" || role === "editor" || role === "viewer",
|
|
104
|
+
writable: role === "owner" || role === "editor",
|
|
105
|
+
runnable: role === "owner" || role === "editor",
|
|
106
|
+
role,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function ensureWorkspaceCollaboration({ flowId, flowSource = "workspace", archived = false, userId }) {
|
|
111
|
+
const id = normalizeUserId(userId);
|
|
112
|
+
const normalizedFlowId = normalizeFlowId(flowId);
|
|
113
|
+
if (!id) return { error: "Authentication required", status: 401 };
|
|
114
|
+
if (!normalizedFlowId) return { error: "Missing flowId", status: 400 };
|
|
115
|
+
const registry = readRegistry();
|
|
116
|
+
const normalizedSource = flowSource === "user" ? "user" : "workspace";
|
|
117
|
+
const key = flowKey(normalizedFlowId, archived, normalizedSource, id);
|
|
118
|
+
let record = Object.values(registry.workspaces).find((item) => item?.flowKey === key) || null;
|
|
119
|
+
if (!record && normalizedSource === "workspace") {
|
|
120
|
+
record = Object.values(registry.workspaces).find((item) => (
|
|
121
|
+
item?.flowId === normalizedFlowId
|
|
122
|
+
&& item?.archived === (archived === true)
|
|
123
|
+
&& (item?.projectSource || item?.flowSource || "workspace") === "workspace"
|
|
124
|
+
)) || null;
|
|
125
|
+
}
|
|
126
|
+
if (record) {
|
|
127
|
+
const access = workspaceCollaborationAccess(record, id);
|
|
128
|
+
if (!access.allowed) return { error: "Workspace collaboration permission denied", status: 403 };
|
|
129
|
+
return { record, workspace: publicWorkspace(record, id), created: false };
|
|
130
|
+
}
|
|
131
|
+
const now = new Date().toISOString();
|
|
132
|
+
const workspaceId = `ws_${crypto.randomBytes(12).toString("hex")}`;
|
|
133
|
+
record = {
|
|
134
|
+
id: workspaceId,
|
|
135
|
+
flowKey: key,
|
|
136
|
+
flowId: normalizedFlowId,
|
|
137
|
+
flowSource: normalizedSource,
|
|
138
|
+
projectSource: normalizedSource,
|
|
139
|
+
archived: archived === true,
|
|
140
|
+
ownerId: id,
|
|
141
|
+
members: { [id]: "owner" },
|
|
142
|
+
invites: {},
|
|
143
|
+
createdAt: now,
|
|
144
|
+
updatedAt: now,
|
|
145
|
+
};
|
|
146
|
+
registry.workspaces[workspaceId] = record;
|
|
147
|
+
writeRegistry(registry);
|
|
148
|
+
return { record, workspace: publicWorkspace(record, id), created: true };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function createWorkspaceCollaborationInvite({
|
|
152
|
+
workspaceId,
|
|
153
|
+
userId,
|
|
154
|
+
role = "editor",
|
|
155
|
+
expiresInMs = DEFAULT_INVITE_TTL_MS,
|
|
156
|
+
}) {
|
|
157
|
+
const registry = readRegistry();
|
|
158
|
+
const record = registry.workspaces[String(workspaceId || "").trim()];
|
|
159
|
+
if (!record) return { error: "Workspace collaboration not found", status: 404 };
|
|
160
|
+
const actorId = normalizeUserId(userId);
|
|
161
|
+
const access = workspaceCollaborationAccess(record, actorId);
|
|
162
|
+
if (access.role !== "owner") return { error: "Only the workspace owner can create invitations", status: 403 };
|
|
163
|
+
const normalizedRole = role === "viewer" ? "viewer" : "editor";
|
|
164
|
+
const token = crypto.randomBytes(24).toString("base64url");
|
|
165
|
+
const now = Date.now();
|
|
166
|
+
const hash = tokenHash(token);
|
|
167
|
+
record.invites = record.invites && typeof record.invites === "object" ? record.invites : {};
|
|
168
|
+
record.invites[hash] = {
|
|
169
|
+
role: normalizedRole,
|
|
170
|
+
createdBy: actorId,
|
|
171
|
+
createdAt: new Date(now).toISOString(),
|
|
172
|
+
expiresAt: new Date(now + Math.max(60_000, Number(expiresInMs) || DEFAULT_INVITE_TTL_MS)).toISOString(),
|
|
173
|
+
};
|
|
174
|
+
record.updatedAt = new Date(now).toISOString();
|
|
175
|
+
writeRegistry(registry);
|
|
176
|
+
return {
|
|
177
|
+
token,
|
|
178
|
+
expiresAt: record.invites[hash].expiresAt,
|
|
179
|
+
workspace: publicWorkspace(record, actorId),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function acceptWorkspaceCollaborationInvite({ token, userId }) {
|
|
184
|
+
const id = normalizeUserId(userId);
|
|
185
|
+
if (!id) return { error: "Authentication required", status: 401 };
|
|
186
|
+
const hash = tokenHash(token);
|
|
187
|
+
const registry = readRegistry();
|
|
188
|
+
const record = Object.values(registry.workspaces)
|
|
189
|
+
.find((item) => item?.invites && item.invites[hash]) || null;
|
|
190
|
+
if (!record) return { error: "Workspace invitation not found", status: 404 };
|
|
191
|
+
const invite = record.invites[hash];
|
|
192
|
+
if (Date.parse(invite.expiresAt || "") <= Date.now()) {
|
|
193
|
+
delete record.invites[hash];
|
|
194
|
+
record.updatedAt = new Date().toISOString();
|
|
195
|
+
writeRegistry(registry);
|
|
196
|
+
return { error: "Workspace invitation has expired", status: 410 };
|
|
197
|
+
}
|
|
198
|
+
record.members = record.members && typeof record.members === "object" ? record.members : {};
|
|
199
|
+
if (id !== record.ownerId) record.members[id] = invite.role === "viewer" ? "viewer" : "editor";
|
|
200
|
+
record.updatedAt = new Date().toISOString();
|
|
201
|
+
writeRegistry(registry);
|
|
202
|
+
return { workspace: publicWorkspace(record, id) };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function workspaceCollaborationSummary(record, userId) {
|
|
206
|
+
return publicWorkspace(record, userId);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function getWorkspaceCollaborationForProject({
|
|
210
|
+
workspaceId,
|
|
211
|
+
flowId,
|
|
212
|
+
flowSource = "user",
|
|
213
|
+
archived = false,
|
|
214
|
+
ownerId = "",
|
|
215
|
+
}) {
|
|
216
|
+
if (workspaceId) return getWorkspaceCollaborationById(workspaceId);
|
|
217
|
+
const normalizedOwner = normalizeUserId(ownerId);
|
|
218
|
+
return Object.values(readRegistry().workspaces).find((record) => (
|
|
219
|
+
record
|
|
220
|
+
&& record.flowId === normalizeFlowId(flowId)
|
|
221
|
+
&& record.archived === (archived === true)
|
|
222
|
+
&& (record.projectSource || record.flowSource || "workspace") === flowSource
|
|
223
|
+
&& (!normalizedOwner || record.ownerId === normalizedOwner)
|
|
224
|
+
)) || null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function listWorkspaceCollaborationsForUser(userId) {
|
|
228
|
+
const actorId = normalizeUserId(userId);
|
|
229
|
+
return Object.values(readRegistry().workspaces).filter((record) => (
|
|
230
|
+
workspaceCollaborationAccess(record, actorId).allowed
|
|
231
|
+
));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function addWorkspaceCollaborationMember({
|
|
235
|
+
workspaceId,
|
|
236
|
+
userId,
|
|
237
|
+
memberUserId,
|
|
238
|
+
role = "editor",
|
|
239
|
+
}) {
|
|
240
|
+
const registry = readRegistry();
|
|
241
|
+
const record = registry.workspaces[String(workspaceId || "").trim()];
|
|
242
|
+
if (!record) return { error: "Workspace collaboration not found", status: 404 };
|
|
243
|
+
const actorId = normalizeUserId(userId);
|
|
244
|
+
const targetId = normalizeUserId(memberUserId);
|
|
245
|
+
if (workspaceCollaborationAccess(record, actorId).role !== "owner") {
|
|
246
|
+
return { error: "Only the workspace owner can add members", status: 403 };
|
|
247
|
+
}
|
|
248
|
+
if (!targetId) return { error: "Missing member user", status: 400 };
|
|
249
|
+
if (targetId === record.ownerId) {
|
|
250
|
+
return { workspace: publicWorkspace(record, actorId), unchanged: true };
|
|
251
|
+
}
|
|
252
|
+
record.members = record.members && typeof record.members === "object" ? record.members : {};
|
|
253
|
+
record.members[targetId] = role === "viewer" ? "viewer" : "editor";
|
|
254
|
+
record.updatedAt = new Date().toISOString();
|
|
255
|
+
writeRegistry(registry);
|
|
256
|
+
return { workspace: publicWorkspace(record, actorId), memberUserId: targetId };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function removeWorkspaceCollaborationMember({
|
|
260
|
+
workspaceId,
|
|
261
|
+
userId,
|
|
262
|
+
memberUserId,
|
|
263
|
+
}) {
|
|
264
|
+
const registry = readRegistry();
|
|
265
|
+
const record = registry.workspaces[String(workspaceId || "").trim()];
|
|
266
|
+
if (!record) return { error: "Workspace collaboration not found", status: 404 };
|
|
267
|
+
const actorId = normalizeUserId(userId);
|
|
268
|
+
const targetId = normalizeUserId(memberUserId || actorId);
|
|
269
|
+
const actorAccess = workspaceCollaborationAccess(record, actorId);
|
|
270
|
+
if (!actorAccess.allowed) return { error: "Workspace collaboration permission denied", status: 403 };
|
|
271
|
+
if (targetId === record.ownerId) {
|
|
272
|
+
return { error: "Workspace owner cannot leave; delete or move the source project instead", status: 400 };
|
|
273
|
+
}
|
|
274
|
+
if (actorAccess.role !== "owner" && targetId !== actorId) {
|
|
275
|
+
return { error: "Only the workspace owner can remove another member", status: 403 };
|
|
276
|
+
}
|
|
277
|
+
if (!record.members?.[targetId]) {
|
|
278
|
+
return { workspace: publicWorkspace(record, actorId), unchanged: true };
|
|
279
|
+
}
|
|
280
|
+
delete record.members[targetId];
|
|
281
|
+
record.updatedAt = new Date().toISOString();
|
|
282
|
+
writeRegistry(registry);
|
|
283
|
+
return {
|
|
284
|
+
workspace: publicWorkspace(record, actorId),
|
|
285
|
+
removedUserId: targetId,
|
|
286
|
+
left: targetId === actorId,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export function updateWorkspaceCollaborationFlow({
|
|
291
|
+
previousFlowId,
|
|
292
|
+
previousArchived = false,
|
|
293
|
+
flowSource = "workspace",
|
|
294
|
+
ownerId = "",
|
|
295
|
+
flowId,
|
|
296
|
+
archived = false,
|
|
297
|
+
}) {
|
|
298
|
+
const registry = readRegistry();
|
|
299
|
+
const record = Object.values(registry.workspaces).find((item) => (
|
|
300
|
+
item?.flowId === normalizeFlowId(previousFlowId)
|
|
301
|
+
&& item?.archived === (previousArchived === true)
|
|
302
|
+
&& (item?.projectSource || item?.flowSource || "workspace") === flowSource
|
|
303
|
+
&& (!ownerId || item?.ownerId === normalizeUserId(ownerId))
|
|
304
|
+
)) || null;
|
|
305
|
+
if (!record) return null;
|
|
306
|
+
record.flowId = normalizeFlowId(flowId);
|
|
307
|
+
record.archived = archived === true;
|
|
308
|
+
record.flowKey = flowKey(
|
|
309
|
+
record.flowId,
|
|
310
|
+
record.archived,
|
|
311
|
+
record.projectSource || record.flowSource || "workspace",
|
|
312
|
+
record.ownerId,
|
|
313
|
+
);
|
|
314
|
+
record.updatedAt = new Date().toISOString();
|
|
315
|
+
writeRegistry(registry);
|
|
316
|
+
return record;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export function deleteWorkspaceCollaborationForFlow(flowId, archived = false) {
|
|
320
|
+
const registry = readRegistry();
|
|
321
|
+
const key = flowKey(flowId, archived);
|
|
322
|
+
const entry = Object.entries(registry.workspaces).find(([, item]) => item?.flowKey === key);
|
|
323
|
+
if (!entry) return false;
|
|
324
|
+
delete registry.workspaces[entry[0]];
|
|
325
|
+
writeRegistry(registry);
|
|
326
|
+
return true;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export function deleteWorkspaceCollaborationById(workspaceId) {
|
|
330
|
+
const registry = readRegistry();
|
|
331
|
+
const id = String(workspaceId || "").trim();
|
|
332
|
+
if (!id || !registry.workspaces[id]) return false;
|
|
333
|
+
delete registry.workspaces[id];
|
|
334
|
+
writeRegistry(registry);
|
|
335
|
+
return true;
|
|
336
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
|
|
3
|
+
const MISSING = Symbol("missing");
|
|
4
|
+
|
|
5
|
+
function isPlainObject(value) {
|
|
6
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function clone(value) {
|
|
10
|
+
if (value === undefined) return undefined;
|
|
11
|
+
return JSON.parse(JSON.stringify(value));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function canonicalize(value) {
|
|
15
|
+
if (Array.isArray(value)) return value.map(canonicalize);
|
|
16
|
+
if (!isPlainObject(value)) return value;
|
|
17
|
+
return Object.fromEntries(
|
|
18
|
+
Object.keys(value)
|
|
19
|
+
.sort()
|
|
20
|
+
.map((key) => [key, canonicalize(value[key])]),
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function equal(left, right) {
|
|
25
|
+
if (left === MISSING || right === MISSING) return left === right;
|
|
26
|
+
return JSON.stringify(canonicalize(left)) === JSON.stringify(canonicalize(right));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizedGraph(graph) {
|
|
30
|
+
const source = isPlainObject(graph) ? graph : {};
|
|
31
|
+
return {
|
|
32
|
+
version: Number(source.version) || 1,
|
|
33
|
+
instances: isPlainObject(source.instances) ? clone(source.instances) : {},
|
|
34
|
+
edges: Array.isArray(source.edges) ? clone(source.edges) : [],
|
|
35
|
+
ui: isPlainObject(source.ui) ? clone(source.ui) : { nodePositions: {} },
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function isProvideInstance(instance) {
|
|
40
|
+
return String(instance?.definitionId || "").startsWith("provide_");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function designInstance(instance) {
|
|
44
|
+
if (!isPlainObject(instance)) return instance;
|
|
45
|
+
const next = clone(instance);
|
|
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
|
+
|
|
59
|
+
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
|
+
);
|
|
64
|
+
delete next.ui.viewport;
|
|
65
|
+
return next;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function workspaceDesignRevision(graph) {
|
|
69
|
+
return crypto.createHash("sha256")
|
|
70
|
+
.update(JSON.stringify(canonicalize(workspaceDesignGraph(graph))))
|
|
71
|
+
.digest("hex")
|
|
72
|
+
.slice(0, 24);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function workspaceRuntimeRevision(graph) {
|
|
76
|
+
return crypto.createHash("sha256")
|
|
77
|
+
.update(JSON.stringify(canonicalize(normalizedGraph(graph))))
|
|
78
|
+
.digest("hex")
|
|
79
|
+
.slice(0, 24);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function edgeKey(edge) {
|
|
83
|
+
return [
|
|
84
|
+
edge?.source || "",
|
|
85
|
+
edge?.target || "",
|
|
86
|
+
edge?.sourceHandle || "",
|
|
87
|
+
edge?.targetHandle || "",
|
|
88
|
+
].map((part) => encodeURIComponent(String(part))).join("|");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function edgesToMap(edges) {
|
|
92
|
+
const result = {};
|
|
93
|
+
for (const edge of Array.isArray(edges) ? edges : []) {
|
|
94
|
+
result[edgeKey(edge)] = edge;
|
|
95
|
+
}
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function graphToMergeShape(graph) {
|
|
100
|
+
const next = normalizedGraph(graph);
|
|
101
|
+
delete next.ui.viewport;
|
|
102
|
+
return {
|
|
103
|
+
...next,
|
|
104
|
+
edges: edgesToMap(next.edges),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function mergeShapeToGraph(shape) {
|
|
109
|
+
const graph = normalizedGraph({
|
|
110
|
+
...shape,
|
|
111
|
+
edges: Object.keys(shape?.edges || {})
|
|
112
|
+
.sort()
|
|
113
|
+
.map((key) => shape.edges[key]),
|
|
114
|
+
});
|
|
115
|
+
delete graph.ui.viewport;
|
|
116
|
+
return graph;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function pathLabel(path) {
|
|
120
|
+
if (!path.length) return "$";
|
|
121
|
+
return path.reduce((label, part) => (
|
|
122
|
+
typeof part === "number"
|
|
123
|
+
? `${label}[${part}]`
|
|
124
|
+
: `${label}.${String(part).replaceAll(".", "\\.")}`
|
|
125
|
+
), "$");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function isRuntimePath(path, graphs) {
|
|
129
|
+
if (path[0] === "ui" && path[1] === "viewport") return true;
|
|
130
|
+
if (path[0] !== "instances" || path.length < 3) return false;
|
|
131
|
+
if (path[2] === "displayReloadKey") return true;
|
|
132
|
+
if (path[2] !== "output" || !Number.isInteger(path[3])) return false;
|
|
133
|
+
if (path[4] !== "value" && path[4] !== "default") return false;
|
|
134
|
+
const nodeId = path[1];
|
|
135
|
+
const instance = graphs.incoming?.instances?.[nodeId]
|
|
136
|
+
|| graphs.current?.instances?.[nodeId]
|
|
137
|
+
|| graphs.base?.instances?.[nodeId];
|
|
138
|
+
return !isProvideInstance(instance);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function mergeValue(base, current, incoming, path, context) {
|
|
142
|
+
if (equal(incoming, base)) return current;
|
|
143
|
+
if (equal(current, base)) return incoming;
|
|
144
|
+
if (equal(incoming, current)) return current;
|
|
145
|
+
|
|
146
|
+
if (isPlainObject(base) && isPlainObject(current) && isPlainObject(incoming)) {
|
|
147
|
+
const merged = {};
|
|
148
|
+
const keys = new Set([...Object.keys(base), ...Object.keys(current), ...Object.keys(incoming)]);
|
|
149
|
+
for (const key of keys) {
|
|
150
|
+
const value = mergeValue(
|
|
151
|
+
Object.prototype.hasOwnProperty.call(base, key) ? base[key] : MISSING,
|
|
152
|
+
Object.prototype.hasOwnProperty.call(current, key) ? current[key] : MISSING,
|
|
153
|
+
Object.prototype.hasOwnProperty.call(incoming, key) ? incoming[key] : MISSING,
|
|
154
|
+
[...path, key],
|
|
155
|
+
context,
|
|
156
|
+
);
|
|
157
|
+
if (value !== MISSING) merged[key] = value;
|
|
158
|
+
}
|
|
159
|
+
return merged;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (
|
|
163
|
+
Array.isArray(base)
|
|
164
|
+
&& Array.isArray(current)
|
|
165
|
+
&& Array.isArray(incoming)
|
|
166
|
+
&& base.length === current.length
|
|
167
|
+
&& base.length === incoming.length
|
|
168
|
+
) {
|
|
169
|
+
return base.map((value, index) => mergeValue(
|
|
170
|
+
value,
|
|
171
|
+
current[index],
|
|
172
|
+
incoming[index],
|
|
173
|
+
[...path, index],
|
|
174
|
+
context,
|
|
175
|
+
));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (isRuntimePath(path, context.graphs)) {
|
|
179
|
+
return incoming;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
context.conflicts.push({
|
|
183
|
+
path: pathLabel(path),
|
|
184
|
+
pathParts: [...path],
|
|
185
|
+
hasBase: base !== MISSING,
|
|
186
|
+
hasCurrent: current !== MISSING,
|
|
187
|
+
hasIncoming: incoming !== MISSING,
|
|
188
|
+
base: base === MISSING ? undefined : clone(base),
|
|
189
|
+
current: current === MISSING ? undefined : clone(current),
|
|
190
|
+
incoming: incoming === MISSING ? undefined : clone(incoming),
|
|
191
|
+
});
|
|
192
|
+
return current;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function mergeWorkspaceGraphs({ baseGraph, currentGraph, incomingGraph }) {
|
|
196
|
+
const base = graphToMergeShape(baseGraph);
|
|
197
|
+
const current = graphToMergeShape(currentGraph);
|
|
198
|
+
const incoming = graphToMergeShape(incomingGraph);
|
|
199
|
+
const context = {
|
|
200
|
+
conflicts: [],
|
|
201
|
+
graphs: { base, current, incoming },
|
|
202
|
+
};
|
|
203
|
+
const merged = mergeValue(base, current, incoming, [], context);
|
|
204
|
+
return {
|
|
205
|
+
graph: mergeShapeToGraph(merged),
|
|
206
|
+
conflicts: context.conflicts,
|
|
207
|
+
};
|
|
208
|
+
}
|