C# से WebView2 में reCAPTCHA कैसे हल करें (WinForms, WPF)

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

WebView2 में reCAPTCHA हल करने के लिए, NavigationCompleted का इंतज़ार करें, ExecuteScriptAsync से sitekey पढ़ें, और उसे एक सादे await के साथ CapSkip को भेजें, ताकि आपका कोड वापस UI थ्रेड पर लौटे। फिर token को हर g-recaptcha-response फ़ील्ड में लिखें और पेज का callback कॉल करें या फॉर्म सबमिट करें। ज़्यादातर नाकामियाँ दो बातों से होती हैं। ExecuteScriptAsync स्ट्रिंग नहीं, JSON लौटाता है, इसलिए sitekey quotes में लिपटा हुआ आता है। और हल पर लगा एक ConfigureAwait(false) अगली WebView2 कॉल पर InvalidOperationException फिंकवा देता है, क्योंकि CoreWebView2 सिर्फ़ उसी थ्रेड पर जवाब देता है जिसने उसे बनाया था। यह गाइड एक छोटा WinForms ऐप बनाती है जो इन दोनों को संभालता है, साथ ही invisible widgets, explicit render और हल के बीच में विंडो बंद करने को भी।

आपको क्या चाहिए

  • किसी Windows मशीन पर चल रहा CapSkip, जिसमें reCAPTCHA सॉल्वर चालू हो।
  • वर्ज़न 8 या उसके बाद का .NET SDK। नीचे का कोड .NET 8 और .NET 10 पर चलाया गया, और SDK 10.0.100 से build किया गया।
  • WebView2 Runtime। Windows 11 में यह पहले से मौजूद होता है, और Edge ब्राउज़र से अलग, अपने आप अपडेट होता है।
  • NuGet से Microsoft.Web.WebView2 और CapSkip पैकेज।
  • सॉल्वर के लिए एक पता। 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 के लिए बिना दिक्कत restore होते हैं। WinForms build, WindowsBase के आपस में टकराते वर्ज़न के बारे में एक हानिरहित MSB3277 warning दिखाता है, क्योंकि WebView2 पैकेज हमेशा अपनी WPF assembly को भी reference करता है। इसका ऐप पर कोई असर नहीं पड़ता, और WPF प्रोजेक्ट में यह कभी नहीं दिखती। UseWPF को true पर सेट करने से भी यह छिप जाती है, लेकिन तब System.IO, implicit usings से बाहर हो जाता है और Path.Combine कंपाइल होना बंद कर देता है।

चरण 1: WebView2 को ऐसा डेटा फ़ोल्डर दें जिसमें वह लिख सके

WebView2 cookies, cache और local storage को एक user data फ़ोल्डर में रखता है। अगर आप कोई फ़ोल्डर तय नहीं करते, तो वह इसे आपकी exe के बगल में WvApp.exe.WebView2 नाम से बनाता है। जब तक आप ऐप को bin\Debug से चलाते हैं, यह काम करता है। जैसे ही ऐप किसी ऐसी जगह इंस्टॉल होता है जहाँ सामान्य यूज़र लिख नहीं सकता, जैसे Program Files, initialisation तुरंत UnauthorizedAccessException के साथ फेल हो जाता है, संदेश होता है Access is denied। इसके बजाय यूज़र के local app data के अंदर कोई फ़ोल्डर तय करें।

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

किसी भी और चीज़ के कंट्रोल को छूने से पहले environment बनाएँ। Source प्रॉपर्टी पहले सेट करने से initialisation डिफ़ॉल्ट environment के साथ शुरू हो जाता है, और फिर बाद में आपके अपने environment के साथ EnsureCoreWebView2Async एक ArgumentException फेंकता है, जिसमें लिखा होता है कि WebView2 पहले ही किसी दूसरे CoreWebView2Environment के साथ initialize हो चुका है। initialisation से पहले कंट्रोल पर ExecuteScriptAsync कॉल करना भी फेल होता है, एक InvalidOperationException के साथ जो आपको EnsureCoreWebView2Async की ओर इशारा करता है। webView.CoreWebView2 के ज़रिए, जैसा इस गाइड के कोड में है, यही गलती NullReferenceException बनती है, क्योंकि CoreWebView2 अभी भी null है। WebView2 user data फ़ोल्डर के बारे में Microsoft का पेज बताता है कि यह फ़ोल्डर कहाँ रखा जा सकता है।

चरण 2: sitekey पढ़ें, और ExecuteScriptAsync जो लौटाता है उसे decode करें

sitekey पेज में होता है, इसलिए उसे JavaScript से पढ़ें। checkbox या invisible widget उसे data-sitekey attribute में रखता है। जो पेज widget को खुद grecaptcha.render से रेंडर करता है, उसमें ऐसा कोई attribute नहीं होता, लेकिन Google का anchor iframe वही key अपने k query parameter में रखता है, इसलिए ऐसे में script उसी का सहारा लेती है।

// 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, script का नतीजा JSON के रूप में serialise करके लौटाता है। sitekey अपने quotes के साथ आता है, और गायब element चार अक्षरों null के रूप में आता है। जो script error फेंकती है, जैसे मौजूद न होने वाले element पर TypeError, वह भी null लौटाती है, और C# की तरफ़ कोई exception नहीं आता। System.Text.Json से deserialise करने पर आपको असली स्ट्रिंग या असली null मिलता है। हाथ से quotes हटाना तब तक वैसा ही दिखता है, जब तक किसी वैल्यू में कोई escaped कैरेक्टर न हो, और तब यह टूट जाता है।

इसे NavigationCompleted के बाद चलाएँ, DOMContentLoaded पर नहीं। Google का api.js asynchronously लोड होता है, इसलिए DOMContentLoaded पर widget का div तो मौजूद होता है, लेकिन response textarea, grecaptcha object और anchor iframe शायद न हों। हमारे रन में, पहली विज़िट पर ये 0.3 से 1.4 सेकंड बाद दिखे, हमेशा NavigationCompleted से पहले। script cache हो जाने के बाद, ये DOMContentLoaded पर पहले से मौजूद थे। इसलिए बहुत जल्दी पढ़ने वाला कोड आपकी मशीन पर काम कर सकता है और किसी नए यूज़र के लिए फेल हो सकता है।

चरण 3: UI थ्रेड छोड़े बिना कैप्चा हल करें

WebView2 में reCAPTCHA हल करने की सबसे ज़्यादा कोशिशें इसी नियम पर टूटती हैं। CoreWebView2 उस UI थ्रेड का होता है जिसने उसे बनाया, और किसी दूसरे थ्रेड से उसे छूने पर exception आता है। library कोड के लिए लिखी गाइड हर await में ConfigureAwait(false) जोड़ने को कहती हैं। WebView2 के event handler में यह सलाह गलत है।

// 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) हल के बाद वाले कोड को thread pool के किसी थ्रेड पर फिर से शुरू होने देता है। तब CoreWebView2 की अगली कॉल उसी InvalidOperationException के साथ फेल होती है। अगर आप script कॉल को Task.Run में लपेटें, तब भी यही होता है। WPF में संदेश अलग होता है, The calling thread cannot access this object because a different thread owns it, और अगर यह किसी async void handler से बाहर निकल जाए, तो ऐप क्रैश हो जाता है। पूरे नियम के लिए Microsoft के WebView2 threading मॉडल पर लिखे नोट्स पढ़ें।

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

WinForms या WPF event handler के अंदर सादा await, UI का synchronisation context पकड़ लेता है, इसलिए उसके बाद का कोड फिर से UI थ्रेड पर चलता है। CapSkip के काम करते समय UI responsive रहता है: हमारे टेस्ट में 7.88 सेकंड के हल के दौरान 100 मिलीसेकंड वाला timer 71 बार चला। GetAwaiter().GetResult() से block करने पर deadlock नहीं होता, क्योंकि CapSkip SDK अंदर ConfigureAwait(false) इस्तेमाल करता है, लेकिन यह पूरे हल के दौरान विंडो को फ़्रीज़ कर देता है।

चरण 4: token inject करें और फॉर्म सबमिट करें

CapSkip के लौटने के बाद, token को वहीं रखें जहाँ Google की script उसे रखती, फिर पेज आगे जो भी करता है, उसे ट्रिगर करें।

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

सिर्फ़ पहला नहीं, हर g-recaptcha-response textarea भरें। दो widgets वाले फॉर्म में, या ऐसे फॉर्म में जिसका अपना textarea Google के textarea के बगल में हो, उस नाम के दो फ़ील्ड होते हैं, और PHP जैसा सर्वर सिर्फ़ आख़िरी वाला रखता है। दो फ़ील्ड वाले हमारे टेस्ट पेज पर, सिर्फ़ पहला सेट करने पर फॉर्म ठुकरा दिया गया, जबकि दोनों सेट करने पर स्वीकार हुआ।

data-callback attribute वाला widget उम्मीद करता है कि पेज का callback token के साथ चले, इसलिए script उसे कॉल करती है। हमारे callback वाले पेज पर, callback के बिना फॉर्म सबमिट करने पर उसे ठुकरा दिया गया। जो पेज widget को grecaptcha.render से रेंडर करता है, वह अपना callback इसके बजाय JavaScript में पास करता है, जहाँ script उसे देख नहीं सकती। function का नाम पेज की अपनी script में ढूँढें और उसे भी इसी तरह token के साथ कॉल करें।

वरना script फॉर्म को form.requestSubmit() से सबमिट करती है, जो पेज के अपने submit handlers और HTML5 validation चलाता है, बिल्कुल उसके बटन पर क्लिक की तरह। एक फ़र्क बचता है: बटन का अपना name और value तभी भेजे जाते हैं जब आप बटन पास करें, जैसे form.requestSubmit(button) में। पुराना form.submit() method handlers और validation को छोड़ देता है, इसलिए खाली छोड़ा गया required फ़ील्ड भी सर्वर तक चला जाता है। checkValidity वाली लाइन आधा खाली फॉर्म पोस्ट करने के बजाय इस स्थिति की रिपोर्ट करती है।

token को JSON string literal के रूप में पास किया जाता है, जो JsonSerializer.Serialize से बनता है, इसलिए उसमें मौजूद quotes या backslashes कभी script को तोड़ नहीं सकते। इसे फ़ौरन inject करें। Google के दस्तावेज़ के अनुसार reCAPTCHA token जारी होने के दो मिनट बाद एक्सपायर हो जाता है।

पूरा WinForms उदाहरण

प्रोजेक्ट को WvApp नाम से बनाएँ, ताकि यह फ़ाइल टेम्पलेट के बनाए Form1.Designer.cs के साथ एक ही namespace साझा करे। 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 मिलते ही handler खुद को हटा लेता है। फॉर्म जिस पेज पर पोस्ट होता है, उसके लिए NavigationCompleted फिर से चलता है, और उस लाइन के बिना ऐप नतीजे वाले पेज को भी हल करने की कोशिश करता। बिना widget वाले पेज पर handler जल्दी लौट जाता है और जुड़ा रहता है, इसलिए अगले navigation के बाद वह फिर जाँच करता है। जो widget उसी पेज पर बाद में आता है, उसके लिए आगे दिखाया गया polling लूप चाहिए। invisible widget को Google के anchor iframe के size parameter से पहचाना जाता है। उस iframe के बनने से पहले, data-size attribute या किसी बटन से जुड़ा widget इसका संकेत देता है। फिर उसे invisible ऑप्शन के साथ CapSkip को भेजा जाता है। विंडो का title स्टेटस दिखाता है। किसी tool के लिए इतना काफ़ी है, और चाहें तो इसकी जगह Label यह दिखा सकता है।

विंडो बंद होना शुरू होते ही CancellationTokenSource cancel हो जाता है। हल के बीच में ऐप बंद करें, तो SDK तुरंत TaskCanceledException के साथ polling रोक देता है, जिसे पहला catch निगल जाता है। cancellation token के बिना, किसी secondary फॉर्म पर चल रहा हल फॉर्म के जाने के बाद भी चलता रहता है। जब वह लौटता है, तब CoreWebView2 null होता है, और अगली script कॉल एक NullReferenceException फेंकती है, जिसे WinForms अपने unhandled exception डायलॉग में दिखाता है।

WPF में वही कोड

WPF के तहत WebView2 में reCAPTCHA हल करने के लिए ऊपर वाले चरण ही रखें। WPF कंट्रोल के नीचे वही CoreWebView2 होता है, इसलिए WebView2 और CapSkip की कॉल नहीं बदलतीं। कंट्रोल को MainWindow.xaml में Microsoft.Web.WebView2.Wpf namespace के साथ declare करें और उसका नाम webView रखें, फिर webView field और Controls.Add वाली लाइन हटा दें। बाकी को MainWindow.xaml.cs में ले जाएँ, उसे Loaded event से शुरू करें और Closing event handler में cancellation token को cancel करें। WPF विंडो में कोई Text प्रॉपर्टी नहीं होती, इसलिए स्टेटस उसकी जगह Title में लिखें। WPF के implicit usings में System.IO शामिल नहीं होता, इसलिए Path.Combine को अपनी अलग using लाइन चाहिए। उस फ़ाइल से टेम्पलेट का System.Windows.Shapes import हटा दें, क्योंकि उसमें अपनी एक Path class है और नाम ambiguous हो जाता है।

iframes के अंदर widgets, और देर से लोड होने वाले widgets

ExecuteScriptAsync सिर्फ़ top document में चलता है। अगर login फॉर्म खुद किसी iframe में है, तो sitekey वाली script वहाँ null लौटाती है। FrameCreated event से पेज के frames इकट्ठा करें, जिसे navigate करने से पहले जोड़ना ज़रूरी है, और हर frame को destroy होते ही सूची से हटा दें। जो frame ख़त्म हो चुका है, उसे कॉल करने पर 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 होता है, इसलिए वही scripts frame के अंदर चलती हैं। पेज URL के रूप में CapSkip को frame का अपना पता भेजें, क्योंकि widget उसी पेज पर है। जब top document कोई sitekey नहीं लौटाता, तब इस method को OnNavigationCompleted से कॉल करें, उन्हीं try और catch blocks के अंदर जो मुख्य हल के लिए हैं। FrameCreated, Google के अपने anchor और challenge iframes के लिए भी चलता है, और लूप उन्हें छोड़ देता है, क्योंकि उनके अंदर script 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 event के बाद widget जोड़ते हैं, जैसे cookie banner के बाद या पहले क्लिक के बाद। तब NavigationCompleted को कुछ नहीं मिलता। एक छोटा polling लूप इसे संभाल लेता है: जिस पेज ने load के दो सेकंड बाद अपना widget जोड़ा, उस पर इसे 2.11 सेकंड बाद key मिल गई।

// 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 को किसी दूसरी मशीन पर चलाना

ऐप और सॉल्वर का एक ही PC पर होना ज़रूरी नहीं। कनेक्शन सेटिंग्स में 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 key validation चालू करें, क्लाइंट में भी वही key रखें। Server मोड शुरू होते ही CapSkip अपना पोर्ट हर पते के लिए खोल देता है, इसलिए उसे सिर्फ़ अपेक्षित मशीनों तक सीमित रखने के लिए बाकी पतों के लिए Windows Firewall में एक block नियम जोड़ें। उसी मशीन पर localhost के बजाय 127.0.0.1 लिखें। हमारे रन में localhost ने पहले IPv6 पता आज़माया और हर नए कनेक्शन पर उसके फेल होने के लिए करीब दो सेकंड इंतज़ार किया। जो सॉल्वर चल नहीं रहा था, उसकी रिपोर्ट आने में दो के बजाय चार सेकंड लगे।

ऐप में CapSkip की errors संभालना

SDK चार exceptions उठाता है, और सभी CapSkipError से निकले हैं। हर नाकामी के लिए अलग संदेश दिखाने के लिए, OnNavigationCompleted के try block को इससे बदलें। cancellation वाला फ़िल्टर सबसे पहले रखें: जब टास्क भेजा जा रहा हो, उसी समय विंडो बंद हो जाए तो NetworkException आता है, और बाद वाला फ़िल्टर उसे 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; }

एक नाम पर ध्यान देना होगा। WinForms प्रोजेक्ट के implicit usings के साथ, TimeoutException का सादा catch CS0104 के साथ कंपाइल नहीं होता, क्योंकि System.TimeoutException और CapSkip.TimeoutException दोनों scope में हैं। ऊपर की तरह, पूरा CapSkip नाम लिखें। RecaptchaAsync, SDK के recaptchaTimeout के बाद हार मान लेता है, जो डिफ़ॉल्ट रूप से 300 सेकंड है और जिसे आप CapSkipClient constructor में सेट करते हैं। धीमा हल आमतौर पर उससे पहले CapSkip के अंदर ही फेल हो जाता है, और तब ApiException के रूप में आता है।

WebView2 में reCAPTCHA हल करते समय आम errors

आप जो देखते हैंकारणफिक्स
InvalidOperationException: CoreWebView2 can only be accessed from the UI threadहल के बाद का कोड ConfigureAwait(false) या Task.Run की वजह से thread pool के किसी थ्रेड पर फिर से शुरू हुआevent handler में सादा await इस्तेमाल करें
sitekey के चारों ओर quotes हैं और हर हल फेल होता हैExecuteScriptAsync का कच्चा JSON सीधे CapSkip को चला गयानतीजे को System.Text.Json से deserialise करें
दिखने वाले widget वाले पेज पर sitekey null हैपेज widget को खुद रेंडर करता है, या उसे देर से जोड़ता है, या widget किसी iframe में हैanchor iframe पढ़ें, key के लिए poll करें, या script को frame में चलाएँ
ऐप शुरू होते ही Access is deniedWebView2 ने अपना डेटा फ़ोल्डर ऐसी exe के बगल में बनाने की कोशिश की, जिसकी जगह पर यूज़र लिख नहीं सकताlocal app data के अंदर किसी फ़ोल्डर के साथ environment बनाएँ
ArgumentException: पहले ही किसी दूसरे environment के साथ initialize हो चुका हैआपका environment बनने से पहले Source सेट हो गयापहले अपने environment के साथ initialise करें, फिर navigate करें
पोस्ट के बाद पेज login फॉर्म फिर से लोड करता हैसिर्फ़ एक response फ़ील्ड भरा गया, callback कभी नहीं चला, या token एक्सपायर हो गयाहर फ़ील्ड भरें, callback कॉल करें, और हल के ठीक बाद inject करें
ऐप login के बाद खुलने वाले पेज को भी हल करने की कोशिश करता हैNavigationCompleted हर navigation पर चलता हैsitekey मिलते ही handler हटा दें
NetworkException: target machine ने कनेक्शन सक्रिय रूप से ठुकरा दियाCapSkip चल नहीं रहा, या host या port गलत हैCapSkip शुरू करें और पते को उसकी कनेक्शन सेटिंग्स से मिलाकर जाँचें
विंडो बंद करने के बाद NullReferenceException डायलॉगफॉर्म बंद होने के बाद कोई हल पूरा हुआCancellationToken पास करें और फॉर्म बंद होने पर उसे cancel करें

FAQ

क्या यह reCAPTCHA v3 और Enterprise के लिए काम करता है?

हाँ, किसी अलग method से नहीं, बल्कि ऑप्शंस से। Enterprise पेज api.js की जगह enterprise.js लोड करता है। RecaptchaAsync के ऑप्शंस में enterprise को 1 पर सेट करके जोड़ें, और इस गाइड का बाकी हिस्सा वैसा ही रहता है। v3 पेज अलग है: उसका sitekey api.js के render parameter में होता है, और पेज token को textarea से नहीं, अपनी script से भेजता है। version को v3 पर सेट करके पास करें, साथ में पेज का action भी, और invisible को छोड़ दें, जिसे SDK v3 के लिए मना कर देता है। C# में reCAPTCHA v2 हल करने की गाइड इन ऑप्शंस को और विस्तार से कवर करती है।

script error को असली null से कैसे अलग पहचानें?

ExecuteScriptWithResultAsync इस्तेमाल करें, जो मौजूदा WebView2 पैकेज में शामिल है। यह बताता है कि script सफल हुई या नहीं, और न होने पर आपको exception का संदेश देता है, जबकि ExecuteScriptAsync दोनों स्थितियों में null लौटाता है। ऊपर के सरल reads के लिए ExecuteScriptAsync ही रखें, जहाँ गायब widget और टूटे selector का मतलब एक ही है: हल करने को कुछ नहीं।

क्या यह मोबाइल WebView में कैप्चा हल करने जैसा ही है?

नहीं। WebView2, Windows डेस्कटॉप ऐप के अंदर Edge engine है, और आप उसे सीधे C# से कंट्रोल करते हैं। Android या iOS ऐप के अंदर का WebView बाहर से चलाया जाता है, आमतौर पर Appium के ज़रिए, जिसके अपने session timeouts होते हैं। वह मामला यहाँ कवर किया गया है: Appium में कैप्चा हल करने की गाइड.

क्या डेस्कटॉप ऐप CapSkip से अलग किसी मशीन पर चल सकता है?

हाँ। कनेक्शन सेटिंग्स में CapSkip को Server मोड पर कर दें, ताकि वह loopback के बजाय किसी नेटवर्क पते पर सुने, और वही पता CapSkipClient के host के रूप में पास करें। तब ऐप उसे उसी HTTP API के ज़रिए किसी भी ऐसी मशीन से कॉल करता है जो पोर्ट तक पहुँच सकती है। जिन पतों को उस तक नहीं पहुँचना चाहिए, उनके लिए Windows Firewall में एक block नियम जोड़ें। सॉल्वर अब भी आपके अपने हार्डवेयर पर चलता है, और कोई भी हल किसी third-party सर्विस से होकर नहीं जाता।

संक्षेप में

WebView2 में reCAPTCHA हल करने के लिए, NavigationCompleted के बाद sitekey पढ़ें और ExecuteScriptAsync से लौटे JSON को deserialise करें। RecaptchaAsync को ConfigureAwait(false) के बिना await करें, ताकि CoreWebView2 की अगली कॉल UI थ्रेड पर हो। हर response फ़ील्ड भरें, फिर पेज का callback कॉल करें या form.requestSubmit() इस्तेमाल करें। WebView2 को ऐसा डेटा फ़ोल्डर दें जिसमें वह लिख सके, विंडो बंद होने पर हल cancel करें, और जब ऐप किसी दूसरी मशीन पर जाए, तो CapSkip को Server मोड पर कर दें।

डेस्कटॉप ऐप और उसका सॉल्वर एक ही Windows PC पर हों, इससे सरल सेटअप कोई नहीं। यही वह काम है जिसके लिए एक लोकल captcha सॉल्वर बना है: token आपकी अपनी मशीन से आता है, और कोई भी हल उससे बाहर नहीं जाता।