@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.
- package/bin/lib/auth.mjs +231 -14
- package/bin/lib/catalog-flows.mjs +12 -1
- package/bin/lib/cli-auth-page.mjs +103 -0
- package/bin/lib/control-while.mjs +336 -0
- package/bin/lib/flow-dsl/codegen.mjs +74 -11
- package/bin/lib/flow-dsl/ir.mjs +33 -2
- package/bin/lib/flow-dsl/lint.mjs +128 -3
- package/bin/lib/flow-dsl/parser.mjs +168 -4
- package/bin/lib/marketplace.mjs +10 -2
- package/bin/lib/node-package-manifest.mjs +7 -2
- package/bin/lib/node-ui-kit.mjs +156 -0
- package/bin/lib/paths.mjs +2 -0
- package/bin/lib/skill-runtime.mjs +6 -0
- package/bin/lib/table.mjs +2 -3
- package/bin/lib/ui-server.mjs +220 -17
- package/bin/lib/workspace-draft.mjs +74 -0
- package/bin/lib/workspace-flow-store.mjs +11 -2
- package/bin/lib/workspace-graph-merge.mjs +3 -0
- package/bin/lib/workspace-routes.mjs +307 -5
- package/bin/lib/workspace-server.mjs +598 -11
- package/bin/pipeline/validate-flow.mjs +3 -3
- package/builtin/nodes/control_parse_json.md +25 -0
- package/builtin/nodes/control_subflow_call.md +27 -0
- package/builtin/nodes/control_while.md +124 -0
- package/builtin/nodes/provide_json.md +14 -0
- package/builtin/nodes/workspace_subflow_input.md +14 -0
- package/builtin/pipelines/new/workspace.flow.js +1 -1
- package/builtin/pipelines/subflow-preview/workspace.flow.js +29 -0
- package/builtin/pipelines/subflow-preview/workspace.layout.json +38 -0
- package/builtin/pipelines/subflow-preview/workspace.nodes.json +19 -0
- package/builtin/pipelines/subflow-preview/workspace.state.json +93 -0
- package/builtin/pipelines/while-subflow-preview/workspace.flow.js +55 -0
- package/builtin/pipelines/while-subflow-preview/workspace.layout.json +66 -0
- package/builtin/pipelines/while-subflow-preview/workspace.nodes.json +49 -0
- package/builtin/pipelines/while-subflow-preview/workspace.state.json +194 -0
- package/builtin/web-ui/dist/assets/{WorkflowAssistantThread-CY19DsYq.js → WorkflowAssistantThread-CTrXOZ00.js} +1 -1
- package/builtin/web-ui/dist/assets/index-5uJFccdX.css +1 -0
- package/builtin/web-ui/dist/assets/index-BeUfNQRL.js +873 -0
- package/builtin/web-ui/dist/index.html +2 -2
- package/package.json +6 -1
- package/reference/flow-control-capabilities.md +41 -16
- package/shared/slot-types.js +58 -0
- package/skills/agentflow-author-flow/SKILL.md +81 -8
- package/skills/agentflow-author-flow/agents/openai.yaml +3 -3
- package/skills/agentflow-cli/SKILL.md +130 -25
- package/skills/agentflow-cli/agents/openai.yaml +2 -2
- package/skills/agentflow-cli/runtime/bin/lib/skill-runtime.mjs +14765 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/agent_subAgent.md +34 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/control_agent_toBool.md +26 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/control_anyOne.md +19 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/control_cancelled.md +26 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/control_cd_workspace.md +50 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/control_delay.md +31 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/control_end.md +13 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/control_if.md +21 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/control_interval_loop.md +55 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/control_load_mcp.md +24 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/control_load_skills.md +35 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/control_parse_json.md +25 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/control_start.md +13 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/control_subflow_call.md +27 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/control_toBool.md +27 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/control_user_workspace.md +24 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/control_wait_until.md +34 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/control_while.md +124 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/display_ascii.md +24 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/display_chart.md +32 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/display_code.md +39 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/display_html.md +32 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/display_image.md +36 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/display_markdown.md +24 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/display_mermaid.md +24 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/display_react_app.md +53 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/display_table.md +32 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/provide_bool.md +14 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/provide_file.md +14 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/provide_json.md +14 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/provide_password.md +14 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/provide_str.md +14 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/tool_display_share_link.md +47 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/tool_get_env.md +20 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/tool_git_checkout.md +70 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/tool_git_worktree_load.md +73 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/tool_git_worktree_unload.md +52 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/tool_gitlab_create_mr.md +114 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/tool_jenkins_build.md +67 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/tool_load_key.md +26 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/tool_nodejs.md +52 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/tool_print.md +21 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/tool_save_key.md +26 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/tool_set_run_env.md +38 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/tool_user_ask.md +27 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/tool_user_check.md +26 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/tool_wecom_send_app_markdown.md +57 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/tool_wecom_send_group_markdown.md +45 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/workspace_one_click_task.md +44 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/workspace_run.md +16 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/workspace_scheduled_run.md +16 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/workspace_subflow_input.md +14 -0
- package/skills/agentflow-cli/runtime/package.json +6 -0
- package/skills/agentflow-cli/scripts/agentflow-auth-store.mjs +113 -0
- package/skills/agentflow-cli/scripts/agentflow-cli.mjs +393 -28
- package/skills/agentflow-cli/scripts/agentflow-runtime.mjs +11 -4
- package/skills/agentflow-flow-add-instances/SKILL.md +2 -2
- package/skills/agentflow-flow-dsl/SKILL.md +83 -10
- package/skills/agentflow-flow-dsl/agents/openai.yaml +2 -2
- package/skills/agentflow-flow-dsl/references/node-calls.md +3 -0
- package/skills/agentflow-flow-dsl/references/subflow-authoring.md +230 -0
- package/skills/agentflow-flow-edit-node-fields/SKILL.md +3 -3
- package/skills/agentflow-flow-recipes/SKILL.md +1 -1
- package/skills/agentflow-flow-recipes/references/recipes.md +1 -2
- package/skills/agentflow-flow-sync-ui/SKILL.md +1 -1
- package/skills/agentflow-node-dsl/SKILL.md +44 -13
- package/skills/agentflow-node-reference/references/builtin-nodes.md +25 -0
- package/skills/agentflow-workspace-markdown/SKILL.md +1 -1
- package/builtin/web-ui/dist/assets/index-B9ppXv7e.css +0 -1
- package/builtin/web-ui/dist/assets/index-CdEQWRrp.js +0 -872
- 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
|
|
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 =
|
|
385
|
-
|
|
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,7 +28,9 @@ 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";
|
|
33
|
+
import { normalizeNodeUiForSlots } from "./node-ui-kit.mjs";
|
|
32
34
|
|
|
33
35
|
/** 从指定目录收集含 flow.yaml 的子目录名。 */
|
|
34
36
|
export function collectPipelineNamesFromDir(dirPath) {
|
|
@@ -38,6 +40,7 @@ export function collectPipelineNamesFromDir(dirPath) {
|
|
|
38
40
|
.filter((e) => e.isDirectory())
|
|
39
41
|
.filter((e) => isFlowDir(path.join(dirPath, e.name)))
|
|
40
42
|
.filter((e) => !isWorkspacePreviewDir(path.join(dirPath, e.name)))
|
|
43
|
+
.filter((e) => !isWorkspaceDraftDir(path.join(dirPath, e.name)))
|
|
41
44
|
.map((e) => e.name);
|
|
42
45
|
}
|
|
43
46
|
|
|
@@ -219,6 +222,7 @@ export function parseNodeFrontmatter(raw) {
|
|
|
219
222
|
displayName: undefined,
|
|
220
223
|
description: undefined,
|
|
221
224
|
guide: undefined,
|
|
225
|
+
ui: undefined,
|
|
222
226
|
runtime: "native",
|
|
223
227
|
type: "",
|
|
224
228
|
paletteHidden: false,
|
|
@@ -242,6 +246,7 @@ export function parseNodeFrontmatter(raw) {
|
|
|
242
246
|
data.paletteHidden = String(parsed.palette ?? "").trim().toLowerCase() === "hidden";
|
|
243
247
|
data.input = normalizeFrontmatterSlots(parsed.input);
|
|
244
248
|
data.output = normalizeFrontmatterSlots(parsed.output);
|
|
249
|
+
data.ui = normalizeNodeUiForSlots(parsed.ui, data.input, data.output);
|
|
245
250
|
return data;
|
|
246
251
|
}
|
|
247
252
|
} catch {
|
|
@@ -320,7 +325,7 @@ export function listNodesJson(workspaceRoot, flowId, flowSource, opts = {}) {
|
|
|
320
325
|
// frontmatter 的 type: 优先于按 id 前缀的推断(workspace_run 等不带前缀的节点靠它归类)
|
|
321
326
|
if (data.type) type = data.type;
|
|
322
327
|
// frontmatter 的 palette: hidden 与 RETIRED_NODE_IDS 等价,让节点自带可见性
|
|
323
|
-
if (data.paletteHidden) continue;
|
|
328
|
+
if (data.paletteHidden && !opts.includeHidden) continue;
|
|
324
329
|
const strippedId =
|
|
325
330
|
id.replace(/^agent_?/i, "").replace(/^control_?/i, "").replace(/^provide_?/i, "").replace(/^tool_?/i, "") || id;
|
|
326
331
|
const label = data.displayName ?? strippedId;
|
|
@@ -343,6 +348,8 @@ export function listNodesJson(workspaceRoot, flowId, flowSource, opts = {}) {
|
|
|
343
348
|
displayName: translatedDisplayName || data.displayName,
|
|
344
349
|
description: translatedDescription || data.description,
|
|
345
350
|
guide: translatedGuide || data.guide,
|
|
351
|
+
ui: data.ui,
|
|
352
|
+
paletteHidden: Boolean(data.paletteHidden),
|
|
346
353
|
inputs: data.input,
|
|
347
354
|
outputs: data.output,
|
|
348
355
|
source: flowIdOpt ? "flow" : "project",
|
|
@@ -364,6 +371,7 @@ export function listNodesJson(workspaceRoot, flowId, flowSource, opts = {}) {
|
|
|
364
371
|
label: manifest.displayName,
|
|
365
372
|
displayName: manifest.displayName,
|
|
366
373
|
description: manifest.description,
|
|
374
|
+
ui: manifest.ui,
|
|
367
375
|
inputs: manifest.input,
|
|
368
376
|
outputs: manifest.output,
|
|
369
377
|
source: flowIdOpt ? "flow" : "project",
|
|
@@ -392,6 +400,7 @@ export function listNodesJson(workspaceRoot, flowId, flowSource, opts = {}) {
|
|
|
392
400
|
label: manifest.displayName,
|
|
393
401
|
displayName: manifest.displayName,
|
|
394
402
|
description: manifest.description,
|
|
403
|
+
ui: manifest.ui,
|
|
395
404
|
inputs: manifest.input,
|
|
396
405
|
outputs: manifest.output,
|
|
397
406
|
source: manifest.source || "marketplace",
|
|
@@ -664,6 +673,7 @@ export function readNodeJson(workspaceRoot, nodeId, flowId, flowSource, opts = {
|
|
|
664
673
|
version: resolved.version,
|
|
665
674
|
packageDir: resolved.packageDir,
|
|
666
675
|
runtime: resolved.runtime,
|
|
676
|
+
ui: resolved.ui,
|
|
667
677
|
};
|
|
668
678
|
}
|
|
669
679
|
const fileName = nodeId.endsWith(".md") ? nodeId : `${nodeId}.md`;
|
|
@@ -724,6 +734,7 @@ export function readNodeJson(workspaceRoot, nodeId, flowId, flowSource, opts = {
|
|
|
724
734
|
executionLogic: content || undefined,
|
|
725
735
|
description: data.description,
|
|
726
736
|
guide: data.guide,
|
|
737
|
+
ui: data.ui,
|
|
727
738
|
};
|
|
728
739
|
} catch (_) {}
|
|
729
740
|
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
function escapeHtml(value) {
|
|
2
|
+
return String(value ?? "")
|
|
3
|
+
.replace(/&/g, "&")
|
|
4
|
+
.replace(/</g, "<")
|
|
5
|
+
.replace(/>/g, ">")
|
|
6
|
+
.replace(/"/g, """)
|
|
7
|
+
.replace(/'/g, "'");
|
|
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
|
+
}
|