@fieldwangai/agentflow 0.1.68 → 0.1.70

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.
@@ -0,0 +1,93 @@
1
+ import fs from "fs";
2
+ import os from "os";
3
+ import path from "path";
4
+
5
+ import {
6
+ AGENTFLOW_HOME_CONFIG_PATH,
7
+ expandAgentflowHomePath,
8
+ getAgentflowDataRoot,
9
+ getAgentflowDataRootOverride,
10
+ writeAgentflowDataRootOverride,
11
+ } from "./paths.mjs";
12
+
13
+ function defaultAgentflowDataRoot() {
14
+ return path.join(os.homedir(), "agentflow");
15
+ }
16
+
17
+ function pathInside(parent, candidate) {
18
+ const base = path.resolve(parent);
19
+ const target = path.resolve(candidate);
20
+ return target === base || target.startsWith(base + path.sep);
21
+ }
22
+
23
+ function normalizeDataRoot(raw) {
24
+ const dataRoot = expandAgentflowHomePath(raw);
25
+ if (dataRoot && !path.isAbsolute(dataRoot)) {
26
+ throw new Error("dataRoot must be an absolute path");
27
+ }
28
+ return dataRoot;
29
+ }
30
+
31
+ function copyEntry(src, dest) {
32
+ const stat = fs.lstatSync(src);
33
+ if (stat.isSymbolicLink()) {
34
+ if (!fs.existsSync(dest)) fs.symlinkSync(fs.readlinkSync(src), dest);
35
+ return;
36
+ }
37
+ if (stat.isDirectory()) {
38
+ fs.mkdirSync(dest, { recursive: true });
39
+ for (const name of fs.readdirSync(src)) {
40
+ copyEntry(path.join(src, name), path.join(dest, name));
41
+ }
42
+ return;
43
+ }
44
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
45
+ fs.copyFileSync(src, dest);
46
+ }
47
+
48
+ function migrateDataRoot(oldRoot, newRoot) {
49
+ const from = path.resolve(oldRoot);
50
+ const to = path.resolve(newRoot);
51
+ if (from === to) return { migrated: false, copied: false };
52
+ if (pathInside(from, to)) {
53
+ throw new Error("dataRoot target cannot be inside the current data root");
54
+ }
55
+ if (pathInside(to, from)) {
56
+ throw new Error("dataRoot target cannot be a parent of the current data root");
57
+ }
58
+ fs.mkdirSync(to, { recursive: true });
59
+ if (!fs.existsSync(from)) return { migrated: true, copied: false };
60
+ for (const name of fs.readdirSync(from)) {
61
+ copyEntry(path.join(from, name), path.join(to, name));
62
+ }
63
+ return { migrated: true, copied: true };
64
+ }
65
+
66
+ export function readAdminStorageConfig() {
67
+ const envDataRoot = normalizeDataRoot(process.env.AGENTFLOW_HOME || "");
68
+ const configuredDataRoot = getAgentflowDataRootOverride();
69
+ return {
70
+ version: 1,
71
+ dataRoot: getAgentflowDataRoot(),
72
+ configuredDataRoot,
73
+ defaultDataRoot: defaultAgentflowDataRoot(),
74
+ envDataRoot,
75
+ envLocked: Boolean(envDataRoot),
76
+ configPath: AGENTFLOW_HOME_CONFIG_PATH,
77
+ };
78
+ }
79
+
80
+ export function writeAdminStorageConfig(config = {}) {
81
+ const current = readAdminStorageConfig();
82
+ if (current.envLocked) {
83
+ throw new Error("AGENTFLOW_HOME is set; update the environment variable instead of Admin Settings");
84
+ }
85
+ const nextRoot = normalizeDataRoot(config.dataRoot ?? "");
86
+ if (!nextRoot) throw new Error("dataRoot is required");
87
+ const migration = migrateDataRoot(current.dataRoot, nextRoot);
88
+ writeAgentflowDataRootOverride(nextRoot);
89
+ return {
90
+ ...readAdminStorageConfig(),
91
+ migration,
92
+ };
93
+ }
package/bin/lib/apply.mjs CHANGED
@@ -24,6 +24,7 @@ import { printEntryAndFlowFiles, printNodeStatusTable, runValidateFlowAndExitIfI
24
24
  import { clearApplyActiveLock, writeApplyActiveLock } from "./run-apply-active-lock.mjs";
25
25
  import { ensureReference, findFlowNameByUuid, getFlowDir, getRunDir } from "./workspace.mjs";
26
26
  import { readMergedEnvObject } from "./user-env.mjs";
27
+ import { appendRunLedgerEvent } from "./run-ledger.mjs";
27
28
 
28
29
  const PARALLEL_PREFIX_COLORS = [
29
30
  (s) => chalk.cyan(s),
@@ -72,6 +73,31 @@ export async function apply(workspaceRoot, flowName, uuidArg, dryRun, agentModel
72
73
  // 提前 ensure:apply-start 携带原始 run 起始时间,UI 计时器可按「这个 uuid 从头到现在的总时长」显示,
73
74
  // 避免每次 resume 仅从 totalExecutedMs 累加看起来像从 resume 才开始计时。
74
75
  const priorRunStartTime = ensureRunStartTime(workspaceRoot, flowName, uuid);
76
+ let runStartTime = priorRunStartTime;
77
+ let totalExecutedMs = priorTotalExecutedMs;
78
+ const writeRunLedger = !dryRun;
79
+ let runLedgerFinished = false;
80
+ const runLedgerBase = {
81
+ kind: "pipeline",
82
+ runId: uuid,
83
+ userId: String(process.env.AGENTFLOW_USER_ID || ""),
84
+ username: String(process.env.AGENTFLOW_USER_ID || ""),
85
+ flowId: flowName,
86
+ flowSource: "user",
87
+ at: priorRunStartTime,
88
+ };
89
+ if (writeRunLedger) appendRunLedgerEvent({ ...runLedgerBase, type: "run_started" });
90
+ const finishRunLedger = (status) => {
91
+ if (!writeRunLedger || runLedgerFinished) return;
92
+ runLedgerFinished = true;
93
+ appendRunLedgerEvent({
94
+ ...runLedgerBase,
95
+ type: "run_finished",
96
+ endedAt: Date.now(),
97
+ durationMs: totalExecutedMs,
98
+ status,
99
+ });
100
+ };
75
101
  emitEvent(workspaceRoot, flowName, uuid, {
76
102
  event: "apply-start",
77
103
  flowName,
@@ -85,8 +111,6 @@ export async function apply(workspaceRoot, flowName, uuidArg, dryRun, agentModel
85
111
  writeApplyActiveLock(workspaceRoot, flowName, uuid);
86
112
 
87
113
  try {
88
- let runStartTime = priorRunStartTime;
89
- let totalExecutedMs = priorTotalExecutedMs;
90
114
  let round = 0;
91
115
  while (round < MAX_LOOP_ROUNDS) {
92
116
  round++;
@@ -100,6 +124,7 @@ export async function apply(workspaceRoot, flowName, uuidArg, dryRun, agentModel
100
124
  if (readyNodes.length === 0) {
101
125
  if (allDone) {
102
126
  saveTotalExecutedMs(workspaceRoot, flowName, uuid, totalExecutedMs);
127
+ finishRunLedger("success");
103
128
  const totalElapsed = formatDuration(totalExecutedMs);
104
129
  emitEvent(workspaceRoot, flowName, uuid, {
105
130
  event: "apply-done",
@@ -163,6 +188,7 @@ export async function apply(workspaceRoot, flowName, uuidArg, dryRun, agentModel
163
188
  if (options.length === 0) {
164
189
  log.info(chalk.yellow(`节点 ${pendId} 未配置任何选项(output 槽位),无法选择。请先在编辑器中添加 output 槽位。`));
165
190
  log.info(chalk.bold.yellow("→ " + t("flow.resume_hint") + " ") + resumeExample);
191
+ finishRunLedger("interrupted");
166
192
  return;
167
193
  }
168
194
 
@@ -177,6 +203,7 @@ export async function apply(workspaceRoot, flowName, uuidArg, dryRun, agentModel
177
203
  }
178
204
  process.stderr.write(chalk.bold.cyan("━━━━━━━━━━━━━━━━━━━━━━━━━\n"));
179
205
  log.info(chalk.bold.yellow("→ " + t("flow.resume_hint") + " ") + resumeExample);
206
+ finishRunLedger("interrupted");
180
207
  return;
181
208
  }
182
209
 
@@ -209,6 +236,7 @@ export async function apply(workspaceRoot, flowName, uuidArg, dryRun, agentModel
209
236
  if (trimmed === "q") {
210
237
  rl.close();
211
238
  log.info(chalk.yellow("用户取消,流程暂停。") + chalk.dim(` 恢复命令: ${resumeExample}`));
239
+ finishRunLedger("interrupted");
212
240
  return;
213
241
  }
214
242
  const idx = parseInt(trimmed, 10);
@@ -250,6 +278,7 @@ export async function apply(workspaceRoot, flowName, uuidArg, dryRun, agentModel
250
278
  }
251
279
  process.stderr.write(chalk.bold.cyan("━━━━━━━━━━━━━━━━━━━━━━━━━\n"));
252
280
  log.info(chalk.bold.yellow("→ " + t("flow.resume_hint") + " ") + resumeExample);
281
+ finishRunLedger("interrupted");
253
282
  return;
254
283
  }
255
284
 
@@ -380,6 +409,7 @@ ${currentContent}
380
409
  } else if (answer.trim().toLowerCase() === "q") {
381
410
  rl.close();
382
411
  log.info(chalk.yellow("用户取消,流程暂停。") + chalk.dim(` 恢复命令: ${resumeExample}`));
412
+ finishRunLedger("interrupted");
383
413
  return;
384
414
  }
385
415
  }
@@ -393,6 +423,7 @@ ${currentContent}
393
423
  }
394
424
 
395
425
  log.info(chalk.bold.yellow("→ " + t("flow.resume_hint") + " ") + resumeExample);
426
+ finishRunLedger("interrupted");
396
427
  return;
397
428
  }
398
429
  const endNodeIds = Array.isArray(parseOut.nodes)
@@ -401,6 +432,7 @@ ${currentContent}
401
432
  const endReached = endNodeIds.some((id) => instanceStatus[id] === "success");
402
433
  if (endReached) {
403
434
  saveTotalExecutedMs(workspaceRoot, flowName, uuid, totalExecutedMs);
435
+ finishRunLedger("success");
404
436
  const totalElapsed = formatDuration(totalExecutedMs);
405
437
  emitEvent(workspaceRoot, flowName, uuid, {
406
438
  event: "apply-done",
@@ -763,6 +795,7 @@ ${currentContent}
763
795
  maxErr.uuid = uuid;
764
796
  throw maxErr;
765
797
  } finally {
798
+ finishRunLedger("failed");
766
799
  clearApplyActiveLock(workspaceRoot, flowName, uuid);
767
800
  }
768
801
  }
@@ -708,6 +708,47 @@ function buildPackagedRuntimeFromScript(command, destDir, opts = {}) {
708
708
  };
709
709
  }
710
710
 
711
+ function safePackageRefPath(raw) {
712
+ const text = String(raw || "").trim().replace(/^["']|["']$/g, "").replace(/\\/g, "/");
713
+ if (!text || path.isAbsolute(text) || text.includes("\n") || text.includes("\r")) return "";
714
+ const normalized = path.posix.normalize(text).replace(/^\/+/, "");
715
+ if (!normalized || normalized === "." || normalized.startsWith("../") || normalized.includes("/../")) return "";
716
+ return normalized;
717
+ }
718
+
719
+ function packageNodeReferenceFiles(destDir, opts = {}) {
720
+ const flowDir = opts.flowDir ? path.resolve(opts.flowDir) : "";
721
+ const packagedFiles = [];
722
+ const result = {};
723
+ const copyRef = (rawRef, targetRel, key) => {
724
+ const clean = safePackageRefPath(rawRef);
725
+ if (!clean || !flowDir) return "";
726
+ const source = path.resolve(flowDir, ...clean.split("/"));
727
+ if (!isInsideDir(source, flowDir) || !fs.existsSync(source) || !fs.statSync(source).isFile()) return "";
728
+ const rel = targetRel.replace(/\\/g, "/");
729
+ const abs = path.join(destDir, rel);
730
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
731
+ fs.copyFileSync(source, abs);
732
+ packagedFiles.push({ from: source, to: rel, role: key });
733
+ result[key] = rel;
734
+ return rel;
735
+ };
736
+ const scriptRef = copyRef(opts.scriptRef, `scripts/${path.basename(safePackageRefPath(opts.scriptRef) || "script.mjs")}`, "scriptRef");
737
+ const implementationRef = copyRef(opts.implementationRef, "implementation.md", "implementationRef");
738
+ let scriptRuntime = null;
739
+ if (scriptRef) {
740
+ const abs = path.join(destDir, scriptRef);
741
+ scriptRuntime = {
742
+ type: "tool_nodejs",
743
+ language: scriptLanguageFor("", scriptRef),
744
+ entry: scriptRef,
745
+ args: [],
746
+ };
747
+ if (!fs.existsSync(abs)) scriptRuntime = null;
748
+ }
749
+ return { ...result, packagedFiles, scriptRuntime };
750
+ }
751
+
711
752
  export function publishNodeFromInstance(workspaceRoot, payload = {}, options = {}) {
712
753
  const label = String(payload.label || payload.instanceId || "node").trim();
713
754
  const id = safePackageId(payload.id || payload.packageId || label);
@@ -732,6 +773,9 @@ export function publishNodeFromInstance(workspaceRoot, payload = {}, options = {
732
773
  ...(slot.showOnNode != null ? { showOnNode: Boolean(slot.showOnNode) } : {}),
733
774
  }));
734
775
  const script = String(payload.script || "").trim();
776
+ const scriptRef = String(payload.scriptRef || "").trim();
777
+ const implementationRef = String(payload.implementationRef || "").trim();
778
+ const implementationMode = String(payload.implementationMode || "").trim();
735
779
  const body = String(payload.body || "").trim();
736
780
  const description = String(payload.description || body || `Published from node ${label}`).trim();
737
781
  const dest = path.join(workspacePackageRoot(workspaceRoot), "nodes", id, version);
@@ -744,13 +788,18 @@ export function publishNodeFromInstance(workspaceRoot, payload = {}, options = {
744
788
  fs.mkdirSync(path.dirname(dest), { recursive: true });
745
789
  fs.rmSync(dest, { recursive: true, force: true });
746
790
  fs.mkdirSync(dest, { recursive: true });
791
+ const packagedRefs = packageNodeReferenceFiles(dest, {
792
+ flowDir: options.flowDir || payload.flowDir,
793
+ scriptRef,
794
+ implementationRef,
795
+ });
747
796
  const packagedScript = script
748
797
  ? buildPackagedRuntimeFromScript(script, dest, {
749
798
  flowDir: options.flowDir || payload.flowDir,
750
799
  workspaceRoot,
751
800
  })
752
801
  : null;
753
- const runtime = packagedScript?.runtime || (
802
+ const runtime = packagedScript?.runtime || packagedRefs.scriptRuntime || (
754
803
  script
755
804
  ? {
756
805
  type: "tool_nodejs",
@@ -775,7 +824,10 @@ export function publishNodeFromInstance(workspaceRoot, payload = {}, options = {
775
824
  createdAt: existingManifest?.createdAt || now,
776
825
  updatedAt: now,
777
826
  };
778
- if (packagedScript?.packagedFiles?.length) manifest.packagedFiles = packagedScript.packagedFiles;
827
+ if (packagedRefs.implementationRef) manifest.implementationRef = packagedRefs.implementationRef;
828
+ if (implementationMode) manifest.implementationMode = implementationMode;
829
+ const packagedFiles = [...(packagedScript?.packagedFiles || []), ...(packagedRefs.packagedFiles || [])];
830
+ if (packagedFiles.length) manifest.packagedFiles = packagedFiles;
779
831
  fs.writeFileSync(path.join(dest, NODE_MANIFEST), yaml.dump(manifest, { lineWidth: -1 }), "utf-8");
780
832
  fs.writeFileSync(
781
833
  path.join(dest, "README.md"),
@@ -791,7 +843,7 @@ export function publishNodeFromInstance(workspaceRoot, payload = {}, options = {
791
843
  definitionId: `marketplace:${id}@${version}`,
792
844
  baseDefinitionId: manifest.baseDefinitionId,
793
845
  marketplaceDefinitionId: `marketplace:${id}@${version}`,
794
- packagedFiles: packagedScript?.packagedFiles || [],
846
+ packagedFiles,
795
847
  };
796
848
  }
797
849
 
package/bin/lib/paths.mjs CHANGED
@@ -20,17 +20,55 @@ export const PACKAGE_AGENTS_DIR = path.join(PACKAGE_ROOT, "agents");
20
20
  export const PACKAGE_AGENTS_JSON = path.join(PACKAGE_AGENTS_DIR, "agents.json");
21
21
 
22
22
  /**
23
- * 用户级 AgentFlow 数据根目录:`AGENTFLOW_HOME` 或 `~/agentflow`。
23
+ * 用户级 AgentFlow 数据根目录:`AGENTFLOW_HOME`、`~/.agentflow/config.json:dataRoot` 或 `~/agentflow`。
24
24
  * 与项目根 workspaceRoot 分离;run、pipelines、agents 等均落盘于此。
25
25
  */
26
+ export const AGENTFLOW_CONFIG_DIR = path.join(os.homedir(), ".agentflow");
27
+ export const AGENTFLOW_HOME_CONFIG_PATH = path.join(AGENTFLOW_CONFIG_DIR, "config.json");
28
+
29
+ export function expandAgentflowHomePath(rawPath) {
30
+ let raw = String(rawPath || "").trim();
31
+ if (!raw) return "";
32
+ if (raw === "~") raw = os.homedir();
33
+ else if (raw.startsWith("~/")) raw = path.join(os.homedir(), raw.slice(2));
34
+ return path.resolve(raw);
35
+ }
36
+
37
+ function readAgentflowHomeConfig() {
38
+ try {
39
+ if (!fs.existsSync(AGENTFLOW_HOME_CONFIG_PATH)) return {};
40
+ const parsed = JSON.parse(fs.readFileSync(AGENTFLOW_HOME_CONFIG_PATH, "utf-8"));
41
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
42
+ } catch {
43
+ return {};
44
+ }
45
+ }
46
+
47
+ export function getAgentflowDataRootOverride() {
48
+ const config = readAgentflowHomeConfig();
49
+ return expandAgentflowHomePath(config.dataRoot || "");
50
+ }
51
+
52
+ export function writeAgentflowDataRootOverride(dataRoot) {
53
+ const nextRoot = expandAgentflowHomePath(dataRoot);
54
+ if (nextRoot && !path.isAbsolute(nextRoot)) {
55
+ throw new Error("dataRoot must be an absolute path");
56
+ }
57
+ const current = readAgentflowHomeConfig();
58
+ const next = { ...current, dataRoot: nextRoot };
59
+ if (!nextRoot) delete next.dataRoot;
60
+ fs.mkdirSync(path.dirname(AGENTFLOW_HOME_CONFIG_PATH), { recursive: true });
61
+ fs.writeFileSync(AGENTFLOW_HOME_CONFIG_PATH, JSON.stringify(next, null, 2) + "\n", "utf-8");
62
+ return nextRoot;
63
+ }
64
+
26
65
  export function getAgentflowDataRoot() {
27
66
  const env = process.env.AGENTFLOW_HOME;
28
67
  if (env != null && String(env).trim() !== "") {
29
- let raw = String(env).trim();
30
- if (raw === "~") raw = os.homedir();
31
- else if (raw.startsWith("~/")) raw = path.join(os.homedir(), raw.slice(2));
32
- return path.resolve(raw);
68
+ return expandAgentflowHomePath(env);
33
69
  }
70
+ const configured = getAgentflowDataRootOverride();
71
+ if (configured) return configured;
34
72
  return path.join(os.homedir(), "agentflow");
35
73
  }
36
74
 
@@ -0,0 +1,96 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import crypto from "crypto";
4
+
5
+ import { getAgentflowDataRoot } from "./paths.mjs";
6
+
7
+ function localDayKey(timeMs) {
8
+ const d = new Date(Number(timeMs) || Date.now());
9
+ const y = d.getFullYear();
10
+ const m = String(d.getMonth() + 1).padStart(2, "0");
11
+ const day = String(d.getDate()).padStart(2, "0");
12
+ return `${y}-${m}-${day}`;
13
+ }
14
+
15
+ function safeSegment(value, fallback = "run") {
16
+ return String(value || fallback)
17
+ .trim()
18
+ .replace(/[^a-zA-Z0-9._-]+/g, "_")
19
+ .replace(/^_+|_+$/g, "")
20
+ .slice(0, 120) || fallback;
21
+ }
22
+
23
+ export function runLedgerId(prefix = "run") {
24
+ return `${safeSegment(prefix, "run")}-${Date.now().toString(36)}-${crypto.randomBytes(4).toString("hex")}`;
25
+ }
26
+
27
+ export function runLedgerDir() {
28
+ return path.join(getAgentflowDataRoot(), "admin", "run-ledger");
29
+ }
30
+
31
+ export function runLedgerPath(timeMs = Date.now()) {
32
+ return path.join(runLedgerDir(), `${localDayKey(timeMs)}.jsonl`);
33
+ }
34
+
35
+ export function appendRunLedgerEvent(event = {}) {
36
+ try {
37
+ const item = {
38
+ version: 1,
39
+ type: String(event.type || ""),
40
+ kind: String(event.kind || ""),
41
+ runId: String(event.runId || runLedgerId()),
42
+ userId: String(event.userId || ""),
43
+ username: String(event.username || event.userId || ""),
44
+ flowId: String(event.flowId || ""),
45
+ flowSource: String(event.flowSource || "user"),
46
+ runNodeId: String(event.runNodeId || ""),
47
+ at: Number(event.at || event.startedAt || Date.now()),
48
+ endedAt: event.endedAt == null ? null : Number(event.endedAt),
49
+ durationMs: Math.max(0, Number(event.durationMs || 0)),
50
+ status: event.status ? String(event.status || "") : "",
51
+ };
52
+ if (!item.type || !item.kind || !item.runId || !item.flowId) return;
53
+ const filePath = runLedgerPath(item.at);
54
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
55
+ fs.appendFileSync(filePath, JSON.stringify(item) + "\n", "utf-8");
56
+ } catch {
57
+ // Usage telemetry must never affect the run itself.
58
+ }
59
+ }
60
+
61
+ function readJsonlObjects(filePath) {
62
+ if (!fs.existsSync(filePath)) return [];
63
+ try {
64
+ const out = [];
65
+ const lines = fs.readFileSync(filePath, "utf-8").split(/\r?\n/);
66
+ for (const line of lines) {
67
+ if (!line.trim()) continue;
68
+ try {
69
+ const parsed = JSON.parse(line);
70
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) out.push(parsed);
71
+ } catch {
72
+ /* ignore malformed line */
73
+ }
74
+ }
75
+ return out;
76
+ } catch {
77
+ return [];
78
+ }
79
+ }
80
+
81
+ export function readRunLedgerEvents(options = {}) {
82
+ const dir = runLedgerDir();
83
+ if (!fs.existsSync(dir)) return [];
84
+ try {
85
+ const sinceMs = Number(options?.sinceMs || 0);
86
+ const sinceKey = Number.isFinite(sinceMs) && sinceMs > 0 ? localDayKey(sinceMs) : "";
87
+ const files = fs.readdirSync(dir, { withFileTypes: true })
88
+ .filter((entry) => entry.isFile() && /^\d{4}-\d{2}-\d{2}\.jsonl$/.test(entry.name))
89
+ .filter((entry) => !sinceKey || entry.name.slice(0, 10) >= sinceKey)
90
+ .map((entry) => path.join(dir, entry.name))
91
+ .sort();
92
+ return files.flatMap((filePath) => readJsonlObjects(filePath));
93
+ } catch {
94
+ return [];
95
+ }
96
+ }