@fieldwangai/agentflow 0.1.69 → 0.1.71
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/lib/apply.mjs +35 -2
- package/bin/lib/catalog-flows.mjs +1 -0
- package/bin/lib/locales/en.json +8 -0
- package/bin/lib/locales/zh.json +8 -0
- package/bin/lib/marketplace.mjs +1 -0
- package/bin/lib/paths.mjs +2 -0
- package/bin/lib/run-ledger.mjs +96 -0
- package/bin/lib/ui-server.mjs +782 -47
- package/bin/lib/wecom.mjs +102 -0
- package/bin/pipeline/pre-process-node.mjs +40 -8
- package/builtin/nodes/tool_wecom_send_app_markdown.md +56 -0
- package/builtin/nodes/tool_wecom_send_group_markdown.md +44 -0
- package/builtin/web-ui/dist/assets/index-B3Wr-I3p.css +1 -0
- package/builtin/web-ui/dist/assets/index-C8JMDUZb.js +296 -0
- package/builtin/web-ui/dist/index.html +2 -2
- package/package.json +1 -1
- package/builtin/web-ui/dist/assets/index-CCvxfovU.css +0 -1
- package/builtin/web-ui/dist/assets/index-DvdPaPBL.js +0 -296
package/bin/lib/ui-server.mjs
CHANGED
|
@@ -92,6 +92,7 @@ import {
|
|
|
92
92
|
} from "./marketplace.mjs";
|
|
93
93
|
import { buildGitContext, inferGitRepoRootFromWorktree, loadGitWorktree, normalizeGitContext, runGit, sanitizeWorktreeName, unloadGitWorktree } from "./git-worktree.mjs";
|
|
94
94
|
import { createGitLabMergeRequest } from "./gitlab-mr.mjs";
|
|
95
|
+
import { sendWecomAppMarkdown, sendWecomGroupMarkdown } from "./wecom.mjs";
|
|
95
96
|
import {
|
|
96
97
|
authSetupRequired,
|
|
97
98
|
buildClearSessionCookie,
|
|
@@ -110,6 +111,11 @@ import {
|
|
|
110
111
|
updateAdminBuiltinPipelineConfig,
|
|
111
112
|
} from "./admin-builtin-pipelines.mjs";
|
|
112
113
|
import { readAdminStorageConfig, writeAdminStorageConfig } from "./admin-storage-config.mjs";
|
|
114
|
+
import {
|
|
115
|
+
appendRunLedgerEvent,
|
|
116
|
+
readRunLedgerEvents,
|
|
117
|
+
runLedgerId,
|
|
118
|
+
} from "./run-ledger.mjs";
|
|
113
119
|
|
|
114
120
|
const MIME = {
|
|
115
121
|
".html": "text/html; charset=utf-8",
|
|
@@ -1498,6 +1504,7 @@ function pipelineCountsForUser(userId) {
|
|
|
1498
1504
|
}
|
|
1499
1505
|
|
|
1500
1506
|
const USAGE_DAY_MS = 24 * 60 * 60 * 1000;
|
|
1507
|
+
const RUN_LEDGER_STALE_MS = 6 * 60 * 60 * 1000;
|
|
1501
1508
|
|
|
1502
1509
|
function startOfLocalDayMs(timeMs) {
|
|
1503
1510
|
const d = new Date(Number(timeMs) || Date.now());
|
|
@@ -1554,7 +1561,12 @@ function buildUsageDailyTrend(runs, days = 14, nowMs = Date.now()) {
|
|
|
1554
1561
|
row[bucket] += 1;
|
|
1555
1562
|
row.totalDurationMs += Math.max(0, Number(run.durationMs || 0));
|
|
1556
1563
|
row._userIds.add(String(run.userId || ""));
|
|
1557
|
-
|
|
1564
|
+
{
|
|
1565
|
+
const flowSource = String(run.flowSource || "user");
|
|
1566
|
+
const flowId = String(run.flowId || "");
|
|
1567
|
+
const userId = String(run.userId || "");
|
|
1568
|
+
row._pipelineKeys.add(flowSource === "workspace" ? `workspace:${flowId}` : `${userId}:${flowSource}:${flowId}`);
|
|
1569
|
+
}
|
|
1558
1570
|
}
|
|
1559
1571
|
return rows.map((row) => {
|
|
1560
1572
|
row.users = row._userIds.size;
|
|
@@ -1566,7 +1578,7 @@ function buildUsageDailyTrend(runs, days = 14, nowMs = Date.now()) {
|
|
|
1566
1578
|
});
|
|
1567
1579
|
}
|
|
1568
1580
|
|
|
1569
|
-
function buildUsageRates(users, runs, nowMs = Date.now()) {
|
|
1581
|
+
function buildUsageRates(users, runs, nowMs = Date.now(), workspacePipelineCount = 0) {
|
|
1570
1582
|
const windowDays = 7;
|
|
1571
1583
|
const sinceMs = startOfLocalDayMs(nowMs) - (windowDays - 1) * USAGE_DAY_MS;
|
|
1572
1584
|
const recentRuns = runs.filter((run) => Number(run?.at || 0) >= sinceMs);
|
|
@@ -1577,13 +1589,16 @@ function buildUsageRates(users, runs, nowMs = Date.now()) {
|
|
|
1577
1589
|
for (const run of recentRuns) {
|
|
1578
1590
|
const userId = String(run.userId || "");
|
|
1579
1591
|
if (userId) activeUsers.add(userId);
|
|
1580
|
-
|
|
1592
|
+
const flowSource = String(run.flowSource || "user");
|
|
1593
|
+
const flowId = String(run.flowId || "");
|
|
1594
|
+
activePipelines.add(flowSource === "workspace" ? `workspace:${flowId}` : `${userId}:${flowSource}:${flowId}`);
|
|
1581
1595
|
const bucket = runStatusBucket(run.status);
|
|
1582
1596
|
statusCounts[bucket] = (statusCounts[bucket] || 0) + 1;
|
|
1583
1597
|
recentDurationMs += Math.max(0, Number(run.durationMs || 0));
|
|
1584
1598
|
}
|
|
1585
1599
|
const totalUsers = users.length;
|
|
1586
|
-
const totalActivePipelines = users.reduce((sum, user) => sum + Math.max(0, Number(user?.pipelines?.active || 0)), 0)
|
|
1600
|
+
const totalActivePipelines = users.reduce((sum, user) => sum + Math.max(0, Number(user?.pipelines?.active || 0)), 0)
|
|
1601
|
+
+ Math.max(0, Number(workspacePipelineCount || 0));
|
|
1587
1602
|
const completedRuns = recentRuns.length - (statusCounts.running || 0);
|
|
1588
1603
|
const badRuns = (statusCounts.failed || 0) + (statusCounts.stopped || 0) + (statusCounts.interrupted || 0) + (statusCounts.unknown || 0);
|
|
1589
1604
|
return {
|
|
@@ -1603,21 +1618,196 @@ function buildUsageRates(users, runs, nowMs = Date.now()) {
|
|
|
1603
1618
|
};
|
|
1604
1619
|
}
|
|
1605
1620
|
|
|
1621
|
+
function appendWorkspaceRunStarted(record) {
|
|
1622
|
+
appendRunLedgerEvent({
|
|
1623
|
+
...record,
|
|
1624
|
+
type: "run_started",
|
|
1625
|
+
kind: "workspace",
|
|
1626
|
+
at: Number(record.startedAt || record.at || Date.now()),
|
|
1627
|
+
});
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
function appendWorkspaceRunFinished(record, status) {
|
|
1631
|
+
appendRunLedgerEvent({
|
|
1632
|
+
...record,
|
|
1633
|
+
type: "run_finished",
|
|
1634
|
+
kind: "workspace",
|
|
1635
|
+
at: Number(record.startedAt || record.at || Date.now()),
|
|
1636
|
+
endedAt: Number(record.endedAt || Date.now()),
|
|
1637
|
+
durationMs: Math.max(0, Number(record.durationMs || (Number(record.endedAt || Date.now()) - Number(record.startedAt || record.at || Date.now())))),
|
|
1638
|
+
status,
|
|
1639
|
+
});
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
function normalizeWorkspaceUsageRecord(parsed, source = "workspace-run") {
|
|
1643
|
+
const userId = String(parsed?.userId || "").trim();
|
|
1644
|
+
const flowId = String(parsed?.flowId || "").trim();
|
|
1645
|
+
const at = Number(parsed?.at || parsed?.startedAt || 0);
|
|
1646
|
+
if (!userId || !flowId || !Number.isFinite(at) || at <= 0) return null;
|
|
1647
|
+
return {
|
|
1648
|
+
userId,
|
|
1649
|
+
username: String(parsed?.username || userId),
|
|
1650
|
+
flowId,
|
|
1651
|
+
flowSource: String(parsed?.flowSource || "user"),
|
|
1652
|
+
runId: String(parsed?.runId || ""),
|
|
1653
|
+
at,
|
|
1654
|
+
endedAt: parsed?.endedAt == null ? null : Number(parsed.endedAt),
|
|
1655
|
+
durationMs: Math.max(0, Number(parsed?.durationMs || 0)),
|
|
1656
|
+
status: runStatusBucket(parsed?.status),
|
|
1657
|
+
source,
|
|
1658
|
+
};
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
function readLegacyWorkspaceRunUsageRecords() {
|
|
1662
|
+
const filePath = path.join(getAgentflowDataRoot(), "admin", "workspace-run-usage.jsonl");
|
|
1663
|
+
if (!fs.existsSync(filePath)) return [];
|
|
1664
|
+
let items = [];
|
|
1665
|
+
try {
|
|
1666
|
+
items = fs.readFileSync(filePath, "utf-8")
|
|
1667
|
+
.split(/\r?\n/)
|
|
1668
|
+
.filter((line) => line.trim())
|
|
1669
|
+
.map((line) => {
|
|
1670
|
+
try { return JSON.parse(line); } catch { return null; }
|
|
1671
|
+
})
|
|
1672
|
+
.filter(Boolean);
|
|
1673
|
+
} catch {
|
|
1674
|
+
items = [];
|
|
1675
|
+
}
|
|
1676
|
+
return items
|
|
1677
|
+
.map((item) => normalizeWorkspaceUsageRecord(item, "workspace-run-legacy"))
|
|
1678
|
+
.filter(Boolean);
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
function readWorkspaceRunLedgerRecords(options = {}) {
|
|
1682
|
+
const byRunId = new Map();
|
|
1683
|
+
for (const event of readRunLedgerEvents(options)) {
|
|
1684
|
+
if (String(event?.kind || "") !== "workspace") continue;
|
|
1685
|
+
const runId = String(event?.runId || "").trim();
|
|
1686
|
+
if (!runId) continue;
|
|
1687
|
+
const existing = byRunId.get(runId) || {};
|
|
1688
|
+
if (event.type === "run_started") {
|
|
1689
|
+
byRunId.set(runId, {
|
|
1690
|
+
...existing,
|
|
1691
|
+
...event,
|
|
1692
|
+
runId,
|
|
1693
|
+
at: Number(event.at || existing.at || Date.now()),
|
|
1694
|
+
status: existing.status || "running",
|
|
1695
|
+
});
|
|
1696
|
+
} else if (event.type === "run_finished") {
|
|
1697
|
+
byRunId.set(runId, {
|
|
1698
|
+
...existing,
|
|
1699
|
+
...event,
|
|
1700
|
+
runId,
|
|
1701
|
+
at: Number(existing.at || event.at || Date.now()),
|
|
1702
|
+
endedAt: event.endedAt == null ? null : Number(event.endedAt),
|
|
1703
|
+
durationMs: Math.max(0, Number(event.durationMs || 0)),
|
|
1704
|
+
status: runStatusBucket(event.status),
|
|
1705
|
+
});
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
const now = Date.now();
|
|
1709
|
+
return Array.from(byRunId.values())
|
|
1710
|
+
.map((item) => {
|
|
1711
|
+
const at = Number(item?.at || 0);
|
|
1712
|
+
const status = runStatusBucket(item?.status);
|
|
1713
|
+
if (status === "running" && at > 0 && now - at > RUN_LEDGER_STALE_MS) {
|
|
1714
|
+
return {
|
|
1715
|
+
...item,
|
|
1716
|
+
endedAt: Number(item?.endedAt || at + RUN_LEDGER_STALE_MS),
|
|
1717
|
+
durationMs: Math.max(0, Number(item?.durationMs || Math.min(now - at, RUN_LEDGER_STALE_MS))),
|
|
1718
|
+
status: "interrupted",
|
|
1719
|
+
};
|
|
1720
|
+
}
|
|
1721
|
+
return item;
|
|
1722
|
+
})
|
|
1723
|
+
.map((item) => normalizeWorkspaceUsageRecord(item, "workspace-run-ledger"))
|
|
1724
|
+
.filter(Boolean);
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
function readWorkspaceRunUsageRecords(options = {}) {
|
|
1728
|
+
const sinceMs = Number(options?.sinceMs || 0);
|
|
1729
|
+
return [
|
|
1730
|
+
...readLegacyWorkspaceRunUsageRecords(),
|
|
1731
|
+
...readWorkspaceRunLedgerRecords(options),
|
|
1732
|
+
].filter((run) => !Number.isFinite(sinceMs) || sinceMs <= 0 || Number(run?.at || 0) >= sinceMs);
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
function activeWorkspaceRunUsageRecords() {
|
|
1736
|
+
const out = [];
|
|
1737
|
+
for (const entry of activeWorkspaceRuns.values()) {
|
|
1738
|
+
const userId = String(entry?.userId || "").trim();
|
|
1739
|
+
const flowId = String(entry?.flowId || "").trim();
|
|
1740
|
+
const at = Number(entry?.startedAt || 0);
|
|
1741
|
+
if (!userId || !flowId || !Number.isFinite(at) || at <= 0) continue;
|
|
1742
|
+
out.push({
|
|
1743
|
+
userId,
|
|
1744
|
+
username: String(entry?.username || userId),
|
|
1745
|
+
flowId,
|
|
1746
|
+
flowSource: String(entry?.flowSource || "user"),
|
|
1747
|
+
runId: String(entry?.runId || ""),
|
|
1748
|
+
at,
|
|
1749
|
+
endedAt: null,
|
|
1750
|
+
durationMs: Math.max(0, Date.now() - at),
|
|
1751
|
+
status: "running",
|
|
1752
|
+
source: "workspace-run-active",
|
|
1753
|
+
});
|
|
1754
|
+
}
|
|
1755
|
+
return out;
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
function dedupeWorkspaceUsageRuns(runs = []) {
|
|
1759
|
+
const byKey = new Map();
|
|
1760
|
+
for (const run of runs) {
|
|
1761
|
+
const runId = String(run?.runId || "").trim();
|
|
1762
|
+
const key = runId || `${run?.userId || ""}:${run?.flowSource || ""}:${run?.flowId || ""}:${run?.at || ""}:${run?.status || ""}`;
|
|
1763
|
+
if (!key) continue;
|
|
1764
|
+
const existing = byKey.get(key);
|
|
1765
|
+
if (!existing) {
|
|
1766
|
+
byKey.set(key, run);
|
|
1767
|
+
continue;
|
|
1768
|
+
}
|
|
1769
|
+
const runScore = (item) => {
|
|
1770
|
+
if (item?.source === "workspace-run-active") return 3;
|
|
1771
|
+
if (item?.status && item.status !== "running") return 2;
|
|
1772
|
+
return 1;
|
|
1773
|
+
};
|
|
1774
|
+
if (runScore(run) >= runScore(existing)) byKey.set(key, run);
|
|
1775
|
+
}
|
|
1776
|
+
return Array.from(byKey.values());
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1606
1779
|
function buildAdminUsageDashboard(workspaceRoot) {
|
|
1607
1780
|
const authUsers = readAuthUsers();
|
|
1781
|
+
const usageSinceMs = startOfLocalDayMs(Date.now()) - 13 * USAGE_DAY_MS;
|
|
1782
|
+
const workspacePipelineCount = listFlowsJson(workspaceRoot, { includeWorkspaceFlows: true })
|
|
1783
|
+
.filter((flow) => flow?.source === "workspace" && !flow?.archived)
|
|
1784
|
+
.length;
|
|
1785
|
+
const workspaceUsageRuns = dedupeWorkspaceUsageRuns([
|
|
1786
|
+
...readWorkspaceRunUsageRecords({ sinceMs: usageSinceMs }),
|
|
1787
|
+
...activeWorkspaceRunUsageRecords(),
|
|
1788
|
+
]);
|
|
1608
1789
|
const userIds = Array.from(new Set([
|
|
1609
1790
|
...Object.keys(authUsers || {}),
|
|
1610
1791
|
...listAgentflowUserIds(),
|
|
1792
|
+
...workspaceUsageRuns.map((run) => run.userId),
|
|
1611
1793
|
].map((id) => String(id || "").trim()).filter(Boolean))).sort((a, b) => a.localeCompare(b));
|
|
1612
1794
|
const allRuns = [];
|
|
1795
|
+
const workspaceUsageByUser = new Map();
|
|
1796
|
+
for (const run of workspaceUsageRuns) {
|
|
1797
|
+
const userId = String(run.userId || "");
|
|
1798
|
+
if (!workspaceUsageByUser.has(userId)) workspaceUsageByUser.set(userId, []);
|
|
1799
|
+
workspaceUsageByUser.get(userId).push(run);
|
|
1800
|
+
}
|
|
1613
1801
|
const users = userIds.map((userId) => {
|
|
1614
1802
|
const user = authUsers[userId] || {};
|
|
1615
1803
|
const pipelineCounts = pipelineCountsForUser(userId);
|
|
1616
|
-
const
|
|
1804
|
+
const pipelineRuns = listRecentRunsFromDisk(workspaceRoot, {
|
|
1617
1805
|
userId,
|
|
1618
1806
|
includeWorkspaceRuns: false,
|
|
1619
1807
|
includeLegacyUserRuns: false,
|
|
1620
1808
|
});
|
|
1809
|
+
const runs = [...pipelineRuns, ...(workspaceUsageByUser.get(userId) || [])]
|
|
1810
|
+
.sort((a, b) => Number(b.at || 0) - Number(a.at || 0));
|
|
1621
1811
|
const statusCounts = {};
|
|
1622
1812
|
let totalDurationMs = 0;
|
|
1623
1813
|
for (const run of runs) {
|
|
@@ -1688,11 +1878,28 @@ function buildAdminUsageDashboard(workspaceRoot) {
|
|
|
1688
1878
|
totalDurationMs: 0,
|
|
1689
1879
|
});
|
|
1690
1880
|
totals.avgDurationMs = totals.runs > 0 ? Math.round(totals.totalDurationMs / totals.runs) : 0;
|
|
1881
|
+
const recentRuns = allRuns
|
|
1882
|
+
.slice()
|
|
1883
|
+
.sort((a, b) => Number(b.at || 0) - Number(a.at || 0))
|
|
1884
|
+
.slice(0, 50)
|
|
1885
|
+
.map((run) => ({
|
|
1886
|
+
userId: String(run.userId || ""),
|
|
1887
|
+
username: String(run.username || run.userId || ""),
|
|
1888
|
+
flowId: String(run.flowId || ""),
|
|
1889
|
+
flowSource: String(run.flowSource || "user"),
|
|
1890
|
+
runId: String(run.runId || ""),
|
|
1891
|
+
at: Number(run.at || 0),
|
|
1892
|
+
endedAt: run.endedAt == null ? null : Number(run.endedAt),
|
|
1893
|
+
durationMs: Math.max(0, Number(run.durationMs || 0)),
|
|
1894
|
+
status: runStatusBucket(run.status),
|
|
1895
|
+
runType: String(run.source || "").startsWith("workspace-run") ? "workspace" : "pipeline",
|
|
1896
|
+
}));
|
|
1691
1897
|
return {
|
|
1692
1898
|
generatedAt: new Date().toISOString(),
|
|
1693
1899
|
totals,
|
|
1694
|
-
usage: buildUsageRates(users, allRuns),
|
|
1900
|
+
usage: buildUsageRates(users, allRuns, Date.now(), workspacePipelineCount),
|
|
1695
1901
|
dailyTrend: buildUsageDailyTrend(allRuns, 14),
|
|
1902
|
+
recentRuns,
|
|
1696
1903
|
users,
|
|
1697
1904
|
};
|
|
1698
1905
|
}
|
|
@@ -1900,6 +2107,57 @@ function hydrateWorkspaceNodeRefsFromFiles(scopedRoot, graph) {
|
|
|
1900
2107
|
return changed ? { ...next, instances } : next;
|
|
1901
2108
|
}
|
|
1902
2109
|
|
|
2110
|
+
function workspaceMergeSlotsWithDefinitionMeta(slots, definitionSlots) {
|
|
2111
|
+
const current = Array.isArray(slots) ? slots : [];
|
|
2112
|
+
const defs = Array.isArray(definitionSlots) ? definitionSlots : [];
|
|
2113
|
+
const byName = new Map(defs.map((slot) => [String(slot?.name || ""), slot]));
|
|
2114
|
+
return current.map((slot, index) => {
|
|
2115
|
+
const def = byName.get(String(slot?.name || "")) || defs[index] || null;
|
|
2116
|
+
if (!def) return slot;
|
|
2117
|
+
const next = { ...slot };
|
|
2118
|
+
for (const key of ["type", "name", "description", "required", "showOnNode"]) {
|
|
2119
|
+
if (next[key] == null || String(next[key]).trim?.() === "") {
|
|
2120
|
+
if (def[key] != null) next[key] = def[key];
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
return next;
|
|
2124
|
+
});
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
function hydrateWorkspaceSlotMetaFromDefinitions(workspaceRoot, scoped = {}, graph = {}, userCtx = {}) {
|
|
2128
|
+
const next = normalizeWorkspaceGraphPayload(graph || {});
|
|
2129
|
+
let definitions = [];
|
|
2130
|
+
try {
|
|
2131
|
+
definitions = listNodesJson(workspaceRoot, scoped.flowId || "", scoped.flowSource || "user", {
|
|
2132
|
+
archived: scoped.archived === true,
|
|
2133
|
+
userId: userCtx?.userId || "",
|
|
2134
|
+
}).nodes || [];
|
|
2135
|
+
} catch {
|
|
2136
|
+
definitions = [];
|
|
2137
|
+
}
|
|
2138
|
+
if (!definitions.length) return next;
|
|
2139
|
+
const defById = new Map(definitions.map((def) => [String(def?.id || ""), def]));
|
|
2140
|
+
const instances = { ...(next.instances || {}) };
|
|
2141
|
+
let changed = false;
|
|
2142
|
+
for (const [nodeId, instance] of Object.entries(instances)) {
|
|
2143
|
+
if (!instance || typeof instance !== "object") continue;
|
|
2144
|
+
const def = defById.get(String(instance.definitionId || ""));
|
|
2145
|
+
if (!def) continue;
|
|
2146
|
+
const input = workspaceMergeSlotsWithDefinitionMeta(instance.input, def.inputs);
|
|
2147
|
+
const output = workspaceMergeSlotsWithDefinitionMeta(instance.output, def.outputs);
|
|
2148
|
+
if (JSON.stringify(input) !== JSON.stringify(instance.input || []) || JSON.stringify(output) !== JSON.stringify(instance.output || [])) {
|
|
2149
|
+
instances[nodeId] = { ...instance, input, output };
|
|
2150
|
+
changed = true;
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
return changed ? { ...next, instances } : next;
|
|
2154
|
+
}
|
|
2155
|
+
|
|
2156
|
+
function hydrateWorkspaceGraphForRuntime(workspaceRoot, scoped = {}, graph = {}, userCtx = {}) {
|
|
2157
|
+
const withRefs = hydrateWorkspaceNodeRefsFromFiles(scoped.root || scoped.scopedRoot || workspaceRoot, graph);
|
|
2158
|
+
return hydrateWorkspaceSlotMetaFromDefinitions(workspaceRoot, scoped, withRefs, userCtx);
|
|
2159
|
+
}
|
|
2160
|
+
|
|
1903
2161
|
function resolveWorkspaceScopeRoot(workspaceRoot, params = {}, opts = {}) {
|
|
1904
2162
|
const flowId = params.flowId != null ? String(params.flowId).trim() : "";
|
|
1905
2163
|
if (!flowId) return { root: path.resolve(workspaceRoot), flowId: "", flowSource: "", archived: false };
|
|
@@ -2084,22 +2342,47 @@ function isWorkspaceSemanticOutputSlot(slot) {
|
|
|
2084
2342
|
return type === "node" || name === "prev" || name === "next";
|
|
2085
2343
|
}
|
|
2086
2344
|
|
|
2087
|
-
function
|
|
2345
|
+
function workspaceLinkedOutputShouldStayPath(slot) {
|
|
2346
|
+
const rawName = String(slot?.name || "").trim();
|
|
2347
|
+
const name = rawName.toLowerCase();
|
|
2348
|
+
const type = String(slot?.type || "").trim().toLowerCase();
|
|
2349
|
+
if (["file", "image", "audio", "video", "binary", "directory", "dir"].includes(type)) return true;
|
|
2350
|
+
if (["file", "filepath", "file_path", "path", "url", "uri", "ref"].includes(name)) return true;
|
|
2351
|
+
return /(?:^|[_-])(file|path|url|uri|ref)$/i.test(rawName) || /(?:File|Path|Url|URL|Uri|URI|Ref)$/.test(rawName);
|
|
2352
|
+
}
|
|
2353
|
+
|
|
2354
|
+
function workspaceResolveLinkedOutputForTarget(value, targetSlot, scopedRoot = "") {
|
|
2355
|
+
const text = String(value ?? "");
|
|
2356
|
+
if (!text.trim() || workspaceLinkedOutputShouldStayPath(targetSlot)) return text;
|
|
2357
|
+
const outputRel = workspaceSafeNodeOutputRelPath(text);
|
|
2358
|
+
if (!outputRel || !String(scopedRoot || "").trim()) return text;
|
|
2359
|
+
try {
|
|
2360
|
+
const abs = workspaceResolveFlowFile(scopedRoot, outputRel, "linked output");
|
|
2361
|
+
const content = workspaceReadTextFileIfExists(abs, 120000);
|
|
2362
|
+
return content || text;
|
|
2363
|
+
} catch {
|
|
2364
|
+
return text;
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
|
|
2368
|
+
function workspaceOutputSlotValueForEdge(graph, outputs, edge, scopedRoot = "") {
|
|
2088
2369
|
const sourceId = String(edge?.source || "");
|
|
2089
2370
|
const slot = workspaceSourceSlotForEdge(graph, edge);
|
|
2090
2371
|
if (isWorkspaceSemanticOutputSlot(slot)) return "";
|
|
2372
|
+
const targetSlot = workspaceTargetSlotForEdge(graph, edge);
|
|
2373
|
+
const resolveValue = (value) => workspaceResolveLinkedOutputForTarget(value, targetSlot, scopedRoot);
|
|
2091
2374
|
const out = outputs.get(sourceId);
|
|
2092
2375
|
const sourceIndex = workspaceHandleIndex(edge?.sourceHandle, "output");
|
|
2093
2376
|
const slotName = String(slot?.name || "").trim();
|
|
2094
2377
|
const isPrimaryOutput = !slot || slotName === "result" || slotName === "content" || sourceIndex === 0;
|
|
2095
|
-
if (isPrimaryOutput && out != null && String(out).trim()) return
|
|
2378
|
+
if (isPrimaryOutput && out != null && String(out).trim()) return resolveValue(out);
|
|
2096
2379
|
if (slot && String(slot?.type || "") !== "node") {
|
|
2097
2380
|
const value = workspaceSlotValue(slot);
|
|
2098
|
-
if (value.trim()) return value;
|
|
2381
|
+
if (value.trim()) return resolveValue(value);
|
|
2099
2382
|
}
|
|
2100
|
-
if (out != null && String(out).trim()) return
|
|
2383
|
+
if (out != null && String(out).trim()) return resolveValue(out);
|
|
2101
2384
|
const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
|
|
2102
|
-
return workspaceInstanceText(instances[sourceId]);
|
|
2385
|
+
return resolveValue(workspaceInstanceText(instances[sourceId]));
|
|
2103
2386
|
}
|
|
2104
2387
|
|
|
2105
2388
|
function workspaceParseJsonObjectFromText(text) {
|
|
@@ -2725,10 +3008,47 @@ function workspaceRelevantInputValues(body, inputValues = {}) {
|
|
|
2725
3008
|
return { values, placeholders };
|
|
2726
3009
|
}
|
|
2727
3010
|
|
|
3011
|
+
function workspaceDownstreamSlotKind(slot) {
|
|
3012
|
+
const name = String(slot?.name || "").trim().toLowerCase();
|
|
3013
|
+
const type = String(slot?.type || "").trim().toLowerCase();
|
|
3014
|
+
if (type === "markdown" || name === "markdown" || name.endsWith("markdown")) return "markdown";
|
|
3015
|
+
if (type === "html" || name === "html" || name.endsWith("html")) return "html";
|
|
3016
|
+
if (type === "mermaid" || name === "mermaid" || name.endsWith("mermaid")) return "mermaid";
|
|
3017
|
+
if (type === "ascii" || name === "ascii") return "ascii";
|
|
3018
|
+
if (type === "chart" || name === "chart" || name.endsWith("chart")) return "chart";
|
|
3019
|
+
if (type === "table" || name === "table" || name.endsWith("table")) return "table";
|
|
3020
|
+
return "";
|
|
3021
|
+
}
|
|
3022
|
+
|
|
3023
|
+
function workspaceDownstreamOutputKindForField(graph, nodeId, field) {
|
|
3024
|
+
const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
|
|
3025
|
+
const edges = Array.isArray(graph?.edges) ? graph.edges : [];
|
|
3026
|
+
const source = instances[String(nodeId || "")] || {};
|
|
3027
|
+
const outputSlots = Array.isArray(source.output) ? source.output : [];
|
|
3028
|
+
for (const edge of edges) {
|
|
3029
|
+
if (String(edge?.source || "") !== String(nodeId)) continue;
|
|
3030
|
+
const sourceIndex = workspaceHandleIndex(edge?.sourceHandle, "output");
|
|
3031
|
+
const sourceSlot = outputSlots[sourceIndex] || null;
|
|
3032
|
+
if (workspaceOutputFieldForSlot(sourceSlot, sourceIndex) !== field) continue;
|
|
3033
|
+
const targetSlot = workspaceTargetSlotForEdge(graph, edge);
|
|
3034
|
+
if (!targetSlot || isWorkspaceSemanticInputSlot(targetSlot)) continue;
|
|
3035
|
+
const kind = workspaceDownstreamSlotKind(targetSlot);
|
|
3036
|
+
if (kind) return kind;
|
|
3037
|
+
}
|
|
3038
|
+
return "";
|
|
3039
|
+
}
|
|
3040
|
+
|
|
3041
|
+
function workspaceDownstreamInputDescription(target, slot) {
|
|
3042
|
+
const description = String(slot?.description || "").trim();
|
|
3043
|
+
if (description) return description;
|
|
3044
|
+
return "";
|
|
3045
|
+
}
|
|
3046
|
+
|
|
2728
3047
|
function workspaceOutputProtocolRequirements(graph, nodeId) {
|
|
2729
3048
|
const instance = graph?.instances?.[nodeId] || {};
|
|
2730
3049
|
const outputSlots = Array.isArray(instance.output) ? instance.output : [];
|
|
2731
3050
|
const displayBindings = workspaceDownstreamOutputDisplayBindings(graph, nodeId);
|
|
3051
|
+
const downstreamInputRequirements = workspaceDownstreamInputRequirements(graph, nodeId);
|
|
2732
3052
|
const displayByField = new Map();
|
|
2733
3053
|
for (const binding of displayBindings) {
|
|
2734
3054
|
if (!displayByField.has(binding.field)) displayByField.set(binding.field, binding.kind);
|
|
@@ -2740,7 +3060,7 @@ function workspaceOutputProtocolRequirements(graph, nodeId) {
|
|
|
2740
3060
|
return name && type !== "node" && name !== "next" && name !== "result" && name !== "content";
|
|
2741
3061
|
})
|
|
2742
3062
|
.map((slot) => String(slot.name).trim());
|
|
2743
|
-
const resultKind = displayByField.get("result") || "";
|
|
3063
|
+
const resultKind = displayByField.get("result") || workspaceDownstreamOutputKindForField(graph, nodeId, "result") || "";
|
|
2744
3064
|
const resultExtByKind = {
|
|
2745
3065
|
html: "html",
|
|
2746
3066
|
markdown: "md",
|
|
@@ -2776,6 +3096,7 @@ function workspaceOutputProtocolRequirements(graph, nodeId) {
|
|
|
2776
3096
|
"## 输出",
|
|
2777
3097
|
"",
|
|
2778
3098
|
`请把${resultKindText}结果写入 \`${resultFile}\`。${resultGuidance}`,
|
|
3099
|
+
downstreamInputRequirements ? `\n${downstreamInputRequirements}` : "",
|
|
2779
3100
|
...(slots.length ? [`额外输出:${slots.map((name) => `\`${name}\``).join("、")}。短值可写在 \`outParams\`,文件值写成 \`outParams.<name>File\`。`] : []),
|
|
2780
3101
|
"最终只输出下面的 agentflow envelope,不要输出解释、进度或其它文字:",
|
|
2781
3102
|
"",
|
|
@@ -2783,6 +3104,42 @@ function workspaceOutputProtocolRequirements(graph, nodeId) {
|
|
|
2783
3104
|
].join("\n");
|
|
2784
3105
|
}
|
|
2785
3106
|
|
|
3107
|
+
function workspaceDownstreamInputRequirements(graph, nodeId) {
|
|
3108
|
+
const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
|
|
3109
|
+
const edges = Array.isArray(graph?.edges) ? graph.edges : [];
|
|
3110
|
+
const source = instances[String(nodeId || "")] || {};
|
|
3111
|
+
const outputSlots = Array.isArray(source.output) ? source.output : [];
|
|
3112
|
+
const rows = [];
|
|
3113
|
+
const seen = new Set();
|
|
3114
|
+
for (const edge of edges) {
|
|
3115
|
+
if (String(edge?.source || "") !== String(nodeId)) continue;
|
|
3116
|
+
const target = instances[String(edge?.target || "")];
|
|
3117
|
+
if (!target) continue;
|
|
3118
|
+
const targetSlot = workspaceTargetSlotForEdge(graph, edge);
|
|
3119
|
+
if (!targetSlot || isWorkspaceSemanticInputSlot(targetSlot)) continue;
|
|
3120
|
+
const targetName = String(targetSlot.name || "").trim();
|
|
3121
|
+
const targetType = String(targetSlot.type || "text").trim();
|
|
3122
|
+
const description = workspaceDownstreamInputDescription(target, targetSlot);
|
|
3123
|
+
if (!targetName && !description) continue;
|
|
3124
|
+
const sourceIndex = workspaceHandleIndex(edge?.sourceHandle, "output");
|
|
3125
|
+
const sourceSlot = outputSlots[sourceIndex] || null;
|
|
3126
|
+
const sourceField = workspaceOutputFieldForSlot(sourceSlot, sourceIndex);
|
|
3127
|
+
const targetLabel = String(target.label || target.definitionId || edge.target || "").trim();
|
|
3128
|
+
const key = `${sourceField}->${edge.target}:${targetName}:${description}`;
|
|
3129
|
+
if (seen.has(key)) continue;
|
|
3130
|
+
seen.add(key);
|
|
3131
|
+
rows.push([
|
|
3132
|
+
`- 输出 \`${sourceField}\` 会连接到下游 \`${targetLabel}\` 的输入 \`${targetName || "input"}\`(type=${targetType})。`,
|
|
3133
|
+
description ? ` 要求:${description}` : "",
|
|
3134
|
+
].filter(Boolean).join("\n"));
|
|
3135
|
+
}
|
|
3136
|
+
if (!rows.length) return "";
|
|
3137
|
+
return [
|
|
3138
|
+
"下游输入格式要求:",
|
|
3139
|
+
...rows,
|
|
3140
|
+
].join("\n");
|
|
3141
|
+
}
|
|
3142
|
+
|
|
2786
3143
|
function workspaceRunPlan(graph, runNodeId, scopedRoot = "") {
|
|
2787
3144
|
const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
|
|
2788
3145
|
const edges = Array.isArray(graph?.edges) ? graph.edges : [];
|
|
@@ -2915,14 +3272,14 @@ function workspaceCachedOutputValueExists(value, scopedRoot = "") {
|
|
|
2915
3272
|
return fs.existsSync(abs) && fs.statSync(abs).isFile();
|
|
2916
3273
|
}
|
|
2917
3274
|
|
|
2918
|
-
function workspaceUpstreamText(graph, nodeId, outputs) {
|
|
3275
|
+
function workspaceUpstreamText(graph, nodeId, outputs, scopedRoot = "") {
|
|
2919
3276
|
const edges = Array.isArray(graph?.edges) ? graph.edges : [];
|
|
2920
3277
|
const incoming = edges
|
|
2921
3278
|
.filter((edge) => String(edge?.target || "") === String(nodeId))
|
|
2922
3279
|
.filter((edge) => !isWorkspaceSemanticInputSlot(workspaceTargetSlotForEdge(graph, edge)));
|
|
2923
3280
|
const contentEdge = incoming.find((edge) => String(edge?.targetHandle || "") === "input-1") || incoming[0];
|
|
2924
3281
|
if (!contentEdge) return "";
|
|
2925
|
-
return workspaceOutputSlotValueForEdge(graph, outputs, contentEdge);
|
|
3282
|
+
return workspaceOutputSlotValueForEdge(graph, outputs, contentEdge, scopedRoot);
|
|
2926
3283
|
}
|
|
2927
3284
|
|
|
2928
3285
|
function workspaceHandleIndex(handle, prefix) {
|
|
@@ -2975,7 +3332,7 @@ function workspaceNodeFileBoundaryBlock(runPackage = {}) {
|
|
|
2975
3332
|
].filter(Boolean).join("\n");
|
|
2976
3333
|
}
|
|
2977
3334
|
|
|
2978
|
-
function workspaceTaskUpstreamText(graph, nodeId, outputs, relevantInputNames = null) {
|
|
3335
|
+
function workspaceTaskUpstreamText(graph, nodeId, outputs, relevantInputNames = null, scopedRoot = "") {
|
|
2979
3336
|
const edges = Array.isArray(graph?.edges) ? graph.edges : [];
|
|
2980
3337
|
const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
|
|
2981
3338
|
const incoming = edges.filter((edge) => String(edge?.target || "") === String(nodeId));
|
|
@@ -2988,10 +3345,10 @@ function workspaceTaskUpstreamText(graph, nodeId, outputs, relevantInputNames =
|
|
|
2988
3345
|
}
|
|
2989
3346
|
const contentEdge = contentEdges.find((edge) => String(edge?.targetHandle || "") === "input-1") || contentEdges[0];
|
|
2990
3347
|
if (!contentEdge) return "";
|
|
2991
|
-
return workspaceOutputSlotValueForEdge(graph, outputs, contentEdge);
|
|
3348
|
+
return workspaceOutputSlotValueForEdge(graph, outputs, contentEdge, scopedRoot);
|
|
2992
3349
|
}
|
|
2993
3350
|
|
|
2994
|
-
function workspaceInputValues(graph, nodeId, outputs) {
|
|
3351
|
+
function workspaceInputValues(graph, nodeId, outputs, scopedRoot = "") {
|
|
2995
3352
|
const values = {};
|
|
2996
3353
|
const edges = Array.isArray(graph?.edges) ? graph.edges : [];
|
|
2997
3354
|
const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
|
|
@@ -3003,7 +3360,7 @@ function workspaceInputValues(graph, nodeId, outputs) {
|
|
|
3003
3360
|
const slot = inputSlots[index] || null;
|
|
3004
3361
|
const name = String(slot?.name || "").trim();
|
|
3005
3362
|
if (!name || isWorkspaceSemanticInputSlot(slot)) continue;
|
|
3006
|
-
const value = workspaceOutputSlotValueForEdge(graph, outputs, edge);
|
|
3363
|
+
const value = workspaceOutputSlotValueForEdge(graph, outputs, edge, scopedRoot);
|
|
3007
3364
|
if (String(value || "").trim()) values[name] = String(value);
|
|
3008
3365
|
}
|
|
3009
3366
|
for (const slot of inputSlots) {
|
|
@@ -3049,28 +3406,46 @@ function workspaceImplementationInlineText(instance) {
|
|
|
3049
3406
|
return "";
|
|
3050
3407
|
}
|
|
3051
3408
|
|
|
3052
|
-
function
|
|
3053
|
-
const parts = [];
|
|
3409
|
+
function workspaceMaterializeImplementationReference(instance, scopedRoot, runPackage = {}) {
|
|
3054
3410
|
const implementationRef = String(instance?.implementationRef || "").trim();
|
|
3055
|
-
if (implementationRef)
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3411
|
+
if (!implementationRef) return null;
|
|
3412
|
+
const abs = workspaceResolveFlowFile(scopedRoot, implementationRef, "implementationRef");
|
|
3413
|
+
const exists = fs.existsSync(abs) && fs.statSync(abs).isFile();
|
|
3414
|
+
const nodeRunDir = String(runPackage?.nodeRunDir || "").trim();
|
|
3415
|
+
let mounted = "";
|
|
3416
|
+
if (exists && nodeRunDir) {
|
|
3417
|
+
try {
|
|
3418
|
+
const mountedRel = path.join("references", "implementation.md");
|
|
3419
|
+
const dest = path.resolve(nodeRunDir, mountedRel);
|
|
3420
|
+
const nodeRunWithSep = nodeRunDir.endsWith(path.sep) ? nodeRunDir : `${nodeRunDir}${path.sep}`;
|
|
3421
|
+
if (dest === nodeRunDir || dest.startsWith(nodeRunWithSep)) {
|
|
3422
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
3423
|
+
fs.copyFileSync(abs, dest);
|
|
3424
|
+
mounted = mountedRel.split(path.sep).join(path.posix.sep);
|
|
3425
|
+
}
|
|
3426
|
+
} catch {
|
|
3427
|
+
mounted = "";
|
|
3060
3428
|
}
|
|
3061
3429
|
}
|
|
3430
|
+
return { implementationRef, exists, mounted };
|
|
3431
|
+
}
|
|
3432
|
+
|
|
3433
|
+
function workspaceImplementationBlock(instance, scopedRoot, runPackage = {}) {
|
|
3434
|
+
const ref = workspaceMaterializeImplementationReference(instance, scopedRoot, runPackage);
|
|
3062
3435
|
const inline = workspaceImplementationInlineText(instance);
|
|
3063
|
-
if (inline)
|
|
3064
|
-
if (!parts.length) return "";
|
|
3436
|
+
if (!ref && !inline) return "";
|
|
3065
3437
|
const mode = String(instance?.implementationMode || "").trim();
|
|
3066
3438
|
return [
|
|
3067
3439
|
"## 参考实现方案",
|
|
3068
3440
|
"",
|
|
3069
3441
|
mode ? `mode: ${mode}` : "",
|
|
3442
|
+
ref ? `- 实现方案文件:\`${ref.mounted || ref.implementationRef}\`` : "",
|
|
3443
|
+
ref?.mounted ? `- 原始流水线路径:\`${ref.implementationRef}\`` : "",
|
|
3444
|
+
ref && !ref.exists ? "- 当前实现方案文件不存在,本次不要依赖旧方案。" : "",
|
|
3445
|
+
inline ? "- 节点存在内联实现方案字段,但本提示不会内联其内容;如需复用,请优先参考实现方案文件。" : "",
|
|
3070
3446
|
"",
|
|
3071
|
-
|
|
3072
|
-
"",
|
|
3073
|
-
"以上方案用于加速本次执行。若发现过期,可以按当前任务修正执行,但最终结果必须以当前输入为准。",
|
|
3447
|
+
"该文件只作为可选参考,用于了解上次执行的实现路径。不要把旧方案当成硬约束;如果与当前任务、输入或输出要求冲突,以当前任务为准。",
|
|
3448
|
+
"只有在需要复用细节或确认历史约定时才读取该文件;不要在最终回复中复述参考方案内容。",
|
|
3074
3449
|
].filter((line) => line !== "").join("\n");
|
|
3075
3450
|
}
|
|
3076
3451
|
|
|
@@ -3482,7 +3857,7 @@ function workspaceUnwrapOutputEnvelopeForDisplay(content) {
|
|
|
3482
3857
|
return structured.structured ? String(structured.result || "") : raw;
|
|
3483
3858
|
}
|
|
3484
3859
|
|
|
3485
|
-
function workspaceUpdateDirectDisplays(graph, sourceId, content, outputs = null) {
|
|
3860
|
+
function workspaceUpdateDirectDisplays(graph, sourceId, content, outputs = null, scopedRoot = "") {
|
|
3486
3861
|
const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
|
|
3487
3862
|
const edges = Array.isArray(graph?.edges) ? graph.edges : [];
|
|
3488
3863
|
const updated = [];
|
|
@@ -3492,7 +3867,7 @@ function workspaceUpdateDirectDisplays(graph, sourceId, content, outputs = null)
|
|
|
3492
3867
|
const targetId = String(edge?.target || "");
|
|
3493
3868
|
const target = instances[targetId];
|
|
3494
3869
|
if (!target || !workspaceDisplayKind(target.definitionId)) continue;
|
|
3495
|
-
const value = outputs ? workspaceOutputSlotValueForEdge(graph, outputs, edge) : String(content || "");
|
|
3870
|
+
const value = outputs ? workspaceOutputSlotValueForEdge(graph, outputs, edge, scopedRoot) : String(content || "");
|
|
3496
3871
|
instances[targetId] = workspaceWriteDisplayContent(target, value || content);
|
|
3497
3872
|
updated.push(targetId);
|
|
3498
3873
|
}
|
|
@@ -3886,7 +4261,12 @@ async function workspaceRunToolNodejsScript({
|
|
|
3886
4261
|
}
|
|
3887
4262
|
|
|
3888
4263
|
async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts = {}) {
|
|
3889
|
-
const graph =
|
|
4264
|
+
const graph = hydrateWorkspaceGraphForRuntime(root, {
|
|
4265
|
+
root: scopedRoot,
|
|
4266
|
+
flowId: payload.flowId || "",
|
|
4267
|
+
flowSource: payload.flowSource || "user",
|
|
4268
|
+
archived: payload.archived === true || payload.flowArchived === true,
|
|
4269
|
+
}, payload.graph || {}, userCtx);
|
|
3890
4270
|
const runNodeId = String(payload?.runNodeId || "").trim();
|
|
3891
4271
|
const { order, pauseNodeIds } = workspaceRunPlan(graph, runNodeId, scopedRoot);
|
|
3892
4272
|
const signal = opts.signal || null;
|
|
@@ -3975,7 +4355,7 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
3975
4355
|
)),
|
|
3976
4356
|
};
|
|
3977
4357
|
outputs.set(nodeId, skillsBlock);
|
|
3978
|
-
workspaceUpdateDirectDisplays(graph, nodeId, skillsBlock, outputs);
|
|
4358
|
+
workspaceUpdateDirectDisplays(graph, nodeId, skillsBlock, outputs, scopedRoot);
|
|
3979
4359
|
emit({ type: "graph", nodeId, graph });
|
|
3980
4360
|
emit({ type: "node-done", nodeId, definitionId: defId });
|
|
3981
4361
|
continue;
|
|
@@ -3995,14 +4375,14 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
3995
4375
|
)),
|
|
3996
4376
|
};
|
|
3997
4377
|
outputs.set(nodeId, mcpBlock);
|
|
3998
|
-
workspaceUpdateDirectDisplays(graph, nodeId, mcpBlock, outputs);
|
|
4378
|
+
workspaceUpdateDirectDisplays(graph, nodeId, mcpBlock, outputs, scopedRoot);
|
|
3999
4379
|
emit({ type: "graph", nodeId, graph });
|
|
4000
4380
|
emit({ type: "node-done", nodeId, definitionId: defId });
|
|
4001
4381
|
continue;
|
|
4002
4382
|
}
|
|
4003
4383
|
|
|
4004
4384
|
if (workspaceDisplayKind(defId)) {
|
|
4005
|
-
const content = workspaceUpstreamText(graph, nodeId, outputs);
|
|
4385
|
+
const content = workspaceUpstreamText(graph, nodeId, outputs, scopedRoot);
|
|
4006
4386
|
graph.instances[nodeId] = workspaceWriteDisplayContent(instance, content);
|
|
4007
4387
|
outputs.set(nodeId, content);
|
|
4008
4388
|
emit({ type: "graph", nodeId, graph });
|
|
@@ -4038,7 +4418,7 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
4038
4418
|
}
|
|
4039
4419
|
|
|
4040
4420
|
if (defId === "control_cd_workspace") {
|
|
4041
|
-
const inputText = workspaceUpstreamText(graph, nodeId, outputs);
|
|
4421
|
+
const inputText = workspaceUpstreamText(graph, nodeId, outputs, scopedRoot);
|
|
4042
4422
|
const inputSlots = Array.isArray(instance.input) ? instance.input : [];
|
|
4043
4423
|
const pathSlot = inputSlots.find((slot) => String(slot?.name || "") === "path") ||
|
|
4044
4424
|
inputSlots.find((slot) => String(slot?.name || "") === "target");
|
|
@@ -4249,9 +4629,36 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
4249
4629
|
continue;
|
|
4250
4630
|
}
|
|
4251
4631
|
|
|
4632
|
+
if (defId === "tool_wecom_send_group_markdown" || defId === "tool_wecom_send_app_markdown") {
|
|
4633
|
+
const inputValues = workspaceInputValues(graph, nodeId, outputs, scopedRoot);
|
|
4634
|
+
const markdown = String(inputValues.markdown || inputValues.content || workspaceSlotValue(workspaceSlotByName(instance, "markdown")) || workspaceUpstreamText(graph, nodeId, outputs, scopedRoot) || "");
|
|
4635
|
+
const result = defId === "tool_wecom_send_app_markdown"
|
|
4636
|
+
? await sendWecomAppMarkdown({
|
|
4637
|
+
markdown,
|
|
4638
|
+
toUser: inputValues.toUser || workspaceSlotValue(workspaceSlotByName(instance, "toUser")),
|
|
4639
|
+
corpId: inputValues.corpId || workspaceSlotValue(workspaceSlotByName(instance, "corpId")),
|
|
4640
|
+
corpSecret: inputValues.corpSecret || workspaceSlotValue(workspaceSlotByName(instance, "corpSecret")),
|
|
4641
|
+
agentId: inputValues.agentId || workspaceSlotValue(workspaceSlotByName(instance, "agentId")),
|
|
4642
|
+
accessToken: inputValues.accessToken || workspaceSlotValue(workspaceSlotByName(instance, "accessToken")),
|
|
4643
|
+
}, runtimeEnvForUser(userCtx))
|
|
4644
|
+
: await sendWecomGroupMarkdown({
|
|
4645
|
+
markdown,
|
|
4646
|
+
webhookUrl: inputValues.webhookUrl || workspaceSlotValue(workspaceSlotByName(instance, "webhookUrl")),
|
|
4647
|
+
webhookKey: inputValues.webhookKey || workspaceSlotValue(workspaceSlotByName(instance, "webhookKey")),
|
|
4648
|
+
}, runtimeEnvForUser(userCtx));
|
|
4649
|
+
let nextInstance = workspaceSetOutputSlot(instance, "sent", "true");
|
|
4650
|
+
nextInstance = workspaceSetOutputSlot(nextInstance, "message", result.message);
|
|
4651
|
+
nextInstance = workspaceSetOutputSlot(nextInstance, "response", JSON.stringify(result.response || {}));
|
|
4652
|
+
graph.instances[nodeId] = nextInstance;
|
|
4653
|
+
outputs.set(nodeId, result.message);
|
|
4654
|
+
emit({ type: "graph", nodeId, graph });
|
|
4655
|
+
emit({ type: "node-done", nodeId, definitionId: defId });
|
|
4656
|
+
continue;
|
|
4657
|
+
}
|
|
4658
|
+
|
|
4252
4659
|
if (defId === "tool_nodejs") {
|
|
4253
4660
|
const prepareStartedAt = Date.now();
|
|
4254
|
-
const inputValues = workspaceInputValues(graph, nodeId, outputs);
|
|
4661
|
+
const inputValues = workspaceInputValues(graph, nodeId, outputs, scopedRoot);
|
|
4255
4662
|
const runPackage = workspaceCreateNodeRunPackage(runTmpRoot, nodeId, {
|
|
4256
4663
|
scopedRoot,
|
|
4257
4664
|
cwd,
|
|
@@ -4288,16 +4695,16 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
4288
4695
|
onActiveChild: opts.onActiveChild,
|
|
4289
4696
|
});
|
|
4290
4697
|
if (implementationUpdate.changed) graph.instances[nodeId] = implementationUpdate.instance;
|
|
4291
|
-
const updatedDisplays = workspaceUpdateDirectDisplays(graph, nodeId, resultContent, outputs);
|
|
4698
|
+
const updatedDisplays = workspaceUpdateDirectDisplays(graph, nodeId, resultContent, outputs, scopedRoot);
|
|
4292
4699
|
if (slotUpdate.changed || implementationUpdate.changed || updatedDisplays.length) emit({ type: "graph", nodeId, displayNodeIds: updatedDisplays, graph });
|
|
4293
4700
|
emit({ type: "node-done", nodeId, definitionId: defId });
|
|
4294
4701
|
continue;
|
|
4295
4702
|
}
|
|
4296
4703
|
|
|
4297
4704
|
const prepareStartedAt = Date.now();
|
|
4298
|
-
const inputValues = workspaceInputValues(graph, nodeId, outputs);
|
|
4705
|
+
const inputValues = workspaceInputValues(graph, nodeId, outputs, scopedRoot);
|
|
4299
4706
|
const relevantInputs = workspaceRelevantInputValues(instance.body || "", inputValues);
|
|
4300
|
-
const upstreamText = workspaceTaskUpstreamText(graph, nodeId, outputs, relevantInputs.placeholders);
|
|
4707
|
+
const upstreamText = workspaceTaskUpstreamText(graph, nodeId, outputs, relevantInputs.placeholders, scopedRoot);
|
|
4301
4708
|
const upstreamSkillBlocks = workspaceUpstreamSkillBlocks(graph, nodeId, outputs);
|
|
4302
4709
|
const promptSkillsBlock = mergeWorkspaceSkillBlocks(upstreamSkillBlocks);
|
|
4303
4710
|
const promptMcpBlock = workspaceUpstreamMcpBlocks(graph, nodeId, outputs);
|
|
@@ -4320,7 +4727,7 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
4320
4727
|
} catch {
|
|
4321
4728
|
// Best-effort debug artifact only.
|
|
4322
4729
|
}
|
|
4323
|
-
const implementationBlock = workspaceImplementationBlock(instance, scopedRoot);
|
|
4730
|
+
const implementationBlock = workspaceImplementationBlock(instance, scopedRoot, runPackage);
|
|
4324
4731
|
const prompt = workspaceNodePrompt(graph, nodeId, promptUpstreamText, promptSkillsBlock, promptMcpBlock, runtimeInputValues, runPackage, implementationBlock);
|
|
4325
4732
|
try {
|
|
4326
4733
|
fs.writeFileSync(path.join(runPackage.nodeRunDir, "prompt.md"), prompt.trimEnd() + "\n", "utf-8");
|
|
@@ -4418,7 +4825,7 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
4418
4825
|
onActiveChild: opts.onActiveChild,
|
|
4419
4826
|
});
|
|
4420
4827
|
if (implementationUpdate.changed) graph.instances[nodeId] = implementationUpdate.instance;
|
|
4421
|
-
const updatedDisplays = workspaceUpdateDirectDisplays(graph, nodeId, resultContent, outputs);
|
|
4828
|
+
const updatedDisplays = workspaceUpdateDirectDisplays(graph, nodeId, resultContent, outputs, scopedRoot);
|
|
4422
4829
|
if (slotUpdate.changed || implementationUpdate.changed || updatedDisplays.length) emit({ type: "graph", nodeId, displayNodeIds: updatedDisplays, graph });
|
|
4423
4830
|
emit({ type: "node-done", nodeId, definitionId: defId });
|
|
4424
4831
|
}
|
|
@@ -4615,11 +5022,267 @@ function broadcastFlowEditorSync(flowId, flowSource, flowArchived = false, userI
|
|
|
4615
5022
|
const activeFlowRuns = new Map();
|
|
4616
5023
|
/** 正在执行的 Workspace 临时 run(flowId → { controller, child, runNodeId, startedAt });同一 flow 只允许一个 run */
|
|
4617
5024
|
const activeWorkspaceRuns = new Map();
|
|
5025
|
+
const WORKSPACE_SCHEDULES_FILENAME = "workspace-schedules.json";
|
|
5026
|
+
const WORKSPACE_SCHEDULE_POLL_MS = 30_000;
|
|
4618
5027
|
|
|
4619
5028
|
function workspaceRunKey(userCtx, flowSource, flowId) {
|
|
4620
5029
|
return `${userCtx?.userId || ""}:${flowSource || "user"}:${flowId}`;
|
|
4621
5030
|
}
|
|
4622
5031
|
|
|
5032
|
+
function normalizeWorkspaceScheduledRunConfig(raw) {
|
|
5033
|
+
let parsed = {};
|
|
5034
|
+
const text = String(raw || "").trim();
|
|
5035
|
+
if (text) {
|
|
5036
|
+
try {
|
|
5037
|
+
parsed = JSON.parse(text);
|
|
5038
|
+
} catch {
|
|
5039
|
+
parsed = {};
|
|
5040
|
+
}
|
|
5041
|
+
}
|
|
5042
|
+
const intervalMinutes = Number(parsed.intervalMinutes);
|
|
5043
|
+
return {
|
|
5044
|
+
enabled: parsed.enabled === true,
|
|
5045
|
+
intervalMinutes: Number.isFinite(intervalMinutes) && intervalMinutes > 0
|
|
5046
|
+
? Math.min(Math.max(Math.round(intervalMinutes), 1), 1440)
|
|
5047
|
+
: 60,
|
|
5048
|
+
};
|
|
5049
|
+
}
|
|
5050
|
+
|
|
5051
|
+
function workspaceSchedulesPath() {
|
|
5052
|
+
return path.join(getAgentflowDataRoot(), WORKSPACE_SCHEDULES_FILENAME);
|
|
5053
|
+
}
|
|
5054
|
+
|
|
5055
|
+
function readWorkspaceScheduleRegistry() {
|
|
5056
|
+
const filePath = workspaceSchedulesPath();
|
|
5057
|
+
if (!fs.existsSync(filePath)) return { version: 1, schedules: {} };
|
|
5058
|
+
try {
|
|
5059
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
5060
|
+
return {
|
|
5061
|
+
version: 1,
|
|
5062
|
+
schedules: parsed?.schedules && typeof parsed.schedules === "object" && !Array.isArray(parsed.schedules)
|
|
5063
|
+
? parsed.schedules
|
|
5064
|
+
: {},
|
|
5065
|
+
};
|
|
5066
|
+
} catch {
|
|
5067
|
+
return { version: 1, schedules: {} };
|
|
5068
|
+
}
|
|
5069
|
+
}
|
|
5070
|
+
|
|
5071
|
+
function writeWorkspaceScheduleRegistry(registry) {
|
|
5072
|
+
const filePath = workspaceSchedulesPath();
|
|
5073
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
5074
|
+
fs.writeFileSync(filePath, JSON.stringify({
|
|
5075
|
+
version: 1,
|
|
5076
|
+
updatedAt: new Date().toISOString(),
|
|
5077
|
+
schedules: registry?.schedules && typeof registry.schedules === "object" ? registry.schedules : {},
|
|
5078
|
+
}, null, 2) + "\n", "utf-8");
|
|
5079
|
+
}
|
|
5080
|
+
|
|
5081
|
+
function workspaceScheduleKey(userId, flowSource, flowId, runNodeId) {
|
|
5082
|
+
return [
|
|
5083
|
+
String(userId || ""),
|
|
5084
|
+
String(flowSource || "user"),
|
|
5085
|
+
String(flowId || ""),
|
|
5086
|
+
String(runNodeId || ""),
|
|
5087
|
+
].join(":");
|
|
5088
|
+
}
|
|
5089
|
+
|
|
5090
|
+
function listWorkspaceScheduleStatusesForFlow(userCtx = {}, flowSource = "user", flowId = "") {
|
|
5091
|
+
const registry = readWorkspaceScheduleRegistry();
|
|
5092
|
+
const userId = String(userCtx.userId || "");
|
|
5093
|
+
return Object.values(registry.schedules || {})
|
|
5094
|
+
.filter((entry) => (
|
|
5095
|
+
String(entry?.userId || "") === userId &&
|
|
5096
|
+
String(entry?.flowSource || "user") === String(flowSource || "user") &&
|
|
5097
|
+
String(entry?.flowId || "") === String(flowId || "")
|
|
5098
|
+
))
|
|
5099
|
+
.sort((a, b) => String(a.runNodeId || "").localeCompare(String(b.runNodeId || "")));
|
|
5100
|
+
}
|
|
5101
|
+
|
|
5102
|
+
function syncWorkspaceSchedulesForGraph(root, scoped, graph, authUser, userCtx = {}) {
|
|
5103
|
+
const flowId = String(scoped?.flowId || "").trim();
|
|
5104
|
+
const flowSource = String(scoped?.flowSource || "user");
|
|
5105
|
+
const userId = String(userCtx.userId || authUser?.userId || "");
|
|
5106
|
+
if (!flowId || !userId) return [];
|
|
5107
|
+
const now = Date.now();
|
|
5108
|
+
const nowIso = new Date(now).toISOString();
|
|
5109
|
+
const registry = readWorkspaceScheduleRegistry();
|
|
5110
|
+
const schedules = { ...(registry.schedules || {}) };
|
|
5111
|
+
const prefix = `${userId}:${flowSource}:${flowId}:`;
|
|
5112
|
+
for (const key of Object.keys(schedules)) {
|
|
5113
|
+
if (key.startsWith(prefix)) delete schedules[key];
|
|
5114
|
+
}
|
|
5115
|
+
const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
|
|
5116
|
+
for (const [runNodeId, instance] of Object.entries(instances)) {
|
|
5117
|
+
if (String(instance?.definitionId || "") !== "workspace_scheduled_run") continue;
|
|
5118
|
+
const config = normalizeWorkspaceScheduledRunConfig(instance.body || "");
|
|
5119
|
+
if (!config.enabled) continue;
|
|
5120
|
+
const key = workspaceScheduleKey(userId, flowSource, flowId, runNodeId);
|
|
5121
|
+
const intervalMs = config.intervalMinutes * 60 * 1000;
|
|
5122
|
+
const previous = registry.schedules?.[key] && typeof registry.schedules[key] === "object" ? registry.schedules[key] : {};
|
|
5123
|
+
const previousNext = Number(previous.nextRunAt || 0);
|
|
5124
|
+
schedules[key] = {
|
|
5125
|
+
...previous,
|
|
5126
|
+
key,
|
|
5127
|
+
enabled: true,
|
|
5128
|
+
userId,
|
|
5129
|
+
username: String(authUser?.username || previous.username || userId),
|
|
5130
|
+
flowId,
|
|
5131
|
+
flowSource,
|
|
5132
|
+
runNodeId,
|
|
5133
|
+
label: String(instance.label || "Scheduled Run"),
|
|
5134
|
+
intervalMinutes: config.intervalMinutes,
|
|
5135
|
+
nextRunAt: Number.isFinite(previousNext) && previousNext > now ? previousNext : now + intervalMs,
|
|
5136
|
+
lastStatus: previous.lastStatus || "armed",
|
|
5137
|
+
updatedAt: nowIso,
|
|
5138
|
+
};
|
|
5139
|
+
}
|
|
5140
|
+
const nextRegistry = { version: 1, schedules };
|
|
5141
|
+
writeWorkspaceScheduleRegistry(nextRegistry);
|
|
5142
|
+
return listWorkspaceScheduleStatusesForFlow(userCtx, flowSource, flowId);
|
|
5143
|
+
}
|
|
5144
|
+
|
|
5145
|
+
function updateWorkspaceScheduleEntry(key, patch) {
|
|
5146
|
+
const registry = readWorkspaceScheduleRegistry();
|
|
5147
|
+
const current = registry.schedules?.[key];
|
|
5148
|
+
if (!current) return null;
|
|
5149
|
+
const next = {
|
|
5150
|
+
...current,
|
|
5151
|
+
...(patch && typeof patch === "object" ? patch : {}),
|
|
5152
|
+
updatedAt: new Date().toISOString(),
|
|
5153
|
+
};
|
|
5154
|
+
registry.schedules[key] = next;
|
|
5155
|
+
writeWorkspaceScheduleRegistry(registry);
|
|
5156
|
+
return next;
|
|
5157
|
+
}
|
|
5158
|
+
|
|
5159
|
+
async function runWorkspaceScheduledEntry(root, entry) {
|
|
5160
|
+
const userCtx = { userId: String(entry.userId || "") };
|
|
5161
|
+
const runKey = workspaceRunKey(userCtx, entry.flowSource || "user", entry.flowId || "");
|
|
5162
|
+
const intervalMs = Math.max(1, Number(entry.intervalMinutes || 60)) * 60 * 1000;
|
|
5163
|
+
const nextRunAt = Date.now() + intervalMs;
|
|
5164
|
+
if (activeWorkspaceRuns.has(runKey)) {
|
|
5165
|
+
updateWorkspaceScheduleEntry(entry.key, {
|
|
5166
|
+
nextRunAt,
|
|
5167
|
+
lastSkippedAt: Date.now(),
|
|
5168
|
+
lastStatus: "skipped: busy",
|
|
5169
|
+
});
|
|
5170
|
+
return;
|
|
5171
|
+
}
|
|
5172
|
+
const scoped = resolveWorkspaceScopeRoot(root, {
|
|
5173
|
+
flowId: entry.flowId || "",
|
|
5174
|
+
flowSource: entry.flowSource || "user",
|
|
5175
|
+
}, userCtx);
|
|
5176
|
+
if (scoped.error || scoped.archived || isReadonlyBuiltinFlowSource(scoped.flowSource)) {
|
|
5177
|
+
updateWorkspaceScheduleEntry(entry.key, {
|
|
5178
|
+
nextRunAt,
|
|
5179
|
+
lastStatus: "error",
|
|
5180
|
+
lastError: scoped.error || "Workspace schedule target is not writable",
|
|
5181
|
+
lastErrorAt: Date.now(),
|
|
5182
|
+
});
|
|
5183
|
+
return;
|
|
5184
|
+
}
|
|
5185
|
+
const graphPath = workspaceGraphPath(scoped.root);
|
|
5186
|
+
const graph = hydrateWorkspaceGraphForRuntime(root, scoped, readWorkspaceGraph(scoped.root).graph, userCtx);
|
|
5187
|
+
const instance = graph.instances?.[entry.runNodeId];
|
|
5188
|
+
const config = normalizeWorkspaceScheduledRunConfig(instance?.body || "");
|
|
5189
|
+
if (!instance || String(instance.definitionId || "") !== "workspace_scheduled_run" || !config.enabled) {
|
|
5190
|
+
updateWorkspaceScheduleEntry(entry.key, {
|
|
5191
|
+
enabled: false,
|
|
5192
|
+
nextRunAt: null,
|
|
5193
|
+
lastStatus: "disabled",
|
|
5194
|
+
});
|
|
5195
|
+
return;
|
|
5196
|
+
}
|
|
5197
|
+
|
|
5198
|
+
const controller = new AbortController();
|
|
5199
|
+
const authUsers = readAuthUsers();
|
|
5200
|
+
const authUser = authUsers[userCtx.userId] || {};
|
|
5201
|
+
const runEntry = {
|
|
5202
|
+
controller,
|
|
5203
|
+
child: null,
|
|
5204
|
+
runId: runLedgerId("workspace"),
|
|
5205
|
+
userId: userCtx.userId,
|
|
5206
|
+
username: String(authUser.username || entry.username || userCtx.userId),
|
|
5207
|
+
runNodeId: String(entry.runNodeId || ""),
|
|
5208
|
+
flowId: String(entry.flowId || ""),
|
|
5209
|
+
flowSource: String(entry.flowSource || "user"),
|
|
5210
|
+
startedAt: Date.now(),
|
|
5211
|
+
scheduled: true,
|
|
5212
|
+
stopChild() {
|
|
5213
|
+
if (this.child && !this.child.killed) {
|
|
5214
|
+
try { this.child.kill("SIGTERM"); } catch (_) {}
|
|
5215
|
+
}
|
|
5216
|
+
},
|
|
5217
|
+
};
|
|
5218
|
+
activeWorkspaceRuns.set(runKey, runEntry);
|
|
5219
|
+
appendWorkspaceRunStarted(runEntry);
|
|
5220
|
+
updateWorkspaceScheduleEntry(entry.key, {
|
|
5221
|
+
lastStatus: "running",
|
|
5222
|
+
lastTriggeredAt: runEntry.startedAt,
|
|
5223
|
+
lastRunId: runEntry.runId,
|
|
5224
|
+
lastError: "",
|
|
5225
|
+
});
|
|
5226
|
+
const setActiveChild = (child) => {
|
|
5227
|
+
runEntry.child = child || null;
|
|
5228
|
+
if (controller.signal.aborted) runEntry.stopChild();
|
|
5229
|
+
};
|
|
5230
|
+
try {
|
|
5231
|
+
const result = await runWorkspaceGraph(root, scoped.root, {
|
|
5232
|
+
flowId: entry.flowId,
|
|
5233
|
+
flowSource: entry.flowSource || "user",
|
|
5234
|
+
runNodeId: entry.runNodeId,
|
|
5235
|
+
graph,
|
|
5236
|
+
}, userCtx, {
|
|
5237
|
+
signal: controller.signal,
|
|
5238
|
+
onActiveChild: setActiveChild,
|
|
5239
|
+
});
|
|
5240
|
+
const currentGraph = readWorkspaceGraph(scoped.root).graph;
|
|
5241
|
+
const touchedIds = workspaceRunTouchedNodeIds(result);
|
|
5242
|
+
const mergedGraph = mergeWorkspaceRunGraph(currentGraph, result.graph, touchedIds);
|
|
5243
|
+
fs.writeFileSync(graphPath, JSON.stringify(mergedGraph, null, 2) + "\n", "utf-8");
|
|
5244
|
+
const endedAt = Date.now();
|
|
5245
|
+
appendWorkspaceRunFinished({ ...runEntry, endedAt, durationMs: endedAt - runEntry.startedAt }, "success");
|
|
5246
|
+
updateWorkspaceScheduleEntry(entry.key, {
|
|
5247
|
+
nextRunAt: Date.now() + intervalMs,
|
|
5248
|
+
lastFinishedAt: endedAt,
|
|
5249
|
+
lastStatus: "success",
|
|
5250
|
+
lastError: "",
|
|
5251
|
+
});
|
|
5252
|
+
} catch (e) {
|
|
5253
|
+
const endedAt = Date.now();
|
|
5254
|
+
appendWorkspaceRunFinished({ ...runEntry, endedAt, durationMs: endedAt - runEntry.startedAt }, "failed");
|
|
5255
|
+
updateWorkspaceScheduleEntry(entry.key, {
|
|
5256
|
+
nextRunAt: Date.now() + intervalMs,
|
|
5257
|
+
lastFinishedAt: endedAt,
|
|
5258
|
+
lastStatus: "failed",
|
|
5259
|
+
lastError: (e && e.message) || String(e),
|
|
5260
|
+
lastErrorAt: endedAt,
|
|
5261
|
+
});
|
|
5262
|
+
log.info(`[workspace-scheduler] failed ${entry.flowId}/${entry.runNodeId}: ${(e && e.message) || String(e)}`);
|
|
5263
|
+
} finally {
|
|
5264
|
+
if (activeWorkspaceRuns.get(runKey) === runEntry) activeWorkspaceRuns.delete(runKey);
|
|
5265
|
+
}
|
|
5266
|
+
}
|
|
5267
|
+
|
|
5268
|
+
function pollWorkspaceSchedules(root) {
|
|
5269
|
+
const now = Date.now();
|
|
5270
|
+
const registry = readWorkspaceScheduleRegistry();
|
|
5271
|
+
for (const entry of Object.values(registry.schedules || {})) {
|
|
5272
|
+
if (!entry || entry.enabled !== true) continue;
|
|
5273
|
+
const nextRunAt = Number(entry.nextRunAt || 0);
|
|
5274
|
+
if (!Number.isFinite(nextRunAt) || nextRunAt <= 0) {
|
|
5275
|
+
updateWorkspaceScheduleEntry(entry.key, {
|
|
5276
|
+
nextRunAt: now + Math.max(1, Number(entry.intervalMinutes || 60)) * 60 * 1000,
|
|
5277
|
+
lastStatus: entry.lastStatus || "armed",
|
|
5278
|
+
});
|
|
5279
|
+
continue;
|
|
5280
|
+
}
|
|
5281
|
+
if (nextRunAt > now) continue;
|
|
5282
|
+
void runWorkspaceScheduledEntry(root, entry);
|
|
5283
|
+
}
|
|
5284
|
+
}
|
|
5285
|
+
|
|
4623
5286
|
/** Cursor/OpenCode 执行目录统一使用当前 UI 启动 workspace。 */
|
|
4624
5287
|
function composerCliWorkspaceForFlowDir(workspaceRoot, _flowDir) {
|
|
4625
5288
|
return path.resolve(workspaceRoot);
|
|
@@ -5274,7 +5937,7 @@ export function startUiServer({
|
|
|
5274
5937
|
return;
|
|
5275
5938
|
}
|
|
5276
5939
|
const { path: graphPath, graph } = readWorkspaceGraph(scoped.root);
|
|
5277
|
-
const hydratedGraph =
|
|
5940
|
+
const hydratedGraph = hydrateWorkspaceGraphForRuntime(root, scoped, graph, userCtx);
|
|
5278
5941
|
json(res, 200, {
|
|
5279
5942
|
ok: true,
|
|
5280
5943
|
graph: hydratedGraph,
|
|
@@ -5284,6 +5947,7 @@ export function startUiServer({
|
|
|
5284
5947
|
flowSource: scoped.flowSource,
|
|
5285
5948
|
archived: scoped.archived,
|
|
5286
5949
|
writable: !(scoped.archived || isReadonlyBuiltinFlowSource(scoped.flowSource)),
|
|
5950
|
+
workspaceSchedules: listWorkspaceScheduleStatusesForFlow(userCtx, scoped.flowSource || "user", scoped.flowId || ""),
|
|
5287
5951
|
});
|
|
5288
5952
|
} catch (e) {
|
|
5289
5953
|
json(res, 500, { error: (e && e.message) || String(e) });
|
|
@@ -5364,12 +6028,30 @@ export function startUiServer({
|
|
|
5364
6028
|
json(res, 400, { error: "Cannot write workspace graph for builtin or archived pipeline" });
|
|
5365
6029
|
return;
|
|
5366
6030
|
}
|
|
5367
|
-
const submittedGraph =
|
|
6031
|
+
const submittedGraph = hydrateWorkspaceGraphForRuntime(root, scoped, payload.graph || payload, userCtx);
|
|
5368
6032
|
const graphPath = workspaceGraphPath(scoped.root);
|
|
5369
|
-
const currentGraph =
|
|
6033
|
+
const currentGraph = hydrateWorkspaceGraphForRuntime(root, scoped, readWorkspaceGraph(scoped.root).graph, userCtx);
|
|
5370
6034
|
const graph = mergeWorkspacePersistentNodeRefs(submittedGraph, currentGraph);
|
|
5371
6035
|
fs.writeFileSync(graphPath, JSON.stringify(graph, null, 2) + "\n", "utf-8");
|
|
5372
|
-
|
|
6036
|
+
const workspaceSchedules = syncWorkspaceSchedulesForGraph(root, scoped, graph, authUser, userCtx);
|
|
6037
|
+
json(res, 200, { ok: true, path: graphPath, graph, workspaceSchedules });
|
|
6038
|
+
} catch (e) {
|
|
6039
|
+
json(res, 500, { error: (e && e.message) || String(e) });
|
|
6040
|
+
}
|
|
6041
|
+
return;
|
|
6042
|
+
}
|
|
6043
|
+
|
|
6044
|
+
if (req.method === "GET" && url.pathname === "/api/workspace/schedules") {
|
|
6045
|
+
try {
|
|
6046
|
+
const flowId = url.searchParams.get("flowId") || "";
|
|
6047
|
+
const flowSource = url.searchParams.get("flowSource") || "user";
|
|
6048
|
+
if (!flowId) {
|
|
6049
|
+
json(res, 400, { error: "Missing flowId" });
|
|
6050
|
+
return;
|
|
6051
|
+
}
|
|
6052
|
+
json(res, 200, {
|
|
6053
|
+
schedules: listWorkspaceScheduleStatusesForFlow(userCtx, flowSource, flowId),
|
|
6054
|
+
});
|
|
5373
6055
|
} catch (e) {
|
|
5374
6056
|
json(res, 500, { error: (e && e.message) || String(e) });
|
|
5375
6057
|
}
|
|
@@ -5413,6 +6095,9 @@ export function startUiServer({
|
|
|
5413
6095
|
const runEntry = {
|
|
5414
6096
|
controller,
|
|
5415
6097
|
child: null,
|
|
6098
|
+
runId: runLedgerId("workspace"),
|
|
6099
|
+
userId: String(userCtx.userId || ""),
|
|
6100
|
+
username: String(authUser?.username || userCtx.userId || ""),
|
|
5416
6101
|
runNodeId: String(payload.runNodeId || "").trim(),
|
|
5417
6102
|
flowId,
|
|
5418
6103
|
flowSource: scoped.flowSource || payload.flowSource || "user",
|
|
@@ -5424,6 +6109,7 @@ export function startUiServer({
|
|
|
5424
6109
|
},
|
|
5425
6110
|
};
|
|
5426
6111
|
activeWorkspaceRuns.set(runKey, runEntry);
|
|
6112
|
+
appendWorkspaceRunStarted(runEntry);
|
|
5427
6113
|
const setActiveChild = (child) => {
|
|
5428
6114
|
runEntry.child = child || null;
|
|
5429
6115
|
if (controller.signal.aborted) runEntry.stopChild();
|
|
@@ -5451,12 +6137,27 @@ export function startUiServer({
|
|
|
5451
6137
|
const touchedIds = workspaceRunTouchedNodeIds(result);
|
|
5452
6138
|
const mergedGraph = mergeWorkspaceRunGraph(currentGraph, result.graph, touchedIds);
|
|
5453
6139
|
fs.writeFileSync(graphPath, JSON.stringify(mergedGraph, null, 2) + "\n", "utf-8");
|
|
6140
|
+
appendWorkspaceRunFinished({
|
|
6141
|
+
...runEntry,
|
|
6142
|
+
endedAt: Date.now(),
|
|
6143
|
+
durationMs: Date.now() - runEntry.startedAt,
|
|
6144
|
+
}, "success");
|
|
5454
6145
|
writeEvent({ type: "done", ok: true, path: graphPath, graph: mergedGraph, order: result.order, touchedNodeIds: Array.from(touchedIds), pauseNodeIds: result.pauseNodeIds || [] });
|
|
5455
6146
|
res.end();
|
|
5456
6147
|
} catch (e) {
|
|
5457
6148
|
if (isWorkspaceRunAbortError(e) || controller.signal.aborted) {
|
|
6149
|
+
appendWorkspaceRunFinished({
|
|
6150
|
+
...runEntry,
|
|
6151
|
+
endedAt: Date.now(),
|
|
6152
|
+
durationMs: Date.now() - runEntry.startedAt,
|
|
6153
|
+
}, "stopped");
|
|
5458
6154
|
writeEvent({ type: "stopped", ok: false, stopped: true, message: "Workspace run stopped" });
|
|
5459
6155
|
} else {
|
|
6156
|
+
appendWorkspaceRunFinished({
|
|
6157
|
+
...runEntry,
|
|
6158
|
+
endedAt: Date.now(),
|
|
6159
|
+
durationMs: Date.now() - runEntry.startedAt,
|
|
6160
|
+
}, "failed");
|
|
5460
6161
|
writeEvent({ type: "error", error: (e && e.message) || String(e) });
|
|
5461
6162
|
}
|
|
5462
6163
|
res.end();
|
|
@@ -5475,11 +6176,26 @@ export function startUiServer({
|
|
|
5475
6176
|
const touchedIds = workspaceRunTouchedNodeIds(result);
|
|
5476
6177
|
const mergedGraph = mergeWorkspaceRunGraph(currentGraph, result.graph, touchedIds);
|
|
5477
6178
|
fs.writeFileSync(graphPath, JSON.stringify(mergedGraph, null, 2) + "\n", "utf-8");
|
|
6179
|
+
appendWorkspaceRunFinished({
|
|
6180
|
+
...runEntry,
|
|
6181
|
+
endedAt: Date.now(),
|
|
6182
|
+
durationMs: Date.now() - runEntry.startedAt,
|
|
6183
|
+
}, "success");
|
|
5478
6184
|
json(res, 200, { ok: true, path: graphPath, ...result, graph: mergedGraph, touchedNodeIds: Array.from(touchedIds) });
|
|
5479
6185
|
} catch (e) {
|
|
5480
6186
|
if (isWorkspaceRunAbortError(e) || controller.signal.aborted) {
|
|
6187
|
+
appendWorkspaceRunFinished({
|
|
6188
|
+
...runEntry,
|
|
6189
|
+
endedAt: Date.now(),
|
|
6190
|
+
durationMs: Date.now() - runEntry.startedAt,
|
|
6191
|
+
}, "stopped");
|
|
5481
6192
|
json(res, 200, { ok: false, stopped: true, message: "Workspace run stopped" });
|
|
5482
6193
|
} else {
|
|
6194
|
+
appendWorkspaceRunFinished({
|
|
6195
|
+
...runEntry,
|
|
6196
|
+
endedAt: Date.now(),
|
|
6197
|
+
durationMs: Date.now() - runEntry.startedAt,
|
|
6198
|
+
}, "failed");
|
|
5483
6199
|
throw e;
|
|
5484
6200
|
}
|
|
5485
6201
|
} finally {
|
|
@@ -7896,6 +8612,25 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
7896
8612
|
res.end(data);
|
|
7897
8613
|
});
|
|
7898
8614
|
|
|
8615
|
+
const workspaceScheduleTimer = setInterval(() => {
|
|
8616
|
+
try {
|
|
8617
|
+
pollWorkspaceSchedules(root);
|
|
8618
|
+
} catch (e) {
|
|
8619
|
+
log.debug(`[workspace-scheduler] poll failed: ${(e && e.message) || String(e)}`);
|
|
8620
|
+
}
|
|
8621
|
+
}, WORKSPACE_SCHEDULE_POLL_MS);
|
|
8622
|
+
try {
|
|
8623
|
+
workspaceScheduleTimer.unref?.();
|
|
8624
|
+
} catch (_) {}
|
|
8625
|
+
server.on("close", () => clearInterval(workspaceScheduleTimer));
|
|
8626
|
+
setTimeout(() => {
|
|
8627
|
+
try {
|
|
8628
|
+
pollWorkspaceSchedules(root);
|
|
8629
|
+
} catch (e) {
|
|
8630
|
+
log.debug(`[workspace-scheduler] initial poll failed: ${(e && e.message) || String(e)}`);
|
|
8631
|
+
}
|
|
8632
|
+
}, 1000).unref?.();
|
|
8633
|
+
|
|
7899
8634
|
return new Promise((resolve, reject) => {
|
|
7900
8635
|
server.once("error", reject);
|
|
7901
8636
|
server.listen(port, host, () => {
|