@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
package/bin/lib/auth.mjs CHANGED
@@ -11,6 +11,17 @@ import {
11
11
 
12
12
  const SESSION_COOKIE = "af_session";
13
13
  const SESSION_TTL_MS = 30 * 24 * 60 * 60 * 1000;
14
+ const CLI_SESSION_TTL_MS = 30 * 24 * 60 * 60 * 1000;
15
+ const CLI_AUTHORIZATION_TTL_MS = 10 * 60 * 1000;
16
+ const CLI_AUTHORIZATION_SCOPES = [
17
+ "workspace:read",
18
+ "workspace:write",
19
+ "workspace:run",
20
+ "flow:publish",
21
+ "schedule:manage",
22
+ "node-package:manage",
23
+ "workflow:manage",
24
+ ];
14
25
 
15
26
  function authRoot() {
16
27
  return path.join(getAgentflowDataRoot(), "auth");
@@ -24,6 +35,10 @@ function sessionsPath() {
24
35
  return path.join(authRoot(), "sessions.json");
25
36
  }
26
37
 
38
+ function cliAuthorizationsPath() {
39
+ return path.join(authRoot(), "cli-authorizations.json");
40
+ }
41
+
27
42
  function userAllowlistPath() {
28
43
  return path.join(authRoot(), "user-allowlist.json");
29
44
  }
@@ -62,6 +77,67 @@ function hashToken(token) {
62
77
  return crypto.createHash("sha256").update(String(token)).digest("hex");
63
78
  }
64
79
 
80
+ function createSessionForUser(userId, options = {}) {
81
+ const users = readAuthUsers();
82
+ const user = users[userId];
83
+ if (!user) return { ok: false, error: "用户不存在" };
84
+ const now = Date.now();
85
+ const token = crypto.randomBytes(32).toString("base64url");
86
+ const sessions = readJsonObject(sessionsPath());
87
+ sessions[hashToken(token)] = {
88
+ userId,
89
+ kind: String(options.kind || "web"),
90
+ clientName: String(options.clientName || "").trim(),
91
+ scopes: Array.isArray(options.scopes) ? options.scopes.map(String).filter(Boolean) : [],
92
+ createdAt: now,
93
+ expiresAt: now + (Number(options.ttlMs) || SESSION_TTL_MS),
94
+ };
95
+ writeJsonObject(sessionsPath(), sessions);
96
+ return {
97
+ ok: true,
98
+ token,
99
+ expiresAt: sessions[hashToken(token)].expiresAt,
100
+ user: { userId, username: user.username || userId, isAdmin: Boolean(user.isAdmin) },
101
+ };
102
+ }
103
+
104
+ function purgeExpiredCliAuthorizations(authorizations, now = Date.now()) {
105
+ let changed = false;
106
+ for (const [requestId, record] of Object.entries(authorizations)) {
107
+ if (Number(record?.expiresAt) > now && record?.status !== "consumed") continue;
108
+ delete authorizations[requestId];
109
+ changed = true;
110
+ }
111
+ return changed;
112
+ }
113
+
114
+ function normalizedCliClientName(value) {
115
+ const text = String(value || "AgentFlow CLI").trim().slice(0, 80);
116
+ return text || "AgentFlow CLI";
117
+ }
118
+
119
+ function cliAuthorizationSummary(requestId, record) {
120
+ if (!record) return null;
121
+ return {
122
+ requestId,
123
+ userCode: String(record.userCode || ""),
124
+ clientName: String(record.clientName || "AgentFlow CLI"),
125
+ scopes: Array.isArray(record.scopes) ? record.scopes.map(String) : [],
126
+ status: String(record.status || "pending"),
127
+ createdAt: Number(record.createdAt) || 0,
128
+ expiresAt: Number(record.expiresAt) || 0,
129
+ approvedUserId: String(record.approvedUserId || ""),
130
+ };
131
+ }
132
+
133
+ function randomUserCode() {
134
+ const alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
135
+ const bytes = crypto.randomBytes(8);
136
+ let out = "";
137
+ for (let i = 0; i < 8; i += 1) out += alphabet[bytes[i] % alphabet.length];
138
+ return `${out.slice(0, 4)}-${out.slice(4)}`;
139
+ }
140
+
65
141
  function parseCookies(header) {
66
142
  const out = {};
67
143
  for (const part of String(header || "").split(";")) {
@@ -322,9 +398,160 @@ export function getAuthUserFromRequest(req) {
322
398
  userId: session.userId,
323
399
  username: user.username || session.userId,
324
400
  isAdmin: Boolean(user.isAdmin),
401
+ sessionKind: String(session.kind || "web"),
402
+ clientName: String(session.clientName || ""),
403
+ scopes: Array.isArray(session.scopes) ? session.scopes.map(String) : [],
404
+ sessionExpiresAt: Number(session.expiresAt) || 0,
405
+ };
406
+ }
407
+
408
+ export function createCliAuthorization({ publicBaseUrl, clientName } = {}) {
409
+ const now = Date.now();
410
+ const requestId = crypto.randomBytes(18).toString("base64url");
411
+ const deviceCode = crypto.randomBytes(32).toString("base64url");
412
+ const approvalNonce = crypto.randomBytes(24).toString("base64url");
413
+ const authorizations = readJsonObject(cliAuthorizationsPath());
414
+ purgeExpiredCliAuthorizations(authorizations, now);
415
+ const active = Object.entries(authorizations)
416
+ .sort((left, right) => Number(left[1]?.createdAt) - Number(right[1]?.createdAt));
417
+ while (active.length >= 256) {
418
+ const [oldestId] = active.shift();
419
+ delete authorizations[oldestId];
420
+ }
421
+ authorizations[requestId] = {
422
+ deviceCodeHash: hashToken(deviceCode),
423
+ approvalNonceHash: hashToken(approvalNonce),
424
+ approvalNonce,
425
+ userCode: randomUserCode(),
426
+ clientName: normalizedCliClientName(clientName),
427
+ scopes: [...CLI_AUTHORIZATION_SCOPES],
428
+ status: "pending",
429
+ createdAt: now,
430
+ expiresAt: now + CLI_AUTHORIZATION_TTL_MS,
431
+ };
432
+ writeJsonObject(cliAuthorizationsPath(), authorizations);
433
+ const base = String(publicBaseUrl || "").replace(/\/+$/, "");
434
+ return {
435
+ ...cliAuthorizationSummary(requestId, authorizations[requestId]),
436
+ deviceCode,
437
+ approvalNonce,
438
+ verificationUrl: `${base}/cli/authorize?request=${encodeURIComponent(requestId)}`,
439
+ pollInterval: 3,
325
440
  };
326
441
  }
327
442
 
443
+ export function getCliAuthorization(requestId, { includeApprovalNonce = false } = {}) {
444
+ const id = String(requestId || "").trim();
445
+ if (!id) return { ok: false, status: 400, error: "缺少授权请求" };
446
+ const authorizations = readJsonObject(cliAuthorizationsPath());
447
+ const changed = purgeExpiredCliAuthorizations(authorizations);
448
+ if (changed) writeJsonObject(cliAuthorizationsPath(), authorizations);
449
+ const record = authorizations[id];
450
+ if (!record) return { ok: false, status: 404, error: "授权请求不存在或已过期" };
451
+ return {
452
+ ok: true,
453
+ authorization: cliAuthorizationSummary(id, record),
454
+ ...(includeApprovalNonce ? { approvalNonce: String(record.approvalNonce || "") } : {}),
455
+ };
456
+ }
457
+
458
+ export function decideCliAuthorization({ requestId, approvalNonce, userId, approved }) {
459
+ const id = String(requestId || "").trim();
460
+ const normalizedUserId = sanitizeAgentflowUserId(userId);
461
+ if (!id || !normalizedUserId) return { ok: false, status: 400, error: "授权请求无效" };
462
+ const authorizations = readJsonObject(cliAuthorizationsPath());
463
+ purgeExpiredCliAuthorizations(authorizations);
464
+ const record = authorizations[id];
465
+ if (!record) {
466
+ writeJsonObject(cliAuthorizationsPath(), authorizations);
467
+ return { ok: false, status: 404, error: "授权请求不存在或已过期" };
468
+ }
469
+ if (record.status !== "pending") {
470
+ return { ok: false, status: 409, error: "授权请求已经处理" };
471
+ }
472
+ const suppliedNonce = String(approvalNonce || "");
473
+ if (!suppliedNonce || hashToken(suppliedNonce) !== String(record.approvalNonceHash || "")) {
474
+ return { ok: false, status: 403, error: "授权确认已失效,请刷新页面" };
475
+ }
476
+ record.status = approved ? "approved" : "denied";
477
+ record.approvedUserId = normalizedUserId;
478
+ record.decidedAt = Date.now();
479
+ authorizations[id] = record;
480
+ writeJsonObject(cliAuthorizationsPath(), authorizations);
481
+ return { ok: true, authorization: cliAuthorizationSummary(id, record) };
482
+ }
483
+
484
+ export function exchangeCliAuthorization(deviceCode) {
485
+ const codeHash = hashToken(String(deviceCode || ""));
486
+ if (!String(deviceCode || "").trim()) return { ok: false, status: 400, error: "缺少 deviceCode" };
487
+ const now = Date.now();
488
+ const authorizations = readJsonObject(cliAuthorizationsPath());
489
+ purgeExpiredCliAuthorizations(authorizations, now);
490
+ const entry = Object.entries(authorizations).find(([, record]) => record?.deviceCodeHash === codeHash);
491
+ if (!entry) {
492
+ writeJsonObject(cliAuthorizationsPath(), authorizations);
493
+ return { ok: false, status: 410, error: "授权请求不存在或已过期", code: "expired_token" };
494
+ }
495
+ const [requestId, record] = entry;
496
+ if (record.status === "pending") {
497
+ if (Number(record.lastPolledAt) > 0 && now - Number(record.lastPolledAt) < 1000) {
498
+ return {
499
+ ok: false,
500
+ status: 429,
501
+ error: "轮询过于频繁",
502
+ code: "slow_down",
503
+ authorization: cliAuthorizationSummary(requestId, record),
504
+ };
505
+ }
506
+ record.lastPolledAt = now;
507
+ authorizations[requestId] = record;
508
+ writeJsonObject(cliAuthorizationsPath(), authorizations);
509
+ return {
510
+ ok: false,
511
+ status: 202,
512
+ error: "等待用户授权",
513
+ code: "authorization_pending",
514
+ authorization: cliAuthorizationSummary(requestId, record),
515
+ };
516
+ }
517
+ if (record.status === "denied") {
518
+ delete authorizations[requestId];
519
+ writeJsonObject(cliAuthorizationsPath(), authorizations);
520
+ return { ok: false, status: 403, error: "用户拒绝了授权", code: "access_denied" };
521
+ }
522
+ if (record.status !== "approved" || !record.approvedUserId) {
523
+ return { ok: false, status: 409, error: "授权状态无效", code: "invalid_grant" };
524
+ }
525
+ const session = createSessionForUser(record.approvedUserId, {
526
+ kind: "cli",
527
+ clientName: record.clientName,
528
+ scopes: record.scopes,
529
+ ttlMs: CLI_SESSION_TTL_MS,
530
+ });
531
+ if (!session.ok) return { ok: false, status: 401, error: session.error || "无法创建 CLI Session" };
532
+ delete authorizations[requestId];
533
+ writeJsonObject(cliAuthorizationsPath(), authorizations);
534
+ return {
535
+ ok: true,
536
+ token: session.token,
537
+ tokenType: "Bearer",
538
+ expiresAt: session.expiresAt,
539
+ user: session.user,
540
+ scopes: Array.isArray(record.scopes) ? record.scopes.map(String) : [],
541
+ };
542
+ }
543
+
544
+ export function revokeSessionToken(token) {
545
+ const raw = String(token || "").trim();
546
+ if (!raw) return false;
547
+ const sessions = readJsonObject(sessionsPath());
548
+ const key = hashToken(raw);
549
+ if (!sessions[key]) return false;
550
+ delete sessions[key];
551
+ writeJsonObject(sessionsPath(), sessions);
552
+ return true;
553
+ }
554
+
328
555
  export function loginOrCreateUser(username, password) {
329
556
  const userId = sanitizeAgentflowUserId(username);
330
557
  if (!userId) {
@@ -364,26 +591,16 @@ export function loginOrCreateUser(username, password) {
364
591
  }
365
592
  }
366
593
 
367
- const token = crypto.randomBytes(32).toString("base64url");
368
- const sessions = readJsonObject(sessionsPath());
369
- sessions[hashToken(token)] = {
370
- userId,
371
- createdAt: Date.now(),
372
- expiresAt: Date.now() + SESSION_TTL_MS,
373
- };
374
- writeJsonObject(sessionsPath(), sessions);
594
+ const session = createSessionForUser(userId, { kind: "web" });
375
595
  return {
376
596
  ok: true,
377
- token,
597
+ token: session.token,
378
598
  user: { userId, username: user.username || userId, isAdmin: Boolean(user.isAdmin) },
379
599
  migration,
380
600
  };
381
601
  }
382
602
 
383
603
  export function logoutRequest(req) {
384
- const token = parseCookies(req.headers.cookie || "")[SESSION_COOKIE];
385
- if (!token) return;
386
- const sessions = readJsonObject(sessionsPath());
387
- delete sessions[hashToken(token)];
388
- writeJsonObject(sessionsPath(), sessions);
604
+ const token = getSessionTokenFromRequest(req);
605
+ revokeSessionToken(token);
389
606
  }
@@ -28,6 +28,7 @@ import {
28
28
  resolveMarketplaceNodePackage,
29
29
  } from "./marketplace.mjs";
30
30
  import { isWorkspacePreviewDir } from "./workspace-preview.mjs";
31
+ import { isWorkspaceDraftDir } from "./workspace-draft.mjs";
31
32
  import { RETIRED_NODE_IDS } from "./legacy-flow-execution.mjs";
32
33
  import { normalizeNodeUiForSlots } from "./node-ui-kit.mjs";
33
34
 
@@ -39,6 +40,7 @@ export function collectPipelineNamesFromDir(dirPath) {
39
40
  .filter((e) => e.isDirectory())
40
41
  .filter((e) => isFlowDir(path.join(dirPath, e.name)))
41
42
  .filter((e) => !isWorkspacePreviewDir(path.join(dirPath, e.name)))
43
+ .filter((e) => !isWorkspaceDraftDir(path.join(dirPath, e.name)))
42
44
  .map((e) => e.name);
43
45
  }
44
46
 
@@ -0,0 +1,103 @@
1
+ function escapeHtml(value) {
2
+ return String(value ?? "")
3
+ .replace(/&/g, "&amp;")
4
+ .replace(/</g, "&lt;")
5
+ .replace(/>/g, "&gt;")
6
+ .replace(/"/g, "&quot;")
7
+ .replace(/'/g, "&#39;");
8
+ }
9
+
10
+ function pageShell(title, body) {
11
+ return `<!doctype html>
12
+ <html lang="zh-CN">
13
+ <head>
14
+ <meta charset="utf-8" />
15
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
16
+ <meta name="referrer" content="no-referrer" />
17
+ <title>${escapeHtml(title)} · AgentFlow</title>
18
+ <style>
19
+ :root { color-scheme: dark; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
20
+ * { box-sizing: border-box; }
21
+ body { margin: 0; min-height: 100vh; display: grid; place-items: center; color: #f6f3ff; background: radial-gradient(circle at top, #2b2048 0, #15121f 42%, #0c0b10 100%); }
22
+ main { width: min(92vw, 580px); border: 1px solid #4b4265; border-radius: 22px; padding: 30px; background: rgba(28, 25, 36, .96); box-shadow: 0 28px 80px rgba(0,0,0,.48); }
23
+ .brand { color: #ae98ff; font-size: 13px; font-weight: 800; letter-spacing: .14em; text-transform: uppercase; }
24
+ h1 { margin: 12px 0 8px; font-size: 28px; }
25
+ p { color: #bdb7cb; line-height: 1.6; }
26
+ .code { display: inline-block; margin: 10px 0 18px; padding: 9px 13px; border: 1px solid #51466e; border-radius: 10px; color: #d4c8ff; background: #15121d; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; letter-spacing: .08em; }
27
+ .card { margin: 20px 0; padding: 16px; border: 1px solid #3d374c; border-radius: 14px; background: #23202b; }
28
+ ul { margin: 10px 0 0; padding-left: 22px; color: #d0cadb; line-height: 1.8; }
29
+ label { display: block; margin: 14px 0 6px; color: #c9c2d7; font-size: 13px; font-weight: 700; }
30
+ input { width: 100%; padding: 12px 13px; border: 1px solid #514a60; border-radius: 10px; color: #fff; background: #15131a; font: inherit; }
31
+ .actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 22px; }
32
+ button { min-width: 112px; padding: 11px 18px; border: 1px solid #554d66; border-radius: 999px; color: #fff; background: #302b39; font: inherit; font-weight: 750; cursor: pointer; }
33
+ button.primary { border-color: #8d72ff; background: #7352ef; }
34
+ button.danger { color: #ffc8c8; }
35
+ .error { padding: 12px 14px; border: 1px solid #8b454d; border-radius: 10px; color: #ffd1d4; background: #351d22; }
36
+ .identity { color: #8edcb5; }
37
+ .fine { margin-top: 22px; color: #837d90; font-size: 12px; }
38
+ </style>
39
+ </head>
40
+ <body><main>${body}</main></body>
41
+ </html>`;
42
+ }
43
+
44
+ const SCOPE_LABELS = new Map([
45
+ ["workspace:read", "读取你的 Workspace 与 Flow"],
46
+ ["workspace:write", "创建和修改 Draft Workspace"],
47
+ ["workspace:run", "运行 Draft 与已发布 Flow"],
48
+ ["flow:publish", "发布 Flow 到你的空间"],
49
+ ["schedule:manage", "配置和触发定时运行"],
50
+ ["node-package:manage", "管理可复用节点包"],
51
+ ["workflow:manage", "读取和更新 Workflow 数据"],
52
+ ]);
53
+
54
+ export function renderCliAuthorizationPage({ authorization, approvalNonce = "", user = null, error = "" } = {}) {
55
+ if (!authorization) {
56
+ return pageShell("CLI 授权", `<div class="brand">AgentFlow CLI</div><h1>授权请求无效</h1><p>${escapeHtml(error || "该链接不存在或已经过期。")}</p>`);
57
+ }
58
+ const requestId = escapeHtml(authorization.requestId);
59
+ const clientName = escapeHtml(authorization.clientName || "AgentFlow CLI");
60
+ const code = escapeHtml(authorization.userCode || "");
61
+ const errorBlock = error ? `<div class="error">${escapeHtml(error)}</div>` : "";
62
+ if (!user) {
63
+ return pageShell("登录并授权", `
64
+ <div class="brand">AgentFlow CLI Authorization</div>
65
+ <h1>登录后授权 ${clientName}</h1>
66
+ <p>请求代码</p><div class="code">${code}</div>
67
+ ${errorBlock}
68
+ <form method="post" action="/cli/authorize/login">
69
+ <input type="hidden" name="request" value="${requestId}" />
70
+ <label for="username">用户名</label><input id="username" name="username" autocomplete="username" required />
71
+ <label for="password">密码</label><input id="password" name="password" type="password" autocomplete="current-password" required />
72
+ <div class="actions"><button class="primary" type="submit">登录并继续</button></div>
73
+ </form>
74
+ <div class="fine">密码只提交给当前 AgentFlow 服务,不会传给 CLI。</div>`);
75
+ }
76
+ const scopes = (authorization.scopes || []).map((scope) => `<li>${escapeHtml(SCOPE_LABELS.get(scope) || scope)}</li>`).join("");
77
+ return pageShell("确认 CLI 授权", `
78
+ <div class="brand">AgentFlow CLI Authorization</div>
79
+ <h1>允许 ${clientName} 访问?</h1>
80
+ <p class="identity">当前账号:${escapeHtml(user.username || user.userId)}</p>
81
+ <p>请求代码</p><div class="code">${code}</div>
82
+ ${errorBlock}
83
+ <div class="card"><strong>授权后 CLI 可以:</strong><ul>${scopes}</ul></div>
84
+ <form method="post" action="/cli/authorize/decision">
85
+ <input type="hidden" name="request" value="${requestId}" />
86
+ <input type="hidden" name="approvalNonce" value="${escapeHtml(approvalNonce)}" />
87
+ <div class="actions">
88
+ <button class="danger" type="submit" name="decision" value="deny">拒绝</button>
89
+ <button class="primary" type="submit" name="decision" value="approve">允许</button>
90
+ </div>
91
+ </form>
92
+ <div class="fine">CLI 将获得独立凭据;撤销 CLI 授权不会退出当前网页。</div>`);
93
+ }
94
+
95
+ export function renderCliAuthorizationResult({ approved, clientName = "AgentFlow CLI", error = "" } = {}) {
96
+ const title = error ? "授权失败" : approved ? "授权完成" : "已拒绝授权";
97
+ const message = error
98
+ ? escapeHtml(error)
99
+ : approved
100
+ ? `已允许 ${escapeHtml(clientName)} 访问。你可以关闭此页面并返回终端。`
101
+ : `没有授予 ${escapeHtml(clientName)} 访问权限。你可以关闭此页面。`;
102
+ return pageShell(title, `<div class="brand">AgentFlow CLI</div><h1>${title}</h1><p>${message}</p>`);
103
+ }
@@ -84,7 +84,7 @@ export function generateFlowSource(ir, opts = {}) {
84
84
  taken.add(name);
85
85
  return name;
86
86
  };
87
- const flowApiRoots = ["agent", "control", "display", "file", "flow", "provide", "tool", "workspace"];
87
+ const flowApiRoots = ["agent", "context", "control", "display", "file", "flow", "provide", "tool", "workspace"];
88
88
  const flowApiBinding = new Map(flowApiRoots.map((root) => [root, uniqueName(root, `${root}Api`)]));
89
89
  const apiCall = (name) => {
90
90
  const [root, ...tail] = String(name || "").split(".");
@@ -210,6 +210,13 @@ export function generateFlowSource(ir, opts = {}) {
210
210
  const pinValue = (id, name, value) => {
211
211
  const text = String(value);
212
212
  if (slotTypeOf(id, name) === "bool" && (text === "true" || text === "false")) return text;
213
+ if (slotTypeOf(id, name) === "json") {
214
+ try {
215
+ return JSON.stringify(JSON.parse(text), null, 2);
216
+ } catch {
217
+ // Keep invalid draft values round-trippable; lint/runtime will report the bad JSON.
218
+ }
219
+ }
213
220
  return textArg(id, name, text);
214
221
  };
215
222
 
@@ -228,7 +235,21 @@ export function generateFlowSource(ir, opts = {}) {
228
235
  const key = isIdentifier(name) ? name : JSON.stringify(name);
229
236
  if (wired.has(name)) {
230
237
  const x = wired.get(name);
231
- lines.push(`${key}: ${outVar.get(`${x.from}|${x.fromSlot}`) || `${x.from}.${x.fromSlot}`}`);
238
+ const sourceDefinitionId = N[x.from]?.definitionId;
239
+ const bundleAlias = node.definitionId === "context_bundle"
240
+ ? ({ knowledgeContext: "knowledge", skillsContext: "skills", workspaceContext: "workspace", mcpContext: "mcp" }[name] || "")
241
+ : "";
242
+ const contextResourceOutput = {
243
+ context_knowledge: "knowledgeContext",
244
+ context_skills: "skillsContext",
245
+ context_workspace: "workspaceContext",
246
+ }[sourceDefinitionId] || "";
247
+ const directContextBundle = name === "context" && sourceDefinitionId === "context_bundle" && x.fromSlot === "context";
248
+ const renderedKey = bundleAlias || key;
249
+ const renderedValue = directContextBundle || (bundleAlias && contextResourceOutput === name && x.fromSlot === name)
250
+ ? x.from
251
+ : (outVar.get(`${x.from}|${x.fromSlot}`) || `${x.from}.${x.fromSlot}`);
252
+ lines.push(`${renderedKey}: ${renderedValue}`);
232
253
  continue;
233
254
  }
234
255
  if (node.inputs[name] !== undefined) {
@@ -60,6 +60,16 @@ function declaredSlotType(node, kind, name) {
60
60
  return "";
61
61
  }
62
62
 
63
+ function parseJsonInput(node, name) {
64
+ const raw = String(node?.inputs?.[name] || "").trim();
65
+ if (!raw) return { value: null, error: "为空" };
66
+ try {
67
+ return { value: JSON.parse(raw), error: "" };
68
+ } catch (error) {
69
+ return { value: null, error: error.message };
70
+ }
71
+ }
72
+
63
73
  function walk(node, visit) {
64
74
  if (!node || typeof node !== "object") return;
65
75
  if (node.type) visit(node);
@@ -179,6 +189,29 @@ export function lintFlowDir(flowDir, opts = {}) {
179
189
  if (definitionId === "control_subflow_call" && !subflows[node.attrs?.subflowId]) {
180
190
  errors.push(`${id}: 引用的子流程 ${node.attrs?.subflowId || "(empty)"} 不存在`);
181
191
  }
192
+ if (definitionId === "context_knowledge") {
193
+ const parsed = parseJsonInput(node, "workspaceIds");
194
+ if (parsed.error) errors.push(`${id}: context.knowledge workspaceIds 必须是 JSON 数组(${parsed.error})`);
195
+ else if (!Array.isArray(parsed.value) || parsed.value.length === 0) errors.push(`${id}: context.knowledge 至少选择一个 Workspace ID`);
196
+ else if (parsed.value.some((value) => typeof value !== "string" || !String(value).trim())) errors.push(`${id}: context.knowledge workspaceIds 只能包含非空字符串 ID`);
197
+ }
198
+ if (definitionId === "context_skills") {
199
+ const parsed = parseJsonInput(node, "skills");
200
+ if (parsed.error) errors.push(`${id}: context.skills skills 必须是 JSON 数组(${parsed.error})`);
201
+ else if (!Array.isArray(parsed.value) || parsed.value.length === 0) errors.push(`${id}: context.skills 至少声明一个 skill`);
202
+ }
203
+ if (definitionId === "context_workspace") {
204
+ const workspaceId = String(node.inputs?.workspaceId || "current").trim();
205
+ const access = String(node.inputs?.access || "read-write").trim().toLowerCase();
206
+ if (!workspaceId) errors.push(`${id}: context.workspace workspaceId 不能为空`);
207
+ if (!["read-only", "read-write"].includes(access)) errors.push(`${id}: context.workspace access 只能是 read-only 或 read-write`);
208
+ }
209
+ if (definitionId === "context_bundle") {
210
+ const incoming = ir.edges.filter((edge) => edge.split("|")[2] === id && edge.split("|")[3] !== "prev");
211
+ if (!incoming.length) errors.push(`${id}: context.bundle 至少连接一个 Context 资源`);
212
+ const literal = Object.entries(node.inputs || {}).filter(([, value]) => String(value || "").trim());
213
+ if (literal.length) errors.push(`${id}: context.bundle 只能连接资源节点,不能内嵌 Context 正文`);
214
+ }
182
215
  if (definitionId === "control_while") {
183
216
  const conditionId = String(node.attrs?.conditionSubflowId || "");
184
217
  const bodyId = String(node.attrs?.bodySubflowId || "");
@@ -206,9 +239,11 @@ export function lintFlowDir(flowDir, opts = {}) {
206
239
  }
207
240
  if (body && !body.outputs?.state) errors.push(`${id}: Body 子流程 ${bodyId} 缺少输出 state`);
208
241
  for (const [contract, name, expected] of [
242
+ [condition?.inputs, "context", "context"],
209
243
  [condition?.inputs, "state", "json"],
210
244
  [condition?.inputs, "iteration", "text"],
211
245
  [condition?.outputs, "decision", "text"],
246
+ [body?.inputs, "context", "context"],
212
247
  [body?.inputs, "state", "json"],
213
248
  [body?.inputs, "iteration", "text"],
214
249
  [body?.inputs, "idempotencyKey", "text"],
@@ -241,6 +276,15 @@ export function lintFlowDir(flowDir, opts = {}) {
241
276
  for (const slot of node.extraIn) if (!defIn.has(slot)) errors.push(`${id}[${definitionId}]: 不存在的输入槽 "${slot}"`);
242
277
  for (const slot of node.extraOut) if (!defOut.has(slot)) errors.push(`${id}[${definitionId}]: 不存在的输出槽 "${slot}"`);
243
278
  }
279
+ if (definitionId === "agent_subAgent") {
280
+ const incomingNames = new Set(ir.edges
281
+ .map((edge) => edge.split("|"))
282
+ .filter((parts) => parts[2] === id)
283
+ .map((parts) => parts[3]));
284
+ if (incomingNames.has("context") && ["knowledgeContext", "skillsContext", "workspaceContext", "mcpContext"].some((name) => incomingNames.has(name))) {
285
+ warnings.push(`${id}: 已连接 context Bundle,不要再重复连接旧 Context 文本引脚`);
286
+ }
287
+ }
244
288
  }
245
289
 
246
290
  const inputSeen = new Map();
@@ -101,6 +101,39 @@ export function parseFlowSource(source, opts = {}) {
101
101
  return null;
102
102
  }
103
103
 
104
+ /** Read a JSON-compatible literal without evaluating user code. */
105
+ function staticJsonOf(node) {
106
+ if (!node) return { ok: false, value: null };
107
+ if (node.type === "Literal") return { ok: true, value: node.value };
108
+ if (node.type === "UnaryExpression" && (node.operator === "-" || node.operator === "+")
109
+ && node.argument.type === "Literal" && typeof node.argument.value === "number") {
110
+ return { ok: true, value: node.operator === "-" ? -node.argument.value : node.argument.value };
111
+ }
112
+ if (node.type === "TemplateLiteral" && node.expressions.length === 0) {
113
+ return { ok: true, value: node.quasis[0].value.cooked };
114
+ }
115
+ if (node.type === "ArrayExpression") {
116
+ const value = [];
117
+ for (const item of node.elements) {
118
+ const parsed = staticJsonOf(item);
119
+ if (!parsed.ok) return { ok: false, value: null };
120
+ value.push(parsed.value);
121
+ }
122
+ return { ok: true, value };
123
+ }
124
+ if (node.type === "ObjectExpression") {
125
+ const value = {};
126
+ for (const prop of node.properties) {
127
+ if (prop.type !== "Property" || prop.computed || prop.kind !== "init") return { ok: false, value: null };
128
+ const parsed = staticJsonOf(prop.value);
129
+ if (!parsed.ok) return { ok: false, value: null };
130
+ value[String(prop.key.name ?? prop.key.value ?? "")] = parsed.value;
131
+ }
132
+ return { ok: true, value };
133
+ }
134
+ return { ok: false, value: null };
135
+ }
136
+
104
137
  // import 绑定 -> 代码节点包
105
138
  const packageOf = new Map();
106
139
  for (const stmt of ast.body) {
@@ -196,6 +229,8 @@ export function parseFlowSource(source, opts = {}) {
196
229
  const def = definitionOf(definitionId);
197
230
  const defInputs = new Set(def.input.map((s) => s.name));
198
231
  const defOutputs = new Set(def.output.map((s) => s.name));
232
+ const inputType = new Map(def.input.map((s) => [s.name, String(s.type || "text")]));
233
+ const outputType = new Map(def.output.map((s) => [s.name, String(s.type || "text")]));
199
234
  const node = nodes[id];
200
235
  if (!obj || obj.type !== "ObjectExpression") return;
201
236
  for (const prop of obj.properties) {
@@ -207,9 +242,32 @@ export function parseFlowSource(source, opts = {}) {
207
242
  unresolvedAt(prop, `${id}: 引脚名不能是动态表达式`);
208
243
  continue;
209
244
  }
210
- const key = prop.key.name ?? prop.key.value;
245
+ const rawKey = prop.key.name ?? prop.key.value;
246
+ const key = definitionId === "context_bundle"
247
+ ? ({ knowledge: "knowledgeContext", skills: "skillsContext", workspace: "workspaceContext", mcp: "mcpContext" }[rawKey] || rawKey)
248
+ : rawKey;
211
249
  const isCustom = !STD_SLOTS.has(key) && !defInputs.has(key);
212
250
 
251
+ // Context resources are values, not control nodes. A bundle accepts the resource variable
252
+ // directly (`{ knowledge }`), and consumers accept the bundle directly (`{ context: ctx }`).
253
+ if (prop.value.type === "Identifier" && nodes[prop.value.name]) {
254
+ const sourceDefinitionId = nodes[prop.value.name].definitionId;
255
+ const contextOutput = {
256
+ context_knowledge: "knowledgeContext",
257
+ context_skills: "skillsContext",
258
+ context_workspace: "workspaceContext",
259
+ context_bundle: "context",
260
+ }[sourceDefinitionId];
261
+ if (contextOutput && (
262
+ (definitionId === "context_bundle" && key === contextOutput)
263
+ || (key === "context" && sourceDefinitionId === "context_bundle")
264
+ )) {
265
+ edges.push(`${prop.value.name}|${contextOutput}|${id}|${key}`);
266
+ if (isCustom) node.extraIn.push(key);
267
+ continue;
268
+ }
269
+ }
270
+
213
271
  const member = memberPath(prop.value);
214
272
  if (member) {
215
273
  edges.push(`${member[0]}|${member[1]}|${id}|${key}`);
@@ -228,6 +286,15 @@ export function parseFlowSource(source, opts = {}) {
228
286
  continue;
229
287
  }
230
288
  const text = stringOf(prop.value);
289
+ if (text === null && (inputType.get(key) === "json" || outputType.get(key) === "json")) {
290
+ const parsed = staticJsonOf(prop.value);
291
+ if (parsed.ok) {
292
+ if (defOutputs.has(key) && !defInputs.has(key)) node.outputs[key] = JSON.stringify(parsed.value);
293
+ else node.inputs[key] = JSON.stringify(parsed.value);
294
+ if (isCustom) node.extraIn.push(key);
295
+ continue;
296
+ }
297
+ }
231
298
  if (text === null) {
232
299
  unresolvedAt(prop, `${id}.${key}: 引脚值既不是上游引用也不是字面量`);
233
300
  continue;