import ExcelJS from "exceljs"; import { promises as fs } from "node:fs"; import path from "node:path"; import { register } from "../engine/registry"; import { RuntimeError } from "../engine/types"; type Table = unknown[][]; const BOOK = "__rpaDatabook"; const META = "__rpaDatabookColumns"; function parse(value: unknown): unknown { if (typeof value !== "string") return value; try { return JSON.parse(value); } catch { return value; } } function table(value: unknown): Table { const source = parse(value); if (!Array.isArray(source)) return source == null || source === "" ? [] : [[source]]; return source.map((row) => Array.isArray(row) ? [...row] : [row]); } function list(value: unknown): unknown[] { const source = parse(value); if (Array.isArray(source)) return source; if (source == null || source === "") return []; return String(source).split(/[,,;;\n]/).map((item) => item.trim()).filter(Boolean); } function index(headers: unknown[], value: unknown): number { const source = String(value ?? "A").trim(); const named = headers.findIndex((header) => String(header) === source); if (named >= 0) return named; if (/^\d+$/.test(source)) return Math.max(0, Number(source) - 1); if (/^[A-Za-z]+$/.test(source)) { let result = 0; for (const char of source.toUpperCase()) result = result * 26 + char.charCodeAt(0) - 64; return Math.max(0, result - 1); } return -1; } function number(value: unknown): number { const result = Number(value); return Number.isFinite(result) ? result : 0; } function required(value: unknown, label: string, nodeId: string, componentId: string): string { const result = String(value ?? "").trim(); if (!result) throw new RuntimeError("validation_error", `${label}为空`, { nodeId, componentId }); return result; } function book(ctx: { vars: Record }): Table { const current = table(ctx.vars[BOOK]); ctx.vars[BOOK] = current; return current; } function calculate(left: unknown, right: unknown, operator: string): unknown { if (operator === "+" && (typeof left === "string" || typeof right === "string")) return `${left ?? ""}${right ?? ""}`; const a = number(left); const b = number(right); if (operator === "-") return a - b; if (operator === "*") return a * b; if (operator === "/") return b === 0 ? null : a / b; if (operator === "%") return b === 0 ? null : a % b; return a + b; } const readData = (_ctx: unknown, _node: unknown, args: Record) => { const source = table(args.df); if (!source.length) return null; const headers = source[0]; const mode = String(args.readRange ?? "cell").toLowerCase(); const row = Math.max(1, Number(args.row_num ?? args.row_name ?? 1)); const column = index(headers, args.col_name ?? args.col_num ?? "A"); if (mode.includes("row")) return source[row - 1] ?? []; if (mode.includes("col")) return source.map((item) => item[column]); if (mode.includes("range")) { const endRow = Math.max(row, Number(args.appoint_row_num ?? row)); const endColumn = index(headers, args.appoint_col_name ?? args.appoint_col_num ?? column + 1); return source.slice(row - 1, endRow).map((item) => item.slice(column, endColumn + 1)); } return source[row - 1]?.[column]; }; register(["Bivartable_ReadDfData", "Bivartable_ReadDfData_V1"], readData); register(["Bivartable_WriteDfData", "Bivartable_WriteTable"], (_ctx, _node, args) => { const source = table(args.df); if (!source.length) source.push([]); const headers = source[0]; const row = Math.max(1, Number(args.row_num ?? args.row ?? 1)); const column = index(headers, args.col_name ?? args.column ?? "A"); while (source.length < row) source.push([]); while (source[row - 1].length <= column) source[row - 1].push(null); source[row - 1][column] = parse(args.data); return source; }); register("Bivartable_InsertDfData", (_ctx, _node, args) => { const source = table(args.df); const direction = String(args.writeRange ?? "row").toLowerCase(); const data = list(args.data); if (direction.includes("col")) { const at = Math.max(0, Number(args.col_num ?? 1) - 1); for (let row = 0; row < Math.max(source.length, data.length); row += 1) { if (!source[row]) source[row] = []; source[row].splice(at, 0, data[row] ?? null); } } else source.splice(Math.max(0, Number(args.row_num ?? 1) - 1), 0, data); return source; }); register("Bivartable_HandleDfMissValue", (_ctx, _node, args) => { const source = table(args.df); const fill = args.value ?? ""; const remove = String(args.handleType ?? "1") === "2"; if (remove) return source.filter((row, rowIndex) => rowIndex === 0 || !row.some((cell) => cell === null || cell === undefined || cell === "")); return source.map((row, rowIndex) => rowIndex === 0 ? row : row.map((cell) => cell === null || cell === undefined || cell === "" ? fill : cell)); }); register(["Bivartable_ColumnOperationDf", "Bivartable_DoCal"], (_ctx, _node, args) => { const source = table(args.df); if (!source.length) return source; const headers = source[0]; const left = index(headers, args.name1 ?? args.cola); const right = index(headers, args.name2 ?? args.colb); const name = String(args.name ?? args.colnew ?? "结果"); const operator = String(args.operator ?? args.cal ?? "+"); const result: Table = [[...headers, name]]; for (const row of source.slice(1)) result.push([...row, calculate(row[left], row[right], operator)]); return result; }); function columnIndexOf(candidate: unknown): number { const text = String(candidate ?? "").trim(); if (/^\d+$/.test(text)) return Number(text) - 1; let result = 0; for (const char of text.toUpperCase()) result = result * 26 + (char.charCodeAt(0) - 64); return result - 1; } register(["Bivartable_SpliceColData", "Bivartable_DoJoint"], (_ctx, _node, args) => { const source = table(args.df); if (!source.length) return source; const headers = source[0]; // catalog 字段:change_type(name/num) + col_name_1/2 或 col_num_1/2;兼容旧键 col1/col2 const byName = String(args.change_type ?? "name") !== "num"; const left = byName ? index(headers, args.col_name_1 ?? args.col1 ?? args.cola) : columnIndexOf(args.col_num_1); const right = byName ? index(headers, args.col_name_2 ?? args.col2 ?? args.colb) : columnIndexOf(args.col_num_2); const name = String(args.new_col_name ?? args.name ?? args.colnew ?? "拼接结果"); const separator = String(args.connector ?? args.separator ?? ""); return [[...headers, name], ...source.slice(1).map((row) => [...row, `${row[left] ?? ""}${separator}${row[right] ?? ""}`])]; }); register("Bivartable_ModifyColName", (_ctx, _node, args) => { const source = table(args.df); if (!source.length) return source; const at = index(source[0], args.srcname); if (at < 0) return source; source[0][at] = args.newname; return source; }); register("Bivartable_ModifyDfName", (_ctx, _node, args) => { const source = table(args.df); const names = list(args.new_col_name_list); if (source.length && names.length) source[0] = names; return source; }); register("Bivartable_ReadDfIndexColumns", (_ctx, _node, args) => ({ columns: table(args.df)[0] || [], index: table(args.df).slice(1).map((_row, indexValue) => indexValue + 1) })); register("Bivartable_Dtype", (_ctx, _node, args) => { const source = table(args.df); if (!source.length) return source; const columns = (String(args.change_type ?? "num") === "name" ? list(args.col_name) : list(args.col_num)).map((item) => index(source[0], item)).filter((item) => item >= 0); const dtype = String(args.dtype ?? "str").toLowerCase(); const convert = (current: unknown) => { if (dtype.includes("int")) return Math.trunc(number(current)); if (dtype.includes("float") || dtype.includes("number")) return number(current); if (dtype.includes("bool")) return [true, 1, "1", "true", "True", "是"].includes(current as never); return current == null ? "" : String(current); }; return [source[0], ...source.slice(1).map((row) => row.map((cell, column) => columns.includes(column) ? convert(cell) : cell))]; }); register("Bivartable_PivotDf", (_ctx, _node, args) => { const source = table(args.df); if (source.length < 2) return source; const headers = source[0]; const rowNames = list(args.row_list).map(String); const columnNames = list(args.col_list).map(String); let operator = parse(args.operator); let valueName = ""; let aggregation = "sum"; if (operator && typeof operator === "object" && !Array.isArray(operator)) { const entries = Object.entries(operator as Record); valueName = entries[0]?.[0] || ""; aggregation = String(entries[0]?.[1] || "sum"); } else valueName = String(operator || ""); if (!valueName) valueName = String(headers.find((header) => !rowNames.includes(String(header)) && !columnNames.includes(String(header))) || headers.at(-1)); const rowIndexes = rowNames.map((name) => index(headers, name)); const columnIndexes = columnNames.map((name) => index(headers, name)); const valueIndex = index(headers, valueName); const columnKeys = [...new Set(source.slice(1).map((row) => columnIndexes.map((column) => String(row[column] ?? "")).join(" / ")))].filter(Boolean); const groups = new Map }>(); for (const row of source.slice(1)) { const rowValues = rowIndexes.map((column) => row[column]); const rowKey = JSON.stringify(rowValues); const columnKey = columnIndexes.map((column) => String(row[column] ?? "")).join(" / "); const group = groups.get(rowKey) || { rows: rowValues, values: new Map() }; const values = group.values.get(columnKey) || []; values.push(number(row[valueIndex])); group.values.set(columnKey, values); groups.set(rowKey, group); } const aggregate = (values: number[]) => aggregation.includes("avg") || aggregation.includes("mean") ? values.reduce((sum, item) => sum + item, 0) / Math.max(1, values.length) : aggregation.includes("count") ? values.length : aggregation.includes("max") ? Math.max(...values) : aggregation.includes("min") ? Math.min(...values) : values.reduce((sum, item) => sum + item, 0); if (!columnIndexes.length) return [[...rowNames, `${valueName}_${aggregation}`], ...[...groups.values()].map((group) => [...group.rows, aggregate([...group.values.values()].flat())])]; return [[...rowNames, ...columnKeys], ...[...groups.values()].map((group) => [...group.rows, ...columnKeys.map((key) => group.values.has(key) ? aggregate(group.values.get(key)!) : args.value ?? "")])]; }); async function exportExcel(source: Table, filePath: string): Promise { const workbook = new ExcelJS.Workbook(); const sheet = workbook.addWorksheet("Sheet1"); sheet.addRows(source as ExcelJS.CellValue[][]); await fs.mkdir(path.dirname(filePath), { recursive: true }); await workbook.xlsx.writeFile(filePath); return filePath; } register(["Bivartable_DfWriteToFile", "Bivartable_DoSave"], async (ctx, node, args) => { const filePath = path.resolve(required(args.filePath ?? args.path ?? args.savePath, "Excel路径", node.id, node.componentId)); try { return await exportExcel(table(args.df), filePath); } catch (error) { // 目标文件被占用(正被 WPS/Excel 打开等)时,自动另存为带时间戳的文件,不让流程失败 const code = (error as NodeJS.ErrnoException | undefined)?.code; const message = error instanceof Error ? error.message : String(error); if (code === "EBUSY" || code === "EPERM" || /EBUSY|resource busy|being used by another/i.test(message)) { const info = path.parse(filePath); const stamp = new Date().toISOString().slice(0, 19).replace(/[-:T]/g, "").replace(/^(\d{8})(\d{6})$/, "$1_$2"); const fallback = path.join(info.dir, `${info.name}_${stamp}${info.ext}`); await exportExcel(table(args.df), fallback); ctx.log(`目标文件被占用(可能正被打开),已另存为:${fallback}`, "warn", node.id); return fallback; } throw error; } }); register("Bivartable_DoOpenExcel", async (_ctx, node, args) => { const filePath = path.resolve(required(args.filePath, "Excel路径", node.id, node.componentId)); const workbook = new ExcelJS.Workbook(); await workbook.xlsx.readFile(filePath); const sheet = workbook.getWorksheet(String(args.sheetName || "Sheet1")) || workbook.worksheets[0]; return sheet.getSheetValues().slice(1).map((row) => Array.isArray(row) ? row.slice(1).map((cell) => cell ?? "") : []); }); register("Databook_Import", async (ctx, node, args) => { const result = await getHandlerResult("Bivartable_DoOpenExcel", ctx, node, { filePath: args.excel_path, sheetName: args.sheet_name }); ctx.vars[BOOK] = result; return result; }); register("Databook_Export", async (ctx, node, args) => { const folder = path.resolve(required(args.export_path, "导出目录", node.id, node.componentId)); const name = String(args.export_name || "数据表格.xlsx"); return exportExcel(book(ctx), path.join(folder, path.extname(name) ? name : `${name}.xlsx`)); }); register("Databook_SetColumnInfo", (ctx, _node, args) => { const meta = (ctx.vars[META] ||= {}) as Record; meta[String(args.column_name)] = args.column_description; return meta; }); register("Databook_GetColumnInfo", (ctx, _node, args) => { const meta = (ctx.vars[META] || {}) as Record; return meta[String(args.column_name)] ?? ""; }); async function getHandlerResult(id: string, ctx: any, node: any, args: Record): Promise { if (id !== "Bivartable_DoOpenExcel") return null; const filePath = path.resolve(required(args.filePath, "Excel路径", node.id, node.componentId)); const workbook = new ExcelJS.Workbook(); await workbook.xlsx.readFile(filePath); const sheet = workbook.getWorksheet(String(args.sheetName || "Sheet1")) || workbook.worksheets[0]; return sheet.getSheetValues().slice(1).map((row) => Array.isArray(row) ? row.slice(1).map((cell) => cell ?? "") : []); }