阶段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,172 @@
|
||||
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<string, unknown> }): 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<string, unknown>) => {
|
||||
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<string, unknown>); 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<string, { rows: unknown[]; values: Map<string, number[]> }>();
|
||||
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<string> {
|
||||
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)); return exportExcel(table(args.df), filePath);
|
||||
});
|
||||
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<string, unknown>; meta[String(args.column_name)] = args.column_description; return meta; });
|
||||
register("Databook_GetColumnInfo", (ctx, _node, args) => { const meta = (ctx.vars[META] || {}) as Record<string, unknown>; return meta[String(args.column_name)] ?? ""; });
|
||||
|
||||
async function getHandlerResult(id: string, ctx: any, node: any, args: Record<string, unknown>): Promise<unknown> {
|
||||
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 ?? "") : []);
|
||||
}
|
||||
Reference in new issue
Block a user