@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
@@ -0,0 +1,240 @@
1
+ import crypto from "crypto";
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import {
5
+ ARCHIVED_PIPELINES_DIR_NAME,
6
+ getAgentflowDataRoot,
7
+ getUserPipelinesRoot,
8
+ sanitizeAgentflowUserId,
9
+ } from "./paths.mjs";
10
+
11
+ const SESSION_COOKIE = "af_session";
12
+ const SESSION_TTL_MS = 30 * 24 * 60 * 60 * 1000;
13
+
14
+ function authRoot() {
15
+ return path.join(getAgentflowDataRoot(), "auth");
16
+ }
17
+
18
+ function usersPath() {
19
+ return path.join(authRoot(), "users.json");
20
+ }
21
+
22
+ function sessionsPath() {
23
+ return path.join(authRoot(), "sessions.json");
24
+ }
25
+
26
+ function readJsonObject(filePath) {
27
+ try {
28
+ if (!fs.existsSync(filePath)) return {};
29
+ const data = JSON.parse(fs.readFileSync(filePath, "utf-8"));
30
+ return data && typeof data === "object" && !Array.isArray(data) ? data : {};
31
+ } catch {
32
+ return {};
33
+ }
34
+ }
35
+
36
+ function writeJsonObject(filePath, data) {
37
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
38
+ fs.writeFileSync(filePath, JSON.stringify(data && typeof data === "object" ? data : {}, null, 2) + "\n", "utf-8");
39
+ }
40
+
41
+ function hashPassword(password, salt = crypto.randomBytes(16).toString("hex")) {
42
+ const hash = crypto.scryptSync(String(password), salt, 64).toString("hex");
43
+ return { salt, hash };
44
+ }
45
+
46
+ function verifyPassword(password, record) {
47
+ if (!record || typeof record.salt !== "string" || typeof record.hash !== "string") return false;
48
+ const next = hashPassword(password, record.salt).hash;
49
+ try {
50
+ return crypto.timingSafeEqual(Buffer.from(next, "hex"), Buffer.from(record.hash, "hex"));
51
+ } catch {
52
+ return false;
53
+ }
54
+ }
55
+
56
+ function hashToken(token) {
57
+ return crypto.createHash("sha256").update(String(token)).digest("hex");
58
+ }
59
+
60
+ function parseCookies(header) {
61
+ const out = {};
62
+ for (const part of String(header || "").split(";")) {
63
+ const idx = part.indexOf("=");
64
+ if (idx <= 0) continue;
65
+ const key = part.slice(0, idx).trim();
66
+ const value = part.slice(idx + 1).trim();
67
+ if (!key) continue;
68
+ try {
69
+ out[key] = decodeURIComponent(value);
70
+ } catch {
71
+ out[key] = value;
72
+ }
73
+ }
74
+ return out;
75
+ }
76
+
77
+ export function readAuthUsers() {
78
+ return readJsonObject(usersPath());
79
+ }
80
+
81
+ export function authSetupRequired() {
82
+ return Object.keys(readAuthUsers()).length === 0;
83
+ }
84
+
85
+ function listFlowDirs(root) {
86
+ try {
87
+ if (!fs.existsSync(root) || !fs.statSync(root).isDirectory()) return [];
88
+ return fs.readdirSync(root, { withFileTypes: true })
89
+ .filter((entry) => entry.isDirectory())
90
+ .filter((entry) => entry.name !== ARCHIVED_PIPELINES_DIR_NAME)
91
+ .filter((entry) => fs.existsSync(path.join(root, entry.name, "flow.yaml")))
92
+ .map((entry) => entry.name)
93
+ .sort((a, b) => a.localeCompare(b));
94
+ } catch {
95
+ return [];
96
+ }
97
+ }
98
+
99
+ function copyMissingFlowDirs(sourceRoot, targetRoot, relativeRoot = "") {
100
+ const fromRoot = path.join(sourceRoot, relativeRoot);
101
+ const toRoot = path.join(targetRoot, relativeRoot);
102
+ const copied = [];
103
+ const skipped = [];
104
+ for (const name of listFlowDirs(fromRoot)) {
105
+ const fromDir = path.join(fromRoot, name);
106
+ const toDir = path.join(toRoot, name);
107
+ if (fs.existsSync(toDir)) {
108
+ skipped.push(path.join(relativeRoot, name).replace(/\\/g, "/"));
109
+ continue;
110
+ }
111
+ fs.mkdirSync(path.dirname(toDir), { recursive: true });
112
+ fs.cpSync(fromDir, toDir, { recursive: true });
113
+ copied.push(path.join(relativeRoot, name).replace(/\\/g, "/"));
114
+ }
115
+ return { copied, skipped };
116
+ }
117
+
118
+ export function migrateLegacyPipelinesToAdminUser(userId) {
119
+ const safeUserId = sanitizeAgentflowUserId(userId);
120
+ if (!safeUserId) return { copied: [], skipped: [], source: "", target: "", error: "invalid userId" };
121
+
122
+ const source = getUserPipelinesRoot("");
123
+ const target = getUserPipelinesRoot(safeUserId);
124
+ if (path.resolve(source) === path.resolve(target)) {
125
+ return { copied: [], skipped: [], source, target };
126
+ }
127
+ if (!fs.existsSync(source)) {
128
+ return { copied: [], skipped: [], source, target };
129
+ }
130
+
131
+ const active = copyMissingFlowDirs(source, target);
132
+ const archived = copyMissingFlowDirs(source, target, ARCHIVED_PIPELINES_DIR_NAME);
133
+ return {
134
+ copied: [...active.copied, ...archived.copied],
135
+ skipped: [...active.skipped, ...archived.skipped],
136
+ source,
137
+ target,
138
+ };
139
+ }
140
+
141
+ export function getSessionCookieName() {
142
+ return SESSION_COOKIE;
143
+ }
144
+
145
+ export function buildSessionCookie(token) {
146
+ const attrs = [
147
+ `${SESSION_COOKIE}=${encodeURIComponent(token)}`,
148
+ "Path=/",
149
+ "HttpOnly",
150
+ "SameSite=Lax",
151
+ `Max-Age=${Math.floor(SESSION_TTL_MS / 1000)}`,
152
+ ];
153
+ return attrs.join("; ");
154
+ }
155
+
156
+ export function buildClearSessionCookie() {
157
+ return `${SESSION_COOKIE}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`;
158
+ }
159
+
160
+ export function getAuthUserFromRequest(req) {
161
+ const token = parseCookies(req.headers.cookie || "")[SESSION_COOKIE];
162
+ if (!token) return null;
163
+ const sessions = readJsonObject(sessionsPath());
164
+ const key = hashToken(token);
165
+ const session = sessions[key];
166
+ if (!session || typeof session.userId !== "string") return null;
167
+ if (Number(session.expiresAt) <= Date.now()) {
168
+ delete sessions[key];
169
+ writeJsonObject(sessionsPath(), sessions);
170
+ return null;
171
+ }
172
+ const users = readAuthUsers();
173
+ const user = users[session.userId];
174
+ if (!user) return null;
175
+ return {
176
+ userId: session.userId,
177
+ username: user.username || session.userId,
178
+ isAdmin: Boolean(user.isAdmin),
179
+ };
180
+ }
181
+
182
+ export function loginOrCreateUser(username, password) {
183
+ const userId = sanitizeAgentflowUserId(username);
184
+ if (!userId) {
185
+ return { ok: false, error: "用户名须以字母开头,仅可使用字母、数字、下划线与连字符,最多 64 字符" };
186
+ }
187
+ const pwd = String(password || "");
188
+ if (pwd.length < 4) return { ok: false, error: "密码至少 4 位" };
189
+
190
+ const users = readAuthUsers();
191
+ const firstUser = Object.keys(users).length === 0;
192
+ let user = users[userId];
193
+ if (!user) {
194
+ const hashed = hashPassword(pwd);
195
+ user = {
196
+ userId,
197
+ username: String(username).trim(),
198
+ salt: hashed.salt,
199
+ hash: hashed.hash,
200
+ isAdmin: firstUser,
201
+ createdAt: new Date().toISOString(),
202
+ };
203
+ users[userId] = user;
204
+ writeJsonObject(usersPath(), users);
205
+ } else if (!verifyPassword(pwd, user)) {
206
+ return { ok: false, error: "用户名或密码错误" };
207
+ }
208
+
209
+ let migration = null;
210
+ if (Boolean(user.isAdmin)) {
211
+ try {
212
+ migration = migrateLegacyPipelinesToAdminUser(userId);
213
+ } catch (e) {
214
+ migration = { copied: [], skipped: [], error: (e && e.message) || String(e) };
215
+ }
216
+ }
217
+
218
+ const token = crypto.randomBytes(32).toString("base64url");
219
+ const sessions = readJsonObject(sessionsPath());
220
+ sessions[hashToken(token)] = {
221
+ userId,
222
+ createdAt: Date.now(),
223
+ expiresAt: Date.now() + SESSION_TTL_MS,
224
+ };
225
+ writeJsonObject(sessionsPath(), sessions);
226
+ return {
227
+ ok: true,
228
+ token,
229
+ user: { userId, username: user.username || userId, isAdmin: Boolean(user.isAdmin) },
230
+ migration,
231
+ };
232
+ }
233
+
234
+ export function logoutRequest(req) {
235
+ const token = parseCookies(req.headers.cookie || "")[SESSION_COOKIE];
236
+ if (!token) return;
237
+ const sessions = readJsonObject(sessionsPath());
238
+ delete sessions[hashToken(token)];
239
+ writeJsonObject(sessionsPath(), sessions);
240
+ }
@@ -278,10 +278,10 @@ description: ${description != null ? String(description).replace(/\n/g, " ") : "
278
278
  }
279
279
  }
280
280
 
281
- export function copyBuiltinJson(workspaceRoot, flowId, targetFlowId) {
281
+ export function copyBuiltinJson(workspaceRoot, flowId, targetFlowId, opts = {}) {
282
282
  const destId = (targetFlowId && targetFlowId.trim()) || flowId;
283
283
  const srcDir = path.join(PACKAGE_BUILTIN_PIPELINES_DIR, flowId);
284
- const pipelinesRoot = getUserPipelinesRoot();
284
+ const pipelinesRoot = getUserPipelinesRoot(opts.userId);
285
285
  const destDir = path.join(pipelinesRoot, destId);
286
286
  if (!fs.existsSync(srcDir) || !fs.existsSync(path.join(srcDir, "flow.yaml"))) {
287
287
  return { success: false, error: t("catalog.builtin_flow_not_found") };
@@ -50,7 +50,7 @@ export function readPipelineListDescription(flowDir) {
50
50
  }
51
51
  }
52
52
 
53
- export function listFlowsJson(workspaceRoot) {
53
+ export function listFlowsJson(workspaceRoot, opts = {}) {
54
54
  const root = path.resolve(workspaceRoot);
55
55
  const out = [];
56
56
  const fromBuiltin = collectPipelineNamesFromDir(PACKAGE_BUILTIN_PIPELINES_DIR);
@@ -59,7 +59,7 @@ export function listFlowsJson(workspaceRoot) {
59
59
  const description = readPipelineListDescription(dir);
60
60
  out.push({ id: name, path: dir, source: "builtin", ...(description ? { description } : {}) });
61
61
  }
62
- const userPipelinesRoot = getUserPipelinesRoot();
62
+ const userPipelinesRoot = getUserPipelinesRoot(opts.userId);
63
63
  const fromUserData = collectPipelineNamesFromDir(userPipelinesRoot);
64
64
  for (const name of fromUserData) {
65
65
  if (name === ARCHIVED_PIPELINES_DIR_NAME) continue;
@@ -129,7 +129,10 @@ function normalizeFrontmatterSlots(arr) {
129
129
  let def = item.default !== undefined && item.default !== null ? item.default : item.value;
130
130
  if (def === undefined || def === null) def = "";
131
131
  else if (typeof def !== "string") def = String(def);
132
- return { type, name, default: def };
132
+ const slot = { type, name, default: def };
133
+ if (item.required != null) slot.required = Boolean(item.required);
134
+ if (item.showOnNode != null) slot.showOnNode = Boolean(item.showOnNode);
135
+ return slot;
133
136
  });
134
137
  }
135
138
 
@@ -200,6 +203,7 @@ export function parseNodeFrontmatter(raw) {
200
203
  export function listNodesJson(workspaceRoot, flowId, flowSource, opts = {}) {
201
204
  const root = path.resolve(workspaceRoot);
202
205
  const archived = Boolean(opts.archived);
206
+ const userPipelinesRoot = getUserPipelinesRoot(opts.userId);
203
207
  const byId = new Map();
204
208
  const pipelineTranslations = {};
205
209
  let marketplaceFlowData = null;
@@ -293,13 +297,9 @@ export function listNodesJson(workspaceRoot, flowId, flowSource, opts = {}) {
293
297
  } catch (_) {}
294
298
  } else if (flowSource === "user") {
295
299
  if (archived) {
296
- addFromDir(
297
- path.join(getUserPipelinesRoot(), ARCHIVED_PIPELINES_DIR_NAME, flowId, "nodes"),
298
- "flow",
299
- flowId,
300
- );
300
+ addFromDir(path.join(userPipelinesRoot, ARCHIVED_PIPELINES_DIR_NAME, flowId, "nodes"), "flow", flowId);
301
301
  } else {
302
- addFromDir(path.join(getUserPipelinesRoot(), flowId, "nodes"), "flow", flowId);
302
+ addFromDir(path.join(userPipelinesRoot, flowId, "nodes"), "flow", flowId);
303
303
  addFromDir(path.join(root, PIPELINES_DIR, flowId, "nodes"), "flow", flowId);
304
304
  addFromDir(path.join(root, LEGACY_PIPELINES_DIR, flowId, "nodes"), "flow", flowId);
305
305
  }
@@ -342,13 +342,14 @@ export function printNodesTable(list) {
342
342
  export function readFlowJson(workspaceRoot, flowId, flowSource, options = {}) {
343
343
  const root = path.resolve(workspaceRoot);
344
344
  const archived = Boolean(options.archived);
345
+ const userPipelinesRoot = getUserPipelinesRoot(options.userId);
345
346
  let flowDir;
346
347
  if (archived) {
347
348
  if (flowSource === "builtin") {
348
349
  return { error: t("catalog.builtin_flow_archive_not_supported") };
349
350
  }
350
351
  if (flowSource === "user") {
351
- flowDir = path.join(getUserPipelinesRoot(), ARCHIVED_PIPELINES_DIR_NAME, flowId);
352
+ flowDir = path.join(userPipelinesRoot, ARCHIVED_PIPELINES_DIR_NAME, flowId);
352
353
  } else if (flowSource === "workspace") {
353
354
  flowDir = path.join(root, PIPELINES_DIR, ARCHIVED_PIPELINES_DIR_NAME, flowId);
354
355
  } else {
@@ -377,7 +378,7 @@ export function readFlowJson(workspaceRoot, flowId, flowSource, options = {}) {
377
378
  if (flowSource === "builtin") {
378
379
  flowDir = path.join(PACKAGE_BUILTIN_PIPELINES_DIR, flowId);
379
380
  } else if (flowSource === "user") {
380
- flowDir = path.join(getUserPipelinesRoot(), flowId);
381
+ flowDir = path.join(userPipelinesRoot, flowId);
381
382
  } else if (flowSource === "workspace") {
382
383
  flowDir = path.join(root, PIPELINES_DIR, flowId);
383
384
  } else {
@@ -422,13 +423,14 @@ export function readFlowJson(workspaceRoot, flowId, flowSource, options = {}) {
422
423
  export function getFlowYamlAbs(workspaceRoot, flowId, flowSource, options = {}) {
423
424
  const root = path.resolve(workspaceRoot);
424
425
  const archived = Boolean(options.archived);
426
+ const userPipelinesRoot = getUserPipelinesRoot(options.userId);
425
427
  let yamlPath;
426
428
  if (archived) {
427
429
  if (flowSource === "builtin") {
428
430
  return { error: t("catalog.builtin_flow_archive_path_not_supported") };
429
431
  }
430
432
  if (flowSource === "user") {
431
- yamlPath = path.join(getUserPipelinesRoot(), ARCHIVED_PIPELINES_DIR_NAME, flowId, "flow.yaml");
433
+ yamlPath = path.join(userPipelinesRoot, ARCHIVED_PIPELINES_DIR_NAME, flowId, "flow.yaml");
432
434
  } else if (flowSource === "workspace") {
433
435
  yamlPath = path.join(root, PIPELINES_DIR, ARCHIVED_PIPELINES_DIR_NAME, flowId, "flow.yaml");
434
436
  if (!fs.existsSync(yamlPath)) {
@@ -447,7 +449,7 @@ export function getFlowYamlAbs(workspaceRoot, flowId, flowSource, options = {})
447
449
  if (flowSource === "builtin") {
448
450
  yamlPath = path.join(PACKAGE_BUILTIN_PIPELINES_DIR, flowId, "flow.yaml");
449
451
  } else if (flowSource === "user") {
450
- yamlPath = path.join(getUserPipelinesRoot(), flowId, "flow.yaml");
452
+ yamlPath = path.join(userPipelinesRoot, flowId, "flow.yaml");
451
453
  if (!fs.existsSync(yamlPath)) {
452
454
  const alt = path.join(root, PIPELINES_DIR, flowId, "flow.yaml");
453
455
  if (fs.existsSync(alt)) yamlPath = alt;
@@ -474,6 +476,7 @@ export function getFlowYamlAbs(workspaceRoot, flowId, flowSource, options = {})
474
476
  export function readNodeJson(workspaceRoot, nodeId, flowId, flowSource, opts = {}) {
475
477
  const root = path.resolve(workspaceRoot);
476
478
  const archived = Boolean(opts.archived);
479
+ const userPipelinesRoot = getUserPipelinesRoot(opts.userId);
477
480
  const marketSpec = parseMarketplaceDefinitionId(nodeId);
478
481
  if (marketSpec) {
479
482
  let flowDir = root;
@@ -515,11 +518,9 @@ export function readNodeJson(workspaceRoot, nodeId, flowId, flowSource, opts = {
515
518
  pathsToTry.push(path.join(PACKAGE_BUILTIN_PIPELINES_DIR, flowId, "nodes", fileName));
516
519
  } else if (flowSource === "user") {
517
520
  if (archived) {
518
- pathsToTry.push(
519
- path.join(getUserPipelinesRoot(), ARCHIVED_PIPELINES_DIR_NAME, flowId, "nodes", fileName),
520
- );
521
+ pathsToTry.push(path.join(userPipelinesRoot, ARCHIVED_PIPELINES_DIR_NAME, flowId, "nodes", fileName));
521
522
  } else {
522
- pathsToTry.push(path.join(getUserPipelinesRoot(), flowId, "nodes", fileName));
523
+ pathsToTry.push(path.join(userPipelinesRoot, flowId, "nodes", fileName));
523
524
  pathsToTry.push(path.join(root, PIPELINES_DIR, flowId, "nodes", fileName));
524
525
  pathsToTry.push(path.join(root, LEGACY_PIPELINES_DIR, flowId, "nodes", fileName));
525
526
  }
@@ -568,6 +569,184 @@ export function readNodeJson(workspaceRoot, nodeId, flowId, flowSource, opts = {
568
569
  return { error: "Node not found: " + nodeId };
569
570
  }
570
571
 
572
+ const NODE_DETAIL_MAX_FILES = 300;
573
+ const NODE_FILE_MAX_BYTES = 256 * 1024;
574
+
575
+ function isTextPreviewPath(filePath) {
576
+ const ext = path.extname(filePath).toLowerCase();
577
+ return [
578
+ ".md", ".mdx", ".txt", ".json", ".yaml", ".yml", ".js", ".mjs", ".cjs", ".ts", ".tsx", ".jsx",
579
+ ".py", ".sh", ".css", ".html", ".xml", ".toml", ".ini", ".env", ".sql",
580
+ ].includes(ext);
581
+ }
582
+
583
+ function listNodeFiles(baseDir, allowAllFiles, primaryFilePath = "") {
584
+ const root = path.resolve(baseDir);
585
+ const out = [];
586
+ const addFile = (filePath) => {
587
+ if (out.length >= NODE_DETAIL_MAX_FILES) return;
588
+ try {
589
+ const stat = fs.statSync(filePath);
590
+ if (!stat.isFile()) return;
591
+ const rel = path.relative(root, filePath).replace(/\\/g, "/");
592
+ out.push({
593
+ path: rel,
594
+ size: stat.size,
595
+ previewable: isTextPreviewPath(filePath),
596
+ });
597
+ } catch (_) {}
598
+ };
599
+ if (!allowAllFiles) {
600
+ if (primaryFilePath) addFile(primaryFilePath);
601
+ return out;
602
+ }
603
+ const walk = (dir) => {
604
+ if (out.length >= NODE_DETAIL_MAX_FILES) return;
605
+ let entries = [];
606
+ try {
607
+ entries = fs.readdirSync(dir, { withFileTypes: true });
608
+ } catch {
609
+ return;
610
+ }
611
+ entries.sort((a, b) => a.name.localeCompare(b.name));
612
+ for (const entry of entries) {
613
+ if (out.length >= NODE_DETAIL_MAX_FILES) return;
614
+ if (entry.name === "node_modules" || entry.name === ".git") continue;
615
+ const p = path.join(dir, entry.name);
616
+ if (entry.isDirectory()) walk(p);
617
+ else if (entry.isFile()) addFile(p);
618
+ }
619
+ };
620
+ walk(root);
621
+ return out;
622
+ }
623
+
624
+ function resolveMarkdownNodeFile(workspaceRoot, nodeId, flowId, flowSource, opts = {}) {
625
+ const root = path.resolve(workspaceRoot);
626
+ const archived = Boolean(opts.archived);
627
+ const userPipelinesRoot = getUserPipelinesRoot(opts.userId);
628
+ const fileName = nodeId.endsWith(".md") ? nodeId : `${nodeId}.md`;
629
+ const pathsToTry = [];
630
+ if (flowId && flowSource) {
631
+ if (flowSource === "builtin") {
632
+ pathsToTry.push(path.join(PACKAGE_BUILTIN_PIPELINES_DIR, flowId, "nodes", fileName));
633
+ } else if (flowSource === "user") {
634
+ if (archived) {
635
+ pathsToTry.push(path.join(userPipelinesRoot, ARCHIVED_PIPELINES_DIR_NAME, flowId, "nodes", fileName));
636
+ } else {
637
+ pathsToTry.push(path.join(userPipelinesRoot, flowId, "nodes", fileName));
638
+ pathsToTry.push(path.join(root, PIPELINES_DIR, flowId, "nodes", fileName));
639
+ pathsToTry.push(path.join(root, LEGACY_PIPELINES_DIR, flowId, "nodes", fileName));
640
+ }
641
+ } else if (flowSource === "workspace") {
642
+ if (archived) {
643
+ pathsToTry.push(path.join(root, PIPELINES_DIR, ARCHIVED_PIPELINES_DIR_NAME, flowId, "nodes", fileName));
644
+ pathsToTry.push(path.join(root, LEGACY_PIPELINES_DIR, ARCHIVED_PIPELINES_DIR_NAME, flowId, "nodes", fileName));
645
+ } else {
646
+ pathsToTry.push(path.join(root, PIPELINES_DIR, flowId, "nodes", fileName));
647
+ pathsToTry.push(path.join(root, LEGACY_PIPELINES_DIR, flowId, "nodes", fileName));
648
+ }
649
+ }
650
+ }
651
+ pathsToTry.push(path.join(root, PROJECT_NODES_DIR, fileName));
652
+ pathsToTry.push(path.join(root, LEGACY_NODES_DIR, fileName));
653
+ pathsToTry.push(path.join(PACKAGE_BUILTIN_NODES_DIR, fileName));
654
+ return pathsToTry.find((p) => fs.existsSync(p) && fs.statSync(p).isFile()) || "";
655
+ }
656
+
657
+ function readNodeUsage(workspaceRoot, nodeId, opts = {}) {
658
+ const usage = [];
659
+ for (const flow of listFlowsJson(workspaceRoot, opts)) {
660
+ const flowPath = getFlowYamlAbs(workspaceRoot, flow.id, flow.source || "user", { archived: Boolean(flow.archived), userId: opts.userId });
661
+ if (!flowPath.path) continue;
662
+ try {
663
+ const data = yaml.load(fs.readFileSync(flowPath.path, "utf-8"));
664
+ const instances = data && typeof data === "object" ? data.instances : null;
665
+ if (!instances || typeof instances !== "object") continue;
666
+ const hits = Object.entries(instances)
667
+ .filter(([, inst]) => inst && inst.definitionId === nodeId)
668
+ .map(([instanceId, inst]) => ({ instanceId, label: inst.label || instanceId }));
669
+ if (hits.length > 0) {
670
+ usage.push({ flowId: flow.id, flowSource: flow.source || "user", archived: Boolean(flow.archived), instances: hits });
671
+ }
672
+ } catch (_) {}
673
+ }
674
+ return usage;
675
+ }
676
+
677
+ function resolveNodeFileScope(workspaceRoot, nodeId, flowId, flowSource, opts = {}) {
678
+ const marketSpec = parseMarketplaceDefinitionId(nodeId);
679
+ if (marketSpec) {
680
+ let flowDir = path.resolve(workspaceRoot);
681
+ let flowData = null;
682
+ if (flowId && flowSource) {
683
+ const flowPath = getFlowYamlAbs(workspaceRoot, flowId, flowSource, opts);
684
+ if (flowPath.path) {
685
+ flowDir = path.dirname(flowPath.path);
686
+ try {
687
+ const parsed = yaml.load(fs.readFileSync(flowPath.path, "utf-8"));
688
+ if (parsed && typeof parsed === "object") flowData = parsed;
689
+ } catch (_) {}
690
+ }
691
+ }
692
+ const resolved = resolveMarketplaceNodePackage(workspaceRoot, flowDir, nodeId, flowData);
693
+ if (!resolved) return null;
694
+ return { baseDir: resolved.packageDir, allowAllFiles: true, primaryFilePath: path.join(resolved.packageDir, "node.yaml"), manifest: resolved };
695
+ }
696
+ const filePath = resolveMarkdownNodeFile(workspaceRoot, nodeId, flowId, flowSource, opts);
697
+ if (!filePath) return null;
698
+ return { baseDir: path.dirname(filePath), allowAllFiles: false, primaryFilePath: filePath, manifest: null };
699
+ }
700
+
701
+ export function readNodeDetailJson(workspaceRoot, nodeId, flowId = "", flowSource = "", opts = {}) {
702
+ if (!nodeId) return { error: "Missing node id" };
703
+ const node = readNodeJson(workspaceRoot, nodeId, flowId, flowSource, opts);
704
+ if (node.error) return node;
705
+ const scope = resolveNodeFileScope(workspaceRoot, nodeId, flowId, flowSource, opts);
706
+ const files = scope ? listNodeFiles(scope.baseDir, scope.allowAllFiles, scope.primaryFilePath) : [];
707
+ return {
708
+ node: { id: nodeId, ...node },
709
+ readOnly: true,
710
+ manifest: scope?.manifest || null,
711
+ runtime: node.runtime || scope?.manifest?.runtime || null,
712
+ body: node.executionLogic || "",
713
+ baseDir: scope?.baseDir || "",
714
+ files,
715
+ usage: readNodeUsage(workspaceRoot, nodeId, opts),
716
+ };
717
+ }
718
+
719
+ export function readNodeFilePreview(workspaceRoot, nodeId, relPath, flowId = "", flowSource = "", opts = {}) {
720
+ if (!nodeId) return { error: "Missing node id" };
721
+ const rel = String(relPath || "").trim();
722
+ if (!rel || rel.includes("\0") || path.isAbsolute(rel) || rel.split(/[\\/]+/).includes("..")) {
723
+ return { error: "Invalid file path" };
724
+ }
725
+ const scope = resolveNodeFileScope(workspaceRoot, nodeId, flowId, flowSource, opts);
726
+ if (!scope) return { error: "Node files not found" };
727
+ if (!scope.allowAllFiles) {
728
+ const primaryRel = path.relative(scope.baseDir, scope.primaryFilePath).replace(/\\/g, "/");
729
+ if (rel !== primaryRel) return { error: "File is outside node preview scope" };
730
+ }
731
+ const base = path.resolve(scope.baseDir);
732
+ const abs = path.resolve(base, rel);
733
+ if (abs !== base && !abs.startsWith(base + path.sep)) return { error: "File is outside node package" };
734
+ if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) return { error: "File not found" };
735
+ const stat = fs.statSync(abs);
736
+ if (!isTextPreviewPath(abs)) {
737
+ return { path: rel, size: stat.size, binary: true, content: "", truncated: false };
738
+ }
739
+ const fd = fs.openSync(abs, "r");
740
+ try {
741
+ const len = Math.min(stat.size, NODE_FILE_MAX_BYTES);
742
+ const buf = Buffer.alloc(len);
743
+ fs.readSync(fd, buf, 0, len, 0);
744
+ return { path: rel, size: stat.size, binary: false, content: buf.toString("utf-8"), truncated: stat.size > len };
745
+ } finally {
746
+ fs.closeSync(fd);
747
+ }
748
+ }
749
+
571
750
  /** 列出所有 pipeline(包内 builtin + ~/agentflow/pipelines + 项目内 .workspace/.cursor agentflow/pipelines);nodes 见 PROJECT_NODES_DIR / LEGACY_NODES_DIR */
572
751
  export function listPipelines(workspaceRoot) {
573
752
  const rows = listFlowsJson(workspaceRoot);
@@ -6,7 +6,8 @@
6
6
  */
7
7
  import fs from "fs";
8
8
  import path from "path";
9
- import { getAgentflowDataRoot } from "./paths.mjs";
9
+ import { getAgentflowDataRoot, sanitizeAgentflowUserId } from "./paths.mjs";
10
+ import { readUserEnvObject } from "./user-env.mjs";
10
11
  import { resolveCliAndModel } from "./model-config.mjs";
11
12
  import { runClaudeCodeAgentWithPrompt, runCursorAgentWithPrompt, runOpenCodeAgentWithPrompt } from "./agent-runners.mjs";
12
13
  import { planComposerTasks, hasPlannerApiAvailable, shouldUsePhased, classifyComplexity, classifyTaskComplexity, PHASED_DEFINITIONS } from "./composer-planner.mjs";
@@ -23,6 +24,11 @@ const MAX_PROMPT_CHARS = 500_000;
23
24
  const MAX_COMPOSER_VALIDATION_REPAIR = 5;
24
25
  const MAX_SCRIPT_INJECT_BYTES = 30_000;
25
26
 
27
+ function agentflowUserEnv(userId) {
28
+ const safe = sanitizeAgentflowUserId(userId);
29
+ return safe ? { ...readUserEnvObject(safe), AGENTFLOW_USER_ID: safe } : {};
30
+ }
31
+
26
32
  // ─── script 内容注入辅助 ─────────────────────────────────────────────────
27
33
 
28
34
  /**
@@ -113,10 +119,12 @@ export function startComposerAgent(opts) {
113
119
  const cliWs = opts.cliWorkspace ? String(opts.cliWorkspace) : getAgentflowDataRoot();
114
120
  const modelKey = opts.modelKey != null ? String(opts.modelKey).trim() : "";
115
121
  const { cli, model } = resolveCliAndModel(uiRoot, modelKey || null, null);
122
+ const env = agentflowUserEnv(opts.agentflowUserId);
116
123
 
117
124
  const common = {
118
125
  onStreamEvent: opts.onStreamEvent,
119
126
  force: Boolean(opts.force),
127
+ env,
120
128
  };
121
129
 
122
130
  if (cli === "opencode") {
@@ -185,6 +193,10 @@ function buildAgentStepPrompt(step, flowContext) {
185
193
  parts.push(`- flowId:${flowContext.flowId}`);
186
194
  parts.push(`- flowSource:${flowContext.flowSource || "user"}`);
187
195
  }
196
+ if (flowContext.userId) {
197
+ parts.push(`- agentflow 用户:${flowContext.userId}`);
198
+ parts.push(`- 执行 agentflow 命令时保留当前环境中的 AGENTFLOW_USER_ID,必要时显式前置 AGENTFLOW_USER_ID='${flowContext.userId}'。`);
199
+ }
188
200
  if (flowContext.skillsHint) {
189
201
  parts.push(flowContext.skillsHint);
190
202
  }
@@ -344,6 +356,7 @@ export async function runComposerPostFlowValidationAndRepair(opts) {
344
356
  const flowYamlAbs = String(opts.flowYamlAbs || "").trim();
345
357
  const cliWs = opts.cliWorkspace ? String(opts.cliWorkspace) : getAgentflowDataRoot();
346
358
  const maxRepair = Math.max(1, Math.min(10, Number(opts.maxRepairAttempts) || MAX_COMPOSER_VALIDATION_REPAIR));
359
+ const env = agentflowUserEnv(opts.agentflowUserId || opts.flowContext?.userId);
347
360
 
348
361
  if (!uiRoot || !flowYamlAbs) {
349
362
  return { ok: true, result: { skipped: true } };
@@ -405,6 +418,7 @@ export async function runComposerPostFlowValidationAndRepair(opts) {
405
418
  onStreamEvent: stepEmit,
406
419
  model: model || undefined,
407
420
  force: Boolean(opts.force),
421
+ env,
408
422
  });
409
423
  setChild(handle.child);
410
424
  await handle.finished;
@@ -413,6 +427,7 @@ export async function runComposerPostFlowValidationAndRepair(opts) {
413
427
  onStreamEvent: stepEmit,
414
428
  model: model || undefined,
415
429
  force: Boolean(opts.force),
430
+ env,
416
431
  });
417
432
  setChild(handle.child);
418
433
  await handle.finished;
@@ -421,6 +436,7 @@ export async function runComposerPostFlowValidationAndRepair(opts) {
421
436
  onStreamEvent: stepEmit,
422
437
  model: model || undefined,
423
438
  force: Boolean(opts.force),
439
+ env,
424
440
  });
425
441
  setChild(handle.child);
426
442
  await handle.finished;
@@ -490,6 +506,7 @@ export function startComposerMultiStep(opts) {
490
506
  const emit = typeof opts.onStreamEvent === "function" ? opts.onStreamEvent : () => {};
491
507
  let aborted = false;
492
508
  let currentChild = null;
509
+ const env = agentflowUserEnv(opts.agentflowUserId || opts.flowContext?.userId);
493
510
 
494
511
  const abort = () => {
495
512
  aborted = true;
@@ -674,6 +691,7 @@ export function startComposerMultiStep(opts) {
674
691
  onStreamEvent: stepEmit,
675
692
  model: model || undefined,
676
693
  force: Boolean(opts.force),
694
+ env,
677
695
  });
678
696
  currentChild = handle.child;
679
697
  await handle.finished;
@@ -682,6 +700,7 @@ export function startComposerMultiStep(opts) {
682
700
  onStreamEvent: stepEmit,
683
701
  model: model || undefined,
684
702
  force: Boolean(opts.force),
703
+ env,
685
704
  });
686
705
  currentChild = handle.child;
687
706
  await handle.finished;
@@ -690,6 +709,7 @@ export function startComposerMultiStep(opts) {
690
709
  onStreamEvent: stepEmit,
691
710
  model: model || undefined,
692
711
  force: Boolean(opts.force),
712
+ env,
693
713
  });
694
714
  currentChild = handle.child;
695
715
  await handle.finished;
@@ -757,6 +777,7 @@ export function startComposerMultiStep(opts) {
757
777
  flowContext: opts.flowContext,
758
778
  modelKey: opts.modelKey,
759
779
  force: Boolean(opts.force),
780
+ agentflowUserId: opts.agentflowUserId || opts.flowContext?.userId,
760
781
  onStreamEvent: emit,
761
782
  getAborted: () => aborted,
762
783
  setCurrentChild: (c) => {