@fieldwangai/agentflow 0.1.29 → 0.1.30
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/agents/agentflow-node-executor-code.md +3 -2
- package/agents/agentflow-node-executor-planning.md +3 -2
- package/agents/agentflow-node-executor-requirement.md +3 -2
- package/agents/agentflow-node-executor-test.md +3 -2
- package/agents/agentflow-node-executor-ui.md +3 -2
- package/agents/agentflow-node-executor.md +3 -2
- package/agents/en/agentflow-node-executor.md +3 -2
- package/agents/zh/agentflow-node-executor.md +3 -2
- package/bin/lib/agent-runners.mjs +38 -13
- package/bin/lib/api-runner.mjs +6 -3
- package/bin/lib/auth.mjs +240 -0
- package/bin/lib/catalog-agents.mjs +2 -2
- package/bin/lib/catalog-flows.mjs +192 -16
- package/bin/lib/composer-agent.mjs +21 -1
- package/bin/lib/composer-skill-router.mjs +10 -78
- package/bin/lib/flow-import.mjs +2 -2
- package/bin/lib/flow-write.mjs +20 -20
- package/bin/lib/help.mjs +2 -2
- package/bin/lib/locales/en.json +25 -1
- package/bin/lib/locales/zh.json +25 -1
- package/bin/lib/main.mjs +6 -1
- package/bin/lib/node-exec-context.mjs +5 -5
- package/bin/lib/node-execute.mjs +14 -9
- package/bin/lib/paths.mjs +64 -13
- package/bin/lib/recent-runs.mjs +2 -2
- package/bin/lib/run-node-statuses-from-disk.mjs +3 -3
- package/bin/lib/runtime-context.mjs +225 -0
- package/bin/lib/scheduler.mjs +41 -38
- package/bin/lib/skill-registry.mjs +145 -0
- package/bin/lib/ui-server.mjs +902 -57
- package/bin/lib/workspace-tree.mjs +4 -3
- package/bin/lib/workspace.mjs +9 -11
- package/bin/pipeline/build-node-prompt.mjs +29 -4
- package/bin/pipeline/get-exec-id.mjs +2 -2
- package/bin/pipeline/get-resolved-values.mjs +1 -0
- package/bin/pipeline/pre-process-node.mjs +306 -6
- package/bin/pipeline/validate-flow.mjs +2 -0
- package/builtin/nodes/agent_subAgent.md +7 -1
- package/builtin/nodes/control_cd_workspace.md +43 -0
- package/builtin/nodes/control_load_skills.md +48 -0
- package/builtin/nodes/display_ascii.md +17 -0
- package/builtin/nodes/display_markdown.md +17 -0
- package/builtin/nodes/display_mermaid.md +17 -0
- package/builtin/nodes/tool_git_checkout.md +54 -0
- package/builtin/nodes/tool_nodejs.md +8 -1
- package/builtin/nodes/tool_print.md +4 -1
- package/builtin/web-ui/dist/assets/index-NdVOJLL9.js +196 -0
- package/builtin/web-ui/dist/assets/index-naVI6LZj.css +1 -0
- package/builtin/web-ui/dist/index.html +2 -2
- package/package.json +1 -1
- package/skills/agentflow-flow-recipes/SKILL.md +24 -0
- package/skills/agentflow-flow-recipes/references/recipes.md +63 -0
- package/skills/agentflow-node-reference/SKILL.md +25 -0
- package/skills/agentflow-node-reference/references/builtin-nodes.md +210 -0
- package/skills/agentflow-placeholder-reference/SKILL.md +24 -0
- package/skills/agentflow-placeholder-reference/references/placeholders.md +20 -0
- package/skills/agentflow-runtime-reference/SKILL.md +25 -0
- package/skills/agentflow-runtime-reference/references/runtime.md +64 -0
- package/builtin/web-ui/dist/assets/index-0vJxkTJz.css +0 -1
- package/builtin/web-ui/dist/assets/index-h69bpxLI.js +0 -190
package/bin/lib/ui-server.mjs
CHANGED
|
@@ -8,10 +8,17 @@
|
|
|
8
8
|
import fs from "fs";
|
|
9
9
|
import http from "http";
|
|
10
10
|
import path from "path";
|
|
11
|
-
import { spawn } from "child_process";
|
|
11
|
+
import { execFile, spawn } from "child_process";
|
|
12
12
|
import busboy from "busboy";
|
|
13
13
|
import { log } from "./log.mjs";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
getFlowYamlAbs,
|
|
16
|
+
listFlowsJson,
|
|
17
|
+
listNodesJson,
|
|
18
|
+
readFlowJson,
|
|
19
|
+
readNodeDetailJson,
|
|
20
|
+
readNodeFilePreview,
|
|
21
|
+
} from "./catalog-flows.mjs";
|
|
15
22
|
import {
|
|
16
23
|
FLOW_YAML_FILENAME,
|
|
17
24
|
archiveFlowPipeline,
|
|
@@ -42,6 +49,7 @@ import {
|
|
|
42
49
|
loadResourcesForIntents,
|
|
43
50
|
loadResourcesForSkillKeys,
|
|
44
51
|
listComposerSkills,
|
|
52
|
+
readComposerSkillDetail,
|
|
45
53
|
buildSkillInjectionBlock,
|
|
46
54
|
buildSkillCompactInjectionBlock,
|
|
47
55
|
} from "./composer-skill-router.mjs";
|
|
@@ -65,6 +73,14 @@ import { runNodeScript } from "./pipeline-scripts.mjs";
|
|
|
65
73
|
import { readFlowSchedule, writeFlowSchedule } from "./schedule-config.mjs";
|
|
66
74
|
import { listScheduleStatuses } from "./scheduler.mjs";
|
|
67
75
|
import { installFlowDependency, listMarketplacePackages, publishNodeFromInstance } from "./marketplace.mjs";
|
|
76
|
+
import {
|
|
77
|
+
authSetupRequired,
|
|
78
|
+
buildClearSessionCookie,
|
|
79
|
+
buildSessionCookie,
|
|
80
|
+
getAuthUserFromRequest,
|
|
81
|
+
loginOrCreateUser,
|
|
82
|
+
logoutRequest,
|
|
83
|
+
} from "./auth.mjs";
|
|
68
84
|
|
|
69
85
|
const MIME = {
|
|
70
86
|
".html": "text/html; charset=utf-8",
|
|
@@ -123,6 +139,99 @@ function readModelListsFromDisk(workspaceRoot) {
|
|
|
123
139
|
}
|
|
124
140
|
}
|
|
125
141
|
|
|
142
|
+
const SKILLHUB_TIMEOUT_MS = 60_000;
|
|
143
|
+
|
|
144
|
+
function runSkillhub(args, opts = {}) {
|
|
145
|
+
return new Promise((resolve) => {
|
|
146
|
+
execFile("skillhub", args, {
|
|
147
|
+
cwd: opts.cwd || process.cwd(),
|
|
148
|
+
timeout: opts.timeoutMs || SKILLHUB_TIMEOUT_MS,
|
|
149
|
+
maxBuffer: opts.maxBuffer || 2 * 1024 * 1024,
|
|
150
|
+
env: {
|
|
151
|
+
...process.env,
|
|
152
|
+
FORCE_COLOR: "0",
|
|
153
|
+
},
|
|
154
|
+
}, (error, stdout, stderr) => {
|
|
155
|
+
const out = String(stdout || "");
|
|
156
|
+
const err = String(stderr || "");
|
|
157
|
+
resolve({
|
|
158
|
+
ok: !error,
|
|
159
|
+
code: error && typeof error.code === "number" ? error.code : 0,
|
|
160
|
+
error: error ? (err.trim() || error.message || "skillhub failed") : "",
|
|
161
|
+
stdout: out,
|
|
162
|
+
stderr: err,
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function parseJsonText(text, fallback = null) {
|
|
169
|
+
const s = String(text || "").trim();
|
|
170
|
+
if (!s) return fallback;
|
|
171
|
+
try {
|
|
172
|
+
return JSON.parse(s);
|
|
173
|
+
} catch {
|
|
174
|
+
const match = s.match(/(\{[\s\S]*\}|\[[\s\S]*\])\s*$/);
|
|
175
|
+
if (!match) return fallback;
|
|
176
|
+
try { return JSON.parse(match[1]); } catch { return fallback; }
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function normalizeSkillhubSearchPayload(raw) {
|
|
181
|
+
const data = raw && typeof raw === "object" ? raw : {};
|
|
182
|
+
const items = Array.isArray(data.items) ? data.items : Array.isArray(data.results) ? data.results : [];
|
|
183
|
+
return {
|
|
184
|
+
total: Number(data.total) || items.length,
|
|
185
|
+
mode: typeof data.mode === "string" ? data.mode : "",
|
|
186
|
+
degraded: Boolean(data.degraded),
|
|
187
|
+
items: items.map((item) => {
|
|
188
|
+
const x = item && typeof item === "object" ? item : {};
|
|
189
|
+
const id = x.id ?? x.skillId ?? x.skill_id ?? "";
|
|
190
|
+
const slug = String(x.slug ?? x.name ?? x.displayName ?? x.display_name ?? id ?? "").trim();
|
|
191
|
+
return {
|
|
192
|
+
id: String(id || slug),
|
|
193
|
+
slug,
|
|
194
|
+
name: String(x.displayName ?? x.display_name ?? x.name ?? slug),
|
|
195
|
+
summary: String(x.summary ?? x.description ?? ""),
|
|
196
|
+
version: String(x.version ?? x.latestVersion ?? x.latest_version ?? ""),
|
|
197
|
+
tags: Array.isArray(x.tags) ? x.tags.map(String) : [],
|
|
198
|
+
};
|
|
199
|
+
}).filter((x) => x.slug || x.name),
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function normalizeSkillhubListPayload(raw) {
|
|
204
|
+
const arr = Array.isArray(raw) ? raw : [];
|
|
205
|
+
return arr.map((x) => ({
|
|
206
|
+
name: String(x?.name ?? ""),
|
|
207
|
+
baseDir: String(x?.baseDir ?? ""),
|
|
208
|
+
path: String(x?.path ?? ""),
|
|
209
|
+
kind: String(x?.kind ?? ""),
|
|
210
|
+
agent: String(x?.agent ?? ""),
|
|
211
|
+
})).filter((x) => x.name);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function skillhubInstallArgs(payload, { uninstall = false } = {}) {
|
|
215
|
+
const slug = String(payload?.slug || payload?.name || "").trim();
|
|
216
|
+
if (!slug && !payload?.collection) return null;
|
|
217
|
+
const args = [uninstall ? "uninstall" : "install"];
|
|
218
|
+
if (payload?.collection) {
|
|
219
|
+
args.push("--collection", String(payload.collection).trim());
|
|
220
|
+
} else {
|
|
221
|
+
args.push(slug);
|
|
222
|
+
}
|
|
223
|
+
if (payload?.skillId) args.push("--skill-id", String(payload.skillId).trim());
|
|
224
|
+
const target = String(payload?.target || "project").trim();
|
|
225
|
+
const agent = String(payload?.agent || "codex").trim();
|
|
226
|
+
if (target === "global") {
|
|
227
|
+
args.push("--global", "--agent", agent);
|
|
228
|
+
} else if (payload?.dir) {
|
|
229
|
+
args.push("--dir", String(payload.dir).trim());
|
|
230
|
+
}
|
|
231
|
+
if (payload?.force) args.push("--force");
|
|
232
|
+
return args;
|
|
233
|
+
}
|
|
234
|
+
|
|
126
235
|
function readBody(req) {
|
|
127
236
|
return new Promise((resolve, reject) => {
|
|
128
237
|
const chunks = [];
|
|
@@ -132,6 +241,231 @@ function readBody(req) {
|
|
|
132
241
|
});
|
|
133
242
|
}
|
|
134
243
|
|
|
244
|
+
const WORKSPACE_FILE_SKIP_DIRS = new Set([
|
|
245
|
+
".git",
|
|
246
|
+
"node_modules",
|
|
247
|
+
".next",
|
|
248
|
+
".nuxt",
|
|
249
|
+
".turbo",
|
|
250
|
+
"dist",
|
|
251
|
+
"build",
|
|
252
|
+
"coverage",
|
|
253
|
+
]);
|
|
254
|
+
|
|
255
|
+
const WORKSPACE_TEXT_EXTS = new Set([
|
|
256
|
+
".md",
|
|
257
|
+
".markdown",
|
|
258
|
+
".txt",
|
|
259
|
+
".json",
|
|
260
|
+
".yaml",
|
|
261
|
+
".yml",
|
|
262
|
+
".js",
|
|
263
|
+
".jsx",
|
|
264
|
+
".ts",
|
|
265
|
+
".tsx",
|
|
266
|
+
".css",
|
|
267
|
+
".html",
|
|
268
|
+
".mjs",
|
|
269
|
+
".cjs",
|
|
270
|
+
]);
|
|
271
|
+
|
|
272
|
+
function resolveWorkspaceFilePath(workspaceRoot, relPath) {
|
|
273
|
+
const root = path.resolve(workspaceRoot);
|
|
274
|
+
const rel = String(relPath || "").replace(/^[/\\]+/, "");
|
|
275
|
+
const abs = path.resolve(root, rel);
|
|
276
|
+
if (abs !== root && !abs.startsWith(root + path.sep)) {
|
|
277
|
+
throw new Error("Path traversal not allowed");
|
|
278
|
+
}
|
|
279
|
+
return { root, rel: path.relative(root, abs).replace(/\\/g, "/"), abs };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function workspaceFileIcon(fileName, isDir = false) {
|
|
283
|
+
if (isDir) return "folder";
|
|
284
|
+
const ext = path.extname(fileName).toLowerCase();
|
|
285
|
+
if (ext === ".md" || ext === ".markdown") return "article";
|
|
286
|
+
if ([".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"].includes(ext)) return "code";
|
|
287
|
+
if ([".yaml", ".yml", ".json"].includes(ext)) return "data_object";
|
|
288
|
+
if (ext === ".css") return "palette";
|
|
289
|
+
if (ext === ".html") return "web";
|
|
290
|
+
return "draft";
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function readWorkspaceFilesRecursive(dir, root, depth = 0, maxDepth = 3, budget = { count: 0 }) {
|
|
294
|
+
if (depth > maxDepth || budget.count > 500) return [];
|
|
295
|
+
let entries;
|
|
296
|
+
try {
|
|
297
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
298
|
+
} catch {
|
|
299
|
+
return [];
|
|
300
|
+
}
|
|
301
|
+
const out = [];
|
|
302
|
+
for (const entry of entries) {
|
|
303
|
+
if (budget.count > 500) break;
|
|
304
|
+
if (entry.name.startsWith(".") && entry.name !== ".agents" && entry.name !== ".codex") continue;
|
|
305
|
+
const abs = path.join(dir, entry.name);
|
|
306
|
+
const rel = path.relative(root, abs).replace(/\\/g, "/");
|
|
307
|
+
if (entry.isDirectory()) {
|
|
308
|
+
if (WORKSPACE_FILE_SKIP_DIRS.has(entry.name)) continue;
|
|
309
|
+
budget.count++;
|
|
310
|
+
out.push({
|
|
311
|
+
type: "directory",
|
|
312
|
+
name: entry.name,
|
|
313
|
+
path: rel,
|
|
314
|
+
icon: workspaceFileIcon(entry.name, true),
|
|
315
|
+
children: readWorkspaceFilesRecursive(abs, root, depth + 1, maxDepth, budget),
|
|
316
|
+
});
|
|
317
|
+
} else if (entry.isFile()) {
|
|
318
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
319
|
+
if (!WORKSPACE_TEXT_EXTS.has(ext)) continue;
|
|
320
|
+
let size = 0;
|
|
321
|
+
try { size = fs.statSync(abs).size; } catch {}
|
|
322
|
+
budget.count++;
|
|
323
|
+
out.push({ type: "file", name: entry.name, path: rel, icon: workspaceFileIcon(entry.name), size });
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
out.sort((a, b) => {
|
|
327
|
+
if (a.type !== b.type) return a.type === "directory" ? -1 : 1;
|
|
328
|
+
return a.name.localeCompare(b.name);
|
|
329
|
+
});
|
|
330
|
+
return out;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function readWorkspaceFiles(workspaceRoot) {
|
|
334
|
+
const root = path.resolve(workspaceRoot);
|
|
335
|
+
return { root, files: readWorkspaceFilesRecursive(root, root) };
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const WORKSPACE_GRAPH_FILENAME = "workspace.graph.json";
|
|
339
|
+
|
|
340
|
+
function workspaceGraphPath(workspaceRoot) {
|
|
341
|
+
return path.join(path.resolve(workspaceRoot), WORKSPACE_GRAPH_FILENAME);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function emptyWorkspaceGraph() {
|
|
345
|
+
return { version: 1, instances: {}, edges: [], ui: { nodePositions: {} } };
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function readWorkspaceGraph(workspaceRoot) {
|
|
349
|
+
const graphPath = workspaceGraphPath(workspaceRoot);
|
|
350
|
+
if (!fs.existsSync(graphPath)) return { path: graphPath, graph: emptyWorkspaceGraph() };
|
|
351
|
+
const raw = fs.readFileSync(graphPath, "utf-8");
|
|
352
|
+
if (!raw.trim()) return { path: graphPath, graph: emptyWorkspaceGraph() };
|
|
353
|
+
const parsed = JSON.parse(raw);
|
|
354
|
+
const graph = parsed && typeof parsed === "object" ? parsed : {};
|
|
355
|
+
return {
|
|
356
|
+
path: graphPath,
|
|
357
|
+
graph: {
|
|
358
|
+
version: Number(graph.version) || 1,
|
|
359
|
+
instances: graph.instances && typeof graph.instances === "object" && !Array.isArray(graph.instances) ? graph.instances : {},
|
|
360
|
+
edges: Array.isArray(graph.edges) ? graph.edges : [],
|
|
361
|
+
ui: graph.ui && typeof graph.ui === "object" ? graph.ui : { nodePositions: {} },
|
|
362
|
+
},
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function normalizeWorkspaceGraphPayload(payload) {
|
|
367
|
+
const graph = payload?.graph && typeof payload.graph === "object" ? payload.graph : payload;
|
|
368
|
+
return {
|
|
369
|
+
version: 1,
|
|
370
|
+
instances: graph?.instances && typeof graph.instances === "object" && !Array.isArray(graph.instances) ? graph.instances : {},
|
|
371
|
+
edges: Array.isArray(graph?.edges) ? graph.edges : [],
|
|
372
|
+
ui: graph?.ui && typeof graph.ui === "object" ? graph.ui : { nodePositions: {} },
|
|
373
|
+
updatedAt: new Date().toISOString(),
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function resolveWorkspaceScopeRoot(workspaceRoot, params = {}, opts = {}) {
|
|
378
|
+
const flowId = params.flowId != null ? String(params.flowId).trim() : "";
|
|
379
|
+
if (!flowId) return { root: path.resolve(workspaceRoot), flowId: "", flowSource: "", archived: false };
|
|
380
|
+
const flowSource = params.flowSource != null && String(params.flowSource).trim()
|
|
381
|
+
? String(params.flowSource).trim()
|
|
382
|
+
: "user";
|
|
383
|
+
const archived = params.archived === true || params.archived === "1" || params.flowArchived === true;
|
|
384
|
+
if (!isValidFlowSourceRead(flowSource)) {
|
|
385
|
+
return { root: "", error: "Invalid flowSource" };
|
|
386
|
+
}
|
|
387
|
+
const result = getPipelineFiles(workspaceRoot, flowId, flowSource, archived, opts);
|
|
388
|
+
if (result.error || !result.path) {
|
|
389
|
+
return { root: "", error: result.error || "Pipeline workspace not found" };
|
|
390
|
+
}
|
|
391
|
+
return { root: path.resolve(result.path), flowId, flowSource, archived };
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function buildWorkspaceGeneratePrompt(payload) {
|
|
395
|
+
const userPrompt = String(payload?.prompt || "").trim();
|
|
396
|
+
const outputKind = String(payload?.outputKind || payload?.kind || "markdown").trim().toLowerCase();
|
|
397
|
+
const allowFlowYaml = payload?.allowFlowYaml === true || payload?.allowFlowYaml === "1";
|
|
398
|
+
const workspaceGraph = payload?.workspaceGraph && typeof payload.workspaceGraph === "object" ? payload.workspaceGraph : null;
|
|
399
|
+
const selectedNodeIds = Array.isArray(payload?.selectedNodeIds)
|
|
400
|
+
? payload.selectedNodeIds.map((id) => String(id || "").trim()).filter(Boolean)
|
|
401
|
+
: [];
|
|
402
|
+
const skillsBlock = typeof payload?.skillsBlock === "string" ? payload.skillsBlock.trim() : "";
|
|
403
|
+
const contexts = Array.isArray(payload?.contexts) ? payload.contexts : [];
|
|
404
|
+
const contextBlocks = contexts
|
|
405
|
+
.map((ctx, idx) => {
|
|
406
|
+
const title = String(ctx?.title || ctx?.path || `context-${idx + 1}`).trim();
|
|
407
|
+
const kind = String(ctx?.kind || "text").trim();
|
|
408
|
+
const content = String(ctx?.content || "").trim();
|
|
409
|
+
if (!content) return "";
|
|
410
|
+
return `### ${title} (${kind})\n\n${content}`;
|
|
411
|
+
})
|
|
412
|
+
.filter(Boolean)
|
|
413
|
+
.join("\n\n---\n\n");
|
|
414
|
+
const kindInstruction =
|
|
415
|
+
outputKind === "mermaid"
|
|
416
|
+
? [
|
|
417
|
+
"你是 workspace Mermaid 图节点的内容生成器。",
|
|
418
|
+
"请根据用户 prompt 和上游节点/文件上下文生成 Mermaid flowchart 源码。",
|
|
419
|
+
"只输出 Mermaid 源码,不要解释,不要包裹 Markdown 代码围栏。",
|
|
420
|
+
"优先使用 `flowchart TD` 或 `graph TD`,节点 ID 使用简单英文/数字/下划线,节点 label 使用清晰短文本。",
|
|
421
|
+
].join("\n")
|
|
422
|
+
: outputKind === "ascii"
|
|
423
|
+
? [
|
|
424
|
+
"你是 workspace ASCII 图节点的内容生成器。",
|
|
425
|
+
"请根据用户 prompt 和上游节点/文件上下文生成等宽字体下可读的 ASCII 图。",
|
|
426
|
+
"只输出 ASCII 图正文,不要解释,不要包裹 Markdown 代码围栏。",
|
|
427
|
+
"使用 +-|/\\<> 等字符表达结构,尽量保持对齐。",
|
|
428
|
+
].join("\n")
|
|
429
|
+
: [
|
|
430
|
+
"你是 workspace Markdown 节点的内容生成器。",
|
|
431
|
+
"请根据用户 prompt 和上游节点/文件上下文,生成可直接保存到工作区的 Markdown 正文。",
|
|
432
|
+
"只输出最终 Markdown 内容,不要解释你如何执行,也不要包裹代码围栏,除非正文本身需要代码块。",
|
|
433
|
+
].join("\n");
|
|
434
|
+
return [
|
|
435
|
+
"你正在 AgentFlow 的 Workspace 工作画布中执行任务。",
|
|
436
|
+
"Workspace 是当前 pipeline 的临时工作区,用于分析、试验、生成中间文件和展示结果。",
|
|
437
|
+
allowFlowYaml
|
|
438
|
+
? "用户已允许你考虑正式 flow.yaml;如需修改仍必须明确说明影响。"
|
|
439
|
+
: "默认不要修改正式 flow.yaml;优先在 workspace 文件、workspace.graph.json 或回复内容中完成任务。",
|
|
440
|
+
workspaceGraph ? `\n## 当前 workspace graph\n\n${JSON.stringify(workspaceGraph, null, 2)}` : "",
|
|
441
|
+
selectedNodeIds.length > 0 ? `\n## 当前用户选中的 workspace 节点\n\n${selectedNodeIds.map((id) => `- ${id}`).join("\n")}` : "",
|
|
442
|
+
skillsBlock ? `\n## Workspace Skills\n\n${skillsBlock}` : "",
|
|
443
|
+
kindInstruction,
|
|
444
|
+
contextBlocks ? `\n## 上下文\n\n${contextBlocks}` : "",
|
|
445
|
+
`\n## 用户 prompt\n\n${userPrompt}`,
|
|
446
|
+
].filter(Boolean).join("\n");
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function isTransientAgentNetworkError(err) {
|
|
450
|
+
const text = [
|
|
451
|
+
err?.message,
|
|
452
|
+
err?.cursorStderrTail,
|
|
453
|
+
err?.stderr,
|
|
454
|
+
err?.stack,
|
|
455
|
+
].filter(Boolean).join("\n");
|
|
456
|
+
return /Client network socket disconnected before secure TLS connection was established/i.test(text) ||
|
|
457
|
+
/secure TLS connection was established/i.test(text) ||
|
|
458
|
+
/\bECONNRESET\b/i.test(text) ||
|
|
459
|
+
/\bETIMEDOUT\b/i.test(text) ||
|
|
460
|
+
/\bEAI_AGAIN\b/i.test(text) ||
|
|
461
|
+
/network socket disconnected/i.test(text) ||
|
|
462
|
+
/socket hang up/i.test(text);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function sleepMs(ms) {
|
|
466
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
467
|
+
}
|
|
468
|
+
|
|
135
469
|
/** ZIP 本地头:PK\x03\x04 / \x05\x06 / \x07\x08 */
|
|
136
470
|
function bufferLooksLikeZip(buf) {
|
|
137
471
|
return (
|
|
@@ -211,12 +545,12 @@ const flowEditorSyncSubscribers = new Map();
|
|
|
211
545
|
/** 每次 broadcastFlowEditorSync 时递增,供轮询端点 /api/flow-editor-sync-poll 使用 */
|
|
212
546
|
const flowEditorSyncVersions = new Map();
|
|
213
547
|
|
|
214
|
-
function flowEditorSyncKey(flowId, flowSource, flowArchived) {
|
|
215
|
-
return `${String(flowId)}\t${String(flowSource)}\t${flowArchived ? "1" : "0"}`;
|
|
548
|
+
function flowEditorSyncKey(flowId, flowSource, flowArchived, userId = "") {
|
|
549
|
+
return `${String(userId || "")}\t${String(flowId)}\t${String(flowSource)}\t${flowArchived ? "1" : "0"}`;
|
|
216
550
|
}
|
|
217
551
|
|
|
218
|
-
function broadcastFlowEditorSync(flowId, flowSource, flowArchived = false) {
|
|
219
|
-
const key = flowEditorSyncKey(flowId, flowSource, flowArchived);
|
|
552
|
+
function broadcastFlowEditorSync(flowId, flowSource, flowArchived = false, userId = "") {
|
|
553
|
+
const key = flowEditorSyncKey(flowId, flowSource, flowArchived, userId);
|
|
220
554
|
|
|
221
555
|
/* 递增轮询版本号 */
|
|
222
556
|
flowEditorSyncVersions.set(key, (flowEditorSyncVersions.get(key) ?? 0) + 1);
|
|
@@ -340,6 +674,7 @@ function normalizeContextInstanceIds(raw) {
|
|
|
340
674
|
* @param {object} opts
|
|
341
675
|
* @param {string} opts.workspaceRoot
|
|
342
676
|
* @param {number} opts.port
|
|
677
|
+
* @param {boolean} [opts.hideCommunityLinks]
|
|
343
678
|
* @param {string} [opts.staticDir] 默认 PACKAGE_ROOT/builtin/web-ui/dist(npm run build 产出)
|
|
344
679
|
* @returns {Promise<import('http').Server>}
|
|
345
680
|
*/
|
|
@@ -347,10 +682,12 @@ export function startUiServer({
|
|
|
347
682
|
workspaceRoot,
|
|
348
683
|
port,
|
|
349
684
|
host = "127.0.0.1",
|
|
685
|
+
hideCommunityLinks = false,
|
|
350
686
|
staticDir = path.join(PACKAGE_ROOT, "builtin", "web-ui", "dist"),
|
|
351
687
|
}) {
|
|
352
688
|
const root = path.resolve(workspaceRoot);
|
|
353
689
|
const uiPort = port;
|
|
690
|
+
const uiConfig = { hideCommunityLinks: Boolean(hideCommunityLinks) };
|
|
354
691
|
|
|
355
692
|
const server = http.createServer(async (req, res) => {
|
|
356
693
|
const url = new URL(req.url || "/", "http://127.0.0.1");
|
|
@@ -363,10 +700,58 @@ export function startUiServer({
|
|
|
363
700
|
return origEnd(...args);
|
|
364
701
|
};
|
|
365
702
|
|
|
703
|
+
if (url.pathname === "/api/auth/me" && req.method === "GET") {
|
|
704
|
+
const user = getAuthUserFromRequest(req);
|
|
705
|
+
json(res, 200, { authenticated: Boolean(user), user: user || null, setupRequired: authSetupRequired() });
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
if (url.pathname === "/api/auth/login" && req.method === "POST") {
|
|
710
|
+
let payload;
|
|
711
|
+
try {
|
|
712
|
+
payload = JSON.parse(await readBody(req));
|
|
713
|
+
} catch {
|
|
714
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
const result = loginOrCreateUser(payload?.username, payload?.password);
|
|
718
|
+
if (!result.ok) {
|
|
719
|
+
json(res, 401, { error: result.error || "Login failed", setupRequired: authSetupRequired() });
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
const body = JSON.stringify({ authenticated: true, user: result.user, setupRequired: false, migration: result.migration || null });
|
|
723
|
+
res.writeHead(200, {
|
|
724
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
725
|
+
"Content-Length": Buffer.byteLength(body),
|
|
726
|
+
"Set-Cookie": buildSessionCookie(result.token),
|
|
727
|
+
});
|
|
728
|
+
res.end(body);
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
if (url.pathname === "/api/auth/logout" && req.method === "POST") {
|
|
733
|
+
logoutRequest(req);
|
|
734
|
+
const body = JSON.stringify({ ok: true });
|
|
735
|
+
res.writeHead(200, {
|
|
736
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
737
|
+
"Content-Length": Buffer.byteLength(body),
|
|
738
|
+
"Set-Cookie": buildClearSessionCookie(),
|
|
739
|
+
});
|
|
740
|
+
res.end(body);
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
const authUser = getAuthUserFromRequest(req);
|
|
745
|
+
const userCtx = authUser ? { userId: authUser.userId } : {};
|
|
746
|
+
if (url.pathname.startsWith("/api/") && !authUser) {
|
|
747
|
+
json(res, 401, { error: "Authentication required", setupRequired: authSetupRequired() });
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
|
|
366
751
|
if (url.pathname === "/api/flows") {
|
|
367
752
|
if (req.method === "GET") {
|
|
368
753
|
try {
|
|
369
|
-
json(res, 200, listFlowsJson(root));
|
|
754
|
+
json(res, 200, listFlowsJson(root, userCtx));
|
|
370
755
|
} catch (e) {
|
|
371
756
|
json(res, 500, { error: (e && e.message) || String(e) });
|
|
372
757
|
}
|
|
@@ -400,7 +785,7 @@ export function startUiServer({
|
|
|
400
785
|
if (ts === "workspace" || ts === "user") {
|
|
401
786
|
targetSpace = ts;
|
|
402
787
|
}
|
|
403
|
-
const existing = listFlowsJson(root);
|
|
788
|
+
const existing = listFlowsJson(root, userCtx);
|
|
404
789
|
if (
|
|
405
790
|
existing.some(
|
|
406
791
|
(f) => f.id === flowId && (f.source ?? "user") === targetSpace && !f.archived,
|
|
@@ -410,7 +795,7 @@ export function startUiServer({
|
|
|
410
795
|
return;
|
|
411
796
|
}
|
|
412
797
|
const flowYaml = buildEmptyUserFlowYaml({ description: desc });
|
|
413
|
-
const result = writeFlowYaml(root, flowId, targetSpace, flowYaml);
|
|
798
|
+
const result = writeFlowYaml(root, flowId, targetSpace, flowYaml, userCtx);
|
|
414
799
|
if (!result.success) {
|
|
415
800
|
json(res, 400, result);
|
|
416
801
|
return;
|
|
@@ -456,7 +841,7 @@ export function startUiServer({
|
|
|
456
841
|
}
|
|
457
842
|
const flowId = idCheck.flowId;
|
|
458
843
|
const targetSpace = parsed.targetSpace === "workspace" ? "workspace" : "user";
|
|
459
|
-
const existing = listFlowsJson(root);
|
|
844
|
+
const existing = listFlowsJson(root, userCtx);
|
|
460
845
|
if (
|
|
461
846
|
existing.some(
|
|
462
847
|
(f) => f.id === flowId && (f.source ?? "user") === targetSpace && !f.archived,
|
|
@@ -487,7 +872,7 @@ export function startUiServer({
|
|
|
487
872
|
filesMap = new Map([["flow.yaml", Buffer.from(text, "utf8")]]);
|
|
488
873
|
}
|
|
489
874
|
|
|
490
|
-
const w = writePipelineTree(root, flowId, targetSpace, filesMap);
|
|
875
|
+
const w = writePipelineTree(root, flowId, targetSpace, filesMap, userCtx);
|
|
491
876
|
if (!w.success) {
|
|
492
877
|
json(res, 400, { error: w.error });
|
|
493
878
|
return;
|
|
@@ -507,7 +892,7 @@ export function startUiServer({
|
|
|
507
892
|
return;
|
|
508
893
|
}
|
|
509
894
|
const { getNodeExecContext } = await import("./node-exec-context.mjs");
|
|
510
|
-
json(res, 200, getNodeExecContext(root, flowId, instanceId, runId));
|
|
895
|
+
json(res, 200, getNodeExecContext(root, flowId, instanceId, runId, userCtx));
|
|
511
896
|
} catch (e) {
|
|
512
897
|
json(res, 500, { error: (e && e.message) || String(e) });
|
|
513
898
|
}
|
|
@@ -516,7 +901,7 @@ export function startUiServer({
|
|
|
516
901
|
|
|
517
902
|
if (req.method === "GET" && url.pathname === "/api/pipeline-recent-runs") {
|
|
518
903
|
try {
|
|
519
|
-
json(res, 200, { runs: listRecentRunsFromDisk(root) });
|
|
904
|
+
json(res, 200, { runs: listRecentRunsFromDisk(root, userCtx) });
|
|
520
905
|
} catch (e) {
|
|
521
906
|
json(res, 500, { error: (e && e.message) || String(e) });
|
|
522
907
|
}
|
|
@@ -532,7 +917,7 @@ export function startUiServer({
|
|
|
532
917
|
return;
|
|
533
918
|
}
|
|
534
919
|
const { getRunNodeStatusesFromDisk } = await import("./run-node-statuses-from-disk.mjs");
|
|
535
|
-
json(res, 200, { statuses: getRunNodeStatusesFromDisk(root, flowId, runId) });
|
|
920
|
+
json(res, 200, { statuses: getRunNodeStatusesFromDisk(root, flowId, runId, userCtx) });
|
|
536
921
|
} catch (e) {
|
|
537
922
|
json(res, 500, { error: (e && e.message) || String(e) });
|
|
538
923
|
}
|
|
@@ -554,7 +939,7 @@ export function startUiServer({
|
|
|
554
939
|
const { getRunDir } = await import("./workspace.mjs");
|
|
555
940
|
const { RUN_LOG_REL } = await import("./paths.mjs");
|
|
556
941
|
const { default: fsMod } = await import("node:fs");
|
|
557
|
-
const logPath = path.join(getRunDir(root, flowId, runId), RUN_LOG_REL);
|
|
942
|
+
const logPath = path.join(getRunDir(root, flowId, runId, userCtx), RUN_LOG_REL);
|
|
558
943
|
if (!fsMod.existsSync(logPath)) {
|
|
559
944
|
json(res, 200, { bytes: 0, text: "" });
|
|
560
945
|
return;
|
|
@@ -596,6 +981,303 @@ export function startUiServer({
|
|
|
596
981
|
return;
|
|
597
982
|
}
|
|
598
983
|
|
|
984
|
+
if (req.method === "GET" && url.pathname === "/api/workspace/files") {
|
|
985
|
+
try {
|
|
986
|
+
const scoped = resolveWorkspaceScopeRoot(root, {
|
|
987
|
+
flowId: url.searchParams.get("flowId") || "",
|
|
988
|
+
flowSource: url.searchParams.get("flowSource") || "user",
|
|
989
|
+
archived: url.searchParams.get("archived") === "1",
|
|
990
|
+
}, userCtx);
|
|
991
|
+
if (scoped.error) {
|
|
992
|
+
json(res, 400, { error: scoped.error });
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
json(res, 200, { ...readWorkspaceFiles(scoped.root), flowId: scoped.flowId, flowSource: scoped.flowSource, archived: scoped.archived });
|
|
996
|
+
} catch (e) {
|
|
997
|
+
json(res, 500, { error: (e && e.message) || String(e) });
|
|
998
|
+
}
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
if (req.method === "GET" && url.pathname === "/api/workspace/graph") {
|
|
1003
|
+
try {
|
|
1004
|
+
const scoped = resolveWorkspaceScopeRoot(root, {
|
|
1005
|
+
flowId: url.searchParams.get("flowId") || "",
|
|
1006
|
+
flowSource: url.searchParams.get("flowSource") || "user",
|
|
1007
|
+
archived: url.searchParams.get("archived") === "1",
|
|
1008
|
+
}, userCtx);
|
|
1009
|
+
if (scoped.error) {
|
|
1010
|
+
json(res, 400, { error: scoped.error });
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
const { path: graphPath, graph } = readWorkspaceGraph(scoped.root);
|
|
1014
|
+
json(res, 200, {
|
|
1015
|
+
ok: true,
|
|
1016
|
+
graph,
|
|
1017
|
+
path: graphPath,
|
|
1018
|
+
root: scoped.root,
|
|
1019
|
+
flowId: scoped.flowId,
|
|
1020
|
+
flowSource: scoped.flowSource,
|
|
1021
|
+
archived: scoped.archived,
|
|
1022
|
+
writable: !(scoped.archived || scoped.flowSource === "builtin"),
|
|
1023
|
+
});
|
|
1024
|
+
} catch (e) {
|
|
1025
|
+
json(res, 500, { error: (e && e.message) || String(e) });
|
|
1026
|
+
}
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
if (req.method === "POST" && url.pathname === "/api/workspace/graph") {
|
|
1031
|
+
let payload;
|
|
1032
|
+
try {
|
|
1033
|
+
payload = JSON.parse(await readBody(req));
|
|
1034
|
+
} catch {
|
|
1035
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
try {
|
|
1039
|
+
const scoped = resolveWorkspaceScopeRoot(root, {
|
|
1040
|
+
flowId: payload.flowId || "",
|
|
1041
|
+
flowSource: payload.flowSource || "user",
|
|
1042
|
+
archived: payload.archived === true || payload.flowArchived === true,
|
|
1043
|
+
}, userCtx);
|
|
1044
|
+
if (scoped.error) {
|
|
1045
|
+
json(res, 400, { error: scoped.error });
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
if (scoped.archived || scoped.flowSource === "builtin") {
|
|
1049
|
+
json(res, 400, { error: "Cannot write workspace graph for builtin or archived pipeline" });
|
|
1050
|
+
return;
|
|
1051
|
+
}
|
|
1052
|
+
const graph = normalizeWorkspaceGraphPayload(payload.graph || payload);
|
|
1053
|
+
const graphPath = workspaceGraphPath(scoped.root);
|
|
1054
|
+
fs.writeFileSync(graphPath, JSON.stringify(graph, null, 2) + "\n", "utf-8");
|
|
1055
|
+
json(res, 200, { ok: true, path: graphPath, graph });
|
|
1056
|
+
} catch (e) {
|
|
1057
|
+
json(res, 500, { error: (e && e.message) || String(e) });
|
|
1058
|
+
}
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
if (req.method === "GET" && url.pathname === "/api/workspace/file") {
|
|
1063
|
+
try {
|
|
1064
|
+
const scoped = resolveWorkspaceScopeRoot(root, {
|
|
1065
|
+
flowId: url.searchParams.get("flowId") || "",
|
|
1066
|
+
flowSource: url.searchParams.get("flowSource") || "user",
|
|
1067
|
+
archived: url.searchParams.get("archived") === "1",
|
|
1068
|
+
}, userCtx);
|
|
1069
|
+
if (scoped.error) {
|
|
1070
|
+
json(res, 400, { error: scoped.error });
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1073
|
+
const { abs, rel } = resolveWorkspaceFilePath(scoped.root, url.searchParams.get("path") || "");
|
|
1074
|
+
if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) {
|
|
1075
|
+
json(res, 404, { error: "File not found" });
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
const stat = fs.statSync(abs);
|
|
1079
|
+
if (stat.size > 2 * 1024 * 1024) {
|
|
1080
|
+
json(res, 413, { error: "File too large" });
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
json(res, 200, { path: rel, content: fs.readFileSync(abs, "utf-8"), size: stat.size });
|
|
1084
|
+
} catch (e) {
|
|
1085
|
+
json(res, /traversal/i.test(String(e.message || e)) ? 403 : 500, { error: (e && e.message) || String(e) });
|
|
1086
|
+
}
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
if (req.method === "POST" && url.pathname === "/api/workspace/file") {
|
|
1091
|
+
let payload;
|
|
1092
|
+
try {
|
|
1093
|
+
payload = JSON.parse(await readBody(req));
|
|
1094
|
+
} catch {
|
|
1095
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
try {
|
|
1099
|
+
const scoped = resolveWorkspaceScopeRoot(root, {
|
|
1100
|
+
flowId: payload.flowId || "",
|
|
1101
|
+
flowSource: payload.flowSource || "user",
|
|
1102
|
+
archived: payload.archived === true || payload.flowArchived === true,
|
|
1103
|
+
}, userCtx);
|
|
1104
|
+
if (scoped.error) {
|
|
1105
|
+
json(res, 400, { error: scoped.error });
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
if (scoped.archived || scoped.flowSource === "builtin") {
|
|
1109
|
+
json(res, 400, { error: "Cannot write to builtin or archived pipeline workspace" });
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
const { abs, rel } = resolveWorkspaceFilePath(scoped.root, payload.path || "");
|
|
1113
|
+
if (!rel) {
|
|
1114
|
+
json(res, 400, { error: "Missing path" });
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
1118
|
+
fs.writeFileSync(abs, String(payload.content ?? ""), "utf-8");
|
|
1119
|
+
json(res, 200, { ok: true, path: rel });
|
|
1120
|
+
} catch (e) {
|
|
1121
|
+
json(res, /traversal/i.test(String(e.message || e)) ? 403 : 500, { error: (e && e.message) || String(e) });
|
|
1122
|
+
}
|
|
1123
|
+
return;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
if (req.method === "POST" && url.pathname === "/api/workspace/folder") {
|
|
1127
|
+
let payload;
|
|
1128
|
+
try {
|
|
1129
|
+
payload = JSON.parse(await readBody(req));
|
|
1130
|
+
} catch {
|
|
1131
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
try {
|
|
1135
|
+
const scoped = resolveWorkspaceScopeRoot(root, {
|
|
1136
|
+
flowId: payload.flowId || "",
|
|
1137
|
+
flowSource: payload.flowSource || "user",
|
|
1138
|
+
archived: payload.archived === true || payload.flowArchived === true,
|
|
1139
|
+
}, userCtx);
|
|
1140
|
+
if (scoped.error) {
|
|
1141
|
+
json(res, 400, { error: scoped.error });
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
if (scoped.archived || scoped.flowSource === "builtin") {
|
|
1145
|
+
json(res, 400, { error: "Cannot write to builtin or archived pipeline workspace" });
|
|
1146
|
+
return;
|
|
1147
|
+
}
|
|
1148
|
+
const { abs, rel } = resolveWorkspaceFilePath(scoped.root, payload.path || "");
|
|
1149
|
+
if (!rel) {
|
|
1150
|
+
json(res, 400, { error: "Missing path" });
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1153
|
+
fs.mkdirSync(abs, { recursive: true });
|
|
1154
|
+
json(res, 200, { ok: true, path: rel });
|
|
1155
|
+
} catch (e) {
|
|
1156
|
+
json(res, /traversal/i.test(String(e.message || e)) ? 403 : 500, { error: (e && e.message) || String(e) });
|
|
1157
|
+
}
|
|
1158
|
+
return;
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
if (req.method === "POST" && url.pathname === "/api/workspace/delete") {
|
|
1162
|
+
let payload;
|
|
1163
|
+
try {
|
|
1164
|
+
payload = JSON.parse(await readBody(req));
|
|
1165
|
+
} catch {
|
|
1166
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
1167
|
+
return;
|
|
1168
|
+
}
|
|
1169
|
+
try {
|
|
1170
|
+
const scoped = resolveWorkspaceScopeRoot(root, {
|
|
1171
|
+
flowId: payload.flowId || "",
|
|
1172
|
+
flowSource: payload.flowSource || "user",
|
|
1173
|
+
archived: payload.archived === true || payload.flowArchived === true,
|
|
1174
|
+
}, userCtx);
|
|
1175
|
+
if (scoped.error) {
|
|
1176
|
+
json(res, 400, { error: scoped.error });
|
|
1177
|
+
return;
|
|
1178
|
+
}
|
|
1179
|
+
if (scoped.archived || scoped.flowSource === "builtin") {
|
|
1180
|
+
json(res, 400, { error: "Cannot write to builtin or archived pipeline workspace" });
|
|
1181
|
+
return;
|
|
1182
|
+
}
|
|
1183
|
+
const { abs, rel } = resolveWorkspaceFilePath(scoped.root, payload.path || "");
|
|
1184
|
+
if (!rel) {
|
|
1185
|
+
json(res, 400, { error: "Missing path" });
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
if (!fs.existsSync(abs)) {
|
|
1189
|
+
json(res, 404, { error: "Path not found" });
|
|
1190
|
+
return;
|
|
1191
|
+
}
|
|
1192
|
+
fs.rmSync(abs, { recursive: true, force: true });
|
|
1193
|
+
json(res, 200, { ok: true, path: rel });
|
|
1194
|
+
} catch (e) {
|
|
1195
|
+
json(res, /traversal/i.test(String(e.message || e)) ? 403 : 500, { error: (e && e.message) || String(e) });
|
|
1196
|
+
}
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
if (req.method === "POST" && url.pathname === "/api/workspace/generate") {
|
|
1201
|
+
let payload;
|
|
1202
|
+
try {
|
|
1203
|
+
payload = JSON.parse(await readBody(req));
|
|
1204
|
+
} catch {
|
|
1205
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
1206
|
+
return;
|
|
1207
|
+
}
|
|
1208
|
+
const prompt = String(payload?.prompt || "").trim();
|
|
1209
|
+
if (!prompt) {
|
|
1210
|
+
json(res, 400, { error: "Missing prompt" });
|
|
1211
|
+
return;
|
|
1212
|
+
}
|
|
1213
|
+
try {
|
|
1214
|
+
const scoped = resolveWorkspaceScopeRoot(root, {
|
|
1215
|
+
flowId: payload.flowId || "",
|
|
1216
|
+
flowSource: payload.flowSource || "user",
|
|
1217
|
+
archived: payload.archived === true || payload.flowArchived === true,
|
|
1218
|
+
}, userCtx);
|
|
1219
|
+
if (scoped.error) {
|
|
1220
|
+
json(res, 400, { error: scoped.error });
|
|
1221
|
+
return;
|
|
1222
|
+
}
|
|
1223
|
+
const selectedSkillKeys = Array.isArray(payload?.selectedSkills)
|
|
1224
|
+
? payload.selectedSkills.map((x) => String(x || "").trim()).filter(Boolean)
|
|
1225
|
+
: [];
|
|
1226
|
+
const selectedSkillResources = selectedSkillKeys.length > 0
|
|
1227
|
+
? loadResourcesForSkillKeys(selectedSkillKeys, PACKAGE_ROOT, scoped.root)
|
|
1228
|
+
: { skills: [], references: [] };
|
|
1229
|
+
const skillsBlock = selectedSkillKeys.length > 0
|
|
1230
|
+
? buildSkillCompactInjectionBlock(selectedSkillResources.skills, selectedSkillResources.references)
|
|
1231
|
+
: "";
|
|
1232
|
+
let content = "";
|
|
1233
|
+
const events = [];
|
|
1234
|
+
const maxAttempts = 3;
|
|
1235
|
+
const promptText = buildWorkspaceGeneratePrompt({ ...payload, skillsBlock });
|
|
1236
|
+
const modelKey = typeof payload?.model === "string" ? payload.model.trim() : "";
|
|
1237
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
1238
|
+
let attemptContent = "";
|
|
1239
|
+
try {
|
|
1240
|
+
if (attempt > 1) {
|
|
1241
|
+
events.push({
|
|
1242
|
+
type: "status",
|
|
1243
|
+
line: `Workspace agent retry ${attempt}/${maxAttempts} after transient network failure...`,
|
|
1244
|
+
});
|
|
1245
|
+
await sleepMs(Math.min(1500 * attempt, 5000));
|
|
1246
|
+
}
|
|
1247
|
+
const handle = startComposerAgent({
|
|
1248
|
+
uiWorkspaceRoot: scoped.root,
|
|
1249
|
+
cliWorkspace: scoped.root,
|
|
1250
|
+
prompt: promptText,
|
|
1251
|
+
modelKey,
|
|
1252
|
+
agentflowUserId: userCtx.userId || "",
|
|
1253
|
+
onStreamEvent: (ev) => {
|
|
1254
|
+
events.push(ev);
|
|
1255
|
+
if (ev?.type === "natural" && typeof ev.text === "string") {
|
|
1256
|
+
attemptContent += (attemptContent ? "\n" : "") + ev.text;
|
|
1257
|
+
}
|
|
1258
|
+
},
|
|
1259
|
+
});
|
|
1260
|
+
await handle.finished;
|
|
1261
|
+
content = attemptContent;
|
|
1262
|
+
break;
|
|
1263
|
+
} catch (e) {
|
|
1264
|
+
if (attempt < maxAttempts && isTransientAgentNetworkError(e)) {
|
|
1265
|
+
events.push({
|
|
1266
|
+
type: "status",
|
|
1267
|
+
line: `Workspace agent transient network error: ${String(e.message || e).slice(0, 220)}`,
|
|
1268
|
+
});
|
|
1269
|
+
continue;
|
|
1270
|
+
}
|
|
1271
|
+
throw e;
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
json(res, 200, { ok: true, content: content.trim(), events });
|
|
1275
|
+
} catch (e) {
|
|
1276
|
+
json(res, 500, { error: (e && e.message) || String(e) });
|
|
1277
|
+
}
|
|
1278
|
+
return;
|
|
1279
|
+
}
|
|
1280
|
+
|
|
599
1281
|
if (req.method === "GET" && url.pathname === "/api/pipeline-files") {
|
|
600
1282
|
const flowId = url.searchParams.get("flowId");
|
|
601
1283
|
const flowSource = url.searchParams.get("flowSource") || "user";
|
|
@@ -605,7 +1287,7 @@ export function startUiServer({
|
|
|
605
1287
|
return;
|
|
606
1288
|
}
|
|
607
1289
|
try {
|
|
608
|
-
const result = getPipelineFiles(root, flowId, flowSource, archived);
|
|
1290
|
+
const result = getPipelineFiles(root, flowId, flowSource, archived, userCtx);
|
|
609
1291
|
json(res, 200, result);
|
|
610
1292
|
} catch (e) {
|
|
611
1293
|
json(res, 500, { error: (e && e.message) || String(e) });
|
|
@@ -623,7 +1305,7 @@ export function startUiServer({
|
|
|
623
1305
|
return;
|
|
624
1306
|
}
|
|
625
1307
|
try {
|
|
626
|
-
const result = getPipelineFiles(root, flowId, flowSource, archived);
|
|
1308
|
+
const result = getPipelineFiles(root, flowId, flowSource, archived, userCtx);
|
|
627
1309
|
if (result.error) {
|
|
628
1310
|
json(res, 404, { error: result.error });
|
|
629
1311
|
return;
|
|
@@ -669,7 +1351,7 @@ export function startUiServer({
|
|
|
669
1351
|
content = String(body);
|
|
670
1352
|
}
|
|
671
1353
|
try {
|
|
672
|
-
const result = getPipelineFiles(root, flowId, flowSource, archived);
|
|
1354
|
+
const result = getPipelineFiles(root, flowId, flowSource, archived, userCtx);
|
|
673
1355
|
if (result.error) {
|
|
674
1356
|
json(res, 404, { error: result.error });
|
|
675
1357
|
return;
|
|
@@ -702,7 +1384,7 @@ export function startUiServer({
|
|
|
702
1384
|
|
|
703
1385
|
if (req.method === "GET" && url.pathname === "/api/ui-context") {
|
|
704
1386
|
try {
|
|
705
|
-
json(res, 200, { workspaceRoot: root });
|
|
1387
|
+
json(res, 200, { workspaceRoot: root, ...uiConfig });
|
|
706
1388
|
} catch (e) {
|
|
707
1389
|
json(res, 500, { error: (e && e.message) || String(e) });
|
|
708
1390
|
}
|
|
@@ -833,15 +1515,108 @@ export function startUiServer({
|
|
|
833
1515
|
return;
|
|
834
1516
|
}
|
|
835
1517
|
|
|
1518
|
+
if (req.method === "GET" && url.pathname === "/api/skillhub/status") {
|
|
1519
|
+
const versionRes = await runSkillhub(["version"], { cwd: root, timeoutMs: 15_000 });
|
|
1520
|
+
const whoRes = await runSkillhub(["whoami"], { cwd: root, timeoutMs: 15_000 });
|
|
1521
|
+
json(res, 200, {
|
|
1522
|
+
available: versionRes.ok,
|
|
1523
|
+
version: versionRes.ok ? versionRes.stdout.trim() : "",
|
|
1524
|
+
loggedIn: whoRes.ok,
|
|
1525
|
+
user: whoRes.ok ? whoRes.stdout.trim() : "",
|
|
1526
|
+
error: versionRes.ok ? "" : versionRes.error,
|
|
1527
|
+
});
|
|
1528
|
+
return;
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
if (req.method === "GET" && url.pathname === "/api/skillhub/list") {
|
|
1532
|
+
const target = url.searchParams.get("target") || "global";
|
|
1533
|
+
const agent = url.searchParams.get("agent") || "codex";
|
|
1534
|
+
const args = ["list", "--json"];
|
|
1535
|
+
if (target === "all") args.push("--all");
|
|
1536
|
+
else if (target === "global") args.push("--global", "--agent", agent);
|
|
1537
|
+
const result = await runSkillhub(args, { cwd: root });
|
|
1538
|
+
if (!result.ok) {
|
|
1539
|
+
json(res, 500, { error: result.error, stdout: result.stdout });
|
|
1540
|
+
return;
|
|
1541
|
+
}
|
|
1542
|
+
json(res, 200, { skills: normalizeSkillhubListPayload(parseJsonText(result.stdout, [])) });
|
|
1543
|
+
return;
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
if (req.method === "GET" && url.pathname === "/api/skillhub/search") {
|
|
1547
|
+
const q = (url.searchParams.get("q") || "").trim();
|
|
1548
|
+
if (!q) {
|
|
1549
|
+
json(res, 200, { total: 0, items: [] });
|
|
1550
|
+
return;
|
|
1551
|
+
}
|
|
1552
|
+
const result = await runSkillhub(["search", "-q", q], { cwd: root });
|
|
1553
|
+
if (!result.ok) {
|
|
1554
|
+
json(res, 500, { error: result.error, stdout: result.stdout });
|
|
1555
|
+
return;
|
|
1556
|
+
}
|
|
1557
|
+
json(res, 200, normalizeSkillhubSearchPayload(parseJsonText(result.stdout, {})));
|
|
1558
|
+
return;
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
if (req.method === "POST" && url.pathname === "/api/skillhub/install") {
|
|
1562
|
+
let payload;
|
|
1563
|
+
try {
|
|
1564
|
+
payload = JSON.parse(await readBody(req));
|
|
1565
|
+
} catch {
|
|
1566
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
1567
|
+
return;
|
|
1568
|
+
}
|
|
1569
|
+
const args = skillhubInstallArgs(payload);
|
|
1570
|
+
if (!args) {
|
|
1571
|
+
json(res, 400, { error: "Missing skill slug or collection" });
|
|
1572
|
+
return;
|
|
1573
|
+
}
|
|
1574
|
+
const result = await runSkillhub(args, { cwd: root, timeoutMs: 180_000, maxBuffer: 4 * 1024 * 1024 });
|
|
1575
|
+
if (!result.ok) {
|
|
1576
|
+
json(res, 500, { error: result.error, stdout: result.stdout });
|
|
1577
|
+
return;
|
|
1578
|
+
}
|
|
1579
|
+
json(res, 200, { ok: true, stdout: result.stdout });
|
|
1580
|
+
return;
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
if (req.method === "POST" && url.pathname === "/api/skillhub/uninstall") {
|
|
1584
|
+
let payload;
|
|
1585
|
+
try {
|
|
1586
|
+
payload = JSON.parse(await readBody(req));
|
|
1587
|
+
} catch {
|
|
1588
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
1589
|
+
return;
|
|
1590
|
+
}
|
|
1591
|
+
const args = skillhubInstallArgs(payload, { uninstall: true });
|
|
1592
|
+
if (!args) {
|
|
1593
|
+
json(res, 400, { error: "Missing skill slug or collection" });
|
|
1594
|
+
return;
|
|
1595
|
+
}
|
|
1596
|
+
const result = await runSkillhub(args, { cwd: root, timeoutMs: 120_000, maxBuffer: 4 * 1024 * 1024 });
|
|
1597
|
+
if (!result.ok) {
|
|
1598
|
+
json(res, 500, { error: result.error, stdout: result.stdout });
|
|
1599
|
+
return;
|
|
1600
|
+
}
|
|
1601
|
+
json(res, 200, { ok: true, stdout: result.stdout });
|
|
1602
|
+
return;
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
if (req.method === "POST" && url.pathname === "/api/skillhub/update") {
|
|
1606
|
+
const result = await runSkillhub(["update"], { cwd: root, timeoutMs: 180_000, maxBuffer: 4 * 1024 * 1024 });
|
|
1607
|
+
if (!result.ok) {
|
|
1608
|
+
json(res, 500, { error: result.error, stdout: result.stdout });
|
|
1609
|
+
return;
|
|
1610
|
+
}
|
|
1611
|
+
json(res, 200, { ok: true, stdout: result.stdout });
|
|
1612
|
+
return;
|
|
1613
|
+
}
|
|
1614
|
+
|
|
836
1615
|
if (req.method === "GET" && url.pathname === "/api/nodes") {
|
|
837
1616
|
const flowId = url.searchParams.get("flowId");
|
|
838
1617
|
const flowSource = url.searchParams.get("flowSource") || "user";
|
|
839
1618
|
const lang = url.searchParams.get("lang") || "en";
|
|
840
|
-
if (!
|
|
841
|
-
json(res, 400, { error: "Missing flowId" });
|
|
842
|
-
return;
|
|
843
|
-
}
|
|
844
|
-
if (!isValidFlowSourceRead(flowSource)) {
|
|
1619
|
+
if (flowId && !isValidFlowSourceRead(flowSource)) {
|
|
845
1620
|
json(res, 400, { error: "Invalid flowSource" });
|
|
846
1621
|
return;
|
|
847
1622
|
}
|
|
@@ -849,7 +1624,60 @@ export function startUiServer({
|
|
|
849
1624
|
try {
|
|
850
1625
|
const { setLanguage } = await import("./i18n.mjs");
|
|
851
1626
|
setLanguage(lang);
|
|
852
|
-
json(res, 200, listNodesJson(root, flowId, flowSource, { archived: nodesArchived }));
|
|
1627
|
+
json(res, 200, listNodesJson(root, flowId || "", flowId ? flowSource : "", { archived: nodesArchived, ...userCtx }));
|
|
1628
|
+
} catch (e) {
|
|
1629
|
+
json(res, 500, { error: (e && e.message) || String(e) });
|
|
1630
|
+
}
|
|
1631
|
+
return;
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
if (req.method === "GET" && url.pathname === "/api/nodes/detail") {
|
|
1635
|
+
const nodeId = url.searchParams.get("id") || "";
|
|
1636
|
+
const flowId = url.searchParams.get("flowId") || "";
|
|
1637
|
+
const flowSource = url.searchParams.get("flowSource") || "";
|
|
1638
|
+
if (!nodeId) {
|
|
1639
|
+
json(res, 400, { error: "Missing node id" });
|
|
1640
|
+
return;
|
|
1641
|
+
}
|
|
1642
|
+
if (flowId && !isValidFlowSourceRead(flowSource || "user")) {
|
|
1643
|
+
json(res, 400, { error: "Invalid flowSource" });
|
|
1644
|
+
return;
|
|
1645
|
+
}
|
|
1646
|
+
const archived = url.searchParams.get("archived") === "1";
|
|
1647
|
+
try {
|
|
1648
|
+
const detail = readNodeDetailJson(root, nodeId, flowId, flowId ? (flowSource || "user") : "", { archived, ...userCtx });
|
|
1649
|
+
if (detail.error) {
|
|
1650
|
+
json(res, 404, { error: detail.error });
|
|
1651
|
+
return;
|
|
1652
|
+
}
|
|
1653
|
+
json(res, 200, detail);
|
|
1654
|
+
} catch (e) {
|
|
1655
|
+
json(res, 500, { error: (e && e.message) || String(e) });
|
|
1656
|
+
}
|
|
1657
|
+
return;
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
if (req.method === "GET" && url.pathname === "/api/nodes/file") {
|
|
1661
|
+
const nodeId = url.searchParams.get("id") || "";
|
|
1662
|
+
const relPath = url.searchParams.get("path") || "";
|
|
1663
|
+
const flowId = url.searchParams.get("flowId") || "";
|
|
1664
|
+
const flowSource = url.searchParams.get("flowSource") || "";
|
|
1665
|
+
if (!nodeId || !relPath) {
|
|
1666
|
+
json(res, 400, { error: "Missing node id or path" });
|
|
1667
|
+
return;
|
|
1668
|
+
}
|
|
1669
|
+
if (flowId && !isValidFlowSourceRead(flowSource || "user")) {
|
|
1670
|
+
json(res, 400, { error: "Invalid flowSource" });
|
|
1671
|
+
return;
|
|
1672
|
+
}
|
|
1673
|
+
const archived = url.searchParams.get("archived") === "1";
|
|
1674
|
+
try {
|
|
1675
|
+
const file = readNodeFilePreview(root, nodeId, relPath, flowId, flowId ? (flowSource || "user") : "", { archived, ...userCtx });
|
|
1676
|
+
if (file.error) {
|
|
1677
|
+
json(res, 404, { error: file.error });
|
|
1678
|
+
return;
|
|
1679
|
+
}
|
|
1680
|
+
json(res, 200, file);
|
|
853
1681
|
} catch (e) {
|
|
854
1682
|
json(res, 500, { error: (e && e.message) || String(e) });
|
|
855
1683
|
}
|
|
@@ -890,7 +1718,7 @@ export function startUiServer({
|
|
|
890
1718
|
return;
|
|
891
1719
|
}
|
|
892
1720
|
try {
|
|
893
|
-
const resolved = resolveFlowDirForWrite(root, flowId, flowSource);
|
|
1721
|
+
const resolved = resolveFlowDirForWrite(root, flowId, flowSource, userCtx);
|
|
894
1722
|
if (resolved.error || !resolved.flowDir) {
|
|
895
1723
|
json(res, 400, { error: resolved.error || "Could not resolve flow directory" });
|
|
896
1724
|
return;
|
|
@@ -916,7 +1744,7 @@ export function startUiServer({
|
|
|
916
1744
|
const flowSource = payload?.flowSource || "user";
|
|
917
1745
|
let flowDir = "";
|
|
918
1746
|
if (flowId && isValidFlowSourceWrite(flowSource)) {
|
|
919
|
-
const resolved = resolveFlowDirForWrite(root, flowId, flowSource);
|
|
1747
|
+
const resolved = resolveFlowDirForWrite(root, flowId, flowSource, userCtx);
|
|
920
1748
|
if (!resolved.error && resolved.flowDir) flowDir = resolved.flowDir;
|
|
921
1749
|
}
|
|
922
1750
|
const result = publishNodeFromInstance(root, payload || {}, { flowDir });
|
|
@@ -939,7 +1767,7 @@ export function startUiServer({
|
|
|
939
1767
|
return;
|
|
940
1768
|
}
|
|
941
1769
|
const flowArchived = url.searchParams.get("archived") === "1";
|
|
942
|
-
const result = readFlowJson(root, flowId, flowSource, { archived: flowArchived });
|
|
1770
|
+
const result = readFlowJson(root, flowId, flowSource, { archived: flowArchived, ...userCtx });
|
|
943
1771
|
if (result.error) {
|
|
944
1772
|
json(res, 404, result);
|
|
945
1773
|
return;
|
|
@@ -965,7 +1793,7 @@ export function startUiServer({
|
|
|
965
1793
|
json(res, 400, { error: "Missing runUuid, instanceId, or content" });
|
|
966
1794
|
return;
|
|
967
1795
|
}
|
|
968
|
-
const runDir = path.join(getRunDir(root, payload.flowId || "unknown", runUuid));
|
|
1796
|
+
const runDir = path.join(getRunDir(root, payload.flowId || "unknown", runUuid, userCtx));
|
|
969
1797
|
const outputPath = path.join(runDir, `output/${instanceId}/node_${instanceId}_content.md`);
|
|
970
1798
|
try {
|
|
971
1799
|
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
@@ -1000,7 +1828,7 @@ ${content}
|
|
|
1000
1828
|
|
|
1001
1829
|
const opencodeCmd = process.env.OPENCODE_CMD || "opencode";
|
|
1002
1830
|
const tmpPromptFile = path.join(
|
|
1003
|
-
getRunDir(root, payload.flowId || "unknown", runUuid),
|
|
1831
|
+
getRunDir(root, payload.flowId || "unknown", runUuid, userCtx),
|
|
1004
1832
|
"intermediate",
|
|
1005
1833
|
`${instanceId}_ai_edit_prompt.txt`,
|
|
1006
1834
|
);
|
|
@@ -1045,7 +1873,7 @@ ${content}
|
|
|
1045
1873
|
json(res, 400, { error: "Missing runUuid or instanceId" });
|
|
1046
1874
|
return;
|
|
1047
1875
|
}
|
|
1048
|
-
const runDir = path.join(getRunDir(root, payload.flowId || "unknown", runUuid));
|
|
1876
|
+
const runDir = path.join(getRunDir(root, payload.flowId || "unknown", runUuid, userCtx));
|
|
1049
1877
|
const resultPath = path.join(runDir, `intermediate/${instanceId}/${instanceId}.result.md`);
|
|
1050
1878
|
try {
|
|
1051
1879
|
fs.mkdirSync(path.dirname(resultPath), { recursive: true });
|
|
@@ -1075,7 +1903,7 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
1075
1903
|
json(res, 400, { error: "Missing runUuid, instanceId, or branch" });
|
|
1076
1904
|
return;
|
|
1077
1905
|
}
|
|
1078
|
-
const runDir = path.join(getRunDir(root, payload.flowId || "unknown", runUuid));
|
|
1906
|
+
const runDir = path.join(getRunDir(root, payload.flowId || "unknown", runUuid, userCtx));
|
|
1079
1907
|
const resultPath = path.join(runDir, `intermediate/${instanceId}/${instanceId}.result.md`);
|
|
1080
1908
|
try {
|
|
1081
1909
|
fs.mkdirSync(path.dirname(resultPath), { recursive: true });
|
|
@@ -1123,7 +1951,7 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
1123
1951
|
return;
|
|
1124
1952
|
}
|
|
1125
1953
|
const flowArchived = Boolean(payload.flowArchived);
|
|
1126
|
-
const result = writeFlowYaml(root, flowId, flowSource, flowYaml, { archived: flowArchived });
|
|
1954
|
+
const result = writeFlowYaml(root, flowId, flowSource, flowYaml, { archived: flowArchived, ...userCtx });
|
|
1127
1955
|
if (!result.success) {
|
|
1128
1956
|
json(res, 400, result);
|
|
1129
1957
|
return;
|
|
@@ -1151,7 +1979,7 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
1151
1979
|
return;
|
|
1152
1980
|
}
|
|
1153
1981
|
const flowArchived = Boolean(payload.flowArchived);
|
|
1154
|
-
broadcastFlowEditorSync(flowId, flowSource, flowArchived);
|
|
1982
|
+
broadcastFlowEditorSync(flowId, flowSource, flowArchived, userCtx.userId);
|
|
1155
1983
|
json(res, 200, { ok: true });
|
|
1156
1984
|
return;
|
|
1157
1985
|
}
|
|
@@ -1168,7 +1996,7 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
1168
1996
|
return;
|
|
1169
1997
|
}
|
|
1170
1998
|
const flowArchived = url.searchParams.get("archived") === "1";
|
|
1171
|
-
const key = flowEditorSyncKey(flowId, flowSource, flowArchived);
|
|
1999
|
+
const key = flowEditorSyncKey(flowId, flowSource, flowArchived, userCtx.userId);
|
|
1172
2000
|
let set = flowEditorSyncSubscribers.get(key);
|
|
1173
2001
|
if (!set) {
|
|
1174
2002
|
set = new Set();
|
|
@@ -1202,7 +2030,7 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
1202
2030
|
return;
|
|
1203
2031
|
}
|
|
1204
2032
|
const flowArchived = url.searchParams.get("archived") === "1";
|
|
1205
|
-
const key = flowEditorSyncKey(flowId, flowSource, flowArchived);
|
|
2033
|
+
const key = flowEditorSyncKey(flowId, flowSource, flowArchived, userCtx.userId);
|
|
1206
2034
|
const serverVer = flowEditorSyncVersions.get(key) ?? 0;
|
|
1207
2035
|
const clientVer = parseInt(url.searchParams.get("v") ?? "0", 10) || 0;
|
|
1208
2036
|
json(res, 200, { version: serverVer, changed: serverVer > clientVer });
|
|
@@ -1232,7 +2060,7 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
1232
2060
|
json(res, 400, { error: "Invalid toSource" });
|
|
1233
2061
|
return;
|
|
1234
2062
|
}
|
|
1235
|
-
const result = moveFlowDirectory(root, flowId.trim(), fromSource, toSource);
|
|
2063
|
+
const result = moveFlowDirectory(root, flowId.trim(), fromSource, toSource, userCtx);
|
|
1236
2064
|
if (!result.success) {
|
|
1237
2065
|
json(res, 400, { error: result.error || "Move failed" });
|
|
1238
2066
|
return;
|
|
@@ -1269,7 +2097,7 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
1269
2097
|
json(res, 200, { success: true, flowId, flowSource });
|
|
1270
2098
|
return;
|
|
1271
2099
|
}
|
|
1272
|
-
const yamlRes = getFlowYamlAbs(root, flowId, flowSource, { archived: false });
|
|
2100
|
+
const yamlRes = getFlowYamlAbs(root, flowId, flowSource, { archived: false, ...userCtx });
|
|
1273
2101
|
if (yamlRes.error || !yamlRes.path) {
|
|
1274
2102
|
json(res, 404, { error: yamlRes.error || "找不到流水线" });
|
|
1275
2103
|
return;
|
|
@@ -1312,7 +2140,7 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
1312
2140
|
json(res, 400, { error: "仅支持归档用户目录或工作区流水线" });
|
|
1313
2141
|
return;
|
|
1314
2142
|
}
|
|
1315
|
-
const result = archiveFlowPipeline(root, flowId, flowSource);
|
|
2143
|
+
const result = archiveFlowPipeline(root, flowId, flowSource, userCtx);
|
|
1316
2144
|
if (!result.success) {
|
|
1317
2145
|
json(res, 400, { error: result.error || "归档失败" });
|
|
1318
2146
|
return;
|
|
@@ -1345,7 +2173,7 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
1345
2173
|
json(res, 400, { error: "仅支持删除用户目录或工作区流水线" });
|
|
1346
2174
|
return;
|
|
1347
2175
|
}
|
|
1348
|
-
const result = deleteFlowPipeline(root, flowId, flowSource, { archived: flowArchived });
|
|
2176
|
+
const result = deleteFlowPipeline(root, flowId, flowSource, { archived: flowArchived, ...userCtx });
|
|
1349
2177
|
if (!result.success) {
|
|
1350
2178
|
json(res, 400, { error: result.error || "删除失败" });
|
|
1351
2179
|
return;
|
|
@@ -1366,7 +2194,7 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
1366
2194
|
json(res, 400, { error: "Invalid flowSource" });
|
|
1367
2195
|
return;
|
|
1368
2196
|
}
|
|
1369
|
-
const yamlRes = getFlowYamlAbs(root, flowId, flowSource, { archived: flowArchived });
|
|
2197
|
+
const yamlRes = getFlowYamlAbs(root, flowId, flowSource, { archived: flowArchived, ...userCtx });
|
|
1370
2198
|
if (yamlRes.error) {
|
|
1371
2199
|
json(res, 404, { error: yamlRes.error });
|
|
1372
2200
|
return;
|
|
@@ -1407,7 +2235,7 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
1407
2235
|
json(res, 400, { error: "Cannot save config to builtin or archived flow" });
|
|
1408
2236
|
return;
|
|
1409
2237
|
}
|
|
1410
|
-
const yamlRes = getFlowYamlAbs(root, flowId, flowSource, { archived: flowArchived });
|
|
2238
|
+
const yamlRes = getFlowYamlAbs(root, flowId, flowSource, { archived: flowArchived, ...userCtx });
|
|
1411
2239
|
if (yamlRes.error) {
|
|
1412
2240
|
json(res, 404, { error: yamlRes.error });
|
|
1413
2241
|
return;
|
|
@@ -1437,12 +2265,12 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
1437
2265
|
json(res, 400, { error: "Invalid flowSource" });
|
|
1438
2266
|
return;
|
|
1439
2267
|
}
|
|
1440
|
-
const result = readFlowSchedule(root, flowId, flowSource, { archived: flowArchived });
|
|
2268
|
+
const result = readFlowSchedule(root, flowId, flowSource, { archived: flowArchived, ...userCtx });
|
|
1441
2269
|
if (!result.success) {
|
|
1442
2270
|
json(res, 400, { error: result.error || "Could not read schedule" });
|
|
1443
2271
|
return;
|
|
1444
2272
|
}
|
|
1445
|
-
const status = listScheduleStatuses(root).find(
|
|
2273
|
+
const status = listScheduleStatuses(root, userCtx).find(
|
|
1446
2274
|
(s) => s.flowId === flowId && (s.flowSource || "user") === (flowSource || "user"),
|
|
1447
2275
|
);
|
|
1448
2276
|
json(res, 200, { schedule: result.schedule, state: result.state || {}, status: status || null });
|
|
@@ -1468,7 +2296,7 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
1468
2296
|
json(res, 400, { error: "Cannot save schedule to builtin or archived flow" });
|
|
1469
2297
|
return;
|
|
1470
2298
|
}
|
|
1471
|
-
const result = writeFlowSchedule(root, flowId, flowSource, payload.schedule || {});
|
|
2299
|
+
const result = writeFlowSchedule(root, flowId, flowSource, payload.schedule || {}, userCtx);
|
|
1472
2300
|
if (!result.success) {
|
|
1473
2301
|
json(res, 400, { error: result.error || "Could not save schedule" });
|
|
1474
2302
|
return;
|
|
@@ -1491,7 +2319,8 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
1491
2319
|
return;
|
|
1492
2320
|
}
|
|
1493
2321
|
const runUuid = typeof payload.uuid === "string" ? payload.uuid.trim() : "";
|
|
1494
|
-
|
|
2322
|
+
const runKey = `${userCtx.userId || ""}:${payload.flowSource || "user"}:${flowId}`;
|
|
2323
|
+
if (activeFlowRuns.has(runKey)) {
|
|
1495
2324
|
json(res, 409, { error: "该流水线已在运行中" });
|
|
1496
2325
|
return;
|
|
1497
2326
|
}
|
|
@@ -1500,7 +2329,7 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
1500
2329
|
// UI 轮询会把 runMode 翻回 stopped,即便 CLI 正在运行也显示 PAUSED。
|
|
1501
2330
|
if (runUuid) {
|
|
1502
2331
|
try {
|
|
1503
|
-
const runDir = getRunDir(root, flowId, runUuid);
|
|
2332
|
+
const runDir = getRunDir(root, flowId, runUuid, userCtx);
|
|
1504
2333
|
const interruptedPath = path.join(runDir, RUN_INTERRUPTED_FILENAME);
|
|
1505
2334
|
if (fs.existsSync(interruptedPath)) fs.unlinkSync(interruptedPath);
|
|
1506
2335
|
} catch (e) {
|
|
@@ -1542,7 +2371,7 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
1542
2371
|
const endSafe = () => {
|
|
1543
2372
|
if (responseEnded) return;
|
|
1544
2373
|
responseEnded = true;
|
|
1545
|
-
activeFlowRuns.delete(
|
|
2374
|
+
activeFlowRuns.delete(runKey);
|
|
1546
2375
|
try {
|
|
1547
2376
|
res.end();
|
|
1548
2377
|
} catch (_) {}
|
|
@@ -1557,7 +2386,7 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
1557
2386
|
child = spawn(process.execPath, args, {
|
|
1558
2387
|
cwd: root,
|
|
1559
2388
|
stdio: ["ignore", "pipe", "pipe"],
|
|
1560
|
-
env: { ...process.env, FORCE_COLOR: "0" },
|
|
2389
|
+
env: { ...process.env, FORCE_COLOR: "0", AGENTFLOW_USER_ID: userCtx.userId || "" },
|
|
1561
2390
|
// detached: true 使 child 成为新进程组 leader,/api/flow/run/stop 时
|
|
1562
2391
|
// 用 process.kill(-pid) 可以一次性 SIGTERM 整棵进程树(含 cursor-agent 等孙进程)
|
|
1563
2392
|
detached: true,
|
|
@@ -1570,7 +2399,7 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
1570
2399
|
|
|
1571
2400
|
/** @type {{ child: import("child_process").ChildProcess, runUuid: string | null }} */
|
|
1572
2401
|
const runEntry = { child, runUuid: runUuid || null };
|
|
1573
|
-
activeFlowRuns.set(
|
|
2402
|
+
activeFlowRuns.set(runKey, runEntry);
|
|
1574
2403
|
log.debug(`[ui] flow/run: spawned pid=${child.pid} flowId=${flowId}${runUuid ? ` uuid=${runUuid}` : ""}`);
|
|
1575
2404
|
|
|
1576
2405
|
let stdoutBuf = "";
|
|
@@ -1643,7 +2472,8 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
1643
2472
|
json(res, 400, { error: "Missing flowId" });
|
|
1644
2473
|
return;
|
|
1645
2474
|
}
|
|
1646
|
-
const
|
|
2475
|
+
const runKey = `${userCtx.userId || ""}:${payload.flowSource || "user"}:${flowId}`;
|
|
2476
|
+
const entry = activeFlowRuns.get(runKey);
|
|
1647
2477
|
if (!entry || !entry.child) {
|
|
1648
2478
|
json(res, 404, { error: "该流水线未在运行" });
|
|
1649
2479
|
return;
|
|
@@ -1661,10 +2491,10 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
1661
2491
|
try { entry.child.kill("SIGTERM"); } catch (_) {}
|
|
1662
2492
|
}
|
|
1663
2493
|
const uuid = entry.runUuid;
|
|
1664
|
-
activeFlowRuns.delete(
|
|
2494
|
+
activeFlowRuns.delete(runKey);
|
|
1665
2495
|
if (uuid) {
|
|
1666
2496
|
try {
|
|
1667
|
-
const runDir = getRunDir(root, flowId, uuid);
|
|
2497
|
+
const runDir = getRunDir(root, flowId, uuid, userCtx);
|
|
1668
2498
|
fs.mkdirSync(runDir, { recursive: true });
|
|
1669
2499
|
fs.writeFileSync(
|
|
1670
2500
|
path.join(runDir, RUN_INTERRUPTED_FILENAME),
|
|
@@ -1684,6 +2514,17 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
1684
2514
|
return;
|
|
1685
2515
|
}
|
|
1686
2516
|
|
|
2517
|
+
if (req.method === "GET" && url.pathname === "/api/skills/detail") {
|
|
2518
|
+
const key = url.searchParams.get("key") || url.searchParams.get("name") || "";
|
|
2519
|
+
const detail = readComposerSkillDetail(PACKAGE_ROOT, root, key);
|
|
2520
|
+
if (!detail) {
|
|
2521
|
+
json(res, 404, { error: "Skill not found" });
|
|
2522
|
+
return;
|
|
2523
|
+
}
|
|
2524
|
+
json(res, 200, { skill: detail });
|
|
2525
|
+
return;
|
|
2526
|
+
}
|
|
2527
|
+
|
|
1687
2528
|
if (req.method === "POST" && url.pathname === "/api/composer-agent") {
|
|
1688
2529
|
let payload;
|
|
1689
2530
|
try {
|
|
@@ -1739,7 +2580,7 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
1739
2580
|
return;
|
|
1740
2581
|
}
|
|
1741
2582
|
const flowArchived = Boolean(payload.flowArchived);
|
|
1742
|
-
const yamlRes = getFlowYamlAbs(root, flowId, flowSource, { archived: flowArchived });
|
|
2583
|
+
const yamlRes = getFlowYamlAbs(root, flowId, flowSource, { archived: flowArchived, ...userCtx });
|
|
1743
2584
|
if (yamlRes.error || !yamlRes.path) {
|
|
1744
2585
|
json(res, 400, { error: yamlRes.error || "Could not resolve flow.yaml" });
|
|
1745
2586
|
return;
|
|
@@ -1750,7 +2591,7 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
1750
2591
|
let editorSyncFlowSource = flowSource;
|
|
1751
2592
|
let flowDirForCli = path.dirname(flowYamlAbs);
|
|
1752
2593
|
if (flowSource === "builtin") {
|
|
1753
|
-
const w = resolveFlowDirForWrite(root, flowId, "workspace");
|
|
2594
|
+
const w = resolveFlowDirForWrite(root, flowId, "workspace", userCtx);
|
|
1754
2595
|
if (w.error || !w.flowDir) {
|
|
1755
2596
|
json(res, 400, { error: w.error || "Could not resolve workspace flow directory" });
|
|
1756
2597
|
return;
|
|
@@ -1783,6 +2624,7 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
1783
2624
|
flowYamlAbs,
|
|
1784
2625
|
flowId,
|
|
1785
2626
|
flowSource,
|
|
2627
|
+
userId: userCtx.userId || "",
|
|
1786
2628
|
intents: multiStepIntents,
|
|
1787
2629
|
canvasInstanceIds: instanceIds,
|
|
1788
2630
|
skillsHint: multiStepResources.skillsHint,
|
|
@@ -1939,6 +2781,7 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
1939
2781
|
thread,
|
|
1940
2782
|
phaseContext,
|
|
1941
2783
|
phaseRole: phaseRole || undefined,
|
|
2784
|
+
agentflowUserId: userCtx.userId || "",
|
|
1942
2785
|
force: true,
|
|
1943
2786
|
onStreamEvent,
|
|
1944
2787
|
});
|
|
@@ -1952,7 +2795,7 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
1952
2795
|
flowSource: flowSource || null,
|
|
1953
2796
|
});
|
|
1954
2797
|
if (flowId && flowSource) {
|
|
1955
|
-
broadcastFlowEditorSync(flowId, flowSource, Boolean(payload.flowArchived));
|
|
2798
|
+
broadcastFlowEditorSync(flowId, flowSource, Boolean(payload.flowArchived), userCtx.userId);
|
|
1956
2799
|
}
|
|
1957
2800
|
try { res.write(JSON.stringify({ type: "done" }) + "\n"); } catch (_) {}
|
|
1958
2801
|
}
|
|
@@ -1989,6 +2832,7 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
1989
2832
|
cliWorkspace,
|
|
1990
2833
|
prompt: finalPrompt,
|
|
1991
2834
|
modelKey: typeof model === "string" ? model.trim() : "",
|
|
2835
|
+
agentflowUserId: userCtx.userId || "",
|
|
1992
2836
|
onStreamEvent,
|
|
1993
2837
|
});
|
|
1994
2838
|
child = handle.child;
|
|
@@ -2007,6 +2851,7 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
2007
2851
|
flowYamlAbs,
|
|
2008
2852
|
flowContext: flowContextForMultiStep,
|
|
2009
2853
|
modelKey: typeof model === "string" ? model.trim() : "",
|
|
2854
|
+
agentflowUserId: userCtx.userId || "",
|
|
2010
2855
|
force: true,
|
|
2011
2856
|
onStreamEvent,
|
|
2012
2857
|
getAborted: () => clientDisconnected || responseEnded,
|
|
@@ -2029,7 +2874,7 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
2029
2874
|
flowSource: flowSource || null,
|
|
2030
2875
|
});
|
|
2031
2876
|
if (flowYamlChanged && flowId && flowSource) {
|
|
2032
|
-
broadcastFlowEditorSync(flowId, flowSource, Boolean(payload.flowArchived));
|
|
2877
|
+
broadcastFlowEditorSync(flowId, flowSource, Boolean(payload.flowArchived), userCtx.userId);
|
|
2033
2878
|
}
|
|
2034
2879
|
try { res.write(JSON.stringify({ type: "done" }) + "\n"); } catch (_) {}
|
|
2035
2880
|
}
|