@fieldwangai/agentflow 0.1.142 → 0.1.144
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/README.md +8 -0
- package/README.zh-CN.md +8 -0
- package/bin/lib/auth.mjs +52 -0
- package/bin/lib/catalog-flows.mjs +27 -2
- package/bin/lib/flow-static-preview.mjs +104 -0
- package/bin/lib/help.mjs +2 -0
- package/bin/lib/main.mjs +64 -0
- package/bin/lib/ui-server.mjs +74 -11
- package/builtin/web-ui/dist/assets/{WorkflowAssistantThread-DGaXikJ9.js → WorkflowAssistantThread-CnqBhGXJ.js} +1 -1
- package/builtin/web-ui/dist/assets/index-5wTTj_jN.css +1 -0
- package/builtin/web-ui/dist/assets/index-BnXKlBQj.js +889 -0
- package/builtin/web-ui/dist/index.html +2 -2
- package/package.json +1 -1
- package/skills/agentflow-author-flow/SKILL.md +49 -0
- package/skills/agentflow-author-flow/agents/openai.yaml +4 -0
- package/skills/agentflow-cli/SKILL.md +20 -2
- package/skills/agentflow-cli/scripts/agentflow-cli.mjs +126 -3
- package/skills/agentflow-cli/scripts/workflow-report-client.mjs +2 -1
- package/skills/agentflow-workflow-report/SKILL.md +3 -0
- package/skills/agentflow-workflow-report/references/protocol.md +16 -4
- package/builtin/web-ui/dist/assets/index-CtG65J3e.css +0 -1
- package/builtin/web-ui/dist/assets/index-pLsH0Uyj.js +0 -835
package/README.md
CHANGED
|
@@ -58,6 +58,9 @@ npm install -g @fieldwangai/agentflow
|
|
|
58
58
|
# Launch Web UI (port 8765)
|
|
59
59
|
agentflow ui
|
|
60
60
|
|
|
61
|
+
# Generate and open a single-file static preview using the platform canvas (no local server)
|
|
62
|
+
agentflow flow preview ./my-flow/flow.yaml
|
|
63
|
+
|
|
61
64
|
# Or run a flow directly
|
|
62
65
|
agentflow apply <FlowName>
|
|
63
66
|
```
|
|
@@ -125,6 +128,8 @@ AgentFlow provides specialized skills for common operations:
|
|
|
125
128
|
|
|
126
129
|
| Skill | Description |
|
|
127
130
|
|-------|-------------|
|
|
131
|
+
| `agentflow-author-flow` | Generate a Flow from natural language in Codex/Cursor, validate it, open a static preview, and publish it to personal, workspace, or team scope after confirmation |
|
|
132
|
+
| `agentflow-cli` | Query, publish, and run platform Flows directly with a token and no MCP |
|
|
128
133
|
| `agentflow-flow-add-instances` | Add new nodes to flow.yaml with proper YAML structure, connection design, and positioning |
|
|
129
134
|
| `agentflow-flow-edit-node-fields` | Edit allowed fields in existing nodes (label, body, role, input/output values) without breaking topology |
|
|
130
135
|
| `agentflow-flow-sync-ui` | Sync flow.yaml changes to Web UI canvas after saving to disk |
|
|
@@ -132,6 +137,8 @@ AgentFlow provides specialized skills for common operations:
|
|
|
132
137
|
|
|
133
138
|
Skills are automatically loaded when relevant tasks are detected, providing domain-specific instructions and workflows.
|
|
134
139
|
|
|
140
|
+
For example, tell Codex/Cursor: “Use `agentflow-author-flow` to generate a Flow that sends a WeCom notification after a Jenkins build, open the local preview first, and publish it to my team after I confirm.” The agent handles the local files, validation, preview, and publish command; the user only confirms the result.
|
|
141
|
+
|
|
135
142
|
## Tutorials
|
|
136
143
|
|
|
137
144
|
- [Quickstart: PR Workflow Automation](docs/wiki/quickstart-pr-workflow.en.md)
|
|
@@ -144,6 +151,7 @@ Skills are automatically loaded when relevant tasks are detected, providing doma
|
|
|
144
151
|
|---------|-------------|
|
|
145
152
|
| `list` | List all pipelines |
|
|
146
153
|
| `ui` | Start Web UI |
|
|
154
|
+
| `flow preview <FlowName\|flow.yaml>` | Generate a single-file static Flow preview; use `--output` to choose the HTML path |
|
|
147
155
|
| `apply` | Execute flow |
|
|
148
156
|
| `validate` | Validate flow structure |
|
|
149
157
|
| `resume` | Resume from breakpoint |
|
package/README.zh-CN.md
CHANGED
|
@@ -58,6 +58,9 @@ npm install -g @fieldwangai/agentflow
|
|
|
58
58
|
# 启动 Web UI(端口 8765)
|
|
59
59
|
agentflow ui
|
|
60
60
|
|
|
61
|
+
# 生成并打开平台同款画布的单文件静态预览(无需本地服务)
|
|
62
|
+
agentflow flow preview ./my-flow/flow.yaml
|
|
63
|
+
|
|
61
64
|
# 或直接运行流程
|
|
62
65
|
agentflow apply <FlowName>
|
|
63
66
|
```
|
|
@@ -125,6 +128,8 @@ AgentFlow 提供专用技能用于常见操作:
|
|
|
125
128
|
|
|
126
129
|
| 技能 | 说明 |
|
|
127
130
|
|------|------|
|
|
131
|
+
| `agentflow-author-flow` | 在 Codex/Cursor 中根据自然语言生成 Flow,自动校验并打开静态预览;确认后发布到个人、Workspace 或团队 |
|
|
132
|
+
| `agentflow-cli` | 通过 token 直接查询、发布和运行平台 Flow,无需 MCP |
|
|
128
133
|
| `agentflow-flow-add-instances` | 向 flow.yaml 添加新节点,包括正确的 YAML 结构、连线设计和位置定位 |
|
|
129
134
|
| `agentflow-flow-edit-node-fields` | 编辑已有节点的允许字段(label、body、role、input/output 值)而不破坏拓扑 |
|
|
130
135
|
| `agentflow-flow-sync-ui` | 保存 flow.yaml 到磁盘后同步变更到 Web UI 画布 |
|
|
@@ -132,6 +137,8 @@ AgentFlow 提供专用技能用于常见操作:
|
|
|
132
137
|
|
|
133
138
|
技能在检测到相关任务时自动加载,提供领域特定的指令和工作流。
|
|
134
139
|
|
|
140
|
+
例如直接对 Codex/Cursor 说:“用 `agentflow-author-flow` 生成一个 Jenkins 构建完成后通知企业微信的 Flow,先打开本地预览,我确认后发布到团队。” Agent 会处理本地文件、校验、预览和发布命令;用户只负责确认效果。
|
|
141
|
+
|
|
135
142
|
## 教程
|
|
136
143
|
|
|
137
144
|
- [快速上手:PR 流程自动化](docs/wiki/quickstart-pr-workflow.zh-CN.md)
|
|
@@ -144,6 +151,7 @@ AgentFlow 提供专用技能用于常见操作:
|
|
|
144
151
|
|------|------|
|
|
145
152
|
| `list` | 列出所有流水线 |
|
|
146
153
|
| `ui` | 启动 Web UI |
|
|
154
|
+
| `flow preview <FlowName\|flow.yaml>` | 生成平台同款画布的单文件静态 Flow 预览;可用 `--output` 指定 HTML |
|
|
147
155
|
| `apply` | 执行流程 |
|
|
148
156
|
| `validate` | 校验流程结构 |
|
|
149
157
|
| `resume` | 断点续跑 |
|
package/bin/lib/auth.mjs
CHANGED
|
@@ -88,6 +88,58 @@ export function readAuthUsers() {
|
|
|
88
88
|
return readJsonObject(usersPath());
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
export function listAuthUsers() {
|
|
92
|
+
return Object.entries(readAuthUsers())
|
|
93
|
+
.map(([userId, user]) => ({
|
|
94
|
+
userId,
|
|
95
|
+
username: String(user?.username || userId),
|
|
96
|
+
isAdmin: Boolean(user?.isAdmin),
|
|
97
|
+
createdAt: String(user?.createdAt || ""),
|
|
98
|
+
updatedAt: String(user?.updatedAt || user?.createdAt || ""),
|
|
99
|
+
}))
|
|
100
|
+
.sort((left, right) => left.username.localeCompare(right.username));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function resetAuthUserPassword(userId, password) {
|
|
104
|
+
const normalizedUserId = sanitizeAgentflowUserId(userId);
|
|
105
|
+
if (!normalizedUserId) return { ok: false, status: 400, error: "用户名无效" };
|
|
106
|
+
const nextPassword = String(password || "");
|
|
107
|
+
if (nextPassword.length < 4) return { ok: false, status: 400, error: "密码至少 4 位" };
|
|
108
|
+
|
|
109
|
+
const users = readAuthUsers();
|
|
110
|
+
const user = users[normalizedUserId];
|
|
111
|
+
if (!user) return { ok: false, status: 404, error: "用户不存在" };
|
|
112
|
+
|
|
113
|
+
const nextCredential = hashPassword(nextPassword);
|
|
114
|
+
users[normalizedUserId] = {
|
|
115
|
+
...user,
|
|
116
|
+
salt: nextCredential.salt,
|
|
117
|
+
hash: nextCredential.hash,
|
|
118
|
+
updatedAt: new Date().toISOString(),
|
|
119
|
+
};
|
|
120
|
+
writeJsonObject(usersPath(), users);
|
|
121
|
+
|
|
122
|
+
const sessions = readJsonObject(sessionsPath());
|
|
123
|
+
let revokedSessions = 0;
|
|
124
|
+
for (const [sessionKey, session] of Object.entries(sessions)) {
|
|
125
|
+
if (session?.userId !== normalizedUserId) continue;
|
|
126
|
+
delete sessions[sessionKey];
|
|
127
|
+
revokedSessions += 1;
|
|
128
|
+
}
|
|
129
|
+
writeJsonObject(sessionsPath(), sessions);
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
ok: true,
|
|
133
|
+
user: {
|
|
134
|
+
userId: normalizedUserId,
|
|
135
|
+
username: String(user.username || normalizedUserId),
|
|
136
|
+
isAdmin: Boolean(user.isAdmin),
|
|
137
|
+
updatedAt: users[normalizedUserId].updatedAt,
|
|
138
|
+
},
|
|
139
|
+
revokedSessions,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
91
143
|
export function authSetupRequired() {
|
|
92
144
|
return Object.keys(readAuthUsers()).length === 0;
|
|
93
145
|
}
|
|
@@ -229,7 +229,7 @@ export function parseNodeFrontmatter(raw) {
|
|
|
229
229
|
* @param {string} workspaceRoot
|
|
230
230
|
* @param {string} flowId
|
|
231
231
|
* @param {string} flowSource
|
|
232
|
-
* @param {{ archived?: boolean }} [opts]
|
|
232
|
+
* @param {{ archived?: boolean, staticFlowPath?: string }} [opts]
|
|
233
233
|
*/
|
|
234
234
|
export function listNodesJson(workspaceRoot, flowId, flowSource, opts = {}) {
|
|
235
235
|
const root = path.resolve(workspaceRoot);
|
|
@@ -238,7 +238,14 @@ export function listNodesJson(workspaceRoot, flowId, flowSource, opts = {}) {
|
|
|
238
238
|
const byId = new Map();
|
|
239
239
|
const pipelineTranslations = {};
|
|
240
240
|
let marketplaceFlowData = null;
|
|
241
|
-
|
|
241
|
+
const staticFlowPath = opts.staticFlowPath ? path.resolve(String(opts.staticFlowPath)) : "";
|
|
242
|
+
const staticFlowDir = staticFlowPath ? path.dirname(staticFlowPath) : "";
|
|
243
|
+
if (staticFlowPath && fs.existsSync(staticFlowPath) && fs.statSync(staticFlowPath).isFile()) {
|
|
244
|
+
try {
|
|
245
|
+
const parsed = yaml.load(fs.readFileSync(staticFlowPath, "utf-8"));
|
|
246
|
+
if (parsed && typeof parsed === "object") marketplaceFlowData = parsed;
|
|
247
|
+
} catch (_) {}
|
|
248
|
+
} else if (flowId && flowSource) {
|
|
242
249
|
const flowPath = getFlowYamlAbs(workspaceRoot, flowId, flowSource, opts);
|
|
243
250
|
if (flowPath.path && fs.existsSync(flowPath.path)) {
|
|
244
251
|
try {
|
|
@@ -315,6 +322,24 @@ export function listNodesJson(workspaceRoot, flowId, flowSource, opts = {}) {
|
|
|
315
322
|
runtime: manifest.runtime,
|
|
316
323
|
});
|
|
317
324
|
}
|
|
325
|
+
if (staticFlowDir) {
|
|
326
|
+
addFromDir(path.join(staticFlowDir, "nodes"), "flow", flowId);
|
|
327
|
+
const flowData = marketplaceFlowData;
|
|
328
|
+
if (flowData?.instances) {
|
|
329
|
+
pipelineTranslations[flowId] = pipelineTranslations[flowId] || {};
|
|
330
|
+
for (const [nodeId, inst] of Object.entries(flowData.instances)) {
|
|
331
|
+
pipelineTranslations[flowId][nodeId] = {
|
|
332
|
+
label: inst?.label,
|
|
333
|
+
body: inst?.body,
|
|
334
|
+
description: inst?.description || inst?.userDescription,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
if (flowData?.ui?.description) {
|
|
339
|
+
pipelineTranslations[flowId] = pipelineTranslations[flowId] || {};
|
|
340
|
+
pipelineTranslations[flowId].__flowDescription = flowData.ui.description;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
318
343
|
if (flowId && flowSource) {
|
|
319
344
|
if (flowSource === "builtin") {
|
|
320
345
|
addFromDir(path.join(PACKAGE_BUILTIN_PIPELINES_DIR, flowId, "nodes"), "flow", flowId);
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
function assetPathFromHtml(indexHtml, pattern, label, distDir) {
|
|
6
|
+
const match = indexHtml.match(pattern);
|
|
7
|
+
if (!match?.[1]) throw new Error(`AgentFlow Web UI ${label} asset is missing; run npm run build:web-ui first`);
|
|
8
|
+
const assetPath = path.join(distDir, match[1].replace(/^\/+/, ""));
|
|
9
|
+
if (!fs.existsSync(assetPath) || !fs.statSync(assetPath).isFile()) {
|
|
10
|
+
throw new Error(`AgentFlow Web UI ${label} asset not found: ${assetPath}`);
|
|
11
|
+
}
|
|
12
|
+
return assetPath;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function safeInlineJson(value) {
|
|
16
|
+
return JSON.stringify(value)
|
|
17
|
+
.replace(/</g, "\\u003c")
|
|
18
|
+
.replace(/\u2028/g, "\\u2028")
|
|
19
|
+
.replace(/\u2029/g, "\\u2029");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function safeInlineScript(source) {
|
|
23
|
+
return String(source).replace(/<\/script/gi, "<\\/script");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function safeInlineStyle(source) {
|
|
27
|
+
return String(source).replace(/<\/style/gi, "<\\/style");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function escapeHtml(value) {
|
|
31
|
+
return String(value)
|
|
32
|
+
.replace(/&/g, "&")
|
|
33
|
+
.replace(/</g, "<")
|
|
34
|
+
.replace(/>/g, ">")
|
|
35
|
+
.replace(/"/g, """);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Build one self-contained, read-only HTML snapshot using the production Web UI bundle.
|
|
40
|
+
* No local HTTP server or AgentFlow process is required after this function returns.
|
|
41
|
+
*/
|
|
42
|
+
export function writeStaticFlowPreview({
|
|
43
|
+
flowId,
|
|
44
|
+
flowPath,
|
|
45
|
+
nodeCatalog,
|
|
46
|
+
outputPath,
|
|
47
|
+
distDir,
|
|
48
|
+
}) {
|
|
49
|
+
const indexPath = path.join(distDir, "index.html");
|
|
50
|
+
if (!fs.existsSync(indexPath)) {
|
|
51
|
+
throw new Error(`AgentFlow Web UI build not found: ${indexPath}; run npm run build:web-ui first`);
|
|
52
|
+
}
|
|
53
|
+
const indexHtml = fs.readFileSync(indexPath, "utf-8");
|
|
54
|
+
const scriptPath = assetPathFromHtml(
|
|
55
|
+
indexHtml,
|
|
56
|
+
/<script[^>]+src=["']([^"']+\.js)["'][^>]*><\/script>/i,
|
|
57
|
+
"JavaScript",
|
|
58
|
+
distDir,
|
|
59
|
+
);
|
|
60
|
+
const stylePath = assetPathFromHtml(
|
|
61
|
+
indexHtml,
|
|
62
|
+
/<link[^>]+href=["']([^"']+\.css)["'][^>]*>/i,
|
|
63
|
+
"CSS",
|
|
64
|
+
distDir,
|
|
65
|
+
);
|
|
66
|
+
const flowYaml = fs.readFileSync(flowPath, "utf-8");
|
|
67
|
+
const payload = {
|
|
68
|
+
format: "agentflow-static-preview-v1",
|
|
69
|
+
flow: { id: String(flowId || "local-preview"), source: "preview", archived: false },
|
|
70
|
+
flowYaml,
|
|
71
|
+
nodeCatalog: nodeCatalog && typeof nodeCatalog === "object" ? nodeCatalog : { nodes: [] },
|
|
72
|
+
revision: crypto.createHash("sha256").update(flowYaml).digest("hex").slice(0, 24),
|
|
73
|
+
};
|
|
74
|
+
const appScript = safeInlineScript(fs.readFileSync(scriptPath, "utf-8"));
|
|
75
|
+
const appStyle = safeInlineStyle(fs.readFileSync(stylePath, "utf-8"));
|
|
76
|
+
const title = `${escapeHtml(flowId || "Flow")} · AgentFlow Preview`;
|
|
77
|
+
const html = `<!doctype html>
|
|
78
|
+
<html class="af-dark" lang="zh-CN">
|
|
79
|
+
<head>
|
|
80
|
+
<meta charset="UTF-8" />
|
|
81
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
82
|
+
<meta name="generator" content="AgentFlow static Flow preview" />
|
|
83
|
+
<title>${title}</title>
|
|
84
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
85
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
86
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Space+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
|
87
|
+
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0" rel="stylesheet" />
|
|
88
|
+
<style>${appStyle}</style>
|
|
89
|
+
<script>window.__AGENTFLOW_STATIC_FLOW_PREVIEW__=${safeInlineJson(payload)};</script>
|
|
90
|
+
</head>
|
|
91
|
+
<body>
|
|
92
|
+
<div id="root"></div>
|
|
93
|
+
<script type="module">${appScript}</script>
|
|
94
|
+
</body>
|
|
95
|
+
</html>
|
|
96
|
+
`;
|
|
97
|
+
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
98
|
+
fs.writeFileSync(outputPath, html, "utf-8");
|
|
99
|
+
return {
|
|
100
|
+
outputPath,
|
|
101
|
+
bytes: Buffer.byteLength(html),
|
|
102
|
+
revision: payload.revision,
|
|
103
|
+
};
|
|
104
|
+
}
|
package/bin/lib/help.mjs
CHANGED
|
@@ -18,6 +18,7 @@ AgentFlow CLI — 使用 Cursor / OpenCode / Claude Code / Codex CLI 流式输
|
|
|
18
18
|
agentflow download <slug|title> [--user|--workspace] [--as <id>] [--raw [--output <dir>]] 从 Hub 下载流程(默认 --user 安装到 ~/agentflow/pipelines/<id>;--workspace 安装到当前工程 .workspace/agentflow/pipelines/<id>;--raw 仅保留压缩包)
|
|
19
19
|
agentflow list 列出所有流水线
|
|
20
20
|
agentflow ui [--host <addr>] [--port <n>] [--scheduler] [--no-open] [--hide-community-links] 本地 HTTP:流水线列表 + React Flow 节点流程图编辑保存(默认 127.0.0.1:8765;可用 AGENTFLOW_UI_HOST)
|
|
21
|
+
agentflow flow preview <FlowName|flow.yaml> [--output <preview.html>] [--no-open] 生成平台同款画布的单文件静态 Flow 预览
|
|
21
22
|
agentflow mcp 启动 AgentFlow MCP stdio server,供 Cursor/Codex 运行流程并读取 display 输出
|
|
22
23
|
agentflow scheduler start [--poll-ms <ms>] 启动定时执行调度器(读取各流水线 schedule.json)
|
|
23
24
|
agentflow scheduler status [--json] 查看定时执行配置与状态
|
|
@@ -88,6 +89,7 @@ Usage:
|
|
|
88
89
|
agentflow download <slug|title> [--user|--workspace] [--as <id>] [--raw [--output <dir>]] Download flow (default --user → ~/agentflow/pipelines/<id>; --workspace → current project's .workspace/agentflow/pipelines/<id>; --raw keeps the archive)
|
|
89
90
|
agentflow list List all pipelines
|
|
90
91
|
agentflow ui [--host <addr>] [--port <n>] [--scheduler] [--no-open] [--hide-community-links] Local HTTP: pipeline list + React Flow node diagram editor (default 127.0.0.1:8765; AGENTFLOW_UI_HOST supported)
|
|
92
|
+
agentflow flow preview <FlowName|flow.yaml> [--output <preview.html>] [--no-open] Generate a single-file static Flow preview using the platform canvas
|
|
91
93
|
agentflow mcp Start the AgentFlow MCP stdio server for Cursor/Codex to run flows and read display outputs
|
|
92
94
|
agentflow scheduler start [--poll-ms <ms>] Start the scheduled-run scheduler (reads each pipeline schedule.json)
|
|
93
95
|
agentflow scheduler status [--json] Show scheduled-run configuration and state
|
package/bin/lib/main.mjs
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { spawn } from "child_process";
|
|
2
|
+
import fs from "fs";
|
|
2
3
|
import path from "path";
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
3
5
|
import chalk from "chalk";
|
|
4
6
|
import { apply, replay, resume } from "./apply.mjs";
|
|
5
7
|
import {
|
|
@@ -33,6 +35,7 @@ import { hubListRemote, hubDownload } from "./hub-remote.mjs";
|
|
|
33
35
|
import { cancelScheduledRun, listScheduleStatuses, startScheduler } from "./scheduler.mjs";
|
|
34
36
|
import { installFlowDependency, listMarketplacePackages, publishNodePackage } from "./marketplace.mjs";
|
|
35
37
|
import { startMcpServer } from "./mcp-server.mjs";
|
|
38
|
+
import { writeStaticFlowPreview } from "./flow-static-preview.mjs";
|
|
36
39
|
|
|
37
40
|
async function readStdin() {
|
|
38
41
|
const chunks = [];
|
|
@@ -346,6 +349,67 @@ export async function main() {
|
|
|
346
349
|
process.stdout.write(JSON.stringify(result.success ? { success: true } : result) + "\n");
|
|
347
350
|
process.exit(result.success ? 0 : 1);
|
|
348
351
|
}
|
|
352
|
+
if (sub === "flow" && argv[0] === "preview") {
|
|
353
|
+
shift();
|
|
354
|
+
const target = shift();
|
|
355
|
+
if (!target) throw new Error("Usage: agentflow flow preview <FlowName|flow.yaml> [--output <preview.html>] [--no-open]");
|
|
356
|
+
let outputPath = "";
|
|
357
|
+
const outputIdx = argv.indexOf("--output");
|
|
358
|
+
if (outputIdx >= 0 && argv[outputIdx + 1]) {
|
|
359
|
+
outputPath = path.resolve(workspaceRoot, argv[outputIdx + 1]);
|
|
360
|
+
argv.splice(outputIdx, 2);
|
|
361
|
+
}
|
|
362
|
+
const noOpen = argv.includes("--no-open");
|
|
363
|
+
if (noOpen) argv.splice(argv.indexOf("--no-open"), 1);
|
|
364
|
+
if (argv.length > 0) throw new Error(`Unknown flow preview option: ${argv[0]}`);
|
|
365
|
+
|
|
366
|
+
const targetPath = path.resolve(workspaceRoot, target);
|
|
367
|
+
let previewFlowPath = "";
|
|
368
|
+
if (fs.existsSync(targetPath)) {
|
|
369
|
+
previewFlowPath = fs.statSync(targetPath).isDirectory()
|
|
370
|
+
? path.join(targetPath, "flow.yaml")
|
|
371
|
+
: targetPath;
|
|
372
|
+
} else {
|
|
373
|
+
const flowDir = getFlowDir(workspaceRoot, target);
|
|
374
|
+
if (flowDir) previewFlowPath = path.join(flowDir, "flow.yaml");
|
|
375
|
+
}
|
|
376
|
+
if (!previewFlowPath || !fs.existsSync(previewFlowPath) || !fs.statSync(previewFlowPath).isFile()) {
|
|
377
|
+
throw new Error(`Flow not found: ${target}`);
|
|
378
|
+
}
|
|
379
|
+
previewFlowPath = fs.realpathSync(previewFlowPath);
|
|
380
|
+
const flowId = path.basename(path.dirname(previewFlowPath)) || "local-preview";
|
|
381
|
+
const safeFlowId = flowId.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "flow-preview";
|
|
382
|
+
if (!outputPath) {
|
|
383
|
+
outputPath = path.join(workspaceRoot, ".workspace", "agentflow", "previews", `${safeFlowId}.html`);
|
|
384
|
+
}
|
|
385
|
+
if (!/\.html?$/i.test(outputPath)) throw new Error("Preview output must be an HTML file");
|
|
386
|
+
const nodeCatalog = listNodesJson(workspaceRoot, flowId, "", {
|
|
387
|
+
staticFlowPath: previewFlowPath,
|
|
388
|
+
marketplaceScope: "all",
|
|
389
|
+
});
|
|
390
|
+
const result = writeStaticFlowPreview({
|
|
391
|
+
flowId,
|
|
392
|
+
flowPath: previewFlowPath,
|
|
393
|
+
nodeCatalog,
|
|
394
|
+
outputPath,
|
|
395
|
+
distDir: path.join(path.dirname(path.dirname(path.dirname(fileURLToPath(import.meta.url)))), "builtin", "web-ui", "dist"),
|
|
396
|
+
});
|
|
397
|
+
const url = pathToFileURL(result.outputPath).href;
|
|
398
|
+
process.stderr.write(`AgentFlow static preview: ${result.outputPath}\nSource: ${previewFlowPath}\n`);
|
|
399
|
+
if (!noOpen) {
|
|
400
|
+
if (process.platform === "win32") {
|
|
401
|
+
const child = spawn("cmd", ["/c", "start", "", url], { detached: true, stdio: "ignore" });
|
|
402
|
+
child.unref();
|
|
403
|
+
} else if (process.platform === "darwin") {
|
|
404
|
+
const child = spawn("open", [url], { detached: true, stdio: "ignore" });
|
|
405
|
+
child.unref();
|
|
406
|
+
} else {
|
|
407
|
+
const child = spawn("xdg-open", [url], { detached: true, stdio: "ignore" });
|
|
408
|
+
child.unref();
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
349
413
|
if (sub === "ui") {
|
|
350
414
|
let port = 8765;
|
|
351
415
|
let host = process.env.AGENTFLOW_UI_HOST || "127.0.0.1";
|
package/bin/lib/ui-server.mjs
CHANGED
|
@@ -109,10 +109,12 @@ import {
|
|
|
109
109
|
getAuthUserFromRequest,
|
|
110
110
|
getSessionTokenFromRequest,
|
|
111
111
|
isAuthUserAllowed,
|
|
112
|
+
listAuthUsers,
|
|
112
113
|
loginOrCreateUser,
|
|
113
114
|
logoutRequest,
|
|
114
115
|
readAuthUsers,
|
|
115
116
|
readUserAllowlist,
|
|
117
|
+
resetAuthUserPassword,
|
|
116
118
|
writeUserAllowlist,
|
|
117
119
|
} from "./auth.mjs";
|
|
118
120
|
import { readGlobalEnvRows, readMergedEnvObject, readUserEnvRows, writeGlobalEnvRows, writeUserEnvRows } from "./user-env.mjs";
|
|
@@ -3905,6 +3907,8 @@ export function prdWorkflowDashboardTimeline(workflows = []) {
|
|
|
3905
3907
|
for (const entry of Array.isArray(workflow?.timeline) ? workflow.timeline : []) {
|
|
3906
3908
|
const memberKey = String(entry?.key || [entry?.source, entry?.kind, entry?.id].filter(Boolean).join(":"));
|
|
3907
3909
|
const identity = prdWorkflowDashboardTimelineIdentity(entry);
|
|
3910
|
+
const rawId = String(entry?.id || "").trim();
|
|
3911
|
+
const projectionId = identity && identity !== rawId.toLowerCase() ? identity : rawId;
|
|
3908
3912
|
const groupKey = prdWorkflowDashboardTimelineGroupKey(entry);
|
|
3909
3913
|
if (!memberKey || !groupKey) continue;
|
|
3910
3914
|
assignedWorkflowIds.add(workflowId);
|
|
@@ -3912,6 +3916,7 @@ export function prdWorkflowDashboardTimeline(workflows = []) {
|
|
|
3912
3916
|
key: memberKey,
|
|
3913
3917
|
kind: String(entry.kind || ""),
|
|
3914
3918
|
id: identity || String(entry.id || ""),
|
|
3919
|
+
projectionId,
|
|
3915
3920
|
title: String(entry.title || entry.id || ""),
|
|
3916
3921
|
date: String(entry.date || ""),
|
|
3917
3922
|
startDate: String(entry.startDate || ""),
|
|
@@ -3928,6 +3933,7 @@ export function prdWorkflowDashboardTimeline(workflows = []) {
|
|
|
3928
3933
|
memberKeys: [],
|
|
3929
3934
|
};
|
|
3930
3935
|
current.kind = String(entry.kind || current.kind);
|
|
3936
|
+
current.projectionId = projectionId || current.projectionId;
|
|
3931
3937
|
current.title = String(entry.title || current.title);
|
|
3932
3938
|
current.date = String(entry.date || current.date);
|
|
3933
3939
|
current.startDate = String(entry.startDate || current.startDate);
|
|
@@ -11676,10 +11682,8 @@ function prdWorkflowMergeProducerTimeline(report, currentSnapshot = {}) {
|
|
|
11676
11682
|
};
|
|
11677
11683
|
}
|
|
11678
11684
|
|
|
11679
|
-
function
|
|
11680
|
-
const operation = String(
|
|
11681
|
-
payload.adminOperation || payload.admin_operation || payload.administrativeOperation || payload.administrative_operation || "",
|
|
11682
|
-
).trim().toLowerCase();
|
|
11685
|
+
function prdWorkflowAdminVersionRepairOperation(value = "", userCtx = {}) {
|
|
11686
|
+
const operation = String(value || "").trim().toLowerCase();
|
|
11683
11687
|
if (!operation) return { requested: false };
|
|
11684
11688
|
if (operation !== "repair-version-membership") {
|
|
11685
11689
|
return { requested: true, status: 400, error: `Unsupported admin Workflow operation: ${operation}` };
|
|
@@ -11687,6 +11691,15 @@ function prdWorkflowAdminVersionRepairIntent(payload = {}, report = {}, userCtx
|
|
|
11687
11691
|
if (userCtx.isAdmin !== true) {
|
|
11688
11692
|
return { requested: true, status: 403, error: "Admin permission required" };
|
|
11689
11693
|
}
|
|
11694
|
+
return { requested: true, operation };
|
|
11695
|
+
}
|
|
11696
|
+
|
|
11697
|
+
function prdWorkflowAdminVersionRepairIntent(payload = {}, report = {}, userCtx = {}) {
|
|
11698
|
+
const intent = prdWorkflowAdminVersionRepairOperation(
|
|
11699
|
+
payload.adminOperation || payload.admin_operation || payload.administrativeOperation || payload.administrative_operation || "",
|
|
11700
|
+
userCtx,
|
|
11701
|
+
);
|
|
11702
|
+
if (!intent.requested || intent.error) return intent;
|
|
11690
11703
|
const forbiddenKeys = ["action", "artifacts", "observation", "globalState", "global_state", "extensions", "extension"]
|
|
11691
11704
|
.filter((key) => Object.prototype.hasOwnProperty.call(payload, key));
|
|
11692
11705
|
if (forbiddenKeys.length) {
|
|
@@ -11714,10 +11727,17 @@ function prdWorkflowAdminVersionRepairIntent(payload = {}, report = {}, userCtx
|
|
|
11714
11727
|
if (!report.expectedRevision) {
|
|
11715
11728
|
return { requested: true, status: 400, error: "Admin version repair requires expectedRevision" };
|
|
11716
11729
|
}
|
|
11717
|
-
|
|
11730
|
+
const rawReason = String(payload.adminReason || payload.admin_reason || "");
|
|
11731
|
+
if (rawReason.length > 500) {
|
|
11732
|
+
return { requested: true, status: 400, error: "Admin version repair reason exceeds 500 characters" };
|
|
11733
|
+
}
|
|
11734
|
+
if (/[\0\r\n]/.test(rawReason)) {
|
|
11735
|
+
return { requested: true, status: 400, error: "Admin version repair reason contains control characters" };
|
|
11736
|
+
}
|
|
11737
|
+
return { ...intent, reason: rawReason.trim() };
|
|
11718
11738
|
}
|
|
11719
11739
|
|
|
11720
|
-
function prdWorkflowMergeAdminVersionTimeline(report, currentSnapshot = {}) {
|
|
11740
|
+
function prdWorkflowMergeAdminVersionTimeline(report, currentSnapshot = {}, adminIntent = {}) {
|
|
11721
11741
|
const source = prdWorkflowRuntimeEventProducer(report?.event || {});
|
|
11722
11742
|
const current = Array.isArray(currentSnapshot?.projections?.timeline) ? currentSnapshot.projections.timeline : [];
|
|
11723
11743
|
const incoming = Array.isArray(report?.projections?.timeline) ? report.projections.timeline : [];
|
|
@@ -11729,6 +11749,7 @@ function prdWorkflowMergeAdminVersionTimeline(report, currentSnapshot = {}) {
|
|
|
11729
11749
|
const administrativeRepair = {
|
|
11730
11750
|
kind: "version-attribution",
|
|
11731
11751
|
operation: "repair-version-membership",
|
|
11752
|
+
...(adminIntent.reason ? { reason: adminIntent.reason } : {}),
|
|
11732
11753
|
};
|
|
11733
11754
|
return {
|
|
11734
11755
|
...report,
|
|
@@ -13478,7 +13499,6 @@ export function startUiServer({
|
|
|
13478
13499
|
const root = path.resolve(workspaceRoot);
|
|
13479
13500
|
const uiPort = port;
|
|
13480
13501
|
const uiConfig = { hideCommunityLinks: Boolean(hideCommunityLinks) };
|
|
13481
|
-
|
|
13482
13502
|
const server = http.createServer(async (req, res) => {
|
|
13483
13503
|
const url = new URL(req.url || "/", "http://127.0.0.1");
|
|
13484
13504
|
const reqStart = Date.now();
|
|
@@ -14356,6 +14376,14 @@ export function startUiServer({
|
|
|
14356
14376
|
}
|
|
14357
14377
|
const flowId = String(url.searchParams.get("flowId") || "").trim();
|
|
14358
14378
|
const flowSource = String(url.searchParams.get("flowSource") || "user").trim() || "user";
|
|
14379
|
+
const adminVersionRepair = prdWorkflowAdminVersionRepairOperation(
|
|
14380
|
+
url.searchParams.get("adminOperation") || url.searchParams.get("admin_operation") || "",
|
|
14381
|
+
userCtx,
|
|
14382
|
+
);
|
|
14383
|
+
if (adminVersionRepair.error) {
|
|
14384
|
+
json(res, adminVersionRepair.status || 400, { error: adminVersionRepair.error });
|
|
14385
|
+
return;
|
|
14386
|
+
}
|
|
14359
14387
|
const workflowScope = resolvePrdWorkflowScope(root, {
|
|
14360
14388
|
tapdId: workflow.id,
|
|
14361
14389
|
flowId,
|
|
@@ -14363,14 +14391,15 @@ export function startUiServer({
|
|
|
14363
14391
|
archived: url.searchParams.get("archived") === "1",
|
|
14364
14392
|
workspaceId: url.searchParams.get("workspaceId") || "",
|
|
14365
14393
|
workflowShare: url.searchParams.get("workflowShare") || "",
|
|
14366
|
-
}, userCtx);
|
|
14394
|
+
}, userCtx, adminVersionRepair.requested ? "admin-version-repair" : "read");
|
|
14367
14395
|
if (workflowScope.error) {
|
|
14368
14396
|
json(res, workflowScope.status || 400, { error: workflowScope.error });
|
|
14369
14397
|
return;
|
|
14370
14398
|
}
|
|
14371
14399
|
const scopedRoot = workflowScope.stateRoot;
|
|
14372
14400
|
prdWorkflowMigrateLegacyState(workflowScope.executionRoot, scopedRoot, workflow.id);
|
|
14373
|
-
const runtimeOnly =
|
|
14401
|
+
const runtimeOnly = adminVersionRepair.requested ||
|
|
14402
|
+
url.searchParams.get("runtimeOnly") === "1" ||
|
|
14374
14403
|
url.searchParams.get("runtime_only") === "1" ||
|
|
14375
14404
|
url.searchParams.get("cached") === "1";
|
|
14376
14405
|
const baseSnapshot = runtimeOnly
|
|
@@ -15606,7 +15635,7 @@ export function startUiServer({
|
|
|
15606
15635
|
return;
|
|
15607
15636
|
}
|
|
15608
15637
|
report = adminVersionRepair.requested
|
|
15609
|
-
? prdWorkflowMergeAdminVersionTimeline(report, currentSnapshot)
|
|
15638
|
+
? prdWorkflowMergeAdminVersionTimeline(report, currentSnapshot, adminVersionRepair)
|
|
15610
15639
|
: prdWorkflowMergeProducerTimeline(report, currentSnapshot);
|
|
15611
15640
|
if (report.error) {
|
|
15612
15641
|
json(res, 400, { error: report.error });
|
|
@@ -16066,7 +16095,7 @@ export function startUiServer({
|
|
|
16066
16095
|
artifacts: [artifact],
|
|
16067
16096
|
links: [{
|
|
16068
16097
|
key: artifactKey,
|
|
16069
|
-
label:
|
|
16098
|
+
label: artifact.label,
|
|
16070
16099
|
kind: artifact.kind,
|
|
16071
16100
|
url: displayUrl,
|
|
16072
16101
|
canonicalUrl: reviewUrl,
|
|
@@ -16652,6 +16681,40 @@ export function startUiServer({
|
|
|
16652
16681
|
}
|
|
16653
16682
|
}
|
|
16654
16683
|
|
|
16684
|
+
if (url.pathname === "/api/admin/users" || url.pathname === "/api/admin/users/reset-password") {
|
|
16685
|
+
if (!authUser?.isAdmin) {
|
|
16686
|
+
json(res, 403, { error: "Admin permission required" });
|
|
16687
|
+
return;
|
|
16688
|
+
}
|
|
16689
|
+
if (req.method === "GET" && url.pathname === "/api/admin/users") {
|
|
16690
|
+
json(res, 200, { users: listAuthUsers() });
|
|
16691
|
+
return;
|
|
16692
|
+
}
|
|
16693
|
+
if (req.method === "POST" && url.pathname === "/api/admin/users/reset-password") {
|
|
16694
|
+
let payload;
|
|
16695
|
+
try {
|
|
16696
|
+
payload = JSON.parse(await readBody(req));
|
|
16697
|
+
} catch {
|
|
16698
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
16699
|
+
return;
|
|
16700
|
+
}
|
|
16701
|
+
const targetUserId = String(payload?.userId || "").trim();
|
|
16702
|
+
if (targetUserId === userCtx.userId) {
|
|
16703
|
+
json(res, 400, { error: "不能在当前会话中重置自己的密码" });
|
|
16704
|
+
return;
|
|
16705
|
+
}
|
|
16706
|
+
const result = resetAuthUserPassword(targetUserId, payload?.password);
|
|
16707
|
+
if (!result.ok) {
|
|
16708
|
+
json(res, result.status || 400, { error: result.error || "Password reset failed" });
|
|
16709
|
+
return;
|
|
16710
|
+
}
|
|
16711
|
+
json(res, 200, result);
|
|
16712
|
+
return;
|
|
16713
|
+
}
|
|
16714
|
+
json(res, 405, { error: "Method not allowed" });
|
|
16715
|
+
return;
|
|
16716
|
+
}
|
|
16717
|
+
|
|
16655
16718
|
if (url.pathname === "/api/flows") {
|
|
16656
16719
|
if (req.method === "GET") {
|
|
16657
16720
|
try {
|