Tích Hợp

Retool + CaptchaAI: Xử lý biểu mẫu CAPTCHA của công cụ nội bộ

Retool không có sẵn component giải CAPTCHA, nên cách làm là gọi ra ngoài: khai báo CaptchaAI thành REST API resource, tạo hai query — một query gửi task tới in.php, một query đọc kết quả từ res.php — rồi dùng JavaScript query nối chúng thành vòng polling. Bài này dựng đúng luồng đó với reCAPTCHA v2.

Bài dành cho người đã có app Retool đang chạy và muốn app tự lấy token g-recaptcha-response thay vì để nhân viên vận hành giải tay từng lần.

Luồng chạy: bốn nhịp từ sitekey đến token

  1. Gửi task kèm googlekeypageurl tới in.php, nhận về ID task.
  2. Gọi res.php mỗi 5 giây cho tới khi có kết quả (polling).
  3. Đọc token trong trường request của phản hồi.
  4. Gắn token vào tham số g-recaptcha-response của request gửi form.

Nhịp hai là chỗ Retool khác các môi trường khác: query REST không tự lặp, nên vòng lặp phải nằm trong JavaScript query. Token reCAPTCHA v2 chỉ sống khoảng hai phút, nên hãy gửi form ngay khi có token.

Bạn cần chuẩn bị gì

  • Workspace Retool (Cloud hoặc self-hosted) với quyền tạo Resource.
  • API key CaptchaAI kèm số dư còn hiệu lực.
  • sitekeypageurl lấy từ mã nguồn trang có reCAPTCHA v2.
  • Quyền ghi Secrets Store để không phải dán API key vào query.

Bước 1: khai báo CaptchaAI thành REST API resource

Trong Retool, vào ResourcesCreate NewREST API:

Trường Giá trị
Tên CaptchaAI
Base URL https://ocr.captchaai.com
Xác thực Không có (API key gửi kèm dạng query parameter)

Lưu resource lại; mọi query ở các bước sau đều trỏ về nó.

Base URL này dùng chung cho cả hai query.

Bước 2: query gửi task submitCaptcha

Tạo query submitCaptcha với cấu hình và query parameter sau:

Mục Giá trị
Resource CaptchaAI
Action Type GET
URL Path /in.php
key {{secretsStore.CAPTCHAAI_API_KEY}}
method userrecaptcha
googlekey {{sitekeyInput.value}}
pageurl {{pageurlInput.value}}
json 1

API key nằm trong Secrets Store của Retool (Settings → Secrets) và gọi qua secretsStore; key dán thẳng vào query sẽ lộ với mọi người mở được app. Transformer dưới đây là tùy chọn:

// Parse the response
const data = {{ submitCaptcha.data }};
if (data.status === 1) {
  return { taskId: data.request, status: 'submitted' };
}
return { error: data.request, status: 'failed' };

Bước 3: query đọc kết quả pollResult

Tạo query pollResult, cũng trỏ về resource CaptchaAI:

Mục Giá trị
Action Type GET
URL Path /res.php
key {{secretsStore.CAPTCHAAI_API_KEY}}
action get
id {{submitCaptcha.data.request}}
json 1

Khi task chưa xong, API trả về CAPCHA_NOT_READY — trạng thái bình thường chứ không phải lỗi. Transformer sau tách ba tình huống đã giải xong, đang chờ và lỗi thật:

const data = {{ pollResult.data }};
if (data.status === 1) {
  return { token: data.request, status: 'solved' };
}
if (data.request === 'CAPCHA_NOT_READY') {
  return { status: 'pending' };
}
return { error: data.request, status: 'error' };

Bước 4: dựng vòng polling bằng JavaScript query

JavaScript query solveCaptcha điều phối chu trình gửi và polling:

// solveCaptcha — JavaScript Query
async function solve() {
  // Submit the CAPTCHA task
  await submitCaptcha.trigger();
  const submitResult = submitCaptcha.data;

  if (submitResult.status !== 1) {
    return { error: submitResult.request, status: 'submit_failed' };
  }

  const taskId = submitResult.request;

  // Wait 15 seconds before first poll
  await new Promise(r => setTimeout(r, 15000));

  // Poll up to 20 times (100 seconds max)
  for (let i = 0; i < 20; i++) {
    await pollResult.trigger({
      additionalScope: { taskId: taskId }
    });

    const result = pollResult.data;

    if (result.status === 1) {
      return { token: result.request, status: 'solved' };
    }

    if (result.request !== 'CAPCHA_NOT_READY') {
      return { error: result.request, status: 'error' };
    }

    // Wait 5 seconds before next poll
    await new Promise(r => setTimeout(r, 5000));
  }

  return { error: 'Polling timeout', status: 'timeout' };
}

return solve();

Mười lăm giây chờ trước lần polling đầu là có chủ đích: reCAPTCHA v2 hiếm khi xong sớm hơn. Hai mươi vòng cách nhau 5 giây giữ tổng thời gian dưới trần 120 giây của query JavaScript.

Bước 5: dựng giao diện cho app

Sáu component là đủ cho một app một nút bấm:

Component Cấu hình
Text Input sitekeyInput Nhãn "reCAPTCHA Sitekey"
Text Input pageurlInput Nhãn "URL trang"
Button solveButton Nhãn "Giải CAPTCHA", onClick solveCaptcha.trigger()
Text trạng thái {{ solveCaptcha.isFetching ? "Solving..." : "" }}
Text Area tokenOutput {{ solveCaptcha.data?.token }}, chế độ chỉ đọc
Status Badge Hiện theo {{ solveCaptcha.data?.status }}

Bước 6: dùng token trong query gửi form

Chuyển token sang query Retool gửi biểu mẫu. Tạo query tên submitForm:

Cài đặt Giá trị
Resource API đích của bạn
Action Type POST
Body Dữ liệu form kèm g-recaptcha-response: {{solveCaptcha.data.token}}

Chỉ bật nút "Gửi biểu mẫu" khi {{ solveCaptcha.data?.status === 'solved' }}. Ràng buộc nhỏ đó chặn phần lớn lỗi gửi form khi token còn rỗng.

Khắc phục sự cố

Vấn đề Nguyên nhân Cách xử lý
ERROR_WRONG_USER_KEY Key sai hoặc thiếu trong Secrets Store Kiểm tra Settings → Secrets
Query trả về text thô, không phải JSON Thiếu json=1 Thêm json: 1 vào query parameter
Polling hết thời gian chờ Loại CAPTCHA cần giải lâu hơn Tăng số vòng lặp từ 20 lên 30
submitCaptcha.dataundefined Query gửi chưa chạy Chạy query gửi trước khi polling
Form vẫn báo CAPTCHA sai Token đã hết hạn Gửi form ngay khi status === 'solved'

Kịch bản thực tế: nhóm vận hành ở TP.HCM

Một công ty outsourcing dùng app Retool để đối soát đơn hàng với cổng của đối tác. Cổng đó bật reCAPTCHA v2 ở form đăng nhập, nên mỗi ca nhân viên phải mở trình duyệt, giải tay rồi dán kết quả lại. Thêm resource CaptchaAI xong, app tự lấy token; phía người dùng chỉ còn một nút bấm.

Hai điểm nên chốt trước khi bàn giao:

  • Chỉ trỏ luồng này vào hệ thống bạn có quyền truy cập hợp lệ: cổng đối tác đã ký hợp đồng, hệ thống nội bộ của đơn vị bạn, hoặc staging.
  • Ghi log mỗi lần giải: thời điểm, người bấm nút, ID task, kết quả. Khi luồng chạm dữ liệu cá nhân, Nghị định 13/2023/NĐ-CP khiến phần log này thành thứ bạn cần lúc rà soát.

Chi phí: tính theo thread, không theo lượt giải

CaptchaAI bán theo thread — số CAPTCHA được giải song song — và mỗi thread không giới hạn lượt giải trong tháng. App nội bộ thường chỉ vài người bấm nút cùng lúc:

Gói Giá Thread
BASIC $15/tháng 5
STANDARD $30/tháng 15
ADVANCE $90/tháng 50

Giá niêm yết bằng USD, thanh toán bằng thẻ quốc tế, không có bảng giá quy đổi VND. Với app một nút bấm như trên, BASIC ($15/tháng, 5 thread) thường đã đủ.

Các loại CAPTCHA khác dùng lại được khung này

Chỉ cần đổi method và bộ tham số trong query gửi:

Loại CAPTCHA Trạng thái
reCAPTCHA v2, v2 Invisible, v2 Enterprise, v3
Cloudflare Turnstile, Cloudflare Challenge
GeeTest v3, CAPTCHA ảnh/OCR, grid-image, BLS
CaptchaFox, Friendly Captcha, Lemin ✅ beta
hCaptcha, FunCaptcha (Arkose Labs) ❌ Chưa hỗ trợ
GeeTest v4 ❌ Sắp ra mắt

Ba loại beta mới mở, nên thử trong app nháp trước.

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

Query JavaScript của Retool timeout ở 120 giây thì làm sao?

Giữ tổng thời gian dưới ngưỡng: 15 giây chờ đầu cộng 20 vòng cách nhau 5 giây là khoảng 115 giây. Nếu vẫn chạm trần, tách thành hai query: một query gửi task và lưu ID, một query polling bấm lại.

Cất API key CaptchaAI trong Retool thế nào cho an toàn?

Dùng Secrets Store (Environment → Secrets) rồi gọi qua {{secretsStore.CAPTCHAAI_API_KEY}}. Khóa nằm ngoài định nghĩa query nên người chỉ có quyền xem app không đọc được.

Một app Retool nội bộ cần bao nhiêu thread?

Bằng số lượt giải chạy đồng thời ở giờ cao điểm, không phải tổng số lượt trong ngày. Năm người cùng bấm nút trong một phút vẫn gọn trong 5 thread của gói BASIC ($15/tháng).

Chuyển luồng này sang Cloudflare Turnstile thì sửa gì?

Đổi method thành turnstile và thay googlekey bằng sitekey; phần polling giữ nguyên. Khác biệt còn lại ở query gửi form: token Turnstile đi trong trường cf-turnstile-response.

Bài viết liên quan

Bước tiếp theo

Dựng resource CaptchaAI trong workspace Retool và để app tự lấy token — lấy API key CaptchaAI rồi quay lại Bước 1.

Hướng dẫn liên quan:

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