@fieldwangai/agentflow 0.1.84 → 0.1.86
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/README.md +21 -5
- package/README.zh-CN.md +21 -5
- package/bin/agentflow.mjs +1 -1
- package/bin/lib/agent-runners.mjs +589 -0
- package/bin/lib/auth.mjs +11 -1
- package/bin/lib/catalog-flows.mjs +2 -0
- package/bin/lib/composer-agent.mjs +163 -1
- package/bin/lib/composer-model-router.mjs +28 -3
- package/bin/lib/composer-planner.mjs +3 -1
- package/bin/lib/help.mjs +16 -14
- package/bin/lib/locales/en.json +5 -1
- package/bin/lib/locales/zh.json +5 -1
- package/bin/lib/main.mjs +5 -0
- package/bin/lib/mcp-server.mjs +328 -0
- package/bin/lib/model-config.mjs +32 -3
- package/bin/lib/model-lists.mjs +68 -3
- package/bin/lib/node-execute.mjs +22 -1
- package/bin/lib/ui-server.mjs +1420 -73
- package/bin/lib/workspace-run-logs.mjs +181 -0
- package/bin/pipeline/validate-flow.mjs +19 -11
- package/bin/pipeline/validate-for-ui.mjs +19 -11
- package/builtin/nodes/display_react_app.md +52 -0
- package/builtin/web-ui/dist/assets/index-CSr7NGO4.js +322 -0
- package/builtin/web-ui/dist/assets/index-DIO0cfyb.css +1 -0
- package/builtin/web-ui/dist/index.html +2 -2
- package/package.json +3 -2
- package/builtin/web-ui/dist/assets/index-CVdxKCZm.css +0 -1
- package/builtin/web-ui/dist/assets/index-w1bX4CFG.js +0 -297
package/bin/lib/ui-server.mjs
CHANGED
|
@@ -102,6 +102,7 @@ import {
|
|
|
102
102
|
buildClearSessionCookie,
|
|
103
103
|
buildSessionCookie,
|
|
104
104
|
getAuthUserFromRequest,
|
|
105
|
+
getSessionTokenFromRequest,
|
|
105
106
|
isAuthUserAllowed,
|
|
106
107
|
loginOrCreateUser,
|
|
107
108
|
logoutRequest,
|
|
@@ -120,6 +121,13 @@ import {
|
|
|
120
121
|
readRunLedgerEvents,
|
|
121
122
|
runLedgerId,
|
|
122
123
|
} from "./run-ledger.mjs";
|
|
124
|
+
import {
|
|
125
|
+
appendWorkspaceRunLogEvent,
|
|
126
|
+
createWorkspaceRunLogSession,
|
|
127
|
+
finishWorkspaceRunLogSession,
|
|
128
|
+
listWorkspaceRunLogs,
|
|
129
|
+
readWorkspaceRunLogEvents,
|
|
130
|
+
} from "./workspace-run-logs.mjs";
|
|
123
131
|
|
|
124
132
|
const MIME = {
|
|
125
133
|
".html": "text/html; charset=utf-8",
|
|
@@ -612,6 +620,125 @@ function normalizeMcpServerConfig(value) {
|
|
|
612
620
|
return next;
|
|
613
621
|
}
|
|
614
622
|
|
|
623
|
+
function mcpHeadersInfo(headers = {}) {
|
|
624
|
+
const entries = Object.entries(headers && typeof headers === "object" && !Array.isArray(headers) ? headers : {})
|
|
625
|
+
.filter(([key]) => String(key || "").trim());
|
|
626
|
+
const unsupported = [];
|
|
627
|
+
let bearer = false;
|
|
628
|
+
for (const [key, value] of entries) {
|
|
629
|
+
const name = String(key || "").trim();
|
|
630
|
+
if (name.toLowerCase() !== "authorization") {
|
|
631
|
+
unsupported.push(name);
|
|
632
|
+
continue;
|
|
633
|
+
}
|
|
634
|
+
if (/^Bearer\s+.+/i.test(String(value ?? "").trim())) bearer = true;
|
|
635
|
+
else unsupported.push(name);
|
|
636
|
+
}
|
|
637
|
+
return { count: entries.length, bearer, unsupported };
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function codexMcpCompatibility(server) {
|
|
641
|
+
const raw = server?.raw && typeof server.raw === "object" && !Array.isArray(server.raw) ? server.raw : {};
|
|
642
|
+
const reasons = [];
|
|
643
|
+
const supported = [];
|
|
644
|
+
const unsupported = [];
|
|
645
|
+
const type = raw.url || server?.url ? "url" : raw.command || server?.command ? "command" : "";
|
|
646
|
+
if (raw.disabled === true) {
|
|
647
|
+
return {
|
|
648
|
+
status: "unsupported",
|
|
649
|
+
label: "Codex disabled",
|
|
650
|
+
reasons: ["该 MCP 已 disabled,Codex 不会启用。"],
|
|
651
|
+
supported,
|
|
652
|
+
unsupported: ["disabled"],
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
if (!type) {
|
|
656
|
+
return {
|
|
657
|
+
status: "unsupported",
|
|
658
|
+
label: "Codex unsupported",
|
|
659
|
+
reasons: ["缺少 url 或 command。"],
|
|
660
|
+
supported,
|
|
661
|
+
unsupported: ["transport"],
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
if (type === "command") {
|
|
666
|
+
supported.push("command", "args");
|
|
667
|
+
const envKeys = Object.keys(raw.env && typeof raw.env === "object" && !Array.isArray(raw.env) ? raw.env : {});
|
|
668
|
+
if (envKeys.length) supported.push("env");
|
|
669
|
+
if (raw.cwd) supported.push("cwd");
|
|
670
|
+
const headers = raw.headers && typeof raw.headers === "object" && !Array.isArray(raw.headers) ? raw.headers : {};
|
|
671
|
+
if (Object.keys(headers).length) {
|
|
672
|
+
unsupported.push("headers");
|
|
673
|
+
reasons.push("Codex stdio MCP 不支持 headers。");
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
if (type === "url") {
|
|
678
|
+
supported.push("url");
|
|
679
|
+
const headers = mcpHeadersInfo(raw.headers);
|
|
680
|
+
if (headers.bearer) supported.push("Authorization Bearer");
|
|
681
|
+
if (headers.unsupported.length) {
|
|
682
|
+
unsupported.push(...headers.unsupported.map((name) => `header:${name}`));
|
|
683
|
+
reasons.push(`Codex URL MCP 仅能等价支持 Authorization: Bearer;不支持自定义 header:${headers.unsupported.join(", ")}。`);
|
|
684
|
+
}
|
|
685
|
+
const envKeys = Object.keys(raw.env && typeof raw.env === "object" && !Array.isArray(raw.env) ? raw.env : {});
|
|
686
|
+
if (envKeys.length) {
|
|
687
|
+
unsupported.push("env");
|
|
688
|
+
reasons.push("Codex URL MCP 不支持 env;如需鉴权请使用 Authorization: Bearer 或 bearer_token_env_var。");
|
|
689
|
+
}
|
|
690
|
+
if (raw.bearer_token_env_var) supported.push("bearer_token_env_var");
|
|
691
|
+
if (raw.oauth_client_id) supported.push("oauth_client_id");
|
|
692
|
+
if (raw.oauth_resource) supported.push("oauth_resource");
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
const known = new Set([
|
|
696
|
+
"url",
|
|
697
|
+
"command",
|
|
698
|
+
"args",
|
|
699
|
+
"env",
|
|
700
|
+
"headers",
|
|
701
|
+
"description",
|
|
702
|
+
"cwd",
|
|
703
|
+
"disabled",
|
|
704
|
+
"bearer_token_env_var",
|
|
705
|
+
"oauth_client_id",
|
|
706
|
+
"oauth_resource",
|
|
707
|
+
"__agentflowPrivateKeys",
|
|
708
|
+
]);
|
|
709
|
+
const unknownKeys = Object.keys(raw).filter((key) => !known.has(key));
|
|
710
|
+
if (unknownKeys.length) {
|
|
711
|
+
unsupported.push(...unknownKeys.map((key) => `field:${key}`));
|
|
712
|
+
reasons.push(`存在 Codex 未确认支持的额外字段:${unknownKeys.join(", ")}。`);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
const status = unsupported.length ? "partial" : "ok";
|
|
716
|
+
return {
|
|
717
|
+
status,
|
|
718
|
+
label: status === "ok" ? "Codex OK" : "Codex partial",
|
|
719
|
+
reasons,
|
|
720
|
+
supported,
|
|
721
|
+
unsupported,
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function cursorMcpCompatibility(server) {
|
|
726
|
+
if (server?.raw?.disabled === true) {
|
|
727
|
+
return { status: "unsupported", label: "Cursor disabled", reasons: ["该 MCP 已 disabled。"] };
|
|
728
|
+
}
|
|
729
|
+
return { status: "ok", label: "Cursor OK", reasons: [] };
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function withMcpBackendCompatibility(server) {
|
|
733
|
+
return {
|
|
734
|
+
...server,
|
|
735
|
+
backends: {
|
|
736
|
+
cursor: cursorMcpCompatibility(server),
|
|
737
|
+
codex: codexMcpCompatibility(server),
|
|
738
|
+
},
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
|
|
615
742
|
function readCursorMcpServers(userCtx = {}) {
|
|
616
743
|
const config = readCursorMcpConfig();
|
|
617
744
|
const privateConfig = readUserMcpPrivate(userCtx);
|
|
@@ -644,7 +771,7 @@ function readCursorMcpServers(userCtx = {}) {
|
|
|
644
771
|
privateEnvKeys,
|
|
645
772
|
privateHeaderKeys,
|
|
646
773
|
};
|
|
647
|
-
}).sort((a, b) => a.name.localeCompare(b.name));
|
|
774
|
+
}).map(withMcpBackendCompatibility).sort((a, b) => a.name.localeCompare(b.name));
|
|
648
775
|
return { path: cursorMcpConfigPath(), servers };
|
|
649
776
|
}
|
|
650
777
|
|
|
@@ -951,9 +1078,11 @@ function readModelListsFromDisk(workspaceRoot) {
|
|
|
951
1078
|
cursor: [],
|
|
952
1079
|
opencode: [],
|
|
953
1080
|
claudeCode: [],
|
|
1081
|
+
codex: [],
|
|
954
1082
|
cursorFetchedAt: null,
|
|
955
1083
|
opencodeFetchedAt: null,
|
|
956
1084
|
claudeCodeFetchedAt: null,
|
|
1085
|
+
codexFetchedAt: null,
|
|
957
1086
|
};
|
|
958
1087
|
try {
|
|
959
1088
|
if (!fs.existsSync(p)) return empty;
|
|
@@ -962,9 +1091,11 @@ function readModelListsFromDisk(workspaceRoot) {
|
|
|
962
1091
|
cursor: Array.isArray(data.cursor) ? data.cursor.map(String) : [],
|
|
963
1092
|
opencode: Array.isArray(data.opencode) ? data.opencode.map(String) : [],
|
|
964
1093
|
claudeCode: Array.isArray(data.claudeCode) ? data.claudeCode.map(String) : [],
|
|
1094
|
+
codex: Array.isArray(data.codex) ? data.codex.map(String) : [],
|
|
965
1095
|
cursorFetchedAt: data.cursorFetchedAt ?? null,
|
|
966
1096
|
opencodeFetchedAt: data.opencodeFetchedAt ?? null,
|
|
967
1097
|
claudeCodeFetchedAt: data.claudeCodeFetchedAt ?? null,
|
|
1098
|
+
codexFetchedAt: data.codexFetchedAt ?? null,
|
|
968
1099
|
};
|
|
969
1100
|
} catch {
|
|
970
1101
|
return empty;
|
|
@@ -1482,6 +1613,195 @@ function readWorkspaceGraph(workspaceRoot) {
|
|
|
1482
1613
|
|
|
1483
1614
|
const DISPLAY_SHARE_FILENAME = "display-shares.json";
|
|
1484
1615
|
const DISPLAY_SHARE_TTL_MS = 30 * 24 * 60 * 60 * 1000;
|
|
1616
|
+
const DISPLAY_SHARE_ALLOWED_EXPIRY_DAYS = new Set([1, 7, 30, 90, 365]);
|
|
1617
|
+
const NODE_STUDIO_DRAFTS_DIRNAME = "node-studio/drafts";
|
|
1618
|
+
const USER_WORKSPACES_FILENAME = "workspaces.json";
|
|
1619
|
+
|
|
1620
|
+
function userWorkspacesPath(userCtx = {}) {
|
|
1621
|
+
return path.join(getAgentflowUserDataRoot(userCtx.userId || ""), USER_WORKSPACES_FILENAME);
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
function normalizeWorkspaceEntry(entry = {}, index = 0, userCtx = {}) {
|
|
1625
|
+
const label = String(entry?.label || entry?.name || "").trim();
|
|
1626
|
+
const kindRaw = String(entry?.kind || entry?.source || "").trim().toLowerCase();
|
|
1627
|
+
const repoUrl = String(entry?.repoUrl || entry?.gitUrl || entry?.url || "").trim();
|
|
1628
|
+
const kind = kindRaw === "git" || repoUrl ? "git" : "local";
|
|
1629
|
+
const branch = String(entry?.branch || "master").trim() || "master";
|
|
1630
|
+
const mountPathRaw = String(entry?.mountPath || "").trim();
|
|
1631
|
+
const rawPath = String(entry?.path || entry?.cwd || "").trim();
|
|
1632
|
+
if (!rawPath && kind !== "git") return null;
|
|
1633
|
+
const idRaw = String(entry?.id || label || mountPathRaw || repoUrl || rawPath || `workspace_${index + 1}`).trim().toLowerCase();
|
|
1634
|
+
const id = idRaw.replace(/[^a-z0-9_-]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 64) || `workspace_${index + 1}`;
|
|
1635
|
+
const mountPath = (mountPathRaw || id).replace(/^\/+/, "").replace(/\.\.(\/|\\|$)/g, "").trim() || id;
|
|
1636
|
+
const defaultGitPath = path.join(getAgentflowUserDataRoot(userCtx.userId || ""), "workspaces", "repos", id);
|
|
1637
|
+
const absPath = path.resolve((rawPath || defaultGitPath).replace(/^~(?=$|\/|\\)/, os.homedir()));
|
|
1638
|
+
const exists = fs.existsSync(absPath) && fs.statSync(absPath).isDirectory();
|
|
1639
|
+
return {
|
|
1640
|
+
id,
|
|
1641
|
+
label: label || path.basename(absPath) || id,
|
|
1642
|
+
kind,
|
|
1643
|
+
path: absPath,
|
|
1644
|
+
repoUrl,
|
|
1645
|
+
branch,
|
|
1646
|
+
mountPath,
|
|
1647
|
+
credentialRef: String(entry?.credentialRef || "").trim(),
|
|
1648
|
+
type: String(entry?.type || (kind === "git" ? "code" : "local")).trim() || (kind === "git" ? "code" : "local"),
|
|
1649
|
+
description: String(entry?.description || "").trim(),
|
|
1650
|
+
visibility: String(entry?.visibility || "personal").trim() || "personal",
|
|
1651
|
+
enabled: entry?.enabled !== false,
|
|
1652
|
+
exists,
|
|
1653
|
+
};
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
function readUserWorkspaces(userCtx = {}) {
|
|
1657
|
+
const p = userWorkspacesPath(userCtx);
|
|
1658
|
+
if (!fs.existsSync(p)) return [];
|
|
1659
|
+
try {
|
|
1660
|
+
const data = JSON.parse(fs.readFileSync(p, "utf-8"));
|
|
1661
|
+
const list = Array.isArray(data?.workspaces) ? data.workspaces : Array.isArray(data) ? data : [];
|
|
1662
|
+
return list.map((entry, index) => normalizeWorkspaceEntry(entry, index, userCtx)).filter(Boolean);
|
|
1663
|
+
} catch {
|
|
1664
|
+
return [];
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
function writeUserWorkspaces(userCtx = {}, entries = []) {
|
|
1669
|
+
const seen = new Set();
|
|
1670
|
+
const workspaces = (Array.isArray(entries) ? entries : [])
|
|
1671
|
+
.map((entry, index) => normalizeWorkspaceEntry(entry, index, userCtx))
|
|
1672
|
+
.filter(Boolean)
|
|
1673
|
+
.filter((entry) => {
|
|
1674
|
+
const key = entry.id || entry.path;
|
|
1675
|
+
if (seen.has(key)) return false;
|
|
1676
|
+
seen.add(key);
|
|
1677
|
+
return true;
|
|
1678
|
+
});
|
|
1679
|
+
const p = userWorkspacesPath(userCtx);
|
|
1680
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
1681
|
+
fs.writeFileSync(p, JSON.stringify({ version: 1, workspaces }, null, 2) + "\n", "utf-8");
|
|
1682
|
+
return workspaces;
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
function listConfiguredWorkspaces(root, scopedRoot, userCtx = {}) {
|
|
1686
|
+
const currentRoot = path.resolve(scopedRoot || root);
|
|
1687
|
+
const homeRoot = path.resolve(os.homedir());
|
|
1688
|
+
const builtins = [
|
|
1689
|
+
{ id: "current", label: "当前流程工作区", kind: "local", path: currentRoot, builtin: true, exists: fs.existsSync(currentRoot) && fs.statSync(currentRoot).isDirectory(), type: "flow", enabled: true },
|
|
1690
|
+
{ id: "home", label: "用户 Home", kind: "local", path: homeRoot, builtin: true, exists: fs.existsSync(homeRoot) && fs.statSync(homeRoot).isDirectory(), type: "local", enabled: true },
|
|
1691
|
+
];
|
|
1692
|
+
const custom = readUserWorkspaces(userCtx).filter((entry) => entry.enabled !== false).map((entry) => ({ ...entry, builtin: false }));
|
|
1693
|
+
const seen = new Set();
|
|
1694
|
+
return [...builtins, ...custom].filter((entry) => {
|
|
1695
|
+
const key = path.resolve(entry.path);
|
|
1696
|
+
if (seen.has(key)) return false;
|
|
1697
|
+
seen.add(key);
|
|
1698
|
+
return true;
|
|
1699
|
+
});
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
function nodeStudioDraftsRoot(userCtx = {}) {
|
|
1703
|
+
return path.join(getAgentflowUserDataRoot(userCtx.userId || ""), NODE_STUDIO_DRAFTS_DIRNAME);
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
function normalizeNodeStudioDraftId(value) {
|
|
1707
|
+
const raw = String(value || "").trim().toLowerCase();
|
|
1708
|
+
const safe = raw.replace(/[^a-z0-9_-]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 64);
|
|
1709
|
+
return safe || `draft_${Date.now().toString(36)}_${crypto.randomBytes(3).toString("hex")}`;
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
function nodeStudioDraftPath(userCtx = {}, draftId = "") {
|
|
1713
|
+
return path.join(nodeStudioDraftsRoot(userCtx), normalizeNodeStudioDraftId(draftId), "draft.json");
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
function emptyNodeStudioDraft(userCtx = {}, draftId = "") {
|
|
1717
|
+
const now = new Date().toISOString();
|
|
1718
|
+
const id = normalizeNodeStudioDraftId(draftId || "untitled_node");
|
|
1719
|
+
return {
|
|
1720
|
+
id,
|
|
1721
|
+
title: "Untitled Node",
|
|
1722
|
+
definitionId: "",
|
|
1723
|
+
createdAt: now,
|
|
1724
|
+
updatedAt: now,
|
|
1725
|
+
ownerUserId: String(userCtx.userId || ""),
|
|
1726
|
+
agentMessages: [],
|
|
1727
|
+
promptDraft: "",
|
|
1728
|
+
manifest: {
|
|
1729
|
+
id,
|
|
1730
|
+
version: "1.0.0",
|
|
1731
|
+
name: "Untitled Node",
|
|
1732
|
+
description: "",
|
|
1733
|
+
baseDefinitionId: "agent_subAgent",
|
|
1734
|
+
runtime: { type: "agent_subAgent" },
|
|
1735
|
+
inputs: [],
|
|
1736
|
+
outputs: [],
|
|
1737
|
+
configSchema: { fields: [] },
|
|
1738
|
+
ui: { card: { icon: "extension", variant: "default", actions: [] } },
|
|
1739
|
+
},
|
|
1740
|
+
config: {},
|
|
1741
|
+
test: { inputs: {}, log: [], status: "not run" },
|
|
1742
|
+
files: {},
|
|
1743
|
+
};
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
function isLegacyNodeStudioDemoDraft(draft) {
|
|
1747
|
+
return (
|
|
1748
|
+
String(draft?.id || "") === "daily_report_demo" &&
|
|
1749
|
+
String(draft?.definitionId || "") === "marketplace:daily_report@1.0.0"
|
|
1750
|
+
);
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
function readNodeStudioDraft(userCtx = {}, draftId = "") {
|
|
1754
|
+
const id = normalizeNodeStudioDraftId(draftId || "");
|
|
1755
|
+
const filePath = nodeStudioDraftPath(userCtx, id);
|
|
1756
|
+
if (!fs.existsSync(filePath)) return null;
|
|
1757
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
1758
|
+
return parsed && typeof parsed === "object" ? parsed : null;
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
function writeNodeStudioDraft(userCtx = {}, draft = {}) {
|
|
1762
|
+
const id = normalizeNodeStudioDraftId(draft.id || "untitled_node");
|
|
1763
|
+
const filePath = nodeStudioDraftPath(userCtx, id);
|
|
1764
|
+
const previous = fs.existsSync(filePath)
|
|
1765
|
+
? JSON.parse(fs.readFileSync(filePath, "utf-8"))
|
|
1766
|
+
: {};
|
|
1767
|
+
const now = new Date().toISOString();
|
|
1768
|
+
const next = {
|
|
1769
|
+
...previous,
|
|
1770
|
+
...draft,
|
|
1771
|
+
id,
|
|
1772
|
+
createdAt: previous.createdAt || draft.createdAt || now,
|
|
1773
|
+
updatedAt: now,
|
|
1774
|
+
ownerUserId: String(userCtx.userId || draft.ownerUserId || ""),
|
|
1775
|
+
};
|
|
1776
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
1777
|
+
fs.writeFileSync(filePath, JSON.stringify(next, null, 2) + "\n", "utf-8");
|
|
1778
|
+
return next;
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
function listNodeStudioDrafts(userCtx = {}) {
|
|
1782
|
+
const rootDir = nodeStudioDraftsRoot(userCtx);
|
|
1783
|
+
if (!fs.existsSync(rootDir)) return [];
|
|
1784
|
+
const rows = [];
|
|
1785
|
+
for (const entry of fs.readdirSync(rootDir, { withFileTypes: true })) {
|
|
1786
|
+
if (!entry.isDirectory()) continue;
|
|
1787
|
+
const filePath = path.join(rootDir, entry.name, "draft.json");
|
|
1788
|
+
if (!fs.existsSync(filePath)) continue;
|
|
1789
|
+
try {
|
|
1790
|
+
const draft = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
1791
|
+
if (isLegacyNodeStudioDemoDraft(draft)) continue;
|
|
1792
|
+
rows.push({
|
|
1793
|
+
id: String(draft.id || entry.name),
|
|
1794
|
+
title: String(draft.title || draft.manifest?.name || entry.name),
|
|
1795
|
+
definitionId: String(draft.definitionId || `marketplace:${draft.manifest?.id || entry.name}@${draft.manifest?.version || "1.0.0"}`),
|
|
1796
|
+
updatedAt: String(draft.updatedAt || ""),
|
|
1797
|
+
});
|
|
1798
|
+
} catch {
|
|
1799
|
+
/* ignore corrupt drafts */
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
rows.sort((a, b) => String(b.updatedAt || "").localeCompare(String(a.updatedAt || "")) || a.id.localeCompare(b.id));
|
|
1803
|
+
return rows;
|
|
1804
|
+
}
|
|
1485
1805
|
|
|
1486
1806
|
function displaySharesPath() {
|
|
1487
1807
|
return path.join(getAgentflowDataRoot(), DISPLAY_SHARE_FILENAME);
|
|
@@ -1931,9 +2251,30 @@ function createDisplayShareId() {
|
|
|
1931
2251
|
return crypto.randomBytes(12).toString("base64url");
|
|
1932
2252
|
}
|
|
1933
2253
|
|
|
1934
|
-
function
|
|
2254
|
+
function normalizeDisplayShareExpiry(input = {}, now = new Date()) {
|
|
2255
|
+
const mode = String(input?.expiresMode || input?.expiryMode || "").trim().toLowerCase();
|
|
2256
|
+
const rawDays = Number(input?.expiresInDays ?? input?.expiryDays ?? input?.ttlDays);
|
|
2257
|
+
if (
|
|
2258
|
+
mode === "permanent" ||
|
|
2259
|
+
mode === "forever" ||
|
|
2260
|
+
input?.permanent === true ||
|
|
2261
|
+
input?.expiresAt === null ||
|
|
2262
|
+
String(input?.expiresAt || "").trim().toLowerCase() === "permanent"
|
|
2263
|
+
) {
|
|
2264
|
+
return { expiresAt: "", expiresMode: "permanent", expiresInDays: null };
|
|
2265
|
+
}
|
|
2266
|
+
let days = Number.isFinite(rawDays) ? Math.round(rawDays) : 30;
|
|
2267
|
+
if (!DISPLAY_SHARE_ALLOWED_EXPIRY_DAYS.has(days)) days = 30;
|
|
1935
2268
|
const time = now instanceof Date ? now.getTime() : Date.now();
|
|
1936
|
-
return
|
|
2269
|
+
return {
|
|
2270
|
+
expiresAt: new Date(time + days * 24 * 60 * 60 * 1000).toISOString(),
|
|
2271
|
+
expiresMode: "days",
|
|
2272
|
+
expiresInDays: days,
|
|
2273
|
+
};
|
|
2274
|
+
}
|
|
2275
|
+
|
|
2276
|
+
function displayShareExpiresAt(now = new Date()) {
|
|
2277
|
+
return normalizeDisplayShareExpiry({ expiresInDays: 30 }, now).expiresAt;
|
|
1937
2278
|
}
|
|
1938
2279
|
|
|
1939
2280
|
function isDisplayShareExpired(share) {
|
|
@@ -1971,12 +2312,13 @@ function normalizeDisplayShareLayout(layout, fallback = "canvas") {
|
|
|
1971
2312
|
return ["canvas", "gallery", "slides", "document", "single"].includes(text) ? text : fallback;
|
|
1972
2313
|
}
|
|
1973
2314
|
|
|
1974
|
-
function createDisplayShareRecord({ userId, flowId, flowSource, archived, title, layout, nodeIds }) {
|
|
2315
|
+
function createDisplayShareRecord({ userId, flowId, flowSource, archived, title, layout, nodeIds, expiresMode, expiresInDays, permanent, expiresAt }) {
|
|
1975
2316
|
const shares = readDisplayShares();
|
|
1976
2317
|
let id = createDisplayShareId();
|
|
1977
2318
|
while (shares[id]) id = createDisplayShareId();
|
|
1978
2319
|
const nowDate = new Date();
|
|
1979
2320
|
const now = nowDate.toISOString();
|
|
2321
|
+
const expiry = normalizeDisplayShareExpiry({ expiresMode, expiresInDays, permanent, expiresAt }, nowDate);
|
|
1980
2322
|
const share = {
|
|
1981
2323
|
id,
|
|
1982
2324
|
userId: String(userId || ""),
|
|
@@ -1988,13 +2330,84 @@ function createDisplayShareRecord({ userId, flowId, flowSource, archived, title,
|
|
|
1988
2330
|
nodeIds: Array.isArray(nodeIds) ? nodeIds : [],
|
|
1989
2331
|
createdAt: now,
|
|
1990
2332
|
updatedAt: now,
|
|
1991
|
-
expiresAt:
|
|
2333
|
+
expiresAt: expiry.expiresAt,
|
|
2334
|
+
expiresMode: expiry.expiresMode,
|
|
2335
|
+
expiresInDays: expiry.expiresInDays,
|
|
1992
2336
|
};
|
|
1993
2337
|
shares[id] = share;
|
|
1994
2338
|
writeDisplayShares(shares);
|
|
1995
2339
|
return share;
|
|
1996
2340
|
}
|
|
1997
2341
|
|
|
2342
|
+
function displayShareSummary(share, baseUrl = "") {
|
|
2343
|
+
return {
|
|
2344
|
+
id: String(share?.id || ""),
|
|
2345
|
+
userId: String(share?.userId || ""),
|
|
2346
|
+
flowId: String(share?.flowId || ""),
|
|
2347
|
+
flowSource: String(share?.flowSource || "user"),
|
|
2348
|
+
archived: share?.archived === true,
|
|
2349
|
+
title: String(share?.title || "AgentFlow Display"),
|
|
2350
|
+
layout: String(share?.layout || "gallery"),
|
|
2351
|
+
nodeIds: Array.isArray(share?.nodeIds) ? share.nodeIds : [],
|
|
2352
|
+
createdAt: String(share?.createdAt || ""),
|
|
2353
|
+
updatedAt: String(share?.updatedAt || ""),
|
|
2354
|
+
expiresAt: String(share?.expiresAt || ""),
|
|
2355
|
+
expiresMode: String(share?.expiresMode || (share?.expiresAt ? "days" : "permanent")),
|
|
2356
|
+
expiresInDays: share?.expiresInDays == null ? null : Number(share.expiresInDays),
|
|
2357
|
+
url: displayShareOutputUrl(share?.id || "", baseUrl),
|
|
2358
|
+
};
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2361
|
+
function listDisplaySharesForUser(userCtx = {}, baseUrl = "") {
|
|
2362
|
+
const userId = String(userCtx?.userId || "");
|
|
2363
|
+
const isAdmin = userCtx?.isAdmin === true;
|
|
2364
|
+
const shares = readDisplayShares();
|
|
2365
|
+
let changed = false;
|
|
2366
|
+
const rows = [];
|
|
2367
|
+
for (const [id, share] of Object.entries(shares)) {
|
|
2368
|
+
if (isDisplayShareExpired(share)) {
|
|
2369
|
+
delete shares[id];
|
|
2370
|
+
changed = true;
|
|
2371
|
+
continue;
|
|
2372
|
+
}
|
|
2373
|
+
if (!isAdmin && String(share?.userId || "") !== userId) continue;
|
|
2374
|
+
rows.push(displayShareSummary(share, baseUrl));
|
|
2375
|
+
}
|
|
2376
|
+
if (changed) writeDisplayShares(shares);
|
|
2377
|
+
rows.sort((a, b) => Date.parse(b.createdAt || "") - Date.parse(a.createdAt || ""));
|
|
2378
|
+
return rows;
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
function updateDisplayShareExpiryForUser(id, userCtx = {}, patch = {}) {
|
|
2382
|
+
const shares = readDisplayShares();
|
|
2383
|
+
const share = shares[id];
|
|
2384
|
+
if (!share) return { status: 404, error: "Display share not found" };
|
|
2385
|
+
const userId = String(userCtx?.userId || "");
|
|
2386
|
+
if (userCtx?.isAdmin !== true && String(share.userId || "") !== userId) return { status: 403, error: "Forbidden" };
|
|
2387
|
+
const expiry = normalizeDisplayShareExpiry(patch, new Date());
|
|
2388
|
+
const updated = {
|
|
2389
|
+
...share,
|
|
2390
|
+
expiresAt: expiry.expiresAt,
|
|
2391
|
+
expiresMode: expiry.expiresMode,
|
|
2392
|
+
expiresInDays: expiry.expiresInDays,
|
|
2393
|
+
updatedAt: new Date().toISOString(),
|
|
2394
|
+
};
|
|
2395
|
+
shares[id] = updated;
|
|
2396
|
+
writeDisplayShares(shares);
|
|
2397
|
+
return { status: 200, share: updated };
|
|
2398
|
+
}
|
|
2399
|
+
|
|
2400
|
+
function deleteDisplayShareForUser(id, userCtx = {}) {
|
|
2401
|
+
const shares = readDisplayShares();
|
|
2402
|
+
const share = shares[id];
|
|
2403
|
+
if (!share) return { status: 404, error: "Display share not found" };
|
|
2404
|
+
const userId = String(userCtx?.userId || "");
|
|
2405
|
+
if (userCtx?.isAdmin !== true && String(share.userId || "") !== userId) return { status: 403, error: "Forbidden" };
|
|
2406
|
+
delete shares[id];
|
|
2407
|
+
writeDisplayShares(shares);
|
|
2408
|
+
return { status: 200 };
|
|
2409
|
+
}
|
|
2410
|
+
|
|
1998
2411
|
function parseDisplayShareNodeIdInput(value) {
|
|
1999
2412
|
return String(value || "")
|
|
2000
2413
|
.split(/[\s,,]+/g)
|
|
@@ -2146,6 +2559,8 @@ function publicDisplayPayloadFromShare(root, share) {
|
|
|
2146
2559
|
createdAt: share.createdAt || "",
|
|
2147
2560
|
updatedAt: share.updatedAt || "",
|
|
2148
2561
|
expiresAt: share.expiresAt || "",
|
|
2562
|
+
expiresMode: share.expiresMode || (share.expiresAt ? "days" : "permanent"),
|
|
2563
|
+
expiresInDays: share.expiresInDays == null ? null : Number(share.expiresInDays),
|
|
2149
2564
|
},
|
|
2150
2565
|
nodes,
|
|
2151
2566
|
};
|
|
@@ -2422,6 +2837,14 @@ function buildWorkspaceGeneratePrompt(payload) {
|
|
|
2422
2837
|
"只输出 ASCII 图正文,不要解释,不要包裹 Markdown 代码围栏。",
|
|
2423
2838
|
"使用 +-|/\\<> 等字符表达结构,尽量保持对齐。",
|
|
2424
2839
|
].join("\n")
|
|
2840
|
+
: outputKind === "react"
|
|
2841
|
+
? [
|
|
2842
|
+
"你是 workspace React 工程节点的内容生成器。",
|
|
2843
|
+
"请根据用户 prompt 和上游节点/文件上下文生成一个可预览的小型 React 工程 JSON。",
|
|
2844
|
+
"只输出 JSON,不要解释,不要包裹 Markdown 代码围栏。",
|
|
2845
|
+
"JSON 必须包含 title、entry、files;files 至少包含 src/App.jsx,可包含 src/styles.css。",
|
|
2846
|
+
"src/App.jsx 里定义或 export default 一个 App 组件;不要依赖未声明的外部包。",
|
|
2847
|
+
].join("\n")
|
|
2425
2848
|
: [
|
|
2426
2849
|
"你是 AgentFlow Workspace Composer。",
|
|
2427
2850
|
"默认以用户当前选择的 workspace 节点作为上下文范围;选中节点不是让你重建整张画布的授权。",
|
|
@@ -2475,6 +2898,8 @@ function buildWorkspaceNodeChatPrompt(payload) {
|
|
|
2475
2898
|
].join("\n")
|
|
2476
2899
|
: nodeKind === "html"
|
|
2477
2900
|
? "只输出完整或片段 HTML,不要解释,不要包裹 Markdown 代码围栏。"
|
|
2901
|
+
: nodeKind === "react"
|
|
2902
|
+
? "只输出 React 工程 JSON,不要解释,不要包裹 Markdown 代码围栏。JSON 必须包含 title、entry、files;files 至少包含 src/App.jsx。"
|
|
2478
2903
|
: nodeKind === "image"
|
|
2479
2904
|
? "只输出新的图片 src,可以是 URL、data URL 或文件路径,不要解释。"
|
|
2480
2905
|
: nodeKind === "mermaid"
|
|
@@ -3030,6 +3455,7 @@ function workspaceDisplayKind(definitionId) {
|
|
|
3030
3455
|
if (id === "display_mermaid") return "mermaid";
|
|
3031
3456
|
if (id === "display_ascii") return "ascii";
|
|
3032
3457
|
if (id === "display_html") return "html";
|
|
3458
|
+
if (id === "display_react_app") return "react";
|
|
3033
3459
|
if (id === "display_image") return "image";
|
|
3034
3460
|
if (id === "display_chart") return "chart";
|
|
3035
3461
|
if (id === "display_table") return "table";
|
|
@@ -3046,6 +3472,7 @@ function workspaceDisplayTextFilePath(value, kind = "") {
|
|
|
3046
3472
|
const ext = clean.split("?")[0].split("#")[0].toLowerCase().split(".").pop() || "";
|
|
3047
3473
|
const allowedByKind = {
|
|
3048
3474
|
html: new Set(["html", "htm"]),
|
|
3475
|
+
react: new Set(["json", "jsx", "tsx", "js", "txt"]),
|
|
3049
3476
|
markdown: new Set(["md", "markdown", "txt"]),
|
|
3050
3477
|
mermaid: new Set(["mmd", "mermaid", "txt"]),
|
|
3051
3478
|
ascii: new Set(["txt", "log"]),
|
|
@@ -3119,6 +3546,7 @@ function workspaceDisplayKindExample(kind, field) {
|
|
|
3119
3546
|
if (kind === "table") return { columns: ["列名1", "列名2"], rows: [["值1", "值2"]] };
|
|
3120
3547
|
if (kind === "chart") return { type: "chart", version: "1.0", renderer: "echarts", option: { xAxis: { type: "category", data: [] }, yAxis: { type: "value" }, series: [{ type: "bar", data: [] }] } };
|
|
3121
3548
|
if (kind === "html") return "<可直接渲染的 HTML>";
|
|
3549
|
+
if (kind === "react") return { title: "React App", entry: "src/App.jsx", files: { "src/App.jsx": "export default function App() { return <main>...</main>; }", "src/styles.css": "body { margin: 0; }" }, inputs: {} };
|
|
3122
3550
|
if (kind === "mermaid") return "flowchart TD\n A[开始] --> B[结束]";
|
|
3123
3551
|
if (kind === "ascii") return "+---+\n| |\n+---+";
|
|
3124
3552
|
if (kind === "image") return "<图片 URL 或 data URL>";
|
|
@@ -3254,12 +3682,16 @@ function workspaceOutputProtocolRequirements(graph, nodeId) {
|
|
|
3254
3682
|
.filter((slot) => {
|
|
3255
3683
|
const name = String(slot?.name || "").trim();
|
|
3256
3684
|
const type = String(slot?.type || "");
|
|
3257
|
-
return name && type !== "node" && name !== "next" && name !== "result" && name !== "content";
|
|
3685
|
+
return name && type !== "node" && name !== "next" && name !== "result" && name !== "content" && name !== "displayType";
|
|
3258
3686
|
})
|
|
3259
3687
|
.map((slot) => String(slot.name).trim());
|
|
3260
|
-
const
|
|
3688
|
+
const configuredResultKind = isWorkspaceOneClickTaskDefinitionId(instance.definitionId)
|
|
3689
|
+
? workspaceContextRunDisplayKind(instance)
|
|
3690
|
+
: "";
|
|
3691
|
+
const resultKind = displayByField.get("result") || workspaceDownstreamOutputKindForField(graph, nodeId, "result") || configuredResultKind || "";
|
|
3261
3692
|
const resultExtByKind = {
|
|
3262
3693
|
html: "html",
|
|
3694
|
+
react: "json",
|
|
3263
3695
|
markdown: "md",
|
|
3264
3696
|
mermaid: "mmd",
|
|
3265
3697
|
ascii: "txt",
|
|
@@ -3271,6 +3703,7 @@ function workspaceOutputProtocolRequirements(graph, nodeId) {
|
|
|
3271
3703
|
const resultKindText = resultKind ? ` ${resultKind}` : "";
|
|
3272
3704
|
const resultGuidance = {
|
|
3273
3705
|
html: "内容必须是可直接放入 iframe 渲染的 HTML;不要使用 Markdown 代码围栏。",
|
|
3706
|
+
react: "内容必须是 React 工程 JSON,包含 title、entry、files;files 至少包含 src/App.jsx,可包含 CSS 文件。",
|
|
3274
3707
|
markdown: "内容必须是 Markdown 正文;除非正文确实需要代码块,否则不要额外包裹代码围栏。",
|
|
3275
3708
|
mermaid: "内容必须是 Mermaid 图表代码,例如 flowchart/sequenceDiagram;不要使用 Markdown 代码围栏。",
|
|
3276
3709
|
ascii: "内容必须是纯文本/ASCII 图或表格;不要输出 HTML 或 Markdown 装饰。",
|
|
@@ -3377,8 +3810,11 @@ function workspaceRunPlan(graph, runNodeId, scopedRoot = "") {
|
|
|
3377
3810
|
if (needed.size !== before) visitControlDownstream(next);
|
|
3378
3811
|
}
|
|
3379
3812
|
};
|
|
3813
|
+
const targetDefId = String(instances[target]?.definitionId || "");
|
|
3814
|
+
const targetIsRunController = targetDefId === "workspace_run" || targetDefId === "workspace_scheduled_run";
|
|
3815
|
+
if (!targetIsRunController) needed.add(target);
|
|
3380
3816
|
visitControlDownstream(target);
|
|
3381
|
-
needed.delete(target);
|
|
3817
|
+
if (targetIsRunController) needed.delete(target);
|
|
3382
3818
|
const dependencyQueue = Array.from(needed);
|
|
3383
3819
|
for (let i = 0; i < dependencyQueue.length; i++) {
|
|
3384
3820
|
const id = dependencyQueue[i];
|
|
@@ -3832,6 +4268,135 @@ function workspaceBuildImplementationPrompt(instance, nodeId, opts = {}) {
|
|
|
3832
4268
|
].filter((line) => line !== "").join("\n");
|
|
3833
4269
|
}
|
|
3834
4270
|
|
|
4271
|
+
function workspaceImplementationPlanNeighbors(graph, nodeId) {
|
|
4272
|
+
const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
|
|
4273
|
+
const edges = Array.isArray(graph?.edges) ? graph.edges : [];
|
|
4274
|
+
const incoming = [];
|
|
4275
|
+
const outgoing = [];
|
|
4276
|
+
for (const edge of edges) {
|
|
4277
|
+
if (String(edge?.target || "") === String(nodeId)) {
|
|
4278
|
+
const sourceId = String(edge?.source || "");
|
|
4279
|
+
if (sourceId) incoming.push(`${sourceId} (${instances[sourceId]?.label || instances[sourceId]?.definitionId || "node"})`);
|
|
4280
|
+
}
|
|
4281
|
+
if (String(edge?.source || "") === String(nodeId)) {
|
|
4282
|
+
const targetId = String(edge?.target || "");
|
|
4283
|
+
if (targetId) outgoing.push(`${targetId} (${instances[targetId]?.label || instances[targetId]?.definitionId || "node"})`);
|
|
4284
|
+
}
|
|
4285
|
+
}
|
|
4286
|
+
return {
|
|
4287
|
+
incoming: workspaceUniqueImplementationList(incoming, 20),
|
|
4288
|
+
outgoing: workspaceUniqueImplementationList(outgoing, 20),
|
|
4289
|
+
};
|
|
4290
|
+
}
|
|
4291
|
+
|
|
4292
|
+
function workspaceBuildPlannedImplementationPrompt(graph, nodeId, opts = {}) {
|
|
4293
|
+
const instance = graph?.instances?.[nodeId] || {};
|
|
4294
|
+
const defId = String(instance?.definitionId || "").trim();
|
|
4295
|
+
const label = String(instance?.label || nodeId || "node").trim();
|
|
4296
|
+
const mode = workspaceImplementationModeForInstance(instance);
|
|
4297
|
+
const inputValues = opts.inputValues || {};
|
|
4298
|
+
const task = workspaceResolveBodyPlaceholders(instance?.body || "", inputValues).trim();
|
|
4299
|
+
const scriptRef = String(instance?.scriptRef || "").trim();
|
|
4300
|
+
const inlineScript = String(instance?.script || "").trim();
|
|
4301
|
+
const previousImplementation = opts.previousImplementation
|
|
4302
|
+
? workspaceClipImplementationText(opts.previousImplementation, 12000)
|
|
4303
|
+
: "";
|
|
4304
|
+
const neighbors = workspaceImplementationPlanNeighbors(graph, nodeId);
|
|
4305
|
+
return [
|
|
4306
|
+
"你要为 AgentFlow Workspace 的一个重复执行节点提前写“实现方案”Markdown。",
|
|
4307
|
+
"这个 implementation.md 会在后续 Scheduled Run 执行同一节点前作为参考上下文,目标是减少重复推理、提前固定执行路径和脚本约定。",
|
|
4308
|
+
"",
|
|
4309
|
+
"要求:",
|
|
4310
|
+
"- 基于当前节点任务、输入槽、上下游关系,写一份可复用的执行方案。",
|
|
4311
|
+
"- 写清楚下次运行应优先采用的步骤、文件路径、输入输出协议、错误处理和可复用约定。",
|
|
4312
|
+
"- 如果是脚本类节点,重点写清楚脚本入口、环境变量、输入 JSON/输出文件协议、幂等性和失败重试策略。",
|
|
4313
|
+
"- 不要假装已经执行过;这是执行前优化计划,不要引用不存在的实际结果。",
|
|
4314
|
+
"- 只输出 Markdown 正文,不要输出代码围栏包裹整篇。",
|
|
4315
|
+
"",
|
|
4316
|
+
"## 节点信息",
|
|
4317
|
+
"",
|
|
4318
|
+
`nodeId: ${nodeId}`,
|
|
4319
|
+
`label: ${label}`,
|
|
4320
|
+
`definitionId: ${defId || "(unknown)"}`,
|
|
4321
|
+
`mode: ${mode}`,
|
|
4322
|
+
scriptRef ? `scriptRef: ${scriptRef}` : "",
|
|
4323
|
+
inlineScript ? `inlineScript: ${workspaceClipImplementationText(inlineScript, 2400)}` : "",
|
|
4324
|
+
"",
|
|
4325
|
+
"## 当前任务",
|
|
4326
|
+
"",
|
|
4327
|
+
workspaceClipImplementationText(task || instance?.body || scriptRef || inlineScript || "(无显式任务)", 8000),
|
|
4328
|
+
"",
|
|
4329
|
+
"## 可见输入",
|
|
4330
|
+
"",
|
|
4331
|
+
JSON.stringify(inputValues || {}, null, 2),
|
|
4332
|
+
"",
|
|
4333
|
+
"## 上下游",
|
|
4334
|
+
"",
|
|
4335
|
+
`incoming: ${neighbors.incoming.length ? neighbors.incoming.join(", ") : "(none)"}`,
|
|
4336
|
+
`outgoing: ${neighbors.outgoing.length ? neighbors.outgoing.join(", ") : "(none)"}`,
|
|
4337
|
+
"",
|
|
4338
|
+
previousImplementation ? "## 上一版实现方案" : "",
|
|
4339
|
+
previousImplementation || "",
|
|
4340
|
+
].filter((line) => line !== "").join("\n");
|
|
4341
|
+
}
|
|
4342
|
+
|
|
4343
|
+
async function workspaceGeneratePlannedImplementationMarkdown({
|
|
4344
|
+
scopedRoot,
|
|
4345
|
+
graph,
|
|
4346
|
+
nodeId,
|
|
4347
|
+
inputValues,
|
|
4348
|
+
implementationPath,
|
|
4349
|
+
previousImplementation,
|
|
4350
|
+
runPackage,
|
|
4351
|
+
modelKey,
|
|
4352
|
+
userCtx,
|
|
4353
|
+
emit,
|
|
4354
|
+
onActiveChild,
|
|
4355
|
+
}) {
|
|
4356
|
+
const prompt = workspaceBuildPlannedImplementationPrompt(graph, nodeId, {
|
|
4357
|
+
inputValues,
|
|
4358
|
+
previousImplementation,
|
|
4359
|
+
});
|
|
4360
|
+
let content = "";
|
|
4361
|
+
let lastAssistant = "";
|
|
4362
|
+
let resultText = "";
|
|
4363
|
+
emit?.({ type: "status", nodeId, line: `Generate implementation plan: ${nodeId}` });
|
|
4364
|
+
const handle = startComposerAgent({
|
|
4365
|
+
uiWorkspaceRoot: scopedRoot,
|
|
4366
|
+
cliWorkspace: runPackage?.nodeRunDir || scopedRoot,
|
|
4367
|
+
prompt,
|
|
4368
|
+
modelKey,
|
|
4369
|
+
agentflowUserId: userCtx?.userId || "",
|
|
4370
|
+
extraEnv: runtimeEnvForUser(userCtx, {
|
|
4371
|
+
AGENTFLOW_IMPLEMENTATION_REF: implementationPath || "",
|
|
4372
|
+
AGENTFLOW_NODE_RUN_DIR: runPackage?.nodeRunDir || "",
|
|
4373
|
+
AGENTFLOW_NODE_TMP_DIR: runPackage?.nodeTmpDir || "",
|
|
4374
|
+
AGENTFLOW_OUTPUTS_DIR: runPackage?.outputsDir || "",
|
|
4375
|
+
}),
|
|
4376
|
+
onStreamEvent: (ev) => {
|
|
4377
|
+
if (ev?.type === "natural" && ev.kind === "assistant" && typeof ev.text === "string") {
|
|
4378
|
+
lastAssistant = ev.text;
|
|
4379
|
+
content += (content ? "\n" : "") + ev.text;
|
|
4380
|
+
} else if (ev?.type === "natural" && ev.kind === "result" && typeof ev.text === "string") {
|
|
4381
|
+
resultText = ev.text;
|
|
4382
|
+
}
|
|
4383
|
+
},
|
|
4384
|
+
onToolCall: (subtype, toolName) => {
|
|
4385
|
+
const sub = subtype ? String(subtype) : "";
|
|
4386
|
+
const tool = toolName ? String(toolName) : "";
|
|
4387
|
+
emit?.({ type: "status", nodeId, line: `优化工具 ${tool || "thinking"}${sub ? ` (${sub})` : ""}` });
|
|
4388
|
+
},
|
|
4389
|
+
});
|
|
4390
|
+
if (typeof onActiveChild === "function") onActiveChild(handle.child || null);
|
|
4391
|
+
try {
|
|
4392
|
+
await handle.finished;
|
|
4393
|
+
} finally {
|
|
4394
|
+
if (typeof onActiveChild === "function") onActiveChild(null);
|
|
4395
|
+
}
|
|
4396
|
+
const markdown = String(resultText || lastAssistant || content || "").trim();
|
|
4397
|
+
return markdown.replace(/^```(?:markdown|md)?\s*/i, "").replace(/```\s*$/i, "").trim();
|
|
4398
|
+
}
|
|
4399
|
+
|
|
3835
4400
|
async function workspaceGenerateImplementationMarkdown({
|
|
3836
4401
|
scopedRoot,
|
|
3837
4402
|
nodeId,
|
|
@@ -4033,39 +4598,125 @@ async function workspaceTryPersistNodeImplementation(scopedRoot, graph, nodeId,
|
|
|
4033
4598
|
}
|
|
4034
4599
|
}
|
|
4035
4600
|
|
|
4036
|
-
function
|
|
4037
|
-
const
|
|
4038
|
-
if (!
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
} catch {
|
|
4043
|
-
/* plain list fallback */
|
|
4044
|
-
}
|
|
4045
|
-
return text.split(/[\n,]+/).map((item) => item.trim()).filter(Boolean);
|
|
4046
|
-
}
|
|
4047
|
-
|
|
4048
|
-
function selectedSkillKeysFromInstance(instance) {
|
|
4049
|
-
const bodyKeys = parseWorkspaceSkillKeys(instance?.body || "");
|
|
4050
|
-
if (bodyKeys.length > 0) return bodyKeys;
|
|
4051
|
-
const slots = [...(Array.isArray(instance?.input) ? instance.input : []), ...(Array.isArray(instance?.output) ? instance.output : [])];
|
|
4052
|
-
const slot = slots.find((item) => item?.name === "skillsContext") || slots.find((item) => item?.name === "skillKeys");
|
|
4053
|
-
return parseWorkspaceSkillKeys(workspaceSlotValue(slot) || "");
|
|
4054
|
-
}
|
|
4055
|
-
|
|
4056
|
-
function selectedMcpServerNamesFromInstance(instance) {
|
|
4057
|
-
const bodyNames = parseWorkspaceSkillKeys(instance?.body || "");
|
|
4058
|
-
if (bodyNames.length > 0) return bodyNames;
|
|
4059
|
-
const slots = [...(Array.isArray(instance?.input) ? instance.input : []), ...(Array.isArray(instance?.output) ? instance.output : [])];
|
|
4060
|
-
const slot = slots.find((item) => item?.name === "mcpContext") || slots.find((item) => item?.name === "serverNames");
|
|
4061
|
-
return parseWorkspaceSkillKeys(workspaceSlotValue(slot) || "");
|
|
4601
|
+
function workspaceShouldOptimizeNodeImplementation(instance) {
|
|
4602
|
+
const defId = String(instance?.definitionId || "").trim();
|
|
4603
|
+
if (!defId) return false;
|
|
4604
|
+
if (defId === "workspace_run" || defId === "workspace_scheduled_run") return false;
|
|
4605
|
+
if (defId.startsWith("display_") || defId.startsWith("provide_") || defId.startsWith("control_")) return false;
|
|
4606
|
+
return defId === "agent_subAgent" || defId === "tool_nodejs" || defId.startsWith("tool_");
|
|
4062
4607
|
}
|
|
4063
4608
|
|
|
4064
|
-
function
|
|
4065
|
-
const
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4609
|
+
async function workspaceOptimizeNodeImplementation(scopedRoot, graph, nodeId, opts = {}) {
|
|
4610
|
+
const current = graph?.instances?.[nodeId];
|
|
4611
|
+
if (!current || !workspaceShouldOptimizeNodeImplementation(current)) {
|
|
4612
|
+
return { optimized: false, skipped: true, nodeId, reason: "not optimizable" };
|
|
4613
|
+
}
|
|
4614
|
+
const implementationRef = String(current.implementationRef || "").trim() || workspaceDefaultImplementationRef(nodeId);
|
|
4615
|
+
const abs = workspaceResolveFlowFile(scopedRoot, implementationRef, "implementationRef");
|
|
4616
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
4617
|
+
const previousImplementation = fs.existsSync(abs) && fs.statSync(abs).isFile()
|
|
4618
|
+
? workspaceReadTextFileIfExists(abs, 60000)
|
|
4619
|
+
: "";
|
|
4620
|
+
const inputValues = workspaceInputValues(graph, nodeId, new Map(), scopedRoot);
|
|
4621
|
+
const runPackage = workspaceCreateNodeRunPackage(opts.runTmpRoot || workspaceCreateRunTmpRoot(scopedRoot, "optimize"), nodeId, {
|
|
4622
|
+
scopedRoot,
|
|
4623
|
+
cwd: scopedRoot,
|
|
4624
|
+
task: workspaceResolveBodyPlaceholders(current.body || current.script || current.scriptRef || "", inputValues),
|
|
4625
|
+
inputValues,
|
|
4626
|
+
});
|
|
4627
|
+
const markdown = await workspaceGeneratePlannedImplementationMarkdown({
|
|
4628
|
+
scopedRoot,
|
|
4629
|
+
graph,
|
|
4630
|
+
nodeId,
|
|
4631
|
+
inputValues: { ...inputValues, ...(runPackage.inputValues || {}) },
|
|
4632
|
+
implementationPath: implementationRef,
|
|
4633
|
+
previousImplementation,
|
|
4634
|
+
runPackage,
|
|
4635
|
+
modelKey: opts.modelKey || "",
|
|
4636
|
+
userCtx: opts.userCtx || {},
|
|
4637
|
+
emit: opts.emit,
|
|
4638
|
+
onActiveChild: opts.onActiveChild,
|
|
4639
|
+
});
|
|
4640
|
+
if (!markdown.trim()) throw new Error(`Implementation plan is empty for node ${nodeId}`);
|
|
4641
|
+
fs.writeFileSync(abs, markdown.trimEnd() + "\n", "utf-8");
|
|
4642
|
+
const explicitMode = String(current.implementationMode || "").trim();
|
|
4643
|
+
graph.instances[nodeId] = {
|
|
4644
|
+
...current,
|
|
4645
|
+
implementationRef,
|
|
4646
|
+
implementationMode: explicitMode || workspaceImplementationModeForInstance(current),
|
|
4647
|
+
};
|
|
4648
|
+
return { optimized: true, nodeId, implementationRef };
|
|
4649
|
+
}
|
|
4650
|
+
|
|
4651
|
+
async function workspaceOptimizeRunImplementations(root, scopedRoot, payload, userCtx = {}, opts = {}) {
|
|
4652
|
+
const graph = hydrateWorkspaceGraphForRuntime(root, {
|
|
4653
|
+
root: scopedRoot,
|
|
4654
|
+
flowId: payload.flowId || "",
|
|
4655
|
+
flowSource: payload.flowSource || "user",
|
|
4656
|
+
archived: payload.archived === true || payload.flowArchived === true,
|
|
4657
|
+
}, payload.graph || {}, userCtx);
|
|
4658
|
+
const runNodeId = String(payload?.runNodeId || "").trim();
|
|
4659
|
+
const plan = workspaceRunPlan(graph, runNodeId, scopedRoot);
|
|
4660
|
+
const runTmpRoot = workspaceCreateRunTmpRoot(scopedRoot, `${runNodeId || "run"}-optimize`);
|
|
4661
|
+
const optimized = [];
|
|
4662
|
+
const skipped = [];
|
|
4663
|
+
for (const nodeId of plan.order) {
|
|
4664
|
+
const instance = graph.instances?.[nodeId];
|
|
4665
|
+
if (!workspaceShouldOptimizeNodeImplementation(instance)) {
|
|
4666
|
+
skipped.push({ nodeId, reason: "not optimizable" });
|
|
4667
|
+
continue;
|
|
4668
|
+
}
|
|
4669
|
+
opts.emit?.({ type: "node-start", nodeId, definitionId: instance.definitionId, phase: "optimize" });
|
|
4670
|
+
const result = await workspaceOptimizeNodeImplementation(scopedRoot, graph, nodeId, {
|
|
4671
|
+
runTmpRoot,
|
|
4672
|
+
modelKey: payload.model || "",
|
|
4673
|
+
userCtx,
|
|
4674
|
+
emit: opts.emit,
|
|
4675
|
+
onActiveChild: opts.onActiveChild,
|
|
4676
|
+
});
|
|
4677
|
+
optimized.push(result);
|
|
4678
|
+
opts.emit?.({ type: "node-done", nodeId, definitionId: instance.definitionId, phase: "optimize", implementationRef: result.implementationRef });
|
|
4679
|
+
}
|
|
4680
|
+
return { ok: true, graph, order: plan.order, optimized, skipped };
|
|
4681
|
+
}
|
|
4682
|
+
|
|
4683
|
+
function parseWorkspaceSkillKeys(raw) {
|
|
4684
|
+
const text = String(raw || "").trim();
|
|
4685
|
+
if (!text) return [];
|
|
4686
|
+
try {
|
|
4687
|
+
const parsed = JSON.parse(text);
|
|
4688
|
+
if (Array.isArray(parsed)) return parsed.map((item) => String(item || "").trim()).filter(Boolean);
|
|
4689
|
+
} catch {
|
|
4690
|
+
/* plain list fallback */
|
|
4691
|
+
}
|
|
4692
|
+
return text.split(/[\n,]+/).map((item) => item.trim()).filter(Boolean);
|
|
4693
|
+
}
|
|
4694
|
+
|
|
4695
|
+
function selectedSkillKeysFromInstance(instance) {
|
|
4696
|
+
const bodyKeys = parseWorkspaceSkillKeys(instance?.body || "");
|
|
4697
|
+
if (bodyKeys.length > 0) return bodyKeys;
|
|
4698
|
+
return selectedSkillKeysFromConfigSlots(instance);
|
|
4699
|
+
}
|
|
4700
|
+
|
|
4701
|
+
function selectedSkillKeysFromConfigSlots(instance) {
|
|
4702
|
+
const slots = [...(Array.isArray(instance?.input) ? instance.input : []), ...(Array.isArray(instance?.output) ? instance.output : [])];
|
|
4703
|
+
const slot = slots.find((item) => item?.name === "skillsContext") || slots.find((item) => item?.name === "skillKeys");
|
|
4704
|
+
return parseWorkspaceSkillKeys(workspaceSlotValue(slot) || "");
|
|
4705
|
+
}
|
|
4706
|
+
|
|
4707
|
+
function selectedMcpServerNamesFromInstance(instance) {
|
|
4708
|
+
const bodyNames = parseWorkspaceSkillKeys(instance?.body || "");
|
|
4709
|
+
if (bodyNames.length > 0) return bodyNames;
|
|
4710
|
+
const slots = [...(Array.isArray(instance?.input) ? instance.input : []), ...(Array.isArray(instance?.output) ? instance.output : [])];
|
|
4711
|
+
const slot = slots.find((item) => item?.name === "mcpContext") || slots.find((item) => item?.name === "serverNames");
|
|
4712
|
+
return parseWorkspaceSkillKeys(workspaceSlotValue(slot) || "");
|
|
4713
|
+
}
|
|
4714
|
+
|
|
4715
|
+
function workspaceUpstreamSkillBlocks(graph, nodeId, outputs) {
|
|
4716
|
+
const edges = Array.isArray(graph?.edges) ? graph.edges : [];
|
|
4717
|
+
const blocks = edges
|
|
4718
|
+
.filter((edge) => String(edge?.target || "") === String(nodeId))
|
|
4719
|
+
.filter((edge) => {
|
|
4069
4720
|
const slot = workspaceTargetSlotForEdge(graph, edge);
|
|
4070
4721
|
return String(slot?.name || "") === "skillsContext";
|
|
4071
4722
|
})
|
|
@@ -4161,6 +4812,34 @@ function workspaceNodeWorkspaceContextBlock(graph, nodeId, outputs, scopedRoot =
|
|
|
4161
4812
|
return lines.join("\n");
|
|
4162
4813
|
}
|
|
4163
4814
|
|
|
4815
|
+
function workspaceDefaultWorkspaceContextBlock(scopedRoot = "", logicalCwd = "") {
|
|
4816
|
+
const root = scopedRoot ? path.resolve(scopedRoot) : "";
|
|
4817
|
+
const cwd = logicalCwd ? path.resolve(logicalCwd) : root;
|
|
4818
|
+
if (!root && !cwd) return "";
|
|
4819
|
+
return [
|
|
4820
|
+
"## Workspace 上下文",
|
|
4821
|
+
"",
|
|
4822
|
+
"当前 Agent 仍在独立节点目录中运行,文件边界以“文件边界”章节为准。",
|
|
4823
|
+
cwd ? `- 当前工作目录上下文:\`${cwd}\`` : "",
|
|
4824
|
+
root ? `- 流程目录:\`${root}\`` : "",
|
|
4825
|
+
"",
|
|
4826
|
+
"使用要求:",
|
|
4827
|
+
"- 读取、搜索、分析当前项目或资料时,优先从“当前工作目录上下文”开始;不要把节点的“当前执行目录”误认为项目根目录。",
|
|
4828
|
+
"- 临时文件和正式产物仍必须按“文件边界”写入本节点的 `tmp/` 与 `outputs/`。",
|
|
4829
|
+
].filter((line) => line !== "").join("\n");
|
|
4830
|
+
}
|
|
4831
|
+
|
|
4832
|
+
function isWorkspaceOneClickTaskDefinitionId(definitionId) {
|
|
4833
|
+
const id = String(definitionId || "");
|
|
4834
|
+
return id === "workspace_one_click_task" || id === "workspace_context_run";
|
|
4835
|
+
}
|
|
4836
|
+
|
|
4837
|
+
function workspaceContextRunDisplayKind(instance) {
|
|
4838
|
+
const raw = workspaceSlotValue(workspaceSlotByName(instance, "displayType")).trim().toLowerCase();
|
|
4839
|
+
if (["markdown", "html", "react", "table", "chart", "ascii", "mermaid"].includes(raw)) return raw;
|
|
4840
|
+
return "markdown";
|
|
4841
|
+
}
|
|
4842
|
+
|
|
4164
4843
|
function mergeWorkspaceSkillBlocks(...values) {
|
|
4165
4844
|
const blocks = values
|
|
4166
4845
|
.map((value) => String(value || ""))
|
|
@@ -4669,6 +5348,12 @@ async function workspaceRunToolNodejsScript({
|
|
|
4669
5348
|
});
|
|
4670
5349
|
}
|
|
4671
5350
|
|
|
5351
|
+
function workspaceNodeModelKey(instance, fallback = "") {
|
|
5352
|
+
const own = String(instance?.model || "").trim();
|
|
5353
|
+
if (own && own !== "default") return own;
|
|
5354
|
+
return String(fallback || "").trim();
|
|
5355
|
+
}
|
|
5356
|
+
|
|
4672
5357
|
async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts = {}) {
|
|
4673
5358
|
const graph = hydrateWorkspaceGraphForRuntime(root, {
|
|
4674
5359
|
root: scopedRoot,
|
|
@@ -4911,17 +5596,49 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
4911
5596
|
const inputSlots = Array.isArray(instance.input) ? instance.input : [];
|
|
4912
5597
|
const pathSlot = inputSlots.find((slot) => String(slot?.name || "") === "path") ||
|
|
4913
5598
|
inputSlots.find((slot) => String(slot?.name || "") === "target");
|
|
5599
|
+
const labelSlot = inputSlots.find((slot) => String(slot?.name || "") === "label");
|
|
5600
|
+
const contextSlot = inputSlots.find((slot) => String(slot?.name || "") === "workspaceContext");
|
|
4914
5601
|
const candidate = workspaceSlotValue(pathSlot) || workspaceInstanceText(instance) || inputText;
|
|
4915
5602
|
const abs = candidate ? path.resolve(scopedRoot, candidate) : scopedRoot;
|
|
4916
|
-
if (fs.existsSync(abs)
|
|
4917
|
-
|
|
5603
|
+
if (!fs.existsSync(abs) || !fs.statSync(abs).isDirectory()) {
|
|
5604
|
+
throw new Error(`Load Workspace path does not exist or is not a directory: ${abs}`);
|
|
5605
|
+
}
|
|
5606
|
+
cwd = abs;
|
|
5607
|
+
const parsedContext = parseJsonText(workspaceSlotValue(contextSlot), {});
|
|
5608
|
+
const configuredContext = parsedContext && typeof parsedContext === "object" && !Array.isArray(parsedContext) ? parsedContext : {};
|
|
5609
|
+
const workspaceContext = {
|
|
5610
|
+
...configuredContext,
|
|
5611
|
+
version: 1,
|
|
5612
|
+
label: workspaceSlotValue(labelSlot) || path.basename(cwd) || "workspace",
|
|
5613
|
+
cwd,
|
|
5614
|
+
workspaceRoot: cwd,
|
|
5615
|
+
pipelineWorkspace: path.resolve(scopedRoot),
|
|
5616
|
+
previous: null,
|
|
5617
|
+
};
|
|
5618
|
+
let nextInstance = workspaceSetOutputSlot(instance, "workspaceContext", JSON.stringify(workspaceContext));
|
|
5619
|
+
nextInstance = workspaceSetOutputSlot(nextInstance, "cwd", cwd);
|
|
5620
|
+
graph.instances[nodeId] = nextInstance;
|
|
5621
|
+
publishNodeOutput(nodeId, JSON.stringify(workspaceContext), { emitGraph: true });
|
|
5622
|
+
emit({ type: "graph", nodeId, graph });
|
|
4918
5623
|
emit({ type: "node-done", nodeId, definitionId: defId });
|
|
4919
5624
|
continue;
|
|
4920
5625
|
}
|
|
4921
5626
|
|
|
4922
5627
|
if (defId === "control_user_workspace") {
|
|
4923
5628
|
cwd = path.resolve(os.homedir());
|
|
4924
|
-
|
|
5629
|
+
const workspaceContext = {
|
|
5630
|
+
version: 1,
|
|
5631
|
+
label: "home",
|
|
5632
|
+
cwd,
|
|
5633
|
+
workspaceRoot: cwd,
|
|
5634
|
+
pipelineWorkspace: path.resolve(scopedRoot),
|
|
5635
|
+
previous: null,
|
|
5636
|
+
};
|
|
5637
|
+
let nextInstance = workspaceSetOutputSlot(instance, "workspaceContext", JSON.stringify(workspaceContext));
|
|
5638
|
+
nextInstance = workspaceSetOutputSlot(nextInstance, "cwd", cwd);
|
|
5639
|
+
graph.instances[nodeId] = nextInstance;
|
|
5640
|
+
publishNodeOutput(nodeId, JSON.stringify(workspaceContext), { emitGraph: true });
|
|
5641
|
+
emit({ type: "graph", nodeId, graph });
|
|
4925
5642
|
emit({ type: "node-done", nodeId, definitionId: defId });
|
|
4926
5643
|
continue;
|
|
4927
5644
|
}
|
|
@@ -5180,6 +5897,10 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
5180
5897
|
title,
|
|
5181
5898
|
layout,
|
|
5182
5899
|
nodeIds,
|
|
5900
|
+
expiresMode: payload.expiresMode,
|
|
5901
|
+
expiresInDays: payload.expiresInDays,
|
|
5902
|
+
permanent: payload.permanent,
|
|
5903
|
+
expiresAt: payload.expiresAt,
|
|
5183
5904
|
});
|
|
5184
5905
|
const url = displayShareOutputUrl(share.id, baseUrl);
|
|
5185
5906
|
let nextInstance = workspaceSetOutputSlot(instance, "url", url);
|
|
@@ -5193,6 +5914,7 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
5193
5914
|
}
|
|
5194
5915
|
|
|
5195
5916
|
if (defId === "tool_nodejs") {
|
|
5917
|
+
const nodeModelKey = workspaceNodeModelKey(instance, modelKey);
|
|
5196
5918
|
const prepareStartedAt = Date.now();
|
|
5197
5919
|
const inputValues = workspaceInputValues(graph, nodeId, outputs, scopedRoot);
|
|
5198
5920
|
const runPackage = workspaceCreateNodeRunPackage(runTmpRoot, nodeId, {
|
|
@@ -5226,7 +5948,7 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
5226
5948
|
resultContent,
|
|
5227
5949
|
structured: normalizedAgentOutput,
|
|
5228
5950
|
runPackage,
|
|
5229
|
-
modelKey,
|
|
5951
|
+
modelKey: nodeModelKey,
|
|
5230
5952
|
userCtx,
|
|
5231
5953
|
emit: (event) => emit({ ...event, nodeId }),
|
|
5232
5954
|
onActiveChild: opts.onActiveChild,
|
|
@@ -5238,12 +5960,15 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
5238
5960
|
continue;
|
|
5239
5961
|
}
|
|
5240
5962
|
|
|
5963
|
+
const isContextRunNode = isWorkspaceOneClickTaskDefinitionId(defId);
|
|
5964
|
+
const nodeModelKey = workspaceNodeModelKey(instance, modelKey);
|
|
5241
5965
|
const prepareStartedAt = Date.now();
|
|
5242
5966
|
const inputValues = workspaceInputValues(graph, nodeId, outputs, scopedRoot);
|
|
5243
5967
|
const relevantInputs = workspaceRelevantInputValues(instance.body || "", inputValues);
|
|
5244
5968
|
const upstreamText = workspaceTaskUpstreamText(graph, nodeId, outputs, relevantInputs.placeholders, scopedRoot);
|
|
5245
5969
|
const upstreamSkillBlocks = workspaceUpstreamSkillBlocks(graph, nodeId, outputs);
|
|
5246
|
-
const
|
|
5970
|
+
const ownSkillBlock = isContextRunNode ? loadSkillsBlockForKeys(selectedSkillKeysFromConfigSlots(instance)) : "";
|
|
5971
|
+
const promptSkillsBlock = mergeWorkspaceSkillBlocks(ownSkillBlock, upstreamSkillBlocks);
|
|
5247
5972
|
const promptMcpBlock = workspaceUpstreamMcpBlocks(graph, nodeId, outputs);
|
|
5248
5973
|
const runPackage = workspaceCreateNodeRunPackage(runTmpRoot, nodeId, {
|
|
5249
5974
|
scopedRoot,
|
|
@@ -5265,7 +5990,10 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
5265
5990
|
// Best-effort debug artifact only.
|
|
5266
5991
|
}
|
|
5267
5992
|
const historyBlock = workspaceNodeHistoryBlock(nodeId, scopedRoot, runPackage);
|
|
5268
|
-
|
|
5993
|
+
let workspaceContextBlock = workspaceNodeWorkspaceContextBlock(graph, nodeId, outputs, scopedRoot, cwd);
|
|
5994
|
+
if (isContextRunNode && workspaceBoolSlot(instance, "includeWorkspaceContext", true) && !workspaceContextBlock) {
|
|
5995
|
+
workspaceContextBlock = workspaceDefaultWorkspaceContextBlock(scopedRoot, cwd);
|
|
5996
|
+
}
|
|
5269
5997
|
const prompt = workspaceNodePrompt(graph, nodeId, promptUpstreamText, promptSkillsBlock, promptMcpBlock, runtimeInputValues, runPackage, historyBlock, workspaceContextBlock);
|
|
5270
5998
|
try {
|
|
5271
5999
|
fs.writeFileSync(path.join(runPackage.nodeRunDir, "prompt.md"), prompt.trimEnd() + "\n", "utf-8");
|
|
@@ -5280,6 +6008,7 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
5280
6008
|
nodeRunDir: runPackage.nodeRunDir,
|
|
5281
6009
|
});
|
|
5282
6010
|
emit({ type: "natural", kind: "prompt", nodeId, text: prompt });
|
|
6011
|
+
emit({ type: "status", nodeId, line: `Model: ${nodeModelKey || "default"}` });
|
|
5283
6012
|
let content = "";
|
|
5284
6013
|
const runHistoryEvents = [];
|
|
5285
6014
|
const maxAttempts = 3;
|
|
@@ -5294,7 +6023,7 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
5294
6023
|
uiWorkspaceRoot: scopedRoot,
|
|
5295
6024
|
cliWorkspace: runPackage.nodeRunDir,
|
|
5296
6025
|
prompt,
|
|
5297
|
-
modelKey,
|
|
6026
|
+
modelKey: nodeModelKey,
|
|
5298
6027
|
agentflowUserId: userCtx.userId || "",
|
|
5299
6028
|
extraEnv: runtimeEnv({
|
|
5300
6029
|
AGENTFLOW_WORKSPACE_TMP_ROOT: runTmpRoot,
|
|
@@ -5364,15 +6093,20 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
5364
6093
|
resultContent,
|
|
5365
6094
|
structured: normalizedAgentOutput,
|
|
5366
6095
|
runPackage,
|
|
5367
|
-
modelKey,
|
|
6096
|
+
modelKey: nodeModelKey,
|
|
5368
6097
|
userCtx,
|
|
5369
6098
|
historyEvents: runHistoryEvents,
|
|
5370
6099
|
emit: (event) => emit({ ...event, nodeId }),
|
|
5371
6100
|
onActiveChild: opts.onActiveChild,
|
|
5372
6101
|
});
|
|
5373
6102
|
if (implementationUpdate.changed) graph.instances[nodeId] = implementationUpdate.instance;
|
|
6103
|
+
let contextRunOutputChanged = false;
|
|
6104
|
+
if (isContextRunNode) {
|
|
6105
|
+
graph.instances[nodeId] = workspaceSetOutputSlot(graph.instances[nodeId] || instance, "displayType", workspaceContextRunDisplayKind(instance));
|
|
6106
|
+
contextRunOutputChanged = true;
|
|
6107
|
+
}
|
|
5374
6108
|
const updatedDisplays = propagateNodeOutputDisplays(nodeId, resultContent);
|
|
5375
|
-
if (slotUpdate.changed || implementationUpdate.changed || updatedDisplays.length) emit({ type: "graph", nodeId, displayNodeIds: updatedDisplays, graph });
|
|
6109
|
+
if (slotUpdate.changed || implementationUpdate.changed || contextRunOutputChanged || updatedDisplays.length) emit({ type: "graph", nodeId, displayNodeIds: updatedDisplays, graph });
|
|
5376
6110
|
emit({ type: "node-done", nodeId, definitionId: defId });
|
|
5377
6111
|
}
|
|
5378
6112
|
} finally {
|
|
@@ -5570,7 +6304,7 @@ const activeFlowRuns = new Map();
|
|
|
5570
6304
|
const activeWorkspaceRuns = new Map();
|
|
5571
6305
|
const WORKSPACE_SCHEDULES_FILENAME = "workspace-schedules.json";
|
|
5572
6306
|
const WORKSPACE_SCHEDULE_POLL_MS = 30_000;
|
|
5573
|
-
const WORKSPACE_IMPLEMENTATION_REFERENCE_ENABLED =
|
|
6307
|
+
const WORKSPACE_IMPLEMENTATION_REFERENCE_ENABLED = true;
|
|
5574
6308
|
const WORKSPACE_IMPLEMENTATION_SUMMARY_ENABLED = false;
|
|
5575
6309
|
const WORKSPACE_NODE_HISTORY_MAX_CHARS = 80000;
|
|
5576
6310
|
|
|
@@ -5592,6 +6326,13 @@ function workspaceRunEntryKey(scopeKey, runId) {
|
|
|
5592
6326
|
return `${scopeKey}:${String(runId || "").trim() || runLedgerId("workspace")}`;
|
|
5593
6327
|
}
|
|
5594
6328
|
|
|
6329
|
+
function workspaceRuntimeNodeLabel(graph, nodeId, fallback = "Workspace Run") {
|
|
6330
|
+
const id = String(nodeId || "").trim();
|
|
6331
|
+
const instance = graph?.instances && typeof graph.instances === "object" ? graph.instances[id] : null;
|
|
6332
|
+
const label = String(instance?.label || "").trim();
|
|
6333
|
+
return label || id || fallback;
|
|
6334
|
+
}
|
|
6335
|
+
|
|
5595
6336
|
function workspaceActiveRunsForScope(scopeKey) {
|
|
5596
6337
|
const key = String(scopeKey || "");
|
|
5597
6338
|
return Array.from(activeWorkspaceRuns.entries())
|
|
@@ -5706,6 +6447,77 @@ function listWorkspaceScheduleStatusesForFlow(userCtx = {}, flowSource = "user",
|
|
|
5706
6447
|
.sort((a, b) => String(a.scheduleNodeId || a.runNodeId || "").localeCompare(String(b.scheduleNodeId || b.runNodeId || "")));
|
|
5707
6448
|
}
|
|
5708
6449
|
|
|
6450
|
+
function listWorkspaceScheduleStatuses(root, userCtx = {}) {
|
|
6451
|
+
const registry = readWorkspaceScheduleRegistry();
|
|
6452
|
+
const userId = String(userCtx.userId || "");
|
|
6453
|
+
const flows = listFlowsJson(root, { ...userCtx, includeWorkspaceFlows: true })
|
|
6454
|
+
.filter((flow) => !flow.archived && !isReadonlyBuiltinFlowSource(flow.source || "user"));
|
|
6455
|
+
const rows = [];
|
|
6456
|
+
for (const flow of flows) {
|
|
6457
|
+
const flowId = String(flow.id || "");
|
|
6458
|
+
const flowSource = String(flow.source || "user");
|
|
6459
|
+
const scoped = resolveWorkspaceScopeRoot(root, { flowId, flowSource }, userCtx);
|
|
6460
|
+
if (scoped.error || !scoped.root) continue;
|
|
6461
|
+
let graph;
|
|
6462
|
+
try {
|
|
6463
|
+
graph = readWorkspaceGraph(scoped.root).graph;
|
|
6464
|
+
} catch {
|
|
6465
|
+
continue;
|
|
6466
|
+
}
|
|
6467
|
+
const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
|
|
6468
|
+
for (const [scheduleNodeId, instance] of Object.entries(instances)) {
|
|
6469
|
+
if (String(instance?.definitionId || "") !== "workspace_scheduled_run") continue;
|
|
6470
|
+
const config = normalizeWorkspaceScheduledRunConfig(instance.body || "");
|
|
6471
|
+
const key = workspaceScheduleKey(userId, flowSource, flowId, scheduleNodeId);
|
|
6472
|
+
const current = registry.schedules?.[key] && typeof registry.schedules[key] === "object" ? registry.schedules[key] : {};
|
|
6473
|
+
const targetRunNodeId = workspaceScheduleInferTargetRunNodeId(graph, scheduleNodeId, config);
|
|
6474
|
+
const scopeKey = workspaceRunKey(userCtx, flowSource, flowId);
|
|
6475
|
+
const running = workspaceActiveRunsForScope(scopeKey).some(([, active]) => (
|
|
6476
|
+
active?.scheduled === true &&
|
|
6477
|
+
String(active?.runNodeId || "") === String(targetRunNodeId || scheduleNodeId)
|
|
6478
|
+
));
|
|
6479
|
+
let nextRunAt = current.nextRunAt || null;
|
|
6480
|
+
let lastStatus = current.lastStatus || (config.enabled ? "armed" : "disabled");
|
|
6481
|
+
let lastError = current.lastError || "";
|
|
6482
|
+
if (config.enabled && !nextRunAt) {
|
|
6483
|
+
try {
|
|
6484
|
+
nextRunAt = workspaceScheduleNextRunAt(config, new Date());
|
|
6485
|
+
} catch (e) {
|
|
6486
|
+
lastStatus = "invalid";
|
|
6487
|
+
lastError = (e && e.message) || String(e);
|
|
6488
|
+
}
|
|
6489
|
+
}
|
|
6490
|
+
rows.push({
|
|
6491
|
+
kind: "workspace",
|
|
6492
|
+
key,
|
|
6493
|
+
flowId,
|
|
6494
|
+
flowSource,
|
|
6495
|
+
scheduleNodeId,
|
|
6496
|
+
runNodeId: targetRunNodeId,
|
|
6497
|
+
label: String(instance.label || "Scheduled Run"),
|
|
6498
|
+
enabled: config.enabled,
|
|
6499
|
+
cron: config.cron,
|
|
6500
|
+
timezone: config.timezone,
|
|
6501
|
+
preset: "",
|
|
6502
|
+
nextRunAt,
|
|
6503
|
+
lastTriggeredAt: current.lastTriggeredAt || null,
|
|
6504
|
+
lastFinishedAt: current.lastFinishedAt || null,
|
|
6505
|
+
lastRunId: current.lastRunId || "",
|
|
6506
|
+
lastStatus,
|
|
6507
|
+
lastError,
|
|
6508
|
+
running,
|
|
6509
|
+
waiting: 0,
|
|
6510
|
+
});
|
|
6511
|
+
}
|
|
6512
|
+
}
|
|
6513
|
+
rows.sort((a, b) => {
|
|
6514
|
+
const ea = a.enabled ? 0 : 1;
|
|
6515
|
+
const eb = b.enabled ? 0 : 1;
|
|
6516
|
+
return ea - eb || String(a.nextRunAt || "").localeCompare(String(b.nextRunAt || "")) || a.flowId.localeCompare(b.flowId);
|
|
6517
|
+
});
|
|
6518
|
+
return rows;
|
|
6519
|
+
}
|
|
6520
|
+
|
|
5709
6521
|
function workspaceScheduleInferTargetRunNodeId(graph, scheduleNodeId, config = {}) {
|
|
5710
6522
|
const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
|
|
5711
6523
|
return String(instances[scheduleNodeId]?.definitionId || "") === "workspace_scheduled_run"
|
|
@@ -5713,6 +6525,35 @@ function workspaceScheduleInferTargetRunNodeId(graph, scheduleNodeId, config = {
|
|
|
5713
6525
|
: "";
|
|
5714
6526
|
}
|
|
5715
6527
|
|
|
6528
|
+
function setWorkspaceScheduleEnabled(root, payload = {}, authUser = {}, userCtx = {}) {
|
|
6529
|
+
const flowId = String(payload.flowId || "").trim();
|
|
6530
|
+
const flowSource = String(payload.flowSource || "user").trim() || "user";
|
|
6531
|
+
const scheduleNodeId = String(payload.scheduleNodeId || "").trim();
|
|
6532
|
+
if (!flowId) return { success: false, error: "Missing flowId" };
|
|
6533
|
+
if (!scheduleNodeId) return { success: false, error: "Missing scheduleNodeId" };
|
|
6534
|
+
if (!isValidFlowSourceWrite(flowSource)) return { success: false, error: "Cannot update readonly workspace schedule" };
|
|
6535
|
+
const scoped = resolveWorkspaceScopeRoot(root, { flowId, flowSource }, userCtx);
|
|
6536
|
+
if (scoped.error) return { success: false, error: scoped.error };
|
|
6537
|
+
if (scoped.archived || isReadonlyBuiltinFlowSource(scoped.flowSource)) {
|
|
6538
|
+
return { success: false, error: "Cannot update schedule for builtin or archived workspace" };
|
|
6539
|
+
}
|
|
6540
|
+
const { graph } = readWorkspaceGraph(scoped.root);
|
|
6541
|
+
const instance = graph.instances?.[scheduleNodeId];
|
|
6542
|
+
if (!instance || String(instance.definitionId || "") !== "workspace_scheduled_run") {
|
|
6543
|
+
return { success: false, error: "Workspace schedule node not found" };
|
|
6544
|
+
}
|
|
6545
|
+
const config = normalizeWorkspaceScheduledRunConfig(instance.body || "");
|
|
6546
|
+
const nextConfig = { ...config, enabled: payload.enabled === true };
|
|
6547
|
+
graph.instances = { ...(graph.instances || {}) };
|
|
6548
|
+
graph.instances[scheduleNodeId] = {
|
|
6549
|
+
...instance,
|
|
6550
|
+
body: JSON.stringify(nextConfig),
|
|
6551
|
+
};
|
|
6552
|
+
fs.writeFileSync(workspaceGraphPath(scoped.root), JSON.stringify(graph, null, 2) + "\n", "utf-8");
|
|
6553
|
+
const workspaceSchedules = syncWorkspaceSchedulesForGraph(root, scoped, graph, authUser, userCtx);
|
|
6554
|
+
return { success: true, workspaceSchedules };
|
|
6555
|
+
}
|
|
6556
|
+
|
|
5716
6557
|
function syncWorkspaceSchedulesForGraph(root, scoped, graph, authUser, userCtx = {}) {
|
|
5717
6558
|
const flowId = String(scoped?.flowId || "").trim();
|
|
5718
6559
|
const flowSource = String(scoped?.flowSource || "user");
|
|
@@ -5730,7 +6571,6 @@ function syncWorkspaceSchedulesForGraph(root, scoped, graph, authUser, userCtx =
|
|
|
5730
6571
|
for (const [scheduleNodeId, instance] of Object.entries(instances)) {
|
|
5731
6572
|
if (String(instance?.definitionId || "") !== "workspace_scheduled_run") continue;
|
|
5732
6573
|
const config = normalizeWorkspaceScheduledRunConfig(instance.body || "");
|
|
5733
|
-
if (!config.enabled) continue;
|
|
5734
6574
|
const targetRunNodeId = workspaceScheduleInferTargetRunNodeId(graph, scheduleNodeId, config);
|
|
5735
6575
|
const key = workspaceScheduleKey(userId, flowSource, flowId, scheduleNodeId);
|
|
5736
6576
|
const previous = registry.schedules?.[key] && typeof registry.schedules[key] === "object" ? registry.schedules[key] : {};
|
|
@@ -5744,9 +6584,15 @@ function syncWorkspaceSchedulesForGraph(root, scoped, graph, authUser, userCtx =
|
|
|
5744
6584
|
let lastStatus = previous.lastStatus || "armed";
|
|
5745
6585
|
let lastError = previous.lastError || "";
|
|
5746
6586
|
try {
|
|
5747
|
-
nextRunAt =
|
|
6587
|
+
nextRunAt = !config.enabled
|
|
6588
|
+
? null
|
|
6589
|
+
: previousMatches && Number.isFinite(previousNext) && previousNext > now
|
|
5748
6590
|
? previousNext
|
|
5749
6591
|
: workspaceScheduleNextRunAt(config, new Date(now));
|
|
6592
|
+
if (!config.enabled) {
|
|
6593
|
+
lastStatus = "disabled";
|
|
6594
|
+
lastError = "";
|
|
6595
|
+
}
|
|
5750
6596
|
if (!targetRunNodeId) {
|
|
5751
6597
|
lastStatus = "invalid";
|
|
5752
6598
|
lastError = "Scheduled Run node is missing";
|
|
@@ -5759,7 +6605,7 @@ function syncWorkspaceSchedulesForGraph(root, scoped, graph, authUser, userCtx =
|
|
|
5759
6605
|
schedules[key] = {
|
|
5760
6606
|
...previous,
|
|
5761
6607
|
key,
|
|
5762
|
-
enabled:
|
|
6608
|
+
enabled: config.enabled,
|
|
5763
6609
|
userId,
|
|
5764
6610
|
username: String(authUser?.username || previous.username || userId),
|
|
5765
6611
|
flowId,
|
|
@@ -5799,6 +6645,21 @@ function updateWorkspaceScheduleEntry(key, patch) {
|
|
|
5799
6645
|
async function runWorkspaceScheduledEntry(root, entry) {
|
|
5800
6646
|
const userCtx = { userId: String(entry.userId || "") };
|
|
5801
6647
|
const scopeKey = workspaceRunKey(userCtx, entry.flowSource || "user", entry.flowId || "");
|
|
6648
|
+
const authUsers = readAuthUsers();
|
|
6649
|
+
const authUser = authUsers[userCtx.userId] || {};
|
|
6650
|
+
const runId = runLedgerId("workspace");
|
|
6651
|
+
const runLog = createWorkspaceRunLogSession({
|
|
6652
|
+
runId,
|
|
6653
|
+
userId: userCtx.userId,
|
|
6654
|
+
username: String(authUser.username || entry.username || userCtx.userId),
|
|
6655
|
+
flowId: String(entry.flowId || ""),
|
|
6656
|
+
flowSource: String(entry.flowSource || "user"),
|
|
6657
|
+
scheduleNodeId: String(entry.scheduleNodeId || entry.key?.split(":").pop() || ""),
|
|
6658
|
+
runNodeId: String(entry.targetRunNodeId || entry.runNodeId || ""),
|
|
6659
|
+
scheduled: true,
|
|
6660
|
+
trigger: "scheduled",
|
|
6661
|
+
label: String(entry.label || "Scheduled Run"),
|
|
6662
|
+
});
|
|
5802
6663
|
const fallbackConfig = {
|
|
5803
6664
|
enabled: true,
|
|
5804
6665
|
cron: String(entry.cron || "0 9 * * *"),
|
|
@@ -5819,10 +6680,14 @@ async function runWorkspaceScheduledEntry(root, entry) {
|
|
|
5819
6680
|
flowSource: entry.flowSource || "user",
|
|
5820
6681
|
}, userCtx);
|
|
5821
6682
|
if (scoped.error || scoped.archived || isReadonlyBuiltinFlowSource(scoped.flowSource)) {
|
|
6683
|
+
const error = scoped.error || "Workspace schedule target is not writable";
|
|
6684
|
+
appendWorkspaceRunLogEvent(runLog.runId, { type: "error", error });
|
|
6685
|
+
finishWorkspaceRunLogSession(runLog.runId, "failed", { error });
|
|
5822
6686
|
updateWorkspaceScheduleEntry(entry.key, {
|
|
5823
6687
|
nextRunAt,
|
|
5824
6688
|
lastStatus: "error",
|
|
5825
|
-
|
|
6689
|
+
lastRunId: runLog.runId,
|
|
6690
|
+
lastError: error,
|
|
5826
6691
|
lastErrorAt: Date.now(),
|
|
5827
6692
|
});
|
|
5828
6693
|
return;
|
|
@@ -5834,31 +6699,44 @@ async function runWorkspaceScheduledEntry(root, entry) {
|
|
|
5834
6699
|
const config = normalizeWorkspaceScheduledRunConfig(instance?.body || "");
|
|
5835
6700
|
nextRunAt = computeNext(config);
|
|
5836
6701
|
if (!instance || String(instance.definitionId || "") !== "workspace_scheduled_run" || !config.enabled) {
|
|
6702
|
+
appendWorkspaceRunLogEvent(runLog.runId, { type: "disabled", scheduleNodeId });
|
|
6703
|
+
finishWorkspaceRunLogSession(runLog.runId, "disabled");
|
|
5837
6704
|
updateWorkspaceScheduleEntry(entry.key, {
|
|
5838
6705
|
enabled: false,
|
|
5839
6706
|
nextRunAt: null,
|
|
5840
6707
|
lastStatus: "disabled",
|
|
6708
|
+
lastRunId: runLog.runId,
|
|
5841
6709
|
});
|
|
5842
6710
|
return;
|
|
5843
6711
|
}
|
|
5844
6712
|
const targetRunNodeId = workspaceScheduleInferTargetRunNodeId(graph, scheduleNodeId, config);
|
|
6713
|
+
const scheduleAlias = workspaceRuntimeNodeLabel(graph, scheduleNodeId, String(entry.label || "Scheduled Run"));
|
|
5845
6714
|
if (!targetRunNodeId) {
|
|
6715
|
+
const error = "Scheduled Run node is missing";
|
|
6716
|
+
appendWorkspaceRunLogEvent(runLog.runId, { type: "invalid", error, scheduleNodeId });
|
|
6717
|
+
finishWorkspaceRunLogSession(runLog.runId, "failed", { error });
|
|
5846
6718
|
updateWorkspaceScheduleEntry(entry.key, {
|
|
5847
6719
|
nextRunAt,
|
|
5848
6720
|
lastStatus: "invalid",
|
|
5849
|
-
|
|
6721
|
+
lastRunId: runLog.runId,
|
|
6722
|
+
lastError: error,
|
|
5850
6723
|
lastErrorAt: Date.now(),
|
|
5851
6724
|
});
|
|
5852
6725
|
return;
|
|
5853
6726
|
}
|
|
6727
|
+
appendWorkspaceRunLogEvent(runLog.runId, { type: "scheduler-triggered", scheduleNodeId, runNodeId: targetRunNodeId, cron: config.cron, timezone: config.timezone });
|
|
5854
6728
|
let plan;
|
|
5855
6729
|
try {
|
|
5856
6730
|
plan = workspaceRunPlan(graph, targetRunNodeId, scoped.root);
|
|
5857
6731
|
} catch (e) {
|
|
6732
|
+
const error = (e && e.message) || String(e);
|
|
6733
|
+
appendWorkspaceRunLogEvent(runLog.runId, { type: "error", error });
|
|
6734
|
+
finishWorkspaceRunLogSession(runLog.runId, "failed", { error, runNodeId: targetRunNodeId });
|
|
5858
6735
|
updateWorkspaceScheduleEntry(entry.key, {
|
|
5859
6736
|
nextRunAt,
|
|
5860
6737
|
lastStatus: "failed",
|
|
5861
|
-
|
|
6738
|
+
lastRunId: runLog.runId,
|
|
6739
|
+
lastError: error,
|
|
5862
6740
|
lastErrorAt: Date.now(),
|
|
5863
6741
|
});
|
|
5864
6742
|
return;
|
|
@@ -5866,19 +6744,25 @@ async function runWorkspaceScheduledEntry(root, entry) {
|
|
|
5866
6744
|
const plannedNodeIds = workspaceRunPlanNodeIds(targetRunNodeId, plan);
|
|
5867
6745
|
const conflict = workspaceFindActiveRunConflict(scopeKey, plannedNodeIds);
|
|
5868
6746
|
if (conflict) {
|
|
6747
|
+
appendWorkspaceRunLogEvent(runLog.runId, {
|
|
6748
|
+
type: "skipped",
|
|
6749
|
+
reason: "busy",
|
|
6750
|
+
runNodeId: targetRunNodeId,
|
|
6751
|
+
conflictRunId: conflict.entry?.runId || "",
|
|
6752
|
+
conflictNodeIds: conflict.conflictNodeIds,
|
|
6753
|
+
});
|
|
6754
|
+
finishWorkspaceRunLogSession(runLog.runId, "skipped", { runNodeId: targetRunNodeId, error: "" });
|
|
5869
6755
|
updateWorkspaceScheduleEntry(entry.key, {
|
|
5870
6756
|
nextRunAt,
|
|
5871
6757
|
lastSkippedAt: Date.now(),
|
|
5872
6758
|
lastStatus: "skipped: busy",
|
|
6759
|
+
lastRunId: runLog.runId,
|
|
5873
6760
|
lastError: "",
|
|
5874
6761
|
});
|
|
5875
6762
|
return;
|
|
5876
6763
|
}
|
|
5877
6764
|
|
|
5878
6765
|
const controller = new AbortController();
|
|
5879
|
-
const authUsers = readAuthUsers();
|
|
5880
|
-
const authUser = authUsers[userCtx.userId] || {};
|
|
5881
|
-
const runId = runLedgerId("workspace");
|
|
5882
6766
|
const runKey = workspaceRunEntryKey(scopeKey, runId);
|
|
5883
6767
|
const runEntry = {
|
|
5884
6768
|
scopeKey,
|
|
@@ -5890,6 +6774,7 @@ async function runWorkspaceScheduledEntry(root, entry) {
|
|
|
5890
6774
|
runNodeId: targetRunNodeId,
|
|
5891
6775
|
flowId: String(entry.flowId || ""),
|
|
5892
6776
|
flowSource: String(entry.flowSource || "user"),
|
|
6777
|
+
label: scheduleAlias,
|
|
5893
6778
|
plannedNodeIds,
|
|
5894
6779
|
startedAt: Date.now(),
|
|
5895
6780
|
scheduled: true,
|
|
@@ -5924,6 +6809,7 @@ async function runWorkspaceScheduledEntry(root, entry) {
|
|
|
5924
6809
|
}, userCtx, {
|
|
5925
6810
|
signal: controller.signal,
|
|
5926
6811
|
onActiveChild: setActiveChild,
|
|
6812
|
+
onEvent: (event) => appendWorkspaceRunLogEvent(runLog.runId, event),
|
|
5927
6813
|
});
|
|
5928
6814
|
const currentGraph = readWorkspaceGraph(scoped.root).graph;
|
|
5929
6815
|
const touchedIds = workspaceRunTouchedNodeIds(result);
|
|
@@ -5931,6 +6817,11 @@ async function runWorkspaceScheduledEntry(root, entry) {
|
|
|
5931
6817
|
fs.writeFileSync(graphPath, JSON.stringify(mergedGraph, null, 2) + "\n", "utf-8");
|
|
5932
6818
|
const endedAt = Date.now();
|
|
5933
6819
|
appendWorkspaceRunFinished({ ...runEntry, endedAt, durationMs: endedAt - runEntry.startedAt }, "success");
|
|
6820
|
+
finishWorkspaceRunLogSession(runLog.runId, "success", {
|
|
6821
|
+
endedAt,
|
|
6822
|
+
durationMs: endedAt - runEntry.startedAt,
|
|
6823
|
+
runNodeId: targetRunNodeId,
|
|
6824
|
+
});
|
|
5934
6825
|
updateWorkspaceScheduleEntry(entry.key, {
|
|
5935
6826
|
nextRunAt: computeNext(config),
|
|
5936
6827
|
lastFinishedAt: endedAt,
|
|
@@ -5939,15 +6830,23 @@ async function runWorkspaceScheduledEntry(root, entry) {
|
|
|
5939
6830
|
});
|
|
5940
6831
|
} catch (e) {
|
|
5941
6832
|
const endedAt = Date.now();
|
|
6833
|
+
const error = (e && e.message) || String(e);
|
|
5942
6834
|
appendWorkspaceRunFinished({ ...runEntry, endedAt, durationMs: endedAt - runEntry.startedAt }, "failed");
|
|
6835
|
+
appendWorkspaceRunLogEvent(runLog.runId, { type: "error", error, ts: endedAt });
|
|
6836
|
+
finishWorkspaceRunLogSession(runLog.runId, "failed", {
|
|
6837
|
+
endedAt,
|
|
6838
|
+
durationMs: endedAt - runEntry.startedAt,
|
|
6839
|
+
runNodeId: targetRunNodeId,
|
|
6840
|
+
error,
|
|
6841
|
+
});
|
|
5943
6842
|
updateWorkspaceScheduleEntry(entry.key, {
|
|
5944
6843
|
nextRunAt: computeNext(config),
|
|
5945
6844
|
lastFinishedAt: endedAt,
|
|
5946
6845
|
lastStatus: "failed",
|
|
5947
|
-
lastError:
|
|
6846
|
+
lastError: error,
|
|
5948
6847
|
lastErrorAt: endedAt,
|
|
5949
6848
|
});
|
|
5950
|
-
log.info(`[workspace-scheduler] failed ${entry.flowId}/${targetRunNodeId}: ${
|
|
6849
|
+
log.info(`[workspace-scheduler] failed ${entry.flowId}/${targetRunNodeId}: ${error}`);
|
|
5951
6850
|
} finally {
|
|
5952
6851
|
if (activeWorkspaceRuns.get(runKey) === runEntry) activeWorkspaceRuns.delete(runKey);
|
|
5953
6852
|
}
|
|
@@ -6171,6 +7070,29 @@ export function startUiServer({
|
|
|
6171
7070
|
|
|
6172
7071
|
const authUser = getAuthUserFromRequest(req);
|
|
6173
7072
|
const userCtx = authUser ? { userId: authUser.userId, isAdmin: Boolean(authUser.isAdmin) } : {};
|
|
7073
|
+
if (req.method === "GET" && url.pathname === "/api/auth/session-token") {
|
|
7074
|
+
if (!authUser?.userId) {
|
|
7075
|
+
json(res, 401, { error: "Unauthorized" });
|
|
7076
|
+
return;
|
|
7077
|
+
}
|
|
7078
|
+
json(res, 200, { token: getSessionTokenFromRequest(req) || "" });
|
|
7079
|
+
return;
|
|
7080
|
+
}
|
|
7081
|
+
if (req.method === "GET" && url.pathname === "/api/display/shares") {
|
|
7082
|
+
try {
|
|
7083
|
+
if (!authUser?.userId) {
|
|
7084
|
+
json(res, 401, { error: "Unauthorized" });
|
|
7085
|
+
return;
|
|
7086
|
+
}
|
|
7087
|
+
json(res, 200, {
|
|
7088
|
+
shares: listDisplaySharesForUser(userCtx, requestPublicBaseUrl(req)),
|
|
7089
|
+
});
|
|
7090
|
+
} catch (e) {
|
|
7091
|
+
json(res, 500, { error: (e && e.message) || String(e) });
|
|
7092
|
+
}
|
|
7093
|
+
return;
|
|
7094
|
+
}
|
|
7095
|
+
|
|
6174
7096
|
if (req.method === "GET" && url.pathname === "/api/display/share") {
|
|
6175
7097
|
try {
|
|
6176
7098
|
const id = String(url.searchParams.get("id") || "").trim();
|
|
@@ -6195,6 +7117,62 @@ export function startUiServer({
|
|
|
6195
7117
|
return;
|
|
6196
7118
|
}
|
|
6197
7119
|
|
|
7120
|
+
if (req.method === "PATCH" && url.pathname === "/api/display/share") {
|
|
7121
|
+
let payload;
|
|
7122
|
+
try {
|
|
7123
|
+
payload = JSON.parse(await readBody(req));
|
|
7124
|
+
} catch {
|
|
7125
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
7126
|
+
return;
|
|
7127
|
+
}
|
|
7128
|
+
try {
|
|
7129
|
+
if (!authUser?.userId) {
|
|
7130
|
+
json(res, 401, { error: "Unauthorized" });
|
|
7131
|
+
return;
|
|
7132
|
+
}
|
|
7133
|
+
const id = String(payload?.id || url.searchParams.get("id") || "").trim();
|
|
7134
|
+
if (!id) {
|
|
7135
|
+
json(res, 400, { error: "Missing display share id" });
|
|
7136
|
+
return;
|
|
7137
|
+
}
|
|
7138
|
+
const result = updateDisplayShareExpiryForUser(id, userCtx, payload);
|
|
7139
|
+
if (result.error) {
|
|
7140
|
+
json(res, result.status || 400, { error: result.error });
|
|
7141
|
+
return;
|
|
7142
|
+
}
|
|
7143
|
+
json(res, 200, {
|
|
7144
|
+
ok: true,
|
|
7145
|
+
share: displayShareSummary(result.share, requestPublicBaseUrl(req)),
|
|
7146
|
+
});
|
|
7147
|
+
} catch (e) {
|
|
7148
|
+
json(res, 500, { error: (e && e.message) || String(e) });
|
|
7149
|
+
}
|
|
7150
|
+
return;
|
|
7151
|
+
}
|
|
7152
|
+
|
|
7153
|
+
if (req.method === "DELETE" && url.pathname === "/api/display/share") {
|
|
7154
|
+
try {
|
|
7155
|
+
if (!authUser?.userId) {
|
|
7156
|
+
json(res, 401, { error: "Unauthorized" });
|
|
7157
|
+
return;
|
|
7158
|
+
}
|
|
7159
|
+
const id = String(url.searchParams.get("id") || "").trim();
|
|
7160
|
+
if (!id) {
|
|
7161
|
+
json(res, 400, { error: "Missing display share id" });
|
|
7162
|
+
return;
|
|
7163
|
+
}
|
|
7164
|
+
const result = deleteDisplayShareForUser(id, userCtx);
|
|
7165
|
+
if (result.error) {
|
|
7166
|
+
json(res, result.status || 400, { error: result.error });
|
|
7167
|
+
return;
|
|
7168
|
+
}
|
|
7169
|
+
json(res, 200, { ok: true });
|
|
7170
|
+
} catch (e) {
|
|
7171
|
+
json(res, 500, { error: (e && e.message) || String(e) });
|
|
7172
|
+
}
|
|
7173
|
+
return;
|
|
7174
|
+
}
|
|
7175
|
+
|
|
6198
7176
|
if (req.method === "GET" && url.pathname === "/api/display/file/raw") {
|
|
6199
7177
|
try {
|
|
6200
7178
|
const id = String(url.searchParams.get("id") || "").trim();
|
|
@@ -6631,6 +7609,40 @@ export function startUiServer({
|
|
|
6631
7609
|
return;
|
|
6632
7610
|
}
|
|
6633
7611
|
|
|
7612
|
+
if (req.method === "GET" && url.pathname === "/api/workspaces") {
|
|
7613
|
+
try {
|
|
7614
|
+
const scoped = resolveWorkspaceScopeRoot(root, {
|
|
7615
|
+
flowId: url.searchParams.get("flowId") || "",
|
|
7616
|
+
flowSource: url.searchParams.get("flowSource") || "user",
|
|
7617
|
+
archived: url.searchParams.get("archived") === "1",
|
|
7618
|
+
}, userCtx);
|
|
7619
|
+
const scopedRoot = scoped.error ? root : scoped.root;
|
|
7620
|
+
json(res, 200, {
|
|
7621
|
+
path: userWorkspacesPath(userCtx),
|
|
7622
|
+
workspaces: listConfiguredWorkspaces(root, scopedRoot, userCtx),
|
|
7623
|
+
customWorkspaces: readUserWorkspaces(userCtx),
|
|
7624
|
+
});
|
|
7625
|
+
} catch (e) {
|
|
7626
|
+
json(res, 500, { error: (e && e.message) || String(e) });
|
|
7627
|
+
}
|
|
7628
|
+
return;
|
|
7629
|
+
}
|
|
7630
|
+
|
|
7631
|
+
if (req.method === "POST" && url.pathname === "/api/workspaces") {
|
|
7632
|
+
try {
|
|
7633
|
+
const payload = await readJson(req);
|
|
7634
|
+
const customWorkspaces = writeUserWorkspaces(userCtx, payload?.workspaces || payload?.customWorkspaces || []);
|
|
7635
|
+
json(res, 200, {
|
|
7636
|
+
path: userWorkspacesPath(userCtx),
|
|
7637
|
+
workspaces: listConfiguredWorkspaces(root, root, userCtx),
|
|
7638
|
+
customWorkspaces,
|
|
7639
|
+
});
|
|
7640
|
+
} catch (e) {
|
|
7641
|
+
json(res, 500, { error: (e && e.message) || String(e) });
|
|
7642
|
+
}
|
|
7643
|
+
return;
|
|
7644
|
+
}
|
|
7645
|
+
|
|
6634
7646
|
if (req.method === "GET" && url.pathname === "/api/workspace/graph") {
|
|
6635
7647
|
try {
|
|
6636
7648
|
const scoped = resolveWorkspaceScopeRoot(root, {
|
|
@@ -6693,6 +7705,10 @@ export function startUiServer({
|
|
|
6693
7705
|
title: payload.title,
|
|
6694
7706
|
layout: payload.layout,
|
|
6695
7707
|
nodeIds,
|
|
7708
|
+
expiresMode: payload.expiresMode,
|
|
7709
|
+
expiresInDays: payload.expiresInDays,
|
|
7710
|
+
permanent: payload.permanent,
|
|
7711
|
+
expiresAt: payload.expiresAt,
|
|
6696
7712
|
});
|
|
6697
7713
|
json(res, 200, { ok: true, share, url: `/display/${encodeURIComponent(share.id)}` });
|
|
6698
7714
|
} catch (e) {
|
|
@@ -6800,6 +7816,57 @@ export function startUiServer({
|
|
|
6800
7816
|
return;
|
|
6801
7817
|
}
|
|
6802
7818
|
|
|
7819
|
+
if (req.method === "POST" && url.pathname === "/api/workspace/run/optimize") {
|
|
7820
|
+
let payload;
|
|
7821
|
+
try {
|
|
7822
|
+
payload = JSON.parse(await readBody(req));
|
|
7823
|
+
} catch {
|
|
7824
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
7825
|
+
return;
|
|
7826
|
+
}
|
|
7827
|
+
try {
|
|
7828
|
+
const scoped = resolveWorkspaceScopeRoot(root, {
|
|
7829
|
+
flowId: payload.flowId || "",
|
|
7830
|
+
flowSource: payload.flowSource || "user",
|
|
7831
|
+
archived: payload.archived === true || payload.flowArchived === true,
|
|
7832
|
+
}, userCtx);
|
|
7833
|
+
if (scoped.error) {
|
|
7834
|
+
json(res, 400, { error: scoped.error });
|
|
7835
|
+
return;
|
|
7836
|
+
}
|
|
7837
|
+
if (scoped.archived || isReadonlyBuiltinFlowSource(scoped.flowSource)) {
|
|
7838
|
+
json(res, 400, { error: "Cannot optimize workspace graph for builtin or archived pipeline" });
|
|
7839
|
+
return;
|
|
7840
|
+
}
|
|
7841
|
+
const flowId = String(payload.flowId || "").trim();
|
|
7842
|
+
if (!flowId) {
|
|
7843
|
+
json(res, 400, { error: "Missing flowId" });
|
|
7844
|
+
return;
|
|
7845
|
+
}
|
|
7846
|
+
const graphPath = workspaceGraphPath(scoped.root);
|
|
7847
|
+
const result = await workspaceOptimizeRunImplementations(root, scoped.root, payload, userCtx, {
|
|
7848
|
+
emit: () => {},
|
|
7849
|
+
});
|
|
7850
|
+
const currentGraph = readWorkspaceGraph(scoped.root).graph;
|
|
7851
|
+
const touchedIds = new Set((result.optimized || []).map((item) => item.nodeId).filter(Boolean));
|
|
7852
|
+
const mergedGraph = mergeWorkspaceRunGraph(currentGraph, result.graph, touchedIds);
|
|
7853
|
+
fs.writeFileSync(graphPath, JSON.stringify(mergedGraph, null, 2) + "\n", "utf-8");
|
|
7854
|
+
const workspaceSchedules = syncWorkspaceSchedulesForGraph(root, scoped, mergedGraph, authUser, userCtx);
|
|
7855
|
+
json(res, 200, {
|
|
7856
|
+
ok: true,
|
|
7857
|
+
path: graphPath,
|
|
7858
|
+
graph: mergedGraph,
|
|
7859
|
+
order: result.order,
|
|
7860
|
+
optimized: result.optimized,
|
|
7861
|
+
skipped: result.skipped,
|
|
7862
|
+
workspaceSchedules,
|
|
7863
|
+
});
|
|
7864
|
+
} catch (e) {
|
|
7865
|
+
json(res, 500, { error: (e && e.message) || String(e) });
|
|
7866
|
+
}
|
|
7867
|
+
return;
|
|
7868
|
+
}
|
|
7869
|
+
|
|
6803
7870
|
if (req.method === "POST" && url.pathname === "/api/workspace/run") {
|
|
6804
7871
|
let payload;
|
|
6805
7872
|
try {
|
|
@@ -6846,6 +7913,7 @@ export function startUiServer({
|
|
|
6846
7913
|
const controller = new AbortController();
|
|
6847
7914
|
const runId = String(payload.runSessionId || payload.runId || "").trim() || runLedgerId("workspace");
|
|
6848
7915
|
const runKey = workspaceRunEntryKey(scopeKey, runId);
|
|
7916
|
+
const runAlias = String(payload.runAlias || "").trim() || workspaceRuntimeNodeLabel(runtimeGraph, runNodeId, "Workspace Run");
|
|
6849
7917
|
const runEntry = {
|
|
6850
7918
|
scopeKey,
|
|
6851
7919
|
controller,
|
|
@@ -6854,6 +7922,7 @@ export function startUiServer({
|
|
|
6854
7922
|
userId: String(userCtx.userId || ""),
|
|
6855
7923
|
username: String(authUser?.username || userCtx.userId || ""),
|
|
6856
7924
|
runNodeId,
|
|
7925
|
+
label: runAlias,
|
|
6857
7926
|
flowId,
|
|
6858
7927
|
flowSource: scoped.flowSource || payload.flowSource || "user",
|
|
6859
7928
|
plannedNodeIds,
|
|
@@ -6864,6 +7933,19 @@ export function startUiServer({
|
|
|
6864
7933
|
}
|
|
6865
7934
|
},
|
|
6866
7935
|
};
|
|
7936
|
+
const runLog = createWorkspaceRunLogSession({
|
|
7937
|
+
runId,
|
|
7938
|
+
userId: runEntry.userId,
|
|
7939
|
+
username: runEntry.username,
|
|
7940
|
+
flowId: runEntry.flowId,
|
|
7941
|
+
flowSource: runEntry.flowSource,
|
|
7942
|
+
scheduleNodeId: String(runtimeGraph.instances?.[runNodeId]?.definitionId || "") === "workspace_scheduled_run" ? runNodeId : "",
|
|
7943
|
+
runNodeId,
|
|
7944
|
+
scheduled: false,
|
|
7945
|
+
trigger: "manual",
|
|
7946
|
+
label: runAlias,
|
|
7947
|
+
startedAt: runEntry.startedAt,
|
|
7948
|
+
});
|
|
6867
7949
|
activeWorkspaceRuns.set(runKey, runEntry);
|
|
6868
7950
|
appendWorkspaceRunStarted(runEntry);
|
|
6869
7951
|
const setActiveChild = (child) => {
|
|
@@ -6882,6 +7964,7 @@ export function startUiServer({
|
|
|
6882
7964
|
"X-Accel-Buffering": "no",
|
|
6883
7965
|
});
|
|
6884
7966
|
const writeEvent = (event) => {
|
|
7967
|
+
appendWorkspaceRunLogEvent(runLog.runId, event);
|
|
6885
7968
|
try { res.write(JSON.stringify(event) + "\n"); } catch (_) {}
|
|
6886
7969
|
};
|
|
6887
7970
|
try {
|
|
@@ -6894,28 +7977,47 @@ export function startUiServer({
|
|
|
6894
7977
|
const touchedIds = workspaceRunTouchedNodeIds(result);
|
|
6895
7978
|
const mergedGraph = mergeWorkspaceRunGraph(currentGraph, result.graph, touchedIds);
|
|
6896
7979
|
fs.writeFileSync(graphPath, JSON.stringify(mergedGraph, null, 2) + "\n", "utf-8");
|
|
7980
|
+
const endedAt = Date.now();
|
|
6897
7981
|
appendWorkspaceRunFinished({
|
|
6898
7982
|
...runEntry,
|
|
6899
|
-
endedAt
|
|
6900
|
-
durationMs:
|
|
7983
|
+
endedAt,
|
|
7984
|
+
durationMs: endedAt - runEntry.startedAt,
|
|
6901
7985
|
}, "success");
|
|
7986
|
+
finishWorkspaceRunLogSession(runLog.runId, "success", {
|
|
7987
|
+
endedAt,
|
|
7988
|
+
durationMs: endedAt - runEntry.startedAt,
|
|
7989
|
+
runNodeId,
|
|
7990
|
+
});
|
|
6902
7991
|
writeEvent({ type: "done", ok: true, path: graphPath, graph: mergedGraph, order: result.order, touchedNodeIds: Array.from(touchedIds), pauseNodeIds: result.pauseNodeIds || [] });
|
|
6903
7992
|
res.end();
|
|
6904
7993
|
} catch (e) {
|
|
7994
|
+
const endedAt = Date.now();
|
|
6905
7995
|
if (isWorkspaceRunAbortError(e) || controller.signal.aborted) {
|
|
6906
7996
|
appendWorkspaceRunFinished({
|
|
6907
7997
|
...runEntry,
|
|
6908
|
-
endedAt
|
|
6909
|
-
durationMs:
|
|
7998
|
+
endedAt,
|
|
7999
|
+
durationMs: endedAt - runEntry.startedAt,
|
|
6910
8000
|
}, "stopped");
|
|
8001
|
+
finishWorkspaceRunLogSession(runLog.runId, "stopped", {
|
|
8002
|
+
endedAt,
|
|
8003
|
+
durationMs: endedAt - runEntry.startedAt,
|
|
8004
|
+
runNodeId,
|
|
8005
|
+
});
|
|
6911
8006
|
writeEvent({ type: "stopped", ok: false, stopped: true, message: "Workspace run stopped" });
|
|
6912
8007
|
} else {
|
|
8008
|
+
const error = (e && e.message) || String(e);
|
|
6913
8009
|
appendWorkspaceRunFinished({
|
|
6914
8010
|
...runEntry,
|
|
6915
|
-
endedAt
|
|
6916
|
-
durationMs:
|
|
8011
|
+
endedAt,
|
|
8012
|
+
durationMs: endedAt - runEntry.startedAt,
|
|
6917
8013
|
}, "failed");
|
|
6918
|
-
|
|
8014
|
+
finishWorkspaceRunLogSession(runLog.runId, "failed", {
|
|
8015
|
+
endedAt,
|
|
8016
|
+
durationMs: endedAt - runEntry.startedAt,
|
|
8017
|
+
runNodeId,
|
|
8018
|
+
error,
|
|
8019
|
+
});
|
|
8020
|
+
writeEvent({ type: "error", error });
|
|
6919
8021
|
}
|
|
6920
8022
|
res.end();
|
|
6921
8023
|
} finally {
|
|
@@ -6927,32 +8029,53 @@ export function startUiServer({
|
|
|
6927
8029
|
const result = await runWorkspaceGraph(root, scoped.root, { ...payload, requestBaseUrl: requestPublicBaseUrl(req) }, userCtx, {
|
|
6928
8030
|
signal: controller.signal,
|
|
6929
8031
|
onActiveChild: setActiveChild,
|
|
8032
|
+
onEvent: (event) => appendWorkspaceRunLogEvent(runLog.runId, event),
|
|
6930
8033
|
});
|
|
6931
8034
|
const graphPath = workspaceGraphPath(scoped.root);
|
|
6932
8035
|
const currentGraph = readWorkspaceGraph(scoped.root).graph;
|
|
6933
8036
|
const touchedIds = workspaceRunTouchedNodeIds(result);
|
|
6934
8037
|
const mergedGraph = mergeWorkspaceRunGraph(currentGraph, result.graph, touchedIds);
|
|
6935
8038
|
fs.writeFileSync(graphPath, JSON.stringify(mergedGraph, null, 2) + "\n", "utf-8");
|
|
8039
|
+
const endedAt = Date.now();
|
|
6936
8040
|
appendWorkspaceRunFinished({
|
|
6937
8041
|
...runEntry,
|
|
6938
|
-
endedAt
|
|
6939
|
-
durationMs:
|
|
8042
|
+
endedAt,
|
|
8043
|
+
durationMs: endedAt - runEntry.startedAt,
|
|
6940
8044
|
}, "success");
|
|
8045
|
+
finishWorkspaceRunLogSession(runLog.runId, "success", {
|
|
8046
|
+
endedAt,
|
|
8047
|
+
durationMs: endedAt - runEntry.startedAt,
|
|
8048
|
+
runNodeId,
|
|
8049
|
+
});
|
|
6941
8050
|
json(res, 200, { ok: true, path: graphPath, ...result, graph: mergedGraph, touchedNodeIds: Array.from(touchedIds) });
|
|
6942
8051
|
} catch (e) {
|
|
8052
|
+
const endedAt = Date.now();
|
|
6943
8053
|
if (isWorkspaceRunAbortError(e) || controller.signal.aborted) {
|
|
6944
8054
|
appendWorkspaceRunFinished({
|
|
6945
8055
|
...runEntry,
|
|
6946
|
-
endedAt
|
|
6947
|
-
durationMs:
|
|
8056
|
+
endedAt,
|
|
8057
|
+
durationMs: endedAt - runEntry.startedAt,
|
|
6948
8058
|
}, "stopped");
|
|
8059
|
+
finishWorkspaceRunLogSession(runLog.runId, "stopped", {
|
|
8060
|
+
endedAt,
|
|
8061
|
+
durationMs: endedAt - runEntry.startedAt,
|
|
8062
|
+
runNodeId,
|
|
8063
|
+
});
|
|
6949
8064
|
json(res, 200, { ok: false, stopped: true, message: "Workspace run stopped" });
|
|
6950
8065
|
} else {
|
|
8066
|
+
const error = (e && e.message) || String(e);
|
|
6951
8067
|
appendWorkspaceRunFinished({
|
|
6952
8068
|
...runEntry,
|
|
6953
|
-
endedAt
|
|
6954
|
-
durationMs:
|
|
8069
|
+
endedAt,
|
|
8070
|
+
durationMs: endedAt - runEntry.startedAt,
|
|
6955
8071
|
}, "failed");
|
|
8072
|
+
appendWorkspaceRunLogEvent(runLog.runId, { type: "error", error, ts: endedAt });
|
|
8073
|
+
finishWorkspaceRunLogSession(runLog.runId, "failed", {
|
|
8074
|
+
endedAt,
|
|
8075
|
+
durationMs: endedAt - runEntry.startedAt,
|
|
8076
|
+
runNodeId,
|
|
8077
|
+
error,
|
|
8078
|
+
});
|
|
6956
8079
|
throw e;
|
|
6957
8080
|
}
|
|
6958
8081
|
} finally {
|
|
@@ -6964,6 +8087,52 @@ export function startUiServer({
|
|
|
6964
8087
|
return;
|
|
6965
8088
|
}
|
|
6966
8089
|
|
|
8090
|
+
if (req.method === "GET" && url.pathname === "/api/workspace/run-logs") {
|
|
8091
|
+
try {
|
|
8092
|
+
const flowId = url.searchParams.get("flowId") || "";
|
|
8093
|
+
const flowSource = url.searchParams.get("flowSource") || "";
|
|
8094
|
+
const scheduleNodeId = url.searchParams.get("scheduleNodeId") || "";
|
|
8095
|
+
const runNodeId = url.searchParams.get("runNodeId") || "";
|
|
8096
|
+
const limit = Number(url.searchParams.get("limit") || 50);
|
|
8097
|
+
json(res, 200, {
|
|
8098
|
+
runs: listWorkspaceRunLogs({
|
|
8099
|
+
userId: userCtx.userId || "",
|
|
8100
|
+
flowId,
|
|
8101
|
+
flowSource,
|
|
8102
|
+
scheduleNodeId,
|
|
8103
|
+
runNodeId,
|
|
8104
|
+
limit,
|
|
8105
|
+
}),
|
|
8106
|
+
});
|
|
8107
|
+
} catch (e) {
|
|
8108
|
+
json(res, 500, { error: (e && e.message) || String(e) });
|
|
8109
|
+
}
|
|
8110
|
+
return;
|
|
8111
|
+
}
|
|
8112
|
+
|
|
8113
|
+
if (req.method === "GET" && url.pathname.startsWith("/api/workspace/run-logs/")) {
|
|
8114
|
+
try {
|
|
8115
|
+
const runId = decodeURIComponent(url.pathname.slice("/api/workspace/run-logs/".length));
|
|
8116
|
+
if (!runId) {
|
|
8117
|
+
json(res, 400, { error: "Missing runId" });
|
|
8118
|
+
return;
|
|
8119
|
+
}
|
|
8120
|
+
const run = listWorkspaceRunLogs({ userId: userCtx.userId || "", limit: 200 })
|
|
8121
|
+
.find((item) => String(item.runId || "") === runId);
|
|
8122
|
+
if (!run) {
|
|
8123
|
+
json(res, 404, { error: "Run log not found" });
|
|
8124
|
+
return;
|
|
8125
|
+
}
|
|
8126
|
+
json(res, 200, {
|
|
8127
|
+
run,
|
|
8128
|
+
events: readWorkspaceRunLogEvents(runId),
|
|
8129
|
+
});
|
|
8130
|
+
} catch (e) {
|
|
8131
|
+
json(res, 500, { error: (e && e.message) || String(e) });
|
|
8132
|
+
}
|
|
8133
|
+
return;
|
|
8134
|
+
}
|
|
8135
|
+
|
|
6967
8136
|
if (req.method === "GET" && url.pathname === "/api/workspace/run/status") {
|
|
6968
8137
|
const flowId = typeof url.searchParams.get("flowId") === "string" ? url.searchParams.get("flowId").trim() : "";
|
|
6969
8138
|
if (!flowId) {
|
|
@@ -6979,10 +8148,12 @@ export function startUiServer({
|
|
|
6979
8148
|
flowId,
|
|
6980
8149
|
flowSource,
|
|
6981
8150
|
runNodeId: entry?.runNodeId || "",
|
|
8151
|
+
label: entry?.label || "",
|
|
6982
8152
|
startedAt: entry?.startedAt || null,
|
|
6983
8153
|
runs: entries.map((item) => ({
|
|
6984
8154
|
runId: item?.runId || "",
|
|
6985
8155
|
runNodeId: item?.runNodeId || "",
|
|
8156
|
+
label: item?.label || "",
|
|
6986
8157
|
startedAt: item?.startedAt || null,
|
|
6987
8158
|
plannedNodeIds: Array.isArray(item?.plannedNodeIds) ? item.plannedNodeIds : [],
|
|
6988
8159
|
scheduled: item?.scheduled === true,
|
|
@@ -8030,6 +9201,60 @@ export function startUiServer({
|
|
|
8030
9201
|
return;
|
|
8031
9202
|
}
|
|
8032
9203
|
|
|
9204
|
+
if (req.method === "GET" && url.pathname === "/api/node-studio/drafts") {
|
|
9205
|
+
try {
|
|
9206
|
+
json(res, 200, { drafts: listNodeStudioDrafts(userCtx) });
|
|
9207
|
+
} catch (e) {
|
|
9208
|
+
json(res, 500, { error: (e && e.message) || String(e) });
|
|
9209
|
+
}
|
|
9210
|
+
return;
|
|
9211
|
+
}
|
|
9212
|
+
|
|
9213
|
+
if (req.method === "GET" && url.pathname === "/api/node-studio/draft") {
|
|
9214
|
+
try {
|
|
9215
|
+
const id = url.searchParams.get("id") || "";
|
|
9216
|
+
if (!id) {
|
|
9217
|
+
json(res, 200, { draft: null });
|
|
9218
|
+
return;
|
|
9219
|
+
}
|
|
9220
|
+
const draft = readNodeStudioDraft(userCtx, id);
|
|
9221
|
+
json(res, 200, { draft: draft && !isLegacyNodeStudioDemoDraft(draft) ? draft : null });
|
|
9222
|
+
} catch (e) {
|
|
9223
|
+
json(res, 500, { error: (e && e.message) || String(e) });
|
|
9224
|
+
}
|
|
9225
|
+
return;
|
|
9226
|
+
}
|
|
9227
|
+
|
|
9228
|
+
if (req.method === "POST" && url.pathname === "/api/node-studio/draft") {
|
|
9229
|
+
let payload;
|
|
9230
|
+
try {
|
|
9231
|
+
payload = JSON.parse(await readBody(req));
|
|
9232
|
+
} catch {
|
|
9233
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
9234
|
+
return;
|
|
9235
|
+
}
|
|
9236
|
+
try {
|
|
9237
|
+
const current = readNodeStudioDraft(userCtx, payload.id || "") || emptyNodeStudioDraft(userCtx, payload.id || "untitled_node");
|
|
9238
|
+
const promptDraft = payload.promptDraft != null ? String(payload.promptDraft) : current.promptDraft || "";
|
|
9239
|
+
const agentMessages = Array.isArray(current.agentMessages) ? [...current.agentMessages] : [];
|
|
9240
|
+
if (payload.appendUserMessage === true && promptDraft.trim()) {
|
|
9241
|
+
const at = new Date().toISOString();
|
|
9242
|
+
agentMessages.push({ role: "user", text: promptDraft.trim(), at });
|
|
9243
|
+
agentMessages.push({ role: "assistant", text: "已记录需求,下一步会由节点 Agent 更新 manifest、脚本和 UI schema。", at });
|
|
9244
|
+
}
|
|
9245
|
+
const draft = writeNodeStudioDraft(userCtx, {
|
|
9246
|
+
...current,
|
|
9247
|
+
...(payload.config && typeof payload.config === "object" ? { config: { ...(current.config || {}), ...payload.config } } : {}),
|
|
9248
|
+
promptDraft,
|
|
9249
|
+
agentMessages,
|
|
9250
|
+
});
|
|
9251
|
+
json(res, 200, { ok: true, draft });
|
|
9252
|
+
} catch (e) {
|
|
9253
|
+
json(res, 500, { error: (e && e.message) || String(e) });
|
|
9254
|
+
}
|
|
9255
|
+
return;
|
|
9256
|
+
}
|
|
9257
|
+
|
|
8033
9258
|
if (req.method === "GET" && url.pathname === "/api/marketplace/nodes") {
|
|
8034
9259
|
try {
|
|
8035
9260
|
const marketplaceScope = url.searchParams.get("scope") === "owned" ? "owned" : "all";
|
|
@@ -8711,6 +9936,128 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
8711
9936
|
return;
|
|
8712
9937
|
}
|
|
8713
9938
|
|
|
9939
|
+
if (req.method === "GET" && url.pathname === "/api/flow/schedules") {
|
|
9940
|
+
try {
|
|
9941
|
+
json(res, 200, { schedules: listScheduleStatuses(root, userCtx) });
|
|
9942
|
+
} catch (e) {
|
|
9943
|
+
json(res, 500, { error: (e && e.message) || String(e) });
|
|
9944
|
+
}
|
|
9945
|
+
return;
|
|
9946
|
+
}
|
|
9947
|
+
|
|
9948
|
+
if (req.method === "GET" && url.pathname === "/api/schedules") {
|
|
9949
|
+
try {
|
|
9950
|
+
const pipelineSchedules = listScheduleStatuses(root, userCtx)
|
|
9951
|
+
.filter((schedule) => (
|
|
9952
|
+
schedule.enabled ||
|
|
9953
|
+
schedule.cron ||
|
|
9954
|
+
schedule.nextRunAt ||
|
|
9955
|
+
schedule.lastTriggeredAt ||
|
|
9956
|
+
schedule.lastRunUuid ||
|
|
9957
|
+
schedule.lastError ||
|
|
9958
|
+
schedule.running ||
|
|
9959
|
+
schedule.waiting
|
|
9960
|
+
))
|
|
9961
|
+
.map((schedule) => ({
|
|
9962
|
+
kind: "pipeline",
|
|
9963
|
+
...schedule,
|
|
9964
|
+
}));
|
|
9965
|
+
const workspaceSchedules = listWorkspaceScheduleStatuses(root, userCtx);
|
|
9966
|
+
json(res, 200, {
|
|
9967
|
+
schedules: [...workspaceSchedules, ...pipelineSchedules].sort((a, b) => {
|
|
9968
|
+
const ea = a.enabled ? 0 : 1;
|
|
9969
|
+
const eb = b.enabled ? 0 : 1;
|
|
9970
|
+
return ea - eb || String(a.nextRunAt || "").localeCompare(String(b.nextRunAt || "")) || String(a.flowId || "").localeCompare(String(b.flowId || ""));
|
|
9971
|
+
}),
|
|
9972
|
+
});
|
|
9973
|
+
} catch (e) {
|
|
9974
|
+
json(res, 500, { error: (e && e.message) || String(e) });
|
|
9975
|
+
}
|
|
9976
|
+
return;
|
|
9977
|
+
}
|
|
9978
|
+
|
|
9979
|
+
if (req.method === "POST" && url.pathname === "/api/schedule/toggle") {
|
|
9980
|
+
let payload;
|
|
9981
|
+
try {
|
|
9982
|
+
payload = JSON.parse(await readBody(req));
|
|
9983
|
+
} catch {
|
|
9984
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
9985
|
+
return;
|
|
9986
|
+
}
|
|
9987
|
+
const kind = String(payload.kind || "").trim();
|
|
9988
|
+
if (kind === "workspace") {
|
|
9989
|
+
try {
|
|
9990
|
+
const result = setWorkspaceScheduleEnabled(root, payload, authUser, userCtx);
|
|
9991
|
+
if (!result.success) {
|
|
9992
|
+
json(res, 400, { error: result.error || "Could not update workspace schedule" });
|
|
9993
|
+
return;
|
|
9994
|
+
}
|
|
9995
|
+
json(res, 200, { success: true });
|
|
9996
|
+
} catch (e) {
|
|
9997
|
+
json(res, 500, { error: (e && e.message) || String(e) });
|
|
9998
|
+
}
|
|
9999
|
+
return;
|
|
10000
|
+
}
|
|
10001
|
+
if (kind === "pipeline") {
|
|
10002
|
+
const flowId = String(payload.flowId || "").trim();
|
|
10003
|
+
const flowSource = String(payload.flowSource || "user").trim() || "user";
|
|
10004
|
+
if (!flowId) {
|
|
10005
|
+
json(res, 400, { error: "Missing flowId" });
|
|
10006
|
+
return;
|
|
10007
|
+
}
|
|
10008
|
+
if (!isValidFlowSourceWrite(flowSource)) {
|
|
10009
|
+
json(res, 400, { error: "Cannot update schedule for builtin or readonly flow" });
|
|
10010
|
+
return;
|
|
10011
|
+
}
|
|
10012
|
+
const current = readFlowSchedule(root, flowId, flowSource, userCtx);
|
|
10013
|
+
if (!current.success) {
|
|
10014
|
+
json(res, 400, { error: current.error || "Could not read schedule" });
|
|
10015
|
+
return;
|
|
10016
|
+
}
|
|
10017
|
+
const result = writeFlowSchedule(root, flowId, flowSource, { ...current.schedule, enabled: payload.enabled === true }, userCtx);
|
|
10018
|
+
if (!result.success) {
|
|
10019
|
+
json(res, 400, { error: result.error || "Could not update schedule" });
|
|
10020
|
+
return;
|
|
10021
|
+
}
|
|
10022
|
+
json(res, 200, { success: true, schedule: result.schedule });
|
|
10023
|
+
return;
|
|
10024
|
+
}
|
|
10025
|
+
json(res, 400, { error: "Invalid schedule kind" });
|
|
10026
|
+
return;
|
|
10027
|
+
}
|
|
10028
|
+
|
|
10029
|
+
if (req.method === "POST" && url.pathname === "/api/flow/schedule/disable") {
|
|
10030
|
+
let payload;
|
|
10031
|
+
try {
|
|
10032
|
+
payload = JSON.parse(await readBody(req));
|
|
10033
|
+
} catch {
|
|
10034
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
10035
|
+
return;
|
|
10036
|
+
}
|
|
10037
|
+
const flowId = String(payload.flowId || "").trim();
|
|
10038
|
+
const flowSource = String(payload.flowSource || "user").trim() || "user";
|
|
10039
|
+
if (!flowId) {
|
|
10040
|
+
json(res, 400, { error: "Missing flowId" });
|
|
10041
|
+
return;
|
|
10042
|
+
}
|
|
10043
|
+
if (!isValidFlowSourceWrite(flowSource)) {
|
|
10044
|
+
json(res, 400, { error: "Cannot disable schedule for builtin or readonly flow" });
|
|
10045
|
+
return;
|
|
10046
|
+
}
|
|
10047
|
+
const current = readFlowSchedule(root, flowId, flowSource, userCtx);
|
|
10048
|
+
if (!current.success) {
|
|
10049
|
+
json(res, 400, { error: current.error || "Could not read schedule" });
|
|
10050
|
+
return;
|
|
10051
|
+
}
|
|
10052
|
+
const result = writeFlowSchedule(root, flowId, flowSource, { ...current.schedule, enabled: false }, userCtx);
|
|
10053
|
+
if (!result.success) {
|
|
10054
|
+
json(res, 400, { error: result.error || "Could not disable schedule" });
|
|
10055
|
+
return;
|
|
10056
|
+
}
|
|
10057
|
+
json(res, 200, { success: true, schedule: result.schedule });
|
|
10058
|
+
return;
|
|
10059
|
+
}
|
|
10060
|
+
|
|
8714
10061
|
if (req.method === "POST" && url.pathname === "/api/flow/run") {
|
|
8715
10062
|
let payload;
|
|
8716
10063
|
try {
|