@fieldwangai/agentflow 0.1.100 → 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 +1151 -96
- 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-DKaacU6h.css +0 -1
- package/builtin/web-ui/dist/assets/index-h35s4X2i.js +0 -346
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 || ""),
|
|
@@ -7421,9 +7533,7 @@ function prdWorkflowReviewRenderTable(lines) {
|
|
|
7421
7533
|
].join("");
|
|
7422
7534
|
}
|
|
7423
7535
|
|
|
7424
|
-
function
|
|
7425
|
-
const { frontmatter, body } = prdWorkflowReviewSplitFrontmatter(markdown);
|
|
7426
|
-
const lines = String(body || "").replace(/\r\n/g, "\n").split("\n");
|
|
7536
|
+
function prdWorkflowReviewMarkdownLinesToHtml(lines) {
|
|
7427
7537
|
const html = [];
|
|
7428
7538
|
let paragraph = [];
|
|
7429
7539
|
let list = [];
|
|
@@ -7500,11 +7610,147 @@ function prdWorkflowReviewMarkdownToHtml(markdown) {
|
|
|
7500
7610
|
}
|
|
7501
7611
|
if (code) html.push(`<pre><code>${htmlEscapeAttribute(prdWorkflowReviewNormalizeText(code.lines.join("\n")))}</code></pre>`);
|
|
7502
7612
|
flushBlocks();
|
|
7503
|
-
return
|
|
7613
|
+
return html.join("\n");
|
|
7614
|
+
}
|
|
7615
|
+
|
|
7616
|
+
function prdWorkflowReviewActionLine(line) {
|
|
7617
|
+
const match = String(line || "").match(/^\s*[-*]\s+(?:\[( |x|X)\]\s+)?(A\d+|Action\s*\d+)(?=\s|[((::.-]|$)(.*)$/i);
|
|
7618
|
+
if (!match) return null;
|
|
7619
|
+
return {
|
|
7620
|
+
checked: match[1] ? match[1].toLowerCase() === "x" : null,
|
|
7621
|
+
label: match[2].replace(/\s+/g, " ").toUpperCase(),
|
|
7622
|
+
title: String(match[3] || "").replace(/^\s*[-::]\s*/, "").trim(),
|
|
7623
|
+
};
|
|
7624
|
+
}
|
|
7625
|
+
|
|
7626
|
+
function prdWorkflowReviewIsActionsHeading(line) {
|
|
7627
|
+
const heading = String(line || "").trim().match(/^(#{1,6})\s+(.+)$/);
|
|
7628
|
+
if (!heading) return null;
|
|
7629
|
+
const title = heading[2].replace(/[*_`]/g, "").trim();
|
|
7630
|
+
if (!/(?:\bTODO\s+Actions?\b|\bActions?\b|待办(?:事项|行动)?|行动项)/i.test(title)) return null;
|
|
7631
|
+
return { level: heading[1].length };
|
|
7504
7632
|
}
|
|
7505
7633
|
|
|
7506
|
-
function
|
|
7507
|
-
const
|
|
7634
|
+
function prdWorkflowReviewRenderActionSection(lines, sectionIndex) {
|
|
7635
|
+
const actionStarts = [];
|
|
7636
|
+
lines.forEach((line, index) => {
|
|
7637
|
+
const action = prdWorkflowReviewActionLine(line);
|
|
7638
|
+
if (action) actionStarts.push({ index, action });
|
|
7639
|
+
});
|
|
7640
|
+
if (!actionStarts.length) return prdWorkflowReviewMarkdownLinesToHtml(lines);
|
|
7641
|
+
|
|
7642
|
+
const ids = new Map();
|
|
7643
|
+
const actions = actionStarts.map(({ index, action }, actionIndex) => {
|
|
7644
|
+
const nextStart = actionStarts[actionIndex + 1]?.index ?? lines.length;
|
|
7645
|
+
let bodyStart = index + 1;
|
|
7646
|
+
const titleParts = [action.title].filter(Boolean);
|
|
7647
|
+
while (bodyStart < nextStart) {
|
|
7648
|
+
const continuation = String(lines[bodyStart] || "");
|
|
7649
|
+
if (!/^\s{2,}\S/.test(continuation) || /^\s*[-*]\s+/.test(continuation) || /^\s*```/.test(continuation)) break;
|
|
7650
|
+
titleParts.push(continuation.trim());
|
|
7651
|
+
bodyStart += 1;
|
|
7652
|
+
}
|
|
7653
|
+
const labelId = action.label.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || actionIndex + 1;
|
|
7654
|
+
const baseId = `action-${labelId}${sectionIndex ? `-${sectionIndex + 1}` : ""}`;
|
|
7655
|
+
const duplicate = ids.get(baseId) || 0;
|
|
7656
|
+
ids.set(baseId, duplicate + 1);
|
|
7657
|
+
return {
|
|
7658
|
+
...action,
|
|
7659
|
+
id: duplicate ? `${baseId}-${sectionIndex + 1}-${duplicate + 1}` : baseId,
|
|
7660
|
+
title: titleParts.join(" ") || action.label,
|
|
7661
|
+
body: lines.slice(bodyStart, nextStart),
|
|
7662
|
+
};
|
|
7663
|
+
});
|
|
7664
|
+
|
|
7665
|
+
const intro = prdWorkflowReviewMarkdownLinesToHtml(lines.slice(0, actionStarts[0].index));
|
|
7666
|
+
const navigation = actions.length > 1
|
|
7667
|
+
? `<nav class="action-index" aria-label="Action 快速跳转"><span class="action-index__label">快速跳转</span>${actions.map((action) => `<a href="#${htmlEscapeAttribute(action.id)}">${htmlEscapeAttribute(action.label)}</a>`).join("")}</nav>`
|
|
7668
|
+
: "";
|
|
7669
|
+
const cards = actions.map((action) => {
|
|
7670
|
+
const status = action.checked === null
|
|
7671
|
+
? ""
|
|
7672
|
+
: `<span class="action-card__status${action.checked ? " is-complete" : ""}">${action.checked ? "已完成" : "待完成"}</span>`;
|
|
7673
|
+
const body = prdWorkflowReviewMarkdownLinesToHtml(action.body);
|
|
7674
|
+
return `<section class="action-card${action.checked ? " is-complete" : ""}" id="${htmlEscapeAttribute(action.id)}">
|
|
7675
|
+
<div class="action-card__header">
|
|
7676
|
+
<span class="action-card__index">${htmlEscapeAttribute(action.label)}</span>
|
|
7677
|
+
<h3 class="action-card__title">${prdWorkflowReviewInlineMarkdown(action.title)}</h3>
|
|
7678
|
+
${status}
|
|
7679
|
+
</div>
|
|
7680
|
+
<div class="action-card__body">${body}</div>
|
|
7681
|
+
</section>`;
|
|
7682
|
+
}).join("\n");
|
|
7683
|
+
return [intro, navigation, cards].filter(Boolean).join("\n");
|
|
7684
|
+
}
|
|
7685
|
+
|
|
7686
|
+
function prdWorkflowReviewBodyToHtml(body) {
|
|
7687
|
+
const lines = String(body || "").replace(/\r\n/g, "\n").split("\n");
|
|
7688
|
+
const html = [];
|
|
7689
|
+
let cursor = 0;
|
|
7690
|
+
let sectionIndex = 0;
|
|
7691
|
+
while (cursor < lines.length) {
|
|
7692
|
+
const actionsHeading = prdWorkflowReviewIsActionsHeading(lines[cursor]);
|
|
7693
|
+
if (!actionsHeading) {
|
|
7694
|
+
const nextHeading = lines.findIndex((line, index) => index > cursor && prdWorkflowReviewIsActionsHeading(line));
|
|
7695
|
+
const end = nextHeading >= 0 ? nextHeading : lines.length;
|
|
7696
|
+
html.push(prdWorkflowReviewMarkdownLinesToHtml(lines.slice(cursor, end)));
|
|
7697
|
+
cursor = end;
|
|
7698
|
+
continue;
|
|
7699
|
+
}
|
|
7700
|
+
let end = cursor + 1;
|
|
7701
|
+
while (end < lines.length) {
|
|
7702
|
+
const heading = String(lines[end] || "").trim().match(/^(#{1,6})\s+/);
|
|
7703
|
+
if (heading && heading[1].length <= actionsHeading.level) break;
|
|
7704
|
+
end += 1;
|
|
7705
|
+
}
|
|
7706
|
+
html.push(prdWorkflowReviewMarkdownLinesToHtml([lines[cursor]]));
|
|
7707
|
+
html.push(prdWorkflowReviewRenderActionSection(lines.slice(cursor + 1, end), sectionIndex));
|
|
7708
|
+
sectionIndex += 1;
|
|
7709
|
+
cursor = end;
|
|
7710
|
+
}
|
|
7711
|
+
return html.filter(Boolean).join("\n");
|
|
7712
|
+
}
|
|
7713
|
+
|
|
7714
|
+
export function prdWorkflowReviewMarkdownToHtml(markdown) {
|
|
7715
|
+
const { frontmatter, body } = prdWorkflowReviewSplitFrontmatter(markdown);
|
|
7716
|
+
return [
|
|
7717
|
+
prdWorkflowReviewRenderFrontmatter(frontmatter),
|
|
7718
|
+
prdWorkflowReviewBodyToHtml(body),
|
|
7719
|
+
].filter(Boolean).join("\n");
|
|
7720
|
+
}
|
|
7721
|
+
|
|
7722
|
+
function prdWorkflowReviewExtractPageTitle(markdown, fallbackTitle) {
|
|
7723
|
+
const { frontmatter, body } = prdWorkflowReviewSplitFrontmatter(markdown);
|
|
7724
|
+
const lines = String(body || "").replace(/\r\n/g, "\n").split("\n");
|
|
7725
|
+
const firstContentIndex = lines.findIndex((line) => String(line || "").trim());
|
|
7726
|
+
const heading = firstContentIndex >= 0
|
|
7727
|
+
? String(lines[firstContentIndex] || "").trim().match(/^#\s+(.+)$/)
|
|
7728
|
+
: null;
|
|
7729
|
+
if (!heading) {
|
|
7730
|
+
return {
|
|
7731
|
+
markdown: String(markdown || ""),
|
|
7732
|
+
title: String(fallbackTitle || "PRD Workflow Review"),
|
|
7733
|
+
};
|
|
7734
|
+
}
|
|
7735
|
+
|
|
7736
|
+
lines.splice(firstContentIndex, 1);
|
|
7737
|
+
const markdownTitle = prdWorkflowReviewNormalizeText(heading[1])
|
|
7738
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
|
|
7739
|
+
.replace(/[`*_~]/g, "")
|
|
7740
|
+
.trim();
|
|
7741
|
+
const bodyWithoutTitle = lines.join("\n").replace(/^\n+/, "");
|
|
7742
|
+
return {
|
|
7743
|
+
markdown: [
|
|
7744
|
+
frontmatter ? `---\n${frontmatter}\n---` : "",
|
|
7745
|
+
bodyWithoutTitle,
|
|
7746
|
+
].filter(Boolean).join("\n\n"),
|
|
7747
|
+
title: markdownTitle || String(fallbackTitle || "PRD Workflow Review"),
|
|
7748
|
+
};
|
|
7749
|
+
}
|
|
7750
|
+
|
|
7751
|
+
export function prdWorkflowReviewHtml(title, markdown, meta = {}) {
|
|
7752
|
+
const page = prdWorkflowReviewExtractPageTitle(markdown, title);
|
|
7753
|
+
const escapedTitle = htmlEscapeAttribute(page.title);
|
|
7508
7754
|
const escapedMeta = htmlEscapeAttribute([
|
|
7509
7755
|
meta.tapdId ? `TAPD ${meta.tapdId}` : "",
|
|
7510
7756
|
meta.stage ? `stage ${meta.stage}` : "",
|
|
@@ -7518,7 +7764,7 @@ function prdWorkflowReviewHtml(title, markdown, meta = {}) {
|
|
|
7518
7764
|
meta.persistence ? `persistence ${meta.persistence}` : "",
|
|
7519
7765
|
].filter(Boolean).join(" · ");
|
|
7520
7766
|
const escapedLifecycle = htmlEscapeAttribute(lifecycle);
|
|
7521
|
-
const renderedMarkdown = prdWorkflowReviewMarkdownToHtml(markdown
|
|
7767
|
+
const renderedMarkdown = prdWorkflowReviewMarkdownToHtml(page.markdown);
|
|
7522
7768
|
const rawHref = htmlEscapeAttribute(meta.rawHref || "?raw=1");
|
|
7523
7769
|
return `<!doctype html>
|
|
7524
7770
|
<html lang="zh-CN" data-theme="dark">
|
|
@@ -7530,57 +7776,80 @@ function prdWorkflowReviewHtml(title, markdown, meta = {}) {
|
|
|
7530
7776
|
:root {
|
|
7531
7777
|
color-scheme: dark;
|
|
7532
7778
|
font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
7533
|
-
--bg: #
|
|
7534
|
-
--
|
|
7535
|
-
--panel:
|
|
7536
|
-
--panel-
|
|
7537
|
-
--
|
|
7538
|
-
--border:
|
|
7539
|
-
--border-soft: rgba(148,163,184,.14);
|
|
7779
|
+
--bg: #1a1b26;
|
|
7780
|
+
--panel: #1f2335;
|
|
7781
|
+
--panel-strong: #24283b;
|
|
7782
|
+
--panel-soft: #292e42;
|
|
7783
|
+
--border: #3b4261;
|
|
7784
|
+
--border-soft: #30364f;
|
|
7540
7785
|
--text: #e5e7eb;
|
|
7541
|
-
--heading: #
|
|
7542
|
-
--muted: #
|
|
7543
|
-
--body: #
|
|
7544
|
-
--link: #
|
|
7545
|
-
--
|
|
7546
|
-
--
|
|
7547
|
-
--
|
|
7548
|
-
--
|
|
7549
|
-
--
|
|
7786
|
+
--heading: #f4f4f5;
|
|
7787
|
+
--muted: #8b90a0;
|
|
7788
|
+
--body: #d4d4d8;
|
|
7789
|
+
--link: #7dcfff;
|
|
7790
|
+
--interactive: #7aa2f7;
|
|
7791
|
+
--purple: #bb9af7;
|
|
7792
|
+
--button: #24283b;
|
|
7793
|
+
--button-text: #cbd0da;
|
|
7794
|
+
--code-bg: #292e42;
|
|
7795
|
+
--code-inline: #7dcfff;
|
|
7796
|
+
--code-block: #16161e;
|
|
7797
|
+
--code-block-text: #e5e7eb;
|
|
7798
|
+
--action-bg: #1f2335;
|
|
7799
|
+
--action-header: #24283b;
|
|
7800
|
+
--pending-bg: rgba(224,175,104,.10);
|
|
7801
|
+
--pending-border: rgba(224,175,104,.34);
|
|
7802
|
+
--pending-text: #e0af68;
|
|
7803
|
+
--complete-bg: rgba(158,206,106,.10);
|
|
7804
|
+
--complete-border: rgba(158,206,106,.34);
|
|
7805
|
+
--complete-text: #9ece6a;
|
|
7806
|
+
--shadow: rgba(9,10,15,.28);
|
|
7550
7807
|
background: var(--bg);
|
|
7551
7808
|
}
|
|
7552
7809
|
:root[data-theme="light"] {
|
|
7553
7810
|
color-scheme: light;
|
|
7554
|
-
--bg: #
|
|
7555
|
-
--
|
|
7556
|
-
--panel:
|
|
7557
|
-
--panel-
|
|
7558
|
-
--
|
|
7559
|
-
--border:
|
|
7560
|
-
--
|
|
7561
|
-
--
|
|
7562
|
-
--
|
|
7563
|
-
--
|
|
7564
|
-
--
|
|
7565
|
-
--
|
|
7566
|
-
--
|
|
7567
|
-
--button
|
|
7568
|
-
--
|
|
7569
|
-
--code-
|
|
7570
|
-
--
|
|
7811
|
+
--bg: #e1e2e7;
|
|
7812
|
+
--panel: #f3f3f5;
|
|
7813
|
+
--panel-strong: #e9e9ed;
|
|
7814
|
+
--panel-soft: #dcdfe7;
|
|
7815
|
+
--border: #c8cad4;
|
|
7816
|
+
--border-soft: #d5d7df;
|
|
7817
|
+
--text: #4c505e;
|
|
7818
|
+
--heading: #343b58;
|
|
7819
|
+
--muted: #7f849c;
|
|
7820
|
+
--body: #4c505e;
|
|
7821
|
+
--link: #007197;
|
|
7822
|
+
--interactive: #2e7de9;
|
|
7823
|
+
--purple: #7847bd;
|
|
7824
|
+
--button: #e7e8ed;
|
|
7825
|
+
--button-text: #4c505e;
|
|
7826
|
+
--code-bg: #dcdfe7;
|
|
7827
|
+
--code-inline: #007197;
|
|
7828
|
+
--code-block: #d5d8e1;
|
|
7829
|
+
--code-block-text: #343b58;
|
|
7830
|
+
--action-bg: #f3f3f5;
|
|
7831
|
+
--action-header: #e9e9ed;
|
|
7832
|
+
--pending-bg: rgba(177,92,0,.08);
|
|
7833
|
+
--pending-border: rgba(177,92,0,.28);
|
|
7834
|
+
--pending-text: #9a5200;
|
|
7835
|
+
--complete-bg: rgba(88,117,57,.10);
|
|
7836
|
+
--complete-border: rgba(88,117,57,.28);
|
|
7837
|
+
--complete-text: #587539;
|
|
7838
|
+
--shadow: rgba(52,59,88,.10);
|
|
7571
7839
|
}
|
|
7572
7840
|
*, *::before, *::after { box-sizing: border-box; }
|
|
7573
7841
|
html, body { overflow-x: hidden; }
|
|
7574
|
-
body { margin: 0; min-height: 100vh; background:
|
|
7575
|
-
main { width: min(100%,
|
|
7842
|
+
body { margin: 0; min-height: 100vh; background: var(--bg); color: var(--text); }
|
|
7843
|
+
main { width: min(100%, 1180px); margin: 0 auto; padding: 40px 24px 72px; min-width: 0; }
|
|
7576
7844
|
header { display: flex; align-items: flex-start; justify-content: space-between; gap: 1rem; margin-bottom: 22px; }
|
|
7577
|
-
h1 { margin: 0 0 10px; font-size: clamp(28px,
|
|
7845
|
+
h1 { margin: 0 0 10px; font-size: clamp(28px, 4vw, 44px); line-height: 1.12; letter-spacing: -.015em; }
|
|
7578
7846
|
.meta { margin: 0; color: var(--muted); font-size: 14px; }
|
|
7579
7847
|
.toolbar { flex: 0 0 auto; display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 10px; }
|
|
7580
|
-
.raw, .theme-toggle { border: 1px solid
|
|
7848
|
+
.raw, .theme-toggle { border: 1px solid var(--border); border-radius: 999px; color: var(--button-text); background: var(--button); padding: 9px 14px; text-decoration: none; font-size: 13px; font-weight: 800; line-height: 1.2; }
|
|
7849
|
+
.raw:hover, .theme-toggle:hover { border-color: var(--interactive); color: var(--link); }
|
|
7581
7850
|
.theme-toggle { cursor: pointer; font-family: inherit; }
|
|
7582
|
-
.lifecycle { margin-top: 10px; display: inline-flex; max-width: 100%; border: 1px solid
|
|
7583
|
-
article { min-width: 0; border: 1px solid var(--border); border-radius: 14px; background: var(--panel); box-shadow: 0
|
|
7851
|
+
.lifecycle { margin-top: 10px; display: inline-flex; max-width: 100%; border: 1px solid var(--border); border-radius: 999px; background: var(--button); color: var(--muted); padding: 6px 10px; font-size: 12px; font-weight: 800; line-height: 1.35; overflow-wrap: anywhere; }
|
|
7852
|
+
article { min-width: 0; border: 1px solid var(--border); border-radius: 14px; background: var(--panel); box-shadow: 0 18px 50px var(--shadow); padding: clamp(20px, 4vw, 34px); }
|
|
7584
7853
|
article > *:first-child { margin-top: 0; }
|
|
7585
7854
|
article > *:last-child { margin-bottom: 0; }
|
|
7586
7855
|
h2, h3, h4, h5, h6 { margin: 1.7em 0 .65em; line-height: 1.25; letter-spacing: 0; color: var(--heading); overflow-wrap: anywhere; }
|
|
@@ -7591,10 +7860,10 @@ function prdWorkflowReviewHtml(title, markdown, meta = {}) {
|
|
|
7591
7860
|
ul { margin: .65rem 0 1rem; padding-left: 1.35rem; }
|
|
7592
7861
|
li { margin: .28rem 0; color: var(--body); }
|
|
7593
7862
|
li input { margin-right: .38rem; transform: translateY(1px); }
|
|
7594
|
-
code { display: inline; max-width: 100%; border: 1px solid var(--border-soft); border-radius: 6px; background: var(--code-bg); color: var(--
|
|
7863
|
+
code { display: inline; max-width: 100%; border: 1px solid var(--border-soft); border-radius: 6px; background: var(--code-bg); color: var(--code-inline); padding: .1rem .34rem; font-family: "SFMono-Regular", Consolas, monospace; font-size: .92em; white-space: normal; overflow-wrap: anywhere; word-break: break-word; }
|
|
7595
7864
|
pre { max-width: 100%; overflow: auto; border: 1px solid var(--border); border-radius: 10px; background: var(--code-block); padding: 16px; line-height: 1.65; }
|
|
7596
|
-
pre code { border: 0; background: transparent; padding: 0; white-space: pre; overflow-wrap: normal; word-break: normal; }
|
|
7597
|
-
blockquote { margin: 1rem 0; border-left: 3px solid
|
|
7865
|
+
pre code { border: 0; background: transparent; color: var(--code-block-text); padding: 0; white-space: pre; overflow-wrap: normal; word-break: normal; }
|
|
7866
|
+
blockquote { margin: 1rem 0; border-left: 3px solid var(--purple); background: var(--panel-soft); padding: .75rem 1rem; color: var(--body); }
|
|
7598
7867
|
a { color: var(--link); text-decoration-thickness: .08em; text-underline-offset: .16em; overflow-wrap: anywhere; }
|
|
7599
7868
|
.table-wrap { max-width: 100%; overflow-x: auto; margin: 1rem 0 1.25rem; border: 1px solid var(--border-soft); border-radius: 10px; background: var(--panel-strong); }
|
|
7600
7869
|
table { width: 100%; max-width: 100%; border-collapse: collapse; table-layout: fixed; }
|
|
@@ -7606,7 +7875,34 @@ function prdWorkflowReviewHtml(title, markdown, meta = {}) {
|
|
|
7606
7875
|
.frontmatter table { min-width: 0; margin-top: .7rem; }
|
|
7607
7876
|
.frontmatter th { width: min(34%, 12rem); background: var(--panel-soft); color: var(--body); }
|
|
7608
7877
|
.frontmatter-list { margin: 0; padding-left: 1.1rem; }
|
|
7609
|
-
|
|
7878
|
+
.action-index { position: sticky; top: 10px; z-index: 4; display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin: 1rem 0 1.25rem; border: 1px solid var(--border); border-radius: 12px; background: var(--panel-strong); box-shadow: 0 8px 22px var(--shadow); padding: 10px 12px; }
|
|
7879
|
+
.action-index__label { margin-right: 2px; color: var(--muted); font-size: 12px; font-weight: 800; }
|
|
7880
|
+
.action-index a { min-width: 38px; border: 1px solid color-mix(in srgb, var(--interactive) 38%, var(--border)); border-radius: 999px; background: color-mix(in srgb, var(--interactive) 10%, var(--button)); color: var(--interactive); padding: 5px 10px; text-align: center; text-decoration: none; font-size: 12px; font-weight: 900; }
|
|
7881
|
+
.action-index a:hover { border-color: var(--link); background: color-mix(in srgb, var(--interactive) 18%, var(--button)); color: var(--link); }
|
|
7882
|
+
.action-card { scroll-margin-top: 78px; margin: 0 0 20px; overflow: hidden; border: 1px solid var(--border); border-radius: 12px; background: var(--action-bg); box-shadow: 0 8px 24px var(--shadow); }
|
|
7883
|
+
.action-card:target { border-color: var(--interactive); box-shadow: 0 0 0 2px color-mix(in srgb, var(--interactive) 18%, transparent), 0 8px 24px var(--shadow); }
|
|
7884
|
+
.action-card__header { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: start; gap: 12px; border-bottom: 1px solid var(--border-soft); background: var(--action-header); padding: 16px 18px; }
|
|
7885
|
+
.action-card__index { display: inline-grid; place-items: center; min-width: 42px; min-height: 30px; border: 1px solid color-mix(in srgb, var(--interactive) 34%, var(--border)); border-radius: 8px; background: color-mix(in srgb, var(--interactive) 10%, var(--panel-soft)); color: var(--interactive); font: 900 13px/1 "SFMono-Regular", Consolas, monospace; }
|
|
7886
|
+
.action-card__title { margin: 3px 0 0; font-size: 16px; line-height: 1.55; letter-spacing: 0; color: var(--heading); }
|
|
7887
|
+
.action-card__status { margin-top: 2px; border: 1px solid var(--pending-border); border-radius: 999px; background: var(--pending-bg); color: var(--pending-text); padding: 5px 9px; font-size: 11px; font-weight: 900; white-space: nowrap; }
|
|
7888
|
+
.action-card__status.is-complete { border-color: var(--complete-border); background: var(--complete-bg); color: var(--complete-text); }
|
|
7889
|
+
.action-card__body { padding: 15px 20px 20px; }
|
|
7890
|
+
.action-card__body > *:first-child { margin-top: 0; }
|
|
7891
|
+
.action-card__body > *:last-child { margin-bottom: 0; }
|
|
7892
|
+
.action-card__body > ul { margin: 0 0 1rem; padding-left: 1.3rem; }
|
|
7893
|
+
.action-card__body > ul > li { margin: .55rem 0; padding-left: .15rem; }
|
|
7894
|
+
.action-card__body pre { margin: .9rem 0 1.1rem; }
|
|
7895
|
+
@media (max-width: 720px) {
|
|
7896
|
+
main { padding: 28px 14px 48px; }
|
|
7897
|
+
header { display: block; }
|
|
7898
|
+
.toolbar { justify-content: flex-start; margin-top: 14px; }
|
|
7899
|
+
article { padding: 18px; }
|
|
7900
|
+
th, td { padding: .58rem .65rem; }
|
|
7901
|
+
.action-index { top: 6px; }
|
|
7902
|
+
.action-card__header { grid-template-columns: auto minmax(0, 1fr); padding: 14px; }
|
|
7903
|
+
.action-card__status { grid-column: 2; justify-self: start; }
|
|
7904
|
+
.action-card__body { padding: 14px 16px 18px; }
|
|
7905
|
+
}
|
|
7610
7906
|
</style>
|
|
7611
7907
|
</head>
|
|
7612
7908
|
<body>
|
|
@@ -9045,7 +9341,45 @@ function workspaceIntervalMinutesToCron(intervalMinutes) {
|
|
|
9045
9341
|
}
|
|
9046
9342
|
|
|
9047
9343
|
function workspaceRunKey(userCtx, flowSource, flowId) {
|
|
9048
|
-
|
|
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;
|
|
9049
9383
|
}
|
|
9050
9384
|
|
|
9051
9385
|
function workspaceRunEntryKey(scopeKey, runId) {
|
|
@@ -9169,9 +9503,19 @@ function workspaceScheduleKey(userId, flowSource, flowId, scheduleNodeId) {
|
|
|
9169
9503
|
].join(":");
|
|
9170
9504
|
}
|
|
9171
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
|
+
|
|
9172
9516
|
function listWorkspaceScheduleStatusesForFlow(userCtx = {}, flowSource = "user", flowId = "") {
|
|
9173
9517
|
const registry = readWorkspaceScheduleRegistry();
|
|
9174
|
-
const userId =
|
|
9518
|
+
const userId = workspaceScheduleOwnerUserId(userCtx, flowSource, flowId);
|
|
9175
9519
|
return Object.values(registry.schedules || {})
|
|
9176
9520
|
.filter((entry) => (
|
|
9177
9521
|
String(entry?.userId || "") === userId &&
|
|
@@ -9183,13 +9527,13 @@ function listWorkspaceScheduleStatusesForFlow(userCtx = {}, flowSource = "user",
|
|
|
9183
9527
|
|
|
9184
9528
|
function listWorkspaceScheduleStatuses(root, userCtx = {}) {
|
|
9185
9529
|
const registry = readWorkspaceScheduleRegistry();
|
|
9186
|
-
const userId = String(userCtx.userId || "");
|
|
9187
9530
|
const flows = listFlowsJson(root, { ...userCtx, includeWorkspaceFlows: true })
|
|
9188
9531
|
.filter((flow) => !flow.archived && !isReadonlyBuiltinFlowSource(flow.source || "user"));
|
|
9189
9532
|
const rows = [];
|
|
9190
9533
|
for (const flow of flows) {
|
|
9191
9534
|
const flowId = String(flow.id || "");
|
|
9192
9535
|
const flowSource = String(flow.source || "user");
|
|
9536
|
+
const scheduleUserId = workspaceScheduleOwnerUserId(userCtx, flowSource, flowId);
|
|
9193
9537
|
const scoped = resolveWorkspaceScopeRoot(root, { flowId, flowSource }, userCtx);
|
|
9194
9538
|
if (scoped.error || !scoped.root) continue;
|
|
9195
9539
|
let graph;
|
|
@@ -9202,7 +9546,7 @@ function listWorkspaceScheduleStatuses(root, userCtx = {}) {
|
|
|
9202
9546
|
for (const [scheduleNodeId, instance] of Object.entries(instances)) {
|
|
9203
9547
|
if (String(instance?.definitionId || "") !== "workspace_scheduled_run") continue;
|
|
9204
9548
|
const config = normalizeWorkspaceScheduledRunConfig(instance.body || "");
|
|
9205
|
-
const key = workspaceScheduleKey(
|
|
9549
|
+
const key = workspaceScheduleKey(scheduleUserId, flowSource, flowId, scheduleNodeId);
|
|
9206
9550
|
const current = registry.schedules?.[key] && typeof registry.schedules[key] === "object" ? registry.schedules[key] : {};
|
|
9207
9551
|
const targetRunNodeId = workspaceScheduleInferTargetRunNodeId(graph, scheduleNodeId, config);
|
|
9208
9552
|
const scopeKey = workspaceRunKey(userCtx, flowSource, flowId);
|
|
@@ -9291,15 +9635,25 @@ function setWorkspaceScheduleEnabled(root, payload = {}, authUser = {}, userCtx
|
|
|
9291
9635
|
function syncWorkspaceSchedulesForGraph(root, scoped, graph, authUser, userCtx = {}) {
|
|
9292
9636
|
const flowId = String(scoped?.flowId || "").trim();
|
|
9293
9637
|
const flowSource = String(scoped?.flowSource || "user");
|
|
9294
|
-
const userId =
|
|
9638
|
+
const userId = workspaceScheduleOwnerUserId(
|
|
9639
|
+
{ userId: userCtx.userId || authUser?.userId || "" },
|
|
9640
|
+
flowSource,
|
|
9641
|
+
flowId,
|
|
9642
|
+
);
|
|
9295
9643
|
if (!flowId || !userId) return [];
|
|
9296
9644
|
const now = Date.now();
|
|
9297
9645
|
const nowIso = new Date(now).toISOString();
|
|
9298
9646
|
const registry = readWorkspaceScheduleRegistry();
|
|
9299
9647
|
const schedules = { ...(registry.schedules || {}) };
|
|
9300
9648
|
const prefix = `${userId}:${flowSource}:${flowId}:`;
|
|
9301
|
-
for (const key of Object.
|
|
9302
|
-
|
|
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];
|
|
9303
9657
|
}
|
|
9304
9658
|
const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
|
|
9305
9659
|
for (const [scheduleNodeId, instance] of Object.entries(instances)) {
|
|
@@ -10971,7 +11325,49 @@ export function startUiServer({
|
|
|
10971
11325
|
if (url.pathname === "/api/flows") {
|
|
10972
11326
|
if (req.method === "GET") {
|
|
10973
11327
|
try {
|
|
10974
|
-
|
|
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);
|
|
10975
11371
|
} catch (e) {
|
|
10976
11372
|
json(res, 500, { error: (e && e.message) || String(e) });
|
|
10977
11373
|
}
|
|
@@ -11023,6 +11419,9 @@ export function startUiServer({
|
|
|
11023
11419
|
json(res, 400, result);
|
|
11024
11420
|
return;
|
|
11025
11421
|
}
|
|
11422
|
+
if (targetSpace === "workspace") {
|
|
11423
|
+
ensureWorkspaceCollaboration({ flowId, userId: userCtx.userId });
|
|
11424
|
+
}
|
|
11026
11425
|
json(res, 200, { success: true, flowId, flowSource: targetSpace });
|
|
11027
11426
|
return;
|
|
11028
11427
|
}
|
|
@@ -11103,6 +11502,9 @@ export function startUiServer({
|
|
|
11103
11502
|
json(res, 400, { error: w.error });
|
|
11104
11503
|
return;
|
|
11105
11504
|
}
|
|
11505
|
+
if (targetSpace === "workspace") {
|
|
11506
|
+
ensureWorkspaceCollaboration({ flowId, userId: userCtx.userId });
|
|
11507
|
+
}
|
|
11106
11508
|
json(res, 200, { success: true, flowId, flowSource: targetSpace });
|
|
11107
11509
|
return;
|
|
11108
11510
|
}
|
|
@@ -11207,11 +11609,188 @@ export function startUiServer({
|
|
|
11207
11609
|
return;
|
|
11208
11610
|
}
|
|
11209
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
|
+
|
|
11210
11788
|
if (req.method === "GET" && url.pathname === "/api/workspace/files") {
|
|
11211
11789
|
try {
|
|
11212
11790
|
const scoped = resolveWorkspaceScopeRoot(root, {
|
|
11213
11791
|
flowId: url.searchParams.get("flowId") || "",
|
|
11214
11792
|
flowSource: url.searchParams.get("flowSource") || "user",
|
|
11793
|
+
workspaceId: url.searchParams.get("workspaceId") || "",
|
|
11215
11794
|
archived: url.searchParams.get("archived") === "1",
|
|
11216
11795
|
}, userCtx);
|
|
11217
11796
|
if (scoped.error) {
|
|
@@ -11230,6 +11809,7 @@ export function startUiServer({
|
|
|
11230
11809
|
const scoped = resolveWorkspaceScopeRoot(root, {
|
|
11231
11810
|
flowId: url.searchParams.get("flowId") || "",
|
|
11232
11811
|
flowSource: url.searchParams.get("flowSource") || "user",
|
|
11812
|
+
workspaceId: url.searchParams.get("workspaceId") || "",
|
|
11233
11813
|
archived: url.searchParams.get("archived") === "1",
|
|
11234
11814
|
}, userCtx);
|
|
11235
11815
|
const scopedRoot = scoped.error ? root : scoped.root;
|
|
@@ -11287,23 +11867,30 @@ export function startUiServer({
|
|
|
11287
11867
|
const scoped = resolveWorkspaceScopeRoot(root, {
|
|
11288
11868
|
flowId: url.searchParams.get("flowId") || "",
|
|
11289
11869
|
flowSource: url.searchParams.get("flowSource") || "user",
|
|
11870
|
+
workspaceId: url.searchParams.get("workspaceId") || "",
|
|
11290
11871
|
archived: url.searchParams.get("archived") === "1",
|
|
11291
11872
|
}, userCtx);
|
|
11292
11873
|
if (scoped.error) {
|
|
11293
|
-
json(res, 400, { error: scoped.error });
|
|
11874
|
+
json(res, scoped.status || 400, { error: scoped.error });
|
|
11294
11875
|
return;
|
|
11295
11876
|
}
|
|
11296
11877
|
const { path: graphPath, graph } = readWorkspaceGraph(scoped.root);
|
|
11297
11878
|
const hydratedGraph = hydrateWorkspaceGraphForRuntime(root, scoped, graph, userCtx);
|
|
11879
|
+
const collaborationAccess = scoped.collaborationAccess || workspaceCollaborationAccess(null, userCtx.userId);
|
|
11298
11880
|
json(res, 200, {
|
|
11299
11881
|
ok: true,
|
|
11300
11882
|
graph: hydratedGraph,
|
|
11883
|
+
revision: workspaceDesignRevision(hydratedGraph),
|
|
11884
|
+
designRevision: workspaceDesignRevision(hydratedGraph),
|
|
11885
|
+
runtimeRevision: workspaceRuntimeRevision(hydratedGraph),
|
|
11301
11886
|
path: graphPath,
|
|
11302
11887
|
root: scoped.root,
|
|
11303
11888
|
flowId: scoped.flowId,
|
|
11304
11889
|
flowSource: scoped.flowSource,
|
|
11305
11890
|
archived: scoped.archived,
|
|
11306
|
-
writable: !(scoped.archived || isReadonlyBuiltinFlowSource(scoped.flowSource))
|
|
11891
|
+
writable: !(scoped.archived || isReadonlyBuiltinFlowSource(scoped.flowSource))
|
|
11892
|
+
&& collaborationAccess.writable !== false,
|
|
11893
|
+
collaboration: workspaceCollaborationSummaryWithUsers(scoped.collaboration, userCtx.userId),
|
|
11307
11894
|
workspaceSchedules: listWorkspaceScheduleStatusesForFlow(userCtx, scoped.flowSource || "user", scoped.flowId || ""),
|
|
11308
11895
|
});
|
|
11309
11896
|
} catch (e) {
|
|
@@ -11324,6 +11911,7 @@ export function startUiServer({
|
|
|
11324
11911
|
const scoped = resolveWorkspaceScopeRoot(root, {
|
|
11325
11912
|
flowId: payload.flowId || "",
|
|
11326
11913
|
flowSource: payload.flowSource || "user",
|
|
11914
|
+
workspaceId: payload.workspaceId || "",
|
|
11327
11915
|
archived: payload.archived === true || payload.flowArchived === true,
|
|
11328
11916
|
}, userCtx);
|
|
11329
11917
|
if (scoped.error) {
|
|
@@ -11368,23 +11956,106 @@ export function startUiServer({
|
|
|
11368
11956
|
const scoped = resolveWorkspaceScopeRoot(root, {
|
|
11369
11957
|
flowId: payload.flowId || "",
|
|
11370
11958
|
flowSource: payload.flowSource || "user",
|
|
11959
|
+
workspaceId: payload.workspaceId || "",
|
|
11371
11960
|
archived: payload.archived === true || payload.flowArchived === true,
|
|
11372
11961
|
}, userCtx);
|
|
11373
11962
|
if (scoped.error) {
|
|
11374
|
-
json(res, 400, { error: scoped.error });
|
|
11963
|
+
json(res, scoped.status || 400, { error: scoped.error });
|
|
11375
11964
|
return;
|
|
11376
11965
|
}
|
|
11377
|
-
if (
|
|
11966
|
+
if (
|
|
11967
|
+
scoped.archived
|
|
11968
|
+
|| isReadonlyBuiltinFlowSource(scoped.flowSource)
|
|
11969
|
+
|| scoped.collaborationAccess?.writable === false
|
|
11970
|
+
) {
|
|
11378
11971
|
json(res, 400, { error: "Cannot write workspace graph for builtin or archived pipeline" });
|
|
11379
11972
|
return;
|
|
11380
11973
|
}
|
|
11381
11974
|
const submittedGraph = hydrateWorkspaceGraphForRuntime(root, scoped, payload.graph || payload, userCtx);
|
|
11382
11975
|
const graphPath = workspaceGraphPath(scoped.root);
|
|
11383
|
-
const
|
|
11384
|
-
const
|
|
11385
|
-
|
|
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);
|
|
11386
12036
|
const workspaceSchedules = syncWorkspaceSchedulesForGraph(root, scoped, graph, authUser, userCtx);
|
|
11387
|
-
|
|
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
|
+
});
|
|
11388
12059
|
} catch (e) {
|
|
11389
12060
|
json(res, 500, { error: (e && e.message) || String(e) });
|
|
11390
12061
|
}
|
|
@@ -11420,6 +12091,7 @@ export function startUiServer({
|
|
|
11420
12091
|
const scoped = resolveWorkspaceScopeRoot(root, {
|
|
11421
12092
|
flowId: payload.flowId || "",
|
|
11422
12093
|
flowSource: payload.flowSource || "user",
|
|
12094
|
+
workspaceId: payload.workspaceId || "",
|
|
11423
12095
|
archived: payload.archived === true || payload.flowArchived === true,
|
|
11424
12096
|
}, userCtx);
|
|
11425
12097
|
if (scoped.error) {
|
|
@@ -11467,13 +12139,18 @@ export function startUiServer({
|
|
|
11467
12139
|
const scoped = resolveWorkspaceScopeRoot(root, {
|
|
11468
12140
|
flowId: payload.flowId || "",
|
|
11469
12141
|
flowSource: payload.flowSource || "user",
|
|
12142
|
+
workspaceId: payload.workspaceId || "",
|
|
11470
12143
|
archived: payload.archived === true || payload.flowArchived === true,
|
|
11471
12144
|
}, userCtx);
|
|
11472
12145
|
if (scoped.error) {
|
|
11473
12146
|
json(res, 400, { error: scoped.error });
|
|
11474
12147
|
return;
|
|
11475
12148
|
}
|
|
11476
|
-
if (
|
|
12149
|
+
if (
|
|
12150
|
+
scoped.archived
|
|
12151
|
+
|| isReadonlyBuiltinFlowSource(scoped.flowSource)
|
|
12152
|
+
|| scoped.collaborationAccess?.writable === false
|
|
12153
|
+
) {
|
|
11477
12154
|
json(res, 400, { error: "Cannot optimize workspace graph for builtin or archived pipeline" });
|
|
11478
12155
|
return;
|
|
11479
12156
|
}
|
|
@@ -11489,12 +12166,20 @@ export function startUiServer({
|
|
|
11489
12166
|
const currentGraph = readWorkspaceGraph(scoped.root).graph;
|
|
11490
12167
|
const touchedIds = new Set((result.optimized || []).map((item) => item.nodeId).filter(Boolean));
|
|
11491
12168
|
const mergedGraph = mergeWorkspaceRunGraph(currentGraph, result.graph, touchedIds);
|
|
11492
|
-
|
|
12169
|
+
writeWorkspaceGraphAtomic(graphPath, mergedGraph);
|
|
12170
|
+
const revision = workspaceDesignRevision(mergedGraph);
|
|
11493
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
|
+
});
|
|
11494
12178
|
json(res, 200, {
|
|
11495
12179
|
ok: true,
|
|
11496
12180
|
path: graphPath,
|
|
11497
12181
|
graph: mergedGraph,
|
|
12182
|
+
revision,
|
|
11498
12183
|
order: result.order,
|
|
11499
12184
|
optimized: result.optimized,
|
|
11500
12185
|
skipped: result.skipped,
|
|
@@ -11518,13 +12203,18 @@ export function startUiServer({
|
|
|
11518
12203
|
const scoped = resolveWorkspaceScopeRoot(root, {
|
|
11519
12204
|
flowId: payload.flowId || "",
|
|
11520
12205
|
flowSource: payload.flowSource || "user",
|
|
12206
|
+
workspaceId: payload.workspaceId || "",
|
|
11521
12207
|
archived: payload.archived === true || payload.flowArchived === true,
|
|
11522
12208
|
}, userCtx);
|
|
11523
12209
|
if (scoped.error) {
|
|
11524
12210
|
json(res, 400, { error: scoped.error });
|
|
11525
12211
|
return;
|
|
11526
12212
|
}
|
|
11527
|
-
if (
|
|
12213
|
+
if (
|
|
12214
|
+
scoped.archived
|
|
12215
|
+
|| isReadonlyBuiltinFlowSource(scoped.flowSource)
|
|
12216
|
+
|| scoped.collaborationAccess?.runnable === false
|
|
12217
|
+
) {
|
|
11528
12218
|
json(res, 400, { error: "Cannot run workspace graph for builtin or archived pipeline" });
|
|
11529
12219
|
return;
|
|
11530
12220
|
}
|
|
@@ -11534,7 +12224,27 @@ export function startUiServer({
|
|
|
11534
12224
|
json(res, 400, { error: "Missing flowId" });
|
|
11535
12225
|
return;
|
|
11536
12226
|
}
|
|
11537
|
-
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);
|
|
11538
12248
|
const runNodeId = String(payload.runNodeId || "").trim();
|
|
11539
12249
|
const plan = workspaceRunPlan(runtimeGraph, runNodeId, scoped.root);
|
|
11540
12250
|
const plannedNodeIds = workspaceRunPlanNodeIds(runNodeId, plan);
|
|
@@ -11583,12 +12293,27 @@ export function startUiServer({
|
|
|
11583
12293
|
});
|
|
11584
12294
|
activeWorkspaceRuns.set(runKey, runEntry);
|
|
11585
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
|
+
});
|
|
11586
12304
|
const setActiveChild = (child, childOptions = {}) => {
|
|
11587
12305
|
runControl.setChild(child, childOptions);
|
|
11588
12306
|
};
|
|
11589
12307
|
const clearActiveRun = (status = "finished") => {
|
|
11590
12308
|
runControl.finish(status);
|
|
11591
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
|
+
});
|
|
11592
12317
|
};
|
|
11593
12318
|
if (wantsStream) {
|
|
11594
12319
|
const graphPath = workspaceGraphPath(scoped.root);
|
|
@@ -11611,7 +12336,12 @@ export function startUiServer({
|
|
|
11611
12336
|
const currentGraph = readWorkspaceGraph(scoped.root).graph;
|
|
11612
12337
|
const touchedIds = workspaceRunTouchedNodeIds(result);
|
|
11613
12338
|
const mergedGraph = mergeWorkspaceRunGraph(currentGraph, result.graph, touchedIds);
|
|
11614
|
-
|
|
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";
|
|
11615
12345
|
const endedAt = Date.now();
|
|
11616
12346
|
appendWorkspaceRunFinished({
|
|
11617
12347
|
...runEntry,
|
|
@@ -11623,7 +12353,14 @@ export function startUiServer({
|
|
|
11623
12353
|
durationMs: endedAt - runEntry.startedAt,
|
|
11624
12354
|
runNodeId,
|
|
11625
12355
|
});
|
|
11626
|
-
|
|
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 || [] });
|
|
11627
12364
|
res.end();
|
|
11628
12365
|
} catch (e) {
|
|
11629
12366
|
const endedAt = Date.now();
|
|
@@ -11670,7 +12407,12 @@ export function startUiServer({
|
|
|
11670
12407
|
const currentGraph = readWorkspaceGraph(scoped.root).graph;
|
|
11671
12408
|
const touchedIds = workspaceRunTouchedNodeIds(result);
|
|
11672
12409
|
const mergedGraph = mergeWorkspaceRunGraph(currentGraph, result.graph, touchedIds);
|
|
11673
|
-
|
|
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";
|
|
11674
12416
|
const endedAt = Date.now();
|
|
11675
12417
|
appendWorkspaceRunFinished({
|
|
11676
12418
|
...runEntry,
|
|
@@ -11682,7 +12424,14 @@ export function startUiServer({
|
|
|
11682
12424
|
durationMs: endedAt - runEntry.startedAt,
|
|
11683
12425
|
runNodeId,
|
|
11684
12426
|
});
|
|
11685
|
-
|
|
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) });
|
|
11686
12435
|
} catch (e) {
|
|
11687
12436
|
const endedAt = Date.now();
|
|
11688
12437
|
if (isWorkspaceRunAbortError(e) || controller.signal.aborted) {
|
|
@@ -11729,9 +12478,18 @@ export function startUiServer({
|
|
|
11729
12478
|
const scheduleNodeId = url.searchParams.get("scheduleNodeId") || "";
|
|
11730
12479
|
const runNodeId = url.searchParams.get("runNodeId") || "";
|
|
11731
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
|
+
}
|
|
11732
12490
|
json(res, 200, {
|
|
11733
12491
|
runs: listWorkspaceRunLogs({
|
|
11734
|
-
userId: userCtx.userId || "",
|
|
12492
|
+
userId: flowSource === "workspace" ? "" : userCtx.userId || "",
|
|
11735
12493
|
flowId,
|
|
11736
12494
|
flowSource,
|
|
11737
12495
|
scheduleNodeId,
|
|
@@ -11752,7 +12510,23 @@ export function startUiServer({
|
|
|
11752
12510
|
json(res, 400, { error: "Missing runId" });
|
|
11753
12511
|
return;
|
|
11754
12512
|
}
|
|
11755
|
-
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
|
+
})
|
|
11756
12530
|
.find((item) => String(item.runId || "") === runId);
|
|
11757
12531
|
if (!run) {
|
|
11758
12532
|
json(res, 404, { error: "Run log not found" });
|
|
@@ -11775,6 +12549,15 @@ export function startUiServer({
|
|
|
11775
12549
|
return;
|
|
11776
12550
|
}
|
|
11777
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
|
+
}
|
|
11778
12561
|
const scopeKey = workspaceRunKey(userCtx, flowSource, flowId);
|
|
11779
12562
|
const entries = workspaceActiveRunsForScope(scopeKey).map(([, entry]) => entry);
|
|
11780
12563
|
const entry = entries[0] || null;
|
|
@@ -11812,7 +12595,21 @@ export function startUiServer({
|
|
|
11812
12595
|
json(res, 400, { error: "Missing flowId" });
|
|
11813
12596
|
return;
|
|
11814
12597
|
}
|
|
11815
|
-
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);
|
|
11816
12613
|
const runId = String(payload.runId || payload.runSessionId || "").trim();
|
|
11817
12614
|
const runNodeId = String(payload.runNodeId || "").trim();
|
|
11818
12615
|
const entries = workspaceActiveRunsForScope(scopeKey);
|
|
@@ -11829,6 +12626,12 @@ export function startUiServer({
|
|
|
11829
12626
|
runNodeId: entry.runNodeId || "",
|
|
11830
12627
|
ts: Date.now(),
|
|
11831
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
|
+
});
|
|
11832
12635
|
const result = await entry.runControl.stop();
|
|
11833
12636
|
if (!result.stopped) {
|
|
11834
12637
|
appendWorkspaceRunLogEvent(entry.runId, {
|
|
@@ -11880,7 +12683,13 @@ export function startUiServer({
|
|
|
11880
12683
|
json(res, 413, { error: "File too large" });
|
|
11881
12684
|
return;
|
|
11882
12685
|
}
|
|
11883
|
-
|
|
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
|
+
});
|
|
11884
12693
|
} catch (e) {
|
|
11885
12694
|
json(res, /traversal/i.test(String(e.message || e)) ? 403 : 500, { error: (e && e.message) || String(e) });
|
|
11886
12695
|
}
|
|
@@ -12001,7 +12810,11 @@ export function startUiServer({
|
|
|
12001
12810
|
json(res, 400, { error: scoped.error });
|
|
12002
12811
|
return;
|
|
12003
12812
|
}
|
|
12004
|
-
if (
|
|
12813
|
+
if (
|
|
12814
|
+
scoped.archived
|
|
12815
|
+
|| isReadonlyBuiltinFlowSource(scoped.flowSource)
|
|
12816
|
+
|| scoped.collaborationAccess?.writable === false
|
|
12817
|
+
) {
|
|
12005
12818
|
json(res, 400, { error: "Cannot write to builtin or archived pipeline workspace" });
|
|
12006
12819
|
return;
|
|
12007
12820
|
}
|
|
@@ -12010,9 +12823,39 @@ export function startUiServer({
|
|
|
12010
12823
|
json(res, 400, { error: "Missing path" });
|
|
12011
12824
|
return;
|
|
12012
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
|
+
}
|
|
12013
12840
|
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
12014
|
-
|
|
12015
|
-
|
|
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 });
|
|
12016
12859
|
} catch (e) {
|
|
12017
12860
|
json(res, /traversal/i.test(String(e.message || e)) ? 403 : 500, { error: (e && e.message) || String(e) });
|
|
12018
12861
|
}
|
|
@@ -12041,7 +12884,7 @@ export function startUiServer({
|
|
|
12041
12884
|
json(res, 400, { error: scoped.error });
|
|
12042
12885
|
return;
|
|
12043
12886
|
}
|
|
12044
|
-
if (scoped.archived || isReadonlyBuiltinFlowSource(scoped.flowSource)) {
|
|
12887
|
+
if (scoped.archived || isReadonlyBuiltinFlowSource(scoped.flowSource) || scoped.collaborationAccess?.writable === false) {
|
|
12045
12888
|
json(res, 400, { error: "Cannot write to builtin or archived pipeline workspace" });
|
|
12046
12889
|
return;
|
|
12047
12890
|
}
|
|
@@ -12051,6 +12894,11 @@ export function startUiServer({
|
|
|
12051
12894
|
const target = uniqueWorkspaceRelPath(scoped.root, targetRel);
|
|
12052
12895
|
fs.mkdirSync(path.dirname(target.abs), { recursive: true });
|
|
12053
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
|
+
});
|
|
12054
12902
|
json(res, 200, {
|
|
12055
12903
|
ok: true,
|
|
12056
12904
|
path: target.rel,
|
|
@@ -12081,7 +12929,7 @@ export function startUiServer({
|
|
|
12081
12929
|
json(res, 400, { error: scoped.error });
|
|
12082
12930
|
return;
|
|
12083
12931
|
}
|
|
12084
|
-
if (scoped.archived || isReadonlyBuiltinFlowSource(scoped.flowSource)) {
|
|
12932
|
+
if (scoped.archived || isReadonlyBuiltinFlowSource(scoped.flowSource) || scoped.collaborationAccess?.writable === false) {
|
|
12085
12933
|
json(res, 400, { error: "Cannot write to builtin or archived pipeline workspace" });
|
|
12086
12934
|
return;
|
|
12087
12935
|
}
|
|
@@ -12091,6 +12939,11 @@ export function startUiServer({
|
|
|
12091
12939
|
return;
|
|
12092
12940
|
}
|
|
12093
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
|
+
});
|
|
12094
12947
|
json(res, 200, { ok: true, path: rel });
|
|
12095
12948
|
} catch (e) {
|
|
12096
12949
|
json(res, /traversal/i.test(String(e.message || e)) ? 403 : 500, { error: (e && e.message) || String(e) });
|
|
@@ -12116,7 +12969,7 @@ export function startUiServer({
|
|
|
12116
12969
|
json(res, 400, { error: scoped.error });
|
|
12117
12970
|
return;
|
|
12118
12971
|
}
|
|
12119
|
-
if (scoped.archived || isReadonlyBuiltinFlowSource(scoped.flowSource)) {
|
|
12972
|
+
if (scoped.archived || isReadonlyBuiltinFlowSource(scoped.flowSource) || scoped.collaborationAccess?.writable === false) {
|
|
12120
12973
|
json(res, 400, { error: "Cannot write to builtin or archived pipeline workspace" });
|
|
12121
12974
|
return;
|
|
12122
12975
|
}
|
|
@@ -12130,6 +12983,12 @@ export function startUiServer({
|
|
|
12130
12983
|
return;
|
|
12131
12984
|
}
|
|
12132
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
|
+
});
|
|
12133
12992
|
json(res, 200, { ok: true, path: rel });
|
|
12134
12993
|
} catch (e) {
|
|
12135
12994
|
json(res, /traversal/i.test(String(e.message || e)) ? 403 : 500, { error: (e && e.message) || String(e) });
|
|
@@ -12166,7 +13025,7 @@ export function startUiServer({
|
|
|
12166
13025
|
json(res, 200, { ok: true, conversations: readWorkspaceConversations(scoped.root) });
|
|
12167
13026
|
return;
|
|
12168
13027
|
}
|
|
12169
|
-
if (scoped.archived || isReadonlyBuiltinFlowSource(scoped.flowSource)) {
|
|
13028
|
+
if (scoped.archived || isReadonlyBuiltinFlowSource(scoped.flowSource) || scoped.collaborationAccess?.writable === false) {
|
|
12170
13029
|
json(res, 400, { error: "Cannot write conversations for builtin or archived pipeline workspace" });
|
|
12171
13030
|
return;
|
|
12172
13031
|
}
|
|
@@ -12201,6 +13060,10 @@ export function startUiServer({
|
|
|
12201
13060
|
json(res, 400, { error: scoped.error });
|
|
12202
13061
|
return;
|
|
12203
13062
|
}
|
|
13063
|
+
if (scoped.collaborationAccess?.writable === false) {
|
|
13064
|
+
json(res, 403, { error: "Workspace collaboration edit permission denied" });
|
|
13065
|
+
return;
|
|
13066
|
+
}
|
|
12204
13067
|
const selectedSkillKeys = Array.isArray(payload?.selectedSkills)
|
|
12205
13068
|
? payload.selectedSkills.map((x) => String(x || "").trim()).filter(Boolean)
|
|
12206
13069
|
: [];
|
|
@@ -12287,10 +13150,14 @@ export function startUiServer({
|
|
|
12287
13150
|
json(res, 400, { error: scoped.error });
|
|
12288
13151
|
return;
|
|
12289
13152
|
}
|
|
13153
|
+
if (scoped.collaborationAccess?.writable === false) {
|
|
13154
|
+
json(res, 403, { error: "Workspace collaboration edit permission denied" });
|
|
13155
|
+
return;
|
|
13156
|
+
}
|
|
12290
13157
|
const targetFilePath = String(payload?.targetFilePath || "").trim();
|
|
12291
13158
|
let targetFile = null;
|
|
12292
13159
|
if (targetFilePath) {
|
|
12293
|
-
if (scoped.archived || isReadonlyBuiltinFlowSource(scoped.flowSource)) {
|
|
13160
|
+
if (scoped.archived || isReadonlyBuiltinFlowSource(scoped.flowSource) || scoped.collaborationAccess?.writable === false) {
|
|
12294
13161
|
json(res, 400, { error: "Cannot edit builtin or archived pipeline workspace" });
|
|
12295
13162
|
return;
|
|
12296
13163
|
}
|
|
@@ -13151,12 +14018,26 @@ export function startUiServer({
|
|
|
13151
14018
|
return;
|
|
13152
14019
|
}
|
|
13153
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
|
+
}
|
|
13154
14032
|
const result = readFlowJson(root, flowId, flowSource, { archived: flowArchived, ...userCtx });
|
|
13155
14033
|
if (result.error) {
|
|
13156
14034
|
json(res, 404, result);
|
|
13157
14035
|
return;
|
|
13158
14036
|
}
|
|
13159
|
-
json(res, 200,
|
|
14037
|
+
json(res, 200, {
|
|
14038
|
+
...result,
|
|
14039
|
+
revision: crypto.createHash("sha256").update(String(result.flowYaml || "")).digest("hex").slice(0, 24),
|
|
14040
|
+
});
|
|
13160
14041
|
return;
|
|
13161
14042
|
}
|
|
13162
14043
|
|
|
@@ -13335,12 +14216,53 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
13335
14216
|
return;
|
|
13336
14217
|
}
|
|
13337
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
|
+
}
|
|
13338
14255
|
const result = writeFlowYaml(root, flowId, flowSource, flowYaml, { archived: flowArchived, ...userCtx });
|
|
13339
14256
|
if (!result.success) {
|
|
13340
14257
|
json(res, 400, result);
|
|
13341
14258
|
return;
|
|
13342
14259
|
}
|
|
13343
|
-
|
|
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
|
+
});
|
|
13344
14266
|
return;
|
|
13345
14267
|
}
|
|
13346
14268
|
|
|
@@ -13363,6 +14285,17 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
13363
14285
|
return;
|
|
13364
14286
|
}
|
|
13365
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
|
+
}
|
|
13366
14299
|
broadcastFlowEditorSync(flowId, flowSource, flowArchived, userCtx.userId);
|
|
13367
14300
|
json(res, 200, { ok: true });
|
|
13368
14301
|
return;
|
|
@@ -13380,6 +14313,17 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
13380
14313
|
return;
|
|
13381
14314
|
}
|
|
13382
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
|
+
}
|
|
13383
14327
|
const key = flowEditorSyncKey(flowId, flowSource, flowArchived, userCtx.userId);
|
|
13384
14328
|
let set = flowEditorSyncSubscribers.get(key);
|
|
13385
14329
|
if (!set) {
|
|
@@ -13414,6 +14358,17 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
13414
14358
|
return;
|
|
13415
14359
|
}
|
|
13416
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
|
+
}
|
|
13417
14372
|
const key = flowEditorSyncKey(flowId, flowSource, flowArchived, userCtx.userId);
|
|
13418
14373
|
const serverVer = flowEditorSyncVersions.get(key) ?? 0;
|
|
13419
14374
|
const clientVer = parseInt(url.searchParams.get("v") ?? "0", 10) || 0;
|
|
@@ -13444,11 +14399,21 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
13444
14399
|
json(res, 400, { error: "Invalid toSource" });
|
|
13445
14400
|
return;
|
|
13446
14401
|
}
|
|
14402
|
+
const collaborationDenied = workspaceFlowCollaborationGuard(flowId, fromSource, false, userCtx, "owner");
|
|
14403
|
+
if (collaborationDenied) {
|
|
14404
|
+
json(res, collaborationDenied.status, { error: collaborationDenied.error });
|
|
14405
|
+
return;
|
|
14406
|
+
}
|
|
13447
14407
|
const result = moveFlowDirectory(root, flowId.trim(), fromSource, toSource, userCtx);
|
|
13448
14408
|
if (!result.success) {
|
|
13449
14409
|
json(res, 400, { error: result.error || "Move failed" });
|
|
13450
14410
|
return;
|
|
13451
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
|
+
}
|
|
13452
14417
|
json(res, 200, { success: true, flowId: flowId.trim(), flowSource: result.flowSource });
|
|
13453
14418
|
return;
|
|
13454
14419
|
}
|
|
@@ -13472,6 +14437,11 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
13472
14437
|
json(res, 400, { error: "仅支持重命名用户目录或工作区流水线" });
|
|
13473
14438
|
return;
|
|
13474
14439
|
}
|
|
14440
|
+
const collaborationDenied = workspaceFlowCollaborationGuard(flowId, flowSource, false, userCtx, "owner");
|
|
14441
|
+
if (collaborationDenied) {
|
|
14442
|
+
json(res, collaborationDenied.status, { error: collaborationDenied.error });
|
|
14443
|
+
return;
|
|
14444
|
+
}
|
|
13475
14445
|
const validation = validateUserPipelineId(newFlowId);
|
|
13476
14446
|
if (!validation.ok) {
|
|
13477
14447
|
json(res, 400, { error: validation.error });
|
|
@@ -13494,6 +14464,12 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
13494
14464
|
}
|
|
13495
14465
|
try {
|
|
13496
14466
|
fs.renameSync(fromDir, toDir);
|
|
14467
|
+
updateWorkspaceCollaborationFlow({
|
|
14468
|
+
previousFlowId: flowId,
|
|
14469
|
+
flowSource,
|
|
14470
|
+
ownerId: userCtx.userId,
|
|
14471
|
+
flowId: validation.flowId,
|
|
14472
|
+
});
|
|
13497
14473
|
json(res, 200, { success: true, flowId: validation.flowId, flowSource });
|
|
13498
14474
|
} catch (e) {
|
|
13499
14475
|
json(res, 500, { error: (e && e.message) || String(e) });
|
|
@@ -13524,11 +14500,24 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
13524
14500
|
json(res, 400, { error: "仅支持归档用户目录或工作区流水线" });
|
|
13525
14501
|
return;
|
|
13526
14502
|
}
|
|
14503
|
+
const collaborationDenied = workspaceFlowCollaborationGuard(flowId, flowSource, false, userCtx, "owner");
|
|
14504
|
+
if (collaborationDenied) {
|
|
14505
|
+
json(res, collaborationDenied.status, { error: collaborationDenied.error });
|
|
14506
|
+
return;
|
|
14507
|
+
}
|
|
13527
14508
|
const result = archiveFlowPipeline(root, flowId, flowSource, userCtx);
|
|
13528
14509
|
if (!result.success) {
|
|
13529
14510
|
json(res, 400, { error: result.error || "归档失败" });
|
|
13530
14511
|
return;
|
|
13531
14512
|
}
|
|
14513
|
+
updateWorkspaceCollaborationFlow({
|
|
14514
|
+
previousFlowId: flowId,
|
|
14515
|
+
previousArchived: false,
|
|
14516
|
+
flowSource,
|
|
14517
|
+
ownerId: userCtx.userId,
|
|
14518
|
+
flowId,
|
|
14519
|
+
archived: true,
|
|
14520
|
+
});
|
|
13532
14521
|
json(res, 200, { success: true, flowId, flowSource, archived: true });
|
|
13533
14522
|
return;
|
|
13534
14523
|
}
|
|
@@ -13557,11 +14546,53 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
13557
14546
|
json(res, 400, { error: "仅支持删除用户目录或工作区流水线" });
|
|
13558
14547
|
return;
|
|
13559
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
|
+
}
|
|
13560
14589
|
const result = deleteFlowPipeline(root, flowId, flowSource, { archived: flowArchived, ...userCtx });
|
|
13561
14590
|
if (!result.success) {
|
|
13562
14591
|
json(res, 400, { error: result.error || "删除失败" });
|
|
13563
14592
|
return;
|
|
13564
14593
|
}
|
|
14594
|
+
if (collaboration?.id) deleteWorkspaceCollaborationById(collaboration.id);
|
|
14595
|
+
else deleteWorkspaceCollaborationForFlow(flowId, flowArchived);
|
|
13565
14596
|
json(res, 200, { success: true, flowId, flowSource, deleted: true });
|
|
13566
14597
|
return;
|
|
13567
14598
|
}
|
|
@@ -13824,8 +14855,20 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
13824
14855
|
json(res, 400, { error: "Missing flowId" });
|
|
13825
14856
|
return;
|
|
13826
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
|
+
}
|
|
13827
14870
|
const runUuid = typeof payload.uuid === "string" ? payload.uuid.trim() : "";
|
|
13828
|
-
const runKey =
|
|
14871
|
+
const runKey = workspaceRunKey(userCtx, flowSource, flowId);
|
|
13829
14872
|
if (activeFlowRuns.has(runKey)) {
|
|
13830
14873
|
json(res, 409, { error: "该流水线已在运行中" });
|
|
13831
14874
|
return;
|
|
@@ -13904,7 +14947,7 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
13904
14947
|
}
|
|
13905
14948
|
|
|
13906
14949
|
/** @type {{ child: import("child_process").ChildProcess, runUuid: string | null }} */
|
|
13907
|
-
const runEntry = { child, runUuid: runUuid || null };
|
|
14950
|
+
const runEntry = { child, runUuid: runUuid || null, userId: userCtx.userId || "" };
|
|
13908
14951
|
activeFlowRuns.set(runKey, runEntry);
|
|
13909
14952
|
log.debug(`[ui] flow/run: spawned pid=${child.pid} flowId=${flowId}${runUuid ? ` uuid=${runUuid}` : ""}`);
|
|
13910
14953
|
|
|
@@ -13978,7 +15021,19 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
13978
15021
|
json(res, 400, { error: "Missing flowId" });
|
|
13979
15022
|
return;
|
|
13980
15023
|
}
|
|
13981
|
-
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);
|
|
13982
15037
|
const entry = activeFlowRuns.get(runKey);
|
|
13983
15038
|
if (!entry || !entry.child) {
|
|
13984
15039
|
json(res, 404, { error: "该流水线未在运行" });
|
|
@@ -14000,7 +15055,7 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
14000
15055
|
activeFlowRuns.delete(runKey);
|
|
14001
15056
|
if (uuid) {
|
|
14002
15057
|
try {
|
|
14003
|
-
const runDir = getRunDir(root, flowId, uuid, userCtx);
|
|
15058
|
+
const runDir = getRunDir(root, flowId, uuid, { userId: entry.userId || userCtx.userId || "" });
|
|
14004
15059
|
fs.mkdirSync(runDir, { recursive: true });
|
|
14005
15060
|
fs.writeFileSync(
|
|
14006
15061
|
path.join(runDir, RUN_INTERRUPTED_FILENAME),
|