Files
PRA-only-demo/src/main/capabilities/ftp.ts
T
yu bc0ed3fcd8 fix: 文件/FTP实测修复三处真Bug
- windows-system.ts: File_OpenFile 打开文件夹时 explorer.exe 成功也返回退出码1, 放行该退出码(实测验证)
- ftp.ts: FTP_CreateDir 的 cd() 改为 await, 消除 basic-ftp 并发冲突(launched a task while another one is still running, 实测验证)
- flowValidation.ts: flow.variables 空值防御, 避免旧流程JSON缺字段时渲染崩溃

实测: FileTest 32/32, FtpTest 10/10 全部通过
2026-10-30 10:40:05 +08:00

130 lines
5.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
// 创建后进入该目录;必须 await,否则下个节点的命令与本次 cd 并发,
// basic-ftp 会以 "task while another one is still running" 关闭客户端
await 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 };
});