@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
package/bin/lib/ui-server.mjs
CHANGED
|
@@ -43,9 +43,12 @@ import {
|
|
|
43
43
|
import { t } from "./i18n.mjs";
|
|
44
44
|
import {
|
|
45
45
|
PACKAGE_ROOT,
|
|
46
|
+
ARCHIVED_PIPELINES_DIR_NAME,
|
|
46
47
|
getAgentflowDataRoot,
|
|
47
48
|
getAgentflowUserConfigAbs,
|
|
48
49
|
getAgentflowUserDataRoot,
|
|
50
|
+
getUserPipelinesRoot,
|
|
51
|
+
listAgentflowUserIds,
|
|
49
52
|
getModelListsAbs,
|
|
50
53
|
getRunDir,
|
|
51
54
|
} from "./paths.mjs";
|
|
@@ -87,7 +90,7 @@ import {
|
|
|
87
90
|
publishFlowSnippet,
|
|
88
91
|
publishNodeFromInstance,
|
|
89
92
|
} from "./marketplace.mjs";
|
|
90
|
-
import { buildGitContext, inferGitRepoRootFromWorktree, loadGitWorktree, normalizeGitContext, runGit, unloadGitWorktree } from "./git-worktree.mjs";
|
|
93
|
+
import { buildGitContext, inferGitRepoRootFromWorktree, loadGitWorktree, normalizeGitContext, runGit, sanitizeWorktreeName, unloadGitWorktree } from "./git-worktree.mjs";
|
|
91
94
|
import { createGitLabMergeRequest } from "./gitlab-mr.mjs";
|
|
92
95
|
import {
|
|
93
96
|
authSetupRequired,
|
|
@@ -106,6 +109,12 @@ import {
|
|
|
106
109
|
readAdminBuiltinPipelineConfig,
|
|
107
110
|
updateAdminBuiltinPipelineConfig,
|
|
108
111
|
} from "./admin-builtin-pipelines.mjs";
|
|
112
|
+
import { readAdminStorageConfig, writeAdminStorageConfig } from "./admin-storage-config.mjs";
|
|
113
|
+
import {
|
|
114
|
+
appendRunLedgerEvent,
|
|
115
|
+
readRunLedgerEvents,
|
|
116
|
+
runLedgerId,
|
|
117
|
+
} from "./run-ledger.mjs";
|
|
109
118
|
|
|
110
119
|
const MIME = {
|
|
111
120
|
".html": "text/html; charset=utf-8",
|
|
@@ -1367,31 +1376,40 @@ function shouldSkipWorkspaceFileRelPath(relPath) {
|
|
|
1367
1376
|
));
|
|
1368
1377
|
}
|
|
1369
1378
|
|
|
1379
|
+
const WORKSPACE_FILES_MAX_ITEMS = 500;
|
|
1380
|
+
|
|
1370
1381
|
function readWorkspaceFilesRecursive(dir, root, depth = 0, maxDepth = 3, budget = { count: 0 }) {
|
|
1371
|
-
if (depth > maxDepth
|
|
1382
|
+
if (depth > maxDepth) return [];
|
|
1383
|
+
if (depth > 0 && budget.count > WORKSPACE_FILES_MAX_ITEMS) return [];
|
|
1372
1384
|
let entries;
|
|
1373
1385
|
try {
|
|
1374
1386
|
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
1375
1387
|
} catch {
|
|
1376
1388
|
return [];
|
|
1377
1389
|
}
|
|
1390
|
+
entries.sort((a, b) => {
|
|
1391
|
+
if (a.isDirectory() !== b.isDirectory()) return a.isDirectory() ? -1 : 1;
|
|
1392
|
+
return a.name.localeCompare(b.name);
|
|
1393
|
+
});
|
|
1378
1394
|
const out = [];
|
|
1379
1395
|
for (const entry of entries) {
|
|
1380
|
-
if (budget.count >
|
|
1396
|
+
if (depth > 0 && budget.count > WORKSPACE_FILES_MAX_ITEMS) break;
|
|
1381
1397
|
if (entry.name.startsWith(".") && entry.name !== ".agents" && entry.name !== ".codex") continue;
|
|
1382
1398
|
const abs = path.join(dir, entry.name);
|
|
1383
1399
|
const rel = path.relative(root, abs).replace(/\\/g, "/");
|
|
1384
1400
|
if (shouldSkipWorkspaceFileRelPath(rel)) continue;
|
|
1385
1401
|
if (entry.isDirectory()) {
|
|
1386
1402
|
if (WORKSPACE_FILE_SKIP_DIRS.has(entry.name)) continue;
|
|
1387
|
-
budget.count++;
|
|
1388
1403
|
out.push({
|
|
1389
1404
|
type: "directory",
|
|
1390
1405
|
name: entry.name,
|
|
1391
1406
|
path: rel,
|
|
1392
1407
|
icon: workspaceFileIcon(entry.name, true),
|
|
1393
|
-
children:
|
|
1408
|
+
children: budget.count > WORKSPACE_FILES_MAX_ITEMS
|
|
1409
|
+
? []
|
|
1410
|
+
: readWorkspaceFilesRecursive(abs, root, depth + 1, maxDepth, budget),
|
|
1394
1411
|
});
|
|
1412
|
+
budget.count++;
|
|
1395
1413
|
} else if (entry.isFile()) {
|
|
1396
1414
|
if (WORKSPACE_FILE_SKIP_FILES.has(entry.name)) continue;
|
|
1397
1415
|
const ext = path.extname(entry.name).toLowerCase();
|
|
@@ -1402,10 +1420,6 @@ function readWorkspaceFilesRecursive(dir, root, depth = 0, maxDepth = 3, budget
|
|
|
1402
1420
|
out.push({ type: "file", name: entry.name, path: rel, icon: workspaceFileIcon(entry.name), size });
|
|
1403
1421
|
}
|
|
1404
1422
|
}
|
|
1405
|
-
out.sort((a, b) => {
|
|
1406
|
-
if (a.type !== b.type) return a.type === "directory" ? -1 : 1;
|
|
1407
|
-
return a.name.localeCompare(b.name);
|
|
1408
|
-
});
|
|
1409
1423
|
return out;
|
|
1410
1424
|
}
|
|
1411
1425
|
|
|
@@ -1464,6 +1478,431 @@ function writeDisplayShares(shares) {
|
|
|
1464
1478
|
fs.writeFileSync(file, JSON.stringify(shares && typeof shares === "object" ? shares : {}, null, 2) + "\n", "utf-8");
|
|
1465
1479
|
}
|
|
1466
1480
|
|
|
1481
|
+
function countFlowYamlDirs(root) {
|
|
1482
|
+
try {
|
|
1483
|
+
if (!fs.existsSync(root) || !fs.statSync(root).isDirectory()) return 0;
|
|
1484
|
+
return fs.readdirSync(root, { withFileTypes: true })
|
|
1485
|
+
.filter((entry) => entry.isDirectory())
|
|
1486
|
+
.filter((entry) => entry.name !== ARCHIVED_PIPELINES_DIR_NAME)
|
|
1487
|
+
.filter((entry) => fs.existsSync(path.join(root, entry.name, FLOW_YAML_FILENAME)))
|
|
1488
|
+
.length;
|
|
1489
|
+
} catch {
|
|
1490
|
+
return 0;
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
function pipelineCountsForUser(userId) {
|
|
1495
|
+
const pipelinesRoot = getUserPipelinesRoot(userId);
|
|
1496
|
+
const active = countFlowYamlDirs(pipelinesRoot);
|
|
1497
|
+
const archived = countFlowYamlDirs(path.join(pipelinesRoot, ARCHIVED_PIPELINES_DIR_NAME));
|
|
1498
|
+
return {
|
|
1499
|
+
active,
|
|
1500
|
+
archived,
|
|
1501
|
+
total: active + archived,
|
|
1502
|
+
};
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
const USAGE_DAY_MS = 24 * 60 * 60 * 1000;
|
|
1506
|
+
const RUN_LEDGER_STALE_MS = 6 * 60 * 60 * 1000;
|
|
1507
|
+
|
|
1508
|
+
function startOfLocalDayMs(timeMs) {
|
|
1509
|
+
const d = new Date(Number(timeMs) || Date.now());
|
|
1510
|
+
return new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
function localDayKey(timeMs) {
|
|
1514
|
+
const d = new Date(Number(timeMs) || Date.now());
|
|
1515
|
+
const y = d.getFullYear();
|
|
1516
|
+
const m = String(d.getMonth() + 1).padStart(2, "0");
|
|
1517
|
+
const day = String(d.getDate()).padStart(2, "0");
|
|
1518
|
+
return `${y}-${m}-${day}`;
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
function runStatusBucket(status) {
|
|
1522
|
+
const s = String(status || "unknown");
|
|
1523
|
+
if (s === "success" || s === "failed" || s === "running" || s === "stopped" || s === "interrupted") return s;
|
|
1524
|
+
return "unknown";
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
function buildUsageDailyTrend(runs, days = 14, nowMs = Date.now()) {
|
|
1528
|
+
const startMs = startOfLocalDayMs(nowMs) - (Math.max(1, days) - 1) * USAGE_DAY_MS;
|
|
1529
|
+
const rows = [];
|
|
1530
|
+
const byDate = new Map();
|
|
1531
|
+
for (let i = 0; i < days; i += 1) {
|
|
1532
|
+
const dateMs = startMs + i * USAGE_DAY_MS;
|
|
1533
|
+
const date = localDayKey(dateMs);
|
|
1534
|
+
const row = {
|
|
1535
|
+
date,
|
|
1536
|
+
runs: 0,
|
|
1537
|
+
success: 0,
|
|
1538
|
+
failed: 0,
|
|
1539
|
+
running: 0,
|
|
1540
|
+
stopped: 0,
|
|
1541
|
+
interrupted: 0,
|
|
1542
|
+
unknown: 0,
|
|
1543
|
+
users: 0,
|
|
1544
|
+
pipelines: 0,
|
|
1545
|
+
totalDurationMs: 0,
|
|
1546
|
+
avgDurationMs: 0,
|
|
1547
|
+
_userIds: new Set(),
|
|
1548
|
+
_pipelineKeys: new Set(),
|
|
1549
|
+
};
|
|
1550
|
+
rows.push(row);
|
|
1551
|
+
byDate.set(date, row);
|
|
1552
|
+
}
|
|
1553
|
+
for (const run of runs) {
|
|
1554
|
+
const at = Number(run?.at || 0);
|
|
1555
|
+
if (!Number.isFinite(at) || at < startMs) continue;
|
|
1556
|
+
const row = byDate.get(localDayKey(at));
|
|
1557
|
+
if (!row) continue;
|
|
1558
|
+
const bucket = runStatusBucket(run.status);
|
|
1559
|
+
row.runs += 1;
|
|
1560
|
+
row[bucket] += 1;
|
|
1561
|
+
row.totalDurationMs += Math.max(0, Number(run.durationMs || 0));
|
|
1562
|
+
row._userIds.add(String(run.userId || ""));
|
|
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
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
return rows.map((row) => {
|
|
1571
|
+
row.users = row._userIds.size;
|
|
1572
|
+
row.pipelines = row._pipelineKeys.size;
|
|
1573
|
+
row.avgDurationMs = row.runs > 0 ? Math.round(row.totalDurationMs / row.runs) : 0;
|
|
1574
|
+
delete row._userIds;
|
|
1575
|
+
delete row._pipelineKeys;
|
|
1576
|
+
return row;
|
|
1577
|
+
});
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
function buildUsageRates(users, runs, nowMs = Date.now(), workspacePipelineCount = 0) {
|
|
1581
|
+
const windowDays = 7;
|
|
1582
|
+
const sinceMs = startOfLocalDayMs(nowMs) - (windowDays - 1) * USAGE_DAY_MS;
|
|
1583
|
+
const recentRuns = runs.filter((run) => Number(run?.at || 0) >= sinceMs);
|
|
1584
|
+
const activeUsers = new Set();
|
|
1585
|
+
const activePipelines = new Set();
|
|
1586
|
+
const statusCounts = {};
|
|
1587
|
+
let recentDurationMs = 0;
|
|
1588
|
+
for (const run of recentRuns) {
|
|
1589
|
+
const userId = String(run.userId || "");
|
|
1590
|
+
if (userId) activeUsers.add(userId);
|
|
1591
|
+
const flowSource = String(run.flowSource || "user");
|
|
1592
|
+
const flowId = String(run.flowId || "");
|
|
1593
|
+
activePipelines.add(flowSource === "workspace" ? `workspace:${flowId}` : `${userId}:${flowSource}:${flowId}`);
|
|
1594
|
+
const bucket = runStatusBucket(run.status);
|
|
1595
|
+
statusCounts[bucket] = (statusCounts[bucket] || 0) + 1;
|
|
1596
|
+
recentDurationMs += Math.max(0, Number(run.durationMs || 0));
|
|
1597
|
+
}
|
|
1598
|
+
const totalUsers = users.length;
|
|
1599
|
+
const totalActivePipelines = users.reduce((sum, user) => sum + Math.max(0, Number(user?.pipelines?.active || 0)), 0)
|
|
1600
|
+
+ Math.max(0, Number(workspacePipelineCount || 0));
|
|
1601
|
+
const completedRuns = recentRuns.length - (statusCounts.running || 0);
|
|
1602
|
+
const badRuns = (statusCounts.failed || 0) + (statusCounts.stopped || 0) + (statusCounts.interrupted || 0) + (statusCounts.unknown || 0);
|
|
1603
|
+
return {
|
|
1604
|
+
windowDays,
|
|
1605
|
+
activeUsers: activeUsers.size,
|
|
1606
|
+
activeUserRate: totalUsers > 0 ? activeUsers.size / totalUsers : 0,
|
|
1607
|
+
activePipelines: activePipelines.size,
|
|
1608
|
+
activePipelineRate: totalActivePipelines > 0 ? activePipelines.size / totalActivePipelines : 0,
|
|
1609
|
+
runs: recentRuns.length,
|
|
1610
|
+
avgRunsPerDay: recentRuns.length / windowDays,
|
|
1611
|
+
successRuns: statusCounts.success || 0,
|
|
1612
|
+
badRuns,
|
|
1613
|
+
runningRuns: statusCounts.running || 0,
|
|
1614
|
+
successRate: completedRuns > 0 ? (statusCounts.success || 0) / completedRuns : 0,
|
|
1615
|
+
failureRate: completedRuns > 0 ? badRuns / completedRuns : 0,
|
|
1616
|
+
avgDurationMs: recentRuns.length > 0 ? Math.round(recentDurationMs / recentRuns.length) : 0,
|
|
1617
|
+
};
|
|
1618
|
+
}
|
|
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
|
+
|
|
1778
|
+
function buildAdminUsageDashboard(workspaceRoot) {
|
|
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
|
+
]);
|
|
1788
|
+
const userIds = Array.from(new Set([
|
|
1789
|
+
...Object.keys(authUsers || {}),
|
|
1790
|
+
...listAgentflowUserIds(),
|
|
1791
|
+
...workspaceUsageRuns.map((run) => run.userId),
|
|
1792
|
+
].map((id) => String(id || "").trim()).filter(Boolean))).sort((a, b) => a.localeCompare(b));
|
|
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
|
+
}
|
|
1800
|
+
const users = userIds.map((userId) => {
|
|
1801
|
+
const user = authUsers[userId] || {};
|
|
1802
|
+
const pipelineCounts = pipelineCountsForUser(userId);
|
|
1803
|
+
const pipelineRuns = listRecentRunsFromDisk(workspaceRoot, {
|
|
1804
|
+
userId,
|
|
1805
|
+
includeWorkspaceRuns: false,
|
|
1806
|
+
includeLegacyUserRuns: false,
|
|
1807
|
+
});
|
|
1808
|
+
const runs = [...pipelineRuns, ...(workspaceUsageByUser.get(userId) || [])]
|
|
1809
|
+
.sort((a, b) => Number(b.at || 0) - Number(a.at || 0));
|
|
1810
|
+
const statusCounts = {};
|
|
1811
|
+
let totalDurationMs = 0;
|
|
1812
|
+
for (const run of runs) {
|
|
1813
|
+
allRuns.push({ ...run, userId, username: user.username || userId });
|
|
1814
|
+
const status = String(run.status || "unknown");
|
|
1815
|
+
statusCounts[status] = (statusCounts[status] || 0) + 1;
|
|
1816
|
+
totalDurationMs += Math.max(0, Number(run.durationMs || 0));
|
|
1817
|
+
}
|
|
1818
|
+
const lastRun = runs[0] || null;
|
|
1819
|
+
return {
|
|
1820
|
+
userId,
|
|
1821
|
+
username: user.username || userId,
|
|
1822
|
+
isAdmin: Boolean(user.isAdmin),
|
|
1823
|
+
pipelines: pipelineCounts,
|
|
1824
|
+
runs: {
|
|
1825
|
+
total: runs.length,
|
|
1826
|
+
running: statusCounts.running || 0,
|
|
1827
|
+
success: statusCounts.success || 0,
|
|
1828
|
+
failed: statusCounts.failed || 0,
|
|
1829
|
+
stopped: statusCounts.stopped || 0,
|
|
1830
|
+
interrupted: statusCounts.interrupted || 0,
|
|
1831
|
+
unknown: statusCounts.unknown || 0,
|
|
1832
|
+
totalDurationMs,
|
|
1833
|
+
avgDurationMs: runs.length > 0 ? Math.round(totalDurationMs / runs.length) : 0,
|
|
1834
|
+
lastRunAt: lastRun?.at || null,
|
|
1835
|
+
lastRunFlowId: lastRun?.flowId || "",
|
|
1836
|
+
lastRunStatus: lastRun?.status || "",
|
|
1837
|
+
recent: runs.slice(0, 5).map((run) => ({
|
|
1838
|
+
flowId: run.flowId,
|
|
1839
|
+
flowSource: run.flowSource,
|
|
1840
|
+
runId: run.runId,
|
|
1841
|
+
at: run.at,
|
|
1842
|
+
endedAt: run.endedAt,
|
|
1843
|
+
durationMs: run.durationMs,
|
|
1844
|
+
status: run.status,
|
|
1845
|
+
})),
|
|
1846
|
+
},
|
|
1847
|
+
};
|
|
1848
|
+
});
|
|
1849
|
+
const totals = users.reduce((acc, user) => {
|
|
1850
|
+
acc.users += 1;
|
|
1851
|
+
acc.admins += user.isAdmin ? 1 : 0;
|
|
1852
|
+
acc.pipelines += user.pipelines.total;
|
|
1853
|
+
acc.activePipelines += user.pipelines.active;
|
|
1854
|
+
acc.archivedPipelines += user.pipelines.archived;
|
|
1855
|
+
acc.runs += user.runs.total;
|
|
1856
|
+
acc.runningRuns += user.runs.running;
|
|
1857
|
+
acc.successRuns += user.runs.success;
|
|
1858
|
+
acc.failedRuns += user.runs.failed;
|
|
1859
|
+
acc.stoppedRuns += user.runs.stopped;
|
|
1860
|
+
acc.interruptedRuns += user.runs.interrupted;
|
|
1861
|
+
acc.unknownRuns += user.runs.unknown;
|
|
1862
|
+
acc.totalDurationMs += user.runs.totalDurationMs;
|
|
1863
|
+
return acc;
|
|
1864
|
+
}, {
|
|
1865
|
+
users: 0,
|
|
1866
|
+
admins: 0,
|
|
1867
|
+
pipelines: 0,
|
|
1868
|
+
activePipelines: 0,
|
|
1869
|
+
archivedPipelines: 0,
|
|
1870
|
+
runs: 0,
|
|
1871
|
+
runningRuns: 0,
|
|
1872
|
+
successRuns: 0,
|
|
1873
|
+
failedRuns: 0,
|
|
1874
|
+
stoppedRuns: 0,
|
|
1875
|
+
interruptedRuns: 0,
|
|
1876
|
+
unknownRuns: 0,
|
|
1877
|
+
totalDurationMs: 0,
|
|
1878
|
+
});
|
|
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
|
+
}));
|
|
1896
|
+
return {
|
|
1897
|
+
generatedAt: new Date().toISOString(),
|
|
1898
|
+
totals,
|
|
1899
|
+
usage: buildUsageRates(users, allRuns, Date.now(), workspacePipelineCount),
|
|
1900
|
+
dailyTrend: buildUsageDailyTrend(allRuns, 14),
|
|
1901
|
+
recentRuns,
|
|
1902
|
+
users,
|
|
1903
|
+
};
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1467
1906
|
function createDisplayShareId() {
|
|
1468
1907
|
return crypto.randomBytes(12).toString("base64url");
|
|
1469
1908
|
}
|
|
@@ -1629,6 +2068,44 @@ function mergeWorkspaceRunGraph(currentGraph, runGraph, touchedIds) {
|
|
|
1629
2068
|
};
|
|
1630
2069
|
}
|
|
1631
2070
|
|
|
2071
|
+
function mergeWorkspacePersistentNodeRefs(incomingGraph, currentGraph) {
|
|
2072
|
+
const incoming = normalizeWorkspaceGraphPayload(incomingGraph || {});
|
|
2073
|
+
const current = normalizeWorkspaceGraphPayload(currentGraph || {});
|
|
2074
|
+
const instances = { ...(incoming.instances || {}) };
|
|
2075
|
+
for (const [id, currentInstance] of Object.entries(current.instances || {})) {
|
|
2076
|
+
const nextInstance = instances[id];
|
|
2077
|
+
if (!nextInstance || typeof nextInstance !== "object") continue;
|
|
2078
|
+
for (const key of ["scriptRef", "implementationRef", "implementationMode"]) {
|
|
2079
|
+
const currentValue = currentInstance?.[key];
|
|
2080
|
+
const nextValue = nextInstance?.[key];
|
|
2081
|
+
if (currentValue != null && String(currentValue).trim() && (nextValue == null || !String(nextValue).trim())) {
|
|
2082
|
+
nextInstance[key] = currentValue;
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
return { ...incoming, instances };
|
|
2087
|
+
}
|
|
2088
|
+
|
|
2089
|
+
function hydrateWorkspaceNodeRefsFromFiles(scopedRoot, graph) {
|
|
2090
|
+
const next = normalizeWorkspaceGraphPayload(graph || {});
|
|
2091
|
+
const instances = { ...(next.instances || {}) };
|
|
2092
|
+
let changed = false;
|
|
2093
|
+
for (const [nodeId, instance] of Object.entries(instances)) {
|
|
2094
|
+
if (!instance || typeof instance !== "object") continue;
|
|
2095
|
+
const defId = String(instance.definitionId || "");
|
|
2096
|
+
if (defId === "workspace_run" || defId === "workspace_scheduled_run") continue;
|
|
2097
|
+
if (!String(instance.implementationRef || "").trim()) {
|
|
2098
|
+
const implementationRef = workspaceDefaultImplementationRef(nodeId);
|
|
2099
|
+
const abs = workspaceResolveFlowFile(scopedRoot, implementationRef, "implementationRef");
|
|
2100
|
+
if (fs.existsSync(abs) && fs.statSync(abs).isFile()) {
|
|
2101
|
+
instances[nodeId] = { ...instance, implementationRef };
|
|
2102
|
+
changed = true;
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
return changed ? { ...next, instances } : next;
|
|
2107
|
+
}
|
|
2108
|
+
|
|
1632
2109
|
function resolveWorkspaceScopeRoot(workspaceRoot, params = {}, opts = {}) {
|
|
1633
2110
|
const flowId = params.flowId != null ? String(params.flowId).trim() : "";
|
|
1634
2111
|
if (!flowId) return { root: path.resolve(workspaceRoot), flowId: "", flowSource: "", archived: false };
|
|
@@ -2214,6 +2691,45 @@ function workspaceResolvePath(baseCwd, raw) {
|
|
|
2214
2691
|
return path.isAbsolute(text) ? path.resolve(text) : path.resolve(baseCwd, text);
|
|
2215
2692
|
}
|
|
2216
2693
|
|
|
2694
|
+
function workspaceShellQuote(value) {
|
|
2695
|
+
return "'" + String(value ?? "").replace(/'/g, "'\\''") + "'";
|
|
2696
|
+
}
|
|
2697
|
+
|
|
2698
|
+
function workspaceSafeFlowRelPath(raw, fieldName = "path") {
|
|
2699
|
+
const text = String(raw || "").trim().replace(/^["']|["']$/g, "");
|
|
2700
|
+
if (!text) return "";
|
|
2701
|
+
if (text.length > 260) throw new Error(`${fieldName} is too long`);
|
|
2702
|
+
if (/[\r\n<>]/.test(text)) throw new Error(`${fieldName} contains invalid characters`);
|
|
2703
|
+
if (/^(?:https?:|data:|blob:|file:|javascript:|mailto:|tel:)/i.test(text)) {
|
|
2704
|
+
throw new Error(`${fieldName} must be a relative file path`);
|
|
2705
|
+
}
|
|
2706
|
+
if (path.isAbsolute(text)) throw new Error(`${fieldName} must be relative`);
|
|
2707
|
+
const normalized = path.posix.normalize(text.replace(/\\/g, "/")).replace(/^\/+/, "");
|
|
2708
|
+
if (!normalized || normalized === "." || normalized.startsWith("../") || normalized.includes("/../")) {
|
|
2709
|
+
throw new Error(`${fieldName} escapes workspace root`);
|
|
2710
|
+
}
|
|
2711
|
+
return normalized;
|
|
2712
|
+
}
|
|
2713
|
+
|
|
2714
|
+
function workspaceResolveFlowFile(scopedRoot, relPath, fieldName = "path") {
|
|
2715
|
+
const clean = workspaceSafeFlowRelPath(relPath, fieldName);
|
|
2716
|
+
if (!clean) return "";
|
|
2717
|
+
const root = path.resolve(scopedRoot);
|
|
2718
|
+
const abs = path.resolve(root, ...clean.split("/"));
|
|
2719
|
+
const rootWithSep = root.endsWith(path.sep) ? root : `${root}${path.sep}`;
|
|
2720
|
+
if (abs !== root && !abs.startsWith(rootWithSep)) {
|
|
2721
|
+
throw new Error(`${fieldName} escapes workspace root`);
|
|
2722
|
+
}
|
|
2723
|
+
return abs;
|
|
2724
|
+
}
|
|
2725
|
+
|
|
2726
|
+
function workspaceReadTextFileIfExists(absPath, maxChars = 60000) {
|
|
2727
|
+
const file = String(absPath || "").trim();
|
|
2728
|
+
if (!file || !fs.existsSync(file) || !fs.statSync(file).isFile()) return "";
|
|
2729
|
+
const raw = fs.readFileSync(file, "utf-8");
|
|
2730
|
+
return raw.length > maxChars ? `${raw.slice(0, maxChars)}\n...[truncated ${raw.length - maxChars} chars]` : raw;
|
|
2731
|
+
}
|
|
2732
|
+
|
|
2217
2733
|
function workspaceSanitizeRepoDirName(repoUrl) {
|
|
2218
2734
|
const raw = String(repoUrl || "").trim().replace(/\.git$/i, "");
|
|
2219
2735
|
const last = raw.split(/[/:]/).filter(Boolean).pop() || "repo";
|
|
@@ -2500,7 +3016,7 @@ function workspaceRunPlan(graph, runNodeId, scopedRoot = "") {
|
|
|
2500
3016
|
const addNeeded = (id) => {
|
|
2501
3017
|
if (!id || needed.has(id)) return;
|
|
2502
3018
|
const defId = String(instances[id]?.definitionId || "");
|
|
2503
|
-
if (id !== target && defId === "workspace_run") {
|
|
3019
|
+
if (id !== target && (defId === "workspace_run" || defId === "workspace_scheduled_run")) {
|
|
2504
3020
|
pauseNodeIds.add(id);
|
|
2505
3021
|
return;
|
|
2506
3022
|
}
|
|
@@ -2726,6 +3242,314 @@ function workspacePromptUpstreamText(upstreamText, runPackage = {}) {
|
|
|
2726
3242
|
return upstreamText;
|
|
2727
3243
|
}
|
|
2728
3244
|
|
|
3245
|
+
function workspaceImplementationInlineText(instance) {
|
|
3246
|
+
const candidates = [instance?.implementation, instance?.implementationPlan];
|
|
3247
|
+
for (const item of candidates) {
|
|
3248
|
+
if (item == null) continue;
|
|
3249
|
+
if (typeof item === "string" && item.trim()) return item.trim();
|
|
3250
|
+
if (typeof item === "object" && !Array.isArray(item)) {
|
|
3251
|
+
const content = item.content ?? item.body ?? item.notes ?? "";
|
|
3252
|
+
if (String(content || "").trim()) return String(content).trim();
|
|
3253
|
+
}
|
|
3254
|
+
}
|
|
3255
|
+
return "";
|
|
3256
|
+
}
|
|
3257
|
+
|
|
3258
|
+
function workspaceMaterializeImplementationReference(instance, scopedRoot, runPackage = {}) {
|
|
3259
|
+
const implementationRef = String(instance?.implementationRef || "").trim();
|
|
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 = "";
|
|
3277
|
+
}
|
|
3278
|
+
}
|
|
3279
|
+
return { implementationRef, exists, mounted };
|
|
3280
|
+
}
|
|
3281
|
+
|
|
3282
|
+
function workspaceImplementationBlock(instance, scopedRoot, runPackage = {}) {
|
|
3283
|
+
const ref = workspaceMaterializeImplementationReference(instance, scopedRoot, runPackage);
|
|
3284
|
+
const inline = workspaceImplementationInlineText(instance);
|
|
3285
|
+
if (!ref && !inline) return "";
|
|
3286
|
+
const mode = String(instance?.implementationMode || "").trim();
|
|
3287
|
+
return [
|
|
3288
|
+
"## 参考实现方案",
|
|
3289
|
+
"",
|
|
3290
|
+
mode ? `mode: ${mode}` : "",
|
|
3291
|
+
ref ? `- 实现方案文件:\`${ref.mounted || ref.implementationRef}\`` : "",
|
|
3292
|
+
ref?.mounted ? `- 原始流水线路径:\`${ref.implementationRef}\`` : "",
|
|
3293
|
+
ref && !ref.exists ? "- 当前实现方案文件不存在,本次不要依赖旧方案。" : "",
|
|
3294
|
+
inline ? "- 节点存在内联实现方案字段,但本提示不会内联其内容;如需复用,请优先参考实现方案文件。" : "",
|
|
3295
|
+
"",
|
|
3296
|
+
"该文件只作为可选参考,用于了解上次执行的实现路径。不要把旧方案当成硬约束;如果与当前任务、输入或输出要求冲突,以当前任务为准。",
|
|
3297
|
+
"只有在需要复用细节或确认历史约定时才读取该文件;不要在最终回复中复述参考方案内容。",
|
|
3298
|
+
].filter((line) => line !== "").join("\n");
|
|
3299
|
+
}
|
|
3300
|
+
|
|
3301
|
+
function workspaceSafeNodeFileName(nodeId) {
|
|
3302
|
+
const text = String(nodeId || "").trim().replace(/[^A-Za-z0-9_-]+/g, "_").replace(/^_+|_+$/g, "");
|
|
3303
|
+
return text || "node";
|
|
3304
|
+
}
|
|
3305
|
+
|
|
3306
|
+
function workspaceDefaultImplementationRef(nodeId) {
|
|
3307
|
+
return `nodes/${workspaceSafeNodeFileName(nodeId)}/implementation.md`;
|
|
3308
|
+
}
|
|
3309
|
+
|
|
3310
|
+
function workspaceImplementationModeForInstance(instance) {
|
|
3311
|
+
const explicit = String(instance?.implementationMode || "").trim();
|
|
3312
|
+
if (explicit) return explicit;
|
|
3313
|
+
return String(instance?.definitionId || "") === "tool_nodejs" ? "script" : "steps";
|
|
3314
|
+
}
|
|
3315
|
+
|
|
3316
|
+
function workspaceClipImplementationText(value, maxChars = 2400) {
|
|
3317
|
+
const text = String(value ?? "").trim();
|
|
3318
|
+
if (!text) return "";
|
|
3319
|
+
return text.length > maxChars ? `${text.slice(0, maxChars)}\n...[truncated ${text.length - maxChars} chars]` : text;
|
|
3320
|
+
}
|
|
3321
|
+
|
|
3322
|
+
function workspaceUniqueImplementationList(items = [], maxItems = 12) {
|
|
3323
|
+
const out = [];
|
|
3324
|
+
const seen = new Set();
|
|
3325
|
+
for (const item of items) {
|
|
3326
|
+
const text = String(item || "").trim();
|
|
3327
|
+
if (!text) continue;
|
|
3328
|
+
const key = text.toLowerCase();
|
|
3329
|
+
if (seen.has(key)) continue;
|
|
3330
|
+
seen.add(key);
|
|
3331
|
+
out.push(text);
|
|
3332
|
+
if (out.length >= maxItems) break;
|
|
3333
|
+
}
|
|
3334
|
+
return out;
|
|
3335
|
+
}
|
|
3336
|
+
|
|
3337
|
+
function workspaceImplementationArtifactCandidates(structured = {}, result = "") {
|
|
3338
|
+
const candidates = [
|
|
3339
|
+
structured.resultFile,
|
|
3340
|
+
structured.result,
|
|
3341
|
+
result,
|
|
3342
|
+
...Object.values(structured.outParams || {}),
|
|
3343
|
+
];
|
|
3344
|
+
return workspaceUniqueImplementationList(candidates, 10)
|
|
3345
|
+
.filter((item) => /^(?:outputs|nodes|artifacts)\//.test(item) && !/[\r\n]/.test(item));
|
|
3346
|
+
}
|
|
3347
|
+
|
|
3348
|
+
function workspaceReadImplementationArtifact(scopedRoot, structured = {}, result = "") {
|
|
3349
|
+
const root = String(scopedRoot || "").trim();
|
|
3350
|
+
if (!root) return null;
|
|
3351
|
+
for (const rel of workspaceImplementationArtifactCandidates(structured, result)) {
|
|
3352
|
+
let abs = "";
|
|
3353
|
+
try {
|
|
3354
|
+
abs = workspaceResolveFlowFile(root, rel, "resultFile");
|
|
3355
|
+
} catch {
|
|
3356
|
+
continue;
|
|
3357
|
+
}
|
|
3358
|
+
if (!abs || !fs.existsSync(abs) || !fs.statSync(abs).isFile()) continue;
|
|
3359
|
+
const ext = path.extname(abs).toLowerCase();
|
|
3360
|
+
const content = workspaceReadTextFileIfExists(abs, 120000);
|
|
3361
|
+
if (!content.trim()) continue;
|
|
3362
|
+
return { rel, abs, ext, content };
|
|
3363
|
+
}
|
|
3364
|
+
return null;
|
|
3365
|
+
}
|
|
3366
|
+
|
|
3367
|
+
function workspaceImplementationArtifactContext(scopedRoot, structured = {}, result = "") {
|
|
3368
|
+
const artifact = workspaceReadImplementationArtifact(scopedRoot, structured, result);
|
|
3369
|
+
if (!artifact) return "无可读取产物文件。";
|
|
3370
|
+
return [
|
|
3371
|
+
`产物路径:${artifact.rel}`,
|
|
3372
|
+
`文件类型:${artifact.ext || "(unknown)"}`,
|
|
3373
|
+
"",
|
|
3374
|
+
"产物内容:",
|
|
3375
|
+
"```",
|
|
3376
|
+
workspaceClipImplementationText(artifact.content, 50000),
|
|
3377
|
+
"```",
|
|
3378
|
+
].join("\n");
|
|
3379
|
+
}
|
|
3380
|
+
|
|
3381
|
+
function workspaceBuildImplementationPrompt(instance, nodeId, opts = {}) {
|
|
3382
|
+
const defId = String(instance?.definitionId || "").trim();
|
|
3383
|
+
const label = String(instance?.label || nodeId || "node").trim();
|
|
3384
|
+
const mode = workspaceImplementationModeForInstance(instance);
|
|
3385
|
+
const inputValues = opts.inputValues || {};
|
|
3386
|
+
const structured = opts.structured && typeof opts.structured === "object" ? opts.structured : {};
|
|
3387
|
+
const result = String(opts.resultContent || structured.result || "").trim();
|
|
3388
|
+
const task = workspaceResolveBodyPlaceholders(instance?.body || "", inputValues).trim();
|
|
3389
|
+
const scriptRef = String(instance?.scriptRef || "").trim();
|
|
3390
|
+
const inlineScript = String(instance?.script || "").trim();
|
|
3391
|
+
const previousImplementation = opts.previousImplementation
|
|
3392
|
+
? workspaceClipImplementationText(opts.previousImplementation, 12000)
|
|
3393
|
+
: "";
|
|
3394
|
+
return [
|
|
3395
|
+
"你要为 AgentFlow 的一个节点写“实现方案”Markdown。这个文件会在下次运行同一个节点前作为上下文给模型参考。",
|
|
3396
|
+
"",
|
|
3397
|
+
"要求:",
|
|
3398
|
+
"- 必须基于实际任务、输入和产物内容自己总结,不要写流水账,不要写“使用 Agent 生成输出”这类空话。",
|
|
3399
|
+
"- 写清楚这次结果到底是如何实现的:核心思路、产物结构、关键文件/路径、关键样式/函数/数据结构、可复用约定。",
|
|
3400
|
+
"- 写清楚下次如果要继续迭代,应该从哪里改、哪些约定不能破坏。",
|
|
3401
|
+
"- 如果产物是 HTML/UI,必须总结页面模块、视觉风格、关键 class/token、交互点和下游展示契约。",
|
|
3402
|
+
"- 如果产物是脚本,必须总结脚本入口、环境变量、输入输出协议和错误处理方式。",
|
|
3403
|
+
"- 只输出 Markdown 正文,不要输出代码围栏包裹整篇,不要解释你在总结。",
|
|
3404
|
+
"",
|
|
3405
|
+
"## 节点信息",
|
|
3406
|
+
"",
|
|
3407
|
+
`nodeId: ${nodeId}`,
|
|
3408
|
+
`label: ${label}`,
|
|
3409
|
+
`definitionId: ${defId || "(unknown)"}`,
|
|
3410
|
+
`mode: ${mode}`,
|
|
3411
|
+
scriptRef ? `scriptRef: ${scriptRef}` : "",
|
|
3412
|
+
inlineScript ? `inlineScript: ${workspaceClipImplementationText(inlineScript, 1200)}` : "",
|
|
3413
|
+
"",
|
|
3414
|
+
"## 当前任务",
|
|
3415
|
+
"",
|
|
3416
|
+
workspaceClipImplementationText(task || instance?.body || scriptRef || inlineScript || "(无显式任务)", 6000),
|
|
3417
|
+
"",
|
|
3418
|
+
"## 输入",
|
|
3419
|
+
"",
|
|
3420
|
+
JSON.stringify(inputValues || {}, null, 2),
|
|
3421
|
+
"",
|
|
3422
|
+
"## 输出",
|
|
3423
|
+
"",
|
|
3424
|
+
JSON.stringify({
|
|
3425
|
+
result: structured.result || result,
|
|
3426
|
+
resultFile: structured.resultFile || "",
|
|
3427
|
+
outParams: structured.outParams || {},
|
|
3428
|
+
}, null, 2),
|
|
3429
|
+
"",
|
|
3430
|
+
previousImplementation ? "## 上一版实现方案" : "",
|
|
3431
|
+
previousImplementation || "",
|
|
3432
|
+
previousImplementation ? "" : "",
|
|
3433
|
+
"## 实际产物上下文",
|
|
3434
|
+
"",
|
|
3435
|
+
workspaceImplementationArtifactContext(opts.scopedRoot, structured, result),
|
|
3436
|
+
].filter((line) => line !== "").join("\n");
|
|
3437
|
+
}
|
|
3438
|
+
|
|
3439
|
+
async function workspaceGenerateImplementationMarkdown({
|
|
3440
|
+
scopedRoot,
|
|
3441
|
+
nodeId,
|
|
3442
|
+
instance,
|
|
3443
|
+
inputValues,
|
|
3444
|
+
resultContent,
|
|
3445
|
+
structured,
|
|
3446
|
+
implementationPath,
|
|
3447
|
+
previousImplementation,
|
|
3448
|
+
runPackage,
|
|
3449
|
+
modelKey,
|
|
3450
|
+
userCtx,
|
|
3451
|
+
emit,
|
|
3452
|
+
onActiveChild,
|
|
3453
|
+
}) {
|
|
3454
|
+
const prompt = workspaceBuildImplementationPrompt(instance, nodeId, {
|
|
3455
|
+
scopedRoot,
|
|
3456
|
+
inputValues,
|
|
3457
|
+
resultContent,
|
|
3458
|
+
structured,
|
|
3459
|
+
previousImplementation,
|
|
3460
|
+
});
|
|
3461
|
+
let content = "";
|
|
3462
|
+
let lastAssistant = "";
|
|
3463
|
+
let resultText = "";
|
|
3464
|
+
emit?.({ type: "status", line: "Summarize implementation plan with model" });
|
|
3465
|
+
const handle = startComposerAgent({
|
|
3466
|
+
uiWorkspaceRoot: scopedRoot,
|
|
3467
|
+
cliWorkspace: runPackage?.nodeRunDir || scopedRoot,
|
|
3468
|
+
prompt,
|
|
3469
|
+
modelKey,
|
|
3470
|
+
agentflowUserId: userCtx?.userId || "",
|
|
3471
|
+
extraEnv: runtimeEnvForUser(userCtx, {
|
|
3472
|
+
AGENTFLOW_IMPLEMENTATION_REF: implementationPath || "",
|
|
3473
|
+
AGENTFLOW_NODE_RUN_DIR: runPackage?.nodeRunDir || "",
|
|
3474
|
+
AGENTFLOW_NODE_TMP_DIR: runPackage?.nodeTmpDir || "",
|
|
3475
|
+
AGENTFLOW_OUTPUTS_DIR: runPackage?.outputsDir || "",
|
|
3476
|
+
}),
|
|
3477
|
+
onStreamEvent: (ev) => {
|
|
3478
|
+
if (ev?.type === "natural" && ev.kind === "assistant" && typeof ev.text === "string") {
|
|
3479
|
+
lastAssistant = ev.text;
|
|
3480
|
+
content += (content ? "\n" : "") + ev.text;
|
|
3481
|
+
} else if (ev?.type === "natural" && ev.kind === "result" && typeof ev.text === "string") {
|
|
3482
|
+
resultText = ev.text;
|
|
3483
|
+
}
|
|
3484
|
+
},
|
|
3485
|
+
onToolCall: (subtype, toolName) => {
|
|
3486
|
+
const sub = subtype ? String(subtype) : "";
|
|
3487
|
+
const tool = toolName ? String(toolName) : "";
|
|
3488
|
+
emit?.({ type: "status", line: `总结方案工具 ${tool || "thinking"}${sub ? ` (${sub})` : ""}` });
|
|
3489
|
+
},
|
|
3490
|
+
});
|
|
3491
|
+
if (typeof onActiveChild === "function") onActiveChild(handle.child || null);
|
|
3492
|
+
try {
|
|
3493
|
+
await handle.finished;
|
|
3494
|
+
} finally {
|
|
3495
|
+
if (typeof onActiveChild === "function") onActiveChild(null);
|
|
3496
|
+
}
|
|
3497
|
+
const markdown = String(resultText || lastAssistant || content || "").trim();
|
|
3498
|
+
return markdown.replace(/^```(?:markdown|md)?\s*/i, "").replace(/```\s*$/i, "").trim();
|
|
3499
|
+
}
|
|
3500
|
+
|
|
3501
|
+
async function workspacePersistNodeImplementation(scopedRoot, graph, nodeId, opts = {}) {
|
|
3502
|
+
const current = graph?.instances?.[nodeId];
|
|
3503
|
+
if (!current || String(current.definitionId || "") === "workspace_run" || String(current.definitionId || "") === "workspace_scheduled_run") {
|
|
3504
|
+
return { changed: false, wrote: false, instance: current };
|
|
3505
|
+
}
|
|
3506
|
+
const existingRef = String(current.implementationRef || "").trim();
|
|
3507
|
+
const implementationRef = existingRef || workspaceDefaultImplementationRef(nodeId);
|
|
3508
|
+
const abs = workspaceResolveFlowFile(scopedRoot, implementationRef, "implementationRef");
|
|
3509
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
3510
|
+
const previousImplementation = fs.existsSync(abs) && fs.statSync(abs).isFile()
|
|
3511
|
+
? workspaceReadTextFileIfExists(abs, 60000)
|
|
3512
|
+
: "";
|
|
3513
|
+
const markdown = await workspaceGenerateImplementationMarkdown({
|
|
3514
|
+
scopedRoot,
|
|
3515
|
+
nodeId,
|
|
3516
|
+
instance: current,
|
|
3517
|
+
inputValues: opts.inputValues || {},
|
|
3518
|
+
resultContent: opts.resultContent || "",
|
|
3519
|
+
structured: opts.structured || {},
|
|
3520
|
+
implementationPath: implementationRef,
|
|
3521
|
+
previousImplementation,
|
|
3522
|
+
runPackage: opts.runPackage,
|
|
3523
|
+
modelKey: opts.modelKey || "",
|
|
3524
|
+
userCtx: opts.userCtx || {},
|
|
3525
|
+
emit: opts.emit,
|
|
3526
|
+
onActiveChild: opts.onActiveChild,
|
|
3527
|
+
});
|
|
3528
|
+
if (!markdown.trim()) throw new Error(`Implementation summary is empty for node ${nodeId}`);
|
|
3529
|
+
fs.writeFileSync(abs, markdown.trimEnd() + "\n", "utf-8");
|
|
3530
|
+
const explicitMode = String(current.implementationMode || "").trim();
|
|
3531
|
+
const next = {
|
|
3532
|
+
...current,
|
|
3533
|
+
implementationRef,
|
|
3534
|
+
...(explicitMode ? { implementationMode: explicitMode } : {}),
|
|
3535
|
+
};
|
|
3536
|
+
const changed = String(current.implementationRef || "") !== implementationRef;
|
|
3537
|
+
return { changed, wrote: true, instance: next, implementationRef };
|
|
3538
|
+
}
|
|
3539
|
+
|
|
3540
|
+
async function workspaceTryPersistNodeImplementation(scopedRoot, graph, nodeId, opts = {}) {
|
|
3541
|
+
try {
|
|
3542
|
+
return await workspacePersistNodeImplementation(scopedRoot, graph, nodeId, opts);
|
|
3543
|
+
} catch (e) {
|
|
3544
|
+
opts.emit?.({
|
|
3545
|
+
type: "natural",
|
|
3546
|
+
kind: "warning",
|
|
3547
|
+
text: `实现方案未更新:${e?.message || String(e)}`,
|
|
3548
|
+
});
|
|
3549
|
+
return { changed: false, wrote: false, instance: graph?.instances?.[nodeId] };
|
|
3550
|
+
}
|
|
3551
|
+
}
|
|
3552
|
+
|
|
2729
3553
|
function parseWorkspaceSkillKeys(raw) {
|
|
2730
3554
|
const text = String(raw || "").trim();
|
|
2731
3555
|
if (!text) return [];
|
|
@@ -2859,6 +3683,7 @@ function workspaceWriteDisplayContent(instance, content) {
|
|
|
2859
3683
|
const unwrapped = workspaceUnwrapOutputEnvelopeForDisplay(content);
|
|
2860
3684
|
const text = kind === "html" ? normalizeHtmlDisplayContent(unwrapped) : String(unwrapped || "");
|
|
2861
3685
|
const primaryName = kind === "image" ? "src" : "content";
|
|
3686
|
+
next.displayReloadKey = `${Date.now()}-${crypto.randomBytes(4).toString("hex")}`;
|
|
2862
3687
|
next.body = text;
|
|
2863
3688
|
next.input = (Array.isArray(next.input) ? next.input : []).map((slot) => (
|
|
2864
3689
|
String(slot?.name || "") === primaryName || String(slot?.type || "") === "text"
|
|
@@ -2898,7 +3723,7 @@ function workspaceUpdateDirectDisplays(graph, sourceId, content, outputs = null)
|
|
|
2898
3723
|
return updated;
|
|
2899
3724
|
}
|
|
2900
3725
|
|
|
2901
|
-
function workspaceNodePrompt(graph, nodeId, upstreamText, skillsBlock, mcpBlock = "", inputValues = {}, nodeTmpDir = "") {
|
|
3726
|
+
function workspaceNodePrompt(graph, nodeId, upstreamText, skillsBlock, mcpBlock = "", inputValues = {}, nodeTmpDir = "", implementationBlock = "") {
|
|
2902
3727
|
const instance = graph.instances[nodeId] || {};
|
|
2903
3728
|
const body = workspaceResolveBodyPlaceholders(instance.body || "", inputValues).trim();
|
|
2904
3729
|
const { values: relevantInputValues, placeholders } = workspaceRelevantInputValues(instance.body || "", inputValues);
|
|
@@ -2911,6 +3736,7 @@ function workspaceNodePrompt(graph, nodeId, upstreamText, skillsBlock, mcpBlock
|
|
|
2911
3736
|
fileBoundary ? `\n${fileBoundary}` : "",
|
|
2912
3737
|
inputBlock ? `\n${inputBlock}` : "",
|
|
2913
3738
|
placeholders.size ? "\n任务只显式引用了上面的输入槽;其它未被 `${...}` 引用的已连接业务输入不要作为分析依据。" : "",
|
|
3739
|
+
implementationBlock ? `\n${implementationBlock}` : "",
|
|
2914
3740
|
skillsBlock ? `\n## 可用能力\n\n${skillsBlock}` : "",
|
|
2915
3741
|
mcpBlock ? `\n## 可用 MCP\n\n${mcpBlock}` : "",
|
|
2916
3742
|
upstreamText ? `\n## 上游正文\n\n${upstreamText}` : "",
|
|
@@ -2919,13 +3745,38 @@ function workspaceNodePrompt(graph, nodeId, upstreamText, skillsBlock, mcpBlock
|
|
|
2919
3745
|
].filter(Boolean).join("\n");
|
|
2920
3746
|
}
|
|
2921
3747
|
|
|
2922
|
-
function
|
|
2923
|
-
return path.join(path.resolve(scopedRoot), ".workspace", "agentflow", "
|
|
3748
|
+
function workspaceDefaultGitRepoRoot(scopedRoot, _userCtx = {}) {
|
|
3749
|
+
return path.join(path.resolve(scopedRoot), ".workspace", "agentflow", "git-repos");
|
|
3750
|
+
}
|
|
3751
|
+
|
|
3752
|
+
function workspaceDefaultWorktreePath(runTmpRoot, nodeId, repoPath, branch = "") {
|
|
3753
|
+
const repoRoot = path.resolve(repoPath);
|
|
3754
|
+
const repoName = sanitizeWorktreeName(path.basename(repoRoot));
|
|
3755
|
+
const branchName = String(branch || "").trim();
|
|
3756
|
+
let refLabel = branchName;
|
|
3757
|
+
if (!refLabel) {
|
|
3758
|
+
const currentBranch = runGit(["rev-parse", "--abbrev-ref", "HEAD"], repoRoot);
|
|
3759
|
+
if (currentBranch.status === 0 && currentBranch.stdout.trim() && currentBranch.stdout.trim() !== "HEAD") {
|
|
3760
|
+
refLabel = currentBranch.stdout.trim();
|
|
3761
|
+
}
|
|
3762
|
+
}
|
|
3763
|
+
if (!refLabel) {
|
|
3764
|
+
const currentCommit = runGit(["rev-parse", "HEAD"], repoRoot);
|
|
3765
|
+
refLabel = currentCommit.status === 0 && currentCommit.stdout.trim()
|
|
3766
|
+
? currentCommit.stdout.trim().slice(0, 12)
|
|
3767
|
+
: "HEAD";
|
|
3768
|
+
}
|
|
3769
|
+
return path.join(
|
|
3770
|
+
path.resolve(runTmpRoot),
|
|
3771
|
+
"worktrees",
|
|
3772
|
+
workspaceSanitizeTmpSegment(nodeId, "node"),
|
|
3773
|
+
repoName,
|
|
3774
|
+
sanitizeWorktreeName(refLabel),
|
|
3775
|
+
);
|
|
2924
3776
|
}
|
|
2925
3777
|
|
|
2926
|
-
function workspaceShouldAutoCleanupWorktree(
|
|
2927
|
-
|
|
2928
|
-
return workspacePathInside(workspaceDefaultWorktreeRoot(scopedRoot), worktreePath);
|
|
3778
|
+
function workspaceShouldAutoCleanupWorktree(worktreePath, hasExplicitWorktreePath) {
|
|
3779
|
+
return Boolean(worktreePath) && !hasExplicitWorktreePath;
|
|
2929
3780
|
}
|
|
2930
3781
|
|
|
2931
3782
|
function workspaceTrackAutoCleanupWorktree(list, item) {
|
|
@@ -2961,7 +3812,7 @@ function workspaceCleanupAutoWorktrees(list, graph, emit) {
|
|
|
2961
3812
|
const result = unloadGitWorktree({
|
|
2962
3813
|
repoPath: entry.repoPath,
|
|
2963
3814
|
worktreePath: entry.worktreePath,
|
|
2964
|
-
force:
|
|
3815
|
+
force: true,
|
|
2965
3816
|
prune: true,
|
|
2966
3817
|
});
|
|
2967
3818
|
emit({
|
|
@@ -3118,6 +3969,146 @@ function workspaceCleanupTmpRoot(runTmpRoot, userCtx = {}, emit = () => {}) {
|
|
|
3118
3969
|
}
|
|
3119
3970
|
}
|
|
3120
3971
|
|
|
3972
|
+
function workspaceOutputFileRefsForNode(instance) {
|
|
3973
|
+
const refs = {};
|
|
3974
|
+
const slots = Array.isArray(instance?.output) ? instance.output : [];
|
|
3975
|
+
for (let index = 0; index < slots.length; index += 1) {
|
|
3976
|
+
const slot = slots[index];
|
|
3977
|
+
const name = String(slot?.name || "").trim();
|
|
3978
|
+
const type = String(slot?.type || "");
|
|
3979
|
+
if (!name || type === "node" || name === "next" || name === "prev") continue;
|
|
3980
|
+
const key = name === "content" || index === 0 ? "result" : name;
|
|
3981
|
+
const safe = workspaceSanitizeTmpSegment(key, "result");
|
|
3982
|
+
refs[key] = `outputs/${safe}.txt`;
|
|
3983
|
+
}
|
|
3984
|
+
if (!refs.result) refs.result = "outputs/result.txt";
|
|
3985
|
+
return refs;
|
|
3986
|
+
}
|
|
3987
|
+
|
|
3988
|
+
function workspaceResolveScriptCommandText(script, values = {}) {
|
|
3989
|
+
return String(script || "").replace(/\$\{([^}]+)\}/g, (_, key) => {
|
|
3990
|
+
const name = String(key || "").trim();
|
|
3991
|
+
return workspaceShellQuote(Object.prototype.hasOwnProperty.call(values, name) ? values[name] : "");
|
|
3992
|
+
});
|
|
3993
|
+
}
|
|
3994
|
+
|
|
3995
|
+
function workspaceDefaultScriptCommand(scriptAbs) {
|
|
3996
|
+
const ext = path.extname(scriptAbs).toLowerCase();
|
|
3997
|
+
if (ext === ".js" || ext === ".mjs" || ext === ".cjs") return `node ${workspaceShellQuote(scriptAbs)}`;
|
|
3998
|
+
if (ext === ".sh" || ext === ".bash") return `bash ${workspaceShellQuote(scriptAbs)}`;
|
|
3999
|
+
if (ext === ".py") return `python3 ${workspaceShellQuote(scriptAbs)}`;
|
|
4000
|
+
return workspaceShellQuote(scriptAbs);
|
|
4001
|
+
}
|
|
4002
|
+
|
|
4003
|
+
function workspaceEnvelopeFromOutputFiles(outputRefs, nodeRunDir) {
|
|
4004
|
+
const entries = Object.entries(outputRefs || {})
|
|
4005
|
+
.map(([name, rel]) => {
|
|
4006
|
+
const abs = path.resolve(nodeRunDir, rel);
|
|
4007
|
+
const nodeRootWithSep = nodeRunDir.endsWith(path.sep) ? nodeRunDir : `${nodeRunDir}${path.sep}`;
|
|
4008
|
+
if (abs !== nodeRunDir && !abs.startsWith(nodeRootWithSep)) return null;
|
|
4009
|
+
if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) return null;
|
|
4010
|
+
return { name, rel };
|
|
4011
|
+
})
|
|
4012
|
+
.filter(Boolean);
|
|
4013
|
+
if (entries.length === 0) return "";
|
|
4014
|
+
const result = entries.find((entry) => entry.name === "result") || entries[0];
|
|
4015
|
+
const outParams = entries.filter((entry) => entry !== result);
|
|
4016
|
+
return [
|
|
4017
|
+
"---agentflow",
|
|
4018
|
+
`resultFile: ${result.rel}`,
|
|
4019
|
+
outParams.length ? "outParams:" : "",
|
|
4020
|
+
...outParams.map((entry) => ` ${entry.name}File: ${entry.rel}`),
|
|
4021
|
+
"---end",
|
|
4022
|
+
].filter(Boolean).join("\n");
|
|
4023
|
+
}
|
|
4024
|
+
|
|
4025
|
+
async function workspaceRunToolNodejsScript({
|
|
4026
|
+
scopedRoot,
|
|
4027
|
+
cwd,
|
|
4028
|
+
instance,
|
|
4029
|
+
inputValues,
|
|
4030
|
+
runPackage,
|
|
4031
|
+
userCtx,
|
|
4032
|
+
emit,
|
|
4033
|
+
}) {
|
|
4034
|
+
const scriptRef = String(instance?.scriptRef || "").trim();
|
|
4035
|
+
const scriptAbs = scriptRef ? workspaceResolveFlowFile(scopedRoot, scriptRef, "scriptRef") : "";
|
|
4036
|
+
if (scriptAbs && (!fs.existsSync(scriptAbs) || !fs.statSync(scriptAbs).isFile())) {
|
|
4037
|
+
throw new Error(`scriptRef not found: ${scriptRef}`);
|
|
4038
|
+
}
|
|
4039
|
+
const outputRefs = workspaceOutputFileRefsForNode(instance);
|
|
4040
|
+
const outputAbs = Object.fromEntries(
|
|
4041
|
+
Object.entries(outputRefs).map(([key, rel]) => [key, path.resolve(runPackage.nodeRunDir, rel)]),
|
|
4042
|
+
);
|
|
4043
|
+
for (const abs of Object.values(outputAbs)) fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
4044
|
+
const constants = {
|
|
4045
|
+
workspaceRoot: path.resolve(scopedRoot),
|
|
4046
|
+
pipelineWorkspace: path.resolve(scopedRoot),
|
|
4047
|
+
flowDir: path.resolve(scopedRoot),
|
|
4048
|
+
cwd: path.resolve(cwd || scopedRoot),
|
|
4049
|
+
nodeRunDir: runPackage.nodeRunDir,
|
|
4050
|
+
nodeTmpDir: runPackage.nodeTmpDir,
|
|
4051
|
+
outputsDir: runPackage.outputsDir,
|
|
4052
|
+
scriptRef: scriptAbs,
|
|
4053
|
+
...inputValues,
|
|
4054
|
+
...outputRefs,
|
|
4055
|
+
};
|
|
4056
|
+
const inlineScript = String(instance?.script || "").trim();
|
|
4057
|
+
const command = inlineScript
|
|
4058
|
+
? workspaceResolveScriptCommandText(inlineScript, constants)
|
|
4059
|
+
: scriptAbs
|
|
4060
|
+
? workspaceDefaultScriptCommand(scriptAbs)
|
|
4061
|
+
: "";
|
|
4062
|
+
if (!command) throw new Error("tool_nodejs requires script or scriptRef");
|
|
4063
|
+
|
|
4064
|
+
emit?.({ type: "status", line: `Run script: ${scriptRef || command.slice(0, 120)}` });
|
|
4065
|
+
|
|
4066
|
+
const env = runtimeEnvForUser(userCtx, {
|
|
4067
|
+
AGENTFLOW_WORKSPACE_ROOT: path.resolve(scopedRoot),
|
|
4068
|
+
AGENTFLOW_NODE_RUN_DIR: runPackage.nodeRunDir,
|
|
4069
|
+
AGENTFLOW_NODE_TMP_DIR: runPackage.nodeTmpDir,
|
|
4070
|
+
AGENTFLOW_OUTPUTS_DIR: runPackage.outputsDir,
|
|
4071
|
+
AGENTFLOW_SCRIPT_REF: scriptAbs,
|
|
4072
|
+
AGENTFLOW_INPUTS_JSON: JSON.stringify(inputValues || {}),
|
|
4073
|
+
AGENTFLOW_OUTPUTS_JSON: JSON.stringify(outputRefs),
|
|
4074
|
+
AGENTFLOW_OUTPUTS_ABS_JSON: JSON.stringify(outputAbs),
|
|
4075
|
+
});
|
|
4076
|
+
|
|
4077
|
+
const started = Date.now();
|
|
4078
|
+
return await new Promise((resolve, reject) => {
|
|
4079
|
+
const child = spawn(command, [], {
|
|
4080
|
+
cwd: runPackage.nodeRunDir,
|
|
4081
|
+
shell: true,
|
|
4082
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
4083
|
+
env,
|
|
4084
|
+
});
|
|
4085
|
+
let stdout = "";
|
|
4086
|
+
let stderr = "";
|
|
4087
|
+
child.stdout.setEncoding("utf-8");
|
|
4088
|
+
child.stderr.setEncoding("utf-8");
|
|
4089
|
+
child.stdout.on("data", (chunk) => {
|
|
4090
|
+
stdout += String(chunk);
|
|
4091
|
+
});
|
|
4092
|
+
child.stderr.on("data", (chunk) => {
|
|
4093
|
+
stderr += String(chunk);
|
|
4094
|
+
});
|
|
4095
|
+
child.on("error", reject);
|
|
4096
|
+
child.on("close", (code) => {
|
|
4097
|
+
if (stderr.trim()) {
|
|
4098
|
+
emit?.({ type: "natural", kind: "warning", text: `[script stderr]\n${stderr.trim().slice(-4000)}` });
|
|
4099
|
+
}
|
|
4100
|
+
if (code !== 0) {
|
|
4101
|
+
reject(new Error(`tool_nodejs script exited ${code}${stderr.trim() ? `: ${stderr.trim().slice(-800)}` : ""}`));
|
|
4102
|
+
return;
|
|
4103
|
+
}
|
|
4104
|
+
const elapsedMs = Math.max(0, Date.now() - started);
|
|
4105
|
+
emit?.({ type: "status", line: `Timing script: ${elapsedMs}ms`, timing: { label: "script", elapsedMs } });
|
|
4106
|
+
const content = stdout.trim() || workspaceEnvelopeFromOutputFiles(outputRefs, runPackage.nodeRunDir);
|
|
4107
|
+
resolve(content);
|
|
4108
|
+
});
|
|
4109
|
+
});
|
|
4110
|
+
}
|
|
4111
|
+
|
|
3121
4112
|
async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts = {}) {
|
|
3122
4113
|
const graph = normalizeWorkspaceGraphPayload(payload.graph || {});
|
|
3123
4114
|
const runNodeId = String(payload?.runNodeId || "").trim();
|
|
@@ -3190,7 +4181,7 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
3190
4181
|
const defId = String(instance.definitionId || "");
|
|
3191
4182
|
emit({ type: "node-start", nodeId, definitionId: defId });
|
|
3192
4183
|
|
|
3193
|
-
if (defId === "workspace_run") {
|
|
4184
|
+
if (defId === "workspace_run" || defId === "workspace_scheduled_run") {
|
|
3194
4185
|
continue;
|
|
3195
4186
|
}
|
|
3196
4187
|
|
|
@@ -3297,7 +4288,7 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
3297
4288
|
const targetRaw = workspaceSlotValue(workspaceSlotByName(instance, "targetDir")).trim();
|
|
3298
4289
|
const targetDir = targetRaw
|
|
3299
4290
|
? workspaceResolvePath(cwd, targetRaw)
|
|
3300
|
-
: path.join(scopedRoot,
|
|
4291
|
+
: path.join(workspaceDefaultGitRepoRoot(scopedRoot, userCtx), workspaceSanitizeRepoDirName(repoUrl));
|
|
3301
4292
|
const pullIfExists = workspaceBoolSlot(instance, "pullIfExists", true);
|
|
3302
4293
|
const includeSubmodules = workspaceBoolSlot(instance, "includeSubmodules", false);
|
|
3303
4294
|
fs.mkdirSync(path.dirname(targetDir), { recursive: true });
|
|
@@ -3369,14 +4360,16 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
3369
4360
|
const worktreeInputSlot = (Array.isArray(instance.input) ? instance.input : [])
|
|
3370
4361
|
.find((slot) => String(slot?.name || "") === "worktreePath") || null;
|
|
3371
4362
|
const rawWorktreePath = workspaceSlotValue(worktreeInputSlot || workspaceSlotByName(instance, "worktreePath")).trim();
|
|
3372
|
-
const worktreePath = rawWorktreePath
|
|
4363
|
+
const worktreePath = rawWorktreePath
|
|
4364
|
+
? workspaceResolvePath(cwd, rawWorktreePath)
|
|
4365
|
+
: (gitContext?.worktreePath ? path.resolve(gitContext.worktreePath) : workspaceDefaultWorktreePath(runTmpRoot, nodeId, repoPath, branch));
|
|
3373
4366
|
const hasExplicitWorktreePath = Boolean(rawWorktreePath) || Boolean(gitContext?.worktreePath);
|
|
3374
4367
|
const previousCwd = cwd;
|
|
3375
4368
|
const force = ["true", "1", "yes", "on"].includes(workspaceSlotValue(workspaceSlotByName(instance, "force")).trim().toLowerCase());
|
|
3376
4369
|
const pruneMissingRaw = workspaceSlotValue(workspaceSlotByName(instance, "pruneMissing")).trim().toLowerCase();
|
|
3377
4370
|
const pruneMissing = pruneMissingRaw !== "false";
|
|
3378
4371
|
const result = loadGitWorktree({ repoPath, branch, worktreePath, pipelineWorkspace: scopedRoot, force, pruneMissing });
|
|
3379
|
-
if (workspaceShouldAutoCleanupWorktree(
|
|
4372
|
+
if (workspaceShouldAutoCleanupWorktree(result.worktreePath, hasExplicitWorktreePath)) {
|
|
3380
4373
|
workspaceTrackAutoCleanupWorktree(autoCleanupWorktrees, {
|
|
3381
4374
|
nodeId,
|
|
3382
4375
|
repoPath: result.repoRoot,
|
|
@@ -3480,6 +4473,51 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
3480
4473
|
continue;
|
|
3481
4474
|
}
|
|
3482
4475
|
|
|
4476
|
+
if (defId === "tool_nodejs") {
|
|
4477
|
+
const prepareStartedAt = Date.now();
|
|
4478
|
+
const inputValues = workspaceInputValues(graph, nodeId, outputs);
|
|
4479
|
+
const runPackage = workspaceCreateNodeRunPackage(runTmpRoot, nodeId, {
|
|
4480
|
+
scopedRoot,
|
|
4481
|
+
cwd,
|
|
4482
|
+
task: String(instance.script || instance.scriptRef || instance.body || "").trim(),
|
|
4483
|
+
inputValues,
|
|
4484
|
+
});
|
|
4485
|
+
const runtimeInputValues = { ...inputValues, ...(runPackage.inputValues || {}) };
|
|
4486
|
+
emitTiming(nodeId, "prepare-script", prepareStartedAt, {
|
|
4487
|
+
inputCount: Object.keys(runtimeInputValues || {}).length,
|
|
4488
|
+
nodeRunDir: runPackage.nodeRunDir,
|
|
4489
|
+
});
|
|
4490
|
+
const content = await workspaceRunToolNodejsScript({
|
|
4491
|
+
scopedRoot,
|
|
4492
|
+
cwd,
|
|
4493
|
+
instance,
|
|
4494
|
+
inputValues: runtimeInputValues,
|
|
4495
|
+
runPackage,
|
|
4496
|
+
userCtx,
|
|
4497
|
+
emit: (event) => emit({ ...event, nodeId }),
|
|
4498
|
+
});
|
|
4499
|
+
const normalizedAgentOutput = workspacePublishAgentOutputFiles(workspaceStructuredAgentOutput(content), runPackage);
|
|
4500
|
+
const resultContent = normalizedAgentOutput.result || content;
|
|
4501
|
+
outputs.set(nodeId, resultContent);
|
|
4502
|
+
const slotUpdate = workspaceApplyAgentOutputSlots(instance, normalizedAgentOutput);
|
|
4503
|
+
if (slotUpdate.changed) graph.instances[nodeId] = slotUpdate.instance;
|
|
4504
|
+
const implementationUpdate = await workspaceTryPersistNodeImplementation(scopedRoot, graph, nodeId, {
|
|
4505
|
+
inputValues: runtimeInputValues,
|
|
4506
|
+
resultContent,
|
|
4507
|
+
structured: normalizedAgentOutput,
|
|
4508
|
+
runPackage,
|
|
4509
|
+
modelKey,
|
|
4510
|
+
userCtx,
|
|
4511
|
+
emit: (event) => emit({ ...event, nodeId }),
|
|
4512
|
+
onActiveChild: opts.onActiveChild,
|
|
4513
|
+
});
|
|
4514
|
+
if (implementationUpdate.changed) graph.instances[nodeId] = implementationUpdate.instance;
|
|
4515
|
+
const updatedDisplays = workspaceUpdateDirectDisplays(graph, nodeId, resultContent, outputs);
|
|
4516
|
+
if (slotUpdate.changed || implementationUpdate.changed || updatedDisplays.length) emit({ type: "graph", nodeId, displayNodeIds: updatedDisplays, graph });
|
|
4517
|
+
emit({ type: "node-done", nodeId, definitionId: defId });
|
|
4518
|
+
continue;
|
|
4519
|
+
}
|
|
4520
|
+
|
|
3483
4521
|
const prepareStartedAt = Date.now();
|
|
3484
4522
|
const inputValues = workspaceInputValues(graph, nodeId, outputs);
|
|
3485
4523
|
const relevantInputs = workspaceRelevantInputValues(instance.body || "", inputValues);
|
|
@@ -3506,7 +4544,8 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
3506
4544
|
} catch {
|
|
3507
4545
|
// Best-effort debug artifact only.
|
|
3508
4546
|
}
|
|
3509
|
-
const
|
|
4547
|
+
const implementationBlock = workspaceImplementationBlock(instance, scopedRoot, runPackage);
|
|
4548
|
+
const prompt = workspaceNodePrompt(graph, nodeId, promptUpstreamText, promptSkillsBlock, promptMcpBlock, runtimeInputValues, runPackage, implementationBlock);
|
|
3510
4549
|
try {
|
|
3511
4550
|
fs.writeFileSync(path.join(runPackage.nodeRunDir, "prompt.md"), prompt.trimEnd() + "\n", "utf-8");
|
|
3512
4551
|
} catch {
|
|
@@ -3592,8 +4631,19 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
3592
4631
|
outputs.set(nodeId, resultContent);
|
|
3593
4632
|
const slotUpdate = workspaceApplyAgentOutputSlots(instance, normalizedAgentOutput);
|
|
3594
4633
|
if (slotUpdate.changed) graph.instances[nodeId] = slotUpdate.instance;
|
|
4634
|
+
const implementationUpdate = await workspaceTryPersistNodeImplementation(scopedRoot, graph, nodeId, {
|
|
4635
|
+
inputValues: runtimeInputValues,
|
|
4636
|
+
resultContent,
|
|
4637
|
+
structured: normalizedAgentOutput,
|
|
4638
|
+
runPackage,
|
|
4639
|
+
modelKey,
|
|
4640
|
+
userCtx,
|
|
4641
|
+
emit: (event) => emit({ ...event, nodeId }),
|
|
4642
|
+
onActiveChild: opts.onActiveChild,
|
|
4643
|
+
});
|
|
4644
|
+
if (implementationUpdate.changed) graph.instances[nodeId] = implementationUpdate.instance;
|
|
3595
4645
|
const updatedDisplays = workspaceUpdateDirectDisplays(graph, nodeId, resultContent, outputs);
|
|
3596
|
-
if (slotUpdate.changed || updatedDisplays.length) emit({ type: "graph", nodeId, displayNodeIds: updatedDisplays, graph });
|
|
4646
|
+
if (slotUpdate.changed || implementationUpdate.changed || updatedDisplays.length) emit({ type: "graph", nodeId, displayNodeIds: updatedDisplays, graph });
|
|
3597
4647
|
emit({ type: "node-done", nodeId, definitionId: defId });
|
|
3598
4648
|
}
|
|
3599
4649
|
} finally {
|
|
@@ -4112,6 +5162,46 @@ export function startUiServer({
|
|
|
4112
5162
|
}
|
|
4113
5163
|
}
|
|
4114
5164
|
|
|
5165
|
+
if (url.pathname === "/api/admin/storage-config") {
|
|
5166
|
+
if (!authUser?.isAdmin) {
|
|
5167
|
+
json(res, 403, { error: "Admin permission required" });
|
|
5168
|
+
return;
|
|
5169
|
+
}
|
|
5170
|
+
if (req.method === "GET") {
|
|
5171
|
+
json(res, 200, { config: readAdminStorageConfig() });
|
|
5172
|
+
return;
|
|
5173
|
+
}
|
|
5174
|
+
if (req.method === "POST") {
|
|
5175
|
+
let payload;
|
|
5176
|
+
try {
|
|
5177
|
+
payload = JSON.parse(await readBody(req));
|
|
5178
|
+
} catch {
|
|
5179
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
5180
|
+
return;
|
|
5181
|
+
}
|
|
5182
|
+
try {
|
|
5183
|
+
const config = writeAdminStorageConfig(payload?.config || payload || {});
|
|
5184
|
+
json(res, 200, { ok: true, config });
|
|
5185
|
+
} catch (e) {
|
|
5186
|
+
json(res, 400, { error: (e && e.message) || String(e) });
|
|
5187
|
+
}
|
|
5188
|
+
return;
|
|
5189
|
+
}
|
|
5190
|
+
}
|
|
5191
|
+
|
|
5192
|
+
if (req.method === "GET" && url.pathname === "/api/admin/usage-dashboard") {
|
|
5193
|
+
if (!authUser?.isAdmin) {
|
|
5194
|
+
json(res, 403, { error: "Admin permission required" });
|
|
5195
|
+
return;
|
|
5196
|
+
}
|
|
5197
|
+
try {
|
|
5198
|
+
json(res, 200, buildAdminUsageDashboard(root));
|
|
5199
|
+
} catch (e) {
|
|
5200
|
+
json(res, 500, { error: (e && e.message) || String(e) });
|
|
5201
|
+
}
|
|
5202
|
+
return;
|
|
5203
|
+
}
|
|
5204
|
+
|
|
4115
5205
|
if (url.pathname === "/api/admin/user-allowlist") {
|
|
4116
5206
|
if (!authUser?.isAdmin) {
|
|
4117
5207
|
json(res, 403, { error: "Admin permission required" });
|
|
@@ -4408,9 +5498,10 @@ export function startUiServer({
|
|
|
4408
5498
|
return;
|
|
4409
5499
|
}
|
|
4410
5500
|
const { path: graphPath, graph } = readWorkspaceGraph(scoped.root);
|
|
5501
|
+
const hydratedGraph = hydrateWorkspaceNodeRefsFromFiles(scoped.root, graph);
|
|
4411
5502
|
json(res, 200, {
|
|
4412
5503
|
ok: true,
|
|
4413
|
-
graph,
|
|
5504
|
+
graph: hydratedGraph,
|
|
4414
5505
|
path: graphPath,
|
|
4415
5506
|
root: scoped.root,
|
|
4416
5507
|
flowId: scoped.flowId,
|
|
@@ -4497,8 +5588,10 @@ export function startUiServer({
|
|
|
4497
5588
|
json(res, 400, { error: "Cannot write workspace graph for builtin or archived pipeline" });
|
|
4498
5589
|
return;
|
|
4499
5590
|
}
|
|
4500
|
-
const
|
|
5591
|
+
const submittedGraph = normalizeWorkspaceGraphPayload(payload.graph || payload);
|
|
4501
5592
|
const graphPath = workspaceGraphPath(scoped.root);
|
|
5593
|
+
const currentGraph = hydrateWorkspaceNodeRefsFromFiles(scoped.root, readWorkspaceGraph(scoped.root).graph);
|
|
5594
|
+
const graph = mergeWorkspacePersistentNodeRefs(submittedGraph, currentGraph);
|
|
4502
5595
|
fs.writeFileSync(graphPath, JSON.stringify(graph, null, 2) + "\n", "utf-8");
|
|
4503
5596
|
json(res, 200, { ok: true, path: graphPath, graph });
|
|
4504
5597
|
} catch (e) {
|
|
@@ -4544,6 +5637,9 @@ export function startUiServer({
|
|
|
4544
5637
|
const runEntry = {
|
|
4545
5638
|
controller,
|
|
4546
5639
|
child: null,
|
|
5640
|
+
runId: runLedgerId("workspace"),
|
|
5641
|
+
userId: String(userCtx.userId || ""),
|
|
5642
|
+
username: String(authUser?.username || userCtx.userId || ""),
|
|
4547
5643
|
runNodeId: String(payload.runNodeId || "").trim(),
|
|
4548
5644
|
flowId,
|
|
4549
5645
|
flowSource: scoped.flowSource || payload.flowSource || "user",
|
|
@@ -4555,6 +5651,7 @@ export function startUiServer({
|
|
|
4555
5651
|
},
|
|
4556
5652
|
};
|
|
4557
5653
|
activeWorkspaceRuns.set(runKey, runEntry);
|
|
5654
|
+
appendWorkspaceRunStarted(runEntry);
|
|
4558
5655
|
const setActiveChild = (child) => {
|
|
4559
5656
|
runEntry.child = child || null;
|
|
4560
5657
|
if (controller.signal.aborted) runEntry.stopChild();
|
|
@@ -4582,12 +5679,27 @@ export function startUiServer({
|
|
|
4582
5679
|
const touchedIds = workspaceRunTouchedNodeIds(result);
|
|
4583
5680
|
const mergedGraph = mergeWorkspaceRunGraph(currentGraph, result.graph, touchedIds);
|
|
4584
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");
|
|
4585
5687
|
writeEvent({ type: "done", ok: true, path: graphPath, graph: mergedGraph, order: result.order, touchedNodeIds: Array.from(touchedIds), pauseNodeIds: result.pauseNodeIds || [] });
|
|
4586
5688
|
res.end();
|
|
4587
5689
|
} catch (e) {
|
|
4588
5690
|
if (isWorkspaceRunAbortError(e) || controller.signal.aborted) {
|
|
5691
|
+
appendWorkspaceRunFinished({
|
|
5692
|
+
...runEntry,
|
|
5693
|
+
endedAt: Date.now(),
|
|
5694
|
+
durationMs: Date.now() - runEntry.startedAt,
|
|
5695
|
+
}, "stopped");
|
|
4589
5696
|
writeEvent({ type: "stopped", ok: false, stopped: true, message: "Workspace run stopped" });
|
|
4590
5697
|
} else {
|
|
5698
|
+
appendWorkspaceRunFinished({
|
|
5699
|
+
...runEntry,
|
|
5700
|
+
endedAt: Date.now(),
|
|
5701
|
+
durationMs: Date.now() - runEntry.startedAt,
|
|
5702
|
+
}, "failed");
|
|
4591
5703
|
writeEvent({ type: "error", error: (e && e.message) || String(e) });
|
|
4592
5704
|
}
|
|
4593
5705
|
res.end();
|
|
@@ -4606,11 +5718,26 @@ export function startUiServer({
|
|
|
4606
5718
|
const touchedIds = workspaceRunTouchedNodeIds(result);
|
|
4607
5719
|
const mergedGraph = mergeWorkspaceRunGraph(currentGraph, result.graph, touchedIds);
|
|
4608
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");
|
|
4609
5726
|
json(res, 200, { ok: true, path: graphPath, ...result, graph: mergedGraph, touchedNodeIds: Array.from(touchedIds) });
|
|
4610
5727
|
} catch (e) {
|
|
4611
5728
|
if (isWorkspaceRunAbortError(e) || controller.signal.aborted) {
|
|
5729
|
+
appendWorkspaceRunFinished({
|
|
5730
|
+
...runEntry,
|
|
5731
|
+
endedAt: Date.now(),
|
|
5732
|
+
durationMs: Date.now() - runEntry.startedAt,
|
|
5733
|
+
}, "stopped");
|
|
4612
5734
|
json(res, 200, { ok: false, stopped: true, message: "Workspace run stopped" });
|
|
4613
5735
|
} else {
|
|
5736
|
+
appendWorkspaceRunFinished({
|
|
5737
|
+
...runEntry,
|
|
5738
|
+
endedAt: Date.now(),
|
|
5739
|
+
durationMs: Date.now() - runEntry.startedAt,
|
|
5740
|
+
}, "failed");
|
|
4614
5741
|
throw e;
|
|
4615
5742
|
}
|
|
4616
5743
|
} finally {
|