@fieldwangai/agentflow 0.1.159 → 0.1.162

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.
Files changed (118) hide show
  1. package/bin/lib/auth.mjs +231 -14
  2. package/bin/lib/catalog-flows.mjs +12 -1
  3. package/bin/lib/cli-auth-page.mjs +103 -0
  4. package/bin/lib/control-while.mjs +336 -0
  5. package/bin/lib/flow-dsl/codegen.mjs +74 -11
  6. package/bin/lib/flow-dsl/ir.mjs +33 -2
  7. package/bin/lib/flow-dsl/lint.mjs +128 -3
  8. package/bin/lib/flow-dsl/parser.mjs +168 -4
  9. package/bin/lib/marketplace.mjs +10 -2
  10. package/bin/lib/node-package-manifest.mjs +7 -2
  11. package/bin/lib/node-ui-kit.mjs +156 -0
  12. package/bin/lib/paths.mjs +2 -0
  13. package/bin/lib/skill-runtime.mjs +6 -0
  14. package/bin/lib/table.mjs +2 -3
  15. package/bin/lib/ui-server.mjs +220 -17
  16. package/bin/lib/workspace-draft.mjs +74 -0
  17. package/bin/lib/workspace-flow-store.mjs +11 -2
  18. package/bin/lib/workspace-graph-merge.mjs +3 -0
  19. package/bin/lib/workspace-routes.mjs +307 -5
  20. package/bin/lib/workspace-server.mjs +598 -11
  21. package/bin/pipeline/validate-flow.mjs +3 -3
  22. package/builtin/nodes/control_parse_json.md +25 -0
  23. package/builtin/nodes/control_subflow_call.md +27 -0
  24. package/builtin/nodes/control_while.md +124 -0
  25. package/builtin/nodes/provide_json.md +14 -0
  26. package/builtin/nodes/workspace_subflow_input.md +14 -0
  27. package/builtin/pipelines/new/workspace.flow.js +1 -1
  28. package/builtin/pipelines/subflow-preview/workspace.flow.js +29 -0
  29. package/builtin/pipelines/subflow-preview/workspace.layout.json +38 -0
  30. package/builtin/pipelines/subflow-preview/workspace.nodes.json +19 -0
  31. package/builtin/pipelines/subflow-preview/workspace.state.json +93 -0
  32. package/builtin/pipelines/while-subflow-preview/workspace.flow.js +55 -0
  33. package/builtin/pipelines/while-subflow-preview/workspace.layout.json +66 -0
  34. package/builtin/pipelines/while-subflow-preview/workspace.nodes.json +49 -0
  35. package/builtin/pipelines/while-subflow-preview/workspace.state.json +194 -0
  36. package/builtin/web-ui/dist/assets/{WorkflowAssistantThread-CY19DsYq.js → WorkflowAssistantThread-CTrXOZ00.js} +1 -1
  37. package/builtin/web-ui/dist/assets/index-5uJFccdX.css +1 -0
  38. package/builtin/web-ui/dist/assets/index-BeUfNQRL.js +873 -0
  39. package/builtin/web-ui/dist/index.html +2 -2
  40. package/package.json +6 -1
  41. package/reference/flow-control-capabilities.md +41 -16
  42. package/shared/slot-types.js +58 -0
  43. package/skills/agentflow-author-flow/SKILL.md +81 -8
  44. package/skills/agentflow-author-flow/agents/openai.yaml +3 -3
  45. package/skills/agentflow-cli/SKILL.md +130 -25
  46. package/skills/agentflow-cli/agents/openai.yaml +2 -2
  47. package/skills/agentflow-cli/runtime/bin/lib/skill-runtime.mjs +14765 -0
  48. package/skills/agentflow-cli/runtime/builtin/nodes/agent_subAgent.md +34 -0
  49. package/skills/agentflow-cli/runtime/builtin/nodes/control_agent_toBool.md +26 -0
  50. package/skills/agentflow-cli/runtime/builtin/nodes/control_anyOne.md +19 -0
  51. package/skills/agentflow-cli/runtime/builtin/nodes/control_cancelled.md +26 -0
  52. package/skills/agentflow-cli/runtime/builtin/nodes/control_cd_workspace.md +50 -0
  53. package/skills/agentflow-cli/runtime/builtin/nodes/control_delay.md +31 -0
  54. package/skills/agentflow-cli/runtime/builtin/nodes/control_end.md +13 -0
  55. package/skills/agentflow-cli/runtime/builtin/nodes/control_if.md +21 -0
  56. package/skills/agentflow-cli/runtime/builtin/nodes/control_interval_loop.md +55 -0
  57. package/skills/agentflow-cli/runtime/builtin/nodes/control_load_mcp.md +24 -0
  58. package/skills/agentflow-cli/runtime/builtin/nodes/control_load_skills.md +35 -0
  59. package/skills/agentflow-cli/runtime/builtin/nodes/control_parse_json.md +25 -0
  60. package/skills/agentflow-cli/runtime/builtin/nodes/control_start.md +13 -0
  61. package/skills/agentflow-cli/runtime/builtin/nodes/control_subflow_call.md +27 -0
  62. package/skills/agentflow-cli/runtime/builtin/nodes/control_toBool.md +27 -0
  63. package/skills/agentflow-cli/runtime/builtin/nodes/control_user_workspace.md +24 -0
  64. package/skills/agentflow-cli/runtime/builtin/nodes/control_wait_until.md +34 -0
  65. package/skills/agentflow-cli/runtime/builtin/nodes/control_while.md +124 -0
  66. package/skills/agentflow-cli/runtime/builtin/nodes/display_ascii.md +24 -0
  67. package/skills/agentflow-cli/runtime/builtin/nodes/display_chart.md +32 -0
  68. package/skills/agentflow-cli/runtime/builtin/nodes/display_code.md +39 -0
  69. package/skills/agentflow-cli/runtime/builtin/nodes/display_html.md +32 -0
  70. package/skills/agentflow-cli/runtime/builtin/nodes/display_image.md +36 -0
  71. package/skills/agentflow-cli/runtime/builtin/nodes/display_markdown.md +24 -0
  72. package/skills/agentflow-cli/runtime/builtin/nodes/display_mermaid.md +24 -0
  73. package/skills/agentflow-cli/runtime/builtin/nodes/display_react_app.md +53 -0
  74. package/skills/agentflow-cli/runtime/builtin/nodes/display_table.md +32 -0
  75. package/skills/agentflow-cli/runtime/builtin/nodes/provide_bool.md +14 -0
  76. package/skills/agentflow-cli/runtime/builtin/nodes/provide_file.md +14 -0
  77. package/skills/agentflow-cli/runtime/builtin/nodes/provide_json.md +14 -0
  78. package/skills/agentflow-cli/runtime/builtin/nodes/provide_password.md +14 -0
  79. package/skills/agentflow-cli/runtime/builtin/nodes/provide_str.md +14 -0
  80. package/skills/agentflow-cli/runtime/builtin/nodes/tool_display_share_link.md +47 -0
  81. package/skills/agentflow-cli/runtime/builtin/nodes/tool_get_env.md +20 -0
  82. package/skills/agentflow-cli/runtime/builtin/nodes/tool_git_checkout.md +70 -0
  83. package/skills/agentflow-cli/runtime/builtin/nodes/tool_git_worktree_load.md +73 -0
  84. package/skills/agentflow-cli/runtime/builtin/nodes/tool_git_worktree_unload.md +52 -0
  85. package/skills/agentflow-cli/runtime/builtin/nodes/tool_gitlab_create_mr.md +114 -0
  86. package/skills/agentflow-cli/runtime/builtin/nodes/tool_jenkins_build.md +67 -0
  87. package/skills/agentflow-cli/runtime/builtin/nodes/tool_load_key.md +26 -0
  88. package/skills/agentflow-cli/runtime/builtin/nodes/tool_nodejs.md +52 -0
  89. package/skills/agentflow-cli/runtime/builtin/nodes/tool_print.md +21 -0
  90. package/skills/agentflow-cli/runtime/builtin/nodes/tool_save_key.md +26 -0
  91. package/skills/agentflow-cli/runtime/builtin/nodes/tool_set_run_env.md +38 -0
  92. package/skills/agentflow-cli/runtime/builtin/nodes/tool_user_ask.md +27 -0
  93. package/skills/agentflow-cli/runtime/builtin/nodes/tool_user_check.md +26 -0
  94. package/skills/agentflow-cli/runtime/builtin/nodes/tool_wecom_send_app_markdown.md +57 -0
  95. package/skills/agentflow-cli/runtime/builtin/nodes/tool_wecom_send_group_markdown.md +45 -0
  96. package/skills/agentflow-cli/runtime/builtin/nodes/workspace_one_click_task.md +44 -0
  97. package/skills/agentflow-cli/runtime/builtin/nodes/workspace_run.md +16 -0
  98. package/skills/agentflow-cli/runtime/builtin/nodes/workspace_scheduled_run.md +16 -0
  99. package/skills/agentflow-cli/runtime/builtin/nodes/workspace_subflow_input.md +14 -0
  100. package/skills/agentflow-cli/runtime/package.json +6 -0
  101. package/skills/agentflow-cli/scripts/agentflow-auth-store.mjs +113 -0
  102. package/skills/agentflow-cli/scripts/agentflow-cli.mjs +393 -28
  103. package/skills/agentflow-cli/scripts/agentflow-runtime.mjs +11 -4
  104. package/skills/agentflow-flow-add-instances/SKILL.md +2 -2
  105. package/skills/agentflow-flow-dsl/SKILL.md +83 -10
  106. package/skills/agentflow-flow-dsl/agents/openai.yaml +2 -2
  107. package/skills/agentflow-flow-dsl/references/node-calls.md +3 -0
  108. package/skills/agentflow-flow-dsl/references/subflow-authoring.md +230 -0
  109. package/skills/agentflow-flow-edit-node-fields/SKILL.md +3 -3
  110. package/skills/agentflow-flow-recipes/SKILL.md +1 -1
  111. package/skills/agentflow-flow-recipes/references/recipes.md +1 -2
  112. package/skills/agentflow-flow-sync-ui/SKILL.md +1 -1
  113. package/skills/agentflow-node-dsl/SKILL.md +44 -13
  114. package/skills/agentflow-node-reference/references/builtin-nodes.md +25 -0
  115. package/skills/agentflow-workspace-markdown/SKILL.md +1 -1
  116. package/builtin/web-ui/dist/assets/index-B9ppXv7e.css +0 -1
  117. package/builtin/web-ui/dist/assets/index-CdEQWRrp.js +0 -872
  118. package/skills/agentflow-workspace-graph/SKILL.md +0 -85
@@ -52,6 +52,7 @@ import { packageResolverFor, scanAvailableNodePackages } from "./flow-dsl/packag
52
52
  import { runStartupStorageMigrations } from "./startup-storage-migrations.mjs";
53
53
  import { getPipelineFiles } from "./workspace-tree.mjs";
54
54
  import { listExpiredWorkspacePreviews } from "./workspace-preview.mjs";
55
+ import { listExpiredWorkspaceDrafts } from "./workspace-draft.mjs";
55
56
  import { LEGACY_FLOW_EXECUTION_DISABLED, LEGACY_FLOW_EXECUTION_MESSAGE } from "./legacy-flow-execution.mjs";
56
57
  import {
57
58
  listRecentComposerSessions,
@@ -71,7 +72,11 @@ import {
71
72
  authSetupRequired,
72
73
  buildClearSessionCookie,
73
74
  buildSessionCookie,
75
+ createCliAuthorization,
76
+ decideCliAuthorization,
77
+ exchangeCliAuthorization,
74
78
  getAuthUserFromRequest,
79
+ getCliAuthorization,
75
80
  getSessionTokenFromRequest,
76
81
  isAuthUserAllowed,
77
82
  listAuthUsers,
@@ -80,8 +85,13 @@ import {
80
85
  readAuthUsers,
81
86
  readUserAllowlist,
82
87
  resetAuthUserPassword,
88
+ revokeSessionToken,
83
89
  writeUserAllowlist,
84
90
  } from "./auth.mjs";
91
+ import {
92
+ renderCliAuthorizationPage,
93
+ renderCliAuthorizationResult,
94
+ } from "./cli-auth-page.mjs";
85
95
  import { readGlobalEnvRows, readUserEnvRows, writeGlobalEnvRows, writeUserEnvRows } from "./user-env.mjs";
86
96
  import {
87
97
  readAdminBuiltinPipelineConfig,
@@ -167,7 +177,7 @@ import {
167
177
  writeDisplayShares,
168
178
  writeWorkspaceGraph,
169
179
  } from "./workspace-server.mjs";
170
- import { handleWorkspaceRoutes } from "./workspace-routes.mjs";
180
+ import { handleWorkspaceRoutes, workspaceGraphWithScheduleMode } from "./workspace-routes.mjs";
171
181
 
172
182
  const MIME = {
173
183
  ".html": "text/html; charset=utf-8",
@@ -206,10 +216,8 @@ const BUILTIN_SKILL_COLLECTIONS = [
206
216
  id: "pipeline",
207
217
  name: "Pipeline",
208
218
  defaultKeys: [
209
- "agentflow-flow-add-instances",
210
- "agentflow-flow-edit-node-fields",
219
+ "agentflow-flow-dsl",
211
220
  "agentflow-flow-recipes",
212
- "agentflow-flow-sync-ui",
213
221
  "agentflow-node-reference",
214
222
  "agentflow-placeholder-reference",
215
223
  "agentflow-runtime-reference",
@@ -219,7 +227,7 @@ const BUILTIN_SKILL_COLLECTIONS = [
219
227
  id: "workspace",
220
228
  name: "Workspace",
221
229
  defaultKeys: [
222
- "agentflow-workspace-graph",
230
+ "agentflow-flow-dsl",
223
231
  "agentflow-workspace-markdown",
224
232
  "agentflow-workspace-mermaid",
225
233
  "agentflow-workspace-ascii",
@@ -233,7 +241,18 @@ const BUILTIN_SKILL_COLLECTIONS = [
233
241
  ],
234
242
  legacyDefaultKeys: [
235
243
  [
236
- "agentflow-workspace-graph",
244
+ "agentflow-workspace-markdown",
245
+ "agentflow-workspace-mermaid",
246
+ "agentflow-workspace-ascii",
247
+ "agentflow-workspace-chart",
248
+ "agentflow-workspace-table",
249
+ "agentflow-workspace-html",
250
+ "agentflow-workspace-image",
251
+ "agentflow-node-reference",
252
+ "agentflow-placeholder-reference",
253
+ "agentflow-runtime-reference",
254
+ ],
255
+ [
237
256
  "agentflow-workspace-markdown",
238
257
  "agentflow-workspace-mermaid",
239
258
  "agentflow-workspace-ascii",
@@ -244,7 +263,6 @@ const BUILTIN_SKILL_COLLECTIONS = [
244
263
  "agentflow-runtime-reference",
245
264
  ],
246
265
  [
247
- "agentflow-workspace-graph",
248
266
  "agentflow-workspace-markdown",
249
267
  "agentflow-workspace-mermaid",
250
268
  "agentflow-workspace-ascii",
@@ -1450,6 +1468,25 @@ function serverPublicBaseUrl(req, host, port, payload = null) {
1450
1468
  return configuredPublicBaseUrl(payload) || requestPublicBaseUrl(req) || normalizePublicBaseUrl(`http://${host}:${port}`);
1451
1469
  }
1452
1470
 
1471
+ function html(res, status, content, headers = {}) {
1472
+ const body = String(content || "");
1473
+ res.writeHead(status, {
1474
+ "Content-Type": "text/html; charset=utf-8",
1475
+ "Content-Length": Buffer.byteLength(body),
1476
+ "Cache-Control": "no-store, max-age=0",
1477
+ "Content-Security-Policy": "default-src 'none'; style-src 'unsafe-inline'; form-action 'self'; base-uri 'none'; frame-ancestors 'none'",
1478
+ "X-Content-Type-Options": "nosniff",
1479
+ "Referrer-Policy": "no-referrer",
1480
+ ...headers,
1481
+ });
1482
+ res.end(body);
1483
+ }
1484
+
1485
+ async function readUrlEncodedBody(req) {
1486
+ const body = await readBody(req);
1487
+ return new URLSearchParams(String(body || ""));
1488
+ }
1489
+
1453
1490
  function publicDisplayPayloadFromShare(root, share) {
1454
1491
  const scoped = resolveWorkspaceScopeRoot(root, {
1455
1492
  flowId: share.flowId || "",
@@ -1684,7 +1721,7 @@ function bufferLooksLikeZip(buf) {
1684
1721
 
1685
1722
  /**
1686
1723
  * @param {import('http').IncomingMessage} req
1687
- * @returns {Promise<{ targetSpace: string, flowIdField: string, file: Buffer, filename: string, gotFile: boolean }>}
1724
+ * @returns {Promise<{ targetSpace: string, flowIdField: string, scheduleMode: string, file: Buffer, filename: string, gotFile: boolean }>}
1688
1725
  */
1689
1726
  function parseFlowsImportForm(req) {
1690
1727
  return new Promise((resolve, reject) => {
@@ -1694,6 +1731,7 @@ function parseFlowsImportForm(req) {
1694
1731
  });
1695
1732
  let targetSpace = "user";
1696
1733
  let flowIdField = "";
1734
+ let scheduleMode = "disabled";
1697
1735
  /** @type {Buffer[]} */
1698
1736
  const chunks = [];
1699
1737
  let filename = "";
@@ -1706,6 +1744,9 @@ function parseFlowsImportForm(req) {
1706
1744
  if (name === "flowId" && typeof val === "string") {
1707
1745
  flowIdField = val;
1708
1746
  }
1747
+ if (name === "scheduleMode" && typeof val === "string") {
1748
+ scheduleMode = val.trim().toLowerCase();
1749
+ }
1709
1750
  });
1710
1751
 
1711
1752
  bb.on("file", (name, file, info) => {
@@ -1725,6 +1766,7 @@ function parseFlowsImportForm(req) {
1725
1766
  resolve({
1726
1767
  targetSpace,
1727
1768
  flowIdField: flowIdField.trim(),
1769
+ scheduleMode,
1728
1770
  file: Buffer.concat(chunks),
1729
1771
  filename,
1730
1772
  gotFile,
@@ -1748,12 +1790,16 @@ function cleanupExpiredWorkspacePreviews(workspaceRoot = "") {
1748
1790
  }
1749
1791
  let removed = 0;
1750
1792
  for (const pipelinesRoot of roots) {
1751
- for (const item of listExpiredWorkspacePreviews(pipelinesRoot)) {
1793
+ const expired = [
1794
+ ...listExpiredWorkspacePreviews(pipelinesRoot),
1795
+ ...listExpiredWorkspaceDrafts(pipelinesRoot),
1796
+ ];
1797
+ for (const item of expired) {
1752
1798
  try {
1753
1799
  fs.rmSync(item.flowDir, { recursive: true, force: true });
1754
1800
  removed += 1;
1755
1801
  } catch (e) {
1756
- log.debug(`[workspace-preview] cleanup failed: ${(e && e.message) || String(e)}`);
1802
+ log.debug(`[workspace-temporary] cleanup failed: ${(e && e.message) || String(e)}`);
1757
1803
  }
1758
1804
  }
1759
1805
  }
@@ -2013,12 +2059,12 @@ function buildComposerPromptWithFlowContext(p) {
2013
2059
  `- flowSource:${p.flowSource}`,
2014
2060
  ...builtinExtra,
2015
2061
  `- 当前关联的节点实例 ID(顺序:画布选中优先,再输入框 @提及):${idsLine}`,
2016
- "- 像普通 agent 请求一样处理用户说明:可能只是问问题,也可能要求编辑文件。不要因为存在 flowId 就默认修改 flow.yaml。",
2062
+ "- 像普通 agent 请求一样处理用户说明:可能只是问问题,也可能要求编辑文件。不要因为存在 flowId 就默认修改流程。",
2017
2063
  "- 按需使用当前环境可用的 skills;如果用户点名某个 skill,遵循该 skill 的 SKILL.md。",
2018
2064
  "- 如果你判断需要编辑 AgentFlow 流程,可按需读取这些本地 skills:",
2019
- " - `skills/agentflow-flow-add-instances/SKILL.md`:新增实例、边和布局",
2020
- " - `skills/agentflow-flow-edit-node-fields/SKILL.md`:只改已有节点字段",
2021
- " - `skills/agentflow-flow-sync-ui/SKILL.md`:保存 flow.yaml 后刷新画布",
2065
+ " - `skills/agentflow-flow-dsl/SKILL.md`:新建或修改 workspace.flow.js、节点、字段、连线、子流程与受控循环",
2066
+ " - `skills/agentflow-node-dsl/SKILL.md`:仅在需要确定性自定义代码节点时使用",
2067
+ " - `skills/agentflow-author-flow/SKILL.md`:需要 Draft、试运行、动态修改、发布或定时时使用完整生命周期",
2022
2068
  "- 如果只是回答问题,不要修改文件。",
2023
2069
  "",
2024
2070
  ...(p.selectedSkillBlock ? [p.selectedSkillBlock, ""] : []),
@@ -2091,6 +2137,132 @@ export function startUiServer({
2091
2137
  return;
2092
2138
  }
2093
2139
 
2140
+ if (url.pathname === "/api/auth/cli/device" && req.method === "POST") {
2141
+ let payload = {};
2142
+ try {
2143
+ const raw = await readBody(req);
2144
+ payload = raw ? JSON.parse(raw) : {};
2145
+ } catch {
2146
+ json(res, 400, { error: "Invalid JSON body" });
2147
+ return;
2148
+ }
2149
+ const authorization = createCliAuthorization({
2150
+ publicBaseUrl: serverPublicBaseUrl(req, host, uiPort),
2151
+ clientName: payload?.clientName,
2152
+ });
2153
+ json(res, 201, {
2154
+ requestId: authorization.requestId,
2155
+ deviceCode: authorization.deviceCode,
2156
+ userCode: authorization.userCode,
2157
+ verificationUrl: authorization.verificationUrl,
2158
+ expiresAt: authorization.expiresAt,
2159
+ pollInterval: authorization.pollInterval,
2160
+ });
2161
+ return;
2162
+ }
2163
+
2164
+ if (url.pathname === "/api/auth/cli/token" && req.method === "POST") {
2165
+ let payload = {};
2166
+ try {
2167
+ const raw = await readBody(req);
2168
+ payload = raw ? JSON.parse(raw) : {};
2169
+ } catch {
2170
+ json(res, 400, { error: "Invalid JSON body" });
2171
+ return;
2172
+ }
2173
+ const result = exchangeCliAuthorization(payload?.deviceCode);
2174
+ if (!result.ok) {
2175
+ json(res, result.status || 400, {
2176
+ error: result.error,
2177
+ code: result.code || "invalid_request",
2178
+ authorization: result.authorization || undefined,
2179
+ });
2180
+ return;
2181
+ }
2182
+ json(res, 200, result);
2183
+ return;
2184
+ }
2185
+
2186
+ if (url.pathname === "/api/auth/cli/revoke" && req.method === "POST") {
2187
+ const token = getSessionTokenFromRequest(req);
2188
+ if (!token || !getAuthUserFromRequest(req)) {
2189
+ json(res, 401, { error: "Unauthorized" });
2190
+ return;
2191
+ }
2192
+ revokeSessionToken(token);
2193
+ json(res, 200, { ok: true });
2194
+ return;
2195
+ }
2196
+
2197
+ if (url.pathname === "/cli/authorize" && req.method === "GET") {
2198
+ const requestId = String(url.searchParams.get("request") || "").trim();
2199
+ const user = getAuthUserFromRequest(req);
2200
+ const result = getCliAuthorization(requestId, { includeApprovalNonce: Boolean(user) });
2201
+ if (result.ok && result.authorization.status !== "pending") {
2202
+ html(res, 200, renderCliAuthorizationResult({
2203
+ approved: result.authorization.status === "approved",
2204
+ clientName: result.authorization.clientName,
2205
+ }));
2206
+ return;
2207
+ }
2208
+ html(res, result.ok ? 200 : result.status || 404, renderCliAuthorizationPage({
2209
+ authorization: result.authorization || null,
2210
+ approvalNonce: result.approvalNonce || "",
2211
+ user,
2212
+ error: result.ok ? "" : result.error,
2213
+ }));
2214
+ return;
2215
+ }
2216
+
2217
+ if (url.pathname === "/cli/authorize/login" && req.method === "POST") {
2218
+ const form = await readUrlEncodedBody(req);
2219
+ const requestId = String(form.get("request") || "").trim();
2220
+ const pending = getCliAuthorization(requestId);
2221
+ if (!pending.ok) {
2222
+ html(res, pending.status || 404, renderCliAuthorizationPage({ error: pending.error }));
2223
+ return;
2224
+ }
2225
+ const result = loginOrCreateUser(form.get("username"), form.get("password"));
2226
+ if (!result.ok) {
2227
+ html(res, result.forbidden ? 403 : 401, renderCliAuthorizationPage({
2228
+ authorization: pending.authorization,
2229
+ error: result.error || "Login failed",
2230
+ }));
2231
+ return;
2232
+ }
2233
+ res.writeHead(303, {
2234
+ Location: `/cli/authorize?request=${encodeURIComponent(requestId)}`,
2235
+ "Set-Cookie": buildSessionCookie(result.token),
2236
+ "Cache-Control": "no-store, max-age=0",
2237
+ });
2238
+ res.end();
2239
+ return;
2240
+ }
2241
+
2242
+ if (url.pathname === "/cli/authorize/decision" && req.method === "POST") {
2243
+ const user = getAuthUserFromRequest(req);
2244
+ if (!user) {
2245
+ html(res, 401, renderCliAuthorizationResult({ error: "登录状态已失效,请重新打开授权链接。" }));
2246
+ return;
2247
+ }
2248
+ const form = await readUrlEncodedBody(req);
2249
+ const requestId = String(form.get("request") || "").trim();
2250
+ const current = getCliAuthorization(requestId);
2251
+ const approved = form.get("decision") === "approve";
2252
+ const result = decideCliAuthorization({
2253
+ requestId,
2254
+ approvalNonce: form.get("approvalNonce"),
2255
+ userId: user.userId,
2256
+ approved,
2257
+ });
2258
+ html(res, result.ok ? 200 : result.status || 400, renderCliAuthorizationResult({
2259
+ approved,
2260
+ clientName: current.authorization?.clientName,
2261
+ error: result.ok ? "" : result.error,
2262
+ }));
2263
+ return;
2264
+ }
2265
+
2094
2266
  if (url.pathname === "/api/app-version" && req.method === "GET") {
2095
2267
  res.setHeader("Cache-Control", "no-store, max-age=0");
2096
2268
  json(res, 200, {
@@ -2706,6 +2878,10 @@ export function startUiServer({
2706
2878
  }
2707
2879
  const flowId = idCheck.flowId;
2708
2880
  const targetSpace = parsed.targetSpace === "workspace" ? "workspace" : "user";
2881
+ if (!["enabled", "disabled", "preserve"].includes(parsed.scheduleMode)) {
2882
+ json(res, 400, { error: "scheduleMode must be enabled, disabled, or preserve" });
2883
+ return;
2884
+ }
2709
2885
  const existing = listFlowsJson(root, {
2710
2886
  ...userCtx,
2711
2887
  includeWorkspaceFlows: targetSpace === "workspace",
@@ -2747,10 +2923,37 @@ export function startUiServer({
2747
2923
  json(res, 400, { error: w.error });
2748
2924
  return;
2749
2925
  }
2750
- if (targetSpace === "workspace") {
2751
- ensureWorkspaceCollaboration({ flowId, userId: userCtx.userId });
2926
+ const targetDir = targetSpace === "workspace"
2927
+ ? path.join(path.resolve(root), PIPELINES_DIR, flowId)
2928
+ : path.join(getUserPipelinesRoot(userCtx.userId), flowId);
2929
+ let collaborationCreated = false;
2930
+ try {
2931
+ if (targetSpace === "workspace") {
2932
+ ensureWorkspaceCollaboration({ flowId, userId: userCtx.userId });
2933
+ collaborationCreated = true;
2934
+ }
2935
+ const scoped = resolveWorkspaceScopeRoot(root, { flowId, flowSource: targetSpace }, userCtx);
2936
+ if (scoped.error) throw new Error(scoped.error);
2937
+ const importedGraph = readWorkspaceGraph(scoped.root, root).graph;
2938
+ const graph = workspaceGraphWithScheduleMode(importedGraph, parsed.scheduleMode);
2939
+ if (graph !== importedGraph) writeWorkspaceGraph(scoped.root, graph, root);
2940
+ const persisted = readWorkspaceGraph(scoped.root, root).graph;
2941
+ const workspaceSchedules = syncWorkspaceSchedulesForGraph(root, scoped, persisted, authUser, userCtx);
2942
+ json(res, 200, {
2943
+ success: true,
2944
+ flowId,
2945
+ flowSource: targetSpace,
2946
+ scheduleMode: parsed.scheduleMode,
2947
+ workspaceSchedules,
2948
+ });
2949
+ } catch (e) {
2950
+ try { fs.rmSync(targetDir, { recursive: true, force: true }); } catch (_) {}
2951
+ if (collaborationCreated) deleteWorkspaceCollaborationForFlow(flowId, false);
2952
+ json(res, 500, {
2953
+ error: `Flow publication rolled back: ${(e && e.message) || String(e)}`,
2954
+ rolledBack: true,
2955
+ });
2752
2956
  }
2753
- json(res, 200, { success: true, flowId, flowSource: targetSpace });
2754
2957
  return;
2755
2958
  }
2756
2959
 
@@ -0,0 +1,74 @@
1
+ import crypto from "crypto";
2
+ import fs from "fs";
3
+ import path from "path";
4
+
5
+ import { getUserPipelinesRoot } from "./paths.mjs";
6
+
7
+ export const WORKSPACE_DRAFT_METADATA_FILENAME = ".agentflow-workspace-draft.json";
8
+ export const DEFAULT_WORKSPACE_DRAFT_TTL_MS = 2 * 60 * 60 * 1000;
9
+ export const MAX_WORKSPACE_DRAFT_TTL_MS = 24 * 60 * 60 * 1000;
10
+
11
+ export function safeWorkspaceDraftId(value = "") {
12
+ const raw = String(value || "").trim();
13
+ return /^draft_[a-z0-9_-]{8,100}$/i.test(raw) ? raw : "";
14
+ }
15
+
16
+ export function createWorkspaceDraftId() {
17
+ return `draft_${crypto.randomBytes(10).toString("hex")}`;
18
+ }
19
+
20
+ export function workspaceDraftFlowDir(draftId, userId = "") {
21
+ const safeId = safeWorkspaceDraftId(draftId);
22
+ if (!safeId) return "";
23
+ return path.join(getUserPipelinesRoot(userId), safeId);
24
+ }
25
+
26
+ export function workspaceDraftMetadataPath(flowDir) {
27
+ return path.join(flowDir, WORKSPACE_DRAFT_METADATA_FILENAME);
28
+ }
29
+
30
+ export function readWorkspaceDraftMetadata(flowDir) {
31
+ try {
32
+ const filePath = workspaceDraftMetadataPath(flowDir);
33
+ if (!fs.existsSync(filePath)) return null;
34
+ const value = JSON.parse(fs.readFileSync(filePath, "utf8"));
35
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
36
+ const flowId = safeWorkspaceDraftId(value.flowId);
37
+ const ownerId = String(value.ownerId || "").trim();
38
+ const expiresAt = String(value.expiresAt || "").trim();
39
+ if (!flowId || !ownerId || !Number.isFinite(Date.parse(expiresAt))) return null;
40
+ return { ...value, flowId, ownerId, expiresAt };
41
+ } catch {
42
+ return null;
43
+ }
44
+ }
45
+
46
+ export function isWorkspaceDraftDir(flowDir) {
47
+ return Boolean(readWorkspaceDraftMetadata(flowDir));
48
+ }
49
+
50
+ export function normalizeWorkspaceDraftTtlMs(value) {
51
+ const requested = Number(value);
52
+ if (!Number.isFinite(requested) || requested <= 0) return DEFAULT_WORKSPACE_DRAFT_TTL_MS;
53
+ return Math.min(MAX_WORKSPACE_DRAFT_TTL_MS, Math.max(60_000, Math.round(requested)));
54
+ }
55
+
56
+ export function writeWorkspaceDraftMetadata(flowDir, metadata) {
57
+ const filePath = workspaceDraftMetadataPath(flowDir);
58
+ const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
59
+ fs.writeFileSync(tempPath, `${JSON.stringify(metadata, null, 2)}\n`, "utf8");
60
+ fs.renameSync(tempPath, filePath);
61
+ }
62
+
63
+ export function listExpiredWorkspaceDrafts(userPipelinesRoot, now = Date.now()) {
64
+ if (!fs.existsSync(userPipelinesRoot)) return [];
65
+ const expired = [];
66
+ for (const entry of fs.readdirSync(userPipelinesRoot, { withFileTypes: true })) {
67
+ if (!entry.isDirectory()) continue;
68
+ const flowDir = path.join(userPipelinesRoot, entry.name);
69
+ const metadata = readWorkspaceDraftMetadata(flowDir);
70
+ if (!metadata) continue;
71
+ if (Date.parse(metadata.expiresAt) <= now) expired.push({ flowDir, metadata });
72
+ }
73
+ return expired;
74
+ }
@@ -108,7 +108,7 @@ function readTextOrNull(file) {
108
108
  }
109
109
 
110
110
  export function emptyDesignGraph() {
111
- return { version: 1, instances: {}, edges: [], ui: { nodePositions: {} } };
111
+ return { version: 1, instances: {}, edges: [], subflows: {}, ui: { nodePositions: {} } };
112
112
  }
113
113
 
114
114
  function normalizeDesignShape(raw) {
@@ -120,6 +120,10 @@ function normalizeDesignShape(raw) {
120
120
  ? graph.instances
121
121
  : {},
122
122
  edges: Array.isArray(graph.edges) ? graph.edges : [],
123
+ subflows:
124
+ graph.subflows && typeof graph.subflows === "object" && !Array.isArray(graph.subflows)
125
+ ? graph.subflows
126
+ : {},
123
127
  ui: graph.ui && typeof graph.ui === "object" ? graph.ui : { nodePositions: {} },
124
128
  };
125
129
  }
@@ -226,7 +230,12 @@ export function designFingerprint(graph) {
226
230
  .filter((e) => e && design.instances[e.source] && design.instances[e.target])
227
231
  .map((e) => `${e.source}|${asText(e.sourceHandle)}|${e.target}|${asText(e.targetHandle)}`)
228
232
  .sort();
229
- return stableStringify({ instances, edges: [...new Set(edges)], ui: normalizeUi(design.ui) });
233
+ return stableStringify({
234
+ instances,
235
+ edges: [...new Set(edges)],
236
+ subflows: design.subflows,
237
+ ui: normalizeUi(design.ui),
238
+ });
230
239
  }
231
240
 
232
241
  // ── 读 ──────────────────────────────────────────────────────────────────────
@@ -34,6 +34,9 @@ function normalizedGraph(graph) {
34
34
  version: Number(source.version) || 1,
35
35
  instances: isPlainObject(source.instances) ? clone(source.instances) : {},
36
36
  edges: Array.isArray(source.edges) ? clone(source.edges) : [],
37
+ // Subflows are first-class design metadata. Dropping them here makes every
38
+ // revision-aware canvas save silently erase call contracts and entry roots.
39
+ subflows: isPlainObject(source.subflows) ? clone(source.subflows) : {},
37
40
  ui: isPlainObject(source.ui) ? clone(source.ui) : { nodePositions: {} },
38
41
  };
39
42
  }