@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
package/bin/lib/paths.mjs CHANGED
@@ -34,6 +34,56 @@ export function getAgentflowDataRoot() {
34
34
  return path.join(os.homedir(), "agentflow");
35
35
  }
36
36
 
37
+ export const AGENTFLOW_DEFAULT_USER_ID = "";
38
+
39
+ export function sanitizeAgentflowUserId(userId) {
40
+ const raw = userId == null ? "" : String(userId).trim();
41
+ if (!raw) return AGENTFLOW_DEFAULT_USER_ID;
42
+ const normalized = raw.toLowerCase();
43
+ if (!/^[a-z][a-z0-9_-]{0,63}$/.test(normalized)) return AGENTFLOW_DEFAULT_USER_ID;
44
+ return normalized;
45
+ }
46
+
47
+ export function getAgentflowUserDataRoot(userId) {
48
+ const safe = sanitizeAgentflowUserId(userId ?? process.env.AGENTFLOW_USER_ID);
49
+ if (!safe) return getAgentflowDataRoot();
50
+ return path.join(getAgentflowDataRoot(), "users", safe);
51
+ }
52
+
53
+ export function listAgentflowUserIds() {
54
+ const usersRoot = path.join(getAgentflowDataRoot(), "users");
55
+ try {
56
+ if (!fs.existsSync(usersRoot) || !fs.statSync(usersRoot).isDirectory()) return [];
57
+ return fs.readdirSync(usersRoot, { withFileTypes: true })
58
+ .filter((entry) => entry.isDirectory())
59
+ .map((entry) => sanitizeAgentflowUserId(entry.name))
60
+ .filter(Boolean)
61
+ .sort((a, b) => a.localeCompare(b));
62
+ } catch {
63
+ return [];
64
+ }
65
+ }
66
+
67
+ export function getAgentflowUserContexts() {
68
+ const ids = listAgentflowUserIds();
69
+ return [{}, ...ids.map((userId) => ({ userId }))];
70
+ }
71
+
72
+ export function resolveUniqueUserPipelineDir(flowName) {
73
+ const name = flowName == null ? "" : String(flowName).trim();
74
+ if (!name) return null;
75
+ const matches = [];
76
+ for (const userId of listAgentflowUserIds()) {
77
+ const dir = path.join(getAgentflowDataRoot(), "users", userId, "pipelines", name);
78
+ try {
79
+ if (fs.existsSync(path.join(dir, "flow.yaml"))) matches.push(dir);
80
+ } catch {
81
+ /* ignore unreadable user dirs */
82
+ }
83
+ }
84
+ return matches.length === 1 ? matches[0] : null;
85
+ }
86
+
37
87
  /** 项目内 runBuild 根目录:`<workspaceRoot>/.workspace/agentflow/runBuild`(legacy:写入路径已迁至 `<flowDir>/runBuild`,仅用于兼容读取) */
38
88
  export function getWorkspaceRunBuildRoot(workspaceRoot) {
39
89
  const root =
@@ -52,23 +102,21 @@ export function getLegacyUserRunBuildRoot() {
52
102
  * 统一 runtime root:每个 flow 的 pipeline 源、scripts、runBuild 共用一个根目录。
53
103
  * - 若 `~/agentflow/pipelines/<name>/flow.yaml` 存在 → user-scope:`~/agentflow/pipelines/<name>`
54
104
  * - 若 `<ws>/.workspace/agentflow/pipelines/<name>/flow.yaml` 存在 → workspace-scope:`<ws>/.workspace/agentflow/pipelines/<name>`
55
- * - archived(`_archived/<name>`)按对应 scope 返回
56
105
  * - 其他(builtin 只读 / 不存在)→ 默认 user-scope 路径(首次 run 时自动创建,builtin 源仍从包内读取但 runBuild 落到用户目录)
106
+ * 归档 flow 不参与普通 runtime root 解析,避免同名 archived flow 被误当作活跃 flow。
57
107
  */
58
- export function getFlowRuntimeRoot(workspaceRoot, flowName) {
108
+ export function getFlowRuntimeRoot(workspaceRoot, flowName, opts = {}) {
59
109
  const root =
60
110
  workspaceRoot != null && String(workspaceRoot).trim() !== ""
61
111
  ? path.resolve(String(workspaceRoot))
62
112
  : process.cwd();
63
- const userRoot = getUserPipelinesRoot();
113
+ const userRoot = getUserPipelinesRoot(opts.userId);
64
114
  const userDir = path.join(userRoot, flowName);
65
115
  if (fs.existsSync(path.join(userDir, "flow.yaml"))) return userDir;
66
- const userArchived = path.join(userRoot, ARCHIVED_PIPELINES_DIR_NAME, flowName);
67
- if (fs.existsSync(path.join(userArchived, "flow.yaml"))) return userArchived;
116
+ const inferredUserDir = resolveUniqueUserPipelineDir(flowName);
117
+ if (inferredUserDir) return inferredUserDir;
68
118
  const wsDir = path.join(root, PIPELINES_DIR, flowName);
69
119
  if (fs.existsSync(path.join(wsDir, "flow.yaml"))) return wsDir;
70
- const wsArchived = path.join(root, PIPELINES_DIR, ARCHIVED_PIPELINES_DIR_NAME, flowName);
71
- if (fs.existsSync(path.join(wsArchived, "flow.yaml"))) return wsArchived;
72
120
  // builtin / legacy / 尚未落盘 → 默认 user 目录,runBuild 首次写入时创建
73
121
  return userDir;
74
122
  }
@@ -78,8 +126,8 @@ export function getFlowRuntimeRoot(workspaceRoot, flowName) {
78
126
  * 新运行走 `<flowRuntimeRoot>/runBuild/<uuid>`;
79
127
  * 若该 uuid 在旧位置(legacy workspace/user runBuild 根)已存在,则返回旧位置,保留 resume 兼容。
80
128
  */
81
- export function getRunDir(workspaceRoot, flowName, uuid) {
82
- const candidates = getRunDirCandidates(workspaceRoot, flowName, uuid);
129
+ export function getRunDir(workspaceRoot, flowName, uuid, opts = {}) {
130
+ const candidates = getRunDirCandidates(workspaceRoot, flowName, uuid, opts);
83
131
  for (const c of candidates) {
84
132
  if (fs.existsSync(c)) return c;
85
133
  }
@@ -93,9 +141,9 @@ export function getRunDir(workspaceRoot, flowName, uuid) {
93
141
  * 2. 旧 workspace:<ws>/.workspace/agentflow/runBuild/<flow>/<uuid>
94
142
  * 3. 旧 user:~/agentflow/runBuild/<flow>/<uuid>
95
143
  */
96
- export function getRunDirCandidates(workspaceRoot, flowName, uuid) {
144
+ export function getRunDirCandidates(workspaceRoot, flowName, uuid, opts = {}) {
97
145
  const candidates = [
98
- path.join(getFlowRuntimeRoot(workspaceRoot, flowName), "runBuild", uuid),
146
+ path.join(getFlowRuntimeRoot(workspaceRoot, flowName, opts), "runBuild", uuid),
99
147
  path.join(getWorkspaceRunBuildRoot(workspaceRoot), flowName, uuid),
100
148
  path.join(getLegacyUserRunBuildRoot(), flowName, uuid),
101
149
  ];
@@ -110,8 +158,8 @@ export function getRunDirCandidates(workspaceRoot, flowName, uuid) {
110
158
  return out;
111
159
  }
112
160
 
113
- export function getUserPipelinesRoot() {
114
- return path.join(getAgentflowDataRoot(), "pipelines");
161
+ export function getUserPipelinesRoot(userId) {
162
+ return path.join(getAgentflowUserDataRoot(userId), "pipelines");
115
163
  }
116
164
 
117
165
  export function getReferenceRootAbs() {
@@ -134,6 +182,10 @@ export function getAgentflowUserConfigAbs() {
134
182
  return path.join(getAgentflowDataRoot(), "config.json");
135
183
  }
136
184
 
185
+ export function getAgentflowUserEnvAbs(userId) {
186
+ return path.join(getAgentflowUserDataRoot(userId), "env.json");
187
+ }
188
+
137
189
  /** CLI / UI 文案用 */
138
190
  export const USER_AGENTFLOW_DIR_LABEL = "~/agentflow";
139
191
  export const USER_AGENTFLOW_PIPELINES_LABEL = "~/agentflow/pipelines";
@@ -204,8 +256,12 @@ export const LOCAL_ONLY_DEFINITION_IDS = new Set([
204
256
  "control_deadline",
205
257
  "control_cancelled",
206
258
  "control_interval_loop",
259
+ "control_cd_workspace",
260
+ "control_user_workspace",
261
+ "control_load_skills",
207
262
  "control_start",
208
263
  "control_end",
264
+ "tool_git_checkout",
209
265
  "tool_print",
210
266
  "tool_user_check",
211
267
  "tool_user_ask",
@@ -151,9 +151,9 @@ function mapFlowSource(src) {
151
151
  * @param {string} workspaceRoot
152
152
  * @returns {Array<{ flowId: string, flowSource: 'workspace'|'user', runId: string, at: number, durationMs: number, endedAt: number|null, status: 'success'|'failed'|'running'|'stopped'|'interrupted'|'unknown' }>}
153
153
  */
154
- export function listRecentRunsFromDisk(workspaceRoot) {
154
+ export function listRecentRunsFromDisk(workspaceRoot, opts = {}) {
155
155
  const out = [];
156
- for (const { flowName, uuid, runDir, source } of listAllRunDirs(workspaceRoot)) {
156
+ for (const { flowName, uuid, runDir, source } of listAllRunDirs(workspaceRoot, opts)) {
157
157
  let at = readKeyFromMemory(runDir, "runStartTime");
158
158
  if (at == null) {
159
159
  try {
@@ -36,8 +36,8 @@ function parseElapsedMsLine(filePath) {
36
36
  * @param {string} uuid
37
37
  * @returns {Record<string, { status: string, elapsed?: string }>}
38
38
  */
39
- export function getRunNodeStatusesFromDisk(workspaceRoot, flowName, uuid) {
40
- const runDir = getRunDir(workspaceRoot, flowName, uuid);
39
+ export function getRunNodeStatusesFromDisk(workspaceRoot, flowName, uuid, opts = {}) {
40
+ const runDir = getRunDir(workspaceRoot, flowName, uuid, opts);
41
41
  const flowJsonPath = path.join(runDir, "intermediate", "flow.json");
42
42
  if (!fs.existsSync(flowJsonPath)) return {};
43
43
 
@@ -51,7 +51,7 @@ export function getRunNodeStatusesFromDisk(workspaceRoot, flowName, uuid) {
51
51
 
52
52
  const order = Array.isArray(flow.order) ? flow.order : [];
53
53
  const nodeDefinitions = flow.nodeDefinitions && typeof flow.nodeDefinitions === "object" ? flow.nodeDefinitions : {};
54
- const execIdMap = loadAllExecIds(workspaceRoot, flowName, uuid, order);
54
+ const execIdMap = loadAllExecIds(workspaceRoot, flowName, uuid, order, opts);
55
55
  const intermediateDir = path.join(runDir, "intermediate");
56
56
  /** @type {Record<string, { status: string, elapsed?: string }>} */
57
57
  const out = {};
@@ -0,0 +1,225 @@
1
+ import path from "path";
2
+ import { getFlowDir } from "./workspace.mjs";
3
+ import { PACKAGE_ROOT, PIPELINES_DIR } from "./paths.mjs";
4
+ import { listSkills, listSkillsFromSources, workspaceSkillSources } from "./skill-registry.mjs";
5
+
6
+ function parseJsonObject(raw) {
7
+ if (raw == null) return null;
8
+ if (typeof raw === "object" && !Array.isArray(raw)) return raw;
9
+ const text = String(raw || "").trim();
10
+ if (!text) return null;
11
+ try {
12
+ const parsed = JSON.parse(text);
13
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
14
+ } catch {
15
+ return null;
16
+ }
17
+ }
18
+
19
+ export function getPipelineFlowDir(workspaceRoot, flowName, flowJson = null) {
20
+ if (flowJson?.flowDir && typeof flowJson.flowDir === "string" && flowJson.flowDir.trim()) {
21
+ return path.isAbsolute(flowJson.flowDir) ? path.resolve(flowJson.flowDir) : path.resolve(workspaceRoot, flowJson.flowDir);
22
+ }
23
+ return getFlowDir(workspaceRoot, flowName) || path.join(path.resolve(workspaceRoot), PIPELINES_DIR, flowName);
24
+ }
25
+
26
+ export function buildDefaultWorkspaceContext(workspaceRoot, flowName, flowJson = null) {
27
+ const pipelineWorkspace = path.resolve(workspaceRoot);
28
+ const flowDir = getPipelineFlowDir(workspaceRoot, flowName, flowJson);
29
+ return {
30
+ version: 1,
31
+ label: "pipeline",
32
+ cwd: pipelineWorkspace,
33
+ workspaceRoot: pipelineWorkspace,
34
+ pipelineWorkspace,
35
+ flowDir,
36
+ previous: null,
37
+ };
38
+ }
39
+
40
+ export function normalizeWorkspaceContext(raw, workspaceRoot, flowName, flowJson = null) {
41
+ const base = buildDefaultWorkspaceContext(workspaceRoot, flowName, flowJson);
42
+ const parsed = parseJsonObject(raw);
43
+ if (!parsed) return base;
44
+ const cwdRaw = parsed.cwd || parsed.workspaceRoot || parsed.path || "";
45
+ const cwd = cwdRaw ? path.resolve(String(cwdRaw)) : base.cwd;
46
+ return {
47
+ ...base,
48
+ ...parsed,
49
+ version: 1,
50
+ cwd,
51
+ workspaceRoot: cwd,
52
+ pipelineWorkspace: path.resolve(parsed.pipelineWorkspace || base.pipelineWorkspace),
53
+ flowDir: path.resolve(parsed.flowDir || base.flowDir),
54
+ previous: parsed.previous && typeof parsed.previous === "object" ? parsed.previous : null,
55
+ };
56
+ }
57
+
58
+ export function normalizeSkillsContext(raw) {
59
+ const parsed = parseJsonObject(raw);
60
+ if (!parsed) return null;
61
+ return {
62
+ version: 1,
63
+ ...parsed,
64
+ skills: Array.isArray(parsed.skills) ? parsed.skills : [],
65
+ skillKeys: Array.isArray(parsed.skillKeys) ? parsed.skillKeys : [],
66
+ sources: Array.isArray(parsed.sources) ? parsed.sources : [],
67
+ warnings: Array.isArray(parsed.warnings) ? parsed.warnings : [],
68
+ };
69
+ }
70
+
71
+ export function expandRuntimePlaceholders(text, workspaceContext, extra = {}) {
72
+ if (text == null) return "";
73
+ const raw = String(text).trim();
74
+ if (!raw) return "";
75
+ const values = {
76
+ workspaceRoot: workspaceContext.workspaceRoot || workspaceContext.cwd || "",
77
+ cwd: workspaceContext.cwd || workspaceContext.workspaceRoot || "",
78
+ pipelineWorkspace: workspaceContext.pipelineWorkspace || "",
79
+ flowDir: workspaceContext.flowDir || "",
80
+ ...extra,
81
+ };
82
+ return raw.replace(/\$\{([^}]+)\}/g, (_, key) => {
83
+ const k = String(key || "").trim();
84
+ return values[k] != null ? String(values[k]) : "";
85
+ });
86
+ }
87
+
88
+ export function resolveWorkspaceTarget(rawTarget, workspaceContext, extra = {}) {
89
+ const expanded = expandRuntimePlaceholders(rawTarget, workspaceContext, extra).trim();
90
+ if (!expanded || expanded === "pipeline" || expanded === "pipeline-workspace") {
91
+ return workspaceContext.pipelineWorkspace;
92
+ }
93
+ if (expanded === "current" || expanded === ".") return workspaceContext.cwd;
94
+ if (expanded === "flowDir") return workspaceContext.flowDir;
95
+ return path.isAbsolute(expanded) ? path.resolve(expanded) : path.resolve(workspaceContext.cwd, expanded);
96
+ }
97
+
98
+ export function scanSkillsFromPaths(paths, opts = {}) {
99
+ const sources = (paths || []).map((entry, index) => ({
100
+ source: entry.source || `runtime-${index}`,
101
+ sourceLabel: entry.sourceLabel || entry.label || entry.dir,
102
+ dir: path.resolve(entry.dir),
103
+ installedBy: entry.installedBy || "runtime",
104
+ }));
105
+ return listSkillsFromSources(sources, { ...opts, warnMissing: true });
106
+ }
107
+
108
+ export function parseSkillKeyList(raw) {
109
+ if (Array.isArray(raw)) return raw.map((x) => String(x || "").trim()).filter(Boolean);
110
+ return String(raw || "")
111
+ .split(/[\s,]+/)
112
+ .map((x) => x.trim())
113
+ .filter(Boolean);
114
+ }
115
+
116
+ function skillBodyFromRegistryItem(skill) {
117
+ return {
118
+ name: skill.name,
119
+ key: skill.key,
120
+ description: skill.description,
121
+ source: skill.source,
122
+ sourceLabel: skill.sourceLabel,
123
+ path: skill.path,
124
+ body: skill.body,
125
+ };
126
+ }
127
+
128
+ export function buildSkillsContextFromRegistry({ workspaceContext, skillKeys = [], mergeMode = "replace" }) {
129
+ const wc = workspaceContext;
130
+ const wanted = parseSkillKeyList(skillKeys);
131
+ const registryWorkspaceRoot = wc.pipelineWorkspace || wc.workspaceRoot || wc.cwd || process.cwd();
132
+ const registry = listSkills(PACKAGE_ROOT, registryWorkspaceRoot);
133
+ const skillBodies = [];
134
+ const warnings = [];
135
+ const seenKeys = new Set();
136
+
137
+ for (const raw of wanted) {
138
+ const key = String(raw || "").trim();
139
+ if (!key) continue;
140
+ const match = registry.find((skill) => skill.key === key || skill.name === key || skill.id === key);
141
+ if (!match) {
142
+ warnings.push(`skill not found: ${key}`);
143
+ continue;
144
+ }
145
+ const dedupeKey = match.key || match.path || match.name;
146
+ if (seenKeys.has(dedupeKey)) continue;
147
+ seenKeys.add(dedupeKey);
148
+ skillBodies.push(skillBodyFromRegistryItem(match));
149
+ }
150
+
151
+ return {
152
+ version: 1,
153
+ workspaceRoot: wc.workspaceRoot,
154
+ cwd: wc.cwd,
155
+ pipelineWorkspace: wc.pipelineWorkspace,
156
+ flowDir: wc.flowDir,
157
+ source: "public-registry",
158
+ mergeMode,
159
+ requestedSkillKeys: wanted,
160
+ skills: skillBodies.map(({ body, ...meta }) => meta),
161
+ skillKeys: skillBodies.map((s) => s.key),
162
+ sources: [...new Set(skillBodies.map((s) => s.sourceLabel || s.source || "").filter(Boolean))],
163
+ loadedCount: skillBodies.length,
164
+ warnings,
165
+ skillBodies,
166
+ };
167
+ }
168
+
169
+ export function buildSkillsContext({ workspaceContext, source = "current-workspace", paths = [], include = [], exclude = [], mergeMode = "replace" }) {
170
+ const wc = workspaceContext;
171
+ const sourcePaths = [];
172
+ const addWorkspace = (root, label) => {
173
+ sourcePaths.push(...workspaceSkillSources(root, label, label));
174
+ };
175
+ if (source === "pipeline-workspace") {
176
+ addWorkspace(wc.pipelineWorkspace, "pipeline");
177
+ } else if (source === "explicit-paths") {
178
+ for (const p of paths) {
179
+ const resolved = path.isAbsolute(p) ? path.resolve(p) : path.resolve(wc.cwd, p);
180
+ sourcePaths.push({ dir: resolved, label: "explicit" });
181
+ }
182
+ } else if (source === "all") {
183
+ addWorkspace(wc.cwd, "current");
184
+ if (path.resolve(wc.cwd) !== path.resolve(wc.pipelineWorkspace)) addWorkspace(wc.pipelineWorkspace, "pipeline");
185
+ } else {
186
+ addWorkspace(wc.cwd, "current");
187
+ }
188
+ const scanned = scanSkillsFromPaths(sourcePaths, { include, exclude });
189
+ return {
190
+ version: 1,
191
+ workspaceRoot: wc.workspaceRoot,
192
+ cwd: wc.cwd,
193
+ source,
194
+ mergeMode,
195
+ skills: scanned.skills.map(({ body, ...meta }) => meta),
196
+ skillKeys: scanned.skills.map((s) => s.key),
197
+ sources: sourcePaths.map((p) => p.dir),
198
+ loadedCount: scanned.skills.length,
199
+ warnings: scanned.warnings,
200
+ skillBodies: scanned.skills.map((s) => ({
201
+ name: s.name,
202
+ key: s.key,
203
+ description: s.description,
204
+ sourceLabel: s.sourceLabel,
205
+ path: s.path,
206
+ body: s.body,
207
+ })),
208
+ };
209
+ }
210
+
211
+ export function renderSkillsContextForPrompt(skillsContext) {
212
+ const ctx = normalizeSkillsContext(skillsContext);
213
+ if (!ctx || !Array.isArray(ctx.skillBodies) || ctx.skillBodies.length === 0) return "";
214
+ const blocks = ctx.skillBodies.slice(0, 20).map((skill) => {
215
+ const body = String(skill.body || "").trim();
216
+ return [
217
+ `### ${skill.name}`,
218
+ skill.description ? `说明:${skill.description}` : "",
219
+ `来源:${skill.path || skill.sourceLabel || ""}`,
220
+ "",
221
+ body.slice(0, 16000),
222
+ ].filter(Boolean).join("\n");
223
+ });
224
+ return ["## 已加载 Skills", "", ...blocks].join("\n\n");
225
+ }
@@ -9,9 +9,10 @@ import {
9
9
  readScheduleState,
10
10
  writeScheduleState,
11
11
  } from "./schedule-config.mjs";
12
- import { getRunDir, PACKAGE_ROOT } from "./paths.mjs";
12
+ import { getAgentflowUserContexts, getRunDir, PACKAGE_ROOT } from "./paths.mjs";
13
13
  import { isApplyProcessAlive } from "./run-apply-active-lock.mjs";
14
14
  import { log } from "./log.mjs";
15
+ import { readUserEnvObject } from "./user-env.mjs";
15
16
  import { writeResult } from "../pipeline/write-result.mjs";
16
17
 
17
18
  const DEFAULT_POLL_MS = 30_000;
@@ -92,13 +93,13 @@ function buildCliInputArgs(flowDir, presetName) {
92
93
  return args;
93
94
  }
94
95
 
95
- function hasHigherPriorityDuplicate(workspaceRoot, flow) {
96
+ function hasHigherPriorityDuplicate(workspaceRoot, flow, opts = {}) {
96
97
  if ((flow.source || "user") !== "workspace") return false;
97
- return listFlowsJson(workspaceRoot).some((f) => f.id === flow.id && !f.archived && (f.source || "user") === "user");
98
+ return listFlowsJson(workspaceRoot, opts).some((f) => f.id === flow.id && !f.archived && (f.source || "user") === "user");
98
99
  }
99
100
 
100
- function getLatestRunUuidForFlow(workspaceRoot, flowId) {
101
- const runRoot = path.dirname(getRunDir(workspaceRoot, flowId, "00000000000000"));
101
+ function getLatestRunUuidForFlow(workspaceRoot, flowId, opts = {}) {
102
+ const runRoot = path.dirname(getRunDir(workspaceRoot, flowId, "00000000000000", opts));
102
103
  if (!fs.existsSync(runRoot)) return null;
103
104
  try {
104
105
  const dirs = fs.readdirSync(runRoot, { withFileTypes: true })
@@ -209,13 +210,13 @@ function readNodeResultStatus(runDir, instanceId) {
209
210
  }
210
211
  }
211
212
 
212
- function isFlowCurrentlyRunning(workspaceRoot, flowId, state) {
213
+ function isFlowCurrentlyRunning(workspaceRoot, flowId, state, opts = {}) {
213
214
  const candidates = [];
214
215
  if (state && typeof state.lastRunUuid === "string") candidates.push(state.lastRunUuid);
215
- const latest = getLatestRunUuidForFlow(workspaceRoot, flowId);
216
+ const latest = getLatestRunUuidForFlow(workspaceRoot, flowId, opts);
216
217
  if (latest) candidates.push(latest);
217
218
  for (const uuid of candidates) {
218
- const runDir = getRunDir(workspaceRoot, flowId, uuid);
219
+ const runDir = getRunDir(workspaceRoot, flowId, uuid, opts);
219
220
  if (isApplyProcessAlive(runDir)) return true;
220
221
  }
221
222
  return false;
@@ -231,7 +232,7 @@ function baseState(flow, schedule, previousState) {
231
232
  };
232
233
  }
233
234
 
234
- function ensureNextRunAt(workspaceRoot, flow, schedule, state) {
235
+ function ensureNextRunAt(workspaceRoot, flow, schedule, state, opts = {}) {
235
236
  const identity = scheduleIdentity(schedule);
236
237
  if (state.scheduleIdentity === identity && state.nextRunAt) return state;
237
238
  const nextRunAt = schedule.enabled && schedule.cron ? computeNextRunAtFromSchedule(schedule) : null;
@@ -240,11 +241,11 @@ function ensureNextRunAt(workspaceRoot, flow, schedule, state) {
240
241
  nextRunAt,
241
242
  lastError: "",
242
243
  };
243
- writeScheduleState(workspaceRoot, flow.id, flow.source || "user", next);
244
+ writeScheduleState(workspaceRoot, flow.id, flow.source || "user", next, opts);
244
245
  return next;
245
246
  }
246
247
 
247
- function startScheduledRun(workspaceRoot, flow, schedule, state) {
248
+ function startScheduledRun(workspaceRoot, flow, schedule, state, opts = {}) {
248
249
  const flowDir = flow.path || "";
249
250
  const agentflowBin = path.join(PACKAGE_ROOT, "bin", "agentflow.mjs");
250
251
  const args = [agentflowBin, "apply", flow.id, "--machine-readable", "--workspace-root", path.resolve(workspaceRoot), "--force"];
@@ -252,7 +253,7 @@ function startScheduledRun(workspaceRoot, flow, schedule, state) {
252
253
  const child = spawn(process.execPath, args, {
253
254
  cwd: path.resolve(workspaceRoot),
254
255
  stdio: ["ignore", "pipe", "pipe"],
255
- env: { ...process.env, FORCE_COLOR: "0" },
256
+ env: { ...process.env, ...readUserEnvObject(opts.userId), FORCE_COLOR: "0", AGENTFLOW_USER_ID: opts.userId || "" },
256
257
  detached: true,
257
258
  });
258
259
 
@@ -276,7 +277,7 @@ function startScheduledRun(workspaceRoot, flow, schedule, state) {
276
277
  lastRunUuid,
277
278
  lastPid: child.pid || null,
278
279
  lastError: "",
279
- });
280
+ }, opts);
280
281
  }
281
282
  } catch {
282
283
  /* ignore non-json lines */
@@ -290,7 +291,7 @@ function startScheduledRun(workspaceRoot, flow, schedule, state) {
290
291
  });
291
292
 
292
293
  child.on("exit", (code, signal) => {
293
- const prev = readScheduleState(workspaceRoot, flow.id, flow.source || "user").state || state;
294
+ const prev = readScheduleState(workspaceRoot, flow.id, flow.source || "user", opts).state || state;
294
295
  writeScheduleState(workspaceRoot, flow.id, flow.source || "user", {
295
296
  ...baseState(flow, schedule, prev),
296
297
  nextRunAt: prev.nextRunAt || computeNextRunAtFromSchedule(schedule),
@@ -300,14 +301,14 @@ function startScheduledRun(workspaceRoot, flow, schedule, state) {
300
301
  lastExitSignal: signal || "",
301
302
  lastFinishedAt: new Date().toISOString(),
302
303
  lastError: code === 0 ? "" : `scheduled run exited with code ${code}${signal ? ` signal ${signal}` : ""}`,
303
- });
304
+ }, opts);
304
305
  });
305
306
 
306
307
  child.unref();
307
308
  return child;
308
309
  }
309
310
 
310
- function startWaitingRunResume(workspaceRoot, flow, waitState) {
311
+ function startWaitingRunResume(workspaceRoot, flow, waitState, opts = {}) {
311
312
  const agentflowBin = path.join(PACKAGE_ROOT, "bin", "agentflow.mjs");
312
313
  const uuid = String(waitState.uuid || "");
313
314
  const instanceId = String(waitState.instanceId || "");
@@ -325,7 +326,7 @@ function startWaitingRunResume(workspaceRoot, flow, waitState) {
325
326
  const child = spawn(process.execPath, args, {
326
327
  cwd: path.resolve(workspaceRoot),
327
328
  stdio: ["ignore", "pipe", "pipe"],
328
- env: { ...process.env, FORCE_COLOR: "0" },
329
+ env: { ...process.env, ...readUserEnvObject(opts.userId), FORCE_COLOR: "0", AGENTFLOW_USER_ID: opts.userId || "" },
329
330
  detached: true,
330
331
  });
331
332
  child.stdout.on("data", () => {});
@@ -411,17 +412,17 @@ export function cancelScheduledRun(workspaceRoot, flowId, uuid) {
411
412
  return { ok: true, flowId, uuid, cancelledAt, updatedWaits: updated, propagatedWaits: propagated, resumePid };
412
413
  }
413
414
 
414
- export function listScheduleStatuses(workspaceRoot) {
415
+ export function listScheduleStatuses(workspaceRoot, opts = {}) {
415
416
  const rows = [];
416
- for (const flow of listFlowsJson(workspaceRoot)) {
417
+ for (const flow of listFlowsJson(workspaceRoot, opts)) {
417
418
  if (flow.archived || flow.source === "builtin") continue;
418
- const scheduleRes = readFlowSchedule(workspaceRoot, flow.id, flow.source || "user");
419
+ const scheduleRes = readFlowSchedule(workspaceRoot, flow.id, flow.source || "user", opts);
419
420
  if (!scheduleRes.success) {
420
421
  rows.push({ flowId: flow.id, flowSource: flow.source || "user", enabled: false, error: scheduleRes.error });
421
422
  continue;
422
423
  }
423
424
  const schedule = scheduleRes.schedule;
424
- const stateRes = readScheduleState(workspaceRoot, flow.id, flow.source || "user");
425
+ const stateRes = readScheduleState(workspaceRoot, flow.id, flow.source || "user", opts);
425
426
  const state = stateRes.success ? stateRes.state : {};
426
427
  rows.push({
427
428
  flowId: flow.id,
@@ -433,10 +434,10 @@ export function listScheduleStatuses(workspaceRoot) {
433
434
  nextRunAt: state.nextRunAt || schedule.nextRunAt || null,
434
435
  lastTriggeredAt: state.lastTriggeredAt || null,
435
436
  lastRunUuid: state.lastRunUuid || null,
436
- lastError: hasHigherPriorityDuplicate(workspaceRoot, flow)
437
+ lastError: hasHigherPriorityDuplicate(workspaceRoot, flow, opts)
437
438
  ? "workspace flow is shadowed by a user flow with the same id"
438
439
  : state.lastError || "",
439
- running: isFlowCurrentlyRunning(workspaceRoot, flow.id, state),
440
+ running: isFlowCurrentlyRunning(workspaceRoot, flow.id, state, opts),
440
441
  waiting: countActiveWaitsForFlow(flow),
441
442
  });
442
443
  }
@@ -454,7 +455,9 @@ export async function startScheduler(workspaceRoot, opts = {}) {
454
455
  log.info(`AgentFlow scheduler started. workspace=${path.resolve(workspaceRoot)} poll=${pollMs}ms`);
455
456
  while (true) {
456
457
  const now = Date.now();
457
- for (const flow of listFlowsJson(workspaceRoot)) {
458
+ const contexts = opts.userId ? [{ userId: opts.userId }] : getAgentflowUserContexts();
459
+ for (const scheduleCtx of contexts) {
460
+ for (const flow of listFlowsJson(workspaceRoot, scheduleCtx)) {
458
461
  if (flow.archived || flow.source === "builtin") continue;
459
462
  const flowSource = flow.source || "user";
460
463
  let resumedWaitingRun = false;
@@ -462,7 +465,7 @@ export async function startScheduler(workspaceRoot, opts = {}) {
462
465
  if (resumedWaitingRun) break;
463
466
  for (const waitState of readWaitStates(run.runDir)) {
464
467
  if (!waitState || !waitState.wakeAt || !waitState.instanceId) continue;
465
- if (waitState.status === "resuming" && !isFlowCurrentlyRunning(workspaceRoot, flow.id, { lastRunUuid: run.uuid })) {
468
+ if (waitState.status === "resuming" && !isFlowCurrentlyRunning(workspaceRoot, flow.id, { lastRunUuid: run.uuid }, scheduleCtx)) {
466
469
  const nodeStatus = readNodeResultStatus(run.runDir, String(waitState.instanceId));
467
470
  writeWaitState(waitState, {
468
471
  status: nodeStatus === "pending" ? "waiting" : "resumed",
@@ -472,7 +475,7 @@ export async function startScheduler(workspaceRoot, opts = {}) {
472
475
  }
473
476
  if (waitState.status !== "waiting") continue;
474
477
  if (Date.parse(waitState.wakeAt) > now) continue;
475
- if (isFlowCurrentlyRunning(workspaceRoot, flow.id, { lastRunUuid: run.uuid })) continue;
478
+ if (isFlowCurrentlyRunning(workspaceRoot, flow.id, { lastRunUuid: run.uuid }, scheduleCtx)) continue;
476
479
  const nextState = {
477
480
  ...waitState,
478
481
  status: "resuming",
@@ -480,7 +483,7 @@ export async function startScheduler(workspaceRoot, opts = {}) {
480
483
  resumeStartedAt: new Date().toISOString(),
481
484
  };
482
485
  try {
483
- const child = startWaitingRunResume(workspaceRoot, flow, { ...waitState, uuid: run.uuid, runDir: run.runDir });
486
+ const child = startWaitingRunResume(workspaceRoot, flow, { ...waitState, uuid: run.uuid, runDir: run.runDir }, scheduleCtx);
484
487
  nextState.resumePid = child.pid || null;
485
488
  writeWaitState(waitState, nextState);
486
489
  resumedWaitingRun = true;
@@ -497,41 +500,41 @@ export async function startScheduler(workspaceRoot, opts = {}) {
497
500
  }
498
501
  }
499
502
 
500
- const scheduleRes = readFlowSchedule(workspaceRoot, flow.id, flowSource);
503
+ const scheduleRes = readFlowSchedule(workspaceRoot, flow.id, flowSource, scheduleCtx);
501
504
  if (!scheduleRes.success) {
502
505
  log.debug(`[scheduler] ${flow.id}: ${scheduleRes.error}`);
503
506
  continue;
504
507
  }
505
508
  const schedule = scheduleRes.schedule;
506
509
  if (!schedule.enabled || !schedule.cron) continue;
507
- if (hasHigherPriorityDuplicate(workspaceRoot, flow)) {
508
- const stateRes = readScheduleState(workspaceRoot, flow.id, flowSource);
510
+ if (hasHigherPriorityDuplicate(workspaceRoot, flow, scheduleCtx)) {
511
+ const stateRes = readScheduleState(workspaceRoot, flow.id, flowSource, scheduleCtx);
509
512
  writeScheduleState(workspaceRoot, flow.id, flowSource, {
510
513
  ...baseState(flow, schedule, stateRes.success ? stateRes.state : {}),
511
514
  nextRunAt: null,
512
515
  lastError: "workspace flow is shadowed by a user flow with the same id; scheduled run skipped",
513
516
  lastErrorAt: new Date().toISOString(),
514
- });
517
+ }, scheduleCtx);
515
518
  continue;
516
519
  }
517
- const stateRes = readScheduleState(workspaceRoot, flow.id, flowSource);
518
- let state = ensureNextRunAt(workspaceRoot, flow, schedule, stateRes.success ? stateRes.state : {});
520
+ const stateRes = readScheduleState(workspaceRoot, flow.id, flowSource, scheduleCtx);
521
+ let state = ensureNextRunAt(workspaceRoot, flow, schedule, stateRes.success ? stateRes.state : {}, scheduleCtx);
519
522
  if (!state.nextRunAt || Date.parse(state.nextRunAt) > now) continue;
520
523
 
521
- if (isFlowCurrentlyRunning(workspaceRoot, flow.id, state)) {
524
+ if (isFlowCurrentlyRunning(workspaceRoot, flow.id, state, scheduleCtx)) {
522
525
  const nextRunAt = computeNextRunAtFromSchedule(schedule);
523
526
  writeScheduleState(workspaceRoot, flow.id, flowSource, {
524
527
  ...baseState(flow, schedule, state),
525
528
  nextRunAt,
526
529
  lastSkippedAt: new Date().toISOString(),
527
530
  lastSkipReason: "running",
528
- });
531
+ }, scheduleCtx);
529
532
  log.info(`[scheduler] skip ${flow.id}: already running; next=${nextRunAt}`);
530
533
  continue;
531
534
  }
532
535
 
533
536
  try {
534
- const child = startScheduledRun(workspaceRoot, flow, schedule, state);
537
+ const child = startScheduledRun(workspaceRoot, flow, schedule, state, scheduleCtx);
535
538
  const nextRunAt = computeNextRunAtFromSchedule(schedule);
536
539
  writeScheduleState(workspaceRoot, flow.id, flowSource, {
537
540
  ...baseState(flow, schedule, state),
@@ -539,7 +542,7 @@ export async function startScheduler(workspaceRoot, opts = {}) {
539
542
  lastTriggeredAt: new Date().toISOString(),
540
543
  lastPid: child.pid || null,
541
544
  lastError: "",
542
- });
545
+ }, scheduleCtx);
543
546
  log.info(`[scheduler] triggered ${flow.id}; pid=${child.pid || "?"}; next=${nextRunAt}`);
544
547
  } catch (e) {
545
548
  const nextRunAt = computeNextRunAtFromSchedule(schedule);
@@ -548,10 +551,11 @@ export async function startScheduler(workspaceRoot, opts = {}) {
548
551
  nextRunAt,
549
552
  lastError: e && e.message ? e.message : String(e),
550
553
  lastErrorAt: new Date().toISOString(),
551
- });
554
+ }, scheduleCtx);
552
555
  log.info(`[scheduler] failed ${flow.id}: ${e && e.message ? e.message : String(e)}`);
553
556
  }
554
557
  }
558
+ }
555
559
  if (once) return;
556
560
  await sleep(pollMs);
557
561
  }