@fieldwangai/agentflow 0.1.69 → 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/apply.mjs +35 -2
- package/bin/lib/run-ledger.mjs +96 -0
- package/bin/lib/ui-server.mjs +277 -19
- package/builtin/web-ui/dist/assets/{index-DvdPaPBL.js → index-BdgCU6QK.js} +92 -92
- 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-CCvxfovU.css +0 -1
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
|
}
|
|
@@ -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
|
+
}
|
package/bin/lib/ui-server.mjs
CHANGED
|
@@ -110,6 +110,11 @@ import {
|
|
|
110
110
|
updateAdminBuiltinPipelineConfig,
|
|
111
111
|
} from "./admin-builtin-pipelines.mjs";
|
|
112
112
|
import { readAdminStorageConfig, writeAdminStorageConfig } from "./admin-storage-config.mjs";
|
|
113
|
+
import {
|
|
114
|
+
appendRunLedgerEvent,
|
|
115
|
+
readRunLedgerEvents,
|
|
116
|
+
runLedgerId,
|
|
117
|
+
} from "./run-ledger.mjs";
|
|
113
118
|
|
|
114
119
|
const MIME = {
|
|
115
120
|
".html": "text/html; charset=utf-8",
|
|
@@ -1498,6 +1503,7 @@ function pipelineCountsForUser(userId) {
|
|
|
1498
1503
|
}
|
|
1499
1504
|
|
|
1500
1505
|
const USAGE_DAY_MS = 24 * 60 * 60 * 1000;
|
|
1506
|
+
const RUN_LEDGER_STALE_MS = 6 * 60 * 60 * 1000;
|
|
1501
1507
|
|
|
1502
1508
|
function startOfLocalDayMs(timeMs) {
|
|
1503
1509
|
const d = new Date(Number(timeMs) || Date.now());
|
|
@@ -1554,7 +1560,12 @@ function buildUsageDailyTrend(runs, days = 14, nowMs = Date.now()) {
|
|
|
1554
1560
|
row[bucket] += 1;
|
|
1555
1561
|
row.totalDurationMs += Math.max(0, Number(run.durationMs || 0));
|
|
1556
1562
|
row._userIds.add(String(run.userId || ""));
|
|
1557
|
-
|
|
1563
|
+
{
|
|
1564
|
+
const flowSource = String(run.flowSource || "user");
|
|
1565
|
+
const flowId = String(run.flowId || "");
|
|
1566
|
+
const userId = String(run.userId || "");
|
|
1567
|
+
row._pipelineKeys.add(flowSource === "workspace" ? `workspace:${flowId}` : `${userId}:${flowSource}:${flowId}`);
|
|
1568
|
+
}
|
|
1558
1569
|
}
|
|
1559
1570
|
return rows.map((row) => {
|
|
1560
1571
|
row.users = row._userIds.size;
|
|
@@ -1566,7 +1577,7 @@ function buildUsageDailyTrend(runs, days = 14, nowMs = Date.now()) {
|
|
|
1566
1577
|
});
|
|
1567
1578
|
}
|
|
1568
1579
|
|
|
1569
|
-
function buildUsageRates(users, runs, nowMs = Date.now()) {
|
|
1580
|
+
function buildUsageRates(users, runs, nowMs = Date.now(), workspacePipelineCount = 0) {
|
|
1570
1581
|
const windowDays = 7;
|
|
1571
1582
|
const sinceMs = startOfLocalDayMs(nowMs) - (windowDays - 1) * USAGE_DAY_MS;
|
|
1572
1583
|
const recentRuns = runs.filter((run) => Number(run?.at || 0) >= sinceMs);
|
|
@@ -1577,13 +1588,16 @@ function buildUsageRates(users, runs, nowMs = Date.now()) {
|
|
|
1577
1588
|
for (const run of recentRuns) {
|
|
1578
1589
|
const userId = String(run.userId || "");
|
|
1579
1590
|
if (userId) activeUsers.add(userId);
|
|
1580
|
-
|
|
1591
|
+
const flowSource = String(run.flowSource || "user");
|
|
1592
|
+
const flowId = String(run.flowId || "");
|
|
1593
|
+
activePipelines.add(flowSource === "workspace" ? `workspace:${flowId}` : `${userId}:${flowSource}:${flowId}`);
|
|
1581
1594
|
const bucket = runStatusBucket(run.status);
|
|
1582
1595
|
statusCounts[bucket] = (statusCounts[bucket] || 0) + 1;
|
|
1583
1596
|
recentDurationMs += Math.max(0, Number(run.durationMs || 0));
|
|
1584
1597
|
}
|
|
1585
1598
|
const totalUsers = users.length;
|
|
1586
|
-
const totalActivePipelines = users.reduce((sum, user) => sum + Math.max(0, Number(user?.pipelines?.active || 0)), 0)
|
|
1599
|
+
const totalActivePipelines = users.reduce((sum, user) => sum + Math.max(0, Number(user?.pipelines?.active || 0)), 0)
|
|
1600
|
+
+ Math.max(0, Number(workspacePipelineCount || 0));
|
|
1587
1601
|
const completedRuns = recentRuns.length - (statusCounts.running || 0);
|
|
1588
1602
|
const badRuns = (statusCounts.failed || 0) + (statusCounts.stopped || 0) + (statusCounts.interrupted || 0) + (statusCounts.unknown || 0);
|
|
1589
1603
|
return {
|
|
@@ -1603,21 +1617,196 @@ function buildUsageRates(users, runs, nowMs = Date.now()) {
|
|
|
1603
1617
|
};
|
|
1604
1618
|
}
|
|
1605
1619
|
|
|
1620
|
+
function appendWorkspaceRunStarted(record) {
|
|
1621
|
+
appendRunLedgerEvent({
|
|
1622
|
+
...record,
|
|
1623
|
+
type: "run_started",
|
|
1624
|
+
kind: "workspace",
|
|
1625
|
+
at: Number(record.startedAt || record.at || Date.now()),
|
|
1626
|
+
});
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
function appendWorkspaceRunFinished(record, status) {
|
|
1630
|
+
appendRunLedgerEvent({
|
|
1631
|
+
...record,
|
|
1632
|
+
type: "run_finished",
|
|
1633
|
+
kind: "workspace",
|
|
1634
|
+
at: Number(record.startedAt || record.at || Date.now()),
|
|
1635
|
+
endedAt: Number(record.endedAt || Date.now()),
|
|
1636
|
+
durationMs: Math.max(0, Number(record.durationMs || (Number(record.endedAt || Date.now()) - Number(record.startedAt || record.at || Date.now())))),
|
|
1637
|
+
status,
|
|
1638
|
+
});
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
function normalizeWorkspaceUsageRecord(parsed, source = "workspace-run") {
|
|
1642
|
+
const userId = String(parsed?.userId || "").trim();
|
|
1643
|
+
const flowId = String(parsed?.flowId || "").trim();
|
|
1644
|
+
const at = Number(parsed?.at || parsed?.startedAt || 0);
|
|
1645
|
+
if (!userId || !flowId || !Number.isFinite(at) || at <= 0) return null;
|
|
1646
|
+
return {
|
|
1647
|
+
userId,
|
|
1648
|
+
username: String(parsed?.username || userId),
|
|
1649
|
+
flowId,
|
|
1650
|
+
flowSource: String(parsed?.flowSource || "user"),
|
|
1651
|
+
runId: String(parsed?.runId || ""),
|
|
1652
|
+
at,
|
|
1653
|
+
endedAt: parsed?.endedAt == null ? null : Number(parsed.endedAt),
|
|
1654
|
+
durationMs: Math.max(0, Number(parsed?.durationMs || 0)),
|
|
1655
|
+
status: runStatusBucket(parsed?.status),
|
|
1656
|
+
source,
|
|
1657
|
+
};
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
function readLegacyWorkspaceRunUsageRecords() {
|
|
1661
|
+
const filePath = path.join(getAgentflowDataRoot(), "admin", "workspace-run-usage.jsonl");
|
|
1662
|
+
if (!fs.existsSync(filePath)) return [];
|
|
1663
|
+
let items = [];
|
|
1664
|
+
try {
|
|
1665
|
+
items = fs.readFileSync(filePath, "utf-8")
|
|
1666
|
+
.split(/\r?\n/)
|
|
1667
|
+
.filter((line) => line.trim())
|
|
1668
|
+
.map((line) => {
|
|
1669
|
+
try { return JSON.parse(line); } catch { return null; }
|
|
1670
|
+
})
|
|
1671
|
+
.filter(Boolean);
|
|
1672
|
+
} catch {
|
|
1673
|
+
items = [];
|
|
1674
|
+
}
|
|
1675
|
+
return items
|
|
1676
|
+
.map((item) => normalizeWorkspaceUsageRecord(item, "workspace-run-legacy"))
|
|
1677
|
+
.filter(Boolean);
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
function readWorkspaceRunLedgerRecords(options = {}) {
|
|
1681
|
+
const byRunId = new Map();
|
|
1682
|
+
for (const event of readRunLedgerEvents(options)) {
|
|
1683
|
+
if (String(event?.kind || "") !== "workspace") continue;
|
|
1684
|
+
const runId = String(event?.runId || "").trim();
|
|
1685
|
+
if (!runId) continue;
|
|
1686
|
+
const existing = byRunId.get(runId) || {};
|
|
1687
|
+
if (event.type === "run_started") {
|
|
1688
|
+
byRunId.set(runId, {
|
|
1689
|
+
...existing,
|
|
1690
|
+
...event,
|
|
1691
|
+
runId,
|
|
1692
|
+
at: Number(event.at || existing.at || Date.now()),
|
|
1693
|
+
status: existing.status || "running",
|
|
1694
|
+
});
|
|
1695
|
+
} else if (event.type === "run_finished") {
|
|
1696
|
+
byRunId.set(runId, {
|
|
1697
|
+
...existing,
|
|
1698
|
+
...event,
|
|
1699
|
+
runId,
|
|
1700
|
+
at: Number(existing.at || event.at || Date.now()),
|
|
1701
|
+
endedAt: event.endedAt == null ? null : Number(event.endedAt),
|
|
1702
|
+
durationMs: Math.max(0, Number(event.durationMs || 0)),
|
|
1703
|
+
status: runStatusBucket(event.status),
|
|
1704
|
+
});
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
const now = Date.now();
|
|
1708
|
+
return Array.from(byRunId.values())
|
|
1709
|
+
.map((item) => {
|
|
1710
|
+
const at = Number(item?.at || 0);
|
|
1711
|
+
const status = runStatusBucket(item?.status);
|
|
1712
|
+
if (status === "running" && at > 0 && now - at > RUN_LEDGER_STALE_MS) {
|
|
1713
|
+
return {
|
|
1714
|
+
...item,
|
|
1715
|
+
endedAt: Number(item?.endedAt || at + RUN_LEDGER_STALE_MS),
|
|
1716
|
+
durationMs: Math.max(0, Number(item?.durationMs || Math.min(now - at, RUN_LEDGER_STALE_MS))),
|
|
1717
|
+
status: "interrupted",
|
|
1718
|
+
};
|
|
1719
|
+
}
|
|
1720
|
+
return item;
|
|
1721
|
+
})
|
|
1722
|
+
.map((item) => normalizeWorkspaceUsageRecord(item, "workspace-run-ledger"))
|
|
1723
|
+
.filter(Boolean);
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
function readWorkspaceRunUsageRecords(options = {}) {
|
|
1727
|
+
const sinceMs = Number(options?.sinceMs || 0);
|
|
1728
|
+
return [
|
|
1729
|
+
...readLegacyWorkspaceRunUsageRecords(),
|
|
1730
|
+
...readWorkspaceRunLedgerRecords(options),
|
|
1731
|
+
].filter((run) => !Number.isFinite(sinceMs) || sinceMs <= 0 || Number(run?.at || 0) >= sinceMs);
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
function activeWorkspaceRunUsageRecords() {
|
|
1735
|
+
const out = [];
|
|
1736
|
+
for (const entry of activeWorkspaceRuns.values()) {
|
|
1737
|
+
const userId = String(entry?.userId || "").trim();
|
|
1738
|
+
const flowId = String(entry?.flowId || "").trim();
|
|
1739
|
+
const at = Number(entry?.startedAt || 0);
|
|
1740
|
+
if (!userId || !flowId || !Number.isFinite(at) || at <= 0) continue;
|
|
1741
|
+
out.push({
|
|
1742
|
+
userId,
|
|
1743
|
+
username: String(entry?.username || userId),
|
|
1744
|
+
flowId,
|
|
1745
|
+
flowSource: String(entry?.flowSource || "user"),
|
|
1746
|
+
runId: String(entry?.runId || ""),
|
|
1747
|
+
at,
|
|
1748
|
+
endedAt: null,
|
|
1749
|
+
durationMs: Math.max(0, Date.now() - at),
|
|
1750
|
+
status: "running",
|
|
1751
|
+
source: "workspace-run-active",
|
|
1752
|
+
});
|
|
1753
|
+
}
|
|
1754
|
+
return out;
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
function dedupeWorkspaceUsageRuns(runs = []) {
|
|
1758
|
+
const byKey = new Map();
|
|
1759
|
+
for (const run of runs) {
|
|
1760
|
+
const runId = String(run?.runId || "").trim();
|
|
1761
|
+
const key = runId || `${run?.userId || ""}:${run?.flowSource || ""}:${run?.flowId || ""}:${run?.at || ""}:${run?.status || ""}`;
|
|
1762
|
+
if (!key) continue;
|
|
1763
|
+
const existing = byKey.get(key);
|
|
1764
|
+
if (!existing) {
|
|
1765
|
+
byKey.set(key, run);
|
|
1766
|
+
continue;
|
|
1767
|
+
}
|
|
1768
|
+
const runScore = (item) => {
|
|
1769
|
+
if (item?.source === "workspace-run-active") return 3;
|
|
1770
|
+
if (item?.status && item.status !== "running") return 2;
|
|
1771
|
+
return 1;
|
|
1772
|
+
};
|
|
1773
|
+
if (runScore(run) >= runScore(existing)) byKey.set(key, run);
|
|
1774
|
+
}
|
|
1775
|
+
return Array.from(byKey.values());
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1606
1778
|
function buildAdminUsageDashboard(workspaceRoot) {
|
|
1607
1779
|
const authUsers = readAuthUsers();
|
|
1780
|
+
const usageSinceMs = startOfLocalDayMs(Date.now()) - 13 * USAGE_DAY_MS;
|
|
1781
|
+
const workspacePipelineCount = listFlowsJson(workspaceRoot, { includeWorkspaceFlows: true })
|
|
1782
|
+
.filter((flow) => flow?.source === "workspace" && !flow?.archived)
|
|
1783
|
+
.length;
|
|
1784
|
+
const workspaceUsageRuns = dedupeWorkspaceUsageRuns([
|
|
1785
|
+
...readWorkspaceRunUsageRecords({ sinceMs: usageSinceMs }),
|
|
1786
|
+
...activeWorkspaceRunUsageRecords(),
|
|
1787
|
+
]);
|
|
1608
1788
|
const userIds = Array.from(new Set([
|
|
1609
1789
|
...Object.keys(authUsers || {}),
|
|
1610
1790
|
...listAgentflowUserIds(),
|
|
1791
|
+
...workspaceUsageRuns.map((run) => run.userId),
|
|
1611
1792
|
].map((id) => String(id || "").trim()).filter(Boolean))).sort((a, b) => a.localeCompare(b));
|
|
1612
1793
|
const allRuns = [];
|
|
1794
|
+
const workspaceUsageByUser = new Map();
|
|
1795
|
+
for (const run of workspaceUsageRuns) {
|
|
1796
|
+
const userId = String(run.userId || "");
|
|
1797
|
+
if (!workspaceUsageByUser.has(userId)) workspaceUsageByUser.set(userId, []);
|
|
1798
|
+
workspaceUsageByUser.get(userId).push(run);
|
|
1799
|
+
}
|
|
1613
1800
|
const users = userIds.map((userId) => {
|
|
1614
1801
|
const user = authUsers[userId] || {};
|
|
1615
1802
|
const pipelineCounts = pipelineCountsForUser(userId);
|
|
1616
|
-
const
|
|
1803
|
+
const pipelineRuns = listRecentRunsFromDisk(workspaceRoot, {
|
|
1617
1804
|
userId,
|
|
1618
1805
|
includeWorkspaceRuns: false,
|
|
1619
1806
|
includeLegacyUserRuns: false,
|
|
1620
1807
|
});
|
|
1808
|
+
const runs = [...pipelineRuns, ...(workspaceUsageByUser.get(userId) || [])]
|
|
1809
|
+
.sort((a, b) => Number(b.at || 0) - Number(a.at || 0));
|
|
1621
1810
|
const statusCounts = {};
|
|
1622
1811
|
let totalDurationMs = 0;
|
|
1623
1812
|
for (const run of runs) {
|
|
@@ -1688,11 +1877,28 @@ function buildAdminUsageDashboard(workspaceRoot) {
|
|
|
1688
1877
|
totalDurationMs: 0,
|
|
1689
1878
|
});
|
|
1690
1879
|
totals.avgDurationMs = totals.runs > 0 ? Math.round(totals.totalDurationMs / totals.runs) : 0;
|
|
1880
|
+
const recentRuns = allRuns
|
|
1881
|
+
.slice()
|
|
1882
|
+
.sort((a, b) => Number(b.at || 0) - Number(a.at || 0))
|
|
1883
|
+
.slice(0, 50)
|
|
1884
|
+
.map((run) => ({
|
|
1885
|
+
userId: String(run.userId || ""),
|
|
1886
|
+
username: String(run.username || run.userId || ""),
|
|
1887
|
+
flowId: String(run.flowId || ""),
|
|
1888
|
+
flowSource: String(run.flowSource || "user"),
|
|
1889
|
+
runId: String(run.runId || ""),
|
|
1890
|
+
at: Number(run.at || 0),
|
|
1891
|
+
endedAt: run.endedAt == null ? null : Number(run.endedAt),
|
|
1892
|
+
durationMs: Math.max(0, Number(run.durationMs || 0)),
|
|
1893
|
+
status: runStatusBucket(run.status),
|
|
1894
|
+
runType: String(run.source || "").startsWith("workspace-run") ? "workspace" : "pipeline",
|
|
1895
|
+
}));
|
|
1691
1896
|
return {
|
|
1692
1897
|
generatedAt: new Date().toISOString(),
|
|
1693
1898
|
totals,
|
|
1694
|
-
usage: buildUsageRates(users, allRuns),
|
|
1899
|
+
usage: buildUsageRates(users, allRuns, Date.now(), workspacePipelineCount),
|
|
1695
1900
|
dailyTrend: buildUsageDailyTrend(allRuns, 14),
|
|
1901
|
+
recentRuns,
|
|
1696
1902
|
users,
|
|
1697
1903
|
};
|
|
1698
1904
|
}
|
|
@@ -3049,28 +3255,46 @@ function workspaceImplementationInlineText(instance) {
|
|
|
3049
3255
|
return "";
|
|
3050
3256
|
}
|
|
3051
3257
|
|
|
3052
|
-
function
|
|
3053
|
-
const parts = [];
|
|
3258
|
+
function workspaceMaterializeImplementationReference(instance, scopedRoot, runPackage = {}) {
|
|
3054
3259
|
const implementationRef = String(instance?.implementationRef || "").trim();
|
|
3055
|
-
if (implementationRef)
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3260
|
+
if (!implementationRef) return null;
|
|
3261
|
+
const abs = workspaceResolveFlowFile(scopedRoot, implementationRef, "implementationRef");
|
|
3262
|
+
const exists = fs.existsSync(abs) && fs.statSync(abs).isFile();
|
|
3263
|
+
const nodeRunDir = String(runPackage?.nodeRunDir || "").trim();
|
|
3264
|
+
let mounted = "";
|
|
3265
|
+
if (exists && nodeRunDir) {
|
|
3266
|
+
try {
|
|
3267
|
+
const mountedRel = path.join("references", "implementation.md");
|
|
3268
|
+
const dest = path.resolve(nodeRunDir, mountedRel);
|
|
3269
|
+
const nodeRunWithSep = nodeRunDir.endsWith(path.sep) ? nodeRunDir : `${nodeRunDir}${path.sep}`;
|
|
3270
|
+
if (dest === nodeRunDir || dest.startsWith(nodeRunWithSep)) {
|
|
3271
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
3272
|
+
fs.copyFileSync(abs, dest);
|
|
3273
|
+
mounted = mountedRel.split(path.sep).join(path.posix.sep);
|
|
3274
|
+
}
|
|
3275
|
+
} catch {
|
|
3276
|
+
mounted = "";
|
|
3060
3277
|
}
|
|
3061
3278
|
}
|
|
3279
|
+
return { implementationRef, exists, mounted };
|
|
3280
|
+
}
|
|
3281
|
+
|
|
3282
|
+
function workspaceImplementationBlock(instance, scopedRoot, runPackage = {}) {
|
|
3283
|
+
const ref = workspaceMaterializeImplementationReference(instance, scopedRoot, runPackage);
|
|
3062
3284
|
const inline = workspaceImplementationInlineText(instance);
|
|
3063
|
-
if (inline)
|
|
3064
|
-
if (!parts.length) return "";
|
|
3285
|
+
if (!ref && !inline) return "";
|
|
3065
3286
|
const mode = String(instance?.implementationMode || "").trim();
|
|
3066
3287
|
return [
|
|
3067
3288
|
"## 参考实现方案",
|
|
3068
3289
|
"",
|
|
3069
3290
|
mode ? `mode: ${mode}` : "",
|
|
3291
|
+
ref ? `- 实现方案文件:\`${ref.mounted || ref.implementationRef}\`` : "",
|
|
3292
|
+
ref?.mounted ? `- 原始流水线路径:\`${ref.implementationRef}\`` : "",
|
|
3293
|
+
ref && !ref.exists ? "- 当前实现方案文件不存在,本次不要依赖旧方案。" : "",
|
|
3294
|
+
inline ? "- 节点存在内联实现方案字段,但本提示不会内联其内容;如需复用,请优先参考实现方案文件。" : "",
|
|
3070
3295
|
"",
|
|
3071
|
-
|
|
3072
|
-
"",
|
|
3073
|
-
"以上方案用于加速本次执行。若发现过期,可以按当前任务修正执行,但最终结果必须以当前输入为准。",
|
|
3296
|
+
"该文件只作为可选参考,用于了解上次执行的实现路径。不要把旧方案当成硬约束;如果与当前任务、输入或输出要求冲突,以当前任务为准。",
|
|
3297
|
+
"只有在需要复用细节或确认历史约定时才读取该文件;不要在最终回复中复述参考方案内容。",
|
|
3074
3298
|
].filter((line) => line !== "").join("\n");
|
|
3075
3299
|
}
|
|
3076
3300
|
|
|
@@ -4320,7 +4544,7 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
4320
4544
|
} catch {
|
|
4321
4545
|
// Best-effort debug artifact only.
|
|
4322
4546
|
}
|
|
4323
|
-
const implementationBlock = workspaceImplementationBlock(instance, scopedRoot);
|
|
4547
|
+
const implementationBlock = workspaceImplementationBlock(instance, scopedRoot, runPackage);
|
|
4324
4548
|
const prompt = workspaceNodePrompt(graph, nodeId, promptUpstreamText, promptSkillsBlock, promptMcpBlock, runtimeInputValues, runPackage, implementationBlock);
|
|
4325
4549
|
try {
|
|
4326
4550
|
fs.writeFileSync(path.join(runPackage.nodeRunDir, "prompt.md"), prompt.trimEnd() + "\n", "utf-8");
|
|
@@ -5413,6 +5637,9 @@ export function startUiServer({
|
|
|
5413
5637
|
const runEntry = {
|
|
5414
5638
|
controller,
|
|
5415
5639
|
child: null,
|
|
5640
|
+
runId: runLedgerId("workspace"),
|
|
5641
|
+
userId: String(userCtx.userId || ""),
|
|
5642
|
+
username: String(authUser?.username || userCtx.userId || ""),
|
|
5416
5643
|
runNodeId: String(payload.runNodeId || "").trim(),
|
|
5417
5644
|
flowId,
|
|
5418
5645
|
flowSource: scoped.flowSource || payload.flowSource || "user",
|
|
@@ -5424,6 +5651,7 @@ export function startUiServer({
|
|
|
5424
5651
|
},
|
|
5425
5652
|
};
|
|
5426
5653
|
activeWorkspaceRuns.set(runKey, runEntry);
|
|
5654
|
+
appendWorkspaceRunStarted(runEntry);
|
|
5427
5655
|
const setActiveChild = (child) => {
|
|
5428
5656
|
runEntry.child = child || null;
|
|
5429
5657
|
if (controller.signal.aborted) runEntry.stopChild();
|
|
@@ -5451,12 +5679,27 @@ export function startUiServer({
|
|
|
5451
5679
|
const touchedIds = workspaceRunTouchedNodeIds(result);
|
|
5452
5680
|
const mergedGraph = mergeWorkspaceRunGraph(currentGraph, result.graph, touchedIds);
|
|
5453
5681
|
fs.writeFileSync(graphPath, JSON.stringify(mergedGraph, null, 2) + "\n", "utf-8");
|
|
5682
|
+
appendWorkspaceRunFinished({
|
|
5683
|
+
...runEntry,
|
|
5684
|
+
endedAt: Date.now(),
|
|
5685
|
+
durationMs: Date.now() - runEntry.startedAt,
|
|
5686
|
+
}, "success");
|
|
5454
5687
|
writeEvent({ type: "done", ok: true, path: graphPath, graph: mergedGraph, order: result.order, touchedNodeIds: Array.from(touchedIds), pauseNodeIds: result.pauseNodeIds || [] });
|
|
5455
5688
|
res.end();
|
|
5456
5689
|
} catch (e) {
|
|
5457
5690
|
if (isWorkspaceRunAbortError(e) || controller.signal.aborted) {
|
|
5691
|
+
appendWorkspaceRunFinished({
|
|
5692
|
+
...runEntry,
|
|
5693
|
+
endedAt: Date.now(),
|
|
5694
|
+
durationMs: Date.now() - runEntry.startedAt,
|
|
5695
|
+
}, "stopped");
|
|
5458
5696
|
writeEvent({ type: "stopped", ok: false, stopped: true, message: "Workspace run stopped" });
|
|
5459
5697
|
} else {
|
|
5698
|
+
appendWorkspaceRunFinished({
|
|
5699
|
+
...runEntry,
|
|
5700
|
+
endedAt: Date.now(),
|
|
5701
|
+
durationMs: Date.now() - runEntry.startedAt,
|
|
5702
|
+
}, "failed");
|
|
5460
5703
|
writeEvent({ type: "error", error: (e && e.message) || String(e) });
|
|
5461
5704
|
}
|
|
5462
5705
|
res.end();
|
|
@@ -5475,11 +5718,26 @@ export function startUiServer({
|
|
|
5475
5718
|
const touchedIds = workspaceRunTouchedNodeIds(result);
|
|
5476
5719
|
const mergedGraph = mergeWorkspaceRunGraph(currentGraph, result.graph, touchedIds);
|
|
5477
5720
|
fs.writeFileSync(graphPath, JSON.stringify(mergedGraph, null, 2) + "\n", "utf-8");
|
|
5721
|
+
appendWorkspaceRunFinished({
|
|
5722
|
+
...runEntry,
|
|
5723
|
+
endedAt: Date.now(),
|
|
5724
|
+
durationMs: Date.now() - runEntry.startedAt,
|
|
5725
|
+
}, "success");
|
|
5478
5726
|
json(res, 200, { ok: true, path: graphPath, ...result, graph: mergedGraph, touchedNodeIds: Array.from(touchedIds) });
|
|
5479
5727
|
} catch (e) {
|
|
5480
5728
|
if (isWorkspaceRunAbortError(e) || controller.signal.aborted) {
|
|
5729
|
+
appendWorkspaceRunFinished({
|
|
5730
|
+
...runEntry,
|
|
5731
|
+
endedAt: Date.now(),
|
|
5732
|
+
durationMs: Date.now() - runEntry.startedAt,
|
|
5733
|
+
}, "stopped");
|
|
5481
5734
|
json(res, 200, { ok: false, stopped: true, message: "Workspace run stopped" });
|
|
5482
5735
|
} else {
|
|
5736
|
+
appendWorkspaceRunFinished({
|
|
5737
|
+
...runEntry,
|
|
5738
|
+
endedAt: Date.now(),
|
|
5739
|
+
durationMs: Date.now() - runEntry.startedAt,
|
|
5740
|
+
}, "failed");
|
|
5483
5741
|
throw e;
|
|
5484
5742
|
}
|
|
5485
5743
|
} finally {
|