Reference

Расширение VS Code для разработки API CaptchaAI

Четыре вещи, ради которых стоит написать собственное расширение редактора под 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-ФЗ «О персональных данных».

Куда двигаться дальше

Каркас выше сознательно оставлен простым. Две доработки окупаются быстрее прочих:

  1. Подсказка по кодам ошибок. Провайдер наведения для строк вида ERROR_* снимает половину походов в документацию.
  2. Журнал запросов. Отдельный 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 нужен именно там, а не на локальном ноутбуке.


Следующие шаги

Комментарии для этой статьи отключены.