From ae4f4a2460c9b121ef108c334a34d193c8804193 Mon Sep 17 00:00:00 2001 From: yu <191972707@qq.com> Date: Mon, 2 Nov 2026 08:23:22 +0800 Subject: [PATCH] feat: manual keyboard mode and ciphertext variables --- package-lock.json | 16 ++++++ package.json | 1 + src/main/capabilities/basic-encryption.ts | 50 ++++++++++++++++++ src/main/capabilities/basic-manual.test.ts | 31 +++++++++++ src/main/capabilities/desktop.ts | 60 +++++++++++++++++++++- src/main/capabilities/index.ts | 1 + src/types/sm-crypto.d.ts | 6 +++ 7 files changed, 163 insertions(+), 2 deletions(-) create mode 100644 src/main/capabilities/basic-encryption.ts create mode 100644 src/main/capabilities/basic-manual.test.ts create mode 100644 src/types/sm-crypto.d.ts diff --git a/package-lock.json b/package-lock.json index 72bf83d..7999f4c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "pngjs": "^7.0.0", "react": "^18.3.1", "react-dom": "^18.3.1", + "sm-crypto": "^0.5.8", "tesseract.js": "^7.0.0", "ws": "^8.22.0", "zustand": "^5.0.2" @@ -6873,6 +6874,12 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "license": "MIT" + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-3.1.0.tgz", @@ -9176,6 +9183,15 @@ "node": ">=10" } }, + "node_modules/sm-crypto": { + "version": "0.5.8", + "resolved": "https://registry.npmmirror.com/sm-crypto/-/sm-crypto-0.5.8.tgz", + "integrity": "sha512-bpz1/PthgZHcLHyIl28mbYZHmeuTDrdWpwHZ7BGzFjQuB7OOhjlbcPEMt44aqUfZ2S5YJXM8v20xYO1j9CC45Q==", + "license": "MIT", + "dependencies": { + "jsbn": "^1.1.0" + } + }, "node_modules/smart-buffer": { "version": "4.2.0", "resolved": "https://registry.npmmirror.com/smart-buffer/-/smart-buffer-4.2.0.tgz", diff --git a/package.json b/package.json index f7ec164..01129b7 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "pngjs": "^7.0.0", "react": "^18.3.1", "react-dom": "^18.3.1", + "sm-crypto": "^0.5.8", "tesseract.js": "^7.0.0", "ws": "^8.22.0", "zustand": "^5.0.2" diff --git a/src/main/capabilities/basic-encryption.ts b/src/main/capabilities/basic-encryption.ts new file mode 100644 index 0000000..c30ba6d --- /dev/null +++ b/src/main/capabilities/basic-encryption.ts @@ -0,0 +1,50 @@ +import { Buffer } from "node:buffer"; +import { register } from "../engine/registry"; +import { RuntimeError, type CapabilityHandler } from "../engine/types"; + +const SM4_KEY = "INPUT_PARAM_KEY4"; +const SM4_KEY_HEX = Buffer.from(SM4_KEY, "utf8").toString("hex"); + +function looksLikeBase64(value: string): boolean { + const text = value.trim().replace(/\s+/g, ""); + if (!text || text.length % 4 !== 0 || !/^[A-Za-z0-9+/]+={0,2}$/.test(text)) return false; + return Buffer.from(text, "base64").toString("base64").replace(/=+$/, "") === text.replace(/=+$/, ""); +} + +function readableUtf8(value: string): boolean { + if (!value) return false; + let controls = 0; + for (const char of value) { + const code = char.codePointAt(0)!; + if (code < 32 && !["\n", "\r", "\t"].includes(char)) controls += 1; + if (code === 0xfffd) controls += 10; + } + return controls === 0; +} + +async function sm4Decrypt(value: string): Promise { + const { sm4 } = await import("sm-crypto"); + const errors: string[] = []; + for (const padding of ["pkcs#7", "none"] as const) { + try { + const output = sm4.decrypt(value.toLowerCase(), SM4_KEY_HEX, { padding }); + if (output && readableUtf8(output)) return output; + errors.push(`${padding}: empty or non-text output`); + } catch (error) { + errors.push(`${padding}: ${error instanceof Error ? error.message : String(error)}`); + } + } + throw new RuntimeError("decryption_failed", "SM4 密文解密失败", { detail: errors.join("; ") }); +} + +const dodecryption: CapabilityHandler = async (_ctx, node, args) => { + const words = String(args.words ?? args.value ?? args.text ?? ""); + if (!words.trim()) throw new RuntimeError("validation_error", "密文内容为空", { nodeId: node.id, componentId: node.componentId }); + if (looksLikeBase64(words)) { + const decoded = Buffer.from(words, "base64").toString("utf8"); + if (readableUtf8(decoded)) return decoded; + } + return sm4Decrypt(words.trim()); +}; + +register("Basic_Dodecryption", dodecryption); diff --git a/src/main/capabilities/basic-manual.test.ts b/src/main/capabilities/basic-manual.test.ts new file mode 100644 index 0000000..8cf2f21 --- /dev/null +++ b/src/main/capabilities/basic-manual.test.ts @@ -0,0 +1,31 @@ +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 "./desktop"; +import "./basic-encryption"; + +const context = {} as RunContext; +const component = {} as CatalogComponent; +const node = (id: string): FlowNode => ({ id, componentId: id, position: { x: 0, y: 0 }, values: {}, options: {} }); +async function run(id: string, args: Record) { + const handler = getHandler(id); + expect(handler, id).toBeTruthy(); + return handler!(context, node(id), args, component); +} + +describe("manual keyboard and encrypted variables", () => { + it("registers manual keyboard switches", async () => { + await expect(run("Keyboard_EnableManualOperation", { random_move: true, random_click: true, random_sleep: false, min_time: 0, max_time: 0 })).resolves.toMatchObject({ enabled: true }); + await expect(run("Keyboard_DisableManualOperation", {})).resolves.toEqual({ enabled: false }); + }); + + it("decodes base64 and SM4 ciphertext", async () => { + await expect(run("Basic_Dodecryption", { words: Buffer.from("hello", "utf8").toString("base64") })).resolves.toBe("hello"); + const { sm4 } = await import("sm-crypto"); + const key = Buffer.from("INPUT_PARAM_KEY4", "utf8").toString("hex"); + const cipher = sm4.encrypt("secret", key, { padding: "pkcs#7" }); + await expect(run("Basic_Dodecryption", { words: cipher })).resolves.toBe("secret"); + await expect(run("Basic_Dodecryption", {})).rejects.toThrow(); + }); +}); diff --git a/src/main/capabilities/desktop.ts b/src/main/capabilities/desktop.ts index 794d6cd..acd213b 100644 --- a/src/main/capabilities/desktop.ts +++ b/src/main/capabilities/desktop.ts @@ -4,6 +4,37 @@ import { register } from "../engine/registry"; import { RuntimeError, type CapabilityHandler } from "../engine/types"; const execFileAsync = promisify(execFile); + +interface ManualOperationSettings { + enabled: boolean; + randomMove: boolean; + randomClick: boolean; + randomSleep: boolean; + minMs: number; + maxMs: number; +} + +const manualOperation: ManualOperationSettings = { + enabled: false, randomMove: true, randomClick: true, randomSleep: true, minMs: 1000, maxMs: 3000 +}; + +function jitter(value: number, amount = 3): number { + return Math.max(0, Math.round(value + (Math.random() * amount * 2 - amount))); +} + +function manualJitter(point: [number, number], move: boolean, click: boolean): [number, number] { + if (!manualOperation.enabled) return point; + const amount = (click && manualOperation.randomClick) || (move && manualOperation.randomMove) ? 3 : 0; + return [jitter(point[0], amount), jitter(point[1], amount)]; +} + +async function manualDelay(): Promise { + if (!manualOperation.enabled || !manualOperation.randomSleep) return; + const low = Math.max(0, manualOperation.minMs); + const high = Math.max(low, manualOperation.maxMs); + const timeout = low + Math.random() * (high - low); + if (timeout > 0) await new Promise((resolve) => setTimeout(resolve, timeout)); +} const USER32 = `Add-Type @' using System; using System.Runtime.InteropServices; @@ -23,6 +54,10 @@ function text(args: Record, keys: string[], fallback = ""): str return fallback; } +function bool(value: unknown): boolean { + return [true, 1, "1", "true", "True", "是", "开启"].includes(value as never); +} + function number(args: Record, keys: string[], fallback = 0): number { const value = Number(text(args, keys, String(fallback))); return Number.isFinite(value) ? value : fallback; @@ -64,12 +99,31 @@ export function psString(value: unknown): string { const keyboardSend: CapabilityHandler = async (_ctx, node, args) => { const keys = text(args, ["keys", "key", "text", "value"]); if (!keys) throw new RuntimeError("validation_error", "键盘输入为空", { nodeId: node.id, componentId: node.componentId }); + await manualDelay(); await powershell(`Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.SendKeys]::SendWait(${psString(keys)})`); + await manualDelay(); return keys; }; +const enableManualOperation: CapabilityHandler = async (_ctx, _node, args) => { + const min = number(args, ["min_time", "minTime"], 1) * 1000; + const max = number(args, ["max_time", "maxTime"], 3) * 1000; + manualOperation.enabled = true; + manualOperation.randomMove = bool(args.random_move ?? true); + manualOperation.randomClick = bool(args.random_click ?? true); + manualOperation.randomSleep = bool(args.random_sleep ?? true); + manualOperation.minMs = min; + manualOperation.maxMs = Math.max(min, max); + return { enabled: true, randomMove: manualOperation.randomMove, randomClick: manualOperation.randomClick, randomSleep: manualOperation.randomSleep, minMs: manualOperation.minMs, maxMs: manualOperation.maxMs }; +}; + +const disableManualOperation: CapabilityHandler = async () => { + manualOperation.enabled = false; + return { enabled: false }; +}; + const mouseClick: CapabilityHandler = async (_ctx, node, args) => { - const [x, y] = point(args.position ?? args.point ?? args.location ?? "[0,0]"); + const [x, y] = manualJitter(point(args.position ?? args.point ?? args.location ?? "[0,0]"), false, true); const button = text(args, ["side", "button"], "0").toLowerCase(); const down = button.includes("right") || button === "1" ? 0x0008 : 0x0002; const up = button.includes("right") || button === "1" ? 0x0010 : 0x0004; @@ -78,7 +132,7 @@ const mouseClick: CapabilityHandler = async (_ctx, node, args) => { }; const mouseMove: CapabilityHandler = async (_ctx, node, args) => { - const [x, y] = point(args.position ?? args.point ?? args.location ?? "[0,0]"); + const [x, y] = manualJitter(point(args.position ?? args.point ?? args.location ?? "[0,0]"), true, false); await powershell(`${USER32}; [RpaUser32]::SetCursorPos(${x},${y})`); return { x, y }; }; @@ -149,6 +203,8 @@ const executeCommand: CapabilityHandler = async (_ctx, node, args) => { register(["Keyboard_Send_V1", "Keyboard_Send", "Keyboard_PressNew", "Keyboard_Press"], keyboardSend); +register("Keyboard_EnableManualOperation", enableManualOperation); +register("Keyboard_DisableManualOperation", disableManualOperation); register(["Mouse_Click", "Mouse_ClickNew"], mouseClick); register(["Mouse_MoveTo", "Mouse_Move"], mouseMove); register(["Mouse_DragTo", "Mouse_Drag"], mouseDrag); diff --git a/src/main/capabilities/index.ts b/src/main/capabilities/index.ts index 524807e..3dba079 100644 --- a/src/main/capabilities/index.ts +++ b/src/main/capabilities/index.ts @@ -33,6 +33,7 @@ import "./window-extended"; import "./data-extended"; import "./utility-extended"; import "./data-table-common"; +import "./basic-encryption"; import "./common-final"; export { capabilityCount, capabilityIds, getHandler, hasCapability, register } from "../engine/registry"; diff --git a/src/types/sm-crypto.d.ts b/src/types/sm-crypto.d.ts new file mode 100644 index 0000000..8d9c2e3 --- /dev/null +++ b/src/types/sm-crypto.d.ts @@ -0,0 +1,6 @@ +declare module "sm-crypto" { + export const sm4: { + encrypt: (plainText: string, key: string, options?: { padding?: "pkcs#5" | "pkcs#7" | "none" }) => string; + decrypt: (cipherText: string, key: string, options?: { padding?: "pkcs#5" | "pkcs#7" | "none" }) => string; + }; +}