Четыре вещи, ради которых стоит написать собственное расширение редактора под CaptchaAI: баланс виден в строке состояния, sitekey извлекается из открытого файла одной командой, тестовое решение CAPTCHA запускается прямо из редактора, а заготовки запросов вставляются по префиксу. Ниже — рабочий каркас на JavaScript: манифест, реализация команд и два файла сниппетов.
Расширение не заменяет интеграцию в продакшене — оно убирает переключение контекста на этапе разработки. При отладке формы с reCAPTCHA v2 ключ подставляется из настроек, задача уходит на in.php, а токен возвращается в буфер обмена.
Что умеет расширение
| Компонент | Что делает |
|---|---|
| Баланс в строке состояния | Держит текущий баланс CaptchaAI на виду и обновляет его раз в пять минут |
| Команда решения | Отправляет задачу CAPTCHA из редактора и опрашивает результат |
| Поиск sitekey | Находит и извлекает ключи сайта из открытого файла |
| Сниппеты | Вставляют заготовку вызова API для reCAPTCHA v2, Turnstile и GeeTest v3 |
| Подсказка по ошибкам | Показывает расшифровку кода ошибки при наведении курсора |
Список намеренно короткий: расширение выигрывает не широтой возможностей, а тем, что каждая операция занимает одно нажатие вместо трёх переходов между окнами.
Дерево файлов
Каркас минимальный: манифест, один модуль реализации и два JSON-файла со сниппетами.
captchaai-vscode/
├── package.json
├── src/
│ └── extension.js
├── snippets/
│ ├── python.json
│ └── javascript.json
└── README.md
Такой набор уже упаковывается в .vsix и ставится локально. Разносить логику по модулям есть смысл позже, когда добавятся типы помимо reCAPTCHA v2 и Turnstile.
Манифест: package.json
Манифест объявляет четыре команды, три настройки и привязку сниппетов к языкам. Обратите внимание на activationEvents: onStartupFinished поднимает расширение сразу после запуска редактора, чтобы строка состояния не появлялась с задержкой.
{
"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"
}
]
}
}
Параметр captchaai.pollInterval вынесен в настройки не случайно. По умолчанию это 5 секунд — на стабильном канале достаточно. Но если вы работаете из Алматы или Минска через нагруженный VPN, 7–8 секунд заметно снижают число холостых запросов к res.php.
Минимальный контракт манифеста
Прежде чем расширять package.json, зафиксируйте три вещи — это экономит время при публикации:
- Одно событие активации, одна команда и один блок настроек — минимум, с которым расширение уже полезно.
- API-ключ живёт в конфигурации, а не в коде команды: упаковка и управление секретами остаются разделёнными.
- Опишите в README наименьшую поверхность
package.json, при которой расширение надёжно тестируется локально.
Реализация: src/extension.js
Модуль делится на четыре части: чтение ключа, строка состояния с балансом, команда решения с опросом результата и поиск sitekey регулярными выражениями. Базовый адрес — https://ocr.captchaai.com, эндпоинты in.php и res.php те же, что и в любой другой интеграции CaptchaAI.
// 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 };
Три места здесь стоит отметить отдельно.
Ветвление по имени параметра. Для метода userrecaptcha ключ сайта передаётся как googlekey, для остальных — как sitekey. Это самая частая причина ошибки ERROR_WRONG_GOOGLEKEY.
Опрос вместо ожидания. Цикл делает до 60 итераций с настраиваемой паузой и обрабатывает CAPCHA_NOT_READY — это не ошибка, а нормальный промежуточный ответ. Опечатка в самом слове (CAPCHA, а не CAPTCHA) — часть протокола, исправлять её нельзя.
Отмена операции. Флаг cancellable: true и проверка cancellation.isCancellationRequested прерывают долгий опрос без перезапуска редактора.
Сниппеты: заготовки запросов
Сниппеты — самая недооценённая часть расширения. Им не нужны ни сеть, ни ключ, а работают они там, где теряется время: при наборе очередного requests.post с шестью полями.
Сниппеты для Python
{
"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"
}
}
Сниппеты для JavaScript
{
"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"
}
}
Префиксы cai-recaptcha-v2, cai-turnstile и cai-balance меняются под свою привычку — они объявлены в поле prefix каждого сниппета. Добавить заготовку под GeeTest v3 — вопрос копирования блока и замены значения method.
Отдельно о том, чего в сниппетах быть не должно: заготовок под hCaptcha и FunCaptcha — CaptchaAI эти типы не решает. GeeTest v4 заявлен как «скоро» и пока недоступен. CaptchaFox (beta), Friendly Captcha (beta) и Lemin (beta) — в бета-статусе: добавлять их можно, но пометьте это в README, чтобы коллега не принял бету за общедоступный тип.
Поиск неисправностей
| Проблема | Причина | Что делать |
|---|---|---|
| В строке состояния «Offline» | Из VS Code не удаётся достучаться до API | Проверьте сеть и firewall, убедитесь, что ocr.captchaai.com доступен |
| Ошибка «Set your API key» | Ключ не задан в настройках | Настройки → поиск «CaptchaAI» → впишите API-ключ |
| Сниппеты не появляются | Неверный языковой режим файла | Сверьте языковой режим файла с языком сниппета (Python или JavaScript) |
| Решение уходит в тайм-аут | Задача не решена или канал медленный | Увеличьте pollInterval, проверьте sitekey и pageurl |
| Поиск sitekey ничего не находит | В файле нет подходящих шаблонов | Убедитесь, что в файле есть data-sitekey, googlekey или sitekey |
Сценарий из практики: распределённая QA-команда
Типичная ситуация для команд, разнесённых по часовым поясам — Москва, Тбилиси, Алматы. Формы регистрации в staging закрыты reCAPTCHA v2, и каждый ручной smoke-тест упирается в проверку. Раньше инженер держал отдельный скрипт: скопировать sitekey из DevTools, запустить скрипт в терминале, дождаться токена, вернуться в браузер — четыре переключения контекста на одну форму.
С расширением остаётся два шага: CaptchaAI: Detect Sitekey in File по открытому шаблону страницы, затем CaptchaAI: Solve reCAPTCHA v2 — токен уже в буфере обмена. Баланс виден там же, поэтому об исчерпании лимита никто не узнаёт в момент демонстрации заказчику.
Расширение заодно делает наглядным расход потоков. CaptchaAI тарифицируется по числу одновременных потоков, а не по количеству решений: BASIC ($15/мес, 5 потоков) закрывает одного разработчика, STANDARD ($30/мес, 15 потоков) — небольшую QA-команду, ADVANCE ($90/мес, 50 потоков) — регулярные прогоны в CI. Для команд, считающих бюджет в валюте с плавающим курсом, фиксированный месячный платёж в USD предсказуемее оплаты за каждое решение.
Ещё один локальный момент. Если рядом с решением CAPTCHA вы собираете данные из форм, собирайте только те, которые вправе обрабатывать: для читателей в РФ это зона 152-ФЗ «О персональных данных».
Куда двигаться дальше
Каркас выше сознательно оставлен простым. Две доработки окупаются быстрее прочих:
- Подсказка по кодам ошибок. Провайдер наведения для строк вида
ERROR_*снимает половину походов в документацию. - Журнал запросов. Отдельный
OutputChannelс историей задач и временем решения превращает расширение в инструмент диагностики.
Частые вопросы
Нужно ли публиковать расширение в Marketplace, чтобы им пользоваться?
Нет. Соберите пакет через vsce package и поставьте локально: code --install-extension captchaai-dev-tools-1.0.0.vsix. Команде достаточно положить .vsix во внутренний репозиторий артефактов.
Где безопаснее хранить API-ключ CaptchaAI?
В системном хранилище секретов через SecretStorage, а не в settings.json. Настройки редактора лежат на диске открытым текстом и легко уезжают в git вместе с папкой .vscode.
Почему решение зависает на «Waiting…»?
Обычно это не зависание, а нормальный опрос: res.php отвечает CAPCHA_NOT_READY, пока задача в работе. Если цикл доходит до конца 60 итераций, проверьте pageurl и соответствие sitekey странице.
Сколько потоков нужно на всю команду?
Расширение отправляет по одной задаче за раз на разработчика, поэтому потоки расходуются скромно. Пяти потоков тарифа BASIC ($15/мес) хватает одному-двум инженерам. При параллельных прогонах в CI считайте потоки по пиковой одновременности, а не по числу решений за день.
Будет ли расширение работать в Cursor, VS Codium и через Remote SSH?
Да, каркас использует только стабильный API редактора, поэтому .vsix ставится и в форки VS Code. Через Remote SSH расширение выполняется на удалённой машине — доступ к ocr.captchaai.com нужен именно там, а не на локальном ноутбуке.