@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
package/bin/lib/ui-server.mjs
CHANGED
|
@@ -84,6 +84,11 @@ import {
|
|
|
84
84
|
import { runNodeScript } from "./pipeline-scripts.mjs";
|
|
85
85
|
import { computeNextRunAt, readFlowSchedule, writeFlowSchedule } from "./schedule-config.mjs";
|
|
86
86
|
import { listScheduleStatuses } from "./scheduler.mjs";
|
|
87
|
+
import {
|
|
88
|
+
mergeWorkspaceGraphs,
|
|
89
|
+
workspaceDesignRevision,
|
|
90
|
+
workspaceRuntimeRevision,
|
|
91
|
+
} from "./workspace-graph-merge.mjs";
|
|
87
92
|
import {
|
|
88
93
|
deleteMarketplaceFlowSnippetPackage,
|
|
89
94
|
deleteMarketplaceNodePackage,
|
|
@@ -129,6 +134,20 @@ import {
|
|
|
129
134
|
readWorkspaceRunLogEvents,
|
|
130
135
|
} from "./workspace-run-logs.mjs";
|
|
131
136
|
import { createWorkspaceRunController } from "./workspace-run-controller.mjs";
|
|
137
|
+
import {
|
|
138
|
+
acceptWorkspaceCollaborationInvite,
|
|
139
|
+
addWorkspaceCollaborationMember,
|
|
140
|
+
deleteWorkspaceCollaborationById,
|
|
141
|
+
deleteWorkspaceCollaborationForFlow,
|
|
142
|
+
ensureWorkspaceCollaboration,
|
|
143
|
+
getWorkspaceCollaborationByFlow,
|
|
144
|
+
getWorkspaceCollaborationForProject,
|
|
145
|
+
listWorkspaceCollaborationsForUser,
|
|
146
|
+
removeWorkspaceCollaborationMember,
|
|
147
|
+
updateWorkspaceCollaborationFlow,
|
|
148
|
+
workspaceCollaborationAccess,
|
|
149
|
+
workspaceCollaborationSummary,
|
|
150
|
+
} from "./workspace-collaboration.mjs";
|
|
132
151
|
|
|
133
152
|
const MIME = {
|
|
134
153
|
".html": "text/html; charset=utf-8",
|
|
@@ -1668,6 +1687,13 @@ function workspaceGraphPath(workspaceRoot) {
|
|
|
1668
1687
|
return path.join(path.resolve(workspaceRoot), WORKSPACE_GRAPH_FILENAME);
|
|
1669
1688
|
}
|
|
1670
1689
|
|
|
1690
|
+
function writeWorkspaceGraphAtomic(graphPath, graph) {
|
|
1691
|
+
fs.mkdirSync(path.dirname(graphPath), { recursive: true });
|
|
1692
|
+
const tmp = `${graphPath}.${process.pid}.${Date.now()}.tmp`;
|
|
1693
|
+
fs.writeFileSync(tmp, JSON.stringify(graph, null, 2) + "\n", "utf-8");
|
|
1694
|
+
fs.renameSync(tmp, graphPath);
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1671
1697
|
function emptyWorkspaceGraph() {
|
|
1672
1698
|
return { version: 1, instances: {}, edges: [], ui: { nodePositions: {} } };
|
|
1673
1699
|
}
|
|
@@ -3079,11 +3105,93 @@ function resolveWorkspaceScopeRoot(workspaceRoot, params = {}, opts = {}) {
|
|
|
3079
3105
|
if (!isValidFlowSourceRead(flowSource)) {
|
|
3080
3106
|
return { root: "", error: "Invalid flowSource" };
|
|
3081
3107
|
}
|
|
3082
|
-
const
|
|
3108
|
+
const workspaceId = String(params.workspaceId || "").trim();
|
|
3109
|
+
let collaboration = getWorkspaceCollaborationForProject({
|
|
3110
|
+
workspaceId,
|
|
3111
|
+
flowId,
|
|
3112
|
+
flowSource,
|
|
3113
|
+
archived,
|
|
3114
|
+
ownerId: workspaceId ? "" : opts.userId,
|
|
3115
|
+
});
|
|
3116
|
+
if (!collaboration && !workspaceId) {
|
|
3117
|
+
collaboration = listWorkspaceCollaborationsForUser(opts.userId).find((record) => (
|
|
3118
|
+
record.flowId === flowId
|
|
3119
|
+
&& record.archived === archived
|
|
3120
|
+
&& (record.projectSource || record.flowSource || "workspace") === flowSource
|
|
3121
|
+
)) || null;
|
|
3122
|
+
}
|
|
3123
|
+
if (!collaboration && flowSource === "workspace") {
|
|
3124
|
+
collaboration = getWorkspaceCollaborationByFlow(flowId, archived);
|
|
3125
|
+
}
|
|
3126
|
+
if (collaboration) {
|
|
3127
|
+
const access = workspaceCollaborationAccess(collaboration, opts.userId);
|
|
3128
|
+
if (!access.allowed) {
|
|
3129
|
+
return { root: "", error: "Workspace collaboration permission denied", status: 403 };
|
|
3130
|
+
}
|
|
3131
|
+
}
|
|
3132
|
+
const physicalFlowSource = collaboration?.projectSource || collaboration?.flowSource || flowSource;
|
|
3133
|
+
const physicalOpts = collaboration && physicalFlowSource === "user"
|
|
3134
|
+
? { ...opts, userId: collaboration.ownerId }
|
|
3135
|
+
: opts;
|
|
3136
|
+
const result = getPipelineFiles(workspaceRoot, flowId, physicalFlowSource, archived, physicalOpts);
|
|
3083
3137
|
if (result.error || !result.path) {
|
|
3084
3138
|
return { root: "", error: result.error || "Pipeline workspace not found" };
|
|
3085
3139
|
}
|
|
3086
|
-
return {
|
|
3140
|
+
return {
|
|
3141
|
+
root: path.resolve(result.path),
|
|
3142
|
+
flowId,
|
|
3143
|
+
flowSource: physicalFlowSource,
|
|
3144
|
+
requestedFlowSource: flowSource,
|
|
3145
|
+
workspaceId: collaboration?.id || workspaceId,
|
|
3146
|
+
archived,
|
|
3147
|
+
collaboration,
|
|
3148
|
+
collaborationAccess: workspaceCollaborationAccess(collaboration, opts.userId),
|
|
3149
|
+
};
|
|
3150
|
+
}
|
|
3151
|
+
|
|
3152
|
+
function workspaceFlowCollaborationGuard(flowId, flowSource, archived, userCtx = {}, capability = "read") {
|
|
3153
|
+
if (flowSource !== "workspace") return null;
|
|
3154
|
+
const collaboration = getWorkspaceCollaborationByFlow(flowId, archived === true);
|
|
3155
|
+
if (!collaboration) return null;
|
|
3156
|
+
const access = workspaceCollaborationAccess(collaboration, userCtx.userId);
|
|
3157
|
+
if (!access.allowed) return { error: "Workspace collaboration permission denied", status: 403 };
|
|
3158
|
+
if (capability === "write" && !access.writable) {
|
|
3159
|
+
return { error: "Workspace collaboration edit permission denied", status: 403 };
|
|
3160
|
+
}
|
|
3161
|
+
if (capability === "run" && !access.runnable) {
|
|
3162
|
+
return { error: "Workspace collaboration run permission denied", status: 403 };
|
|
3163
|
+
}
|
|
3164
|
+
if (capability === "owner" && access.role !== "owner") {
|
|
3165
|
+
return { error: "Only the workspace owner can manage this workflow", status: 403 };
|
|
3166
|
+
}
|
|
3167
|
+
return null;
|
|
3168
|
+
}
|
|
3169
|
+
|
|
3170
|
+
function findWorkspaceShareUser(username) {
|
|
3171
|
+
const query = String(username || "").trim().toLowerCase();
|
|
3172
|
+
if (!query) return null;
|
|
3173
|
+
const users = readAuthUsers();
|
|
3174
|
+
for (const [userId, user] of Object.entries(users)) {
|
|
3175
|
+
const storedUsername = String(user?.username || userId).trim();
|
|
3176
|
+
if (String(userId).toLowerCase() === query || storedUsername.toLowerCase() === query) {
|
|
3177
|
+
return { userId: String(userId), username: storedUsername };
|
|
3178
|
+
}
|
|
3179
|
+
}
|
|
3180
|
+
return null;
|
|
3181
|
+
}
|
|
3182
|
+
|
|
3183
|
+
function workspaceCollaborationSummaryWithUsers(record, userId) {
|
|
3184
|
+
const summary = workspaceCollaborationSummary(record, userId);
|
|
3185
|
+
if (!summary) return null;
|
|
3186
|
+
const users = readAuthUsers();
|
|
3187
|
+
return {
|
|
3188
|
+
...summary,
|
|
3189
|
+
ownerUsername: String(users[summary.ownerId]?.username || summary.ownerId),
|
|
3190
|
+
members: (summary.members || []).map((member) => ({
|
|
3191
|
+
...member,
|
|
3192
|
+
username: String(users[member.userId]?.username || member.userId),
|
|
3193
|
+
})),
|
|
3194
|
+
};
|
|
3087
3195
|
}
|
|
3088
3196
|
|
|
3089
3197
|
function workspaceConversationsPath(scopedRoot) {
|
|
@@ -7069,7 +7177,8 @@ const flowEditorSyncSubscribers = new Map();
|
|
|
7069
7177
|
const flowEditorSyncVersions = new Map();
|
|
7070
7178
|
|
|
7071
7179
|
function flowEditorSyncKey(flowId, flowSource, flowArchived, userId = "") {
|
|
7072
|
-
|
|
7180
|
+
const actorScope = flowSource === "workspace" ? "" : String(userId || "");
|
|
7181
|
+
return `${actorScope}\t${String(flowId)}\t${String(flowSource)}\t${flowArchived ? "1" : "0"}`;
|
|
7073
7182
|
}
|
|
7074
7183
|
|
|
7075
7184
|
function broadcastFlowEditorSync(flowId, flowSource, flowArchived = false, userId = "") {
|
|
@@ -7093,6 +7202,8 @@ function broadcastFlowEditorSync(flowId, flowSource, flowArchived = false, userI
|
|
|
7093
7202
|
const activeFlowRuns = new Map();
|
|
7094
7203
|
/** 正在执行的 Workspace 临时 run(runId/sessionId → { controller, child, runNodeId, startedAt, plannedNodeIds }) */
|
|
7095
7204
|
const activeWorkspaceRuns = new Map();
|
|
7205
|
+
const workspaceCollaborationSubscribers = new Map();
|
|
7206
|
+
const workspaceCollaborationSequences = new Map();
|
|
7096
7207
|
const prdWorkflowSubscribers = new Map();
|
|
7097
7208
|
const prdWorkflowIdempotency = new Map();
|
|
7098
7209
|
const prdWorkflowActionLocks = new Map();
|
|
@@ -7105,8 +7216,9 @@ const WORKSPACE_IMPLEMENTATION_SUMMARY_ENABLED = false;
|
|
|
7105
7216
|
const WORKSPACE_NODE_HISTORY_MAX_CHARS = 80000;
|
|
7106
7217
|
|
|
7107
7218
|
function prdWorkflowKey(userCtx = {}, flowSource = "user", flowId = "", tapdId = "") {
|
|
7219
|
+
const actorScope = flowSource === "workspace" ? "shared" : String(userCtx?.userId || "");
|
|
7108
7220
|
return [
|
|
7109
|
-
|
|
7221
|
+
actorScope,
|
|
7110
7222
|
String(flowSource || "user"),
|
|
7111
7223
|
String(flowId || ""),
|
|
7112
7224
|
String(tapdId || ""),
|
|
@@ -9229,7 +9341,45 @@ function workspaceIntervalMinutesToCron(intervalMinutes) {
|
|
|
9229
9341
|
}
|
|
9230
9342
|
|
|
9231
9343
|
function workspaceRunKey(userCtx, flowSource, flowId) {
|
|
9232
|
-
|
|
9344
|
+
const source = flowSource || "user";
|
|
9345
|
+
const collaboration = listWorkspaceCollaborationsForUser(userCtx?.userId).find((record) => (
|
|
9346
|
+
record.flowId === flowId
|
|
9347
|
+
&& record.archived !== true
|
|
9348
|
+
&& (record.projectSource || record.flowSource || "workspace") === source
|
|
9349
|
+
));
|
|
9350
|
+
if (collaboration?.id) return `shared:${collaboration.id}`;
|
|
9351
|
+
const actorScope = source === "workspace" ? "shared" : userCtx?.userId || "";
|
|
9352
|
+
return `${actorScope}:${source}:${flowId}`;
|
|
9353
|
+
}
|
|
9354
|
+
|
|
9355
|
+
function workspaceCollaborationEventKey(userCtx, flowSource, flowId, archived = false) {
|
|
9356
|
+
const source = flowSource || "user";
|
|
9357
|
+
const collaboration = listWorkspaceCollaborationsForUser(userCtx?.userId).find((record) => (
|
|
9358
|
+
record.flowId === flowId
|
|
9359
|
+
&& record.archived === (archived === true)
|
|
9360
|
+
&& (record.projectSource || record.flowSource || "workspace") === source
|
|
9361
|
+
));
|
|
9362
|
+
if (collaboration?.id) return `shared:${collaboration.id}:${archived ? "1" : "0"}`;
|
|
9363
|
+
const actorScope = source === "workspace" ? "shared" : userCtx?.userId || "";
|
|
9364
|
+
return `${actorScope}:${source}:${flowId}:${archived ? "1" : "0"}`;
|
|
9365
|
+
}
|
|
9366
|
+
|
|
9367
|
+
function broadcastWorkspaceCollaborationEvent(userCtx, flowSource, flowId, archived, event = {}) {
|
|
9368
|
+
const key = workspaceCollaborationEventKey(userCtx, flowSource, flowId, archived);
|
|
9369
|
+
const seq = (workspaceCollaborationSequences.get(key) || 0) + 1;
|
|
9370
|
+
workspaceCollaborationSequences.set(key, seq);
|
|
9371
|
+
const payload = JSON.stringify({
|
|
9372
|
+
seq,
|
|
9373
|
+
at: new Date().toISOString(),
|
|
9374
|
+
...event,
|
|
9375
|
+
});
|
|
9376
|
+
const subscribers = workspaceCollaborationSubscribers.get(key);
|
|
9377
|
+
if (!subscribers?.size) return seq;
|
|
9378
|
+
const chunk = `id: ${seq}\ndata: ${payload}\n\n`;
|
|
9379
|
+
for (const clientRes of subscribers) {
|
|
9380
|
+
try { clientRes.write(chunk); } catch (_) {}
|
|
9381
|
+
}
|
|
9382
|
+
return seq;
|
|
9233
9383
|
}
|
|
9234
9384
|
|
|
9235
9385
|
function workspaceRunEntryKey(scopeKey, runId) {
|
|
@@ -9353,9 +9503,19 @@ function workspaceScheduleKey(userId, flowSource, flowId, scheduleNodeId) {
|
|
|
9353
9503
|
].join(":");
|
|
9354
9504
|
}
|
|
9355
9505
|
|
|
9506
|
+
function workspaceScheduleOwnerUserId(userCtx = {}, flowSource = "user", flowId = "") {
|
|
9507
|
+
const collaboration = listWorkspaceCollaborationsForUser(userCtx.userId).find((record) => (
|
|
9508
|
+
record.flowId === flowId
|
|
9509
|
+
&& record.archived !== true
|
|
9510
|
+
&& (record.projectSource || record.flowSource || "workspace") === flowSource
|
|
9511
|
+
));
|
|
9512
|
+
if (collaboration?.ownerId) return String(collaboration.ownerId);
|
|
9513
|
+
return String(userCtx.userId || "");
|
|
9514
|
+
}
|
|
9515
|
+
|
|
9356
9516
|
function listWorkspaceScheduleStatusesForFlow(userCtx = {}, flowSource = "user", flowId = "") {
|
|
9357
9517
|
const registry = readWorkspaceScheduleRegistry();
|
|
9358
|
-
const userId =
|
|
9518
|
+
const userId = workspaceScheduleOwnerUserId(userCtx, flowSource, flowId);
|
|
9359
9519
|
return Object.values(registry.schedules || {})
|
|
9360
9520
|
.filter((entry) => (
|
|
9361
9521
|
String(entry?.userId || "") === userId &&
|
|
@@ -9367,13 +9527,13 @@ function listWorkspaceScheduleStatusesForFlow(userCtx = {}, flowSource = "user",
|
|
|
9367
9527
|
|
|
9368
9528
|
function listWorkspaceScheduleStatuses(root, userCtx = {}) {
|
|
9369
9529
|
const registry = readWorkspaceScheduleRegistry();
|
|
9370
|
-
const userId = String(userCtx.userId || "");
|
|
9371
9530
|
const flows = listFlowsJson(root, { ...userCtx, includeWorkspaceFlows: true })
|
|
9372
9531
|
.filter((flow) => !flow.archived && !isReadonlyBuiltinFlowSource(flow.source || "user"));
|
|
9373
9532
|
const rows = [];
|
|
9374
9533
|
for (const flow of flows) {
|
|
9375
9534
|
const flowId = String(flow.id || "");
|
|
9376
9535
|
const flowSource = String(flow.source || "user");
|
|
9536
|
+
const scheduleUserId = workspaceScheduleOwnerUserId(userCtx, flowSource, flowId);
|
|
9377
9537
|
const scoped = resolveWorkspaceScopeRoot(root, { flowId, flowSource }, userCtx);
|
|
9378
9538
|
if (scoped.error || !scoped.root) continue;
|
|
9379
9539
|
let graph;
|
|
@@ -9386,7 +9546,7 @@ function listWorkspaceScheduleStatuses(root, userCtx = {}) {
|
|
|
9386
9546
|
for (const [scheduleNodeId, instance] of Object.entries(instances)) {
|
|
9387
9547
|
if (String(instance?.definitionId || "") !== "workspace_scheduled_run") continue;
|
|
9388
9548
|
const config = normalizeWorkspaceScheduledRunConfig(instance.body || "");
|
|
9389
|
-
const key = workspaceScheduleKey(
|
|
9549
|
+
const key = workspaceScheduleKey(scheduleUserId, flowSource, flowId, scheduleNodeId);
|
|
9390
9550
|
const current = registry.schedules?.[key] && typeof registry.schedules[key] === "object" ? registry.schedules[key] : {};
|
|
9391
9551
|
const targetRunNodeId = workspaceScheduleInferTargetRunNodeId(graph, scheduleNodeId, config);
|
|
9392
9552
|
const scopeKey = workspaceRunKey(userCtx, flowSource, flowId);
|
|
@@ -9475,15 +9635,25 @@ function setWorkspaceScheduleEnabled(root, payload = {}, authUser = {}, userCtx
|
|
|
9475
9635
|
function syncWorkspaceSchedulesForGraph(root, scoped, graph, authUser, userCtx = {}) {
|
|
9476
9636
|
const flowId = String(scoped?.flowId || "").trim();
|
|
9477
9637
|
const flowSource = String(scoped?.flowSource || "user");
|
|
9478
|
-
const userId =
|
|
9638
|
+
const userId = workspaceScheduleOwnerUserId(
|
|
9639
|
+
{ userId: userCtx.userId || authUser?.userId || "" },
|
|
9640
|
+
flowSource,
|
|
9641
|
+
flowId,
|
|
9642
|
+
);
|
|
9479
9643
|
if (!flowId || !userId) return [];
|
|
9480
9644
|
const now = Date.now();
|
|
9481
9645
|
const nowIso = new Date(now).toISOString();
|
|
9482
9646
|
const registry = readWorkspaceScheduleRegistry();
|
|
9483
9647
|
const schedules = { ...(registry.schedules || {}) };
|
|
9484
9648
|
const prefix = `${userId}:${flowSource}:${flowId}:`;
|
|
9485
|
-
for (const key of Object.
|
|
9486
|
-
|
|
9649
|
+
for (const [key, entry] of Object.entries(schedules)) {
|
|
9650
|
+
const sameSharedFlow = (
|
|
9651
|
+
flowSource === "workspace"
|
|
9652
|
+
&& scoped?.collaboration
|
|
9653
|
+
&& String(entry?.flowSource || "") === flowSource
|
|
9654
|
+
&& String(entry?.flowId || "") === flowId
|
|
9655
|
+
);
|
|
9656
|
+
if (sameSharedFlow || key.startsWith(prefix)) delete schedules[key];
|
|
9487
9657
|
}
|
|
9488
9658
|
const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
|
|
9489
9659
|
for (const [scheduleNodeId, instance] of Object.entries(instances)) {
|
|
@@ -11155,7 +11325,49 @@ export function startUiServer({
|
|
|
11155
11325
|
if (url.pathname === "/api/flows") {
|
|
11156
11326
|
if (req.method === "GET") {
|
|
11157
11327
|
try {
|
|
11158
|
-
|
|
11328
|
+
const flows = listFlowsJson(root, { ...userCtx, includeWorkspaceFlows: true })
|
|
11329
|
+
.filter((flow) => (
|
|
11330
|
+
!workspaceFlowCollaborationGuard(
|
|
11331
|
+
flow.id,
|
|
11332
|
+
flow.source || "user",
|
|
11333
|
+
flow.archived === true,
|
|
11334
|
+
userCtx,
|
|
11335
|
+
"read",
|
|
11336
|
+
)
|
|
11337
|
+
))
|
|
11338
|
+
.map((flow) => {
|
|
11339
|
+
const source = flow.source || "user";
|
|
11340
|
+
const collaboration = source === "workspace"
|
|
11341
|
+
? getWorkspaceCollaborationByFlow(flow.id, flow.archived === true)
|
|
11342
|
+
: getWorkspaceCollaborationForProject({
|
|
11343
|
+
flowId: flow.id,
|
|
11344
|
+
flowSource: source,
|
|
11345
|
+
archived: flow.archived === true,
|
|
11346
|
+
ownerId: userCtx.userId,
|
|
11347
|
+
});
|
|
11348
|
+
return collaboration
|
|
11349
|
+
? { ...flow, collaboration: workspaceCollaborationSummaryWithUsers(collaboration, userCtx.userId) }
|
|
11350
|
+
: flow;
|
|
11351
|
+
});
|
|
11352
|
+
const existingCollaborationIds = new Set(flows.map((flow) => flow.collaboration?.id).filter(Boolean));
|
|
11353
|
+
for (const record of listWorkspaceCollaborationsForUser(userCtx.userId)) {
|
|
11354
|
+
const source = record.projectSource || record.flowSource || "workspace";
|
|
11355
|
+
if (source !== "user" || record.ownerId === userCtx.userId) continue;
|
|
11356
|
+
if (existingCollaborationIds.has(record.id)) continue;
|
|
11357
|
+
const ownerFlow = listFlowsJson(root, { userId: record.ownerId })
|
|
11358
|
+
.find((flow) => (
|
|
11359
|
+
flow.id === record.flowId
|
|
11360
|
+
&& (flow.source || "user") === "user"
|
|
11361
|
+
&& Boolean(flow.archived) === Boolean(record.archived)
|
|
11362
|
+
));
|
|
11363
|
+
if (!ownerFlow) continue;
|
|
11364
|
+
flows.push({
|
|
11365
|
+
...ownerFlow,
|
|
11366
|
+
collaboration: workspaceCollaborationSummaryWithUsers(record, userCtx.userId),
|
|
11367
|
+
});
|
|
11368
|
+
existingCollaborationIds.add(record.id);
|
|
11369
|
+
}
|
|
11370
|
+
json(res, 200, flows);
|
|
11159
11371
|
} catch (e) {
|
|
11160
11372
|
json(res, 500, { error: (e && e.message) || String(e) });
|
|
11161
11373
|
}
|
|
@@ -11207,6 +11419,9 @@ export function startUiServer({
|
|
|
11207
11419
|
json(res, 400, result);
|
|
11208
11420
|
return;
|
|
11209
11421
|
}
|
|
11422
|
+
if (targetSpace === "workspace") {
|
|
11423
|
+
ensureWorkspaceCollaboration({ flowId, userId: userCtx.userId });
|
|
11424
|
+
}
|
|
11210
11425
|
json(res, 200, { success: true, flowId, flowSource: targetSpace });
|
|
11211
11426
|
return;
|
|
11212
11427
|
}
|
|
@@ -11287,6 +11502,9 @@ export function startUiServer({
|
|
|
11287
11502
|
json(res, 400, { error: w.error });
|
|
11288
11503
|
return;
|
|
11289
11504
|
}
|
|
11505
|
+
if (targetSpace === "workspace") {
|
|
11506
|
+
ensureWorkspaceCollaboration({ flowId, userId: userCtx.userId });
|
|
11507
|
+
}
|
|
11290
11508
|
json(res, 200, { success: true, flowId, flowSource: targetSpace });
|
|
11291
11509
|
return;
|
|
11292
11510
|
}
|
|
@@ -11391,11 +11609,188 @@ export function startUiServer({
|
|
|
11391
11609
|
return;
|
|
11392
11610
|
}
|
|
11393
11611
|
|
|
11612
|
+
if (req.method === "POST" && url.pathname === "/api/workspace/collaboration/accept") {
|
|
11613
|
+
try {
|
|
11614
|
+
const payload = JSON.parse(await readBody(req));
|
|
11615
|
+
const accepted = acceptWorkspaceCollaborationInvite({
|
|
11616
|
+
token: payload?.token,
|
|
11617
|
+
userId: userCtx.userId,
|
|
11618
|
+
});
|
|
11619
|
+
if (accepted.error) {
|
|
11620
|
+
json(res, accepted.status || 400, { error: accepted.error });
|
|
11621
|
+
return;
|
|
11622
|
+
}
|
|
11623
|
+
json(res, 200, { ok: true, workspace: accepted.workspace });
|
|
11624
|
+
} catch (e) {
|
|
11625
|
+
json(res, 400, { error: (e && e.message) || String(e) });
|
|
11626
|
+
}
|
|
11627
|
+
return;
|
|
11628
|
+
}
|
|
11629
|
+
|
|
11630
|
+
if (req.method === "POST" && url.pathname === "/api/workspace/collaboration/share") {
|
|
11631
|
+
try {
|
|
11632
|
+
const payload = JSON.parse(await readBody(req));
|
|
11633
|
+
const flowId = String(payload?.flowId || "").trim();
|
|
11634
|
+
const flowSource = String(payload?.flowSource || "user").trim();
|
|
11635
|
+
const archived = payload?.archived === true || payload?.flowArchived === true;
|
|
11636
|
+
if (flowSource !== "workspace" && flowSource !== "user") {
|
|
11637
|
+
json(res, 400, { error: "当前 Project 不支持协作分享" });
|
|
11638
|
+
return;
|
|
11639
|
+
}
|
|
11640
|
+
const scoped = resolveWorkspaceScopeRoot(root, {
|
|
11641
|
+
flowId,
|
|
11642
|
+
flowSource,
|
|
11643
|
+
workspaceId: payload.workspaceId || "",
|
|
11644
|
+
archived,
|
|
11645
|
+
}, userCtx);
|
|
11646
|
+
if (scoped.error) {
|
|
11647
|
+
json(res, scoped.status || 400, { error: scoped.error });
|
|
11648
|
+
return;
|
|
11649
|
+
}
|
|
11650
|
+
const ensured = ensureWorkspaceCollaboration({
|
|
11651
|
+
flowId,
|
|
11652
|
+
flowSource,
|
|
11653
|
+
archived,
|
|
11654
|
+
userId: userCtx.userId,
|
|
11655
|
+
});
|
|
11656
|
+
if (ensured.error) {
|
|
11657
|
+
json(res, ensured.status || 400, { error: ensured.error });
|
|
11658
|
+
return;
|
|
11659
|
+
}
|
|
11660
|
+
const targetUser = findWorkspaceShareUser(payload?.username || payload?.userId);
|
|
11661
|
+
if (!targetUser) {
|
|
11662
|
+
json(res, 404, { error: "未找到该用户名,请确认对方已经登录或注册 AgentFlow" });
|
|
11663
|
+
return;
|
|
11664
|
+
}
|
|
11665
|
+
const added = addWorkspaceCollaborationMember({
|
|
11666
|
+
workspaceId: ensured.workspace.id,
|
|
11667
|
+
userId: userCtx.userId,
|
|
11668
|
+
memberUserId: targetUser.userId,
|
|
11669
|
+
role: payload?.role,
|
|
11670
|
+
});
|
|
11671
|
+
if (added.error) {
|
|
11672
|
+
json(res, added.status || 400, { error: added.error });
|
|
11673
|
+
return;
|
|
11674
|
+
}
|
|
11675
|
+
const record = getWorkspaceCollaborationForProject({ workspaceId: ensured.workspace.id });
|
|
11676
|
+
broadcastWorkspaceCollaborationEvent(userCtx, flowSource, flowId, archived, {
|
|
11677
|
+
type: "member.added",
|
|
11678
|
+
actorId: userCtx.userId || "",
|
|
11679
|
+
memberUserId: targetUser.userId,
|
|
11680
|
+
});
|
|
11681
|
+
json(res, 200, {
|
|
11682
|
+
ok: true,
|
|
11683
|
+
workspace: workspaceCollaborationSummaryWithUsers(record, userCtx.userId),
|
|
11684
|
+
member: { userId: targetUser.userId, username: targetUser.username, role: "editor" },
|
|
11685
|
+
});
|
|
11686
|
+
} catch (e) {
|
|
11687
|
+
json(res, 400, { error: (e && e.message) || String(e) });
|
|
11688
|
+
}
|
|
11689
|
+
return;
|
|
11690
|
+
}
|
|
11691
|
+
|
|
11692
|
+
if (req.method === "DELETE" && url.pathname === "/api/workspace/collaboration/share") {
|
|
11693
|
+
try {
|
|
11694
|
+
const payload = JSON.parse(await readBody(req));
|
|
11695
|
+
const flowId = String(payload?.flowId || "").trim();
|
|
11696
|
+
const flowSource = String(payload?.flowSource || "user").trim();
|
|
11697
|
+
const archived = payload?.archived === true || payload?.flowArchived === true;
|
|
11698
|
+
if (!flowId || (flowSource !== "workspace" && flowSource !== "user")) {
|
|
11699
|
+
json(res, 400, { error: "Missing shared project" });
|
|
11700
|
+
return;
|
|
11701
|
+
}
|
|
11702
|
+
const record = getWorkspaceCollaborationForProject({
|
|
11703
|
+
workspaceId: payload.workspaceId || "",
|
|
11704
|
+
flowId,
|
|
11705
|
+
flowSource,
|
|
11706
|
+
archived,
|
|
11707
|
+
ownerId: userCtx.userId,
|
|
11708
|
+
}) || listWorkspaceCollaborationsForUser(userCtx.userId).find((item) => (
|
|
11709
|
+
item.flowId === flowId
|
|
11710
|
+
&& (item.projectSource || item.flowSource || "workspace") === flowSource
|
|
11711
|
+
&& item.archived === archived
|
|
11712
|
+
));
|
|
11713
|
+
if (!record) {
|
|
11714
|
+
json(res, 404, { error: "Workspace collaboration not found" });
|
|
11715
|
+
return;
|
|
11716
|
+
}
|
|
11717
|
+
const requestedUser = String(payload?.username || payload?.memberUserId || "").trim();
|
|
11718
|
+
const targetUser = requestedUser ? findWorkspaceShareUser(requestedUser) : null;
|
|
11719
|
+
if (requestedUser && !targetUser) {
|
|
11720
|
+
json(res, 404, { error: "未找到该用户" });
|
|
11721
|
+
return;
|
|
11722
|
+
}
|
|
11723
|
+
const removed = removeWorkspaceCollaborationMember({
|
|
11724
|
+
workspaceId: record.id,
|
|
11725
|
+
userId: userCtx.userId,
|
|
11726
|
+
memberUserId: targetUser?.userId || userCtx.userId,
|
|
11727
|
+
});
|
|
11728
|
+
if (removed.error) {
|
|
11729
|
+
json(res, removed.status || 400, { error: removed.error });
|
|
11730
|
+
return;
|
|
11731
|
+
}
|
|
11732
|
+
broadcastWorkspaceCollaborationEvent(userCtx, flowSource, flowId, archived, {
|
|
11733
|
+
type: removed.left ? "member.left" : "member.removed",
|
|
11734
|
+
actorId: userCtx.userId || "",
|
|
11735
|
+
memberUserId: removed.removedUserId || "",
|
|
11736
|
+
});
|
|
11737
|
+
const nextRecord = getWorkspaceCollaborationForProject({ workspaceId: record.id });
|
|
11738
|
+
json(res, 200, {
|
|
11739
|
+
ok: true,
|
|
11740
|
+
left: removed.left === true,
|
|
11741
|
+
removedUserId: removed.removedUserId || "",
|
|
11742
|
+
workspace: removed.left ? null : workspaceCollaborationSummaryWithUsers(nextRecord, userCtx.userId),
|
|
11743
|
+
});
|
|
11744
|
+
} catch (e) {
|
|
11745
|
+
json(res, 400, { error: (e && e.message) || String(e) });
|
|
11746
|
+
}
|
|
11747
|
+
return;
|
|
11748
|
+
}
|
|
11749
|
+
|
|
11750
|
+
if (req.method === "GET" && url.pathname === "/api/workspace/events") {
|
|
11751
|
+
const scoped = resolveWorkspaceScopeRoot(root, {
|
|
11752
|
+
flowId: url.searchParams.get("flowId") || "",
|
|
11753
|
+
flowSource: url.searchParams.get("flowSource") || "user",
|
|
11754
|
+
workspaceId: url.searchParams.get("workspaceId") || "",
|
|
11755
|
+
archived: url.searchParams.get("archived") === "1",
|
|
11756
|
+
}, userCtx);
|
|
11757
|
+
if (scoped.error) {
|
|
11758
|
+
json(res, scoped.status || 400, { error: scoped.error });
|
|
11759
|
+
return;
|
|
11760
|
+
}
|
|
11761
|
+
const key = workspaceCollaborationEventKey(userCtx, scoped.flowSource, scoped.flowId, scoped.archived);
|
|
11762
|
+
let subscribers = workspaceCollaborationSubscribers.get(key);
|
|
11763
|
+
if (!subscribers) {
|
|
11764
|
+
subscribers = new Set();
|
|
11765
|
+
workspaceCollaborationSubscribers.set(key, subscribers);
|
|
11766
|
+
}
|
|
11767
|
+
res.writeHead(200, {
|
|
11768
|
+
"Content-Type": "text/event-stream; charset=utf-8",
|
|
11769
|
+
"Cache-Control": "no-cache, no-transform",
|
|
11770
|
+
Connection: "keep-alive",
|
|
11771
|
+
"X-Accel-Buffering": "no",
|
|
11772
|
+
});
|
|
11773
|
+
res.write(`event: connected\ndata: ${JSON.stringify({ seq: workspaceCollaborationSequences.get(key) || 0 })}\n\n`);
|
|
11774
|
+
subscribers.add(res);
|
|
11775
|
+
const heartbeat = setInterval(() => {
|
|
11776
|
+
try { res.write(`: heartbeat ${Date.now()}\n\n`); } catch (_) {}
|
|
11777
|
+
}, 15_000);
|
|
11778
|
+
const detach = () => {
|
|
11779
|
+
clearInterval(heartbeat);
|
|
11780
|
+
subscribers.delete(res);
|
|
11781
|
+
if (subscribers.size === 0) workspaceCollaborationSubscribers.delete(key);
|
|
11782
|
+
};
|
|
11783
|
+
req.on("close", detach);
|
|
11784
|
+
res.on("close", detach);
|
|
11785
|
+
return;
|
|
11786
|
+
}
|
|
11787
|
+
|
|
11394
11788
|
if (req.method === "GET" && url.pathname === "/api/workspace/files") {
|
|
11395
11789
|
try {
|
|
11396
11790
|
const scoped = resolveWorkspaceScopeRoot(root, {
|
|
11397
11791
|
flowId: url.searchParams.get("flowId") || "",
|
|
11398
11792
|
flowSource: url.searchParams.get("flowSource") || "user",
|
|
11793
|
+
workspaceId: url.searchParams.get("workspaceId") || "",
|
|
11399
11794
|
archived: url.searchParams.get("archived") === "1",
|
|
11400
11795
|
}, userCtx);
|
|
11401
11796
|
if (scoped.error) {
|
|
@@ -11414,6 +11809,7 @@ export function startUiServer({
|
|
|
11414
11809
|
const scoped = resolveWorkspaceScopeRoot(root, {
|
|
11415
11810
|
flowId: url.searchParams.get("flowId") || "",
|
|
11416
11811
|
flowSource: url.searchParams.get("flowSource") || "user",
|
|
11812
|
+
workspaceId: url.searchParams.get("workspaceId") || "",
|
|
11417
11813
|
archived: url.searchParams.get("archived") === "1",
|
|
11418
11814
|
}, userCtx);
|
|
11419
11815
|
const scopedRoot = scoped.error ? root : scoped.root;
|
|
@@ -11471,23 +11867,30 @@ export function startUiServer({
|
|
|
11471
11867
|
const scoped = resolveWorkspaceScopeRoot(root, {
|
|
11472
11868
|
flowId: url.searchParams.get("flowId") || "",
|
|
11473
11869
|
flowSource: url.searchParams.get("flowSource") || "user",
|
|
11870
|
+
workspaceId: url.searchParams.get("workspaceId") || "",
|
|
11474
11871
|
archived: url.searchParams.get("archived") === "1",
|
|
11475
11872
|
}, userCtx);
|
|
11476
11873
|
if (scoped.error) {
|
|
11477
|
-
json(res, 400, { error: scoped.error });
|
|
11874
|
+
json(res, scoped.status || 400, { error: scoped.error });
|
|
11478
11875
|
return;
|
|
11479
11876
|
}
|
|
11480
11877
|
const { path: graphPath, graph } = readWorkspaceGraph(scoped.root);
|
|
11481
11878
|
const hydratedGraph = hydrateWorkspaceGraphForRuntime(root, scoped, graph, userCtx);
|
|
11879
|
+
const collaborationAccess = scoped.collaborationAccess || workspaceCollaborationAccess(null, userCtx.userId);
|
|
11482
11880
|
json(res, 200, {
|
|
11483
11881
|
ok: true,
|
|
11484
11882
|
graph: hydratedGraph,
|
|
11883
|
+
revision: workspaceDesignRevision(hydratedGraph),
|
|
11884
|
+
designRevision: workspaceDesignRevision(hydratedGraph),
|
|
11885
|
+
runtimeRevision: workspaceRuntimeRevision(hydratedGraph),
|
|
11485
11886
|
path: graphPath,
|
|
11486
11887
|
root: scoped.root,
|
|
11487
11888
|
flowId: scoped.flowId,
|
|
11488
11889
|
flowSource: scoped.flowSource,
|
|
11489
11890
|
archived: scoped.archived,
|
|
11490
|
-
writable: !(scoped.archived || isReadonlyBuiltinFlowSource(scoped.flowSource))
|
|
11891
|
+
writable: !(scoped.archived || isReadonlyBuiltinFlowSource(scoped.flowSource))
|
|
11892
|
+
&& collaborationAccess.writable !== false,
|
|
11893
|
+
collaboration: workspaceCollaborationSummaryWithUsers(scoped.collaboration, userCtx.userId),
|
|
11491
11894
|
workspaceSchedules: listWorkspaceScheduleStatusesForFlow(userCtx, scoped.flowSource || "user", scoped.flowId || ""),
|
|
11492
11895
|
});
|
|
11493
11896
|
} catch (e) {
|
|
@@ -11508,6 +11911,7 @@ export function startUiServer({
|
|
|
11508
11911
|
const scoped = resolveWorkspaceScopeRoot(root, {
|
|
11509
11912
|
flowId: payload.flowId || "",
|
|
11510
11913
|
flowSource: payload.flowSource || "user",
|
|
11914
|
+
workspaceId: payload.workspaceId || "",
|
|
11511
11915
|
archived: payload.archived === true || payload.flowArchived === true,
|
|
11512
11916
|
}, userCtx);
|
|
11513
11917
|
if (scoped.error) {
|
|
@@ -11552,23 +11956,106 @@ export function startUiServer({
|
|
|
11552
11956
|
const scoped = resolveWorkspaceScopeRoot(root, {
|
|
11553
11957
|
flowId: payload.flowId || "",
|
|
11554
11958
|
flowSource: payload.flowSource || "user",
|
|
11959
|
+
workspaceId: payload.workspaceId || "",
|
|
11555
11960
|
archived: payload.archived === true || payload.flowArchived === true,
|
|
11556
11961
|
}, userCtx);
|
|
11557
11962
|
if (scoped.error) {
|
|
11558
|
-
json(res, 400, { error: scoped.error });
|
|
11963
|
+
json(res, scoped.status || 400, { error: scoped.error });
|
|
11559
11964
|
return;
|
|
11560
11965
|
}
|
|
11561
|
-
if (
|
|
11966
|
+
if (
|
|
11967
|
+
scoped.archived
|
|
11968
|
+
|| isReadonlyBuiltinFlowSource(scoped.flowSource)
|
|
11969
|
+
|| scoped.collaborationAccess?.writable === false
|
|
11970
|
+
) {
|
|
11562
11971
|
json(res, 400, { error: "Cannot write workspace graph for builtin or archived pipeline" });
|
|
11563
11972
|
return;
|
|
11564
11973
|
}
|
|
11565
11974
|
const submittedGraph = hydrateWorkspaceGraphForRuntime(root, scoped, payload.graph || payload, userCtx);
|
|
11566
11975
|
const graphPath = workspaceGraphPath(scoped.root);
|
|
11567
|
-
const
|
|
11568
|
-
const
|
|
11569
|
-
|
|
11976
|
+
const currentStoredGraph = readWorkspaceGraph(scoped.root).graph;
|
|
11977
|
+
const currentGraph = hydrateWorkspaceGraphForRuntime(root, scoped, currentStoredGraph, userCtx);
|
|
11978
|
+
const currentRevision = workspaceDesignRevision(currentGraph);
|
|
11979
|
+
const baseRevision = String(payload.baseRevision || "").trim();
|
|
11980
|
+
if (scoped.collaboration && !baseRevision) {
|
|
11981
|
+
json(res, 428, {
|
|
11982
|
+
error: "Shared workspace save requires baseRevision",
|
|
11983
|
+
currentRevision,
|
|
11984
|
+
});
|
|
11985
|
+
return;
|
|
11986
|
+
}
|
|
11987
|
+
let nextGraph = submittedGraph;
|
|
11988
|
+
let merged = false;
|
|
11989
|
+
const baseGraph = payload.baseGraph;
|
|
11990
|
+
if (baseRevision && baseGraph && typeof baseGraph === "object") {
|
|
11991
|
+
const actualBaseRevision = workspaceDesignRevision(baseGraph);
|
|
11992
|
+
if (actualBaseRevision !== baseRevision) {
|
|
11993
|
+
json(res, 400, {
|
|
11994
|
+
error: "Workspace 合并基线与 baseRevision 不匹配",
|
|
11995
|
+
conflict: "invalid-merge-base",
|
|
11996
|
+
expectedRevision: baseRevision,
|
|
11997
|
+
actualBaseRevision,
|
|
11998
|
+
currentRevision,
|
|
11999
|
+
});
|
|
12000
|
+
return;
|
|
12001
|
+
}
|
|
12002
|
+
const mergeResult = mergeWorkspaceGraphs({
|
|
12003
|
+
baseGraph,
|
|
12004
|
+
currentGraph,
|
|
12005
|
+
incomingGraph: submittedGraph,
|
|
12006
|
+
});
|
|
12007
|
+
if (mergeResult.conflicts.length) {
|
|
12008
|
+
json(res, 409, {
|
|
12009
|
+
error: `Workspace 存在 ${mergeResult.conflicts.length} 处同字段冲突`,
|
|
12010
|
+
conflict: "field-conflict",
|
|
12011
|
+
expectedRevision: baseRevision,
|
|
12012
|
+
currentRevision,
|
|
12013
|
+
conflictPaths: mergeResult.conflicts.map((item) => item.path),
|
|
12014
|
+
conflictItems: mergeResult.conflicts,
|
|
12015
|
+
mergeGraph: mergeResult.graph,
|
|
12016
|
+
currentGraph,
|
|
12017
|
+
});
|
|
12018
|
+
return;
|
|
12019
|
+
}
|
|
12020
|
+
nextGraph = mergeResult.graph;
|
|
12021
|
+
merged = baseRevision !== currentRevision
|
|
12022
|
+
|| workspaceRuntimeRevision(baseGraph) !== workspaceRuntimeRevision(currentGraph);
|
|
12023
|
+
} else if (baseRevision && baseRevision !== currentRevision) {
|
|
12024
|
+
json(res, 409, {
|
|
12025
|
+
error: "Workspace 已被其他成员更新,当前客户端缺少合并基线,请刷新后重试",
|
|
12026
|
+
conflict: "missing-merge-base",
|
|
12027
|
+
expectedRevision: baseRevision,
|
|
12028
|
+
currentRevision,
|
|
12029
|
+
});
|
|
12030
|
+
return;
|
|
12031
|
+
}
|
|
12032
|
+
const graph = mergeWorkspacePersistentNodeRefs(nextGraph, currentGraph);
|
|
12033
|
+
writeWorkspaceGraphAtomic(graphPath, graph);
|
|
12034
|
+
const revision = workspaceDesignRevision(graph);
|
|
12035
|
+
const runtimeRevision = workspaceRuntimeRevision(graph);
|
|
11570
12036
|
const workspaceSchedules = syncWorkspaceSchedulesForGraph(root, scoped, graph, authUser, userCtx);
|
|
11571
|
-
|
|
12037
|
+
broadcastWorkspaceCollaborationEvent(
|
|
12038
|
+
userCtx,
|
|
12039
|
+
scoped.flowSource,
|
|
12040
|
+
scoped.flowId,
|
|
12041
|
+
scoped.archived,
|
|
12042
|
+
{
|
|
12043
|
+
type: "graph.committed",
|
|
12044
|
+
revision,
|
|
12045
|
+
actorId: userCtx.userId || "",
|
|
12046
|
+
clientId: String(payload.clientId || ""),
|
|
12047
|
+
},
|
|
12048
|
+
);
|
|
12049
|
+
json(res, 200, {
|
|
12050
|
+
ok: true,
|
|
12051
|
+
path: graphPath,
|
|
12052
|
+
graph,
|
|
12053
|
+
revision,
|
|
12054
|
+
designRevision: revision,
|
|
12055
|
+
runtimeRevision,
|
|
12056
|
+
merged,
|
|
12057
|
+
workspaceSchedules,
|
|
12058
|
+
});
|
|
11572
12059
|
} catch (e) {
|
|
11573
12060
|
json(res, 500, { error: (e && e.message) || String(e) });
|
|
11574
12061
|
}
|
|
@@ -11604,6 +12091,7 @@ export function startUiServer({
|
|
|
11604
12091
|
const scoped = resolveWorkspaceScopeRoot(root, {
|
|
11605
12092
|
flowId: payload.flowId || "",
|
|
11606
12093
|
flowSource: payload.flowSource || "user",
|
|
12094
|
+
workspaceId: payload.workspaceId || "",
|
|
11607
12095
|
archived: payload.archived === true || payload.flowArchived === true,
|
|
11608
12096
|
}, userCtx);
|
|
11609
12097
|
if (scoped.error) {
|
|
@@ -11651,13 +12139,18 @@ export function startUiServer({
|
|
|
11651
12139
|
const scoped = resolveWorkspaceScopeRoot(root, {
|
|
11652
12140
|
flowId: payload.flowId || "",
|
|
11653
12141
|
flowSource: payload.flowSource || "user",
|
|
12142
|
+
workspaceId: payload.workspaceId || "",
|
|
11654
12143
|
archived: payload.archived === true || payload.flowArchived === true,
|
|
11655
12144
|
}, userCtx);
|
|
11656
12145
|
if (scoped.error) {
|
|
11657
12146
|
json(res, 400, { error: scoped.error });
|
|
11658
12147
|
return;
|
|
11659
12148
|
}
|
|
11660
|
-
if (
|
|
12149
|
+
if (
|
|
12150
|
+
scoped.archived
|
|
12151
|
+
|| isReadonlyBuiltinFlowSource(scoped.flowSource)
|
|
12152
|
+
|| scoped.collaborationAccess?.writable === false
|
|
12153
|
+
) {
|
|
11661
12154
|
json(res, 400, { error: "Cannot optimize workspace graph for builtin or archived pipeline" });
|
|
11662
12155
|
return;
|
|
11663
12156
|
}
|
|
@@ -11673,12 +12166,20 @@ export function startUiServer({
|
|
|
11673
12166
|
const currentGraph = readWorkspaceGraph(scoped.root).graph;
|
|
11674
12167
|
const touchedIds = new Set((result.optimized || []).map((item) => item.nodeId).filter(Boolean));
|
|
11675
12168
|
const mergedGraph = mergeWorkspaceRunGraph(currentGraph, result.graph, touchedIds);
|
|
11676
|
-
|
|
12169
|
+
writeWorkspaceGraphAtomic(graphPath, mergedGraph);
|
|
12170
|
+
const revision = workspaceDesignRevision(mergedGraph);
|
|
11677
12171
|
const workspaceSchedules = syncWorkspaceSchedulesForGraph(root, scoped, mergedGraph, authUser, userCtx);
|
|
12172
|
+
broadcastWorkspaceCollaborationEvent(userCtx, scoped.flowSource, scoped.flowId, scoped.archived, {
|
|
12173
|
+
type: "graph.committed",
|
|
12174
|
+
revision,
|
|
12175
|
+
actorId: userCtx.userId || "",
|
|
12176
|
+
clientId: String(payload.clientId || ""),
|
|
12177
|
+
});
|
|
11678
12178
|
json(res, 200, {
|
|
11679
12179
|
ok: true,
|
|
11680
12180
|
path: graphPath,
|
|
11681
12181
|
graph: mergedGraph,
|
|
12182
|
+
revision,
|
|
11682
12183
|
order: result.order,
|
|
11683
12184
|
optimized: result.optimized,
|
|
11684
12185
|
skipped: result.skipped,
|
|
@@ -11702,13 +12203,18 @@ export function startUiServer({
|
|
|
11702
12203
|
const scoped = resolveWorkspaceScopeRoot(root, {
|
|
11703
12204
|
flowId: payload.flowId || "",
|
|
11704
12205
|
flowSource: payload.flowSource || "user",
|
|
12206
|
+
workspaceId: payload.workspaceId || "",
|
|
11705
12207
|
archived: payload.archived === true || payload.flowArchived === true,
|
|
11706
12208
|
}, userCtx);
|
|
11707
12209
|
if (scoped.error) {
|
|
11708
12210
|
json(res, 400, { error: scoped.error });
|
|
11709
12211
|
return;
|
|
11710
12212
|
}
|
|
11711
|
-
if (
|
|
12213
|
+
if (
|
|
12214
|
+
scoped.archived
|
|
12215
|
+
|| isReadonlyBuiltinFlowSource(scoped.flowSource)
|
|
12216
|
+
|| scoped.collaborationAccess?.runnable === false
|
|
12217
|
+
) {
|
|
11712
12218
|
json(res, 400, { error: "Cannot run workspace graph for builtin or archived pipeline" });
|
|
11713
12219
|
return;
|
|
11714
12220
|
}
|
|
@@ -11718,7 +12224,27 @@ export function startUiServer({
|
|
|
11718
12224
|
json(res, 400, { error: "Missing flowId" });
|
|
11719
12225
|
return;
|
|
11720
12226
|
}
|
|
11721
|
-
const
|
|
12227
|
+
const canonicalStoredGraph = readWorkspaceGraph(scoped.root).graph;
|
|
12228
|
+
const canonicalGraph = hydrateWorkspaceGraphForRuntime(
|
|
12229
|
+
root,
|
|
12230
|
+
scoped,
|
|
12231
|
+
canonicalStoredGraph,
|
|
12232
|
+
userCtx,
|
|
12233
|
+
);
|
|
12234
|
+
const canonicalRevision = workspaceDesignRevision(canonicalGraph);
|
|
12235
|
+
const expectedRevision = String(payload.expectedRevision || payload.baseRevision || "").trim();
|
|
12236
|
+
if (scoped.collaboration && expectedRevision && expectedRevision !== canonicalRevision) {
|
|
12237
|
+
json(res, 409, {
|
|
12238
|
+
error: "Workspace 已更新,请刷新后再运行",
|
|
12239
|
+
conflict: "revision-mismatch",
|
|
12240
|
+
expectedRevision,
|
|
12241
|
+
currentRevision: canonicalRevision,
|
|
12242
|
+
});
|
|
12243
|
+
return;
|
|
12244
|
+
}
|
|
12245
|
+
const runtimeGraph = scoped.collaboration
|
|
12246
|
+
? canonicalGraph
|
|
12247
|
+
: hydrateWorkspaceGraphForRuntime(root, scoped, payload.graph || canonicalGraph, userCtx);
|
|
11722
12248
|
const runNodeId = String(payload.runNodeId || "").trim();
|
|
11723
12249
|
const plan = workspaceRunPlan(runtimeGraph, runNodeId, scoped.root);
|
|
11724
12250
|
const plannedNodeIds = workspaceRunPlanNodeIds(runNodeId, plan);
|
|
@@ -11767,12 +12293,27 @@ export function startUiServer({
|
|
|
11767
12293
|
});
|
|
11768
12294
|
activeWorkspaceRuns.set(runKey, runEntry);
|
|
11769
12295
|
appendWorkspaceRunStarted(runEntry);
|
|
12296
|
+
broadcastWorkspaceCollaborationEvent(userCtx, scoped.flowSource, scoped.flowId, scoped.archived, {
|
|
12297
|
+
type: "run.started",
|
|
12298
|
+
runId,
|
|
12299
|
+
runNodeId,
|
|
12300
|
+
plannedNodeIds,
|
|
12301
|
+
revision: canonicalRevision,
|
|
12302
|
+
actorId: userCtx.userId || "",
|
|
12303
|
+
});
|
|
11770
12304
|
const setActiveChild = (child, childOptions = {}) => {
|
|
11771
12305
|
runControl.setChild(child, childOptions);
|
|
11772
12306
|
};
|
|
11773
12307
|
const clearActiveRun = (status = "finished") => {
|
|
11774
12308
|
runControl.finish(status);
|
|
11775
12309
|
if (activeWorkspaceRuns.get(runKey) === runEntry) activeWorkspaceRuns.delete(runKey);
|
|
12310
|
+
broadcastWorkspaceCollaborationEvent(userCtx, scoped.flowSource, scoped.flowId, scoped.archived, {
|
|
12311
|
+
type: "run.finished",
|
|
12312
|
+
status,
|
|
12313
|
+
runId,
|
|
12314
|
+
runNodeId,
|
|
12315
|
+
actorId: userCtx.userId || "",
|
|
12316
|
+
});
|
|
11776
12317
|
};
|
|
11777
12318
|
if (wantsStream) {
|
|
11778
12319
|
const graphPath = workspaceGraphPath(scoped.root);
|
|
@@ -11795,7 +12336,12 @@ export function startUiServer({
|
|
|
11795
12336
|
const currentGraph = readWorkspaceGraph(scoped.root).graph;
|
|
11796
12337
|
const touchedIds = workspaceRunTouchedNodeIds(result);
|
|
11797
12338
|
const mergedGraph = mergeWorkspaceRunGraph(currentGraph, result.graph, touchedIds);
|
|
11798
|
-
|
|
12339
|
+
writeWorkspaceGraphAtomic(graphPath, mergedGraph);
|
|
12340
|
+
const revision = workspaceDesignRevision(mergedGraph);
|
|
12341
|
+
const runtimeRevision = workspaceRuntimeRevision(mergedGraph);
|
|
12342
|
+
const collaborationEventType = revision === workspaceDesignRevision(currentGraph)
|
|
12343
|
+
? "runtime.committed"
|
|
12344
|
+
: "graph.committed";
|
|
11799
12345
|
const endedAt = Date.now();
|
|
11800
12346
|
appendWorkspaceRunFinished({
|
|
11801
12347
|
...runEntry,
|
|
@@ -11807,7 +12353,14 @@ export function startUiServer({
|
|
|
11807
12353
|
durationMs: endedAt - runEntry.startedAt,
|
|
11808
12354
|
runNodeId,
|
|
11809
12355
|
});
|
|
11810
|
-
|
|
12356
|
+
broadcastWorkspaceCollaborationEvent(userCtx, scoped.flowSource, scoped.flowId, scoped.archived, {
|
|
12357
|
+
type: collaborationEventType,
|
|
12358
|
+
revision,
|
|
12359
|
+
runtimeRevision,
|
|
12360
|
+
actorId: userCtx.userId || "",
|
|
12361
|
+
source: "run",
|
|
12362
|
+
});
|
|
12363
|
+
writeEvent({ type: "done", ok: true, path: graphPath, graph: mergedGraph, revision, runtimeRevision, order: result.order, touchedNodeIds: Array.from(touchedIds), pauseNodeIds: result.pauseNodeIds || [] });
|
|
11811
12364
|
res.end();
|
|
11812
12365
|
} catch (e) {
|
|
11813
12366
|
const endedAt = Date.now();
|
|
@@ -11854,7 +12407,12 @@ export function startUiServer({
|
|
|
11854
12407
|
const currentGraph = readWorkspaceGraph(scoped.root).graph;
|
|
11855
12408
|
const touchedIds = workspaceRunTouchedNodeIds(result);
|
|
11856
12409
|
const mergedGraph = mergeWorkspaceRunGraph(currentGraph, result.graph, touchedIds);
|
|
11857
|
-
|
|
12410
|
+
writeWorkspaceGraphAtomic(graphPath, mergedGraph);
|
|
12411
|
+
const revision = workspaceDesignRevision(mergedGraph);
|
|
12412
|
+
const runtimeRevision = workspaceRuntimeRevision(mergedGraph);
|
|
12413
|
+
const collaborationEventType = revision === workspaceDesignRevision(currentGraph)
|
|
12414
|
+
? "runtime.committed"
|
|
12415
|
+
: "graph.committed";
|
|
11858
12416
|
const endedAt = Date.now();
|
|
11859
12417
|
appendWorkspaceRunFinished({
|
|
11860
12418
|
...runEntry,
|
|
@@ -11866,7 +12424,14 @@ export function startUiServer({
|
|
|
11866
12424
|
durationMs: endedAt - runEntry.startedAt,
|
|
11867
12425
|
runNodeId,
|
|
11868
12426
|
});
|
|
11869
|
-
|
|
12427
|
+
broadcastWorkspaceCollaborationEvent(userCtx, scoped.flowSource, scoped.flowId, scoped.archived, {
|
|
12428
|
+
type: collaborationEventType,
|
|
12429
|
+
revision,
|
|
12430
|
+
runtimeRevision,
|
|
12431
|
+
actorId: userCtx.userId || "",
|
|
12432
|
+
source: "run",
|
|
12433
|
+
});
|
|
12434
|
+
json(res, 200, { ok: true, path: graphPath, ...result, graph: mergedGraph, revision, runtimeRevision, touchedNodeIds: Array.from(touchedIds) });
|
|
11870
12435
|
} catch (e) {
|
|
11871
12436
|
const endedAt = Date.now();
|
|
11872
12437
|
if (isWorkspaceRunAbortError(e) || controller.signal.aborted) {
|
|
@@ -11913,9 +12478,18 @@ export function startUiServer({
|
|
|
11913
12478
|
const scheduleNodeId = url.searchParams.get("scheduleNodeId") || "";
|
|
11914
12479
|
const runNodeId = url.searchParams.get("runNodeId") || "";
|
|
11915
12480
|
const limit = Number(url.searchParams.get("limit") || 50);
|
|
12481
|
+
const scoped = resolveWorkspaceScopeRoot(root, {
|
|
12482
|
+
flowId,
|
|
12483
|
+
flowSource: flowSource || "user",
|
|
12484
|
+
archived: url.searchParams.get("archived") === "1",
|
|
12485
|
+
}, userCtx);
|
|
12486
|
+
if (scoped.error) {
|
|
12487
|
+
json(res, scoped.status || 400, { error: scoped.error });
|
|
12488
|
+
return;
|
|
12489
|
+
}
|
|
11916
12490
|
json(res, 200, {
|
|
11917
12491
|
runs: listWorkspaceRunLogs({
|
|
11918
|
-
userId: userCtx.userId || "",
|
|
12492
|
+
userId: flowSource === "workspace" ? "" : userCtx.userId || "",
|
|
11919
12493
|
flowId,
|
|
11920
12494
|
flowSource,
|
|
11921
12495
|
scheduleNodeId,
|
|
@@ -11936,7 +12510,23 @@ export function startUiServer({
|
|
|
11936
12510
|
json(res, 400, { error: "Missing runId" });
|
|
11937
12511
|
return;
|
|
11938
12512
|
}
|
|
11939
|
-
const
|
|
12513
|
+
const flowId = url.searchParams.get("flowId") || "";
|
|
12514
|
+
const flowSource = url.searchParams.get("flowSource") || "user";
|
|
12515
|
+
const scoped = resolveWorkspaceScopeRoot(root, {
|
|
12516
|
+
flowId,
|
|
12517
|
+
flowSource,
|
|
12518
|
+
archived: url.searchParams.get("archived") === "1",
|
|
12519
|
+
}, userCtx);
|
|
12520
|
+
if (scoped.error) {
|
|
12521
|
+
json(res, scoped.status || 400, { error: scoped.error });
|
|
12522
|
+
return;
|
|
12523
|
+
}
|
|
12524
|
+
const run = listWorkspaceRunLogs({
|
|
12525
|
+
userId: flowSource === "workspace" ? "" : userCtx.userId || "",
|
|
12526
|
+
flowId,
|
|
12527
|
+
flowSource,
|
|
12528
|
+
limit: 200,
|
|
12529
|
+
})
|
|
11940
12530
|
.find((item) => String(item.runId || "") === runId);
|
|
11941
12531
|
if (!run) {
|
|
11942
12532
|
json(res, 404, { error: "Run log not found" });
|
|
@@ -11959,6 +12549,15 @@ export function startUiServer({
|
|
|
11959
12549
|
return;
|
|
11960
12550
|
}
|
|
11961
12551
|
const flowSource = url.searchParams.get("flowSource") || "user";
|
|
12552
|
+
const scoped = resolveWorkspaceScopeRoot(root, {
|
|
12553
|
+
flowId,
|
|
12554
|
+
flowSource,
|
|
12555
|
+
archived: url.searchParams.get("archived") === "1",
|
|
12556
|
+
}, userCtx);
|
|
12557
|
+
if (scoped.error) {
|
|
12558
|
+
json(res, scoped.status || 400, { error: scoped.error });
|
|
12559
|
+
return;
|
|
12560
|
+
}
|
|
11962
12561
|
const scopeKey = workspaceRunKey(userCtx, flowSource, flowId);
|
|
11963
12562
|
const entries = workspaceActiveRunsForScope(scopeKey).map(([, entry]) => entry);
|
|
11964
12563
|
const entry = entries[0] || null;
|
|
@@ -11996,7 +12595,21 @@ export function startUiServer({
|
|
|
11996
12595
|
json(res, 400, { error: "Missing flowId" });
|
|
11997
12596
|
return;
|
|
11998
12597
|
}
|
|
11999
|
-
const
|
|
12598
|
+
const flowSource = payload.flowSource || "user";
|
|
12599
|
+
const scoped = resolveWorkspaceScopeRoot(root, {
|
|
12600
|
+
flowId,
|
|
12601
|
+
flowSource,
|
|
12602
|
+
archived: payload.archived === true || payload.flowArchived === true,
|
|
12603
|
+
}, userCtx);
|
|
12604
|
+
if (scoped.error) {
|
|
12605
|
+
json(res, scoped.status || 400, { error: scoped.error });
|
|
12606
|
+
return;
|
|
12607
|
+
}
|
|
12608
|
+
if (scoped.collaborationAccess?.runnable === false) {
|
|
12609
|
+
json(res, 403, { error: "Workspace collaboration run permission denied" });
|
|
12610
|
+
return;
|
|
12611
|
+
}
|
|
12612
|
+
const scopeKey = workspaceRunKey(userCtx, flowSource, flowId);
|
|
12000
12613
|
const runId = String(payload.runId || payload.runSessionId || "").trim();
|
|
12001
12614
|
const runNodeId = String(payload.runNodeId || "").trim();
|
|
12002
12615
|
const entries = workspaceActiveRunsForScope(scopeKey);
|
|
@@ -12013,6 +12626,12 @@ export function startUiServer({
|
|
|
12013
12626
|
runNodeId: entry.runNodeId || "",
|
|
12014
12627
|
ts: Date.now(),
|
|
12015
12628
|
});
|
|
12629
|
+
broadcastWorkspaceCollaborationEvent(userCtx, scoped.flowSource, scoped.flowId, scoped.archived, {
|
|
12630
|
+
type: "run.stop-requested",
|
|
12631
|
+
runId: entry.runId,
|
|
12632
|
+
runNodeId: entry.runNodeId || "",
|
|
12633
|
+
actorId: userCtx.userId || "",
|
|
12634
|
+
});
|
|
12016
12635
|
const result = await entry.runControl.stop();
|
|
12017
12636
|
if (!result.stopped) {
|
|
12018
12637
|
appendWorkspaceRunLogEvent(entry.runId, {
|
|
@@ -12064,7 +12683,13 @@ export function startUiServer({
|
|
|
12064
12683
|
json(res, 413, { error: "File too large" });
|
|
12065
12684
|
return;
|
|
12066
12685
|
}
|
|
12067
|
-
|
|
12686
|
+
const content = fs.readFileSync(abs, "utf-8");
|
|
12687
|
+
json(res, 200, {
|
|
12688
|
+
path: rel,
|
|
12689
|
+
content,
|
|
12690
|
+
size: stat.size,
|
|
12691
|
+
revision: crypto.createHash("sha256").update(content).digest("hex"),
|
|
12692
|
+
});
|
|
12068
12693
|
} catch (e) {
|
|
12069
12694
|
json(res, /traversal/i.test(String(e.message || e)) ? 403 : 500, { error: (e && e.message) || String(e) });
|
|
12070
12695
|
}
|
|
@@ -12185,7 +12810,11 @@ export function startUiServer({
|
|
|
12185
12810
|
json(res, 400, { error: scoped.error });
|
|
12186
12811
|
return;
|
|
12187
12812
|
}
|
|
12188
|
-
if (
|
|
12813
|
+
if (
|
|
12814
|
+
scoped.archived
|
|
12815
|
+
|| isReadonlyBuiltinFlowSource(scoped.flowSource)
|
|
12816
|
+
|| scoped.collaborationAccess?.writable === false
|
|
12817
|
+
) {
|
|
12189
12818
|
json(res, 400, { error: "Cannot write to builtin or archived pipeline workspace" });
|
|
12190
12819
|
return;
|
|
12191
12820
|
}
|
|
@@ -12194,9 +12823,39 @@ export function startUiServer({
|
|
|
12194
12823
|
json(res, 400, { error: "Missing path" });
|
|
12195
12824
|
return;
|
|
12196
12825
|
}
|
|
12826
|
+
const content = String(payload.content ?? "");
|
|
12827
|
+
const baseRevision = String(payload.baseRevision || "").trim();
|
|
12828
|
+
if (baseRevision && fs.existsSync(abs) && fs.statSync(abs).isFile()) {
|
|
12829
|
+
const currentContent = fs.readFileSync(abs, "utf-8");
|
|
12830
|
+
const currentRevision = crypto.createHash("sha256").update(currentContent).digest("hex");
|
|
12831
|
+
if (currentRevision !== baseRevision) {
|
|
12832
|
+
json(res, 409, {
|
|
12833
|
+
error: "文件已被其他成员更新,请处理冲突后重试",
|
|
12834
|
+
conflict: "revision-mismatch",
|
|
12835
|
+
currentRevision,
|
|
12836
|
+
});
|
|
12837
|
+
return;
|
|
12838
|
+
}
|
|
12839
|
+
}
|
|
12197
12840
|
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
12198
|
-
|
|
12199
|
-
|
|
12841
|
+
const tmp = `${abs}.${process.pid}.${Date.now()}.tmp`;
|
|
12842
|
+
fs.writeFileSync(tmp, content, "utf-8");
|
|
12843
|
+
fs.renameSync(tmp, abs);
|
|
12844
|
+
const revision = crypto.createHash("sha256").update(content).digest("hex");
|
|
12845
|
+
broadcastWorkspaceCollaborationEvent(
|
|
12846
|
+
userCtx,
|
|
12847
|
+
scoped.flowSource,
|
|
12848
|
+
scoped.flowId,
|
|
12849
|
+
scoped.archived,
|
|
12850
|
+
{
|
|
12851
|
+
type: "file.committed",
|
|
12852
|
+
path: rel,
|
|
12853
|
+
revision,
|
|
12854
|
+
actorId: userCtx.userId || "",
|
|
12855
|
+
clientId: String(payload.clientId || ""),
|
|
12856
|
+
},
|
|
12857
|
+
);
|
|
12858
|
+
json(res, 200, { ok: true, path: rel, revision });
|
|
12200
12859
|
} catch (e) {
|
|
12201
12860
|
json(res, /traversal/i.test(String(e.message || e)) ? 403 : 500, { error: (e && e.message) || String(e) });
|
|
12202
12861
|
}
|
|
@@ -12225,7 +12884,7 @@ export function startUiServer({
|
|
|
12225
12884
|
json(res, 400, { error: scoped.error });
|
|
12226
12885
|
return;
|
|
12227
12886
|
}
|
|
12228
|
-
if (scoped.archived || isReadonlyBuiltinFlowSource(scoped.flowSource)) {
|
|
12887
|
+
if (scoped.archived || isReadonlyBuiltinFlowSource(scoped.flowSource) || scoped.collaborationAccess?.writable === false) {
|
|
12229
12888
|
json(res, 400, { error: "Cannot write to builtin or archived pipeline workspace" });
|
|
12230
12889
|
return;
|
|
12231
12890
|
}
|
|
@@ -12235,6 +12894,11 @@ export function startUiServer({
|
|
|
12235
12894
|
const target = uniqueWorkspaceRelPath(scoped.root, targetRel);
|
|
12236
12895
|
fs.mkdirSync(path.dirname(target.abs), { recursive: true });
|
|
12237
12896
|
fs.writeFileSync(target.abs, parsed.file);
|
|
12897
|
+
broadcastWorkspaceCollaborationEvent(userCtx, scoped.flowSource, scoped.flowId, scoped.archived, {
|
|
12898
|
+
type: "file.committed",
|
|
12899
|
+
path: target.rel,
|
|
12900
|
+
actorId: userCtx.userId || "",
|
|
12901
|
+
});
|
|
12238
12902
|
json(res, 200, {
|
|
12239
12903
|
ok: true,
|
|
12240
12904
|
path: target.rel,
|
|
@@ -12265,7 +12929,7 @@ export function startUiServer({
|
|
|
12265
12929
|
json(res, 400, { error: scoped.error });
|
|
12266
12930
|
return;
|
|
12267
12931
|
}
|
|
12268
|
-
if (scoped.archived || isReadonlyBuiltinFlowSource(scoped.flowSource)) {
|
|
12932
|
+
if (scoped.archived || isReadonlyBuiltinFlowSource(scoped.flowSource) || scoped.collaborationAccess?.writable === false) {
|
|
12269
12933
|
json(res, 400, { error: "Cannot write to builtin or archived pipeline workspace" });
|
|
12270
12934
|
return;
|
|
12271
12935
|
}
|
|
@@ -12275,6 +12939,11 @@ export function startUiServer({
|
|
|
12275
12939
|
return;
|
|
12276
12940
|
}
|
|
12277
12941
|
fs.mkdirSync(abs, { recursive: true });
|
|
12942
|
+
broadcastWorkspaceCollaborationEvent(userCtx, scoped.flowSource, scoped.flowId, scoped.archived, {
|
|
12943
|
+
type: "file.tree-changed",
|
|
12944
|
+
path: rel,
|
|
12945
|
+
actorId: userCtx.userId || "",
|
|
12946
|
+
});
|
|
12278
12947
|
json(res, 200, { ok: true, path: rel });
|
|
12279
12948
|
} catch (e) {
|
|
12280
12949
|
json(res, /traversal/i.test(String(e.message || e)) ? 403 : 500, { error: (e && e.message) || String(e) });
|
|
@@ -12300,7 +12969,7 @@ export function startUiServer({
|
|
|
12300
12969
|
json(res, 400, { error: scoped.error });
|
|
12301
12970
|
return;
|
|
12302
12971
|
}
|
|
12303
|
-
if (scoped.archived || isReadonlyBuiltinFlowSource(scoped.flowSource)) {
|
|
12972
|
+
if (scoped.archived || isReadonlyBuiltinFlowSource(scoped.flowSource) || scoped.collaborationAccess?.writable === false) {
|
|
12304
12973
|
json(res, 400, { error: "Cannot write to builtin or archived pipeline workspace" });
|
|
12305
12974
|
return;
|
|
12306
12975
|
}
|
|
@@ -12314,6 +12983,12 @@ export function startUiServer({
|
|
|
12314
12983
|
return;
|
|
12315
12984
|
}
|
|
12316
12985
|
fs.rmSync(abs, { recursive: true, force: true });
|
|
12986
|
+
broadcastWorkspaceCollaborationEvent(userCtx, scoped.flowSource, scoped.flowId, scoped.archived, {
|
|
12987
|
+
type: "file.tree-changed",
|
|
12988
|
+
path: rel,
|
|
12989
|
+
deleted: true,
|
|
12990
|
+
actorId: userCtx.userId || "",
|
|
12991
|
+
});
|
|
12317
12992
|
json(res, 200, { ok: true, path: rel });
|
|
12318
12993
|
} catch (e) {
|
|
12319
12994
|
json(res, /traversal/i.test(String(e.message || e)) ? 403 : 500, { error: (e && e.message) || String(e) });
|
|
@@ -12350,7 +13025,7 @@ export function startUiServer({
|
|
|
12350
13025
|
json(res, 200, { ok: true, conversations: readWorkspaceConversations(scoped.root) });
|
|
12351
13026
|
return;
|
|
12352
13027
|
}
|
|
12353
|
-
if (scoped.archived || isReadonlyBuiltinFlowSource(scoped.flowSource)) {
|
|
13028
|
+
if (scoped.archived || isReadonlyBuiltinFlowSource(scoped.flowSource) || scoped.collaborationAccess?.writable === false) {
|
|
12354
13029
|
json(res, 400, { error: "Cannot write conversations for builtin or archived pipeline workspace" });
|
|
12355
13030
|
return;
|
|
12356
13031
|
}
|
|
@@ -12385,6 +13060,10 @@ export function startUiServer({
|
|
|
12385
13060
|
json(res, 400, { error: scoped.error });
|
|
12386
13061
|
return;
|
|
12387
13062
|
}
|
|
13063
|
+
if (scoped.collaborationAccess?.writable === false) {
|
|
13064
|
+
json(res, 403, { error: "Workspace collaboration edit permission denied" });
|
|
13065
|
+
return;
|
|
13066
|
+
}
|
|
12388
13067
|
const selectedSkillKeys = Array.isArray(payload?.selectedSkills)
|
|
12389
13068
|
? payload.selectedSkills.map((x) => String(x || "").trim()).filter(Boolean)
|
|
12390
13069
|
: [];
|
|
@@ -12471,10 +13150,14 @@ export function startUiServer({
|
|
|
12471
13150
|
json(res, 400, { error: scoped.error });
|
|
12472
13151
|
return;
|
|
12473
13152
|
}
|
|
13153
|
+
if (scoped.collaborationAccess?.writable === false) {
|
|
13154
|
+
json(res, 403, { error: "Workspace collaboration edit permission denied" });
|
|
13155
|
+
return;
|
|
13156
|
+
}
|
|
12474
13157
|
const targetFilePath = String(payload?.targetFilePath || "").trim();
|
|
12475
13158
|
let targetFile = null;
|
|
12476
13159
|
if (targetFilePath) {
|
|
12477
|
-
if (scoped.archived || isReadonlyBuiltinFlowSource(scoped.flowSource)) {
|
|
13160
|
+
if (scoped.archived || isReadonlyBuiltinFlowSource(scoped.flowSource) || scoped.collaborationAccess?.writable === false) {
|
|
12478
13161
|
json(res, 400, { error: "Cannot edit builtin or archived pipeline workspace" });
|
|
12479
13162
|
return;
|
|
12480
13163
|
}
|
|
@@ -13335,12 +14018,26 @@ export function startUiServer({
|
|
|
13335
14018
|
return;
|
|
13336
14019
|
}
|
|
13337
14020
|
const flowArchived = url.searchParams.get("archived") === "1";
|
|
14021
|
+
const collaborationDenied = workspaceFlowCollaborationGuard(
|
|
14022
|
+
flowId,
|
|
14023
|
+
flowSource,
|
|
14024
|
+
flowArchived,
|
|
14025
|
+
userCtx,
|
|
14026
|
+
"read",
|
|
14027
|
+
);
|
|
14028
|
+
if (collaborationDenied) {
|
|
14029
|
+
json(res, collaborationDenied.status, { error: collaborationDenied.error });
|
|
14030
|
+
return;
|
|
14031
|
+
}
|
|
13338
14032
|
const result = readFlowJson(root, flowId, flowSource, { archived: flowArchived, ...userCtx });
|
|
13339
14033
|
if (result.error) {
|
|
13340
14034
|
json(res, 404, result);
|
|
13341
14035
|
return;
|
|
13342
14036
|
}
|
|
13343
|
-
json(res, 200,
|
|
14037
|
+
json(res, 200, {
|
|
14038
|
+
...result,
|
|
14039
|
+
revision: crypto.createHash("sha256").update(String(result.flowYaml || "")).digest("hex").slice(0, 24),
|
|
14040
|
+
});
|
|
13344
14041
|
return;
|
|
13345
14042
|
}
|
|
13346
14043
|
|
|
@@ -13519,12 +14216,53 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
13519
14216
|
return;
|
|
13520
14217
|
}
|
|
13521
14218
|
const flowArchived = Boolean(payload.flowArchived);
|
|
14219
|
+
const collaborationDenied = workspaceFlowCollaborationGuard(
|
|
14220
|
+
flowId,
|
|
14221
|
+
flowSource,
|
|
14222
|
+
flowArchived,
|
|
14223
|
+
userCtx,
|
|
14224
|
+
"write",
|
|
14225
|
+
);
|
|
14226
|
+
if (collaborationDenied) {
|
|
14227
|
+
json(res, collaborationDenied.status, { error: collaborationDenied.error });
|
|
14228
|
+
return;
|
|
14229
|
+
}
|
|
14230
|
+
if (flowSource === "workspace" && getWorkspaceCollaborationByFlow(flowId, flowArchived)) {
|
|
14231
|
+
const current = readFlowJson(root, flowId, flowSource, { archived: flowArchived, ...userCtx });
|
|
14232
|
+
if (current.error) {
|
|
14233
|
+
json(res, 404, current);
|
|
14234
|
+
return;
|
|
14235
|
+
}
|
|
14236
|
+
const currentRevision = crypto.createHash("sha256")
|
|
14237
|
+
.update(String(current.flowYaml || ""))
|
|
14238
|
+
.digest("hex")
|
|
14239
|
+
.slice(0, 24);
|
|
14240
|
+
const baseRevision = String(payload.baseRevision || "").trim();
|
|
14241
|
+
if (!baseRevision) {
|
|
14242
|
+
json(res, 428, { error: "Shared workspace save requires baseRevision", currentRevision });
|
|
14243
|
+
return;
|
|
14244
|
+
}
|
|
14245
|
+
if (baseRevision !== currentRevision) {
|
|
14246
|
+
json(res, 409, {
|
|
14247
|
+
error: "Workflow 已被其他成员更新,请处理冲突后重试",
|
|
14248
|
+
conflict: "revision-mismatch",
|
|
14249
|
+
expectedRevision: baseRevision,
|
|
14250
|
+
currentRevision,
|
|
14251
|
+
});
|
|
14252
|
+
return;
|
|
14253
|
+
}
|
|
14254
|
+
}
|
|
13522
14255
|
const result = writeFlowYaml(root, flowId, flowSource, flowYaml, { archived: flowArchived, ...userCtx });
|
|
13523
14256
|
if (!result.success) {
|
|
13524
14257
|
json(res, 400, result);
|
|
13525
14258
|
return;
|
|
13526
14259
|
}
|
|
13527
|
-
|
|
14260
|
+
broadcastFlowEditorSync(flowId, flowSource, flowArchived, userCtx.userId);
|
|
14261
|
+
const saved = readFlowJson(root, flowId, flowSource, { archived: flowArchived, ...userCtx });
|
|
14262
|
+
json(res, 200, {
|
|
14263
|
+
success: true,
|
|
14264
|
+
revision: crypto.createHash("sha256").update(String(saved.flowYaml || "")).digest("hex").slice(0, 24),
|
|
14265
|
+
});
|
|
13528
14266
|
return;
|
|
13529
14267
|
}
|
|
13530
14268
|
|
|
@@ -13547,6 +14285,17 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
13547
14285
|
return;
|
|
13548
14286
|
}
|
|
13549
14287
|
const flowArchived = Boolean(payload.flowArchived);
|
|
14288
|
+
const collaborationDenied = workspaceFlowCollaborationGuard(
|
|
14289
|
+
flowId,
|
|
14290
|
+
flowSource,
|
|
14291
|
+
flowArchived,
|
|
14292
|
+
userCtx,
|
|
14293
|
+
"read",
|
|
14294
|
+
);
|
|
14295
|
+
if (collaborationDenied) {
|
|
14296
|
+
json(res, collaborationDenied.status, { error: collaborationDenied.error });
|
|
14297
|
+
return;
|
|
14298
|
+
}
|
|
13550
14299
|
broadcastFlowEditorSync(flowId, flowSource, flowArchived, userCtx.userId);
|
|
13551
14300
|
json(res, 200, { ok: true });
|
|
13552
14301
|
return;
|
|
@@ -13564,6 +14313,17 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
13564
14313
|
return;
|
|
13565
14314
|
}
|
|
13566
14315
|
const flowArchived = url.searchParams.get("archived") === "1";
|
|
14316
|
+
const collaborationDenied = workspaceFlowCollaborationGuard(
|
|
14317
|
+
flowId,
|
|
14318
|
+
flowSource,
|
|
14319
|
+
flowArchived,
|
|
14320
|
+
userCtx,
|
|
14321
|
+
"read",
|
|
14322
|
+
);
|
|
14323
|
+
if (collaborationDenied) {
|
|
14324
|
+
json(res, collaborationDenied.status, { error: collaborationDenied.error });
|
|
14325
|
+
return;
|
|
14326
|
+
}
|
|
13567
14327
|
const key = flowEditorSyncKey(flowId, flowSource, flowArchived, userCtx.userId);
|
|
13568
14328
|
let set = flowEditorSyncSubscribers.get(key);
|
|
13569
14329
|
if (!set) {
|
|
@@ -13598,6 +14358,17 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
13598
14358
|
return;
|
|
13599
14359
|
}
|
|
13600
14360
|
const flowArchived = url.searchParams.get("archived") === "1";
|
|
14361
|
+
const collaborationDenied = workspaceFlowCollaborationGuard(
|
|
14362
|
+
flowId,
|
|
14363
|
+
flowSource,
|
|
14364
|
+
flowArchived,
|
|
14365
|
+
userCtx,
|
|
14366
|
+
"read",
|
|
14367
|
+
);
|
|
14368
|
+
if (collaborationDenied) {
|
|
14369
|
+
json(res, collaborationDenied.status, { error: collaborationDenied.error });
|
|
14370
|
+
return;
|
|
14371
|
+
}
|
|
13601
14372
|
const key = flowEditorSyncKey(flowId, flowSource, flowArchived, userCtx.userId);
|
|
13602
14373
|
const serverVer = flowEditorSyncVersions.get(key) ?? 0;
|
|
13603
14374
|
const clientVer = parseInt(url.searchParams.get("v") ?? "0", 10) || 0;
|
|
@@ -13628,11 +14399,21 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
13628
14399
|
json(res, 400, { error: "Invalid toSource" });
|
|
13629
14400
|
return;
|
|
13630
14401
|
}
|
|
14402
|
+
const collaborationDenied = workspaceFlowCollaborationGuard(flowId, fromSource, false, userCtx, "owner");
|
|
14403
|
+
if (collaborationDenied) {
|
|
14404
|
+
json(res, collaborationDenied.status, { error: collaborationDenied.error });
|
|
14405
|
+
return;
|
|
14406
|
+
}
|
|
13631
14407
|
const result = moveFlowDirectory(root, flowId.trim(), fromSource, toSource, userCtx);
|
|
13632
14408
|
if (!result.success) {
|
|
13633
14409
|
json(res, 400, { error: result.error || "Move failed" });
|
|
13634
14410
|
return;
|
|
13635
14411
|
}
|
|
14412
|
+
if (fromSource === "workspace" && toSource !== "workspace") {
|
|
14413
|
+
deleteWorkspaceCollaborationForFlow(flowId.trim(), false);
|
|
14414
|
+
} else if (fromSource !== "workspace" && toSource === "workspace") {
|
|
14415
|
+
ensureWorkspaceCollaboration({ flowId: flowId.trim(), userId: userCtx.userId });
|
|
14416
|
+
}
|
|
13636
14417
|
json(res, 200, { success: true, flowId: flowId.trim(), flowSource: result.flowSource });
|
|
13637
14418
|
return;
|
|
13638
14419
|
}
|
|
@@ -13656,6 +14437,11 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
13656
14437
|
json(res, 400, { error: "仅支持重命名用户目录或工作区流水线" });
|
|
13657
14438
|
return;
|
|
13658
14439
|
}
|
|
14440
|
+
const collaborationDenied = workspaceFlowCollaborationGuard(flowId, flowSource, false, userCtx, "owner");
|
|
14441
|
+
if (collaborationDenied) {
|
|
14442
|
+
json(res, collaborationDenied.status, { error: collaborationDenied.error });
|
|
14443
|
+
return;
|
|
14444
|
+
}
|
|
13659
14445
|
const validation = validateUserPipelineId(newFlowId);
|
|
13660
14446
|
if (!validation.ok) {
|
|
13661
14447
|
json(res, 400, { error: validation.error });
|
|
@@ -13678,6 +14464,12 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
13678
14464
|
}
|
|
13679
14465
|
try {
|
|
13680
14466
|
fs.renameSync(fromDir, toDir);
|
|
14467
|
+
updateWorkspaceCollaborationFlow({
|
|
14468
|
+
previousFlowId: flowId,
|
|
14469
|
+
flowSource,
|
|
14470
|
+
ownerId: userCtx.userId,
|
|
14471
|
+
flowId: validation.flowId,
|
|
14472
|
+
});
|
|
13681
14473
|
json(res, 200, { success: true, flowId: validation.flowId, flowSource });
|
|
13682
14474
|
} catch (e) {
|
|
13683
14475
|
json(res, 500, { error: (e && e.message) || String(e) });
|
|
@@ -13708,11 +14500,24 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
13708
14500
|
json(res, 400, { error: "仅支持归档用户目录或工作区流水线" });
|
|
13709
14501
|
return;
|
|
13710
14502
|
}
|
|
14503
|
+
const collaborationDenied = workspaceFlowCollaborationGuard(flowId, flowSource, false, userCtx, "owner");
|
|
14504
|
+
if (collaborationDenied) {
|
|
14505
|
+
json(res, collaborationDenied.status, { error: collaborationDenied.error });
|
|
14506
|
+
return;
|
|
14507
|
+
}
|
|
13711
14508
|
const result = archiveFlowPipeline(root, flowId, flowSource, userCtx);
|
|
13712
14509
|
if (!result.success) {
|
|
13713
14510
|
json(res, 400, { error: result.error || "归档失败" });
|
|
13714
14511
|
return;
|
|
13715
14512
|
}
|
|
14513
|
+
updateWorkspaceCollaborationFlow({
|
|
14514
|
+
previousFlowId: flowId,
|
|
14515
|
+
previousArchived: false,
|
|
14516
|
+
flowSource,
|
|
14517
|
+
ownerId: userCtx.userId,
|
|
14518
|
+
flowId,
|
|
14519
|
+
archived: true,
|
|
14520
|
+
});
|
|
13716
14521
|
json(res, 200, { success: true, flowId, flowSource, archived: true });
|
|
13717
14522
|
return;
|
|
13718
14523
|
}
|
|
@@ -13741,11 +14546,53 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
13741
14546
|
json(res, 400, { error: "仅支持删除用户目录或工作区流水线" });
|
|
13742
14547
|
return;
|
|
13743
14548
|
}
|
|
14549
|
+
const collaboration = getWorkspaceCollaborationForProject({
|
|
14550
|
+
workspaceId: payload.workspaceId || "",
|
|
14551
|
+
flowId,
|
|
14552
|
+
flowSource,
|
|
14553
|
+
archived: flowArchived,
|
|
14554
|
+
ownerId: userCtx.userId,
|
|
14555
|
+
}) || listWorkspaceCollaborationsForUser(userCtx.userId).find((record) => (
|
|
14556
|
+
record.flowId === flowId
|
|
14557
|
+
&& record.archived === flowArchived
|
|
14558
|
+
&& (record.projectSource || record.flowSource || "workspace") === flowSource
|
|
14559
|
+
)) || null;
|
|
14560
|
+
const collaborationAccess = workspaceCollaborationAccess(collaboration, userCtx.userId);
|
|
14561
|
+
if (collaboration && collaborationAccess.allowed && collaborationAccess.role !== "owner") {
|
|
14562
|
+
const left = removeWorkspaceCollaborationMember({
|
|
14563
|
+
workspaceId: collaboration.id,
|
|
14564
|
+
userId: userCtx.userId,
|
|
14565
|
+
});
|
|
14566
|
+
if (left.error) {
|
|
14567
|
+
json(res, left.status || 400, { error: left.error });
|
|
14568
|
+
return;
|
|
14569
|
+
}
|
|
14570
|
+
broadcastWorkspaceCollaborationEvent(userCtx, flowSource, flowId, flowArchived, {
|
|
14571
|
+
type: "member.left",
|
|
14572
|
+
actorId: userCtx.userId || "",
|
|
14573
|
+
memberUserId: userCtx.userId || "",
|
|
14574
|
+
});
|
|
14575
|
+
json(res, 200, {
|
|
14576
|
+
success: true,
|
|
14577
|
+
flowId,
|
|
14578
|
+
flowSource,
|
|
14579
|
+
deleted: false,
|
|
14580
|
+
left: true,
|
|
14581
|
+
});
|
|
14582
|
+
return;
|
|
14583
|
+
}
|
|
14584
|
+
const collaborationDenied = workspaceFlowCollaborationGuard(flowId, flowSource, flowArchived, userCtx, "owner");
|
|
14585
|
+
if (collaborationDenied) {
|
|
14586
|
+
json(res, collaborationDenied.status, { error: collaborationDenied.error });
|
|
14587
|
+
return;
|
|
14588
|
+
}
|
|
13744
14589
|
const result = deleteFlowPipeline(root, flowId, flowSource, { archived: flowArchived, ...userCtx });
|
|
13745
14590
|
if (!result.success) {
|
|
13746
14591
|
json(res, 400, { error: result.error || "删除失败" });
|
|
13747
14592
|
return;
|
|
13748
14593
|
}
|
|
14594
|
+
if (collaboration?.id) deleteWorkspaceCollaborationById(collaboration.id);
|
|
14595
|
+
else deleteWorkspaceCollaborationForFlow(flowId, flowArchived);
|
|
13749
14596
|
json(res, 200, { success: true, flowId, flowSource, deleted: true });
|
|
13750
14597
|
return;
|
|
13751
14598
|
}
|
|
@@ -14008,8 +14855,20 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
14008
14855
|
json(res, 400, { error: "Missing flowId" });
|
|
14009
14856
|
return;
|
|
14010
14857
|
}
|
|
14858
|
+
const flowSource = payload.flowSource || "user";
|
|
14859
|
+
const collaborationDenied = workspaceFlowCollaborationGuard(
|
|
14860
|
+
flowId,
|
|
14861
|
+
flowSource,
|
|
14862
|
+
false,
|
|
14863
|
+
userCtx,
|
|
14864
|
+
"run",
|
|
14865
|
+
);
|
|
14866
|
+
if (collaborationDenied) {
|
|
14867
|
+
json(res, collaborationDenied.status, { error: collaborationDenied.error });
|
|
14868
|
+
return;
|
|
14869
|
+
}
|
|
14011
14870
|
const runUuid = typeof payload.uuid === "string" ? payload.uuid.trim() : "";
|
|
14012
|
-
const runKey =
|
|
14871
|
+
const runKey = workspaceRunKey(userCtx, flowSource, flowId);
|
|
14013
14872
|
if (activeFlowRuns.has(runKey)) {
|
|
14014
14873
|
json(res, 409, { error: "该流水线已在运行中" });
|
|
14015
14874
|
return;
|
|
@@ -14088,7 +14947,7 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
14088
14947
|
}
|
|
14089
14948
|
|
|
14090
14949
|
/** @type {{ child: import("child_process").ChildProcess, runUuid: string | null }} */
|
|
14091
|
-
const runEntry = { child, runUuid: runUuid || null };
|
|
14950
|
+
const runEntry = { child, runUuid: runUuid || null, userId: userCtx.userId || "" };
|
|
14092
14951
|
activeFlowRuns.set(runKey, runEntry);
|
|
14093
14952
|
log.debug(`[ui] flow/run: spawned pid=${child.pid} flowId=${flowId}${runUuid ? ` uuid=${runUuid}` : ""}`);
|
|
14094
14953
|
|
|
@@ -14162,7 +15021,19 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
14162
15021
|
json(res, 400, { error: "Missing flowId" });
|
|
14163
15022
|
return;
|
|
14164
15023
|
}
|
|
14165
|
-
const
|
|
15024
|
+
const flowSource = payload.flowSource || "user";
|
|
15025
|
+
const collaborationDenied = workspaceFlowCollaborationGuard(
|
|
15026
|
+
flowId,
|
|
15027
|
+
flowSource,
|
|
15028
|
+
false,
|
|
15029
|
+
userCtx,
|
|
15030
|
+
"run",
|
|
15031
|
+
);
|
|
15032
|
+
if (collaborationDenied) {
|
|
15033
|
+
json(res, collaborationDenied.status, { error: collaborationDenied.error });
|
|
15034
|
+
return;
|
|
15035
|
+
}
|
|
15036
|
+
const runKey = workspaceRunKey(userCtx, flowSource, flowId);
|
|
14166
15037
|
const entry = activeFlowRuns.get(runKey);
|
|
14167
15038
|
if (!entry || !entry.child) {
|
|
14168
15039
|
json(res, 404, { error: "该流水线未在运行" });
|
|
@@ -14184,7 +15055,7 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
14184
15055
|
activeFlowRuns.delete(runKey);
|
|
14185
15056
|
if (uuid) {
|
|
14186
15057
|
try {
|
|
14187
|
-
const runDir = getRunDir(root, flowId, uuid, userCtx);
|
|
15058
|
+
const runDir = getRunDir(root, flowId, uuid, { userId: entry.userId || userCtx.userId || "" });
|
|
14188
15059
|
fs.mkdirSync(runDir, { recursive: true });
|
|
14189
15060
|
fs.writeFileSync(
|
|
14190
15061
|
path.join(runDir, RUN_INTERRUPTED_FILENAME),
|