Tài Liệu Tham Khảo

Tiện ích mở rộng VS Code cho phát triển API CaptchaAI

Khoảng 300 dòng JavaScript trong extension.js cộng một package.json khai báo bốn lệnh: chừng đó là đủ để giải reCAPTCHA v2 hay Cloudflare Turnstile ngay trong VS Code, không cần mở terminal. Tiện ích hiện số dư, dò sitekey trong file đang mở và đưa token về đúng con trỏ.

Bản 1.0 gồm những gì

Tính năng Nó làm gì
Thanh trạng thái Số dư CaptchaAI, làm mới mỗi 5 phút
Lệnh solve Gửi task giải từ VS Code, có nút hủy
Dò sitekey Quét file đang mở, trích ra sitekey
Snippet Khung gọi API cho reCAPTCHA, Turnstile, GeeTest v3
Tra mã lỗi Di chuột để xem mô tả

Chốt phạm vi trước khi viết code

  • Chốt trước danh sách activation event, lệnh và mục cấu hình.
  • Tách chỗ đọc API key khỏi phần xử lý lệnh, để sau này chuyển sang SecretStorage cho dễ.

Cấu trúc dự án và package.json

Một file code, hai file snippet, không cần bundler.

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

Bốn lệnh, ba mục cấu hình (captchaai.apiKey, captchaai.showBalance, captchaai.pollInterval) và hai file snippet. onStartupFinished là activation event nhẹ nhất dùng được ở đây.

{
  "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"
      }
    ]
  }
}

Viết extension.js

extension.js gồm ba khối. Khối số dư gọi res.php với action=getbalance mỗi 5 phút. Khối solve hỏi sitekey và pageurl, gửi task tới in.php rồi polling res.php tới khi có token — withProgress cho phép hủy giữa chừng. Khối dò sitekey chạy bốn regex trên file đang mở.

Chỗ dễ sai nhất: reCAPTCHA truyền sitekey qua googlekey, còn Turnstile dùng đúng tên sitekey — bỏ nhánh if (method === "userrecaptcha") là API trả lỗi ngay lần chạy đầu.

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

Snippet gõ tắt cho Python và JavaScript

cai-recaptcha-v2 trong file Python là có luôn đoạn gửi task cộng vòng polling, kèm điểm dừng để tab qua từng chỗ cần điền. Ba prefix: cai-recaptcha-v2, cai-turnstile, cai-balance.

Snippet cho 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"
  }
}

Snippet cho JavaScript

Cùng luồng đó, viết bằng fetchasync/await.

{
  "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"
  }
}

Giữ API key an toàn

Settings của VS Code là file JSON trên ổ đĩa, không mã hóa:

  • Điền key ở User Settings, không phải Workspace Settings — .vscode/settings.json rất hay bị commit nhầm.
  • Thêm .vscode/ vào .gitignore ở repo dùng chung với khách hàng.

Chắc hơn thì đọc key qua API SecretStorage, tức keychain hệ điều hành. Phía server, xem cách bảo mật API key và whitelist IP.

Nhóm QA gia công ở TP.HCM dùng thế nào

Nhóm QA của một công ty gia công ở TP.HCM kiểm thử form đăng ký có Cloudflare Turnstile trên staging của khách. Trước đây mỗi tester chạy script riêng và không ai biết hạn mức còn lại cho tới lúc task lỗi; giờ token vào thẳng clipboard, số dư nằm ngay trên thanh trạng thái.

CaptchaAI tính tiền theo thread — mỗi thread là một CAPTCHA giải đồng thời — chứ không theo lượt giải. Nhu cầu dev/QA kiểu này thường vừa gói BASIC ($15/tháng, 5 thread); CI chạy song song nhiều job mới cần STANDARD ($30/tháng, 15 thread).

Xử lý lỗi thường gặp

Vấn đề Nguyên nhân Cách xử lý
Thanh trạng thái "Offline" Không gọi được API Kiểm tra mạng/firewall tới ocr.captchaai.com
Báo "Set your API key" Chưa cấu hình key Settings → "CaptchaAI" → điền API key
Lệnh solve timeout Task lỗi hoặc mạng chậm Tăng captchaai.pollInterval, soát lại sitekey
Dò sitekey không ra gì File không khớp regex Xác nhận có data-sitekey hoặc googlekey

Câu hỏi thường gặp

Tiện ích giải được những loại CAPTCHA nào?

Đúng các loại CaptchaAI hỗ trợ: reCAPTCHA v2/v3 (kể cả Enterprise), Cloudflare Turnstile, Cloudflare Challenge, GeeTest v3, CAPTCHA ảnh/OCR, grid image, BLS; CaptchaFox, Friendly Captcha và Lemin ở mức beta. hCaptcha và FunCaptcha không được hỗ trợ, GeeTest v4 mới sắp ra mắt.

Nên dùng polling hay callback?

Polling. Callback cần URL công khai để CaptchaAI gửi kết quả về, trong khi máy dev thường nằm sau NAT hoặc VPN công ty; callback hợp với service chạy trên server.

Nhóm dev nhỏ tốn khoảng bao nhiêu mỗi tháng?

Gói thấp nhất là BASIC ($15/tháng, 5 thread) — 5 CAPTCHA giải đồng thời, không giới hạn số lượt. Nhóm 3–5 người vừa code vừa test gần như không chạm trần này.

Bài viết liên quan

Bước tiếp theo

Bấm F5, lấy API key, rồi chạy "CaptchaAI: Solve Turnstile" trên form staging bạn quản lý. Xong thì đóng gói .vsix cho cả nhóm.

Đọc thêm:

Os comentários estão desativados para este artigo.