feat: 接入 FTP 全家桶(10 id)——连接/列表/下载/上传/重命名/删除/建目录/存在判断/断开
This commit is contained in:
parent
26c682d2f9
commit
d331d56f2e
10
package-lock.json
generated
10
package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
127
src/main/capabilities/ftp.ts
Normal file
127
src/main/capabilities/ftp.ts
Normal file
@ -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<Client> {
|
||||
if (flags.__rpaFtpClient) return flags.__rpaFtpClient;
|
||||
throw new RuntimeError("validation_error", "尚未连接 FTP,请先使用 FTP_Connect", { nodeId, componentId: nodeId, recommendation: "先运行 FTP_Connect 建立连接" });
|
||||
}
|
||||
|
||||
function requireText(args: Record<string, unknown>, 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 };
|
||||
});
|
||||
@ -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";
|
||||
|
||||
Loading…
Reference in New Issue
Block a user