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()interrogeres.phpavecaction=getbalanceet é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 lespollIntervalsecondes ; 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 :
- Empaquetez le dossier avec
vsce package. - Installez le fichier obtenu :
code --install-extension captchaai-dev-tools-1.0.0.vsix. - Pour une équipe, déposez le
.vsixsur 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
- sécuriser sa clé API et filtrer les IP autorisées
- CaptchaAI comparé à CapMonster Cloud
- webhooks ou polling : quel mode de récupération choisir
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 :