Signed-off-by: Ronni Skansing <rskansing@gmail.com>
This commit is contained in:
Ronni Skansing
2026-05-25 10:21:32 +02:00
parent 50a440c744
commit e1e1d28bba
2 changed files with 101 additions and 18 deletions
+46 -12
View File
@@ -9,6 +9,7 @@ import (
"image"
"image/draw"
"image/jpeg"
"math"
"math/rand"
"net/http"
"net/url"
@@ -1292,30 +1293,63 @@ func (m *RemoteBrowserController) dispatchInput(page *rod.Page, msg []byte) {
btn = proto.InputMouseButtonRight
}
mods := int(cmd.Modifiers)
// Shared Buttons bitmask values for pointer events.
zeroButtons := 0
oneButton := 1
nowTs := func() proto.TimeSinceEpoch {
return proto.TimeSinceEpoch(float64(time.Now().UnixNano()) / 1e9)
}
switch cmd.Type {
case "mousemove":
// Add ±0.5 px uniform noise so canvas-quantized coordinates have
// subpixel variation, matching natural pointer imprecision.
jx := cmd.X + (rand.Float64()*2-1)*0.5
jy := cmd.Y + (rand.Float64()*2-1)*0.5
// Add ±1 px integer noise then round: keeps movementX == clientX-prevClientX
// consistent (subpixel CDP coordinates create a float/int mismatch detectors
// check), while still adding the ±1 px variation that breaks exact-integer paths.
jx := math.Round(cmd.X + (rand.Float64()*2-1)*0.5)
jy := math.Round(cmd.Y + (rand.Float64()*2-1)*0.5)
proto.InputDispatchMouseEvent{
Type: proto.InputDispatchMouseEventTypeMouseMoved,
X: jx, Y: jy, Modifiers: mods,
Type: proto.InputDispatchMouseEventTypeMouseMoved,
X: jx,
Y: jy,
Modifiers: mods,
Timestamp: nowTs(),
Button: proto.InputMouseButtonNone,
Buttons: &zeroButtons,
PointerType: proto.InputDispatchMouseEventPointerTypeMouse,
}.Call(page) //nolint:errcheck
case "mousedown":
proto.InputDispatchMouseEvent{
Type: proto.InputDispatchMouseEventTypeMousePressed,
X: cmd.X, Y: cmd.Y, Button: btn, ClickCount: 1, Modifiers: mods,
Type: proto.InputDispatchMouseEventTypeMousePressed,
X: cmd.X,
Y: cmd.Y,
Modifiers: mods,
Timestamp: nowTs(),
Button: btn,
Buttons: &oneButton,
ClickCount: 1,
PointerType: proto.InputDispatchMouseEventPointerTypeMouse,
}.Call(page) //nolint:errcheck
case "mouseup":
proto.InputDispatchMouseEvent{
Type: proto.InputDispatchMouseEventTypeMouseReleased,
X: cmd.X, Y: cmd.Y, Button: btn, ClickCount: 1, Modifiers: mods,
Type: proto.InputDispatchMouseEventTypeMouseReleased,
X: cmd.X,
Y: cmd.Y,
Modifiers: mods,
Timestamp: nowTs(),
Button: btn,
Buttons: &zeroButtons,
ClickCount: 1,
PointerType: proto.InputDispatchMouseEventPointerTypeMouse,
}.Call(page) //nolint:errcheck
case "scroll":
proto.InputDispatchMouseEvent{
Type: proto.InputDispatchMouseEventTypeMouseWheel,
X: cmd.X, Y: cmd.Y, DeltaX: cmd.DeltaX, DeltaY: cmd.DeltaY, Modifiers: mods,
Type: proto.InputDispatchMouseEventTypeMouseWheel,
X: cmd.X,
Y: cmd.Y,
DeltaX: cmd.DeltaX,
DeltaY: cmd.DeltaY,
Modifiers: mods,
Timestamp: nowTs(),
PointerType: proto.InputDispatchMouseEventPointerTypeMouse,
}.Call(page) //nolint:errcheck
case "keydown":
proto.InputDispatchKeyEvent{
+55 -6
View File
@@ -664,6 +664,27 @@ func RegisterBrowserBindings(vm *goja.Runtime, pc *goja.Object, page *rod.Page,
// consecutive moveMouse and clickXY calls produce a continuous path.
var mouseX, mouseY float64
// cdpMouseMove dispatches a MouseMoved event with all required fields set.
// Using proto.InputDispatchMouseEvent directly (instead of page.Mouse.MoveTo)
// lets us set Timestamp, PointerType, and Buttons - fields that rod omits
// and that detectors use to distinguish CDP-injected events from hardware input.
zeroButtons := 0
cdpMouseMove := func(x, y float64) {
// Round to integers: Chrome stores cursor position as float internally and
// computes movementX/Y from the float delta, but event.clientX/Y are integers.
// Subpixel coordinates create a movementX != clientX-prevClientX mismatch
// that detectors use as a CDP fingerprint.
proto.InputDispatchMouseEvent{
Type: proto.InputDispatchMouseEventTypeMouseMoved,
X: math.Round(x),
Y: math.Round(y),
Timestamp: proto.TimeSinceEpoch(float64(time.Now().UnixNano()) / 1e9),
Button: proto.InputMouseButtonNone,
Buttons: &zeroButtons,
PointerType: proto.InputDispatchMouseEventPointerTypeMouse,
}.Call(page) //nolint:errcheck
}
// humanMoveTo moves the CDP cursor from the last tracked position to
// (targetX, targetY) along a cubic Bezier curve with ease-in-out timing
// and optional micro-jitter, mimicking natural hand movement.
@@ -680,7 +701,7 @@ func RegisterBrowserBindings(vm *goja.Runtime, pc *goja.Object, page *rod.Page,
dx, dy := targetX-startX, targetY-startY
dist := math.Sqrt(dx*dx + dy*dy)
if dist < 2 {
page.Mouse.MoveTo(proto.Point{X: targetX, Y: targetY}) //nolint:errcheck
cdpMouseMove(targetX, targetY)
mouseX, mouseY = targetX, targetY
return
}
@@ -719,12 +740,12 @@ func RegisterBrowserBindings(vm *goja.Runtime, pc *goja.Object, page *rod.Page,
bx += (rand.Float64()*2 - 1) * jitterPx
by += (rand.Float64()*2 - 1) * jitterPx
}
page.Mouse.MoveTo(proto.Point{X: bx, Y: by}) //nolint:errcheck
cdpMouseMove(bx, by)
if i < steps {
time.Sleep(stepDur)
}
}
page.Mouse.MoveTo(proto.Point{X: targetX, Y: targetY}) //nolint:errcheck
cdpMouseMove(targetX, targetY)
mouseX, mouseY = targetX, targetY
}
@@ -750,11 +771,39 @@ func RegisterBrowserBindings(vm *goja.Runtime, pc *goja.Object, page *rod.Page,
})
pc.Set("clickXY", func(call goja.FunctionCall) goja.Value {
x := call.Argument(0).ToFloat()
y := call.Argument(1).ToFloat()
x := math.Round(call.Argument(0).ToFloat())
y := math.Round(call.Argument(1).ToFloat())
dbg(fmt.Sprintf("→ clickXY %.0f,%.0f", x, y))
humanMoveTo(x, y, -1, -1)
must(page.Mouse.Click(proto.InputMouseButtonLeft, 1))
// Dispatch mousedown and mouseup directly so we can set Timestamp,
// PointerType, and Buttons - and add a realistic hold duration.
// page.Mouse.Click omits these fields and has 0 ms hold time.
oneButton := 1
proto.InputDispatchMouseEvent{
Type: proto.InputDispatchMouseEventTypeMousePressed,
X: x,
Y: y,
Timestamp: proto.TimeSinceEpoch(float64(time.Now().UnixNano()) / 1e9),
Button: proto.InputMouseButtonLeft,
Buttons: &oneButton,
ClickCount: 1,
PointerType: proto.InputDispatchMouseEventPointerTypeMouse,
}.Call(page) //nolint:errcheck
// Human click hold: 80-150 ms between press and release.
time.Sleep(time.Duration(80+rand.Intn(70)) * time.Millisecond)
proto.InputDispatchMouseEvent{
Type: proto.InputDispatchMouseEventTypeMouseReleased,
X: x,
Y: y,
Timestamp: proto.TimeSinceEpoch(float64(time.Now().UnixNano()) / 1e9),
Button: proto.InputMouseButtonLeft,
Buttons: &zeroButtons,
ClickCount: 1,
PointerType: proto.InputDispatchMouseEventPointerTypeMouse,
}.Call(page) //nolint:errcheck
// Sync rod's internal position tracker so any subsequent rod operations
// (e.g. el.Click) see the correct cursor location.
page.Mouse.MoveTo(proto.Point{X: x, Y: y}) //nolint:errcheck
dbg(fmt.Sprintf("✓ clickXY %.0f,%.0f", x, y))
return goja.Undefined()
})