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, 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 { if (value && typeof value === "object" && !Array.isArray(value)) return value as Record; if (typeof value === "string" && value.trim()) { try { const parsed = JSON.parse(value); if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed as Record; } 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 { 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; 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"]));