@fieldwangai/agentflow 0.1.85 → 0.1.87

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.
@@ -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);
@@ -1988,13 +2300,27 @@ function normalizeDisplayShareNodeIds(ids, graph) {
1988
2300
  const id = String(rawId || "").trim();
1989
2301
  if (!id || seen.has(id)) continue;
1990
2302
  const instance = instances[id];
1991
- if (!workspaceDisplayKind(instance?.definitionId)) continue;
2303
+ if (!workspaceDisplayKindFromInstance(instance)) continue;
1992
2304
  seen.add(id);
1993
2305
  out.push(id);
1994
2306
  }
1995
2307
  return out;
1996
2308
  }
1997
2309
 
2310
+ function workspaceDisplayContentFromInstance(instance, kind = "") {
2311
+ const slots = [...(Array.isArray(instance?.input) ? instance.input : []), ...(Array.isArray(instance?.output) ? instance.output : [])];
2312
+ const primaryName = kind === "image" ? "src" : "content";
2313
+ const slotText = (slot) => workspaceSlotValue(slot);
2314
+ const hasSlotText = (slot) => slotText(slot).trim();
2315
+ const contentSlot =
2316
+ slots.find((slot) => String(slot?.name || "") === primaryName && hasSlotText(slot)) ||
2317
+ slots.find((slot) => String(slot?.name || "") === "result" && hasSlotText(slot)) ||
2318
+ slots.find((slot) => String(slot?.name || "") === "filePath" && hasSlotText(slot)) ||
2319
+ slots.find((slot) => String(slot?.type || "") === "text" && hasSlotText(slot));
2320
+ const slotContent = contentSlot ? slotText(contentSlot) : "";
2321
+ return String(isWorkspaceOneClickTaskDefinitionId(instance?.definitionId) ? slotContent : (instance?.body || slotContent));
2322
+ }
2323
+
1998
2324
  function normalizeDisplayShareLayout(layout, fallback = "canvas") {
1999
2325
  const text = String(layout || "").trim();
2000
2326
  return ["canvas", "gallery", "slides", "document", "single"].includes(text) ? text : fallback;
@@ -2206,8 +2532,8 @@ function publicDisplayPayloadFromShare(root, share) {
2206
2532
  const nodes = nodeIds.map((id) => {
2207
2533
  const instance = instances[id] || {};
2208
2534
  const definitionId = String(instance.definitionId || "");
2209
- const kind = workspaceDisplayKind(definitionId);
2210
- const rawBody = String(instance.body || "");
2535
+ const kind = workspaceDisplayKindFromInstance(instance);
2536
+ const rawBody = workspaceDisplayContentFromInstance(instance, kind);
2211
2537
  const filePath = workspaceDisplayTextFilePath(rawBody, kind);
2212
2538
  let body = rawBody;
2213
2539
  if (filePath) {
@@ -2499,6 +2825,20 @@ function buildWorkspaceGeneratePrompt(payload) {
2499
2825
  ? payload.selectedNodeIds.map((id) => String(id || "").trim()).filter(Boolean)
2500
2826
  : [];
2501
2827
  const skillsBlock = typeof payload?.skillsBlock === "string" ? payload.skillsBlock.trim() : "";
2828
+ const history = Array.isArray(payload?.messages) ? payload.messages : [];
2829
+ const historyBlock = history
2830
+ .slice(-16)
2831
+ .map((msg) => {
2832
+ const text = String(msg?.text || "").trim();
2833
+ if (!text) return "";
2834
+ const kind = String(msg?.kind || "").trim();
2835
+ if (kind === "raw" || kind === "prompt" || kind === "thinking") return "";
2836
+ const role = msg?.role === "user" ? "user" : (msg?.error ? "error" : "assistant");
2837
+ if (kind === "run-summary" || kind === "activity") return `context: ${text}`;
2838
+ return `${role}: ${text}`;
2839
+ })
2840
+ .filter(Boolean)
2841
+ .join("\n\n");
2502
2842
  const contexts = Array.isArray(payload?.contexts) ? payload.contexts : [];
2503
2843
  const contextBlocks = contexts
2504
2844
  .map((ctx, idx) => {
@@ -2525,6 +2865,14 @@ function buildWorkspaceGeneratePrompt(payload) {
2525
2865
  "只输出 ASCII 图正文,不要解释,不要包裹 Markdown 代码围栏。",
2526
2866
  "使用 +-|/\\<> 等字符表达结构,尽量保持对齐。",
2527
2867
  ].join("\n")
2868
+ : outputKind === "react"
2869
+ ? [
2870
+ "你是 workspace React 工程节点的内容生成器。",
2871
+ "请根据用户 prompt 和上游节点/文件上下文生成一个可预览的小型 React 工程 JSON。",
2872
+ "只输出 JSON,不要解释,不要包裹 Markdown 代码围栏。",
2873
+ "JSON 必须包含 title、entry、files;files 至少包含 src/App.jsx,可包含 src/styles.css。",
2874
+ "src/App.jsx 里定义或 export default 一个 App 组件;不要依赖未声明的外部包。",
2875
+ ].join("\n")
2528
2876
  : [
2529
2877
  "你是 AgentFlow Workspace Composer。",
2530
2878
  "默认以用户当前选择的 workspace 节点作为上下文范围;选中节点不是让你重建整张画布的授权。",
@@ -2547,6 +2895,7 @@ function buildWorkspaceGeneratePrompt(payload) {
2547
2895
  skillsBlock ? `\n## Selected Skills\n\n${skillsBlock}` : "",
2548
2896
  kindInstruction,
2549
2897
  contextBlocks ? `\n## 上下文\n\n${contextBlocks}` : "",
2898
+ historyBlock ? `\n## 对话历史\n\n${historyBlock}` : "",
2550
2899
  `\n## 用户 prompt\n\n${userPrompt}`,
2551
2900
  ].filter(Boolean).join("\n");
2552
2901
  }
@@ -2578,6 +2927,8 @@ function buildWorkspaceNodeChatPrompt(payload) {
2578
2927
  ].join("\n")
2579
2928
  : nodeKind === "html"
2580
2929
  ? "只输出完整或片段 HTML,不要解释,不要包裹 Markdown 代码围栏。"
2930
+ : nodeKind === "react"
2931
+ ? "只输出 React 工程 JSON,不要解释,不要包裹 Markdown 代码围栏。JSON 必须包含 title、entry、files;files 至少包含 src/App.jsx。"
2581
2932
  : nodeKind === "image"
2582
2933
  ? "只输出新的图片 src,可以是 URL、data URL 或文件路径,不要解释。"
2583
2934
  : nodeKind === "mermaid"
@@ -3133,12 +3484,21 @@ function workspaceDisplayKind(definitionId) {
3133
3484
  if (id === "display_mermaid") return "mermaid";
3134
3485
  if (id === "display_ascii") return "ascii";
3135
3486
  if (id === "display_html") return "html";
3487
+ if (id === "display_react_app") return "react";
3136
3488
  if (id === "display_image") return "image";
3137
3489
  if (id === "display_chart") return "chart";
3138
3490
  if (id === "display_table") return "table";
3139
3491
  return "";
3140
3492
  }
3141
3493
 
3494
+ function workspaceDisplayKindFromInstance(instance) {
3495
+ const direct = workspaceDisplayKind(instance?.definitionId);
3496
+ if (direct) return direct;
3497
+ if (!isWorkspaceOneClickTaskDefinitionId(instance?.definitionId)) return "";
3498
+ if (!workspaceDisplayContentFromInstance(instance, workspaceContextRunDisplayKind(instance)).trim()) return "";
3499
+ return workspaceContextRunDisplayKind(instance);
3500
+ }
3501
+
3142
3502
  function workspaceDisplayTextFilePath(value, kind = "") {
3143
3503
  const text = String(value || "").trim();
3144
3504
  if (!text || text.length > 260) return "";
@@ -3149,6 +3509,7 @@ function workspaceDisplayTextFilePath(value, kind = "") {
3149
3509
  const ext = clean.split("?")[0].split("#")[0].toLowerCase().split(".").pop() || "";
3150
3510
  const allowedByKind = {
3151
3511
  html: new Set(["html", "htm"]),
3512
+ react: new Set(["json", "jsx", "tsx", "js", "txt"]),
3152
3513
  markdown: new Set(["md", "markdown", "txt"]),
3153
3514
  mermaid: new Set(["mmd", "mermaid", "txt"]),
3154
3515
  ascii: new Set(["txt", "log"]),
@@ -3222,6 +3583,7 @@ function workspaceDisplayKindExample(kind, field) {
3222
3583
  if (kind === "table") return { columns: ["列名1", "列名2"], rows: [["值1", "值2"]] };
3223
3584
  if (kind === "chart") return { type: "chart", version: "1.0", renderer: "echarts", option: { xAxis: { type: "category", data: [] }, yAxis: { type: "value" }, series: [{ type: "bar", data: [] }] } };
3224
3585
  if (kind === "html") return "<可直接渲染的 HTML>";
3586
+ 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
3587
  if (kind === "mermaid") return "flowchart TD\n A[开始] --> B[结束]";
3226
3588
  if (kind === "ascii") return "+---+\n| |\n+---+";
3227
3589
  if (kind === "image") return "<图片 URL 或 data URL>";
@@ -3357,12 +3719,16 @@ function workspaceOutputProtocolRequirements(graph, nodeId) {
3357
3719
  .filter((slot) => {
3358
3720
  const name = String(slot?.name || "").trim();
3359
3721
  const type = String(slot?.type || "");
3360
- return name && type !== "node" && name !== "next" && name !== "result" && name !== "content";
3722
+ return name && type !== "node" && name !== "next" && name !== "result" && name !== "content" && name !== "displayType";
3361
3723
  })
3362
3724
  .map((slot) => String(slot.name).trim());
3363
- const resultKind = displayByField.get("result") || workspaceDownstreamOutputKindForField(graph, nodeId, "result") || "";
3725
+ const configuredResultKind = isWorkspaceOneClickTaskDefinitionId(instance.definitionId)
3726
+ ? workspaceContextRunDisplayKind(instance)
3727
+ : "";
3728
+ const resultKind = displayByField.get("result") || workspaceDownstreamOutputKindForField(graph, nodeId, "result") || configuredResultKind || "";
3364
3729
  const resultExtByKind = {
3365
3730
  html: "html",
3731
+ react: "json",
3366
3732
  markdown: "md",
3367
3733
  mermaid: "mmd",
3368
3734
  ascii: "txt",
@@ -3374,6 +3740,7 @@ function workspaceOutputProtocolRequirements(graph, nodeId) {
3374
3740
  const resultKindText = resultKind ? ` ${resultKind}` : "";
3375
3741
  const resultGuidance = {
3376
3742
  html: "内容必须是可直接放入 iframe 渲染的 HTML;不要使用 Markdown 代码围栏。",
3743
+ react: "内容必须是 React 工程 JSON,包含 title、entry、files;files 至少包含 src/App.jsx,可包含 CSS 文件。",
3377
3744
  markdown: "内容必须是 Markdown 正文;除非正文确实需要代码块,否则不要额外包裹代码围栏。",
3378
3745
  mermaid: "内容必须是 Mermaid 图表代码,例如 flowchart/sequenceDiagram;不要使用 Markdown 代码围栏。",
3379
3746
  ascii: "内容必须是纯文本/ASCII 图或表格;不要输出 HTML 或 Markdown 装饰。",
@@ -3480,8 +3847,11 @@ function workspaceRunPlan(graph, runNodeId, scopedRoot = "") {
3480
3847
  if (needed.size !== before) visitControlDownstream(next);
3481
3848
  }
3482
3849
  };
3850
+ const targetDefId = String(instances[target]?.definitionId || "");
3851
+ const targetIsRunController = targetDefId === "workspace_run" || targetDefId === "workspace_scheduled_run";
3852
+ if (!targetIsRunController) needed.add(target);
3483
3853
  visitControlDownstream(target);
3484
- needed.delete(target);
3854
+ if (targetIsRunController) needed.delete(target);
3485
3855
  const dependencyQueue = Array.from(needed);
3486
3856
  for (let i = 0; i < dependencyQueue.length; i++) {
3487
3857
  const id = dependencyQueue[i];
@@ -4362,6 +4732,10 @@ function parseWorkspaceSkillKeys(raw) {
4362
4732
  function selectedSkillKeysFromInstance(instance) {
4363
4733
  const bodyKeys = parseWorkspaceSkillKeys(instance?.body || "");
4364
4734
  if (bodyKeys.length > 0) return bodyKeys;
4735
+ return selectedSkillKeysFromConfigSlots(instance);
4736
+ }
4737
+
4738
+ function selectedSkillKeysFromConfigSlots(instance) {
4365
4739
  const slots = [...(Array.isArray(instance?.input) ? instance.input : []), ...(Array.isArray(instance?.output) ? instance.output : [])];
4366
4740
  const slot = slots.find((item) => item?.name === "skillsContext") || slots.find((item) => item?.name === "skillKeys");
4367
4741
  return parseWorkspaceSkillKeys(workspaceSlotValue(slot) || "");
@@ -4475,6 +4849,34 @@ function workspaceNodeWorkspaceContextBlock(graph, nodeId, outputs, scopedRoot =
4475
4849
  return lines.join("\n");
4476
4850
  }
4477
4851
 
4852
+ function workspaceDefaultWorkspaceContextBlock(scopedRoot = "", logicalCwd = "") {
4853
+ const root = scopedRoot ? path.resolve(scopedRoot) : "";
4854
+ const cwd = logicalCwd ? path.resolve(logicalCwd) : root;
4855
+ if (!root && !cwd) return "";
4856
+ return [
4857
+ "## Workspace 上下文",
4858
+ "",
4859
+ "当前 Agent 仍在独立节点目录中运行,文件边界以“文件边界”章节为准。",
4860
+ cwd ? `- 当前工作目录上下文:\`${cwd}\`` : "",
4861
+ root ? `- 流程目录:\`${root}\`` : "",
4862
+ "",
4863
+ "使用要求:",
4864
+ "- 读取、搜索、分析当前项目或资料时,优先从“当前工作目录上下文”开始;不要把节点的“当前执行目录”误认为项目根目录。",
4865
+ "- 临时文件和正式产物仍必须按“文件边界”写入本节点的 `tmp/` 与 `outputs/`。",
4866
+ ].filter((line) => line !== "").join("\n");
4867
+ }
4868
+
4869
+ function isWorkspaceOneClickTaskDefinitionId(definitionId) {
4870
+ const id = String(definitionId || "");
4871
+ return id === "workspace_one_click_task" || id === "workspace_context_run";
4872
+ }
4873
+
4874
+ function workspaceContextRunDisplayKind(instance) {
4875
+ const raw = workspaceSlotValue(workspaceSlotByName(instance, "displayType")).trim().toLowerCase();
4876
+ if (["markdown", "html", "react", "table", "chart", "ascii", "mermaid"].includes(raw)) return raw;
4877
+ return "markdown";
4878
+ }
4879
+
4478
4880
  function mergeWorkspaceSkillBlocks(...values) {
4479
4881
  const blocks = values
4480
4882
  .map((value) => String(value || ""))
@@ -4983,6 +5385,12 @@ async function workspaceRunToolNodejsScript({
4983
5385
  });
4984
5386
  }
4985
5387
 
5388
+ function workspaceNodeModelKey(instance, fallback = "") {
5389
+ const own = String(instance?.model || "").trim();
5390
+ if (own && own !== "default") return own;
5391
+ return String(fallback || "").trim();
5392
+ }
5393
+
4986
5394
  async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts = {}) {
4987
5395
  const graph = hydrateWorkspaceGraphForRuntime(root, {
4988
5396
  root: scopedRoot,
@@ -5225,17 +5633,49 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
5225
5633
  const inputSlots = Array.isArray(instance.input) ? instance.input : [];
5226
5634
  const pathSlot = inputSlots.find((slot) => String(slot?.name || "") === "path") ||
5227
5635
  inputSlots.find((slot) => String(slot?.name || "") === "target");
5636
+ const labelSlot = inputSlots.find((slot) => String(slot?.name || "") === "label");
5637
+ const contextSlot = inputSlots.find((slot) => String(slot?.name || "") === "workspaceContext");
5228
5638
  const candidate = workspaceSlotValue(pathSlot) || workspaceInstanceText(instance) || inputText;
5229
5639
  const abs = candidate ? path.resolve(scopedRoot, candidate) : scopedRoot;
5230
- if (fs.existsSync(abs) && fs.statSync(abs).isDirectory()) cwd = abs;
5231
- publishNodeOutput(nodeId, cwd, { emitGraph: true });
5640
+ if (!fs.existsSync(abs) || !fs.statSync(abs).isDirectory()) {
5641
+ throw new Error(`Load Workspace path does not exist or is not a directory: ${abs}`);
5642
+ }
5643
+ cwd = abs;
5644
+ const parsedContext = parseJsonText(workspaceSlotValue(contextSlot), {});
5645
+ const configuredContext = parsedContext && typeof parsedContext === "object" && !Array.isArray(parsedContext) ? parsedContext : {};
5646
+ const workspaceContext = {
5647
+ ...configuredContext,
5648
+ version: 1,
5649
+ label: workspaceSlotValue(labelSlot) || path.basename(cwd) || "workspace",
5650
+ cwd,
5651
+ workspaceRoot: cwd,
5652
+ pipelineWorkspace: path.resolve(scopedRoot),
5653
+ previous: null,
5654
+ };
5655
+ let nextInstance = workspaceSetOutputSlot(instance, "workspaceContext", JSON.stringify(workspaceContext));
5656
+ nextInstance = workspaceSetOutputSlot(nextInstance, "cwd", cwd);
5657
+ graph.instances[nodeId] = nextInstance;
5658
+ publishNodeOutput(nodeId, JSON.stringify(workspaceContext), { emitGraph: true });
5659
+ emit({ type: "graph", nodeId, graph });
5232
5660
  emit({ type: "node-done", nodeId, definitionId: defId });
5233
5661
  continue;
5234
5662
  }
5235
5663
 
5236
5664
  if (defId === "control_user_workspace") {
5237
5665
  cwd = path.resolve(os.homedir());
5238
- publishNodeOutput(nodeId, cwd, { emitGraph: true });
5666
+ const workspaceContext = {
5667
+ version: 1,
5668
+ label: "home",
5669
+ cwd,
5670
+ workspaceRoot: cwd,
5671
+ pipelineWorkspace: path.resolve(scopedRoot),
5672
+ previous: null,
5673
+ };
5674
+ let nextInstance = workspaceSetOutputSlot(instance, "workspaceContext", JSON.stringify(workspaceContext));
5675
+ nextInstance = workspaceSetOutputSlot(nextInstance, "cwd", cwd);
5676
+ graph.instances[nodeId] = nextInstance;
5677
+ publishNodeOutput(nodeId, JSON.stringify(workspaceContext), { emitGraph: true });
5678
+ emit({ type: "graph", nodeId, graph });
5239
5679
  emit({ type: "node-done", nodeId, definitionId: defId });
5240
5680
  continue;
5241
5681
  }
@@ -5511,6 +5951,7 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
5511
5951
  }
5512
5952
 
5513
5953
  if (defId === "tool_nodejs") {
5954
+ const nodeModelKey = workspaceNodeModelKey(instance, modelKey);
5514
5955
  const prepareStartedAt = Date.now();
5515
5956
  const inputValues = workspaceInputValues(graph, nodeId, outputs, scopedRoot);
5516
5957
  const runPackage = workspaceCreateNodeRunPackage(runTmpRoot, nodeId, {
@@ -5544,7 +5985,7 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
5544
5985
  resultContent,
5545
5986
  structured: normalizedAgentOutput,
5546
5987
  runPackage,
5547
- modelKey,
5988
+ modelKey: nodeModelKey,
5548
5989
  userCtx,
5549
5990
  emit: (event) => emit({ ...event, nodeId }),
5550
5991
  onActiveChild: opts.onActiveChild,
@@ -5556,12 +5997,15 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
5556
5997
  continue;
5557
5998
  }
5558
5999
 
6000
+ const isContextRunNode = isWorkspaceOneClickTaskDefinitionId(defId);
6001
+ const nodeModelKey = workspaceNodeModelKey(instance, modelKey);
5559
6002
  const prepareStartedAt = Date.now();
5560
6003
  const inputValues = workspaceInputValues(graph, nodeId, outputs, scopedRoot);
5561
6004
  const relevantInputs = workspaceRelevantInputValues(instance.body || "", inputValues);
5562
6005
  const upstreamText = workspaceTaskUpstreamText(graph, nodeId, outputs, relevantInputs.placeholders, scopedRoot);
5563
6006
  const upstreamSkillBlocks = workspaceUpstreamSkillBlocks(graph, nodeId, outputs);
5564
- const promptSkillsBlock = mergeWorkspaceSkillBlocks(upstreamSkillBlocks);
6007
+ const ownSkillBlock = isContextRunNode ? loadSkillsBlockForKeys(selectedSkillKeysFromConfigSlots(instance)) : "";
6008
+ const promptSkillsBlock = mergeWorkspaceSkillBlocks(ownSkillBlock, upstreamSkillBlocks);
5565
6009
  const promptMcpBlock = workspaceUpstreamMcpBlocks(graph, nodeId, outputs);
5566
6010
  const runPackage = workspaceCreateNodeRunPackage(runTmpRoot, nodeId, {
5567
6011
  scopedRoot,
@@ -5583,7 +6027,10 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
5583
6027
  // Best-effort debug artifact only.
5584
6028
  }
5585
6029
  const historyBlock = workspaceNodeHistoryBlock(nodeId, scopedRoot, runPackage);
5586
- const workspaceContextBlock = workspaceNodeWorkspaceContextBlock(graph, nodeId, outputs, scopedRoot, cwd);
6030
+ let workspaceContextBlock = workspaceNodeWorkspaceContextBlock(graph, nodeId, outputs, scopedRoot, cwd);
6031
+ if (isContextRunNode && workspaceBoolSlot(instance, "includeWorkspaceContext", true) && !workspaceContextBlock) {
6032
+ workspaceContextBlock = workspaceDefaultWorkspaceContextBlock(scopedRoot, cwd);
6033
+ }
5587
6034
  const prompt = workspaceNodePrompt(graph, nodeId, promptUpstreamText, promptSkillsBlock, promptMcpBlock, runtimeInputValues, runPackage, historyBlock, workspaceContextBlock);
5588
6035
  try {
5589
6036
  fs.writeFileSync(path.join(runPackage.nodeRunDir, "prompt.md"), prompt.trimEnd() + "\n", "utf-8");
@@ -5598,6 +6045,7 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
5598
6045
  nodeRunDir: runPackage.nodeRunDir,
5599
6046
  });
5600
6047
  emit({ type: "natural", kind: "prompt", nodeId, text: prompt });
6048
+ emit({ type: "status", nodeId, line: `Model: ${nodeModelKey || "default"}` });
5601
6049
  let content = "";
5602
6050
  const runHistoryEvents = [];
5603
6051
  const maxAttempts = 3;
@@ -5612,7 +6060,7 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
5612
6060
  uiWorkspaceRoot: scopedRoot,
5613
6061
  cliWorkspace: runPackage.nodeRunDir,
5614
6062
  prompt,
5615
- modelKey,
6063
+ modelKey: nodeModelKey,
5616
6064
  agentflowUserId: userCtx.userId || "",
5617
6065
  extraEnv: runtimeEnv({
5618
6066
  AGENTFLOW_WORKSPACE_TMP_ROOT: runTmpRoot,
@@ -5682,15 +6130,20 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
5682
6130
  resultContent,
5683
6131
  structured: normalizedAgentOutput,
5684
6132
  runPackage,
5685
- modelKey,
6133
+ modelKey: nodeModelKey,
5686
6134
  userCtx,
5687
6135
  historyEvents: runHistoryEvents,
5688
6136
  emit: (event) => emit({ ...event, nodeId }),
5689
6137
  onActiveChild: opts.onActiveChild,
5690
6138
  });
5691
6139
  if (implementationUpdate.changed) graph.instances[nodeId] = implementationUpdate.instance;
6140
+ let contextRunOutputChanged = false;
6141
+ if (isContextRunNode) {
6142
+ graph.instances[nodeId] = workspaceSetOutputSlot(graph.instances[nodeId] || instance, "displayType", workspaceContextRunDisplayKind(instance));
6143
+ contextRunOutputChanged = true;
6144
+ }
5692
6145
  const updatedDisplays = propagateNodeOutputDisplays(nodeId, resultContent);
5693
- if (slotUpdate.changed || implementationUpdate.changed || updatedDisplays.length) emit({ type: "graph", nodeId, displayNodeIds: updatedDisplays, graph });
6146
+ if (slotUpdate.changed || implementationUpdate.changed || contextRunOutputChanged || updatedDisplays.length) emit({ type: "graph", nodeId, displayNodeIds: updatedDisplays, graph });
5694
6147
  emit({ type: "node-done", nodeId, definitionId: defId });
5695
6148
  }
5696
6149
  } finally {
@@ -5910,6 +6363,13 @@ function workspaceRunEntryKey(scopeKey, runId) {
5910
6363
  return `${scopeKey}:${String(runId || "").trim() || runLedgerId("workspace")}`;
5911
6364
  }
5912
6365
 
6366
+ function workspaceRuntimeNodeLabel(graph, nodeId, fallback = "Workspace Run") {
6367
+ const id = String(nodeId || "").trim();
6368
+ const instance = graph?.instances && typeof graph.instances === "object" ? graph.instances[id] : null;
6369
+ const label = String(instance?.label || "").trim();
6370
+ return label || id || fallback;
6371
+ }
6372
+
5913
6373
  function workspaceActiveRunsForScope(scopeKey) {
5914
6374
  const key = String(scopeKey || "");
5915
6375
  return Array.from(activeWorkspaceRuns.entries())
@@ -6287,6 +6747,7 @@ async function runWorkspaceScheduledEntry(root, entry) {
6287
6747
  return;
6288
6748
  }
6289
6749
  const targetRunNodeId = workspaceScheduleInferTargetRunNodeId(graph, scheduleNodeId, config);
6750
+ const scheduleAlias = workspaceRuntimeNodeLabel(graph, scheduleNodeId, String(entry.label || "Scheduled Run"));
6290
6751
  if (!targetRunNodeId) {
6291
6752
  const error = "Scheduled Run node is missing";
6292
6753
  appendWorkspaceRunLogEvent(runLog.runId, { type: "invalid", error, scheduleNodeId });
@@ -6350,6 +6811,7 @@ async function runWorkspaceScheduledEntry(root, entry) {
6350
6811
  runNodeId: targetRunNodeId,
6351
6812
  flowId: String(entry.flowId || ""),
6352
6813
  flowSource: String(entry.flowSource || "user"),
6814
+ label: scheduleAlias,
6353
6815
  plannedNodeIds,
6354
6816
  startedAt: Date.now(),
6355
6817
  scheduled: true,
@@ -6645,6 +7107,14 @@ export function startUiServer({
6645
7107
 
6646
7108
  const authUser = getAuthUserFromRequest(req);
6647
7109
  const userCtx = authUser ? { userId: authUser.userId, isAdmin: Boolean(authUser.isAdmin) } : {};
7110
+ if (req.method === "GET" && url.pathname === "/api/auth/session-token") {
7111
+ if (!authUser?.userId) {
7112
+ json(res, 401, { error: "Unauthorized" });
7113
+ return;
7114
+ }
7115
+ json(res, 200, { token: getSessionTokenFromRequest(req) || "" });
7116
+ return;
7117
+ }
6648
7118
  if (req.method === "GET" && url.pathname === "/api/display/shares") {
6649
7119
  try {
6650
7120
  if (!authUser?.userId) {
@@ -7176,6 +7646,40 @@ export function startUiServer({
7176
7646
  return;
7177
7647
  }
7178
7648
 
7649
+ if (req.method === "GET" && url.pathname === "/api/workspaces") {
7650
+ try {
7651
+ const scoped = resolveWorkspaceScopeRoot(root, {
7652
+ flowId: url.searchParams.get("flowId") || "",
7653
+ flowSource: url.searchParams.get("flowSource") || "user",
7654
+ archived: url.searchParams.get("archived") === "1",
7655
+ }, userCtx);
7656
+ const scopedRoot = scoped.error ? root : scoped.root;
7657
+ json(res, 200, {
7658
+ path: userWorkspacesPath(userCtx),
7659
+ workspaces: listConfiguredWorkspaces(root, scopedRoot, userCtx),
7660
+ customWorkspaces: readUserWorkspaces(userCtx),
7661
+ });
7662
+ } catch (e) {
7663
+ json(res, 500, { error: (e && e.message) || String(e) });
7664
+ }
7665
+ return;
7666
+ }
7667
+
7668
+ if (req.method === "POST" && url.pathname === "/api/workspaces") {
7669
+ try {
7670
+ const payload = await readJson(req);
7671
+ const customWorkspaces = writeUserWorkspaces(userCtx, payload?.workspaces || payload?.customWorkspaces || []);
7672
+ json(res, 200, {
7673
+ path: userWorkspacesPath(userCtx),
7674
+ workspaces: listConfiguredWorkspaces(root, root, userCtx),
7675
+ customWorkspaces,
7676
+ });
7677
+ } catch (e) {
7678
+ json(res, 500, { error: (e && e.message) || String(e) });
7679
+ }
7680
+ return;
7681
+ }
7682
+
7179
7683
  if (req.method === "GET" && url.pathname === "/api/workspace/graph") {
7180
7684
  try {
7181
7685
  const scoped = resolveWorkspaceScopeRoot(root, {
@@ -7446,6 +7950,7 @@ export function startUiServer({
7446
7950
  const controller = new AbortController();
7447
7951
  const runId = String(payload.runSessionId || payload.runId || "").trim() || runLedgerId("workspace");
7448
7952
  const runKey = workspaceRunEntryKey(scopeKey, runId);
7953
+ const runAlias = String(payload.runAlias || "").trim() || workspaceRuntimeNodeLabel(runtimeGraph, runNodeId, "Workspace Run");
7449
7954
  const runEntry = {
7450
7955
  scopeKey,
7451
7956
  controller,
@@ -7454,6 +7959,7 @@ export function startUiServer({
7454
7959
  userId: String(userCtx.userId || ""),
7455
7960
  username: String(authUser?.username || userCtx.userId || ""),
7456
7961
  runNodeId,
7962
+ label: runAlias,
7457
7963
  flowId,
7458
7964
  flowSource: scoped.flowSource || payload.flowSource || "user",
7459
7965
  plannedNodeIds,
@@ -7474,7 +7980,7 @@ export function startUiServer({
7474
7980
  runNodeId,
7475
7981
  scheduled: false,
7476
7982
  trigger: "manual",
7477
- label: String(runtimeGraph.instances?.[runNodeId]?.label || "Workspace Run"),
7983
+ label: runAlias,
7478
7984
  startedAt: runEntry.startedAt,
7479
7985
  });
7480
7986
  activeWorkspaceRuns.set(runKey, runEntry);
@@ -7679,10 +8185,12 @@ export function startUiServer({
7679
8185
  flowId,
7680
8186
  flowSource,
7681
8187
  runNodeId: entry?.runNodeId || "",
8188
+ label: entry?.label || "",
7682
8189
  startedAt: entry?.startedAt || null,
7683
8190
  runs: entries.map((item) => ({
7684
8191
  runId: item?.runId || "",
7685
8192
  runNodeId: item?.runNodeId || "",
8193
+ label: item?.label || "",
7686
8194
  startedAt: item?.startedAt || null,
7687
8195
  plannedNodeIds: Array.isArray(item?.plannedNodeIds) ? item.plannedNodeIds : [],
7688
8196
  scheduled: item?.scheduled === true,
@@ -8730,6 +9238,60 @@ export function startUiServer({
8730
9238
  return;
8731
9239
  }
8732
9240
 
9241
+ if (req.method === "GET" && url.pathname === "/api/node-studio/drafts") {
9242
+ try {
9243
+ json(res, 200, { drafts: listNodeStudioDrafts(userCtx) });
9244
+ } catch (e) {
9245
+ json(res, 500, { error: (e && e.message) || String(e) });
9246
+ }
9247
+ return;
9248
+ }
9249
+
9250
+ if (req.method === "GET" && url.pathname === "/api/node-studio/draft") {
9251
+ try {
9252
+ const id = url.searchParams.get("id") || "";
9253
+ if (!id) {
9254
+ json(res, 200, { draft: null });
9255
+ return;
9256
+ }
9257
+ const draft = readNodeStudioDraft(userCtx, id);
9258
+ json(res, 200, { draft: draft && !isLegacyNodeStudioDemoDraft(draft) ? draft : null });
9259
+ } catch (e) {
9260
+ json(res, 500, { error: (e && e.message) || String(e) });
9261
+ }
9262
+ return;
9263
+ }
9264
+
9265
+ if (req.method === "POST" && url.pathname === "/api/node-studio/draft") {
9266
+ let payload;
9267
+ try {
9268
+ payload = JSON.parse(await readBody(req));
9269
+ } catch {
9270
+ json(res, 400, { error: "Invalid JSON body" });
9271
+ return;
9272
+ }
9273
+ try {
9274
+ const current = readNodeStudioDraft(userCtx, payload.id || "") || emptyNodeStudioDraft(userCtx, payload.id || "untitled_node");
9275
+ const promptDraft = payload.promptDraft != null ? String(payload.promptDraft) : current.promptDraft || "";
9276
+ const agentMessages = Array.isArray(current.agentMessages) ? [...current.agentMessages] : [];
9277
+ if (payload.appendUserMessage === true && promptDraft.trim()) {
9278
+ const at = new Date().toISOString();
9279
+ agentMessages.push({ role: "user", text: promptDraft.trim(), at });
9280
+ agentMessages.push({ role: "assistant", text: "已记录需求,下一步会由节点 Agent 更新 manifest、脚本和 UI schema。", at });
9281
+ }
9282
+ const draft = writeNodeStudioDraft(userCtx, {
9283
+ ...current,
9284
+ ...(payload.config && typeof payload.config === "object" ? { config: { ...(current.config || {}), ...payload.config } } : {}),
9285
+ promptDraft,
9286
+ agentMessages,
9287
+ });
9288
+ json(res, 200, { ok: true, draft });
9289
+ } catch (e) {
9290
+ json(res, 500, { error: (e && e.message) || String(e) });
9291
+ }
9292
+ return;
9293
+ }
9294
+
8733
9295
  if (req.method === "GET" && url.pathname === "/api/marketplace/nodes") {
8734
9296
  try {
8735
9297
  const marketplaceScope = url.searchParams.get("scope") === "owned" ? "owned" : "all";