From 8eb3873d27ffb679eddc2a2f230b5a58f9d6708a Mon Sep 17 00:00:00 2001 From: samosvalishe Date: Tue, 4 Aug 2026 10:37:08 +0300 Subject: [PATCH] =?UTF-8?q?fix(captcha):=20=D1=80=D0=B0=D0=B7=D0=BD=D0=BE?= =?UTF-8?q?=D1=81=D0=B8=D1=82=D1=8C=20=D0=BF=D0=BE=D0=BF=D1=8B=D1=82=D0=BA?= =?UTF-8?q?=D0=B8=20=D1=81=D0=BB=D0=B0=D0=B9=D0=B4=D0=B5=D1=80=D0=B0=20?= =?UTF-8?q?=D0=BF=D0=BE=20=D0=B4=D0=BE=D1=80=D0=BE=D0=B6=D0=BA=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Соседние кандидаты отличаются одним свапом и получают почти равный скор, поэтому топ-2 ранжирования - всегда пара соседей, и вторая попытка промахивается вместе с первой. Вторая берётся не ближе трёх позиций от первой, с откатом на соседей, если дальних кандидатов не хватило. --- .../vk/internal/captcha/captcha_test.go | 46 +++++++++++++++++++ .../provider/vk/internal/captcha/slider.go | 34 ++++++++++++-- 2 files changed, 76 insertions(+), 4 deletions(-) diff --git a/internal/provider/vk/internal/captcha/captcha_test.go b/internal/provider/vk/internal/captcha/captcha_test.go index 55379da..b6d00a0 100644 --- a/internal/provider/vk/internal/captcha/captcha_test.go +++ b/internal/provider/vk/internal/captcha/captcha_test.go @@ -137,3 +137,49 @@ func TestCaptchaDomainFromRedirectURI(t *testing.T) { }) } } + +func TestPickSliderAttempts(t *testing.T) { + tests := []struct { + name string + indexes []int + limit int + want []int + }{ + { + name: "skips neighbours of the top guess", + indexes: []int{20, 19, 21, 18, 40, 5}, + limit: 2, + want: []int{20, 40}, + }, + { + name: "falls back to neighbours when nothing is far enough", + indexes: []int{20, 19, 21}, + limit: 3, + want: []int{20, 19, 21}, + }, + { + name: "limit above candidate count", + indexes: []int{7}, + limit: 4, + want: []int{7}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + guesses := make([]sliderGuess, 0, len(tt.indexes)) + for _, idx := range tt.indexes { + guesses = append(guesses, sliderGuess{Index: idx}) + } + got := pickSliderAttempts(guesses, tt.limit) + if len(got) != len(tt.want) { + t.Fatalf("len = %d, want %d", len(got), len(tt.want)) + } + for i := range tt.want { + if got[i].Index != tt.want[i] { + t.Fatalf("attempts = %v, want %v", got, tt.want) + } + } + }) + } +} diff --git a/internal/provider/vk/internal/captcha/slider.go b/internal/provider/vk/internal/captcha/slider.go index 3025027..374192e 100644 --- a/internal/provider/vk/internal/captcha/slider.go +++ b/internal/provider/vk/internal/captcha/slider.go @@ -11,6 +11,7 @@ import ( _ "image/jpeg" // регистрация JPEG-декодера для image.Decode "math" "runtime" + "slices" "sort" "strconv" "strings" @@ -75,6 +76,7 @@ func (s *captchaSession) solveSliderCaptcha( if limit <= 0 { return "", errors.New("slider has no attempts available") } + attempts := pickSliderAttempts(guesses, limit) s.logger().Debugf("[Captcha] slider guesses ranked: total=%d limit=%d", len(guesses), limit) if err := s.sendComponentDone(sessionToken); err != nil { @@ -87,15 +89,15 @@ func (s *captchaSession) solveSliderCaptcha( if !s.profile.Touch() { handle = approachFrom(s.profile, s.layout, handle) } - for i := 0; i < limit; i++ { - s.logger().Debugf("[Captcha] slider attempt %d/%d (guess #%d)", i+1, limit, guesses[i].Index) + for i, guess := range attempts { + s.logger().Debugf("[Captcha] slider attempt %d/%d (guess #%d)", i+1, len(attempts), guess.Index) answerData, err := json.Marshal(struct { Value []int `json:"value"` - }{Value: guesses[i].Swaps}) + }{Value: guess.Swaps}) if err != nil { return "", err } - target := s.layout.sliderTarget(guesses[i].Index, len(guesses)) + target := s.layout.sliderTarget(guess.Index, len(guesses)) g := gesture{ from: handle, to: target, @@ -352,6 +354,30 @@ func rankSliderGuesses(img image.Image, gridSize int, swaps []int) ([]sliderGues return guesses, nil } +// pickSliderAttempts разносит попытки по дорожке: соседние кандидаты отличаются +// одним свапом и получают почти равный скор, поэтому вторая попытка рядом с +// первой промахивается вместе с ней. +func pickSliderAttempts(guesses []sliderGuess, limit int) []sliderGuess { + const minGap = 3 + out := make([]sliderGuess, 0, limit) + taken := func(index, gap int) bool { + return slices.ContainsFunc(out, func(g sliderGuess) bool { + return absInt(g.Index-index) < gap + }) + } + for _, gap := range []int{minGap, 1} { + for _, g := range guesses { + if len(out) == limit { + return out + } + if !taken(g.Index, gap) { + out = append(out, g) + } + } + } + return out +} + func activeSwapsForIndex(swaps []int, index int) []int { if index <= 0 { return []int{}