@fieldwangai/agentflow 0.1.147 → 0.1.153
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 +12 -0
- package/README.zh-CN.md +6 -0
- package/bin/lib/catalog-flows.mjs +4 -0
- package/bin/lib/flow-write.mjs +38 -0
- package/bin/lib/help.mjs +2 -2
- package/bin/lib/legacy-flow-execution.mjs +6 -0
- package/bin/lib/main.mjs +4 -0
- package/bin/lib/ui-server.mjs +169 -0
- package/bin/lib/workspace-preview.mjs +74 -0
- package/builtin/web-ui/dist/assets/{WorkflowAssistantThread-1vv8x7wy.js → WorkflowAssistantThread-B9xslwI0.js} +1 -1
- package/builtin/web-ui/dist/assets/{index-JETcIFk7.js → index-DmUV7ZCL.js} +112 -113
- package/builtin/web-ui/dist/index.html +1 -1
- package/package.json +1 -1
- package/skills/agentflow-author-flow/SKILL.md +8 -41
- package/skills/agentflow-cli/SKILL.md +38 -3
- package/skills/agentflow-cli/scripts/agentflow-cli.mjs +16 -0
- package/skills/agentflow-flow-add-instances/SKILL.md +7 -248
- package/skills/agentflow-flow-edit-node-fields/SKILL.md +4 -73
- package/skills/agentflow-flow-recipes/SKILL.md +4 -18
- package/skills/agentflow-flow-sync-ui/SKILL.md +3 -54
- package/skills/agentflow-node-reference/SKILL.md +4 -3
- package/skills/agentflow-placeholder-reference/SKILL.md +2 -2
- package/skills/agentflow-runtime-reference/SKILL.md +2 -2
- package/skills/agentflow-workspace-graph/SKILL.md +3 -3
package/README.md
CHANGED
|
@@ -24,6 +24,13 @@
|
|
|
24
24
|
|
|
25
25
|

|
|
26
26
|
|
|
27
|
+
### Terminology
|
|
28
|
+
|
|
29
|
+
In this repository, an executable node graph is called a **Flow** (also
|
|
30
|
+
shown as a Pipeline in the editor) and is stored in `flow.yaml`. A product
|
|
31
|
+
requirement **Workflow** is a separate TAPD-backed record addressed as
|
|
32
|
+
`tapd:<id>`; it is not created, archived, or updated by the Flow editor.
|
|
33
|
+
|
|
27
34
|
## The Problem
|
|
28
35
|
|
|
29
36
|
Coding agents like Cursor, Claude Code, and Codex are great — until the task gets long.
|
|
@@ -61,6 +68,11 @@ agentflow ui
|
|
|
61
68
|
# Generate and open a single-file static preview using the platform canvas (no local server)
|
|
62
69
|
agentflow flow preview ./my-flow/flow.yaml
|
|
63
70
|
|
|
71
|
+
# Upload a Workspace graph to a server-side temporary preview project
|
|
72
|
+
node skills/agentflow-cli/scripts/agentflow-cli.mjs workspace-preview \
|
|
73
|
+
--file .workspace/agentflow/pipelines/my-flow/workspace.graph.json \
|
|
74
|
+
--ttl-seconds 7200
|
|
75
|
+
|
|
64
76
|
# Or run a flow directly
|
|
65
77
|
agentflow apply <FlowName>
|
|
66
78
|
```
|
package/README.zh-CN.md
CHANGED
|
@@ -24,6 +24,12 @@
|
|
|
24
24
|
|
|
25
25
|

|
|
26
26
|
|
|
27
|
+
### 术语边界
|
|
28
|
+
|
|
29
|
+
本仓库中的可执行节点图称为 **Flow**(编辑器中也显示为流水线),以
|
|
30
|
+
`flow.yaml` 保存。产品需求 **Workflow** 是另一套由 TAPD 驱动、使用
|
|
31
|
+
`tapd:<id>` 标识的需求实体;它不由 Flow 编辑器创建、归档或修改。
|
|
32
|
+
|
|
27
33
|
## 解决什么问题
|
|
28
34
|
|
|
29
35
|
Cursor、Claude Code、Codex 这些 Coding Agent 很好用——直到任务变长。
|
|
@@ -21,6 +21,8 @@ import {
|
|
|
21
21
|
} from "./admin-builtin-pipelines.mjs";
|
|
22
22
|
import { Table } from "./table.mjs";
|
|
23
23
|
import { listMarketplaceNodes, parseMarketplaceDefinitionId, resolveMarketplaceNodePackage } from "./marketplace.mjs";
|
|
24
|
+
import { isWorkspacePreviewDir } from "./workspace-preview.mjs";
|
|
25
|
+
import { LEGACY_FLOW_NODE_IDS } from "./legacy-flow-execution.mjs";
|
|
24
26
|
|
|
25
27
|
/** 从指定目录收集含 flow.yaml 的子目录名。 */
|
|
26
28
|
export function collectPipelineNamesFromDir(dirPath) {
|
|
@@ -29,6 +31,7 @@ export function collectPipelineNamesFromDir(dirPath) {
|
|
|
29
31
|
return entries
|
|
30
32
|
.filter((e) => e.isDirectory())
|
|
31
33
|
.filter((e) => fs.existsSync(path.join(dirPath, e.name, "flow.yaml")))
|
|
34
|
+
.filter((e) => !isWorkspacePreviewDir(path.join(dirPath, e.name)))
|
|
32
35
|
.map((e) => e.name);
|
|
33
36
|
}
|
|
34
37
|
|
|
@@ -259,6 +262,7 @@ export function listNodesJson(workspaceRoot, flowId, flowSource, opts = {}) {
|
|
|
259
262
|
const files = fs.readdirSync(dir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith(".md"));
|
|
260
263
|
for (const e of files) {
|
|
261
264
|
const id = e.name.replace(/\.mdx?$/i, "").replace(/\.markdown$/i, "");
|
|
265
|
+
if (LEGACY_FLOW_NODE_IDS.has(id)) continue;
|
|
262
266
|
let type = "agent";
|
|
263
267
|
if (/^control/i.test(id)) type = "control";
|
|
264
268
|
else if (/^provide/i.test(id)) type = "provide";
|
package/bin/lib/flow-write.mjs
CHANGED
|
@@ -247,6 +247,44 @@ export function archiveFlowPipeline(workspaceRoot, flowId, flowSource, opts = {}
|
|
|
247
247
|
return { success: true };
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
+
/**
|
|
251
|
+
* 将归档流水线目录恢复到活动目录(仅 user / workspace)。
|
|
252
|
+
* @param {string} workspaceRoot
|
|
253
|
+
* @param {string} flowId
|
|
254
|
+
* @param {FlowWriteSource} flowSource
|
|
255
|
+
* @returns {{ success: true } | { success: false, error: string }}
|
|
256
|
+
*/
|
|
257
|
+
export function restoreArchivedFlowPipeline(workspaceRoot, flowId, flowSource, opts = {}) {
|
|
258
|
+
if (flowSource !== "user" && flowSource !== "workspace") {
|
|
259
|
+
return { success: false, error: "仅支持恢复用户目录或工作区流水线" };
|
|
260
|
+
}
|
|
261
|
+
if (!flowId || typeof flowId !== "string" || /[/\\.]/.test(flowId) || flowId === "..") {
|
|
262
|
+
return { success: false, error: "invalid flowId" };
|
|
263
|
+
}
|
|
264
|
+
const archivedRes = resolveArchivedFlowDirForWrite(workspaceRoot, flowId, flowSource, opts);
|
|
265
|
+
if (archivedRes.error || !archivedRes.flowDir) {
|
|
266
|
+
return { success: false, error: archivedRes.error || "无法解析归档路径" };
|
|
267
|
+
}
|
|
268
|
+
const fromDir = archivedRes.flowDir;
|
|
269
|
+
if (!fs.existsSync(path.join(fromDir, FLOW_YAML_FILENAME))) {
|
|
270
|
+
return { success: false, error: "找不到归档流水线" };
|
|
271
|
+
}
|
|
272
|
+
const activeRes = resolveFlowDirForWrite(workspaceRoot, flowId, flowSource, opts);
|
|
273
|
+
if (activeRes.error || !activeRes.flowDir) {
|
|
274
|
+
return { success: false, error: activeRes.error || "无法解析活动路径" };
|
|
275
|
+
}
|
|
276
|
+
if (fs.existsSync(activeRes.flowDir)) {
|
|
277
|
+
return { success: false, error: "活动目录已存在同名流水线" };
|
|
278
|
+
}
|
|
279
|
+
try {
|
|
280
|
+
fs.mkdirSync(path.dirname(activeRes.flowDir), { recursive: true });
|
|
281
|
+
fs.renameSync(fromDir, activeRes.flowDir);
|
|
282
|
+
return { success: true };
|
|
283
|
+
} catch (e) {
|
|
284
|
+
return { success: false, error: (e && e.message) || String(e) };
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
250
288
|
/**
|
|
251
289
|
* 在用户目录与工作区之间移动整个流水线目录(含 nodes 等)。
|
|
252
290
|
* @param {string} workspaceRoot
|
package/bin/lib/help.mjs
CHANGED
|
@@ -26,7 +26,7 @@ AgentFlow CLI — 使用 Cursor / OpenCode / Claude Code / Codex CLI 流式输
|
|
|
26
26
|
agentflow marketplace list [--json] 查看 workspace 本地节点市场
|
|
27
27
|
agentflow marketplace publish-node <dir> 发布本地节点包到 workspace market
|
|
28
28
|
agentflow marketplace install-node <FlowName> <nodeSpec> 将 market 节点依赖写入 flow
|
|
29
|
-
agentflow apply <FlowName> [uuid]
|
|
29
|
+
agentflow apply <FlowName> [uuid] 已下线;请改用 Workspace Run
|
|
30
30
|
agentflow validate <FlowName> [uuid] 校验流程;终端下输出易读结果,--json 或管道时输出 JSON;传 uuid 时写入 runDir/intermediate/validation.json
|
|
31
31
|
agentflow resume <FlowName> <uuid> [instanceId] 将 pending 与 failed 节点标为已确认并继续 apply
|
|
32
32
|
agentflow replay [flowName] <uuid> <instanceId>
|
|
@@ -97,7 +97,7 @@ Usage:
|
|
|
97
97
|
agentflow marketplace list [--json] Show workspace local marketplace packages
|
|
98
98
|
agentflow marketplace publish-node <dir> Publish a local node package to the workspace market
|
|
99
99
|
agentflow marketplace install-node <FlowName> <nodeSpec> Add a marketplace node dependency to a flow
|
|
100
|
-
agentflow apply <FlowName> [uuid]
|
|
100
|
+
agentflow apply <FlowName> [uuid] Retired; use Workspace Run instead
|
|
101
101
|
agentflow validate <FlowName> [uuid] Validate flow; readable output in terminal, JSON with --json or pipe; writes to runDir/intermediate/validation.json when uuid provided
|
|
102
102
|
agentflow resume <FlowName> <uuid> [instanceId] Mark pending and failed nodes as acknowledged and continue apply
|
|
103
103
|
agentflow replay [flowName] <uuid> <instanceId>
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export const LEGACY_FLOW_EXECUTION_DISABLED = true;
|
|
2
|
+
|
|
3
|
+
export const LEGACY_FLOW_EXECUTION_MESSAGE =
|
|
4
|
+
"Legacy Start/End Pipeline execution has been retired. Use the Workspace graph with Run or Scheduled Run.";
|
|
5
|
+
|
|
6
|
+
export const LEGACY_FLOW_NODE_IDS = new Set(["control_start", "control_end"]);
|
package/bin/lib/main.mjs
CHANGED
|
@@ -36,6 +36,7 @@ import { cancelScheduledRun, listScheduleStatuses, startScheduler } from "./sche
|
|
|
36
36
|
import { installFlowDependency, listMarketplacePackages, publishNodePackage } from "./marketplace.mjs";
|
|
37
37
|
import { startMcpServer } from "./mcp-server.mjs";
|
|
38
38
|
import { writeStaticFlowPreview } from "./flow-static-preview.mjs";
|
|
39
|
+
import { LEGACY_FLOW_EXECUTION_DISABLED, LEGACY_FLOW_EXECUTION_MESSAGE } from "./legacy-flow-execution.mjs";
|
|
39
40
|
|
|
40
41
|
async function readStdin() {
|
|
41
42
|
const chunks = [];
|
|
@@ -553,6 +554,7 @@ export async function main() {
|
|
|
553
554
|
if (sub === "list") {
|
|
554
555
|
listPipelines(workspaceRoot);
|
|
555
556
|
} else if (sub === "apply") {
|
|
557
|
+
if (LEGACY_FLOW_EXECUTION_DISABLED) throw new Error(LEGACY_FLOW_EXECUTION_MESSAGE);
|
|
556
558
|
const aiMode = argv[0] === "-ai" || argv[0] === "--ai";
|
|
557
559
|
if (aiMode) {
|
|
558
560
|
argv.shift();
|
|
@@ -584,12 +586,14 @@ export async function main() {
|
|
|
584
586
|
}
|
|
585
587
|
await apply(workspaceRoot, flowName, uuidArg, dryRun, agentModel, force, parallel, cliInputs);
|
|
586
588
|
} else if (sub === "resume") {
|
|
589
|
+
if (LEGACY_FLOW_EXECUTION_DISABLED) throw new Error(LEGACY_FLOW_EXECUTION_MESSAGE);
|
|
587
590
|
const flowName = shift();
|
|
588
591
|
const uuidArg = shift();
|
|
589
592
|
if (!flowName || !uuidArg) throw new Error("Usage: agentflow resume <FlowName> <uuid> [instanceId]");
|
|
590
593
|
const instanceIdOpt = argv.length > 0 && !argv[0].startsWith("--") ? shift() : undefined;
|
|
591
594
|
await resume(workspaceRoot, flowName, uuidArg, instanceIdOpt, agentModel, force, parallel);
|
|
592
595
|
} else if (sub === "replay") {
|
|
596
|
+
if (LEGACY_FLOW_EXECUTION_DISABLED) throw new Error(LEGACY_FLOW_EXECUTION_MESSAGE);
|
|
593
597
|
const a = shift(),
|
|
594
598
|
b = shift(),
|
|
595
599
|
c = shift();
|
package/bin/lib/ui-server.mjs
CHANGED
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
deleteFlowPipeline,
|
|
31
31
|
moveFlowDirectory,
|
|
32
32
|
resolveFlowDirForWrite,
|
|
33
|
+
restoreArchivedFlowPipeline,
|
|
33
34
|
validateUserPipelineId,
|
|
34
35
|
writeFlowYaml,
|
|
35
36
|
} from "./flow-write.mjs";
|
|
@@ -73,6 +74,16 @@ import {
|
|
|
73
74
|
writePipelineTree,
|
|
74
75
|
} from "./flow-import.mjs";
|
|
75
76
|
import { getWorkspaceTree, getPipelineFiles } from "./workspace-tree.mjs";
|
|
77
|
+
import {
|
|
78
|
+
DEFAULT_WORKSPACE_PREVIEW_TTL_MS,
|
|
79
|
+
createWorkspacePreviewId,
|
|
80
|
+
listExpiredWorkspacePreviews,
|
|
81
|
+
normalizeWorkspacePreviewTtlMs,
|
|
82
|
+
readWorkspacePreviewMetadata,
|
|
83
|
+
workspacePreviewFlowDir,
|
|
84
|
+
writeWorkspacePreviewMetadata,
|
|
85
|
+
} from "./workspace-preview.mjs";
|
|
86
|
+
import { LEGACY_FLOW_EXECUTION_DISABLED, LEGACY_FLOW_EXECUTION_MESSAGE } from "./legacy-flow-execution.mjs";
|
|
76
87
|
import {
|
|
77
88
|
createComposerSession,
|
|
78
89
|
logComposerEvent,
|
|
@@ -8236,6 +8247,23 @@ function isValidFlowSourceWrite(s) {
|
|
|
8236
8247
|
return s === "user" || s === "workspace";
|
|
8237
8248
|
}
|
|
8238
8249
|
|
|
8250
|
+
function cleanupExpiredWorkspacePreviews() {
|
|
8251
|
+
const roots = new Set(listAgentflowUserIds().map((id) => getUserPipelinesRoot(id)));
|
|
8252
|
+
roots.add(getUserPipelinesRoot(""));
|
|
8253
|
+
let removed = 0;
|
|
8254
|
+
for (const pipelinesRoot of roots) {
|
|
8255
|
+
for (const item of listExpiredWorkspacePreviews(pipelinesRoot)) {
|
|
8256
|
+
try {
|
|
8257
|
+
fs.rmSync(item.flowDir, { recursive: true, force: true });
|
|
8258
|
+
removed += 1;
|
|
8259
|
+
} catch (e) {
|
|
8260
|
+
log.debug(`[workspace-preview] cleanup failed: ${(e && e.message) || String(e)}`);
|
|
8261
|
+
}
|
|
8262
|
+
}
|
|
8263
|
+
}
|
|
8264
|
+
return removed;
|
|
8265
|
+
}
|
|
8266
|
+
|
|
8239
8267
|
/** Composer 打开的画布通过 SSE 订阅;POST /api/flow-editor-sync 向对应 flow 推送刷新 */
|
|
8240
8268
|
const flowEditorSyncSubscribers = new Map();
|
|
8241
8269
|
/** 每次 broadcastFlowEditorSync 时递增,供轮询端点 /api/flow-editor-sync-poll 使用 */
|
|
@@ -13577,6 +13605,16 @@ export function startUiServer({
|
|
|
13577
13605
|
isAdmin: Boolean(authUser.isAdmin),
|
|
13578
13606
|
adminOwnerId: String(url.searchParams.get("adminOwnerId") || "").trim(),
|
|
13579
13607
|
} : {};
|
|
13608
|
+
const legacyFlowManagementPath = new Set([
|
|
13609
|
+
"/api/flow/run-config",
|
|
13610
|
+
"/api/flow/schedule",
|
|
13611
|
+
"/api/flow/schedules",
|
|
13612
|
+
"/api/flow/schedule/disable",
|
|
13613
|
+
]);
|
|
13614
|
+
if (LEGACY_FLOW_EXECUTION_DISABLED && legacyFlowManagementPath.has(url.pathname)) {
|
|
13615
|
+
json(res, 410, { error: LEGACY_FLOW_EXECUTION_MESSAGE, code: "legacy_flow_execution_disabled" });
|
|
13616
|
+
return;
|
|
13617
|
+
}
|
|
13580
13618
|
if (req.method === "GET" && url.pathname.startsWith("/w/")) {
|
|
13581
13619
|
const parts = url.pathname.split("/").filter(Boolean);
|
|
13582
13620
|
if (parts.length !== 2) {
|
|
@@ -17367,6 +17405,77 @@ export function startUiServer({
|
|
|
17367
17405
|
return;
|
|
17368
17406
|
}
|
|
17369
17407
|
|
|
17408
|
+
if (req.method === "POST" && url.pathname === "/api/workspace/preview") {
|
|
17409
|
+
if (!authUser?.userId) {
|
|
17410
|
+
json(res, 401, { error: "Authentication required" });
|
|
17411
|
+
return;
|
|
17412
|
+
}
|
|
17413
|
+
let payload;
|
|
17414
|
+
try {
|
|
17415
|
+
payload = JSON.parse(await readBody(req, 4 * 1024 * 1024));
|
|
17416
|
+
} catch {
|
|
17417
|
+
json(res, 400, { error: "Invalid JSON" });
|
|
17418
|
+
return;
|
|
17419
|
+
}
|
|
17420
|
+
const graph = payload?.graph;
|
|
17421
|
+
if (!graph || typeof graph !== "object" || Array.isArray(graph)) {
|
|
17422
|
+
json(res, 400, { error: "graph must be an object" });
|
|
17423
|
+
return;
|
|
17424
|
+
}
|
|
17425
|
+
const instances = graph.instances && typeof graph.instances === "object" && !Array.isArray(graph.instances)
|
|
17426
|
+
? graph.instances
|
|
17427
|
+
: {};
|
|
17428
|
+
if (Object.values(instances).some((item) => String(item?.definitionId || "") === "workspace_scheduled_run")) {
|
|
17429
|
+
json(res, 400, { error: "Temporary Workspace preview cannot contain scheduled-run nodes" });
|
|
17430
|
+
return;
|
|
17431
|
+
}
|
|
17432
|
+
const rawRequestedId = String(payload.previewId || "").trim();
|
|
17433
|
+
const flowId = rawRequestedId || createWorkspacePreviewId();
|
|
17434
|
+
const flowDir = workspacePreviewFlowDir(flowId, authUser.userId);
|
|
17435
|
+
if (!flowDir) {
|
|
17436
|
+
json(res, 400, { error: "Invalid previewId" });
|
|
17437
|
+
return;
|
|
17438
|
+
}
|
|
17439
|
+
const existing = readWorkspacePreviewMetadata(flowDir);
|
|
17440
|
+
if (existing && existing.ownerId !== authUser.userId) {
|
|
17441
|
+
json(res, 403, { error: "Preview ownership denied" });
|
|
17442
|
+
return;
|
|
17443
|
+
}
|
|
17444
|
+
if (rawRequestedId && !existing && fs.existsSync(flowDir)) {
|
|
17445
|
+
json(res, 409, { error: "Preview project already exists but is not a preview" });
|
|
17446
|
+
return;
|
|
17447
|
+
}
|
|
17448
|
+
const now = Date.now();
|
|
17449
|
+
const ttlInput = payload.ttlMs != null
|
|
17450
|
+
? Number(payload.ttlMs)
|
|
17451
|
+
: payload.ttlSeconds != null
|
|
17452
|
+
? Number(payload.ttlSeconds) * 1000
|
|
17453
|
+
: DEFAULT_WORKSPACE_PREVIEW_TTL_MS;
|
|
17454
|
+
const ttlMs = normalizeWorkspacePreviewTtlMs(ttlInput);
|
|
17455
|
+
const metadata = {
|
|
17456
|
+
version: 1,
|
|
17457
|
+
flowId,
|
|
17458
|
+
ownerId: authUser.userId,
|
|
17459
|
+
title: String(payload.title || "Workspace Preview").trim().slice(0, 200),
|
|
17460
|
+
createdAt: existing?.createdAt || new Date(now).toISOString(),
|
|
17461
|
+
updatedAt: new Date(now).toISOString(),
|
|
17462
|
+
expiresAt: new Date(now + ttlMs).toISOString(),
|
|
17463
|
+
};
|
|
17464
|
+
try {
|
|
17465
|
+
fs.mkdirSync(flowDir, { recursive: true });
|
|
17466
|
+
fs.writeFileSync(path.join(flowDir, "flow.yaml"), "instances: {}\nedges: []\n", "utf8");
|
|
17467
|
+
fs.writeFileSync(path.join(flowDir, "workspace.graph.json"), `${JSON.stringify(graph, null, 2)}\n`, "utf8");
|
|
17468
|
+
writeWorkspacePreviewMetadata(flowDir, metadata);
|
|
17469
|
+
} catch (e) {
|
|
17470
|
+
json(res, 500, { error: (e && e.message) || String(e) });
|
|
17471
|
+
return;
|
|
17472
|
+
}
|
|
17473
|
+
const baseUrl = `${url.protocol}//${url.host}`;
|
|
17474
|
+
const workspaceUrl = `${baseUrl}/workspace?flowId=${encodeURIComponent(flowId)}&flowSource=user`;
|
|
17475
|
+
json(res, 200, { ok: true, flowId, flowSource: "user", preview: true, expiresAt: metadata.expiresAt, url: workspaceUrl });
|
|
17476
|
+
return;
|
|
17477
|
+
}
|
|
17478
|
+
|
|
17370
17479
|
if (req.method === "GET" && url.pathname === "/api/workspace/graph") {
|
|
17371
17480
|
try {
|
|
17372
17481
|
const scoped = resolveWorkspaceScopeRoot(root, {
|
|
@@ -20121,6 +20230,46 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
20121
20230
|
return;
|
|
20122
20231
|
}
|
|
20123
20232
|
|
|
20233
|
+
if (req.method === "POST" && url.pathname === "/api/flow/restore") {
|
|
20234
|
+
let payload;
|
|
20235
|
+
try {
|
|
20236
|
+
payload = JSON.parse(await readBody(req));
|
|
20237
|
+
} catch {
|
|
20238
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
20239
|
+
return;
|
|
20240
|
+
}
|
|
20241
|
+
const flowId = typeof payload.flowId === "string" ? payload.flowId.trim() : "";
|
|
20242
|
+
const flowSource = payload.flowSource || "user";
|
|
20243
|
+
if (!flowId) {
|
|
20244
|
+
json(res, 400, { error: "Missing or invalid flowId" });
|
|
20245
|
+
return;
|
|
20246
|
+
}
|
|
20247
|
+
if (flowSource !== "user" && flowSource !== "workspace") {
|
|
20248
|
+
json(res, 400, { error: "仅支持恢复用户目录或工作区流水线" });
|
|
20249
|
+
return;
|
|
20250
|
+
}
|
|
20251
|
+
const collaborationDenied = workspaceFlowCollaborationGuard(flowId, flowSource, true, userCtx, "owner");
|
|
20252
|
+
if (collaborationDenied) {
|
|
20253
|
+
json(res, collaborationDenied.status, { error: collaborationDenied.error });
|
|
20254
|
+
return;
|
|
20255
|
+
}
|
|
20256
|
+
const result = restoreArchivedFlowPipeline(root, flowId, flowSource, userCtx);
|
|
20257
|
+
if (!result.success) {
|
|
20258
|
+
json(res, 400, { error: result.error || "恢复失败" });
|
|
20259
|
+
return;
|
|
20260
|
+
}
|
|
20261
|
+
updateWorkspaceCollaborationFlow({
|
|
20262
|
+
previousFlowId: flowId,
|
|
20263
|
+
previousArchived: true,
|
|
20264
|
+
flowSource,
|
|
20265
|
+
ownerId: userCtx.userId,
|
|
20266
|
+
flowId,
|
|
20267
|
+
archived: false,
|
|
20268
|
+
});
|
|
20269
|
+
json(res, 200, { success: true, flowId, flowSource, archived: false });
|
|
20270
|
+
return;
|
|
20271
|
+
}
|
|
20272
|
+
|
|
20124
20273
|
if (req.method === "POST" && url.pathname === "/api/flow/delete") {
|
|
20125
20274
|
let payload;
|
|
20126
20275
|
try {
|
|
@@ -20442,6 +20591,10 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
20442
20591
|
}
|
|
20443
20592
|
|
|
20444
20593
|
if (req.method === "POST" && url.pathname === "/api/flow/run") {
|
|
20594
|
+
if (LEGACY_FLOW_EXECUTION_DISABLED) {
|
|
20595
|
+
json(res, 410, { error: LEGACY_FLOW_EXECUTION_MESSAGE, code: "legacy_flow_execution_disabled" });
|
|
20596
|
+
return;
|
|
20597
|
+
}
|
|
20445
20598
|
let payload;
|
|
20446
20599
|
try {
|
|
20447
20600
|
payload = JSON.parse(await readBody(req));
|
|
@@ -21170,6 +21323,22 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
21170
21323
|
}, 1000).unref?.();
|
|
21171
21324
|
}
|
|
21172
21325
|
|
|
21326
|
+
const workspacePreviewCleanupTimer = setInterval(() => {
|
|
21327
|
+
try {
|
|
21328
|
+
const removed = cleanupExpiredWorkspacePreviews();
|
|
21329
|
+
if (removed > 0) log.debug(`[workspace-preview] removed ${removed} expired preview project(s)`);
|
|
21330
|
+
} catch (e) {
|
|
21331
|
+
log.debug(`[workspace-preview] cleanup poll failed: ${(e && e.message) || String(e)}`);
|
|
21332
|
+
}
|
|
21333
|
+
}, 60_000);
|
|
21334
|
+
try {
|
|
21335
|
+
workspacePreviewCleanupTimer.unref?.();
|
|
21336
|
+
} catch (_) {}
|
|
21337
|
+
server.on("close", () => clearInterval(workspacePreviewCleanupTimer));
|
|
21338
|
+
try {
|
|
21339
|
+
cleanupExpiredWorkspacePreviews();
|
|
21340
|
+
} catch (_) {}
|
|
21341
|
+
|
|
21173
21342
|
return new Promise((resolve, reject) => {
|
|
21174
21343
|
server.once("error", reject);
|
|
21175
21344
|
server.listen(port, host, () => {
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import crypto from "crypto";
|
|
4
|
+
import { getUserPipelinesRoot } from "./paths.mjs";
|
|
5
|
+
|
|
6
|
+
export const WORKSPACE_PREVIEW_METADATA_FILENAME = ".agentflow-workspace-preview.json";
|
|
7
|
+
export const DEFAULT_WORKSPACE_PREVIEW_TTL_MS = 2 * 60 * 60 * 1000;
|
|
8
|
+
export const MAX_WORKSPACE_PREVIEW_TTL_MS = 24 * 60 * 60 * 1000;
|
|
9
|
+
|
|
10
|
+
function safePreviewId(value = "") {
|
|
11
|
+
const raw = String(value || "").trim();
|
|
12
|
+
return /^preview_[a-z0-9_-]{8,100}$/i.test(raw) ? raw : "";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function workspacePreviewMetadataPath(flowDir) {
|
|
16
|
+
return path.join(flowDir, WORKSPACE_PREVIEW_METADATA_FILENAME);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function readWorkspacePreviewMetadata(flowDir) {
|
|
20
|
+
try {
|
|
21
|
+
const filePath = workspacePreviewMetadataPath(flowDir);
|
|
22
|
+
if (!fs.existsSync(filePath)) return null;
|
|
23
|
+
const value = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
24
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
25
|
+
const flowId = safePreviewId(value.flowId);
|
|
26
|
+
const ownerId = String(value.ownerId || "").trim();
|
|
27
|
+
const expiresAt = String(value.expiresAt || "").trim();
|
|
28
|
+
if (!flowId || !ownerId || !Number.isFinite(Date.parse(expiresAt))) return null;
|
|
29
|
+
return { ...value, flowId, ownerId, expiresAt };
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function isWorkspacePreviewDir(flowDir) {
|
|
36
|
+
return Boolean(readWorkspacePreviewMetadata(flowDir));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function workspacePreviewFlowDir(flowId, userId = "") {
|
|
40
|
+
const safeId = safePreviewId(flowId);
|
|
41
|
+
if (!safeId) return "";
|
|
42
|
+
return path.join(getUserPipelinesRoot(userId), safeId);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function createWorkspacePreviewId() {
|
|
46
|
+
return `preview_${crypto.randomBytes(10).toString("hex")}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function normalizeWorkspacePreviewTtlMs(value) {
|
|
50
|
+
const requested = Number(value);
|
|
51
|
+
if (!Number.isFinite(requested) || requested <= 0) return DEFAULT_WORKSPACE_PREVIEW_TTL_MS;
|
|
52
|
+
return Math.min(MAX_WORKSPACE_PREVIEW_TTL_MS, Math.max(60_000, Math.round(requested)));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function writeWorkspacePreviewMetadata(flowDir, metadata) {
|
|
56
|
+
const filePath = workspacePreviewMetadataPath(flowDir);
|
|
57
|
+
const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
58
|
+
fs.writeFileSync(tempPath, `${JSON.stringify(metadata, null, 2)}\n`, "utf8");
|
|
59
|
+
fs.renameSync(tempPath, filePath);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function listExpiredWorkspacePreviews(userPipelinesRoot, now = Date.now()) {
|
|
63
|
+
if (!fs.existsSync(userPipelinesRoot)) return [];
|
|
64
|
+
const expired = [];
|
|
65
|
+
for (const entry of fs.readdirSync(userPipelinesRoot, { withFileTypes: true })) {
|
|
66
|
+
if (!entry.isDirectory()) continue;
|
|
67
|
+
const flowDir = path.join(userPipelinesRoot, entry.name);
|
|
68
|
+
const metadata = readWorkspacePreviewMetadata(flowDir);
|
|
69
|
+
if (!metadata) continue;
|
|
70
|
+
if (Date.parse(metadata.expiresAt) <= now) expired.push({ flowDir, metadata });
|
|
71
|
+
}
|
|
72
|
+
return expired;
|
|
73
|
+
}
|
|
74
|
+
|