@fieldwangai/agentflow 0.1.138 → 0.1.142

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.
@@ -11,6 +11,8 @@ const WORKFLOW_ACTION_STATUSES = new Set([
11
11
  "cancelled",
12
12
  "observed",
13
13
  ]);
14
+ const WORKFLOW_CHECKLIST_COMPLETION_POLICIES = new Set(["all_required", "any_required", "manual"]);
15
+ const WORKFLOW_CHECKLIST_ITEM_STATUSES = new Set(["pending", "passed", "failed", "blocked", "skipped"]);
14
16
  const UNSAFE_OBJECT_KEYS = new Set(["__proto__", "prototype", "constructor"]);
15
17
 
16
18
  function plainObject(value) {
@@ -109,6 +111,98 @@ function normalizeStringList(value, maxItems = 100) {
109
111
  return uniqueValues(list.map((item) => cleanString(item, 240)).filter(Boolean)).slice(0, maxItems);
110
112
  }
111
113
 
114
+ function normalizeChecklistSection(value, index = 0) {
115
+ const raw = plainObject(value);
116
+ const rawContent = raw.content ?? raw.value ?? raw.text ?? "";
117
+ const content = Array.isArray(rawContent)
118
+ ? rawContent.map((item) => cleanString(item, 4000)).filter(Boolean).slice(0, 100)
119
+ : cleanString(rawContent, 12000);
120
+ return {
121
+ key: cleanString(raw.key || raw.id || `section-${index + 1}`, 120),
122
+ title: cleanString(raw.title || raw.label || `Section ${index + 1}`, 500),
123
+ content,
124
+ };
125
+ }
126
+
127
+ function normalizeWorkflowChecklist(value) {
128
+ const raw = plainObject(value);
129
+ const rawDocument = plainObject(raw.document);
130
+ const items = (Array.isArray(raw.items) ? raw.items : []).map((value, index) => {
131
+ const item = plainObject(value);
132
+ const rawDetail = item.detail;
133
+ const detailObject = plainObject(rawDetail);
134
+ const sections = Array.isArray(detailObject.sections)
135
+ ? detailObject.sections.map((section, sectionIndex) => normalizeChecklistSection(section, sectionIndex))
136
+ : [];
137
+ const summary = typeof rawDetail === "string"
138
+ ? cleanString(rawDetail, 4000)
139
+ : cleanString(detailObject.summary || detailObject.description, 4000);
140
+ return {
141
+ key: cleanString(item.key || item.id, 240),
142
+ title: cleanString(item.title || item.label || item.key || item.id || `Item ${index + 1}`, 500),
143
+ required: item.required !== false,
144
+ ...(summary || sections.length ? { detail: { ...(summary ? { summary } : {}), ...(sections.length ? { sections } : {}) } } : {}),
145
+ ...(item.evidenceRequired === true || item.evidence_required === true ? { evidenceRequired: true } : {}),
146
+ };
147
+ });
148
+ const completionPolicy = cleanString(raw.completionPolicy || raw.completion_policy || "all_required", 40).toLowerCase();
149
+ const documentUrl = cleanString(rawDocument.url || rawDocument.href, 4000);
150
+ return {
151
+ schemaVersion: 1,
152
+ completionPolicy,
153
+ document: {
154
+ title: cleanString(rawDocument.title || rawDocument.label || "Checklist 详情", 500),
155
+ ...(rawDocument.artifactKey || rawDocument.artifact_key ? { artifactKey: cleanString(rawDocument.artifactKey || rawDocument.artifact_key, 500) } : {}),
156
+ ...(documentUrl ? { url: documentUrl } : {}),
157
+ },
158
+ items,
159
+ };
160
+ }
161
+
162
+ function validateWorkflowChecklist(value) {
163
+ if (!value || typeof value !== "object" || Array.isArray(value)) return "action.checklist must be an object";
164
+ const raw = plainObject(value);
165
+ const schemaVersion = Number(raw.schemaVersion ?? raw.schema_version ?? 1);
166
+ if (schemaVersion !== 1) return `Unsupported action.checklist schemaVersion: ${schemaVersion}`;
167
+ const completionPolicy = rawString(raw.completionPolicy || raw.completion_policy || "all_required").toLowerCase();
168
+ if (!WORKFLOW_CHECKLIST_COMPLETION_POLICIES.has(completionPolicy)) {
169
+ return `Invalid action.checklist completionPolicy: ${completionPolicy}`;
170
+ }
171
+ if (!Array.isArray(raw.items) || raw.items.length === 0) return "action.checklist requires at least one item";
172
+ if (raw.items.length > 100) return "action.checklist supports at most 100 items";
173
+ const seen = new Set();
174
+ for (let index = 0; index < raw.items.length; index += 1) {
175
+ const item = raw.items[index];
176
+ if (!item || typeof item !== "object" || Array.isArray(item)) return `action.checklist.items[${index}] must be an object`;
177
+ const key = rawString(item.key || item.id);
178
+ if (!key || key.length > 240 || /[\0\r\n]/.test(key)) return `action.checklist.items[${index}].key is invalid`;
179
+ if (seen.has(key)) return `action.checklist.items[${index}].key must be unique`;
180
+ seen.add(key);
181
+ if (stringExceeds(item.title || item.label || key, 500)) return `action.checklist.items[${index}].title exceeds 500 characters`;
182
+ if (item.detail != null && typeof item.detail !== "string" && (!item.detail || typeof item.detail !== "object" || Array.isArray(item.detail))) {
183
+ return `action.checklist.items[${index}].detail must be a string or object`;
184
+ }
185
+ const detail = plainObject(item.detail);
186
+ if (typeof item.detail === "string" && stringExceeds(item.detail, 4000)) return `action.checklist.items[${index}].detail exceeds 4000 characters`;
187
+ if (stringExceeds(detail.summary || detail.description, 4000)) return `action.checklist.items[${index}].detail.summary exceeds 4000 characters`;
188
+ if (detail.sections != null && !Array.isArray(detail.sections)) return `action.checklist.items[${index}].detail.sections must be an array`;
189
+ if (Array.isArray(detail.sections) && detail.sections.length > 50) return `action.checklist.items[${index}].detail.sections supports at most 50 entries`;
190
+ }
191
+ const document = plainObject(raw.document);
192
+ if (stringExceeds(document.title || document.label, 500)) return "action.checklist.document.title exceeds 500 characters";
193
+ if (stringExceeds(document.artifactKey || document.artifact_key, 500)) return "action.checklist.document.artifactKey exceeds 500 characters";
194
+ const documentUrl = rawString(document.url || document.href);
195
+ if (documentUrl && !isSafeWorkflowUrl(documentUrl)) return "action.checklist.document.url must use http, https, or an absolute application path";
196
+ return "";
197
+ }
198
+
199
+ export function normalizeWorkflowChecklistItemStatus(value) {
200
+ const raw = cleanString(value, 40).toLowerCase();
201
+ const aliases = { done: "passed", complete: "passed", completed: "passed", success: "passed", error: "failed", cancelled: "skipped", canceled: "skipped" };
202
+ const normalized = aliases[raw] || raw || "pending";
203
+ return WORKFLOW_CHECKLIST_ITEM_STATUSES.has(normalized) ? normalized : "pending";
204
+ }
205
+
112
206
  function normalizeWorkflowArtifact(value, index = 0, defaultScope = "action") {
113
207
  const raw = plainObject(value);
114
208
  const url = cleanString(raw.url || raw.href, 4000);
@@ -347,6 +441,11 @@ export function normalizeWorkflowReport(payload = {}) {
347
441
  if (stringExceeds(rawAction.issueKey || rawAction.issue_key, 240)) return { error: "Workflow action issueKey exceeds 240 characters" };
348
442
  const rawTags = Array.isArray(rawAction.tags) ? rawAction.tags : rawAction.tags == null ? [] : [rawAction.tags];
349
443
  if (rawTags.length > 100 || rawTags.some((tag) => stringExceeds(tag, 240))) return { error: "Workflow action tags exceed supported limits" };
444
+ const hasChecklist = hasOwn(rawAction, "checklist");
445
+ if (hasChecklist) {
446
+ const checklistError = validateWorkflowChecklist(rawAction.checklist);
447
+ if (checklistError) return { error: checklistError };
448
+ }
350
449
  if (hasAction && !isKnownActionStatus(rawAction.status)) return { error: `Invalid workflow action status: ${rawAction.status}` };
351
450
  const rawOccurredAt = rawString(rawAction.occurredAt || rawAction.occurred_at || rawAction.completedAt || rawAction.startedAt);
352
451
  if (rawOccurredAt && !Number.isFinite(Date.parse(rawOccurredAt))) return { error: "action.occurredAt must be an ISO-compatible date" };
@@ -361,6 +460,7 @@ export function normalizeWorkflowReport(payload = {}) {
361
460
  ...(rawAction.platform ? { platform: cleanString(rawAction.platform, 80) } : {}),
362
461
  ...(rawAction.issueKey || rawAction.issue_key ? { issueKey: cleanString(rawAction.issueKey || rawAction.issue_key, 240) } : {}),
363
462
  ...(rawAction.tags != null ? { tags: normalizeStringList(rawAction.tags) } : {}),
463
+ ...(hasChecklist ? { checklist: normalizeWorkflowChecklist(rawAction.checklist) } : {}),
364
464
  occurredAt: cleanString(
365
465
  rawAction.occurredAt ||
366
466
  rawAction.occurred_at ||
@@ -539,6 +639,7 @@ export function normalizeWorkflowReport(payload = {}) {
539
639
  ...(action.platform ? { platform: action.platform } : {}),
540
640
  ...(action.issueKey ? { issueKey: action.issueKey } : {}),
541
641
  ...(action.tags ? { tags: action.tags } : {}),
642
+ ...(action.checklist ? { checklist: action.checklist } : {}),
542
643
  ...(action.occurredAt ? { occurredAt: action.occurredAt } : {}),
543
644
  } : {
544
645
  title: cleanString(payload.title || "Workflow 全局状态更新", 500),
@@ -600,6 +701,24 @@ function resourceVersion(value) {
600
701
  return `rv:${semanticHash(value)}`;
601
702
  }
602
703
 
704
+ function actionDefinitionForVersion(event = {}) {
705
+ const action = plainObject(event.actionModel);
706
+ if (!Object.keys(action).length) return event;
707
+ const checklist = plainObject(action.checklist);
708
+ if (!Object.keys(checklist).length) return action;
709
+ const items = Array.isArray(checklist.items)
710
+ ? checklist.items.map((item) => {
711
+ const clean = { ...plainObject(item) };
712
+ delete clean.state;
713
+ return clean;
714
+ })
715
+ : [];
716
+ const cleanChecklist = { ...checklist, items };
717
+ delete cleanChecklist.progress;
718
+ delete cleanChecklist.source;
719
+ return { ...action, checklist: cleanChecklist };
720
+ }
721
+
603
722
  function addObjectResourceVersions(out, prefix, value, path = []) {
604
723
  if (value === undefined) return;
605
724
  if (path.length) out[`${prefix}:${path.join(".")}`] = resourceVersion(value);
@@ -617,8 +736,15 @@ export function workflowSnapshotResourceVersions(snapshot = {}) {
617
736
  : [];
618
737
  for (const event of runtimeEvents) {
619
738
  const source = cleanString(event?.source || event?.producer || "agentflow", 120).toLowerCase() || "agentflow";
739
+ const checklistState = plainObject(event?.checklistState || event?.checklist_state);
740
+ const checklistSource = cleanString(checklistState.producer || checklistState.source, 120).toLowerCase();
741
+ const checklistActionKey = cleanString(checklistState.actionKey || checklistState.action_key, 240);
742
+ const checklistItemKey = cleanString(checklistState.itemKey || checklistState.item_key, 240);
743
+ if (checklistSource && checklistActionKey && checklistItemKey) {
744
+ out[`checklist:${checklistSource}:${checklistActionKey}:${checklistItemKey}`] = resourceVersion(checklistState);
745
+ }
620
746
  const actionKey = cleanString(event?.actionModel?.key || event?.action || event?.actionId || event?.stageKey, 240);
621
- if (actionKey && event?.auxiliary !== true) out[`action:${source}:${actionKey}`] = resourceVersion(event);
747
+ if (actionKey && event?.auxiliary !== true) out[`action:${source}:${actionKey}`] = resourceVersion(actionDefinitionForVersion(event));
622
748
  for (const artifact of Array.isArray(event?.artifacts) ? event.artifacts : []) {
623
749
  const producer = cleanString(artifact?.producer || source, 120).toLowerCase() || source;
624
750
  const key = cleanString(artifact?.key || artifact?.artifactKey || artifact?.artifact_key, 500);
@@ -51,6 +51,13 @@ import {
51
51
  import { buildGitContext, inferGitRepoRootFromWorktree, loadGitWorktree, normalizeGitContext, unloadGitWorktree } from "../lib/git-worktree.mjs";
52
52
  import { createGitLabMergeRequest } from "../lib/gitlab-mr.mjs";
53
53
  import { sendWecomAppMarkdown, sendWecomGroupMarkdown } from "../lib/wecom.mjs";
54
+ import {
55
+ advanceJenkinsBuild,
56
+ createJenkinsSkillInvoker,
57
+ normalizeJenkinsBuildConfig,
58
+ readJenkinsBuildState,
59
+ writeJenkinsBuildState,
60
+ } from "../lib/jenkins.mjs";
54
61
 
55
62
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
56
63
 
@@ -500,6 +507,106 @@ async function emitWecomMarkdownNode(workspaceRoot, flowName, uuid, instanceId,
500
507
  return emitLocalNoopPrompt(workspaceRoot, runDir, instanceId, "wecom-markdown", `${result.message}\n`);
501
508
  }
502
509
 
510
+ function emitJenkinsBuildNode(workspaceRoot, flowName, uuid, instanceId, execId) {
511
+ const runDir = getRunDir(workspaceRoot, flowName, uuid);
512
+ const data = getResolvedValues(workspaceRoot, flowName, uuid, instanceId);
513
+ if (!data.ok) throw new Error("getResolvedValues failed");
514
+ const inputs = data.resolvedInputs || {};
515
+ const config = normalizeJenkinsBuildConfig(inputs);
516
+ const statePath = path.join(runDir, "state", `${instanceId}.jenkins.json`);
517
+ const priorState = readJenkinsBuildState(statePath);
518
+ if (priorState?.job && priorState.job !== config.job) {
519
+ throw new Error(`checkpoint job mismatch: ${priorState.job} != ${config.job}`);
520
+ }
521
+ let invoker = null;
522
+ const invoke = (operation, args) => {
523
+ if (!invoker) {
524
+ invoker = createJenkinsSkillInvoker({
525
+ workspaceRoot,
526
+ credentialRef: config.credentialRef,
527
+ env: process.env,
528
+ });
529
+ }
530
+ return invoker(operation, args);
531
+ };
532
+ const result = advanceJenkinsBuild({
533
+ state: priorState,
534
+ config,
535
+ invoke,
536
+ persistState: (state) => writeJenkinsBuildState(statePath, state),
537
+ nowMs: Date.now(),
538
+ cancelled: readCancelFlag(runDir),
539
+ });
540
+ writeJenkinsBuildState(statePath, result.state);
541
+ writeOutputSlot(runDir, instanceId, execId, "status", result.outputs?.status || result.state?.status || "");
542
+ writeOutputSlot(runDir, instanceId, execId, "url", result.outputs?.url || result.state?.url || result.state?.buildUrl || "");
543
+ writeOutputSlot(runDir, instanceId, execId, "qrUrl", result.outputs?.qrUrl || result.state?.qrUrl || "");
544
+
545
+ const message = result.message || result.state?.message || "Jenkins Build";
546
+ if (result.kind === "failed") {
547
+ writeResult(
548
+ workspaceRoot,
549
+ flowName,
550
+ uuid,
551
+ instanceId,
552
+ { status: "failed", message },
553
+ { execId, preserveBody: false, body: JSON.stringify({ status: "ERROR", message }, null, 2) },
554
+ );
555
+ throw new Error(message);
556
+ }
557
+
558
+ const promptPath = emitLocalNoopPrompt(
559
+ workspaceRoot,
560
+ runDir,
561
+ instanceId,
562
+ result.kind === "waiting" ? "jenkins-waiting" : "jenkins-complete",
563
+ `${message}\n`,
564
+ );
565
+ if (result.kind === "waiting") {
566
+ const waitId = `${uuid}:${instanceId}:jenkins-build`;
567
+ writeWaitState(runDir, {
568
+ waitId,
569
+ status: "waiting",
570
+ reason: "tool_jenkins_build",
571
+ resumeMode: "rerun",
572
+ flowName,
573
+ uuid,
574
+ instanceId,
575
+ execId,
576
+ phase: result.state?.phase || "queued",
577
+ buildNumber: result.state?.buildNumber || "",
578
+ wakeAt: result.wakeAt,
579
+ createdAt: priorState?.createdAt || new Date().toISOString(),
580
+ });
581
+ writeResult(
582
+ workspaceRoot,
583
+ flowName,
584
+ uuid,
585
+ instanceId,
586
+ { status: "pending", message },
587
+ { execId, preserveBody: false, body: JSON.stringify({ phase: result.state?.phase, wakeAt: result.wakeAt }, null, 2) },
588
+ );
589
+ } else {
590
+ writeResult(
591
+ workspaceRoot,
592
+ flowName,
593
+ uuid,
594
+ instanceId,
595
+ { status: "success", message },
596
+ {
597
+ execId,
598
+ preserveBody: false,
599
+ body: JSON.stringify({
600
+ status: result.outputs?.status || result.state?.status || "",
601
+ url: result.outputs?.url || result.state?.url || "",
602
+ qrUrl: result.outputs?.qrUrl || result.state?.qrUrl || "",
603
+ }, null, 2),
604
+ },
605
+ );
606
+ }
607
+ return { promptPath, lifecycle: result.kind };
608
+ }
609
+
503
610
  function emitCdWorkspaceNode(workspaceRoot, flowName, uuid, instanceId, execId) {
504
611
  const runDir = getRunDir(workspaceRoot, flowName, uuid);
505
612
  const { inputs, workspaceContext } = resolveNodeRuntimeContexts(workspaceRoot, flowName, uuid, instanceId);
@@ -1203,26 +1310,29 @@ async function main() {
1203
1310
  return;
1204
1311
  }
1205
1312
 
1206
- if (definitionId === "tool_git_checkout" || definitionId === "tool_git_worktree_load" || definitionId === "tool_git_worktree_unload" || definitionId === "tool_gitlab_create_mr" || definitionId === "tool_wecom_send_group_markdown" || definitionId === "tool_wecom_send_app_markdown" || definitionId === "control_cd_workspace" || definitionId === "control_user_workspace" || definitionId === "control_load_skills" || definitionId === "tool_print") {
1313
+ if (definitionId === "tool_git_checkout" || definitionId === "tool_git_worktree_load" || definitionId === "tool_git_worktree_unload" || definitionId === "tool_gitlab_create_mr" || definitionId === "tool_jenkins_build" || definitionId === "tool_wecom_send_group_markdown" || definitionId === "tool_wecom_send_app_markdown" || definitionId === "control_cd_workspace" || definitionId === "control_user_workspace" || definitionId === "control_load_skills" || definitionId === "tool_print") {
1207
1314
  try {
1208
- const promptPath =
1315
+ const localResult =
1209
1316
  definitionId === "tool_git_checkout"
1210
- ? emitGitCheckoutNode(workspaceRoot, flowName, uuid, instanceId, execId, resultPathRel)
1317
+ ? { promptPath: emitGitCheckoutNode(workspaceRoot, flowName, uuid, instanceId, execId, resultPathRel) }
1211
1318
  : definitionId === "tool_git_worktree_load"
1212
- ? emitGitWorktreeLoadNode(workspaceRoot, flowName, uuid, instanceId, execId)
1319
+ ? { promptPath: emitGitWorktreeLoadNode(workspaceRoot, flowName, uuid, instanceId, execId) }
1213
1320
  : definitionId === "tool_git_worktree_unload"
1214
- ? emitGitWorktreeUnloadNode(workspaceRoot, flowName, uuid, instanceId, execId)
1321
+ ? { promptPath: emitGitWorktreeUnloadNode(workspaceRoot, flowName, uuid, instanceId, execId) }
1215
1322
  : definitionId === "tool_gitlab_create_mr"
1216
- ? await emitGitLabCreateMrNode(workspaceRoot, flowName, uuid, instanceId, execId)
1323
+ ? { promptPath: await emitGitLabCreateMrNode(workspaceRoot, flowName, uuid, instanceId, execId) }
1324
+ : definitionId === "tool_jenkins_build"
1325
+ ? emitJenkinsBuildNode(workspaceRoot, flowName, uuid, instanceId, execId)
1217
1326
  : definitionId === "tool_wecom_send_group_markdown" || definitionId === "tool_wecom_send_app_markdown"
1218
- ? await emitWecomMarkdownNode(workspaceRoot, flowName, uuid, instanceId, execId, definitionId)
1327
+ ? { promptPath: await emitWecomMarkdownNode(workspaceRoot, flowName, uuid, instanceId, execId, definitionId) }
1219
1328
  : definitionId === "control_cd_workspace"
1220
- ? emitCdWorkspaceNode(workspaceRoot, flowName, uuid, instanceId, execId)
1329
+ ? { promptPath: emitCdWorkspaceNode(workspaceRoot, flowName, uuid, instanceId, execId) }
1221
1330
  : definitionId === "control_user_workspace"
1222
- ? emitUserWorkspaceNode(workspaceRoot, flowName, uuid, instanceId, execId)
1331
+ ? { promptPath: emitUserWorkspaceNode(workspaceRoot, flowName, uuid, instanceId, execId) }
1223
1332
  : definitionId === "control_load_skills"
1224
- ? emitLoadSkillsNode(workspaceRoot, flowName, uuid, instanceId, execId)
1225
- : emitToolPrintNode(workspaceRoot, flowName, uuid, instanceId, execId);
1333
+ ? { promptPath: emitLoadSkillsNode(workspaceRoot, flowName, uuid, instanceId, execId) }
1334
+ : { promptPath: emitToolPrintNode(workspaceRoot, flowName, uuid, instanceId, execId) };
1335
+ const promptPath = localResult.promptPath;
1226
1336
  writeCacheJsonForNode(workspaceRoot, flowName, uuid, instanceId, execId);
1227
1337
  logToRunTag(workspaceRoot, flowName, uuid, "pre-process", { event: "runtime-context-node", instanceId, definitionId });
1228
1338
  console.log(JSON.stringify({
@@ -1233,6 +1343,7 @@ async function main() {
1233
1343
  subagent: "agentflow-node-executor",
1234
1344
  optionalPromptPath: promptPath,
1235
1345
  definitionId,
1346
+ ...(localResult.lifecycle ? { nodeLifecycle: localResult.lifecycle } : {}),
1236
1347
  }));
1237
1348
  return;
1238
1349
  } catch (e) {
@@ -21,11 +21,11 @@ const LOG_FILE = "logs/log.txt";
21
21
  * @param {string} tag - 来源标识:get-ready-nodes | check-cache | pre-process | post-process | result
22
22
  * @param {string|object} message - 文本或对象(对象会 JSON.stringify)
23
23
  */
24
- export function logToRunTag(workspaceRoot, flowName, uuid, tag, message) {
24
+ export function logToRunTag(workspaceRoot, flowName, uuid, tag, message, opts = {}) {
25
25
  if (!workspaceRoot || !flowName || !uuid) return;
26
26
  const text = typeof message === "string" ? message : JSON.stringify(message);
27
27
  try {
28
- const runDir = getRunDir(workspaceRoot, flowName, uuid);
28
+ const runDir = opts.runDir || getRunDir(workspaceRoot, flowName, uuid);
29
29
  const logPath = path.join(runDir, LOG_FILE);
30
30
  fs.mkdirSync(path.dirname(logPath), { recursive: true });
31
31
  const line = `[${new Date().toISOString()}] [${tag}] ${text}\n`;
@@ -12,7 +12,7 @@
12
12
  * import { writeResult } from "./write-result.mjs";
13
13
  * writeResult(workspaceRoot, flowName, uuid, instanceId, fields, options?)
14
14
  * fields: { status, message, finishedAt?, outputPath?, branch?, cacheNotMetReason?, elapsedMs? }
15
- * options: { preserveBody?: boolean, body?: string, execId?: number }
15
+ * options: { preserveBody?: boolean, body?: string, execId?: number, runDir?: string }
16
16
  */
17
17
 
18
18
  import fs from "fs";
@@ -53,10 +53,10 @@ function getExistingBody(resultPath) {
53
53
  * @param {string} uuid - 本次 run 的 uuid
54
54
  * @param {string} instanceId - 节点 instance id
55
55
  * @param {{ status: string, message: string, finishedAt?: string, outputPath?: string, branch?: string, cacheNotMetReason?: string, elapsedMs?: number }} fields - 必填 status、message;可选其余(elapsedMs 为节点执行耗时毫秒,供 UI 展示)
56
- * @param {{ preserveBody?: boolean, body?: string, execId?: number }} [options] - preserveBody:保留已有正文;body:指定正文内容;execId:本轮 execId,缺省则从 memory 读取
56
+ * @param {{ preserveBody?: boolean, body?: string, execId?: number, runDir?: string }} [options] - preserveBody:保留已有正文;body:指定正文内容;execId:本轮 execId,缺省则从 memory 读取;runDir:多用户调度时使用已解析的精确运行目录
57
57
  */
58
58
  export function writeResult(workspaceRoot, flowName, uuid, instanceId, fields, options = {}) {
59
- const runDir = getRunDir(workspaceRoot, flowName, uuid);
59
+ const runDir = options.runDir || getRunDir(workspaceRoot, flowName, uuid);
60
60
  const execId = options.execId ?? loadExecId(workspaceRoot, flowName, uuid, instanceId);
61
61
  const resultBasename = intermediateResultBasename(instanceId, execId);
62
62
  const resultPath = path.join(runDir, intermediateDirForNode(instanceId), resultBasename);
@@ -114,7 +114,7 @@ export function writeResult(workspaceRoot, flowName, uuid, instanceId, fields, o
114
114
  branch: branch ?? undefined,
115
115
  cacheNotMetReason: cacheNotMetReason ?? undefined,
116
116
  resultPathRel: path.relative(runDir, resultPath),
117
- });
117
+ }, { runDir });
118
118
  }
119
119
 
120
120
  function main() {
@@ -0,0 +1,64 @@
1
+ ---
2
+ # Built-in node: durable Jenkins build
3
+ description: |
4
+ Trigger one Jenkins job and durably monitor it until completion.
5
+
6
+ The node persists queue/build checkpoints and never keeps a process blocked while waiting.
7
+ Scheduler re-enters the same node at `pollInterval`; an existing queueId/buildNumber is reused,
8
+ so a resumed run does not trigger the job again. Jenkins FAILURE/ABORTED/TIMEOUT are business
9
+ outcomes and continue to downstream notification nodes. Authentication, configuration, and
10
+ repeated platform request errors fail the AgentFlow node.
11
+
12
+ Credentials are read from environment configuration. `credentialRef: team-ci` selects
13
+ `JENKINS_TEAM_CI_BASE_URL`, `JENKINS_TEAM_CI_USERNAME`, and `JENKINS_TEAM_CI_TOKEN`, falling
14
+ back to the standard `JENKINS_BASE_URL`, `JENKINS_USERNAME`, and `JENKINS_TOKEN` variables.
15
+ displayName: Jenkins Build
16
+ input:
17
+ - type: node
18
+ name: prev
19
+ default: ""
20
+ - type: text
21
+ name: job
22
+ default: ""
23
+ required: true
24
+ showOnNode: true
25
+ - type: text
26
+ name: parameters
27
+ default: "{}"
28
+ description: "Jenkins Job 参数 JSON object。"
29
+ showOnNode: true
30
+ - type: text
31
+ name: credentialRef
32
+ default: ""
33
+ description: "Project Deployment 中配置的 Jenkins 凭证引用;不在 flow.yaml 中填写 token。"
34
+ showOnNode: false
35
+ - type: text
36
+ name: pollInterval
37
+ default: "30s"
38
+ showOnNode: false
39
+ - type: text
40
+ name: timeout
41
+ default: "2h"
42
+ showOnNode: false
43
+ output:
44
+ - type: node
45
+ name: next
46
+ default: ""
47
+ - type: text
48
+ name: status
49
+ default: ""
50
+ required: true
51
+ showOnNode: true
52
+ - type: text
53
+ name: url
54
+ default: ""
55
+ required: true
56
+ description: "优先返回安装包 URL;没有安装包时返回 Jenkins Build URL。"
57
+ showOnNode: true
58
+ - type: text
59
+ name: qrUrl
60
+ default: ""
61
+ description: "解析到二维码时返回,否则为空。"
62
+ showOnNode: true
63
+ ---
64
+ Trigger Jenkins job `${job}` with `${parameters}`, wait durably, then output `${status}`, `${url}`, and optional `${qrUrl}`.