@fieldwangai/agentflow 0.1.29 → 0.1.31

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.
Files changed (69) hide show
  1. package/agents/agentflow-node-executor-code.md +3 -2
  2. package/agents/agentflow-node-executor-planning.md +3 -2
  3. package/agents/agentflow-node-executor-requirement.md +3 -2
  4. package/agents/agentflow-node-executor-test.md +3 -2
  5. package/agents/agentflow-node-executor-ui.md +3 -2
  6. package/agents/agentflow-node-executor.md +3 -2
  7. package/agents/en/agentflow-node-executor.md +3 -2
  8. package/agents/zh/agentflow-node-executor.md +3 -2
  9. package/bin/lib/agent-runners.mjs +63 -14
  10. package/bin/lib/api-runner.mjs +30 -4
  11. package/bin/lib/apply.mjs +6 -5
  12. package/bin/lib/auth.mjs +240 -0
  13. package/bin/lib/catalog-agents.mjs +2 -2
  14. package/bin/lib/catalog-flows.mjs +196 -17
  15. package/bin/lib/composer-agent.mjs +22 -1
  16. package/bin/lib/composer-skill-router.mjs +10 -78
  17. package/bin/lib/flow-import.mjs +2 -2
  18. package/bin/lib/flow-write.mjs +20 -20
  19. package/bin/lib/help.mjs +2 -2
  20. package/bin/lib/locales/en.json +29 -1
  21. package/bin/lib/locales/zh.json +31 -3
  22. package/bin/lib/main.mjs +6 -1
  23. package/bin/lib/node-exec-context.mjs +5 -5
  24. package/bin/lib/node-execute.mjs +15 -10
  25. package/bin/lib/paths.mjs +69 -13
  26. package/bin/lib/recent-runs.mjs +2 -2
  27. package/bin/lib/run-node-statuses-from-disk.mjs +3 -3
  28. package/bin/lib/runtime-context.mjs +225 -0
  29. package/bin/lib/scheduler.mjs +42 -38
  30. package/bin/lib/skill-registry.mjs +145 -0
  31. package/bin/lib/ui-server.mjs +1517 -57
  32. package/bin/lib/user-env.mjs +83 -0
  33. package/bin/lib/workspace-tree.mjs +4 -3
  34. package/bin/lib/workspace.mjs +9 -11
  35. package/bin/pipeline/build-node-prompt.mjs +29 -4
  36. package/bin/pipeline/get-env.mjs +5 -29
  37. package/bin/pipeline/get-exec-id.mjs +2 -2
  38. package/bin/pipeline/get-resolved-values.mjs +1 -0
  39. package/bin/pipeline/pre-process-node.mjs +328 -6
  40. package/bin/pipeline/run-tool-nodejs.mjs +7 -0
  41. package/bin/pipeline/validate-flow.mjs +2 -0
  42. package/builtin/nodes/agent_subAgent.md +12 -3
  43. package/builtin/nodes/control_cd_workspace.md +45 -0
  44. package/builtin/nodes/control_load_skills.md +50 -0
  45. package/builtin/nodes/control_user_workspace.md +20 -0
  46. package/builtin/nodes/display_ascii.md +22 -0
  47. package/builtin/nodes/display_markdown.md +22 -0
  48. package/builtin/nodes/display_mermaid.md +22 -0
  49. package/builtin/nodes/tool_git_checkout.md +57 -0
  50. package/builtin/nodes/tool_nodejs.md +8 -1
  51. package/builtin/nodes/tool_print.md +4 -1
  52. package/builtin/web-ui/dist/assets/index-BVWwQpvg.css +1 -0
  53. package/builtin/web-ui/dist/assets/index-CvNy1n3f.js +197 -0
  54. package/builtin/web-ui/dist/index.html +2 -2
  55. package/package.json +1 -1
  56. package/skills/agentflow-flow-recipes/SKILL.md +24 -0
  57. package/skills/agentflow-flow-recipes/references/recipes.md +63 -0
  58. package/skills/agentflow-node-reference/SKILL.md +25 -0
  59. package/skills/agentflow-node-reference/references/builtin-nodes.md +210 -0
  60. package/skills/agentflow-placeholder-reference/SKILL.md +24 -0
  61. package/skills/agentflow-placeholder-reference/references/placeholders.md +20 -0
  62. package/skills/agentflow-runtime-reference/SKILL.md +25 -0
  63. package/skills/agentflow-runtime-reference/references/runtime.md +64 -0
  64. package/skills/agentflow-workspace-ascii/SKILL.md +42 -0
  65. package/skills/agentflow-workspace-graph/SKILL.md +67 -0
  66. package/skills/agentflow-workspace-markdown/SKILL.md +44 -0
  67. package/skills/agentflow-workspace-mermaid/SKILL.md +43 -0
  68. package/builtin/web-ui/dist/assets/index-0vJxkTJz.css +0 -1
  69. package/builtin/web-ui/dist/assets/index-h69bpxLI.js +0 -190
@@ -7,11 +7,19 @@
7
7
  */
8
8
  import fs from "fs";
9
9
  import http from "http";
10
+ import os from "os";
10
11
  import path from "path";
11
- import { spawn } from "child_process";
12
+ import { execFile, spawn } from "child_process";
12
13
  import busboy from "busboy";
13
14
  import { log } from "./log.mjs";
14
- import { getFlowYamlAbs, listFlowsJson, listNodesJson, readFlowJson } from "./catalog-flows.mjs";
15
+ import {
16
+ getFlowYamlAbs,
17
+ listFlowsJson,
18
+ listNodesJson,
19
+ readFlowJson,
20
+ readNodeDetailJson,
21
+ readNodeFilePreview,
22
+ } from "./catalog-flows.mjs";
15
23
  import {
16
24
  FLOW_YAML_FILENAME,
17
25
  archiveFlowPipeline,
@@ -33,6 +41,7 @@ import { t } from "./i18n.mjs";
33
41
  import {
34
42
  PACKAGE_ROOT,
35
43
  getAgentflowUserConfigAbs,
44
+ getAgentflowUserDataRoot,
36
45
  getModelListsAbs,
37
46
  getRunDir,
38
47
  } from "./paths.mjs";
@@ -42,6 +51,7 @@ import {
42
51
  loadResourcesForIntents,
43
52
  loadResourcesForSkillKeys,
44
53
  listComposerSkills,
54
+ readComposerSkillDetail,
45
55
  buildSkillInjectionBlock,
46
56
  buildSkillCompactInjectionBlock,
47
57
  } from "./composer-skill-router.mjs";
@@ -65,6 +75,15 @@ import { runNodeScript } from "./pipeline-scripts.mjs";
65
75
  import { readFlowSchedule, writeFlowSchedule } from "./schedule-config.mjs";
66
76
  import { listScheduleStatuses } from "./scheduler.mjs";
67
77
  import { installFlowDependency, listMarketplacePackages, publishNodeFromInstance } from "./marketplace.mjs";
78
+ import {
79
+ authSetupRequired,
80
+ buildClearSessionCookie,
81
+ buildSessionCookie,
82
+ getAuthUserFromRequest,
83
+ loginOrCreateUser,
84
+ logoutRequest,
85
+ } from "./auth.mjs";
86
+ import { readUserEnvObject, readUserEnvRows, writeUserEnvRows } from "./user-env.mjs";
68
87
 
69
88
  const MIME = {
70
89
  ".html": "text/html; charset=utf-8",
@@ -76,6 +95,49 @@ const MIME = {
76
95
  };
77
96
 
78
97
  const RUN_CONFIG_FILENAME = "run-config.json";
98
+ const SKILL_COLLECTIONS_FILENAME = "skill-collections.json";
99
+ const BUILTIN_SKILL_COLLECTIONS = [
100
+ {
101
+ id: "pipeline",
102
+ name: "Pipeline",
103
+ defaultKeys: [
104
+ "agentflow-flow-add-instances",
105
+ "agentflow-flow-edit-node-fields",
106
+ "agentflow-flow-recipes",
107
+ "agentflow-flow-sync-ui",
108
+ "agentflow-node-reference",
109
+ "agentflow-placeholder-reference",
110
+ "agentflow-runtime-reference",
111
+ ],
112
+ },
113
+ {
114
+ id: "workspace",
115
+ name: "Workspace",
116
+ defaultKeys: [
117
+ "agentflow-workspace-graph",
118
+ "agentflow-workspace-markdown",
119
+ "agentflow-workspace-mermaid",
120
+ "agentflow-workspace-ascii",
121
+ "agentflow-node-reference",
122
+ "agentflow-placeholder-reference",
123
+ "agentflow-runtime-reference",
124
+ ],
125
+ legacyDefaultKeys: [
126
+ [
127
+ "agentflow-flow-add-instances",
128
+ "agentflow-flow-edit-node-fields",
129
+ "agentflow-node-reference",
130
+ "agentflow-placeholder-reference",
131
+ "agentflow-runtime-reference",
132
+ ],
133
+ [
134
+ "agentflow-node-reference",
135
+ "agentflow-placeholder-reference",
136
+ "agentflow-runtime-reference",
137
+ ],
138
+ ],
139
+ },
140
+ ];
79
141
 
80
142
  function json(res, status, obj) {
81
143
  const body = JSON.stringify(obj);
@@ -86,6 +148,155 @@ function json(res, status, obj) {
86
148
  res.end(body);
87
149
  }
88
150
 
151
+ function skillCollectionsAbs(userCtx = {}) {
152
+ return path.join(getAgentflowUserDataRoot(userCtx.userId), SKILL_COLLECTIONS_FILENAME);
153
+ }
154
+
155
+ function slugifySkillCollectionId(name, fallback = "collection") {
156
+ const raw = String(name || "").trim().toLowerCase();
157
+ const id = raw
158
+ .replace(/[^a-z0-9_-]+/g, "-")
159
+ .replace(/^-+|-+$/g, "")
160
+ .slice(0, 48);
161
+ return id || fallback;
162
+ }
163
+
164
+ function isBuiltinSkillCollectionId(id) {
165
+ return BUILTIN_SKILL_COLLECTIONS.some((collection) => collection.id === id);
166
+ }
167
+
168
+ function resolveSkillKeys(skillNamesOrKeys = [], availableSkills = []) {
169
+ const byToken = buildSkillKeyLookup(availableSkills);
170
+ return skillNamesOrKeys.map((key) => byToken.get(key)).filter(Boolean);
171
+ }
172
+
173
+ function buildSkillKeyLookup(availableSkills = []) {
174
+ const byToken = new Map();
175
+ for (const skill of availableSkills) {
176
+ const key = String(skill?.key || "").trim();
177
+ if (!key) continue;
178
+ for (const token of [skill.key, skill.name, skill.id]) {
179
+ const normalized = String(token || "").trim();
180
+ if (normalized && !byToken.has(normalized)) byToken.set(normalized, key);
181
+ }
182
+ }
183
+ return byToken;
184
+ }
185
+
186
+ function defaultSkillKeysForCollection(def, availableSkills = []) {
187
+ const exact = resolveSkillKeys(def.defaultKeys, availableSkills);
188
+ if (exact.length > 0) return exact;
189
+ return availableSkills
190
+ .filter((skill) => String(skill?.name || skill?.id || skill?.key || "").includes("agentflow-"))
191
+ .map((skill) => String(skill.key || "").trim())
192
+ .filter(Boolean);
193
+ }
194
+
195
+ function sameSkillKeySet(a = [], b = []) {
196
+ if (a.length !== b.length) return false;
197
+ const set = new Set(a);
198
+ return b.every((key) => set.has(key));
199
+ }
200
+
201
+ function normalizeSkillCollectionConfig(value) {
202
+ const now = Date.now();
203
+ const seenIds = new Set();
204
+ const collections = [];
205
+ const input = Array.isArray(value?.collections) ? value.collections : [];
206
+ for (const item of input) {
207
+ if (!item || typeof item !== "object") continue;
208
+ const name = String(item.name || item.id || "").trim().slice(0, 80);
209
+ if (!name) continue;
210
+ let id = slugifySkillCollectionId(item.id || name);
211
+ let suffix = 2;
212
+ while (seenIds.has(id)) {
213
+ id = `${slugifySkillCollectionId(item.id || name)}-${suffix++}`;
214
+ }
215
+ seenIds.add(id);
216
+ const skillSeen = new Set();
217
+ const skillKeys = [];
218
+ for (const key of Array.isArray(item.skillKeys) ? item.skillKeys : []) {
219
+ const normalized = String(key || "").trim();
220
+ if (!normalized || skillSeen.has(normalized)) continue;
221
+ skillSeen.add(normalized);
222
+ skillKeys.push(normalized);
223
+ }
224
+ collections.push({
225
+ id,
226
+ name,
227
+ skillKeys,
228
+ builtin: Boolean(item.builtin) || isBuiltinSkillCollectionId(id),
229
+ createdAt: Number.isFinite(item.createdAt) ? Number(item.createdAt) : now,
230
+ updatedAt: Number.isFinite(item.updatedAt) ? Number(item.updatedAt) : now,
231
+ });
232
+ }
233
+ return { version: 1, collections };
234
+ }
235
+
236
+ function withBuiltinSkillCollections(config, availableSkills = []) {
237
+ const normalized = normalizeSkillCollectionConfig(config);
238
+ const byId = new Map(normalized.collections.map((collection) => [collection.id, collection]));
239
+ const out = [];
240
+ const now = Date.now();
241
+ for (const def of BUILTIN_SKILL_COLLECTIONS) {
242
+ const existing = byId.get(def.id);
243
+ if (existing) {
244
+ const nextDefaultKeys = defaultSkillKeysForCollection(def, availableSkills);
245
+ const legacyDefaultSets = (Array.isArray(def.legacyDefaultKeys) ? def.legacyDefaultKeys : [])
246
+ .map((keys) => Array.isArray(keys) ? resolveSkillKeys(keys, availableSkills) : [])
247
+ .filter((keys) => keys.length > 0);
248
+ const shouldMigrateLegacyDefault =
249
+ existing.skillKeys.length > 0 &&
250
+ legacyDefaultSets.some((keys) => sameSkillKeySet(existing.skillKeys, keys));
251
+ out.push({
252
+ ...existing,
253
+ name: def.name,
254
+ builtin: true,
255
+ skillKeys: existing.skillKeys.length > 0 && !shouldMigrateLegacyDefault ? existing.skillKeys : nextDefaultKeys,
256
+ });
257
+ byId.delete(def.id);
258
+ } else {
259
+ out.push({
260
+ id: def.id,
261
+ name: def.name,
262
+ builtin: true,
263
+ skillKeys: defaultSkillKeysForCollection(def, availableSkills),
264
+ createdAt: now,
265
+ updatedAt: now,
266
+ });
267
+ }
268
+ }
269
+ out.push(...Array.from(byId.values()).map((collection) => ({ ...collection, builtin: false })));
270
+ return { version: 1, collections: out };
271
+ }
272
+
273
+ function readSkillCollectionConfig(userCtx = {}, availableSkills = []) {
274
+ const p = skillCollectionsAbs(userCtx);
275
+ try {
276
+ if (!fs.existsSync(p)) return withBuiltinSkillCollections({}, availableSkills);
277
+ return withBuiltinSkillCollections(JSON.parse(fs.readFileSync(p, "utf-8")), availableSkills);
278
+ } catch {
279
+ return withBuiltinSkillCollections({}, availableSkills);
280
+ }
281
+ }
282
+
283
+ function writeSkillCollectionConfig(userCtx = {}, payload = {}, availableSkills = []) {
284
+ const p = skillCollectionsAbs(userCtx);
285
+ const config = withBuiltinSkillCollections(payload, availableSkills);
286
+ fs.mkdirSync(path.dirname(p), { recursive: true });
287
+ fs.writeFileSync(p, JSON.stringify(config, null, 2) + "\n", "utf-8");
288
+ return config;
289
+ }
290
+
291
+ function runtimeEnvForUser(userCtx = {}, extra = {}) {
292
+ return {
293
+ ...process.env,
294
+ ...readUserEnvObject(userCtx.userId),
295
+ ...extra,
296
+ AGENTFLOW_USER_ID: userCtx.userId || "",
297
+ };
298
+ }
299
+
89
300
  function readAgentflowUserConfigObject() {
90
301
  const p = getAgentflowUserConfigAbs();
91
302
  try {
@@ -123,6 +334,99 @@ function readModelListsFromDisk(workspaceRoot) {
123
334
  }
124
335
  }
125
336
 
337
+ const SKILLHUB_TIMEOUT_MS = 60_000;
338
+
339
+ function runSkillhub(args, opts = {}) {
340
+ return new Promise((resolve) => {
341
+ execFile("skillhub", args, {
342
+ cwd: opts.cwd || process.cwd(),
343
+ timeout: opts.timeoutMs || SKILLHUB_TIMEOUT_MS,
344
+ maxBuffer: opts.maxBuffer || 2 * 1024 * 1024,
345
+ env: {
346
+ ...process.env,
347
+ FORCE_COLOR: "0",
348
+ },
349
+ }, (error, stdout, stderr) => {
350
+ const out = String(stdout || "");
351
+ const err = String(stderr || "");
352
+ resolve({
353
+ ok: !error,
354
+ code: error && typeof error.code === "number" ? error.code : 0,
355
+ error: error ? (err.trim() || error.message || "skillhub failed") : "",
356
+ stdout: out,
357
+ stderr: err,
358
+ });
359
+ });
360
+ });
361
+ }
362
+
363
+ function parseJsonText(text, fallback = null) {
364
+ const s = String(text || "").trim();
365
+ if (!s) return fallback;
366
+ try {
367
+ return JSON.parse(s);
368
+ } catch {
369
+ const match = s.match(/(\{[\s\S]*\}|\[[\s\S]*\])\s*$/);
370
+ if (!match) return fallback;
371
+ try { return JSON.parse(match[1]); } catch { return fallback; }
372
+ }
373
+ }
374
+
375
+ function normalizeSkillhubSearchPayload(raw) {
376
+ const data = raw && typeof raw === "object" ? raw : {};
377
+ const items = Array.isArray(data.items) ? data.items : Array.isArray(data.results) ? data.results : [];
378
+ return {
379
+ total: Number(data.total) || items.length,
380
+ mode: typeof data.mode === "string" ? data.mode : "",
381
+ degraded: Boolean(data.degraded),
382
+ items: items.map((item) => {
383
+ const x = item && typeof item === "object" ? item : {};
384
+ const id = x.id ?? x.skillId ?? x.skill_id ?? "";
385
+ const slug = String(x.slug ?? x.name ?? x.displayName ?? x.display_name ?? id ?? "").trim();
386
+ return {
387
+ id: String(id || slug),
388
+ slug,
389
+ name: String(x.displayName ?? x.display_name ?? x.name ?? slug),
390
+ summary: String(x.summary ?? x.description ?? ""),
391
+ version: String(x.version ?? x.latestVersion ?? x.latest_version ?? ""),
392
+ tags: Array.isArray(x.tags) ? x.tags.map(String) : [],
393
+ };
394
+ }).filter((x) => x.slug || x.name),
395
+ };
396
+ }
397
+
398
+ function normalizeSkillhubListPayload(raw) {
399
+ const arr = Array.isArray(raw) ? raw : [];
400
+ return arr.map((x) => ({
401
+ name: String(x?.name ?? ""),
402
+ baseDir: String(x?.baseDir ?? ""),
403
+ path: String(x?.path ?? ""),
404
+ kind: String(x?.kind ?? ""),
405
+ agent: String(x?.agent ?? ""),
406
+ })).filter((x) => x.name);
407
+ }
408
+
409
+ function skillhubInstallArgs(payload, { uninstall = false } = {}) {
410
+ const slug = String(payload?.slug || payload?.name || "").trim();
411
+ if (!slug && !payload?.collection) return null;
412
+ const args = [uninstall ? "uninstall" : "install"];
413
+ if (payload?.collection) {
414
+ args.push("--collection", String(payload.collection).trim());
415
+ } else {
416
+ args.push(slug);
417
+ }
418
+ if (payload?.skillId) args.push("--skill-id", String(payload.skillId).trim());
419
+ const target = String(payload?.target || "project").trim();
420
+ const agent = String(payload?.agent || "codex").trim();
421
+ if (target === "global") {
422
+ args.push("--global", "--agent", agent);
423
+ } else if (payload?.dir) {
424
+ args.push("--dir", String(payload.dir).trim());
425
+ }
426
+ if (payload?.force) args.push("--force");
427
+ return args;
428
+ }
429
+
126
430
  function readBody(req) {
127
431
  return new Promise((resolve, reject) => {
128
432
  const chunks = [];
@@ -132,6 +436,550 @@ function readBody(req) {
132
436
  });
133
437
  }
134
438
 
439
+ const WORKSPACE_FILE_SKIP_DIRS = new Set([
440
+ ".git",
441
+ "node_modules",
442
+ ".next",
443
+ ".nuxt",
444
+ ".turbo",
445
+ "dist",
446
+ "build",
447
+ "coverage",
448
+ ]);
449
+
450
+ const WORKSPACE_TEXT_EXTS = new Set([
451
+ ".md",
452
+ ".markdown",
453
+ ".txt",
454
+ ".json",
455
+ ".yaml",
456
+ ".yml",
457
+ ".js",
458
+ ".jsx",
459
+ ".ts",
460
+ ".tsx",
461
+ ".css",
462
+ ".html",
463
+ ".mjs",
464
+ ".cjs",
465
+ ]);
466
+
467
+ function resolveWorkspaceFilePath(workspaceRoot, relPath) {
468
+ const root = path.resolve(workspaceRoot);
469
+ const rel = String(relPath || "").replace(/^[/\\]+/, "");
470
+ const abs = path.resolve(root, rel);
471
+ if (abs !== root && !abs.startsWith(root + path.sep)) {
472
+ throw new Error("Path traversal not allowed");
473
+ }
474
+ return { root, rel: path.relative(root, abs).replace(/\\/g, "/"), abs };
475
+ }
476
+
477
+ function workspaceFileIcon(fileName, isDir = false) {
478
+ if (isDir) return "folder";
479
+ const ext = path.extname(fileName).toLowerCase();
480
+ if (ext === ".md" || ext === ".markdown") return "article";
481
+ if ([".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"].includes(ext)) return "code";
482
+ if ([".yaml", ".yml", ".json"].includes(ext)) return "data_object";
483
+ if (ext === ".css") return "palette";
484
+ if (ext === ".html") return "web";
485
+ return "draft";
486
+ }
487
+
488
+ function readWorkspaceFilesRecursive(dir, root, depth = 0, maxDepth = 3, budget = { count: 0 }) {
489
+ if (depth > maxDepth || budget.count > 500) return [];
490
+ let entries;
491
+ try {
492
+ entries = fs.readdirSync(dir, { withFileTypes: true });
493
+ } catch {
494
+ return [];
495
+ }
496
+ const out = [];
497
+ for (const entry of entries) {
498
+ if (budget.count > 500) break;
499
+ if (entry.name.startsWith(".") && entry.name !== ".agents" && entry.name !== ".codex") continue;
500
+ const abs = path.join(dir, entry.name);
501
+ const rel = path.relative(root, abs).replace(/\\/g, "/");
502
+ if (entry.isDirectory()) {
503
+ if (WORKSPACE_FILE_SKIP_DIRS.has(entry.name)) continue;
504
+ budget.count++;
505
+ out.push({
506
+ type: "directory",
507
+ name: entry.name,
508
+ path: rel,
509
+ icon: workspaceFileIcon(entry.name, true),
510
+ children: readWorkspaceFilesRecursive(abs, root, depth + 1, maxDepth, budget),
511
+ });
512
+ } else if (entry.isFile()) {
513
+ const ext = path.extname(entry.name).toLowerCase();
514
+ if (!WORKSPACE_TEXT_EXTS.has(ext)) continue;
515
+ let size = 0;
516
+ try { size = fs.statSync(abs).size; } catch {}
517
+ budget.count++;
518
+ out.push({ type: "file", name: entry.name, path: rel, icon: workspaceFileIcon(entry.name), size });
519
+ }
520
+ }
521
+ out.sort((a, b) => {
522
+ if (a.type !== b.type) return a.type === "directory" ? -1 : 1;
523
+ return a.name.localeCompare(b.name);
524
+ });
525
+ return out;
526
+ }
527
+
528
+ function readWorkspaceFiles(workspaceRoot) {
529
+ const root = path.resolve(workspaceRoot);
530
+ return { root, files: readWorkspaceFilesRecursive(root, root) };
531
+ }
532
+
533
+ const WORKSPACE_GRAPH_FILENAME = "workspace.graph.json";
534
+
535
+ function workspaceGraphPath(workspaceRoot) {
536
+ return path.join(path.resolve(workspaceRoot), WORKSPACE_GRAPH_FILENAME);
537
+ }
538
+
539
+ function emptyWorkspaceGraph() {
540
+ return { version: 1, instances: {}, edges: [], ui: { nodePositions: {} } };
541
+ }
542
+
543
+ function readWorkspaceGraph(workspaceRoot) {
544
+ const graphPath = workspaceGraphPath(workspaceRoot);
545
+ if (!fs.existsSync(graphPath)) return { path: graphPath, graph: emptyWorkspaceGraph() };
546
+ const raw = fs.readFileSync(graphPath, "utf-8");
547
+ if (!raw.trim()) return { path: graphPath, graph: emptyWorkspaceGraph() };
548
+ const parsed = JSON.parse(raw);
549
+ const graph = parsed && typeof parsed === "object" ? parsed : {};
550
+ return {
551
+ path: graphPath,
552
+ graph: {
553
+ version: Number(graph.version) || 1,
554
+ instances: graph.instances && typeof graph.instances === "object" && !Array.isArray(graph.instances) ? graph.instances : {},
555
+ edges: Array.isArray(graph.edges) ? graph.edges : [],
556
+ ui: graph.ui && typeof graph.ui === "object" ? graph.ui : { nodePositions: {} },
557
+ },
558
+ };
559
+ }
560
+
561
+ function normalizeWorkspaceGraphPayload(payload) {
562
+ const graph = payload?.graph && typeof payload.graph === "object" ? payload.graph : payload;
563
+ return {
564
+ version: 1,
565
+ instances: graph?.instances && typeof graph.instances === "object" && !Array.isArray(graph.instances) ? graph.instances : {},
566
+ edges: Array.isArray(graph?.edges) ? graph.edges : [],
567
+ ui: graph?.ui && typeof graph.ui === "object" ? graph.ui : { nodePositions: {} },
568
+ updatedAt: new Date().toISOString(),
569
+ };
570
+ }
571
+
572
+ function resolveWorkspaceScopeRoot(workspaceRoot, params = {}, opts = {}) {
573
+ const flowId = params.flowId != null ? String(params.flowId).trim() : "";
574
+ if (!flowId) return { root: path.resolve(workspaceRoot), flowId: "", flowSource: "", archived: false };
575
+ const flowSource = params.flowSource != null && String(params.flowSource).trim()
576
+ ? String(params.flowSource).trim()
577
+ : "user";
578
+ const archived = params.archived === true || params.archived === "1" || params.flowArchived === true;
579
+ if (!isValidFlowSourceRead(flowSource)) {
580
+ return { root: "", error: "Invalid flowSource" };
581
+ }
582
+ const result = getPipelineFiles(workspaceRoot, flowId, flowSource, archived, opts);
583
+ if (result.error || !result.path) {
584
+ return { root: "", error: result.error || "Pipeline workspace not found" };
585
+ }
586
+ return { root: path.resolve(result.path), flowId, flowSource, archived };
587
+ }
588
+
589
+ function buildWorkspaceGeneratePrompt(payload) {
590
+ const userPrompt = String(payload?.prompt || "").trim();
591
+ const outputKind = String(payload?.outputKind || payload?.kind || "markdown").trim().toLowerCase();
592
+ const allowFlowYaml = payload?.allowFlowYaml === true || payload?.allowFlowYaml === "1";
593
+ const workspaceGraph = payload?.workspaceGraph && typeof payload.workspaceGraph === "object" ? payload.workspaceGraph : null;
594
+ const selectedNodeIds = Array.isArray(payload?.selectedNodeIds)
595
+ ? payload.selectedNodeIds.map((id) => String(id || "").trim()).filter(Boolean)
596
+ : [];
597
+ const skillsBlock = typeof payload?.skillsBlock === "string" ? payload.skillsBlock.trim() : "";
598
+ const contexts = Array.isArray(payload?.contexts) ? payload.contexts : [];
599
+ const contextBlocks = contexts
600
+ .map((ctx, idx) => {
601
+ const title = String(ctx?.title || ctx?.path || `context-${idx + 1}`).trim();
602
+ const kind = String(ctx?.kind || "text").trim();
603
+ const content = String(ctx?.content || "").trim();
604
+ if (!content) return "";
605
+ return `### ${title} (${kind})\n\n${content}`;
606
+ })
607
+ .filter(Boolean)
608
+ .join("\n\n---\n\n");
609
+ const kindInstruction =
610
+ outputKind === "mermaid"
611
+ ? [
612
+ "你是 workspace Mermaid 图节点的内容生成器。",
613
+ "请根据用户 prompt 和上游节点/文件上下文生成 Mermaid flowchart 源码。",
614
+ "只输出 Mermaid 源码,不要解释,不要包裹 Markdown 代码围栏。",
615
+ "优先使用 `flowchart TD` 或 `graph TD`,节点 ID 使用简单英文/数字/下划线,节点 label 使用清晰短文本。",
616
+ ].join("\n")
617
+ : outputKind === "ascii"
618
+ ? [
619
+ "你是 workspace ASCII 图节点的内容生成器。",
620
+ "请根据用户 prompt 和上游节点/文件上下文生成等宽字体下可读的 ASCII 图。",
621
+ "只输出 ASCII 图正文,不要解释,不要包裹 Markdown 代码围栏。",
622
+ "使用 +-|/\\<> 等字符表达结构,尽量保持对齐。",
623
+ ].join("\n")
624
+ : [
625
+ "你是 AgentFlow Workspace Composer。",
626
+ "优先根据已选择的 Skills 操作 workspace.graph.json,创建或修改 workspace 画布节点、连线与展示节点。",
627
+ "如果用户请求需要项目分析、加载代码、整理流程或生成展示结果,不要只给泛泛回答;应先让 Skills 驱动画布建模,例如创建 Git/工作目录/Load Skills/Agent/Markdown Display 等合适节点。",
628
+ "只有当用户明确只是询问概念或无需画布变更时,才直接输出 Markdown 回复。",
629
+ ].join("\n");
630
+ return [
631
+ "你正在 AgentFlow 的 Workspace 工作画布中执行任务。",
632
+ "Workspace 是当前 pipeline 的临时工作区,用于分析、试验、生成中间文件和展示结果。",
633
+ "Workspace 与 Pipeline 各自有独立的 Skill collection;此处只使用当前 Workspace Composer 选择的 collections / skills 作为本次行为规则与编辑依据。",
634
+ "当 Skills 提到修改 flow.yaml / instances / edges / ui 时,在 Workspace 视图下应映射为修改当前工作区的 workspace.graph.json,除非用户显式勾选并要求修改正式 flow.yaml。",
635
+ "workspace.graph.json 使用 JSON:{ version, instances, edges, ui: { nodePositions, nodeSizes } }。instances 的结构与 flow.yaml instances 一致;edges 使用 source/target/sourceHandle/targetHandle;ui.nodePositions 记录节点坐标。",
636
+ allowFlowYaml
637
+ ? "用户已允许你考虑正式 flow.yaml;如需修改仍必须明确说明影响。"
638
+ : "默认不要修改正式 flow.yaml;优先在 workspace 文件、workspace.graph.json 或回复内容中完成任务。",
639
+ workspaceGraph ? `\n## 当前 workspace graph\n\n${JSON.stringify(workspaceGraph, null, 2)}` : "",
640
+ selectedNodeIds.length > 0 ? `\n## 当前用户选中的 workspace 节点\n\n${selectedNodeIds.map((id) => `- ${id}`).join("\n")}` : "",
641
+ skillsBlock ? `\n## Selected Skills\n\n${skillsBlock}` : "",
642
+ kindInstruction,
643
+ contextBlocks ? `\n## 上下文\n\n${contextBlocks}` : "",
644
+ `\n## 用户 prompt\n\n${userPrompt}`,
645
+ ].filter(Boolean).join("\n");
646
+ }
647
+
648
+ function workspaceSlotValue(slot) {
649
+ if (!slot || typeof slot !== "object") return "";
650
+ for (const key of ["value", "default"]) {
651
+ if (slot[key] != null && String(slot[key]).trim()) return String(slot[key]);
652
+ }
653
+ return "";
654
+ }
655
+
656
+ function workspaceInstanceText(instance) {
657
+ const body = String(instance?.body || "").trim();
658
+ if (body) return body;
659
+ const slots = [...(Array.isArray(instance?.input) ? instance.input : []), ...(Array.isArray(instance?.output) ? instance.output : [])];
660
+ const textSlot = slots.find((slot) => String(slot?.type || "") === "text" && workspaceSlotValue(slot).trim());
661
+ return textSlot ? workspaceSlotValue(textSlot) : "";
662
+ }
663
+
664
+ function workspaceDisplayKind(definitionId) {
665
+ const id = String(definitionId || "");
666
+ if (id === "display_markdown") return "markdown";
667
+ if (id === "display_mermaid") return "mermaid";
668
+ if (id === "display_ascii") return "ascii";
669
+ return "";
670
+ }
671
+
672
+ function workspaceRunOrder(graph, runNodeId) {
673
+ const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
674
+ const edges = Array.isArray(graph?.edges) ? graph.edges : [];
675
+ const target = String(runNodeId || "").trim();
676
+ if (!target || !instances[target]) throw new Error("Missing workspace run node");
677
+ const upstream = new Map();
678
+ const downstream = new Map();
679
+ for (const edge of edges) {
680
+ const source = String(edge?.source || "");
681
+ const dest = String(edge?.target || "");
682
+ if (!source || !dest || !instances[source] || !instances[dest]) continue;
683
+ if (!upstream.has(dest)) upstream.set(dest, []);
684
+ upstream.get(dest).push(source);
685
+ if (!downstream.has(source)) downstream.set(source, []);
686
+ downstream.get(source).push(dest);
687
+ }
688
+ const reachable = new Set();
689
+ const visit = (id) => {
690
+ if (!id || reachable.has(id)) return;
691
+ reachable.add(id);
692
+ for (const next of downstream.get(id) || []) visit(next);
693
+ };
694
+ visit(target);
695
+ reachable.delete(target);
696
+ const needed = reachable;
697
+ const indegree = new Map(Array.from(needed).map((id) => [id, 0]));
698
+ for (const id of needed) {
699
+ for (const prev of upstream.get(id) || []) {
700
+ if (needed.has(prev)) indegree.set(id, (indegree.get(id) || 0) + 1);
701
+ }
702
+ }
703
+ const ready = Array.from(needed).filter((id) => (indegree.get(id) || 0) === 0);
704
+ const ordered = [];
705
+ while (ready.length) {
706
+ const id = ready.shift();
707
+ ordered.push(id);
708
+ for (const next of downstream.get(id) || []) {
709
+ if (!needed.has(next)) continue;
710
+ const n = (indegree.get(next) || 0) - 1;
711
+ indegree.set(next, n);
712
+ if (n === 0) ready.push(next);
713
+ }
714
+ }
715
+ if (ordered.length !== needed.size) {
716
+ throw new Error("Workspace run graph contains a cycle");
717
+ }
718
+ return ordered;
719
+ }
720
+
721
+ function workspaceUpstreamText(graph, nodeId, outputs) {
722
+ const edges = Array.isArray(graph?.edges) ? graph.edges : [];
723
+ const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
724
+ const incoming = edges.filter((edge) => String(edge?.target || "") === String(nodeId));
725
+ const contentEdge = incoming.find((edge) => String(edge?.targetHandle || "") === "input-1") || incoming[0];
726
+ if (!contentEdge) return "";
727
+ const sourceId = String(contentEdge.source || "");
728
+ const out = outputs.get(sourceId);
729
+ if (out != null && String(out).trim()) return String(out);
730
+ return workspaceInstanceText(instances[sourceId]);
731
+ }
732
+
733
+ function parseWorkspaceSkillKeys(raw) {
734
+ const text = String(raw || "").trim();
735
+ if (!text) return [];
736
+ try {
737
+ const parsed = JSON.parse(text);
738
+ if (Array.isArray(parsed)) return parsed.map((item) => String(item || "").trim()).filter(Boolean);
739
+ } catch {
740
+ /* plain list fallback */
741
+ }
742
+ return text.split(/[\n,]+/).map((item) => item.trim()).filter(Boolean);
743
+ }
744
+
745
+ function selectedSkillKeysFromInstance(instance) {
746
+ const bodyKeys = parseWorkspaceSkillKeys(instance?.body || "");
747
+ if (bodyKeys.length > 0) return bodyKeys;
748
+ const slots = [...(Array.isArray(instance?.input) ? instance.input : []), ...(Array.isArray(instance?.output) ? instance.output : [])];
749
+ const slot = slots.find((item) => item?.name === "skillsContext") || slots.find((item) => item?.name === "skillKeys");
750
+ return parseWorkspaceSkillKeys(workspaceSlotValue(slot) || "");
751
+ }
752
+
753
+ function workspaceUpstreamSkillBlocks(graph, nodeId, outputs) {
754
+ const edges = Array.isArray(graph?.edges) ? graph.edges : [];
755
+ return edges
756
+ .filter((edge) => String(edge?.target || "") === String(nodeId))
757
+ .map((edge) => String(outputs.get(String(edge.source || "")) || ""))
758
+ .filter((text) => text.includes("##") || text.includes("Skill"))
759
+ .join("\n\n---\n\n");
760
+ }
761
+
762
+ function workspaceWriteDisplayContent(instance, content) {
763
+ const next = { ...(instance || {}) };
764
+ const text = String(content || "");
765
+ next.body = text;
766
+ next.input = (Array.isArray(next.input) ? next.input : []).map((slot) => (
767
+ String(slot?.name || "") === "content" || String(slot?.type || "") === "text"
768
+ ? { ...slot, default: text, value: text }
769
+ : slot
770
+ ));
771
+ next.output = (Array.isArray(next.output) ? next.output : []).map((slot) => (
772
+ String(slot?.name || "") === "content" || String(slot?.type || "") === "text"
773
+ ? { ...slot, default: text, value: text }
774
+ : slot
775
+ ));
776
+ return next;
777
+ }
778
+
779
+ function workspaceUpdateDirectDisplays(graph, sourceId, content) {
780
+ const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
781
+ const edges = Array.isArray(graph?.edges) ? graph.edges : [];
782
+ const updated = [];
783
+ for (const edge of edges) {
784
+ if (String(edge?.source || "") !== String(sourceId)) continue;
785
+ const targetId = String(edge?.target || "");
786
+ const target = instances[targetId];
787
+ if (!target || !workspaceDisplayKind(target.definitionId)) continue;
788
+ instances[targetId] = workspaceWriteDisplayContent(target, content);
789
+ updated.push(targetId);
790
+ }
791
+ return updated;
792
+ }
793
+
794
+ function workspaceNodePrompt(graph, nodeId, upstreamText, skillsBlock) {
795
+ const instance = graph.instances[nodeId] || {};
796
+ const body = String(instance.body || "").trim();
797
+ const label = String(instance.label || nodeId).trim();
798
+ return [
799
+ "你正在执行 AgentFlow Workspace 画布中的一个临时节点。",
800
+ "只输出该节点要传给下游展示/后续节点的正文,不要解释运行过程。",
801
+ skillsBlock ? `\n## Selected Skills\n\n${skillsBlock}` : "",
802
+ upstreamText ? `\n## 上游上下文\n\n${upstreamText}` : "",
803
+ `\n## 当前节点\n\n- id: ${nodeId}\n- label: ${label}\n- definitionId: ${instance.definitionId || ""}`,
804
+ `\n## 节点任务\n\n${body || upstreamText}`,
805
+ ].filter(Boolean).join("\n");
806
+ }
807
+
808
+ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts = {}) {
809
+ const graph = normalizeWorkspaceGraphPayload(payload.graph || {});
810
+ const runNodeId = String(payload?.runNodeId || "").trim();
811
+ const order = workspaceRunOrder(graph, runNodeId);
812
+ const fallbackSelectedSkillKeys = Array.isArray(payload?.selectedSkills)
813
+ ? payload.selectedSkills.map((x) => String(x || "").trim()).filter(Boolean)
814
+ : [];
815
+ const skillsBlockCache = new Map();
816
+ const loadSkillsBlockForKeys = (keys) => {
817
+ const normalized = Array.from(new Set((keys || []).map((x) => String(x || "").trim()).filter(Boolean)));
818
+ const cacheKey = normalized.join("\n");
819
+ if (skillsBlockCache.has(cacheKey)) return skillsBlockCache.get(cacheKey);
820
+ const selectedSkillResources = normalized.length > 0
821
+ ? loadResourcesForSkillKeys(normalized, PACKAGE_ROOT, scopedRoot)
822
+ : { skills: [], references: [] };
823
+ const block = normalized.length > 0
824
+ ? buildSkillCompactInjectionBlock(selectedSkillResources.skills, selectedSkillResources.references)
825
+ : "";
826
+ skillsBlockCache.set(cacheKey, block);
827
+ return block;
828
+ };
829
+ const outputs = new Map();
830
+ const events = [];
831
+ const emit = (event) => {
832
+ events.push(event);
833
+ if (typeof opts.onEvent === "function") opts.onEvent(event);
834
+ };
835
+ let cwd = scopedRoot;
836
+ const modelKey = typeof payload?.model === "string" ? payload.model.trim() : "";
837
+
838
+ for (const nodeId of order) {
839
+ const instance = graph.instances[nodeId];
840
+ if (!instance) continue;
841
+ const defId = String(instance.definitionId || "");
842
+ emit({ type: "node-start", nodeId, definitionId: defId });
843
+
844
+ if (defId === "workspace_run") {
845
+ continue;
846
+ }
847
+
848
+ if (defId === "control_load_skills") {
849
+ const nodeSkillKeys = selectedSkillKeysFromInstance(instance);
850
+ const activeSkillKeys = nodeSkillKeys.length > 0 ? nodeSkillKeys : fallbackSelectedSkillKeys;
851
+ const skillsBlock = loadSkillsBlockForKeys(activeSkillKeys);
852
+ graph.instances[nodeId] = {
853
+ ...instance,
854
+ output: (Array.isArray(instance.output) ? instance.output : []).map((slot) => (
855
+ String(slot?.name || "") === "skillsContext" || String(slot?.type || "") === "text"
856
+ ? { ...slot, default: skillsBlock, value: skillsBlock }
857
+ : slot
858
+ )),
859
+ };
860
+ outputs.set(nodeId, skillsBlock);
861
+ workspaceUpdateDirectDisplays(graph, nodeId, skillsBlock);
862
+ emit({ type: "graph", nodeId, graph });
863
+ emit({ type: "node-done", nodeId, definitionId: defId });
864
+ continue;
865
+ }
866
+
867
+ if (workspaceDisplayKind(defId)) {
868
+ const content = workspaceUpstreamText(graph, nodeId, outputs);
869
+ graph.instances[nodeId] = workspaceWriteDisplayContent(instance, content);
870
+ outputs.set(nodeId, content);
871
+ emit({ type: "graph", nodeId, graph });
872
+ emit({ type: "node-done", nodeId, definitionId: defId });
873
+ continue;
874
+ }
875
+
876
+ if (defId === "provide_str") {
877
+ const content = workspaceInstanceText(instance);
878
+ outputs.set(nodeId, content);
879
+ emit({ type: "node-done", nodeId, definitionId: defId });
880
+ continue;
881
+ }
882
+
883
+ if (defId === "provide_file") {
884
+ const fileValue = workspaceSlotValue(Array.isArray(instance.output) ? instance.output[0] : null) || workspaceInstanceText(instance);
885
+ const abs = path.resolve(scopedRoot, fileValue);
886
+ if (!abs.startsWith(path.resolve(scopedRoot) + path.sep) && abs !== path.resolve(scopedRoot)) {
887
+ throw new Error(`Workspace file is outside root: ${fileValue}`);
888
+ }
889
+ const content = fs.existsSync(abs) && fs.statSync(abs).isFile() ? fs.readFileSync(abs, "utf-8") : fileValue;
890
+ outputs.set(nodeId, content);
891
+ emit({ type: "node-done", nodeId, definitionId: defId });
892
+ continue;
893
+ }
894
+
895
+ if (defId === "control_cd_workspace") {
896
+ const inputText = workspaceUpstreamText(graph, nodeId, outputs);
897
+ const inputSlots = Array.isArray(instance.input) ? instance.input : [];
898
+ const pathSlot = inputSlots.find((slot) => String(slot?.name || "") === "path") ||
899
+ inputSlots.find((slot) => String(slot?.name || "") === "target");
900
+ const candidate = workspaceSlotValue(pathSlot) || workspaceInstanceText(instance) || inputText;
901
+ const abs = candidate ? path.resolve(scopedRoot, candidate) : scopedRoot;
902
+ if (fs.existsSync(abs) && fs.statSync(abs).isDirectory()) cwd = abs;
903
+ outputs.set(nodeId, cwd);
904
+ emit({ type: "node-done", nodeId, definitionId: defId });
905
+ continue;
906
+ }
907
+
908
+ if (defId === "control_user_workspace") {
909
+ cwd = path.resolve(os.homedir());
910
+ outputs.set(nodeId, cwd);
911
+ emit({ type: "node-done", nodeId, definitionId: defId });
912
+ continue;
913
+ }
914
+
915
+ const upstreamText = workspaceUpstreamText(graph, nodeId, outputs);
916
+ const body = String(instance.body || "").trim();
917
+ if (defId === "agent_subAgent" && !body && !String(upstreamText || "").trim()) {
918
+ throw new Error(`Workspace node ${nodeId} has no task. Fill the node body or connect upstream text.`);
919
+ }
920
+ const upstreamSkillBlocks = workspaceUpstreamSkillBlocks(graph, nodeId, outputs);
921
+ const prompt = workspaceNodePrompt(graph, nodeId, upstreamText, upstreamSkillBlocks || loadSkillsBlockForKeys(fallbackSelectedSkillKeys));
922
+ let content = "";
923
+ const maxAttempts = 3;
924
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
925
+ let attemptContent = "";
926
+ try {
927
+ const handle = startComposerAgent({
928
+ uiWorkspaceRoot: scopedRoot,
929
+ cliWorkspace: cwd,
930
+ prompt,
931
+ modelKey,
932
+ agentflowUserId: userCtx.userId || "",
933
+ onStreamEvent: (ev) => {
934
+ emit({ ...ev, nodeId });
935
+ if (ev?.type === "natural" && ev.kind === "assistant" && typeof ev.text === "string") {
936
+ attemptContent += (attemptContent ? "\n" : "") + ev.text;
937
+ const updatedDisplays = workspaceUpdateDirectDisplays(graph, nodeId, attemptContent);
938
+ if (updatedDisplays.length) emit({ type: "graph", nodeId, displayNodeIds: updatedDisplays, graph });
939
+ }
940
+ },
941
+ });
942
+ await handle.finished;
943
+ content = attemptContent.trim();
944
+ break;
945
+ } catch (e) {
946
+ if (attempt < maxAttempts && isTransientAgentNetworkError(e)) {
947
+ emit({ type: "status", nodeId, line: `Workspace node retry ${attempt + 1}/${maxAttempts} after network error` });
948
+ await sleepMs(Math.min(1500 * attempt, 5000));
949
+ continue;
950
+ }
951
+ throw e;
952
+ }
953
+ }
954
+ outputs.set(nodeId, content);
955
+ const updatedDisplays = workspaceUpdateDirectDisplays(graph, nodeId, content);
956
+ if (updatedDisplays.length) emit({ type: "graph", nodeId, displayNodeIds: updatedDisplays, graph });
957
+ emit({ type: "node-done", nodeId, definitionId: defId });
958
+ }
959
+ graph.updatedAt = new Date().toISOString();
960
+ return { graph, events, order };
961
+ }
962
+
963
+ function isTransientAgentNetworkError(err) {
964
+ const text = [
965
+ err?.message,
966
+ err?.cursorStderrTail,
967
+ err?.stderr,
968
+ err?.stack,
969
+ ].filter(Boolean).join("\n");
970
+ return /Client network socket disconnected before secure TLS connection was established/i.test(text) ||
971
+ /secure TLS connection was established/i.test(text) ||
972
+ /\bECONNRESET\b/i.test(text) ||
973
+ /\bETIMEDOUT\b/i.test(text) ||
974
+ /\bEAI_AGAIN\b/i.test(text) ||
975
+ /network socket disconnected/i.test(text) ||
976
+ /socket hang up/i.test(text);
977
+ }
978
+
979
+ function sleepMs(ms) {
980
+ return new Promise((resolve) => setTimeout(resolve, ms));
981
+ }
982
+
135
983
  /** ZIP 本地头:PK\x03\x04 / \x05\x06 / \x07\x08 */
136
984
  function bufferLooksLikeZip(buf) {
137
985
  return (
@@ -211,12 +1059,12 @@ const flowEditorSyncSubscribers = new Map();
211
1059
  /** 每次 broadcastFlowEditorSync 时递增,供轮询端点 /api/flow-editor-sync-poll 使用 */
212
1060
  const flowEditorSyncVersions = new Map();
213
1061
 
214
- function flowEditorSyncKey(flowId, flowSource, flowArchived) {
215
- return `${String(flowId)}\t${String(flowSource)}\t${flowArchived ? "1" : "0"}`;
1062
+ function flowEditorSyncKey(flowId, flowSource, flowArchived, userId = "") {
1063
+ return `${String(userId || "")}\t${String(flowId)}\t${String(flowSource)}\t${flowArchived ? "1" : "0"}`;
216
1064
  }
217
1065
 
218
- function broadcastFlowEditorSync(flowId, flowSource, flowArchived = false) {
219
- const key = flowEditorSyncKey(flowId, flowSource, flowArchived);
1066
+ function broadcastFlowEditorSync(flowId, flowSource, flowArchived = false, userId = "") {
1067
+ const key = flowEditorSyncKey(flowId, flowSource, flowArchived, userId);
220
1068
 
221
1069
  /* 递增轮询版本号 */
222
1070
  flowEditorSyncVersions.set(key, (flowEditorSyncVersions.get(key) ?? 0) + 1);
@@ -340,6 +1188,7 @@ function normalizeContextInstanceIds(raw) {
340
1188
  * @param {object} opts
341
1189
  * @param {string} opts.workspaceRoot
342
1190
  * @param {number} opts.port
1191
+ * @param {boolean} [opts.hideCommunityLinks]
343
1192
  * @param {string} [opts.staticDir] 默认 PACKAGE_ROOT/builtin/web-ui/dist(npm run build 产出)
344
1193
  * @returns {Promise<import('http').Server>}
345
1194
  */
@@ -347,10 +1196,12 @@ export function startUiServer({
347
1196
  workspaceRoot,
348
1197
  port,
349
1198
  host = "127.0.0.1",
1199
+ hideCommunityLinks = false,
350
1200
  staticDir = path.join(PACKAGE_ROOT, "builtin", "web-ui", "dist"),
351
1201
  }) {
352
1202
  const root = path.resolve(workspaceRoot);
353
1203
  const uiPort = port;
1204
+ const uiConfig = { hideCommunityLinks: Boolean(hideCommunityLinks) };
354
1205
 
355
1206
  const server = http.createServer(async (req, res) => {
356
1207
  const url = new URL(req.url || "/", "http://127.0.0.1");
@@ -363,10 +1214,58 @@ export function startUiServer({
363
1214
  return origEnd(...args);
364
1215
  };
365
1216
 
1217
+ if (url.pathname === "/api/auth/me" && req.method === "GET") {
1218
+ const user = getAuthUserFromRequest(req);
1219
+ json(res, 200, { authenticated: Boolean(user), user: user || null, setupRequired: authSetupRequired() });
1220
+ return;
1221
+ }
1222
+
1223
+ if (url.pathname === "/api/auth/login" && req.method === "POST") {
1224
+ let payload;
1225
+ try {
1226
+ payload = JSON.parse(await readBody(req));
1227
+ } catch {
1228
+ json(res, 400, { error: "Invalid JSON body" });
1229
+ return;
1230
+ }
1231
+ const result = loginOrCreateUser(payload?.username, payload?.password);
1232
+ if (!result.ok) {
1233
+ json(res, 401, { error: result.error || "Login failed", setupRequired: authSetupRequired() });
1234
+ return;
1235
+ }
1236
+ const body = JSON.stringify({ authenticated: true, user: result.user, setupRequired: false, migration: result.migration || null });
1237
+ res.writeHead(200, {
1238
+ "Content-Type": "application/json; charset=utf-8",
1239
+ "Content-Length": Buffer.byteLength(body),
1240
+ "Set-Cookie": buildSessionCookie(result.token),
1241
+ });
1242
+ res.end(body);
1243
+ return;
1244
+ }
1245
+
1246
+ if (url.pathname === "/api/auth/logout" && req.method === "POST") {
1247
+ logoutRequest(req);
1248
+ const body = JSON.stringify({ ok: true });
1249
+ res.writeHead(200, {
1250
+ "Content-Type": "application/json; charset=utf-8",
1251
+ "Content-Length": Buffer.byteLength(body),
1252
+ "Set-Cookie": buildClearSessionCookie(),
1253
+ });
1254
+ res.end(body);
1255
+ return;
1256
+ }
1257
+
1258
+ const authUser = getAuthUserFromRequest(req);
1259
+ const userCtx = authUser ? { userId: authUser.userId } : {};
1260
+ if (url.pathname.startsWith("/api/") && !authUser) {
1261
+ json(res, 401, { error: "Authentication required", setupRequired: authSetupRequired() });
1262
+ return;
1263
+ }
1264
+
366
1265
  if (url.pathname === "/api/flows") {
367
1266
  if (req.method === "GET") {
368
1267
  try {
369
- json(res, 200, listFlowsJson(root));
1268
+ json(res, 200, listFlowsJson(root, userCtx));
370
1269
  } catch (e) {
371
1270
  json(res, 500, { error: (e && e.message) || String(e) });
372
1271
  }
@@ -400,7 +1299,7 @@ export function startUiServer({
400
1299
  if (ts === "workspace" || ts === "user") {
401
1300
  targetSpace = ts;
402
1301
  }
403
- const existing = listFlowsJson(root);
1302
+ const existing = listFlowsJson(root, userCtx);
404
1303
  if (
405
1304
  existing.some(
406
1305
  (f) => f.id === flowId && (f.source ?? "user") === targetSpace && !f.archived,
@@ -410,7 +1309,7 @@ export function startUiServer({
410
1309
  return;
411
1310
  }
412
1311
  const flowYaml = buildEmptyUserFlowYaml({ description: desc });
413
- const result = writeFlowYaml(root, flowId, targetSpace, flowYaml);
1312
+ const result = writeFlowYaml(root, flowId, targetSpace, flowYaml, userCtx);
414
1313
  if (!result.success) {
415
1314
  json(res, 400, result);
416
1315
  return;
@@ -456,7 +1355,7 @@ export function startUiServer({
456
1355
  }
457
1356
  const flowId = idCheck.flowId;
458
1357
  const targetSpace = parsed.targetSpace === "workspace" ? "workspace" : "user";
459
- const existing = listFlowsJson(root);
1358
+ const existing = listFlowsJson(root, userCtx);
460
1359
  if (
461
1360
  existing.some(
462
1361
  (f) => f.id === flowId && (f.source ?? "user") === targetSpace && !f.archived,
@@ -487,7 +1386,7 @@ export function startUiServer({
487
1386
  filesMap = new Map([["flow.yaml", Buffer.from(text, "utf8")]]);
488
1387
  }
489
1388
 
490
- const w = writePipelineTree(root, flowId, targetSpace, filesMap);
1389
+ const w = writePipelineTree(root, flowId, targetSpace, filesMap, userCtx);
491
1390
  if (!w.success) {
492
1391
  json(res, 400, { error: w.error });
493
1392
  return;
@@ -507,7 +1406,7 @@ export function startUiServer({
507
1406
  return;
508
1407
  }
509
1408
  const { getNodeExecContext } = await import("./node-exec-context.mjs");
510
- json(res, 200, getNodeExecContext(root, flowId, instanceId, runId));
1409
+ json(res, 200, getNodeExecContext(root, flowId, instanceId, runId, userCtx));
511
1410
  } catch (e) {
512
1411
  json(res, 500, { error: (e && e.message) || String(e) });
513
1412
  }
@@ -516,7 +1415,7 @@ export function startUiServer({
516
1415
 
517
1416
  if (req.method === "GET" && url.pathname === "/api/pipeline-recent-runs") {
518
1417
  try {
519
- json(res, 200, { runs: listRecentRunsFromDisk(root) });
1418
+ json(res, 200, { runs: listRecentRunsFromDisk(root, userCtx) });
520
1419
  } catch (e) {
521
1420
  json(res, 500, { error: (e && e.message) || String(e) });
522
1421
  }
@@ -532,7 +1431,7 @@ export function startUiServer({
532
1431
  return;
533
1432
  }
534
1433
  const { getRunNodeStatusesFromDisk } = await import("./run-node-statuses-from-disk.mjs");
535
- json(res, 200, { statuses: getRunNodeStatusesFromDisk(root, flowId, runId) });
1434
+ json(res, 200, { statuses: getRunNodeStatusesFromDisk(root, flowId, runId, userCtx) });
536
1435
  } catch (e) {
537
1436
  json(res, 500, { error: (e && e.message) || String(e) });
538
1437
  }
@@ -554,7 +1453,7 @@ export function startUiServer({
554
1453
  const { getRunDir } = await import("./workspace.mjs");
555
1454
  const { RUN_LOG_REL } = await import("./paths.mjs");
556
1455
  const { default: fsMod } = await import("node:fs");
557
- const logPath = path.join(getRunDir(root, flowId, runId), RUN_LOG_REL);
1456
+ const logPath = path.join(getRunDir(root, flowId, runId, userCtx), RUN_LOG_REL);
558
1457
  if (!fsMod.existsSync(logPath)) {
559
1458
  json(res, 200, { bytes: 0, text: "" });
560
1459
  return;
@@ -596,6 +1495,357 @@ export function startUiServer({
596
1495
  return;
597
1496
  }
598
1497
 
1498
+ if (req.method === "GET" && url.pathname === "/api/workspace/files") {
1499
+ try {
1500
+ const scoped = resolveWorkspaceScopeRoot(root, {
1501
+ flowId: url.searchParams.get("flowId") || "",
1502
+ flowSource: url.searchParams.get("flowSource") || "user",
1503
+ archived: url.searchParams.get("archived") === "1",
1504
+ }, userCtx);
1505
+ if (scoped.error) {
1506
+ json(res, 400, { error: scoped.error });
1507
+ return;
1508
+ }
1509
+ json(res, 200, { ...readWorkspaceFiles(scoped.root), flowId: scoped.flowId, flowSource: scoped.flowSource, archived: scoped.archived });
1510
+ } catch (e) {
1511
+ json(res, 500, { error: (e && e.message) || String(e) });
1512
+ }
1513
+ return;
1514
+ }
1515
+
1516
+ if (req.method === "GET" && url.pathname === "/api/workspace/graph") {
1517
+ try {
1518
+ const scoped = resolveWorkspaceScopeRoot(root, {
1519
+ flowId: url.searchParams.get("flowId") || "",
1520
+ flowSource: url.searchParams.get("flowSource") || "user",
1521
+ archived: url.searchParams.get("archived") === "1",
1522
+ }, userCtx);
1523
+ if (scoped.error) {
1524
+ json(res, 400, { error: scoped.error });
1525
+ return;
1526
+ }
1527
+ const { path: graphPath, graph } = readWorkspaceGraph(scoped.root);
1528
+ json(res, 200, {
1529
+ ok: true,
1530
+ graph,
1531
+ path: graphPath,
1532
+ root: scoped.root,
1533
+ flowId: scoped.flowId,
1534
+ flowSource: scoped.flowSource,
1535
+ archived: scoped.archived,
1536
+ writable: !(scoped.archived || scoped.flowSource === "builtin"),
1537
+ });
1538
+ } catch (e) {
1539
+ json(res, 500, { error: (e && e.message) || String(e) });
1540
+ }
1541
+ return;
1542
+ }
1543
+
1544
+ if (req.method === "POST" && url.pathname === "/api/workspace/graph") {
1545
+ let payload;
1546
+ try {
1547
+ payload = JSON.parse(await readBody(req));
1548
+ } catch {
1549
+ json(res, 400, { error: "Invalid JSON body" });
1550
+ return;
1551
+ }
1552
+ try {
1553
+ const scoped = resolveWorkspaceScopeRoot(root, {
1554
+ flowId: payload.flowId || "",
1555
+ flowSource: payload.flowSource || "user",
1556
+ archived: payload.archived === true || payload.flowArchived === true,
1557
+ }, userCtx);
1558
+ if (scoped.error) {
1559
+ json(res, 400, { error: scoped.error });
1560
+ return;
1561
+ }
1562
+ if (scoped.archived || scoped.flowSource === "builtin") {
1563
+ json(res, 400, { error: "Cannot write workspace graph for builtin or archived pipeline" });
1564
+ return;
1565
+ }
1566
+ const graph = normalizeWorkspaceGraphPayload(payload.graph || payload);
1567
+ const graphPath = workspaceGraphPath(scoped.root);
1568
+ fs.writeFileSync(graphPath, JSON.stringify(graph, null, 2) + "\n", "utf-8");
1569
+ json(res, 200, { ok: true, path: graphPath, graph });
1570
+ } catch (e) {
1571
+ json(res, 500, { error: (e && e.message) || String(e) });
1572
+ }
1573
+ return;
1574
+ }
1575
+
1576
+ if (req.method === "POST" && url.pathname === "/api/workspace/run") {
1577
+ let payload;
1578
+ try {
1579
+ payload = JSON.parse(await readBody(req));
1580
+ } catch {
1581
+ json(res, 400, { error: "Invalid JSON body" });
1582
+ return;
1583
+ }
1584
+ try {
1585
+ const scoped = resolveWorkspaceScopeRoot(root, {
1586
+ flowId: payload.flowId || "",
1587
+ flowSource: payload.flowSource || "user",
1588
+ archived: payload.archived === true || payload.flowArchived === true,
1589
+ }, userCtx);
1590
+ if (scoped.error) {
1591
+ json(res, 400, { error: scoped.error });
1592
+ return;
1593
+ }
1594
+ if (scoped.archived || scoped.flowSource === "builtin") {
1595
+ json(res, 400, { error: "Cannot run workspace graph for builtin or archived pipeline" });
1596
+ return;
1597
+ }
1598
+ const wantsStream = /\bapplication\/x-ndjson\b/i.test(req.headers.accept || "") || payload.stream === true;
1599
+ if (wantsStream) {
1600
+ const graphPath = workspaceGraphPath(scoped.root);
1601
+ res.writeHead(200, {
1602
+ "Content-Type": "application/x-ndjson; charset=utf-8",
1603
+ "Cache-Control": "no-cache",
1604
+ "X-Accel-Buffering": "no",
1605
+ });
1606
+ const writeEvent = (event) => {
1607
+ res.write(JSON.stringify(event) + "\n");
1608
+ };
1609
+ try {
1610
+ const result = await runWorkspaceGraph(root, scoped.root, payload, userCtx, { onEvent: writeEvent });
1611
+ fs.writeFileSync(graphPath, JSON.stringify(result.graph, null, 2) + "\n", "utf-8");
1612
+ writeEvent({ type: "done", ok: true, path: graphPath, graph: result.graph, order: result.order });
1613
+ res.end();
1614
+ } catch (e) {
1615
+ writeEvent({ type: "error", error: (e && e.message) || String(e) });
1616
+ res.end();
1617
+ }
1618
+ return;
1619
+ }
1620
+ const result = await runWorkspaceGraph(root, scoped.root, payload, userCtx);
1621
+ const graphPath = workspaceGraphPath(scoped.root);
1622
+ fs.writeFileSync(graphPath, JSON.stringify(result.graph, null, 2) + "\n", "utf-8");
1623
+ json(res, 200, { ok: true, path: graphPath, ...result });
1624
+ } catch (e) {
1625
+ json(res, 500, { error: (e && e.message) || String(e) });
1626
+ }
1627
+ return;
1628
+ }
1629
+
1630
+ if (req.method === "GET" && url.pathname === "/api/workspace/file") {
1631
+ try {
1632
+ const scoped = resolveWorkspaceScopeRoot(root, {
1633
+ flowId: url.searchParams.get("flowId") || "",
1634
+ flowSource: url.searchParams.get("flowSource") || "user",
1635
+ archived: url.searchParams.get("archived") === "1",
1636
+ }, userCtx);
1637
+ if (scoped.error) {
1638
+ json(res, 400, { error: scoped.error });
1639
+ return;
1640
+ }
1641
+ const { abs, rel } = resolveWorkspaceFilePath(scoped.root, url.searchParams.get("path") || "");
1642
+ if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) {
1643
+ json(res, 404, { error: "File not found" });
1644
+ return;
1645
+ }
1646
+ const stat = fs.statSync(abs);
1647
+ if (stat.size > 2 * 1024 * 1024) {
1648
+ json(res, 413, { error: "File too large" });
1649
+ return;
1650
+ }
1651
+ json(res, 200, { path: rel, content: fs.readFileSync(abs, "utf-8"), size: stat.size });
1652
+ } catch (e) {
1653
+ json(res, /traversal/i.test(String(e.message || e)) ? 403 : 500, { error: (e && e.message) || String(e) });
1654
+ }
1655
+ return;
1656
+ }
1657
+
1658
+ if (req.method === "POST" && url.pathname === "/api/workspace/file") {
1659
+ let payload;
1660
+ try {
1661
+ payload = JSON.parse(await readBody(req));
1662
+ } catch {
1663
+ json(res, 400, { error: "Invalid JSON body" });
1664
+ return;
1665
+ }
1666
+ try {
1667
+ const scoped = resolveWorkspaceScopeRoot(root, {
1668
+ flowId: payload.flowId || "",
1669
+ flowSource: payload.flowSource || "user",
1670
+ archived: payload.archived === true || payload.flowArchived === true,
1671
+ }, userCtx);
1672
+ if (scoped.error) {
1673
+ json(res, 400, { error: scoped.error });
1674
+ return;
1675
+ }
1676
+ if (scoped.archived || scoped.flowSource === "builtin") {
1677
+ json(res, 400, { error: "Cannot write to builtin or archived pipeline workspace" });
1678
+ return;
1679
+ }
1680
+ const { abs, rel } = resolveWorkspaceFilePath(scoped.root, payload.path || "");
1681
+ if (!rel) {
1682
+ json(res, 400, { error: "Missing path" });
1683
+ return;
1684
+ }
1685
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
1686
+ fs.writeFileSync(abs, String(payload.content ?? ""), "utf-8");
1687
+ json(res, 200, { ok: true, path: rel });
1688
+ } catch (e) {
1689
+ json(res, /traversal/i.test(String(e.message || e)) ? 403 : 500, { error: (e && e.message) || String(e) });
1690
+ }
1691
+ return;
1692
+ }
1693
+
1694
+ if (req.method === "POST" && url.pathname === "/api/workspace/folder") {
1695
+ let payload;
1696
+ try {
1697
+ payload = JSON.parse(await readBody(req));
1698
+ } catch {
1699
+ json(res, 400, { error: "Invalid JSON body" });
1700
+ return;
1701
+ }
1702
+ try {
1703
+ const scoped = resolveWorkspaceScopeRoot(root, {
1704
+ flowId: payload.flowId || "",
1705
+ flowSource: payload.flowSource || "user",
1706
+ archived: payload.archived === true || payload.flowArchived === true,
1707
+ }, userCtx);
1708
+ if (scoped.error) {
1709
+ json(res, 400, { error: scoped.error });
1710
+ return;
1711
+ }
1712
+ if (scoped.archived || scoped.flowSource === "builtin") {
1713
+ json(res, 400, { error: "Cannot write to builtin or archived pipeline workspace" });
1714
+ return;
1715
+ }
1716
+ const { abs, rel } = resolveWorkspaceFilePath(scoped.root, payload.path || "");
1717
+ if (!rel) {
1718
+ json(res, 400, { error: "Missing path" });
1719
+ return;
1720
+ }
1721
+ fs.mkdirSync(abs, { recursive: true });
1722
+ json(res, 200, { ok: true, path: rel });
1723
+ } catch (e) {
1724
+ json(res, /traversal/i.test(String(e.message || e)) ? 403 : 500, { error: (e && e.message) || String(e) });
1725
+ }
1726
+ return;
1727
+ }
1728
+
1729
+ if (req.method === "POST" && url.pathname === "/api/workspace/delete") {
1730
+ let payload;
1731
+ try {
1732
+ payload = JSON.parse(await readBody(req));
1733
+ } catch {
1734
+ json(res, 400, { error: "Invalid JSON body" });
1735
+ return;
1736
+ }
1737
+ try {
1738
+ const scoped = resolveWorkspaceScopeRoot(root, {
1739
+ flowId: payload.flowId || "",
1740
+ flowSource: payload.flowSource || "user",
1741
+ archived: payload.archived === true || payload.flowArchived === true,
1742
+ }, userCtx);
1743
+ if (scoped.error) {
1744
+ json(res, 400, { error: scoped.error });
1745
+ return;
1746
+ }
1747
+ if (scoped.archived || scoped.flowSource === "builtin") {
1748
+ json(res, 400, { error: "Cannot write to builtin or archived pipeline workspace" });
1749
+ return;
1750
+ }
1751
+ const { abs, rel } = resolveWorkspaceFilePath(scoped.root, payload.path || "");
1752
+ if (!rel) {
1753
+ json(res, 400, { error: "Missing path" });
1754
+ return;
1755
+ }
1756
+ if (!fs.existsSync(abs)) {
1757
+ json(res, 404, { error: "Path not found" });
1758
+ return;
1759
+ }
1760
+ fs.rmSync(abs, { recursive: true, force: true });
1761
+ json(res, 200, { ok: true, path: rel });
1762
+ } catch (e) {
1763
+ json(res, /traversal/i.test(String(e.message || e)) ? 403 : 500, { error: (e && e.message) || String(e) });
1764
+ }
1765
+ return;
1766
+ }
1767
+
1768
+ if (req.method === "POST" && url.pathname === "/api/workspace/generate") {
1769
+ let payload;
1770
+ try {
1771
+ payload = JSON.parse(await readBody(req));
1772
+ } catch {
1773
+ json(res, 400, { error: "Invalid JSON body" });
1774
+ return;
1775
+ }
1776
+ const prompt = String(payload?.prompt || "").trim();
1777
+ if (!prompt) {
1778
+ json(res, 400, { error: "Missing prompt" });
1779
+ return;
1780
+ }
1781
+ try {
1782
+ const scoped = resolveWorkspaceScopeRoot(root, {
1783
+ flowId: payload.flowId || "",
1784
+ flowSource: payload.flowSource || "user",
1785
+ archived: payload.archived === true || payload.flowArchived === true,
1786
+ }, userCtx);
1787
+ if (scoped.error) {
1788
+ json(res, 400, { error: scoped.error });
1789
+ return;
1790
+ }
1791
+ const selectedSkillKeys = Array.isArray(payload?.selectedSkills)
1792
+ ? payload.selectedSkills.map((x) => String(x || "").trim()).filter(Boolean)
1793
+ : [];
1794
+ const selectedSkillResources = selectedSkillKeys.length > 0
1795
+ ? loadResourcesForSkillKeys(selectedSkillKeys, PACKAGE_ROOT, scoped.root)
1796
+ : { skills: [], references: [] };
1797
+ const skillsBlock = selectedSkillKeys.length > 0
1798
+ ? buildSkillCompactInjectionBlock(selectedSkillResources.skills, selectedSkillResources.references)
1799
+ : "";
1800
+ let content = "";
1801
+ const events = [];
1802
+ const maxAttempts = 3;
1803
+ const promptText = buildWorkspaceGeneratePrompt({ ...payload, skillsBlock });
1804
+ const modelKey = typeof payload?.model === "string" ? payload.model.trim() : "";
1805
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
1806
+ let attemptContent = "";
1807
+ try {
1808
+ if (attempt > 1) {
1809
+ events.push({
1810
+ type: "status",
1811
+ line: `Workspace agent retry ${attempt}/${maxAttempts} after transient network failure...`,
1812
+ });
1813
+ await sleepMs(Math.min(1500 * attempt, 5000));
1814
+ }
1815
+ const handle = startComposerAgent({
1816
+ uiWorkspaceRoot: scoped.root,
1817
+ cliWorkspace: scoped.root,
1818
+ prompt: promptText,
1819
+ modelKey,
1820
+ agentflowUserId: userCtx.userId || "",
1821
+ onStreamEvent: (ev) => {
1822
+ events.push(ev);
1823
+ if (ev?.type === "natural" && ev.kind === "assistant" && typeof ev.text === "string") {
1824
+ attemptContent += (attemptContent ? "\n" : "") + ev.text;
1825
+ }
1826
+ },
1827
+ });
1828
+ await handle.finished;
1829
+ content = attemptContent;
1830
+ break;
1831
+ } catch (e) {
1832
+ if (attempt < maxAttempts && isTransientAgentNetworkError(e)) {
1833
+ events.push({
1834
+ type: "status",
1835
+ line: `Workspace agent transient network error: ${String(e.message || e).slice(0, 220)}`,
1836
+ });
1837
+ continue;
1838
+ }
1839
+ throw e;
1840
+ }
1841
+ }
1842
+ json(res, 200, { ok: true, content: content.trim(), events });
1843
+ } catch (e) {
1844
+ json(res, 500, { error: (e && e.message) || String(e) });
1845
+ }
1846
+ return;
1847
+ }
1848
+
599
1849
  if (req.method === "GET" && url.pathname === "/api/pipeline-files") {
600
1850
  const flowId = url.searchParams.get("flowId");
601
1851
  const flowSource = url.searchParams.get("flowSource") || "user";
@@ -605,7 +1855,7 @@ export function startUiServer({
605
1855
  return;
606
1856
  }
607
1857
  try {
608
- const result = getPipelineFiles(root, flowId, flowSource, archived);
1858
+ const result = getPipelineFiles(root, flowId, flowSource, archived, userCtx);
609
1859
  json(res, 200, result);
610
1860
  } catch (e) {
611
1861
  json(res, 500, { error: (e && e.message) || String(e) });
@@ -623,7 +1873,7 @@ export function startUiServer({
623
1873
  return;
624
1874
  }
625
1875
  try {
626
- const result = getPipelineFiles(root, flowId, flowSource, archived);
1876
+ const result = getPipelineFiles(root, flowId, flowSource, archived, userCtx);
627
1877
  if (result.error) {
628
1878
  json(res, 404, { error: result.error });
629
1879
  return;
@@ -669,7 +1919,7 @@ export function startUiServer({
669
1919
  content = String(body);
670
1920
  }
671
1921
  try {
672
- const result = getPipelineFiles(root, flowId, flowSource, archived);
1922
+ const result = getPipelineFiles(root, flowId, flowSource, archived, userCtx);
673
1923
  if (result.error) {
674
1924
  json(res, 404, { error: result.error });
675
1925
  return;
@@ -702,7 +1952,7 @@ export function startUiServer({
702
1952
 
703
1953
  if (req.method === "GET" && url.pathname === "/api/ui-context") {
704
1954
  try {
705
- json(res, 200, { workspaceRoot: root });
1955
+ json(res, 200, { workspaceRoot: root, ...uiConfig });
706
1956
  } catch (e) {
707
1957
  json(res, 500, { error: (e && e.message) || String(e) });
708
1958
  }
@@ -812,6 +2062,32 @@ export function startUiServer({
812
2062
  return;
813
2063
  }
814
2064
 
2065
+ if (req.method === "GET" && url.pathname === "/api/user-env") {
2066
+ try {
2067
+ json(res, 200, { env: readUserEnvRows(userCtx.userId) });
2068
+ } catch (e) {
2069
+ json(res, 500, { error: (e && e.message) || String(e) });
2070
+ }
2071
+ return;
2072
+ }
2073
+
2074
+ if (req.method === "POST" && url.pathname === "/api/user-env") {
2075
+ let payload;
2076
+ try {
2077
+ payload = JSON.parse(await readBody(req));
2078
+ } catch {
2079
+ json(res, 400, { error: "Invalid JSON body" });
2080
+ return;
2081
+ }
2082
+ try {
2083
+ const envRows = writeUserEnvRows(userCtx.userId, payload?.env || []);
2084
+ json(res, 200, { success: true, env: envRows });
2085
+ } catch (e) {
2086
+ json(res, 500, { error: (e && e.message) || String(e) });
2087
+ }
2088
+ return;
2089
+ }
2090
+
815
2091
  if (req.method === "POST" && url.pathname === "/api/update-model-lists") {
816
2092
  try {
817
2093
  let opencodeProviderOverride = "";
@@ -833,15 +2109,108 @@ export function startUiServer({
833
2109
  return;
834
2110
  }
835
2111
 
2112
+ if (req.method === "GET" && url.pathname === "/api/skillhub/status") {
2113
+ const versionRes = await runSkillhub(["version"], { cwd: root, timeoutMs: 15_000 });
2114
+ const whoRes = await runSkillhub(["whoami"], { cwd: root, timeoutMs: 15_000 });
2115
+ json(res, 200, {
2116
+ available: versionRes.ok,
2117
+ version: versionRes.ok ? versionRes.stdout.trim() : "",
2118
+ loggedIn: whoRes.ok,
2119
+ user: whoRes.ok ? whoRes.stdout.trim() : "",
2120
+ error: versionRes.ok ? "" : versionRes.error,
2121
+ });
2122
+ return;
2123
+ }
2124
+
2125
+ if (req.method === "GET" && url.pathname === "/api/skillhub/list") {
2126
+ const target = url.searchParams.get("target") || "global";
2127
+ const agent = url.searchParams.get("agent") || "codex";
2128
+ const args = ["list", "--json"];
2129
+ if (target === "all") args.push("--all");
2130
+ else if (target === "global") args.push("--global", "--agent", agent);
2131
+ const result = await runSkillhub(args, { cwd: root });
2132
+ if (!result.ok) {
2133
+ json(res, 500, { error: result.error, stdout: result.stdout });
2134
+ return;
2135
+ }
2136
+ json(res, 200, { skills: normalizeSkillhubListPayload(parseJsonText(result.stdout, [])) });
2137
+ return;
2138
+ }
2139
+
2140
+ if (req.method === "GET" && url.pathname === "/api/skillhub/search") {
2141
+ const q = (url.searchParams.get("q") || "").trim();
2142
+ if (!q) {
2143
+ json(res, 200, { total: 0, items: [] });
2144
+ return;
2145
+ }
2146
+ const result = await runSkillhub(["search", "-q", q], { cwd: root });
2147
+ if (!result.ok) {
2148
+ json(res, 500, { error: result.error, stdout: result.stdout });
2149
+ return;
2150
+ }
2151
+ json(res, 200, normalizeSkillhubSearchPayload(parseJsonText(result.stdout, {})));
2152
+ return;
2153
+ }
2154
+
2155
+ if (req.method === "POST" && url.pathname === "/api/skillhub/install") {
2156
+ let payload;
2157
+ try {
2158
+ payload = JSON.parse(await readBody(req));
2159
+ } catch {
2160
+ json(res, 400, { error: "Invalid JSON body" });
2161
+ return;
2162
+ }
2163
+ const args = skillhubInstallArgs(payload);
2164
+ if (!args) {
2165
+ json(res, 400, { error: "Missing skill slug or collection" });
2166
+ return;
2167
+ }
2168
+ const result = await runSkillhub(args, { cwd: root, timeoutMs: 180_000, maxBuffer: 4 * 1024 * 1024 });
2169
+ if (!result.ok) {
2170
+ json(res, 500, { error: result.error, stdout: result.stdout });
2171
+ return;
2172
+ }
2173
+ json(res, 200, { ok: true, stdout: result.stdout });
2174
+ return;
2175
+ }
2176
+
2177
+ if (req.method === "POST" && url.pathname === "/api/skillhub/uninstall") {
2178
+ let payload;
2179
+ try {
2180
+ payload = JSON.parse(await readBody(req));
2181
+ } catch {
2182
+ json(res, 400, { error: "Invalid JSON body" });
2183
+ return;
2184
+ }
2185
+ const args = skillhubInstallArgs(payload, { uninstall: true });
2186
+ if (!args) {
2187
+ json(res, 400, { error: "Missing skill slug or collection" });
2188
+ return;
2189
+ }
2190
+ const result = await runSkillhub(args, { cwd: root, timeoutMs: 120_000, maxBuffer: 4 * 1024 * 1024 });
2191
+ if (!result.ok) {
2192
+ json(res, 500, { error: result.error, stdout: result.stdout });
2193
+ return;
2194
+ }
2195
+ json(res, 200, { ok: true, stdout: result.stdout });
2196
+ return;
2197
+ }
2198
+
2199
+ if (req.method === "POST" && url.pathname === "/api/skillhub/update") {
2200
+ const result = await runSkillhub(["update"], { cwd: root, timeoutMs: 180_000, maxBuffer: 4 * 1024 * 1024 });
2201
+ if (!result.ok) {
2202
+ json(res, 500, { error: result.error, stdout: result.stdout });
2203
+ return;
2204
+ }
2205
+ json(res, 200, { ok: true, stdout: result.stdout });
2206
+ return;
2207
+ }
2208
+
836
2209
  if (req.method === "GET" && url.pathname === "/api/nodes") {
837
2210
  const flowId = url.searchParams.get("flowId");
838
2211
  const flowSource = url.searchParams.get("flowSource") || "user";
839
2212
  const lang = url.searchParams.get("lang") || "en";
840
- if (!flowId) {
841
- json(res, 400, { error: "Missing flowId" });
842
- return;
843
- }
844
- if (!isValidFlowSourceRead(flowSource)) {
2213
+ if (flowId && !isValidFlowSourceRead(flowSource)) {
845
2214
  json(res, 400, { error: "Invalid flowSource" });
846
2215
  return;
847
2216
  }
@@ -849,7 +2218,60 @@ export function startUiServer({
849
2218
  try {
850
2219
  const { setLanguage } = await import("./i18n.mjs");
851
2220
  setLanguage(lang);
852
- json(res, 200, listNodesJson(root, flowId, flowSource, { archived: nodesArchived }));
2221
+ json(res, 200, listNodesJson(root, flowId || "", flowId ? flowSource : "", { archived: nodesArchived, ...userCtx }));
2222
+ } catch (e) {
2223
+ json(res, 500, { error: (e && e.message) || String(e) });
2224
+ }
2225
+ return;
2226
+ }
2227
+
2228
+ if (req.method === "GET" && url.pathname === "/api/nodes/detail") {
2229
+ const nodeId = url.searchParams.get("id") || "";
2230
+ const flowId = url.searchParams.get("flowId") || "";
2231
+ const flowSource = url.searchParams.get("flowSource") || "";
2232
+ if (!nodeId) {
2233
+ json(res, 400, { error: "Missing node id" });
2234
+ return;
2235
+ }
2236
+ if (flowId && !isValidFlowSourceRead(flowSource || "user")) {
2237
+ json(res, 400, { error: "Invalid flowSource" });
2238
+ return;
2239
+ }
2240
+ const archived = url.searchParams.get("archived") === "1";
2241
+ try {
2242
+ const detail = readNodeDetailJson(root, nodeId, flowId, flowId ? (flowSource || "user") : "", { archived, ...userCtx });
2243
+ if (detail.error) {
2244
+ json(res, 404, { error: detail.error });
2245
+ return;
2246
+ }
2247
+ json(res, 200, detail);
2248
+ } catch (e) {
2249
+ json(res, 500, { error: (e && e.message) || String(e) });
2250
+ }
2251
+ return;
2252
+ }
2253
+
2254
+ if (req.method === "GET" && url.pathname === "/api/nodes/file") {
2255
+ const nodeId = url.searchParams.get("id") || "";
2256
+ const relPath = url.searchParams.get("path") || "";
2257
+ const flowId = url.searchParams.get("flowId") || "";
2258
+ const flowSource = url.searchParams.get("flowSource") || "";
2259
+ if (!nodeId || !relPath) {
2260
+ json(res, 400, { error: "Missing node id or path" });
2261
+ return;
2262
+ }
2263
+ if (flowId && !isValidFlowSourceRead(flowSource || "user")) {
2264
+ json(res, 400, { error: "Invalid flowSource" });
2265
+ return;
2266
+ }
2267
+ const archived = url.searchParams.get("archived") === "1";
2268
+ try {
2269
+ const file = readNodeFilePreview(root, nodeId, relPath, flowId, flowId ? (flowSource || "user") : "", { archived, ...userCtx });
2270
+ if (file.error) {
2271
+ json(res, 404, { error: file.error });
2272
+ return;
2273
+ }
2274
+ json(res, 200, file);
853
2275
  } catch (e) {
854
2276
  json(res, 500, { error: (e && e.message) || String(e) });
855
2277
  }
@@ -890,7 +2312,7 @@ export function startUiServer({
890
2312
  return;
891
2313
  }
892
2314
  try {
893
- const resolved = resolveFlowDirForWrite(root, flowId, flowSource);
2315
+ const resolved = resolveFlowDirForWrite(root, flowId, flowSource, userCtx);
894
2316
  if (resolved.error || !resolved.flowDir) {
895
2317
  json(res, 400, { error: resolved.error || "Could not resolve flow directory" });
896
2318
  return;
@@ -916,7 +2338,7 @@ export function startUiServer({
916
2338
  const flowSource = payload?.flowSource || "user";
917
2339
  let flowDir = "";
918
2340
  if (flowId && isValidFlowSourceWrite(flowSource)) {
919
- const resolved = resolveFlowDirForWrite(root, flowId, flowSource);
2341
+ const resolved = resolveFlowDirForWrite(root, flowId, flowSource, userCtx);
920
2342
  if (!resolved.error && resolved.flowDir) flowDir = resolved.flowDir;
921
2343
  }
922
2344
  const result = publishNodeFromInstance(root, payload || {}, { flowDir });
@@ -939,7 +2361,7 @@ export function startUiServer({
939
2361
  return;
940
2362
  }
941
2363
  const flowArchived = url.searchParams.get("archived") === "1";
942
- const result = readFlowJson(root, flowId, flowSource, { archived: flowArchived });
2364
+ const result = readFlowJson(root, flowId, flowSource, { archived: flowArchived, ...userCtx });
943
2365
  if (result.error) {
944
2366
  json(res, 404, result);
945
2367
  return;
@@ -965,7 +2387,7 @@ export function startUiServer({
965
2387
  json(res, 400, { error: "Missing runUuid, instanceId, or content" });
966
2388
  return;
967
2389
  }
968
- const runDir = path.join(getRunDir(root, payload.flowId || "unknown", runUuid));
2390
+ const runDir = path.join(getRunDir(root, payload.flowId || "unknown", runUuid, userCtx));
969
2391
  const outputPath = path.join(runDir, `output/${instanceId}/node_${instanceId}_content.md`);
970
2392
  try {
971
2393
  fs.mkdirSync(path.dirname(outputPath), { recursive: true });
@@ -1000,7 +2422,7 @@ ${content}
1000
2422
 
1001
2423
  const opencodeCmd = process.env.OPENCODE_CMD || "opencode";
1002
2424
  const tmpPromptFile = path.join(
1003
- getRunDir(root, payload.flowId || "unknown", runUuid),
2425
+ getRunDir(root, payload.flowId || "unknown", runUuid, userCtx),
1004
2426
  "intermediate",
1005
2427
  `${instanceId}_ai_edit_prompt.txt`,
1006
2428
  );
@@ -1045,7 +2467,7 @@ ${content}
1045
2467
  json(res, 400, { error: "Missing runUuid or instanceId" });
1046
2468
  return;
1047
2469
  }
1048
- const runDir = path.join(getRunDir(root, payload.flowId || "unknown", runUuid));
2470
+ const runDir = path.join(getRunDir(root, payload.flowId || "unknown", runUuid, userCtx));
1049
2471
  const resultPath = path.join(runDir, `intermediate/${instanceId}/${instanceId}.result.md`);
1050
2472
  try {
1051
2473
  fs.mkdirSync(path.dirname(resultPath), { recursive: true });
@@ -1075,7 +2497,7 @@ finishedAt: "${new Date().toISOString()}"
1075
2497
  json(res, 400, { error: "Missing runUuid, instanceId, or branch" });
1076
2498
  return;
1077
2499
  }
1078
- const runDir = path.join(getRunDir(root, payload.flowId || "unknown", runUuid));
2500
+ const runDir = path.join(getRunDir(root, payload.flowId || "unknown", runUuid, userCtx));
1079
2501
  const resultPath = path.join(runDir, `intermediate/${instanceId}/${instanceId}.result.md`);
1080
2502
  try {
1081
2503
  fs.mkdirSync(path.dirname(resultPath), { recursive: true });
@@ -1123,7 +2545,7 @@ finishedAt: "${new Date().toISOString()}"
1123
2545
  return;
1124
2546
  }
1125
2547
  const flowArchived = Boolean(payload.flowArchived);
1126
- const result = writeFlowYaml(root, flowId, flowSource, flowYaml, { archived: flowArchived });
2548
+ const result = writeFlowYaml(root, flowId, flowSource, flowYaml, { archived: flowArchived, ...userCtx });
1127
2549
  if (!result.success) {
1128
2550
  json(res, 400, result);
1129
2551
  return;
@@ -1151,7 +2573,7 @@ finishedAt: "${new Date().toISOString()}"
1151
2573
  return;
1152
2574
  }
1153
2575
  const flowArchived = Boolean(payload.flowArchived);
1154
- broadcastFlowEditorSync(flowId, flowSource, flowArchived);
2576
+ broadcastFlowEditorSync(flowId, flowSource, flowArchived, userCtx.userId);
1155
2577
  json(res, 200, { ok: true });
1156
2578
  return;
1157
2579
  }
@@ -1168,7 +2590,7 @@ finishedAt: "${new Date().toISOString()}"
1168
2590
  return;
1169
2591
  }
1170
2592
  const flowArchived = url.searchParams.get("archived") === "1";
1171
- const key = flowEditorSyncKey(flowId, flowSource, flowArchived);
2593
+ const key = flowEditorSyncKey(flowId, flowSource, flowArchived, userCtx.userId);
1172
2594
  let set = flowEditorSyncSubscribers.get(key);
1173
2595
  if (!set) {
1174
2596
  set = new Set();
@@ -1202,7 +2624,7 @@ finishedAt: "${new Date().toISOString()}"
1202
2624
  return;
1203
2625
  }
1204
2626
  const flowArchived = url.searchParams.get("archived") === "1";
1205
- const key = flowEditorSyncKey(flowId, flowSource, flowArchived);
2627
+ const key = flowEditorSyncKey(flowId, flowSource, flowArchived, userCtx.userId);
1206
2628
  const serverVer = flowEditorSyncVersions.get(key) ?? 0;
1207
2629
  const clientVer = parseInt(url.searchParams.get("v") ?? "0", 10) || 0;
1208
2630
  json(res, 200, { version: serverVer, changed: serverVer > clientVer });
@@ -1232,7 +2654,7 @@ finishedAt: "${new Date().toISOString()}"
1232
2654
  json(res, 400, { error: "Invalid toSource" });
1233
2655
  return;
1234
2656
  }
1235
- const result = moveFlowDirectory(root, flowId.trim(), fromSource, toSource);
2657
+ const result = moveFlowDirectory(root, flowId.trim(), fromSource, toSource, userCtx);
1236
2658
  if (!result.success) {
1237
2659
  json(res, 400, { error: result.error || "Move failed" });
1238
2660
  return;
@@ -1269,7 +2691,7 @@ finishedAt: "${new Date().toISOString()}"
1269
2691
  json(res, 200, { success: true, flowId, flowSource });
1270
2692
  return;
1271
2693
  }
1272
- const yamlRes = getFlowYamlAbs(root, flowId, flowSource, { archived: false });
2694
+ const yamlRes = getFlowYamlAbs(root, flowId, flowSource, { archived: false, ...userCtx });
1273
2695
  if (yamlRes.error || !yamlRes.path) {
1274
2696
  json(res, 404, { error: yamlRes.error || "找不到流水线" });
1275
2697
  return;
@@ -1312,7 +2734,7 @@ finishedAt: "${new Date().toISOString()}"
1312
2734
  json(res, 400, { error: "仅支持归档用户目录或工作区流水线" });
1313
2735
  return;
1314
2736
  }
1315
- const result = archiveFlowPipeline(root, flowId, flowSource);
2737
+ const result = archiveFlowPipeline(root, flowId, flowSource, userCtx);
1316
2738
  if (!result.success) {
1317
2739
  json(res, 400, { error: result.error || "归档失败" });
1318
2740
  return;
@@ -1345,7 +2767,7 @@ finishedAt: "${new Date().toISOString()}"
1345
2767
  json(res, 400, { error: "仅支持删除用户目录或工作区流水线" });
1346
2768
  return;
1347
2769
  }
1348
- const result = deleteFlowPipeline(root, flowId, flowSource, { archived: flowArchived });
2770
+ const result = deleteFlowPipeline(root, flowId, flowSource, { archived: flowArchived, ...userCtx });
1349
2771
  if (!result.success) {
1350
2772
  json(res, 400, { error: result.error || "删除失败" });
1351
2773
  return;
@@ -1366,7 +2788,7 @@ finishedAt: "${new Date().toISOString()}"
1366
2788
  json(res, 400, { error: "Invalid flowSource" });
1367
2789
  return;
1368
2790
  }
1369
- const yamlRes = getFlowYamlAbs(root, flowId, flowSource, { archived: flowArchived });
2791
+ const yamlRes = getFlowYamlAbs(root, flowId, flowSource, { archived: flowArchived, ...userCtx });
1370
2792
  if (yamlRes.error) {
1371
2793
  json(res, 404, { error: yamlRes.error });
1372
2794
  return;
@@ -1407,7 +2829,7 @@ finishedAt: "${new Date().toISOString()}"
1407
2829
  json(res, 400, { error: "Cannot save config to builtin or archived flow" });
1408
2830
  return;
1409
2831
  }
1410
- const yamlRes = getFlowYamlAbs(root, flowId, flowSource, { archived: flowArchived });
2832
+ const yamlRes = getFlowYamlAbs(root, flowId, flowSource, { archived: flowArchived, ...userCtx });
1411
2833
  if (yamlRes.error) {
1412
2834
  json(res, 404, { error: yamlRes.error });
1413
2835
  return;
@@ -1437,12 +2859,12 @@ finishedAt: "${new Date().toISOString()}"
1437
2859
  json(res, 400, { error: "Invalid flowSource" });
1438
2860
  return;
1439
2861
  }
1440
- const result = readFlowSchedule(root, flowId, flowSource, { archived: flowArchived });
2862
+ const result = readFlowSchedule(root, flowId, flowSource, { archived: flowArchived, ...userCtx });
1441
2863
  if (!result.success) {
1442
2864
  json(res, 400, { error: result.error || "Could not read schedule" });
1443
2865
  return;
1444
2866
  }
1445
- const status = listScheduleStatuses(root).find(
2867
+ const status = listScheduleStatuses(root, userCtx).find(
1446
2868
  (s) => s.flowId === flowId && (s.flowSource || "user") === (flowSource || "user"),
1447
2869
  );
1448
2870
  json(res, 200, { schedule: result.schedule, state: result.state || {}, status: status || null });
@@ -1468,7 +2890,7 @@ finishedAt: "${new Date().toISOString()}"
1468
2890
  json(res, 400, { error: "Cannot save schedule to builtin or archived flow" });
1469
2891
  return;
1470
2892
  }
1471
- const result = writeFlowSchedule(root, flowId, flowSource, payload.schedule || {});
2893
+ const result = writeFlowSchedule(root, flowId, flowSource, payload.schedule || {}, userCtx);
1472
2894
  if (!result.success) {
1473
2895
  json(res, 400, { error: result.error || "Could not save schedule" });
1474
2896
  return;
@@ -1491,7 +2913,8 @@ finishedAt: "${new Date().toISOString()}"
1491
2913
  return;
1492
2914
  }
1493
2915
  const runUuid = typeof payload.uuid === "string" ? payload.uuid.trim() : "";
1494
- if (activeFlowRuns.has(flowId)) {
2916
+ const runKey = `${userCtx.userId || ""}:${payload.flowSource || "user"}:${flowId}`;
2917
+ if (activeFlowRuns.has(runKey)) {
1495
2918
  json(res, 409, { error: "该流水线已在运行中" });
1496
2919
  return;
1497
2920
  }
@@ -1500,7 +2923,7 @@ finishedAt: "${new Date().toISOString()}"
1500
2923
  // UI 轮询会把 runMode 翻回 stopped,即便 CLI 正在运行也显示 PAUSED。
1501
2924
  if (runUuid) {
1502
2925
  try {
1503
- const runDir = getRunDir(root, flowId, runUuid);
2926
+ const runDir = getRunDir(root, flowId, runUuid, userCtx);
1504
2927
  const interruptedPath = path.join(runDir, RUN_INTERRUPTED_FILENAME);
1505
2928
  if (fs.existsSync(interruptedPath)) fs.unlinkSync(interruptedPath);
1506
2929
  } catch (e) {
@@ -1542,7 +2965,7 @@ finishedAt: "${new Date().toISOString()}"
1542
2965
  const endSafe = () => {
1543
2966
  if (responseEnded) return;
1544
2967
  responseEnded = true;
1545
- activeFlowRuns.delete(flowId);
2968
+ activeFlowRuns.delete(runKey);
1546
2969
  try {
1547
2970
  res.end();
1548
2971
  } catch (_) {}
@@ -1557,7 +2980,7 @@ finishedAt: "${new Date().toISOString()}"
1557
2980
  child = spawn(process.execPath, args, {
1558
2981
  cwd: root,
1559
2982
  stdio: ["ignore", "pipe", "pipe"],
1560
- env: { ...process.env, FORCE_COLOR: "0" },
2983
+ env: runtimeEnvForUser(userCtx, { FORCE_COLOR: "0" }),
1561
2984
  // detached: true 使 child 成为新进程组 leader,/api/flow/run/stop 时
1562
2985
  // 用 process.kill(-pid) 可以一次性 SIGTERM 整棵进程树(含 cursor-agent 等孙进程)
1563
2986
  detached: true,
@@ -1570,7 +2993,7 @@ finishedAt: "${new Date().toISOString()}"
1570
2993
 
1571
2994
  /** @type {{ child: import("child_process").ChildProcess, runUuid: string | null }} */
1572
2995
  const runEntry = { child, runUuid: runUuid || null };
1573
- activeFlowRuns.set(flowId, runEntry);
2996
+ activeFlowRuns.set(runKey, runEntry);
1574
2997
  log.debug(`[ui] flow/run: spawned pid=${child.pid} flowId=${flowId}${runUuid ? ` uuid=${runUuid}` : ""}`);
1575
2998
 
1576
2999
  let stdoutBuf = "";
@@ -1643,7 +3066,8 @@ finishedAt: "${new Date().toISOString()}"
1643
3066
  json(res, 400, { error: "Missing flowId" });
1644
3067
  return;
1645
3068
  }
1646
- const entry = activeFlowRuns.get(flowId);
3069
+ const runKey = `${userCtx.userId || ""}:${payload.flowSource || "user"}:${flowId}`;
3070
+ const entry = activeFlowRuns.get(runKey);
1647
3071
  if (!entry || !entry.child) {
1648
3072
  json(res, 404, { error: "该流水线未在运行" });
1649
3073
  return;
@@ -1661,10 +3085,10 @@ finishedAt: "${new Date().toISOString()}"
1661
3085
  try { entry.child.kill("SIGTERM"); } catch (_) {}
1662
3086
  }
1663
3087
  const uuid = entry.runUuid;
1664
- activeFlowRuns.delete(flowId);
3088
+ activeFlowRuns.delete(runKey);
1665
3089
  if (uuid) {
1666
3090
  try {
1667
- const runDir = getRunDir(root, flowId, uuid);
3091
+ const runDir = getRunDir(root, flowId, uuid, userCtx);
1668
3092
  fs.mkdirSync(runDir, { recursive: true });
1669
3093
  fs.writeFileSync(
1670
3094
  path.join(runDir, RUN_INTERRUPTED_FILENAME),
@@ -1684,6 +3108,38 @@ finishedAt: "${new Date().toISOString()}"
1684
3108
  return;
1685
3109
  }
1686
3110
 
3111
+ if (req.method === "GET" && url.pathname === "/api/skill-collections") {
3112
+ json(res, 200, readSkillCollectionConfig(userCtx, listComposerSkills(PACKAGE_ROOT, root)));
3113
+ return;
3114
+ }
3115
+
3116
+ if (req.method === "POST" && url.pathname === "/api/skill-collections") {
3117
+ let payload;
3118
+ try {
3119
+ payload = JSON.parse(await readBody(req));
3120
+ } catch {
3121
+ json(res, 400, { error: "Invalid JSON body" });
3122
+ return;
3123
+ }
3124
+ try {
3125
+ json(res, 200, writeSkillCollectionConfig(userCtx, payload, listComposerSkills(PACKAGE_ROOT, root)));
3126
+ } catch (e) {
3127
+ json(res, 500, { error: (e && e.message) || String(e) });
3128
+ }
3129
+ return;
3130
+ }
3131
+
3132
+ if (req.method === "GET" && url.pathname === "/api/skills/detail") {
3133
+ const key = url.searchParams.get("key") || url.searchParams.get("name") || "";
3134
+ const detail = readComposerSkillDetail(PACKAGE_ROOT, root, key);
3135
+ if (!detail) {
3136
+ json(res, 404, { error: "Skill not found" });
3137
+ return;
3138
+ }
3139
+ json(res, 200, { skill: detail });
3140
+ return;
3141
+ }
3142
+
1687
3143
  if (req.method === "POST" && url.pathname === "/api/composer-agent") {
1688
3144
  let payload;
1689
3145
  try {
@@ -1739,7 +3195,7 @@ finishedAt: "${new Date().toISOString()}"
1739
3195
  return;
1740
3196
  }
1741
3197
  const flowArchived = Boolean(payload.flowArchived);
1742
- const yamlRes = getFlowYamlAbs(root, flowId, flowSource, { archived: flowArchived });
3198
+ const yamlRes = getFlowYamlAbs(root, flowId, flowSource, { archived: flowArchived, ...userCtx });
1743
3199
  if (yamlRes.error || !yamlRes.path) {
1744
3200
  json(res, 400, { error: yamlRes.error || "Could not resolve flow.yaml" });
1745
3201
  return;
@@ -1750,7 +3206,7 @@ finishedAt: "${new Date().toISOString()}"
1750
3206
  let editorSyncFlowSource = flowSource;
1751
3207
  let flowDirForCli = path.dirname(flowYamlAbs);
1752
3208
  if (flowSource === "builtin") {
1753
- const w = resolveFlowDirForWrite(root, flowId, "workspace");
3209
+ const w = resolveFlowDirForWrite(root, flowId, "workspace", userCtx);
1754
3210
  if (w.error || !w.flowDir) {
1755
3211
  json(res, 400, { error: w.error || "Could not resolve workspace flow directory" });
1756
3212
  return;
@@ -1783,6 +3239,7 @@ finishedAt: "${new Date().toISOString()}"
1783
3239
  flowYamlAbs,
1784
3240
  flowId,
1785
3241
  flowSource,
3242
+ userId: userCtx.userId || "",
1786
3243
  intents: multiStepIntents,
1787
3244
  canvasInstanceIds: instanceIds,
1788
3245
  skillsHint: multiStepResources.skillsHint,
@@ -1939,6 +3396,7 @@ finishedAt: "${new Date().toISOString()}"
1939
3396
  thread,
1940
3397
  phaseContext,
1941
3398
  phaseRole: phaseRole || undefined,
3399
+ agentflowUserId: userCtx.userId || "",
1942
3400
  force: true,
1943
3401
  onStreamEvent,
1944
3402
  });
@@ -1952,7 +3410,7 @@ finishedAt: "${new Date().toISOString()}"
1952
3410
  flowSource: flowSource || null,
1953
3411
  });
1954
3412
  if (flowId && flowSource) {
1955
- broadcastFlowEditorSync(flowId, flowSource, Boolean(payload.flowArchived));
3413
+ broadcastFlowEditorSync(flowId, flowSource, Boolean(payload.flowArchived), userCtx.userId);
1956
3414
  }
1957
3415
  try { res.write(JSON.stringify({ type: "done" }) + "\n"); } catch (_) {}
1958
3416
  }
@@ -1989,6 +3447,7 @@ finishedAt: "${new Date().toISOString()}"
1989
3447
  cliWorkspace,
1990
3448
  prompt: finalPrompt,
1991
3449
  modelKey: typeof model === "string" ? model.trim() : "",
3450
+ agentflowUserId: userCtx.userId || "",
1992
3451
  onStreamEvent,
1993
3452
  });
1994
3453
  child = handle.child;
@@ -2007,6 +3466,7 @@ finishedAt: "${new Date().toISOString()}"
2007
3466
  flowYamlAbs,
2008
3467
  flowContext: flowContextForMultiStep,
2009
3468
  modelKey: typeof model === "string" ? model.trim() : "",
3469
+ agentflowUserId: userCtx.userId || "",
2010
3470
  force: true,
2011
3471
  onStreamEvent,
2012
3472
  getAborted: () => clientDisconnected || responseEnded,
@@ -2029,7 +3489,7 @@ finishedAt: "${new Date().toISOString()}"
2029
3489
  flowSource: flowSource || null,
2030
3490
  });
2031
3491
  if (flowYamlChanged && flowId && flowSource) {
2032
- broadcastFlowEditorSync(flowId, flowSource, Boolean(payload.flowArchived));
3492
+ broadcastFlowEditorSync(flowId, flowSource, Boolean(payload.flowArchived), userCtx.userId);
2033
3493
  }
2034
3494
  try { res.write(JSON.stringify({ type: "done" }) + "\n"); } catch (_) {}
2035
3495
  }