@fieldwangai/agentflow 0.1.41 → 0.1.43
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/lib/ui-server.mjs +288 -11
- package/builtin/web-ui/dist/assets/index-BCquw8dA.js +218 -0
- package/builtin/web-ui/dist/assets/index-CXO3VpkR.css +1 -0
- package/builtin/web-ui/dist/index.html +2 -2
- package/package.json +1 -1
- package/skills/agentflow-workspace-chart/SKILL.md +102 -0
- package/skills/agentflow-workspace-graph/SKILL.md +4 -0
- package/skills/agentflow-workspace-html/SKILL.md +65 -0
- package/skills/agentflow-workspace-image/SKILL.md +52 -0
- package/skills/agentflow-workspace-table/SKILL.md +65 -0
- package/builtin/web-ui/dist/assets/index-DyBxNnlo.css +0 -1
- package/builtin/web-ui/dist/assets/index-TXmzGUhf.js +0 -218
package/bin/lib/ui-server.mjs
CHANGED
|
@@ -108,8 +108,13 @@ const MIME = {
|
|
|
108
108
|
".js": "text/javascript; charset=utf-8",
|
|
109
109
|
".css": "text/css; charset=utf-8",
|
|
110
110
|
".json": "application/json; charset=utf-8",
|
|
111
|
-
".
|
|
111
|
+
".png": "image/png",
|
|
112
|
+
".jpg": "image/jpeg",
|
|
113
|
+
".jpeg": "image/jpeg",
|
|
114
|
+
".gif": "image/gif",
|
|
115
|
+
".webp": "image/webp",
|
|
112
116
|
".svg": "image/svg+xml",
|
|
117
|
+
".ico": "image/x-icon",
|
|
113
118
|
};
|
|
114
119
|
|
|
115
120
|
const RUN_CONFIG_FILENAME = "run-config.json";
|
|
@@ -136,11 +141,35 @@ const BUILTIN_SKILL_COLLECTIONS = [
|
|
|
136
141
|
"agentflow-workspace-markdown",
|
|
137
142
|
"agentflow-workspace-mermaid",
|
|
138
143
|
"agentflow-workspace-ascii",
|
|
144
|
+
"agentflow-workspace-chart",
|
|
145
|
+
"agentflow-workspace-table",
|
|
146
|
+
"agentflow-workspace-html",
|
|
147
|
+
"agentflow-workspace-image",
|
|
139
148
|
"agentflow-node-reference",
|
|
140
149
|
"agentflow-placeholder-reference",
|
|
141
150
|
"agentflow-runtime-reference",
|
|
142
151
|
],
|
|
143
152
|
legacyDefaultKeys: [
|
|
153
|
+
[
|
|
154
|
+
"agentflow-workspace-graph",
|
|
155
|
+
"agentflow-workspace-markdown",
|
|
156
|
+
"agentflow-workspace-mermaid",
|
|
157
|
+
"agentflow-workspace-ascii",
|
|
158
|
+
"agentflow-workspace-chart",
|
|
159
|
+
"agentflow-workspace-table",
|
|
160
|
+
"agentflow-node-reference",
|
|
161
|
+
"agentflow-placeholder-reference",
|
|
162
|
+
"agentflow-runtime-reference",
|
|
163
|
+
],
|
|
164
|
+
[
|
|
165
|
+
"agentflow-workspace-graph",
|
|
166
|
+
"agentflow-workspace-markdown",
|
|
167
|
+
"agentflow-workspace-mermaid",
|
|
168
|
+
"agentflow-workspace-ascii",
|
|
169
|
+
"agentflow-node-reference",
|
|
170
|
+
"agentflow-placeholder-reference",
|
|
171
|
+
"agentflow-runtime-reference",
|
|
172
|
+
],
|
|
144
173
|
[
|
|
145
174
|
"agentflow-flow-add-instances",
|
|
146
175
|
"agentflow-flow-edit-node-fields",
|
|
@@ -1101,6 +1130,7 @@ const WORKSPACE_TEXT_EXTS = new Set([
|
|
|
1101
1130
|
".mjs",
|
|
1102
1131
|
".cjs",
|
|
1103
1132
|
]);
|
|
1133
|
+
const WORKSPACE_IMAGE_EXTS = new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg"]);
|
|
1104
1134
|
|
|
1105
1135
|
function resolveWorkspaceFilePath(workspaceRoot, relPath) {
|
|
1106
1136
|
const root = path.resolve(workspaceRoot);
|
|
@@ -1120,9 +1150,33 @@ function workspaceFileIcon(fileName, isDir = false) {
|
|
|
1120
1150
|
if ([".yaml", ".yml", ".json"].includes(ext)) return "data_object";
|
|
1121
1151
|
if (ext === ".css") return "palette";
|
|
1122
1152
|
if (ext === ".html") return "web";
|
|
1153
|
+
if (WORKSPACE_IMAGE_EXTS.has(ext)) return "image";
|
|
1123
1154
|
return "draft";
|
|
1124
1155
|
}
|
|
1125
1156
|
|
|
1157
|
+
function sanitizeWorkspaceUploadName(filename) {
|
|
1158
|
+
const parsed = path.parse(String(filename || "image").replace(/\\/g, "/").split("/").pop() || "image");
|
|
1159
|
+
const stem = (parsed.name || "image")
|
|
1160
|
+
.trim()
|
|
1161
|
+
.replace(/[^a-zA-Z0-9._-]+/g, "-")
|
|
1162
|
+
.replace(/^-+|-+$/g, "")
|
|
1163
|
+
.slice(0, 80) || "image";
|
|
1164
|
+
const ext = String(parsed.ext || "").toLowerCase();
|
|
1165
|
+
return `${stem}${WORKSPACE_IMAGE_EXTS.has(ext) ? ext : ".png"}`;
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
function uniqueWorkspaceRelPath(workspaceRoot, relPath) {
|
|
1169
|
+
let { abs, rel } = resolveWorkspaceFilePath(workspaceRoot, relPath);
|
|
1170
|
+
if (!fs.existsSync(abs)) return { abs, rel };
|
|
1171
|
+
const parsed = path.parse(rel);
|
|
1172
|
+
for (let i = 1; i < 1000; i += 1) {
|
|
1173
|
+
const candidate = path.posix.join(parsed.dir, `${parsed.name}-${i}${parsed.ext}`);
|
|
1174
|
+
const resolved = resolveWorkspaceFilePath(workspaceRoot, candidate);
|
|
1175
|
+
if (!fs.existsSync(resolved.abs)) return resolved;
|
|
1176
|
+
}
|
|
1177
|
+
return { abs, rel };
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1126
1180
|
function readWorkspaceFilesRecursive(dir, root, depth = 0, maxDepth = 3, budget = { count: 0 }) {
|
|
1127
1181
|
if (depth > maxDepth || budget.count > 500) return [];
|
|
1128
1182
|
let entries;
|
|
@@ -1150,7 +1204,7 @@ function readWorkspaceFilesRecursive(dir, root, depth = 0, maxDepth = 3, budget
|
|
|
1150
1204
|
} else if (entry.isFile()) {
|
|
1151
1205
|
if (WORKSPACE_FILE_SKIP_FILES.has(entry.name)) continue;
|
|
1152
1206
|
const ext = path.extname(entry.name).toLowerCase();
|
|
1153
|
-
if (!WORKSPACE_TEXT_EXTS.has(ext)) continue;
|
|
1207
|
+
if (!WORKSPACE_TEXT_EXTS.has(ext) && !WORKSPACE_IMAGE_EXTS.has(ext)) continue;
|
|
1154
1208
|
let size = 0;
|
|
1155
1209
|
try { size = fs.statSync(abs).size; } catch {}
|
|
1156
1210
|
budget.count++;
|
|
@@ -1420,20 +1474,123 @@ function workspaceUnescapeLooseJsonString(value) {
|
|
|
1420
1474
|
.trim();
|
|
1421
1475
|
}
|
|
1422
1476
|
|
|
1477
|
+
function workspaceFindMatchingDelimiter(text, openIndex, openChar = "{", closeChar = "}") {
|
|
1478
|
+
const raw = String(text || "");
|
|
1479
|
+
if (raw[openIndex] !== openChar) return -1;
|
|
1480
|
+
let depth = 0;
|
|
1481
|
+
let quote = "";
|
|
1482
|
+
let escaped = false;
|
|
1483
|
+
for (let i = openIndex; i < raw.length; i += 1) {
|
|
1484
|
+
const ch = raw[i];
|
|
1485
|
+
if (quote) {
|
|
1486
|
+
if (escaped) {
|
|
1487
|
+
escaped = false;
|
|
1488
|
+
} else if (ch === "\\") {
|
|
1489
|
+
escaped = true;
|
|
1490
|
+
} else if (ch === quote) {
|
|
1491
|
+
quote = "";
|
|
1492
|
+
}
|
|
1493
|
+
continue;
|
|
1494
|
+
}
|
|
1495
|
+
if (ch === '"' || ch === "'") {
|
|
1496
|
+
quote = ch;
|
|
1497
|
+
continue;
|
|
1498
|
+
}
|
|
1499
|
+
if (ch === openChar) depth += 1;
|
|
1500
|
+
if (ch === closeChar) {
|
|
1501
|
+
depth -= 1;
|
|
1502
|
+
if (depth === 0) return i;
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
return -1;
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
function workspaceParseLooseJsonValue(text, startIndex, limitIndex = String(text || "").length) {
|
|
1509
|
+
const raw = String(text || "");
|
|
1510
|
+
let i = startIndex;
|
|
1511
|
+
while (i < limitIndex && /\s/.test(raw[i])) i += 1;
|
|
1512
|
+
if (i >= limitIndex) return { value: "", end: i };
|
|
1513
|
+
const ch = raw[i];
|
|
1514
|
+
if (ch === "{" || ch === "[") {
|
|
1515
|
+
const close = workspaceFindMatchingDelimiter(raw, i, ch, ch === "{" ? "}" : "]");
|
|
1516
|
+
const end = close >= 0 ? close + 1 : limitIndex;
|
|
1517
|
+
const slice = raw.slice(i, end).trim();
|
|
1518
|
+
try {
|
|
1519
|
+
return { value: workspaceStringifyOutputValue(JSON.parse(slice)), end };
|
|
1520
|
+
} catch {
|
|
1521
|
+
return { value: slice, end };
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
if (ch === '"' || ch === "'") {
|
|
1525
|
+
const quote = ch;
|
|
1526
|
+
let escaped = false;
|
|
1527
|
+
let end = i + 1;
|
|
1528
|
+
for (; end < limitIndex; end += 1) {
|
|
1529
|
+
const c = raw[end];
|
|
1530
|
+
if (escaped) {
|
|
1531
|
+
escaped = false;
|
|
1532
|
+
} else if (c === "\\") {
|
|
1533
|
+
escaped = true;
|
|
1534
|
+
} else if (c === quote) {
|
|
1535
|
+
break;
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
const body = raw.slice(i + 1, end < limitIndex ? end : limitIndex);
|
|
1539
|
+
return { value: workspaceUnescapeLooseJsonString(body), end: Math.min(end + 1, limitIndex) };
|
|
1540
|
+
}
|
|
1541
|
+
let end = i;
|
|
1542
|
+
while (end < limitIndex && raw[end] !== "," && raw[end] !== "\n" && raw[end] !== "\r" && raw[end] !== "}") end += 1;
|
|
1543
|
+
const slice = raw.slice(i, end).trim().replace(/^["'`]|["'`]$/g, "");
|
|
1544
|
+
return { value: workspaceUnescapeLooseJsonString(slice), end };
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1423
1547
|
function workspaceExtractLooseOutParams(raw) {
|
|
1424
1548
|
const text = String(raw || "");
|
|
1425
1549
|
const out = {};
|
|
1426
1550
|
const startMatch = /["']outParams["']\s*:\s*\{/i.exec(text);
|
|
1427
1551
|
if (!startMatch) return out;
|
|
1428
|
-
const
|
|
1429
|
-
const
|
|
1430
|
-
const
|
|
1431
|
-
const
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1552
|
+
const openIndex = text.indexOf("{", startMatch.index);
|
|
1553
|
+
const closeIndex = workspaceFindMatchingDelimiter(text, openIndex);
|
|
1554
|
+
const endLimit = closeIndex >= 0 ? closeIndex : text.length;
|
|
1555
|
+
const block = text.slice(openIndex, closeIndex >= 0 ? closeIndex + 1 : text.length);
|
|
1556
|
+
try {
|
|
1557
|
+
const parsed = JSON.parse(block);
|
|
1558
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
1559
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
1560
|
+
const name = String(key || "").trim();
|
|
1561
|
+
if (name) out[name] = workspaceStringifyOutputValue(value);
|
|
1562
|
+
}
|
|
1563
|
+
return out;
|
|
1564
|
+
}
|
|
1565
|
+
} catch {
|
|
1566
|
+
/* fall through to loose top-level scanning */
|
|
1567
|
+
}
|
|
1568
|
+
let i = openIndex + 1;
|
|
1569
|
+
while (i < endLimit) {
|
|
1570
|
+
while (i < endLimit && /[\s,]/.test(text[i])) i += 1;
|
|
1571
|
+
if (i >= endLimit) break;
|
|
1572
|
+
let key = "";
|
|
1573
|
+
if (text[i] === '"' || text[i] === "'") {
|
|
1574
|
+
const quote = text[i];
|
|
1575
|
+
const keyStart = i + 1;
|
|
1576
|
+
i = keyStart;
|
|
1577
|
+
while (i < endLimit && text[i] !== quote) i += 1;
|
|
1578
|
+
key = text.slice(keyStart, i).trim();
|
|
1579
|
+
i += 1;
|
|
1580
|
+
} else {
|
|
1581
|
+
const keyStart = i;
|
|
1582
|
+
while (i < endLimit && /[A-Za-z0-9_-]/.test(text[i])) i += 1;
|
|
1583
|
+
key = text.slice(keyStart, i).trim();
|
|
1584
|
+
}
|
|
1585
|
+
while (i < endLimit && /\s/.test(text[i])) i += 1;
|
|
1586
|
+
if (text[i] !== ":") {
|
|
1587
|
+
i += 1;
|
|
1588
|
+
continue;
|
|
1589
|
+
}
|
|
1590
|
+
i += 1;
|
|
1591
|
+
const parsedValue = workspaceParseLooseJsonValue(text, i, endLimit);
|
|
1592
|
+
if (key) out[key] = String(parsedValue.value ?? "").trim();
|
|
1593
|
+
i = parsedValue.end;
|
|
1437
1594
|
}
|
|
1438
1595
|
return out;
|
|
1439
1596
|
}
|
|
@@ -2528,6 +2685,47 @@ function parseFlowsImportForm(req) {
|
|
|
2528
2685
|
});
|
|
2529
2686
|
}
|
|
2530
2687
|
|
|
2688
|
+
function parseWorkspaceUploadForm(req) {
|
|
2689
|
+
return new Promise((resolve, reject) => {
|
|
2690
|
+
const bb = busboy({
|
|
2691
|
+
headers: req.headers,
|
|
2692
|
+
limits: { files: 1, fileSize: 10 * 1024 * 1024, parts: 32 },
|
|
2693
|
+
});
|
|
2694
|
+
const fields = {};
|
|
2695
|
+
const chunks = [];
|
|
2696
|
+
let filename = "";
|
|
2697
|
+
let mimeType = "";
|
|
2698
|
+
let gotFile = false;
|
|
2699
|
+
bb.on("field", (name, val) => {
|
|
2700
|
+
fields[String(name || "")] = String(val || "");
|
|
2701
|
+
});
|
|
2702
|
+
bb.on("file", (name, file, info) => {
|
|
2703
|
+
if (name !== "file") {
|
|
2704
|
+
file.resume();
|
|
2705
|
+
return;
|
|
2706
|
+
}
|
|
2707
|
+
gotFile = true;
|
|
2708
|
+
filename = info.filename || "";
|
|
2709
|
+
mimeType = info.mimeType || "";
|
|
2710
|
+
file.on("data", (d) => chunks.push(d));
|
|
2711
|
+
file.on("limit", () => {
|
|
2712
|
+
reject(new Error("FILE_TOO_LARGE"));
|
|
2713
|
+
});
|
|
2714
|
+
});
|
|
2715
|
+
bb.on("finish", () => {
|
|
2716
|
+
resolve({
|
|
2717
|
+
fields,
|
|
2718
|
+
file: Buffer.concat(chunks),
|
|
2719
|
+
filename,
|
|
2720
|
+
mimeType,
|
|
2721
|
+
gotFile,
|
|
2722
|
+
});
|
|
2723
|
+
});
|
|
2724
|
+
bb.on("error", reject);
|
|
2725
|
+
req.pipe(bb);
|
|
2726
|
+
});
|
|
2727
|
+
}
|
|
2728
|
+
|
|
2531
2729
|
/** GET 读 flow / nodes / SSE 等 */
|
|
2532
2730
|
function isValidFlowSourceRead(s) {
|
|
2533
2731
|
return s === "builtin" || s === "admin" || s === "user" || s === "workspace";
|
|
@@ -3295,6 +3493,37 @@ export function startUiServer({
|
|
|
3295
3493
|
return;
|
|
3296
3494
|
}
|
|
3297
3495
|
|
|
3496
|
+
if (req.method === "GET" && url.pathname === "/api/workspace/file/raw") {
|
|
3497
|
+
try {
|
|
3498
|
+
const scoped = resolveWorkspaceScopeRoot(root, {
|
|
3499
|
+
flowId: url.searchParams.get("flowId") || "",
|
|
3500
|
+
flowSource: url.searchParams.get("flowSource") || "user",
|
|
3501
|
+
archived: url.searchParams.get("archived") === "1",
|
|
3502
|
+
}, userCtx);
|
|
3503
|
+
if (scoped.error) {
|
|
3504
|
+
json(res, 400, { error: scoped.error });
|
|
3505
|
+
return;
|
|
3506
|
+
}
|
|
3507
|
+
const { abs } = resolveWorkspaceFilePath(scoped.root, url.searchParams.get("path") || "");
|
|
3508
|
+
if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) {
|
|
3509
|
+
json(res, 404, { error: "File not found" });
|
|
3510
|
+
return;
|
|
3511
|
+
}
|
|
3512
|
+
const ext = path.extname(abs).toLowerCase();
|
|
3513
|
+
const type = MIME[ext] || "application/octet-stream";
|
|
3514
|
+
const data = fs.readFileSync(abs);
|
|
3515
|
+
res.writeHead(200, {
|
|
3516
|
+
"Content-Type": type,
|
|
3517
|
+
"Content-Length": data.length,
|
|
3518
|
+
"Cache-Control": "no-store",
|
|
3519
|
+
});
|
|
3520
|
+
res.end(data);
|
|
3521
|
+
} catch (e) {
|
|
3522
|
+
json(res, /traversal/i.test(String(e.message || e)) ? 403 : 500, { error: (e && e.message) || String(e) });
|
|
3523
|
+
}
|
|
3524
|
+
return;
|
|
3525
|
+
}
|
|
3526
|
+
|
|
3298
3527
|
if (req.method === "POST" && url.pathname === "/api/workspace/file") {
|
|
3299
3528
|
let payload;
|
|
3300
3529
|
try {
|
|
@@ -3331,6 +3560,54 @@ export function startUiServer({
|
|
|
3331
3560
|
return;
|
|
3332
3561
|
}
|
|
3333
3562
|
|
|
3563
|
+
if (req.method === "POST" && url.pathname === "/api/workspace/upload") {
|
|
3564
|
+
let parsed;
|
|
3565
|
+
try {
|
|
3566
|
+
parsed = await parseWorkspaceUploadForm(req);
|
|
3567
|
+
} catch (e) {
|
|
3568
|
+
json(res, /FILE_TOO_LARGE/.test(String(e.message || e)) ? 413 : 400, { error: (e && e.message) || String(e) });
|
|
3569
|
+
return;
|
|
3570
|
+
}
|
|
3571
|
+
try {
|
|
3572
|
+
if (!parsed.gotFile || !parsed.file.length) {
|
|
3573
|
+
json(res, 400, { error: "Missing upload file" });
|
|
3574
|
+
return;
|
|
3575
|
+
}
|
|
3576
|
+
const scoped = resolveWorkspaceScopeRoot(root, {
|
|
3577
|
+
flowId: parsed.fields.flowId || "",
|
|
3578
|
+
flowSource: parsed.fields.flowSource || "user",
|
|
3579
|
+
archived: parsed.fields.archived === "1" || parsed.fields.archived === "true" || parsed.fields.flowArchived === "true",
|
|
3580
|
+
}, userCtx);
|
|
3581
|
+
if (scoped.error) {
|
|
3582
|
+
json(res, 400, { error: scoped.error });
|
|
3583
|
+
return;
|
|
3584
|
+
}
|
|
3585
|
+
if (scoped.archived || isReadonlyBuiltinFlowSource(scoped.flowSource)) {
|
|
3586
|
+
json(res, 400, { error: "Cannot write to builtin or archived pipeline workspace" });
|
|
3587
|
+
return;
|
|
3588
|
+
}
|
|
3589
|
+
const safeName = sanitizeWorkspaceUploadName(parsed.filename);
|
|
3590
|
+
const ext = path.extname(safeName).toLowerCase();
|
|
3591
|
+
if (!WORKSPACE_IMAGE_EXTS.has(ext) || (parsed.mimeType && !/^image\//i.test(parsed.mimeType))) {
|
|
3592
|
+
json(res, 400, { error: "Only image uploads are supported" });
|
|
3593
|
+
return;
|
|
3594
|
+
}
|
|
3595
|
+
const targetDir = String(parsed.fields.dir || "img").trim().replace(/^[/\\]+/, "") || "img";
|
|
3596
|
+
const target = uniqueWorkspaceRelPath(scoped.root, path.posix.join(targetDir.replace(/\\/g, "/"), safeName));
|
|
3597
|
+
fs.mkdirSync(path.dirname(target.abs), { recursive: true });
|
|
3598
|
+
fs.writeFileSync(target.abs, parsed.file);
|
|
3599
|
+
json(res, 200, {
|
|
3600
|
+
ok: true,
|
|
3601
|
+
path: target.rel,
|
|
3602
|
+
size: parsed.file.length,
|
|
3603
|
+
mimeType: parsed.mimeType,
|
|
3604
|
+
});
|
|
3605
|
+
} catch (e) {
|
|
3606
|
+
json(res, /traversal/i.test(String(e.message || e)) ? 403 : 500, { error: (e && e.message) || String(e) });
|
|
3607
|
+
}
|
|
3608
|
+
return;
|
|
3609
|
+
}
|
|
3610
|
+
|
|
3334
3611
|
if (req.method === "POST" && url.pathname === "/api/workspace/folder") {
|
|
3335
3612
|
let payload;
|
|
3336
3613
|
try {
|