@fieldwangai/agentflow 0.1.85 → 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 +539 -14
- 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-Lb9rIMqs.js +0 -250
- package/builtin/web-ui/dist/assets/index-aqphFZlG.css +0 -1
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,
|
|
@@ -619,6 +620,125 @@ function normalizeMcpServerConfig(value) {
|
|
|
619
620
|
return next;
|
|
620
621
|
}
|
|
621
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
|
+
|
|
622
742
|
function readCursorMcpServers(userCtx = {}) {
|
|
623
743
|
const config = readCursorMcpConfig();
|
|
624
744
|
const privateConfig = readUserMcpPrivate(userCtx);
|
|
@@ -651,7 +771,7 @@ function readCursorMcpServers(userCtx = {}) {
|
|
|
651
771
|
privateEnvKeys,
|
|
652
772
|
privateHeaderKeys,
|
|
653
773
|
};
|
|
654
|
-
}).sort((a, b) => a.name.localeCompare(b.name));
|
|
774
|
+
}).map(withMcpBackendCompatibility).sort((a, b) => a.name.localeCompare(b.name));
|
|
655
775
|
return { path: cursorMcpConfigPath(), servers };
|
|
656
776
|
}
|
|
657
777
|
|
|
@@ -958,9 +1078,11 @@ function readModelListsFromDisk(workspaceRoot) {
|
|
|
958
1078
|
cursor: [],
|
|
959
1079
|
opencode: [],
|
|
960
1080
|
claudeCode: [],
|
|
1081
|
+
codex: [],
|
|
961
1082
|
cursorFetchedAt: null,
|
|
962
1083
|
opencodeFetchedAt: null,
|
|
963
1084
|
claudeCodeFetchedAt: null,
|
|
1085
|
+
codexFetchedAt: null,
|
|
964
1086
|
};
|
|
965
1087
|
try {
|
|
966
1088
|
if (!fs.existsSync(p)) return empty;
|
|
@@ -969,9 +1091,11 @@ function readModelListsFromDisk(workspaceRoot) {
|
|
|
969
1091
|
cursor: Array.isArray(data.cursor) ? data.cursor.map(String) : [],
|
|
970
1092
|
opencode: Array.isArray(data.opencode) ? data.opencode.map(String) : [],
|
|
971
1093
|
claudeCode: Array.isArray(data.claudeCode) ? data.claudeCode.map(String) : [],
|
|
1094
|
+
codex: Array.isArray(data.codex) ? data.codex.map(String) : [],
|
|
972
1095
|
cursorFetchedAt: data.cursorFetchedAt ?? null,
|
|
973
1096
|
opencodeFetchedAt: data.opencodeFetchedAt ?? null,
|
|
974
1097
|
claudeCodeFetchedAt: data.claudeCodeFetchedAt ?? null,
|
|
1098
|
+
codexFetchedAt: data.codexFetchedAt ?? null,
|
|
975
1099
|
};
|
|
976
1100
|
} catch {
|
|
977
1101
|
return empty;
|
|
@@ -1490,6 +1614,194 @@ function readWorkspaceGraph(workspaceRoot) {
|
|
|
1490
1614
|
const DISPLAY_SHARE_FILENAME = "display-shares.json";
|
|
1491
1615
|
const DISPLAY_SHARE_TTL_MS = 30 * 24 * 60 * 60 * 1000;
|
|
1492
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
|
+
}
|
|
1493
1805
|
|
|
1494
1806
|
function displaySharesPath() {
|
|
1495
1807
|
return path.join(getAgentflowDataRoot(), DISPLAY_SHARE_FILENAME);
|
|
@@ -2525,6 +2837,14 @@ function buildWorkspaceGeneratePrompt(payload) {
|
|
|
2525
2837
|
"只输出 ASCII 图正文,不要解释,不要包裹 Markdown 代码围栏。",
|
|
2526
2838
|
"使用 +-|/\\<> 等字符表达结构,尽量保持对齐。",
|
|
2527
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")
|
|
2528
2848
|
: [
|
|
2529
2849
|
"你是 AgentFlow Workspace Composer。",
|
|
2530
2850
|
"默认以用户当前选择的 workspace 节点作为上下文范围;选中节点不是让你重建整张画布的授权。",
|
|
@@ -2578,6 +2898,8 @@ function buildWorkspaceNodeChatPrompt(payload) {
|
|
|
2578
2898
|
].join("\n")
|
|
2579
2899
|
: nodeKind === "html"
|
|
2580
2900
|
? "只输出完整或片段 HTML,不要解释,不要包裹 Markdown 代码围栏。"
|
|
2901
|
+
: nodeKind === "react"
|
|
2902
|
+
? "只输出 React 工程 JSON,不要解释,不要包裹 Markdown 代码围栏。JSON 必须包含 title、entry、files;files 至少包含 src/App.jsx。"
|
|
2581
2903
|
: nodeKind === "image"
|
|
2582
2904
|
? "只输出新的图片 src,可以是 URL、data URL 或文件路径,不要解释。"
|
|
2583
2905
|
: nodeKind === "mermaid"
|
|
@@ -3133,6 +3455,7 @@ function workspaceDisplayKind(definitionId) {
|
|
|
3133
3455
|
if (id === "display_mermaid") return "mermaid";
|
|
3134
3456
|
if (id === "display_ascii") return "ascii";
|
|
3135
3457
|
if (id === "display_html") return "html";
|
|
3458
|
+
if (id === "display_react_app") return "react";
|
|
3136
3459
|
if (id === "display_image") return "image";
|
|
3137
3460
|
if (id === "display_chart") return "chart";
|
|
3138
3461
|
if (id === "display_table") return "table";
|
|
@@ -3149,6 +3472,7 @@ function workspaceDisplayTextFilePath(value, kind = "") {
|
|
|
3149
3472
|
const ext = clean.split("?")[0].split("#")[0].toLowerCase().split(".").pop() || "";
|
|
3150
3473
|
const allowedByKind = {
|
|
3151
3474
|
html: new Set(["html", "htm"]),
|
|
3475
|
+
react: new Set(["json", "jsx", "tsx", "js", "txt"]),
|
|
3152
3476
|
markdown: new Set(["md", "markdown", "txt"]),
|
|
3153
3477
|
mermaid: new Set(["mmd", "mermaid", "txt"]),
|
|
3154
3478
|
ascii: new Set(["txt", "log"]),
|
|
@@ -3222,6 +3546,7 @@ function workspaceDisplayKindExample(kind, field) {
|
|
|
3222
3546
|
if (kind === "table") return { columns: ["列名1", "列名2"], rows: [["值1", "值2"]] };
|
|
3223
3547
|
if (kind === "chart") return { type: "chart", version: "1.0", renderer: "echarts", option: { xAxis: { type: "category", data: [] }, yAxis: { type: "value" }, series: [{ type: "bar", data: [] }] } };
|
|
3224
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: {} };
|
|
3225
3550
|
if (kind === "mermaid") return "flowchart TD\n A[开始] --> B[结束]";
|
|
3226
3551
|
if (kind === "ascii") return "+---+\n| |\n+---+";
|
|
3227
3552
|
if (kind === "image") return "<图片 URL 或 data URL>";
|
|
@@ -3357,12 +3682,16 @@ function workspaceOutputProtocolRequirements(graph, nodeId) {
|
|
|
3357
3682
|
.filter((slot) => {
|
|
3358
3683
|
const name = String(slot?.name || "").trim();
|
|
3359
3684
|
const type = String(slot?.type || "");
|
|
3360
|
-
return name && type !== "node" && name !== "next" && name !== "result" && name !== "content";
|
|
3685
|
+
return name && type !== "node" && name !== "next" && name !== "result" && name !== "content" && name !== "displayType";
|
|
3361
3686
|
})
|
|
3362
3687
|
.map((slot) => String(slot.name).trim());
|
|
3363
|
-
const
|
|
3688
|
+
const configuredResultKind = isWorkspaceOneClickTaskDefinitionId(instance.definitionId)
|
|
3689
|
+
? workspaceContextRunDisplayKind(instance)
|
|
3690
|
+
: "";
|
|
3691
|
+
const resultKind = displayByField.get("result") || workspaceDownstreamOutputKindForField(graph, nodeId, "result") || configuredResultKind || "";
|
|
3364
3692
|
const resultExtByKind = {
|
|
3365
3693
|
html: "html",
|
|
3694
|
+
react: "json",
|
|
3366
3695
|
markdown: "md",
|
|
3367
3696
|
mermaid: "mmd",
|
|
3368
3697
|
ascii: "txt",
|
|
@@ -3374,6 +3703,7 @@ function workspaceOutputProtocolRequirements(graph, nodeId) {
|
|
|
3374
3703
|
const resultKindText = resultKind ? ` ${resultKind}` : "";
|
|
3375
3704
|
const resultGuidance = {
|
|
3376
3705
|
html: "内容必须是可直接放入 iframe 渲染的 HTML;不要使用 Markdown 代码围栏。",
|
|
3706
|
+
react: "内容必须是 React 工程 JSON,包含 title、entry、files;files 至少包含 src/App.jsx,可包含 CSS 文件。",
|
|
3377
3707
|
markdown: "内容必须是 Markdown 正文;除非正文确实需要代码块,否则不要额外包裹代码围栏。",
|
|
3378
3708
|
mermaid: "内容必须是 Mermaid 图表代码,例如 flowchart/sequenceDiagram;不要使用 Markdown 代码围栏。",
|
|
3379
3709
|
ascii: "内容必须是纯文本/ASCII 图或表格;不要输出 HTML 或 Markdown 装饰。",
|
|
@@ -3480,8 +3810,11 @@ function workspaceRunPlan(graph, runNodeId, scopedRoot = "") {
|
|
|
3480
3810
|
if (needed.size !== before) visitControlDownstream(next);
|
|
3481
3811
|
}
|
|
3482
3812
|
};
|
|
3813
|
+
const targetDefId = String(instances[target]?.definitionId || "");
|
|
3814
|
+
const targetIsRunController = targetDefId === "workspace_run" || targetDefId === "workspace_scheduled_run";
|
|
3815
|
+
if (!targetIsRunController) needed.add(target);
|
|
3483
3816
|
visitControlDownstream(target);
|
|
3484
|
-
needed.delete(target);
|
|
3817
|
+
if (targetIsRunController) needed.delete(target);
|
|
3485
3818
|
const dependencyQueue = Array.from(needed);
|
|
3486
3819
|
for (let i = 0; i < dependencyQueue.length; i++) {
|
|
3487
3820
|
const id = dependencyQueue[i];
|
|
@@ -4362,6 +4695,10 @@ function parseWorkspaceSkillKeys(raw) {
|
|
|
4362
4695
|
function selectedSkillKeysFromInstance(instance) {
|
|
4363
4696
|
const bodyKeys = parseWorkspaceSkillKeys(instance?.body || "");
|
|
4364
4697
|
if (bodyKeys.length > 0) return bodyKeys;
|
|
4698
|
+
return selectedSkillKeysFromConfigSlots(instance);
|
|
4699
|
+
}
|
|
4700
|
+
|
|
4701
|
+
function selectedSkillKeysFromConfigSlots(instance) {
|
|
4365
4702
|
const slots = [...(Array.isArray(instance?.input) ? instance.input : []), ...(Array.isArray(instance?.output) ? instance.output : [])];
|
|
4366
4703
|
const slot = slots.find((item) => item?.name === "skillsContext") || slots.find((item) => item?.name === "skillKeys");
|
|
4367
4704
|
return parseWorkspaceSkillKeys(workspaceSlotValue(slot) || "");
|
|
@@ -4475,6 +4812,34 @@ function workspaceNodeWorkspaceContextBlock(graph, nodeId, outputs, scopedRoot =
|
|
|
4475
4812
|
return lines.join("\n");
|
|
4476
4813
|
}
|
|
4477
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
|
+
|
|
4478
4843
|
function mergeWorkspaceSkillBlocks(...values) {
|
|
4479
4844
|
const blocks = values
|
|
4480
4845
|
.map((value) => String(value || ""))
|
|
@@ -4983,6 +5348,12 @@ async function workspaceRunToolNodejsScript({
|
|
|
4983
5348
|
});
|
|
4984
5349
|
}
|
|
4985
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
|
+
|
|
4986
5357
|
async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts = {}) {
|
|
4987
5358
|
const graph = hydrateWorkspaceGraphForRuntime(root, {
|
|
4988
5359
|
root: scopedRoot,
|
|
@@ -5225,17 +5596,49 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
5225
5596
|
const inputSlots = Array.isArray(instance.input) ? instance.input : [];
|
|
5226
5597
|
const pathSlot = inputSlots.find((slot) => String(slot?.name || "") === "path") ||
|
|
5227
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");
|
|
5228
5601
|
const candidate = workspaceSlotValue(pathSlot) || workspaceInstanceText(instance) || inputText;
|
|
5229
5602
|
const abs = candidate ? path.resolve(scopedRoot, candidate) : scopedRoot;
|
|
5230
|
-
if (fs.existsSync(abs)
|
|
5231
|
-
|
|
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 });
|
|
5232
5623
|
emit({ type: "node-done", nodeId, definitionId: defId });
|
|
5233
5624
|
continue;
|
|
5234
5625
|
}
|
|
5235
5626
|
|
|
5236
5627
|
if (defId === "control_user_workspace") {
|
|
5237
5628
|
cwd = path.resolve(os.homedir());
|
|
5238
|
-
|
|
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 });
|
|
5239
5642
|
emit({ type: "node-done", nodeId, definitionId: defId });
|
|
5240
5643
|
continue;
|
|
5241
5644
|
}
|
|
@@ -5511,6 +5914,7 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
5511
5914
|
}
|
|
5512
5915
|
|
|
5513
5916
|
if (defId === "tool_nodejs") {
|
|
5917
|
+
const nodeModelKey = workspaceNodeModelKey(instance, modelKey);
|
|
5514
5918
|
const prepareStartedAt = Date.now();
|
|
5515
5919
|
const inputValues = workspaceInputValues(graph, nodeId, outputs, scopedRoot);
|
|
5516
5920
|
const runPackage = workspaceCreateNodeRunPackage(runTmpRoot, nodeId, {
|
|
@@ -5544,7 +5948,7 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
5544
5948
|
resultContent,
|
|
5545
5949
|
structured: normalizedAgentOutput,
|
|
5546
5950
|
runPackage,
|
|
5547
|
-
modelKey,
|
|
5951
|
+
modelKey: nodeModelKey,
|
|
5548
5952
|
userCtx,
|
|
5549
5953
|
emit: (event) => emit({ ...event, nodeId }),
|
|
5550
5954
|
onActiveChild: opts.onActiveChild,
|
|
@@ -5556,12 +5960,15 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
5556
5960
|
continue;
|
|
5557
5961
|
}
|
|
5558
5962
|
|
|
5963
|
+
const isContextRunNode = isWorkspaceOneClickTaskDefinitionId(defId);
|
|
5964
|
+
const nodeModelKey = workspaceNodeModelKey(instance, modelKey);
|
|
5559
5965
|
const prepareStartedAt = Date.now();
|
|
5560
5966
|
const inputValues = workspaceInputValues(graph, nodeId, outputs, scopedRoot);
|
|
5561
5967
|
const relevantInputs = workspaceRelevantInputValues(instance.body || "", inputValues);
|
|
5562
5968
|
const upstreamText = workspaceTaskUpstreamText(graph, nodeId, outputs, relevantInputs.placeholders, scopedRoot);
|
|
5563
5969
|
const upstreamSkillBlocks = workspaceUpstreamSkillBlocks(graph, nodeId, outputs);
|
|
5564
|
-
const
|
|
5970
|
+
const ownSkillBlock = isContextRunNode ? loadSkillsBlockForKeys(selectedSkillKeysFromConfigSlots(instance)) : "";
|
|
5971
|
+
const promptSkillsBlock = mergeWorkspaceSkillBlocks(ownSkillBlock, upstreamSkillBlocks);
|
|
5565
5972
|
const promptMcpBlock = workspaceUpstreamMcpBlocks(graph, nodeId, outputs);
|
|
5566
5973
|
const runPackage = workspaceCreateNodeRunPackage(runTmpRoot, nodeId, {
|
|
5567
5974
|
scopedRoot,
|
|
@@ -5583,7 +5990,10 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
5583
5990
|
// Best-effort debug artifact only.
|
|
5584
5991
|
}
|
|
5585
5992
|
const historyBlock = workspaceNodeHistoryBlock(nodeId, scopedRoot, runPackage);
|
|
5586
|
-
|
|
5993
|
+
let workspaceContextBlock = workspaceNodeWorkspaceContextBlock(graph, nodeId, outputs, scopedRoot, cwd);
|
|
5994
|
+
if (isContextRunNode && workspaceBoolSlot(instance, "includeWorkspaceContext", true) && !workspaceContextBlock) {
|
|
5995
|
+
workspaceContextBlock = workspaceDefaultWorkspaceContextBlock(scopedRoot, cwd);
|
|
5996
|
+
}
|
|
5587
5997
|
const prompt = workspaceNodePrompt(graph, nodeId, promptUpstreamText, promptSkillsBlock, promptMcpBlock, runtimeInputValues, runPackage, historyBlock, workspaceContextBlock);
|
|
5588
5998
|
try {
|
|
5589
5999
|
fs.writeFileSync(path.join(runPackage.nodeRunDir, "prompt.md"), prompt.trimEnd() + "\n", "utf-8");
|
|
@@ -5598,6 +6008,7 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
5598
6008
|
nodeRunDir: runPackage.nodeRunDir,
|
|
5599
6009
|
});
|
|
5600
6010
|
emit({ type: "natural", kind: "prompt", nodeId, text: prompt });
|
|
6011
|
+
emit({ type: "status", nodeId, line: `Model: ${nodeModelKey || "default"}` });
|
|
5601
6012
|
let content = "";
|
|
5602
6013
|
const runHistoryEvents = [];
|
|
5603
6014
|
const maxAttempts = 3;
|
|
@@ -5612,7 +6023,7 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
5612
6023
|
uiWorkspaceRoot: scopedRoot,
|
|
5613
6024
|
cliWorkspace: runPackage.nodeRunDir,
|
|
5614
6025
|
prompt,
|
|
5615
|
-
modelKey,
|
|
6026
|
+
modelKey: nodeModelKey,
|
|
5616
6027
|
agentflowUserId: userCtx.userId || "",
|
|
5617
6028
|
extraEnv: runtimeEnv({
|
|
5618
6029
|
AGENTFLOW_WORKSPACE_TMP_ROOT: runTmpRoot,
|
|
@@ -5682,15 +6093,20 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
5682
6093
|
resultContent,
|
|
5683
6094
|
structured: normalizedAgentOutput,
|
|
5684
6095
|
runPackage,
|
|
5685
|
-
modelKey,
|
|
6096
|
+
modelKey: nodeModelKey,
|
|
5686
6097
|
userCtx,
|
|
5687
6098
|
historyEvents: runHistoryEvents,
|
|
5688
6099
|
emit: (event) => emit({ ...event, nodeId }),
|
|
5689
6100
|
onActiveChild: opts.onActiveChild,
|
|
5690
6101
|
});
|
|
5691
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
|
+
}
|
|
5692
6108
|
const updatedDisplays = propagateNodeOutputDisplays(nodeId, resultContent);
|
|
5693
|
-
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 });
|
|
5694
6110
|
emit({ type: "node-done", nodeId, definitionId: defId });
|
|
5695
6111
|
}
|
|
5696
6112
|
} finally {
|
|
@@ -5910,6 +6326,13 @@ function workspaceRunEntryKey(scopeKey, runId) {
|
|
|
5910
6326
|
return `${scopeKey}:${String(runId || "").trim() || runLedgerId("workspace")}`;
|
|
5911
6327
|
}
|
|
5912
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
|
+
|
|
5913
6336
|
function workspaceActiveRunsForScope(scopeKey) {
|
|
5914
6337
|
const key = String(scopeKey || "");
|
|
5915
6338
|
return Array.from(activeWorkspaceRuns.entries())
|
|
@@ -6287,6 +6710,7 @@ async function runWorkspaceScheduledEntry(root, entry) {
|
|
|
6287
6710
|
return;
|
|
6288
6711
|
}
|
|
6289
6712
|
const targetRunNodeId = workspaceScheduleInferTargetRunNodeId(graph, scheduleNodeId, config);
|
|
6713
|
+
const scheduleAlias = workspaceRuntimeNodeLabel(graph, scheduleNodeId, String(entry.label || "Scheduled Run"));
|
|
6290
6714
|
if (!targetRunNodeId) {
|
|
6291
6715
|
const error = "Scheduled Run node is missing";
|
|
6292
6716
|
appendWorkspaceRunLogEvent(runLog.runId, { type: "invalid", error, scheduleNodeId });
|
|
@@ -6350,6 +6774,7 @@ async function runWorkspaceScheduledEntry(root, entry) {
|
|
|
6350
6774
|
runNodeId: targetRunNodeId,
|
|
6351
6775
|
flowId: String(entry.flowId || ""),
|
|
6352
6776
|
flowSource: String(entry.flowSource || "user"),
|
|
6777
|
+
label: scheduleAlias,
|
|
6353
6778
|
plannedNodeIds,
|
|
6354
6779
|
startedAt: Date.now(),
|
|
6355
6780
|
scheduled: true,
|
|
@@ -6645,6 +7070,14 @@ export function startUiServer({
|
|
|
6645
7070
|
|
|
6646
7071
|
const authUser = getAuthUserFromRequest(req);
|
|
6647
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
|
+
}
|
|
6648
7081
|
if (req.method === "GET" && url.pathname === "/api/display/shares") {
|
|
6649
7082
|
try {
|
|
6650
7083
|
if (!authUser?.userId) {
|
|
@@ -7176,6 +7609,40 @@ export function startUiServer({
|
|
|
7176
7609
|
return;
|
|
7177
7610
|
}
|
|
7178
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
|
+
|
|
7179
7646
|
if (req.method === "GET" && url.pathname === "/api/workspace/graph") {
|
|
7180
7647
|
try {
|
|
7181
7648
|
const scoped = resolveWorkspaceScopeRoot(root, {
|
|
@@ -7446,6 +7913,7 @@ export function startUiServer({
|
|
|
7446
7913
|
const controller = new AbortController();
|
|
7447
7914
|
const runId = String(payload.runSessionId || payload.runId || "").trim() || runLedgerId("workspace");
|
|
7448
7915
|
const runKey = workspaceRunEntryKey(scopeKey, runId);
|
|
7916
|
+
const runAlias = String(payload.runAlias || "").trim() || workspaceRuntimeNodeLabel(runtimeGraph, runNodeId, "Workspace Run");
|
|
7449
7917
|
const runEntry = {
|
|
7450
7918
|
scopeKey,
|
|
7451
7919
|
controller,
|
|
@@ -7454,6 +7922,7 @@ export function startUiServer({
|
|
|
7454
7922
|
userId: String(userCtx.userId || ""),
|
|
7455
7923
|
username: String(authUser?.username || userCtx.userId || ""),
|
|
7456
7924
|
runNodeId,
|
|
7925
|
+
label: runAlias,
|
|
7457
7926
|
flowId,
|
|
7458
7927
|
flowSource: scoped.flowSource || payload.flowSource || "user",
|
|
7459
7928
|
plannedNodeIds,
|
|
@@ -7474,7 +7943,7 @@ export function startUiServer({
|
|
|
7474
7943
|
runNodeId,
|
|
7475
7944
|
scheduled: false,
|
|
7476
7945
|
trigger: "manual",
|
|
7477
|
-
label:
|
|
7946
|
+
label: runAlias,
|
|
7478
7947
|
startedAt: runEntry.startedAt,
|
|
7479
7948
|
});
|
|
7480
7949
|
activeWorkspaceRuns.set(runKey, runEntry);
|
|
@@ -7679,10 +8148,12 @@ export function startUiServer({
|
|
|
7679
8148
|
flowId,
|
|
7680
8149
|
flowSource,
|
|
7681
8150
|
runNodeId: entry?.runNodeId || "",
|
|
8151
|
+
label: entry?.label || "",
|
|
7682
8152
|
startedAt: entry?.startedAt || null,
|
|
7683
8153
|
runs: entries.map((item) => ({
|
|
7684
8154
|
runId: item?.runId || "",
|
|
7685
8155
|
runNodeId: item?.runNodeId || "",
|
|
8156
|
+
label: item?.label || "",
|
|
7686
8157
|
startedAt: item?.startedAt || null,
|
|
7687
8158
|
plannedNodeIds: Array.isArray(item?.plannedNodeIds) ? item.plannedNodeIds : [],
|
|
7688
8159
|
scheduled: item?.scheduled === true,
|
|
@@ -8730,6 +9201,60 @@ export function startUiServer({
|
|
|
8730
9201
|
return;
|
|
8731
9202
|
}
|
|
8732
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
|
+
|
|
8733
9258
|
if (req.method === "GET" && url.pathname === "/api/marketplace/nodes") {
|
|
8734
9259
|
try {
|
|
8735
9260
|
const marketplaceScope = url.searchParams.get("scope") === "owned" ? "owned" : "all";
|