Files
PRA-only-demo/packages/rpa-core/src/capabilities/data.ts
T
yu 624d13f2ef fix: StrUtil_Replace enum mapping - search select value was treated as literal text
Catalog contract: search is a select ("0"=content / "1"=digits / "2"=phone / "3"=email
/ "4"=ID card / "5"=custom regex) with the target in Config. Old implementation used
first(args, ["search","Config"]) so the enum value "0" was split on as literal text,
making content-replace never work (same family as W26-W33 enum mapping bugs).
Now implements all six modes with preset patterns, regex mode guarded by try/catch,
and falls back to legacy literal behavior when Config is empty.
2026-11-05 13:22:43 +08:00

579 lines
26 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 { 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"]));
// catalog 契约:search 为下拉枚举("0"内容/"1"数字/"2"手机号/"3"邮箱/"4"身份证/"5"自定义正则),查找目标在 Config
// 旧实现直接把枚举值 "0" 当字面文本分割,导致按内容替换永远不生效(W26 系枚举映射 bug 同族)
const modeRaw = first(args, ["search"]);
const mode = str(modeRaw === undefined ? "0" : modeRaw);
const config = str(first(args, ["Config"]));
const replacement = str(first(args, ["replace"]));
const presets: Record<string, RegExp> = {
"1": /\d+/g,
"2": /1[3-9]\d{9}/g,
"3": /[\w.+-]+@[\w-]+(?:\.[\w-]+)+/g,
"4": /\d{17}[\dXx]/g
};
if (presets[mode]) return text.replace(presets[mode], replacement);
if (mode === "5") {
try {
return text.replace(new RegExp(config, "g"), replacement);
} catch {
return text;
}
}
// "0" = 指定内容;兼容旧契约:Config 为空时退回把 search 当字面文本
const target = config || mode;
return text.split(target).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;
});