如何在 Huey 任务中识别验证码:在 Windows 上使用 SQLite

huey captcha - How to Solve CAPTCHAs in Huey Tasks With SQLite on Windows

要在 Huey 中识别验证码,就把队列放在 tasks 模块旁边的一个 SqliteHuey 文件里,在带重试的任务中调用 CapSkip,并用线程 worker 启动消费者(consumer)。在 Windows 上,整套技术栈就是这些:一个 SQLite 文件,不需要 Redis 服务器,识别工具也在同一台电脑上。这套 Huey 验证码识别方案有四个陷阱,本指南会逐一讲到。消费者是一个 exe,而不是 huey_consumer.py。Huey 的 timeout 选项在线程 worker 上不起作用,所以一次卡住的识别会永远占住一个 worker。在默认设置下,Huey 还在重试时,读取结果就会报告第一次失败的尝试。另外,CancelExecution 的第一个参数是 retry,所以看似理所当然的停止重试写法,反而可能让任务无休止地重试。

你需要什么

  • 在 Windows 机器上运行的 CapSkip,并已开启 reCAPTCHA 识别功能。
  • Python 3.10 或更高版本,这是 CapSkip SDK 的最低要求。Huey 3.4.0 自己没有声明最低版本。下面的所有内容都在 Python 3.13.7 和 3.14.0 上,配合 huey 3.4.0 和 capskip 1.3.0 运行过。
  • 不需要 Redis。SqliteHuey 把队列、调度计划、锁和结果都保存在一个 SQLite 文件中。
  • 识别工具的地址。在 Local 模式下,CapSkip 只在 127.0.0.1 上响应,仅供本机使用。在 Server 模式下,它监听你的网络地址或公网 IP,这样另一台机器上的消费者就能通过 API 调用它。两种模式的设置位置都是 连接设置.

第 1 步:安装 Huey 和 CapSkip SDK

在项目文件夹中创建一个虚拟环境,并安装三个包。requests 包有两个用处:完整示例用它获取并提交登录页面,后面的超时修复也会用到它。

# Windows, in PowerShell or cmd: a virtual environment in your project folder
python -m venv .venv
.venv\Scripts\python -m pip install huey capskip requests

第 2 步:创建队列和识别客户端

Huey 需要的一切都在 tasks.py 里。消费者导入它来找到各个任务,每个往队列里放任务的脚本也要导入它。

# tasks.py
import os
from pathlib import Path

from capskip import CapSkip
from huey import SqliteHuey

# The whole queue is one SQLite file. Keep it next to this module, so the
# consumer and every script that imports tasks use the same file, whatever
# folder they are started from.
huey = SqliteHuey(
    "captcha",
    filename=str(Path(__file__).with_name("huey.db")),
    store_intermediate_errors=False,  # get() waits for the last retry
)

# Local mode: CapSkip on this PC. Server mode: set CAPSKIP_HOST (and
# CAPSKIP_API_KEY if key validation is on) to reach it on another machine.
solver = CapSkip(
    apiKey=os.environ.get("CAPSKIP_API_KEY", "capskip"),
    host=os.environ.get("CAPSKIP_HOST", "127.0.0.1"),
    port=int(os.environ.get("CAPSKIP_PORT", "8080")),
)

像上面那样,用完整路径指定数据库文件。默认文件名是 huey.db,位于进程启动时所在的文件夹中。这样一来,从另一个文件夹启动的脚本会创建它自己的空 huey.db,把任务放进那里的队列,然后等待一个永远不会到来的结果。把文件固定在 tasks.py 旁边,所有进程用的就是同一个队列。消费者运行期间,它旁边会有两个辅助文件:huey.db-wal 和 huey.db-shm。它们属于 SQLite 的预写日志(write-ahead log),是正常现象,强制停止后留在原处也没关系。

SQLite 扛住了负载,没有出错。12 个 worker,加上三个合计入队 600 个任务的脚本,都没有遇到提示数据库被锁定(database is locked)的错误。在每个进程内部,Huey 通过一把锁共享同一个连接;在进程之间,SQLite 会为这个文件最多等待 5 秒。关于这个文件,有一点要知道:任务参数和结果都是经 pickle 序列化后以明文字节写进去的。作为参数传入的密码在表中可以直接读出来,所以要把机密信息放在环境变量或 tasks.py 里,而不是放在任务参数中。

第 3 步:编写 Huey 验证码识别任务,并决定重试哪些失败

Huey 任务抛出的每个异常都会用掉一次重试。这对某些 CapSkip 失败来说是对的,对另一些则是浪费,所以任务会对它们分类处理。

from capskip import ApiException, ValidationException
from huey import CancelExecution


# Any exception uses one of the two retries, 15 s apart: NetworkException
# when CapSkip is not running, TimeoutException, an unsolvable task.
@huey.task(retries=2, retry_delay=15)
def solve_recaptcha(sitekey, page_url):
    try:
        return solver.recaptcha(sitekey=sitekey, url=page_url)["code"]
    except (ApiException, ValidationException) as exc:
        if "ERROR_CAPTCHA_UNSOLVABLE" in str(exc):
            raise  # this attempt failed; a fresh one may not
        # ERROR_GOOGLEKEY, ERROR_PAGEURL, ERROR_BAD_PARAMETERS and
        # ERROR_KEY_DOES_NOT_EXIST fail the same way on every attempt.
        # CancelExecution's first argument is retry: False ends the task now.
        raise CancelExecution(False, f"{type(exc).__name__}: {exc}") from exc

有三种失败值得再试一次:CapSkip 还没运行时的 NetworkException、TimeoutException,以及 ERROR_CAPTCHA_UNSOLVABLE,后者是 CapSkip 报告这次识别没有成功的方式。消费者会记录这个错误及其 traceback,然后再记一行,说它正在把任务重新入队,并给出剩余的重试次数。在我们的测试中,retry_delay 为 15 秒时,下一次尝试在失败后 16.1 秒开始。

格式错误的 sitekey、不带 http 或 https 的页面 URL,或者错误的 API 密钥(在开启密钥校验时),都会在任何识别开始之前被拒绝,分别返回 ERROR_GOOGLEKEY、ERROR_PAGEURL 或 ERROR_KEY_DOES_NOT_EXIST。空的 sitekey 或页面 URL 会得到 ERROR_BAD_PARAMETERS。这些错误每次都以同样的方式失败,所以重试只是浪费时间。Huey 的 CancelExecution 只有在 retry 为 False 时,才会结束任务而不重试。不带参数直接抛出时,只要任务还有剩余重试次数,它仍然会重试。还要看清它的签名:它的第一个位置参数是 retry,而不是消息。如果写成只带一条消息的 CancelExecution,这条消息就会成为 retry 的值,并被视为真。日志随后会说这个任务将被重试,任务会一遍又一遍地运行,直到你停止消费者。把消息和 retry=False 一起传入会引发 TypeError,因为这样 retry 会得到两个值。要像代码里那样,把 False 放在第一个。这时消费者会记录一条 WARNING,说任务抛出了 CancelExecution,不会被重试。

这种分类有一个局限:被 Google 自己拒绝的 sitekey,到你这里时仍然是 ERROR_CAPTCHA_UNSOLVABLE,因为 CapSkip 会这样报告每一次失败的识别。这种情况要耗掉三次尝试才会失败。如果某个页面每次都失败,先检查它的 sitekey。

第 4 步:在 Windows 上启动消费者

在存放 tasks.py 的文件夹中运行消费者。按路径调用这个 exe 时,不需要激活虚拟环境。

# In the folder that holds tasks.py, in PowerShell or cmd
.venv\Scripts\huey_consumer tasks.huey -w 4

较老的指南以及消费者自己的用法提示,写的都是 huey_consumer.py。Huey 3 只安装 huey_consumer.exe,所以在 PowerShell 和 cmd 中用那个名称都会失败,提示无法识别该命令。上面的命令在两者中都能用,.venv\Scripts\python -m huey.bin.huey_consumer tasks.huey 也可以。如果从另一个文件夹启动,消费者会停止,报出 Error importing tasks.huey 和 ModuleNotFoundError: No module named tasks,因为它只把当前文件夹加入搜索路径。

在 Windows 上,线程 worker 是唯一现实的选择。使用 -k process 时,消费者会立即退出,报错说 huey cannot be run in process-mode on Windows,因为进程 worker 需要 fork(),而 Windows 没有它。Greenlet worker 则需要安装 gevent。无论如何,线程都适合这项工作,因为 worker 在一次识别中几乎所有时间都在等待 CapSkip。

每个 worker 发送自己的请求,所以 worker 的数量也就是 CapSkip 同时收到的识别数量。12 个 worker 处理 12 个排队任务时,12 个请求在 30 毫秒内全部到达了 CapSkip。CapSkip 的 reCAPTCHA 部分默认同时进行 10 个识别(由 Max. Threads 设置),其余的排队等待。少量登录用四个 worker 就够了。队列很长时,把 -w 往这个上限调高。

第 5 步:把识别放入队列,并只读取一次结果

调用任务并不会运行它。调用只会把任务写入队列,并返回一个可以等待的 Result 句柄。

# enqueue.py, next to tasks.py: .venv\Scripts\python enqueue.py
from huey.exceptions import ResultTimeout, TaskException

from tasks import huey, solve_recaptcha

result = solve_recaptcha("YOUR_SITEKEY", "https://example.com/login")
try:
    # Three attempts of up to about 5.5 minutes each, plus two 15 s delays.
    token = result.get(blocking=True, timeout=1100)
except TaskException as exc:
    print("solve failed:", exc)
except ResultTimeout:
    print("no result yet: is the consumer running?")
else:
    print("token:", token[:30], "...")
    # Reading the result deleted it from huey.db. This handle keeps its
    # copy, but a new handle for the same task id finds nothing.
    print("read again:", huey.result(result.id))  # None

1100 秒的等待涵盖了最坏情况:三次尝试,每次都跑满 SDK 的时限,再加上两次重试延迟。失败的任务会抛出 TaskException,它的消息是最后一次的错误,它的 metadata 里有剩余重试次数和 traceback。如果到时没有任何结果,你会得到 ResultTimeout,这通常意味着消费者没有运行,或者用的是另一个数据库文件。

第 2 步中 store_intermediate_errors 那一行在这里就派上用场了。在 Huey 的默认设置下,每次失败的尝试都会被存为结果。在我们的测试中,Huey 还在运行重试时,get() 在第一次失败后就抛出了 TaskException。只有在句柄上调用 reset() 之后,才能看到重试的结果。把这个选项关掉后,get() 会等待最后一次尝试。

一个结果只能读取一次。读取会把它从文件中删除,就像 CapSkip 对待它自己的答案一样。读取它的那个句柄会保留一份副本,但之后用同一个任务 id 调用 huey.result() 会返回 None。如果代码中有两处需要这个值,就给 get() 传入 preserve=True。返回 None 的任务根本不会存储任何东西,所以等待它的结果会以 ResultTimeout 告终。

为什么卡住的识别需要自己的超时

Huey 3 允许给任务设置 timeout 选项,但在线程 worker 上它不起作用。线程无法从外部中断,所以只有任务自己调用 task.check_timeout() 时,Huey 才会检查它。一个设置了 timeout=2、却睡眠了五秒的任务正常完成了。能够强制执行超时的进程 worker,在 Windows 上会被拒绝。

这一点很重要,因为 CapSkip SDK 不给它的请求设置任何 HTTP 超时。如果 CapSkip 接受了连接却始终不应答,这次调用就会永远等下去。在我们的测试中,两个 worker 就停在这种状态,没有任何一行日志,排在它们后面的任务也一直没有开始。识别工具恢复 45 秒后,它们仍然卡着。修复办法是给 SDK 的 HTTP 调用设置超时。把下面的代码放在 tasks.py 的模块级别,这样它会在任何识别之前运行。

import functools
import types

import requests
import capskip.api

# capskip 1.3.0 calls requests.post/get with no timeout, and Huey cannot stop
# a thread worker that is stuck in one. Give every SDK call 5 s to connect
# and 30 s to answer. Module level in tasks.py, so it runs before any solve.
capskip.api.requests = types.SimpleNamespace(
    RequestException=requests.RequestException,
    post=functools.partial(requests.post, timeout=(5, 30)),
    get=functools.partial(requests.get, timeout=(5, 30)),
)

有了它,挂起的提交会在 30.0 秒后抛出 NetworkException,任务随后被重试。挂起的状态查询持续时间更长,因为 SDK 把一次失败的轮询当成尚未就绪,会一直轮询到它自己 300 秒的 recaptchaTimeout 为止。那次尝试在大约 333 秒后以 TimeoutException 结束,而重试识别成功了。无论哪种情况,worker 都会重新空闲下来。这个修复替换的是 capskip.api 中的 requests 引用,capskip.api 属于 SDK 的内部实现,所以升级 capskip 后要再检查一遍。

不会自我重叠的定时识别

周期任务可以按定时器重复进行识别,比如用来让登录状态保持有效。下面的完整示例会执行实际的登录。周期任务不接受参数,所以 sitekey 和 URL 写在函数里。

from huey import crontab


# Every 15 minutes. The lock goes under periodic_task: when a run is due
# while the previous one is still solving, another worker skips it instead
# of starting a second solve for the same login.
@huey.periodic_task(crontab(minute="*/15"))
@huey.lock_task("login")
def refresh_login():
    result = solver.recaptcha(sitekey="YOUR_SITEKEY", url="https://example.com/login")
    print("new token:", result["code"][:30], "...")

消费者启动时会检查一次它的周期任务,之后从启动时刻算起每 60 秒检查一次。所以在当前这一分钟到期的运行会立即开始。如果一次识别耗时超过间隔,Huey 仍会把下一次运行放入队列。有多个 worker 时,第一次运行还在识别,空闲的 worker 就会启动下一次。锁可以阻止这种情况:新的运行会记录一条 WARNING,说它因为无法获取锁而没有运行,然后被跳过。只有一个 worker 时不会出现重叠,但迟到的运行会在队列里堆积,然后一个接一个地执行。给 periodic_task 装饰器加上一个以秒为单位的 expires 参数,比如 expires=600,就能丢弃等待太久的运行。

锁保存在数据库文件中,所以崩溃后依然存在。强制停止之后,残留下来的锁会让之后的每一次运行都被跳过。用 -f 启动消费者可以清除装饰器锁,它会在日志里说发现了残留的锁。Huey 3.4 还给 lock_task 添加了 ttl 选项,但 SqliteHuey 不支持它,会在获取锁时抛出 NotImplementedError。

完整的 Huey 验证码识别任务文件

这个版本会获取登录页面,读取 sitekey 和 CSRF token,在提交之前立刻识别验证码,并带着 Referer 请求头提交表单。它以 log_in 提供按需登录,以 scheduled_login 提供定时登录。两者共用一把锁,所以它们永远不会同时为同一个账号进行识别。替换 URL 和登录凭据,然后用 -f 启动消费者。

# tasks.py: pip install huey capskip requests
# Start: .venv\Scripts\huey_consumer tasks.huey -w 4 -f
import functools
import logging
import os
import re
import types
from pathlib import Path

import requests
import capskip.api
from capskip import ApiException, CapSkip, ValidationException
from huey import CancelExecution, SqliteHuey, crontab

LOGIN_URL = "https://example.com/login"
USERNAME, PASSWORD = "YOUR_USERNAME", "YOUR_PASSWORD"

huey = SqliteHuey(
    "captcha",
    filename=str(Path(__file__).with_name("huey.db")),  # same file from any folder
    store_intermediate_errors=False,  # get() waits for the last retry
)
log = logging.getLogger("huey.captcha")  # printed by the consumer

# The SDK's HTTP calls have no timeout; give them one so a stuck CapSkip
# cannot hold a worker thread forever.
capskip.api.requests = types.SimpleNamespace(
    RequestException=requests.RequestException,
    post=functools.partial(requests.post, timeout=(5, 30)),
    get=functools.partial(requests.get, timeout=(5, 30)),
)
solver = CapSkip(
    apiKey=os.environ.get("CAPSKIP_API_KEY", "capskip"),
    host=os.environ.get("CAPSKIP_HOST", "127.0.0.1"),
    port=int(os.environ.get("CAPSKIP_PORT", "8080")),
)


def solve(sitekey, page_url):
    try:
        return solver.recaptcha(sitekey=sitekey, url=page_url)["code"]
    except (ApiException, ValidationException) as exc:
        if "ERROR_CAPTCHA_UNSOLVABLE" in str(exc):
            raise  # worth another attempt
        # A refused sitekey, page URL or API key fails the same way every time.
        log.error("not retrying: %s: %s", type(exc).__name__, exc)
        raise CancelExecution(False, f"{type(exc).__name__}: {exc}") from exc


def field(html, pattern):
    match = re.search(pattern, html)
    if not match:
        raise ValueError(f"not found on the login page: {pattern}")
    return match.group(1)


def login():
    with requests.Session() as session:
        page = session.get(LOGIN_URL, timeout=30)
        sitekey = field(page.text, r'data-sitekey="([^"]+)"')
        csrf = field(page.text, r'name="csrfmiddlewaretoken" value="([^"]+)"')
        token = solve(sitekey, LOGIN_URL)  # solve right before the post
        reply = session.post(LOGIN_URL, timeout=30, headers={"Referer": LOGIN_URL}, data={
            "csrfmiddlewaretoken": csrf,
            "username": USERNAME,
            "password": PASSWORD,
            "g-recaptcha-response": token,
        })
        reply.raise_for_status()  # a refused login fails the attempt
        log.info("logged in (HTTP %s)", reply.status_code)
        return reply.status_code


# Both tasks take the same lock, so an on-demand login never runs while a
# scheduled one is solving, and the other way round.

# On demand: from tasks import log_in; log_in().get(blocking=True, timeout=1100)
@huey.task(retries=2, retry_delay=15)  # a locked attempt uses a retry
@huey.lock_task("login")
def log_in():
    return login()


# On a schedule; a run that comes due while the lock is held is skipped.
@huey.periodic_task(crontab(minute="*/15"))
@huey.lock_task("login")
def scheduled_login():
    login()

识别和提交发生在同一次尝试中,所以每次重试都会获取一个新页面并重新识别。token 永远不会从失败的尝试中沿用下来,这一点很重要,因为 Google 只让 reCAPTCHA token 存活两分钟。被拒绝的登录如果返回 HTTP 错误(比如 403),会通过 raise_for_status() 让这次尝试失败,然后重试两次,每次都用新页面、新识别。CapSkip 不按次收费,所以这样做除了时间之外没有任何成本。很多站点对错误的密码会返回 200 并再次显示表单,所以要为你的站点加一项检查,比如检查提交后的 URL。按需调用的 log_in 如果发现定时运行正持有锁,就会以同样的 WARNING 被跳过,而那次尝试会用掉一次重试。两次间隔 15 秒的重试,让它总共大约有 30 秒的等待时间。如果定时识别耗时更长,三次尝试都会被跳过,get() 会抛出 TaskException,其消息中带有 TaskLockedException。如果你的识别通常比这更久,就调高 log_in 的 retry_delay。

要从另一个脚本按需登录,就从 tasks 导入 log_in,调用它,然后像第 5 步那样用 get(blocking=True, timeout=1100) 等待结果。返回值是提交后最终的 HTTP 状态。任务输出的日志行会和 Huey 自己的日志一起显示在消费者的窗口中。

干净地停止消费者

在消费者的窗口中按一次 Ctrl+C。Huey 会停止接收新任务,等待正在运行的任务完成,然后退出。在我们的测试中,在识别中途被停止的消费者,在 18 秒后、也就是那次识别一完成就退出了。空闲的消费者不到十分之一秒就退出了。第二次按 Ctrl+C 的行为因版本而异:Python 3.14 会立即退出,而 3.13 仍会等待正在运行的任务,所以如果没有上面的超时设置,一次卡住的识别就只能靠强行结束进程来解决。

任何比优雅停止更快的方式,都会丢掉正在运行的任务。这包括在 3.14 上第二次按 Ctrl+C、按 Ctrl+Break,以及使用 -t 选项,它会限制优雅停止的等待时间:使用 -t 5 时,消费者在按下 Ctrl+C 后大约 5.1 秒退出。丢失的任务已经离开了队列,所以它不会再次运行,而它的锁会残留下来。这就是完整示例要用 -f 启动的原因。

在另一台机器上运行 CapSkip

消费者和识别工具不必在同一台电脑上。在连接设置里把 CapSkip 切换到 Server 模式,它就会监听你的网络地址或公网 IP。你的 tasks.py 已经从环境变量中读取地址,所以在启动消费者之前设置好这些变量即可。

# PowerShell, before starting the consumer: CapSkip runs in Server mode
# on another Windows machine on your network
$env:CAPSKIP_HOST = "192.168.1.50"
$env:CAPSKIP_API_KEY = "YOUR_API_KEY"
# $env:CAPSKIP_PORT = "8080"   only if you changed the port
.venv\Scripts\huey_consumer tasks.huey -w 4 -f

如果链路要经过公网,请使用静态公网 IP,并打开 API 密钥校验,在 CAPSKIP_API_KEY 中使用同一个密钥。Server 模式启动时,CapSkip 会向所有地址开放端口,所以要想只让你预期的机器访问,就为其他地址添加一条 Windows Firewall 阻止规则。这样,消费者就可以运行在任何能访问该端口的机器上,而识别工具仍然是你自己的 Windows 机器,识别也不计量。

Huey 验证码识别的常见错误

你所看到的原因修复
提示无法识别 huey_consumer.pyHuey 3 只安装 huey_consumer.exe运行 .venv\Scripts\huey_consumer
huey cannot be run in process-mode on Windows进程 worker 需要 fork()使用默认的线程 worker
ModuleNotFoundError: No module named tasks消费者不是在存放 tasks.py 的文件夹中启动的从那个文件夹启动它
消费者在运行,却出现 ResultTimeout脚本和消费者用的是不同的 huey.db 文件给 SqliteHuey 传入文件的完整路径
get() 报告失败,之后任务却成功了store_intermediate_errors 处于开启状态,所以每次失败的尝试都会被存储设置 store_intermediate_errors=False
本该停止的任务一直在重试CancelExecution 把消息当成了它的 retry 参数抛出 CancelExecution(False, message)
worker 不再领取任务,也不记录任何日志某次识别卡在一个没有超时的 HTTP 调用上给 SDK 的请求设置超时
每一次定时运行都被跳过被强行终止的消费者残留下来的锁用 -f 重新启动消费者

常见问题

可以用 RedisHuey 代替 SqliteHuey 吗?

可以。本指南中的任务、重试和锁都以同样的方式工作。再安装 redis 包,然后用 RedisHuey 代替 SqliteHuey 创建队列。不过,RedisHuey 需要一台 Redis 服务器,而 Windows 并不自带。在已经运行着 CapSkip 的电脑上,用 SQLite 文件就省得再运行第二个服务。如果队列要分布在多台机器上,Redis 更合适,可参考这篇 在 Celery 任务中识别验证码的指南.

为什么空闲一段时间后的第一个任务会晚开始?

空闲的消费者检查数据库的频率会越来越低,默认最长每 10 秒检查一次。在我们的测试中,用四个 worker 时,空闲一段时间后的第一个任务等待了 3 到 7 秒。用 -m 1 启动消费者,可以把这段等待的上限设为一秒;这样等待时间就一直保持在一秒以内。

这在 Django 项目中能用吗?

能,通过 huey.contrib.djhuey 即可。在 HUEY 设置中把 huey_class 设为 huey.SqliteHuey,并设置一个 filename,然后用 manage.py run_huey 启动消费者。有一个默认行为会让人意外:开启 DEBUG 时,djhuey 会在请求内部立即运行任务,所以一次识别会让页面一直阻塞,直到 CapSkip 应答。要测试真正的队列,就在 HUEY 设置中把 immediate 设为 False。

消费者能和 CapSkip 运行在不同的机器上吗?

可以。在连接设置里把 CapSkip 切换到 Server 模式,让它监听网络地址而不是回环地址,并在消费者启动之前把 CAPSKIP_HOST 设为这个地址。这样,任务就能从任何可以访问该端口的机器上,通过同一套 HTTP API 调用它。为不该访问它的地址添加一条 Windows Firewall 阻止规则。识别工具仍然运行在你自己的硬件上,没有任何一次识别会经过第三方服务。

简短版结论

在 Windows 上编写 Huey 验证码识别任务时,把 SqliteHuey 文件固定在 tasks.py 旁边,关闭 store_intermediate_errors,只重试那些再试一次就可能解决的 CapSkip 失败,其余的用 CancelExecution(False, message) 结束。在存放 tasks.py 的文件夹中,以线程 worker 启动 .venv\Scripts\huey_consumer,给 SDK 的 HTTP 调用设置超时,记住每个结果只能读取一次,并给定时识别加上锁。官方的 Huey 消费者文档 列出了这里用到的每一个消费者选项。

一个文件就是整个队列,识别工具也在同一台电脑上,整项工作都留在你自己掌控的硬件上。这正是本地 验证码绕过的意义所在:每次重试都是免费的,所以任务可以放心地再试一次。