feat: manual keyboard mode and ciphertext variables
This commit is contained in:
parent
9a5f86c92a
commit
ae4f4a2460
16
package-lock.json
generated
16
package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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"
|
||||
|
||||
50
src/main/capabilities/basic-encryption.ts
Normal file
50
src/main/capabilities/basic-encryption.ts
Normal file
@ -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<string> {
|
||||
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);
|
||||
31
src/main/capabilities/basic-manual.test.ts
Normal file
31
src/main/capabilities/basic-manual.test.ts
Normal file
@ -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<string, unknown>) {
|
||||
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();
|
||||
});
|
||||
});
|
||||
@ -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<void> {
|
||||
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<string, unknown>, 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<string, unknown>, 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);
|
||||
|
||||
@ -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";
|
||||
|
||||
6
src/types/sm-crypto.d.ts
vendored
Normal file
6
src/types/sm-crypto.d.ts
vendored
Normal file
@ -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;
|
||||
};
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user