阶段1-rpa-core建包: engine+31个纯Node能力迁入packages/rpa-core, 领域类型单一源化, createEngine工厂收拢模块级状态
- engine/(registry/types/runtime/expression)物理迁入包, 解耦shared/types(域类型归包, 宿主type-only re-export) - 31个纯Node能力+27个测试迁入包; 宿主留守Electron/浏览器/AI依赖能力, index.ts经@rpa/core/capabilities拉起 - 新增createEngine()/defaultEngine: excel缓存/Excel句柄/db连接/FTP客户端/OCR实例/拟真设置挂引擎实例 - remaining-core子流程依赖改为host-bridge注入, 包内不再import宿主flows - 宿主main/index.ts启动时注入子流程源; electron-vite/vitest/tsconfig配@rpa/core源码别名 - 验证: typecheck绿, 快照909个id diff为零, 332测试过(2个桌面冒烟因会话无交互SetForegroundWindow被拒, 环境阻塞)
This commit is contained in:
1 parent
4139400dbd
commit
caa8e19332
111 files changed
+581
-337
No files matched your search
@@ -0,0 +1,558 @@
|
||||
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"]));
|
||||
// 目录选项值为 "1"(大写)/"2"(小写),文字模式兼容 大写/小写/首字母大写
|
||||
if (mode.includes("首")) return text ? text[0].toUpperCase() + text.slice(1) : "";
|
||||
if (mode === "1" || mode.includes("大写")) return text.toUpperCase();
|
||||
if (mode === "2" || mode.includes("小写")) return text.toLowerCase();
|
||||
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 = str(first(args, ["path"])).trim();
|
||||
if (!filePath) throw new RuntimeError("validation_error", "CSV 路径为空", { nodeId: node.id, componentId: node.componentId });
|
||||
const text = await fs.readFile(path.resolve(filePath), "utf8");
|
||||
return parseCsv(text);
|
||||
});
|
||||
register(["CSV_Write", "CSV_WriteNew"], async (_ctx, node, args) => {
|
||||
const filePath = str(first(args, ["path"])).trim();
|
||||
if (!filePath) throw new RuntimeError("validation_error", "CSV 路径为空", { nodeId: node.id, componentId: node.componentId });
|
||||
const table = asArray(first(args, ["table"])).map((row) => asArray(row));
|
||||
const resolved = path.resolve(filePath);
|
||||
await fs.mkdir(path.dirname(resolved), { recursive: true });
|
||||
await fs.writeFile(resolved, toCsv(table), "utf8");
|
||||
return resolved;
|
||||
});
|
||||
|
||||
/* ---------------- 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) => {
|
||||
const value = first(args, ["val"]);
|
||||
if (typeof value === "number") return value !== 0;
|
||||
return bool(value);
|
||||
});
|
||||
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;
|
||||
});
|
||||
// 原始目录缺少选项映射。新界面使用语义值;数字别名兼容此前默认的 "1"。
|
||||
const JUDGE_TYPE_ALIASES: Record<string, string> = { "1": "number", "2": "null", "3": "dict", "4": "string", "5": "list", "6": "bool", "7": "int" };
|
||||
const CONVERT_TYPE_ALIASES: Record<string, string> = { "1": "number", "2": "int", "3": "bool", "4": "string" };
|
||||
|
||||
function targetType(raw: unknown, aliases: Record<string, string>, supported: string[], nodeId: string, componentId: string): string {
|
||||
const text = String(raw ?? "number").trim().toLowerCase();
|
||||
const synonyms: Record<string, string> = { float: "number", boolean: "bool", str: "string", text: "string", array: "list", dictionary: "dict", object: "dict", empty: "null" };
|
||||
const type = aliases[text] || synonyms[text] || text;
|
||||
if (!supported.includes(type)) throw new RuntimeError("validation_error", "目标数据类型无效,请重新选择类型", { nodeId, componentId });
|
||||
return type;
|
||||
}
|
||||
|
||||
register("Basic_JudgeDataType", (_ctx, node, args) => {
|
||||
const value = first(args, ["val"]);
|
||||
const type = targetType(args.into_type, JUDGE_TYPE_ALIASES, ["number", "null", "dict", "string", "list", "bool", "int"], node.id, node.componentId);
|
||||
switch (type) {
|
||||
case "number": return typeof value === "number" && Number.isFinite(value);
|
||||
case "int": return typeof value === "number" && Number.isInteger(value);
|
||||
case "null": return value === null || value === undefined || value === "";
|
||||
case "dict": return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
||||
case "string": return typeof value === "string";
|
||||
case "list": return Array.isArray(value);
|
||||
case "bool": return typeof value === "boolean";
|
||||
}
|
||||
});
|
||||
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) => {
|
||||
const value = first(args, ["val"]);
|
||||
const type = targetType(args.into_type, CONVERT_TYPE_ALIASES, ["number", "int", "bool", "string"], node.id, node.componentId);
|
||||
if (type === "string") return str(value);
|
||||
if (type === "bool") {
|
||||
if (value == null || value === "") return false;
|
||||
if (typeof value === "boolean") return value;
|
||||
if (typeof value === "number" && Number.isFinite(value)) return value !== 0;
|
||||
const text = String(value).trim().toLowerCase();
|
||||
if (["true", "1", "是", "开启"].includes(text)) return true;
|
||||
if (["false", "0", "否", "关闭"].includes(text)) return false;
|
||||
throw new RuntimeError("validation_error", "输入值无法转换为布尔类型", { nodeId: node.id, componentId: node.componentId });
|
||||
}
|
||||
if ((typeof value !== "number" && typeof value !== "string") || (typeof value === "string" && !value.trim()) || !Number.isFinite(Number(value))) {
|
||||
throw new RuntimeError("validation_error", "输入值不是有效数字", { nodeId: node.id, componentId: node.componentId });
|
||||
}
|
||||
const result = type === "int" ? Math.trunc(Number(value)) : Number(value);
|
||||
if (type === "int" && !Number.isSafeInteger(result)) throw new RuntimeError("validation_error", "转换后的整数超出安全范围", { nodeId: node.id, componentId: node.componentId });
|
||||
return result;
|
||||
});
|
||||
Reference in new issue
Block a user