feat: 接入本地执行内核与运行面板

这个提交包含在:
yu 于 2026-10-26 17:41:39 +08:00 提交
1 父节点 32ca0a1af8
当前提交 6a2cc24898
23 个文件已更改
+2937 -41

没有与搜索条件相匹配的文件

+134
查看文件
@@ -0,0 +1,134 @@
import { register } from "../engine/registry";
import { RuntimeError } from "../engine/types";
import { browserSession } from "../browser/session";
import type { BrowserLike, RunContext } from "../engine/types";
async function session(ctx: RunContext): Promise<BrowserLike> {
if (!ctx.browser) ctx.browser = await browserSession();
return ctx.browser;
}
function selectorOf(args: Record<string, unknown>): unknown {
const selector = args.uiSelector ?? args.selector ?? args.target ?? args.uiWindowSelector;
if (selector === undefined || selector === null || selector === "") {
throw new RuntimeError("validation_error", "元素选择器为空", {
recommendation: "填写 css:#id、text:提交、aria:button=提交 或使用拾取器"
});
}
return selector;
}
register(["WebBrowser_CreateV3", "WebBrowser_Create", "WebBrowser_CreateBackground"], async (ctx, node, args) => {
const browser = await session(ctx);
const url = String(args.url ?? "");
if (url) {
const result = await browser.navigate(url);
ctx.log(`已打开 ${result.url}`, "info", node.id);
return result;
}
return { url: await browser.currentUrl(), title: "" };
});
register(["WebBrowser_GoUrl", "WebBrowser_Navigate"], async (ctx, node, args) => {
const browser = await session(ctx);
const url = String(args.url ?? "");
if (!url) {
throw new RuntimeError("validation_error", "跳转地址为空", {
nodeId: node.id,
componentId: node.componentId,
recommendation: "填写要访问的 URL"
});
}
const result = await browser.navigate(url);
ctx.log(`已跳转 ${result.url}`, "info", node.id);
return result;
});
register("WebBrowser_Switch", async (ctx, node) => {
await session(ctx);
ctx.log("网页切换已由统一运行浏览器接管", "info", node.id);
return true;
});
register("WebBrowser_Close", async (ctx) => {
if (ctx.browser) await ctx.browser.close();
ctx.browser = undefined;
return true;
});
register(["Element_Click_V6", "Element_Click_V3", "Element_Click"], async (ctx, node, args) => {
const browser = await session(ctx);
const result = await browser.click(selectorOf(args));
ctx.log(`已点击 ${(result as { selector?: string }).selector || ""}`, "info", node.id);
return result;
});
register(["Element_SetValue_V6", "Element_SetValue_V4", "Element_SetValue"], async (ctx, node, args) => {
const browser = await session(ctx);
const text = String(args.text ?? args.value ?? "");
const result = await browser.type(selectorOf(args), text, true);
ctx.log(`已输入 ${text.length > 40 ? `${text.slice(0, 40)}…` : text}`, "info", node.id);
if (String(args.isEnter ?? "0") === "1" || String(args.isEnter) === "true") {
await browser.runJs("document.activeElement && document.activeElement.blur()");
}
return result;
});
register(["Element_GetText_V5", "Element_GetText"], async (ctx, node, args) => {
const browser = await session(ctx);
const attrType = String(args.attr_type ?? "");
const attrName = String(args.attr_name ?? "");
const attribute = attrType && attrType !== "default" ? attrName || attrType : undefined;
const text = await browser.getText(selectorOf(args), attribute);
ctx.log(`读取到 ${text.length > 60 ? `${text.slice(0, 60)}…` : text}`, "info", node.id);
return text;
});
register(["Element_Exists", "Element_ElementExists"], async (ctx, node, args) => {
const browser = await session(ctx);
const exists = await browser.exists(selectorOf(args));
ctx.log(exists ? "元素存在" : "元素不存在", "info", node.id);
return exists;
});
register(["Element_SelectOperation_v1", "Element_SelectOperation"], async (ctx, node, args) => {
const browser = await session(ctx);
const value = String(args.itemsToSelect ?? args.choose ?? args.value ?? "");
const result = await browser.select(selectorOf(args), value);
ctx.log(`已选择 ${value}`, "info", node.id);
return result;
});
register(["WebBrowser_RunJs", "WebBrowser_RunJsInIeByTitle"], async (ctx, node, args) => {
const browser = await session(ctx);
const script = String(args.jsCode ?? args.script ?? args.code ?? "");
if (!script) throw new RuntimeError("validation_error", "JS 代码为空", { nodeId: node.id, componentId: node.componentId });
return browser.runJs(script);
});
register("WebBrowser_Screenshot", async (ctx, node, args) => {
const browser = await session(ctx);
const target = String(args.file_path ?? args.image_path ?? `${ctx.evidenceDir}/${node.id}.png`);
const file = await browser.screenshot(target);
ctx.log(`截图已保存 ${file}`, "info", node.id);
return file;
});
register("Image_Screenshot", async (ctx, node, args) => {
const { desktopCapturer, screen } = await import("electron");
const folder = String(args.outImagePath ?? args.image_path ?? ctx.evidenceDir);
const name = String(args.outImageName ?? `${node.id}.png`);
const { promises: fs } = await import("node:fs");
const path = await import("node:path");
const target = path.resolve(folder.endsWith(".png") ? folder : path.join(folder, name));
const primary = screen.getPrimaryDisplay();
const sources = await desktopCapturer.getSources({
types: ["screen"],
thumbnailSize: { width: primary.size.width, height: primary.size.height }
});
const source = sources[0];
if (!source) throw new RuntimeError("desktop_error", "无法获取屏幕截图");
await fs.mkdir(path.dirname(target), { recursive: true });
await fs.writeFile(target, source.thumbnail.toPNG());
return target;
});
+84
查看文件
@@ -0,0 +1,84 @@
import { register } from "../engine/registry";
import { BreakSignal, ContinueSignal, RuntimeError, StopSignal } from "../engine/types";
import { interpolate, toNumber } from "../engine/expression";
function sleep(ms: number, signal: AbortSignal): Promise<void> {
if (ms <= 0) return Promise.resolve();
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
signal.removeEventListener("abort", onAbort);
resolve();
}, ms);
const onAbort = () => {
clearTimeout(timer);
reject(new RuntimeError("cancelled", "运行已取消"));
};
if (signal.aborted) return onAbort();
signal.addEventListener("abort", onAbort, { once: true });
});
}
register(["Trigger.Manual", "Trigger.Schedule", "Trigger.FileArrival"], () => null);
register("Basic_Delay_V2", async (ctx, node, args) => {
const random = String(args.is_random ?? "0") === "1" || String(args.is_random) === "true";
const seconds = random
? toNumber(args.min_second, 1) + Math.random() * Math.max(0, toNumber(args.max_second, 5) - toNumber(args.min_second, 1))
: toNumber(args.second, 1);
ctx.log(`等待 ${seconds.toFixed(2)} 秒`, "info", node.id);
await sleep(Math.max(0, seconds) * 1000, ctx.signal);
return seconds;
});
register(["Basic_Delay_V1", "Basic_Delay"], async (ctx, node, args) => {
const seconds = toNumber(args.second ?? args.time ?? 1, 1);
await sleep(Math.max(0, seconds) * 1000, ctx.signal);
return seconds;
});
register("Basic_DebugOutput", (ctx, node, args) => {
const message = String(args.msg ?? args.message ?? "");
ctx.log(message || "(空)", "info", node.id);
return message;
});
register("Basic_SetVariable", (ctx, node, args) => {
const name = String(node.options?.outputVar || "").trim();
const value = args.value;
if (!name) {
throw new RuntimeError("validation_error", "「设置变量」未配置输出变量", {
nodeId: node.id,
componentId: node.componentId,
recommendation: "在属性面板的输出变量里填写变量名"
});
}
ctx.vars[name] = value;
ctx.log(`${name} = ${typeof value === "object" ? JSON.stringify(value) : String(value)}`, "info", node.id);
return value;
});
register("logic_control_var", (ctx, node, args) => {
const name = String(interpolate(args.var_name ?? node.options?.outputVar ?? "", ctx.vars) || "").trim();
if (!name) {
throw new RuntimeError("validation_error", "「设置变量」未填写变量名称", {
nodeId: node.id,
componentId: node.componentId
});
}
const value = interpolate(args.var_value ?? args.value, ctx.vars);
ctx.vars[name] = value;
ctx.log(`${name} = ${typeof value === "object" ? JSON.stringify(value) : String(value)}`, "info", node.id);
return value;
});
register("logic_control_break_for", () => {
throw new BreakSignal();
});
register("logic_control_next_for", () => {
throw new ContinueSignal();
});
register(["logic_control_stop", "logic_control_stop_return"], () => {
throw new StopSignal();
});
+515
查看文件
@@ -0,0 +1,515 @@
import { promises as fs } from "node:fs";
import path from "node:path";
import { register } from "../engine/registry";
import { RuntimeError } from "../engine/types";
const str = (value: unknown): string => (value == null ? "" : String(value));
const num = (value: unknown, fallback = 0): number => {
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : fallback;
};
const bool = (value: unknown): boolean =>
value === true || value === "true" || value === "1" || value === "True" || value === "是" || value === "开启";
function first(args: Record<string, unknown>, keys: string[]): unknown {
for (const key of keys) {
if (Object.prototype.hasOwnProperty.call(args, key)) return args[key];
}
return undefined;
}
function asArray(value: unknown): unknown[] {
if (Array.isArray(value)) return value;
if (value == null || value === "") return [];
if (typeof value === "string") {
try {
const parsed = JSON.parse(value);
return Array.isArray(parsed) ? parsed : [parsed];
} catch {
return value.split(/[,,\n]/).map((item) => item.trim()).filter(Boolean);
}
}
return [value];
}
function asObject(value: unknown): Record<string, unknown> {
if (value && typeof value === "object" && !Array.isArray(value)) return value as Record<string, unknown>;
if (typeof value === "string" && value.trim()) {
try {
const parsed = JSON.parse(value);
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed as Record<string, unknown>;
} catch {
// ignore
}
}
return {};
}
function toDate(value: unknown): Date {
if (value instanceof Date) return value;
if (typeof value === "number") return new Date(value < 1e12 ? value * 1000 : value);
const text = str(value).trim();
if (!text) return new Date();
if (/^\d+$/.test(text)) {
const numeric = Number(text);
return new Date(numeric < 1e12 ? numeric * 1000 : numeric);
}
const normalized = text.replace(/\//g, "-").replace(" ", "T");
const date = new Date(normalized);
return Number.isNaN(date.getTime()) ? new Date(text) : date;
}
function formatDate(value: unknown, pattern = "YYYY-MM-DD HH:mm:ss"): string {
const date = toDate(value);
if (Number.isNaN(date.getTime())) return "";
const pad = (input: number) => String(input).padStart(2, "0");
const fmt = pattern
.replace(/yyyy/g, "YYYY")
.replace(/yy/g, "YY")
.replace(/dd/g, "DD");
return fmt
.replace(/YYYY/g, String(date.getFullYear()))
.replace(/YY/g, String(date.getFullYear()).slice(-2))
.replace(/MM/g, pad(date.getMonth() + 1))
.replace(/DD/g, pad(date.getDate()))
.replace(/HH/g, pad(date.getHours()))
.replace(/mm/g, pad(date.getMinutes()))
.replace(/ss/g, pad(date.getSeconds()));
}
function cutText(text: string, args: Record<string, unknown>): string {
const startType = str(first(args, ["start_cut_type"]));
const endType = str(first(args, ["end_cut_type"]));
let start = 0;
if (startType.includes("文本")) {
const token = str(first(args, ["start_cut_str", "start_cut_pos"]));
const index = text.indexOf(token);
start = index >= 0 ? index + token.length : 0;
} else {
start = Math.max(0, num(first(args, ["start_cut_pos", "start"]), 0));
}
if (endType.includes("长度")) {
const length = Math.max(0, num(first(args, ["end_cut_length", "length"]), text.length));
return text.slice(start, start + length);
}
if (endType.includes("文本")) {
const token = str(first(args, ["end_cut_txt", "end_cut_str"]));
const index = text.indexOf(token, start);
return text.slice(start, index >= 0 ? index : text.length);
}
const end = num(first(args, ["end_cut_pos", "end"]), text.length);
return text.slice(start, end);
}
/* ---------------- StrUtil ---------------- */
register("StrUtil_Len", (_ctx, _node, args) => str(first(args, ["string"])).length);
register("StrUtil_InStr", (_ctx, _node, args) => {
const text = str(first(args, ["string"]));
const search = str(first(args, ["search"]));
const start = Math.max(0, num(first(args, ["startIndex"]), 0));
return text.indexOf(search, start);
});
register("StrUtil_Replace", (_ctx, _node, args) => {
const text = str(first(args, ["string"]));
const search = str(first(args, ["search", "Config"]));
const replacement = str(first(args, ["replace"]));
return text.split(search).join(replacement);
});
register(["StrUtil_LowerCase"], (_ctx, _node, args) => str(first(args, ["string"])).toLowerCase());
register(["StrUtil_UpperCase"], (_ctx, _node, args) => str(first(args, ["string"])).toUpperCase());
register("StrUtil_CaseChange", (_ctx, _node, args) => {
const text = str(first(args, ["string"]));
const mode = str(first(args, ["case"]));
if (mode.includes("小写")) return text.toLowerCase();
if (mode.includes("大写")) return text.toUpperCase();
if (mode.includes("首")) return text ? text[0].toUpperCase() + text.slice(1) : "";
return text;
});
register("StrUtil_Trim", (_ctx, _node, args) => str(first(args, ["string"])).trim());
register("StrUtil_Ltrim", (_ctx, _node, args) => str(first(args, ["string"])).replace(/^\s+/, ""));
register("StrUtil_Rtrim", (_ctx, _node, args) => str(first(args, ["string"])).replace(/\s+$/, ""));
register("StrUtil_DelTrim", (_ctx, _node, args) => str(first(args, ["string"])).trim());
register("StrUtil_Left", (_ctx, _node, args) => str(first(args, ["string"])).slice(0, Math.max(0, num(first(args, ["length"]), 0))));
register("StrUtil_Right", (_ctx, _node, args) => {
const text = str(first(args, ["string"]));
const length = Math.max(0, num(first(args, ["length"]), 0));
return length ? text.slice(-length) : "";
});
register("StrUtil_Mid", (_ctx, _node, args) => {
const text = str(first(args, ["string"]));
const start = Math.max(0, num(first(args, ["start"]), 0));
const length = Math.max(0, num(first(args, ["length"]), text.length));
return text.slice(start, start + length);
});
register(["StrUtil_CutOutStr", "StrUtil_CutOutStrV1"], (_ctx, _node, args) => cutText(str(first(args, ["string"])), args));
register("StrUtil_Split", (_ctx, _node, args) =>
str(first(args, ["string"])).split(str(first(args, ["spliter"])))
);
register(["StrUtil_SplitStr"], (_ctx, _node, args) => {
const text = str(first(args, ["string"]));
const custom = str(first(args, ["diy_symbol"]));
const symbol = custom || str(first(args, ["split_symbol"])) || ",";
const regex = bool(first(args, ["is_re"]));
let parts: string[];
if (regex) {
try {
parts = text.split(new RegExp(symbol));
} catch {
parts = text.split(symbol);
}
} else {
parts = text.split(symbol);
}
const trim = args.del_trim === undefined ? true : bool(args.del_trim);
if (trim) parts = parts.map((item) => item.trim());
return parts.filter((item) => item !== "" || !trim);
});
register("StrUtil_AddToStr", (_ctx, _node, args) => {
const text = str(first(args, ["string"]));
const addition = str(first(args, ["add_str"]));
const type = str(first(args, ["add_type"]));
const symbol = str(first(args, ["symbol"]));
if (type.includes("前")) return addition + symbol + text;
return text + symbol + addition;
});
register("StrUtil_ReverseStr", (_ctx, _node, args) => {
const text = str(first(args, ["string"]));
return bool(first(args, ["letter_reverse"])) ? [...text].reverse().join("") : text.split(/\s+/).reverse().join(" ");
});
register("StrUtil_ListToString", (_ctx, _node, args) => {
const list = asArray(first(args, ["str_list"]));
const connector = str(first(args, ["connectors_custom", "standard"])) || ",";
return list.map(str).join(connector);
});
register("StrUtil_CompleteStr", (_ctx, _node, args) => {
const text = str(first(args, ["string"]));
const total = Math.max(0, num(first(args, ["sum_length"]), text.length));
const pad = str(first(args, ["com_string"])) || " ";
const mode = str(first(args, ["com_type"]));
const fill = pad.repeat(Math.max(0, total - text.length)).slice(0, Math.max(0, total - text.length));
return mode.includes("右") ? text + fill : fill + text;
});
register("StrUtil_CharCode", (_ctx, _node, args) => String.fromCharCode(num(first(args, ["code"]), 0)));
register("StrUtil_CharAsc", (_ctx, _node, args) => str(first(args, ["char"])).charCodeAt(0));
register("StrUtil_CreateRandomStr", (_ctx, _node, args) => {
const length = Math.max(0, num(first(args, ["string_length"]), 8));
const alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
return Array.from({ length }, () => alphabet[Math.floor(Math.random() * alphabet.length)]).join("");
});
register("StrUtil_LookUpStr", (_ctx, _node, args) => {
const text = str(first(args, ["string"]));
const search = str(first(args, ["search"]));
const start = Math.max(0, num(first(args, ["startIndex"]), 0));
return text.indexOf(search, start);
});
/* ---------------- Time ---------------- */
register("Time_Now", () => formatDate(new Date()));
register(["Time_GetDate", "Time_GetDateV1", "Time_GetDateV2"], (_ctx, _node, args) => {
let date = toDate(first(args, ["date_start", "time_data"]));
const offset = num(first(args, ["offset_unit_num"]), 0);
const mode = str(first(args, ["offset_status"]));
if (offset) date = new Date(date.getTime() + (mode.includes("减") ? -offset : offset) * 86400000);
const type = str(first(args, ["get_type"]));
if (type.includes("时间戳")) return Math.floor(date.getTime() / 1000);
if (type.includes("日期") && !type.includes("时间")) return formatDate(date, "YYYY-MM-DD");
return formatDate(date, type || "YYYY-MM-DD HH:mm:ss");
});
register("Time_Date", () => formatDate(new Date(), "YYYY-MM-DD"));
register("Time_Time", () => formatDate(new Date(), "HH:mm:ss"));
register("Time_TimeStamp", () => Math.floor(Date.now() / 1000));
register(["Time_Format", "Time_FormatNew"], (_ctx, _node, args) =>
formatDate(first(args, ["timestamp", "date_input"]), str(first(args, ["format"])) || "YYYY-MM-DD HH:mm:ss")
);
register("Time_DatetimeToTimestamp", (_ctx, _node, args) => Math.floor(toDate(first(args, ["datetime"])).getTime() / 1000));
register("Time_DateToTimestamp", (_ctx, _node, args) => Math.floor(toDate(first(args, ["datetime"])).getTime() / 1000));
register(["Time_DateOffset", "Time_DateOffset_V1"], (_ctx, _node, args) => {
const date = toDate(first(args, ["date_input"]));
const amount = num(first(args, ["offset_unit_num"]), 0);
const mode = str(first(args, ["offset_status"]));
const delta = mode.includes("减") ? -amount : amount;
const pattern = str(first(args, ["get_type"])) || str(first(args, ["is_fmt"])) || "YYYY-MM-DD HH:mm:ss";
return formatDate(new Date(date.getTime() + delta * 86400000), pattern.includes("YYYY") ? pattern : "YYYY-MM-DD HH:mm:ss");
});
for (const [id, getter] of [
["Time_Year", (date: Date) => date.getFullYear()],
["Time_Month", (date: Date) => date.getMonth() + 1],
["Time_Day", (date: Date) => date.getDate()],
["Time_Hour", (date: Date) => date.getHours()],
["Time_Minute", (date: Date) => date.getMinutes()],
["Time_Second", (date: Date) => date.getSeconds()],
["Time_WeekDay", (date: Date) => date.getDay()]
] as Array<[string, (date: Date) => number]>) {
register(id, (_ctx, _node, args) => getter(toDate(first(args, ["timestamp", "datetime"]))));
}
register("Time_CalDate", (_ctx, _node, args) => {
const start = toDate(first(args, ["date_start"]));
const end = toDate(first(args, ["date_end"]));
const unit = str(first(args, ["time_type"]));
const ms = end.getTime() - start.getTime();
if (unit.includes("时")) return ms / 3600000;
if (unit.includes("分")) return ms / 60000;
if (unit.includes("秒")) return ms / 1000;
return ms / 86400000;
});
register("Time_TimeTypeConversion", (_ctx, _node, args) =>
formatDate(first(args, ["datetime"]), str(first(args, ["format"])) || "YYYY-MM-DD HH:mm:ss")
);
/* ---------------- List ---------------- */
register("List_CreateList", (_ctx, _node, args) => asArray(first(args, ["data"])));
register("List_ListLength", (_ctx, _node, args) => asArray(first(args, ["arrayRet"])).length);
register("List_InsertList", (_ctx, _node, args) => {
const list = asArray(first(args, ["arrayRet"]));
const item = first(args, ["data"]);
const type = str(first(args, ["insertType"]));
if (type.includes("位置") || type.includes("指定")) {
const position = Math.max(0, Math.min(list.length, num(first(args, ["position"]), 0)));
return [...list.slice(0, position), item, ...list.slice(position)];
}
return [...list, item];
});
register("List_DeleteList", (_ctx, _node, args) => {
const list = asArray(first(args, ["arrayRet"]));
const type = str(first(args, ["deleteType"]));
if (type.includes("位置")) {
const position = Math.max(0, num(first(args, ["position"]), 0));
return list.filter((_, index) => index !== position);
}
const target = first(args, ["data"]);
return list.filter((item) => item !== target);
});
register("List_GetListItem", (_ctx, _node, args) => asArray(first(args, ["arrayRet"]))[num(first(args, ["position"]), 0)]);
register("List_SetListItem", (_ctx, _node, args) => {
const list = [...asArray(first(args, ["arrayRet"]))];
const position = Math.max(0, num(first(args, ["position"]), 0));
list[position] = first(args, ["data"]);
return list;
});
register("List_GetIndex", (_ctx, _node, args) => asArray(first(args, ["arrayRet"])).indexOf(first(args, ["data"])));
register("List_ListMerge", (_ctx, _node, args) => [
...asArray(first(args, ["arrayRet1"])),
...asArray(first(args, ["arrayRet2"]))
]);
register("List_ListDuplication", (_ctx, _node, args) => [...new Set(asArray(first(args, ["arrayRet"])))]);
register("List_ListSorted", (_ctx, _node, args) => {
const list = [...asArray(first(args, ["arrayRet"]))];
const mode = str(first(args, ["sortedType"]));
return list.sort((a, b) => {
const result = String(a).localeCompare(String(b), "zh-CN", { numeric: true });
return mode.includes("降") ? -result : result;
});
});
register("List_ListIntersection", (_ctx, _node, args) => {
const second = new Set(asArray(first(args, ["arrayRet2"])));
return asArray(first(args, ["arrayRet1"])).filter((item) => second.has(item));
});
register("List_ClearList", () => []);
/* ---------------- Dict ---------------- */
register("Dict_Create", (_ctx, _node, args) => asObject(first(args, ["value"])));
register("Dict_GetItem", (_ctx, _node, args) => {
const object = asObject(first(args, ["dictObj"]));
const key = str(first(args, ["key"]));
if (key in object) return object[key];
return first(args, ["default_value"]);
});
register("Dict_SetItem", (_ctx, _node, args) => {
const object = asObject(first(args, ["dictObj"]));
const value = first(args, ["value"]);
if (value && typeof value === "object") {
const record = value as Record<string, unknown>;
if ("key" in record) return { ...object, [str(record.key)]: record.value };
return { ...object, ...record };
}
return { ...object, [str(first(args, ["key"]))]: value };
});
register("Dict_Keys", (_ctx, _node, args) => Object.keys(asObject(first(args, ["dictObj"]))));
register("Dict_Values", (_ctx, _node, args) => Object.values(asObject(first(args, ["dictObj"]))));
register("Dict_Items", (_ctx, _node, args) => Object.entries(asObject(first(args, ["dictObj"]))));
register("Dict_Delete", (_ctx, _node, args) => {
const object = { ...asObject(first(args, ["dictObj"])) };
delete object[str(first(args, ["key"]))];
return object;
});
register("Dict_Clear", () => ({}));
register("Dict_Update", (_ctx, _node, args) => ({
...asObject(first(args, ["dictObj"])),
...asObject(first(args, ["targetDict"]))
}));
/* ---------------- Regex ---------------- */
register(["Regex_Test", "Regex_TestNew"], (_ctx, _node, args) => {
try {
return new RegExp(str(first(args, ["pattern", "Config"]))).test(str(first(args, ["text"])));
} catch {
return false;
}
});
register(["Regex_FindAll", "Regex_FindAllNew"], (_ctx, _node, args) => {
try {
const regex = new RegExp(str(first(args, ["pattern", "Config"])), "g");
return [...str(first(args, ["text"])).matchAll(regex)].map((match) => match[0]);
} catch {
return [];
}
});
register(["Regex_Find", "Regex_FindNew", "Regex_FindStr", "Regex_FindStrV1", "Regex_FindNew_1"], (_ctx, _node, args) => {
try {
const pattern = str(first(args, ["pattern", "Config", "expression"]));
const regex = new RegExp(pattern);
const match = regex.exec(str(first(args, ["text", "string"])));
if (!match) return "";
const index = Math.max(0, num(first(args, ["index", "extract_pos"]), 0));
return match[index] ?? match[0];
} catch {
return "";
}
});
/* ---------------- Math ---------------- */
register("Math_Round", (_ctx, _node, args) => {
const digit = num(first(args, ["digit"]), 0);
const factor = 10 ** digit;
return Math.round(num(first(args, ["data"])) * factor) / factor;
});
register("Math_Abs", (_ctx, _node, args) => Math.abs(num(first(args, ["data"]))));
register("Math_Int", (_ctx, _node, args) => {
const value = num(first(args, ["data"]));
const position = str(first(args, ["position"]));
return position.includes("上") ? Math.ceil(value) : Math.floor(value);
});
register("Math_Sqrt", (_ctx, _node, args) => Math.sqrt(num(first(args, ["data"]))));
register("Math_Pow", (_ctx, _node, args) => Math.pow(num(first(args, ["x"])), num(first(args, ["y"]))));
register("Math_Exp", (_ctx, _node, args) => Math.exp(num(first(args, ["data"]))));
register("Math_Sgn", (_ctx, _node, args) => Math.sign(num(first(args, ["data"]))));
register("Math_Hex", (_ctx, _node, args) => num(first(args, ["num"])).toString(16).toUpperCase());
register("Math_Oct", (_ctx, _node, args) => num(first(args, ["num"])).toString(8));
register("Math_Scale", (_ctx, _node, args) => {
const source = str(first(args, ["sourceNum"]));
const from = num(first(args, ["sourceNumScale"]), 10);
const to = num(first(args, ["targetNumScale"]), 10);
return parseInt(source, from).toString(to).toUpperCase();
});
for (const [id, fn] of [
["Math_Sin", (value: number, angle: boolean) => Math.sin(angle ? (value * Math.PI) / 180 : value)],
["Math_Cos", (value: number, angle: boolean) => Math.cos(angle ? (value * Math.PI) / 180 : value)],
["Math_Tan", (value: number, angle: boolean) => Math.tan(angle ? (value * Math.PI) / 180 : value)]
] as Array<[string, (value: number, angle: boolean) => number]>) {
register(id, (_ctx, _node, args) => {
const unit = str(first(args, ["unit"]));
return fn(num(first(args, ["data"])), unit.includes("角度"));
});
}
/* ---------------- JSON / CSV ---------------- */
register("JSON_Stringify", (_ctx, _node, args) => JSON.stringify(first(args, ["string"])));
register("JSON_Parse", (_ctx, _node, args) => {
try {
return JSON.parse(str(first(args, ["string"])));
} catch {
return null;
}
});
function parseCsv(text: string): string[][] {
const rows: string[][] = [];
let row: string[] = [];
let cell = "";
let quoted = false;
for (let i = 0; i < text.length; i += 1) {
const char = text[i];
if (quoted) {
if (char === '"' && text[i + 1] === '"') {
cell += '"';
i += 1;
} else if (char === '"') {
quoted = false;
} else {
cell += char;
}
} else if (char === '"') {
quoted = true;
} else if (char === ",") {
row.push(cell);
cell = "";
} else if (char === "\n") {
row.push(cell);
rows.push(row);
row = [];
cell = "";
} else if (char !== "\r") {
cell += char;
}
}
if (cell || row.length) {
row.push(cell);
rows.push(row);
}
return rows;
}
function toCsv(rows: unknown[][]): string {
return rows
.map((row) =>
row
.map((cell) => {
const text = str(cell);
return /[",\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
})
.join(",")
)
.join("\r\n");
}
register(["CSV_Read", "CSV_ReadNew"], async (_ctx, node, args) => {
const filePath = path.resolve(str(first(args, ["path"])));
if (!filePath) throw new RuntimeError("validation_error", "CSV 路径为空", { nodeId: node.id, componentId: node.componentId });
const text = await fs.readFile(filePath, "utf8");
return parseCsv(text);
});
register(["CSV_Write", "CSV_WriteNew"], async (_ctx, node, args) => {
const filePath = path.resolve(str(first(args, ["path"])));
if (!filePath) throw new RuntimeError("validation_error", "CSV 路径为空", { nodeId: node.id, componentId: node.componentId });
const table = asArray(first(args, ["table"])).map((row) => asArray(row));
await fs.mkdir(path.dirname(filePath), { recursive: true });
await fs.writeFile(filePath, toCsv(table), "utf8");
return filePath;
});
/* ---------------- Basic ---------------- */
register(["Basic_ConvertNumber", "Basic_StringToNum"], (_ctx, _node, args) => num(first(args, ["val"])));
register("Basic_ConvertString", (_ctx, _node, args) => str(first(args, ["val"])));
register("Basic_ConvertInt", (_ctx, _node, args) => Math.trunc(num(first(args, ["val"]))));
register("Basic_ConvertBool", (_ctx, _node, args) => bool(first(args, ["val"])));
register("Basic_IsNull", (_ctx, _node, args) => {
const value = first(args, ["val"]);
return value === null || value === undefined || value === "";
});
register("Basic_IsArray", (_ctx, _node, args) => Array.isArray(first(args, ["val"])));
register("Basic_IsDictionary", (_ctx, _node, args) => {
const value = first(args, ["val"]);
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
});
register("Basic_IsNumber", (_ctx, _node, args) => Number.isFinite(Number(first(args, ["val"]))));
register("Basic_Type", (_ctx, _node, args) => {
const value = first(args, ["val"]);
if (value === null) return "null";
if (Array.isArray(value)) return "list";
return typeof value;
});
register("Basic_JudgeDataType", (_ctx, _node, args) => {
const value = first(args, ["val"]);
if (value === null || value === undefined) return "null";
if (Array.isArray(value)) return "list";
if (typeof value === "object") return "dict";
return typeof value;
});
register("Basic_Clone", (_ctx, _node, args) => structuredClone(first(args, ["val"])));
register("Basic_RandomInt", (_ctx, _node, args) => {
const min = num(first(args, ["min", "start"]), 0);
const max = num(first(args, ["max", "end"]), 100);
return Math.floor(min + Math.random() * (max - min + 1));
});
register("Basic_Rnd", () => Math.random());
register("Basic_DataTypeConversion", (_ctx, _node, args) => first(args, ["val"]));
+213
查看文件
@@ -0,0 +1,213 @@
import { promises as fs } from "node:fs";
import path from "node:path";
import AdmZip from "adm-zip";
import { register } from "../engine/registry";
import { RuntimeError } from "../engine/types";
function pick(args: Record<string, unknown>, keys: string[]): string {
for (const key of keys) {
const value = args[key];
if (value !== undefined && value !== null && value !== "") return String(value);
}
return "";
}
function truthy(value: unknown): boolean {
return value === true || value === "true" || value === "1" || value === "True" || value === "是" || value === "开启";
}
async function readText(filePath: string, encode = "utf-8"): Promise<string> {
const buffer = await fs.readFile(filePath);
const encoding = (encode || "utf-8").toLowerCase().replace("_", "-");
if (encoding === "utf-8" || encoding === "utf8" || encoding === "auto") return buffer.toString("utf8");
try {
return new TextDecoder(encoding).decode(buffer);
} catch {
return buffer.toString("utf8");
}
}
async function writeText(filePath: string, content: string, append: boolean, encode = "utf-8"): Promise<number> {
await fs.mkdir(path.dirname(filePath), { recursive: true });
if (append) await fs.appendFile(filePath, content, "utf8");
else await fs.writeFile(filePath, content, "utf8");
const stat = await fs.stat(filePath);
return stat.size;
}
function requirePath(value: string, label: string, nodeId: string, componentId: string): string {
if (!value.trim()) {
throw new RuntimeError("validation_error", `${label}为空`, {
nodeId,
componentId,
recommendation: "在属性面板选择文件或目录"
});
}
return path.resolve(value);
}
register("File_Read", async (_ctx, node, args) => {
const filePath = requirePath(pick(args, ["path", "file", "file_path"]), "文件路径", node.id, node.componentId);
return readText(filePath, pick(args, ["encode"]) || "utf-8");
});
register("File_Write", async (_ctx, node, args) => {
const filePath = requirePath(pick(args, ["path", "file", "file_path"]), "文件路径", node.id, node.componentId);
const content = String(args.content ?? args.value ?? "");
const append = truthy(args.append);
return writeText(filePath, content, append, pick(args, ["encode"]) || "utf-8");
});
register(["File_CreateFile"], async (_ctx, node, args) => {
const filePath = requirePath(pick(args, ["path", "file", "file_path"]), "文件路径", node.id, node.componentId);
await fs.mkdir(path.dirname(filePath), { recursive: true });
await fs.writeFile(filePath, "", { flag: "a" });
return filePath;
});
register("File_Copy", async (_ctx, node, args) => {
const source = requirePath(pick(args, ["pathSrc", "src", "source"]), "源路径", node.id, node.componentId);
const target = requirePath(pick(args, ["pathDest", "dest", "target"]), "目标路径", node.id, node.componentId);
const overwrite = truthy(args.cover ?? args.overwrite);
const stat = await fs.stat(source);
if (stat.isDirectory()) {
await fs.cp(source, target, { recursive: true, force: overwrite });
} else {
await fs.mkdir(path.dirname(target), { recursive: true });
await fs.copyFile(source, target);
}
return target;
});
register(["File_File_Move", "File_Move"], async (_ctx, node, args) => {
const source = requirePath(pick(args, ["pathSrc", "src", "source"]), "源路径", node.id, node.componentId);
const target = requirePath(pick(args, ["pathDest", "dest", "target"]), "目标路径", node.id, node.componentId);
await fs.mkdir(path.dirname(target), { recursive: true });
try {
await fs.rename(source, target);
} catch {
await fs.cp(source, target, { recursive: true, force: true });
await fs.rm(source, { recursive: true, force: true });
}
return target;
});
register(["File_DeleteFile", "File_Delete"], async (_ctx, node, args) => {
const type = String(args.file_type ?? "file");
const target = type === "folder"
? pick(args, ["folder_path", "path"])
: pick(args, ["file_path", "path"]);
const resolved = requirePath(target, "待删除路径", node.id, node.componentId);
await fs.rm(resolved, { recursive: true, force: true });
return resolved;
});
register(["File_FileExists", "File_PathExists"], async (_ctx, node, args) => {
const target = requirePath(pick(args, ["path", "file", "file_path"]), "文件路径", node.id, node.componentId);
try {
await fs.access(target);
return true;
} catch {
return false;
}
});
register("File_GetFileList", async (_ctx, node, args) => {
const folder = requirePath(pick(args, ["folder_path", "folder", "path"]), "文件夹", node.id, node.componentId);
const keyword = pick(args, ["key_word"]);
const ignore = pick(args, ["ignore"]);
const recursive = truthy(args.recursive);
const fullPath = truthy(args.is_full_path ?? "1");
const results: string[] = [];
async function walk(dir: string): Promise<void> {
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const absolute = path.join(dir, entry.name);
if (entry.isDirectory()) {
if (recursive) await walk(absolute);
continue;
}
if (keyword && !entry.name.includes(keyword)) continue;
if (ignore && entry.name.includes(ignore)) continue;
results.push(fullPath ? absolute : entry.name);
}
}
await walk(folder);
results.sort((a, b) => a.localeCompare(b, "zh-CN"));
return results;
});
register(["File_MakeDir", "File_MakeFolder", "File_CreateFolder"], async (_ctx, node, args) => {
const base = pick(args, ["path", "folder_path", "folder"]);
const name = pick(args, ["name"]);
const target = requirePath(name ? path.join(base, name) : base, "文件夹路径", node.id, node.componentId);
await fs.mkdir(target, { recursive: true });
return target;
});
register(["File_PathSize", "File_PathSizeNew"], async (_ctx, node, args) => {
const target = requirePath(pick(args, ["path", "file"]), "路径", node.id, node.componentId);
const stat = await fs.stat(target);
return stat.size;
});
register("File_GetFileInfo", async (_ctx, node, args) => {
const target = requirePath(pick(args, ["path", "file"]), "路径", node.id, node.componentId);
const stat = await fs.stat(target);
return {
path: target,
name: path.basename(target),
size: stat.size,
isDirectory: stat.isDirectory(),
createdAt: stat.birthtime.toISOString(),
modifiedAt: stat.mtime.toISOString()
};
});
register("File_WaitFile", async (ctx, node, args) => {
const target = requirePath(pick(args, ["path", "file"]), "文件路径", node.id, node.componentId);
const timeout = Math.max(1, Number(args.time_out ?? args.timeout ?? 30)) * 1000;
const started = Date.now();
while (Date.now() - started < timeout) {
if (ctx.signal.aborted) throw new RuntimeError("cancelled", "运行已取消");
try {
await fs.access(target);
return true;
} catch {
await new Promise((resolve) => setTimeout(resolve, 500));
}
}
throw new RuntimeError("timeout", `等待文件超时:${target}`, {
nodeId: node.id,
componentId: node.componentId,
recommendation: "检查文件是否生成,或调大超时时间"
});
});
register("File_CompressFile", async (_ctx, node, args) => {
const source = requirePath(pick(args, ["pathSrc", "path", "folder_path"]), "源路径", node.id, node.componentId);
const target = requirePath(pick(args, ["pathDest", "zip_path", "out_path"]), "压缩包路径", node.id, node.componentId);
const zip = new AdmZip();
const stat = await fs.stat(source);
if (stat.isDirectory()) zip.addLocalFolder(source);
else zip.addLocalFile(source);
await fs.mkdir(path.dirname(target), { recursive: true });
zip.writeZip(target);
return target;
});
register("File_DecompressFile", async (_ctx, node, args) => {
const source = requirePath(pick(args, ["folder_path", "path", "zip_path"]), "压缩包路径", node.id, node.componentId);
const target = requirePath(pick(args, ["out_path", "pathDest", "target"]), "解压目录", node.id, node.componentId);
await fs.mkdir(target, { recursive: true });
new AdmZip(source).extractAllTo(target, true);
return target;
});
register("File_Rename", async (_ctx, node, args) => {
const source = requirePath(pick(args, ["pathSrc", "path"]), "源路径", node.id, node.componentId);
const newName = pick(args, ["reName", "name"]);
const target = path.join(path.dirname(source), newName);
await fs.rename(source, target);
return target;
});
+143
查看文件
@@ -0,0 +1,143 @@
import { promises as fs } from "node:fs";
import path from "node:path";
import { register } from "../engine/registry";
import { RuntimeError } from "../engine/types";
function parseObject(value: unknown): Record<string, string> {
if (!value) return {};
if (typeof value === "object") {
const result: Record<string, string> = {};
for (const [key, item] of Object.entries(value as Record<string, unknown>)) result[key] = String(item ?? "");
return result;
}
const text = String(value).trim();
if (!text) return {};
try {
const parsed = JSON.parse(text);
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
const result: Record<string, string> = {};
for (const [key, item] of Object.entries(parsed as Record<string, unknown>)) result[key] = String(item ?? "");
return result;
}
} catch {
// fall through to query string
}
const result: Record<string, string> = {};
for (const pair of text.split("&")) {
if (!pair) continue;
const [key, ...rest] = pair.split("=");
result[decodeURIComponent(key)] = decodeURIComponent(rest.join("="));
}
return result;
}
function withQuery(url: string, params: Record<string, string>): string {
if (!Object.keys(params).length) return url;
const target = new URL(url);
for (const [key, value] of Object.entries(params)) target.searchParams.set(key, value);
return target.toString();
}
async function request(
nodeId: string,
componentId: string,
method: string,
args: Record<string, unknown>,
signal: AbortSignal
): Promise<unknown> {
const rawUrl = String(args.url ?? args.web_page ?? "").trim();
if (!rawUrl) {
throw new RuntimeError("validation_error", "URL 为空", {
nodeId,
componentId,
recommendation: "在属性面板填写请求地址"
});
}
const timeout = Math.max(1000, Number(args.timeout ?? args.time_out ?? 60000));
const headers = parseObject(args.headers);
const cookies = parseObject(args.cookies);
if (Object.keys(cookies).length) {
headers.Cookie = Object.entries(cookies).map(([key, value]) => `${key}=${value}`).join("; ");
}
const params = parseObject(args.param);
const body = String(args.body ?? args.data ?? "");
let url = rawUrl;
let payload: string | undefined;
if (method === "GET") {
url = withQuery(url, params);
} else if (body) {
payload = body;
} else if (Object.keys(params).length) {
const asJson = /json/i.test(headers["Content-Type"] || headers["content-type"] || "");
if (asJson) {
payload = JSON.stringify(params);
headers["Content-Type"] = headers["Content-Type"] || "application/json";
} else {
payload = new URLSearchParams(params).toString();
headers["Content-Type"] = headers["Content-Type"] || "application/x-www-form-urlencoded";
}
}
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeout);
const onAbort = () => controller.abort();
if (signal.aborted) controller.abort();
signal.addEventListener("abort", onAbort, { once: true });
try {
const response = await fetch(url, { method, headers, body: payload, signal: controller.signal });
const contentType = response.headers.get("content-type") || "";
const text = await response.text();
if (!response.ok && !/Http_Http_Request/i.test(componentId)) {
throw new RuntimeError("network_error", `HTTP ${response.status} ${response.statusText}`, {
nodeId,
componentId,
recommendation: "检查地址、参数与鉴权信息"
});
}
const savePath = String(args.file_path ?? "").trim();
if (savePath) {
await fs.mkdir(path.dirname(path.resolve(savePath)), { recursive: true });
await fs.writeFile(path.resolve(savePath), text, "utf8");
}
if (/json/i.test(contentType) || (text.trim().startsWith("{") || text.trim().startsWith("["))) {
try {
return JSON.parse(text);
} catch {
return text;
}
}
return text;
} catch (error) {
if (error instanceof RuntimeError) throw error;
if ((error as Error).name === "AbortError") {
throw new RuntimeError("timeout", `请求超时(${timeout}ms)`, {
nodeId,
componentId,
recommendation: "增加超时时间或检查网络"
});
}
throw new RuntimeError("network_error", (error as Error).message, {
nodeId,
componentId,
recommendation: "检查 URL、代理与网络连接"
});
} finally {
clearTimeout(timer);
signal.removeEventListener("abort", onAbort);
}
}
register(["Http_Get", "Http_Get_V1", "Http_Get_V2"], (ctx, node, args) =>
request(node.id, node.componentId, "GET", args, ctx.signal)
);
register(["Http_Post", "Http_Post_V2", "Http_Post_V3"], (ctx, node, args) =>
request(node.id, node.componentId, "POST", args, ctx.signal)
);
register("Http_Http_Request", (ctx, node, args) => {
const method = String(args.method ?? "GET").toUpperCase();
return request(node.id, node.componentId, method, args, ctx.signal);
});
+7
查看文件
@@ -0,0 +1,7 @@
import "./control";
import "./files";
import "./http";
import "./data";
import "./browser";
export { capabilityCount, capabilityIds, getHandler, hasCapability, register } from "../engine/registry";