@fieldwangai/agentflow 0.1.160 → 0.1.163

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 (111) hide show
  1. package/bin/lib/auth.mjs +231 -14
  2. package/bin/lib/catalog-flows.mjs +2 -0
  3. package/bin/lib/cli-auth-page.mjs +103 -0
  4. package/bin/lib/flow-dsl/codegen.mjs +23 -2
  5. package/bin/lib/flow-dsl/lint.mjs +44 -0
  6. package/bin/lib/flow-dsl/parser.mjs +68 -1
  7. package/bin/lib/marketplace-usage.mjs +218 -0
  8. package/bin/lib/marketplace.mjs +183 -3
  9. package/bin/lib/node-package-manifest.mjs +1 -1
  10. package/bin/lib/skill-runtime.mjs +6 -0
  11. package/bin/lib/spaces.mjs +200 -0
  12. package/bin/lib/table.mjs +2 -3
  13. package/bin/lib/ui-server.mjs +471 -84
  14. package/bin/lib/workspace-draft.mjs +74 -0
  15. package/bin/lib/workspace-routes.mjs +484 -4
  16. package/bin/lib/workspace-server.mjs +373 -30
  17. package/bin/lib/workspace-state.mjs +2 -1
  18. package/builtin/nodes/agent_subAgent.md +5 -1
  19. package/builtin/nodes/context_bundle.md +44 -0
  20. package/builtin/nodes/context_knowledge.md +29 -0
  21. package/builtin/nodes/context_skills.md +29 -0
  22. package/builtin/nodes/context_workspace.md +36 -0
  23. package/builtin/nodes/control_while.md +12 -5
  24. package/builtin/nodes/tool_git_worktree_load.md +4 -3
  25. package/builtin/pipelines/new/workspace.flow.js +1 -1
  26. package/builtin/web-ui/dist/assets/{WorkflowAssistantThread-Bfo9Ythw.js → WorkflowAssistantThread-DmhIgoD7.js} +1 -1
  27. package/builtin/web-ui/dist/assets/index-CI9J6Unt.js +873 -0
  28. package/builtin/web-ui/dist/assets/index-DY5vE7v1.css +1 -0
  29. package/builtin/web-ui/dist/index.html +2 -2
  30. package/package.json +5 -1
  31. package/shared/slot-types.js +1 -0
  32. package/skills/agentflow-author-flow/SKILL.md +81 -8
  33. package/skills/agentflow-author-flow/agents/openai.yaml +3 -3
  34. package/skills/agentflow-cli/SKILL.md +179 -27
  35. package/skills/agentflow-cli/agents/openai.yaml +2 -2
  36. package/skills/agentflow-cli/runtime/bin/lib/skill-runtime.mjs +14879 -0
  37. package/skills/agentflow-cli/runtime/builtin/nodes/agent_subAgent.md +38 -0
  38. package/skills/agentflow-cli/runtime/builtin/nodes/context_bundle.md +44 -0
  39. package/skills/agentflow-cli/runtime/builtin/nodes/context_knowledge.md +29 -0
  40. package/skills/agentflow-cli/runtime/builtin/nodes/context_skills.md +29 -0
  41. package/skills/agentflow-cli/runtime/builtin/nodes/context_workspace.md +36 -0
  42. package/skills/agentflow-cli/runtime/builtin/nodes/control_agent_toBool.md +26 -0
  43. package/skills/agentflow-cli/runtime/builtin/nodes/control_anyOne.md +19 -0
  44. package/skills/agentflow-cli/runtime/builtin/nodes/control_cancelled.md +26 -0
  45. package/skills/agentflow-cli/runtime/builtin/nodes/control_cd_workspace.md +50 -0
  46. package/skills/agentflow-cli/runtime/builtin/nodes/control_delay.md +31 -0
  47. package/skills/agentflow-cli/runtime/builtin/nodes/control_end.md +13 -0
  48. package/skills/agentflow-cli/runtime/builtin/nodes/control_if.md +21 -0
  49. package/skills/agentflow-cli/runtime/builtin/nodes/control_interval_loop.md +55 -0
  50. package/skills/agentflow-cli/runtime/builtin/nodes/control_load_mcp.md +24 -0
  51. package/skills/agentflow-cli/runtime/builtin/nodes/control_load_skills.md +35 -0
  52. package/skills/agentflow-cli/runtime/builtin/nodes/control_parse_json.md +25 -0
  53. package/skills/agentflow-cli/runtime/builtin/nodes/control_start.md +13 -0
  54. package/skills/agentflow-cli/runtime/builtin/nodes/control_subflow_call.md +27 -0
  55. package/skills/agentflow-cli/runtime/builtin/nodes/control_toBool.md +27 -0
  56. package/skills/agentflow-cli/runtime/builtin/nodes/control_user_workspace.md +24 -0
  57. package/skills/agentflow-cli/runtime/builtin/nodes/control_wait_until.md +34 -0
  58. package/skills/agentflow-cli/runtime/builtin/nodes/control_while.md +131 -0
  59. package/skills/agentflow-cli/runtime/builtin/nodes/display_ascii.md +24 -0
  60. package/skills/agentflow-cli/runtime/builtin/nodes/display_chart.md +32 -0
  61. package/skills/agentflow-cli/runtime/builtin/nodes/display_code.md +39 -0
  62. package/skills/agentflow-cli/runtime/builtin/nodes/display_html.md +32 -0
  63. package/skills/agentflow-cli/runtime/builtin/nodes/display_image.md +36 -0
  64. package/skills/agentflow-cli/runtime/builtin/nodes/display_markdown.md +24 -0
  65. package/skills/agentflow-cli/runtime/builtin/nodes/display_mermaid.md +24 -0
  66. package/skills/agentflow-cli/runtime/builtin/nodes/display_react_app.md +53 -0
  67. package/skills/agentflow-cli/runtime/builtin/nodes/display_table.md +32 -0
  68. package/skills/agentflow-cli/runtime/builtin/nodes/provide_bool.md +14 -0
  69. package/skills/agentflow-cli/runtime/builtin/nodes/provide_file.md +14 -0
  70. package/skills/agentflow-cli/runtime/builtin/nodes/provide_json.md +14 -0
  71. package/skills/agentflow-cli/runtime/builtin/nodes/provide_password.md +14 -0
  72. package/skills/agentflow-cli/runtime/builtin/nodes/provide_str.md +14 -0
  73. package/skills/agentflow-cli/runtime/builtin/nodes/tool_display_share_link.md +47 -0
  74. package/skills/agentflow-cli/runtime/builtin/nodes/tool_get_env.md +20 -0
  75. package/skills/agentflow-cli/runtime/builtin/nodes/tool_git_checkout.md +70 -0
  76. package/skills/agentflow-cli/runtime/builtin/nodes/tool_git_worktree_load.md +74 -0
  77. package/skills/agentflow-cli/runtime/builtin/nodes/tool_git_worktree_unload.md +52 -0
  78. package/skills/agentflow-cli/runtime/builtin/nodes/tool_gitlab_create_mr.md +114 -0
  79. package/skills/agentflow-cli/runtime/builtin/nodes/tool_jenkins_build.md +67 -0
  80. package/skills/agentflow-cli/runtime/builtin/nodes/tool_load_key.md +26 -0
  81. package/skills/agentflow-cli/runtime/builtin/nodes/tool_nodejs.md +52 -0
  82. package/skills/agentflow-cli/runtime/builtin/nodes/tool_print.md +21 -0
  83. package/skills/agentflow-cli/runtime/builtin/nodes/tool_save_key.md +26 -0
  84. package/skills/agentflow-cli/runtime/builtin/nodes/tool_set_run_env.md +38 -0
  85. package/skills/agentflow-cli/runtime/builtin/nodes/tool_user_ask.md +27 -0
  86. package/skills/agentflow-cli/runtime/builtin/nodes/tool_user_check.md +26 -0
  87. package/skills/agentflow-cli/runtime/builtin/nodes/tool_wecom_send_app_markdown.md +57 -0
  88. package/skills/agentflow-cli/runtime/builtin/nodes/tool_wecom_send_group_markdown.md +45 -0
  89. package/skills/agentflow-cli/runtime/builtin/nodes/workspace_one_click_task.md +44 -0
  90. package/skills/agentflow-cli/runtime/builtin/nodes/workspace_run.md +16 -0
  91. package/skills/agentflow-cli/runtime/builtin/nodes/workspace_scheduled_run.md +16 -0
  92. package/skills/agentflow-cli/runtime/builtin/nodes/workspace_subflow_input.md +14 -0
  93. package/skills/agentflow-cli/runtime/package.json +6 -0
  94. package/skills/agentflow-cli/scripts/agentflow-auth-store.mjs +113 -0
  95. package/skills/agentflow-cli/scripts/agentflow-cli.mjs +457 -28
  96. package/skills/agentflow-cli/scripts/agentflow-runtime.mjs +11 -4
  97. package/skills/agentflow-flow-add-instances/SKILL.md +2 -2
  98. package/skills/agentflow-flow-dsl/SKILL.md +73 -8
  99. package/skills/agentflow-flow-dsl/agents/openai.yaml +2 -2
  100. package/skills/agentflow-flow-dsl/references/node-calls.md +6 -2
  101. package/skills/agentflow-flow-dsl/references/subflow-authoring.md +34 -7
  102. package/skills/agentflow-flow-edit-node-fields/SKILL.md +3 -3
  103. package/skills/agentflow-flow-recipes/SKILL.md +1 -1
  104. package/skills/agentflow-flow-recipes/references/recipes.md +1 -2
  105. package/skills/agentflow-flow-sync-ui/SKILL.md +1 -1
  106. package/skills/agentflow-node-dsl/SKILL.md +5 -13
  107. package/skills/agentflow-node-reference/references/builtin-nodes.md +37 -5
  108. package/skills/agentflow-workspace-markdown/SKILL.md +1 -1
  109. package/builtin/web-ui/dist/assets/index-XbeI5foV.js +0 -872
  110. package/builtin/web-ui/dist/assets/index-e1omCEau.css +0 -1
  111. 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,
@@ -120,7 +130,6 @@ import { json, readBody } from "./http-util.mjs";
120
130
 
121
131
  // 从 ui-server 拆出去的 Workspace 子系统;路由仍在下面的 startUiServer 里
122
132
  import {
123
- USER_WORKSPACES_FILENAME,
124
133
  WORKSPACE_DEFERRED_RUN_POLL_MS,
125
134
  WORKSPACE_SCHEDULE_POLL_MS,
126
135
  activeWorkspaceRunUsageRecords,
@@ -131,6 +140,7 @@ import {
131
140
  displayShareOutputUrl,
132
141
  isReadonlyBuiltinFlowSource,
133
142
  isValidFlowSourceRead,
143
+ listConfiguredWorkspaces,
134
144
  listWorkspaceScheduleStatuses,
135
145
  normalizeDisplayShareExpiry,
136
146
  normalizeDisplayShareNodeIds,
@@ -141,11 +151,11 @@ import {
141
151
  readCursorMcpConfig,
142
152
  readCursorMcpServers,
143
153
  readDisplayShares,
154
+ readUserWorkspaces,
144
155
  readUserMcpPrivate,
145
156
  readWorkspaceGraph,
146
157
  readWorkspaceRunUsageRecords,
147
158
  readWorkspaceScheduleRegistry,
148
- readWorkspacesFromPath,
149
159
  resolveWorkspaceFilePath,
150
160
  resolveWorkspaceScopeRoot,
151
161
  runStatusBucket,
@@ -163,11 +173,22 @@ import {
163
173
  workspaceDownloadContentDisposition,
164
174
  workspaceFlowCollaborationGuard,
165
175
  workspaceScheduleNextRunAt,
166
- workspacesPath,
167
176
  writeDisplayShares,
168
177
  writeWorkspaceGraph,
169
178
  } from "./workspace-server.mjs";
170
- import { handleWorkspaceRoutes } from "./workspace-routes.mjs";
179
+ import { handleWorkspaceRoutes, workspaceGraphWithScheduleMode } from "./workspace-routes.mjs";
180
+ import {
181
+ createSpace,
182
+ deleteSpacePage,
183
+ getSpaceById,
184
+ getSpaceByRoute,
185
+ listSpacesForUser,
186
+ readSpaces,
187
+ normalizeSpacePagePath,
188
+ normalizeSpaceVisibility,
189
+ updateSpace,
190
+ upsertSpacePage,
191
+ } from "./spaces.mjs";
171
192
 
172
193
  const MIME = {
173
194
  ".html": "text/html; charset=utf-8",
@@ -206,10 +227,8 @@ const BUILTIN_SKILL_COLLECTIONS = [
206
227
  id: "pipeline",
207
228
  name: "Pipeline",
208
229
  defaultKeys: [
209
- "agentflow-flow-add-instances",
210
- "agentflow-flow-edit-node-fields",
230
+ "agentflow-flow-dsl",
211
231
  "agentflow-flow-recipes",
212
- "agentflow-flow-sync-ui",
213
232
  "agentflow-node-reference",
214
233
  "agentflow-placeholder-reference",
215
234
  "agentflow-runtime-reference",
@@ -219,7 +238,7 @@ const BUILTIN_SKILL_COLLECTIONS = [
219
238
  id: "workspace",
220
239
  name: "Workspace",
221
240
  defaultKeys: [
222
- "agentflow-workspace-graph",
241
+ "agentflow-flow-dsl",
223
242
  "agentflow-workspace-markdown",
224
243
  "agentflow-workspace-mermaid",
225
244
  "agentflow-workspace-ascii",
@@ -233,7 +252,18 @@ const BUILTIN_SKILL_COLLECTIONS = [
233
252
  ],
234
253
  legacyDefaultKeys: [
235
254
  [
236
- "agentflow-workspace-graph",
255
+ "agentflow-workspace-markdown",
256
+ "agentflow-workspace-mermaid",
257
+ "agentflow-workspace-ascii",
258
+ "agentflow-workspace-chart",
259
+ "agentflow-workspace-table",
260
+ "agentflow-workspace-html",
261
+ "agentflow-workspace-image",
262
+ "agentflow-node-reference",
263
+ "agentflow-placeholder-reference",
264
+ "agentflow-runtime-reference",
265
+ ],
266
+ [
237
267
  "agentflow-workspace-markdown",
238
268
  "agentflow-workspace-mermaid",
239
269
  "agentflow-workspace-ascii",
@@ -244,7 +274,6 @@ const BUILTIN_SKILL_COLLECTIONS = [
244
274
  "agentflow-runtime-reference",
245
275
  ],
246
276
  [
247
- "agentflow-workspace-graph",
248
277
  "agentflow-workspace-markdown",
249
278
  "agentflow-workspace-mermaid",
250
279
  "agentflow-workspace-ascii",
@@ -917,70 +946,6 @@ function skillhubInstallArgs(payload, { uninstall = false } = {}) {
917
946
  }
918
947
 
919
948
  const DISPLAY_SHARE_TTL_MS = 30 * 24 * 60 * 60 * 1000;
920
- function legacyUserWorkspacesPath(userCtx = {}) {
921
- return path.join(getAgentflowUserDataRoot(userCtx.userId || ""), USER_WORKSPACES_FILENAME);
922
- }
923
-
924
- function readLegacyAdminWorkspaces(userCtx = {}) {
925
- const users = readAuthUsers();
926
- const candidates = [];
927
- for (const [userId, user] of Object.entries(users || {})) {
928
- if (user?.isAdmin) candidates.push(String(userId || ""));
929
- }
930
- if (userCtx?.isAdmin && userCtx.userId) candidates.unshift(String(userCtx.userId));
931
- const seenPaths = new Set();
932
- const seenEntries = new Set();
933
- const out = [];
934
- for (const userId of candidates) {
935
- const p = legacyUserWorkspacesPath({ userId });
936
- const resolved = path.resolve(p);
937
- if (seenPaths.has(resolved) || resolved === path.resolve(workspacesPath())) continue;
938
- seenPaths.add(resolved);
939
- for (const entry of readWorkspacesFromPath(p, { userId })) {
940
- const key = entry.id || entry.path || entry.repoUrl;
941
- if (seenEntries.has(key)) continue;
942
- seenEntries.add(key);
943
- out.push(entry);
944
- }
945
- }
946
- return out;
947
- }
948
-
949
- function readUserWorkspaces(userCtx = {}) {
950
- const globalPath = workspacesPath();
951
- const globalWorkspaces = fs.existsSync(globalPath) ? readWorkspacesFromPath(globalPath, userCtx) : [];
952
- const adminLegacy = readLegacyAdminWorkspaces(userCtx);
953
- if (globalWorkspaces.length || adminLegacy.length) {
954
- const seen = new Set();
955
- const out = [];
956
- for (const entry of [...globalWorkspaces, ...adminLegacy]) {
957
- const key = entry.id || entry.path || entry.repoUrl;
958
- if (seen.has(key)) continue;
959
- seen.add(key);
960
- out.push(entry);
961
- }
962
- return out;
963
- }
964
- return readWorkspacesFromPath(legacyUserWorkspacesPath(userCtx), userCtx);
965
- }
966
-
967
- function listConfiguredWorkspaces(root, scopedRoot, userCtx = {}) {
968
- const currentRoot = path.resolve(scopedRoot || root);
969
- const homeRoot = path.resolve(os.homedir());
970
- const builtins = [
971
- { id: "current", label: "当前流程工作区", kind: "local", path: currentRoot, builtin: true, exists: fs.existsSync(currentRoot) && fs.statSync(currentRoot).isDirectory(), type: "flow", enabled: true },
972
- { id: "home", label: "用户 Home", kind: "local", path: homeRoot, builtin: true, exists: fs.existsSync(homeRoot) && fs.statSync(homeRoot).isDirectory(), type: "local", enabled: true },
973
- ];
974
- const custom = readUserWorkspaces(userCtx).filter((entry) => entry.enabled !== false).map((entry) => ({ ...entry, builtin: false }));
975
- const seen = new Set();
976
- return [...builtins, ...custom].filter((entry) => {
977
- const key = path.resolve(entry.path);
978
- if (seen.has(key)) return false;
979
- seen.add(key);
980
- return true;
981
- });
982
- }
983
-
984
949
  function workflowBindableWorkspaces(userCtx = {}) {
985
950
  return readUserWorkspaces(userCtx)
986
951
  .filter((entry) => entry.enabled !== false && entry.exists)
@@ -1333,6 +1298,12 @@ function isDisplayShareExpired(share) {
1333
1298
  return Number.isFinite(expiresAt) && expiresAt <= Date.now();
1334
1299
  }
1335
1300
 
1301
+ function canReadDisplayShare(share, userCtx = {}) {
1302
+ if (!share) return false;
1303
+ if (String(share.visibility || "public") !== "private") return true;
1304
+ return userCtx?.isAdmin === true || String(share.userId || "") === String(userCtx?.userId || "");
1305
+ }
1306
+
1336
1307
  function getDisplayShareOrExpired(id) {
1337
1308
  const shares = readDisplayShares();
1338
1309
  const share = shares[id];
@@ -1358,10 +1329,78 @@ function displayShareSummary(share, baseUrl = "") {
1358
1329
  expiresAt: String(share?.expiresAt || ""),
1359
1330
  expiresMode: String(share?.expiresMode || (share?.expiresAt ? "days" : "permanent")),
1360
1331
  expiresInDays: share?.expiresInDays == null ? null : Number(share.expiresInDays),
1332
+ visibility: String(share?.visibility || "public") === "private" ? "private" : "public",
1361
1333
  url: displayShareOutputUrl(share?.id || "", baseUrl),
1362
1334
  };
1363
1335
  }
1364
1336
 
1337
+ function spacePublicSummary(space = {}, baseUrl = "") {
1338
+ const ownerId = String(space.ownerId || "");
1339
+ const slug = String(space.slug || "");
1340
+ const rootPath = `/s/${encodeURIComponent(ownerId)}/${encodeURIComponent(slug)}`;
1341
+ const base = normalizePublicBaseUrl(baseUrl);
1342
+ const absoluteRoot = base ? new URL(rootPath, base.endsWith("/") ? base : `${base}/`).href : rootPath;
1343
+ return {
1344
+ id: String(space.id || ""),
1345
+ ownerId,
1346
+ slug,
1347
+ title: String(space.title || slug),
1348
+ description: String(space.description || ""),
1349
+ visibility: normalizeSpaceVisibility(space.visibility),
1350
+ status: String(space.status || "active"),
1351
+ pages: (Array.isArray(space.pages) ? space.pages : []).map((page) => ({
1352
+ id: String(page.id || ""),
1353
+ title: String(page.title || ""),
1354
+ path: normalizeSpacePagePath(page.path || "/"),
1355
+ shareId: String(page.shareId || ""),
1356
+ hidden: page.hidden === true,
1357
+ order: Number(page.order || 0),
1358
+ })),
1359
+ url: absoluteRoot,
1360
+ createdAt: String(space.createdAt || ""),
1361
+ updatedAt: String(space.updatedAt || ""),
1362
+ };
1363
+ }
1364
+
1365
+ function canManageSpace(space, userCtx = {}) {
1366
+ return Boolean(space) && (userCtx?.isAdmin === true || String(space.ownerId || "") === String(userCtx?.userId || ""));
1367
+ }
1368
+
1369
+ function displayShareSpaceReferences(shareId = "") {
1370
+ const wanted = String(shareId || "").trim();
1371
+ if (!wanted) return [];
1372
+ const refs = [];
1373
+ for (const space of readSpaces()) {
1374
+ for (const page of Array.isArray(space.pages) ? space.pages : []) {
1375
+ if (String(page.shareId || "") !== wanted) continue;
1376
+ refs.push({ spaceId: space.id, ownerId: space.ownerId, slug: space.slug, pageId: page.id, path: page.path });
1377
+ }
1378
+ }
1379
+ return refs;
1380
+ }
1381
+
1382
+ function syncSpaceShareVisibility(space) {
1383
+ if (!space) return;
1384
+ const shares = readDisplayShares();
1385
+ let changed = false;
1386
+ for (const page of Array.isArray(space.pages) ? space.pages : []) {
1387
+ const share = shares[String(page.shareId || "")];
1388
+ if (!share || String(share.userId || "") !== String(space.ownerId || "")) continue;
1389
+ const visibility = normalizeSpaceVisibility(space.visibility);
1390
+ if (share.visibility === visibility && !share.expiresAt) continue;
1391
+ shares[page.shareId] = {
1392
+ ...share,
1393
+ visibility,
1394
+ expiresAt: "",
1395
+ expiresMode: "permanent",
1396
+ expiresInDays: null,
1397
+ updatedAt: new Date().toISOString(),
1398
+ };
1399
+ changed = true;
1400
+ }
1401
+ if (changed) writeDisplayShares(shares);
1402
+ }
1403
+
1365
1404
  function listDisplaySharesForUser(userCtx = {}, baseUrl = "") {
1366
1405
  const userId = String(userCtx?.userId || "");
1367
1406
  const isAdmin = userCtx?.isAdmin === true;
@@ -1389,6 +1428,9 @@ function updateDisplayShareExpiryForUser(id, userCtx = {}, patch = {}) {
1389
1428
  const userId = String(userCtx?.userId || "");
1390
1429
  if (userCtx?.isAdmin !== true && String(share.userId || "") !== userId) return { status: 403, error: "Forbidden" };
1391
1430
  const expiry = normalizeDisplayShareExpiry(patch, new Date());
1431
+ if (displayShareSpaceReferences(id).length > 0 && expiry.expiresMode !== "permanent") {
1432
+ return { status: 409, error: "Space 页面使用中的展示内容必须永久有效;请先从空间移除页面" };
1433
+ }
1392
1434
  const updated = {
1393
1435
  ...share,
1394
1436
  expiresAt: expiry.expiresAt,
@@ -1407,6 +1449,9 @@ function deleteDisplayShareForUser(id, userCtx = {}) {
1407
1449
  if (!share) return { status: 404, error: "Display share not found" };
1408
1450
  const userId = String(userCtx?.userId || "");
1409
1451
  if (userCtx?.isAdmin !== true && String(share.userId || "") !== userId) return { status: 403, error: "Forbidden" };
1452
+ if (displayShareSpaceReferences(id).length > 0) {
1453
+ return { status: 409, error: "展示内容仍被 Space 页面引用;请先从空间移除页面" };
1454
+ }
1410
1455
  delete shares[id];
1411
1456
  writeDisplayShares(shares);
1412
1457
  return { status: 200 };
@@ -1450,6 +1495,25 @@ function serverPublicBaseUrl(req, host, port, payload = null) {
1450
1495
  return configuredPublicBaseUrl(payload) || requestPublicBaseUrl(req) || normalizePublicBaseUrl(`http://${host}:${port}`);
1451
1496
  }
1452
1497
 
1498
+ function html(res, status, content, headers = {}) {
1499
+ const body = String(content || "");
1500
+ res.writeHead(status, {
1501
+ "Content-Type": "text/html; charset=utf-8",
1502
+ "Content-Length": Buffer.byteLength(body),
1503
+ "Cache-Control": "no-store, max-age=0",
1504
+ "Content-Security-Policy": "default-src 'none'; style-src 'unsafe-inline'; form-action 'self'; base-uri 'none'; frame-ancestors 'none'",
1505
+ "X-Content-Type-Options": "nosniff",
1506
+ "Referrer-Policy": "no-referrer",
1507
+ ...headers,
1508
+ });
1509
+ res.end(body);
1510
+ }
1511
+
1512
+ async function readUrlEncodedBody(req) {
1513
+ const body = await readBody(req);
1514
+ return new URLSearchParams(String(body || ""));
1515
+ }
1516
+
1453
1517
  function publicDisplayPayloadFromShare(root, share) {
1454
1518
  const scoped = resolveWorkspaceScopeRoot(root, {
1455
1519
  flowId: share.flowId || "",
@@ -1684,7 +1748,7 @@ function bufferLooksLikeZip(buf) {
1684
1748
 
1685
1749
  /**
1686
1750
  * @param {import('http').IncomingMessage} req
1687
- * @returns {Promise<{ targetSpace: string, flowIdField: string, file: Buffer, filename: string, gotFile: boolean }>}
1751
+ * @returns {Promise<{ targetSpace: string, flowIdField: string, scheduleMode: string, file: Buffer, filename: string, gotFile: boolean }>}
1688
1752
  */
1689
1753
  function parseFlowsImportForm(req) {
1690
1754
  return new Promise((resolve, reject) => {
@@ -1694,6 +1758,7 @@ function parseFlowsImportForm(req) {
1694
1758
  });
1695
1759
  let targetSpace = "user";
1696
1760
  let flowIdField = "";
1761
+ let scheduleMode = "disabled";
1697
1762
  /** @type {Buffer[]} */
1698
1763
  const chunks = [];
1699
1764
  let filename = "";
@@ -1706,6 +1771,9 @@ function parseFlowsImportForm(req) {
1706
1771
  if (name === "flowId" && typeof val === "string") {
1707
1772
  flowIdField = val;
1708
1773
  }
1774
+ if (name === "scheduleMode" && typeof val === "string") {
1775
+ scheduleMode = val.trim().toLowerCase();
1776
+ }
1709
1777
  });
1710
1778
 
1711
1779
  bb.on("file", (name, file, info) => {
@@ -1725,6 +1793,7 @@ function parseFlowsImportForm(req) {
1725
1793
  resolve({
1726
1794
  targetSpace,
1727
1795
  flowIdField: flowIdField.trim(),
1796
+ scheduleMode,
1728
1797
  file: Buffer.concat(chunks),
1729
1798
  filename,
1730
1799
  gotFile,
@@ -1748,12 +1817,16 @@ function cleanupExpiredWorkspacePreviews(workspaceRoot = "") {
1748
1817
  }
1749
1818
  let removed = 0;
1750
1819
  for (const pipelinesRoot of roots) {
1751
- for (const item of listExpiredWorkspacePreviews(pipelinesRoot)) {
1820
+ const expired = [
1821
+ ...listExpiredWorkspacePreviews(pipelinesRoot),
1822
+ ...listExpiredWorkspaceDrafts(pipelinesRoot),
1823
+ ];
1824
+ for (const item of expired) {
1752
1825
  try {
1753
1826
  fs.rmSync(item.flowDir, { recursive: true, force: true });
1754
1827
  removed += 1;
1755
1828
  } catch (e) {
1756
- log.debug(`[workspace-preview] cleanup failed: ${(e && e.message) || String(e)}`);
1829
+ log.debug(`[workspace-temporary] cleanup failed: ${(e && e.message) || String(e)}`);
1757
1830
  }
1758
1831
  }
1759
1832
  }
@@ -2013,12 +2086,12 @@ function buildComposerPromptWithFlowContext(p) {
2013
2086
  `- flowSource:${p.flowSource}`,
2014
2087
  ...builtinExtra,
2015
2088
  `- 当前关联的节点实例 ID(顺序:画布选中优先,再输入框 @提及):${idsLine}`,
2016
- "- 像普通 agent 请求一样处理用户说明:可能只是问问题,也可能要求编辑文件。不要因为存在 flowId 就默认修改 flow.yaml。",
2089
+ "- 像普通 agent 请求一样处理用户说明:可能只是问问题,也可能要求编辑文件。不要因为存在 flowId 就默认修改流程。",
2017
2090
  "- 按需使用当前环境可用的 skills;如果用户点名某个 skill,遵循该 skill 的 SKILL.md。",
2018
2091
  "- 如果你判断需要编辑 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 后刷新画布",
2092
+ " - `skills/agentflow-flow-dsl/SKILL.md`:新建或修改 workspace.flow.js、节点、字段、连线、子流程与受控循环",
2093
+ " - `skills/agentflow-node-dsl/SKILL.md`:仅在需要确定性自定义代码节点时使用",
2094
+ " - `skills/agentflow-author-flow/SKILL.md`:需要 Draft、试运行、动态修改、发布或定时时使用完整生命周期",
2022
2095
  "- 如果只是回答问题,不要修改文件。",
2023
2096
  "",
2024
2097
  ...(p.selectedSkillBlock ? [p.selectedSkillBlock, ""] : []),
@@ -2091,6 +2164,132 @@ export function startUiServer({
2091
2164
  return;
2092
2165
  }
2093
2166
 
2167
+ if (url.pathname === "/api/auth/cli/device" && req.method === "POST") {
2168
+ let payload = {};
2169
+ try {
2170
+ const raw = await readBody(req);
2171
+ payload = raw ? JSON.parse(raw) : {};
2172
+ } catch {
2173
+ json(res, 400, { error: "Invalid JSON body" });
2174
+ return;
2175
+ }
2176
+ const authorization = createCliAuthorization({
2177
+ publicBaseUrl: serverPublicBaseUrl(req, host, uiPort),
2178
+ clientName: payload?.clientName,
2179
+ });
2180
+ json(res, 201, {
2181
+ requestId: authorization.requestId,
2182
+ deviceCode: authorization.deviceCode,
2183
+ userCode: authorization.userCode,
2184
+ verificationUrl: authorization.verificationUrl,
2185
+ expiresAt: authorization.expiresAt,
2186
+ pollInterval: authorization.pollInterval,
2187
+ });
2188
+ return;
2189
+ }
2190
+
2191
+ if (url.pathname === "/api/auth/cli/token" && req.method === "POST") {
2192
+ let payload = {};
2193
+ try {
2194
+ const raw = await readBody(req);
2195
+ payload = raw ? JSON.parse(raw) : {};
2196
+ } catch {
2197
+ json(res, 400, { error: "Invalid JSON body" });
2198
+ return;
2199
+ }
2200
+ const result = exchangeCliAuthorization(payload?.deviceCode);
2201
+ if (!result.ok) {
2202
+ json(res, result.status || 400, {
2203
+ error: result.error,
2204
+ code: result.code || "invalid_request",
2205
+ authorization: result.authorization || undefined,
2206
+ });
2207
+ return;
2208
+ }
2209
+ json(res, 200, result);
2210
+ return;
2211
+ }
2212
+
2213
+ if (url.pathname === "/api/auth/cli/revoke" && req.method === "POST") {
2214
+ const token = getSessionTokenFromRequest(req);
2215
+ if (!token || !getAuthUserFromRequest(req)) {
2216
+ json(res, 401, { error: "Unauthorized" });
2217
+ return;
2218
+ }
2219
+ revokeSessionToken(token);
2220
+ json(res, 200, { ok: true });
2221
+ return;
2222
+ }
2223
+
2224
+ if (url.pathname === "/cli/authorize" && req.method === "GET") {
2225
+ const requestId = String(url.searchParams.get("request") || "").trim();
2226
+ const user = getAuthUserFromRequest(req);
2227
+ const result = getCliAuthorization(requestId, { includeApprovalNonce: Boolean(user) });
2228
+ if (result.ok && result.authorization.status !== "pending") {
2229
+ html(res, 200, renderCliAuthorizationResult({
2230
+ approved: result.authorization.status === "approved",
2231
+ clientName: result.authorization.clientName,
2232
+ }));
2233
+ return;
2234
+ }
2235
+ html(res, result.ok ? 200 : result.status || 404, renderCliAuthorizationPage({
2236
+ authorization: result.authorization || null,
2237
+ approvalNonce: result.approvalNonce || "",
2238
+ user,
2239
+ error: result.ok ? "" : result.error,
2240
+ }));
2241
+ return;
2242
+ }
2243
+
2244
+ if (url.pathname === "/cli/authorize/login" && req.method === "POST") {
2245
+ const form = await readUrlEncodedBody(req);
2246
+ const requestId = String(form.get("request") || "").trim();
2247
+ const pending = getCliAuthorization(requestId);
2248
+ if (!pending.ok) {
2249
+ html(res, pending.status || 404, renderCliAuthorizationPage({ error: pending.error }));
2250
+ return;
2251
+ }
2252
+ const result = loginOrCreateUser(form.get("username"), form.get("password"));
2253
+ if (!result.ok) {
2254
+ html(res, result.forbidden ? 403 : 401, renderCliAuthorizationPage({
2255
+ authorization: pending.authorization,
2256
+ error: result.error || "Login failed",
2257
+ }));
2258
+ return;
2259
+ }
2260
+ res.writeHead(303, {
2261
+ Location: `/cli/authorize?request=${encodeURIComponent(requestId)}`,
2262
+ "Set-Cookie": buildSessionCookie(result.token),
2263
+ "Cache-Control": "no-store, max-age=0",
2264
+ });
2265
+ res.end();
2266
+ return;
2267
+ }
2268
+
2269
+ if (url.pathname === "/cli/authorize/decision" && req.method === "POST") {
2270
+ const user = getAuthUserFromRequest(req);
2271
+ if (!user) {
2272
+ html(res, 401, renderCliAuthorizationResult({ error: "登录状态已失效,请重新打开授权链接。" }));
2273
+ return;
2274
+ }
2275
+ const form = await readUrlEncodedBody(req);
2276
+ const requestId = String(form.get("request") || "").trim();
2277
+ const current = getCliAuthorization(requestId);
2278
+ const approved = form.get("decision") === "approve";
2279
+ const result = decideCliAuthorization({
2280
+ requestId,
2281
+ approvalNonce: form.get("approvalNonce"),
2282
+ userId: user.userId,
2283
+ approved,
2284
+ });
2285
+ html(res, result.ok ? 200 : result.status || 400, renderCliAuthorizationResult({
2286
+ approved,
2287
+ clientName: current.authorization?.clientName,
2288
+ error: result.ok ? "" : result.error,
2289
+ }));
2290
+ return;
2291
+ }
2292
+
2094
2293
  if (url.pathname === "/api/app-version" && req.method === "GET") {
2095
2294
  res.setHeader("Cache-Control", "no-store, max-age=0");
2096
2295
  json(res, 200, {
@@ -2167,6 +2366,154 @@ export function startUiServer({
2167
2366
  json(res, 200, { token: getSessionTokenFromRequest(req) || "" });
2168
2367
  return;
2169
2368
  }
2369
+
2370
+ if (req.method === "GET" && url.pathname === "/api/spaces/public") {
2371
+ try {
2372
+ const ownerId = String(url.searchParams.get("owner") || "").trim();
2373
+ const slug = String(url.searchParams.get("slug") || "").trim();
2374
+ const space = getSpaceByRoute(ownerId, slug);
2375
+ if (!space || (!canManageSpace(space, userCtx) && normalizeSpaceVisibility(space.visibility) === "private")) {
2376
+ json(res, 404, { error: "Space not found" });
2377
+ return;
2378
+ }
2379
+ if (space.status === "paused" && !canManageSpace(space, userCtx)) {
2380
+ json(res, 410, { error: "Space is paused" });
2381
+ return;
2382
+ }
2383
+ json(res, 200, {
2384
+ space: spacePublicSummary(space, requestPublicBaseUrl(req)),
2385
+ manageable: canManageSpace(space, userCtx),
2386
+ });
2387
+ } catch (e) {
2388
+ json(res, 500, { error: (e && e.message) || String(e) });
2389
+ }
2390
+ return;
2391
+ }
2392
+
2393
+ if (req.method === "GET" && url.pathname === "/api/spaces") {
2394
+ if (!authUser?.userId) {
2395
+ json(res, 401, { error: "Unauthorized" });
2396
+ return;
2397
+ }
2398
+ const spaces = listSpacesForUser(userCtx.userId, userCtx.isAdmin)
2399
+ .map((space) => spacePublicSummary(space, requestPublicBaseUrl(req)));
2400
+ json(res, 200, { spaces });
2401
+ return;
2402
+ }
2403
+
2404
+ if (req.method === "POST" && url.pathname === "/api/spaces") {
2405
+ if (!authUser?.userId) {
2406
+ json(res, 401, { error: "Unauthorized" });
2407
+ return;
2408
+ }
2409
+ try {
2410
+ const payload = JSON.parse(await readBody(req));
2411
+ const result = createSpace({
2412
+ ownerId: userCtx.userId,
2413
+ slug: payload.slug,
2414
+ title: payload.title,
2415
+ description: payload.description,
2416
+ visibility: payload.visibility,
2417
+ });
2418
+ if (result.error) {
2419
+ json(res, 400, { error: result.error });
2420
+ return;
2421
+ }
2422
+ json(res, 200, { ok: true, space: spacePublicSummary(result.space, requestPublicBaseUrl(req)) });
2423
+ } catch (e) {
2424
+ json(res, 400, { error: (e && e.message) || String(e) });
2425
+ }
2426
+ return;
2427
+ }
2428
+
2429
+ if (req.method === "PATCH" && url.pathname === "/api/spaces") {
2430
+ if (!authUser?.userId) {
2431
+ json(res, 401, { error: "Unauthorized" });
2432
+ return;
2433
+ }
2434
+ try {
2435
+ const payload = JSON.parse(await readBody(req));
2436
+ const current = getSpaceById(payload.id);
2437
+ if (!canManageSpace(current, userCtx)) {
2438
+ json(res, current ? 403 : 404, { error: current ? "Forbidden" : "Space not found" });
2439
+ return;
2440
+ }
2441
+ const result = updateSpace(payload.id, payload);
2442
+ if (result.error) {
2443
+ json(res, 400, { error: result.error });
2444
+ return;
2445
+ }
2446
+ syncSpaceShareVisibility(result.space);
2447
+ json(res, 200, { ok: true, space: spacePublicSummary(result.space, requestPublicBaseUrl(req)) });
2448
+ } catch (e) {
2449
+ json(res, 400, { error: (e && e.message) || String(e) });
2450
+ }
2451
+ return;
2452
+ }
2453
+
2454
+ if (req.method === "POST" && url.pathname === "/api/spaces/page") {
2455
+ if (!authUser?.userId) {
2456
+ json(res, 401, { error: "Unauthorized" });
2457
+ return;
2458
+ }
2459
+ try {
2460
+ const payload = JSON.parse(await readBody(req));
2461
+ const space = getSpaceById(payload.spaceId);
2462
+ if (!canManageSpace(space, userCtx)) {
2463
+ json(res, space ? 403 : 404, { error: space ? "Forbidden" : "Space not found" });
2464
+ return;
2465
+ }
2466
+ const { share } = getDisplayShareOrExpired(String(payload.shareId || "").trim());
2467
+ if (!share || (userCtx.isAdmin !== true && String(share.userId || "") !== String(userCtx.userId || ""))) {
2468
+ json(res, 400, { error: "Display share not found or not owned by current user" });
2469
+ return;
2470
+ }
2471
+ const foreignSpaceRef = displayShareSpaceReferences(share.id).find((ref) => ref.spaceId !== space.id);
2472
+ if (foreignSpaceRef) {
2473
+ json(res, 409, { error: "同一展示内容不能绑定到多个空间;请从 Workspace 重新生成一次展示" });
2474
+ return;
2475
+ }
2476
+ const result = upsertSpacePage(space.id, {
2477
+ title: payload.title,
2478
+ path: payload.path,
2479
+ shareId: share.id,
2480
+ hidden: payload.hidden === true,
2481
+ });
2482
+ if (result.error) {
2483
+ json(res, 400, { error: result.error });
2484
+ return;
2485
+ }
2486
+ syncSpaceShareVisibility(result.space);
2487
+ json(res, 200, {
2488
+ ok: true,
2489
+ page: result.page,
2490
+ space: spacePublicSummary(result.space, requestPublicBaseUrl(req)),
2491
+ });
2492
+ } catch (e) {
2493
+ json(res, 400, { error: (e && e.message) || String(e) });
2494
+ }
2495
+ return;
2496
+ }
2497
+
2498
+ if (req.method === "DELETE" && url.pathname === "/api/spaces/page") {
2499
+ if (!authUser?.userId) {
2500
+ json(res, 401, { error: "Unauthorized" });
2501
+ return;
2502
+ }
2503
+ const space = getSpaceById(url.searchParams.get("spaceId") || "");
2504
+ if (!canManageSpace(space, userCtx)) {
2505
+ json(res, space ? 403 : 404, { error: space ? "Forbidden" : "Space not found" });
2506
+ return;
2507
+ }
2508
+ const result = deleteSpacePage(space.id, url.searchParams.get("pageId") || "");
2509
+ if (result.error) {
2510
+ json(res, 404, { error: result.error });
2511
+ return;
2512
+ }
2513
+ json(res, 200, { ok: true, space: spacePublicSummary(result.space, requestPublicBaseUrl(req)) });
2514
+ return;
2515
+ }
2516
+
2170
2517
  if (req.method === "GET" && url.pathname === "/api/display/shares") {
2171
2518
  try {
2172
2519
  if (!authUser?.userId) {
@@ -2194,6 +2541,10 @@ export function startUiServer({
2194
2541
  json(res, expired ? 410 : 404, { error: expired ? "Display share has expired" : "Display share not found" });
2195
2542
  return;
2196
2543
  }
2544
+ if (!canReadDisplayShare(share, userCtx)) {
2545
+ json(res, 404, { error: "Display share not found" });
2546
+ return;
2547
+ }
2197
2548
  const payload = publicDisplayPayloadFromShare(root, share);
2198
2549
  if (payload.error) {
2199
2550
  json(res, 404, { error: payload.error });
@@ -2274,6 +2625,10 @@ export function startUiServer({
2274
2625
  json(res, expired ? 410 : 404, { error: expired ? "Display share has expired" : "Display share not found" });
2275
2626
  return;
2276
2627
  }
2628
+ if (!canReadDisplayShare(share, userCtx)) {
2629
+ json(res, 404, { error: "Display share not found" });
2630
+ return;
2631
+ }
2277
2632
  const scoped = resolveWorkspaceScopeRoot(root, {
2278
2633
  flowId: share.flowId || "",
2279
2634
  flowSource: share.flowSource || "user",
@@ -2706,6 +3061,10 @@ export function startUiServer({
2706
3061
  }
2707
3062
  const flowId = idCheck.flowId;
2708
3063
  const targetSpace = parsed.targetSpace === "workspace" ? "workspace" : "user";
3064
+ if (!["enabled", "disabled", "preserve"].includes(parsed.scheduleMode)) {
3065
+ json(res, 400, { error: "scheduleMode must be enabled, disabled, or preserve" });
3066
+ return;
3067
+ }
2709
3068
  const existing = listFlowsJson(root, {
2710
3069
  ...userCtx,
2711
3070
  includeWorkspaceFlows: targetSpace === "workspace",
@@ -2747,10 +3106,37 @@ export function startUiServer({
2747
3106
  json(res, 400, { error: w.error });
2748
3107
  return;
2749
3108
  }
2750
- if (targetSpace === "workspace") {
2751
- ensureWorkspaceCollaboration({ flowId, userId: userCtx.userId });
3109
+ const targetDir = targetSpace === "workspace"
3110
+ ? path.join(path.resolve(root), PIPELINES_DIR, flowId)
3111
+ : path.join(getUserPipelinesRoot(userCtx.userId), flowId);
3112
+ let collaborationCreated = false;
3113
+ try {
3114
+ if (targetSpace === "workspace") {
3115
+ ensureWorkspaceCollaboration({ flowId, userId: userCtx.userId });
3116
+ collaborationCreated = true;
3117
+ }
3118
+ const scoped = resolveWorkspaceScopeRoot(root, { flowId, flowSource: targetSpace }, userCtx);
3119
+ if (scoped.error) throw new Error(scoped.error);
3120
+ const importedGraph = readWorkspaceGraph(scoped.root, root).graph;
3121
+ const graph = workspaceGraphWithScheduleMode(importedGraph, parsed.scheduleMode);
3122
+ if (graph !== importedGraph) writeWorkspaceGraph(scoped.root, graph, root);
3123
+ const persisted = readWorkspaceGraph(scoped.root, root).graph;
3124
+ const workspaceSchedules = syncWorkspaceSchedulesForGraph(root, scoped, persisted, authUser, userCtx);
3125
+ json(res, 200, {
3126
+ success: true,
3127
+ flowId,
3128
+ flowSource: targetSpace,
3129
+ scheduleMode: parsed.scheduleMode,
3130
+ workspaceSchedules,
3131
+ });
3132
+ } catch (e) {
3133
+ try { fs.rmSync(targetDir, { recursive: true, force: true }); } catch (_) {}
3134
+ if (collaborationCreated) deleteWorkspaceCollaborationForFlow(flowId, false);
3135
+ json(res, 500, {
3136
+ error: `Flow publication rolled back: ${(e && e.message) || String(e)}`,
3137
+ rolledBack: true,
3138
+ });
2752
3139
  }
2753
- json(res, 200, { success: true, flowId, flowSource: targetSpace });
2754
3140
  return;
2755
3141
  }
2756
3142
 
@@ -2893,6 +3279,7 @@ export function startUiServer({
2893
3279
  expiresInDays: payload.expiresInDays,
2894
3280
  permanent: payload.permanent,
2895
3281
  expiresAt: payload.expiresAt,
3282
+ visibility: payload.visibility,
2896
3283
  });
2897
3284
  json(res, 200, { ok: true, share, url: `/display/${encodeURIComponent(share.id)}` });
2898
3285
  } catch (e) {