@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 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
- if (flowId && flowSource) {
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, "&amp;")
33
+ .replace(/</g, "&lt;")
34
+ .replace(/>/g, "&gt;")
35
+ .replace(/"/g, "&quot;");
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";
@@ -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 prdWorkflowAdminVersionRepairIntent(payload = {}, report = {}, userCtx = {}) {
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
- return { requested: true, operation };
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 = url.searchParams.get("runtimeOnly") === "1" ||
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: "Markdown Review",
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 {