@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.
@@ -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
- ".ico": "image/x-icon",
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 start = startMatch.index + startMatch[0].length;
1429
- const end = text.indexOf("}", start);
1430
- const block = end >= start ? text.slice(start, end) : text.slice(start);
1431
- const pairRe = /["']?([A-Za-z_][A-Za-z0-9_-]*)["']?\s*:\s*(?:"([^"]*)"|'([^']*)'|([^,\n\r}]+))/g;
1432
- let match;
1433
- while ((match = pairRe.exec(block))) {
1434
- const key = String(match[1] || "").trim();
1435
- const value = match[2] ?? match[3] ?? match[4] ?? "";
1436
- if (key) out[key] = workspaceUnescapeLooseJsonString(value).replace(/^["'`]|["'`]$/g, "").trim();
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 {