Cara Memecahkan reCAPTCHA di WebView2 dengan C# (WinForms, WPF)

solve recaptcha in webview2 - How to Solve reCAPTCHA in WebView2 With C# (WinForms, WPF)

Untuk memecahkan reCAPTCHA di WebView2, tunggu NavigationCompleted, baca sitekey dengan ExecuteScriptAsync, lalu kirim sitekey itu ke CapSkip dengan await biasa agar kode Anda kembali ke thread UI. Setelah itu, tulis token ke setiap field g-recaptcha-response lalu panggil callback milik halaman atau kirim form-nya. Dua detail menjadi penyebab sebagian besar kegagalan. ExecuteScriptAsync mengembalikan JSON, bukan string, sehingga sitekey datang terbungkus tanda kutip. Lalu satu ConfigureAwait(false) pada pemecahan membuat pemanggilan WebView2 berikutnya melempar InvalidOperationException, karena CoreWebView2 hanya menjawab di thread yang membuatnya. Panduan ini membangun aplikasi WinForms kecil yang menangani keduanya, ditambah widget invisible, render eksplisit, dan penutupan jendela di tengah pemecahan.

Apa yang Anda butuhkan

  • CapSkip yang berjalan di komputer Windows, dengan solver reCAPTCHA diaktifkan.
  • SDK .NET 8 atau lebih baru. Kode di bawah sudah dijalankan di .NET 8 dan .NET 10, dan di-build dengan SDK 10.0.100.
  • WebView2 Runtime. Windows 11 sudah menyertakannya, dan runtime ini memperbarui dirinya sendiri secara otomatis, terpisah dari browser Edge.
  • Paket Microsoft.Web.WebView2 dan CapSkip dari NuGet.
  • Sebuah alamat untuk pemecah CAPTCHA. Dalam mode Local, CapSkip menjawab di 127.0.0.1 hanya untuk perangkat itu. Dalam mode Server, CapSkip mendengarkan di alamat jaringan atau IP publik Anda, sehingga aplikasi di mesin lain dapat memanggilnya melalui API. Keduanya diatur di pengaturan koneksi.
dotnet new winforms -n WvApp
cd WvApp
dotnet add package Microsoft.Web.WebView2
dotnet add package CapSkip

Kedua paket berhasil di-restore untuk net8.0-windows dan net10.0-windows. Build WinForms menampilkan satu peringatan MSB3277 yang tidak berbahaya tentang versi WindowsBase yang saling bertentangan, karena paket WebView2 selalu ikut mereferensikan assembly WPF-nya. Peringatan itu tidak memengaruhi aplikasi, dan proyek WPF tidak pernah menampilkannya. Menyetel UseWPF ke true juga menyembunyikannya, tetapi System.IO lalu keluar dari implicit usings dan Path.Combine tidak bisa dikompilasi lagi.

Langkah 1: beri WebView2 folder data yang bisa ditulisinya

WebView2 menyimpan cookie, cache, dan local storage di folder data pengguna. Jika Anda tidak menentukannya, WebView2 membuat folder itu di samping file exe Anda, sebagai WvApp.exe.WebView2. Itu berfungsi selama Anda menjalankan aplikasi dari bin\Debug. Begitu aplikasi diinstal di lokasi yang tidak bisa ditulisi pengguna standar, seperti Program Files, inisialisasi langsung gagal dengan UnauthorizedAccessException, Access is denied. Sebagai gantinya, tentukan folder di bawah local app data milik pengguna.

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);
}

Buat environment sebelum ada hal lain yang menyentuh kontrol. Menyetel properti Source lebih dulu akan memulai inisialisasi dengan environment default, lalu EnsureCoreWebView2Async yang dipanggil belakangan dengan environment Anda sendiri melempar ArgumentException yang menyatakan WebView2 sudah diinisialisasi dengan CoreWebView2Environment yang berbeda. Memanggil ExecuteScriptAsync pada kontrol sebelum inisialisasi juga gagal, dengan InvalidOperationException yang mengarahkan Anda ke EnsureCoreWebView2Async. Melalui webView.CoreWebView2, seperti pada kode panduan ini, kesalahan yang sama menjadi NullReferenceException, karena CoreWebView2 masih null. Halaman Microsoft tentang folder data pengguna WebView2 menjelaskan di mana saja folder itu boleh berada.

Langkah 2: baca sitekey, lalu decode hasil yang dikembalikan ExecuteScriptAsync

Sitekey ada di halaman, jadi baca dengan JavaScript. Widget kotak centang (checkbox) atau invisible membawanya di atribut data-sitekey. Halaman yang merender widget sendiri dengan grecaptcha.render tidak memiliki atribut itu, tetapi iframe anchor milik Google membawa key yang sama di parameter query k, sehingga skrip beralih ke sana sebagai cadangan.

// 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 mengembalikan hasil skrip dalam bentuk serialisasi JSON. Sitekey datang lengkap dengan tanda kutipnya, dan elemen yang tidak ada datang sebagai empat huruf null. Skrip yang melempar error, misalnya TypeError pada elemen yang tidak ada, juga mengembalikan null, tanpa exception apa pun di sisi C#. Deserialisasi dengan System.Text.Json memberi Anda string sungguhan atau null sungguhan. Memangkas tanda kutip secara manual terlihat sama saja sampai sebuah nilai berisi karakter escape, dan saat itulah cara tersebut rusak.

Jalankan ini setelah NavigationCompleted, bukan DOMContentLoaded. api.js milik Google dimuat secara asinkron, jadi saat DOMContentLoaded div widget sudah ada, tetapi textarea respons, objek grecaptcha, dan iframe anchor mungkin belum ada. Pada kunjungan pertama, semuanya muncul antara 0.3 dan 1.4 detik kemudian dalam pengujian kami, selalu sebelum NavigationCompleted. Begitu skrip sudah di-cache, semuanya sudah ada saat DOMContentLoaded. Jadi kode yang membaca terlalu awal bisa berfungsi di mesin Anda tetapi gagal bagi pengguna baru.

Langkah 3: pecahkan CAPTCHA tanpa meninggalkan thread UI

Inilah aturan yang paling sering menggagalkan upaya memecahkan reCAPTCHA di WebView2. CoreWebView2 dimiliki thread UI yang membuatnya, dan menyentuhnya dari thread lain mana pun akan melempar exception. Panduan untuk kode library menyarankan menambahkan ConfigureAwait(false) ke setiap await. Di event handler WebView2, saran itu salah.

// 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) membuat kode setelah pemecahan dilanjutkan di thread dari thread pool. Pemanggilan CoreWebView2 berikutnya lalu gagal dengan InvalidOperationException tadi. Hal yang sama terjadi jika Anda membungkus pemanggilan skrip dengan Task.Run. Di WPF, pesannya berbeda, The calling thread cannot access this object because a different thread owns it, dan jika exception itu lolos dari handler async void, aplikasi akan crash. Baca catatan Microsoft tentang model threading WebView2 untuk aturan lengkapnya.

// 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");

Satu await biasa di dalam event handler WinForms atau WPF menangkap synchronization context milik UI, sehingga kode setelahnya kembali berjalan di thread UI. UI tetap responsif selama CapSkip bekerja: dalam pengujian kami, timer 100 milidetik berdetak 71 kali selama pemecahan 7.88 detik. Memblokir dengan GetAwaiter().GetResult() tidak menyebabkan deadlock, karena SDK CapSkip memakai ConfigureAwait(false) di dalamnya, tetapi cara itu membekukan jendela selama seluruh pemecahan.

Langkah 4: sisipkan token dan kirim form

Begitu CapSkip mengembalikan hasil, taruh token di tempat yang semestinya diisi skrip Google, lalu picu apa pun yang dilakukan halaman selanjutnya.

// 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';
    })
    """;

Isi setiap textarea g-recaptcha-response, bukan hanya yang pertama. Form dengan dua widget, atau dengan textarea miliknya sendiri di samping milik Google, memiliki dua field dengan nama itu, dan server seperti PHP hanya menyimpan yang terakhir. Di halaman uji kami yang memiliki dua field, mengisi field pertama saja ditolak, sedangkan mengisi keduanya diterima.

Widget dengan atribut data-callback mengharapkan callback milik halaman dijalankan dengan token, jadi skrip memanggilnya. Di halaman callback kami, mengirim form tanpa callback ditolak. Halaman yang merender widget dengan grecaptcha.render justru meneruskan callback-nya lewat JavaScript, di tempat yang tidak bisa dilihat skrip. Cari nama fungsinya di skrip milik halaman itu sendiri lalu panggil dengan token dengan cara yang sama.

Jika tidak, skrip mengirim form dengan form.requestSubmit(), yang menjalankan submit handler dan validasi HTML5 milik halaman itu sendiri, seperti klik pada tombolnya. Masih ada satu perbedaan: name dan value milik tombol itu sendiri hanya dikirim jika Anda meneruskan tombolnya, seperti pada form.requestSubmit(button). Metode lama form.submit() melewati handler dan validasi, sehingga field wajib yang dibiarkan kosong tetap terkirim ke server. Baris checkValidity melaporkan kasus itu alih-alih mengirim form yang setengah kosong.

Token diteruskan sebagai string literal JSON, yang dibuat dengan JsonSerializer.Serialize, sehingga tanda kutip atau backslash di dalamnya tidak akan pernah merusak skrip. Sisipkan token itu segera. Google mendokumentasikan bahwa token reCAPTCHA kedaluwarsa dua menit setelah diterbitkan.

Contoh WinForms lengkap

Buat proyek dengan nama WvApp, agar file ini berbagi namespace dengan Form1.Designer.cs yang dibuat template. Ganti Form1.cs dengan kode di bawah, ubah LoginUrl ke halaman Anda, lalu jalankan dengan dotnet run. Program.cs dibiarkan seperti yang ditulis template.

// 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; }
    }
}

Handler melepas dirinya sendiri begitu menemukan sitekey. NavigationCompleted terpicu lagi untuk halaman tujuan kiriman form, dan tanpa baris itu aplikasi juga akan mencoba memecahkan halaman hasil. Di halaman tanpa widget, handler kembali lebih awal dan tetap terpasang, sehingga handler memeriksa lagi setelah navigasi berikutnya. Widget yang muncul belakangan di halaman yang sama membutuhkan loop polling yang ditunjukkan di bagian bawah. Widget invisible dideteksi dari parameter size pada iframe anchor milik Google. Sebelum iframe itu ada, atribut data-size atau widget yang terikat ke tombol menjadi penandanya. Widget itu lalu dikirim ke CapSkip dengan opsi invisible. Judul jendela menampilkan status. Itu cukup untuk sebuah tool, dan Label bisa menampilkannya sebagai gantinya.

CancellationTokenSource dibatalkan saat jendela mulai ditutup. Tutup aplikasi di tengah pemecahan, dan SDK langsung berhenti melakukan polling dengan TaskCanceledException, yang ditelan oleh catch pertama. Tanpa token itu, pemecahan pada form sekunder terus berjalan setelah form-nya hilang. Saat pemecahan selesai, CoreWebView2 sudah null, dan pemanggilan skrip berikutnya melempar NullReferenceException yang ditampilkan WinForms di dialog unhandled exception-nya.

Kode yang sama di WPF

Untuk memecahkan reCAPTCHA di WebView2 pada WPF, pertahankan langkah-langkah di atas. Kontrol WPF memiliki CoreWebView2 yang sama di baliknya, sehingga pemanggilan WebView2 dan CapSkip tidak berubah. Deklarasikan kontrol di MainWindow.xaml dengan namespace Microsoft.Web.WebView2.Wpf dan beri nama webView, lalu hapus field webView dan baris Controls.Add. Pindahkan sisanya ke MainWindow.xaml.cs, jalankan dari event Loaded, dan batalkan token di event handler Closing. Jendela WPF tidak memiliki properti Text, jadi tulis status ke Title. Implicit usings WPF tidak menyertakan System.IO, sehingga Path.Combine membutuhkan baris using tersendiri. Hapus import System.Windows.Shapes bawaan template dari file itu, karena namespace itu memiliki kelas Path sendiri dan namanya menjadi ambigu.

Widget di dalam iframe, dan widget yang dimuat belakangan

ExecuteScriptAsync hanya berjalan di dokumen teratas. Jika form login itu sendiri berada di dalam iframe, skrip sitekey mengembalikan null di sana. Kumpulkan frame milik halaman dengan event FrameCreated, yang harus dipasang sebelum Anda melakukan navigasi, dan buang setiap frame saat frame itu dihancurkan. Frame yang sudah hilang melempar InvalidOperationException jika Anda memanggilnya.

// 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);
};

Setiap CoreWebView2Frame memiliki ExecuteScriptAsync sendiri, sehingga skrip yang sama berjalan di dalam frame. Kirim alamat frame itu sendiri ke CapSkip sebagai URL halaman, karena di halaman itulah widget berada. Panggil metode ini dari OnNavigationCompleted saat dokumen teratas tidak mengembalikan sitekey, di dalam blok try dan catch yang sama dengan pemecahan utama. FrameCreated juga terpicu untuk iframe anchor dan challenge milik Google sendiri, dan loop melewatinya, karena skrip mengembalikan null di dalamnya.

// 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;
    }
}

Beberapa halaman menambahkan widget setelah event load, misalnya setelah banner cookie atau klik pertama. NavigationCompleted lalu tidak menemukan apa pun. Loop polling singkat menanganinya: di halaman yang menambahkan widget dua detik setelah load, kode ini menemukan key setelah 2.11 detik.

// 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;
}

Menjalankan CapSkip di mesin lain

Aplikasi dan pemecah CAPTCHA tidak harus berada di PC yang sama. Alihkan CapSkip ke mode Server di pengaturan koneksi, dan CapSkip akan mendengarkan di alamat jaringan atau IP publik Anda. Lalu arahkan klien ke sana.

// 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);

Gunakan IP publik statis jika rutenya melewati internet, dan aktifkan validasi API key, dengan key yang sama di klien. CapSkip membuka port-nya untuk semua alamat saat mode Server dimulai, jadi untuk membatasinya hanya pada mesin yang Anda harapkan, tambahkan aturan blokir Windows Firewall untuk alamat lainnya. Di mesin yang sama, tulis 127.0.0.1, bukan localhost. Dalam pengujian kami, localhost mencoba alamat IPv6 lebih dulu dan menunggu sekitar dua detik sampai percobaan itu gagal pada setiap koneksi baru. Pemecah CAPTCHA yang tidak berjalan baru dilaporkan setelah empat detik, bukan dua.

Menangani error CapSkip di aplikasi

SDK memunculkan empat exception, semuanya turunan dari CapSkipError. Untuk menampilkan pesan yang berbeda bagi setiap kegagalan, ganti blok try di OnNavigationCompleted dengan blok ini. Letakkan filter pembatalan paling awal: jendela yang ditutup saat tugas sedang dikirim memunculkan NetworkException, dan filter yang diletakkan belakangan akan menampilkannya sebagai error.

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; }

Ada satu nama yang perlu diperhatikan. Dengan implicit usings proyek WinForms, catch TimeoutException tanpa namespace gagal dikompilasi dengan CS0104, karena System.TimeoutException dan CapSkip.TimeoutException sama-sama berada dalam scope. Tulis nama lengkapnya dengan namespace CapSkip, seperti di atas. RecaptchaAsync menyerah setelah recaptchaTimeout milik SDK, 300 detik secara default, yang Anda setel di constructor CapSkipClient. Pemecahan yang lambat biasanya gagal lebih awal di dalam CapSkip sendiri, lalu sampai ke kode Anda sebagai ApiException.

Kesalahan umum saat memecahkan reCAPTCHA di WebView2

Apa yang Anda lihatPenyebabPerbaiki
InvalidOperationException: CoreWebView2 can only be accessed from the UI threadKode setelah pemecahan dilanjutkan di thread dari thread pool, akibat ConfigureAwait(false) atau Task.RunGunakan await biasa di event handler
Sitekey diapit tanda kutip dan setiap pemecahan gagalJSON mentah dari ExecuteScriptAsync dikirim ke CapSkipDeserialisasi hasilnya dengan System.Text.Json
Sitekey bernilai null di halaman dengan widget yang terlihatHalaman merender widget sendiri, menambahkannya belakangan, atau widget berada di dalam iframeBaca iframe anchor, lakukan polling untuk key, atau jalankan skrip di dalam frame
Access is denied begitu aplikasi dijalankanWebView2 mencoba membuat folder datanya di samping exe yang tidak bisa ditulisi penggunaBuat environment dengan folder di bawah local app data
ArgumentException: sudah diinisialisasi dengan environment yang berbedaSource disetel sebelum environment Anda dibuatInisialisasi dengan environment Anda lebih dulu, baru lakukan navigasi
Halaman memuat ulang form login setelah form dikirimHanya satu field respons yang diisi, callback tidak pernah dijalankan, atau token sudah kedaluwarsaIsi setiap field, panggil callback, dan sisipkan token tepat setelah pemecahan
Aplikasi mencoba memecahkan halaman yang dibuka setelah loginNavigationCompleted terpicu untuk setiap navigasiLepas handler begitu sitekey ditemukan
NetworkException: mesin tujuan secara aktif menolak koneksiCapSkip tidak berjalan, atau host atau port-nya salahJalankan CapSkip lalu cocokkan alamatnya dengan pengaturan koneksinya
Dialog NullReferenceException setelah menutup jendelaSebuah pemecahan selesai setelah form-nya ditutupBerikan CancellationToken dan batalkan saat form ditutup

FAQ

Apakah ini berfungsi untuk reCAPTCHA v3 dan Enterprise?

Ya, dengan opsi, bukan dengan metode yang berbeda. Halaman Enterprise memuat enterprise.js sebagai pengganti api.js. Tambahkan enterprise bernilai 1 ke opsi RecaptchaAsync, dan bagian lain panduan ini tetap sama. Halaman v3 berbeda: sitekey-nya berada di parameter render pada api.js, dan halaman mengirim token dari skripnya sendiri, bukan dari textarea. Berikan version bernilai v3 dan action milik halaman, dan jangan sertakan invisible, yang ditolak SDK untuk v3. Artikel panduan memecahkan reCAPTCHA v2 di C# membahas opsi tersebut secara lebih rinci.

Bagaimana cara membedakan error skrip dari null yang sebenarnya?

Gunakan ExecuteScriptWithResultAsync, yang sudah tersedia di paket WebView2 saat ini. Metode ini melaporkan apakah skrip berhasil dan memberi Anda pesan exception jika gagal, sedangkan ExecuteScriptAsync mengembalikan null untuk kedua kasus. Tetap gunakan ExecuteScriptAsync untuk pembacaan sederhana di atas, karena di sana widget yang tidak ada dan selector yang rusak berarti hal yang sama: tidak ada yang perlu dipecahkan.

Apakah ini sama dengan memecahkan CAPTCHA di WebView seluler?

Tidak. WebView2 adalah engine Edge di dalam aplikasi desktop Windows, dan Anda mengendalikannya langsung dari C#. WebView di dalam aplikasi Android atau iOS dikendalikan dari luar, biasanya melalui Appium, yang memiliki timeout sesi sendiri. Kasus itu dibahas di panduan memecahkan CAPTCHA di Appium.

Bisakah aplikasi desktop berjalan di mesin yang berbeda dari CapSkip?

Bisa. Alihkan CapSkip ke mode Server di pengaturan koneksi, agar ia mendengarkan di alamat jaringan, bukan di loopback, lalu berikan alamat itu sebagai host CapSkipClient. Aplikasi kemudian memanggilnya melalui API HTTP yang sama dari mesin mana pun yang bisa menjangkau port tersebut. Tambahkan aturan blokir Windows Firewall untuk alamat yang seharusnya tidak bisa menjangkaunya. Pemecah CAPTCHA tetap berjalan di perangkat milik Anda, dan tidak ada pemecahan yang melewati layanan pihak ketiga.

Versi singkatnya

Untuk memecahkan reCAPTCHA di WebView2, baca sitekey setelah NavigationCompleted dan deserialisasi JSON yang dikembalikan ExecuteScriptAsync. Lakukan await pada RecaptchaAsync tanpa ConfigureAwait(false), agar pemanggilan CoreWebView2 berikutnya terjadi di thread UI. Isi setiap field respons, lalu panggil callback milik halaman atau gunakan form.requestSubmit(). Beri WebView2 folder data yang bisa ditulisinya, batalkan pemecahan saat jendela ditutup, dan alihkan CapSkip ke mode Server saat aplikasi pindah ke mesin lain.

Aplikasi desktop dan pemecah CAPTCHA-nya di PC Windows yang sama adalah penyiapan paling sederhana yang ada. Untuk itulah sebuah pemecah captcha lokal dibuat: token berasal dari mesin Anda sendiri, dan tidak ada pemecahan yang keluar dari mesin itu.