@fieldwangai/agentflow 0.1.29 → 0.1.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agents/agentflow-node-executor-code.md +3 -2
- package/agents/agentflow-node-executor-planning.md +3 -2
- package/agents/agentflow-node-executor-requirement.md +3 -2
- package/agents/agentflow-node-executor-test.md +3 -2
- package/agents/agentflow-node-executor-ui.md +3 -2
- package/agents/agentflow-node-executor.md +3 -2
- package/agents/en/agentflow-node-executor.md +3 -2
- package/agents/zh/agentflow-node-executor.md +3 -2
- package/bin/lib/agent-runners.mjs +38 -13
- package/bin/lib/api-runner.mjs +6 -3
- package/bin/lib/auth.mjs +240 -0
- package/bin/lib/catalog-agents.mjs +2 -2
- package/bin/lib/catalog-flows.mjs +192 -16
- package/bin/lib/composer-agent.mjs +21 -1
- package/bin/lib/composer-skill-router.mjs +10 -78
- package/bin/lib/flow-import.mjs +2 -2
- package/bin/lib/flow-write.mjs +20 -20
- package/bin/lib/help.mjs +2 -2
- package/bin/lib/locales/en.json +25 -1
- package/bin/lib/locales/zh.json +25 -1
- package/bin/lib/main.mjs +6 -1
- package/bin/lib/node-exec-context.mjs +5 -5
- package/bin/lib/node-execute.mjs +14 -9
- package/bin/lib/paths.mjs +64 -13
- package/bin/lib/recent-runs.mjs +2 -2
- package/bin/lib/run-node-statuses-from-disk.mjs +3 -3
- package/bin/lib/runtime-context.mjs +225 -0
- package/bin/lib/scheduler.mjs +41 -38
- package/bin/lib/skill-registry.mjs +145 -0
- package/bin/lib/ui-server.mjs +902 -57
- package/bin/lib/workspace-tree.mjs +4 -3
- package/bin/lib/workspace.mjs +9 -11
- package/bin/pipeline/build-node-prompt.mjs +29 -4
- package/bin/pipeline/get-exec-id.mjs +2 -2
- package/bin/pipeline/get-resolved-values.mjs +1 -0
- package/bin/pipeline/pre-process-node.mjs +306 -6
- package/bin/pipeline/validate-flow.mjs +2 -0
- package/builtin/nodes/agent_subAgent.md +7 -1
- package/builtin/nodes/control_cd_workspace.md +43 -0
- package/builtin/nodes/control_load_skills.md +48 -0
- package/builtin/nodes/display_ascii.md +17 -0
- package/builtin/nodes/display_markdown.md +17 -0
- package/builtin/nodes/display_mermaid.md +17 -0
- package/builtin/nodes/tool_git_checkout.md +54 -0
- package/builtin/nodes/tool_nodejs.md +8 -1
- package/builtin/nodes/tool_print.md +4 -1
- package/builtin/web-ui/dist/assets/index-NdVOJLL9.js +196 -0
- package/builtin/web-ui/dist/assets/index-naVI6LZj.css +1 -0
- package/builtin/web-ui/dist/index.html +2 -2
- package/package.json +1 -1
- package/skills/agentflow-flow-recipes/SKILL.md +24 -0
- package/skills/agentflow-flow-recipes/references/recipes.md +63 -0
- package/skills/agentflow-node-reference/SKILL.md +25 -0
- package/skills/agentflow-node-reference/references/builtin-nodes.md +210 -0
- package/skills/agentflow-placeholder-reference/SKILL.md +24 -0
- package/skills/agentflow-placeholder-reference/references/placeholders.md +20 -0
- package/skills/agentflow-runtime-reference/SKILL.md +25 -0
- package/skills/agentflow-runtime-reference/references/runtime.md +64 -0
- package/builtin/web-ui/dist/assets/index-0vJxkTJz.css +0 -1
- package/builtin/web-ui/dist/assets/index-h69bpxLI.js +0 -190
|
@@ -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
|
+
}
|
package/bin/lib/scheduler.mjs
CHANGED
|
@@ -9,7 +9,7 @@ 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
15
|
import { writeResult } from "../pipeline/write-result.mjs";
|
|
@@ -92,13 +92,13 @@ function buildCliInputArgs(flowDir, presetName) {
|
|
|
92
92
|
return args;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
function hasHigherPriorityDuplicate(workspaceRoot, flow) {
|
|
95
|
+
function hasHigherPriorityDuplicate(workspaceRoot, flow, opts = {}) {
|
|
96
96
|
if ((flow.source || "user") !== "workspace") return false;
|
|
97
|
-
return listFlowsJson(workspaceRoot).some((f) => f.id === flow.id && !f.archived && (f.source || "user") === "user");
|
|
97
|
+
return listFlowsJson(workspaceRoot, opts).some((f) => f.id === flow.id && !f.archived && (f.source || "user") === "user");
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
function getLatestRunUuidForFlow(workspaceRoot, flowId) {
|
|
101
|
-
const runRoot = path.dirname(getRunDir(workspaceRoot, flowId, "00000000000000"));
|
|
100
|
+
function getLatestRunUuidForFlow(workspaceRoot, flowId, opts = {}) {
|
|
101
|
+
const runRoot = path.dirname(getRunDir(workspaceRoot, flowId, "00000000000000", opts));
|
|
102
102
|
if (!fs.existsSync(runRoot)) return null;
|
|
103
103
|
try {
|
|
104
104
|
const dirs = fs.readdirSync(runRoot, { withFileTypes: true })
|
|
@@ -209,13 +209,13 @@ function readNodeResultStatus(runDir, instanceId) {
|
|
|
209
209
|
}
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
-
function isFlowCurrentlyRunning(workspaceRoot, flowId, state) {
|
|
212
|
+
function isFlowCurrentlyRunning(workspaceRoot, flowId, state, opts = {}) {
|
|
213
213
|
const candidates = [];
|
|
214
214
|
if (state && typeof state.lastRunUuid === "string") candidates.push(state.lastRunUuid);
|
|
215
|
-
const latest = getLatestRunUuidForFlow(workspaceRoot, flowId);
|
|
215
|
+
const latest = getLatestRunUuidForFlow(workspaceRoot, flowId, opts);
|
|
216
216
|
if (latest) candidates.push(latest);
|
|
217
217
|
for (const uuid of candidates) {
|
|
218
|
-
const runDir = getRunDir(workspaceRoot, flowId, uuid);
|
|
218
|
+
const runDir = getRunDir(workspaceRoot, flowId, uuid, opts);
|
|
219
219
|
if (isApplyProcessAlive(runDir)) return true;
|
|
220
220
|
}
|
|
221
221
|
return false;
|
|
@@ -231,7 +231,7 @@ function baseState(flow, schedule, previousState) {
|
|
|
231
231
|
};
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
-
function ensureNextRunAt(workspaceRoot, flow, schedule, state) {
|
|
234
|
+
function ensureNextRunAt(workspaceRoot, flow, schedule, state, opts = {}) {
|
|
235
235
|
const identity = scheduleIdentity(schedule);
|
|
236
236
|
if (state.scheduleIdentity === identity && state.nextRunAt) return state;
|
|
237
237
|
const nextRunAt = schedule.enabled && schedule.cron ? computeNextRunAtFromSchedule(schedule) : null;
|
|
@@ -240,11 +240,11 @@ function ensureNextRunAt(workspaceRoot, flow, schedule, state) {
|
|
|
240
240
|
nextRunAt,
|
|
241
241
|
lastError: "",
|
|
242
242
|
};
|
|
243
|
-
writeScheduleState(workspaceRoot, flow.id, flow.source || "user", next);
|
|
243
|
+
writeScheduleState(workspaceRoot, flow.id, flow.source || "user", next, opts);
|
|
244
244
|
return next;
|
|
245
245
|
}
|
|
246
246
|
|
|
247
|
-
function startScheduledRun(workspaceRoot, flow, schedule, state) {
|
|
247
|
+
function startScheduledRun(workspaceRoot, flow, schedule, state, opts = {}) {
|
|
248
248
|
const flowDir = flow.path || "";
|
|
249
249
|
const agentflowBin = path.join(PACKAGE_ROOT, "bin", "agentflow.mjs");
|
|
250
250
|
const args = [agentflowBin, "apply", flow.id, "--machine-readable", "--workspace-root", path.resolve(workspaceRoot), "--force"];
|
|
@@ -252,7 +252,7 @@ function startScheduledRun(workspaceRoot, flow, schedule, state) {
|
|
|
252
252
|
const child = spawn(process.execPath, args, {
|
|
253
253
|
cwd: path.resolve(workspaceRoot),
|
|
254
254
|
stdio: ["ignore", "pipe", "pipe"],
|
|
255
|
-
env: { ...process.env, FORCE_COLOR: "0" },
|
|
255
|
+
env: { ...process.env, FORCE_COLOR: "0", AGENTFLOW_USER_ID: opts.userId || "" },
|
|
256
256
|
detached: true,
|
|
257
257
|
});
|
|
258
258
|
|
|
@@ -276,7 +276,7 @@ function startScheduledRun(workspaceRoot, flow, schedule, state) {
|
|
|
276
276
|
lastRunUuid,
|
|
277
277
|
lastPid: child.pid || null,
|
|
278
278
|
lastError: "",
|
|
279
|
-
});
|
|
279
|
+
}, opts);
|
|
280
280
|
}
|
|
281
281
|
} catch {
|
|
282
282
|
/* ignore non-json lines */
|
|
@@ -290,7 +290,7 @@ function startScheduledRun(workspaceRoot, flow, schedule, state) {
|
|
|
290
290
|
});
|
|
291
291
|
|
|
292
292
|
child.on("exit", (code, signal) => {
|
|
293
|
-
const prev = readScheduleState(workspaceRoot, flow.id, flow.source || "user").state || state;
|
|
293
|
+
const prev = readScheduleState(workspaceRoot, flow.id, flow.source || "user", opts).state || state;
|
|
294
294
|
writeScheduleState(workspaceRoot, flow.id, flow.source || "user", {
|
|
295
295
|
...baseState(flow, schedule, prev),
|
|
296
296
|
nextRunAt: prev.nextRunAt || computeNextRunAtFromSchedule(schedule),
|
|
@@ -300,14 +300,14 @@ function startScheduledRun(workspaceRoot, flow, schedule, state) {
|
|
|
300
300
|
lastExitSignal: signal || "",
|
|
301
301
|
lastFinishedAt: new Date().toISOString(),
|
|
302
302
|
lastError: code === 0 ? "" : `scheduled run exited with code ${code}${signal ? ` signal ${signal}` : ""}`,
|
|
303
|
-
});
|
|
303
|
+
}, opts);
|
|
304
304
|
});
|
|
305
305
|
|
|
306
306
|
child.unref();
|
|
307
307
|
return child;
|
|
308
308
|
}
|
|
309
309
|
|
|
310
|
-
function startWaitingRunResume(workspaceRoot, flow, waitState) {
|
|
310
|
+
function startWaitingRunResume(workspaceRoot, flow, waitState, opts = {}) {
|
|
311
311
|
const agentflowBin = path.join(PACKAGE_ROOT, "bin", "agentflow.mjs");
|
|
312
312
|
const uuid = String(waitState.uuid || "");
|
|
313
313
|
const instanceId = String(waitState.instanceId || "");
|
|
@@ -325,7 +325,7 @@ function startWaitingRunResume(workspaceRoot, flow, waitState) {
|
|
|
325
325
|
const child = spawn(process.execPath, args, {
|
|
326
326
|
cwd: path.resolve(workspaceRoot),
|
|
327
327
|
stdio: ["ignore", "pipe", "pipe"],
|
|
328
|
-
env: { ...process.env, FORCE_COLOR: "0" },
|
|
328
|
+
env: { ...process.env, FORCE_COLOR: "0", AGENTFLOW_USER_ID: opts.userId || "" },
|
|
329
329
|
detached: true,
|
|
330
330
|
});
|
|
331
331
|
child.stdout.on("data", () => {});
|
|
@@ -411,17 +411,17 @@ export function cancelScheduledRun(workspaceRoot, flowId, uuid) {
|
|
|
411
411
|
return { ok: true, flowId, uuid, cancelledAt, updatedWaits: updated, propagatedWaits: propagated, resumePid };
|
|
412
412
|
}
|
|
413
413
|
|
|
414
|
-
export function listScheduleStatuses(workspaceRoot) {
|
|
414
|
+
export function listScheduleStatuses(workspaceRoot, opts = {}) {
|
|
415
415
|
const rows = [];
|
|
416
|
-
for (const flow of listFlowsJson(workspaceRoot)) {
|
|
416
|
+
for (const flow of listFlowsJson(workspaceRoot, opts)) {
|
|
417
417
|
if (flow.archived || flow.source === "builtin") continue;
|
|
418
|
-
const scheduleRes = readFlowSchedule(workspaceRoot, flow.id, flow.source || "user");
|
|
418
|
+
const scheduleRes = readFlowSchedule(workspaceRoot, flow.id, flow.source || "user", opts);
|
|
419
419
|
if (!scheduleRes.success) {
|
|
420
420
|
rows.push({ flowId: flow.id, flowSource: flow.source || "user", enabled: false, error: scheduleRes.error });
|
|
421
421
|
continue;
|
|
422
422
|
}
|
|
423
423
|
const schedule = scheduleRes.schedule;
|
|
424
|
-
const stateRes = readScheduleState(workspaceRoot, flow.id, flow.source || "user");
|
|
424
|
+
const stateRes = readScheduleState(workspaceRoot, flow.id, flow.source || "user", opts);
|
|
425
425
|
const state = stateRes.success ? stateRes.state : {};
|
|
426
426
|
rows.push({
|
|
427
427
|
flowId: flow.id,
|
|
@@ -433,10 +433,10 @@ export function listScheduleStatuses(workspaceRoot) {
|
|
|
433
433
|
nextRunAt: state.nextRunAt || schedule.nextRunAt || null,
|
|
434
434
|
lastTriggeredAt: state.lastTriggeredAt || null,
|
|
435
435
|
lastRunUuid: state.lastRunUuid || null,
|
|
436
|
-
lastError: hasHigherPriorityDuplicate(workspaceRoot, flow)
|
|
436
|
+
lastError: hasHigherPriorityDuplicate(workspaceRoot, flow, opts)
|
|
437
437
|
? "workspace flow is shadowed by a user flow with the same id"
|
|
438
438
|
: state.lastError || "",
|
|
439
|
-
running: isFlowCurrentlyRunning(workspaceRoot, flow.id, state),
|
|
439
|
+
running: isFlowCurrentlyRunning(workspaceRoot, flow.id, state, opts),
|
|
440
440
|
waiting: countActiveWaitsForFlow(flow),
|
|
441
441
|
});
|
|
442
442
|
}
|
|
@@ -454,7 +454,9 @@ export async function startScheduler(workspaceRoot, opts = {}) {
|
|
|
454
454
|
log.info(`AgentFlow scheduler started. workspace=${path.resolve(workspaceRoot)} poll=${pollMs}ms`);
|
|
455
455
|
while (true) {
|
|
456
456
|
const now = Date.now();
|
|
457
|
-
|
|
457
|
+
const contexts = opts.userId ? [{ userId: opts.userId }] : getAgentflowUserContexts();
|
|
458
|
+
for (const scheduleCtx of contexts) {
|
|
459
|
+
for (const flow of listFlowsJson(workspaceRoot, scheduleCtx)) {
|
|
458
460
|
if (flow.archived || flow.source === "builtin") continue;
|
|
459
461
|
const flowSource = flow.source || "user";
|
|
460
462
|
let resumedWaitingRun = false;
|
|
@@ -462,7 +464,7 @@ export async function startScheduler(workspaceRoot, opts = {}) {
|
|
|
462
464
|
if (resumedWaitingRun) break;
|
|
463
465
|
for (const waitState of readWaitStates(run.runDir)) {
|
|
464
466
|
if (!waitState || !waitState.wakeAt || !waitState.instanceId) continue;
|
|
465
|
-
if (waitState.status === "resuming" && !isFlowCurrentlyRunning(workspaceRoot, flow.id, { lastRunUuid: run.uuid })) {
|
|
467
|
+
if (waitState.status === "resuming" && !isFlowCurrentlyRunning(workspaceRoot, flow.id, { lastRunUuid: run.uuid }, scheduleCtx)) {
|
|
466
468
|
const nodeStatus = readNodeResultStatus(run.runDir, String(waitState.instanceId));
|
|
467
469
|
writeWaitState(waitState, {
|
|
468
470
|
status: nodeStatus === "pending" ? "waiting" : "resumed",
|
|
@@ -472,7 +474,7 @@ export async function startScheduler(workspaceRoot, opts = {}) {
|
|
|
472
474
|
}
|
|
473
475
|
if (waitState.status !== "waiting") continue;
|
|
474
476
|
if (Date.parse(waitState.wakeAt) > now) continue;
|
|
475
|
-
if (isFlowCurrentlyRunning(workspaceRoot, flow.id, { lastRunUuid: run.uuid })) continue;
|
|
477
|
+
if (isFlowCurrentlyRunning(workspaceRoot, flow.id, { lastRunUuid: run.uuid }, scheduleCtx)) continue;
|
|
476
478
|
const nextState = {
|
|
477
479
|
...waitState,
|
|
478
480
|
status: "resuming",
|
|
@@ -480,7 +482,7 @@ export async function startScheduler(workspaceRoot, opts = {}) {
|
|
|
480
482
|
resumeStartedAt: new Date().toISOString(),
|
|
481
483
|
};
|
|
482
484
|
try {
|
|
483
|
-
const child = startWaitingRunResume(workspaceRoot, flow, { ...waitState, uuid: run.uuid, runDir: run.runDir });
|
|
485
|
+
const child = startWaitingRunResume(workspaceRoot, flow, { ...waitState, uuid: run.uuid, runDir: run.runDir }, scheduleCtx);
|
|
484
486
|
nextState.resumePid = child.pid || null;
|
|
485
487
|
writeWaitState(waitState, nextState);
|
|
486
488
|
resumedWaitingRun = true;
|
|
@@ -497,41 +499,41 @@ export async function startScheduler(workspaceRoot, opts = {}) {
|
|
|
497
499
|
}
|
|
498
500
|
}
|
|
499
501
|
|
|
500
|
-
const scheduleRes = readFlowSchedule(workspaceRoot, flow.id, flowSource);
|
|
502
|
+
const scheduleRes = readFlowSchedule(workspaceRoot, flow.id, flowSource, scheduleCtx);
|
|
501
503
|
if (!scheduleRes.success) {
|
|
502
504
|
log.debug(`[scheduler] ${flow.id}: ${scheduleRes.error}`);
|
|
503
505
|
continue;
|
|
504
506
|
}
|
|
505
507
|
const schedule = scheduleRes.schedule;
|
|
506
508
|
if (!schedule.enabled || !schedule.cron) continue;
|
|
507
|
-
if (hasHigherPriorityDuplicate(workspaceRoot, flow)) {
|
|
508
|
-
const stateRes = readScheduleState(workspaceRoot, flow.id, flowSource);
|
|
509
|
+
if (hasHigherPriorityDuplicate(workspaceRoot, flow, scheduleCtx)) {
|
|
510
|
+
const stateRes = readScheduleState(workspaceRoot, flow.id, flowSource, scheduleCtx);
|
|
509
511
|
writeScheduleState(workspaceRoot, flow.id, flowSource, {
|
|
510
512
|
...baseState(flow, schedule, stateRes.success ? stateRes.state : {}),
|
|
511
513
|
nextRunAt: null,
|
|
512
514
|
lastError: "workspace flow is shadowed by a user flow with the same id; scheduled run skipped",
|
|
513
515
|
lastErrorAt: new Date().toISOString(),
|
|
514
|
-
});
|
|
516
|
+
}, scheduleCtx);
|
|
515
517
|
continue;
|
|
516
518
|
}
|
|
517
|
-
const stateRes = readScheduleState(workspaceRoot, flow.id, flowSource);
|
|
518
|
-
let state = ensureNextRunAt(workspaceRoot, flow, schedule, stateRes.success ? stateRes.state : {});
|
|
519
|
+
const stateRes = readScheduleState(workspaceRoot, flow.id, flowSource, scheduleCtx);
|
|
520
|
+
let state = ensureNextRunAt(workspaceRoot, flow, schedule, stateRes.success ? stateRes.state : {}, scheduleCtx);
|
|
519
521
|
if (!state.nextRunAt || Date.parse(state.nextRunAt) > now) continue;
|
|
520
522
|
|
|
521
|
-
if (isFlowCurrentlyRunning(workspaceRoot, flow.id, state)) {
|
|
523
|
+
if (isFlowCurrentlyRunning(workspaceRoot, flow.id, state, scheduleCtx)) {
|
|
522
524
|
const nextRunAt = computeNextRunAtFromSchedule(schedule);
|
|
523
525
|
writeScheduleState(workspaceRoot, flow.id, flowSource, {
|
|
524
526
|
...baseState(flow, schedule, state),
|
|
525
527
|
nextRunAt,
|
|
526
528
|
lastSkippedAt: new Date().toISOString(),
|
|
527
529
|
lastSkipReason: "running",
|
|
528
|
-
});
|
|
530
|
+
}, scheduleCtx);
|
|
529
531
|
log.info(`[scheduler] skip ${flow.id}: already running; next=${nextRunAt}`);
|
|
530
532
|
continue;
|
|
531
533
|
}
|
|
532
534
|
|
|
533
535
|
try {
|
|
534
|
-
const child = startScheduledRun(workspaceRoot, flow, schedule, state);
|
|
536
|
+
const child = startScheduledRun(workspaceRoot, flow, schedule, state, scheduleCtx);
|
|
535
537
|
const nextRunAt = computeNextRunAtFromSchedule(schedule);
|
|
536
538
|
writeScheduleState(workspaceRoot, flow.id, flowSource, {
|
|
537
539
|
...baseState(flow, schedule, state),
|
|
@@ -539,7 +541,7 @@ export async function startScheduler(workspaceRoot, opts = {}) {
|
|
|
539
541
|
lastTriggeredAt: new Date().toISOString(),
|
|
540
542
|
lastPid: child.pid || null,
|
|
541
543
|
lastError: "",
|
|
542
|
-
});
|
|
544
|
+
}, scheduleCtx);
|
|
543
545
|
log.info(`[scheduler] triggered ${flow.id}; pid=${child.pid || "?"}; next=${nextRunAt}`);
|
|
544
546
|
} catch (e) {
|
|
545
547
|
const nextRunAt = computeNextRunAtFromSchedule(schedule);
|
|
@@ -548,10 +550,11 @@ export async function startScheduler(workspaceRoot, opts = {}) {
|
|
|
548
550
|
nextRunAt,
|
|
549
551
|
lastError: e && e.message ? e.message : String(e),
|
|
550
552
|
lastErrorAt: new Date().toISOString(),
|
|
551
|
-
});
|
|
553
|
+
}, scheduleCtx);
|
|
552
554
|
log.info(`[scheduler] failed ${flow.id}: ${e && e.message ? e.message : String(e)}`);
|
|
553
555
|
}
|
|
554
556
|
}
|
|
557
|
+
}
|
|
555
558
|
if (once) return;
|
|
556
559
|
await sleep(pollMs);
|
|
557
560
|
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import os from "os";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import yaml from "js-yaml";
|
|
5
|
+
|
|
6
|
+
const fileCache = new Map();
|
|
7
|
+
const CACHE_TTL_MS = 60_000;
|
|
8
|
+
|
|
9
|
+
function readFileCached(absPath) {
|
|
10
|
+
const now = Date.now();
|
|
11
|
+
const cached = fileCache.get(absPath);
|
|
12
|
+
if (cached && now - cached.ts < CACHE_TTL_MS) return cached.content;
|
|
13
|
+
try {
|
|
14
|
+
const content = fs.readFileSync(absPath, "utf-8");
|
|
15
|
+
fileCache.set(absPath, { content, ts: now });
|
|
16
|
+
return content;
|
|
17
|
+
} catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function stripSkillFrontmatter(content) {
|
|
23
|
+
const raw = String(content || "");
|
|
24
|
+
const match = raw.match(/^---\s*\r?\n[\s\S]*?\r?\n---\s*\r?\n?/);
|
|
25
|
+
return match ? raw.slice(match[0].length).trim() : raw.trim();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function parseSkillFrontmatter(content) {
|
|
29
|
+
const match = String(content || "").match(/^---\s*\r?\n([\s\S]*?)\r?\n---/);
|
|
30
|
+
if (!match) return {};
|
|
31
|
+
try {
|
|
32
|
+
return yaml.load(match[1]) || {};
|
|
33
|
+
} catch {
|
|
34
|
+
return {};
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function parseSkillFile(absPath, source = {}) {
|
|
39
|
+
const content = readFileCached(absPath);
|
|
40
|
+
if (!content) return null;
|
|
41
|
+
const meta = parseSkillFrontmatter(content);
|
|
42
|
+
const name = String(meta.name || path.basename(path.dirname(absPath))).trim();
|
|
43
|
+
if (!name) return null;
|
|
44
|
+
const body = stripSkillFrontmatter(content);
|
|
45
|
+
const sourceId = String(source.source || source.id || "unknown").trim() || "unknown";
|
|
46
|
+
const sourceLabel = String(source.sourceLabel || source.label || sourceId).trim() || sourceId;
|
|
47
|
+
return {
|
|
48
|
+
key: `${sourceId}:${name}`,
|
|
49
|
+
id: name,
|
|
50
|
+
name,
|
|
51
|
+
description: String(meta.description || "").trim(),
|
|
52
|
+
source: sourceId,
|
|
53
|
+
sourceLabel,
|
|
54
|
+
path: absPath,
|
|
55
|
+
body,
|
|
56
|
+
content,
|
|
57
|
+
installedBy: source.installedBy || "",
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function listSkillFiles(dir) {
|
|
62
|
+
try {
|
|
63
|
+
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) return [];
|
|
64
|
+
return fs.readdirSync(dir, { withFileTypes: true })
|
|
65
|
+
.filter((entry) => entry.isDirectory())
|
|
66
|
+
.map((entry) => path.join(dir, entry.name, "SKILL.md"))
|
|
67
|
+
.filter((skillPath) => fs.existsSync(skillPath));
|
|
68
|
+
} catch {
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function workspaceSkillSources(root, prefix, labelPrefix) {
|
|
74
|
+
if (!root) return [];
|
|
75
|
+
const abs = path.resolve(root);
|
|
76
|
+
return [
|
|
77
|
+
{ source: `${prefix}-agents`, sourceLabel: `${labelPrefix}/.agents`, dir: path.join(abs, ".agents", "skills"), installedBy: "filesystem" },
|
|
78
|
+
{ source: `${prefix}-cursor`, sourceLabel: `${labelPrefix}/.cursor`, dir: path.join(abs, ".cursor", "skills"), installedBy: "filesystem" },
|
|
79
|
+
{ source: `${prefix}-codex`, sourceLabel: `${labelPrefix}/.codex`, dir: path.join(abs, ".codex", "skills"), installedBy: "filesystem" },
|
|
80
|
+
];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function defaultSkillSources(packageRoot, workspaceRoot) {
|
|
84
|
+
const sources = [
|
|
85
|
+
{
|
|
86
|
+
source: "builtin",
|
|
87
|
+
sourceLabel: "AgentFlow",
|
|
88
|
+
dir: path.join(path.resolve(packageRoot), "skills"),
|
|
89
|
+
installedBy: "agentflow",
|
|
90
|
+
},
|
|
91
|
+
];
|
|
92
|
+
if (workspaceRoot) sources.push(...workspaceSkillSources(workspaceRoot, "workspace", "工作区"));
|
|
93
|
+
const home = os.homedir();
|
|
94
|
+
sources.push(
|
|
95
|
+
{ source: "global-agents", sourceLabel: "全局 .agents", dir: path.join(home, ".agents", "skills"), installedBy: "global" },
|
|
96
|
+
{ source: "global-cursor", sourceLabel: "全局 .cursor", dir: path.join(home, ".cursor", "skills"), installedBy: "global" },
|
|
97
|
+
{ source: "global-codex", sourceLabel: "全局 .codex", dir: path.join(home, ".codex", "skills"), installedBy: "skillhub" },
|
|
98
|
+
);
|
|
99
|
+
return sources;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function listSkillsFromSources(sources, opts = {}) {
|
|
103
|
+
const include = new Set((opts.include || []).map((x) => String(x).trim()).filter(Boolean));
|
|
104
|
+
const exclude = new Set((opts.exclude || []).map((x) => String(x).trim()).filter(Boolean));
|
|
105
|
+
const skills = [];
|
|
106
|
+
const warnings = [];
|
|
107
|
+
const seenKeys = new Set();
|
|
108
|
+
const seenPaths = new Set();
|
|
109
|
+
for (const source of sources || []) {
|
|
110
|
+
const dir = path.resolve(source.dir || "");
|
|
111
|
+
if (!fs.existsSync(dir)) {
|
|
112
|
+
if (opts.warnMissing) warnings.push(`skills path not found: ${dir}`);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
for (const skillPath of listSkillFiles(dir)) {
|
|
116
|
+
const skill = parseSkillFile(skillPath, source);
|
|
117
|
+
if (!skill) continue;
|
|
118
|
+
if (include.size > 0 && !include.has(skill.name) && !include.has(skill.key)) continue;
|
|
119
|
+
if (exclude.has(skill.name) || exclude.has(skill.key)) continue;
|
|
120
|
+
if (seenPaths.has(skill.path)) continue;
|
|
121
|
+
if (seenKeys.has(skill.key)) continue;
|
|
122
|
+
seenPaths.add(skill.path);
|
|
123
|
+
seenKeys.add(skill.key);
|
|
124
|
+
skills.push(skill);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
skills.sort((a, b) => {
|
|
128
|
+
const bySource = a.sourceLabel.localeCompare(b.sourceLabel);
|
|
129
|
+
if (bySource !== 0) return bySource;
|
|
130
|
+
return a.name.localeCompare(b.name);
|
|
131
|
+
});
|
|
132
|
+
return { skills, warnings };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function listSkills(packageRoot, workspaceRoot, opts = {}) {
|
|
136
|
+
return listSkillsFromSources(defaultSkillSources(packageRoot, workspaceRoot), opts).skills;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function readSkillDetail(packageRoot, workspaceRoot, keyOrName) {
|
|
140
|
+
const wanted = String(keyOrName || "").trim();
|
|
141
|
+
if (!wanted) return null;
|
|
142
|
+
const item = listSkills(packageRoot, workspaceRoot).find((skill) => skill.key === wanted || skill.name === wanted);
|
|
143
|
+
if (!item) return null;
|
|
144
|
+
return item;
|
|
145
|
+
}
|