From d4de0ec245691120ad2ae252f8afe4d196203898 Mon Sep 17 00:00:00 2001 From: yu <191972707@qq.com> Date: Tue, 3 Nov 2026 15:22:53 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20ConvertBool=E6=95=B0=E5=AD=97=E8=AF=AD?= =?UTF-8?q?=E4=B9=89=E5=AF=B9=E9=BD=90(=E6=95=B0=E5=AD=971=E4=B8=BAtrue),?= =?UTF-8?q?=20=E5=87=AD=E6=8D=AE=E8=AF=BB=E5=86=99=E6=94=B9=E7=BB=8Fps?= =?UTF-8?q?=E5=BC=BA=E5=88=B6UTF8=E8=A7=A3=E7=A0=81=E4=B8=AD=E6=96=87?= =?UTF-8?q?=E5=B9=B6=E8=BF=94=E5=9B=9E=E6=95=B4=E6=9D=A1=E5=87=AD=E6=8D=AE?= =?UTF-8?q?=E5=9D=97,=20=E8=A1=A5=E5=BA=94=E7=94=A8=E4=B8=8E=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=E5=86=92=E7=83=9F=E5=9B=9E=E5=BD=9210=E9=A1=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../capabilities/app-system-smoke.test.ts | 157 ++++++++++++++++++ src/main/capabilities/data.ts | 6 +- src/main/capabilities/windows-system.ts | 23 ++- 3 files changed, 180 insertions(+), 6 deletions(-) create mode 100644 src/main/capabilities/app-system-smoke.test.ts diff --git a/src/main/capabilities/app-system-smoke.test.ts b/src/main/capabilities/app-system-smoke.test.ts new file mode 100644 index 0000000..942d5d4 --- /dev/null +++ b/src/main/capabilities/app-system-smoke.test.ts @@ -0,0 +1,157 @@ +import { describe, expect, it } from "vitest"; +import { getHandler } from "../engine/registry"; +import type { CatalogComponent, FlowNode } from "../../shared/types"; +import type { RunContext } from "../engine/types"; +import "./data"; +import "./desktop"; +import "./system-extended"; +import "./utility-extended"; +import "./common-final"; +import "./control"; +import "./windows-system"; +import "./basic-encryption"; + +const ctx = { + signal: new AbortController().signal, + log: () => {}, + vars: {}, + 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-app", componentId: "smoke", position: { x: 0, y: 0 }, values: {}, options: {} } as unknown as FlowNode; + +async function run(componentId: string, args: Record, ctxOverride?: RunContext): Promise { + const handler = getHandler(componentId); + if (!handler) throw new Error(`组件 ${componentId} 未注册运行时`); + const n = { ...node, componentId } as unknown as FlowNode; + return handler(ctxOverride ?? ctx, n, args, {} as CatalogComponent); +} + +describe("应用与系统组件冒烟", () => { + it("数据类型判断族", async () => { + expect(await run("Basic_IsNumber", { val: "12.5" })).toBe(true); + expect(await run("Basic_IsNumber", { val: "abc" })).toBe(false); + expect(await run("Basic_IsNull", { val: "" })).toBe(true); + expect(await run("Basic_IsNull", { val: "x" })).toBe(false); + expect(await run("Basic_IsArray", { val: [1, 2] })).toBe(true); + expect(await run("Basic_IsDictionary", { val: { a: 1 } })).toBe(true); + expect(await run("Basic_Type", { val: [1] })).toBe("list"); + expect(await run("Basic_Type", { val: "s" })).toBe("string"); + expect(await run("Basic_JudgeDataType", { val: 5, into_type: "int" })).toBe(true); + expect(await run("Basic_JudgeDataType", { val: 5.5, into_type: "int" })).toBe(false); + console.log("[数据判断族] 通过"); + }); + + it("数据转换族", async () => { + expect(await run("Basic_ConvertString", { val: 123 })).toBe("123"); + expect(await run("Basic_ConvertInt", { val: "12.9" })).toBe(12); + expect(await run("Basic_ConvertBool", { val: 1 })).toBe(true); + expect(await run("Basic_ConvertNumber", { val: "3.14" })).toBe(3.14); + expect(await run("Basic_DataTypeConversion", { val: "是", into_type: "bool" })).toBe(true); + expect(await run("Basic_DataTypeConversion", { val: 8, into_type: "string" })).toBe("8"); + console.log("[数据转换族] 通过"); + }); + + it("复制/随机/密文", async () => { + const src = { a: [1, { b: 2 }] }; + const cloned = (await run("Basic_Clone", { val: src })) as typeof src; + expect(cloned).toEqual(src); + expect(cloned).not.toBe(src); + const ri = (await run("Basic_RandomInt", { min: 5, max: 5 })) as number; + expect(ri).toBe(5); + const rnd = (await run("Basic_Rnd", {})) as number; + expect(rnd).toBeGreaterThanOrEqual(0); + expect(rnd).toBeLessThan(1); + expect(await run("Basic_Dodecryption", { words: Buffer.from("hello-世界", "utf8").toString("base64") })).toBe("hello-世界"); + console.log("[复制/随机/密文] 通过"); + }); + + it("延迟/调试输出/设置变量", async () => { + const logs: string[] = []; + const ctxLog = { ...ctx, log: (m: string) => logs.push(m), vars: {} } as unknown as RunContext; + const t0 = Date.now(); + expect(await run("Basic_Delay", { mileSecond: 100 })).toBe(0.1); + expect(Date.now() - t0).toBeGreaterThanOrEqual(95); + await run("Basic_DebugOutput", { msg: "冒烟输出" }, ctxLog); + expect(logs.join()).toContain("冒烟输出"); + const setNode = { ...node, options: { outputVar: "v_test" } } as unknown as FlowNode; + const handler = getHandler("Basic_SetVariable_V1")!; + expect(await handler(ctxLog, setNode, { value: "42", variableType: "rpa_int" }, {} as CatalogComponent)).toBe(42); + expect((ctxLog as unknown as { vars: Record }).vars.v_test).toBe(42); + console.log("[延迟/输出/变量] 通过"); + }); + + it("自定义配置与业务计数", async () => { + const cfg = (await run("CustomConfig_GetConfig", { + jsonlist: JSON.stringify([{ name: "host", value: "127.0.0.1" }]), + configName: "host" + })) as string; + expect(cfg).toBe("127.0.0.1"); + const ctxCount = { ...ctx, vars: {} } as unknown as RunContext; + expect(await run("Basic_BusinessOrderCount", { start_count: 100, count: 3 }, ctxCount)).toBe(103); + expect(await run("Basic_BusinessOrderCount", { start_count: 100, count: 3 }, ctxCount)).toBe(106); + console.log("[配置/计数] 通过"); + }); + + it("系统路径与环境变量", async () => { + expect(String(await run("System_GetTempPath", {}))).toMatch(/Temp|tmp/i); + expect(String(await run("System_GetHomePath", {}))).toMatch(/Users|home/i); + expect(String(await run("System_GetSystemPath", { path_name: "desktop" }))).toMatch(/Desktop/); + expect(await run("System_GetEnv", { name: "PATH" })).toBeTruthy(); + expect(await run("System_SetEnv", { name: "RPA_SMOKE", value: "ok" })).toBe("ok"); + expect(await run("System_GetEnv", { name: "RPA_SMOKE" })).toBe("ok"); + expect(await run("Basic_GetProcessInfo", {})).toBeTruthy(); + console.log("[系统路径/环境变量] 通过"); + }); + + it("输入法状态获取", async () => { + const st = (await run("System_GetInputMethodState", {})) as { culture: string; layout: string }; + console.log("[输入法]", JSON.stringify(st)); + expect(st.culture).toBeTruthy(); + }, 60000); + + it("执行命令行两种组件", async () => { + const out1 = String(await run("System_ExecuteCommand", { command: "echo rpa-smoke-123" })); + expect(out1).toContain("rpa-smoke-123"); + const out2 = String(await run("System_ExecuteCommandV1", { command_type: "1", dos_command: "echo dos-ok-456" })); + expect(out2).toContain("dos-ok-456"); + const out3 = String(await run("System_ExecuteCommandV1", { command_type: "3", ps_command: "Write-Output 'ps-ok-789'" })); + expect(out3).toContain("ps-ok-789"); + console.log("[执行命令] 通过"); + }, 120000); + + it("应用生命周期: 启动→查PID→关闭", async () => { + const pid = (await run("App_Run", { executeable: "notepad.exe" })) as number; + expect(Number.isInteger(pid)).toBe(true); + await new Promise((r) => setTimeout(r, 1200)); + // Win11 notepad.exe 为应用别名:Start-Process 返回的启动器 PID 会立即退出,真实进程另获新 PID, + // 因此不比较具体 PID,只验证「能查到运行中的 notepad 进程」 + const single = (await run("App_GetPid", { pattern: "notepad" })) as number | null; + expect(single).toBeTruthy(); + const all = (await run("App_GetPids", { p_name: "notepad" })) as number[]; + expect(all.length).toBeGreaterThan(0); + const killed = await run("App_KillV1", { close_type: "name", p_name: "notepad" }); + console.log("[应用生命周期] 启动PID:", pid, "查询到:", all, "关闭:", JSON.stringify(killed)); + await new Promise((r) => setTimeout(r, 800)); + const after = (await run("App_GetPids", { pattern: "notepad" })) as number[]; + expect(after.length).toBe(0); + }, 120000); + + it("Windows凭据: 添加→读取→删除", async () => { + const target = "RPA_SMOKETEST_GENERIC"; + expect((await run("System_AddCredential", { application: target, name: "smoke-user", password: "p@ss" })) as { added: boolean }).toEqual({ added: true, target }); + const read = (await run("System_ReadCredential", { application: target })) as { exists: boolean; detail: string }; + expect(read.exists).toBe(true); + expect(read.detail).toContain("smoke-user"); + expect((await run("System_DeleteCredential", { application: target })) as { deleted: boolean }).toEqual({ deleted: true, target }); + const readAfter = (await run("System_ReadCredential", { application: target })) as { exists: boolean }; + expect(readAfter.exists).toBe(false); + console.log("[凭据闭环] 通过"); + }, 120000); +}); diff --git a/src/main/capabilities/data.ts b/src/main/capabilities/data.ts index 1e14914..607a4f0 100644 --- a/src/main/capabilities/data.ts +++ b/src/main/capabilities/data.ts @@ -481,7 +481,11 @@ register(["CSV_Write", "CSV_WriteNew"], async (_ctx, node, args) => { register(["Basic_ConvertNumber", "Basic_StringToNum"], (_ctx, _node, args) => num(first(args, ["val"]))); register("Basic_ConvertString", (_ctx, _node, args) => str(first(args, ["val"]))); register("Basic_ConvertInt", (_ctx, _node, args) => Math.trunc(num(first(args, ["val"])))); -register("Basic_ConvertBool", (_ctx, _node, args) => bool(first(args, ["val"]))); +register("Basic_ConvertBool", (_ctx, _node, args) => { + const value = first(args, ["val"]); + if (typeof value === "number") return value !== 0; + return bool(value); +}); register("Basic_IsNull", (_ctx, _node, args) => { const value = first(args, ["val"]); return value === null || value === undefined || value === ""; diff --git a/src/main/capabilities/windows-system.ts b/src/main/capabilities/windows-system.ts index 85e1a02..f65795e 100644 --- a/src/main/capabilities/windows-system.ts +++ b/src/main/capabilities/windows-system.ts @@ -5,6 +5,13 @@ import { promisify } from "node:util"; const run = promisify(execFile); const ps = (script: string) => run("powershell.exe", ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", script], { windowsHide: true, timeout: 60000 }); +// cmdkey 在中文 Windows 上输出 GBK,直接 execFile 按 UTF-8 解码会乱码;统一经 PowerShell 强制 UTF-8 控制台编码捕获 +const cmdkey = async (args: string[]): Promise => { + const quoted = args.map((item) => "'" + item.replace(/'/g, "''") + "'"); + const script = "[Console]::OutputEncoding=[System.Text.UTF8Encoding]::new($false); & cmdkey " + quoted.join(" "); + const { stdout } = await run("powershell.exe", ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", script], { windowsHide: true, timeout: 60000 }); + return stdout; +}; function reqText(args: Record, keys: string[], label: string, nodeId: string): string { for (const key of keys) { @@ -42,10 +49,16 @@ register("System_ScreenSaverStop", async () => { register("System_ReadCredential", async (ctx, node, args) => { const app = reqText(args, ["application"], "凭据目标", node.id); - const { stdout } = await run("cmdkey", ["/list"], { windowsHide: true }); + const stdout = await cmdkey(["/list"]); const exists = stdout.includes(app); - const userLine = stdout.split(/\r?\n/).find(l => l.includes(app)); - return { target: app, exists, detail: exists ? (userLine ?? "").trim() : "" }; + // cmdkey /list 每条凭据是「目标/类型/用户」多行块(空行分隔);取整块才有用户名等有效信息 + const lines = stdout.split(/\r?\n/); + const idx = lines.findIndex((l) => l.includes(app)); + const block: string[] = []; + if (idx >= 0) { + for (let i = idx; i < lines.length && lines[i].trim(); i++) block.push(lines[i].trim()); + } + return { target: app, exists, detail: exists ? block.join(" | ") : "" }; }); register("System_AddCredential", async (ctx, node, args) => { @@ -53,13 +66,13 @@ register("System_AddCredential", async (ctx, node, args) => { const user = reqText(args, ["name"], "用户名", node.id); const pwd = String(args.password ?? ""); const psArgs = pwd ? ["/generic:" + app, "/user:" + user, "/pass:" + pwd] : ["/generic:" + app, "/user:" + user]; - await run("cmdkey", psArgs, { windowsHide: true }); + await cmdkey(psArgs); return { added: true, target: app }; }); register("System_DeleteCredential", async (ctx, node, args) => { const app = reqText(args, ["application"], "凭据目标", node.id); - await run("cmdkey", ["/delete:" + app], { windowsHide: true }); + await cmdkey(["/delete:" + app]); return { deleted: true, target: app }; });