@fieldwangai/agentflow 0.1.137 → 0.1.141

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.
@@ -4,7 +4,7 @@ import path from "path";
4
4
  import { getAgentflowDataRoot } from "./paths.mjs";
5
5
  import { getTeamForUser } from "./teams.mjs";
6
6
 
7
- const REGISTRY_VERSION = 2;
7
+ const REGISTRY_VERSION = 4;
8
8
 
9
9
  function registryPath() {
10
10
  return path.join(getAgentflowDataRoot(), "collaboration", "prd-workflows.json");
@@ -63,6 +63,48 @@ function normalizedMemberMap(value) {
63
63
  .filter(([userId, role]) => userId && role));
64
64
  }
65
65
 
66
+ function normalizeKnowledgeBindings(value) {
67
+ if (!Array.isArray(value)) return [];
68
+ const seen = new Set();
69
+ return value.flatMap((entry) => {
70
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) return [];
71
+ const workspaceId = String(entry.workspaceId || entry.id || "").trim();
72
+ if (!workspaceId || seen.has(workspaceId)) return [];
73
+ seen.add(workspaceId);
74
+ return [{
75
+ workspaceId,
76
+ label: String(entry.label || workspaceId).trim() || workspaceId,
77
+ kind: String(entry.kind || "git").trim().toLowerCase(),
78
+ type: String(entry.type || "knowledge").trim().toLowerCase(),
79
+ branch: String(entry.branch || "").trim(),
80
+ boundBy: normalizeUserId(entry.boundBy),
81
+ boundAt: String(entry.boundAt || "").trim(),
82
+ }];
83
+ });
84
+ }
85
+
86
+ function normalizeProjectBindings(value) {
87
+ if (!Array.isArray(value)) return [];
88
+ const seen = new Set();
89
+ return value.flatMap((entry) => {
90
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) return [];
91
+ const workspaceId = String(entry.workspaceId || "").trim();
92
+ const flowId = String(entry.flowId || "").trim();
93
+ const flowSource = String(entry.flowSource || "user").trim() || "user";
94
+ if (!workspaceId || !flowId || seen.has(workspaceId)) return [];
95
+ seen.add(workspaceId);
96
+ return [{
97
+ workspaceId,
98
+ flowId,
99
+ flowSource,
100
+ archived: entry.archived === true,
101
+ ownerId: normalizeUserId(entry.ownerId),
102
+ boundBy: normalizeUserId(entry.boundBy),
103
+ boundAt: String(entry.boundAt || "").trim(),
104
+ }];
105
+ });
106
+ }
107
+
66
108
  function collaborationMembers(record) {
67
109
  const ownerId = normalizeUserId(record?.ownerId);
68
110
  const explicit = normalizedMemberMap(record?.members);
@@ -107,6 +149,8 @@ function publicWorkflow(record, userId = "") {
107
149
  ? record.authority.unresolvedParticipants.map((value) => String(value || "")).filter(Boolean)
108
150
  : [],
109
151
  } : null,
152
+ knowledgeBindings: normalizeKnowledgeBindings(record.knowledgeBindings),
153
+ projectBindingCount: normalizeProjectBindings(record.projectBindings).length,
110
154
  shareActive: Boolean(record.shareToken),
111
155
  shareCreatedAt: record.shareCreatedAt || "",
112
156
  createdAt: record.createdAt || "",
@@ -232,6 +276,100 @@ export function prdWorkflowCollaborationSummary(record, userId) {
232
276
  return publicWorkflow(record, userId);
233
277
  }
234
278
 
279
+ export function setPrdWorkflowKnowledgeBindings({ tapdId, userId, bindings = [] }) {
280
+ const record = getPrdWorkflowCollaborationForUser(tapdId, userId);
281
+ if (!record) return { error: "PRD Workflow collaboration not found", status: 404 };
282
+ const actorId = normalizeUserId(userId);
283
+ if (prdWorkflowCollaborationAccess(record, actorId).role !== "owner") {
284
+ return { error: "Only the Workflow owner can manage knowledge bindings", status: 403 };
285
+ }
286
+ const registry = readRegistry();
287
+ const stored = registry.workflows[record.id];
288
+ if (!stored) return { error: "PRD Workflow collaboration not found", status: 404 };
289
+ const now = new Date().toISOString();
290
+ stored.knowledgeBindings = normalizeKnowledgeBindings(bindings).map((binding) => ({
291
+ ...binding,
292
+ boundBy: actorId,
293
+ boundAt: binding.boundAt || now,
294
+ }));
295
+ stored.updatedAt = now;
296
+ writeRegistry(registry);
297
+ return {
298
+ record: stored,
299
+ workflow: publicWorkflow(stored, actorId),
300
+ knowledgeBindings: normalizeKnowledgeBindings(stored.knowledgeBindings),
301
+ };
302
+ }
303
+
304
+ export function listPrdWorkflowProjectBindings({ tapdId, userId }) {
305
+ const record = getPrdWorkflowCollaborationForUser(tapdId, userId);
306
+ if (!record) return { error: "PRD Workflow collaboration not found", status: 404 };
307
+ return {
308
+ record,
309
+ workflow: publicWorkflow(record, userId),
310
+ projectBindings: normalizeProjectBindings(record.projectBindings),
311
+ };
312
+ }
313
+
314
+ export function bindPrdWorkflowProject({ tapdId, userId, project }) {
315
+ const record = getPrdWorkflowCollaborationForUser(tapdId, userId);
316
+ if (!record) return { error: "PRD Workflow collaboration not found", status: 404 };
317
+ const normalized = normalizeProjectBindings([project])[0];
318
+ if (!normalized) return { error: "Project binding requires workspaceId and flowId", status: 400 };
319
+ const registry = readRegistry();
320
+ const stored = registry.workflows[record.id];
321
+ if (!stored) return { error: "PRD Workflow collaboration not found", status: 404 };
322
+ const actorId = normalizeUserId(userId);
323
+ const now = new Date().toISOString();
324
+ const current = normalizeProjectBindings(stored.projectBindings);
325
+ const existing = current.find((binding) => binding.workspaceId === normalized.workspaceId);
326
+ stored.projectBindings = [
327
+ ...current.filter((binding) => binding.workspaceId !== normalized.workspaceId),
328
+ {
329
+ ...normalized,
330
+ boundBy: existing?.boundBy || actorId,
331
+ boundAt: existing?.boundAt || now,
332
+ },
333
+ ];
334
+ stored.updatedAt = now;
335
+ writeRegistry(registry);
336
+ return {
337
+ record: stored,
338
+ workflow: publicWorkflow(stored, actorId),
339
+ projectBindings: normalizeProjectBindings(stored.projectBindings),
340
+ created: !existing,
341
+ };
342
+ }
343
+
344
+ export function unbindPrdWorkflowProject({ tapdId, userId, workspaceId }) {
345
+ const record = getPrdWorkflowCollaborationForUser(tapdId, userId);
346
+ if (!record) return { error: "PRD Workflow collaboration not found", status: 404 };
347
+ const id = String(workspaceId || "").trim();
348
+ if (!id) return { error: "Missing workspaceId", status: 400 };
349
+ const registry = readRegistry();
350
+ const stored = registry.workflows[record.id];
351
+ if (!stored) return { error: "PRD Workflow collaboration not found", status: 404 };
352
+ const current = normalizeProjectBindings(stored.projectBindings);
353
+ const next = current.filter((binding) => binding.workspaceId !== id);
354
+ if (next.length === current.length) {
355
+ return {
356
+ record: stored,
357
+ workflow: publicWorkflow(stored, userId),
358
+ projectBindings: current,
359
+ unchanged: true,
360
+ };
361
+ }
362
+ stored.projectBindings = next;
363
+ stored.updatedAt = new Date().toISOString();
364
+ writeRegistry(registry);
365
+ return {
366
+ record: stored,
367
+ workflow: publicWorkflow(stored, userId),
368
+ projectBindings: next,
369
+ removedWorkspaceId: id,
370
+ };
371
+ }
372
+
235
373
  export function ensurePrdWorkflowShareLink({ tapdId, userId }) {
236
374
  const ensured = ensurePrdWorkflowCollaboration({ tapdId, userId });
237
375
  if (ensured.error) return ensured;
@@ -9,6 +9,21 @@ import { isApplyProcessAlive } from "./run-apply-active-lock.mjs";
9
9
  /** Web UI 调用 /api/flow/run/stop 时写入,用于与「未跑完但未标记」区分 */
10
10
  export const RUN_INTERRUPTED_FILENAME = "run-interrupted.json";
11
11
 
12
+ function hasActiveDurableWait(runDir) {
13
+ const paths = [path.join(runDir, "wait-states.json"), path.join(runDir, "wait-state.json")];
14
+ for (const filePath of paths) {
15
+ if (!fs.existsSync(filePath)) continue;
16
+ try {
17
+ const parsed = JSON.parse(fs.readFileSync(filePath, "utf-8"));
18
+ const waits = Array.isArray(parsed?.waits) ? parsed.waits : parsed && typeof parsed === "object" ? [parsed] : [];
19
+ if (waits.some((wait) => wait && (wait.status === "waiting" || wait.status === "resuming"))) return true;
20
+ } catch {
21
+ /* ignore corrupt wait files */
22
+ }
23
+ }
24
+ return false;
25
+ }
26
+
12
27
  /** @param {string} filePath */
13
28
  function parseResultStatusFromFile(filePath) {
14
29
  try {
@@ -112,6 +127,7 @@ function inferRunStatusFromRunDir(runDir) {
112
127
 
113
128
  if (anyResult || fs.existsSync(flowJsonPath)) {
114
129
  if (isApplyProcessAlive(runDir)) return "running";
130
+ if (hasActiveDurableWait(runDir)) return "running";
115
131
  return "interrupted";
116
132
  }
117
133
  return "unknown";
@@ -17,6 +17,28 @@ function parseResultStatus(filePath) {
17
17
  }
18
18
  }
19
19
 
20
+ function readJenkinsState(runDir, instanceId) {
21
+ const statePath = path.join(runDir, "state", `${instanceId}.jenkins.json`);
22
+ if (!fs.existsSync(statePath)) return null;
23
+ try {
24
+ const parsed = JSON.parse(fs.readFileSync(statePath, "utf-8"));
25
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
26
+ } catch {
27
+ return null;
28
+ }
29
+ }
30
+
31
+ function jenkinsUiStatus(executionStatus, state) {
32
+ if (!state) return executionStatus;
33
+ if (executionStatus === "pending" || state.phase === "queued" || state.phase === "running" || state.phase === "triggering") {
34
+ return "waiting";
35
+ }
36
+ if (executionStatus === "success" && state.phase === "complete") {
37
+ return String(state.status || "").toUpperCase() === "SUCCESS" ? "success" : "outcome_failed";
38
+ }
39
+ return executionStatus;
40
+ }
41
+
20
42
  /** @param {string} filePath @returns {number | null} */
21
43
  function parseElapsedMsLine(filePath) {
22
44
  try {
@@ -34,7 +56,7 @@ function parseElapsedMsLine(filePath) {
34
56
  * @param {string} workspaceRoot
35
57
  * @param {string} flowName
36
58
  * @param {string} uuid
37
- * @returns {Record<string, { status: string, elapsed?: string }>}
59
+ * @returns {Record<string, { status: string, elapsed?: string, executionStatus?: string, phase?: string, jenkinsStatus?: string, message?: string, buildNumber?: string, url?: string, qrUrl?: string, startedAt?: string, wakeAt?: string }>}
38
60
  */
39
61
  export function getRunNodeStatusesFromDisk(workspaceRoot, flowName, uuid, opts = {}) {
40
62
  const runDir = getRunDir(workspaceRoot, flowName, uuid, opts);
@@ -70,9 +92,25 @@ export function getRunNodeStatusesFromDisk(workspaceRoot, flowName, uuid, opts =
70
92
  const low = String(status).toLowerCase();
71
93
  if (low === "completed" || low === "done") uiStatus = "success";
72
94
 
73
- /** @type {{ status: string, elapsed?: string }} */
95
+ const jenkinsState = defId === "tool_jenkins_build" ? readJenkinsState(runDir, instanceId) : null;
96
+ if (jenkinsState) uiStatus = jenkinsUiStatus(uiStatus, jenkinsState);
97
+
98
+ /** @type {{ status: string, elapsed?: string, executionStatus?: string, phase?: string, jenkinsStatus?: string, message?: string, buildNumber?: string, url?: string, qrUrl?: string, startedAt?: string, wakeAt?: string }} */
74
99
  const row = { status: uiStatus };
75
- if (uiStatus === "success" && fs.existsSync(resultPath)) {
100
+ if (jenkinsState) {
101
+ row.executionStatus = status;
102
+ row.phase = String(jenkinsState.phase || "");
103
+ row.jenkinsStatus = String(jenkinsState.status || "");
104
+ row.message = String(jenkinsState.message || "");
105
+ row.buildNumber = String(jenkinsState.buildNumber || "");
106
+ row.url = String(jenkinsState.url || jenkinsState.buildUrl || "");
107
+ row.qrUrl = String(jenkinsState.qrUrl || "");
108
+ row.startedAt = String(jenkinsState.startedAt || "");
109
+ row.wakeAt = String(jenkinsState.wakeAt || "");
110
+ const started = Date.parse(jenkinsState.startedAt || "");
111
+ const ended = Date.parse(jenkinsState.completedAt || "");
112
+ if (Number.isFinite(started) && Number.isFinite(ended) && ended >= started) row.elapsed = formatDuration(ended - started);
113
+ } else if (uiStatus === "success" && fs.existsSync(resultPath)) {
76
114
  const ms = parseElapsedMsLine(resultPath);
77
115
  if (ms != null && ms > 0) {
78
116
  row.elapsed = formatDuration(ms);
@@ -9,7 +9,7 @@ import {
9
9
  readScheduleState,
10
10
  writeScheduleState,
11
11
  } from "./schedule-config.mjs";
12
- import { getAgentflowUserContexts, getRunDir, PACKAGE_ROOT } from "./paths.mjs";
12
+ import { getAgentflowUserContexts, getFlowRuntimeRoot, 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 { readMergedEnvObject } from "./user-env.mjs";
@@ -112,17 +112,28 @@ function getLatestRunUuidForFlow(workspaceRoot, flowId, opts = {}) {
112
112
  }
113
113
  }
114
114
 
115
- function listRunDirsForFlow(flow) {
116
- const flowDir = flow.path || "";
117
- const runRoot = flowDir ? path.join(flowDir, "runBuild") : "";
118
- if (!runRoot || !fs.existsSync(runRoot)) return [];
119
- try {
120
- return fs.readdirSync(runRoot, { withFileTypes: true })
121
- .filter((e) => e.isDirectory() && /^\d{14}$/.test(e.name))
122
- .map((e) => ({ uuid: e.name, runDir: path.join(runRoot, e.name) }));
123
- } catch {
124
- return [];
115
+ function listRunDirsForFlow(workspaceRoot, flow, opts = {}) {
116
+ const roots = [
117
+ flow.path ? path.join(flow.path, "runBuild") : "",
118
+ path.join(getFlowRuntimeRoot(workspaceRoot, flow.id, opts), "runBuild"),
119
+ ].filter(Boolean);
120
+ const seen = new Set();
121
+ const runs = [];
122
+ for (const runRoot of roots) {
123
+ const resolved = path.resolve(runRoot);
124
+ if (seen.has(resolved) || !fs.existsSync(resolved)) continue;
125
+ seen.add(resolved);
126
+ try {
127
+ for (const entry of fs.readdirSync(resolved, { withFileTypes: true })) {
128
+ if (entry.isDirectory() && /^\d{14}$/.test(entry.name)) {
129
+ runs.push({ uuid: entry.name, runDir: path.join(resolved, entry.name) });
130
+ }
131
+ }
132
+ } catch {
133
+ /* ignore unreadable run roots */
134
+ }
125
135
  }
136
+ return runs.sort((a, b) => b.uuid.localeCompare(a.uuid));
126
137
  }
127
138
 
128
139
  function readJsonObject(filePath) {
@@ -317,12 +328,23 @@ function startWaitingRunResume(workspaceRoot, flow, waitState, opts = {}) {
317
328
  const agentflowBin = path.join(PACKAGE_ROOT, "bin", "agentflow.mjs");
318
329
  const uuid = String(waitState.uuid || "");
319
330
  const instanceId = String(waitState.instanceId || "");
331
+ const rerunCurrentNode = waitState.resumeMode === "rerun";
332
+ if (rerunCurrentNode) {
333
+ writeResult(
334
+ workspaceRoot,
335
+ flow.id,
336
+ uuid,
337
+ instanceId,
338
+ { status: "cache_not_met", message: "后台任务到期,重新检查外部状态" },
339
+ { execId: Number(waitState.execId) || undefined, preserveBody: true, runDir: waitState.runDir },
340
+ );
341
+ }
320
342
  const args = [
321
343
  agentflowBin,
322
- "resume",
344
+ rerunCurrentNode ? "apply" : "resume",
323
345
  flow.id,
324
346
  uuid,
325
- instanceId,
347
+ ...(rerunCurrentNode ? [] : [instanceId]),
326
348
  "--machine-readable",
327
349
  "--workspace-root",
328
350
  path.resolve(workspaceRoot),
@@ -356,9 +378,9 @@ function startWaitingRunResume(workspaceRoot, flow, waitState, opts = {}) {
356
378
  return child;
357
379
  }
358
380
 
359
- function countActiveWaitsForFlow(flow) {
381
+ function countActiveWaitsForFlow(workspaceRoot, flow, opts = {}) {
360
382
  let count = 0;
361
- for (const run of listRunDirsForFlow(flow)) {
383
+ for (const run of listRunDirsForFlow(workspaceRoot, flow, opts)) {
362
384
  for (const waitState of readWaitStates(run.runDir)) {
363
385
  if (waitState && (waitState.status === "waiting" || waitState.status === "resuming")) count += 1;
364
386
  }
@@ -378,10 +400,10 @@ function hasNodeBranchEdge(runDir, instanceId, branchName) {
378
400
  return flow.edges.some((e) => e && e.source === instanceId && (e.sourceHandle || "output-0") === sourceHandle);
379
401
  }
380
402
 
381
- export function cancelScheduledRun(workspaceRoot, flowId, uuid) {
382
- const flow = listFlowsJson(workspaceRoot).find((f) => f.id === flowId && !f.archived && f.source !== "builtin");
403
+ export function cancelScheduledRun(workspaceRoot, flowId, uuid, opts = {}) {
404
+ const flow = listFlowsJson(workspaceRoot, opts).find((f) => f.id === flowId && !f.archived);
383
405
  if (!flow) return { ok: false, error: `flow not found: ${flowId}` };
384
- const runDir = getRunDir(workspaceRoot, flow.id, uuid);
406
+ const runDir = getRunDir(workspaceRoot, flow.id, uuid, opts);
385
407
  if (!fs.existsSync(runDir)) return { ok: false, error: `run not found: ${flowId}/${uuid}` };
386
408
  const cancelledAt = new Date().toISOString();
387
409
  fs.writeFileSync(path.join(runDir, "cancelled.json"), JSON.stringify({ cancelled: true, cancelledAt }, null, 2) + "\n", "utf-8");
@@ -396,16 +418,22 @@ export function cancelScheduledRun(workspaceRoot, flowId, uuid) {
396
418
  instanceId &&
397
419
  hasNodeBranchEdge(runDir, instanceId, "cancelled") &&
398
420
  !resumePid;
399
- if (canPropagate) {
421
+ const canRerunForCancellation = waitState.resumeMode === "rerun" && instanceId && !resumePid;
422
+ if (canRerunForCancellation) {
423
+ const child = startWaitingRunResume(workspaceRoot, flow, { ...waitState, uuid, runDir, branch: "cancelled" }, opts);
424
+ resumePid = child.pid || null;
425
+ writeWaitState(waitState, { status: "resuming", branch: "cancelled", cancelledAt, resumePid, resumeStartedAt: cancelledAt });
426
+ propagated += 1;
427
+ } else if (canPropagate) {
400
428
  writeResult(
401
429
  workspaceRoot,
402
430
  flow.id,
403
431
  uuid,
404
432
  instanceId,
405
433
  { status: "success", message: "已取消", branch: "cancelled" },
406
- { execId: Number(waitState.execId) || undefined, preserveBody: false },
434
+ { execId: Number(waitState.execId) || undefined, preserveBody: false, runDir },
407
435
  );
408
- const child = startWaitingRunResume(workspaceRoot, flow, { ...waitState, uuid, runDir, branch: "cancelled" });
436
+ const child = startWaitingRunResume(workspaceRoot, flow, { ...waitState, uuid, runDir, branch: "cancelled" }, opts);
409
437
  resumePid = child.pid || null;
410
438
  writeWaitState(waitState, { status: "resuming", branch: "cancelled", cancelledAt, resumePid, resumeStartedAt: cancelledAt });
411
439
  propagated += 1;
@@ -443,7 +471,7 @@ export function listScheduleStatuses(workspaceRoot, opts = {}) {
443
471
  ? "workspace flow is shadowed by a user flow with the same id"
444
472
  : state.lastError || "",
445
473
  running: isFlowCurrentlyRunning(workspaceRoot, flow.id, state, opts),
446
- waiting: countActiveWaitsForFlow(flow),
474
+ waiting: countActiveWaitsForFlow(workspaceRoot, flow, opts),
447
475
  });
448
476
  }
449
477
  rows.sort((a, b) => {
@@ -463,17 +491,21 @@ export async function startScheduler(workspaceRoot, opts = {}) {
463
491
  const contexts = opts.userId ? [{ userId: opts.userId }] : getAgentflowUserContexts();
464
492
  for (const scheduleCtx of contexts) {
465
493
  for (const flow of listFlowsJson(workspaceRoot, scheduleCtx)) {
466
- if (flow.archived || flow.source === "builtin") continue;
494
+ if (flow.archived) continue;
467
495
  const flowSource = flow.source || "user";
468
496
  let resumedWaitingRun = false;
469
- for (const run of listRunDirsForFlow(flow)) {
497
+ for (const run of listRunDirsForFlow(workspaceRoot, flow, scheduleCtx)) {
470
498
  if (resumedWaitingRun) break;
471
499
  for (const waitState of readWaitStates(run.runDir)) {
472
500
  if (!waitState || !waitState.wakeAt || !waitState.instanceId) continue;
473
501
  if (waitState.status === "resuming" && !isFlowCurrentlyRunning(workspaceRoot, flow.id, { lastRunUuid: run.uuid }, scheduleCtx)) {
474
502
  const nodeStatus = readNodeResultStatus(run.runDir, String(waitState.instanceId));
475
503
  writeWaitState(waitState, {
476
- status: nodeStatus === "pending" ? "waiting" : "resumed",
504
+ status:
505
+ nodeStatus === "pending" ||
506
+ (waitState.resumeMode === "rerun" && nodeStatus !== "success" && nodeStatus !== "failed")
507
+ ? "waiting"
508
+ : "resumed",
477
509
  reconciledAt: new Date().toISOString(),
478
510
  });
479
511
  continue;
@@ -505,6 +537,8 @@ export async function startScheduler(workspaceRoot, opts = {}) {
505
537
  }
506
538
  }
507
539
 
540
+ if (flow.source === "builtin") continue;
541
+
508
542
  const scheduleRes = readFlowSchedule(workspaceRoot, flow.id, flowSource, scheduleCtx);
509
543
  if (!scheduleRes.success) {
510
544
  log.debug(`[scheduler] ${flow.id}: ${scheduleRes.error}`);