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, 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, 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, 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).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).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; });