Cara Memecahkan CAPTCHA di Background Task Trigger.dev

Pemecahan captcha di Trigger.dev gagal pada deploy pertama karena alasan yang sama sekali tidak berhubungan dengan kodenya. Trigger.dev tidak memanggil aplikasi Anda. Trigger.dev membangun task Anda menjadi image Docker lalu menjalankannya di mesin miliknya sendiri, jadi 127.0.0.1 di dalam sebuah task adalah container itu, bukan mesin tempat solver Anda berada. Mode Server memperbaikinya lewat satu pengaturan. Hal kedua yang harus tepat adalah maxDuration, karena waktu polling ke solver diperhitungkan penuh terhadap batas itu.
Apa yang Anda butuhkan
- Project Trigger.dev dengan SDK terinstal dan trigger.config.ts di root.
- CapSkip berjalan di mesin Windows, dengan client Node ditambahkan ke project yang sama agar ikut masuk ke image yang di-deploy.
- Sitekey dan URL halaman datang lewat payload task alih-alih ditulis hardcode, sehingga satu task melayani semua formulir.
- Mode Server, plus alamat solver yang bisa dijangkau. Ini bukan pilihan opsional di Trigger.dev Cloud, karena alasan di Langkah 1.
# npm install capskip npm install @trigger.dev/sdk capskip
Langkah 1: di mana task sebenarnya berjalan, dan mode apa yang dibutuhkan
Sebagian besar panduan platform bisa menaruh bagian ini di akhir. Panduan ini tidak bisa, karena bagian inilah yang menentukan apakah hal lain bisa berjalan. Penjelasan Trigger.dev sendiri tentang deploy cukup gamblang: kodenya dikemas menjadi image Docker lalu di-deploy ke instance Trigger.dev Anda, dan setiap run dieksekusi di environment terisolasi yang mereka kelola. Task Anda tidak berjalan di tempat editor Anda berada.
Jadi loopback di dalam fungsi run mengarah ke container task itu. Tidak ada apa pun yang mendengarkan di port 8080 di sana, dan kegagalannya berupa connection refused yang muncul sebagai NetworkException pada setiap percobaan.
Ada dua mode koneksi. Mode Local mengikat ke 127.0.0.1 dan hanya melayani perangkat itu, dan itu memang tepat ketika otomatisasi dan solver Anda berada di satu mesin. Mode Server mengikat ke alamat jaringan atau IP publik Anda, sehingga mesin lain, sebuah VPS, atau platform hosting bisa menjangkau mesin Windows yang sama lewat API. Mode Server hanya mengubah alamat mana yang didengarkan solver. Perangkat kerasnya tetap milik Anda dan tetap tanpa kuota. Kedua mode itu ada di pengaturan koneksi.
| Cara Anda menjalankan Trigger.dev | Mode koneksi yang mana |
|---|---|
| CLI dev, di mesin CapSkip | Mode Local. 127.0.0.1 memang benar |
| Self-hosted, di jaringan Anda sendiri | Pakai mode Server dengan alamat LAN pemecah CAPTCHA |
| Trigger.dev Cloud | Pakai mode Server dengan IP publik statis dan sebuah aturan firewall |
IP publik statis layak disiapkan untuk baris ketiga, supaya alamatnya tidak berpindah di bawah deployment yang sedang berjalan. Simpan alamat itu di variabel lingkungan, bukan di kode sumber, karena CLI dev dan task yang di-deploy membutuhkan nilai yang berbeda.
// npm install capskip
import { CapSkip } from "capskip";
// 127.0.0.1 while the dev CLI runs it on your machine,
// the solver's reachable address once it is deployed.
export const solver = new CapSkip({
host: process.env.CAPSKIP_HOST ?? "127.0.0.1",
port: Number(process.env.CAPSKIP_PORT ?? 8080),
recaptchaTimeout: 120,
});Langkah 2: maxDuration harus mencakup pemecahan
Trigger.dev mengukur sebuah run terhadap maxDuration dalam satuan detik, dengan minimum lima detik yang terdokumentasi. Pengecualiannya disebut secara eksplisit: waktu yang dihabiskan di wait.for, triggerAndWait, dan batchTriggerAndWait tidak dihitung. Request HTTP yang di-await tidak ada dalam daftar itu, jadi setiap detik yang dipakai client untuk melakukan polling ke solver dihitung penuh.
Itu penting karena memecahkan CAPTCHA sebagian besarnya adalah menunggu. Pemecahan reCAPTCHA v2 umumnya berjalan lima belas sampai empat puluh lima detik, dan antrean yang padat bisa membuatnya lebih lama lagi. Tetapkan maxDuration dengan memasukkan pemecahan ke dalam anggarannya, bukan hanya sisa pekerjaan task.
// trigger.config.ts sets the project-wide floor.
import { defineConfig } from "@trigger.dev/sdk";
export default defineConfig({
project: "proj_YOUR_PROJECT_REF",
maxDuration: 60,
});
// A solving task overrides it. 60s is not enough on its own:
// the solve alone can use most of that budget.
export const solveAndSubmit = task({
id: "solve-and-submit",
maxDuration: 300,
run: async (payload) => { /* ... */ },
});Jaga agar batas atas milik client tetap di bawahnya, supaya client yang menyerah lebih dulu dan melempar error yang bisa Anda baca. Client Node memakai default 300 detik untuk reCAPTCHA, Turnstile, dan GeeTest, serta 120 detik untuk CAPTCHA gambar. Menurunkan timeout client reCAPTCHA menjadi 120, di bawah maxDuration 300, menyisakan ruang untuk apa pun yang dilakukan task dengan token itu.
Ada satu jebakan yang perlu disebut. Karena wait.for dikecualikan dari maxDuration, ia terlihat seperti cara gratis untuk menjeda sebuah task. Ia tidak gratis bagi token. Token reCAPTCHA berlaku sekitar dua menit waktu berjalan, dan jam milik platform serta jam milik token adalah dua jam yang berbeda. Pecahkan dan pakai token di bagian kode yang sama, lalu baca berapa lama token reCAPTCHA bertahan sekali sebelum Anda merancang alur di sekitarnya.
Langkah 3: retry, dan error mana yang layak mendapatkannya
Task melakukan retry tiga kali secara default, dengan exponential backoff yang Anda atur lewat factor, minTimeoutInMs, maxTimeoutInMs, dan randomize. Config yang dihasilkan CLI menonaktifkan retry di environment DEV, dan itulah sebabnya task yang melakukan retry di production terlihat langsung gagal di mesin Anda.
Task yang di-retry menjalankan ulang seluruh fungsi run, jadi ia memecahkan CAPTCHA lagi. Tidak ada token basi yang diwariskan, dan itu membuat nilai default-nya masuk akal. Yang layak disetel justru kegagalan mana yang pantas mendapat percobaan sama sekali.
| Exception yang mana | Apa artinya | Layak diulang? |
|---|---|---|
| NetworkException | CapSkip tidak terjangkau, atau sedang mulai ulang | Ya. Untuk inilah retry ada |
| TimeoutException | Polling melewati batas atas milik client sendiri | Sekali, mungkin. Jarang layak tiga kali |
| ApiException | API mengembalikan sebuah kode error | Tergantung kode errornya. Biasanya tidak |
| ValidationException | Parameternya salah dan akan tetap salah | Tidak. Lempar AbortTaskRunError |
AbortTaskRunError menggagalkan percobaan itu dan menonaktifkan retry, dan itulah yang pantas untuk request yang salah bentuk. Sitekey yang salah tidak akan berubah menjadi benar pada percobaan ketiga, dan tiga percobaan yang masing-masing sembilan puluh detik berarti empat setengah menit yang habis hanya untuk membuktikan hal itu.
import { task, AbortTaskRunError } from "@trigger.dev/sdk";
import { ValidationException } from "capskip";
try {
const { code } = await solver.recaptcha(sitekey, pageUrl);
return await postForm(pageUrl, code);
} catch (err) {
// Wrong parameters will be wrong on all three attempts.
if (err instanceof ValidationException) {
throw new AbortTaskRunError(err.message);
}
throw err; // everything else takes the normal backoff
}Langkah 4: task lengkapnya
Semua hal di atas dalam satu file. Client dibangun di scope modul supaya ia dibangun sekali per container, bukan sekali per run, dan ia tidak membawa state apa pun per run.
// npm install capskip
import { task, AbortTaskRunError } from "@trigger.dev/sdk";
import { CapSkip, ValidationException } from "capskip";
const solver = new CapSkip({
host: process.env.CAPSKIP_HOST ?? "127.0.0.1",
port: 8080,
recaptchaTimeout: 120,
});
export const submitSignup = task({
id: "submit-signup",
maxDuration: 300,
retry: { maxAttempts: 3, minTimeoutInMs: 2000 },
queue: { concurrencyLimit: 10 },
run: async (payload, { ctx }) => {
const { sitekey, pageUrl, email } = payload;
try {
// Solve and submit together. The token is short lived.
const { code } = await solver.recaptcha(sitekey, pageUrl);
const res = await postSignup(pageUrl, email, code);
return { status: res.status, runId: ctx.run.id };
} catch (err) {
if (err instanceof ValidationException) {
throw new AbortTaskRunError(err.message);
}
throw err;
}
},
});Pemanggilan itu adalah reCAPTCHA v2. Jenis lainnya berbentuk sama: oper invisible atau enterprise disetel ke 1, atau version disetel ke v3 dengan sebuah action, atau panggil turnstile atau geetest sebagai gantinya. Daftar lengkapnya ada di halaman pemecah CAPTCHA Node.js.
Langkah 5: konkurensi, dan di mana batas yang sebenarnya berada
Opsi queue membatasi berapa banyak run dari sebuah task yang berjalan sekaligus. Dengan solver berbayar per pemecahan, angka itu sebenarnya alat pengendali pengeluaran, dan karena itulah orang menyetelnya rendah. Di sini angka itu soal kapasitas satu mesin, jadi setel sesuai yang sanggup ditangani solver dan situs targetnya, bukan sesuai yang sanggup Anda bayar.
Ada dua hal yang benar-benar membatasinya. Mesin Windows yang menjalankan CapSkip, dan seberapa cepat situs tujuan Anda mau menerima request sebelum ia mulai menerapkan rate limiting. Yang kedua biasanya lebih ketat. Tidak ada yang mengantre di belakang saldo, dan tidak ada yang gagal di akhir bulan.
export const submitSignup = task({
id: "submit-signup",
// Sized for the solver machine and the target site,
// not for a credit balance.
queue: { concurrencyLimit: 10 },
maxDuration: 300,
run: async (payload) => { /* ... */ },
});Kesalahan umum dan artinya
| Apa yang Anda lihat | Penyebab | Perbaiki |
|---|---|---|
| Berjalan dengan CLI dev, tapi NetworkException setelah di-deploy | Task yang di-deploy adalah container, jadi loopback berarti container itu | Mode Server, lalu set CAPSKIP_HOST untuk environment yang di-deploy |
| Run dihentikan di tengah proses pemecahan | maxDuration lebih pendek daripada waktu yang dibutuhkan pemecahan | Naikkan nilainya pada task, di atas timeout milik client |
| Task langsung gagal di DEV tapi melakukan retry di production | Config yang dihasilkan menonaktifkan retry di DEV | Memang begitu. Uji perilaku retry di environment yang sudah di-deploy |
| Tiga percobaan, kegagalan yang sama, beberapa menit terbuang | Error parameter diperlakukan sebagai gangguan sementara | Lempar AbortTaskRunError untuk ValidationException |
| Token ditolak setelah wait.for | Menunggu itu gratis bagi maxDuration, tapi tidak bagi token | Pecahkan setelah penantian, tepat sebelum mengirim |
| ERROR_WRONG_USER_KEY di dalam ApiException | CAPSKIP_API_KEY belum diset di environment yang di-deploy | Set di variabel lingkungan Trigger.dev, lalu deploy ulang |
| CAPCHA_NOT_READY dari polling buatan sendiri | Hasilnya dibaca sebelum selesai | Biarkan client yang melakukan polling. Ia melambat dengan sendirinya |
Respons terakhir itu memang ditulis persis seperti kelihatannya, dan huruf yang hilang bukan salah ketik dari pihak kami, karena API memang benar-benar mengembalikannya seperti itu. Penjelasan lengkapnya ada di panduan CAPCHA_NOT_READY.
FAQ
Bisakah task Trigger.dev Cloud menjangkau solver di meja saya?
Bisa, dengan Mode Server. Task berjalan di container yang dikelola Trigger.dev, jadi loopback di sana berarti container itu. Ikat CapSkip ke IP publik Anda di pengaturan koneksi, pasang aturan firewall di depannya yang hanya mengizinkan alamat yang Anda harapkan, lalu set CAPSKIP_HOST di variabel lingkungan Trigger.dev. IP publik statis disarankan supaya alamatnya tidak berpindah tanpa Anda sadari.
Apakah self-hosting Trigger.dev mengubah semua ini?
Yang berubah alamatnya, bukan modelnya. Run yang self-hosted tetap mengeksekusi kode Anda di dalam container pada instance tersebut, bukan memanggil aplikasi Anda, jadi loopback tetap berarti container. Bedanya, instance itu biasanya berada di jaringan Anda sendiri, jadi Mode Server bisa memakai alamat LAN alih-alih alamat publik, dan tidak ada aturan firewall yang perlu menghadap internet.
Haruskah pemecahan dijadikan task tersendiri yang dipanggil task lain?
Biasanya tidak. Memisahkannya berarti token melintasi batas antar-task dan mengendap di sebuah payload sementara task induknya melanjutkan, dan itu cara tercepat untuk memakai token yang sudah kedaluwarsa. Simpan pemecahan dan apa pun yang memakai tokennya di dalam fungsi run yang sama, lalu kembalikan hasilnya, bukan kredensialnya. Task terpisah hanya masuk akal kalau yang dikembalikannya sama sekali bukan token.
Apa bedanya dengan melakukannya di Inngest?
Model deployment-nya berkebalikan, dan itu mengubah seluruh jawabannya. Inngest memanggil aplikasi Anda lewat HTTP, jadi kode Anda berjalan di mana pun Anda men-deploy-nya dan mode koneksi menjadi pertanyaan tentang hosting Anda sendiri. Trigger.dev menjalankan kode Anda di mesin miliknya, jadi pada penawaran cloud-nya Mode Server sudah ditentukan untuk Anda. Versi Inngest-nya, termasuk alasan mengapa pemecahan di sana harus berada di dalam satu step, ada di panduan Inngest.
Versi singkatnya
Jalankan CapSkip dalam Mode Server dan set CAPSKIP_HOST di environment Trigger.dev, karena task yang di-deploy adalah container dan loopback di sana berarti container itu. Beri task sebuah maxDuration yang mencakup pemecahan, karena polling ke endpoint HTTP bukan salah satu penantian yang dikecualikan. Jaga timeout client tetap di bawahnya. Biarkan tiga retry default berlaku, tapi lempar AbortTaskRunError untuk kesalahan parameter. Pecahkan dan kirim di dalam fungsi run yang sama, jangan pernah melintasi penantian atau batas antar-task.
- Endpoint mentah di balik klien didokumentasikan di dokumentasi API CapSkip.
- Tantangan kotak centangnya sendiri dijelaskan di halaman pemecah reCAPTCHA v2.
Perlu Anda timbang sebelum memilih batas konkurensi: CapSkip adalah pemecah captcha yang berjalan di perangkat keras yang sudah Anda miliki, jadi angka yang Anda pilih adalah keputusan kapasitas, bukan keputusan anggaran.
