mirror of
https://github.com/lukaszraczylo/mcp-captcharesolver.git
synced 2026-10-12 00:45:28 +00:00
feat(stealth): humanized interaction plan (bezier + cadence)
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
package stealth
|
||||
|
||||
import (
|
||||
"math"
|
||||
"math/rand/v2"
|
||||
)
|
||||
|
||||
// Action is a high-level target the caller wants to perform.
|
||||
type Action struct {
|
||||
Type string `json:"type"` // click | type | scroll
|
||||
Text string `json:"text,omitempty"`
|
||||
X int `json:"x,omitempty"`
|
||||
Y int `json:"y,omitempty"`
|
||||
}
|
||||
|
||||
// InteractionParams is the input to PlanInteraction.
|
||||
type InteractionParams struct {
|
||||
Actions []Action
|
||||
Seed uint64
|
||||
}
|
||||
|
||||
// Step is one low-level humanized event with a relative timestamp (ms).
|
||||
type Step struct {
|
||||
Type string `json:"type"` // move | down | up | key
|
||||
Key string `json:"key,omitempty"`
|
||||
AtMs float64 `json:"at_ms"`
|
||||
X int `json:"x,omitempty"`
|
||||
Y int `json:"y,omitempty"`
|
||||
}
|
||||
|
||||
// InteractionPlan is a humanized timeline the caller replays.
|
||||
type InteractionPlan struct {
|
||||
Steps []Step `json:"steps"`
|
||||
}
|
||||
|
||||
// PlanInteraction converts target actions into a humanized event timeline:
|
||||
// cubic-Bézier mouse paths with jitter and Fitts-law-derived durations, plus
|
||||
// per-key typing cadence. Deterministic for a given Seed.
|
||||
func PlanInteraction(p InteractionParams) InteractionPlan {
|
||||
r := rand.New(rand.NewPCG(p.Seed, p.Seed^0xa0761d6478bd642f)) //nolint:gosec // math/rand intentional: humanized timing is not a security primitive
|
||||
var steps []Step
|
||||
t := 0.0
|
||||
cx, cy := 0, 0
|
||||
for _, a := range p.Actions {
|
||||
switch a.Type {
|
||||
case "click":
|
||||
seg, dt := mousePath(r, cx, cy, a.X, a.Y, t)
|
||||
steps = append(steps, seg...)
|
||||
t += dt
|
||||
steps = append(steps, Step{Type: "down", X: a.X, Y: a.Y, AtMs: t})
|
||||
t += 40 + r.Float64()*60 // dwell
|
||||
steps = append(steps, Step{Type: "up", X: a.X, Y: a.Y, AtMs: t})
|
||||
t += 60 + r.Float64()*120
|
||||
cx, cy = a.X, a.Y
|
||||
case "type":
|
||||
for _, ch := range a.Text {
|
||||
t += 60 + r.Float64()*120 // inter-key cadence
|
||||
steps = append(steps, Step{Type: "key", Key: string(ch), AtMs: t})
|
||||
}
|
||||
}
|
||||
}
|
||||
return InteractionPlan{Steps: steps}
|
||||
}
|
||||
|
||||
func mousePath(r *rand.Rand, x0, y0, x1, y1 int, start float64) ([]Step, float64) {
|
||||
dist := math.Hypot(float64(x1-x0), float64(y1-y0))
|
||||
// Fitts-law-ish duration; bounded.
|
||||
dur := 120 + 100*math.Log2(1+dist/40)
|
||||
n := 12 + r.IntN(8)
|
||||
// random control points around the straight line for a curved path
|
||||
c1x := float64(x0) + (float64(x1-x0))*0.3 + (r.Float64()-0.5)*dist*0.3
|
||||
c1y := float64(y0) + (float64(y1-y0))*0.3 + (r.Float64()-0.5)*dist*0.3
|
||||
c2x := float64(x0) + (float64(x1-x0))*0.7 + (r.Float64()-0.5)*dist*0.3
|
||||
c2y := float64(y0) + (float64(y1-y0))*0.7 + (r.Float64()-0.5)*dist*0.3
|
||||
steps := make([]Step, 0, n)
|
||||
for i := 1; i <= n; i++ {
|
||||
u := float64(i) / float64(n)
|
||||
bx := cubic(u, float64(x0), c1x, c2x, float64(x1))
|
||||
by := cubic(u, float64(y0), c1y, c2y, float64(y1))
|
||||
jitter := 0.0
|
||||
if i < n { // no jitter on the final landing point
|
||||
jitter = (r.Float64() - 0.5) * 2
|
||||
}
|
||||
steps = append(steps, Step{
|
||||
Type: "move",
|
||||
X: int(bx + jitter),
|
||||
Y: int(by + jitter),
|
||||
AtMs: start + dur*u,
|
||||
})
|
||||
}
|
||||
return steps, dur
|
||||
}
|
||||
|
||||
func cubic(u, p0, p1, p2, p3 float64) float64 {
|
||||
v := 1 - u
|
||||
return v*v*v*p0 + 3*v*v*u*p1 + 3*v*u*u*p2 + u*u*u*p3
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package stealth
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestPlanInteraction(t *testing.T) {
|
||||
plan := PlanInteraction(InteractionParams{
|
||||
Actions: []Action{{Type: "click", X: 50, Y: 50}, {Type: "click", X: 150, Y: 150}},
|
||||
Seed: 7,
|
||||
})
|
||||
if len(plan.Steps) == 0 {
|
||||
t.Fatal("no steps")
|
||||
}
|
||||
// monotonic non-decreasing timing
|
||||
last := -1.0
|
||||
for _, s := range plan.Steps {
|
||||
if s.AtMs < last {
|
||||
t.Fatalf("timing not monotonic: %v then %v", last, s.AtMs)
|
||||
}
|
||||
last = s.AtMs
|
||||
}
|
||||
// last mouse step must land on the final target
|
||||
var lastMove *Step
|
||||
for i := range plan.Steps {
|
||||
if plan.Steps[i].Type == "move" {
|
||||
lastMove = &plan.Steps[i]
|
||||
}
|
||||
}
|
||||
if lastMove == nil || lastMove.X != 150 || lastMove.Y != 150 {
|
||||
t.Fatalf("final move did not reach target: %+v", lastMove)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user