Cara Memecahkan CAPTCHA di dalam Activity Workflow Temporal

temporal captcha - How to Solve CAPTCHA in a Temporal Workflow Activity

Pemecahan captcha di Temporal berada di dalam sebuah Activity. Bukan di metode workflow, bukan di helper yang dipanggil workflow, tetapi di Activity. Kode workflow diputar ulang dari history setiap kali workflow dilanjutkan, jadi kode itu harus deterministik: tanpa panggilan jaringan, tanpa keacakan, tanpa membaca jam. Satu pemecahan mencakup ketiganya sekaligus. Begitu berada di dalam Activity, sisanya hanyalah retry policy dan satu disiplin soal timing, dan totalnya sekitar empat puluh baris.

Apa yang Anda butuhkan

  • Python 3.10 atau yang lebih baru, Temporal Python SDK, dan CapSkip Python SDK.
  • Sebuah Temporal Service untuk dihubungi. Server dev lokal maupun Temporal Cloud sama-sama bisa dipakai di sini.
  • URL halaman formulir yang terproteksi, beserta sitekey-nya.
  • CapSkip dalam Mode Local ketika worker dan solver berbagi satu mesin, atau Mode Server ketika tidak. Keduanya dijelaskan di pengaturan koneksi.
# pip install temporalio
pip install -U temporalio capskip httpx

# A local service to develop against.
temporal server start-dev

Mengapa pemecahannya tidak boleh berada di kode workflow

Temporal memutar ulang history sebuah workflow untuk membangun kembali state-nya setelah worker restart, deploy, atau tidur seminggu. Agar itu menghasilkan jawaban yang sama dua kali, kode workflow harus deterministik. SDK-nya tegas soal apa yang dilarang: tanpa IO jaringan, tanpa threading, tanpa keacakan, tanpa panggilan eksternal ke proses lain, tanpa mutasi state global. Bahkan kode workflow dijalankan di sandbox yang mengimpor ulang modul setiap run, dan itulah sebabnya impor activity dibungkus dalam blok pass-through.

Satu pemecahan CAPTCHA melanggar aturan itu tiga kali sekaligus. Ia adalah panggilan jaringan, token yang dikembalikan berbeda pada setiap percobaan, dan lamanya bergantung pada mesinnya. Taruh di metode workflow dan ia akan tampak bekerja saat pengembangan, lalu menghasilkan error non-determinisme begitu ada worker yang restart di tengah run.

Kabar baiknya, batasan ini justru memberi Anda sesuatu. Activity di-retry oleh Temporal sendiri, dengan policy yang Anda deklarasikan alih-alih loop yang Anda tulis, dan hasilnya dicatat di history. Jadi pemecahan yang berhasil tidak pernah diulang pada replay, dan itu persis yang Anda inginkan untuk sesuatu yang jawabannya sekali pakai.

Langkah 1: activity pemecahan

CapSkip SDK untuk Python menyertakan klien asyncio sungguhan, bukan alias, jadi activity async adalah pilihan yang paling pas dan tidak butuh thread pool. Ambil sitekey dan URL halaman sebagai argumen, lalu kembalikan token-nya.

# pip install capskip
import os
from temporalio import activity
from capskip import AsyncCapSkip

@activity.defn
async def solve_recaptcha(sitekey: str, page_url: str) -> str:
    solver = AsyncCapSkip(
        host=os.environ.get("CAPSKIP_HOST", "127.0.0.1"),
        port=8080,
        apiKey=os.environ.get("CAPSKIP_API_KEY", "capskip"),
    )
    result = await solver.recaptcha(sitekey=sitekey, url=page_url)
    return result["code"]

Satu metode mencakup reCAPTCHA v2, Invisible, Enterprise, dan v3. Varian-varian itu adalah opsi pada panggilan yang sama, bukan metode terpisah: invisible bernilai 1, enterprise bernilai 1, atau version bernilai v3 dengan sebuah action. Turnstile dan GeeTest punya metodenya sendiri dengan bentuk yang sama, dan daftar parameter lengkapnya ada di dokumentasi API CapSkip.

Jika Anda lebih suka memakai klien sinkron, activity-nya harus berupa def biasa dan worker-nya butuh activity_executor, karena Temporal menjalankan activity sinkron di thread pool. Versi async di atas menghindari itu sepenuhnya, dan ini salah satu dari sedikit hal di mana Python SDK memang lebih enak dipakai daripada yang lain.

Langkah 2: timeout yang lebih panjang daripada milik solver

Setiap activity butuh start_to_close_timeout, dan di sinilah orang diam-diam merusak pemecahan mereka sendiri. CapSkip melakukan polling hingga 300 detik untuk reCAPTCHA, Turnstile, dan GeeTest, serta 120 detik untuk CAPTCHA gambar. Setel timeout activity di bawah itu dan Temporal akan membatalkan percobaannya saat solver masih bekerja, lalu mengulang, dan Anda punya dua pemecahan berjalan untuk satu formulir.

Beri activity ruang lebih di atas batas milik solver. Enam menit terhadap batas atas solver lima menit sudah nyaman.

# Longer than recaptchaTimeout, which defaults to 300s.
from datetime import timedelta
from temporalio.common import RetryPolicy

SOLVE_TIMEOUT = timedelta(minutes=6)

SOLVE_RETRIES = RetryPolicy(
    initial_interval=timedelta(seconds=5),
    backoff_coefficient=2.0,
    maximum_attempts=4,
    # These fail identically every time. Do not burn attempts.
    non_retryable_error_types=["ValidationException", "ApiException"],
)

Daftar non-retryable dicocokkan berdasarkan nama kelas exception, dan layak diisi. ValidationException berarti argumen hilang atau salah bentuk, dan ApiException berarti API menolak permintaannya, biasanya karena sitekey tidak cocok dengan URL halaman. Keduanya tidak akan membaik pada percobaan kedua. NetworkException dan TimeoutException adalah dua yang benar-benar layak di-retry: yang pertama berarti solver tidak berjalan atau host-nya salah, yang kedua berarti pemecahannya melampaui timeout polling. Keempatnya diturunkan dari basis yang sama, jadi menangkap CapSkipError bisa dipakai kalau Anda lebih suka menangani semuanya di satu tempat.

Langkah 3: pecahkan terakhir, bukan pertama

Eksekusi durabel membuat kedaluwarsa token lebih mudah salah ditangani di sini daripada di orkestrator mana pun. Workflow Temporal bisa menunggu sinyal, tidur sehari, lalu lanjut, dan hasil activity yang tercatat kembali dari history tanpa berubah. Jadi workflow yang memecahkan lebih awal, menunggu persetujuan, lalu mengirim akan memutar ulang token yang dibuat kemarin.

Token reCAPTCHA hanya diterima sekali dan kedaluwarsa dalam sekitar dua menit. Susun workflow-nya agar pemecahan menjadi langkah tepat sebelum pengiriman, tanpa apa pun yang bisa memblokir di antaranya. Bentuk umum masalah itu dibahas di panduan kedaluwarsa token reCAPTCHA.

Contoh lengkap yang berfungsi

Tiga activity dan satu workflow yang memanggilnya secara berurutan. Baca sitekey, pecahkan, kirim. Setiap activity punya timeout dan retry policy-nya sendiri, dan masing-masing muncul terpisah di Temporal UI, jadi saat ada yang lambat Anda bisa melihat langkah mana penyebabnya.

# activities.py
import os, re, httpx
from temporalio import activity
from capskip import AsyncCapSkip

@activity.defn
async def read_sitekey(page_url: str) -> str:
    async with httpx.AsyncClient(timeout=30) as client:
        html = (await client.get(page_url)).text
    found = re.search(r'data-sitekey=["\']([^"\']+)', html)
    if not found:
        raise RuntimeError("No data-sitekey on the page.")
    return found.group(1)

@activity.defn
async def solve_recaptcha(sitekey: str, page_url: str) -> str:
    solver = AsyncCapSkip(host=os.environ.get("CAPSKIP_HOST", "127.0.0.1"))
    return (await solver.recaptcha(sitekey=sitekey, url=page_url))["code"]

@activity.defn
async def submit_form(page_url: str, token: str) -> int:
    async with httpx.AsyncClient(timeout=30) as client:
        reply = await client.post(
            page_url, data={"g-recaptcha-response": token}
        )
    return reply.status_code

Workflow-nya sendiri tidak memuat logika apa pun selain urutan. Itulah intinya: semua yang bisa gagal ada di activity, dan workflow adalah bagian deterministik yang selamat dari replay.

# workflow.py
from datetime import timedelta
from temporalio import workflow
from temporalio.common import RetryPolicy

with workflow.unsafe.imports_passed_through():
    from activities import read_sitekey, solve_recaptcha, submit_form

@workflow.defn
class SubmitProtectedForm:
    @workflow.run
    async def run(self, page_url: str) -> int:
        sitekey = await workflow.execute_activity(
            read_sitekey, page_url,
            start_to_close_timeout=timedelta(seconds=60),
        )
        # Solve directly before the submit. Tokens go stale.
        token = await workflow.execute_activity(
            solve_recaptcha, args=[sitekey, page_url],
            start_to_close_timeout=timedelta(minutes=6),
            retry_policy=RetryPolicy(
                maximum_attempts=4,
                non_retryable_error_types=["ValidationException"],
            ),
        )
        return await workflow.execute_activity(
            submit_form, args=[page_url, token],
            start_to_close_timeout=timedelta(seconds=60),
        )

Perhatikan daftar args pada activity yang berargumen dua. Satu argumen posisional bisa dioper langsung, tetapi lebih dari satu harus lewat args, dan salah di situ adalah kesalahan pertama yang paling umum di SDK ini.

# worker.py - run this where CapSkip can be reached
import asyncio
from temporalio.client import Client
from temporalio.worker import Worker
from activities import read_sitekey, solve_recaptcha, submit_form
from workflow import SubmitProtectedForm

async def main():
    client = await Client.connect("localhost:7233")
    worker = Worker(
        client,
        task_queue="captcha-queue",
        workflows=[SubmitProtectedForm],
        activities=[read_sitekey, solve_recaptcha, submit_form],
    )
    await worker.run()

asyncio.run(main())

Di mana worker berjalan, dan di mana solver berjalan

Temporal memisahkan keduanya dengan rapi, dan itu menguntungkan Anda. Temporal Service menjadwalkan pekerjaan dan menyimpan history. Ia tidak pernah menjalankan kode Anda. Worker yang Anda jalankan di infrastruktur sendiri memegang koneksi keluar yang panjang ke service itu dan mengambil task dari antrean. Tidak ada koneksi masuk yang pernah dibuka ke jaringan Anda.

Jadi worker di mesin Windows yang sama dengan CapSkip memanggil 127.0.0.1:8080 persis seperti skrip di meja Anda, dan itu tetap berlaku di Temporal Cloud. Kata cloud di Temporal Cloud merujuk pada lapisan orkestrasi, bukan lapisan komputasi.

Begitu worker-nya pindah, host-nya berubah dan tidak ada lagi yang berubah. Worker di dalam container, di VM Linux, atau di Kubernetes tidak bisa menjangkau solver Windows lewat loopback, jadi solver-nya beralih ke Mode Server. Mode Local mengikat ke 127.0.0.1 dan hanya melayani perangkat itu. Mode Server mengikat ke IP jaringan atau IP publik Anda, dan IP publik statis menjaga alamatnya tetap stabil. Di kedua mode, itu tetap hardware Anda dan tetap tanpa meter, jadi workflow yang berjalan sepuluh ribu kali sehari biayanya sama dengan yang berjalan sekali.

# One environment variable, no code change.
# CAPSKIP_HOST=10.0.0.12 on the worker.
solver = AsyncCapSkip(host=os.environ["CAPSKIP_HOST"], port=8080)

Nyalakan validasi key begitu solver mendengarkan di alamat jaringan, dan beri setiap armada worker key-nya sendiri agar satu key bisa dicabut tanpa mengusik yang lain. Kedua mode dibahas langkah demi langkah di panduan penyiapan CapSkip.

Kesalahan umum dan artinya

Apa yang Anda lihatPenyebabPerbaiki
Error non-determinisme saat replayPemecahannya dipanggil dari kode workflowPindahkan ke sebuah activity dan panggil dengan execute_activity
RestrictedWorkflowAccessError saat imporModul activity diimpor ke dalam sandboxImpor di dalam workflow.unsafe.imports_passed_through
Activity dibatalkan di tengah pemecahan, lalu diulangstart_to_close_timeout lebih pendek daripada milik solverSetel di atas 300 detik untuk reCAPTCHA, Turnstile, dan GeeTest
Formulir menolak token yang tampak benarWorkflow terblokir antara pemecahan dan pengirimanJadikan pemecahan sebagai langkah tepat sebelum pengiriman
Empat percobaan terbuang pada kegagalan yang samaError deterministik ikut di-retryCantumkan ValidationException dan ApiException sebagai non-retryable
NetworkException pada setiap percobaanWorker tidak berada di mesin yang menjalankan solverAlihkan solver ke Mode Server dan setel host-nya
TypeError soal argumen pada sebuah activityDua argumen posisional dioper langsungOper keduanya sebagai list melalui parameter args
TimeoutException dari SDKProses pemecahan melampaui recaptchaTimeoutNaikkan di atas nilai default 300 detik

FAQ

Benarkah workflow Temporal Cloud bisa memanggil 127.0.0.1?

Ya, karena Temporal Cloud tidak menjalankan kode Anda. Worker Anda yang menjalankannya, di mana pun Anda memulainya, dan worker itu menghubungi service ke luar. Loopback di worker itu berarti mesin worker itu sendiri, jadi solver di mesin itu menjawab seperti biasa. Tidak ada yang berubah soal itu ketika Anda pindah dari server dev ke Cloud.

Apakah activity pemecahan perlu melakukan heartbeat?

Tidak bisa dengan berguna, karena panggilan SDK-nya memblokir sampai token tiba dan tidak ada titik di dalamnya untuk melapor. Beri activity itu start_to_close_timeout dengan ruang lebih yang nyata, dan biarkan percobaan yang gagal diulang oleh policy. Heartbeat cocok untuk activity yang melakukan perulangan atas pekerjaan yang Anda kendalikan.

Bagaimana cara memecahkan satu batch tanpa membanjiri solver?

Setel max_concurrent_activities di worker, atau beri pekerjaan pemecahan task queue dan worker-nya sendiri dengan batas rendah. Itu melakukan throttling di tempat pekerjaannya dieksekusi, yang lebih andal daripada mencoba merenggangkan waktu mulai workflow. Di dalam satu proses, klien async menyebar pekerjaan dengan asyncio, dan itu dibahas di panduan memecahkan CAPTCHA secara paralel.

Apa bedanya dengan melakukannya di Airflow?

Airflow adalah scheduler dengan DAG, dan tidak ada yang mencegah Anda melakukan panggilan jaringan saat graf-nya sedang dibangun, yang merupakan kumpulan jebakan yang berbeda. Temporal adalah eksekusi durabel, jadi batasannya adalah determinisme dan jawabannya selalu sebuah activity. Sisi solver-nya identik di keduanya, dan versi Airflow-nya ditulis di panduan CAPTCHA Airflow.

Versi singkatnya

Taruh pemecahannya di sebuah activity, jangan pernah di metode workflow. Beri start_to_close_timeout yang lebih panjang daripada batas atas 300 detik milik solver, tandai ValidationException dan ApiException sebagai non-retryable, dan susun workflow-nya agar pemecahan menjadi hal terakhir sebelum pengiriman. Jalankan worker di tempat CapSkip berada dan host-nya tetap 127.0.0.1. Sisa permukaan Python-nya ada di halaman pemecah CAPTCHA Python, dan tiga panggilan yang sama ada di Node.js, PHP, dan C# seperti tercantum di halaman SDK pemecahan CAPTCHA. Opsi reCAPTCHA-nya sendiri ada di halaman pemecah reCAPTCHA v2.

Satu hal yang layak diketahui sebelum Anda mengarahkan sebuah jadwal ke sini. CapSkip adalah pemecah captcha yang berjalan di hardware yang sudah Anda miliki, jadi workflow yang berjalan setiap menit biayanya sama dengan yang berjalan sekali seminggu.