From d1b769a0625a6d706e1cda8f6df6ab80871df856 Mon Sep 17 00:00:00 2001 From: samosvalishe Date: Wed, 29 Jul 2026 01:53:49 +0300 Subject: [PATCH] =?UTF-8?q?fix(captcha):=20=D0=B4=D0=BE=D0=B6=D0=B8=D0=BC?= =?UTF-8?q?=D0=B0=D1=82=D1=8C=20=D1=8D=D1=81=D0=BA=D0=B0=D0=BB=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D1=8E=20checkbox=20->=20slider=20=D0=B2=20=D1=82=D0=BE?= =?UTF-8?q?=D0=B9=20=D0=B6=D0=B5=20=D1=81=D0=B5=D1=81=D1=81=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit VK меняет тип челленджа на captchaNotRobot.check, а решатель считал это фатальным и переигрывал сессию целиком (новый PoW + settings + initSession). Теперь content_settings читается и из check-ответа, слайдер решается на месте. --- .../provider/vk/internal/captcha/captcha.go | 56 +++++++++++++++---- .../provider/vk/internal/captcha/slider.go | 5 +- 2 files changed, 49 insertions(+), 12 deletions(-) diff --git a/internal/provider/vk/internal/captcha/captcha.go b/internal/provider/vk/internal/captcha/captcha.go index 963eb71..cd3ff05 100644 --- a/internal/provider/vk/internal/captcha/captcha.go +++ b/internal/provider/vk/internal/captcha/captcha.go @@ -73,10 +73,12 @@ type captchaCheck struct { Status string SuccessToken string ShowType string + Content captchaContentRef } type captchaShowTypeError struct { ShowType string + Content captchaContentRef } func (e *captchaShowTypeError) Error() string { @@ -217,6 +219,9 @@ func (s *captchaSession) solveOnce(captchaErr *Error) (string, error) { default: return "", fmt.Errorf("unsupported captcha type: %s", showType) } + if err != nil { + token, err = s.escalate(captchaErr.SessionToken, sliderContent, err) + } if err != nil { // Живой посетитель, уходя с нерешённой captcha, закрывает виджет. if _, leaveErr := s.captchaRequest("captchaNotRobot.leaveCaptcha", base); leaveErr != nil { @@ -231,6 +236,28 @@ func (s *captchaSession) solveOnce(captchaErr *Error) (string, error) { return token, nil } +// escalate добивает сессию, если VK на check-е сменил тип челленджа: виджет в +// браузере дорисовывает слайдер на месте, а не переоткрывает captcha. +func (s *captchaSession) escalate(sessionToken string, initContent captchaContentRef, cause error) (string, error) { + var mismatch *captchaShowTypeError + if !errors.As(cause, &mismatch) || !strings.EqualFold(mismatch.ShowType, "slider") { + return "", cause + } + content := mismatch.Content + if content.Value == "" { + content = initContent + } + if content.Value == "" { + return "", cause + } + s.logger().Debugf("[Captcha] escalated to slider in-session (content source=%s)", content.Source) + // Перерисовка виджета плюс пауза на осознание. + if err := s.dwell(500, 1100); err != nil { + return "", err + } + return s.solveSliderCaptcha(sessionToken, content) +} + // dwell выдерживает паузу [minMs, maxMs): телеметрия виджета тикает по таймеру, // поэтому длина её массивов обязана биться с реальным временем сессии. func (s *captchaSession) dwell(minMs, maxMs int) error { @@ -250,19 +277,25 @@ func parseCaptchaInitSession(raw map[string]any) (string, captchaContentRef) { if !ok { return "", captchaContentRef{} } - showType := captchaStringifyAny(resp["show_captcha_type"]) + return captchaStringifyAny(resp["show_captcha_type"]), parseSliderContentRef(resp["content_settings"]) +} + +func parseSliderContentRef(raw any) captchaContentRef { content := captchaContentRef{} - if data, err := json.Marshal(resp["content_settings"]); err == nil { - var settings []captchaInitSetting - if json.Unmarshal(data, &settings) == nil { - for _, setting := range settings { - if setting.Type == "slider" { - content = setting.contentRef() - } - } + data, err := json.Marshal(raw) + if err != nil { + return content + } + var settings []captchaInitSetting + if json.Unmarshal(data, &settings) != nil { + return content + } + for _, setting := range settings { + if setting.Type == "slider" { + content = setting.contentRef() } } - return showType, content + return content } type captchaContentRef struct { @@ -456,6 +489,7 @@ func parseCaptchaCheck(raw map[string]any) (*captchaCheck, error) { Status: captchaStringifyAny(resp["status"]), SuccessToken: captchaStringifyAny(resp["success_token"]), ShowType: captchaStringifyAny(resp["show_captcha_type"]), + Content: parseSliderContentRef(resp["content_settings"]), } if out.Status == "" { return nil, fmt.Errorf("captcha check status missing: %v", raw) @@ -513,7 +547,7 @@ func (s *captchaSession) solveCheckboxCaptcha(sessionToken string) (string, erro return "", err } if check.ShowType != "" && !strings.EqualFold(check.ShowType, "checkbox") { - return "", &captchaShowTypeError{ShowType: check.ShowType} + return "", &captchaShowTypeError{ShowType: check.ShowType, Content: check.Content} } if strings.EqualFold(check.Status, "error_limit") { return "", errCaptchaRateLimit diff --git a/internal/provider/vk/internal/captcha/slider.go b/internal/provider/vk/internal/captcha/slider.go index d80b99b..afbb60c 100644 --- a/internal/provider/vk/internal/captcha/slider.go +++ b/internal/provider/vk/internal/captcha/slider.go @@ -199,9 +199,12 @@ func splitSliderSteps(steps []int) (int, []int, int, error) { tail := append([]int(nil), steps[1:]...) attempts := 4 if len(tail)%2 != 0 { + // Штатный формат: [size, ...пары свапов, attempts]. attempts = tail[len(tail)-1] tail = tail[:len(tail)-1] - Log.Warnf("[Captcha] slider payload had odd-length tail; fallback attempts=%d", attempts) + Log.Debugf("[Captcha] slider attempts from payload=%d", attempts) + } else { + Log.Debugf("[Captcha] slider payload without attempts counter; default=%d", attempts) } if attempts <= 0 { attempts = 4