Как решить reCAPTCHA в WebView2 на C# (WinForms, WPF)

Чтобы решить reCAPTCHA в WebView2, дождитесь NavigationCompleted, прочитайте sitekey через ExecuteScriptAsync и отправьте его в CapSkip с обычным await, чтобы ваш код вернулся в UI-поток. Затем запишите токен в каждое поле g-recaptcha-response и вызовите колбэк страницы или отправьте форму. Большинство сбоев вызывают две детали. ExecuteScriptAsync возвращает JSON, а не строку, поэтому sitekey приходит в кавычках. А один ConfigureAwait(false) на решении заставляет следующий вызов WebView2 выбросить InvalidOperationException, потому что CoreWebView2 отвечает только в том потоке, который его создал. В этом руководстве мы соберём небольшое приложение WinForms, которое справляется с обеими проблемами, а также с невидимыми виджетами, явной отрисовкой виджета и закрытием окна посреди решения.
Что понадобится
- CapSkip, запущенный на машине с Windows, с включённым решателем reCAPTCHA.
- SDK .NET 8 или новее. Код ниже запускался на .NET 8 и .NET 10 и собирался с SDK 10.0.100.
- WebView2 Runtime. В Windows 11 он уже есть и обновляется автоматически, отдельно от браузера Edge.
- Пакеты Microsoft.Web.WebView2 и CapSkip из NuGet.
- Адрес решателя. В режиме Local CapSkip отвечает на 127.0.0.1 и только для этого устройства. В режиме Server он слушает ваш сетевой адрес или публичный IP, чтобы приложение на другой машине могло обращаться к нему через API. Оба режима задаются в разделе Настройки подключения.
dotnet new winforms -n WvApp cd WvApp dotnet add package Microsoft.Web.WebView2 dotnet add package CapSkip
Оба пакета восстанавливаются без ошибок для net8.0-windows и net10.0-windows. Сборка проекта WinForms выводит одно безобидное предупреждение MSB3277 о конфликтующих версиях WindowsBase, потому что пакет WebView2 всегда ссылается и на свою сборку для WPF. На работу приложения это не влияет, а в проекте WPF этого предупреждения не бывает. Если задать UseWPF значение true, оно тоже исчезнет, но тогда System.IO выпадает из неявных using, и Path.Combine перестаёт компилироваться.
Шаг 1: даём WebView2 папку данных с правом записи
WebView2 хранит cookies, кэш и локальное хранилище в папке пользовательских данных. Если вы её не укажете, он создаст её рядом с вашим exe под именем WvApp.exe.WebView2. Это работает, пока вы запускаете приложение из bin\Debug. Но как только приложение установлено туда, куда обычный пользователь писать не может, например в Program Files, инициализация сразу падает с UnauthorizedAccessException и сообщением Access is denied. Поэтому укажите папку в локальных данных приложений пользователя.
async Task StartAsync()
{
var dataDir = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "WvApp");
var env = await CoreWebView2Environment.CreateAsync(null, dataDir);
await webView.EnsureCoreWebView2Async(env);
webView.CoreWebView2.NavigationCompleted += OnNavigationCompleted;
webView.CoreWebView2.Navigate(LoginUrl);
}Создайте окружение до того, как что-либо ещё затронет элемент управления. Если сначала задать свойство Source, инициализация начнётся с окружением по умолчанию, и последующий EnsureCoreWebView2Async с вашим собственным окружением выбросит ArgumentException с сообщением о том, что WebView2 уже инициализирован с другим CoreWebView2Environment. Вызов ExecuteScriptAsync у элемента управления до инициализации тоже завершится ошибкой: InvalidOperationException, который отсылает вас к EnsureCoreWebView2Async. При обращении через webView.CoreWebView2, как в коде этого руководства, та же ошибка проявляется как NullReferenceException, потому что CoreWebView2 всё ещё равен null. Страница Microsoft о папке пользовательских данных WebView2 рассказывает, где может находиться эта папка.
Шаг 2: читаем sitekey и декодируем то, что возвращает ExecuteScriptAsync
Sitekey находится на странице, поэтому читайте его через JavaScript. Виджет с чекбоксом или невидимый виджет хранит его в атрибуте data-sitekey. У страницы, которая сама отрисовывает виджет через grecaptcha.render, такого атрибута нет, но iframe anchor от Google несёт тот же ключ в параметре запроса k, и в этом случае скрипт берёт ключ оттуда.
// data-sitekey first, then the anchor iframe that an explicit render leaves
// behind (api2 for reCAPTCHA, enterprise for reCAPTCHA Enterprise)
const string FindSitekeyJs = """
(() => {
const el = document.querySelector('.g-recaptcha[data-sitekey]');
if (el) return el.getAttribute('data-sitekey');
const f = document.querySelector('iframe[src*="/recaptcha/"][src*="/anchor?"]');
return f ? new URL(f.src).searchParams.get('k') : null;
})()
""";
async Task<string?> EvalStringAsync(string js) =>
JsonSerializer.Deserialize<string>(await webView.CoreWebView2.ExecuteScriptAsync(js));ExecuteScriptAsync возвращает результат скрипта, сериализованный в JSON. Sitekey приходит вместе с кавычками, а отсутствующий элемент приходит в виде четырёх букв null. Скрипт, который выбрасывает ошибку, например TypeError на несуществующем элементе, тоже возвращает null, и на стороне C# никакого исключения не возникает. Десериализация через System.Text.Json даёт вам настоящую строку или настоящий null. Если срезать кавычки вручную, результат выглядит так же, пока в значении не встретится экранированный символ, и тогда всё ломается.
Запускайте это после NavigationCompleted, а не после DOMContentLoaded. api.js от Google загружается асинхронно, поэтому на момент DOMContentLoaded div виджета уже существует, а textarea для ответа, объект grecaptcha и iframe anchor могут ещё не появиться. При первом посещении в наших запусках они появлялись спустя от 0.3 до 1.4 секунды, всегда до NavigationCompleted. Когда скрипт уже был в кэше, они были на месте уже к DOMContentLoaded. Поэтому код, который читает слишком рано, может работать на вашей машине и ломаться у нового пользователя.
Шаг 3: решаем капчу, не покидая UI-поток
Именно это правило ломает большинство попыток решить reCAPTCHA в WebView2. CoreWebView2 принадлежит UI-потоку, который его создал, и обращение к нему из любого другого потока выбрасывает исключение. Руководства для библиотечного кода советуют добавлять ConfigureAwait(false) к каждому await. В обработчике событий WebView2 этот совет ошибочен.
// In OnNavigationCompleted, where core is webView.CoreWebView2.
// Throws: ConfigureAwait(false) resumes on a thread-pool thread
var result = await solver.RecaptchaAsync(sitekey, core.Source).ConfigureAwait(false);
await webView.CoreWebView2.ExecuteScriptAsync("document.title");
// System.InvalidOperationException: CoreWebView2 can only be accessed from the UI thread.ConfigureAwait(false) позволяет коду после решения продолжиться в потоке из пула потоков. Тогда следующий вызов CoreWebView2 падает с тем самым InvalidOperationException. То же самое происходит, если обернуть вызов скрипта в Task.Run. В WPF сообщение другое, The calling thread cannot access this object because a different thread owns it, и если исключение вырвется из обработчика async void, приложение упадёт. Заметки Microsoft о модели потоков WebView2 описывают это правило полностью.
// Works: a plain await inside a WinForms event handler comes back on the UI
// thread
var result = await solver.RecaptchaAsync(sitekey, core.Source);
await webView.CoreWebView2.ExecuteScriptAsync("document.title");Обычный await внутри обработчика событий WinForms или WPF захватывает контекст синхронизации UI, поэтому код после него снова выполняется в UI-потоке. Пока CapSkip работает, интерфейс остаётся отзывчивым: в нашем тесте 100-миллисекундный таймер сработал 71 раз за решение длиной 7.88 секунды. Блокировка через GetAwaiter().GetResult() не приводит к взаимоблокировке, потому что SDK CapSkip внутри использует ConfigureAwait(false), но окно зависает на всё время решения.
Шаг 4: внедряем токен и отправляем форму
Когда CapSkip вернёт ответ, положите токен туда, куда его положил бы скрипт Google, а затем запустите то, что страница делает дальше.
// fill every response textarea, then call data-callback or submit the form
const string InjectJs = """
(token => {
const boxes = document.querySelectorAll('textarea[name="g-recaptcha-response"]');
boxes.forEach(b => { b.value = token; });
const cb = document.querySelector('.g-recaptcha[data-callback]')?.dataset.callback;
if (cb && typeof window[cb] === 'function') { window[cb](token); return 'called ' + cb; }
const form = boxes[0]?.form;
if (!form) return 'no form found';
if (!form.checkValidity()) return 'form has empty required fields';
form.requestSubmit();
return 'submitted the form';
})
""";Заполняйте каждую textarea g-recaptcha-response, а не только первую. В форме с двумя виджетами или с собственной textarea рядом с textarea от Google два поля с таким именем, а сервер, например на PHP, оставляет только последнее. На нашей тестовой странице с двумя полями заполнение только первого было отклонено, а заполнение обоих принято.
Виджет с атрибутом data-callback ожидает, что колбэк страницы будет вызван с токеном, поэтому скрипт его вызывает. На нашей странице с колбэком отправка формы без вызова колбэка была отклонена. Страница, которая отрисовывает виджет через grecaptcha.render, передаёт колбэк в JavaScript, и скрипт его не видит. Найдите имя функции в собственном скрипте страницы и вызовите её с токеном тем же способом.
Иначе скрипт отправляет форму через form.requestSubmit(), который запускает собственные обработчики отправки страницы и валидацию HTML5, как клик по её кнопке. Остаётся одно отличие: имя и значение самой кнопки отправляются, только если передать кнопку, как в form.requestSubmit(button). Старый метод form.submit() пропускает обработчики и валидацию, поэтому обязательное поле, оставленное пустым, всё равно уходит на сервер. Строка с checkValidity сообщает о таком случае, вместо того чтобы отправлять наполовину пустую форму.
Токен передаётся в виде строкового литерала JSON, созданного через JsonSerializer.Serialize, поэтому кавычки или обратные слеши в нём никогда не сломают скрипт. Внедряйте его сразу. Google указывает в документации, что токен reCAPTCHA истекает через две минуты после выдачи.
Полный пример для WinForms
Создайте проект с именем WvApp, чтобы этот файл находился в одном пространстве имён с Form1.Designer.cs, созданным шаблоном. Замените Form1.cs кодом ниже, поменяйте LoginUrl на адрес своей страницы и запустите через dotnet run. Program.cs остаётся таким, каким его создал шаблон.
// Form1.cs, in a project made with: dotnet new winforms -n WvApp
using System.Text.Json;
using CapSkip;
using Microsoft.Web.WebView2.Core;
using Microsoft.Web.WebView2.WinForms;
namespace WvApp;
public partial class Form1 : Form
{
const string LoginUrl = "https://example.com/login";
// data-sitekey first, then the anchor iframe that an explicit render leaves
// behind (api2 for reCAPTCHA, enterprise for reCAPTCHA Enterprise)
const string FindSitekeyJs = """
(() => {
const el = document.querySelector('.g-recaptcha[data-sitekey]');
if (el) return el.getAttribute('data-sitekey');
const f = document.querySelector('iframe[src*="/recaptcha/"][src*="/anchor?"]');
return f ? new URL(f.src).searchParams.get('k') : null;
})()
""";
const string IsInvisibleJs = """
(() => {
const f = document.querySelector('iframe[src*="/recaptcha/"][src*="/anchor?"]');
if (f) return new URL(f.src).searchParams.get('size') === 'invisible';
const el = document.querySelector('.g-recaptcha[data-sitekey]');
if (!el) return false;
if (el.getAttribute('data-size') === 'invisible') return true;
return el.tagName === 'BUTTON' && !el.hasAttribute('data-action');
})()
""";
// fill every response textarea, then call data-callback or submit the form
const string InjectJs = """
(token => {
const boxes = document.querySelectorAll('textarea[name="g-recaptcha-response"]');
boxes.forEach(b => { b.value = token; });
const cb = document.querySelector('.g-recaptcha[data-callback]')?.dataset.callback;
if (cb && typeof window[cb] === 'function') { window[cb](token); return 'called ' + cb; }
const form = boxes[0]?.form;
if (!form) return 'no form found';
if (!form.checkValidity()) return 'form has empty required fields';
form.requestSubmit();
return 'submitted the form';
})
""";
readonly WebView2 webView = new() { Dock = DockStyle.Fill };
readonly CapSkipClient solver = new(host: "127.0.0.1", port: 8080);
readonly CancellationTokenSource cts = new();
public Form1()
{
InitializeComponent();
Controls.Add(webView);
Load += async (_, _) => await StartAsync();
FormClosing += (_, _) => cts.Cancel();
}
async Task StartAsync()
{
var dataDir = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "WvApp");
var env = await CoreWebView2Environment.CreateAsync(null, dataDir);
await webView.EnsureCoreWebView2Async(env);
webView.CoreWebView2.NavigationCompleted += OnNavigationCompleted;
webView.CoreWebView2.Navigate(LoginUrl);
}
async Task<string?> EvalStringAsync(string js) =>
JsonSerializer.Deserialize<string>(await webView.CoreWebView2.ExecuteScriptAsync(js));
async void OnNavigationCompleted(object? sender, CoreWebView2NavigationCompletedEventArgs e)
{
var core = webView.CoreWebView2;
var sitekey = await EvalStringAsync(FindSitekeyJs);
if (sitekey is null) return; // no widget on this page
core.NavigationCompleted -= OnNavigationCompleted; // solve once, not on the result page
var invisible = await core.ExecuteScriptAsync(IsInvisibleJs) == "true";
var options = invisible ? new Dictionary<string, object?> { ["invisible"] = 1 } : null;
Text = "Solving reCAPTCHA...";
try
{
var result = await solver.RecaptchaAsync(sitekey, core.Source, options, cts.Token);
var token = JsonSerializer.Serialize(result.Code);
Text = "reCAPTCHA solved, " + await EvalStringAsync($"({InjectJs})({token})");
}
catch (Exception) when (cts.IsCancellationRequested) { } // window closed mid-solve
catch (CapSkipError ex) { Text = "CapSkip error: " + ex.Message; }
}
}Обработчик отписывается сам, как только находит sitekey. NavigationCompleted срабатывает снова для страницы, на которую форма отправляет данные, и без этой строки приложение попыталось бы решить ещё и страницу с результатом. На странице без виджета обработчик выходит раньше и остаётся подписанным, поэтому проверяет снова после следующей навигации. Для виджета, который появляется на той же странице позже, нужен цикл опроса, показанный ниже. Невидимый виджет определяется по параметру size в iframe anchor от Google. Пока этого iframe нет, на невидимый виджет указывает атрибут data-size или привязка виджета к кнопке. Затем он отправляется в CapSkip с опцией invisible. Статус показывается в заголовке окна. Для утилиты этого достаточно, а вместо заголовка статус может показывать Label.
CancellationTokenSource отменяется, когда окно начинает закрываться. Закройте приложение посреди решения, и SDK сразу прекратит опрос с TaskCanceledException, который проглатывает первый catch. Без токена решение на дополнительной форме продолжается и после того, как форма закрыта. Когда оно возвращается, CoreWebView2 равен null, и следующий вызов скрипта выбрасывает NullReferenceException, который WinForms показывает в своём диалоге необработанного исключения.
Тот же код в WPF
Чтобы решить reCAPTCHA в WebView2 под WPF, сохраните шаги выше. Под элементом управления WPF лежит тот же CoreWebView2, поэтому вызовы WebView2 и CapSkip не меняются. Объявите элемент управления в MainWindow.xaml с пространством имён Microsoft.Web.WebView2.Wpf и назовите его webView, а затем удалите поле webView и строку Controls.Add. Остальное перенесите в MainWindow.xaml.cs, запускайте из события Loaded и отменяйте токен в обработчике события Closing. У окна WPF нет свойства Text, поэтому пишите статус в Title. Неявные using в WPF не включают System.IO, поэтому для Path.Combine нужна отдельная строка using. Удалите из этого файла импорт System.Windows.Shapes, добавленный шаблоном, потому что в нём есть собственный класс Path, и имя становится неоднозначным.
Виджеты внутри iframe и виджеты, которые загружаются поздно
ExecuteScriptAsync выполняется только в документе верхнего уровня. Если сама форма входа находится в iframe, скрипт чтения sitekey вернёт там null. Собирайте фреймы страницы через событие FrameCreated, подписаться на которое нужно до навигации, и удаляйте каждый фрейм из списка, когда он уничтожается. Фрейм, которого уже нет, выбрасывает InvalidOperationException, если к нему обратиться.
// a field: the page's iframes, filled before Navigate() and pruned as they go
readonly List<CoreWebView2Frame> frames = new();
// in StartAsync, before Navigate():
webView.CoreWebView2.FrameCreated += (_, e) =>
{
var frame = e.Frame; // read it once: each read returns a new wrapper
frames.Add(frame);
frame.Destroyed += (_, _) => frames.Remove(frame);
};У каждого CoreWebView2Frame есть собственный ExecuteScriptAsync, поэтому те же скрипты выполняются внутри фрейма. Передайте CapSkip в качестве URL страницы собственный адрес фрейма, потому что виджет находится именно на этой странице. Вызывайте этот метод из OnNavigationCompleted, когда документ верхнего уровня не вернул sitekey, внутри тех же блоков try и catch, что и основное решение. FrameCreated срабатывает и для собственных iframe anchor и challenge от Google, и цикл их пропускает, потому что скрипт внутри них возвращает null.
// widget inside a cross-origin iframe: run the same scripts in that frame
async Task SolveInFramesAsync()
{
foreach (var frame in frames.ToList())
{
var key = JsonSerializer.Deserialize<string>(await frame.ExecuteScriptAsync(FindSitekeyJs));
if (key is null) continue;
var href = JsonSerializer.Deserialize<string>(await frame.ExecuteScriptAsync("location.href"))!;
var result = await solver.RecaptchaAsync(key, href, null, cts.Token);
var token = JsonSerializer.Serialize(result.Code);
await frame.ExecuteScriptAsync($"({InjectJs})({token})");
return;
}
}Некоторые страницы добавляют виджет после события load, например после баннера с cookies или первого клика. Тогда NavigationCompleted ничего не находит. С этим справляется короткий цикл опроса: на странице, которая добавляла виджет через две секунды после загрузки, он нашёл ключ через 2.11 секунды.
// for pages that add the widget after the load event
async Task<string?> WaitForSitekeyAsync(TimeSpan timeout)
{
var deadline = DateTime.UtcNow + timeout;
while (DateTime.UtcNow < deadline)
{
var key = await EvalStringAsync(FindSitekeyJs);
if (key is not null) return key;
await Task.Delay(250);
}
return null;
}Запуск CapSkip на другой машине
Приложению и решателю не обязательно работать на одном ПК. Переключите CapSkip в режим Server в настройках подключения, и он будет слушать ваш сетевой адрес или публичный IP. Затем направьте клиент на этот адрес.
// CapSkip in Server mode on another Windows machine on your network var solver = new CapSkipClient(apiKey: "YOUR_API_KEY", host: "192.168.1.50", port: 8080);
Если маршрут идёт через интернет, используйте статический публичный IP и включите проверку API-ключа, указав тот же ключ в клиенте. При запуске режима Server CapSkip открывает свой порт для всех адресов, поэтому, чтобы доступ к нему был только у ожидаемых машин, добавьте в Windows Firewall правило, блокирующее все остальные. На той же машине пишите 127.0.0.1, а не localhost. В наших запусках localhost сначала пробовал адрес IPv6 и при каждом новом подключении около двух секунд ждал, пока эта попытка не провалится. Ошибка о том, что решатель не запущен, приходила через четыре секунды вместо двух.
Обработка ошибок CapSkip в приложении
SDK выбрасывает четыре исключения, и все они наследуются от CapSkipError. Чтобы показывать для каждого сбоя своё сообщение, замените блок try в OnNavigationCompleted вот этим. Фильтр отмены оставьте первым: окно, закрытое во время отправки задачи, вызывает NetworkException, и фильтр, стоящий ниже, показал бы его как ошибку.
try
{
var result = await solver.RecaptchaAsync(sitekey, core.Source, options, cts.Token);
var token = JsonSerializer.Serialize(result.Code);
Text = "reCAPTCHA solved, " + await EvalStringAsync($"({InjectJs})({token})");
}
catch (Exception) when (cts.IsCancellationRequested) { } // window closed mid-solve
catch (NetworkException ex) { Text = "Is CapSkip running? " + ex.Message; }
catch (ApiException ex) { Text = "CapSkip could not solve it: " + ex.Message; }
catch (CapSkip.TimeoutException) { Text = "No answer before the timeout"; }
catch (CapSkipError ex) { Text = "CapSkip error: " + ex.Message; }Одно имя требует внимания. С неявными using проекта WinForms голый catch по TimeoutException не компилируется и выдаёт CS0104, потому что в области видимости находятся и System.TimeoutException, и CapSkip.TimeoutException. Пишите полное имя из CapSkip, как выше. RecaptchaAsync сдаётся по истечении recaptchaTimeout из SDK (по умолчанию 300 секунд), который задаётся в конструкторе CapSkipClient. Медленное решение обычно проваливается раньше внутри самого CapSkip и тогда приходит как ApiException.
Частые ошибки при решении reCAPTCHA в WebView2
| Что вы видите | Причина | Исправить |
|---|---|---|
| InvalidOperationException: CoreWebView2 can only be accessed from the UI thread | Код после решения продолжился в потоке из пула потоков из-за ConfigureAwait(false) или Task.Run | Используйте в обработчике событий обычный await |
| Sitekey пришёл в кавычках, и каждое решение проваливается | В CapSkip ушёл сырой JSON из ExecuteScriptAsync | Десериализуйте результат через System.Text.Json |
| Sitekey равен null на странице с видимым виджетом | Страница сама отрисовывает виджет, добавляет его поздно или он находится в iframe | Читайте iframe anchor, опрашивайте страницу в ожидании ключа или выполняйте скрипт во фрейме |
| Access is denied сразу при запуске приложения | WebView2 попытался создать папку данных рядом с exe, в папку которого пользователь не может писать | Создайте окружение с папкой в локальных данных приложений |
| ArgumentException: WebView2 уже инициализирован с другим окружением | Source был задан до того, как было создано ваше окружение | Сначала инициализируйте с вашим окружением, затем выполняйте навигацию |
| После отправки страница снова загружает форму входа | Заполнено только одно поле ответа, колбэк так и не вызвался или токен истёк | Заполните каждое поле, вызовите колбэк и внедряйте токен сразу после решения |
| Приложение пытается решить страницу, на которую попадает после входа | NavigationCompleted срабатывает при каждой навигации | Отписывайте обработчик, как только найден sitekey |
| NetworkException: целевой компьютер активно отверг подключение | CapSkip не запущен либо неверно указаны хост или порт | Запустите CapSkip и сверьте адрес с его настройками подключения |
| Диалог с NullReferenceException после закрытия окна | Решение завершилось уже после того, как его форма была закрыта | Передавайте CancellationToken и отменяйте его при закрытии формы |
FAQ
Работает ли это для reCAPTCHA v3 и Enterprise?
Да, с помощью опций, а не другого метода. Страница Enterprise загружает enterprise.js вместо api.js. Добавьте в опции RecaptchaAsync параметр enterprise со значением 1, а всё остальное в этом руководстве останется прежним. Со страницей v3 всё иначе: её sitekey находится в параметре render у api.js, а токен страница отправляет из собственного скрипта, а не из textarea. Передайте version со значением v3 и action страницы и не указывайте invisible: для v3 SDK эту опцию отклоняет. В руководстве по решению reCAPTCHA v2 на C# эти опции разобраны подробнее.
Как отличить ошибку скрипта от настоящего null?
Используйте ExecuteScriptWithResultAsync, который есть в текущем пакете WebView2. Он сообщает, успешно ли выполнился скрипт, и выдаёт сообщение исключения, если нет, тогда как ExecuteScriptAsync в обоих случаях возвращает null. Оставьте ExecuteScriptAsync для простых чтений выше, где отсутствующий виджет и сломанный селектор означают одно и то же: решать нечего.
Это то же самое, что решение капчи в мобильном WebView?
Нет. WebView2 представляет собой движок Edge внутри настольного приложения Windows, и вы управляете им напрямую из C#. WebView внутри приложения для Android или iOS управляется снаружи, обычно через Appium, у которого свои таймауты сессии. Этот случай разобран в руководстве по решению капчи в Appium.
Может ли настольное приложение работать не на той машине, где запущен CapSkip?
Да. Переведите CapSkip в режим Server в настройках подключения, чтобы он слушал сетевой адрес, а не локальную петлю, и передайте этот адрес как host в CapSkipClient. Тогда приложение обращается к нему через тот же HTTP API с любой машины, которой доступен порт. Добавьте блокирующее правило Windows Firewall для адресов, у которых не должно быть к нему доступа. Решатель по-прежнему работает на вашем собственном железе, и ни одно решение не проходит через сторонний сервис.
Коротко
Чтобы решить reCAPTCHA в WebView2, читайте sitekey после NavigationCompleted и десериализуйте JSON, который возвращает ExecuteScriptAsync. Ожидайте RecaptchaAsync через await без ConfigureAwait(false), чтобы следующий вызов CoreWebView2 выполнился в UI-потоке. Заполните каждое поле ответа, а затем вызовите колбэк страницы или используйте form.requestSubmit(). Дайте WebView2 папку данных с правом записи, отменяйте решение при закрытии окна и переключите CapSkip в режим Server, когда приложение переезжает на другую машину.
- Все типы капчи, которые поддерживает пакет для .NET, перечислены на странице сервиса распознавания капчи для C#.
- Как работает reCAPTCHA с чекбоксом и что покрывает решатель, описано на странице сервиса распознавания reCAPTCHA v2.
Настольное приложение и его решатель на одном ПК с Windows образуют самую простую схему из всех возможных. Именно для этого локальный сервис распознавания капчи и предназначен: токен приходит с вашей собственной машины, и ни одно решение её не покидает.
