@fieldwangai/agentflow 0.1.143 → 0.1.145
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 +52 -5
- package/builtin/web-ui/dist/assets/{WorkflowAssistantThread-BttvNPJr.js → WorkflowAssistantThread-C6uRyg9i.js} +1 -1
- package/builtin/web-ui/dist/assets/index-B1ETSKkD.js +889 -0
- package/builtin/web-ui/dist/assets/index-Bsx7D8Av.css +1 -0
- package/builtin/web-ui/dist/index.html +28 -3
- 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 +15 -2
- package/skills/agentflow-cli/scripts/agentflow-cli.mjs +120 -1
- package/skills/agentflow-workflow-report/SKILL.md +1 -0
- package/skills/agentflow-workflow-report/references/protocol.md +3 -0
- package/builtin/web-ui/dist/assets/index-CtG65J3e.css +0 -1
- package/builtin/web-ui/dist/assets/index-c8AUUsSF.js +0 -839
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);
|
|
@@ -11721,10 +11727,17 @@ function prdWorkflowAdminVersionRepairIntent(payload = {}, report = {}, userCtx
|
|
|
11721
11727
|
if (!report.expectedRevision) {
|
|
11722
11728
|
return { requested: true, status: 400, error: "Admin version repair requires expectedRevision" };
|
|
11723
11729
|
}
|
|
11724
|
-
|
|
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() };
|
|
11725
11738
|
}
|
|
11726
11739
|
|
|
11727
|
-
function prdWorkflowMergeAdminVersionTimeline(report, currentSnapshot = {}) {
|
|
11740
|
+
function prdWorkflowMergeAdminVersionTimeline(report, currentSnapshot = {}, adminIntent = {}) {
|
|
11728
11741
|
const source = prdWorkflowRuntimeEventProducer(report?.event || {});
|
|
11729
11742
|
const current = Array.isArray(currentSnapshot?.projections?.timeline) ? currentSnapshot.projections.timeline : [];
|
|
11730
11743
|
const incoming = Array.isArray(report?.projections?.timeline) ? report.projections.timeline : [];
|
|
@@ -11736,6 +11749,7 @@ function prdWorkflowMergeAdminVersionTimeline(report, currentSnapshot = {}) {
|
|
|
11736
11749
|
const administrativeRepair = {
|
|
11737
11750
|
kind: "version-attribution",
|
|
11738
11751
|
operation: "repair-version-membership",
|
|
11752
|
+
...(adminIntent.reason ? { reason: adminIntent.reason } : {}),
|
|
11739
11753
|
};
|
|
11740
11754
|
return {
|
|
11741
11755
|
...report,
|
|
@@ -13485,7 +13499,6 @@ export function startUiServer({
|
|
|
13485
13499
|
const root = path.resolve(workspaceRoot);
|
|
13486
13500
|
const uiPort = port;
|
|
13487
13501
|
const uiConfig = { hideCommunityLinks: Boolean(hideCommunityLinks) };
|
|
13488
|
-
|
|
13489
13502
|
const server = http.createServer(async (req, res) => {
|
|
13490
13503
|
const url = new URL(req.url || "/", "http://127.0.0.1");
|
|
13491
13504
|
const reqStart = Date.now();
|
|
@@ -15622,7 +15635,7 @@ export function startUiServer({
|
|
|
15622
15635
|
return;
|
|
15623
15636
|
}
|
|
15624
15637
|
report = adminVersionRepair.requested
|
|
15625
|
-
? prdWorkflowMergeAdminVersionTimeline(report, currentSnapshot)
|
|
15638
|
+
? prdWorkflowMergeAdminVersionTimeline(report, currentSnapshot, adminVersionRepair)
|
|
15626
15639
|
: prdWorkflowMergeProducerTimeline(report, currentSnapshot);
|
|
15627
15640
|
if (report.error) {
|
|
15628
15641
|
json(res, 400, { error: report.error });
|
|
@@ -16082,7 +16095,7 @@ export function startUiServer({
|
|
|
16082
16095
|
artifacts: [artifact],
|
|
16083
16096
|
links: [{
|
|
16084
16097
|
key: artifactKey,
|
|
16085
|
-
label:
|
|
16098
|
+
label: artifact.label,
|
|
16086
16099
|
kind: artifact.kind,
|
|
16087
16100
|
url: displayUrl,
|
|
16088
16101
|
canonicalUrl: reviewUrl,
|
|
@@ -16668,6 +16681,40 @@ export function startUiServer({
|
|
|
16668
16681
|
}
|
|
16669
16682
|
}
|
|
16670
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
|
+
|
|
16671
16718
|
if (url.pathname === "/api/flows") {
|
|
16672
16719
|
if (req.method === "GET") {
|
|
16673
16720
|
try {
|