feat: manual keyboard mode and ciphertext variables

This commit is contained in:
yu 2026-11-02 08:23:22 +08:00
parent 9a5f86c92a
commit ae4f4a2460
7 changed files with 163 additions and 2 deletions

16
package-lock.json generated
View File

@ -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",

View File

@ -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"

View 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);

View 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();
});
});

View File

@ -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);

View File

@ -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
View 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;
};
}