From 1c0a30285e5557a710474259ee2256ddcebe3377 Mon Sep 17 00:00:00 2001 From: Ulzii Otgonbaatar Date: Wed, 11 Feb 2026 10:05:07 -0700 Subject: [PATCH 1/8] feat: add human-like Bezier mouse movement - Add mousetrajectory lib with Bernstein Bezier curves, jitter, easeOutQuad - MoveMouse supports smooth=true for curved path, smooth=false for instant - OpenAPI: smooth, steps (5-80), step_delay_ms (3-30) on MoveMouseRequest - Add cursor-trail demo (before/after instant vs Bezier) Co-authored-by: Cursor --- demo/mouse-movement/README.md | 62 ++ demo/mouse-movement/cursor-trail-demo.html | 132 ++++ .../demo-mouse-movement-video.ts | 104 +++ demo/mouse-movement/package-lock.json | 615 ++++++++++++++++++ demo/mouse-movement/package.json | 20 + server/cmd/api/api/computer.go | 127 +++- server/lib/mousetrajectory/mousetrajectory.go | 222 +++++++ .../mousetrajectory/mousetrajectory_test.go | 57 ++ server/lib/oapi/oapi.go | 288 ++++---- server/openapi.yaml | 14 + 10 files changed, 1494 insertions(+), 147 deletions(-) create mode 100644 demo/mouse-movement/README.md create mode 100644 demo/mouse-movement/cursor-trail-demo.html create mode 100644 demo/mouse-movement/demo-mouse-movement-video.ts create mode 100644 demo/mouse-movement/package-lock.json create mode 100644 demo/mouse-movement/package.json create mode 100644 server/lib/mousetrajectory/mousetrajectory.go create mode 100644 server/lib/mousetrajectory/mousetrajectory_test.go diff --git a/demo/mouse-movement/README.md b/demo/mouse-movement/README.md new file mode 100644 index 00000000..4a0517f8 --- /dev/null +++ b/demo/mouse-movement/README.md @@ -0,0 +1,62 @@ +# Mouse Movement Demo — Before/After Video + +Create a before/after video demonstrating human-like Bezier curve mouse movement vs instant teleport, inspired by [Camoufox's stealth overview](https://camoufox.com/stealth/) and [cursor movement docs](https://camoufox.com/fingerprint/cursor-movement). + +## What You'll Record + +- **BEFORE**: Instant movement (`smooth: false`) — cursor jumps in straight lines between targets +- **AFTER**: Human-like Bezier movement (`smooth: true`) — curved, natural trajectory + +The cursor trail overlay makes the difference visually obvious. + +## Prerequisites + +- Kernel browser session running kernel-images-private (with Bezier support in `server/cmd/api/api/computer.go`) +- `KERNEL_BROWSER_ID` and `KERNEL_API_KEY` (or equivalent auth) +- Screen recorder (OBS, QuickTime, or `ffmpeg`) + +## Steps + +### 1. Start Screen Recording + +Record the **browser live view** URL. Options: + +- **OBS**: Add Browser source or window capture for the live view tab +- **QuickTime** (macOS): File → New Screen Recording, select the live view window +- **ffmpeg**: + ```bash + ffmpeg -f avfoundation -i "1" -c:v libx264 -crf 18 mouse-demo.mp4 + ``` + +### 2. Run the Demo Script + +```bash +cd demo/mouse-movement +npm install +KERNEL_BROWSER_ID= KERNEL_API_KEY= npm run demo +``` + +### 3. What Happens + +1. The script loads the cursor trail demo page (`cursor-trail-demo.html`) into the browser +2. **BEFORE** phase: Moves the mouse along the path with `smooth: false` — straight lines +3. Pause and clear trail +4. **AFTER** phase: Same path with `smooth: true` — Bezier curves +5. The trail shows the curved vs straight paths + +### 4. Edit the Video + +- Trim to show BEFORE and AFTER clearly +- Optional: split screen or side-by-side comparison +- Add captions: "Instant movement" vs "Human-like Bezier movement" + +## Files + +| File | Purpose | +|------|---------| +| `cursor-trail-demo.html` | Page that draws the cursor path as the mouse moves | +| `demo-mouse-movement-video.ts` | Script that runs before/after moveMouse with smooth on/off | + +## Implementation + +The Bezier trajectory and `smooth` movement are implemented in `server/cmd/api/api/computer.go` and `server/lib/mousetrajectory/`. When `smooth: true` is sent in the move_mouse request body, the instance uses Bernstein Bezier curves for human-like movement. diff --git a/demo/mouse-movement/cursor-trail-demo.html b/demo/mouse-movement/cursor-trail-demo.html new file mode 100644 index 00000000..938651c9 --- /dev/null +++ b/demo/mouse-movement/cursor-trail-demo.html @@ -0,0 +1,132 @@ + + + + + + Mouse Movement Demo — Cursor Trail + + + + +
+ Recording cursor trail… + Move the mouse to see the path +
+
+ + + + diff --git a/demo/mouse-movement/demo-mouse-movement-video.ts b/demo/mouse-movement/demo-mouse-movement-video.ts new file mode 100644 index 00000000..3c61b131 --- /dev/null +++ b/demo/mouse-movement/demo-mouse-movement-video.ts @@ -0,0 +1,104 @@ +/** + * Before/After mouse movement demo for video recording. + * + * Demonstrates: + * - BEFORE: Instant mouse movement (smooth: false) — cursor teleports in straight lines + * - AFTER: Human-like Bezier curve movement (smooth: true) — natural curved trajectory + * + * Inspired by https://camoufox.com/stealth/ and https://camoufox.com/fingerprint/cursor-movement + * + * Usage: + * 1. Ensure KERNEL_BROWSER_ID and KERNEL_API_KEY are set + * 2. Start screen recording (OBS, QuickTime, ffmpeg) on the browser live view + * 3. Run: npm run demo + * 4. Record: BEFORE (instant) then AFTER (smooth Bezier) segments + */ + +import Kernel from "@onkernel/sdk"; +import { chromium } from "playwright-core"; +import { readFileSync } from "fs"; +import { join, dirname } from "path"; +import { fileURLToPath } from "url"; + +const BROWSER_ID = process.env.KERNEL_BROWSER_ID!; + +function sleep(ms: number) { + return new Promise((r) => setTimeout(r, ms)); +} + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +// Movement path chosen to clearly show the difference: diagonal + arc +const DEMO_PATH: [number, number][] = [ + [200, 200], + [600, 350], + [1000, 200], + [700, 500], + [400, 400], + [800, 300], +]; + +(async () => { + if (!BROWSER_ID) { + console.error("Set KERNEL_BROWSER_ID"); + process.exit(1); + } + + const kernel = new Kernel(); + const session = await kernel.browsers.retrieve(BROWSER_ID); + + console.log("Session:", BROWSER_ID); + console.log("Live view (record this):", session.browser_live_view_url); + + const browser = await chromium.connectOverCDP(session.cdp_ws_url); + const page = browser.contexts()[0].pages()[0] ?? (await browser.newPage()); + + // Load cursor trail demo page + const demoHtml = readFileSync( + join(__dirname, "cursor-trail-demo.html"), + "utf-8" + ); + await page.setContent(demoHtml, { waitUntil: "domcontentloaded" }); + await page.setViewportSize({ width: 1280, height: 720 }); + + await sleep(500); + + // --- BEFORE: Instant movement (smooth: false) --- + await page.evaluate(() => { + (window as any).demoApi?.setMode("BEFORE: Instant movement (smooth: false)", "instant"); + (window as any).demoApi?.clear(); + }); + await sleep(800); + + console.log("[BEFORE] Running instant mouse moves..."); + for (let i = 0; i < DEMO_PATH.length; i++) { + const [x, y] = DEMO_PATH[i]; + await kernel.browsers.computer.moveMouse(BROWSER_ID, { x, y, smooth: false }); + await sleep(400); + } + await sleep(2000); + + // --- Clear and switch to AFTER --- + await page.evaluate(() => { + (window as any).demoApi?.setMode("AFTER: Human-like Bezier movement (smooth: true)", "smooth"); + (window as any).demoApi?.clear(); + }); + await sleep(1500); + + // --- AFTER: Smooth Bezier movement (smooth: true) --- + console.log("[AFTER] Running smooth Bezier mouse moves..."); + for (let i = 0; i < DEMO_PATH.length; i++) { + const [x, y] = DEMO_PATH[i]; + await kernel.browsers.computer.moveMouse(BROWSER_ID, { + x, + y, + smooth: true, + step_delay_ms: 12, + }); + await sleep(400); + } + await sleep(3000); + + console.log("Demo complete. Stop recording."); + browser.close(); +})(); diff --git a/demo/mouse-movement/package-lock.json b/demo/mouse-movement/package-lock.json new file mode 100644 index 00000000..207e862e --- /dev/null +++ b/demo/mouse-movement/package-lock.json @@ -0,0 +1,615 @@ +{ + "name": "mouse-movement-demo", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mouse-movement-demo", + "version": "1.0.0", + "dependencies": { + "@onkernel/sdk": "^0.25.0", + "playwright-core": "^1.57.0" + }, + "devDependencies": { + "@types/node": "^22.15.0", + "tsx": "^4.21.0", + "typescript": "^5.9.3" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@onkernel/sdk": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@onkernel/sdk/-/sdk-0.25.0.tgz", + "integrity": "sha512-kMZ/499V9KCksvI/IxYx8juBKViWaCBLsoCOaZdfvLuXBy15310V+yoNUqu+J5q4Yfh+rHBcpFxn9H+GHfHfOQ==", + "license": "Apache-2.0" + }, + "node_modules/@types/node": { + "version": "22.19.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.11.tgz", + "integrity": "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", + "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/playwright-core": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", + "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/demo/mouse-movement/package.json b/demo/mouse-movement/package.json new file mode 100644 index 00000000..4cbfb199 --- /dev/null +++ b/demo/mouse-movement/package.json @@ -0,0 +1,20 @@ +{ + "name": "mouse-movement-demo", + "version": "1.0.0", + "type": "module", + "scripts": { + "demo": "tsx demo-mouse-movement-video.ts" + }, + "dependencies": { + "@onkernel/sdk": "^0.25.0", + "playwright-core": "^1.57.0" + }, + "devDependencies": { + "@types/node": "^22.15.0", + "tsx": "^4.21.0", + "typescript": "^5.9.3" + }, + "engines": { + "node": ">=22.0.0" + } +} diff --git a/server/cmd/api/api/computer.go b/server/cmd/api/api/computer.go index be7766ff..9494200e 100644 --- a/server/cmd/api/api/computer.go +++ b/server/cmd/api/api/computer.go @@ -6,7 +6,9 @@ import ( "errors" "fmt" "io" + "log/slog" "math" + "math/rand" "os" "os/exec" "strconv" @@ -15,6 +17,7 @@ import ( "time" "github.com/onkernel/kernel-images/server/lib/logger" + "github.com/onkernel/kernel-images/server/lib/mousetrajectory" oapi "github.com/onkernel/kernel-images/server/lib/oapi" ) @@ -51,28 +54,27 @@ func (s *ApiService) doMoveMouse(ctx context.Context, body oapi.MoveMouseRequest return &validationError{msg: fmt.Sprintf("coordinates exceed screen bounds (max: %dx%d)", screenWidth-1, screenHeight-1)} } - // Build xdotool arguments - args := []string{} + useSmooth := body.Smooth != nil && *body.Smooth + if useSmooth { + return s.moveMouseSmooth(ctx, log, body) + } + return s.moveMouseInstant(ctx, log, body) +} - // Hold modifier keys (keydown) +func (s *ApiService) moveMouseInstant(ctx context.Context, log *slog.Logger, body oapi.MoveMouseRequest) (oapi.MoveMouseResponseObject, error) { + args := []string{} if body.HoldKeys != nil { for _, key := range *body.HoldKeys { args = append(args, "keydown", key) } } - - // Move the cursor to the desired coordinates args = append(args, "mousemove", strconv.Itoa(body.X), strconv.Itoa(body.Y)) - - // Release modifier keys (keyup) if body.HoldKeys != nil { for _, key := range *body.HoldKeys { args = append(args, "keyup", key) } } - log.Info("executing xdotool", "args", args) - output, err := defaultXdoTool.Run(ctx, args...) if err != nil { log.Error("xdotool command failed", "err", err, "output", string(output)) @@ -83,6 +85,113 @@ func (s *ApiService) doMoveMouse(ctx context.Context, body oapi.MoveMouseRequest } func (s *ApiService) MoveMouse(ctx context.Context, request oapi.MoveMouseRequestObject) (oapi.MoveMouseResponseObject, error) { + return oapi.MoveMouse200Response{}, nil +} + +func (s *ApiService) moveMouseSmooth(ctx context.Context, log *slog.Logger, body oapi.MoveMouseRequest) (oapi.MoveMouseResponseObject, error) { + fromX, fromY, err := s.getMouseLocation(ctx) + if err != nil { + log.Error("failed to get mouse location for smooth move", "error", err) + return oapi.MoveMouse500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{ + Message: "failed to get current mouse position: " + err.Error()}, + }, nil + } + + opts := &mousetrajectory.Options{} + if body.Steps != nil && *body.Steps > 0 { + opts.MaxPoints = *body.Steps + } + traj := mousetrajectory.NewHumanizeMouseTrajectoryWithOptions( + float64(fromX), float64(fromY), float64(body.X), float64(body.Y), opts) + points := traj.GetPointsInt() + if len(points) < 2 { + return s.moveMouseInstant(ctx, log, body) + } + + stepDelayMs := 10 + if body.StepDelayMs != nil && *body.StepDelayMs >= 3 && *body.StepDelayMs <= 30 { + stepDelayMs = *body.StepDelayMs + } + + // Hold modifiers + if body.HoldKeys != nil { + args := []string{} + for _, key := range *body.HoldKeys { + args = append(args, "keydown", key) + } + if output, err := defaultXdoTool.Run(ctx, args...); err != nil { + log.Error("xdotool keydown failed", "err", err, "output", string(output)) + return oapi.MoveMouse500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{ + Message: "failed to hold modifier keys"}, + }, nil + } + defer func() { + if body.HoldKeys != nil { + args := []string{} + for _, key := range *body.HoldKeys { + args = append(args, "keyup", key) + } + _, _ = defaultXdoTool.Run(ctx, args...) + } + }() + } + + // Move along Bezier path: mousemove_relative for each step with delay + for i := 1; i < len(points); i++ { + dx := points[i][0] - points[i-1][0] + dy := points[i][1] - points[i-1][1] + args := []string{"mousemove_relative"} + if dx < 0 || dy < 0 { + args = append(args, "--", strconv.Itoa(dx), strconv.Itoa(dy)) + } else { + args = append(args, strconv.Itoa(dx), strconv.Itoa(dy)) + } + if output, err := defaultXdoTool.Run(ctx, args...); err != nil { + log.Error("xdotool mousemove_relative failed", "err", err, "output", string(output), "step", i) + return oapi.MoveMouse500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{ + Message: "failed during smooth mouse movement"}, + }, nil + } + jitter := stepDelayMs + if stepDelayMs > 3 { + jitter = stepDelayMs + rand.Intn(5) - 2 + if jitter < 3 { + jitter = 3 + } + } + time.Sleep(time.Duration(jitter) * time.Millisecond) + } + + log.Info("executed smooth mouse movement", "points", len(points)) + return oapi.MoveMouse200Response{}, nil +} + +// getMouseLocation returns the current cursor position via xdotool getmouselocation --shell. +func (s *ApiService) getMouseLocation(ctx context.Context) (x, y int, err error) { + output, err := defaultXdoTool.Run(ctx, "getmouselocation", "--shell") + if err != nil { + return 0, 0, fmt.Errorf("xdotool getmouselocation failed: %w (output: %s)", err, string(output)) + } + lines := strings.Split(strings.TrimSpace(string(output)), "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + if strings.HasPrefix(line, "X=") { + if v, e := strconv.Atoi(strings.TrimPrefix(line, "X=")); e == nil { + x = v + } + } else if strings.HasPrefix(line, "Y=") { + if v, e := strconv.Atoi(strings.TrimPrefix(line, "Y=")); e == nil { + y = v + } + } + } + return x, y, nil +} + +func (s *ApiService) ClickMouse(ctx context.Context, request oapi.ClickMouseRequestObject) (oapi.ClickMouseResponseObject, error) { + log := logger.FromContext(ctx) + + // serialize input operations to avoid overlapping xdotool commands s.inputMu.Lock() defer s.inputMu.Unlock() diff --git a/server/lib/mousetrajectory/mousetrajectory.go b/server/lib/mousetrajectory/mousetrajectory.go new file mode 100644 index 00000000..83a8191f --- /dev/null +++ b/server/lib/mousetrajectory/mousetrajectory.go @@ -0,0 +1,222 @@ +package mousetrajectory + +import ( + "math" + "math/rand" +) + +// HumanizeMouseTrajectory generates human-like mouse movement points from (fromX, fromY) +// to (toX, toY) using Bezier curves with randomized control points, distortion, and easing. +// +// Ported from Camoufox MouseTrajectories.hpp, which was adapted from: +// https://github.com/riflosnake/HumanCursor/blob/main/humancursor/utilities/human_curve_generator.py +type HumanizeMouseTrajectory struct { + fromX, fromY float64 + toX, toY float64 + points [][2]float64 + rng *rand.Rand +} + +// Options configures trajectory generation. +type Options struct { + // MaxPoints overrides the auto-computed point count. 0 = auto. Range 5-80. + MaxPoints int +} + +// NewHumanizeMouseTrajectory creates a trajectory from (fromX, fromY) to (toX, toY). +// Uses the default entropy source for randomization. +func NewHumanizeMouseTrajectory(fromX, fromY, toX, toY float64) *HumanizeMouseTrajectory { + return NewHumanizeMouseTrajectoryWithOptions(fromX, fromY, toX, toY, nil) +} + +// NewHumanizeMouseTrajectoryWithOptions creates a trajectory with optional overrides. +func NewHumanizeMouseTrajectoryWithOptions(fromX, fromY, toX, toY float64, opts *Options) *HumanizeMouseTrajectory { + t := &HumanizeMouseTrajectory{ + fromX: fromX, fromY: fromY, + toX: toX, toY: toY, + rng: rand.New(rand.NewSource(rand.Int63())), + } + t.generateCurve(opts) + return t +} + +// NewHumanizeMouseTrajectoryWithSeed creates a trajectory with a fixed seed (for tests). +func NewHumanizeMouseTrajectoryWithSeed(fromX, fromY, toX, toY float64, seed int64) *HumanizeMouseTrajectory { + t := &HumanizeMouseTrajectory{ + fromX: fromX, fromY: fromY, + toX: toX, toY: toY, + rng: rand.New(rand.NewSource(seed)), + } + t.generateCurve(nil) + return t +} + +// GetPoints returns the trajectory as a slice of [x, y] pairs (floats, caller rounds). +func (t *HumanizeMouseTrajectory) GetPoints() [][2]float64 { + return t.points +} + +// GetPointsInt returns the trajectory as integer coordinates suitable for xdotool. +func (t *HumanizeMouseTrajectory) GetPointsInt() [][2]int { + out := make([][2]int, len(t.points)) + for i, p := range t.points { + out[i][0] = int(math.Round(p[0])) + out[i][1] = int(math.Round(p[1])) + } + return out +} + +func (t *HumanizeMouseTrajectory) generateCurve(opts *Options) { + left := math.Min(t.fromX, t.toX) - 80 + right := math.Max(t.fromX, t.toX) + 80 + down := math.Min(t.fromY, t.toY) - 80 + up := math.Max(t.fromY, t.toY) + 80 + + knots := t.generateInternalKnots(left, right, down, up, 2) + curvePoints := t.generatePoints(knots) + curvePoints = t.distortPoints(curvePoints, 1.0, 1.0, 0.5) + t.points = t.tweenPoints(curvePoints, opts) +} + +func (t *HumanizeMouseTrajectory) generateInternalKnots(l, r, d, u float64, knotsCount int) [][2]float64 { + knotsX := t.randomChoiceDoubles(l, r, knotsCount) + knotsY := t.randomChoiceDoubles(d, u, knotsCount) + knots := make([][2]float64, knotsCount) + for i := 0; i < knotsCount; i++ { + knots[i] = [2]float64{knotsX[i], knotsY[i]} + } + return knots +} + +func (t *HumanizeMouseTrajectory) randomChoiceDoubles(min, max float64, size int) []float64 { + out := make([]float64, size) + for i := 0; i < size; i++ { + out[i] = min + t.rng.Float64()*(max-min) + } + return out +} + +func factorial(n int) int64 { + if n < 0 { + return -1 + } + result := int64(1) + for i := 2; i <= n; i++ { + result *= int64(i) + } + return result +} + +func binomial(n, k int) float64 { + return float64(factorial(n)) / (float64(factorial(k)) * float64(factorial(n-k))) +} + +func bernsteinPolynomialPoint(x float64, i, n int) float64 { + return binomial(n, i) * math.Pow(x, float64(i)) * math.Pow(1-x, float64(n-i)) +} + +func bernsteinPolynomial(points [][2]float64, t float64) [2]float64 { + n := len(points) - 1 + var x, y float64 + for i := 0; i <= n; i++ { + bern := bernsteinPolynomialPoint(t, i, n) + x += points[i][0] * bern + y += points[i][1] * bern + } + return [2]float64{x, y} +} + +func (t *HumanizeMouseTrajectory) generatePoints(knots [][2]float64) [][2]float64 { + midPtsCnt := int(math.Max(math.Max(math.Abs(t.fromX-t.toX), math.Abs(t.fromY-t.toY)), 2)) + controlPoints := make([][2]float64, 0, len(knots)+2) + controlPoints = append(controlPoints, [2]float64{t.fromX, t.fromY}) + controlPoints = append(controlPoints, knots...) + controlPoints = append(controlPoints, [2]float64{t.toX, t.toY}) + + curvePoints := make([][2]float64, midPtsCnt) + for i := 0; i < midPtsCnt; i++ { + tt := float64(i) / float64(midPtsCnt-1) + curvePoints[i] = bernsteinPolynomial(controlPoints, tt) + } + return curvePoints +} + +func (t *HumanizeMouseTrajectory) distortPoints(points [][2]float64, distortionMean, distortionStDev, distortionFreq float64) [][2]float64 { + if len(points) < 3 { + return points + } + distorted := make([][2]float64, len(points)) + distorted[0] = points[0] + + for i := 1; i < len(points)-1; i++ { + x, y := points[i][0], points[i][1] + if t.rng.Float64() < distortionFreq { + delta := math.Round(normalDist(t.rng, distortionMean, distortionStDev)) + y += delta + } + distorted[i] = [2]float64{x, y} + } + distorted[len(points)-1] = points[len(points)-1] + return distorted +} + +func normalDist(rng *rand.Rand, mean, stdDev float64) float64 { + // Box-Muller transform + u1 := rng.Float64() + u2 := rng.Float64() + if u1 <= 0 { + u1 = 1e-10 + } + return mean + stdDev*math.Sqrt(-2*math.Log(u1))*math.Cos(2*math.Pi*u2) +} + +func (t *HumanizeMouseTrajectory) easeOutQuad(n float64) float64 { + return -n * (n - 2) +} + +const ( + defaultMaxTime = 150 + defaultMinTime = 0 +) + +func (t *HumanizeMouseTrajectory) tweenPoints(points [][2]float64, opts *Options) [][2]float64 { + var totalLength float64 + for i := 1; i < len(points); i++ { + dx := points[i][0] - points[i-1][0] + dy := points[i][1] - points[i-1][1] + totalLength += math.Sqrt(dx*dx + dy*dy) + } + + targetPoints := int(math.Min( + float64(defaultMaxTime), + math.Max(float64(defaultMinTime+2), math.Pow(totalLength, 0.25)*20))) + + if opts != nil && opts.MaxPoints > 0 { + if opts.MaxPoints < 5 { + opts.MaxPoints = 5 + } + if opts.MaxPoints > 80 { + opts.MaxPoints = 80 + } + targetPoints = opts.MaxPoints + } + + if targetPoints < 2 { + targetPoints = 2 + } + + res := make([][2]float64, targetPoints) + for i := 0; i < targetPoints; i++ { + tt := float64(i) / float64(targetPoints-1) + easedT := t.easeOutQuad(tt) + idx := int(easedT * float64(len(points)-1)) + if idx < 0 { + idx = 0 + } + if idx >= len(points) { + idx = len(points) - 1 + } + res[i] = points[idx] + } + return res +} diff --git a/server/lib/mousetrajectory/mousetrajectory_test.go b/server/lib/mousetrajectory/mousetrajectory_test.go new file mode 100644 index 00000000..fc816a4e --- /dev/null +++ b/server/lib/mousetrajectory/mousetrajectory_test.go @@ -0,0 +1,57 @@ +package mousetrajectory + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestHumanizeMouseTrajectory_DeterministicWithSeed(t *testing.T) { + traj := NewHumanizeMouseTrajectoryWithSeed(0, 0, 100, 100, 42) + points1 := traj.GetPointsInt() + + traj2 := NewHumanizeMouseTrajectoryWithSeed(0, 0, 100, 100, 42) + points2 := traj2.GetPointsInt() + + require.Len(t, points1, len(points2)) + for i := range points1 { + assert.Equal(t, points1[i], points2[i], "point %d should match", i) + } +} + +func TestHumanizeMouseTrajectory_StartAndEnd(t *testing.T) { + traj := NewHumanizeMouseTrajectoryWithSeed(50, 50, 200, 150, 123) + points := traj.GetPointsInt() + + require.GreaterOrEqual(t, len(points), 2, "should have at least 2 points") + assert.Equal(t, 50, points[0][0]) + assert.Equal(t, 50, points[0][1]) + assert.Equal(t, 200, points[len(points)-1][0]) + assert.Equal(t, 150, points[len(points)-1][1]) +} + +func TestHumanizeMouseTrajectory_WithStepsOverride(t *testing.T) { + opts := &Options{MaxPoints: 15} + traj := NewHumanizeMouseTrajectoryWithOptions(0, 0, 100, 100, opts) + points := traj.GetPointsInt() + + assert.Len(t, points, 15, "should have exactly 15 points when MaxPoints=15") +} + +func TestHumanizeMouseTrajectory_CurvedPath(t *testing.T) { + traj := NewHumanizeMouseTrajectoryWithSeed(0, 0, 100, 0, 999) + points := traj.GetPointsInt() + + // For a horizontal move, the Bezier adds control points, so the path may curve + // Middle points should not all lie exactly on the line y=0 (curved path) + require.GreaterOrEqual(t, len(points), 3) + allOnLine := true + for i := 1; i < len(points)-1; i++ { + if points[i][1] != 0 { + allOnLine = false + break + } + } + assert.False(t, allOnLine, "path should be curved, not a straight line") +} diff --git a/server/lib/oapi/oapi.go b/server/lib/oapi/oapi.go index d4d2712b..8924c945 100644 --- a/server/lib/oapi/oapi.go +++ b/server/lib/oapi/oapi.go @@ -325,6 +325,15 @@ type MoveMouseRequest struct { // HoldKeys Modifier keys to hold during the move HoldKeys *[]string `json:"hold_keys,omitempty"` + // Smooth Use human-like Bezier curve path instead of instant teleport (recommended for bot detection evasion) + Smooth *bool `json:"smooth,omitempty"` + + // StepDelayMs Delay in milliseconds between steps when smooth=true. Adds small random jitter. Omit for auto (5-15ms). + StepDelayMs *int `json:"step_delay_ms,omitempty"` + + // Steps Override auto-computed point count when smooth=true. If omitted, derived from path length. + Steps *int `json:"steps,omitempty"` + // X X coordinate to move the cursor to X int `json:"x"` @@ -12298,144 +12307,147 @@ func (sh *strictHandler) StopRecording(w http.ResponseWriter, r *http.Request) { // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "i7R+/eBG/l8AAAD//yIMPJrxsgAA", + "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", + "jx9cyP8LAAD//1/o87DltAAA", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/server/openapi.yaml b/server/openapi.yaml index b0bc70d9..0a2ef60f 100644 --- a/server/openapi.yaml +++ b/server/openapi.yaml @@ -1232,6 +1232,20 @@ components: description: Modifier keys to hold during the move items: type: string + smooth: + type: boolean + description: Use human-like Bezier curve path instead of instant teleport (recommended for bot detection evasion) + default: false + steps: + type: integer + description: Override auto-computed point count when smooth=true. If omitted, derived from path length. + minimum: 5 + maximum: 80 + step_delay_ms: + type: integer + description: Delay in milliseconds between steps when smooth=true. Adds small random jitter. Omit for auto (5-15ms). + minimum: 3 + maximum: 30 additionalProperties: false ScreenshotRegion: type: object From 30369073de445886196211d9a77653c31f47ba2d Mon Sep 17 00:00:00 2001 From: Ulzii Otgonbaatar Date: Wed, 11 Feb 2026 12:23:14 -0700 Subject: [PATCH 2/8] refactor: address self-review feedback for Bezier mouse movement - Fix demo README: kernel-images or kernel-images-private (not just private) - Use await browser.close() for clean demo script shutdown - Add named constants in mousetrajectory (boundsPadding, knotsCount, etc.) - Avoid mutating opts.MaxPoints; use local var instead - Add ctx.Done() check and sleepWithContext in Bezier step loop for cancellation - Clarify OpenAPI: steps/step_delay_ms ignored when smooth=false - Add TestHumanizeMouseTrajectory_ZeroLengthPath edge case Co-authored-by: Cursor --- demo/mouse-movement/README.md | 2 +- .../demo-mouse-movement-video.ts | 2 +- server/cmd/api/api/computer.go | 111 ++++--- server/lib/mousetrajectory/mousetrajectory.go | 49 +-- .../mousetrajectory/mousetrajectory_test.go | 12 + server/lib/oapi/oapi.go | 286 +++++++++--------- server/openapi.yaml | 4 +- 7 files changed, 265 insertions(+), 201 deletions(-) diff --git a/demo/mouse-movement/README.md b/demo/mouse-movement/README.md index 4a0517f8..cfd28e1c 100644 --- a/demo/mouse-movement/README.md +++ b/demo/mouse-movement/README.md @@ -11,7 +11,7 @@ The cursor trail overlay makes the difference visually obvious. ## Prerequisites -- Kernel browser session running kernel-images-private (with Bezier support in `server/cmd/api/api/computer.go`) +- Kernel browser session with Bezier support (kernel-images or kernel-images-private) - `KERNEL_BROWSER_ID` and `KERNEL_API_KEY` (or equivalent auth) - Screen recorder (OBS, QuickTime, or `ffmpeg`) diff --git a/demo/mouse-movement/demo-mouse-movement-video.ts b/demo/mouse-movement/demo-mouse-movement-video.ts index 3c61b131..b94b2b0f 100644 --- a/demo/mouse-movement/demo-mouse-movement-video.ts +++ b/demo/mouse-movement/demo-mouse-movement-video.ts @@ -100,5 +100,5 @@ const DEMO_PATH: [number, number][] = [ await sleep(3000); console.log("Demo complete. Stop recording."); - browser.close(); + await browser.close(); })(); diff --git a/server/cmd/api/api/computer.go b/server/cmd/api/api/computer.go index 9494200e..5b49a485 100644 --- a/server/cmd/api/api/computer.go +++ b/server/cmd/api/api/computer.go @@ -56,12 +56,12 @@ func (s *ApiService) doMoveMouse(ctx context.Context, body oapi.MoveMouseRequest useSmooth := body.Smooth != nil && *body.Smooth if useSmooth { - return s.moveMouseSmooth(ctx, log, body) + return s.doMoveMouseSmooth(ctx, log, body) } - return s.moveMouseInstant(ctx, log, body) + return s.doMoveMouseInstant(ctx, log, body) } -func (s *ApiService) moveMouseInstant(ctx context.Context, log *slog.Logger, body oapi.MoveMouseRequest) (oapi.MoveMouseResponseObject, error) { +func (s *ApiService) doMoveMouseInstant(ctx context.Context, log *slog.Logger, body oapi.MoveMouseRequest) error { args := []string{} if body.HoldKeys != nil { for _, key := range *body.HoldKeys { @@ -80,21 +80,65 @@ func (s *ApiService) moveMouseInstant(ctx context.Context, log *slog.Logger, bod log.Error("xdotool command failed", "err", err, "output", string(output)) return &executionError{msg: "failed to move mouse"} } - return nil } func (s *ApiService) MoveMouse(ctx context.Context, request oapi.MoveMouseRequestObject) (oapi.MoveMouseResponseObject, error) { + s.inputMu.Lock() + defer s.inputMu.Unlock() + + if request.Body == nil { + return oapi.MoveMouse400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{ + Message: "request body is required"}, + }, nil + } + body := *request.Body + + // Get current resolution for bounds validation + screenWidth, screenHeight, _, err := s.getCurrentResolution(ctx) + if err != nil { + log := logger.FromContext(ctx) + log.Error("failed to get current resolution", "error", err) + return oapi.MoveMouse500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{ + Message: "failed to get current display resolution"}, + }, nil + } + + // Ensure non-negative coordinates and within screen bounds + if body.X < 0 || body.Y < 0 { + return oapi.MoveMouse400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{ + Message: "coordinates must be non-negative"}, + }, nil + } + if body.X >= screenWidth || body.Y >= screenHeight { + return oapi.MoveMouse400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{ + Message: fmt.Sprintf("coordinates exceed screen bounds (max: %dx%d)", screenWidth-1, screenHeight-1)}, + }, nil + } + + useSmooth := body.Smooth != nil && *body.Smooth + log := logger.FromContext(ctx) + if useSmooth { + return s.moveMouseSmooth(ctx, log, body) + } + return s.moveMouseInstant(ctx, log, body) +} + +func (s *ApiService) moveMouseInstant(ctx context.Context, log *slog.Logger, body oapi.MoveMouseRequest) (oapi.MoveMouseResponseObject, error) { + if err := s.doMoveMouseInstant(ctx, log, body); err != nil { + if isValidationErr(err) { + return oapi.MoveMouse400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: err.Error()}}, nil + } + return oapi.MoveMouse500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: err.Error()}}, nil + } return oapi.MoveMouse200Response{}, nil } -func (s *ApiService) moveMouseSmooth(ctx context.Context, log *slog.Logger, body oapi.MoveMouseRequest) (oapi.MoveMouseResponseObject, error) { +func (s *ApiService) doMoveMouseSmooth(ctx context.Context, log *slog.Logger, body oapi.MoveMouseRequest) error { fromX, fromY, err := s.getMouseLocation(ctx) if err != nil { log.Error("failed to get mouse location for smooth move", "error", err) - return oapi.MoveMouse500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{ - Message: "failed to get current mouse position: " + err.Error()}, - }, nil + return &executionError{msg: "failed to get current mouse position: " + err.Error()} } opts := &mousetrajectory.Options{} @@ -105,7 +149,7 @@ func (s *ApiService) moveMouseSmooth(ctx context.Context, log *slog.Logger, body float64(fromX), float64(fromY), float64(body.X), float64(body.Y), opts) points := traj.GetPointsInt() if len(points) < 2 { - return s.moveMouseInstant(ctx, log, body) + return s.doMoveMouseInstant(ctx, log, body) } stepDelayMs := 10 @@ -121,9 +165,7 @@ func (s *ApiService) moveMouseSmooth(ctx context.Context, log *slog.Logger, body } if output, err := defaultXdoTool.Run(ctx, args...); err != nil { log.Error("xdotool keydown failed", "err", err, "output", string(output)) - return oapi.MoveMouse500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{ - Message: "failed to hold modifier keys"}, - }, nil + return &executionError{msg: "failed to hold modifier keys"} } defer func() { if body.HoldKeys != nil { @@ -138,6 +180,12 @@ func (s *ApiService) moveMouseSmooth(ctx context.Context, log *slog.Logger, body // Move along Bezier path: mousemove_relative for each step with delay for i := 1; i < len(points); i++ { + select { + case <-ctx.Done(): + return &executionError{msg: "smooth mouse movement cancelled"} + default: + } + dx := points[i][0] - points[i-1][0] dy := points[i][1] - points[i-1][1] args := []string{"mousemove_relative"} @@ -148,9 +196,7 @@ func (s *ApiService) moveMouseSmooth(ctx context.Context, log *slog.Logger, body } if output, err := defaultXdoTool.Run(ctx, args...); err != nil { log.Error("xdotool mousemove_relative failed", "err", err, "output", string(output), "step", i) - return oapi.MoveMouse500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{ - Message: "failed during smooth mouse movement"}, - }, nil + return &executionError{msg: "failed during smooth mouse movement"} } jitter := stepDelayMs if stepDelayMs > 3 { @@ -159,10 +205,22 @@ func (s *ApiService) moveMouseSmooth(ctx context.Context, log *slog.Logger, body jitter = 3 } } - time.Sleep(time.Duration(jitter) * time.Millisecond) + if err := sleepWithContext(ctx, time.Duration(jitter)*time.Millisecond); err != nil { + return &executionError{msg: "smooth mouse movement interrupted"} + } } log.Info("executed smooth mouse movement", "points", len(points)) + return nil +} + +func (s *ApiService) moveMouseSmooth(ctx context.Context, log *slog.Logger, body oapi.MoveMouseRequest) (oapi.MoveMouseResponseObject, error) { + if err := s.doMoveMouseSmooth(ctx, log, body); err != nil { + if isValidationErr(err) { + return oapi.MoveMouse400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: err.Error()}}, nil + } + return oapi.MoveMouse500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: err.Error()}}, nil + } return oapi.MoveMouse200Response{}, nil } @@ -188,27 +246,6 @@ func (s *ApiService) getMouseLocation(ctx context.Context) (x, y int, err error) return x, y, nil } -func (s *ApiService) ClickMouse(ctx context.Context, request oapi.ClickMouseRequestObject) (oapi.ClickMouseResponseObject, error) { - log := logger.FromContext(ctx) - - // serialize input operations to avoid overlapping xdotool commands - s.inputMu.Lock() - defer s.inputMu.Unlock() - - if request.Body == nil { - return oapi.MoveMouse400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{ - Message: "request body is required"}, - }, nil - } - if err := s.doMoveMouse(ctx, *request.Body); err != nil { - if isValidationErr(err) { - return oapi.MoveMouse400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: err.Error()}}, nil - } - return oapi.MoveMouse500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: err.Error()}}, nil - } - return oapi.MoveMouse200Response{}, nil -} - func (s *ApiService) doClickMouse(ctx context.Context, body oapi.ClickMouseRequest) error { log := logger.FromContext(ctx) diff --git a/server/lib/mousetrajectory/mousetrajectory.go b/server/lib/mousetrajectory/mousetrajectory.go index 83a8191f..6d80e0ca 100644 --- a/server/lib/mousetrajectory/mousetrajectory.go +++ b/server/lib/mousetrajectory/mousetrajectory.go @@ -66,15 +66,34 @@ func (t *HumanizeMouseTrajectory) GetPointsInt() [][2]int { return out } +const ( + // Bounds padding for Bezier control point region (pixels beyond start/end). + boundsPadding = 80 + // Number of internal knots for the Bezier curve (more = curvier). + knotsCount = 2 + // Distortion parameters for human-like jitter: mean, stdev, frequency. + distortionMean = 1.0 + distortionStDev = 1.0 + distortionFreq = 0.5 +) + +const ( + defaultMaxTime = 150 + defaultMinTime = 0 + pathLengthScale = 20 // Multiplier for path-length-based point count + minPoints = 5 + maxPoints = 80 +) + func (t *HumanizeMouseTrajectory) generateCurve(opts *Options) { - left := math.Min(t.fromX, t.toX) - 80 - right := math.Max(t.fromX, t.toX) + 80 - down := math.Min(t.fromY, t.toY) - 80 - up := math.Max(t.fromY, t.toY) + 80 + left := math.Min(t.fromX, t.toX) - boundsPadding + right := math.Max(t.fromX, t.toX) + boundsPadding + down := math.Min(t.fromY, t.toY) - boundsPadding + up := math.Max(t.fromY, t.toY) + boundsPadding - knots := t.generateInternalKnots(left, right, down, up, 2) + knots := t.generateInternalKnots(left, right, down, up, knotsCount) curvePoints := t.generatePoints(knots) - curvePoints = t.distortPoints(curvePoints, 1.0, 1.0, 0.5) + curvePoints = t.distortPoints(curvePoints, distortionMean, distortionStDev, distortionFreq) t.points = t.tweenPoints(curvePoints, opts) } @@ -174,11 +193,6 @@ func (t *HumanizeMouseTrajectory) easeOutQuad(n float64) float64 { return -n * (n - 2) } -const ( - defaultMaxTime = 150 - defaultMinTime = 0 -) - func (t *HumanizeMouseTrajectory) tweenPoints(points [][2]float64, opts *Options) [][2]float64 { var totalLength float64 for i := 1; i < len(points); i++ { @@ -189,16 +203,17 @@ func (t *HumanizeMouseTrajectory) tweenPoints(points [][2]float64, opts *Options targetPoints := int(math.Min( float64(defaultMaxTime), - math.Max(float64(defaultMinTime+2), math.Pow(totalLength, 0.25)*20))) + math.Max(float64(defaultMinTime+2), math.Pow(totalLength, 0.25)*pathLengthScale))) if opts != nil && opts.MaxPoints > 0 { - if opts.MaxPoints < 5 { - opts.MaxPoints = 5 + maxPts := opts.MaxPoints + if maxPts < minPoints { + maxPts = minPoints } - if opts.MaxPoints > 80 { - opts.MaxPoints = 80 + if maxPts > maxPoints { + maxPts = maxPoints } - targetPoints = opts.MaxPoints + targetPoints = maxPts } if targetPoints < 2 { diff --git a/server/lib/mousetrajectory/mousetrajectory_test.go b/server/lib/mousetrajectory/mousetrajectory_test.go index fc816a4e..3de269d1 100644 --- a/server/lib/mousetrajectory/mousetrajectory_test.go +++ b/server/lib/mousetrajectory/mousetrajectory_test.go @@ -39,6 +39,18 @@ func TestHumanizeMouseTrajectory_WithStepsOverride(t *testing.T) { assert.Len(t, points, 15, "should have exactly 15 points when MaxPoints=15") } +func TestHumanizeMouseTrajectory_ZeroLengthPath(t *testing.T) { + // Same start and end: should produce at least 2 points, both at (0,0) + traj := NewHumanizeMouseTrajectoryWithSeed(0, 0, 0, 0, 42) + points := traj.GetPointsInt() + + require.GreaterOrEqual(t, len(points), 2, "zero-length path should have at least 2 points") + assert.Equal(t, 0, points[0][0]) + assert.Equal(t, 0, points[0][1]) + assert.Equal(t, 0, points[len(points)-1][0]) + assert.Equal(t, 0, points[len(points)-1][1]) +} + func TestHumanizeMouseTrajectory_CurvedPath(t *testing.T) { traj := NewHumanizeMouseTrajectoryWithSeed(0, 0, 100, 0, 999) points := traj.GetPointsInt() diff --git a/server/lib/oapi/oapi.go b/server/lib/oapi/oapi.go index 8924c945..7491d652 100644 --- a/server/lib/oapi/oapi.go +++ b/server/lib/oapi/oapi.go @@ -328,10 +328,10 @@ type MoveMouseRequest struct { // Smooth Use human-like Bezier curve path instead of instant teleport (recommended for bot detection evasion) Smooth *bool `json:"smooth,omitempty"` - // StepDelayMs Delay in milliseconds between steps when smooth=true. Adds small random jitter. Omit for auto (5-15ms). + // StepDelayMs Delay in milliseconds between steps when smooth=true. Adds small random jitter. Ignored when smooth=false. Omit for auto (~10ms with jitter). StepDelayMs *int `json:"step_delay_ms,omitempty"` - // Steps Override auto-computed point count when smooth=true. If omitted, derived from path length. + // Steps Override auto-computed point count when smooth=true. Ignored when smooth=false. If omitted, derived from path length. Steps *int `json:"steps,omitempty"` // X X coordinate to move the cursor to @@ -12307,147 +12307,147 @@ func (sh *strictHandler) StopRecording(w http.ResponseWriter, r *http.Request) { // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "jx9cyP8LAAD//1/o87DltAAA", + "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/server/openapi.yaml b/server/openapi.yaml index 0a2ef60f..60aecdd4 100644 --- a/server/openapi.yaml +++ b/server/openapi.yaml @@ -1238,12 +1238,12 @@ components: default: false steps: type: integer - description: Override auto-computed point count when smooth=true. If omitted, derived from path length. + description: Override auto-computed point count when smooth=true. Ignored when smooth=false. If omitted, derived from path length. minimum: 5 maximum: 80 step_delay_ms: type: integer - description: Delay in milliseconds between steps when smooth=true. Adds small random jitter. Omit for auto (5-15ms). + description: Delay in milliseconds between steps when smooth=true. Adds small random jitter. Ignored when smooth=false. Omit for auto (~10ms with jitter). minimum: 3 maximum: 30 additionalProperties: false From caa2fd37d19c818429c706020662b582cfe7b61c Mon Sep 17 00:00:00 2001 From: Ulzii Otgonbaatar Date: Wed, 11 Feb 2026 12:23:29 -0700 Subject: [PATCH 3/8] chore: remove demo/mouse-movement per review feedback Demo not needed in this repo. Co-authored-by: Cursor --- demo/mouse-movement/README.md | 62 -- demo/mouse-movement/cursor-trail-demo.html | 132 ---- .../demo-mouse-movement-video.ts | 104 --- demo/mouse-movement/package-lock.json | 615 ------------------ demo/mouse-movement/package.json | 20 - 5 files changed, 933 deletions(-) delete mode 100644 demo/mouse-movement/README.md delete mode 100644 demo/mouse-movement/cursor-trail-demo.html delete mode 100644 demo/mouse-movement/demo-mouse-movement-video.ts delete mode 100644 demo/mouse-movement/package-lock.json delete mode 100644 demo/mouse-movement/package.json diff --git a/demo/mouse-movement/README.md b/demo/mouse-movement/README.md deleted file mode 100644 index cfd28e1c..00000000 --- a/demo/mouse-movement/README.md +++ /dev/null @@ -1,62 +0,0 @@ -# Mouse Movement Demo — Before/After Video - -Create a before/after video demonstrating human-like Bezier curve mouse movement vs instant teleport, inspired by [Camoufox's stealth overview](https://camoufox.com/stealth/) and [cursor movement docs](https://camoufox.com/fingerprint/cursor-movement). - -## What You'll Record - -- **BEFORE**: Instant movement (`smooth: false`) — cursor jumps in straight lines between targets -- **AFTER**: Human-like Bezier movement (`smooth: true`) — curved, natural trajectory - -The cursor trail overlay makes the difference visually obvious. - -## Prerequisites - -- Kernel browser session with Bezier support (kernel-images or kernel-images-private) -- `KERNEL_BROWSER_ID` and `KERNEL_API_KEY` (or equivalent auth) -- Screen recorder (OBS, QuickTime, or `ffmpeg`) - -## Steps - -### 1. Start Screen Recording - -Record the **browser live view** URL. Options: - -- **OBS**: Add Browser source or window capture for the live view tab -- **QuickTime** (macOS): File → New Screen Recording, select the live view window -- **ffmpeg**: - ```bash - ffmpeg -f avfoundation -i "1" -c:v libx264 -crf 18 mouse-demo.mp4 - ``` - -### 2. Run the Demo Script - -```bash -cd demo/mouse-movement -npm install -KERNEL_BROWSER_ID= KERNEL_API_KEY= npm run demo -``` - -### 3. What Happens - -1. The script loads the cursor trail demo page (`cursor-trail-demo.html`) into the browser -2. **BEFORE** phase: Moves the mouse along the path with `smooth: false` — straight lines -3. Pause and clear trail -4. **AFTER** phase: Same path with `smooth: true` — Bezier curves -5. The trail shows the curved vs straight paths - -### 4. Edit the Video - -- Trim to show BEFORE and AFTER clearly -- Optional: split screen or side-by-side comparison -- Add captions: "Instant movement" vs "Human-like Bezier movement" - -## Files - -| File | Purpose | -|------|---------| -| `cursor-trail-demo.html` | Page that draws the cursor path as the mouse moves | -| `demo-mouse-movement-video.ts` | Script that runs before/after moveMouse with smooth on/off | - -## Implementation - -The Bezier trajectory and `smooth` movement are implemented in `server/cmd/api/api/computer.go` and `server/lib/mousetrajectory/`. When `smooth: true` is sent in the move_mouse request body, the instance uses Bernstein Bezier curves for human-like movement. diff --git a/demo/mouse-movement/cursor-trail-demo.html b/demo/mouse-movement/cursor-trail-demo.html deleted file mode 100644 index 938651c9..00000000 --- a/demo/mouse-movement/cursor-trail-demo.html +++ /dev/null @@ -1,132 +0,0 @@ - - - - - - Mouse Movement Demo — Cursor Trail - - - - -
- Recording cursor trail… - Move the mouse to see the path -
-
- - - - diff --git a/demo/mouse-movement/demo-mouse-movement-video.ts b/demo/mouse-movement/demo-mouse-movement-video.ts deleted file mode 100644 index b94b2b0f..00000000 --- a/demo/mouse-movement/demo-mouse-movement-video.ts +++ /dev/null @@ -1,104 +0,0 @@ -/** - * Before/After mouse movement demo for video recording. - * - * Demonstrates: - * - BEFORE: Instant mouse movement (smooth: false) — cursor teleports in straight lines - * - AFTER: Human-like Bezier curve movement (smooth: true) — natural curved trajectory - * - * Inspired by https://camoufox.com/stealth/ and https://camoufox.com/fingerprint/cursor-movement - * - * Usage: - * 1. Ensure KERNEL_BROWSER_ID and KERNEL_API_KEY are set - * 2. Start screen recording (OBS, QuickTime, ffmpeg) on the browser live view - * 3. Run: npm run demo - * 4. Record: BEFORE (instant) then AFTER (smooth Bezier) segments - */ - -import Kernel from "@onkernel/sdk"; -import { chromium } from "playwright-core"; -import { readFileSync } from "fs"; -import { join, dirname } from "path"; -import { fileURLToPath } from "url"; - -const BROWSER_ID = process.env.KERNEL_BROWSER_ID!; - -function sleep(ms: number) { - return new Promise((r) => setTimeout(r, ms)); -} - -const __dirname = dirname(fileURLToPath(import.meta.url)); - -// Movement path chosen to clearly show the difference: diagonal + arc -const DEMO_PATH: [number, number][] = [ - [200, 200], - [600, 350], - [1000, 200], - [700, 500], - [400, 400], - [800, 300], -]; - -(async () => { - if (!BROWSER_ID) { - console.error("Set KERNEL_BROWSER_ID"); - process.exit(1); - } - - const kernel = new Kernel(); - const session = await kernel.browsers.retrieve(BROWSER_ID); - - console.log("Session:", BROWSER_ID); - console.log("Live view (record this):", session.browser_live_view_url); - - const browser = await chromium.connectOverCDP(session.cdp_ws_url); - const page = browser.contexts()[0].pages()[0] ?? (await browser.newPage()); - - // Load cursor trail demo page - const demoHtml = readFileSync( - join(__dirname, "cursor-trail-demo.html"), - "utf-8" - ); - await page.setContent(demoHtml, { waitUntil: "domcontentloaded" }); - await page.setViewportSize({ width: 1280, height: 720 }); - - await sleep(500); - - // --- BEFORE: Instant movement (smooth: false) --- - await page.evaluate(() => { - (window as any).demoApi?.setMode("BEFORE: Instant movement (smooth: false)", "instant"); - (window as any).demoApi?.clear(); - }); - await sleep(800); - - console.log("[BEFORE] Running instant mouse moves..."); - for (let i = 0; i < DEMO_PATH.length; i++) { - const [x, y] = DEMO_PATH[i]; - await kernel.browsers.computer.moveMouse(BROWSER_ID, { x, y, smooth: false }); - await sleep(400); - } - await sleep(2000); - - // --- Clear and switch to AFTER --- - await page.evaluate(() => { - (window as any).demoApi?.setMode("AFTER: Human-like Bezier movement (smooth: true)", "smooth"); - (window as any).demoApi?.clear(); - }); - await sleep(1500); - - // --- AFTER: Smooth Bezier movement (smooth: true) --- - console.log("[AFTER] Running smooth Bezier mouse moves..."); - for (let i = 0; i < DEMO_PATH.length; i++) { - const [x, y] = DEMO_PATH[i]; - await kernel.browsers.computer.moveMouse(BROWSER_ID, { - x, - y, - smooth: true, - step_delay_ms: 12, - }); - await sleep(400); - } - await sleep(3000); - - console.log("Demo complete. Stop recording."); - await browser.close(); -})(); diff --git a/demo/mouse-movement/package-lock.json b/demo/mouse-movement/package-lock.json deleted file mode 100644 index 207e862e..00000000 --- a/demo/mouse-movement/package-lock.json +++ /dev/null @@ -1,615 +0,0 @@ -{ - "name": "mouse-movement-demo", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "mouse-movement-demo", - "version": "1.0.0", - "dependencies": { - "@onkernel/sdk": "^0.25.0", - "playwright-core": "^1.57.0" - }, - "devDependencies": { - "@types/node": "^22.15.0", - "tsx": "^4.21.0", - "typescript": "^5.9.3" - }, - "engines": { - "node": ">=22.0.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", - "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", - "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", - "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", - "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", - "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", - "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", - "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", - "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", - "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", - "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", - "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", - "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", - "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", - "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", - "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", - "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", - "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", - "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", - "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", - "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", - "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", - "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", - "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", - "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", - "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", - "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@onkernel/sdk": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@onkernel/sdk/-/sdk-0.25.0.tgz", - "integrity": "sha512-kMZ/499V9KCksvI/IxYx8juBKViWaCBLsoCOaZdfvLuXBy15310V+yoNUqu+J5q4Yfh+rHBcpFxn9H+GHfHfOQ==", - "license": "Apache-2.0" - }, - "node_modules/@types/node": { - "version": "22.19.11", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.11.tgz", - "integrity": "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/esbuild": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", - "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.3", - "@esbuild/android-arm": "0.27.3", - "@esbuild/android-arm64": "0.27.3", - "@esbuild/android-x64": "0.27.3", - "@esbuild/darwin-arm64": "0.27.3", - "@esbuild/darwin-x64": "0.27.3", - "@esbuild/freebsd-arm64": "0.27.3", - "@esbuild/freebsd-x64": "0.27.3", - "@esbuild/linux-arm": "0.27.3", - "@esbuild/linux-arm64": "0.27.3", - "@esbuild/linux-ia32": "0.27.3", - "@esbuild/linux-loong64": "0.27.3", - "@esbuild/linux-mips64el": "0.27.3", - "@esbuild/linux-ppc64": "0.27.3", - "@esbuild/linux-riscv64": "0.27.3", - "@esbuild/linux-s390x": "0.27.3", - "@esbuild/linux-x64": "0.27.3", - "@esbuild/netbsd-arm64": "0.27.3", - "@esbuild/netbsd-x64": "0.27.3", - "@esbuild/openbsd-arm64": "0.27.3", - "@esbuild/openbsd-x64": "0.27.3", - "@esbuild/openharmony-arm64": "0.27.3", - "@esbuild/sunos-x64": "0.27.3", - "@esbuild/win32-arm64": "0.27.3", - "@esbuild/win32-ia32": "0.27.3", - "@esbuild/win32-x64": "0.27.3" - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/get-tsconfig": { - "version": "4.13.6", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", - "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-pkg-maps": "^1.0.0" - }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" - } - }, - "node_modules/playwright-core": { - "version": "1.58.2", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", - "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", - "license": "Apache-2.0", - "bin": { - "playwright-core": "cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/resolve-pkg-maps": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" - } - }, - "node_modules/tsx": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", - "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "~0.27.0", - "get-tsconfig": "^4.7.5" - }, - "bin": { - "tsx": "dist/cli.mjs" - }, - "engines": { - "node": ">=18.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, - "license": "MIT" - } - } -} diff --git a/demo/mouse-movement/package.json b/demo/mouse-movement/package.json deleted file mode 100644 index 4cbfb199..00000000 --- a/demo/mouse-movement/package.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "mouse-movement-demo", - "version": "1.0.0", - "type": "module", - "scripts": { - "demo": "tsx demo-mouse-movement-video.ts" - }, - "dependencies": { - "@onkernel/sdk": "^0.25.0", - "playwright-core": "^1.57.0" - }, - "devDependencies": { - "@types/node": "^22.15.0", - "tsx": "^4.21.0", - "typescript": "^5.9.3" - }, - "engines": { - "node": ">=22.0.0" - } -} From 74df88832c06405cbc87d5830ed407267cc87bd4 Mon Sep 17 00:00:00 2001 From: Ulzii Otgonbaatar Date: Wed, 11 Feb 2026 12:24:18 -0700 Subject: [PATCH 4/8] feat: default smooth=true for MoveMouse (Bezier by default) Co-authored-by: Cursor --- server/cmd/api/api/computer.go | 4 ++-- server/openapi.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/server/cmd/api/api/computer.go b/server/cmd/api/api/computer.go index 5b49a485..1b675ce2 100644 --- a/server/cmd/api/api/computer.go +++ b/server/cmd/api/api/computer.go @@ -54,7 +54,7 @@ func (s *ApiService) doMoveMouse(ctx context.Context, body oapi.MoveMouseRequest return &validationError{msg: fmt.Sprintf("coordinates exceed screen bounds (max: %dx%d)", screenWidth-1, screenHeight-1)} } - useSmooth := body.Smooth != nil && *body.Smooth + useSmooth := body.Smooth == nil || *body.Smooth // default true when omitted if useSmooth { return s.doMoveMouseSmooth(ctx, log, body) } @@ -116,7 +116,7 @@ func (s *ApiService) MoveMouse(ctx context.Context, request oapi.MoveMouseReques }, nil } - useSmooth := body.Smooth != nil && *body.Smooth + useSmooth := body.Smooth == nil || *body.Smooth // default true when omitted log := logger.FromContext(ctx) if useSmooth { return s.moveMouseSmooth(ctx, log, body) diff --git a/server/openapi.yaml b/server/openapi.yaml index 60aecdd4..9f0483a0 100644 --- a/server/openapi.yaml +++ b/server/openapi.yaml @@ -1235,7 +1235,7 @@ components: smooth: type: boolean description: Use human-like Bezier curve path instead of instant teleport (recommended for bot detection evasion) - default: false + default: true steps: type: integer description: Override auto-computed point count when smooth=true. Ignored when smooth=false. If omitted, derived from path length. From e891d46a010e7f1383592d10683fa82292e461c2 Mon Sep 17 00:00:00 2001 From: Ulzii Otgonbaatar Date: Wed, 11 Feb 2026 12:27:22 -0700 Subject: [PATCH 5/8] refactor: replace steps/step_delay_ms with duration_sec (0.05-5s) Align with Camoufox humanize:minTime/maxTime. Steps and delay auto-computed from path length and target duration. Co-authored-by: Cursor --- server/cmd/api/api/computer.go | 20 ++- server/lib/oapi/oapi.go | 291 ++++++++++++++++----------------- server/openapi.yaml | 15 +- 3 files changed, 161 insertions(+), 165 deletions(-) diff --git a/server/cmd/api/api/computer.go b/server/cmd/api/api/computer.go index 1b675ce2..81d9ffc5 100644 --- a/server/cmd/api/api/computer.go +++ b/server/cmd/api/api/computer.go @@ -141,20 +141,24 @@ func (s *ApiService) doMoveMouseSmooth(ctx context.Context, log *slog.Logger, bo return &executionError{msg: "failed to get current mouse position: " + err.Error()} } - opts := &mousetrajectory.Options{} - if body.Steps != nil && *body.Steps > 0 { - opts.MaxPoints = *body.Steps - } traj := mousetrajectory.NewHumanizeMouseTrajectoryWithOptions( - float64(fromX), float64(fromY), float64(body.X), float64(body.Y), opts) + float64(fromX), float64(fromY), float64(body.X), float64(body.Y), nil) points := traj.GetPointsInt() if len(points) < 2 { return s.doMoveMouseInstant(ctx, log, body) } - stepDelayMs := 10 - if body.StepDelayMs != nil && *body.StepDelayMs >= 3 && *body.StepDelayMs <= 30 { - stepDelayMs = *body.StepDelayMs + numSteps := len(points) - 1 + stepDelayMs := 10 // default when duration_sec not specified + if body.DurationSec != nil && *body.DurationSec >= 0.05 && *body.DurationSec <= 5 && numSteps > 0 { + durationMs := int(*body.DurationSec * 1000) + stepDelayMs = durationMs / numSteps + if stepDelayMs < 3 { + stepDelayMs = 3 + } + if stepDelayMs > 30 { + stepDelayMs = 30 + } } // Hold modifiers diff --git a/server/lib/oapi/oapi.go b/server/lib/oapi/oapi.go index 7491d652..4218ec28 100644 --- a/server/lib/oapi/oapi.go +++ b/server/lib/oapi/oapi.go @@ -322,18 +322,15 @@ type MousePositionResponse struct { // MoveMouseRequest defines model for MoveMouseRequest. type MoveMouseRequest struct { + // DurationSec Target total duration in seconds when smooth=true. Steps and per-step delay are auto-computed from path length and this value. Aligns with Camoufox humanize:minTime/maxTime. Ignored when smooth=false. Omit for auto (path-length-based timing). + DurationSec *float32 `json:"duration_sec,omitempty"` + // HoldKeys Modifier keys to hold during the move HoldKeys *[]string `json:"hold_keys,omitempty"` // Smooth Use human-like Bezier curve path instead of instant teleport (recommended for bot detection evasion) Smooth *bool `json:"smooth,omitempty"` - // StepDelayMs Delay in milliseconds between steps when smooth=true. Adds small random jitter. Ignored when smooth=false. Omit for auto (~10ms with jitter). - StepDelayMs *int `json:"step_delay_ms,omitempty"` - - // Steps Override auto-computed point count when smooth=true. Ignored when smooth=false. If omitted, derived from path length. - Steps *int `json:"steps,omitempty"` - // X X coordinate to move the cursor to X int `json:"x"` @@ -12307,147 +12304,147 @@ func (sh *strictHandler) StopRecording(w http.ResponseWriter, r *http.Request) { // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "H4sIAAAAAAAC/+x9eXMbN/bgV0H1TpWlHZKifGR+8dT+odhyok0cqyxnM5PQywG7H0mM0EAPgKZEu5zP", - "voUH9I3mJctH9leViikSxwPegYd34X0UyzSTAoTR0dP3kQKdSaEB//iOJq/hPzloc66UVParWAoDwtiP", - "NMs4i6lhUpz8W0thv9PxElJqP/1FwTx6Gv2Pk2r8E/erPnGjffjwYRAloGPFMjtI9NROSPyM0YdB9EyK", - "OWfxp5q9mM5OfSEMKEH5J5q6mI5cgVqBIr7hIPpZmhcyF8knguNnaQjOF9nffHNHCiZePpNplhtQZ7Ft", - "XiDKQpIkzH5F+aWSGSjDLAHNKdfQnuGMzOxQRM5J7IcjFMfTxEgCtxDnBoi2gwvDKOfrUTSIstq47yPf", - "wX5sjv5KJaAgIZxpY6fojjwi5/iBSUG0kZkmUhCzBDJnShsCdmfshMxAqrftY3NDLL5SJi5cz9NBZNYZ", - "RE8jqhRd44Yq+E/OFCTR09/LNbwt28nZv8FR3zPO4uuXMtew6yY392eWG+Poobk9OCRxv9o9YZbsaGzI", - "DTPLaBCByFMLG4e5iQaRYoul/TdlScIhGkQzGl9Hg2gu1Q1VSQ10bRQTCwt6bEGfuq/b079ZZ4CIt208", - "bmqzJvLG/plnkR8mOMFS8mR6DWsdWl7C5gwUsT/b9dm2JMltV8SxG7WG3M7oTZQNIpGnU+zlp5vTnBtE", - "botx8nQGyi7OsBRwcgUZUNOY149ut30ByN+33VX8g8RSqoQJanC3ygFIJjXze9Ydad0d6Z+HjNQi09vI", - "Dh0k0ibx7ysGNBMLDm0hUJcBVJOMKsfHTmqMyJslkH9ZUP5F5gx4QjRwiI0mN0sWLyeiGiUDNZcqHRAq", - "ErdyqdzpllhycL2tNKXMCoglFBBkVNEUDCg9mojzWxobviZSlL+7nqmFp6ArCxBJc23IDEim5IolkIwm", - "oiO4HHeklg23ypaODLDSWtHFbt2fK7po907lCnbr/VKuoN07U6C15bxtnS9twx9hXeurYyU539bxClvV", - "u4GZxrnS7ujb2BXMM2xY780Bsq0dbaNKfvcIrgLH5ZFSo7BRTYTV8dvYbzfy1MCtiepbWW5NA7eNlRcL", - "CQnDatAty7Si9w3cmnJ7WmyOIwe5XAE18JwpiI1U68POo1QmgV19lbnuJClGJ7YhOZKxoZy4VQ4IjBYj", - "8rcnT45H5LmTvyhe//bkCSoG1FjVKXoa/d/fx8O/vX3/aPD4w1+iwF5l1Cy7QJzNtORW2lRA2IZ2hhiX", - "3prkZPQ/u4O3NhNnCm3mc+Bg4JKa5WH7uGUJBeAJTvPxAX8NMR4ni8OgZ0kX9ovEanl4aPsDShWT1FZC", - "zni2pCJPQbGYSEWW62wJoo1/Onx3NvxtPPx2+PavfwkutrswpjNO11b1Z4s917ME1I86a3qWKwXCkMSN", - "TVw7wgTJ2C1wHTy+FcwV6OVUUQPbh/StiW1tB/7hHTlK6doePyLnnLA5EdKQBAzEhs44HAcnvWFJiKDa", - "s2GzjfAHt7Z9At2PDmvFZo/+WuqtTpENCdAEOF03VLtxW1V5bpvY1aeMc6YhliLRZAbmBkAUgFjdFTUN", - "bagynnqt/CeUS68lWO4aIViCpRbQcQgnd9Fv7V7spd6GBUr7FvX77YCs39aVyYwypcslmqWS+WJpdTDu", - "gFgwsRiRl1Yj8ioWoYZwoNqQhySTTBjduGW1Qa5tSEpv/ZXqYf1+9bC7mo0/agPZFNE9TZvK/JM9Ua6A", - "U8NWQOyQurVqcmQZzyKDCWbvrwTnPN6OeBxtmoGaalik/o5f3TjG/VeOEiDEhoMqA0X8OHYhJf2Rlw4I", - "ctqA6HTrRaD3bChNE60zH7SmCwiQYWvgomFwbHcVuOR0fYNMfJjRwfeqXy6qIUlstZOOph5UWawadYV/", - "n/xvuqLuIw7QMDG8wetGAmRJNaFxDBqZ5UFGF/BgQB6gEefWPHCXkwczJW80qAdkRRWz0trfPNKMw1My", - "iegNZYbYzqOFNPLowdKYTD89OQHXZhTL9MHx34kCkytBas0NMxyOjv8+iSYipBPZy6rMzVRD3KC2bzrU", - "9pLeItlAaT2xfS1pefYotTPCNPlmjNTl+kRPH43He9Eabv6O9KAR4D3JwXaynNOigmp1HXqAgsqbQyHx", - "E0/C9tit9mdOGYcktOuqBLp7zVhRnoPHJCRktvZ3V6sXszmhYn3shEUCKgDPlaEioSpxhiwyVzLFAeoL", - "68CjTSJzs2EwmZssN7uOliPBd4f7dQlmCapakOeXhPgu85zzdTXkTEoOVHSoo5ggRCAvGIcLMZddecT0", - "NGFqM1SoQDNNaHUbGAXgsTe7ZGrpvzvcT/aIS/GgdqZZ5JORs5ql1ERPo4QaGGLvwO6Fr0p2We5yNGNG", - "kyN7JxqQSZSom1s1tP9NIqsXT6Khuhmqof1vEh2PQjMIGoL7O6qB2J8KPXxup5QquBM7X6oKladLJOwd", - "TGdrAwE6uWLvULDgzyMyJvMaGAz0aLvVCtfooWtMNijooIZDv+l95HS11gbS81V5IrcRo7EBiZdULICA", - "bdg1We9CfnQ+h9jyw850eCguy6kORep+VBK2quCWol2lbkJ59vr87M15NIh+fX2B/z4//+kcP7w+//ns", - "5XlAjQ/ZMgb9CstPTBvEW2CNVlu0a+vuGBOOgS1LgzAFIe7kJyilUkAF/0kuemjrjHC5wLnWleitOX26", - "RFbTuVpSSS7KQ8pqHqM+ZUAbmmaBk8me9Xb6CqIbqkmmZJLHjop2EW89ml996hDC8Mp36U3Wr72Hsivh", - "d7WlF2a1w23ofSPsbDvv2Ff3tDzc4Y5o7wh73RF1KmUhD7x6GNSqftFAlnlKxZCzayDfwTsLRpyrVXGw", - "Cm2AJnYX7UdqmR84ZFIZcqQglmkKIoEE5f2sNF8gw62oZlIcB4Vg4Ga3+2WuuMPZj7jQ/2VUDiNyliSa", - "6JRyThQViUzJv5kxoEbkYiGkvR3X++CGjMirlBmEnuZGkqM/TsepRtuE73zc0oprSvGj3ith4H6+AqVY", - "AjjN0HtQEnetJrHMhQksaAPYF3MiUwtgMiAJKLayOLACB9HGQSy84aKA/L/qkD85xK9VmQ4KdiJGHsST", - "u460F28ebqJNQJvpNlMzaGOBd94mt7fbLLWDSKt428Ba5iqGncds69XFBIPaKkI79Oq6LoT3uHh9DwIt", - "uK9+JEWgSfcQk9cNWWOptxsukdgzEHRxcxhtvzXI6+BaLqmJl94KfKAw7jEDP+83/5a88/DxeH9j8PNe", - "I3CTlXMNzrG5ZIslaEPoijJOZxxcFyuonMUdycdrFF4P+2Y8eDQePHwyOB2/DYOIWztlCYft+JoT/NqC", - "nGtw3nGrlTtpxNkKyIrBjdW4Svv/iQJcptWDY8NWEFaBFaDJdRovlUyZhf19/+zYlDzzTQmdG1C19Rc6", - "vJEEhM4VEGYITWjmXE4CboiFumHqQJrAvVwCTeY5H+Bs5Te8hzx7re/Pe63u1WHxcLybDb7tit1TluWK", - "Fr79DfZx36pUNixNofaBRvHWwVsnUYvu8cC1pQqIoVnmlMmDTeSlTzHdpgddw5qgH9bHGsUw2kstCs//", - "kzeZ29H1Op1JjpPjRCNyTuMlsVMQvZQ5T8gMCK21JTrPrDrkDD+3iTRS8ok40gDkH6enuJZ1ShKYo3FZ", - "Cn08It5QqAkTMc8TIJPoNZqPJtGATKKrJZsb9/GZUdx9OuP+qxdPJtFo4szmzk7MtLP7xwgg5VpaKGOZ", - "zvyRpb1L1o33V1NYHvAvnO2vb+gMh91jQ1vSGnc3KK+VtAL//Bbij2YLpnZ5KXpv1sLKESFzHYw7U4um", - "6+D3t90gQjcSVYs8hbabYytVUT1VUprt2vbr3Bv13X44LdN2JZliK8ZhAT1ih+ppriFgimgPSbUjB9va", - "DiVyjqdHIeO7sV9u7YGbPm40njxSEb0Ezsstt2dBLoIX0vgmMNavUl1bHq5u5ke0bpk49iN6M6ObhInQ", - "ArbrXCBW/eT1PuRO9Dh73wmtPBcrpqRAh0xp57ewajDlUey3vrYbFeV3bPX7mef7EdhvhXfo3MqGdzLB", - "0zrTlQgr19FlwuJUCl72quDOolnnAAreMuCWmWnY5+OXSmwTtFuHR3AW+ensm8dhg9w3j4cgbPeEuKZk", - "ls/njrN6LPK7DiZz0z/Yh37s/ciqaKv90HfFFvaQRep1PNyi3ibKNDZvCLXozfnrl9HmcetmQd/8x4uf", - "fooG0cXPb6JB9MMvl9utgX7uDUT8GlXRQ08TVGMpuXzzz+GMxtf2Ht63DbHkAZL9GW6IAZUyu/JY8jwV", - "eptvdhApebNtLNtkTycvjjpwgG7YsauM3jTivzl/NY+e/r4tLrBzdH8YtI14lHNpr3ZTY9bbT8Ez35pQ", - "kmnIEzksV390+eafx23B6jR7PIiK2Gd05NsTqee4DCPtwjv324hzF5r6IuwdAS0wB6K0M5Ntdvg0XXHw", - "toPXA+T5Rc06TmdWIFGi7Wib+CELRYS9uiqRdfE8LGr979NQd5dAMaTa8j0khFUBZoFDtjRa5zlLwoKY", - "WnV8Sk3YKI5Ga4eNOpn5bnvYxXtZzVCT6z2xUQRwaezsTtl+qZTl0ywOrO9cG5ZSexl5dvkLydF5kIGK", - "QRi6qJ+CAiNRthyj58XxSdi8sVdL6s5Wt13bdJRBlELa5zmsIFagEfMkhdTqiA760qnYc4IHzS2XFU5N", - "w1OlciEs+tyyIQmfRf2ITdiBOTTPqaFWkt0o5gygLdJzTnsmsjzgiEyooTspFkl9ltFW62E57tuta76T", - "vmjB8fF12g7XXaFtYUD0EUkVN4UNiG8+inY1qfilKKCVV3gf3enqnGR0zSW1ZJop0FZCiUWJQR9tIRXh", - "bA7xOubeq6zvis3Si1gRi11FUAWFsFPypyZIHfetZYVgpOVOoqEUpG5wpskEO06iPpa18AdOAWcIdz8X", - "bjvcgniZi+s6wD74pQyp2Y2JXSg0qHCsyZwJppe7HRtVvHPRq+/Q2Hr/dudh92tdBm7Xfm840HY+5Cpo", - "facDgW0JDzx863CGhMhVrACEXkrzGha7pBztZqf/wdnny/Dzhb80bgjW7rHc/ooW230G2tFl7cZ6YNXX", - "bMhhbrlFCbiTE3uPMYOus2IXBsXGbkPZIRZoVSJ6S95QkzCCLNvMLtrXq8cNnd5uNoT/IBV7JwXmruBc", - "hKYyF2ZEXOyCvWjg95pgyOGACFjQxvcWD2FJ5yDYEqr+fyzE8Q7zJ/JGBKbPs/Dkd4k3KPObdjeCbuMK", - "aly6Xy0JqznV/kyx95A7u5M7mWl7Si2WJCC2BFM6t3flU/CdtvpEfbsesF8wDpf21qk1k0IfBv9CyTwL", - "GyrwJx+npsj3jdvevgGRgZSxbx4/Pt4vQ0zeiJBd3MKKP6ElvID3lx54dwmeu1lKjXepYm+d+8t5WtAF", - "mRyavbUhmLGe6rifynpJcw310Gap8H4PseX9pLS17mmsrXsOMccxZKutB5E3wsjHW5myPnlwQ6wK80L/", - "Sk38URPyymxJvD5h4nI4DNwyLlvBdjtXye1+PFL25esdYh96IzlwB+6Y1jdXNIVwpMLrSrctGlkUzzPL", - "sT6ISRPtal74HTiu4/zheJvRLGhCKpzAAeNPTYEF5L2PlFyIQBcEfSGuHAH3O2oqOOqOiiI6bfPubNyQ", - "lN5i1DJ7Bxfi5Xf9EGCIq/ax1i+/2xEjpy0uPN0xEuHKyOyuhCZVDHac7fxykaaQMGqAr7HKB7pHZW7I", - "QtEY5jknepkbqwWNyJsl0yTFeBq0MTCBDmGl8sxAQlYsAYmbFbYP75PV6jjYAnSPKa3tVO+9Nd27JURa", - "PdAoeQ16axxHkbjeunHCLXrnXcK9MwcsJUYkuFIRWw9CHLcr7mwz5u/rmBgVPY1+BCWAk4uULkCTs8uL", - "aBCtQGkHynh0OhqjZpCBoBmLnkaPRuPRIx9djht2UgQencw5XRSnQhw4Fl6CWgAGEWFL57KHW6bR+iMF", - "6AHJM3uJJq1BA6FLK0aJzjNQK6alSgYTQUVCMPMrF4Zx3Lay9XNYvZGSazKJONMGBBOLSYTR3JwJIEwT", - "OUOut/rjXKoiBQkFpY+xw3gOSytOxiWoGJh4WczyAtfvUAHafCeT9V4Vi1rcXuxmy7RdLMntoZEkxW31", - "KTG/T6Lh8JpJfe3iW4bDhGk64zBcZPkkent8eEiKAyhMVlU7o3JwUWlVHa2H43FAg0X4Hb4TzAMsl+aR", - "3U6M+jCIHruRQpfhcsaTdtmuD4PoyS79mjWvsABUnqZUraOn0S+OLksQOc1FvPRIsMB7mLFbRb15xiVN", - "hnBrQKCiO6QiGRZtLc6lDoiAX7AbFl2RiqSWHMshyDuWEariJVtZhoFbgxWMzBJSkgsrYk+WMoWTa+Ts", - "k2rqk0k+Hj+Krf6On2AwERoMUZZf0voMblVMHMCGpODCifiEbOj267xc6plIXvs93sSOac4Ny6gyJ/a+", - "O0yooZs4strK/ri3qo1lTYd+3BP0tFolscZ/zeHDuUwvJLc4xVuXvZtzGoPPQSzQtR/WWwfs2fA3Onw3", - "Hn47mg7fvj8dPHzyJHw5fMeyqdUCuiD+VhFkke1u8UUtZJkLCSgpoIL6COsFFTF7KRVsDtqMrFg8rhtV", - "Z0xYFtx25pXg+aSwkLa/UbzVsHuYjDsNGfZLanCkAMkgIOYc15TMwTRRQJPPLfA6IqjEZo3Ij6i2Akkf", - "14VguUQvDb3ecjIr9IKw1DsvwhEFka0KDJ1Seaik+hJaZ5cXJKacj8iZ/5UqKKxYkFgpVxXT8+UKlpIn", - "nkjhNua5vUoSLuPrAdGSCEkk3jfRh0hKYaNJTIWLnOBAV4Bp6tuq6ZUFuIqNJ6wM33c2t6KwFiZMjyYC", - "NXIXeGhVdXt1i5eeqxJwgRBWa4rL0F30cWPtJJztGtYzSVVSbNdEFPp/Rtd2FAHmRqpromQukqFRLCOc", - "GhDxGmcDjNMVCVuxJKfcDxOSvIG6iHfQgDYZuTdUYDxUBTnjvCSocC725+TAkh02VIysU3aL2Vql1gqW", - "a6KvKrJ2T1gLVHE7EFmu7k1Ro65g7s+KoSuW5txFXzneqxd2DF/dWjgqa7uF0VPazu8JO92qcTsj56PM", - "X0ukCtWjdWb9FdNsxjgz6/K28MXw6A8s8SHd8qaeGttEc7NqYfjww0wVFN7oQCooypVXGhDpTXr2AKNF", - "kqWdVhlXYGdgpxftkksLtgKXY+fPZw5UAx4x9WoPWwodhQR/Wd3qnkizW7/xQLlhB/pC5AWC4nKQUZYh", - "mijioUUxCzCOYKZlpdJeIfE9mEaCeHSPDBvORA/zLkbguZWWi/gYu/g9mILValN4H2Ax0y7St1kONLy5", - "ZaL6PZF5t9DonY5Hvwt2ZZ+X1F8WKckN7PhQvcpxVkkavQvGGiVYN8hRn/dZzYPOeZSZohSlldeO/Gh/", - "rtzHteS1iQilpI3IC5S/FjAFS3sZsteHbu7bgGiAibDAhPPXCDWkqGYVL5gZzRVAAvrayGwk1eLk1v4v", - "U9LIk9vTU/ch45SJEzdYAvPR0slz77VYSiGVrhunhxxWUK3XXiy8Tyr2W4HeR+0tCQ4LMgkaPH1C5T2x", - "Q6d07oHcgAhFavmStAV3xtev1EiXOxC+LiN8+kXVG3oNVSTQfWmMnYCmDx5HG08cltIFnGQuAK+aabuR", - "p3OwVAAQHPSzIvQZzUyurP5fIajweG1Bpy8HHRZiLlSLrHw4E19b7e1EWt4uQqzsd6am49UkaVNbbJg7", - "GlnBXg1sxEo52wkThMsFRlIZFl9rV9TRxfE5S0+NgsgMlnTFLEnTNVlRtf47MTkaK3xJ1oKBRxPxq1VS", - "Z9Isa0vBAYu1Egz08iYcX0l84KS5E284sxPwaeP+S47KMVAVriY4ds4hvEaj0QWA+4hiLwr/5QW7v8EN", - "h754/c9kOET1moyJM6Q6hdyZUv8VkpBXRcTUPbFfvUL4gdLRk9cXcol2wFS6gkMPNVYz3kObK2ps9QhH", - "7xS+J7x0y4sfhhnn+11nX9Kpha9WGAtYPxZ8peSG8zfgKfWlHe5LeQiUMvnEBo1mOe3A8fWLt2AUpaVj", - "bFnUmbgDmh+Pv93er/mg0Ef0i/Ysx5LGXJ+4QvLTMmMdySQPmSObxfbvyyYZLul/qJOninZz6/yCWNet", - "lFAMuqi2v8CLqy6/A15c+fv7xkv3dYCDbT4lStwSk7tx1uPt/ZrvVH0UYxFCXi+B2cZb4Y3dgLIXziP6", - "ZWMLY5n/BIhCfJQ4kjeCS5pY7pq+YxiztwATihE1uRKaUPLbxaULSqw50V0RD0SXLm4WtbjjetXRFv79", - "/M+Z+o1l6PQvnvXBRPWdXwEpPPtWgy4WhTVdbL//5IDiwMUuFBHYTRoY1AMqtkV0v93rcPb7eqcLpd31", - "Yo1lsCISVn2Dv0a69MiqixBCC0LzS+6hV22SHQjWUDV6pw05MlTVIkDSwvCCAX52rOONdD0RGwib/KZN", - "QuR8DkoTzRYCC0sLw9dkTrUBVU6IqfcimYgE6l/Zz1QBFul4xzJ/IabxksHKQjID0x4F2Sjs9ahxld2j", - "r4WtBu+7ZZrK5aJ1cER+YIslKPdXWdrWlQCFEr2azHJDDL0GwqVYgBpNxNBhQpun5A+LbTcEOR0QHzVt", - "EQsJOfrj0Xg8fDIek5ffnehj29EH2TY7PhqQGeVUxFaVsj1PEAPk6I/TJ7W+DnHNrn8bFPgsujwZD/+r", - "0akD5ukAvy17PBwPH5c9ejBSo5YpDhPV0VEVeSk+Vem2fquiQe03BzJ+0KHk4X2loufeO4nFN563/z8T", - "jaa57FI8Wvk1LYKnvVhsioayxvWuMmFrGfEv4YTdTyes6nx3CQq1vFoR8a+QbL4H0yiDXhR66WCvJBvO", - "tEE9XffSTVWN/bDD5OuklGrVAVKprm/cJQd8hbSCAcGIeRer2KUNLETed30rijDfo9v5Y1zd0M1bmTu+", - "QjzhCrDsLoZYb2JmBTQpL91BXn4NNPFX7t1YGScrVEI7/pfCzTI2YIZVeZE76RIo+u3qPppl7DMRi8Vv", - "dZXBN+ML4tDgBP20ltXcy93d5PL7C/DryWI/lONrQxXheF8hIq/ABJ44qaHuBBPe9ZJlJYZdBH+/0/aM", - "c3lTBPpjwooLT5eKuEQTDv5A8GEwClLpZYB7QmfUk9hSqAcfLZOl1Eh6UlEOKeJfK8jlFdrdyvoXAnXf", - "hA+f7LG5Uv/mhDbchY+W7IFYKvM8vnZRF8j/mHt9rc4OhWlzYx4bRcML8purbetS1pjRlW2zExoWeiQi", - "xBzOuvnRWGNf0k/qtQ5qyXjlxdnI3fignl91h+SnTfxwIGH/xrKKrGsI/NMQOa3nVLZItEPv3riyheD3", - "NY328cVEbGeM7SbShkV0Ilom0f6MSm/j/GjMVVhVwg/MtyxOxRGylRkGn49p7adsWtHd5sIBVeVFDk5F", - "wIOz6u6qIyiWFQWkPGyYL4mvSllyGg6xzbDqt/V13Za8KPBwL+LizO/hn1xktMm1R2zctHMeWzeBWgme", - "+7oDBKr87I7bA6sX4LKDBYp/Eew/OYRK01RceeO3Y2u1j+5dE5dJPnb9gM9EbG4xdSO1zwUVi5omhrt1", - "8r7Y8g++jAm4ikRtepNZRW4tIwUaHrylwdsdSjxusj1sNzUE6rMWiJJZ9vUj6gpr7NgVYVJxwHjURtKJ", - "iz/tNSW5+rov9Llr9glx1TYLGbg1DtqgPWibP6D+fGwonvvqvFamtroL+/hcLK9JE1z1++gfw6ur8+Ez", - "B9vwTfBV1ZeQMOqL58yJHR7r3vpw36O2EDtueO4KL11H1AWcch++RjLFje7ssk8ndGK3pFh7md8cZPSr", - "bbKLwfN5TfmiHePnJ/R7l6XR5mUBxd7aicUrZ6iWffP4cR+YqXs/PwjWxoqLjvl2OfHvaI490JpRlAb/", - "6o9RNEvZk7OIh6xCtbhc6JNqY8MuOrnw9c575HCLINwzlBsptxA0xQvdZQmdYP3t8DRzybm8CUceNIpO", - "18oittEsBV+X+RmEzYsnNJkmHrQNjNl/quwzT23t4dmqBlNftz36bCda+Vr11qPMEtYXfXqFTgYLNJEr", - "UHZqxyAZp+sbrNd84mtk7FDBRc2YUVStyWXZ2799ISz34bOdVTlVRM2tIXRBmdDuJj5T8kaDIv6RiYmQ", - "gnAZU76U2jz99uHDhyPyBoPIEsAnNGhcPHDzIKMLeDAgD/y4D1x9nQd+yAfV82M+A0qVjyuYYsQKOKzG", - "Y3KFj6mIRiGXkOHEb0G17mfudLiPm11nrs+U9RCAA5+4COWFV5v7JdZaqZaAKT1XCLmjiABxegZxMgm5", - "o/+iX3v86d5yZ7vPS31aOug+iheggKpgkvJtvogaO8EXMJsIxvectmIY35C6XxQ3nh/7PDiuv5QVOgrd", - "01dfGG7pBuS+rx7V+nByzZrZuUFE/8gwzXP7vbz2XNcmlXDLW1y7XxYOQmj9LcQvqgrQqx+/yvgCK0rK", - "xxwLtbWf4txz4Ftpzj23+OehuubTk/9Nd3cPUOp9jnMD8enyjb3g9bf5Et+npr17PsfcokJHmP/lq4xS", - "rj2G55bXj/qE7aDTYKs/jdRpPD34mfSn2kuAAeL7rv4y31drcatOPvdU4WY6lLnZZoirNk/mZqNF7jPJ", - "oztYlgLvKm61MbVeTLQ6bvvJxP92oNyDA6VG1TI3LYNZ+bLJSeWEDUtXlzlcPfp3n4nanbdH+us29b1h", - "89lStD9TbYsysTtTsGJ4ZyzeMak/i9LBuk8u65ViRfZZHfEbvWel06p8RaWKnhgRLKkkU3tUNCsl5UUd", - "PO8VKLv3ObJQ6IXdWNveYdkuGnHDTtLs8Z3TCWqvKjnXY0PAlb8OX/j3RIdnG9/1lPPq2dXuY6Qj8n1O", - "FRUGXLzcDMjrF88ePXr07WizB6QBypWLRzkIkuIt7QMBsaA8HD/cxNjMSjLGOT7WqeRCgdYDkmGtWGLU", - "2tk+sUK4am73azBqPTybm9C7cVf5YuFyRbFkLT4yUXvjqXrgQa0dE1SL2PiC+4evOOHUlbnSyIuAIZo7", - "SBTO3OnRmz9YvMar71r7tcwH2HSgNN7+7QbZd/i1eBtDlVB+tAQ7ynl92Oa2dR5ZCYTe3ffhG35gLnj2", - "nm5i0eK14a+vQhTuQFkhsZJrI/JK8DUmGFSyLgNFLp7jKwsz90SvNvgQBJaDsxJk1MWyzDYhufbs2r3h", - "OPC02/7qlQ+F+7zF+IzMmscPLuT/BQAA//+mCapZJrUAAA==", + "H4sIAAAAAAAC/+x9e3MbN/LgV0HNbZWlW5KSX9mLt+4PxZYTXeLYZTmX3YQ+LjjTJPETBpgAGEq0y/vZ", + "r9DAvDEckrL8yG+rUjFF4tFAP9Dd6G68j2KZZlKAMDp68j5SoDMpNOAf39HkNfyRgzbnSkllv4qlMCCM", + "/UizjLOYGibFyX9pKex3Ol5BSu2nvyhYRE+i/3FSjX/iftUnbrQPHz6MogR0rFhmB4me2AmJnzH6MIqe", + "SrHgLP5UsxfT2akvhAElKP9EUxfTkUtQa1DENxxFP0vzXOYi+URw/CwNwfki+5tv7kjBxKunMs1yA+os", + "ts0LRFlIkoTZryh/pWQGyjBLQAvKNbRnOCNzOxSRCxL74QjF8TQxksANxLkBou3gwjDK+WYSjaKsNu77", + "yHewH5ujv1QJKEgIZ9rYKbojT8g5fmBSEG1kpokUxKyALJjShoDdGTshM5DqoX1sbojFV8rEhet5fxSZ", + "TQbRk4gqRTe4oQr+yJmCJHrye7mGt2U7Of8vcNT3lLP46oXMNey6yc39mefGOHpobg8OSdyvdk+YJTsa", + "G3LNzCoaRSDy1MLGYWGiUaTYcmX/TVmScIhG0ZzGV9EoWkh1TVVSA10bxcTSgh5b0Gfu6/b0bzYZIOJt", + "G4+b2qyJvLZ/5lnkhwlOsJI8mV3BRoeWl7AFA0Xsz3Z9ti1JctsVcexGrSG3M3oTZaNI5OkMe/npFjTn", + "BpHbYpw8nYOyizMsBZxcQQbUNOb1o9ttXwLy9013Ff8gsZQqYYIa3K1yAJJJzfyedUfadEf65yEjtcj0", + "JrJDB4m0Sfz7igHNxJJDWwjUZQDVJKPK8bGTGhPyZgXkXxaUf5EFA54QDRxio8n1isWrqahGyUAtpEpH", + "hIrErVwqd7ollhxcbytNKbMCYgUFBBlVNAUDSk+m4vyGxoZviBTl765nauEp6MoCRNJcGzIHkim5Zgkk", + "k6noCC7HHallw0HZ0pEBVlorutyt+zNFl+3eqVzDbr1fyDW0e2cKtLacN9T5lW34I2xqfXWsJOdDHS+x", + "Vb0bmFmcK+2Ovq1dwTzFhvXeHCAb7GgbVfK7R3AVOC6PlBqFTWoirI7fxn67kWcGbkxU38pyaxq4bay8", + "WEhIGFaDDizTit43cGPK7WmxOY4c5HIF1MAzpiA2Um0OO49SmQR29WXmupOkGJ3YhuRIxoZy4lY5IjBZ", + "TsjfHj8+npBnTv6ieP3b48eoGFBjVafoSfT/fj8d/+3t+4ejRx/+EgX2KqNm1QXibK4lt9KmAsI2tDPE", + "uPTWJCeT/9kdvLWZOFNoM58BBwOvqFkdto8DSygAT3Cajw/4a4jxOFkeBj1LurBfJFbLw0PbH1CqmKS2", + "EnLGsxUVeQqKxUQqstpkKxBt/NPxu7Pxb6fjb8dv//qX4GK7C2M643RjVX+23HM9K0D9qLOmp7lSIAxJ", + "3NjEtSNMkIzdANfB41vBQoFezRQ1MDykb01sazvwD+/IUUo39vgROeeELYiQhiRgIDZ0zuE4OOk1S0IE", + "1Z4Nm22FP7i17RPobnRYKzZ79NdSb3WKbEiAJsDppqHanbZVlWe2iV19yjhnGmIpEk3mYK4BRAGI1V1R", + "09CGKuOp18p/Qrn0WoLlrgmCJVhqAT0N4eQ2+q3di73U27BAaVtRv9+MyOZtXZnMKFO6XKJZKZkvV1YH", + "4w6IJRPLCXlhNSKvYhFqCAeqDXlAMsmE0Q0rqw1ybUNSeuNNqgd1++pBdzVbf9QGshmie5Y2lfnHe6Jc", + "AaeGrYHYIXVr1eTIMp5FBhPM2q8E5zweRjyONstAzTQsU2/jVxbHab/JUQKE2HBQZaCIH8cupKQ/8sIB", + "Qe43ILo/aAj0ng2la6J15oPWdAkBMmwNXDQMju1MgVecbq6RiQ9zOvhedeOiGpLEVjvpaOpBlcWqUZf4", + "98n/oWvqPuIADRfDGzQ3EiArqgmNY9DILPcyuoR7I3IPnTg35p4zTu7NlbzWoO6RNVXMSmtveaQZhydk", + "GtFrygyxnSdLaeTRvZUxmX5ycgKuzSSW6b3jvxMFJleC1JobZjgcHf99Gk1FSCeyxqrMzUxD3KC2bzrU", + "9oLeINlA6T2xfS1pefYotTPCNPnmFKnL9YmePDw93YvWcPN3pAeNAO9JDraT5ZwWFVSr69ADFFTeHAqJ", + "n3gStsdutT8LyjgkoV1XJdBdM2NNeQ4ek5CQ+cbbrlYvZgtCxebYCYsEVACeS0NFQlXiHFlkoWSKA9QX", + "1oFHm0TmZstgMjdZbnYdLUeC7w736wrMClS1IM8vCfFdFjnnm2rIuZQcqOhQRzFBiECeMw4XYiG78ojp", + "WcLUdqhQgWaa0MoamATgsZZdMrP03x3uJ3vEpXhQO9cs8snEec1SaqInUUINjLF3YPfCppJdljOO5sxo", + "cmRtohGZRom6vlFj+980snrxNBqr67Ea2/+m0fEkNIOgIbi/oxqI/anQwxd2SqmCO7GzUVWoPF0iYe9g", + "Nt8YCNDJJXuHggV/npBTsqiBwUBPhr1WuEYPXWOyUUEHNRz6Te8jp8uNNpCer8sTuY0YjQ1IvKJiCQRs", + "w67Lehfyo4sFxJYfdqbDQ3FZTnUoUvejkrBXBbcU/Sp1F8rT1+dnb86jUfTr6wv899n5T+f44fX5z2cv", + "zgNqfMiXMepXWH5i2iDeAmu02qJdW3fHmHAMbFkahCkIcad7glIqBVTwn+Syh7bOCJdLnGtTid7apU+X", + "yGo6V0sqyWV5SFnNY9KnDGhD0yxwMtmz3k5fQXRNNcmUTPLYUdEu4q1H86tPHUIYmnyvvMv6tb+h7Er4", + "XX3phVvtcB963wg7+847/tX9bOMkV0gBld7WQBZVS7DKrrHWh29a09TI9QoE0amUZvW/jcphQi7RZrDK", + "aAZqbC0IZ7YQqoDQ3Mix99MnjhJRBHAQS7Py/nWmneoyIWecLYVGk5w8panMF/KGrPKUCvYOnqRMWGI6", + "SemN/XdCLpZCWmOzDhQuekJepsyg8LcQkCM76dhNOp5TDYk9WplYHjd0zcd1O2ty+rjcfYH20m1NbGti", + "7WViuyU1tGu7522d9BcNbpPGnF0B+Q7eWSjiXK0LtURoAzSxNGg/Uis6gUMmlSFHCmKZpiASiyCpyLx0", + "/qC4WlPNpDgOHiFDXFMZ+QXhEyMP4p5dR9qLiw53piagzWzIKQzaWODdvZDTZYZ8qqNIq3hoYC1zFcPO", + "Y7Y14GKCUW0VoR16eVUXl3uYSN+DQF/ryx9JERLSPW7k1SBZX4jEnlagCx1/Mqzfy6vgWl5RE6+8v/Yw", + "jPc5bJ/1O2pLUfLg0en+bttnve7aCblYEJkyYyAZkVyDu4JcseUKtCF0TRmncw6ui2Vp5xtH8vFnv9eY", + "vjkdPTwdPXg8un/6Ngwibu2MJRyG8bUg+LUFOdfg7rGt/uzkM2drIGsG11Y3Kj31JwpwmVZjjQ1bQ1hZ", + "VYDO0Vm8UjJlFvb3/bNjU/LUNyV0YUDV1l9o20YSEDpXQJghNKGZuxwScE0s1A2nBNIE7uUKaLLI+Qhn", + "K7/hPeTZ6yd/1usfL8nm4YPT3bzl7UvTwxSCAU92oQsU55qlKTzo0H3d8nfWSdSi+3Tk2lqVwNAsc2rf", + "wc7s8vYvHTpyr2BD8MbURwXFMNnrBA7P/5N3btvR9SadS46T40QTck7jFbFTEL2SOU/IHAittSU6z+zR", + "61w0N4k0UvKpONIA5B/37+NaNilJYIFuYCn08YR4l54mTMQ8T4BMo9fo6JlG1pi/XLGFcR+fGsXdpzPu", + "v3r+eBpNps7B7Ty6TDsPfYwAUq6lhTKW6dwfWdpfnrrx/moKHwH+hbP99Q2d47B7bGhLWuPuBuW1klbg", + "n99A/NG8ttQuL8V7lo2wckTIXAcjxNSy6eT//W033M+NRNUyT6F9ITFIVVTPlJRNJ314Gbl3v7v9cIqx", + "7UoyxdaMwxJ6xA7Vs1xDwGnQHpJqRw62tR1K5BxPj0LGd6O03NoDNjluNJ48UhG9As7LLbdnQS6CpmN8", + "HRjrV6muLA9XNvQRrfsQjv2I3iHoJmEitIBhnQvEup+83ocu/jzO3neCIM/Fmikp8Oqk9MhbWDWY8ij2", + "W1/bjYryO171/Rzp/Qjs95c7dA6y4a2c5bTOdCXCynV0mbA0U1PdR2l2/XUTtXEABa0MuGFmFr6d8Usl", + "tgl6mMMjON/5bP7No7Dr7JtHYxC2e0JcUzLPFwvHWT2+810Hk7npH+xDP/Z+ZFVc1H7ou2RLe8gi9Toe", + "blFvE2UamzeEWvTm/PWLaPu4dQeeb/7jxU8/RaPo4uc30Sj64ZdXw347P/cWIn6NquihpwmqsZS8evPP", + "8ZzGV5D0b0MseYBkf4ZrYkClzK48ljxPhR66RR1FSl4PjWWb7Hkdi6OOHKBbduwyo9eNSG3OXy6iJ78P", + "RfB1ju4Po7a7jXIurWk3M2YzfAqe+daEkkxDnshxufqjV2/+edwWrE6zx4OoiFLGK3d7IvUcl2GkXfhr", + "+DbinEFTX4S1EdAddiBKOzPZZodP0xUHbzt4PUCeX9T82HRuBRIl2o62jR+yUOzWy8sSWRfPwqLW/z4L", + "dXepDmOqLd9DQlgVChY4ZEv3cp6zJCyIqVXHZ9SE3dfoXnbYqJOZ77aHB7uX1Qw1ud4TG0WolcbO7pTt", + "l0pZPsviwPrOtWEptcbI01e/kBzd/BmoGIShy/opWPlAtxyj58XxSdiisVcr6s5Wt11DOsooSiHtu+Or", + "IFagEfMkhdTqiA768vqv5wQPulteVTg1jTsllQth0eeWDUn4LOpHbMIOzHZ5Rg21kuxaMecAbZGeu15n", + "IssDV4YJNXQnxSKpzzIZ9B6W474dXPOt9EULjo+E03a47gptCwOij0iqCCdsQHzzSbSrS8UvRQGt7m/3", + "0Z0uz0lGN1xSS6aZAm0llFiWGPRxEVIRzhYQb2Lu73/1bbFZ3vdVxGJXEVRBIXx9+FMTpM5Fq2WFYEzk", + "TqKhFKRucKbJFDtOoz6WtfAHTgHnCHc/FxdsuAXxKhdXdYB9mEoZ/LIbE7ugZVDhqJAFE0yvdjs2qsjk", + "olffoTFof7vzsPu1LkOsa7/XVJw9DrkKWt/pQGBbwgMP3zqcISFyGSsAoVfSvIblLslBu/npf3D++TJQ", + "fOmNxi1h1T2e21/RY7vPQDteLrux7ln1NRtzWFhuUQJudd28x5jBq7NiF0bFxg6h7BAPtCoRPZDh0ySM", + "IMs284D2vdXjhs5utjvCf5CKvZMCs0xwLkJTmQszIS7KwBoa+L0mGBw4IgKWtPG9xUNY0jkIBoLK/6+F", + "ON5h/kRei8D0eRae/DZX22Um0u5O0CGuoMYl5tXSpZpT7c8Uew+583VyJ4dsT6nFkgTEQNiju/au7hR8", + "p8E7Ud+uB+znjMMra3VqzaTQh8G/VDLPwo4K/MlHlCnyfcPa2zd0MZDc9c2jR8f75XLJaxHyi1tY8Sf0", + "hBfw/tID7y5hbtcrqdGWKvbWXX+5mxa8gkwOzbPaEnZYT0rcT2V9RXMN9SBkqdC+h9jyflL6Wvd01tZv", + "DjEbMeSrrYd7NwK+TweZsj55cEOsCvNc/0pN/FFT58q8RjSfMMU4HLBtGZetYdjPVXK7H4+Ufflmh9iH", + "3kgO3IFbJuAtFE0hHKnwutJti0YWxYvMcuwalGIJaKJddQq/A8d1nD84HXKaBV1IxSVwwPlTU2ABee8j", + "pQEi0AVBX4hLR8D9FzUVHKFYuu27s3VDUnqD8cXsHVyIF9/1Q4DBqNpHRb/4bkeM3G9x4f0dIxEujcxu", + "S2hSxWDHGeaXizSFhFEDfIP1OPB6VOaGLBWNYZFzole5sVrQhLxZMU1SjKdBHwMTeCGsVJ4ZSMiaJSBx", + "s8L+4X3yTx0HW4DuMPm0nZS9t6Z7u9RFqwcaJa9AD8ZxFCnmLYsTbvB23qXGO3fASmJEgivqMHgQ4rhd", + "cWebMW+vYwpT9CT6EZQATi5SugRNzl5dRKNoDUo7UE4n9yenqBlkIGjGoifRw8np5KGPA8cNOykCj04W", + "nC6LUyEOHAsvQC0Bg4iwpbuyhxum0fsjBegRyTNrRJPWoIHQpTWjROcZqDXTUiWjqaAiIZijlQvDOG5b", + "2foZrN9IyTWZRpxpA4KJ5TTCuGvOBBCmiZwj11v9cSFVkSyEgtLH2GE8h6UVJ+MSVAxMvCpmeY7rd6gA", + "bb6TyWav2kItbi92s+XaLpbk9tBIkuK2+uSV36fReHzFpL5y8S3jccI0nXMYL7N8Gr09PjwkxQEUJquq", + "nVE5uKi0quLVg9PTgAaL8Dt8JxjuXC7NI7udwvRhFD1yI4WM4XLGk3aBrQ+j6PEu/ZrVqbBUU56mVG2i", + "J9Evji5LEDnNRbzySLDAe5ixW0W9ecYlTcZwY0CgojumIhkXbS3OpQ6IgF+wG5ZHkYqklhzLIcg7lhGq", + "4hVbW4aBG4O1hswKUpILK2JPVjKFkyvk7JNq6pNpfnr6MLb6O36C0VRoMERZfknrM7hVMXEAG5KCC6fi", + "E7Kh26/zcqlnInnt93gbO6Y5NyyjypxYe3ecUEO3cWS1lf1xb1Uby5oO/bgneNNqlcQa/zWHD2cdPZfc", + "4hStLmubcxqDzxYs0LUf1lsH7Nn4Nzp+dzr+djIbv31/f/Tg8eOwcfiOZTOrBXRB/K0iyCIv3eKLWsgy", + "FxJQUkAF9RFW9ili9lIq2AK0mVixeFx3qs6ZsCw4dOaV4Pn0rZC2v1W81bB7mIy7H3Lsl9TgSAGSUUDM", + "Oa4pmYNpooAmn1vgdURQic0akR9RbQWSPq4LwXKJXhp6veVkXugFYal3XoQjCiJbtRI6Re1QSfXFrs5e", + "XZCYcj4hZ/5XqqDwYkFipVxV9s4XFlhJnngihZuY59aUJFzGVyOiJRGSSLQ38Q6RlMJGk5gKFznBga4B", + "E8qH6t6VpbKKjSesDN93PreiBBamNk+mAjVyF3hoVXVrusUrz1UJuEAIqzXFZegu3nFjlSOc7Qo2c0lV", + "UmzXVBT6f0Y3dhQB5lqqK6JkLpKxUSwjnBoQ8QZnA4zTFQlbsySn3A8TkryBCoa30IC2Obm31Eo8VAU5", + "47wkqHDW9OfkwJIdttR2rFN2i9laRdEKlmuiryqHdkdYC9RbOxBZrkJNUU2uYO7PiqFLlubcRV853quX", + "YAybbi0clVXYwugpfed3hJ1ufbedkfNR5q8lUoUqxzq3/pppNmecmU1pLXwxPPoDS3xIt7yuJ7E20dys", + "Lxg+/DBTBYU3XiAVFOUKIY2I9C49e4DRIn/TTquMyykd2elFuzjSkq3B5dj585kD1YBHTL0uw0BJopDg", + "L+tQ3RFpdistHig37EBfiLxAUFy6K8oyRBNFPLQoZgnGEcysrCnaKyS+B9NI5Y7ukGHDOeNh3sUIPLfS", + "chEfYxe/B1OwWm0KfwdYzLSL9G0W7gxvbplSfkdk3i0Jeqvj0e+CXdnnJfUXRUpyAzs+VK+6OKskjd4F", + "Y41iqVvkqM/7rObBy3mUmaIUpdWtHfnR/lxdH9eS16YilJI2Ic9R/lrAFKysMWTNh27u24hogKmwwITz", + "1wg1pKg7FS+ZmSwUQAL6yshsItXy5Mb+L1PSyJOb+/fdh4xTJk7cYAksJisnz/2txUoKqXTdOT3msIZq", + "vdaw8HdSsd8KvH3U3pPgsCCToMPTJ1TeETt0itweyA2IUKSWL0lbcGd83aRGutyB8HUZ4dMvqt7QK6gi", + "ge5KY+wENH3wONp64rCULuEkcwF41UzDTp7OwVIBQHDQz4rQpzQzubL6f4Wg4sZrAJ2+cHNYiLlQLbL2", + "4Ux8Y7W3E2l5uwixst+Zmo5Xk6RNbbHh7mhkBXs1sBEr5XwnTBAulxhJZVh8pV35RRfH5zw9NQoic1jR", + "NbMkTTdkTdXm78Tk6KzwxVMLBp5Mxa9WSZ1Ls6otBQcs1kow0Mu7cHzN75GT5k684cxOwKcN+5cclWOg", + "KlxNcOwuh9CMRqcLAPcRxV4U/ssLdm/Bjce+zPzPZDx2JVtOiXOkOoXcuVL/FZKQl0XE1B2xX72W94HS", + "0ZPXF2JEO2AqXcGhhxqrGe+hzRXVsHqEo78UviO8dAuBH4YZd/e7yb6kUwvflzAWsH4s+JrGjcvfwE2p", + "L+1wV8pDoJTJJ3ZoNAtfB46vX7wHoygCHWPLos7ELdD86PTb4X7Np38+4r1oz3IsaSz0iSv5Pisz1pFM", + "8pA7slkW/658kuHi+4de8lTRbm6dXxDrupUSikEX1fYXeHF14HfAiytUf9d46dbxP9jnU6LELTG5HWc9", + "Gu7XfFHqoziLEPJ6sco23orb2C0oe+5uRL9sbGEs858AUYiPEkfyWnBJE8tds3cMY/aWYEIxoiZXQhNK", + "frt45YISa5forogHoksXlkUt7rheH7SFfz//M6Z+Yxle+hcP8GCi+s7vdRQ3+1aDLhaFNV1svz9yQHHg", + "YheKCOwmDYzqARVDEd1v9zqc/b7eyqC0u16ssQxWRMKqb/DXSJceWXURQmhBaH7JPfSqTbIDwRqqJu+0", + "IUeGqloESFo4XjDAz451vJWup2ILYZPftEmIXCxAaaLZUmAJaGH4hiyoNqDKCTH1XiRTkUD9K/uZKsAi", + "He9Y5g1iGq8YrC0kczDtUZCNwrceNa6ye/S1sNXofbdMU7lc9A5OyA9suQLl/iqL0BKdUs6hRK8m89wQ", + "Q6+AcCmWoCZTMXaY0OYJ+bfFthuC3B8RHzVtEQsJOfr3w9PT8ePTU/LiuxN9bDv6INtmx4cjMqecitiq", + "UrbnCWKAHP37/uNaX4e4Zte/jQp8Fl0en47/V6NTB8z7I/y27PHgdPyo7NGDkRq1zHCYqI6OqshL8alK", + "t/VbFY1qvzmQ8YMOJQ/vKxU9995KLL7xvP3fTDSa5rJL8Wjl16wInvZisSkaymrUu8qEwYLfX8IJu59O", + "WFXk7hIUanm1ct9fIdl8D6ZRsLwo9NLBXkk2nGmDerrupZuqbvphh8nXSSnVqgOkUplv3CUHfIW0ggHB", + "iHkXq9ilDax53We+FUWY7/Da+WOYbnjNW7k7vkI84Qqw7C6GWG9jZgU0KY3uIC+/Bpp4k3s3VsbJCpXQ", + "jv+lcLOMDZhxVV7kVroEin67uo/mGftMxGLxW5ky+Lp7QRwanKCf1bKae7m7m1x+dwF+PVnsh3J8bagi", + "HO8rROQlmMBjJDXUnWDCu16xrMSwi+Dvv7Q941xeF4H+mLDiwtOlIi7RhIM/EHwYjIJUehngHruZ9CS2", + "FOrBR8tkKTWSnlSUQ4r41wpyeYV2t7L+hUDdN+HDJ3tsr9S/PaENd+GjJXsglso8j69d1AXyPxZeX6uz", + "Q+Ha3JrHRtHxgvzmatu6lDVmdOXb7ISGhR6JCDGH825+NNbYl/STeq2DWjJeaTgbuRsf1POrbpH8tI0f", + "DiTs31hWkXUNgX8aIqf1nMoWiXbo3TtXBgh+X9doH19MxTBjDLtIGx7RqWi5RPszKr2P86MxV+FVCT8F", + "3/I4FUfIIDOMPh/T2k/ZrKK77YUDqsqLHJyKgAdn1d1VR1AsKwpIedgwXxJfMLLkNB5jm3HVb/Ad3Ja8", + "KPBwJ+LizO/hn1xktMm1R2xct3MeW5ZArQTPXdkAgSo/u+P2wOoFuOxggeJfBPsjh1Bpmoorr/12DFb7", + "6NqauEzysesHfCZic4upO6l9LqhY1jQx3K2T98WWf/BlTMBVJGrTm8wqcms5KdDx4D0N3u9Q4nGb72HY", + "1RCoz1ogSmbZ14+oS6yxY1eEScUB51EbSScu/rTXleTq6z7X567ZJ8RV2y1k4MY4aIP+oKH7gPpDr6F4", + "7svzWpnayhb28blYXpMmuOr30T/Gl5fn46cOtvGb4PunLyBh1BfPWRA7PNa99eG+R20hdty4uStu6Tqi", + "LnAp9+FrJFPc6M4u+3RCJ3ZLirXG/PYgo19tk10cns9qyhftOD8/4b13WRptURZQ7K2dWLxyhmrZN48e", + "9YGZupfug2BtrbjomG+XE/+W7tgDvRlFafCv/hhFt5Q9OYt4yCpUi8ulPqk2NnxFJ5e+3nmPHG4RhHuG", + "civlFoKmeEu7LKETrL8dnmYhOZfX4ciDRtHpWlnENpql4JsyP4OwRfGEJtPEg7aFMftPlX3mqa09PFvV", + "YObrtkef7UQr35UePMosYX3Rp1foZLBAE7kGZad2DJJxurnGes0nvkbGDhVc1JwZRdWGvCp7+7cvhOU+", + "fLazKqeKqLkxhC4pE9pZ4nMlrzUo4h+ZmAopCJcx5SupzZNvHzx4MCFvMIgsAXxCg8bFAzf3MrqEeyNy", + "z497z9XXueeHvFc9P+YzoFT5uIIpRqyAw2o8Jlf4mIpoFHIJOU78FlTrfupOh7uw7DpzfaashwAc+MRF", + "KC+82twvsdZKtQRM6blEyB1FBIjTM4iTScgd/YZ+7fGnO8ud7T4v9WnpoPsoXoACqoJJyrf5ImrsBF/A", + "bCIY33MaxDC+IXW3KG48P/Z5cFx/KSt0FLqnr74w3NItyH1fPar14eSKNbNzg4j+kWGa57BdXnuua5tK", + "OPAW1+7GwkEIrb+F+EVVAXr541cZX2BFSfmYY6G29lOcew58kObcc4t/HqprPj35H7q7fYBS73OcW4hP", + "l2/sBc3f5kt8n5r27vgcc4sKHWH+l68ySrn2GJ5bXj/qE7aDToOt/jRSp/H04GfSn2ovAQaI77v6y3xf", + "rcetOvncU4Xb6VDmZsgRV22ezM1Wj9xnkke38CwF3lUc9DG1Xky0Om77ycT/XKDcwQVKjaplbloOs/Jl", + "k5PqEjYsXV3mcPXo310manfeHumv29T3hs1nS9H+TLUtysTuTMGaoc1YvGNSfxalg3WfXNYrxYrsszri", + "t96elZdW5SsqVfTEhGBJJZnao6JZKSkv6uD5W4Gye99FFgq98DXW0Dssw6IRN+wkzR7dOp2g9qqSu3ps", + "CLjy1/Fz/57o+Gzru55yUT272n2MdEK+z6miwoCLl5sDef386cOHD7+dbL8BaYBy6eJRDoKkeEv7QEAs", + "KA9OH2xjbGYlGeMcH+tUcqlA6xHJsFYsMWrjfJ9YIVw1t/s1GLUZny1M6N24y3y5dLmiWLIWH5movfFU", + "PfCgNo4JqkVsfcH9w1eccOrKXGnkRcAQzR0kCmfu9OjNHyxe49W3rf1a5gNsO1Aab/92g+w7/Fq8jaFK", + "KD9agh3lvD5sc9s6j6wEQu/u+vANPzAXPHvvb2PR4rXhr69CFO5AWSGxkmsT8lLwDSYYVLIuA0UunuEr", + "C3P3RK82+BAEloOzEmTSxbLMtiG59uzaneE48LTb/uqVD4X7vMX4jMyaxw8u5P8HAAD//6IVIG3QtAAA", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/server/openapi.yaml b/server/openapi.yaml index 9f0483a0..dde59f75 100644 --- a/server/openapi.yaml +++ b/server/openapi.yaml @@ -1236,16 +1236,11 @@ components: type: boolean description: Use human-like Bezier curve path instead of instant teleport (recommended for bot detection evasion) default: true - steps: - type: integer - description: Override auto-computed point count when smooth=true. Ignored when smooth=false. If omitted, derived from path length. - minimum: 5 - maximum: 80 - step_delay_ms: - type: integer - description: Delay in milliseconds between steps when smooth=true. Adds small random jitter. Ignored when smooth=false. Omit for auto (~10ms with jitter). - minimum: 3 - maximum: 30 + duration_sec: + type: number + description: Target total duration in seconds when smooth=true. Steps and per-step delay are auto-computed from path length and this value. Aligns with Camoufox humanize:minTime/maxTime. Ignored when smooth=false. Omit for auto (path-length-based timing). + minimum: 0.05 + maximum: 5 additionalProperties: false ScreenshotRegion: type: object From 5f0f707290278d1965eb13e721d1ad1c5b683534 Mon Sep 17 00:00:00 2001 From: Ulzii Otgonbaatar Date: Wed, 11 Feb 2026 12:28:52 -0700 Subject: [PATCH 6/8] fix: unify cancellation error message to 'mouse movement cancelled' Co-authored-by: Cursor --- server/cmd/api/api/computer.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/cmd/api/api/computer.go b/server/cmd/api/api/computer.go index 81d9ffc5..1ebbb890 100644 --- a/server/cmd/api/api/computer.go +++ b/server/cmd/api/api/computer.go @@ -186,7 +186,7 @@ func (s *ApiService) doMoveMouseSmooth(ctx context.Context, log *slog.Logger, bo for i := 1; i < len(points); i++ { select { case <-ctx.Done(): - return &executionError{msg: "smooth mouse movement cancelled"} + return &executionError{msg: "mouse movement cancelled"} default: } @@ -210,7 +210,7 @@ func (s *ApiService) doMoveMouseSmooth(ctx context.Context, log *slog.Logger, bo } } if err := sleepWithContext(ctx, time.Duration(jitter)*time.Millisecond); err != nil { - return &executionError{msg: "smooth mouse movement interrupted"} + return &executionError{msg: "mouse movement cancelled"} } } From 6e50819c0043c2b37a679d39315fc2e331739091 Mon Sep 17 00:00:00 2001 From: Ulzii Otgonbaatar Date: Wed, 11 Feb 2026 12:34:57 -0700 Subject: [PATCH 7/8] fix: use background context for deferred keyup in moveMouseSmooth The deferred modifier key release was using the request context, which fails if cancelled mid-movement, leaving keys stuck. Use context.Background() to match the established cleanup pattern. Co-authored-by: Cursor --- server/cmd/api/api/computer.go | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/server/cmd/api/api/computer.go b/server/cmd/api/api/computer.go index 1ebbb890..d28a8263 100644 --- a/server/cmd/api/api/computer.go +++ b/server/cmd/api/api/computer.go @@ -177,7 +177,8 @@ func (s *ApiService) doMoveMouseSmooth(ctx context.Context, log *slog.Logger, bo for _, key := range *body.HoldKeys { args = append(args, "keyup", key) } - _, _ = defaultXdoTool.Run(ctx, args...) + // Use background context for cleanup so keys are released even on cancellation. + _, _ = defaultXdoTool.Run(context.Background(), args...) } }() } @@ -234,20 +235,7 @@ func (s *ApiService) getMouseLocation(ctx context.Context) (x, y int, err error) if err != nil { return 0, 0, fmt.Errorf("xdotool getmouselocation failed: %w (output: %s)", err, string(output)) } - lines := strings.Split(strings.TrimSpace(string(output)), "\n") - for _, line := range lines { - line = strings.TrimSpace(line) - if strings.HasPrefix(line, "X=") { - if v, e := strconv.Atoi(strings.TrimPrefix(line, "X=")); e == nil { - x = v - } - } else if strings.HasPrefix(line, "Y=") { - if v, e := strconv.Atoi(strings.TrimPrefix(line, "Y=")); e == nil { - y = v - } - } - } - return x, y, nil + return parseMousePosition(string(output)) } func (s *ApiService) doClickMouse(ctx context.Context, body oapi.ClickMouseRequest) error { From 6397a136bf88c75b2b7388df340d0cb971d15fb4 Mon Sep 17 00:00:00 2001 From: Ulzii Otgonbaatar Date: Wed, 11 Feb 2026 12:46:57 -0700 Subject: [PATCH 8/8] refactor: simplify MoveMouse handler, fix duration_sec accuracy - Eliminate duplicated validation by having MoveMouse call doMoveMouse directly instead of reimplementing resolution/bounds checks - Remove unnecessary moveMouseSmooth/moveMouseInstant wrapper functions - Compute trajectory MaxPoints from duration_sec so the requested duration is actually achievable (remove 30ms step delay upper clamp) - Skip zero-delta mousemove_relative steps to avoid no-op xdotool calls - Always pass -- to mousemove_relative for robustness with negative args - Remove redundant nil check in deferred HoldKeys cleanup - Clean up duration_sec OpenAPI description (remove internal references) - Rename defaultMaxTime/defaultMinTime to defaultMaxPoints/defaultMinPoints - Export MinPoints/MaxPoints constants from mousetrajectory package - Add clamping tests for MaxPoints below min and above max Co-authored-by: Cursor --- server/cmd/api/api/computer.go | 90 ++---- server/lib/mousetrajectory/mousetrajectory.go | 28 +- .../mousetrajectory/mousetrajectory_test.go | 18 ++ server/lib/oapi/oapi.go | 284 +++++++++--------- server/openapi.yaml | 2 +- 5 files changed, 205 insertions(+), 217 deletions(-) diff --git a/server/cmd/api/api/computer.go b/server/cmd/api/api/computer.go index d28a8263..7d62eb47 100644 --- a/server/cmd/api/api/computer.go +++ b/server/cmd/api/api/computer.go @@ -92,40 +92,7 @@ func (s *ApiService) MoveMouse(ctx context.Context, request oapi.MoveMouseReques Message: "request body is required"}, }, nil } - body := *request.Body - - // Get current resolution for bounds validation - screenWidth, screenHeight, _, err := s.getCurrentResolution(ctx) - if err != nil { - log := logger.FromContext(ctx) - log.Error("failed to get current resolution", "error", err) - return oapi.MoveMouse500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{ - Message: "failed to get current display resolution"}, - }, nil - } - - // Ensure non-negative coordinates and within screen bounds - if body.X < 0 || body.Y < 0 { - return oapi.MoveMouse400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{ - Message: "coordinates must be non-negative"}, - }, nil - } - if body.X >= screenWidth || body.Y >= screenHeight { - return oapi.MoveMouse400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{ - Message: fmt.Sprintf("coordinates exceed screen bounds (max: %dx%d)", screenWidth-1, screenHeight-1)}, - }, nil - } - - useSmooth := body.Smooth == nil || *body.Smooth // default true when omitted - log := logger.FromContext(ctx) - if useSmooth { - return s.moveMouseSmooth(ctx, log, body) - } - return s.moveMouseInstant(ctx, log, body) -} - -func (s *ApiService) moveMouseInstant(ctx context.Context, log *slog.Logger, body oapi.MoveMouseRequest) (oapi.MoveMouseResponseObject, error) { - if err := s.doMoveMouseInstant(ctx, log, body); err != nil { + if err := s.doMoveMouse(ctx, *request.Body); err != nil { if isValidationErr(err) { return oapi.MoveMouse400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: err.Error()}}, nil } @@ -141,24 +108,39 @@ func (s *ApiService) doMoveMouseSmooth(ctx context.Context, log *slog.Logger, bo return &executionError{msg: "failed to get current mouse position: " + err.Error()} } + // When duration_sec is specified, compute the number of trajectory points + // to achieve that duration at a ~10ms step delay (human-like event frequency). + // Otherwise let the library auto-compute from path length. + const defaultStepDelayMs = 10 + var opts *mousetrajectory.Options + if body.DurationSec != nil && *body.DurationSec >= 0.05 && *body.DurationSec <= 5 { + durationMs := int(*body.DurationSec * 1000) + targetPoints := durationMs / defaultStepDelayMs + if targetPoints < mousetrajectory.MinPoints { + targetPoints = mousetrajectory.MinPoints + } + if targetPoints > mousetrajectory.MaxPoints { + targetPoints = mousetrajectory.MaxPoints + } + opts = &mousetrajectory.Options{MaxPoints: targetPoints} + } + traj := mousetrajectory.NewHumanizeMouseTrajectoryWithOptions( - float64(fromX), float64(fromY), float64(body.X), float64(body.Y), nil) + float64(fromX), float64(fromY), float64(body.X), float64(body.Y), opts) points := traj.GetPointsInt() if len(points) < 2 { return s.doMoveMouseInstant(ctx, log, body) } + // Compute per-step delay to achieve the target duration. numSteps := len(points) - 1 - stepDelayMs := 10 // default when duration_sec not specified + stepDelayMs := defaultStepDelayMs if body.DurationSec != nil && *body.DurationSec >= 0.05 && *body.DurationSec <= 5 && numSteps > 0 { durationMs := int(*body.DurationSec * 1000) stepDelayMs = durationMs / numSteps if stepDelayMs < 3 { stepDelayMs = 3 } - if stepDelayMs > 30 { - stepDelayMs = 30 - } } // Hold modifiers @@ -172,14 +154,12 @@ func (s *ApiService) doMoveMouseSmooth(ctx context.Context, log *slog.Logger, bo return &executionError{msg: "failed to hold modifier keys"} } defer func() { - if body.HoldKeys != nil { - args := []string{} - for _, key := range *body.HoldKeys { - args = append(args, "keyup", key) - } - // Use background context for cleanup so keys are released even on cancellation. - _, _ = defaultXdoTool.Run(context.Background(), args...) + args := []string{} + for _, key := range *body.HoldKeys { + args = append(args, "keyup", key) } + // Use background context for cleanup so keys are released even on cancellation. + _, _ = defaultXdoTool.Run(context.Background(), args...) }() } @@ -193,12 +173,10 @@ func (s *ApiService) doMoveMouseSmooth(ctx context.Context, log *slog.Logger, bo dx := points[i][0] - points[i-1][0] dy := points[i][1] - points[i-1][1] - args := []string{"mousemove_relative"} - if dx < 0 || dy < 0 { - args = append(args, "--", strconv.Itoa(dx), strconv.Itoa(dy)) - } else { - args = append(args, strconv.Itoa(dx), strconv.Itoa(dy)) + if dx == 0 && dy == 0 { + continue } + args := []string{"mousemove_relative", "--", strconv.Itoa(dx), strconv.Itoa(dy)} if output, err := defaultXdoTool.Run(ctx, args...); err != nil { log.Error("xdotool mousemove_relative failed", "err", err, "output", string(output), "step", i) return &executionError{msg: "failed during smooth mouse movement"} @@ -219,16 +197,6 @@ func (s *ApiService) doMoveMouseSmooth(ctx context.Context, log *slog.Logger, bo return nil } -func (s *ApiService) moveMouseSmooth(ctx context.Context, log *slog.Logger, body oapi.MoveMouseRequest) (oapi.MoveMouseResponseObject, error) { - if err := s.doMoveMouseSmooth(ctx, log, body); err != nil { - if isValidationErr(err) { - return oapi.MoveMouse400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: err.Error()}}, nil - } - return oapi.MoveMouse500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: err.Error()}}, nil - } - return oapi.MoveMouse200Response{}, nil -} - // getMouseLocation returns the current cursor position via xdotool getmouselocation --shell. func (s *ApiService) getMouseLocation(ctx context.Context) (x, y int, err error) { output, err := defaultXdoTool.Run(ctx, "getmouselocation", "--shell") diff --git a/server/lib/mousetrajectory/mousetrajectory.go b/server/lib/mousetrajectory/mousetrajectory.go index 6d80e0ca..5bdc81b0 100644 --- a/server/lib/mousetrajectory/mousetrajectory.go +++ b/server/lib/mousetrajectory/mousetrajectory.go @@ -72,17 +72,19 @@ const ( // Number of internal knots for the Bezier curve (more = curvier). knotsCount = 2 // Distortion parameters for human-like jitter: mean, stdev, frequency. - distortionMean = 1.0 + distortionMean = 1.0 distortionStDev = 1.0 - distortionFreq = 0.5 + distortionFreq = 0.5 ) const ( - defaultMaxTime = 150 - defaultMinTime = 0 - pathLengthScale = 20 // Multiplier for path-length-based point count - minPoints = 5 - maxPoints = 80 + defaultMaxPoints = 150 // Upper bound for auto-computed point count + defaultMinPoints = 0 // Lower bound for auto-computed point count (before clamp to MinPoints) + pathLengthScale = 20 // Multiplier for path-length-based point count + // MinPoints is the minimum number of trajectory points. + MinPoints = 5 + // MaxPoints is the maximum number of trajectory points. + MaxPoints = 80 ) func (t *HumanizeMouseTrajectory) generateCurve(opts *Options) { @@ -202,16 +204,16 @@ func (t *HumanizeMouseTrajectory) tweenPoints(points [][2]float64, opts *Options } targetPoints := int(math.Min( - float64(defaultMaxTime), - math.Max(float64(defaultMinTime+2), math.Pow(totalLength, 0.25)*pathLengthScale))) + float64(defaultMaxPoints), + math.Max(float64(defaultMinPoints+2), math.Pow(totalLength, 0.25)*pathLengthScale))) if opts != nil && opts.MaxPoints > 0 { maxPts := opts.MaxPoints - if maxPts < minPoints { - maxPts = minPoints + if maxPts < MinPoints { + maxPts = MinPoints } - if maxPts > maxPoints { - maxPts = maxPoints + if maxPts > MaxPoints { + maxPts = MaxPoints } targetPoints = maxPts } diff --git a/server/lib/mousetrajectory/mousetrajectory_test.go b/server/lib/mousetrajectory/mousetrajectory_test.go index 3de269d1..e2915b48 100644 --- a/server/lib/mousetrajectory/mousetrajectory_test.go +++ b/server/lib/mousetrajectory/mousetrajectory_test.go @@ -51,6 +51,24 @@ func TestHumanizeMouseTrajectory_ZeroLengthPath(t *testing.T) { assert.Equal(t, 0, points[len(points)-1][1]) } +func TestHumanizeMouseTrajectory_MaxPointsClampedToMin(t *testing.T) { + // MaxPoints below MinPoints should be clamped up to MinPoints + opts := &Options{MaxPoints: 2} + traj := NewHumanizeMouseTrajectoryWithOptions(0, 0, 100, 100, opts) + points := traj.GetPointsInt() + + assert.Len(t, points, MinPoints, "MaxPoints below MinPoints should clamp to MinPoints") +} + +func TestHumanizeMouseTrajectory_MaxPointsClampedToMax(t *testing.T) { + // MaxPoints above MaxPoints should be clamped down to MaxPoints + opts := &Options{MaxPoints: 200} + traj := NewHumanizeMouseTrajectoryWithOptions(0, 0, 100, 100, opts) + points := traj.GetPointsInt() + + assert.Len(t, points, MaxPoints, "MaxPoints above MaxPoints should clamp to MaxPoints") +} + func TestHumanizeMouseTrajectory_CurvedPath(t *testing.T) { traj := NewHumanizeMouseTrajectoryWithSeed(0, 0, 100, 0, 999) points := traj.GetPointsInt() diff --git a/server/lib/oapi/oapi.go b/server/lib/oapi/oapi.go index 4218ec28..686fbc48 100644 --- a/server/lib/oapi/oapi.go +++ b/server/lib/oapi/oapi.go @@ -322,7 +322,7 @@ type MousePositionResponse struct { // MoveMouseRequest defines model for MoveMouseRequest. type MoveMouseRequest struct { - // DurationSec Target total duration in seconds when smooth=true. Steps and per-step delay are auto-computed from path length and this value. Aligns with Camoufox humanize:minTime/maxTime. Ignored when smooth=false. Omit for auto (path-length-based timing). + // DurationSec Target total duration in seconds for the mouse movement when smooth=true. Steps and per-step delay are auto-computed to achieve this duration. Ignored when smooth=false. Omit for automatic timing based on distance. DurationSec *float32 `json:"duration_sec,omitempty"` // HoldKeys Modifier keys to hold during the move @@ -12304,147 +12304,147 @@ func (sh *strictHandler) StopRecording(w http.ResponseWriter, r *http.Request) { // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/server/openapi.yaml b/server/openapi.yaml index dde59f75..3d09aca3 100644 --- a/server/openapi.yaml +++ b/server/openapi.yaml @@ -1238,7 +1238,7 @@ components: default: true duration_sec: type: number - description: Target total duration in seconds when smooth=true. Steps and per-step delay are auto-computed from path length and this value. Aligns with Camoufox humanize:minTime/maxTime. Ignored when smooth=false. Omit for auto (path-length-based timing). + description: Target total duration in seconds for the mouse movement when smooth=true. Steps and per-step delay are auto-computed to achieve this duration. Ignored when smooth=false. Omit for automatic timing based on distance. minimum: 0.05 maximum: 5 additionalProperties: false