@fieldwangai/agentflow 0.1.68 → 0.1.69
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/marketplace.mjs +55 -3
- package/bin/lib/paths.mjs +43 -5
- package/bin/lib/ui-server.mjs +894 -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-CCvxfovU.css +1 -0
- package/builtin/web-ui/dist/assets/index-DvdPaPBL.js +296 -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,7 @@ import {
|
|
|
106
109
|
readAdminBuiltinPipelineConfig,
|
|
107
110
|
updateAdminBuiltinPipelineConfig,
|
|
108
111
|
} from "./admin-builtin-pipelines.mjs";
|
|
112
|
+
import { readAdminStorageConfig, writeAdminStorageConfig } from "./admin-storage-config.mjs";
|
|
109
113
|
|
|
110
114
|
const MIME = {
|
|
111
115
|
".html": "text/html; charset=utf-8",
|
|
@@ -1367,31 +1371,40 @@ function shouldSkipWorkspaceFileRelPath(relPath) {
|
|
|
1367
1371
|
));
|
|
1368
1372
|
}
|
|
1369
1373
|
|
|
1374
|
+
const WORKSPACE_FILES_MAX_ITEMS = 500;
|
|
1375
|
+
|
|
1370
1376
|
function readWorkspaceFilesRecursive(dir, root, depth = 0, maxDepth = 3, budget = { count: 0 }) {
|
|
1371
|
-
if (depth > maxDepth
|
|
1377
|
+
if (depth > maxDepth) return [];
|
|
1378
|
+
if (depth > 0 && budget.count > WORKSPACE_FILES_MAX_ITEMS) return [];
|
|
1372
1379
|
let entries;
|
|
1373
1380
|
try {
|
|
1374
1381
|
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
1375
1382
|
} catch {
|
|
1376
1383
|
return [];
|
|
1377
1384
|
}
|
|
1385
|
+
entries.sort((a, b) => {
|
|
1386
|
+
if (a.isDirectory() !== b.isDirectory()) return a.isDirectory() ? -1 : 1;
|
|
1387
|
+
return a.name.localeCompare(b.name);
|
|
1388
|
+
});
|
|
1378
1389
|
const out = [];
|
|
1379
1390
|
for (const entry of entries) {
|
|
1380
|
-
if (budget.count >
|
|
1391
|
+
if (depth > 0 && budget.count > WORKSPACE_FILES_MAX_ITEMS) break;
|
|
1381
1392
|
if (entry.name.startsWith(".") && entry.name !== ".agents" && entry.name !== ".codex") continue;
|
|
1382
1393
|
const abs = path.join(dir, entry.name);
|
|
1383
1394
|
const rel = path.relative(root, abs).replace(/\\/g, "/");
|
|
1384
1395
|
if (shouldSkipWorkspaceFileRelPath(rel)) continue;
|
|
1385
1396
|
if (entry.isDirectory()) {
|
|
1386
1397
|
if (WORKSPACE_FILE_SKIP_DIRS.has(entry.name)) continue;
|
|
1387
|
-
budget.count++;
|
|
1388
1398
|
out.push({
|
|
1389
1399
|
type: "directory",
|
|
1390
1400
|
name: entry.name,
|
|
1391
1401
|
path: rel,
|
|
1392
1402
|
icon: workspaceFileIcon(entry.name, true),
|
|
1393
|
-
children:
|
|
1403
|
+
children: budget.count > WORKSPACE_FILES_MAX_ITEMS
|
|
1404
|
+
? []
|
|
1405
|
+
: readWorkspaceFilesRecursive(abs, root, depth + 1, maxDepth, budget),
|
|
1394
1406
|
});
|
|
1407
|
+
budget.count++;
|
|
1395
1408
|
} else if (entry.isFile()) {
|
|
1396
1409
|
if (WORKSPACE_FILE_SKIP_FILES.has(entry.name)) continue;
|
|
1397
1410
|
const ext = path.extname(entry.name).toLowerCase();
|
|
@@ -1402,10 +1415,6 @@ function readWorkspaceFilesRecursive(dir, root, depth = 0, maxDepth = 3, budget
|
|
|
1402
1415
|
out.push({ type: "file", name: entry.name, path: rel, icon: workspaceFileIcon(entry.name), size });
|
|
1403
1416
|
}
|
|
1404
1417
|
}
|
|
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
1418
|
return out;
|
|
1410
1419
|
}
|
|
1411
1420
|
|
|
@@ -1464,6 +1473,230 @@ function writeDisplayShares(shares) {
|
|
|
1464
1473
|
fs.writeFileSync(file, JSON.stringify(shares && typeof shares === "object" ? shares : {}, null, 2) + "\n", "utf-8");
|
|
1465
1474
|
}
|
|
1466
1475
|
|
|
1476
|
+
function countFlowYamlDirs(root) {
|
|
1477
|
+
try {
|
|
1478
|
+
if (!fs.existsSync(root) || !fs.statSync(root).isDirectory()) return 0;
|
|
1479
|
+
return fs.readdirSync(root, { withFileTypes: true })
|
|
1480
|
+
.filter((entry) => entry.isDirectory())
|
|
1481
|
+
.filter((entry) => entry.name !== ARCHIVED_PIPELINES_DIR_NAME)
|
|
1482
|
+
.filter((entry) => fs.existsSync(path.join(root, entry.name, FLOW_YAML_FILENAME)))
|
|
1483
|
+
.length;
|
|
1484
|
+
} catch {
|
|
1485
|
+
return 0;
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
function pipelineCountsForUser(userId) {
|
|
1490
|
+
const pipelinesRoot = getUserPipelinesRoot(userId);
|
|
1491
|
+
const active = countFlowYamlDirs(pipelinesRoot);
|
|
1492
|
+
const archived = countFlowYamlDirs(path.join(pipelinesRoot, ARCHIVED_PIPELINES_DIR_NAME));
|
|
1493
|
+
return {
|
|
1494
|
+
active,
|
|
1495
|
+
archived,
|
|
1496
|
+
total: active + archived,
|
|
1497
|
+
};
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
const USAGE_DAY_MS = 24 * 60 * 60 * 1000;
|
|
1501
|
+
|
|
1502
|
+
function startOfLocalDayMs(timeMs) {
|
|
1503
|
+
const d = new Date(Number(timeMs) || Date.now());
|
|
1504
|
+
return new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
function localDayKey(timeMs) {
|
|
1508
|
+
const d = new Date(Number(timeMs) || Date.now());
|
|
1509
|
+
const y = d.getFullYear();
|
|
1510
|
+
const m = String(d.getMonth() + 1).padStart(2, "0");
|
|
1511
|
+
const day = String(d.getDate()).padStart(2, "0");
|
|
1512
|
+
return `${y}-${m}-${day}`;
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
function runStatusBucket(status) {
|
|
1516
|
+
const s = String(status || "unknown");
|
|
1517
|
+
if (s === "success" || s === "failed" || s === "running" || s === "stopped" || s === "interrupted") return s;
|
|
1518
|
+
return "unknown";
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
function buildUsageDailyTrend(runs, days = 14, nowMs = Date.now()) {
|
|
1522
|
+
const startMs = startOfLocalDayMs(nowMs) - (Math.max(1, days) - 1) * USAGE_DAY_MS;
|
|
1523
|
+
const rows = [];
|
|
1524
|
+
const byDate = new Map();
|
|
1525
|
+
for (let i = 0; i < days; i += 1) {
|
|
1526
|
+
const dateMs = startMs + i * USAGE_DAY_MS;
|
|
1527
|
+
const date = localDayKey(dateMs);
|
|
1528
|
+
const row = {
|
|
1529
|
+
date,
|
|
1530
|
+
runs: 0,
|
|
1531
|
+
success: 0,
|
|
1532
|
+
failed: 0,
|
|
1533
|
+
running: 0,
|
|
1534
|
+
stopped: 0,
|
|
1535
|
+
interrupted: 0,
|
|
1536
|
+
unknown: 0,
|
|
1537
|
+
users: 0,
|
|
1538
|
+
pipelines: 0,
|
|
1539
|
+
totalDurationMs: 0,
|
|
1540
|
+
avgDurationMs: 0,
|
|
1541
|
+
_userIds: new Set(),
|
|
1542
|
+
_pipelineKeys: new Set(),
|
|
1543
|
+
};
|
|
1544
|
+
rows.push(row);
|
|
1545
|
+
byDate.set(date, row);
|
|
1546
|
+
}
|
|
1547
|
+
for (const run of runs) {
|
|
1548
|
+
const at = Number(run?.at || 0);
|
|
1549
|
+
if (!Number.isFinite(at) || at < startMs) continue;
|
|
1550
|
+
const row = byDate.get(localDayKey(at));
|
|
1551
|
+
if (!row) continue;
|
|
1552
|
+
const bucket = runStatusBucket(run.status);
|
|
1553
|
+
row.runs += 1;
|
|
1554
|
+
row[bucket] += 1;
|
|
1555
|
+
row.totalDurationMs += Math.max(0, Number(run.durationMs || 0));
|
|
1556
|
+
row._userIds.add(String(run.userId || ""));
|
|
1557
|
+
row._pipelineKeys.add(`${run.userId || ""}:${run.flowSource || ""}:${run.flowId || ""}`);
|
|
1558
|
+
}
|
|
1559
|
+
return rows.map((row) => {
|
|
1560
|
+
row.users = row._userIds.size;
|
|
1561
|
+
row.pipelines = row._pipelineKeys.size;
|
|
1562
|
+
row.avgDurationMs = row.runs > 0 ? Math.round(row.totalDurationMs / row.runs) : 0;
|
|
1563
|
+
delete row._userIds;
|
|
1564
|
+
delete row._pipelineKeys;
|
|
1565
|
+
return row;
|
|
1566
|
+
});
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
function buildUsageRates(users, runs, nowMs = Date.now()) {
|
|
1570
|
+
const windowDays = 7;
|
|
1571
|
+
const sinceMs = startOfLocalDayMs(nowMs) - (windowDays - 1) * USAGE_DAY_MS;
|
|
1572
|
+
const recentRuns = runs.filter((run) => Number(run?.at || 0) >= sinceMs);
|
|
1573
|
+
const activeUsers = new Set();
|
|
1574
|
+
const activePipelines = new Set();
|
|
1575
|
+
const statusCounts = {};
|
|
1576
|
+
let recentDurationMs = 0;
|
|
1577
|
+
for (const run of recentRuns) {
|
|
1578
|
+
const userId = String(run.userId || "");
|
|
1579
|
+
if (userId) activeUsers.add(userId);
|
|
1580
|
+
activePipelines.add(`${userId}:${run.flowSource || ""}:${run.flowId || ""}`);
|
|
1581
|
+
const bucket = runStatusBucket(run.status);
|
|
1582
|
+
statusCounts[bucket] = (statusCounts[bucket] || 0) + 1;
|
|
1583
|
+
recentDurationMs += Math.max(0, Number(run.durationMs || 0));
|
|
1584
|
+
}
|
|
1585
|
+
const totalUsers = users.length;
|
|
1586
|
+
const totalActivePipelines = users.reduce((sum, user) => sum + Math.max(0, Number(user?.pipelines?.active || 0)), 0);
|
|
1587
|
+
const completedRuns = recentRuns.length - (statusCounts.running || 0);
|
|
1588
|
+
const badRuns = (statusCounts.failed || 0) + (statusCounts.stopped || 0) + (statusCounts.interrupted || 0) + (statusCounts.unknown || 0);
|
|
1589
|
+
return {
|
|
1590
|
+
windowDays,
|
|
1591
|
+
activeUsers: activeUsers.size,
|
|
1592
|
+
activeUserRate: totalUsers > 0 ? activeUsers.size / totalUsers : 0,
|
|
1593
|
+
activePipelines: activePipelines.size,
|
|
1594
|
+
activePipelineRate: totalActivePipelines > 0 ? activePipelines.size / totalActivePipelines : 0,
|
|
1595
|
+
runs: recentRuns.length,
|
|
1596
|
+
avgRunsPerDay: recentRuns.length / windowDays,
|
|
1597
|
+
successRuns: statusCounts.success || 0,
|
|
1598
|
+
badRuns,
|
|
1599
|
+
runningRuns: statusCounts.running || 0,
|
|
1600
|
+
successRate: completedRuns > 0 ? (statusCounts.success || 0) / completedRuns : 0,
|
|
1601
|
+
failureRate: completedRuns > 0 ? badRuns / completedRuns : 0,
|
|
1602
|
+
avgDurationMs: recentRuns.length > 0 ? Math.round(recentDurationMs / recentRuns.length) : 0,
|
|
1603
|
+
};
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
function buildAdminUsageDashboard(workspaceRoot) {
|
|
1607
|
+
const authUsers = readAuthUsers();
|
|
1608
|
+
const userIds = Array.from(new Set([
|
|
1609
|
+
...Object.keys(authUsers || {}),
|
|
1610
|
+
...listAgentflowUserIds(),
|
|
1611
|
+
].map((id) => String(id || "").trim()).filter(Boolean))).sort((a, b) => a.localeCompare(b));
|
|
1612
|
+
const allRuns = [];
|
|
1613
|
+
const users = userIds.map((userId) => {
|
|
1614
|
+
const user = authUsers[userId] || {};
|
|
1615
|
+
const pipelineCounts = pipelineCountsForUser(userId);
|
|
1616
|
+
const runs = listRecentRunsFromDisk(workspaceRoot, {
|
|
1617
|
+
userId,
|
|
1618
|
+
includeWorkspaceRuns: false,
|
|
1619
|
+
includeLegacyUserRuns: false,
|
|
1620
|
+
});
|
|
1621
|
+
const statusCounts = {};
|
|
1622
|
+
let totalDurationMs = 0;
|
|
1623
|
+
for (const run of runs) {
|
|
1624
|
+
allRuns.push({ ...run, userId, username: user.username || userId });
|
|
1625
|
+
const status = String(run.status || "unknown");
|
|
1626
|
+
statusCounts[status] = (statusCounts[status] || 0) + 1;
|
|
1627
|
+
totalDurationMs += Math.max(0, Number(run.durationMs || 0));
|
|
1628
|
+
}
|
|
1629
|
+
const lastRun = runs[0] || null;
|
|
1630
|
+
return {
|
|
1631
|
+
userId,
|
|
1632
|
+
username: user.username || userId,
|
|
1633
|
+
isAdmin: Boolean(user.isAdmin),
|
|
1634
|
+
pipelines: pipelineCounts,
|
|
1635
|
+
runs: {
|
|
1636
|
+
total: runs.length,
|
|
1637
|
+
running: statusCounts.running || 0,
|
|
1638
|
+
success: statusCounts.success || 0,
|
|
1639
|
+
failed: statusCounts.failed || 0,
|
|
1640
|
+
stopped: statusCounts.stopped || 0,
|
|
1641
|
+
interrupted: statusCounts.interrupted || 0,
|
|
1642
|
+
unknown: statusCounts.unknown || 0,
|
|
1643
|
+
totalDurationMs,
|
|
1644
|
+
avgDurationMs: runs.length > 0 ? Math.round(totalDurationMs / runs.length) : 0,
|
|
1645
|
+
lastRunAt: lastRun?.at || null,
|
|
1646
|
+
lastRunFlowId: lastRun?.flowId || "",
|
|
1647
|
+
lastRunStatus: lastRun?.status || "",
|
|
1648
|
+
recent: runs.slice(0, 5).map((run) => ({
|
|
1649
|
+
flowId: run.flowId,
|
|
1650
|
+
flowSource: run.flowSource,
|
|
1651
|
+
runId: run.runId,
|
|
1652
|
+
at: run.at,
|
|
1653
|
+
endedAt: run.endedAt,
|
|
1654
|
+
durationMs: run.durationMs,
|
|
1655
|
+
status: run.status,
|
|
1656
|
+
})),
|
|
1657
|
+
},
|
|
1658
|
+
};
|
|
1659
|
+
});
|
|
1660
|
+
const totals = users.reduce((acc, user) => {
|
|
1661
|
+
acc.users += 1;
|
|
1662
|
+
acc.admins += user.isAdmin ? 1 : 0;
|
|
1663
|
+
acc.pipelines += user.pipelines.total;
|
|
1664
|
+
acc.activePipelines += user.pipelines.active;
|
|
1665
|
+
acc.archivedPipelines += user.pipelines.archived;
|
|
1666
|
+
acc.runs += user.runs.total;
|
|
1667
|
+
acc.runningRuns += user.runs.running;
|
|
1668
|
+
acc.successRuns += user.runs.success;
|
|
1669
|
+
acc.failedRuns += user.runs.failed;
|
|
1670
|
+
acc.stoppedRuns += user.runs.stopped;
|
|
1671
|
+
acc.interruptedRuns += user.runs.interrupted;
|
|
1672
|
+
acc.unknownRuns += user.runs.unknown;
|
|
1673
|
+
acc.totalDurationMs += user.runs.totalDurationMs;
|
|
1674
|
+
return acc;
|
|
1675
|
+
}, {
|
|
1676
|
+
users: 0,
|
|
1677
|
+
admins: 0,
|
|
1678
|
+
pipelines: 0,
|
|
1679
|
+
activePipelines: 0,
|
|
1680
|
+
archivedPipelines: 0,
|
|
1681
|
+
runs: 0,
|
|
1682
|
+
runningRuns: 0,
|
|
1683
|
+
successRuns: 0,
|
|
1684
|
+
failedRuns: 0,
|
|
1685
|
+
stoppedRuns: 0,
|
|
1686
|
+
interruptedRuns: 0,
|
|
1687
|
+
unknownRuns: 0,
|
|
1688
|
+
totalDurationMs: 0,
|
|
1689
|
+
});
|
|
1690
|
+
totals.avgDurationMs = totals.runs > 0 ? Math.round(totals.totalDurationMs / totals.runs) : 0;
|
|
1691
|
+
return {
|
|
1692
|
+
generatedAt: new Date().toISOString(),
|
|
1693
|
+
totals,
|
|
1694
|
+
usage: buildUsageRates(users, allRuns),
|
|
1695
|
+
dailyTrend: buildUsageDailyTrend(allRuns, 14),
|
|
1696
|
+
users,
|
|
1697
|
+
};
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1467
1700
|
function createDisplayShareId() {
|
|
1468
1701
|
return crypto.randomBytes(12).toString("base64url");
|
|
1469
1702
|
}
|
|
@@ -1629,6 +1862,44 @@ function mergeWorkspaceRunGraph(currentGraph, runGraph, touchedIds) {
|
|
|
1629
1862
|
};
|
|
1630
1863
|
}
|
|
1631
1864
|
|
|
1865
|
+
function mergeWorkspacePersistentNodeRefs(incomingGraph, currentGraph) {
|
|
1866
|
+
const incoming = normalizeWorkspaceGraphPayload(incomingGraph || {});
|
|
1867
|
+
const current = normalizeWorkspaceGraphPayload(currentGraph || {});
|
|
1868
|
+
const instances = { ...(incoming.instances || {}) };
|
|
1869
|
+
for (const [id, currentInstance] of Object.entries(current.instances || {})) {
|
|
1870
|
+
const nextInstance = instances[id];
|
|
1871
|
+
if (!nextInstance || typeof nextInstance !== "object") continue;
|
|
1872
|
+
for (const key of ["scriptRef", "implementationRef", "implementationMode"]) {
|
|
1873
|
+
const currentValue = currentInstance?.[key];
|
|
1874
|
+
const nextValue = nextInstance?.[key];
|
|
1875
|
+
if (currentValue != null && String(currentValue).trim() && (nextValue == null || !String(nextValue).trim())) {
|
|
1876
|
+
nextInstance[key] = currentValue;
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
return { ...incoming, instances };
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
function hydrateWorkspaceNodeRefsFromFiles(scopedRoot, graph) {
|
|
1884
|
+
const next = normalizeWorkspaceGraphPayload(graph || {});
|
|
1885
|
+
const instances = { ...(next.instances || {}) };
|
|
1886
|
+
let changed = false;
|
|
1887
|
+
for (const [nodeId, instance] of Object.entries(instances)) {
|
|
1888
|
+
if (!instance || typeof instance !== "object") continue;
|
|
1889
|
+
const defId = String(instance.definitionId || "");
|
|
1890
|
+
if (defId === "workspace_run" || defId === "workspace_scheduled_run") continue;
|
|
1891
|
+
if (!String(instance.implementationRef || "").trim()) {
|
|
1892
|
+
const implementationRef = workspaceDefaultImplementationRef(nodeId);
|
|
1893
|
+
const abs = workspaceResolveFlowFile(scopedRoot, implementationRef, "implementationRef");
|
|
1894
|
+
if (fs.existsSync(abs) && fs.statSync(abs).isFile()) {
|
|
1895
|
+
instances[nodeId] = { ...instance, implementationRef };
|
|
1896
|
+
changed = true;
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
return changed ? { ...next, instances } : next;
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1632
1903
|
function resolveWorkspaceScopeRoot(workspaceRoot, params = {}, opts = {}) {
|
|
1633
1904
|
const flowId = params.flowId != null ? String(params.flowId).trim() : "";
|
|
1634
1905
|
if (!flowId) return { root: path.resolve(workspaceRoot), flowId: "", flowSource: "", archived: false };
|
|
@@ -2214,6 +2485,45 @@ function workspaceResolvePath(baseCwd, raw) {
|
|
|
2214
2485
|
return path.isAbsolute(text) ? path.resolve(text) : path.resolve(baseCwd, text);
|
|
2215
2486
|
}
|
|
2216
2487
|
|
|
2488
|
+
function workspaceShellQuote(value) {
|
|
2489
|
+
return "'" + String(value ?? "").replace(/'/g, "'\\''") + "'";
|
|
2490
|
+
}
|
|
2491
|
+
|
|
2492
|
+
function workspaceSafeFlowRelPath(raw, fieldName = "path") {
|
|
2493
|
+
const text = String(raw || "").trim().replace(/^["']|["']$/g, "");
|
|
2494
|
+
if (!text) return "";
|
|
2495
|
+
if (text.length > 260) throw new Error(`${fieldName} is too long`);
|
|
2496
|
+
if (/[\r\n<>]/.test(text)) throw new Error(`${fieldName} contains invalid characters`);
|
|
2497
|
+
if (/^(?:https?:|data:|blob:|file:|javascript:|mailto:|tel:)/i.test(text)) {
|
|
2498
|
+
throw new Error(`${fieldName} must be a relative file path`);
|
|
2499
|
+
}
|
|
2500
|
+
if (path.isAbsolute(text)) throw new Error(`${fieldName} must be relative`);
|
|
2501
|
+
const normalized = path.posix.normalize(text.replace(/\\/g, "/")).replace(/^\/+/, "");
|
|
2502
|
+
if (!normalized || normalized === "." || normalized.startsWith("../") || normalized.includes("/../")) {
|
|
2503
|
+
throw new Error(`${fieldName} escapes workspace root`);
|
|
2504
|
+
}
|
|
2505
|
+
return normalized;
|
|
2506
|
+
}
|
|
2507
|
+
|
|
2508
|
+
function workspaceResolveFlowFile(scopedRoot, relPath, fieldName = "path") {
|
|
2509
|
+
const clean = workspaceSafeFlowRelPath(relPath, fieldName);
|
|
2510
|
+
if (!clean) return "";
|
|
2511
|
+
const root = path.resolve(scopedRoot);
|
|
2512
|
+
const abs = path.resolve(root, ...clean.split("/"));
|
|
2513
|
+
const rootWithSep = root.endsWith(path.sep) ? root : `${root}${path.sep}`;
|
|
2514
|
+
if (abs !== root && !abs.startsWith(rootWithSep)) {
|
|
2515
|
+
throw new Error(`${fieldName} escapes workspace root`);
|
|
2516
|
+
}
|
|
2517
|
+
return abs;
|
|
2518
|
+
}
|
|
2519
|
+
|
|
2520
|
+
function workspaceReadTextFileIfExists(absPath, maxChars = 60000) {
|
|
2521
|
+
const file = String(absPath || "").trim();
|
|
2522
|
+
if (!file || !fs.existsSync(file) || !fs.statSync(file).isFile()) return "";
|
|
2523
|
+
const raw = fs.readFileSync(file, "utf-8");
|
|
2524
|
+
return raw.length > maxChars ? `${raw.slice(0, maxChars)}\n...[truncated ${raw.length - maxChars} chars]` : raw;
|
|
2525
|
+
}
|
|
2526
|
+
|
|
2217
2527
|
function workspaceSanitizeRepoDirName(repoUrl) {
|
|
2218
2528
|
const raw = String(repoUrl || "").trim().replace(/\.git$/i, "");
|
|
2219
2529
|
const last = raw.split(/[/:]/).filter(Boolean).pop() || "repo";
|
|
@@ -2500,7 +2810,7 @@ function workspaceRunPlan(graph, runNodeId, scopedRoot = "") {
|
|
|
2500
2810
|
const addNeeded = (id) => {
|
|
2501
2811
|
if (!id || needed.has(id)) return;
|
|
2502
2812
|
const defId = String(instances[id]?.definitionId || "");
|
|
2503
|
-
if (id !== target && defId === "workspace_run") {
|
|
2813
|
+
if (id !== target && (defId === "workspace_run" || defId === "workspace_scheduled_run")) {
|
|
2504
2814
|
pauseNodeIds.add(id);
|
|
2505
2815
|
return;
|
|
2506
2816
|
}
|
|
@@ -2726,6 +3036,296 @@ function workspacePromptUpstreamText(upstreamText, runPackage = {}) {
|
|
|
2726
3036
|
return upstreamText;
|
|
2727
3037
|
}
|
|
2728
3038
|
|
|
3039
|
+
function workspaceImplementationInlineText(instance) {
|
|
3040
|
+
const candidates = [instance?.implementation, instance?.implementationPlan];
|
|
3041
|
+
for (const item of candidates) {
|
|
3042
|
+
if (item == null) continue;
|
|
3043
|
+
if (typeof item === "string" && item.trim()) return item.trim();
|
|
3044
|
+
if (typeof item === "object" && !Array.isArray(item)) {
|
|
3045
|
+
const content = item.content ?? item.body ?? item.notes ?? "";
|
|
3046
|
+
if (String(content || "").trim()) return String(content).trim();
|
|
3047
|
+
}
|
|
3048
|
+
}
|
|
3049
|
+
return "";
|
|
3050
|
+
}
|
|
3051
|
+
|
|
3052
|
+
function workspaceImplementationBlock(instance, scopedRoot) {
|
|
3053
|
+
const parts = [];
|
|
3054
|
+
const implementationRef = String(instance?.implementationRef || "").trim();
|
|
3055
|
+
if (implementationRef) {
|
|
3056
|
+
const abs = workspaceResolveFlowFile(scopedRoot, implementationRef, "implementationRef");
|
|
3057
|
+
const body = workspaceReadTextFileIfExists(abs, 60000);
|
|
3058
|
+
if (body.trim()) {
|
|
3059
|
+
parts.push(`### ${implementationRef}\n\n${body.trim()}`);
|
|
3060
|
+
}
|
|
3061
|
+
}
|
|
3062
|
+
const inline = workspaceImplementationInlineText(instance);
|
|
3063
|
+
if (inline) parts.push(`### Inline implementation\n\n${inline}`);
|
|
3064
|
+
if (!parts.length) return "";
|
|
3065
|
+
const mode = String(instance?.implementationMode || "").trim();
|
|
3066
|
+
return [
|
|
3067
|
+
"## 参考实现方案",
|
|
3068
|
+
"",
|
|
3069
|
+
mode ? `mode: ${mode}` : "",
|
|
3070
|
+
"",
|
|
3071
|
+
...parts,
|
|
3072
|
+
"",
|
|
3073
|
+
"以上方案用于加速本次执行。若发现过期,可以按当前任务修正执行,但最终结果必须以当前输入为准。",
|
|
3074
|
+
].filter((line) => line !== "").join("\n");
|
|
3075
|
+
}
|
|
3076
|
+
|
|
3077
|
+
function workspaceSafeNodeFileName(nodeId) {
|
|
3078
|
+
const text = String(nodeId || "").trim().replace(/[^A-Za-z0-9_-]+/g, "_").replace(/^_+|_+$/g, "");
|
|
3079
|
+
return text || "node";
|
|
3080
|
+
}
|
|
3081
|
+
|
|
3082
|
+
function workspaceDefaultImplementationRef(nodeId) {
|
|
3083
|
+
return `nodes/${workspaceSafeNodeFileName(nodeId)}/implementation.md`;
|
|
3084
|
+
}
|
|
3085
|
+
|
|
3086
|
+
function workspaceImplementationModeForInstance(instance) {
|
|
3087
|
+
const explicit = String(instance?.implementationMode || "").trim();
|
|
3088
|
+
if (explicit) return explicit;
|
|
3089
|
+
return String(instance?.definitionId || "") === "tool_nodejs" ? "script" : "steps";
|
|
3090
|
+
}
|
|
3091
|
+
|
|
3092
|
+
function workspaceClipImplementationText(value, maxChars = 2400) {
|
|
3093
|
+
const text = String(value ?? "").trim();
|
|
3094
|
+
if (!text) return "";
|
|
3095
|
+
return text.length > maxChars ? `${text.slice(0, maxChars)}\n...[truncated ${text.length - maxChars} chars]` : text;
|
|
3096
|
+
}
|
|
3097
|
+
|
|
3098
|
+
function workspaceUniqueImplementationList(items = [], maxItems = 12) {
|
|
3099
|
+
const out = [];
|
|
3100
|
+
const seen = new Set();
|
|
3101
|
+
for (const item of items) {
|
|
3102
|
+
const text = String(item || "").trim();
|
|
3103
|
+
if (!text) continue;
|
|
3104
|
+
const key = text.toLowerCase();
|
|
3105
|
+
if (seen.has(key)) continue;
|
|
3106
|
+
seen.add(key);
|
|
3107
|
+
out.push(text);
|
|
3108
|
+
if (out.length >= maxItems) break;
|
|
3109
|
+
}
|
|
3110
|
+
return out;
|
|
3111
|
+
}
|
|
3112
|
+
|
|
3113
|
+
function workspaceImplementationArtifactCandidates(structured = {}, result = "") {
|
|
3114
|
+
const candidates = [
|
|
3115
|
+
structured.resultFile,
|
|
3116
|
+
structured.result,
|
|
3117
|
+
result,
|
|
3118
|
+
...Object.values(structured.outParams || {}),
|
|
3119
|
+
];
|
|
3120
|
+
return workspaceUniqueImplementationList(candidates, 10)
|
|
3121
|
+
.filter((item) => /^(?:outputs|nodes|artifacts)\//.test(item) && !/[\r\n]/.test(item));
|
|
3122
|
+
}
|
|
3123
|
+
|
|
3124
|
+
function workspaceReadImplementationArtifact(scopedRoot, structured = {}, result = "") {
|
|
3125
|
+
const root = String(scopedRoot || "").trim();
|
|
3126
|
+
if (!root) return null;
|
|
3127
|
+
for (const rel of workspaceImplementationArtifactCandidates(structured, result)) {
|
|
3128
|
+
let abs = "";
|
|
3129
|
+
try {
|
|
3130
|
+
abs = workspaceResolveFlowFile(root, rel, "resultFile");
|
|
3131
|
+
} catch {
|
|
3132
|
+
continue;
|
|
3133
|
+
}
|
|
3134
|
+
if (!abs || !fs.existsSync(abs) || !fs.statSync(abs).isFile()) continue;
|
|
3135
|
+
const ext = path.extname(abs).toLowerCase();
|
|
3136
|
+
const content = workspaceReadTextFileIfExists(abs, 120000);
|
|
3137
|
+
if (!content.trim()) continue;
|
|
3138
|
+
return { rel, abs, ext, content };
|
|
3139
|
+
}
|
|
3140
|
+
return null;
|
|
3141
|
+
}
|
|
3142
|
+
|
|
3143
|
+
function workspaceImplementationArtifactContext(scopedRoot, structured = {}, result = "") {
|
|
3144
|
+
const artifact = workspaceReadImplementationArtifact(scopedRoot, structured, result);
|
|
3145
|
+
if (!artifact) return "无可读取产物文件。";
|
|
3146
|
+
return [
|
|
3147
|
+
`产物路径:${artifact.rel}`,
|
|
3148
|
+
`文件类型:${artifact.ext || "(unknown)"}`,
|
|
3149
|
+
"",
|
|
3150
|
+
"产物内容:",
|
|
3151
|
+
"```",
|
|
3152
|
+
workspaceClipImplementationText(artifact.content, 50000),
|
|
3153
|
+
"```",
|
|
3154
|
+
].join("\n");
|
|
3155
|
+
}
|
|
3156
|
+
|
|
3157
|
+
function workspaceBuildImplementationPrompt(instance, nodeId, opts = {}) {
|
|
3158
|
+
const defId = String(instance?.definitionId || "").trim();
|
|
3159
|
+
const label = String(instance?.label || nodeId || "node").trim();
|
|
3160
|
+
const mode = workspaceImplementationModeForInstance(instance);
|
|
3161
|
+
const inputValues = opts.inputValues || {};
|
|
3162
|
+
const structured = opts.structured && typeof opts.structured === "object" ? opts.structured : {};
|
|
3163
|
+
const result = String(opts.resultContent || structured.result || "").trim();
|
|
3164
|
+
const task = workspaceResolveBodyPlaceholders(instance?.body || "", inputValues).trim();
|
|
3165
|
+
const scriptRef = String(instance?.scriptRef || "").trim();
|
|
3166
|
+
const inlineScript = String(instance?.script || "").trim();
|
|
3167
|
+
const previousImplementation = opts.previousImplementation
|
|
3168
|
+
? workspaceClipImplementationText(opts.previousImplementation, 12000)
|
|
3169
|
+
: "";
|
|
3170
|
+
return [
|
|
3171
|
+
"你要为 AgentFlow 的一个节点写“实现方案”Markdown。这个文件会在下次运行同一个节点前作为上下文给模型参考。",
|
|
3172
|
+
"",
|
|
3173
|
+
"要求:",
|
|
3174
|
+
"- 必须基于实际任务、输入和产物内容自己总结,不要写流水账,不要写“使用 Agent 生成输出”这类空话。",
|
|
3175
|
+
"- 写清楚这次结果到底是如何实现的:核心思路、产物结构、关键文件/路径、关键样式/函数/数据结构、可复用约定。",
|
|
3176
|
+
"- 写清楚下次如果要继续迭代,应该从哪里改、哪些约定不能破坏。",
|
|
3177
|
+
"- 如果产物是 HTML/UI,必须总结页面模块、视觉风格、关键 class/token、交互点和下游展示契约。",
|
|
3178
|
+
"- 如果产物是脚本,必须总结脚本入口、环境变量、输入输出协议和错误处理方式。",
|
|
3179
|
+
"- 只输出 Markdown 正文,不要输出代码围栏包裹整篇,不要解释你在总结。",
|
|
3180
|
+
"",
|
|
3181
|
+
"## 节点信息",
|
|
3182
|
+
"",
|
|
3183
|
+
`nodeId: ${nodeId}`,
|
|
3184
|
+
`label: ${label}`,
|
|
3185
|
+
`definitionId: ${defId || "(unknown)"}`,
|
|
3186
|
+
`mode: ${mode}`,
|
|
3187
|
+
scriptRef ? `scriptRef: ${scriptRef}` : "",
|
|
3188
|
+
inlineScript ? `inlineScript: ${workspaceClipImplementationText(inlineScript, 1200)}` : "",
|
|
3189
|
+
"",
|
|
3190
|
+
"## 当前任务",
|
|
3191
|
+
"",
|
|
3192
|
+
workspaceClipImplementationText(task || instance?.body || scriptRef || inlineScript || "(无显式任务)", 6000),
|
|
3193
|
+
"",
|
|
3194
|
+
"## 输入",
|
|
3195
|
+
"",
|
|
3196
|
+
JSON.stringify(inputValues || {}, null, 2),
|
|
3197
|
+
"",
|
|
3198
|
+
"## 输出",
|
|
3199
|
+
"",
|
|
3200
|
+
JSON.stringify({
|
|
3201
|
+
result: structured.result || result,
|
|
3202
|
+
resultFile: structured.resultFile || "",
|
|
3203
|
+
outParams: structured.outParams || {},
|
|
3204
|
+
}, null, 2),
|
|
3205
|
+
"",
|
|
3206
|
+
previousImplementation ? "## 上一版实现方案" : "",
|
|
3207
|
+
previousImplementation || "",
|
|
3208
|
+
previousImplementation ? "" : "",
|
|
3209
|
+
"## 实际产物上下文",
|
|
3210
|
+
"",
|
|
3211
|
+
workspaceImplementationArtifactContext(opts.scopedRoot, structured, result),
|
|
3212
|
+
].filter((line) => line !== "").join("\n");
|
|
3213
|
+
}
|
|
3214
|
+
|
|
3215
|
+
async function workspaceGenerateImplementationMarkdown({
|
|
3216
|
+
scopedRoot,
|
|
3217
|
+
nodeId,
|
|
3218
|
+
instance,
|
|
3219
|
+
inputValues,
|
|
3220
|
+
resultContent,
|
|
3221
|
+
structured,
|
|
3222
|
+
implementationPath,
|
|
3223
|
+
previousImplementation,
|
|
3224
|
+
runPackage,
|
|
3225
|
+
modelKey,
|
|
3226
|
+
userCtx,
|
|
3227
|
+
emit,
|
|
3228
|
+
onActiveChild,
|
|
3229
|
+
}) {
|
|
3230
|
+
const prompt = workspaceBuildImplementationPrompt(instance, nodeId, {
|
|
3231
|
+
scopedRoot,
|
|
3232
|
+
inputValues,
|
|
3233
|
+
resultContent,
|
|
3234
|
+
structured,
|
|
3235
|
+
previousImplementation,
|
|
3236
|
+
});
|
|
3237
|
+
let content = "";
|
|
3238
|
+
let lastAssistant = "";
|
|
3239
|
+
let resultText = "";
|
|
3240
|
+
emit?.({ type: "status", line: "Summarize implementation plan with model" });
|
|
3241
|
+
const handle = startComposerAgent({
|
|
3242
|
+
uiWorkspaceRoot: scopedRoot,
|
|
3243
|
+
cliWorkspace: runPackage?.nodeRunDir || scopedRoot,
|
|
3244
|
+
prompt,
|
|
3245
|
+
modelKey,
|
|
3246
|
+
agentflowUserId: userCtx?.userId || "",
|
|
3247
|
+
extraEnv: runtimeEnvForUser(userCtx, {
|
|
3248
|
+
AGENTFLOW_IMPLEMENTATION_REF: implementationPath || "",
|
|
3249
|
+
AGENTFLOW_NODE_RUN_DIR: runPackage?.nodeRunDir || "",
|
|
3250
|
+
AGENTFLOW_NODE_TMP_DIR: runPackage?.nodeTmpDir || "",
|
|
3251
|
+
AGENTFLOW_OUTPUTS_DIR: runPackage?.outputsDir || "",
|
|
3252
|
+
}),
|
|
3253
|
+
onStreamEvent: (ev) => {
|
|
3254
|
+
if (ev?.type === "natural" && ev.kind === "assistant" && typeof ev.text === "string") {
|
|
3255
|
+
lastAssistant = ev.text;
|
|
3256
|
+
content += (content ? "\n" : "") + ev.text;
|
|
3257
|
+
} else if (ev?.type === "natural" && ev.kind === "result" && typeof ev.text === "string") {
|
|
3258
|
+
resultText = ev.text;
|
|
3259
|
+
}
|
|
3260
|
+
},
|
|
3261
|
+
onToolCall: (subtype, toolName) => {
|
|
3262
|
+
const sub = subtype ? String(subtype) : "";
|
|
3263
|
+
const tool = toolName ? String(toolName) : "";
|
|
3264
|
+
emit?.({ type: "status", line: `总结方案工具 ${tool || "thinking"}${sub ? ` (${sub})` : ""}` });
|
|
3265
|
+
},
|
|
3266
|
+
});
|
|
3267
|
+
if (typeof onActiveChild === "function") onActiveChild(handle.child || null);
|
|
3268
|
+
try {
|
|
3269
|
+
await handle.finished;
|
|
3270
|
+
} finally {
|
|
3271
|
+
if (typeof onActiveChild === "function") onActiveChild(null);
|
|
3272
|
+
}
|
|
3273
|
+
const markdown = String(resultText || lastAssistant || content || "").trim();
|
|
3274
|
+
return markdown.replace(/^```(?:markdown|md)?\s*/i, "").replace(/```\s*$/i, "").trim();
|
|
3275
|
+
}
|
|
3276
|
+
|
|
3277
|
+
async function workspacePersistNodeImplementation(scopedRoot, graph, nodeId, opts = {}) {
|
|
3278
|
+
const current = graph?.instances?.[nodeId];
|
|
3279
|
+
if (!current || String(current.definitionId || "") === "workspace_run" || String(current.definitionId || "") === "workspace_scheduled_run") {
|
|
3280
|
+
return { changed: false, wrote: false, instance: current };
|
|
3281
|
+
}
|
|
3282
|
+
const existingRef = String(current.implementationRef || "").trim();
|
|
3283
|
+
const implementationRef = existingRef || workspaceDefaultImplementationRef(nodeId);
|
|
3284
|
+
const abs = workspaceResolveFlowFile(scopedRoot, implementationRef, "implementationRef");
|
|
3285
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
3286
|
+
const previousImplementation = fs.existsSync(abs) && fs.statSync(abs).isFile()
|
|
3287
|
+
? workspaceReadTextFileIfExists(abs, 60000)
|
|
3288
|
+
: "";
|
|
3289
|
+
const markdown = await workspaceGenerateImplementationMarkdown({
|
|
3290
|
+
scopedRoot,
|
|
3291
|
+
nodeId,
|
|
3292
|
+
instance: current,
|
|
3293
|
+
inputValues: opts.inputValues || {},
|
|
3294
|
+
resultContent: opts.resultContent || "",
|
|
3295
|
+
structured: opts.structured || {},
|
|
3296
|
+
implementationPath: implementationRef,
|
|
3297
|
+
previousImplementation,
|
|
3298
|
+
runPackage: opts.runPackage,
|
|
3299
|
+
modelKey: opts.modelKey || "",
|
|
3300
|
+
userCtx: opts.userCtx || {},
|
|
3301
|
+
emit: opts.emit,
|
|
3302
|
+
onActiveChild: opts.onActiveChild,
|
|
3303
|
+
});
|
|
3304
|
+
if (!markdown.trim()) throw new Error(`Implementation summary is empty for node ${nodeId}`);
|
|
3305
|
+
fs.writeFileSync(abs, markdown.trimEnd() + "\n", "utf-8");
|
|
3306
|
+
const explicitMode = String(current.implementationMode || "").trim();
|
|
3307
|
+
const next = {
|
|
3308
|
+
...current,
|
|
3309
|
+
implementationRef,
|
|
3310
|
+
...(explicitMode ? { implementationMode: explicitMode } : {}),
|
|
3311
|
+
};
|
|
3312
|
+
const changed = String(current.implementationRef || "") !== implementationRef;
|
|
3313
|
+
return { changed, wrote: true, instance: next, implementationRef };
|
|
3314
|
+
}
|
|
3315
|
+
|
|
3316
|
+
async function workspaceTryPersistNodeImplementation(scopedRoot, graph, nodeId, opts = {}) {
|
|
3317
|
+
try {
|
|
3318
|
+
return await workspacePersistNodeImplementation(scopedRoot, graph, nodeId, opts);
|
|
3319
|
+
} catch (e) {
|
|
3320
|
+
opts.emit?.({
|
|
3321
|
+
type: "natural",
|
|
3322
|
+
kind: "warning",
|
|
3323
|
+
text: `实现方案未更新:${e?.message || String(e)}`,
|
|
3324
|
+
});
|
|
3325
|
+
return { changed: false, wrote: false, instance: graph?.instances?.[nodeId] };
|
|
3326
|
+
}
|
|
3327
|
+
}
|
|
3328
|
+
|
|
2729
3329
|
function parseWorkspaceSkillKeys(raw) {
|
|
2730
3330
|
const text = String(raw || "").trim();
|
|
2731
3331
|
if (!text) return [];
|
|
@@ -2859,6 +3459,7 @@ function workspaceWriteDisplayContent(instance, content) {
|
|
|
2859
3459
|
const unwrapped = workspaceUnwrapOutputEnvelopeForDisplay(content);
|
|
2860
3460
|
const text = kind === "html" ? normalizeHtmlDisplayContent(unwrapped) : String(unwrapped || "");
|
|
2861
3461
|
const primaryName = kind === "image" ? "src" : "content";
|
|
3462
|
+
next.displayReloadKey = `${Date.now()}-${crypto.randomBytes(4).toString("hex")}`;
|
|
2862
3463
|
next.body = text;
|
|
2863
3464
|
next.input = (Array.isArray(next.input) ? next.input : []).map((slot) => (
|
|
2864
3465
|
String(slot?.name || "") === primaryName || String(slot?.type || "") === "text"
|
|
@@ -2898,7 +3499,7 @@ function workspaceUpdateDirectDisplays(graph, sourceId, content, outputs = null)
|
|
|
2898
3499
|
return updated;
|
|
2899
3500
|
}
|
|
2900
3501
|
|
|
2901
|
-
function workspaceNodePrompt(graph, nodeId, upstreamText, skillsBlock, mcpBlock = "", inputValues = {}, nodeTmpDir = "") {
|
|
3502
|
+
function workspaceNodePrompt(graph, nodeId, upstreamText, skillsBlock, mcpBlock = "", inputValues = {}, nodeTmpDir = "", implementationBlock = "") {
|
|
2902
3503
|
const instance = graph.instances[nodeId] || {};
|
|
2903
3504
|
const body = workspaceResolveBodyPlaceholders(instance.body || "", inputValues).trim();
|
|
2904
3505
|
const { values: relevantInputValues, placeholders } = workspaceRelevantInputValues(instance.body || "", inputValues);
|
|
@@ -2911,6 +3512,7 @@ function workspaceNodePrompt(graph, nodeId, upstreamText, skillsBlock, mcpBlock
|
|
|
2911
3512
|
fileBoundary ? `\n${fileBoundary}` : "",
|
|
2912
3513
|
inputBlock ? `\n${inputBlock}` : "",
|
|
2913
3514
|
placeholders.size ? "\n任务只显式引用了上面的输入槽;其它未被 `${...}` 引用的已连接业务输入不要作为分析依据。" : "",
|
|
3515
|
+
implementationBlock ? `\n${implementationBlock}` : "",
|
|
2914
3516
|
skillsBlock ? `\n## 可用能力\n\n${skillsBlock}` : "",
|
|
2915
3517
|
mcpBlock ? `\n## 可用 MCP\n\n${mcpBlock}` : "",
|
|
2916
3518
|
upstreamText ? `\n## 上游正文\n\n${upstreamText}` : "",
|
|
@@ -2919,13 +3521,38 @@ function workspaceNodePrompt(graph, nodeId, upstreamText, skillsBlock, mcpBlock
|
|
|
2919
3521
|
].filter(Boolean).join("\n");
|
|
2920
3522
|
}
|
|
2921
3523
|
|
|
2922
|
-
function
|
|
2923
|
-
return path.join(path.resolve(scopedRoot), ".workspace", "agentflow", "
|
|
3524
|
+
function workspaceDefaultGitRepoRoot(scopedRoot, _userCtx = {}) {
|
|
3525
|
+
return path.join(path.resolve(scopedRoot), ".workspace", "agentflow", "git-repos");
|
|
2924
3526
|
}
|
|
2925
3527
|
|
|
2926
|
-
function
|
|
2927
|
-
|
|
2928
|
-
|
|
3528
|
+
function workspaceDefaultWorktreePath(runTmpRoot, nodeId, repoPath, branch = "") {
|
|
3529
|
+
const repoRoot = path.resolve(repoPath);
|
|
3530
|
+
const repoName = sanitizeWorktreeName(path.basename(repoRoot));
|
|
3531
|
+
const branchName = String(branch || "").trim();
|
|
3532
|
+
let refLabel = branchName;
|
|
3533
|
+
if (!refLabel) {
|
|
3534
|
+
const currentBranch = runGit(["rev-parse", "--abbrev-ref", "HEAD"], repoRoot);
|
|
3535
|
+
if (currentBranch.status === 0 && currentBranch.stdout.trim() && currentBranch.stdout.trim() !== "HEAD") {
|
|
3536
|
+
refLabel = currentBranch.stdout.trim();
|
|
3537
|
+
}
|
|
3538
|
+
}
|
|
3539
|
+
if (!refLabel) {
|
|
3540
|
+
const currentCommit = runGit(["rev-parse", "HEAD"], repoRoot);
|
|
3541
|
+
refLabel = currentCommit.status === 0 && currentCommit.stdout.trim()
|
|
3542
|
+
? currentCommit.stdout.trim().slice(0, 12)
|
|
3543
|
+
: "HEAD";
|
|
3544
|
+
}
|
|
3545
|
+
return path.join(
|
|
3546
|
+
path.resolve(runTmpRoot),
|
|
3547
|
+
"worktrees",
|
|
3548
|
+
workspaceSanitizeTmpSegment(nodeId, "node"),
|
|
3549
|
+
repoName,
|
|
3550
|
+
sanitizeWorktreeName(refLabel),
|
|
3551
|
+
);
|
|
3552
|
+
}
|
|
3553
|
+
|
|
3554
|
+
function workspaceShouldAutoCleanupWorktree(worktreePath, hasExplicitWorktreePath) {
|
|
3555
|
+
return Boolean(worktreePath) && !hasExplicitWorktreePath;
|
|
2929
3556
|
}
|
|
2930
3557
|
|
|
2931
3558
|
function workspaceTrackAutoCleanupWorktree(list, item) {
|
|
@@ -2961,7 +3588,7 @@ function workspaceCleanupAutoWorktrees(list, graph, emit) {
|
|
|
2961
3588
|
const result = unloadGitWorktree({
|
|
2962
3589
|
repoPath: entry.repoPath,
|
|
2963
3590
|
worktreePath: entry.worktreePath,
|
|
2964
|
-
force:
|
|
3591
|
+
force: true,
|
|
2965
3592
|
prune: true,
|
|
2966
3593
|
});
|
|
2967
3594
|
emit({
|
|
@@ -3118,6 +3745,146 @@ function workspaceCleanupTmpRoot(runTmpRoot, userCtx = {}, emit = () => {}) {
|
|
|
3118
3745
|
}
|
|
3119
3746
|
}
|
|
3120
3747
|
|
|
3748
|
+
function workspaceOutputFileRefsForNode(instance) {
|
|
3749
|
+
const refs = {};
|
|
3750
|
+
const slots = Array.isArray(instance?.output) ? instance.output : [];
|
|
3751
|
+
for (let index = 0; index < slots.length; index += 1) {
|
|
3752
|
+
const slot = slots[index];
|
|
3753
|
+
const name = String(slot?.name || "").trim();
|
|
3754
|
+
const type = String(slot?.type || "");
|
|
3755
|
+
if (!name || type === "node" || name === "next" || name === "prev") continue;
|
|
3756
|
+
const key = name === "content" || index === 0 ? "result" : name;
|
|
3757
|
+
const safe = workspaceSanitizeTmpSegment(key, "result");
|
|
3758
|
+
refs[key] = `outputs/${safe}.txt`;
|
|
3759
|
+
}
|
|
3760
|
+
if (!refs.result) refs.result = "outputs/result.txt";
|
|
3761
|
+
return refs;
|
|
3762
|
+
}
|
|
3763
|
+
|
|
3764
|
+
function workspaceResolveScriptCommandText(script, values = {}) {
|
|
3765
|
+
return String(script || "").replace(/\$\{([^}]+)\}/g, (_, key) => {
|
|
3766
|
+
const name = String(key || "").trim();
|
|
3767
|
+
return workspaceShellQuote(Object.prototype.hasOwnProperty.call(values, name) ? values[name] : "");
|
|
3768
|
+
});
|
|
3769
|
+
}
|
|
3770
|
+
|
|
3771
|
+
function workspaceDefaultScriptCommand(scriptAbs) {
|
|
3772
|
+
const ext = path.extname(scriptAbs).toLowerCase();
|
|
3773
|
+
if (ext === ".js" || ext === ".mjs" || ext === ".cjs") return `node ${workspaceShellQuote(scriptAbs)}`;
|
|
3774
|
+
if (ext === ".sh" || ext === ".bash") return `bash ${workspaceShellQuote(scriptAbs)}`;
|
|
3775
|
+
if (ext === ".py") return `python3 ${workspaceShellQuote(scriptAbs)}`;
|
|
3776
|
+
return workspaceShellQuote(scriptAbs);
|
|
3777
|
+
}
|
|
3778
|
+
|
|
3779
|
+
function workspaceEnvelopeFromOutputFiles(outputRefs, nodeRunDir) {
|
|
3780
|
+
const entries = Object.entries(outputRefs || {})
|
|
3781
|
+
.map(([name, rel]) => {
|
|
3782
|
+
const abs = path.resolve(nodeRunDir, rel);
|
|
3783
|
+
const nodeRootWithSep = nodeRunDir.endsWith(path.sep) ? nodeRunDir : `${nodeRunDir}${path.sep}`;
|
|
3784
|
+
if (abs !== nodeRunDir && !abs.startsWith(nodeRootWithSep)) return null;
|
|
3785
|
+
if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) return null;
|
|
3786
|
+
return { name, rel };
|
|
3787
|
+
})
|
|
3788
|
+
.filter(Boolean);
|
|
3789
|
+
if (entries.length === 0) return "";
|
|
3790
|
+
const result = entries.find((entry) => entry.name === "result") || entries[0];
|
|
3791
|
+
const outParams = entries.filter((entry) => entry !== result);
|
|
3792
|
+
return [
|
|
3793
|
+
"---agentflow",
|
|
3794
|
+
`resultFile: ${result.rel}`,
|
|
3795
|
+
outParams.length ? "outParams:" : "",
|
|
3796
|
+
...outParams.map((entry) => ` ${entry.name}File: ${entry.rel}`),
|
|
3797
|
+
"---end",
|
|
3798
|
+
].filter(Boolean).join("\n");
|
|
3799
|
+
}
|
|
3800
|
+
|
|
3801
|
+
async function workspaceRunToolNodejsScript({
|
|
3802
|
+
scopedRoot,
|
|
3803
|
+
cwd,
|
|
3804
|
+
instance,
|
|
3805
|
+
inputValues,
|
|
3806
|
+
runPackage,
|
|
3807
|
+
userCtx,
|
|
3808
|
+
emit,
|
|
3809
|
+
}) {
|
|
3810
|
+
const scriptRef = String(instance?.scriptRef || "").trim();
|
|
3811
|
+
const scriptAbs = scriptRef ? workspaceResolveFlowFile(scopedRoot, scriptRef, "scriptRef") : "";
|
|
3812
|
+
if (scriptAbs && (!fs.existsSync(scriptAbs) || !fs.statSync(scriptAbs).isFile())) {
|
|
3813
|
+
throw new Error(`scriptRef not found: ${scriptRef}`);
|
|
3814
|
+
}
|
|
3815
|
+
const outputRefs = workspaceOutputFileRefsForNode(instance);
|
|
3816
|
+
const outputAbs = Object.fromEntries(
|
|
3817
|
+
Object.entries(outputRefs).map(([key, rel]) => [key, path.resolve(runPackage.nodeRunDir, rel)]),
|
|
3818
|
+
);
|
|
3819
|
+
for (const abs of Object.values(outputAbs)) fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
3820
|
+
const constants = {
|
|
3821
|
+
workspaceRoot: path.resolve(scopedRoot),
|
|
3822
|
+
pipelineWorkspace: path.resolve(scopedRoot),
|
|
3823
|
+
flowDir: path.resolve(scopedRoot),
|
|
3824
|
+
cwd: path.resolve(cwd || scopedRoot),
|
|
3825
|
+
nodeRunDir: runPackage.nodeRunDir,
|
|
3826
|
+
nodeTmpDir: runPackage.nodeTmpDir,
|
|
3827
|
+
outputsDir: runPackage.outputsDir,
|
|
3828
|
+
scriptRef: scriptAbs,
|
|
3829
|
+
...inputValues,
|
|
3830
|
+
...outputRefs,
|
|
3831
|
+
};
|
|
3832
|
+
const inlineScript = String(instance?.script || "").trim();
|
|
3833
|
+
const command = inlineScript
|
|
3834
|
+
? workspaceResolveScriptCommandText(inlineScript, constants)
|
|
3835
|
+
: scriptAbs
|
|
3836
|
+
? workspaceDefaultScriptCommand(scriptAbs)
|
|
3837
|
+
: "";
|
|
3838
|
+
if (!command) throw new Error("tool_nodejs requires script or scriptRef");
|
|
3839
|
+
|
|
3840
|
+
emit?.({ type: "status", line: `Run script: ${scriptRef || command.slice(0, 120)}` });
|
|
3841
|
+
|
|
3842
|
+
const env = runtimeEnvForUser(userCtx, {
|
|
3843
|
+
AGENTFLOW_WORKSPACE_ROOT: path.resolve(scopedRoot),
|
|
3844
|
+
AGENTFLOW_NODE_RUN_DIR: runPackage.nodeRunDir,
|
|
3845
|
+
AGENTFLOW_NODE_TMP_DIR: runPackage.nodeTmpDir,
|
|
3846
|
+
AGENTFLOW_OUTPUTS_DIR: runPackage.outputsDir,
|
|
3847
|
+
AGENTFLOW_SCRIPT_REF: scriptAbs,
|
|
3848
|
+
AGENTFLOW_INPUTS_JSON: JSON.stringify(inputValues || {}),
|
|
3849
|
+
AGENTFLOW_OUTPUTS_JSON: JSON.stringify(outputRefs),
|
|
3850
|
+
AGENTFLOW_OUTPUTS_ABS_JSON: JSON.stringify(outputAbs),
|
|
3851
|
+
});
|
|
3852
|
+
|
|
3853
|
+
const started = Date.now();
|
|
3854
|
+
return await new Promise((resolve, reject) => {
|
|
3855
|
+
const child = spawn(command, [], {
|
|
3856
|
+
cwd: runPackage.nodeRunDir,
|
|
3857
|
+
shell: true,
|
|
3858
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
3859
|
+
env,
|
|
3860
|
+
});
|
|
3861
|
+
let stdout = "";
|
|
3862
|
+
let stderr = "";
|
|
3863
|
+
child.stdout.setEncoding("utf-8");
|
|
3864
|
+
child.stderr.setEncoding("utf-8");
|
|
3865
|
+
child.stdout.on("data", (chunk) => {
|
|
3866
|
+
stdout += String(chunk);
|
|
3867
|
+
});
|
|
3868
|
+
child.stderr.on("data", (chunk) => {
|
|
3869
|
+
stderr += String(chunk);
|
|
3870
|
+
});
|
|
3871
|
+
child.on("error", reject);
|
|
3872
|
+
child.on("close", (code) => {
|
|
3873
|
+
if (stderr.trim()) {
|
|
3874
|
+
emit?.({ type: "natural", kind: "warning", text: `[script stderr]\n${stderr.trim().slice(-4000)}` });
|
|
3875
|
+
}
|
|
3876
|
+
if (code !== 0) {
|
|
3877
|
+
reject(new Error(`tool_nodejs script exited ${code}${stderr.trim() ? `: ${stderr.trim().slice(-800)}` : ""}`));
|
|
3878
|
+
return;
|
|
3879
|
+
}
|
|
3880
|
+
const elapsedMs = Math.max(0, Date.now() - started);
|
|
3881
|
+
emit?.({ type: "status", line: `Timing script: ${elapsedMs}ms`, timing: { label: "script", elapsedMs } });
|
|
3882
|
+
const content = stdout.trim() || workspaceEnvelopeFromOutputFiles(outputRefs, runPackage.nodeRunDir);
|
|
3883
|
+
resolve(content);
|
|
3884
|
+
});
|
|
3885
|
+
});
|
|
3886
|
+
}
|
|
3887
|
+
|
|
3121
3888
|
async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts = {}) {
|
|
3122
3889
|
const graph = normalizeWorkspaceGraphPayload(payload.graph || {});
|
|
3123
3890
|
const runNodeId = String(payload?.runNodeId || "").trim();
|
|
@@ -3190,7 +3957,7 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
3190
3957
|
const defId = String(instance.definitionId || "");
|
|
3191
3958
|
emit({ type: "node-start", nodeId, definitionId: defId });
|
|
3192
3959
|
|
|
3193
|
-
if (defId === "workspace_run") {
|
|
3960
|
+
if (defId === "workspace_run" || defId === "workspace_scheduled_run") {
|
|
3194
3961
|
continue;
|
|
3195
3962
|
}
|
|
3196
3963
|
|
|
@@ -3297,7 +4064,7 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
3297
4064
|
const targetRaw = workspaceSlotValue(workspaceSlotByName(instance, "targetDir")).trim();
|
|
3298
4065
|
const targetDir = targetRaw
|
|
3299
4066
|
? workspaceResolvePath(cwd, targetRaw)
|
|
3300
|
-
: path.join(scopedRoot,
|
|
4067
|
+
: path.join(workspaceDefaultGitRepoRoot(scopedRoot, userCtx), workspaceSanitizeRepoDirName(repoUrl));
|
|
3301
4068
|
const pullIfExists = workspaceBoolSlot(instance, "pullIfExists", true);
|
|
3302
4069
|
const includeSubmodules = workspaceBoolSlot(instance, "includeSubmodules", false);
|
|
3303
4070
|
fs.mkdirSync(path.dirname(targetDir), { recursive: true });
|
|
@@ -3369,14 +4136,16 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
3369
4136
|
const worktreeInputSlot = (Array.isArray(instance.input) ? instance.input : [])
|
|
3370
4137
|
.find((slot) => String(slot?.name || "") === "worktreePath") || null;
|
|
3371
4138
|
const rawWorktreePath = workspaceSlotValue(worktreeInputSlot || workspaceSlotByName(instance, "worktreePath")).trim();
|
|
3372
|
-
const worktreePath = rawWorktreePath
|
|
4139
|
+
const worktreePath = rawWorktreePath
|
|
4140
|
+
? workspaceResolvePath(cwd, rawWorktreePath)
|
|
4141
|
+
: (gitContext?.worktreePath ? path.resolve(gitContext.worktreePath) : workspaceDefaultWorktreePath(runTmpRoot, nodeId, repoPath, branch));
|
|
3373
4142
|
const hasExplicitWorktreePath = Boolean(rawWorktreePath) || Boolean(gitContext?.worktreePath);
|
|
3374
4143
|
const previousCwd = cwd;
|
|
3375
4144
|
const force = ["true", "1", "yes", "on"].includes(workspaceSlotValue(workspaceSlotByName(instance, "force")).trim().toLowerCase());
|
|
3376
4145
|
const pruneMissingRaw = workspaceSlotValue(workspaceSlotByName(instance, "pruneMissing")).trim().toLowerCase();
|
|
3377
4146
|
const pruneMissing = pruneMissingRaw !== "false";
|
|
3378
4147
|
const result = loadGitWorktree({ repoPath, branch, worktreePath, pipelineWorkspace: scopedRoot, force, pruneMissing });
|
|
3379
|
-
if (workspaceShouldAutoCleanupWorktree(
|
|
4148
|
+
if (workspaceShouldAutoCleanupWorktree(result.worktreePath, hasExplicitWorktreePath)) {
|
|
3380
4149
|
workspaceTrackAutoCleanupWorktree(autoCleanupWorktrees, {
|
|
3381
4150
|
nodeId,
|
|
3382
4151
|
repoPath: result.repoRoot,
|
|
@@ -3480,6 +4249,51 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
3480
4249
|
continue;
|
|
3481
4250
|
}
|
|
3482
4251
|
|
|
4252
|
+
if (defId === "tool_nodejs") {
|
|
4253
|
+
const prepareStartedAt = Date.now();
|
|
4254
|
+
const inputValues = workspaceInputValues(graph, nodeId, outputs);
|
|
4255
|
+
const runPackage = workspaceCreateNodeRunPackage(runTmpRoot, nodeId, {
|
|
4256
|
+
scopedRoot,
|
|
4257
|
+
cwd,
|
|
4258
|
+
task: String(instance.script || instance.scriptRef || instance.body || "").trim(),
|
|
4259
|
+
inputValues,
|
|
4260
|
+
});
|
|
4261
|
+
const runtimeInputValues = { ...inputValues, ...(runPackage.inputValues || {}) };
|
|
4262
|
+
emitTiming(nodeId, "prepare-script", prepareStartedAt, {
|
|
4263
|
+
inputCount: Object.keys(runtimeInputValues || {}).length,
|
|
4264
|
+
nodeRunDir: runPackage.nodeRunDir,
|
|
4265
|
+
});
|
|
4266
|
+
const content = await workspaceRunToolNodejsScript({
|
|
4267
|
+
scopedRoot,
|
|
4268
|
+
cwd,
|
|
4269
|
+
instance,
|
|
4270
|
+
inputValues: runtimeInputValues,
|
|
4271
|
+
runPackage,
|
|
4272
|
+
userCtx,
|
|
4273
|
+
emit: (event) => emit({ ...event, nodeId }),
|
|
4274
|
+
});
|
|
4275
|
+
const normalizedAgentOutput = workspacePublishAgentOutputFiles(workspaceStructuredAgentOutput(content), runPackage);
|
|
4276
|
+
const resultContent = normalizedAgentOutput.result || content;
|
|
4277
|
+
outputs.set(nodeId, resultContent);
|
|
4278
|
+
const slotUpdate = workspaceApplyAgentOutputSlots(instance, normalizedAgentOutput);
|
|
4279
|
+
if (slotUpdate.changed) graph.instances[nodeId] = slotUpdate.instance;
|
|
4280
|
+
const implementationUpdate = await workspaceTryPersistNodeImplementation(scopedRoot, graph, nodeId, {
|
|
4281
|
+
inputValues: runtimeInputValues,
|
|
4282
|
+
resultContent,
|
|
4283
|
+
structured: normalizedAgentOutput,
|
|
4284
|
+
runPackage,
|
|
4285
|
+
modelKey,
|
|
4286
|
+
userCtx,
|
|
4287
|
+
emit: (event) => emit({ ...event, nodeId }),
|
|
4288
|
+
onActiveChild: opts.onActiveChild,
|
|
4289
|
+
});
|
|
4290
|
+
if (implementationUpdate.changed) graph.instances[nodeId] = implementationUpdate.instance;
|
|
4291
|
+
const updatedDisplays = workspaceUpdateDirectDisplays(graph, nodeId, resultContent, outputs);
|
|
4292
|
+
if (slotUpdate.changed || implementationUpdate.changed || updatedDisplays.length) emit({ type: "graph", nodeId, displayNodeIds: updatedDisplays, graph });
|
|
4293
|
+
emit({ type: "node-done", nodeId, definitionId: defId });
|
|
4294
|
+
continue;
|
|
4295
|
+
}
|
|
4296
|
+
|
|
3483
4297
|
const prepareStartedAt = Date.now();
|
|
3484
4298
|
const inputValues = workspaceInputValues(graph, nodeId, outputs);
|
|
3485
4299
|
const relevantInputs = workspaceRelevantInputValues(instance.body || "", inputValues);
|
|
@@ -3506,7 +4320,8 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
3506
4320
|
} catch {
|
|
3507
4321
|
// Best-effort debug artifact only.
|
|
3508
4322
|
}
|
|
3509
|
-
const
|
|
4323
|
+
const implementationBlock = workspaceImplementationBlock(instance, scopedRoot);
|
|
4324
|
+
const prompt = workspaceNodePrompt(graph, nodeId, promptUpstreamText, promptSkillsBlock, promptMcpBlock, runtimeInputValues, runPackage, implementationBlock);
|
|
3510
4325
|
try {
|
|
3511
4326
|
fs.writeFileSync(path.join(runPackage.nodeRunDir, "prompt.md"), prompt.trimEnd() + "\n", "utf-8");
|
|
3512
4327
|
} catch {
|
|
@@ -3592,8 +4407,19 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
3592
4407
|
outputs.set(nodeId, resultContent);
|
|
3593
4408
|
const slotUpdate = workspaceApplyAgentOutputSlots(instance, normalizedAgentOutput);
|
|
3594
4409
|
if (slotUpdate.changed) graph.instances[nodeId] = slotUpdate.instance;
|
|
4410
|
+
const implementationUpdate = await workspaceTryPersistNodeImplementation(scopedRoot, graph, nodeId, {
|
|
4411
|
+
inputValues: runtimeInputValues,
|
|
4412
|
+
resultContent,
|
|
4413
|
+
structured: normalizedAgentOutput,
|
|
4414
|
+
runPackage,
|
|
4415
|
+
modelKey,
|
|
4416
|
+
userCtx,
|
|
4417
|
+
emit: (event) => emit({ ...event, nodeId }),
|
|
4418
|
+
onActiveChild: opts.onActiveChild,
|
|
4419
|
+
});
|
|
4420
|
+
if (implementationUpdate.changed) graph.instances[nodeId] = implementationUpdate.instance;
|
|
3595
4421
|
const updatedDisplays = workspaceUpdateDirectDisplays(graph, nodeId, resultContent, outputs);
|
|
3596
|
-
if (slotUpdate.changed || updatedDisplays.length) emit({ type: "graph", nodeId, displayNodeIds: updatedDisplays, graph });
|
|
4422
|
+
if (slotUpdate.changed || implementationUpdate.changed || updatedDisplays.length) emit({ type: "graph", nodeId, displayNodeIds: updatedDisplays, graph });
|
|
3597
4423
|
emit({ type: "node-done", nodeId, definitionId: defId });
|
|
3598
4424
|
}
|
|
3599
4425
|
} finally {
|
|
@@ -4112,6 +4938,46 @@ export function startUiServer({
|
|
|
4112
4938
|
}
|
|
4113
4939
|
}
|
|
4114
4940
|
|
|
4941
|
+
if (url.pathname === "/api/admin/storage-config") {
|
|
4942
|
+
if (!authUser?.isAdmin) {
|
|
4943
|
+
json(res, 403, { error: "Admin permission required" });
|
|
4944
|
+
return;
|
|
4945
|
+
}
|
|
4946
|
+
if (req.method === "GET") {
|
|
4947
|
+
json(res, 200, { config: readAdminStorageConfig() });
|
|
4948
|
+
return;
|
|
4949
|
+
}
|
|
4950
|
+
if (req.method === "POST") {
|
|
4951
|
+
let payload;
|
|
4952
|
+
try {
|
|
4953
|
+
payload = JSON.parse(await readBody(req));
|
|
4954
|
+
} catch {
|
|
4955
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
4956
|
+
return;
|
|
4957
|
+
}
|
|
4958
|
+
try {
|
|
4959
|
+
const config = writeAdminStorageConfig(payload?.config || payload || {});
|
|
4960
|
+
json(res, 200, { ok: true, config });
|
|
4961
|
+
} catch (e) {
|
|
4962
|
+
json(res, 400, { error: (e && e.message) || String(e) });
|
|
4963
|
+
}
|
|
4964
|
+
return;
|
|
4965
|
+
}
|
|
4966
|
+
}
|
|
4967
|
+
|
|
4968
|
+
if (req.method === "GET" && url.pathname === "/api/admin/usage-dashboard") {
|
|
4969
|
+
if (!authUser?.isAdmin) {
|
|
4970
|
+
json(res, 403, { error: "Admin permission required" });
|
|
4971
|
+
return;
|
|
4972
|
+
}
|
|
4973
|
+
try {
|
|
4974
|
+
json(res, 200, buildAdminUsageDashboard(root));
|
|
4975
|
+
} catch (e) {
|
|
4976
|
+
json(res, 500, { error: (e && e.message) || String(e) });
|
|
4977
|
+
}
|
|
4978
|
+
return;
|
|
4979
|
+
}
|
|
4980
|
+
|
|
4115
4981
|
if (url.pathname === "/api/admin/user-allowlist") {
|
|
4116
4982
|
if (!authUser?.isAdmin) {
|
|
4117
4983
|
json(res, 403, { error: "Admin permission required" });
|
|
@@ -4408,9 +5274,10 @@ export function startUiServer({
|
|
|
4408
5274
|
return;
|
|
4409
5275
|
}
|
|
4410
5276
|
const { path: graphPath, graph } = readWorkspaceGraph(scoped.root);
|
|
5277
|
+
const hydratedGraph = hydrateWorkspaceNodeRefsFromFiles(scoped.root, graph);
|
|
4411
5278
|
json(res, 200, {
|
|
4412
5279
|
ok: true,
|
|
4413
|
-
graph,
|
|
5280
|
+
graph: hydratedGraph,
|
|
4414
5281
|
path: graphPath,
|
|
4415
5282
|
root: scoped.root,
|
|
4416
5283
|
flowId: scoped.flowId,
|
|
@@ -4497,8 +5364,10 @@ export function startUiServer({
|
|
|
4497
5364
|
json(res, 400, { error: "Cannot write workspace graph for builtin or archived pipeline" });
|
|
4498
5365
|
return;
|
|
4499
5366
|
}
|
|
4500
|
-
const
|
|
5367
|
+
const submittedGraph = normalizeWorkspaceGraphPayload(payload.graph || payload);
|
|
4501
5368
|
const graphPath = workspaceGraphPath(scoped.root);
|
|
5369
|
+
const currentGraph = hydrateWorkspaceNodeRefsFromFiles(scoped.root, readWorkspaceGraph(scoped.root).graph);
|
|
5370
|
+
const graph = mergeWorkspacePersistentNodeRefs(submittedGraph, currentGraph);
|
|
4502
5371
|
fs.writeFileSync(graphPath, JSON.stringify(graph, null, 2) + "\n", "utf-8");
|
|
4503
5372
|
json(res, 200, { ok: true, path: graphPath, graph });
|
|
4504
5373
|
} catch (e) {
|