mirror of
https://github.com/lukaszraczylo/mcp-captcharesolver.git
synced 2026-10-16 01:07:54 +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