@fieldwangai/agentflow 0.1.42 → 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";
@@ -1125,6 +1130,7 @@ const WORKSPACE_TEXT_EXTS = new Set([
1125
1130
  ".mjs",
1126
1131
  ".cjs",
1127
1132
  ]);
1133
+ const WORKSPACE_IMAGE_EXTS = new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg"]);
1128
1134
 
1129
1135
  function resolveWorkspaceFilePath(workspaceRoot, relPath) {
1130
1136
  const root = path.resolve(workspaceRoot);
@@ -1144,9 +1150,33 @@ function workspaceFileIcon(fileName, isDir = false) {
1144
1150
  if ([".yaml", ".yml", ".json"].includes(ext)) return "data_object";
1145
1151
  if (ext === ".css") return "palette";
1146
1152
  if (ext === ".html") return "web";
1153
+ if (WORKSPACE_IMAGE_EXTS.has(ext)) return "image";
1147
1154
  return "draft";
1148
1155
  }
1149
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
+
1150
1180
  function readWorkspaceFilesRecursive(dir, root, depth = 0, maxDepth = 3, budget = { count: 0 }) {
1151
1181
  if (depth > maxDepth || budget.count > 500) return [];
1152
1182
  let entries;
@@ -1174,7 +1204,7 @@ function readWorkspaceFilesRecursive(dir, root, depth = 0, maxDepth = 3, budget
1174
1204
  } else if (entry.isFile()) {
1175
1205
  if (WORKSPACE_FILE_SKIP_FILES.has(entry.name)) continue;
1176
1206
  const ext = path.extname(entry.name).toLowerCase();
1177
- if (!WORKSPACE_TEXT_EXTS.has(ext)) continue;
1207
+ if (!WORKSPACE_TEXT_EXTS.has(ext) && !WORKSPACE_IMAGE_EXTS.has(ext)) continue;
1178
1208
  let size = 0;
1179
1209
  try { size = fs.statSync(abs).size; } catch {}
1180
1210
  budget.count++;
@@ -2655,6 +2685,47 @@ function parseFlowsImportForm(req) {
2655
2685
  });
2656
2686
  }
2657
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
+
2658
2729
  /** GET 读 flow / nodes / SSE 等 */
2659
2730
  function isValidFlowSourceRead(s) {
2660
2731
  return s === "builtin" || s === "admin" || s === "user" || s === "workspace";
@@ -3422,6 +3493,37 @@ export function startUiServer({
3422
3493
  return;
3423
3494
  }
3424
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
+
3425
3527
  if (req.method === "POST" && url.pathname === "/api/workspace/file") {
3426
3528
  let payload;
3427
3529
  try {
@@ -3458,6 +3560,54 @@ export function startUiServer({
3458
3560
  return;
3459
3561
  }
3460
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
+
3461
3611
  if (req.method === "POST" && url.pathname === "/api/workspace/folder") {
3462
3612
  let payload;
3463
3613
  try {