Pola kerjanya selalu sama setiap kali integrasi CAPTCHA rewel: pindah ke browser untuk melihat saldo, buka terminal untuk memanggil res.php dengan curl, lalu kembali ke editor sambil menyalin sitekey dari tab sebelah. Sebuah ekstensi VS Code buatan sendiri memangkas putaran itu. Artikel ini memuat manifes, kode aktivasi, dan berkas snippet supaya saldo CaptchaAI tampil di status bar, satu perintah mengirim task solve, dan sitekey di file yang terbuka ditemukan otomatis. Semuanya berjalan lokal, tanpa perlu menerbitkan apa pun ke Marketplace.
Kapan ekstensi VS Code untuk CaptchaAI layak dibuat
Ekstensi ini bukan keharusan. Kalau integrasi Anda hanya satu skrip Python yang jalan sekali sehari, curl sudah cukup. Nilainya muncul saat pekerjaan berpindah-pindah: kontraktor scraping dengan beberapa klien sebulan, tim price-monitoring dengan puluhan halaman login staging, atau QA engineer yang menguji ulang form Turnstile tiap kali frontend berubah. Di pola kerja itu, waktu hilang bukan karena solve lambat, melainkan karena Anda terus berpindah jendela.
Konteks jaringan juga menentukan. Banyak tim di Indonesia menjalankan worker di ap-southeast-1 (Singapura) atau ap-southeast-3 (Jakarta), sementara pengembangan berlangsung di laptop. Perintah solve dari editor berjalan lewat jaringan laptop Anda, bukan lewat jalur worker produksi — berguna untuk memverifikasi sitekey dan pageurl, tetapi bukan alat ukur latensi produksi.
Lima fitur yang benar-benar dipakai harian
| Fitur | Fungsinya |
|---|---|
| Saldo di status bar | Saldo CaptchaAI terlihat tanpa membuka dashboard |
| Perintah solve | Kirim task CAPTCHA dari Command Palette |
| Deteksi sitekey | Menyisir file terbuka lalu menampilkan sitekey |
| Snippet kode | Boilerplate reCAPTCHA v2, Turnstile, cek saldo |
| Info kode error | Arahkan kursor ke kode error untuk penjelasannya |
Susunan folder ekstensi
Empat berkas inti sudah cukup: package.json mendeklarasikan isi ekstensi, src/extension.js menampung logikanya, dan dua berkas JSON di snippets/ menyimpan potongan kode per bahasa.
captchaai-vscode/
├── package.json
├── src/
│ └── extension.js
├── snippets/
│ ├── python.json
│ └── javascript.json
└── README.md
package.json: mendaftarkan perintah dan pengaturan
Manifes adalah kontrak antara ekstensi dan VS Code. Ada tiga bagian yang menentukan: activationEvents mengatur kapan ekstensi dimuat, contributes.commands mendaftarkan perintah di Command Palette, dan contributes.configuration membuat API key, tampilan saldo, serta interval polling bisa diubah dari Settings.
{
"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"
}
]
}
}
Kontrak manifes minimal yang perlu dijaga
- Deklarasikan sejak awal: satu activation event, seluruh perintah, dan entri konfigurasi yang aman dibagikan. Menambahnya belakangan berarti mengubah manifes dan memuat ulang jendela.
- Pisahkan API key dari kode perintah, supaya proses packaging tidak ikut membawa kredensial.
- Jaga permukaan
package.jsontetap kecil: makin sedikit yang dideklarasikan, makin sedikit yang rusak saat versi VS Code naik.
Isi extension.js: saldo, perintah solve, dan deteksi sitekey
Berkas src/extension.js menampung tiga alur terpisah. updateBalance() memanggil res.php dengan action=getbalance lalu menulis hasilnya ke status bar. solveCaptcha() menjalankan pola empat langkah yang sama seperti integrasi produksi — kirim ke in.php, simpan task ID, polling res.php sampai CAPCHA_NOT_READY menghilang, lalu pakai token — bedanya token disalin ke clipboard. detectSitekey() murni bekerja pada teks: empat pola regex untuk data-sitekey, googlekey, sitekey, dan render= pada URL reCAPTCHA.
Satu detail yang sering terlewat: method menentukan nama parameter sitekey. userrecaptcha memakai googlekey, turnstile memakai sitekey. Salah nama parameter memicu pesan error dari in.php, jadi tampilkan isi request apa adanya.
// 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 };
Batas waktunya implisit: 60 putaran polling dikalikan pollInterval bawaan 5 detik memberi jendela sekitar 5 menit. Kalau sering menyentuhnya, periksa pageurl dan sitekey sebelum menaikkan jumlah putaran.
Snippet Python dan JavaScript untuk panggilan rutin
Snippet mengambil alih bagian yang paling membosankan: mengetik ulang struktur permintaan in.php dan loop polling-nya. Berkas snippet dipetakan per bahasa lewat contributes.snippets.
Snippet Python
Tersedia tiga prefix: cai-recaptcha-v2 untuk pola kirim-dan-polling lengkap, cai-turnstile untuk pengirimannya saja, dan cai-balance untuk cek saldo. Tab stop ${1} dipakai ulang sehingga API key cukup diketik sekali.
{
"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
Versi JavaScript memakai fetch dan URLSearchParams, jadi tanpa dependensi tambahan di Node.js modern. Prefix-nya sengaja disamakan dengan versi Python supaya kebiasaan mengetik Anda tidak berubah saat berpindah bahasa.
{
"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"
}
}
Memasang ekstensi VS Code secara lokal
Ekstensi internal tidak perlu masuk Marketplace. Saat pengembangan, tekan F5 untuk membuka Extension Development Host. Untuk distribusi, jalankan npx @vscode/vsce package lalu pasang hasilnya dengan code --install-extension captchaai-dev-tools-1.0.0.vsix; berkas .vsix itu bisa dibagikan lewat repositori internal tim. Kalau ingin menerbitkannya, ikuti panduan penerbitan ekstensi VS Code.
Menyimpan API key tanpa menaruhnya di settings.json
Nilai captchaai.apiKey yang diisi lewat Settings berakhir sebagai teks biasa di settings.json. Untuk pemakaian pribadi itu wajar, tetapi dua kebiasaan ini layak dijaga. Pertama, simpan key di User Settings, bukan Workspace Settings — yang terakhir ada di .vscode/settings.json dan sering ikut ter-commit. Kedua, untuk versi yang dibagikan ke tim, ganti pembacaan konfigurasi dengan API SecretStorage supaya key masuk ke penyimpanan kredensial sistem operasi.
Kalau key terlanjur ikut ter-commit, rotasi key itu dari dashboard CaptchaAI. Menghapus commit tidak membatalkan key yang sudah terekspos.
Mengatasi masalah yang paling sering muncul
| Gejala | Penyebab | Perbaikan |
|---|---|---|
| Status bar menampilkan "Offline" | API tidak terjangkau | Periksa jaringan/firewall; pastikan ocr.captchaai.com bisa diakses |
| Error "Set your API key" | Konfigurasi masih kosong | Settings → cari "CaptchaAI" → isi API key |
| Snippet tidak muncul | Mode bahasa file tidak cocok | Setel mode bahasa file ke Python atau JavaScript |
| Solve berhenti karena batas waktu | Task gagal atau jaringan lambat | Naikkan pollInterval; verifikasi sitekey dan pageurl |
| Deteksi sitekey nihil | Tidak ada pola cocok | Pastikan file memuat data-sitekey, googlekey, atau sitekey |
Pertanyaan yang sering muncul
Bisakah saya menambahkan snippet untuk hCaptcha?
Tidak ada gunanya. hCaptcha dan FunCaptcha belum didukung CaptchaAI, jadi snippet untuk keduanya hanya menghasilkan error dari in.php. Yang layak dijadikan prefix adalah reCAPTCHA v2 dan v3 (termasuk Enterprise), Cloudflare Turnstile, Cloudflare Challenge, GeeTest v3, CAPTCHA gambar/OCR, Grid Image, dan BLS — ditambah CaptchaFox (beta), Friendly Captcha (beta), dan Lemin (beta). GeeTest v4 baru berstatus segera hadir.
Berapa thread yang terpakai kalau saya solve dari editor?
Satu. Satu perintah solve berarti satu CAPTCHA berjalan, dan thread itu bebas kembali begitu token keluar. Karena penagihan CaptchaAI berbasis thread bersamaan, bukan per solve, uji coba dari editor praktis tidak menambah tagihan: pada paket BASIC ($15/bulan, 5 thread) sekalipun masih tersisa empat thread untuk worker Anda.
Bisakah ekstensi ini jalan di Codespaces atau Remote SSH?
Bisa, dengan satu catatan. Manifes ini tidak mendeklarasikan entri browser, jadi ekstensi berjalan di extension host Node — pada Codespaces maupun Remote SSH, host itu ada di mesin remote. Permintaan ke ocr.captchaai.com pun keluar dari jaringan mesin itu: kalau egress-nya dibatasi, status bar menampilkan "Offline" meski koneksi laptop normal.
Aman tidak membagikan berkas .vsix ini ke satu tim?
Aman selama .vsix tidak membawa kredensial. Konfigurasi tersimpan di sisi pengguna, jadi paket yang Anda kemas hanya berisi kode dan snippet. Minta tiap anggota tim mengisi captchaai.apiKey sendiri di User Settings — pemakaian bisa ditelusuri per orang.
Bacaan terkait
- Whitelist IP dan pengamanan API key
- CaptchaAI dibandingkan CapMonster Cloud
- Webhook atau polling untuk menerima hasil solve
Langkah berikutnya
Pasang versi lokalnya, ambil API key Anda, lalu uji perintah solve pertama di halaman staging Anda sendiri.
Panduan lanjutan: