Tutorials

Уведомления Slack Bot о событиях CAPTCHA

О том, что конвейер решения CAPTCHA сломался, команда обычно узнаёт последней — от аналитика, у которого утренняя выгрузка пришла пустой. Чтобы этого не происходило, достаточно трёх типов сообщений в Slack: сбой отдельной задачи, падение баланса ниже порога и рост доли ошибок на скользящем окне. Ниже — готовый код на Python и Node.js, который ставится на действующий конвейер без внешних систем мониторинга.

Slack здесь удобен не тем, что он красивее логов, а тем, что он уже открыт у дежурного. Incoming Webhook — это один POST-запрос с JSON, без SDK, без OAuth и без отдельного сервиса, который потом придётся поддерживать.


Что вообще стоит отправлять в канал

Перед кодом полезно договориться о границе между «событием для лога» и «событием для человека». В Slack идёт только второе:

  • Состояние счёта. Баланс ниже порога — это событие, которое решается только человеком и только заранее.
  • Сломанная интеграция. Неверный sitekey, истёкший API-ключ, смена типа CAPTCHA на целевой странице.
  • Статистика с трендом. Не каждая ошибка, а доля ошибок за последние N попыток.

Отдельный тайм-аут при решении — нормальная часть работы сети, его место в логе. Если отправлять в канал всё подряд, через неделю его просто замьютят.


Шаг 1: создайте webhook в Slack

  1. Откройте api.slack.com/apps → Create New App
  2. Выберите Incoming Webhooks → Activate
  3. Нажмите Add New Webhook to Workspace → укажите канал
  4. Скопируйте URL webhook

URL равносилен праву писать в канал, поэтому храните его рядом с API-ключом — в переменных окружения, а не в репозитории.


Шаг 2: базовая функция отправки на Python

Все три алерта строятся над одним помощником. Цвет вложения задаёт визуальный приоритет, а fields раскладывает контекст по колонкам, чтобы сообщение читалось с телефона.

import requests
import json
from datetime import datetime

SLACK_WEBHOOK_URL = "https://hooks.slack.com/services/T00/B00/xxx"


def send_slack_alert(title, message, color="#ff0000", fields=None):
    """Send a formatted Slack alert."""
    attachment = {
        "color": color,
        "title": title,
        "text": message,
        "ts": int(datetime.now().timestamp()),
    }
    if fields:
        attachment["fields"] = [
            {"title": k, "value": str(v), "short": True}
            for k, v in fields.items()
        ]

    payload = {"attachments": [attachment]}
    resp = requests.post(SLACK_WEBHOOK_URL, json=payload, timeout=10)
    return resp.status_code == 200

Тайм-аут 10 с здесь не декорация: без него висящий запрос к Slack способен заблокировать воркер, который должен был решать задачи.


Шаг 3: алерт о сбое задачи

Первый тип сообщения — конкретный код ошибки по ID задачи. Он нужен в основном во время выкатки новой интеграции, когда важно видеть каждый промах.

def notify_solve_failure(task_id, captcha_type, error_code, site_url):
    send_slack_alert(
        title="CAPTCHA Solve Failed",
        message=f"Task `{task_id}` failed with `{error_code}`",
        color="#ff0000",
        fields={
            "Type": captcha_type,
            "Error": error_code,
            "Site": site_url,
            "Time": datetime.now().strftime("%H:%M:%S"),
        },
    )

# Use after a failed solve
result = poll_for_result(task_id)
if result.get("error"):
    notify_solve_failure(task_id, "recaptcha_v2", result["error"], "https://example.com")

В боевом режиме этот алерт обычно сужают до невосстанавливаемых ошибок — неверный ключ, нулевой баланс, неподдерживаемый тип. Здесь же полезно свериться с матрицей поддержки: CaptchaAI решает reCAPTCHA v2 и v3, Cloudflare Turnstile и Challenge, GeeTest v3, текстовые и grid-капчи, BLS; CaptchaFox (beta), Friendly Captcha (beta) и Lemin (beta) пока доступны только в бета-режиме. hCaptcha и FunCaptcha не поддерживаются, поддержка GeeTest v4 заявлена как «скоро». Если сайт молча переехал на неподдерживаемый тип, поток ошибок в Slack станет первым сигналом.


Шаг 4: контроль баланса через res.php

Баланс читается одним GET-запросом к res.php с action=getbalance. Опрос раз в пять минут покрывает практически любой темп расхода.

def check_balance_alert(api_key, threshold=5.0):
    """Alert when balance drops below threshold."""
    resp = requests.get("https://ocr.captchaai.com/res.php", params={
        "key": api_key, "action": "getbalance", "json": "1"
    }).json()

    balance = float(resp.get("request", 0))

    if balance < threshold:
        send_slack_alert(
            title="Low CaptchaAI Balance",
            message=f"Balance is ${balance:.2f} (threshold: ${threshold:.2f})",
            color="#ff9900",
            fields={
                "Current Balance": f"${balance:.2f}",
                "Threshold": f"${threshold:.2f}",
            },
        )
    return balance

# Run periodically
import threading

def balance_monitor(api_key, interval=300):
    """Check balance every 5 minutes."""
    check_balance_alert(api_key)
    timer = threading.Timer(interval, balance_monitor, args=[api_key, interval])
    timer.daemon = True
    timer.start()

balance_monitor("YOUR_API_KEY")

Тарифы CaptchaAI считаются по потокам, а не по числу решений: BASIC ($15/мес, 5 потоков), STANDARD ($30/мес, 15 потоков), ADVANCE ($90/мес, 50 потоков), дальше PREMIUM ($170/мес, 100 потоков), CORPORATE ($240/мес, 150 потоков), ENTERPRISE ($300/мес, 200 потоков) и уровни VIP-1 ($1,500), VIP-2 ($4,500), VIP-3 ($7,500). Поэтому алерт по балансу по сути говорит о сроке продления подписки, а не об «остатке решений».

Для команд в Минске, Алматы или Тбилиси, где трансграничный платёж по карте может пройти не с первого раза, именно этот алерт обычно окупается быстрее остальных. Ставьте порог с запасом на несколько дней работы, а не на несколько часов.


Шаг 5: доля ошибок на скользящем окне

Самый полезный сигнал — не единичная ошибка, а изменение фона. Класс ниже держит окно из 50 последних попыток, не стреляет раньше 20 замеров и соблюдает паузу в 300 с между сообщениями.

from collections import deque

class ErrorRateNotifier:
    def __init__(self, window=50, threshold=0.3, cooldown=300):
        self.results = deque(maxlen=window)
        self.threshold = threshold
        self.cooldown = cooldown
        self.last_alert = 0

    def record(self, success):
        self.results.append(success)

        if len(self.results) < 20:
            return

        error_rate = 1 - sum(self.results) / len(self.results)

        import time
        now = time.time()
        if error_rate > self.threshold and (now - self.last_alert) > self.cooldown:
            self.last_alert = now
            send_slack_alert(
                title="High CAPTCHA Error Rate",
                message=f"Error rate: {error_rate:.0%} over last {len(self.results)} tasks",
                color="#ff0000",
                fields={
                    "Error Rate": f"{error_rate:.1%}",
                    "Window": f"{len(self.results)} tasks",
                    "Threshold": f"{self.threshold:.0%}",
                },
            )

notifier = ErrorRateNotifier()

# After each solve attempt
notifier.record(success=True)   # solved
notifier.record(success=False)  # failed

Порог 0,3 — разумное начальное значение, но его стоит подобрать по своей статистике за пару недель. Если вы работаете с несколькими типами сразу, заведите отдельный экземпляр ErrorRateNotifier на каждый — иначе стабильный поток reCAPTCHA v2 замаскирует проблему с Cloudflare Turnstile или GeeTest v3.


То же самое на Node.js

Если конвейер живёт в Node.js, логика повторяется один в один: тот же формат вложения, те же два алерта и setInterval вместо threading.Timer.

const axios = require('axios');

const SLACK_WEBHOOK = 'https://hooks.slack.com/services/T00/B00/xxx';

async function sendSlackAlert(title, message, color = '#ff0000', fields = {}) {
  const attachment = {
    color,
    title,
    text: message,
    ts: Math.floor(Date.now() / 1000),
    fields: Object.entries(fields).map(([k, v]) => ({
      title: k, value: String(v), short: true,
    })),
  };

  await axios.post(SLACK_WEBHOOK, { attachments: [attachment] });
}

// Failure alert
async function notifySolveFailure(taskId, type, error) {
  await sendSlackAlert(
    'CAPTCHA Solve Failed',
    `Task \`${taskId}\` failed: \`${error}\``,
    '#ff0000',
    { Type: type, Error: error }
  );
}

// Balance alert
async function checkBalance(apiKey, threshold = 5.0) {
  const resp = await axios.get('https://ocr.captchaai.com/res.php', {
    params: { key: apiKey, action: 'getbalance', json: 1 },
  });
  const balance = parseFloat(resp.data.request);

  if (balance < threshold) {
    await sendSlackAlert(
      'Low CaptchaAI Balance',
      `Balance: $${balance.toFixed(2)}`,
      '#ff9900',
      { Balance: `$${balance.toFixed(2)}`, Threshold: `$${threshold.toFixed(2)}` }
    );
  }
  return balance;
}

// Periodic check
setInterval(() => checkBalance('YOUR_API_KEY'), 5 * 60 * 1000);

Ежедневная сводка вместо потока сообщений

Дайджест раз в сутки снимает большую часть шума: команда видит динамику, а канал остаётся читаемым.

def send_daily_summary(stats):
    """Send a daily digest to Slack."""
    send_slack_alert(
        title="Daily CAPTCHA Summary",
        message=f"{stats['total']} tasks processed",
        color="#36a64f",
        fields={
            "Solved": stats["solved"],
            "Failed": stats["failed"],
            "Avg Solve Time": f"{stats['avg_time_ms']}ms",
            "Total Cost": f"${stats['total_cost']:.2f}",
            "Success Rate": f"{stats['success_rate']:.1%}",
        },
    )

Поля в сводке заполняются из вашей собственной телеметрии — тех же счётчиков, что и в структурированном журналировании операций CAPTCHA. Если телеметрии пока нет, сначала заведите её, а потом возвращайтесь к дайджесту.


Где обычно ломается

Проблема Причина Что сделать
Webhook отвечает 403 URL неверен или отозван Пересоздайте webhook в Slack
Слишком много сообщений Нет паузы между алертами Добавьте cooldown от 300 с
Сообщения приходят с задержкой Запрос без тайм-аута Задайте timeout=10 в клиенте
Канал пустой Webhook привязан к другому каналу Проверьте привязку в настройках приложения
Алерты игнорируют Порог выставлен слишком низко Поднимите порог и увеличьте окно

Отдельно про содержимое сообщений: канал Slack — это ещё одно место хранения данных. Если в вашем контуре есть персональные данные, проверьте требования, под которые вы попадаете, — для проектов в РФ это 152-ФЗ «О персональных данных». На практике безопаснее отправлять в канал ID задачи, тип и код ошибки, а не полезную нагрузку целиком.


Часто задаваемые вопросы

Насколько часто безопасно опрашивать баланс?

Раз в 5 мин более чем достаточно: это 288 запросов в сутки и практически нулевая нагрузка. Запрос баланса не занимает поток и не мешает решать задачи параллельно.

Можно ли обойтись без Slack и писать в Telegram?

Да. Логика порогов от транспорта не зависит: замените тело send_slack_alert на вызов Bot API и соберите те же поля в обычный текст. Остальной код менять не нужно.

Куда ставить проверку баланса при запуске в Docker?

В тот же процесс, что и воркеры, но отдельным фоновым потоком (daemon=True в примере выше). Если контейнеров несколько, оставьте проверку баланса только в одном — иначе одно событие превратится в N одинаковых сообщений.

Как понять, что пороги настроены верно?

Простое правило: если за неделю ни один алерт не привёл к действию, пороги слишком чувствительны. Если команда узнала о сбое из другого источника — слишком высокие.

Нужен ли отдельный канал под эти сообщения?

Да, и желательно два: один для срочных алертов с уведомлениями, второй — для ежедневных сводок без звука. Так дежурный видит только то, на что надо реагировать сейчас.


Подключите мониторинг к своему конвейеру

Получите API-ключ на captchaai.com, подставьте URL webhook в примеры выше — и первые алерты придут в тот же день.


Связанные руководства

Комментарии для этой статьи отключены.