- 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 全部通过
130 lines
5.5 KiB
TypeScript
130 lines
5.5 KiB
TypeScript
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 };
|
||
});
|