@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.
- package/bin/lib/admin-storage-config.mjs +93 -0
- package/bin/lib/apply.mjs +35 -2
- package/bin/lib/marketplace.mjs +55 -3
- package/bin/lib/paths.mjs +43 -5
- package/bin/lib/run-ledger.mjs +96 -0
- package/bin/lib/ui-server.mjs +1152 -25
- package/builtin/nodes/tool_git_checkout.md +1 -1
- package/builtin/nodes/tool_git_worktree_load.md +2 -1
- package/builtin/web-ui/dist/assets/index-BdgCU6QK.js +296 -0
- package/builtin/web-ui/dist/assets/index-ByGcCRCN.css +1 -0
- package/builtin/web-ui/dist/index.html +2 -2
- package/package.json +1 -1
- package/builtin/web-ui/dist/assets/index-B7EYZmUN.css +0 -1
- package/builtin/web-ui/dist/assets/index-D3NG6W8U.js +0 -264
|
@@ -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
|
}
|
package/bin/lib/marketplace.mjs
CHANGED
|
@@ -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 (
|
|
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
|
|
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
|
-
|
|
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
|
+
}
|