Tutorials

CAPTCHA 管道状态的 Discord Webhook 警报

验证码识别管道出问题时,最怕的不是报错本身,而是没人第一时间发现。与其为 CAPTCHA 管道单独搭一套告警面板,不如把余额、错误率、队列深度这些状态直接推送到团队已经在用的 Discord 频道——十几行 Python 代码,配合一个 webhook,就能把这套通知跑起来。

国内不少做海外电商、社媒自动化的团队,本来就习惯把 CI、监控消息统一丢进 Discord 或飞书群,CAPTCHA 管道的告警走同一条路径就好。

第一步:创建 Discord Webhook

在推送任何消息之前,先拿到一个 webhook 地址,步骤不多,5 分钟能搞定:

  1. 打开你的 Discord 服务器设置
  2. 进入 集成 → Webhooks
  3. 点击 New Webhook,命名为“CaptchaAI Alerts”,方便和其他机器人区分
  4. 复制 webhook URL——这串地址本身就是访问凭证,不要提交到代码仓库
  5. 存成环境变量 DISCORD_WEBHOOK_URL,代码里只读取环境变量,不要硬编码

Python 实现:完整告警系统

下面这套实现覆盖四类场景:余额不足、错误率飙升、队列积压、每日汇总,外加一条“管道已恢复”的确认消息。COLORS 字典把严重程度映射成 embed 的边框颜色——绿色恢复正常,橙色需要关注,红色要马上处理。PipelineMonitor 在内存里滚动保存最近 5 分钟的结果,并用 cooldown 控制同一类告警的最短发送间隔,避免反复刷屏。

import os
import time
import requests
from datetime import datetime

API_KEY = os.environ["CAPTCHAAI_API_KEY"]
DISCORD_WEBHOOK = os.environ["DISCORD_WEBHOOK_URL"]

session = requests.Session()


class DiscordCaptchaAlerts:
    COLORS = {
        "success": 0x2ECC71,   # Green
        "warning": 0xF39C12,   # Orange
        "error": 0xE74C3C,     # Red
        "info": 0x3498DB,      # Blue
    }

    def __init__(self, webhook_url):
        self.webhook_url = webhook_url

    def send_embed(self, title, description, color_key="info", fields=None):
        embed = {
            "title": title,
            "description": description,
            "color": self.COLORS.get(color_key, self.COLORS["info"]),
            "timestamp": datetime.utcnow().isoformat() + "Z",
            "footer": {"text": "CaptchaAI Pipeline Monitor"}
        }
        if fields:
            embed["fields"] = fields

        payload = {"embeds": [embed]}
        resp = requests.post(
            self.webhook_url, json=payload, timeout=10
        )
        resp.raise_for_status()

    def balance_alert(self, balance, threshold):
        severity = "error" if balance < 2 else "warning"
        self.send_embed(
            title="💰 Balance Alert",
            description=f"CaptchaAI balance is **${balance:.2f}**",
            color_key=severity,
            fields=[
                {"name": "Threshold", "value": f"${threshold:.2f}", "inline": True},
                {"name": "Severity", "value": severity.upper(), "inline": True},
                {"name": "Action", "value": "Top up your balance at captchaai.com", "inline": False}
            ]
        )

    def error_spike(self, error_rate, error_count, total_count, top_errors):
        error_list = "\n".join(
            f"• `{code}`: {count}" for code, count in top_errors.items()
        )
        self.send_embed(
            title="⚠️ Error Rate Spike",
            description=f"Error rate: **{error_rate:.1%}** ({error_count}/{total_count})",
            color_key="error",
            fields=[
                {"name": "Error Breakdown", "value": error_list or "No details", "inline": False},
                {"name": "Window", "value": "Last 5 minutes", "inline": True}
            ]
        )

    def queue_alert(self, depth, workers_active):
        self.send_embed(
            title="📊 Queue Backup",
            description=f"Queue depth: **{depth}** pending tasks",
            color_key="warning",
            fields=[
                {"name": "Active Workers", "value": str(workers_active), "inline": True},
                {"name": "Est. Drain Time", "value": f"{depth // max(workers_active, 1)} min", "inline": True}
            ]
        )

    def daily_summary(self, stats):
        self.send_embed(
            title="📈 Daily CAPTCHA Summary",
            description=f"**{stats['total']}** tasks processed",
            color_key="success" if stats["success_rate"] > 0.92 else "warning",
            fields=[
                {"name": "Success Rate", "value": f"{stats['success_rate']:.1%}", "inline": True},
                {"name": "Avg Latency", "value": f"{stats['avg_latency']:.1f}s", "inline": True},
                {"name": "Total Cost", "value": f"${stats['cost']:.2f}", "inline": True},
                {"name": "Errors", "value": str(stats["errors"]), "inline": True},
                {"name": "Balance", "value": f"${stats['balance']:.2f}", "inline": True},
                {"name": "Peak Queue", "value": str(stats["peak_queue"]), "inline": True},
            ]
        )

    def solve_recovered(self, previous_rate, current_rate):
        self.send_embed(
            title="✅ Pipeline Recovered",
            description=f"Solve rate recovered: {previous_rate:.1%} → {current_rate:.1%}",
            color_key="success"
        )


alerts = DiscordCaptchaAlerts(DISCORD_WEBHOOK)


class PipelineMonitor:
    def __init__(self, check_interval=60):
        self.check_interval = check_interval
        self.results = []  # (timestamp, success, error_code)
        self.last_balance_alert = 0
        self.last_error_alert = 0
        self.cooldown = 300  # 5 minutes between alerts

    def record(self, success, error_code=None):
        self.results.append((time.time(), success, error_code))
        # Keep last 5 min
        cutoff = time.time() - 300
        self.results = [r for r in self.results if r[0] > cutoff]

    def run_checks(self):
        now = time.time()

        # Balance check
        if now - self.last_balance_alert > self.cooldown:
            balance = self._check_balance()
            if balance is not None and balance < 10:
                alerts.balance_alert(balance, threshold=10)
                self.last_balance_alert = now

        # Error rate check
        if now - self.last_error_alert > self.cooldown and len(self.results) > 10:
            total = len(self.results)
            errors = [r for r in self.results if not r[1]]
            error_rate = len(errors) / total

            if error_rate > 0.15:
                error_breakdown = {}
                for _, _, code in errors:
                    if code:
                        error_breakdown[code] = error_breakdown.get(code, 0) + 1
                alerts.error_spike(error_rate, len(errors), total, error_breakdown)
                self.last_error_alert = now

    def _check_balance(self):
        try:
            resp = session.get("https://ocr.captchaai.com/res.php", params={
                "key": API_KEY, "action": "getbalance", "json": 1
            })
            data = resp.json()
            if data.get("status") == 1:
                return float(data["request"])
        except Exception:
            pass
        return None


monitor = PipelineMonitor()

JavaScript 实现:Webhook 客户端

如果管道跑在 Node.js 里,逻辑是一样的:用 axios 把 embed 结构 POST 到 webhook 地址,monitorBalance 每 5 分钟检查一次余额,低于阈值就触发告警。接到你现有的定时任务或 setInterval 循环里即可,不需要额外引入消息队列。

const axios = require("axios");

const API_KEY = process.env.CAPTCHAAI_API_KEY;
const DISCORD_WEBHOOK = process.env.DISCORD_WEBHOOK_URL;

const COLORS = {
  success: 0x2ecc71,
  warning: 0xf39c12,
  error: 0xe74c3c,
  info: 0x3498db,
};

async function sendDiscordEmbed(title, description, colorKey = "info", fields = []) {
  await axios.post(DISCORD_WEBHOOK, {
    embeds: [
      {
        title,
        description,
        color: COLORS[colorKey] || COLORS.info,
        timestamp: new Date().toISOString(),
        footer: { text: "CaptchaAI Pipeline Monitor" },
        fields,
      },
    ],
  }, { timeout: 10000 });
}

async function alertBalance(balance, threshold = 10) {
  const severity = balance < 2 ? "error" : "warning";
  await sendDiscordEmbed(
    "💰 Balance Alert",
    `CaptchaAI balance is **$${balance.toFixed(2)}**`,
    severity,
    [
      { name: "Threshold", value: `$${threshold.toFixed(2)}`, inline: true },
      { name: "Severity", value: severity.toUpperCase(), inline: true },
    ]
  );
}

async function alertErrorSpike(errorRate, details = {}) {
  await sendDiscordEmbed(
    "⚠️ Error Rate Spike",
    `Error rate: **${(errorRate * 100).toFixed(1)}%**`,
    "error",
    [
      { name: "Total Tasks", value: String(details.total || 0), inline: true },
      { name: "Errors", value: String(details.errors || 0), inline: true },
    ]
  );
}

async function sendDailySummary(stats) {
  const color = stats.successRate > 0.92 ? "success" : "warning";
  await sendDiscordEmbed(
    "📈 Daily CAPTCHA Summary",
    `**${stats.total}** tasks processed`,
    color,
    [
      { name: "Success Rate", value: `${(stats.successRate * 100).toFixed(1)}%`, inline: true },
      { name: "Avg Latency", value: `${stats.avgLatency.toFixed(1)}s`, inline: true },
      { name: "Balance", value: `$${stats.balance.toFixed(2)}`, inline: true },
    ]
  );
}

// Balance monitoring loop
async function monitorBalance() {
  try {
    const resp = await axios.get("https://ocr.captchaai.com/res.php", {
      params: { key: API_KEY, action: "getbalance", json: 1 },
    });
    if (resp.data.status === 1) {
      const balance = parseFloat(resp.data.request);
      if (balance < 10) await alertBalance(balance);
    }
  } catch (err) {
    console.error("Balance check failed:", err.message);
  }
}

setInterval(monitorBalance, 300000); // Every 5 minutes

module.exports = { alertBalance, alertErrorSpike, sendDailySummary };

Discord 告警效果预览

接入之后,频道里大致会收到这样的消息:

场景 消息内容
余额告警 💰 Balance Alert —— CaptchaAI balance is $8.42(Threshold: $10.00,Severity: WARNING)
错误率飙升 ⚠️ Error Rate Spike —— Error rate: 22.5% (45/200),ERROR_CAPTCHA_UNSOLVABLE: 30,TIMEOUT: 15
每日汇总 📈 Daily CAPTCHA Summary —— 12,450 tasks processed,Success Rate: 95.2%,Avg Latency: 22.4s,Balance: $142.30

常见故障排查

接入后最容易踩的坑,基本逃不出这四类:

问题 原因 处理方式
400 Bad Request embed 结构不合法 检查 fields 数组格式,确认所有取值都是字符串
触发限流 (429) 单位时间内消息发送过多 在同类告警之间加冷却时间(至少 5 分钟)
webhook 已失效 有人在服务器里把它删除了 重新创建一个 webhook,更新环境变量
embed 不显示 缺少 embeds 数组 把 embed 对象包在 {"embeds": [...]}

常见问题

Discord webhook 每分钟最多能发多少条消息?会被限流吗?

Discord 对同一个 webhook 有速率限制,短时间内发太多消息会收到 429。做法是给同类告警加冷却时间——上面的实现里 cooldown = 300,同一类告警 5 分钟内最多触发一次,基本能避开限流。

webhook URL 泄露了怎么办?

webhook URL 本身就是访问凭证,任何人拿到它都能往你的频道发消息。一旦怀疑泄露,直接在 Discord 后台删除这个 webhook、重新创建一个,再更新 DISCORD_WEBHOOK_URL 环境变量,旧地址会立刻失效。

可以在告警里 @提及 具体成员或身份组吗?

可以。在 payload 里加一个 "content": "<@USER_ID>""content": "<@&ROLE_ID>" 字段即可。建议只给余额耗尽、错误率飙升这类需要马上处理的告警加 @提及,日常汇总不用打扰人。

生产环境的关键告警,能只靠 Discord 吗?

不建议。Discord 适合做团队可见性强的辅助通知,但没有升级、排班这些机制。真正需要值班响应的告警,还是应该走 PagerDuty 或 Opsgenie,Discord 作为补充渠道保留历史记录就够了。

embed 消息的颜色和字段可以自定义吗?

可以,COLORS 字典和 fields 数组都是你自己定义的。除了教程里的余额、错误率、队列、每日汇总,你也可以按自己管道的关键指标加新的 embed 类型,比如单个 CAPTCHA 类型的识别耗时。

下一步

把管道告警接到团队本来就在用的地方——获取 CaptchaAI API Key,然后按上面的步骤连上 Discord。

相关指南:

该文章已禁用评论。