From 58aae7c92fc110db792084bfffb6456a4ac8793e Mon Sep 17 00:00:00 2001 From: yu <191972707@qq.com> Date: Tue, 3 Nov 2026 15:51:45 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=A4=A7=E5=B0=8F=E5=86=99=E8=BD=AC?= =?UTF-8?q?=E6=8D=A2=E7=BB=84=E4=BB=B6=E9=80=89=E9=A1=B9=E5=80=BC1/2?= =?UTF-8?q?=E4=B8=8D=E7=94=9F=E6=95=88(=E9=80=89=E8=BD=AC=E6=8D=A2?= =?UTF-8?q?=E6=96=B9=E5=BC=8F=E8=BF=94=E5=9B=9E=E5=8E=9F=E6=96=87),=20CSV?= =?UTF-8?q?=E8=AF=BB=E5=86=99=E7=A9=BA=E8=B7=AF=E5=BE=84=E7=BB=99=E7=BB=93?= =?UTF-8?q?=E6=9E=84=E5=8C=96=E6=8A=A5=E9=94=99,=20=E8=A1=A5=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=A4=84=E7=90=86=E4=B8=8E=E6=96=87=E4=BB=B6=E6=97=8F?= =?UTF-8?q?=E5=86=92=E7=83=9F=E5=9B=9E=E5=BD=9211=E9=A1=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/capabilities/data-file-smoke.test.ts | 286 ++++++++++++++++++ src/main/capabilities/data.ts | 18 +- 2 files changed, 296 insertions(+), 8 deletions(-) create mode 100644 src/main/capabilities/data-file-smoke.test.ts diff --git a/src/main/capabilities/data-file-smoke.test.ts b/src/main/capabilities/data-file-smoke.test.ts new file mode 100644 index 0000000..c5487f0 --- /dev/null +++ b/src/main/capabilities/data-file-smoke.test.ts @@ -0,0 +1,286 @@ +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { readFileSync } from "node:fs"; +import { afterAll, describe, expect, it } from "vitest"; +import { getHandler } from "../engine/registry"; +import { RuntimeError } from "../engine/types"; +import type { CatalogComponent, FlowNode } from "../../shared/types"; +import type { RunContext } from "../engine/types"; +import "./data"; +import "./data-extended"; +import "./files"; +import "./control"; + +const ctx = { + signal: new AbortController().signal, + log: () => {}, + vars: {} as Record, + evidenceDir: "D:/office-test2", + runId: "smoke-run", + flow: { id: "smoke-flow", name: "冒烟", description: "", enabled: true, trigger: null, meta: { appVersion: "dev", createdAt: "", updatedAt: "" }, nodes: [] } +} as unknown as RunContext; + +const node = { id: "smoke-data", componentId: "smoke", position: { x: 0, y: 0 }, values: {}, options: {} } as unknown as FlowNode; + +async function run(componentId: string, args: Record, context?: RunContext): Promise { + const handler = getHandler(componentId); + if (!handler) throw new Error(`组件 ${componentId} 未注册运行时`); + return handler(context ?? ctx, { ...node, componentId } as unknown as FlowNode, args, {} as CatalogComponent); +} + +const sandbox = mkdtempSync(path.join(tmpdir(), "rpa-data-smoke-")); +afterAll(() => { rmSync(sandbox, { recursive: true, force: true }); }); + +describe("数据处理/文件/编排 冒烟", () => { + it("字符串族 StrUtil", async () => { + expect(await run("StrUtil_Len", { string: "你好world" })).toBe(7); + expect(await run("StrUtil_InStr", { string: "hello", search: "ll" })).toBe(2); + expect(await run("StrUtil_Replace", { string: "a-b-c", search: "-", replace: "+" })).toBe("a+b+c"); + expect(await run("StrUtil_LowerCase", { string: "AbC" })).toBe("abc"); + expect(await run("StrUtil_UpperCase", { string: "AbC" })).toBe("ABC"); + expect(await run("StrUtil_CaseChange", { string: "abc", case: "首字母大写" })).toBe("Abc"); + expect(await run("StrUtil_CaseChange", { string: "abc", case: "1" })).toBe("ABC"); + expect(await run("StrUtil_CaseChange", { string: "ABC", case: "2" })).toBe("abc"); + expect(await run("StrUtil_Trim", { string: " x " })).toBe("x"); + expect(await run("StrUtil_Left", { string: "hello", length: 2 })).toBe("he"); + expect(await run("StrUtil_Right", { string: "hello", length: 2 })).toBe("lo"); + expect(await run("StrUtil_Mid", { string: "hello", start: 1, length: 3 })).toBe("ell"); + expect(await run("StrUtil_CutOutStrV1", { string: "a[内容]b", start_cut_pos: "a[", end: "]" })).toContain("内容"); + expect(await run("StrUtil_Split", { string: "a,b,c", spliter: "," })).toEqual(["a", "b", "c"]); + expect(await run("StrUtil_SplitStr", { string: "a; b ;c", diy_symbol: ";" })).toEqual(["a", "b", "c"]); + expect(await run("StrUtil_AddToStr", { string: "尾", add_str: "头", add_type: "添加到前面", symbol: "-" })).toBe("头-尾"); + expect(await run("StrUtil_ReverseStr", { string: "ab cd", letter_reverse: true })).toBe("dc ba"); + expect(await run("StrUtil_ListToString", { str_list: ["a", "b"], connectors_custom: "|" })).toBe("a|b"); + expect(await run("StrUtil_CompleteStr", { string: "7", sum_length: 3, com_string: "0", com_type: "左边补" })).toBe("007"); + expect(await run("StrUtil_CharCode", { code: 65 })).toBe("A"); + expect(await run("StrUtil_CharAsc", { char: "A" })).toBe(65); + expect(await run("StrUtil_CreateRandomStr", { string_length: 12 })).toHaveLength(12); + console.log("[字符串族] 20 项通过"); + }); + + it("列表与集合族 List/Collection", async () => { + expect(await run("List_CreateList", { data: JSON.stringify([1, 2]) })).toEqual([1, 2]); + expect(await run("List_ListLength", { arrayRet: [1, 2, 3] })).toBe(3); + expect(await run("List_InsertList", { arrayRet: ["a", "c"], data: "b", insertType: "指定位置", position: 1 })).toEqual(["a", "b", "c"]); + expect(await run("List_DeleteList", { arrayRet: ["a", "b", "c"], data: "b" })).toEqual(["a", "c"]); + expect(await run("List_DeleteList", { arrayRet: ["a", "b", "c"], deleteType: "按位置", position: 0 })).toEqual(["b", "c"]); + expect(await run("List_GetListItem", { arrayRet: [10, 20], position: 1 })).toBe(20); + expect(await run("List_SetListItem", { arrayRet: [1, 2], data: 9, position: 0 })).toEqual([9, 2]); + expect(await run("List_GetIndex", { arrayRet: ["x", "y"], data: "y" })).toBe(1); + expect(await run("List_ListMerge", { arrayRet1: [1], arrayRet2: [2, 3] })).toEqual([1, 2, 3]); + expect(await run("List_ListDuplication", { arrayRet: [1, 1, 2] })).toEqual([1, 2]); + expect(await run("List_ListSorted", { arrayRet: [3, 1, 2] })).toEqual([1, 2, 3]); + expect(await run("List_ListIntersection", { arrayRet1: [1, 2, 3], arrayRet2: [2, 3, 4] })).toEqual([2, 3]); + expect(await run("List_ClearList", {})).toEqual([]); + expect(await run("Collection_Create", {})).toEqual([]); + const withItem = await run("Collection_Add", { collection: [], value: "a" }) as unknown[]; + expect(withItem).toEqual(["a"]); + expect(await run("Collection_HasItem", { collection: ["a"], item: "a" })).toBe(true); + expect(await run("Collection_HasItem", { collection: ["a"], item: "z" })).toBe(false); + expect(await run("Collection_Count", { collection: ["a", "b"] })).toBe(2); + expect(await run("Collection_Remove", { collection: ["a", "b"], value: "a" })).toEqual(["b"]); + console.log("[列表/集合族] 18 项通过"); + }); + + it("字典族 Dict", async () => { + expect(await run("Dict_Create", { value: JSON.stringify({ a: 1 }) })).toEqual({ a: 1 }); + expect(await run("Dict_SetItem", { dictObj: { a: 1 }, key: "b", value: 2 })).toEqual({ a: 1, b: 2 }); + expect(await run("Dict_GetItem", { dictObj: { a: 1 }, key: "a" })).toBe(1); + expect(await run("Dict_GetItem", { dictObj: {}, key: "x", default_value: "dft" })).toBe("dft"); + expect(await run("Dict_Keys", { dictObj: { a: 1, b: 2 } })).toEqual(["a", "b"]); + expect(await run("Dict_Values", { dictObj: { a: 1, b: 2 } })).toEqual([1, 2]); + expect(await run("Dict_Items", { dictObj: { a: 1 } })).toEqual([["a", 1]]); + expect(await run("Dict_Delete", { dictObj: { a: 1, b: 2 }, key: "a" })).toEqual({ b: 2 }); + expect(await run("Dict_Update", { dictObj: { a: 1 }, targetDict: { b: 2 } })).toEqual({ a: 1, b: 2 }); + expect(await run("Dict_Clear", {})).toEqual({}); + console.log("[字典族] 10 项通过"); + }); + + it("数学族 Math 与正则族 Regex", async () => { + expect(await run("Math_Abs", { data: -3.5 })).toBe(3.5); + expect(await run("Math_Round", { data: 3.14159, digit: 2 })).toBe(3.14); + expect(await run("Math_Int", { data: 3.2, position: "向上取整" })).toBe(4); + expect(await run("Math_Sqrt", { data: 16 })).toBe(4); + expect(await run("Math_Pow", { x: 2, y: 10 })).toBe(1024); + expect(await run("Math_Sgn", { data: -9 })).toBe(-1); + expect(await run("Math_Exp", { data: 0 })).toBe(1); + expect(await run("Math_Hex", { num: 255 })).toBe("FF"); + expect(await run("Math_Oct", { num: 8 })).toBe("10"); + expect(await run("Math_Scale", { sourceNum: "1010", sourceNumScale: 2, targetNumScale: 10 })).toBe("10"); + expect(await run("Math_Sin", { data: 90, unit: "角度" })).toBeCloseTo(1, 5); + expect(await run("Regex_Test", { text: "abc123", pattern: "\\d+" })).toBe(true); + expect(await run("Regex_TestNew", { text: "abc", pattern: "\\d" })).toBe(false); + expect(await run("Regex_FindAll", { text: "a1b22c333", pattern: "\\d+" })).toEqual(["1", "22", "333"]); + expect(await run("Regex_FindStr", { text: "订单A-100,订单B-200", pattern: "订单[AB]-(\\d+)", index: 1 })).toBe("100"); + expect(await run("Regex_FindAllNew", { text: "2026-11-03", pattern: "\\d+" })).toEqual(["2026", "11", "03"]); + console.log("[数学/正则族] 16 项通过"); + }); + + it("时间族 Time", async () => { + expect(String(await run("Time_Now", {}))).toMatch(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/); + expect(String(await run("Time_Date", {}))).toMatch(/^\d{4}-\d{2}-\d{2}$/); + expect(String(await run("Time_Time", {}))).toMatch(/^\d{2}:\d{2}:\d{2}$/); + const ts = await run("Time_TimeStamp", {}) as number; + expect(ts).toBeGreaterThan(1_700_000_000); + expect(await run("Time_Format", { timestamp: 1767000000, format: "YYYY-MM-DD" })).toMatch(/^\d{4}-\d{2}-\d{2}$/); + expect(await run("Time_DatetimeToTimestamp", { datetime: "2026-01-01 00:00:00" })).toBe(typeof 1_767_225_600 === "number" ? Math.floor(new Date("2026-01-01 00:00:00").getTime() / 1000) : 0); + expect(await run("Time_Year", { datetime: "2026-11-03 10:20:30" })).toBe(2026); + expect(await run("Time_Month", { datetime: "2026-11-03 10:20:30" })).toBe(11); + expect(await run("Time_Day", { datetime: "2026-11-03 10:20:30" })).toBe(3); + expect(await run("Time_Hour", { datetime: "2026-11-03 10:20:30" })).toBe(10); + expect(await run("Time_Minute", { datetime: "2026-11-03 10:20:30" })).toBe(20); + expect(await run("Time_Second", { datetime: "2026-11-03 10:20:30" })).toBe(30); + expect(typeof (await run("Time_WeekDay", { datetime: "2026-11-03" }))).toBe("number"); + expect(await run("Time_DateOffset_V1", { date_input: "2026-11-01", offset_unit_num: 2, offset_status: "加", get_type: "YYYY-MM-DD" })).toBe("2026-11-03"); + expect(await run("Time_CalDate", { date_start: "2026-11-01", date_end: "2026-11-03", time_type: "天" })).toBeCloseTo(2, 5); + expect(await run("Time_TimeTypeConversion", { datetime: "2026-11-03 08:00:00", format: "YYYY/MM/DD" })).toBe("2026/11/03"); + console.log("[时间族] 15 项通过"); + }); + + it("JSON 与 CSV 读写回环", async () => { + expect(await run("JSON_Stringify", { string: { a: 1 } })).toBe('{"a":1}'); + expect(await run("JSON_Parse", { string: '{"a":1}' })).toEqual({ a: 1 }); + expect(await run("JSON_Parse", { string: "bad" })).toBeNull(); + const csvPath = path.join(sandbox, "t.csv"); + await run("CSV_WriteNew", { path: csvPath, table: [["名", "值"], ["a,b", "含\"引号\""], ["c", "d"]] }); + const back = await run("CSV_ReadNew", { path: csvPath }) as string[][]; + expect(back[0]).toEqual(["名", "值"]); + expect(back[1]).toEqual(["a,b", '含"引号"']); + console.log("[JSON/CSV] 通过(含引号逗号转义回环)"); + }); + + it("Ini 族完整生命周期", async () => { + const iniPath = path.join(sandbox, "config.ini"); + await run("Ini_Write", { file: iniPath, section: "server", key: "host", value: "127.0.0.1" }); + await run("Ini_Write", { file: iniPath, section: "server", key: "port", value: "8080" }); + await run("Ini_Write", { file: iniPath, section: "log", key: "level", value: "info" }); + expect(await run("Ini_Read", { file: iniPath, section: "server", key: "host" })).toBe("127.0.0.1"); + expect(await run("Ini_ListSection", { file: iniPath })).toEqual(["server", "log"]); + expect(await run("Ini_ListKey", { file: iniPath, section: "server" })).toEqual(["host", "port"]); + expect(await run("Ini_DeleteKey", { file: iniPath, section: "server", key: "port" })).toBe(true); + expect(await run("Ini_Read", { file: iniPath, section: "server", key: "port" })).toBe(""); + expect(await run("Ini_DeleteSection", { file: iniPath, section: "log" })).toBe(true); + expect(await run("Ini_ListSection", { file: iniPath })).toEqual(["server"]); + console.log("[Ini族] 写读列举删除全链通过"); + }); + + it("文件族全生命周期(沙盒目录)", async () => { + const dir = path.join(sandbox, "files"); + const created = await run("File_CreateFile", { folder_path: dir, name: "a.txt" }) as string; + expect(created).toContain("a.txt"); + await run("File_Write", { path: created, content: "第一行" }); + await run("File_Write", { path: created, content: "第二行", append: true }); + expect(await run("File_Read", { path: created })).toBe("第一行第二行"); + expect(await run("File_FileExists", { path: created })).toBe(true); + expect(await run("File_PathExists", { path: path.join(dir, "nope.txt") })).toBe(false); + expect(await run("File_PathSize", { path: created })).toBeGreaterThan(0); + const info = await run("File_GetFileInfo", { file_path: created }) as { name: string; size: number }; + expect(info.name).toBe("a.txt"); + expect(info.size).toBeGreaterThan(0); + const copy = path.join(dir, "b.txt"); + await run("File_Copy", { pathSrc: created, pathDest: copy }); + expect(await run("File_Read", { path: copy })).toBe("第一行第二行"); + const sub = path.join(dir, "sub"); + await run("File_CreateFolder", { path: sub }); + await run("File_Copy", { pathSrc: copy, pathDest: path.join(sub, "c.txt") }); + const flat = await run("File_GetFileList", { folder_path: dir }) as string[]; + expect(flat.length).toBe(2); + const all = await run("File_GetFileList", { folder_path: dir, recursive: true }) as string[]; + expect(all.length).toBe(3); + const kw = await run("File_GetFileList", { folder_path: dir, key_word: "a." }) as string[]; + expect(kw.length).toBe(1); + const moved = await run("File_Move", { pathSrc: copy, pathDest: path.join(dir, "moved.txt") }); + expect(String(moved)).toContain("moved.txt"); + expect(await run("File_FileExists", { path: copy })).toBe(false); + const renamed = await run("File_Rename", { pathSrc: moved, name: "renamed.txt" }); + expect(String(renamed)).toContain("renamed.txt"); + const zip = await run("File_CompressFile", { pathSrc: created, pathDest: path.join(sandbox, "pack.zip") }); + expect(String(zip)).toContain("pack.zip"); + const unzipDir = path.join(sandbox, "unpacked"); + await run("File_DecompressFile", { path: path.join(sandbox, "pack.zip"), target: unzipDir }); + expect(await run("File_FileExists", { path: path.join(unzipDir, "a.txt") })).toBe(true); + expect(await run("File_WaitFile", { file_path: created, time_out: 1 })).toBe(true); + await expect(run("File_WaitFile", { file_path: path.join(dir, "ghost.txt"), time_out: 1 })).rejects.toThrow(/超时/); + await run("File_Delete", { file_path: renamed }); + expect(await run("File_FileExists", { path: renamed })).toBe(false); + console.log("[文件族] 建/写/读/拷/列举/移/改名/压缩解压/等待/删除 全链通过"); + }, 60000); + + it("数据表族 Bivartable", async () => { + const table = [["部门", "姓名", "工资"], ["技术", "张三", 100], ["销售", "李四", 80], ["技术", "王五", 90], ["销售", "李四", 80]]; + const bodyOnly = [["技术", "张三", 100], ["销售", "李四", 80], ["技术", "王五", 90], ["销售", "李四", 80]]; + const withHeader = await run("Bivartable_DoCreate", { data: JSON.stringify(bodyOnly), columnList: JSON.stringify(["部门", "姓名", "工资"]) }) as unknown[][]; + expect(withHeader[0]).toEqual(["部门", "姓名", "工资"]); + expect(withHeader.length).toBe(5); + expect(await run("Bivartable_GetSumValue", { df: table, sumvaluecols: JSON.stringify(["工资"]) })).toBe(350); + expect(await run("Bivartable_GetMaxValue", { df: table, maxvaluecols: JSON.stringify(["工资"]) })).toBe(100); + expect(await run("Bivartable_GetMinValue", { df: table, minvaluecols: JSON.stringify(["工资"]) })).toBe(80); + expect(await run("Bivartable_GetAvgValue", { df: table, avgvaluecols: JSON.stringify(["工资"]) })).toBe(87.5); + const grouped = await run("Bivartable_GetSumValue", { df: table, sumvaluecols: JSON.stringify(["工资"]), groupby: JSON.stringify(["部门"]) }) as unknown[][]; + expect(grouped[0]).toEqual(["部门", "工资"]); + expect(grouped).toContainEqual(["技术", 190]); + expect(grouped).toContainEqual(["销售", 160]); + const filtered = await run("Bivartable_DoFilter", { df: table, col_name: "部门", choice: "1", criteria: "技术" }) as unknown[][]; + expect(filtered.length).toBe(3); + const expr = await run("Bivartable_DoFilter", { df: table, rule: '["工资"] > 85' }) as unknown[][]; + expect(expr.length).toBe(3); + const dedup = await run("Bivartable_DeleteDuplicateRow", { df: table }) as unknown[][]; + expect(dedup.length).toBe(4); + console.log("[数据表族] 创建/聚合(含分组)/过滤(简单+表达式)/去重 通过"); + }); + + it("编排控制与日志", async () => { + const varsCtx = { ...ctx, vars: {} } as unknown as RunContext; + expect(await run("logic_control_var", { var_name: "k1", var_value: "v1" }, varsCtx)).toBe("v1"); + expect((varsCtx as unknown as { vars: Record }).vars.k1).toBe("v1"); + await expect(run("logic_control_var", {})).rejects.toThrow(/变量名称/); + await expect(run("logic_control_break_for", {})).rejects.toBeTruthy(); + await expect(run("logic_control_next_for", {})).rejects.toBeTruthy(); + await expect(run("logic_control_stop", {})).rejects.toBeTruthy(); + await expect(run("logic_control_stop_return", {})).rejects.toBeTruthy(); + expect(await run("Trigger.Manual", {})).toBeNull(); + expect(await run("Trigger.Schedule", {})).toBeNull(); + expect(await run("Trigger.FileArrival", {})).toBeNull(); + const logs: Array<{ m: string; lv: string }> = []; + const logCtx = { ...ctx, log: (m: string, lv: string) => logs.push({ m, lv }) } as unknown as RunContext; + expect(await run("Log_Info", { msg: "提示信息" }, logCtx)).toBe("提示信息"); + expect(await run("Log_Error", { msg: "错误信息" }, logCtx)).toBe("错误信息"); + expect(logs.some((l) => l.lv === "error")).toBe(true); + await expect(run("ExceptionUtil_Throws", { msg: "自定义异常" })).rejects.toThrow(/自定义异常/); + console.log("[编排/日志] 变量/循环控制信号/触发器/日志/自定义异常 通过"); + }); + + it("全量健康检查: 已注册组件空参调用不产生非结构化崩溃", async () => { + const ids: string[] = JSON.parse( + readFileSync("D:/wordkb/2026-10-28-10-36-47/PRA-only-demo/live-ids.json", "utf8") + ) as string[]; + const skip = new Set([ + "FTP_Connect", "FTP_Upload", "FTP_Download", "FTP_Delete", "FTP_Rename", "FTP_Cwd", "FTP_GetFileList", "FTP_CreateDir", "FTP_Exist", "FTP_Disconnect", + "Database_DBConnect", "Database_SQLExecute", "Database_SingleSQLQuery", "Database_AllSQLQuery_V1", "Database_BatchSQLExecute", "Database_BatchInsertExecute", "Database_CloseDBConnect", + "File_OpenFile", "File_StartBusiness", "File_UploadBusinessResults", "File_UploadBusinessResults_v1", "File_Collectupload", + "File_Delete", "File_DeleteFile", "File_EmptyFolder", "File_WaitFile", + "logic_control_break_for", "logic_control_next_for", "logic_control_stop", "logic_control_stop_return", + "child_modules_v1", "concurrent_child_process_v1", "Element_exception_monitor_v2", "logic_control_if_ai_vision" + ]); + const crashes: Array<{ id: string; error: string }> = []; + let checked = 0; + for (const id of ids) { + const handler = getHandler(id); + if (!handler || skip.has(id)) continue; + checked += 1; + try { + await handler(ctx, { ...node, componentId: id } as unknown as FlowNode, {}, {} as CatalogComponent); + } catch (error) { + if (!(error instanceof RuntimeError)) { + crashes.push({ id, error: `${(error as Error).name}: ${(error as Error).message}` }); + } + } + } + console.log(`[全量健康检查] 空参调用 ${checked} 个已注册组件, 结构化报错全部正常, 非结构化崩溃 ${crashes.length} 个`); + if (crashes.length) console.log(crashes.map((c) => `${c.id}: ${c.error}`).join("\n")); + expect(crashes).toEqual([]); + }, 120000); +}); + + diff --git a/src/main/capabilities/data.ts b/src/main/capabilities/data.ts index 607a4f0..50e042a 100644 --- a/src/main/capabilities/data.ts +++ b/src/main/capabilities/data.ts @@ -120,9 +120,10 @@ register(["StrUtil_UpperCase"], (_ctx, _node, args) => str(first(args, ["string" 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(); + // 目录选项值为 "1"(大写)/"2"(小写),文字模式兼容 大写/小写/首字母大写 if (mode.includes("首")) return text ? text[0].toUpperCase() + text.slice(1) : ""; + if (mode === "1" || mode.includes("大写")) return text.toUpperCase(); + if (mode === "2" || mode.includes("小写")) return text.toLowerCase(); return text; }); register("StrUtil_Trim", (_ctx, _node, args) => str(first(args, ["string"])).trim()); @@ -463,18 +464,19 @@ function toCsv(rows: unknown[][]): string { } register(["CSV_Read", "CSV_ReadNew"], async (_ctx, node, args) => { - const filePath = path.resolve(str(first(args, ["path"]))); + const filePath = str(first(args, ["path"])).trim(); if (!filePath) throw new RuntimeError("validation_error", "CSV 路径为空", { nodeId: node.id, componentId: node.componentId }); - const text = await fs.readFile(filePath, "utf8"); + const text = await fs.readFile(path.resolve(filePath), "utf8"); return parseCsv(text); }); register(["CSV_Write", "CSV_WriteNew"], async (_ctx, node, args) => { - const filePath = path.resolve(str(first(args, ["path"]))); + const filePath = str(first(args, ["path"])).trim(); 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; + const resolved = path.resolve(filePath); + await fs.mkdir(path.dirname(resolved), { recursive: true }); + await fs.writeFile(resolved, toCsv(table), "utf8"); + return resolved; }); /* ---------------- Basic ---------------- */