@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.
- 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 +63 -14
- package/bin/lib/api-runner.mjs +30 -4
- package/bin/lib/apply.mjs +6 -5
- package/bin/lib/auth.mjs +240 -0
- package/bin/lib/catalog-agents.mjs +2 -2
- package/bin/lib/catalog-flows.mjs +196 -17
- package/bin/lib/composer-agent.mjs +22 -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 +29 -1
- package/bin/lib/locales/zh.json +31 -3
- package/bin/lib/main.mjs +6 -1
- package/bin/lib/node-exec-context.mjs +5 -5
- package/bin/lib/node-execute.mjs +15 -10
- package/bin/lib/paths.mjs +69 -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 +42 -38
- package/bin/lib/skill-registry.mjs +145 -0
- package/bin/lib/ui-server.mjs +1517 -57
- package/bin/lib/user-env.mjs +83 -0
- 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-env.mjs +5 -29
- 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 +328 -6
- package/bin/pipeline/run-tool-nodejs.mjs +7 -0
- package/bin/pipeline/validate-flow.mjs +2 -0
- package/builtin/nodes/agent_subAgent.md +12 -3
- package/builtin/nodes/control_cd_workspace.md +45 -0
- package/builtin/nodes/control_load_skills.md +50 -0
- package/builtin/nodes/control_user_workspace.md +20 -0
- package/builtin/nodes/display_ascii.md +22 -0
- package/builtin/nodes/display_markdown.md +22 -0
- package/builtin/nodes/display_mermaid.md +22 -0
- package/builtin/nodes/tool_git_checkout.md +57 -0
- package/builtin/nodes/tool_nodejs.md +8 -1
- package/builtin/nodes/tool_print.md +4 -1
- package/builtin/web-ui/dist/assets/index-BVWwQpvg.css +1 -0
- package/builtin/web-ui/dist/assets/index-CvNy1n3f.js +197 -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/skills/agentflow-workspace-ascii/SKILL.md +42 -0
- package/skills/agentflow-workspace-graph/SKILL.md +67 -0
- package/skills/agentflow-workspace-markdown/SKILL.md +44 -0
- package/skills/agentflow-workspace-mermaid/SKILL.md +43 -0
- package/builtin/web-ui/dist/assets/index-0vJxkTJz.css +0 -1
- package/builtin/web-ui/dist/assets/index-h69bpxLI.js +0 -190
package/bin/lib/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
|
|
67
|
-
if (
|
|
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(
|
|
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",
|
package/bin/lib/recent-runs.mjs
CHANGED
|
@@ -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
|
+
}
|
package/bin/lib/scheduler.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|