diff --git a/package-lock.json b/package-lock.json index ae17b21..5d1eb09 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@xyflow/react": "^12.12.0", "adm-zip": "^0.5.16", + "basic-ftp": "^6.2.3", "exceljs": "^4.4.0", "lucide-react": "^0.468.0", "nodemailer": "^6.9.16", @@ -3325,6 +3326,15 @@ "node": ">=6.0.0" } }, + "node_modules/basic-ftp": { + "version": "6.2.3", + "resolved": "https://registry.npmmirror.com/basic-ftp/-/basic-ftp-6.2.3.tgz", + "integrity": "sha512-T7GYtQ2eKJUN5YORG7A8u2WfB9JMN0a4/wl3MI64qq2qTRfa3FXzoNq0DDojZDkDyStcL+TkfKsz4JHEZx5kGQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/big-integer": { "version": "1.6.52", "resolved": "https://registry.npmmirror.com/big-integer/-/big-integer-1.6.52.tgz", diff --git a/package.json b/package.json index 1a0d096..538f3a5 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "dependencies": { "@xyflow/react": "^12.12.0", "adm-zip": "^0.5.16", + "basic-ftp": "^6.2.3", "exceljs": "^4.4.0", "lucide-react": "^0.468.0", "nodemailer": "^6.9.16", diff --git a/src/main/capabilities/ftp.ts b/src/main/capabilities/ftp.ts new file mode 100644 index 0000000..d707fcd --- /dev/null +++ b/src/main/capabilities/ftp.ts @@ -0,0 +1,127 @@ +import { Client } from "basic-ftp"; +import path from "node:path"; +import fs from "node:fs"; +import { register } from "../engine/registry"; +import { RuntimeError } from "../engine/types"; + +const flags = globalThis as { __rpaFtpClient?: Client }; + +async function requireClient(nodeId: string): Promise { + if (flags.__rpaFtpClient) return flags.__rpaFtpClient; + throw new RuntimeError("validation_error", "尚未连接 FTP,请先使用 FTP_Connect", { nodeId, componentId: nodeId, recommendation: "先运行 FTP_Connect 建立连接" }); +} + +function requireText(args: Record, keys: string[], label: string, nodeId: string): string { + for (const key of keys) { + const v = String(args[key] ?? "").trim(); + if (v) return v; + } + throw new RuntimeError("validation_error", label + "为空", { nodeId, componentId: nodeId }); +} + +register("FTP_Connect", async (ctx, node, args) => { + const host = requireText(args, ["address"], "FTP 地址", node.id); + const port = Number(args.port ?? 21) || 21; + const user = String(args.user ?? "anonymous"); + const password = String(args.pass_word ?? args.pass_word_file ?? "guest"); + const secure = String(args.ftp_type ?? "").toLowerCase().includes("ftps"); + const client = new Client(10_000); + await client.access({ host, port, secure, user, password }); + flags.__rpaFtpClient = client; + ctx.log("已连接 FTP:" + host + ":" + port, "info", node.id); + return { connected: true, host, port }; +}); + +register("FTP_Disconnect", async (ctx, node) => { + flags.__rpaFtpClient?.close(); + flags.__rpaFtpClient = undefined; + ctx.log("已断开 FTP 连接", "info", node.id); + return { disconnected: true }; +}); + +register("FTP_GetFileList", async (ctx, node, args) => { + const client = await requireClient(node.id); + const list = await client.list(""); + const entries = list.map(item => ({ name: item.name, size: item.size, isDirectory: item.isDirectory, modifiedAt: item.modifiedAt instanceof Date ? item.modifiedAt.toISOString() : String(item.modifiedAt ?? "") })); + ctx.log("已获取文件列表:" + entries.length + " 项", "info", node.id); + return entries; +}); + +register("FTP_Cwd", async (ctx, node, args) => { + const client = await requireClient(node.id); + const dir = requireText(args, ["dir"], "目标目录", node.id); + await client.cd(dir); + ctx.log("已切换工作目录:" + dir, "info", node.id); + return { cwd: dir }; +}); + +register("FTP_Download", async (ctx, node, args) => { + const client = await requireClient(node.id); + const remote = requireText(args, ["file_path"], "远程文件路径", node.id); + const localDir = String(args.folder_path ?? "").trim(); + const target = requireText(args, ["target_path"], "本地保存路径", node.id) || (localDir ? path.join(localDir, path.basename(remote)) : path.basename(remote)); + fs.mkdirSync(path.dirname(path.resolve(target)), { recursive: true }); + if (String(args.file_type ?? "0") === "1") { + fs.mkdirSync(path.resolve(target), { recursive: true }); + await client.ensureDir(remote); + await client.downloadToDir(target, remote); + } else { + await client.downloadTo(target, remote); + } + ctx.log("已下载到:" + target, "info", node.id); + return { downloaded: true, local: target }; +}); + +register("FTP_Upload", async (ctx, node, args) => { + const client = await requireClient(node.id); + const local = requireText(args, ["file_path"], "本地文件路径", node.id); + const remote = requireText(args, ["remote_path"], "远程目标路径", node.id); + if (String(args.file_type ?? "0") === "1" || fs.statSync(local).isDirectory()) { + await client.uploadFromDir(local, remote); + } else { + await client.uploadFrom(local, remote); + } + ctx.log("已上传:" + local + " → " + remote, "info", node.id); + return { uploaded: true, remote }; +}); + +register("FTP_Rename", async (ctx, node, args) => { + const client = await requireClient(node.id); + const from = requireText(args, ["file_path"], "原路径", node.id); + const to = requireText(args, ["toname"], "新名称", node.id); + const parent = path.posix.dirname(from); + await client.rename(from, parent === "." ? to : path.posix.join(parent, to)); + ctx.log("已重命名:" + from + " → " + to, "info", node.id); + return { renamed: true }; +}); + +register("FTP_Delete", async (ctx, node, args) => { + const client = await requireClient(node.id); + const remote = requireText(args, ["file_path"], "远程路径", node.id); + if (String(args.file_type ?? "0") === "1") { + await client.removeDir(remote); + } else { + await client.remove(remote); + } + ctx.log("已删除:" + remote, "info", node.id); + return { deleted: true }; +}); + +register("FTP_CreateDir", async (ctx, node, args) => { + const client = await requireClient(node.id); + const dir = requireText(args, ["dirname"], "目录名", node.id); + await client.ensureDir(dir); + client.cd(dir).catch(() => {}); + ctx.log("已创建目录:" + dir, "info", node.id); + return { created: true, dir }; +}); + +register("FTP_Exist", async (ctx, node, args) => { + const client = await requireClient(node.id); + const target = requireText(args, ["path"], "远程路径", node.id); + const parent = path.posix.dirname(target); + const name = path.posix.basename(target); + const list = await client.list(parent === "." ? "" : parent); + const exists = list.some(item => item.name === name); + return { exists, path: target }; +}); diff --git a/src/main/capabilities/index.ts b/src/main/capabilities/index.ts index fd1e084..2fa9c50 100644 --- a/src/main/capabilities/index.ts +++ b/src/main/capabilities/index.ts @@ -15,6 +15,7 @@ import "./web-crawl"; import "./human-dialog"; import "./time-caps"; import "./windows-system"; +import "./ftp"; import "./pdf"; import "./word"; import "./word-extended";