@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.
@@ -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 resultKind = displayByField.get("result") || workspaceDownstreamOutputKindForField(graph, nodeId, "result") || "";
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) && fs.statSync(abs).isDirectory()) cwd = abs;
5231
- publishNodeOutput(nodeId, cwd, { emitGraph: true });
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
- publishNodeOutput(nodeId, cwd, { emitGraph: true });
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 promptSkillsBlock = mergeWorkspaceSkillBlocks(upstreamSkillBlocks);
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
- const workspaceContextBlock = workspaceNodeWorkspaceContextBlock(graph, nodeId, outputs, scopedRoot, cwd);
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: String(runtimeGraph.instances?.[runNodeId]?.label || "Workspace Run"),
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";