文件
PRA-only-demo/packages/rpa-core/src/capabilities/office.ts
T
yu caa8e19332 阶段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被拒, 环境阻塞)
2026-11-04 12:36:03 +08:00

250 行
13 KiB
TypeScript
原始文件 Blame 文件历史

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
import ExcelJS from "exceljs";
import { promises as fs } from "node:fs";
import path from "node:path";
import { register } from "../engine/registry";
import { RuntimeError, type CapabilityHandler } from "../engine/types";
import { defaultEngine } from "../engine/engine";
import { acquireWorkbook, publishWorkbook, releaseWorkbook } from "./excel-store";
interface ExcelHandle {
id: string;
path: string;
}
// 句柄 id → 工作簿;本体挂在引擎实例上(任务 1.3),此处委托 defaultEngine。
const workbooks = defaultEngine.excelHandles;
function value(args: Record<string, unknown>, keys: string[], fallback = ""): unknown {
for (const key of keys) {
if (args[key] !== undefined && args[key] !== null && args[key] !== "") return args[key];
}
return fallback;
}
function text(args: Record<string, unknown>, keys: string[], fallback = ""): string {
return String(value(args, keys, fallback));
}
function bool(valueToRead: unknown, fallback = false): boolean {
if (valueToRead === undefined || valueToRead === null || valueToRead === "") return fallback;
if (typeof valueToRead === "string") return ["true", "1", "yes", "是", "开启"].includes(valueToRead.trim().toLowerCase());
return [true, "true", "1", 1, "是", "开启"].includes(valueToRead as never);
}
function required(valueToRead: unknown, label: string, nodeId: string, componentId: string): string {
const result = String(valueToRead || "").trim();
if (!result) throw new RuntimeError("validation_error", `${label}为空`, { nodeId, componentId, recommendation: `请配置${label}` });
return result;
}
function newHandle(filePath: string, workbook: ExcelJS.Workbook): ExcelHandle {
const handle = { id: `excel_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 7)}`, path: filePath };
workbooks.set(handle.id, workbook);
publishWorkbook(filePath, workbook);
return handle;
}
async function resolveWorkbook(args: Record<string, unknown>, nodeId: string, componentId: string): Promise<{ handle: ExcelHandle; workbook: ExcelJS.Workbook }> {
const raw = value(args, ["workbook", "objExcel", "objExcelWorkBook", "file", "path"]);
const id = typeof raw === "object" && raw !== null ? String((raw as Partial<ExcelHandle>).id || "") : String(raw || "");
const existing = id ? workbooks.get(id) : undefined;
// 直接返回流程变量持有的原始句柄对象:saveAs 等处理器对其 path 的改写需要
// 同步反映到后续节点引用的同一个对象上
if (existing) return { handle: raw as ExcelHandle, workbook: existing };
const filePath = required(typeof raw === "object" ? (raw as Partial<ExcelHandle>).path : raw, "Excel文件路径", nodeId, componentId);
const workbook = await acquireWorkbook(filePath);
return { handle: { id, path: filePath }, workbook };
}
function sheetOf(workbook: ExcelJS.Workbook, name: string): ExcelJS.Worksheet {
const sheet = name ? workbook.getWorksheet(name) : workbook.worksheets[0];
if (!sheet) throw new Error(`Sheet 不存在:${name || "第一个 Sheet"}`);
return sheet;
}
function parseRange(range: string, sheet: ExcelJS.Worksheet): { start: string; end?: string } {
if (range.trim()) {
const [start, end] = range.split(":").map((item) => item.trim());
return { start, end };
}
const lastRow = Math.max(1, sheet.rowCount);
const lastColumn = Math.max(1, sheet.columnCount);
return { start: "A1", end: `${sheet.getColumn(lastColumn).letter}${lastRow}` };
}
function cellValue(valueToRead: ExcelJS.CellValue): unknown {
if (valueToRead && typeof valueToRead === "object" && "text" in valueToRead) return valueToRead.text;
if (valueToRead && typeof valueToRead === "object" && "result" in valueToRead) return valueToRead.result;
return valueToRead ?? "";
}
function tableFromSheet(sheet: ExcelJS.Worksheet, range: string): unknown[][] {
const target = parseRange(range, sheet);
const startRow = Number(sheet.getCell(target.start).row);
const endRow = target.end ? Number(sheet.getCell(target.end).row) : sheet.rowCount;
const rows = sheet.getRows(startRow, Math.max(1, endRow - startRow + 1)) || [];
const startColumn = Number(sheet.getCell(target.start).col);
const endColumn = target.end ? Number(sheet.getCell(target.end).col) : sheet.columnCount;
return rows.map((row) => {
const values: unknown[] = [];
for (let column = startColumn; column <= endColumn; column += 1) values.push(cellValue(row.getCell(column).value));
return values;
});
}
function rowsFrom(valueToRead: unknown): unknown[][] {
if (Array.isArray(valueToRead)) {
return valueToRead.map((row) => Array.isArray(row) ? row : [row]);
}
if (typeof valueToRead === "string") {
try {
return rowsFrom(JSON.parse(valueToRead));
} catch {
return [[valueToRead]];
}
}
return [[valueToRead]];
}
const newExcel: CapabilityHandler = async (_ctx, node, args) => {
const folder = required(value(args, ["filePath", "path", "folder"]), "文件路径", node.id, node.componentId);
const name = text(args, ["name"], "新建工作簿.xlsx");
const filePath = path.resolve(folder, path.extname(name) ? name : `${name}.xlsx`);
const makedirs = bool(value(args, ["isMakedirs"]), true);
if (!makedirs) {
try {
await fs.access(path.dirname(filePath));
} catch {
throw new RuntimeError("validation_error", `目录不存在:${path.dirname(filePath)}`, { nodeId: node.id, componentId: node.componentId, recommendation: "将「是否创建该路径」设为 True,或先创建目录" });
}
}
if (!bool(value(args, ["override"]), true)) {
try {
await fs.access(filePath);
throw new RuntimeError("validation_error", `文件已存在:${filePath}`, { nodeId: node.id, componentId: node.componentId, recommendation: "开启覆盖同名文件,或修改文件名称" });
} catch (error) {
if (error instanceof RuntimeError) throw error;
}
}
const workbook = new ExcelJS.Workbook();
workbook.addWorksheet("Sheet1");
await fs.mkdir(path.dirname(filePath), { recursive: true });
await workbook.xlsx.writeFile(filePath);
return newHandle(filePath, workbook);
};
const openExcel: CapabilityHandler = async (_ctx, node, args) => {
const filePath = path.resolve(required(value(args, ["file", "filePath", "path"]), "Excel文件", node.id, node.componentId));
const workbook = await acquireWorkbook(filePath);
return newHandle(filePath, workbook);
};
const readExcel: CapabilityHandler = async (_ctx, node, args) => {
const { workbook } = await resolveWorkbook(args, node.id, node.componentId);
const sheet = sheetOf(workbook, text(args, ["sheet", "sheetName"]));
return tableFromSheet(sheet, text(args, ["range", "excelRng"]));
};
const writeCell: CapabilityHandler = async (_ctx, node, args) => {
const { handle, workbook } = await resolveWorkbook(args, node.id, node.componentId);
const sheet = sheetOf(workbook, text(args, ["sheet", "sheetName"]));
const address = `${text(args, ["column"], "A")}${text(args, ["row"], "1")}`;
sheet.getCell(address).value = value(args, ["content", "value"]) as ExcelJS.CellValue;
if (bool(value(args, ["save"]), true)) await workbook.xlsx.writeFile(handle.path);
return handle;
};
const writeRange: CapabilityHandler = async (_ctx, node, args) => {
const { handle, workbook } = await resolveWorkbook(args, node.id, node.componentId);
const sheet = sheetOf(workbook, text(args, ["sheet", "sheetName"]));
const cellPos = text(args, ["cellPos"]);
const cellMatch = cellPos.match(/^([A-Za-z]+)(\d+)$/);
const column = cellMatch ? cellMatch[1] : text(args, ["column"], "A");
const row = cellMatch ? cellMatch[2] : text(args, ["row"], "1");
const start = `${column}${row}`;
const startCell = sheet.getCell(start);
const startRow = startCell.row;
const startColumn = startCell.col;
for (const [rowIndex, row] of rowsFrom(value(args, ["contents", "data", "table"])).entries()) {
for (const [columnIndex, cell] of row.entries()) sheet.getCell(startRow + rowIndex, startColumn + columnIndex).value = cell as ExcelJS.CellValue;
}
if (bool(value(args, ["save", "isSave"]), true)) await workbook.xlsx.writeFile(handle.path);
return handle;
};
const saveExcel: CapabilityHandler = async (_ctx, node, args) => {
const { handle, workbook } = await resolveWorkbook(args, node.id, node.componentId);
await workbook.xlsx.writeFile(handle.path);
return handle.path;
};
const saveAsExcel: CapabilityHandler = async (_ctx, node, args) => {
const { handle, workbook } = await resolveWorkbook(args, node.id, node.componentId);
// CS 版(Excel_Cs_SaveAs)用 path=目录 + name=文件名;Legacy(Excel_SaveAs)用 filename=完整路径
const rawName = text(args, ["name"]);
const raw = rawName
? path.resolve(text(args, ["path", "file", "savePath"], ""), rawName)
: required(value(args, ["path", "file", "savePath", "filename", "saveFilePath"]), "另存为路径", node.id, node.componentId);
// Legacy Excel_SaveAs 用 isAbsolutePath 标记 filename 是否为绝对路径,相对路径基于原文件目录
const absolute = rawName ? true : bool(value(args, ["isAbsolutePath"]), true);
const previousPath = handle.path;
handle.path = absolute ? path.resolve(raw) : path.resolve(path.dirname(previousPath), raw);
await fs.mkdir(path.dirname(handle.path), { recursive: true });
await workbook.xlsx.writeFile(handle.path);
// 另存后同一实例以新路径登记,旧路径的缓存不再代表最新内容,释放之
publishWorkbook(handle.path, workbook);
if (path.resolve(previousPath) !== handle.path) releaseWorkbook(previousPath);
return handle;
};
register(["Excel_Cs_NewV5", "Excel_Cs_New", "Excel_New"], newExcel);
register(["Excel_Cs_OpenV4", "Excel_Cs_Open", "Excel_Open"], openExcel);
register(["Excel_Cs_ReadContentV2", "Excel_Cs_ReadRangeV2", "Excel_ReadRange"], readExcel);
register(["Excel_Cs_WriteCell", "Excel_WriteCell"], writeCell);
register(["Excel_Cs_WriteRange", "Excel_WriteRange"], writeRange);
register(["Excel_Save", "Excel_Cs_Save", "Excel_SaveV2"], saveExcel);
register(["Excel_Cs_SaveAs", "Excel_SaveAs"], saveAsExcel);
register(["Excel_GetSheetsName", "Excel_GetSheetNames"], async (_ctx, node, args) => {
const { workbook } = await resolveWorkbook(args, node.id, node.componentId);
return workbook.worksheets.map((sheet) => sheet.name);
});
register(["Excel_Cs_Rename", "Excel_RenameSheet"], async (_ctx, node, args) => {
const { handle, workbook } = await resolveWorkbook(args, node.id, node.componentId);
const oldName = required(value(args, ["sheet", "oldName"]), "原 Sheet 名称", node.id, node.componentId);
const newName = required(value(args, ["newSheet", "newName", "name", "sheetName"]), "新 Sheet 名称", node.id, node.componentId);
const sheet = sheetOf(workbook, oldName);
sheet.name = newName;
await workbook.xlsx.writeFile(handle.path);
return handle;
});
register(["Excel_Cs_Insert", "Excel_InsertSheet"], async (_ctx, node, args) => {
const { handle, workbook } = await resolveWorkbook(args, node.id, node.componentId);
const name = required(value(args, ["name", "sheetName"]), "Sheet名称", node.id, node.componentId);
if (workbook.getWorksheet(name)) throw new RuntimeError("validation_error", `Sheet 已存在:${name}`, { nodeId: node.id, componentId: node.componentId });
workbook.addWorksheet(name);
if (bool(value(args, ["save"]), false)) await workbook.xlsx.writeFile(handle.path);
return handle;
});
register("Excel_Cs_InsertRow", async (_ctx, node, args) => {
const { handle, workbook } = await resolveWorkbook(args, node.id, node.componentId);
const sheet = sheetOf(workbook, text(args, ["sheet", "sheetName"]));
const index = Math.max(1, Number(value(args, ["index"], "1")) || 1);
const count = Math.max(1, Number(value(args, ["count"], "1")) || 1);
const rows = rowsFrom(value(args, ["contents"]));
const inserts = Array.from({ length: count }, (_, rowIndex) => rows[rowIndex] || []);
sheet.spliceRows(index, 0, ...inserts);
if (bool(value(args, ["save"]), true)) await workbook.xlsx.writeFile(handle.path);
return handle;
});
register("Excel_Cs_InsertColumn", async (_ctx, node, args) => {
const { handle, workbook } = await resolveWorkbook(args, node.id, node.componentId);
const sheet = sheetOf(workbook, text(args, ["sheet", "sheetName"]));
const index = Math.max(1, Number(value(args, ["index"], "1")) || 1);
const count = Math.max(1, Number(value(args, ["count"], "1")) || 1);
const values = rowsFrom(value(args, ["contents"])).flat();
const inserts = Array.from({ length: count }, (_, columnIndex) => [values[columnIndex] ?? ""]);
sheet.spliceColumns(index, 0, ...inserts);
if (bool(value(args, ["save"]), true)) await workbook.xlsx.writeFile(handle.path);
return handle;
});