Reference

Créer une extension VS Code pour l'API CaptchaAI

Une extension VS Code maison supprime le va-et-vient entre l'éditeur et le terminal pendant une intégration CaptchaAI : le solde apparaît dans la barre d'état, une commande de la palette envoie un défi CAPTCHA à résoudre puis recopie le token dans le presse-papiers, et les sitekeys du fichier ouvert se repèrent en une seule action. Le projet tient en quatre fichiers — package.json, extension.js et deux fichiers de snippets — et s'installe localement, sans passer par le Marketplace.

Le code s'appuie sur l'API HTTP de CaptchaAI (in.php pour l'envoi, res.php pour interroger le résultat) et couvre les types pris en charge : reCAPTCHA v2 et v3, Cloudflare Turnstile, GeeTest v3 et les CAPTCHA image/OCR.

Extension maison ou simple script : comment trancher

Écrire une extension coûte une demi-journée. Voici quand l'investissement se rentabilise, et quand un script en ligne de commande suffit.

Situation Extension VS Code utile Script ou outil interne suffisant
Les développeurs testent souvent les résolutions depuis l'éditeur Oui
Vous voulez détecter des sitekeys et insérer des snippets dans le code ouvert Oui
Vous faites seulement quelques appels API ponctuels Un script CLI léger suffit souvent
Le workflow principal vit en CI, en backend ou dans le navigateur L'extension apporte alors peu de valeur directe

Les cinq fonctions couvertes par le projet

Fonction Rôle dans l'éditeur
Solde en barre d'état Affiche le solde CaptchaAI et le rafraîchit toutes les 5 minutes
Commande de résolution Envoie un défi CAPTCHA depuis la palette de commandes
Détection de sitekey Repère data-sitekey, googlekey et sitekey dans le fichier ouvert
Snippets de code Insère les appels API prêts à l'emploi pour reCAPTCHA v2, Turnstile et GeeTest v3
Aide sur les codes d'erreur Affiche la description du code au survol

Arborescence du projet

Quatre fichiers suffisent. Gardez les snippets dans des fichiers JSON séparés : VS Code les recharge sans redémarrage, contrairement au code de l'extension lui-même.

captchaai-vscode/
├── package.json
├── src/
│   └── extension.js
├── snippets/
│   ├── python.json
│   └── javascript.json
└── README.md

package.json : déclarer les commandes et les paramètres

Le manifeste déclare quatre commandes, trois paramètres (apiKey, showBalance, pollInterval) et les deux fichiers de snippets. Gardez activationEvents sur onStartupFinished : le solde s'affiche dès l'ouverture de l'éditeur.

{
  "name": "captchaai-dev-tools",
  "displayName": "CaptchaAI Dev Tools",
  "description": "CaptchaAI API development tools for VS Code",
  "version": "1.0.0",
  "engines": { "vscode": "^1.80.0" },
  "categories": ["Snippets", "Other"],
  "activationEvents": ["onStartupFinished"],
  "main": "./src/extension.js",
  "contributes": {
    "commands": [
      {
        "command": "captchaai.checkBalance",
        "title": "CaptchaAI: Check Balance"
      },
      {
        "command": "captchaai.solveRecaptcha",
        "title": "CaptchaAI: Solve reCAPTCHA v2"
      },
      {
        "command": "captchaai.solveTurnstile",
        "title": "CaptchaAI: Solve Turnstile"
      },
      {
        "command": "captchaai.detectSitekey",
        "title": "CaptchaAI: Detect Sitekey in File"
      }
    ],
    "configuration": {
      "title": "CaptchaAI",
      "properties": {
        "captchaai.apiKey": {
          "type": "string",
          "default": "",
          "description": "Your CaptchaAI API key"
        },
        "captchaai.showBalance": {
          "type": "boolean",
          "default": true,
          "description": "Show balance in status bar"
        },
        "captchaai.pollInterval": {
          "type": "number",
          "default": 5,
          "description": "Poll interval in seconds"
        }
      }
    },
    "snippets": [
      {
        "language": "python",
        "path": "./snippets/python.json"
      },
      {
        "language": "javascript",
        "path": "./snippets/javascript.json"
      }
    ]
  }
}

Le cœur de l'extension : solde, résolution, détection

extension.js regroupe trois blocs indépendants :

  • updateBalance() interroge res.php avec action=getbalance et écrit le solde dans la barre d'état.
  • solveCaptcha() demande le sitekey et l'URL, envoie la tâche à in.php, puis interroge le résultat toutes les pollInterval secondes ; la boucle s'arrête après 60 itérations.
  • detectSitekey() applique quatre expressions régulières au fichier actif.
// src/extension.js
const vscode = require("vscode");

const API_BASE = "https://ocr.captchaai.com";

function getApiKey() {
  const config = vscode.workspace.getConfiguration("captchaai");
  const key = config.get("apiKey");
  if (!key) {
    vscode.window.showErrorMessage(
      "CaptchaAI: Set your API key in Settings → CaptchaAI"
    );
    return null;
  }
  return key;
}

// --- Balance Status Bar ---

let balanceStatusBar;
let balanceInterval;

async function updateBalance() {
  const key = getApiKey();
  if (!key) return;

  try {
    const url = new URL(`${API_BASE}/res.php`);
    url.searchParams.set("key", key);
    url.searchParams.set("action", "getbalance");
    url.searchParams.set("json", "1");

    const response = await fetch(url);
    const result = await response.json();

    if (result.status === 1) {
      const balance = parseFloat(result.request).toFixed(2);
      balanceStatusBar.text = `$(credit-card) CaptchaAI: $${balance}`;
      balanceStatusBar.tooltip = `CaptchaAI Balance: $${balance}`;
    } else {
      balanceStatusBar.text = "$(warning) CaptchaAI: Error";
    }
  } catch {
    balanceStatusBar.text = "$(warning) CaptchaAI: Offline";
  }
}

// --- Solve Command ---

async function solveCaptcha(method, extraFields) {
  const key = getApiKey();
  if (!key) return;

  const sitekey = await vscode.window.showInputBox({
    prompt: "Enter the CAPTCHA sitekey",
    placeHolder: "6LeIxAcTAAAAAJcZ...",
  });
  if (!sitekey) return;

  const pageurl = await vscode.window.showInputBox({
    prompt: "Enter the page URL",
    placeHolder: "https://example.com",
  });
  if (!pageurl) return;

  const params = {
    key,
    method,
    pageurl,
    json: 1,
    ...extraFields,
  };

  if (method === "userrecaptcha") {
    params.googlekey = sitekey;
  } else {
    params.sitekey = sitekey;
  }

  // Submit
  vscode.window.withProgress(
    {
      location: vscode.ProgressLocation.Notification,
      title: "CaptchaAI: Solving...",
      cancellable: true,
    },
    async (progress, cancellation) => {
      try {
        const submitResponse = await fetch(`${API_BASE}/in.php`, {
          method: "POST",
          body: new URLSearchParams(params),
        });
        const submitResult = await submitResponse.json();

        if (submitResult.status !== 1) {
          vscode.window.showErrorMessage(
            `CaptchaAI: ${submitResult.request || "Submit failed"}`
          );
          return;
        }

        const taskId = submitResult.request;
        progress.report({ message: `Task ${taskId} submitted` });

        // Poll
        const config = vscode.workspace.getConfiguration("captchaai");
        const interval = config.get("pollInterval") * 1000;

        for (let i = 0; i < 60; i++) {
          if (cancellation.isCancellationRequested) return;

          await new Promise((r) => setTimeout(r, interval));

          const pollUrl = new URL(`${API_BASE}/res.php`);
          pollUrl.searchParams.set("key", key);
          pollUrl.searchParams.set("action", "get");
          pollUrl.searchParams.set("id", taskId);
          pollUrl.searchParams.set("json", "1");

          const pollResponse = await fetch(pollUrl);
          const pollResult = await pollResponse.json();

          if (pollResult.request === "CAPCHA_NOT_READY") {
            progress.report({ message: `Waiting... (${(i + 1) * (interval / 1000)}s)` });
            continue;
          }

          if (pollResult.status === 1) {
            const token = pollResult.request;

            // Copy to clipboard
            await vscode.env.clipboard.writeText(token);
            vscode.window.showInformationMessage(
              `CaptchaAI: Solved! Token copied to clipboard (${token.length} chars)`
            );

            // Also insert at cursor if editor is active
            const editor = vscode.window.activeTextEditor;
            if (editor) {
              const action = await vscode.window.showQuickPick(
                ["Copy only", "Insert at cursor"],
                { placeHolder: "Token copied. Insert into editor?" }
              );
              if (action === "Insert at cursor") {
                editor.edit((editBuilder) => {
                  editBuilder.insert(editor.selection.active, token);
                });
              }
            }
            return;
          }

          vscode.window.showErrorMessage(
            `CaptchaAI: ${pollResult.request || "Solve failed"}`
          );
          return;
        }

        vscode.window.showErrorMessage("CaptchaAI: Solve timed out");
      } catch (err) {
        vscode.window.showErrorMessage(`CaptchaAI: ${err.message}`);
      }
    }
  );
}

// --- Sitekey Detection ---

async function detectSitekey() {
  const editor = vscode.window.activeTextEditor;
  if (!editor) {
    vscode.window.showWarningMessage("No active editor");
    return;
  }

  const text = editor.document.getText();
  const patterns = [
    { regex: /data-sitekey=["']([^"']+)["']/g, type: "HTML data-sitekey" },
    { regex: /googlekey['":\s]+["']([a-zA-Z0-9_-]{40})["']/g, type: "API googlekey" },
    { regex: /sitekey['":\s]+["']([a-zA-Z0-9_-]{20,})["']/g, type: "sitekey parameter" },
    { regex: /render=([a-zA-Z0-9_-]{40})/g, type: "reCAPTCHA render" },
  ];

  const found = [];
  for (const { regex, type } of patterns) {
    let match;
    while ((match = regex.exec(text)) !== null) {
      found.push({ key: match[1], type, position: match.index });
    }
  }

  if (found.length === 0) {
    vscode.window.showInformationMessage("No sitekeys found in current file");
    return;
  }

  const items = found.map((f) => ({
    label: f.key,
    description: f.type,
    detail: `Position: ${f.position}`,
    key: f.key,
  }));

  const selected = await vscode.window.showQuickPick(items, {
    placeHolder: `Found ${found.length} sitekey(s) — select to copy`,
  });

  if (selected) {
    await vscode.env.clipboard.writeText(selected.key);
    vscode.window.showInformationMessage(`Sitekey copied: ${selected.key}`);
  }
}

// --- Activation ---

function activate(context) {
  // Balance status bar
  const config = vscode.workspace.getConfiguration("captchaai");

  if (config.get("showBalance")) {
    balanceStatusBar = vscode.window.createStatusBarItem(
      vscode.StatusBarAlignment.Right,
      100
    );
    balanceStatusBar.command = "captchaai.checkBalance";
    balanceStatusBar.text = "$(credit-card) CaptchaAI";
    balanceStatusBar.show();

    updateBalance();
    balanceInterval = setInterval(updateBalance, 300000); // Every 5 minutes

    context.subscriptions.push(balanceStatusBar);
  }

  // Register commands
  context.subscriptions.push(
    vscode.commands.registerCommand("captchaai.checkBalance", async () => {
      await updateBalance();
      vscode.window.showInformationMessage(balanceStatusBar.tooltip);
    }),

    vscode.commands.registerCommand("captchaai.solveRecaptcha", () => {
      solveCaptcha("userrecaptcha", {});
    }),

    vscode.commands.registerCommand("captchaai.solveTurnstile", () => {
      solveCaptcha("turnstile", {});
    }),

    vscode.commands.registerCommand("captchaai.detectSitekey", detectSitekey)
  );
}

function deactivate() {
  if (balanceInterval) clearInterval(balanceInterval);
}

module.exports = { activate, deactivate };

Snippets Python et JavaScript

Les snippets évitent de recopier la structure de l'appel API à chaque script. Tapez le préfixe (cai-recaptcha-v2, cai-turnstile, cai-balance), puis passez d'un champ à l'autre avec Tab.

Snippets Python : reCAPTCHA v2, Turnstile et solde

{
  "CaptchaAI reCAPTCHA v2": {
    "prefix": "cai-recaptcha-v2",
    "body": [
      "import requests",
      "",
      "# Submit reCAPTCHA v2 task",
      "response = requests.post(",
      "    \"https://ocr.captchaai.com/in.php\",",
      "    data={",
      "        \"key\": \"${1:YOUR_API_KEY}\",",
      "        \"method\": \"userrecaptcha\",",
      "        \"googlekey\": \"${2:SITE_KEY}\",",
      "        \"pageurl\": \"${3:https://example.com}\",",
      "        \"json\": 1,",
      "    },",
      ")",
      "task_id = response.json()[\"request\"]",
      "",
      "# Poll for result",
      "import time",
      "while True:",
      "    time.sleep(5)",
      "    result = requests.get(",
      "        \"https://ocr.captchaai.com/res.php\",",
      "        params={\"key\": \"${1}\", \"action\": \"get\", \"id\": task_id, \"json\": 1},",
      "    ).json()",
      "    if result[\"request\"] != \"CAPCHA_NOT_READY\":",
      "        token = result[\"request\"]",
      "        break"
    ],
    "description": "CaptchaAI reCAPTCHA v2 solve"
  },
  "CaptchaAI Turnstile": {
    "prefix": "cai-turnstile",
    "body": [
      "import requests",
      "",
      "response = requests.post(",
      "    \"https://ocr.captchaai.com/in.php\",",
      "    data={",
      "        \"key\": \"${1:YOUR_API_KEY}\",",
      "        \"method\": \"turnstile\",",
      "        \"sitekey\": \"${2:SITE_KEY}\",",
      "        \"pageurl\": \"${3:https://example.com}\",",
      "        \"json\": 1,",
      "    },",
      ")",
      "task_id = response.json()[\"request\"]"
    ],
    "description": "CaptchaAI Turnstile solve"
  },
  "CaptchaAI Balance Check": {
    "prefix": "cai-balance",
    "body": [
      "import requests",
      "",
      "balance = requests.get(",
      "    \"https://ocr.captchaai.com/res.php\",",
      "    params={\"key\": \"${1:YOUR_API_KEY}\", \"action\": \"getbalance\", \"json\": 1},",
      ").json()",
      "print(f\"Balance: \\${balance['request']}\")"
    ],
    "description": "CaptchaAI balance check"
  }
}

Snippet JavaScript : reCAPTCHA v2 avec fetch

{
  "CaptchaAI reCAPTCHA v2": {
    "prefix": "cai-recaptcha-v2",
    "body": [
      "const response = await fetch('https://ocr.captchaai.com/in.php', {",
      "  method: 'POST',",
      "  body: new URLSearchParams({",
      "    key: '${1:YOUR_API_KEY}',",
      "    method: 'userrecaptcha',",
      "    googlekey: '${2:SITE_KEY}',",
      "    pageurl: '${3:https://example.com}',",
      "    json: 1,",
      "  }),",
      "});",
      "const { request: taskId } = await response.json();",
      "",
      "// Poll for result",
      "let token;",
      "while (true) {",
      "  await new Promise(r => setTimeout(r, 5000));",
      "  const url = new URL('https://ocr.captchaai.com/res.php');",
      "  url.searchParams.set('key', '${1}');",
      "  url.searchParams.set('action', 'get');",
      "  url.searchParams.set('id', taskId);",
      "  url.searchParams.set('json', '1');",
      "  const result = await (await fetch(url)).json();",
      "  if (result.request !== 'CAPCHA_NOT_READY') {",
      "    token = result.request;",
      "    break;",
      "  }",
      "}"
    ],
    "description": "CaptchaAI reCAPTCHA v2 solve"
  }
}

Installer l'extension localement

Aucune publication sur le Marketplace n'est nécessaire pour un usage interne :

  1. Empaquetez le dossier avec vsce package.
  2. Installez le fichier obtenu : code --install-extension captchaai-dev-tools-1.0.0.vsix.
  3. Pour une équipe, déposez le .vsix sur un partage interne.

Un cas d'usage concret : recette d'un formulaire d'inscription

Une équipe QA basée à Lille recette un tunnel d'inscription protégé par Turnstile, en préproduction chez OVHcloud. Chaque test manuel imposait d'ouvrir un terminal, de lancer un script et d'attendre le token. Avec la commande « CaptchaAI: Solve Turnstile », le testeur saisit le sitekey, récupère le token dans le presse-papiers et l'injecte dans le champ cf-turnstile-response sans quitter l'éditeur.

Deux précautions ici : ne testez que des environnements dont vous avez la responsabilité, et côté RGPD, ne journalisez ni URL contenant un identifiant de session ni token complet.

Ce que coûtent les résolutions lancées depuis l'éditeur

Les appels déclenchés depuis l'éditeur consomment le même quota qu'un script ou un worker. La facturation CaptchaAI repose sur des abonnements mensuels avec un nombre de threads simultanés, pas sur un crédit à l'unité.

Le plan BASIC ($15/mois, 5 threads) suffit à une phase d'intégration, où les résolutions partent une par une ; STANDARD ($30/mois, 15 threads) sert aux exécutions parallèles. Facturation en dollars US.

Dépannage

Problème Cause probable Correctif
Le solde affiche « Offline » L'API n'est pas joignable depuis le poste Vérifiez le réseau et le pare-feu ; ocr.captchaai.com doit être accessible
Message « Set your API key » Le paramètre captchaai.apiKey est vide Paramètres → rechercher « CaptchaAI » → coller la clé API
Aucun snippet proposé Le mode de langue du fichier ne correspond pas Vérifiez en bas à droite que le fichier est bien en mode Python ou JavaScript
La résolution expire après 60 itérations Réseau lent ou paramètres invalides Relisez le sitekey et l'URL, puis ajustez pollInterval
La détection ne trouve aucun sitekey Aucun motif reconnu dans le fichier Le fichier doit contenir data-sitekey, googlekey ou sitekey

FAQ

Comment éviter que ma clé API se retrouve dans un dépôt Git ?

Ne la mettez pas dans les paramètres du workspace, qui sont versionnés avec le projet. Préférez les paramètres utilisateur de VS Code, ou mieux, l'API SecretStorage, qui place la clé dans le trousseau du système d'exploitation.

L'extension peut-elle résoudre hCaptcha ou FunCaptcha ?

Non — ces deux types ne sont pas pris en charge par CaptchaAI, et GeeTest v4 est annoncé comme à venir. Vous pouvez câbler reCAPTCHA v2 et v3, Cloudflare Turnstile et Challenge, GeeTest v3, les CAPTCHA image/OCR et les grilles d'images ; CaptchaFox (bêta), Friendly Captcha (bêta) et Lemin (bêta) restent en bêta.

Quel intervalle de polling faut-il régler ?

La valeur par défaut de 5 secondes convient à un usage manuel. Descendre à 1 ou 2 secondes multiplie les requêtes vers res.php sans accélérer la résolution ; 10 secondes ajoute une latence inutile.

Faut-il redémarrer VS Code après chaque modification du code ?

Pour extension.js, oui : lancez « Developer: Reload Window » ou la session de débogage (F5). Les fichiers de snippets, eux, sont relus à chaud.

Plusieurs développeurs peuvent-ils partager la même clé API ?

Oui, mais les résolutions simultanées se partagent les threads du plan : avec BASIC, cinq résolutions tournent en parallèle et la sixième attend. Au-delà, passez à STANDARD.

Articles connexes

Prochaines étapes

Reprenez les quatre fichiers, remplacez YOUR_API_KEY par votre clé et lancez la commande de résolution sur un environnement de test : récupérez votre clé API.

Guides associés :

Les commentaires sont désactivés pour cet article.