@fieldwangai/agentflow 0.1.67 → 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 +975 -35
- 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-BD8MsiMa.js +0 -264
- package/builtin/web-ui/dist/assets/index-BFZ6_IPB.css +0 -1
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,
|
|
@@ -97,6 +100,7 @@ import {
|
|
|
97
100
|
isAuthUserAllowed,
|
|
98
101
|
loginOrCreateUser,
|
|
99
102
|
logoutRequest,
|
|
103
|
+
readAuthUsers,
|
|
100
104
|
readUserAllowlist,
|
|
101
105
|
writeUserAllowlist,
|
|
102
106
|
} from "./auth.mjs";
|
|
@@ -105,6 +109,7 @@ import {
|
|
|
105
109
|
readAdminBuiltinPipelineConfig,
|
|
106
110
|
updateAdminBuiltinPipelineConfig,
|
|
107
111
|
} from "./admin-builtin-pipelines.mjs";
|
|
112
|
+
import { readAdminStorageConfig, writeAdminStorageConfig } from "./admin-storage-config.mjs";
|
|
108
113
|
|
|
109
114
|
const MIME = {
|
|
110
115
|
".html": "text/html; charset=utf-8",
|
|
@@ -257,8 +262,38 @@ function createFeedbackItem(payload, user) {
|
|
|
257
262
|
};
|
|
258
263
|
}
|
|
259
264
|
|
|
260
|
-
function skillCollectionsAbs(
|
|
261
|
-
return path.join(
|
|
265
|
+
function skillCollectionsAbs() {
|
|
266
|
+
return path.join(getAgentflowDataRoot(), "admin", SKILL_COLLECTIONS_FILENAME);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function legacyAdminSkillCollectionPaths() {
|
|
270
|
+
const users = readAuthUsers();
|
|
271
|
+
return Object.entries(users || {})
|
|
272
|
+
.filter(([, user]) => user?.isAdmin)
|
|
273
|
+
.map(([userId, user]) => path.join(getAgentflowUserDataRoot(user.userId || userId), SKILL_COLLECTIONS_FILENAME));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function readSkillCollectionFile(filePath) {
|
|
277
|
+
try {
|
|
278
|
+
if (!fs.existsSync(filePath)) return null;
|
|
279
|
+
const data = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
280
|
+
return data && typeof data === "object" && !Array.isArray(data) ? data : null;
|
|
281
|
+
} catch {
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function mergeSkillCollectionConfigs(configs = []) {
|
|
287
|
+
const merged = [];
|
|
288
|
+
const seen = new Set();
|
|
289
|
+
for (const config of configs) {
|
|
290
|
+
for (const collection of normalizeSkillCollectionConfig(config).collections) {
|
|
291
|
+
if (!collection.id || seen.has(collection.id)) continue;
|
|
292
|
+
seen.add(collection.id);
|
|
293
|
+
merged.push(collection);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return { version: 1, collections: merged };
|
|
262
297
|
}
|
|
263
298
|
|
|
264
299
|
function slugifySkillCollectionId(name, fallback = "collection") {
|
|
@@ -382,8 +417,12 @@ function withBuiltinSkillCollections(config, availableSkills = []) {
|
|
|
382
417
|
function readSkillCollectionConfig(userCtx = {}, availableSkills = []) {
|
|
383
418
|
const p = skillCollectionsAbs(userCtx);
|
|
384
419
|
try {
|
|
385
|
-
|
|
386
|
-
return withBuiltinSkillCollections(
|
|
420
|
+
const globalConfig = readSkillCollectionFile(p);
|
|
421
|
+
if (globalConfig) return withBuiltinSkillCollections(globalConfig, availableSkills);
|
|
422
|
+
const legacyConfigs = legacyAdminSkillCollectionPaths()
|
|
423
|
+
.map((legacyPath) => readSkillCollectionFile(legacyPath))
|
|
424
|
+
.filter(Boolean);
|
|
425
|
+
return withBuiltinSkillCollections(mergeSkillCollectionConfigs(legacyConfigs), availableSkills);
|
|
387
426
|
} catch {
|
|
388
427
|
return withBuiltinSkillCollections({}, availableSkills);
|
|
389
428
|
}
|
|
@@ -1332,31 +1371,40 @@ function shouldSkipWorkspaceFileRelPath(relPath) {
|
|
|
1332
1371
|
));
|
|
1333
1372
|
}
|
|
1334
1373
|
|
|
1374
|
+
const WORKSPACE_FILES_MAX_ITEMS = 500;
|
|
1375
|
+
|
|
1335
1376
|
function readWorkspaceFilesRecursive(dir, root, depth = 0, maxDepth = 3, budget = { count: 0 }) {
|
|
1336
|
-
if (depth > maxDepth
|
|
1377
|
+
if (depth > maxDepth) return [];
|
|
1378
|
+
if (depth > 0 && budget.count > WORKSPACE_FILES_MAX_ITEMS) return [];
|
|
1337
1379
|
let entries;
|
|
1338
1380
|
try {
|
|
1339
1381
|
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
1340
1382
|
} catch {
|
|
1341
1383
|
return [];
|
|
1342
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
|
+
});
|
|
1343
1389
|
const out = [];
|
|
1344
1390
|
for (const entry of entries) {
|
|
1345
|
-
if (budget.count >
|
|
1391
|
+
if (depth > 0 && budget.count > WORKSPACE_FILES_MAX_ITEMS) break;
|
|
1346
1392
|
if (entry.name.startsWith(".") && entry.name !== ".agents" && entry.name !== ".codex") continue;
|
|
1347
1393
|
const abs = path.join(dir, entry.name);
|
|
1348
1394
|
const rel = path.relative(root, abs).replace(/\\/g, "/");
|
|
1349
1395
|
if (shouldSkipWorkspaceFileRelPath(rel)) continue;
|
|
1350
1396
|
if (entry.isDirectory()) {
|
|
1351
1397
|
if (WORKSPACE_FILE_SKIP_DIRS.has(entry.name)) continue;
|
|
1352
|
-
budget.count++;
|
|
1353
1398
|
out.push({
|
|
1354
1399
|
type: "directory",
|
|
1355
1400
|
name: entry.name,
|
|
1356
1401
|
path: rel,
|
|
1357
1402
|
icon: workspaceFileIcon(entry.name, true),
|
|
1358
|
-
children:
|
|
1403
|
+
children: budget.count > WORKSPACE_FILES_MAX_ITEMS
|
|
1404
|
+
? []
|
|
1405
|
+
: readWorkspaceFilesRecursive(abs, root, depth + 1, maxDepth, budget),
|
|
1359
1406
|
});
|
|
1407
|
+
budget.count++;
|
|
1360
1408
|
} else if (entry.isFile()) {
|
|
1361
1409
|
if (WORKSPACE_FILE_SKIP_FILES.has(entry.name)) continue;
|
|
1362
1410
|
const ext = path.extname(entry.name).toLowerCase();
|
|
@@ -1367,10 +1415,6 @@ function readWorkspaceFilesRecursive(dir, root, depth = 0, maxDepth = 3, budget
|
|
|
1367
1415
|
out.push({ type: "file", name: entry.name, path: rel, icon: workspaceFileIcon(entry.name), size });
|
|
1368
1416
|
}
|
|
1369
1417
|
}
|
|
1370
|
-
out.sort((a, b) => {
|
|
1371
|
-
if (a.type !== b.type) return a.type === "directory" ? -1 : 1;
|
|
1372
|
-
return a.name.localeCompare(b.name);
|
|
1373
|
-
});
|
|
1374
1418
|
return out;
|
|
1375
1419
|
}
|
|
1376
1420
|
|
|
@@ -1408,6 +1452,7 @@ function readWorkspaceGraph(workspaceRoot) {
|
|
|
1408
1452
|
}
|
|
1409
1453
|
|
|
1410
1454
|
const DISPLAY_SHARE_FILENAME = "display-shares.json";
|
|
1455
|
+
const DISPLAY_SHARE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
1411
1456
|
|
|
1412
1457
|
function displaySharesPath() {
|
|
1413
1458
|
return path.join(getAgentflowDataRoot(), DISPLAY_SHARE_FILENAME);
|
|
@@ -1428,10 +1473,254 @@ function writeDisplayShares(shares) {
|
|
|
1428
1473
|
fs.writeFileSync(file, JSON.stringify(shares && typeof shares === "object" ? shares : {}, null, 2) + "\n", "utf-8");
|
|
1429
1474
|
}
|
|
1430
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
|
+
|
|
1431
1700
|
function createDisplayShareId() {
|
|
1432
1701
|
return crypto.randomBytes(12).toString("base64url");
|
|
1433
1702
|
}
|
|
1434
1703
|
|
|
1704
|
+
function displayShareExpiresAt(now = new Date()) {
|
|
1705
|
+
const time = now instanceof Date ? now.getTime() : Date.now();
|
|
1706
|
+
return new Date(time + DISPLAY_SHARE_TTL_MS).toISOString();
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
function isDisplayShareExpired(share) {
|
|
1710
|
+
const expiresAt = Date.parse(String(share?.expiresAt || ""));
|
|
1711
|
+
return Number.isFinite(expiresAt) && expiresAt <= Date.now();
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
function getDisplayShareOrExpired(id) {
|
|
1715
|
+
const shares = readDisplayShares();
|
|
1716
|
+
const share = shares[id];
|
|
1717
|
+
if (!share) return { shares, share: null, expired: false };
|
|
1718
|
+
if (!isDisplayShareExpired(share)) return { shares, share, expired: false };
|
|
1719
|
+
delete shares[id];
|
|
1720
|
+
writeDisplayShares(shares);
|
|
1721
|
+
return { shares, share: null, expired: true };
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1435
1724
|
function normalizeDisplayShareNodeIds(ids, graph) {
|
|
1436
1725
|
const out = [];
|
|
1437
1726
|
const seen = new Set();
|
|
@@ -1519,6 +1808,7 @@ function publicDisplayPayloadFromShare(root, share) {
|
|
|
1519
1808
|
: null,
|
|
1520
1809
|
createdAt: share.createdAt || "",
|
|
1521
1810
|
updatedAt: share.updatedAt || "",
|
|
1811
|
+
expiresAt: share.expiresAt || "",
|
|
1522
1812
|
},
|
|
1523
1813
|
nodes,
|
|
1524
1814
|
};
|
|
@@ -1572,6 +1862,44 @@ function mergeWorkspaceRunGraph(currentGraph, runGraph, touchedIds) {
|
|
|
1572
1862
|
};
|
|
1573
1863
|
}
|
|
1574
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
|
+
|
|
1575
1903
|
function resolveWorkspaceScopeRoot(workspaceRoot, params = {}, opts = {}) {
|
|
1576
1904
|
const flowId = params.flowId != null ? String(params.flowId).trim() : "";
|
|
1577
1905
|
if (!flowId) return { root: path.resolve(workspaceRoot), flowId: "", flowSource: "", archived: false };
|
|
@@ -2157,6 +2485,45 @@ function workspaceResolvePath(baseCwd, raw) {
|
|
|
2157
2485
|
return path.isAbsolute(text) ? path.resolve(text) : path.resolve(baseCwd, text);
|
|
2158
2486
|
}
|
|
2159
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
|
+
|
|
2160
2527
|
function workspaceSanitizeRepoDirName(repoUrl) {
|
|
2161
2528
|
const raw = String(repoUrl || "").trim().replace(/\.git$/i, "");
|
|
2162
2529
|
const last = raw.split(/[/:]/).filter(Boolean).pop() || "repo";
|
|
@@ -2443,7 +2810,7 @@ function workspaceRunPlan(graph, runNodeId, scopedRoot = "") {
|
|
|
2443
2810
|
const addNeeded = (id) => {
|
|
2444
2811
|
if (!id || needed.has(id)) return;
|
|
2445
2812
|
const defId = String(instances[id]?.definitionId || "");
|
|
2446
|
-
if (id !== target && defId === "workspace_run") {
|
|
2813
|
+
if (id !== target && (defId === "workspace_run" || defId === "workspace_scheduled_run")) {
|
|
2447
2814
|
pauseNodeIds.add(id);
|
|
2448
2815
|
return;
|
|
2449
2816
|
}
|
|
@@ -2669,6 +3036,296 @@ function workspacePromptUpstreamText(upstreamText, runPackage = {}) {
|
|
|
2669
3036
|
return upstreamText;
|
|
2670
3037
|
}
|
|
2671
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
|
+
|
|
2672
3329
|
function parseWorkspaceSkillKeys(raw) {
|
|
2673
3330
|
const text = String(raw || "").trim();
|
|
2674
3331
|
if (!text) return [];
|
|
@@ -2802,6 +3459,7 @@ function workspaceWriteDisplayContent(instance, content) {
|
|
|
2802
3459
|
const unwrapped = workspaceUnwrapOutputEnvelopeForDisplay(content);
|
|
2803
3460
|
const text = kind === "html" ? normalizeHtmlDisplayContent(unwrapped) : String(unwrapped || "");
|
|
2804
3461
|
const primaryName = kind === "image" ? "src" : "content";
|
|
3462
|
+
next.displayReloadKey = `${Date.now()}-${crypto.randomBytes(4).toString("hex")}`;
|
|
2805
3463
|
next.body = text;
|
|
2806
3464
|
next.input = (Array.isArray(next.input) ? next.input : []).map((slot) => (
|
|
2807
3465
|
String(slot?.name || "") === primaryName || String(slot?.type || "") === "text"
|
|
@@ -2841,7 +3499,7 @@ function workspaceUpdateDirectDisplays(graph, sourceId, content, outputs = null)
|
|
|
2841
3499
|
return updated;
|
|
2842
3500
|
}
|
|
2843
3501
|
|
|
2844
|
-
function workspaceNodePrompt(graph, nodeId, upstreamText, skillsBlock, mcpBlock = "", inputValues = {}, nodeTmpDir = "") {
|
|
3502
|
+
function workspaceNodePrompt(graph, nodeId, upstreamText, skillsBlock, mcpBlock = "", inputValues = {}, nodeTmpDir = "", implementationBlock = "") {
|
|
2845
3503
|
const instance = graph.instances[nodeId] || {};
|
|
2846
3504
|
const body = workspaceResolveBodyPlaceholders(instance.body || "", inputValues).trim();
|
|
2847
3505
|
const { values: relevantInputValues, placeholders } = workspaceRelevantInputValues(instance.body || "", inputValues);
|
|
@@ -2854,6 +3512,7 @@ function workspaceNodePrompt(graph, nodeId, upstreamText, skillsBlock, mcpBlock
|
|
|
2854
3512
|
fileBoundary ? `\n${fileBoundary}` : "",
|
|
2855
3513
|
inputBlock ? `\n${inputBlock}` : "",
|
|
2856
3514
|
placeholders.size ? "\n任务只显式引用了上面的输入槽;其它未被 `${...}` 引用的已连接业务输入不要作为分析依据。" : "",
|
|
3515
|
+
implementationBlock ? `\n${implementationBlock}` : "",
|
|
2857
3516
|
skillsBlock ? `\n## 可用能力\n\n${skillsBlock}` : "",
|
|
2858
3517
|
mcpBlock ? `\n## 可用 MCP\n\n${mcpBlock}` : "",
|
|
2859
3518
|
upstreamText ? `\n## 上游正文\n\n${upstreamText}` : "",
|
|
@@ -2862,13 +3521,38 @@ function workspaceNodePrompt(graph, nodeId, upstreamText, skillsBlock, mcpBlock
|
|
|
2862
3521
|
].filter(Boolean).join("\n");
|
|
2863
3522
|
}
|
|
2864
3523
|
|
|
2865
|
-
function
|
|
2866
|
-
return path.join(path.resolve(scopedRoot), ".workspace", "agentflow", "
|
|
3524
|
+
function workspaceDefaultGitRepoRoot(scopedRoot, _userCtx = {}) {
|
|
3525
|
+
return path.join(path.resolve(scopedRoot), ".workspace", "agentflow", "git-repos");
|
|
2867
3526
|
}
|
|
2868
3527
|
|
|
2869
|
-
function
|
|
2870
|
-
|
|
2871
|
-
|
|
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;
|
|
2872
3556
|
}
|
|
2873
3557
|
|
|
2874
3558
|
function workspaceTrackAutoCleanupWorktree(list, item) {
|
|
@@ -2904,7 +3588,7 @@ function workspaceCleanupAutoWorktrees(list, graph, emit) {
|
|
|
2904
3588
|
const result = unloadGitWorktree({
|
|
2905
3589
|
repoPath: entry.repoPath,
|
|
2906
3590
|
worktreePath: entry.worktreePath,
|
|
2907
|
-
force:
|
|
3591
|
+
force: true,
|
|
2908
3592
|
prune: true,
|
|
2909
3593
|
});
|
|
2910
3594
|
emit({
|
|
@@ -3061,6 +3745,146 @@ function workspaceCleanupTmpRoot(runTmpRoot, userCtx = {}, emit = () => {}) {
|
|
|
3061
3745
|
}
|
|
3062
3746
|
}
|
|
3063
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
|
+
|
|
3064
3888
|
async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts = {}) {
|
|
3065
3889
|
const graph = normalizeWorkspaceGraphPayload(payload.graph || {});
|
|
3066
3890
|
const runNodeId = String(payload?.runNodeId || "").trim();
|
|
@@ -3133,7 +3957,7 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
3133
3957
|
const defId = String(instance.definitionId || "");
|
|
3134
3958
|
emit({ type: "node-start", nodeId, definitionId: defId });
|
|
3135
3959
|
|
|
3136
|
-
if (defId === "workspace_run") {
|
|
3960
|
+
if (defId === "workspace_run" || defId === "workspace_scheduled_run") {
|
|
3137
3961
|
continue;
|
|
3138
3962
|
}
|
|
3139
3963
|
|
|
@@ -3240,7 +4064,7 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
3240
4064
|
const targetRaw = workspaceSlotValue(workspaceSlotByName(instance, "targetDir")).trim();
|
|
3241
4065
|
const targetDir = targetRaw
|
|
3242
4066
|
? workspaceResolvePath(cwd, targetRaw)
|
|
3243
|
-
: path.join(scopedRoot,
|
|
4067
|
+
: path.join(workspaceDefaultGitRepoRoot(scopedRoot, userCtx), workspaceSanitizeRepoDirName(repoUrl));
|
|
3244
4068
|
const pullIfExists = workspaceBoolSlot(instance, "pullIfExists", true);
|
|
3245
4069
|
const includeSubmodules = workspaceBoolSlot(instance, "includeSubmodules", false);
|
|
3246
4070
|
fs.mkdirSync(path.dirname(targetDir), { recursive: true });
|
|
@@ -3312,14 +4136,16 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
3312
4136
|
const worktreeInputSlot = (Array.isArray(instance.input) ? instance.input : [])
|
|
3313
4137
|
.find((slot) => String(slot?.name || "") === "worktreePath") || null;
|
|
3314
4138
|
const rawWorktreePath = workspaceSlotValue(worktreeInputSlot || workspaceSlotByName(instance, "worktreePath")).trim();
|
|
3315
|
-
const worktreePath = rawWorktreePath
|
|
4139
|
+
const worktreePath = rawWorktreePath
|
|
4140
|
+
? workspaceResolvePath(cwd, rawWorktreePath)
|
|
4141
|
+
: (gitContext?.worktreePath ? path.resolve(gitContext.worktreePath) : workspaceDefaultWorktreePath(runTmpRoot, nodeId, repoPath, branch));
|
|
3316
4142
|
const hasExplicitWorktreePath = Boolean(rawWorktreePath) || Boolean(gitContext?.worktreePath);
|
|
3317
4143
|
const previousCwd = cwd;
|
|
3318
4144
|
const force = ["true", "1", "yes", "on"].includes(workspaceSlotValue(workspaceSlotByName(instance, "force")).trim().toLowerCase());
|
|
3319
4145
|
const pruneMissingRaw = workspaceSlotValue(workspaceSlotByName(instance, "pruneMissing")).trim().toLowerCase();
|
|
3320
4146
|
const pruneMissing = pruneMissingRaw !== "false";
|
|
3321
4147
|
const result = loadGitWorktree({ repoPath, branch, worktreePath, pipelineWorkspace: scopedRoot, force, pruneMissing });
|
|
3322
|
-
if (workspaceShouldAutoCleanupWorktree(
|
|
4148
|
+
if (workspaceShouldAutoCleanupWorktree(result.worktreePath, hasExplicitWorktreePath)) {
|
|
3323
4149
|
workspaceTrackAutoCleanupWorktree(autoCleanupWorktrees, {
|
|
3324
4150
|
nodeId,
|
|
3325
4151
|
repoPath: result.repoRoot,
|
|
@@ -3423,6 +4249,51 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
3423
4249
|
continue;
|
|
3424
4250
|
}
|
|
3425
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
|
+
|
|
3426
4297
|
const prepareStartedAt = Date.now();
|
|
3427
4298
|
const inputValues = workspaceInputValues(graph, nodeId, outputs);
|
|
3428
4299
|
const relevantInputs = workspaceRelevantInputValues(instance.body || "", inputValues);
|
|
@@ -3449,7 +4320,8 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
3449
4320
|
} catch {
|
|
3450
4321
|
// Best-effort debug artifact only.
|
|
3451
4322
|
}
|
|
3452
|
-
const
|
|
4323
|
+
const implementationBlock = workspaceImplementationBlock(instance, scopedRoot);
|
|
4324
|
+
const prompt = workspaceNodePrompt(graph, nodeId, promptUpstreamText, promptSkillsBlock, promptMcpBlock, runtimeInputValues, runPackage, implementationBlock);
|
|
3453
4325
|
try {
|
|
3454
4326
|
fs.writeFileSync(path.join(runPackage.nodeRunDir, "prompt.md"), prompt.trimEnd() + "\n", "utf-8");
|
|
3455
4327
|
} catch {
|
|
@@ -3535,8 +4407,19 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
3535
4407
|
outputs.set(nodeId, resultContent);
|
|
3536
4408
|
const slotUpdate = workspaceApplyAgentOutputSlots(instance, normalizedAgentOutput);
|
|
3537
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;
|
|
3538
4421
|
const updatedDisplays = workspaceUpdateDirectDisplays(graph, nodeId, resultContent, outputs);
|
|
3539
|
-
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 });
|
|
3540
4423
|
emit({ type: "node-done", nodeId, definitionId: defId });
|
|
3541
4424
|
}
|
|
3542
4425
|
} finally {
|
|
@@ -3927,9 +4810,9 @@ export function startUiServer({
|
|
|
3927
4810
|
json(res, 400, { error: "Missing display share id" });
|
|
3928
4811
|
return;
|
|
3929
4812
|
}
|
|
3930
|
-
const share =
|
|
4813
|
+
const { share, expired } = getDisplayShareOrExpired(id);
|
|
3931
4814
|
if (!share) {
|
|
3932
|
-
json(res, 404, { error: "Display share not found" });
|
|
4815
|
+
json(res, expired ? 410 : 404, { error: expired ? "Display share has expired" : "Display share not found" });
|
|
3933
4816
|
return;
|
|
3934
4817
|
}
|
|
3935
4818
|
const payload = publicDisplayPayloadFromShare(root, share);
|
|
@@ -3951,9 +4834,9 @@ export function startUiServer({
|
|
|
3951
4834
|
json(res, 400, { error: "Missing display share id" });
|
|
3952
4835
|
return;
|
|
3953
4836
|
}
|
|
3954
|
-
const share =
|
|
4837
|
+
const { share, expired } = getDisplayShareOrExpired(id);
|
|
3955
4838
|
if (!share) {
|
|
3956
|
-
json(res, 404, { error: "Display share not found" });
|
|
4839
|
+
json(res, expired ? 410 : 404, { error: expired ? "Display share has expired" : "Display share not found" });
|
|
3957
4840
|
return;
|
|
3958
4841
|
}
|
|
3959
4842
|
const scoped = resolveWorkspaceScopeRoot(root, {
|
|
@@ -4055,6 +4938,46 @@ export function startUiServer({
|
|
|
4055
4938
|
}
|
|
4056
4939
|
}
|
|
4057
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
|
+
|
|
4058
4981
|
if (url.pathname === "/api/admin/user-allowlist") {
|
|
4059
4982
|
if (!authUser?.isAdmin) {
|
|
4060
4983
|
json(res, 403, { error: "Admin permission required" });
|
|
@@ -4351,9 +5274,10 @@ export function startUiServer({
|
|
|
4351
5274
|
return;
|
|
4352
5275
|
}
|
|
4353
5276
|
const { path: graphPath, graph } = readWorkspaceGraph(scoped.root);
|
|
5277
|
+
const hydratedGraph = hydrateWorkspaceNodeRefsFromFiles(scoped.root, graph);
|
|
4354
5278
|
json(res, 200, {
|
|
4355
5279
|
ok: true,
|
|
4356
|
-
graph,
|
|
5280
|
+
graph: hydratedGraph,
|
|
4357
5281
|
path: graphPath,
|
|
4358
5282
|
root: scoped.root,
|
|
4359
5283
|
flowId: scoped.flowId,
|
|
@@ -4394,7 +5318,8 @@ export function startUiServer({
|
|
|
4394
5318
|
const shares = readDisplayShares();
|
|
4395
5319
|
let id = createDisplayShareId();
|
|
4396
5320
|
while (shares[id]) id = createDisplayShareId();
|
|
4397
|
-
const
|
|
5321
|
+
const nowDate = new Date();
|
|
5322
|
+
const now = nowDate.toISOString();
|
|
4398
5323
|
const share = {
|
|
4399
5324
|
id,
|
|
4400
5325
|
userId: authUser.userId,
|
|
@@ -4402,10 +5327,11 @@ export function startUiServer({
|
|
|
4402
5327
|
flowSource: scoped.flowSource || "user",
|
|
4403
5328
|
archived: scoped.archived === true,
|
|
4404
5329
|
title: String(payload.title || "").trim() || "AgentFlow Display",
|
|
4405
|
-
layout: ["canvas", "gallery", "slides", "document"].includes(String(payload.layout || "")) ? String(payload.layout) : "canvas",
|
|
5330
|
+
layout: ["canvas", "gallery", "slides", "document", "single"].includes(String(payload.layout || "")) ? String(payload.layout) : "canvas",
|
|
4406
5331
|
nodeIds,
|
|
4407
5332
|
createdAt: now,
|
|
4408
5333
|
updatedAt: now,
|
|
5334
|
+
expiresAt: displayShareExpiresAt(nowDate),
|
|
4409
5335
|
};
|
|
4410
5336
|
shares[id] = share;
|
|
4411
5337
|
writeDisplayShares(shares);
|
|
@@ -4438,8 +5364,10 @@ export function startUiServer({
|
|
|
4438
5364
|
json(res, 400, { error: "Cannot write workspace graph for builtin or archived pipeline" });
|
|
4439
5365
|
return;
|
|
4440
5366
|
}
|
|
4441
|
-
const
|
|
5367
|
+
const submittedGraph = normalizeWorkspaceGraphPayload(payload.graph || payload);
|
|
4442
5368
|
const graphPath = workspaceGraphPath(scoped.root);
|
|
5369
|
+
const currentGraph = hydrateWorkspaceNodeRefsFromFiles(scoped.root, readWorkspaceGraph(scoped.root).graph);
|
|
5370
|
+
const graph = mergeWorkspacePersistentNodeRefs(submittedGraph, currentGraph);
|
|
4443
5371
|
fs.writeFileSync(graphPath, JSON.stringify(graph, null, 2) + "\n", "utf-8");
|
|
4444
5372
|
json(res, 200, { ok: true, path: graphPath, graph });
|
|
4445
5373
|
} catch (e) {
|
|
@@ -5481,6 +6409,10 @@ export function startUiServer({
|
|
|
5481
6409
|
json(res, 400, { error: "Missing skill slug or collection" });
|
|
5482
6410
|
return;
|
|
5483
6411
|
}
|
|
6412
|
+
if (payload?.collection && !authUser?.isAdmin) {
|
|
6413
|
+
json(res, 403, { error: "Admin required" });
|
|
6414
|
+
return;
|
|
6415
|
+
}
|
|
5484
6416
|
const beforeSkills = payload?.collection ? listComposerSkills(PACKAGE_ROOT, root) : [];
|
|
5485
6417
|
const result = await runSkillhub(args, { cwd: root, timeoutMs: 180_000, maxBuffer: 4 * 1024 * 1024 });
|
|
5486
6418
|
if (!result.ok) {
|
|
@@ -5510,6 +6442,10 @@ export function startUiServer({
|
|
|
5510
6442
|
json(res, 400, { error: "Missing skill slug or collection" });
|
|
5511
6443
|
return;
|
|
5512
6444
|
}
|
|
6445
|
+
if (payload?.collection && !authUser?.isAdmin) {
|
|
6446
|
+
json(res, 403, { error: "Admin required" });
|
|
6447
|
+
return;
|
|
6448
|
+
}
|
|
5513
6449
|
const result = await runSkillhub(args, { cwd: root, timeoutMs: 120_000, maxBuffer: 4 * 1024 * 1024 });
|
|
5514
6450
|
if (!result.ok) {
|
|
5515
6451
|
json(res, 500, { error: result.error, stdout: result.stdout });
|
|
@@ -6499,6 +7435,10 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
6499
7435
|
}
|
|
6500
7436
|
|
|
6501
7437
|
if (req.method === "POST" && url.pathname === "/api/skill-collections") {
|
|
7438
|
+
if (!authUser?.isAdmin) {
|
|
7439
|
+
json(res, 403, { error: "Admin required" });
|
|
7440
|
+
return;
|
|
7441
|
+
}
|
|
6502
7442
|
let payload;
|
|
6503
7443
|
try {
|
|
6504
7444
|
payload = JSON.parse(await readBody(req));
|