Как решать капчу в задачах Huey с SQLite в Windows

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

Чтобы решать капчу в Huey, храните очередь в файле SqliteHuey рядом с модулем задач, вызывайте CapSkip внутри задачи с повторами и запускайте консьюмер с потоковыми воркерами. В Windows этим весь стек и ограничивается: один файл SQLite, никакого сервера Redis и решатель на том же ПК. У такой схемы решения капчи в Huey четыре ловушки, и в этом руководстве разобрана каждая. Консьюмер представляет собой exe, а не huey_consumer.py. Опция timeout в Huey ничего не делает на потоковых воркерах, поэтому одно зависшее решение занимает воркер навсегда. Со значениями по умолчанию чтение результата сообщает о первой неудачной попытке, пока Huey ещё выполняет повторы. А CancelExecution принимает retry первым аргументом, поэтому очевидный способ прекратить повторы может, наоборот, повторять задачу бесконечно.

Что понадобится

  • CapSkip, запущенный на машине с Windows, с включённым решателем reCAPTCHA.
  • Python 3.10 или новее: это минимальная версия для SDK CapSkip. 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 и SDK CapSkip

Создайте виртуальное окружение в папке проекта и установите три пакета. Пакет 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 справился с нагрузкой без ошибок. Двенадцать воркеров и три скрипта, которые вместе поставили в очередь 600 задач, не получили ни одной ошибки о том, что база данных заблокирована. Внутри каждого процесса 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

Три сбоя стоят ещё одной попытки: NetworkException, когда CapSkip ещё не запущен, TimeoutException и ERROR_CAPTCHA_UNSOLVABLE, которым CapSkip сообщает, что решение на этот раз не удалось. Консьюмер пишет в лог ошибку с traceback, а затем строку о том, что снова ставит задачу в очередь, с числом оставшихся повторов. При retry_delay в 15 секунд следующая попытка в наших запусках начиналась через 16.1 секунды после сбоя.

Некорректный sitekey, URL страницы без http или https и неверный API-ключ (при включённой проверке ключа) отклоняются ещё до начала решения с ошибками ERROR_GOOGLEKEY, ERROR_PAGEURL или ERROR_KEY_DOES_NOT_EXIST. Пустой sitekey или URL страницы получает ERROR_BAD_PARAMETERS. Такие запросы каждый раз падают одинаково, поэтому повторы только тратят время. CancelExecution в Huey завершает задачу без повтора, только когда retry равен False. Выброшенный без аргументов, он всё равно повторяет задачу, пока у неё остаются повторы. Прочитайте и его сигнатуру: первый позиционный аргумент у него retry, а не сообщение. Если написать CancelExecution с одним только сообщением, сообщение становится значением retry и считается истинным. Тогда в логе написано, что задача будет повторена, и задача выполняется снова и снова, пока вы не остановите консьюмер. Передача сообщения вместе с retry=False выбрасывает TypeError, потому что тогда retry получает два значения. Передавайте False первым, как это сделано в коде. Тогда консьюмер пишет в лог WARNING о том, что задача выбросила CancelExecution и не будет повторена.

У такой сортировки есть ограничение: sitekey, который отклоняет сам Google, всё равно приходит к вам как 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 реальный выбор только один: потоковые воркеры. С -k process консьюмер сразу завершается с ошибкой huey cannot be run in process-mode on Windows, потому что процессным воркерам нужен fork(), которого в Windows нет. Воркерам на greenlet нужен установленный gevent. Потоки и так подходят для этой работы, ведь воркер почти всё время решения ждёт CapSkip.

Каждый воркер отправляет собственный запрос, поэтому число воркеров равно и числу решений, которые CapSkip получает одновременно. С 12 воркерами и 12 задачами в очереди все 12 запросов дошли до CapSkip в пределах 30 миллисекунд. Раздел reCAPTCHA в CapSkip по умолчанию выполняет 10 решений одновременно (настройка Max. Threads), а остальные ставит в очередь. Для нескольких входов хватит четырёх воркеров. Когда очередь длинная, поднимайте -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, сообщение которого содержит последнюю ошибку, а метаданные хранят число оставшихся повторов и traceback. Если вовремя ничего не пришло, вы получаете ResultTimeout, и обычно это значит, что консьюмер не запущен или использует другой файл базы данных.

Здесь важна строка store_intermediate_errors из шага 2. Со значением по умолчанию в Huey каждая неудачная попытка сохраняется как результат. В нашем тесте get() выбросил TaskException после первого сбоя, пока Huey выполнял повтор. Результат повтора стал виден только после вызова reset() у объекта Result. Если эту опцию выключить, get() ждёт последней попытки.

Результат можно прочитать один раз. Чтение удаляет его из файла, так же как CapSkip поступает со своими ответами. Объект Result, который его прочитал, сохраняет свою копию, но huey.result() с тем же id задачи после этого возвращает None. Если значение нужно двум частям вашего кода, передайте preserve=True в get(). Задача, которая возвращает None, вообще ничего не сохраняет, поэтому ожидание её результата заканчивается ResultTimeout.

Почему зависшему решению нужен собственный таймаут

Huey 3 принимает у задачи опцию timeout, но на потоковых воркерах она ничего не делает. Поток нельзя прервать снаружи, поэтому Huey проверяет таймаут, только если сама задача вызывает task.check_timeout(). Задача с timeout=2, которая спала пять секунд, завершилась нормально. А процессные воркеры, где таймаут можно было бы применить принудительно, Huey в Windows запускать отказывается.

Это важно, потому что SDK CapSkip не задаёт HTTP-таймаут для своих запросов. Если CapSkip принимает соединение и так и не отвечает, вызов ждёт вечно. В нашем тесте два воркера висели в таком состоянии без единой строки в логе, а задачи, стоявшие за ними в очереди, так и не начались. Они всё ещё висели через 45 секунд после того, как решатель пришёл в себя. Исправление состоит в том, чтобы задать таймаут HTTP-вызовам SDK. Поместите этот код в 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)),
)

С ним зависшая отправка задачи выбросила NetworkException через 30.0 секунды, и задача была повторена. Зависшая проверка статуса длится дольше, потому что SDK принимает неудачный опрос за признак того, что ответ ещё не готов, и продолжает опрашивать до собственного recaptchaTimeout в 300 секунд. Эта попытка закончилась TimeoutException примерно через 333 секунды, а повтор решил капчу. В любом случае воркер снова освобождается. Исправление подменяет ссылку на 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 всё равно ставит следующий запуск в очередь. При нескольких воркерах свободный воркер начал бы его, пока первый запуск ещё решает капчу. Блокировка это предотвращает: новый запуск пишет в лог WARNING о том, что он не выполнен, так как не удалось получить блокировку, и пропускается. С одним воркером ничего не пересекается, но запоздавшие запуски копятся в очереди, а потом выполняются один за другим. Чтобы отбрасывать запуски, которые ждали слишком долго, добавьте в декоратор periodic_task аргумент expires в секундах, например expires=600.

Блокировки хранятся в файле базы данных, поэтому переживают падение. После жёсткой остановки оставшаяся блокировка заставляет пропускаться каждый следующий запуск. Запустите консьюмер с -f, чтобы сбросить блокировки декораторов, и он запишет в лог, что нашёл устаревшие блокировки. В Huey 3.4 у lock_task также появилась опция ttl, но SqliteHuey её не поддерживает и выбрасывает NotImplementedError в момент захвата блокировки.

Полный файл задачи Huey для решения капчи

Эта версия загружает страницу входа, читает sitekey и CSRF-токен, решает капчу прямо перед отправкой и отправляет форму с заголовком 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()

Решение происходит в той же попытке, что и отправка, поэтому повтор всегда загружает свежую страницу и решает капчу заново. Токен никогда не переносится из неудачной попытки, и это важно, потому что Google даёт токену reCAPTCHA жить две минуты. Отклонённый вход, который возвращает ошибку HTTP, например 403, проваливает попытку через raise_for_status() и повторяется дважды, каждый раз с новой страницей и новым решением. CapSkip не берёт плату за каждое решение, поэтому это стоит только времени. Многие сайты отвечают на неверный пароль кодом 200 и снова показывают форму, поэтому добавьте проверку под свой сайт, например URL после отправки. Вызов log_in по запросу, который застаёт запланированный запуск с захваченной блокировкой, пропускается с тем же WARNING, и эта попытка расходует повтор. Два повтора с интервалом 15 секунд дают ему в сумме около 30 секунд ожидания. Если запланированное решение длится дольше, все три попытки пропускаются, и get() выбрасывает TaskException с TaskLockedException в сообщении. Поднимите retry_delay у log_in, если ваши решения обычно длятся дольше.

Чтобы выполнить вход по запросу из другого скрипта, импортируйте log_in из tasks, вызовите его и дождитесь результата через get(blocking=True, timeout=1100), как в шаге 5. Возвращаемое значение равно итоговому HTTP-статусу после отправки. Строки лога из задачи появляются в окне консьюмера рядом с собственными строками Huey.

Корректная остановка консьюмера

Нажмите Ctrl+C один раз в окне консьюмера. Huey перестаёт брать новые задачи, ждёт выполняющиеся и завершается. В наших запусках консьюмер, остановленный посреди решения, завершился через 18 секунд, как только это решение закончилось. Простаивающий консьюмер завершался меньше чем за десятую долю секунды. Второе нажатие Ctrl+C ведёт себя по-разному в зависимости от версии: Python 3.14 завершается сразу, а 3.13 всё равно ждёт выполняющуюся задачу, поэтому зависшее решение без описанного выше таймаута можно было бы только убить.

Любая остановка быстрее корректной теряет выполнявшуюся задачу. Сюда относятся второе нажатие Ctrl+C на 3.14, Ctrl+Break и опция -t, которая ограничивает время корректного ожидания: с -t 5 консьюмер завершился примерно через 5.1 секунды после Ctrl+C. Потерянная задача уже покинула очередь, поэтому снова не выполняется, а её блокировка остаётся. Именно поэтому полный пример запускается с -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.py не распознаётся как командаHuey 3 устанавливает только huey_consumer.exeЗапускайте .venv\Scripts\huey_consumer
huey cannot be run in process-mode on WindowsПроцессным воркерам нужен fork()Используйте потоковые воркеры по умолчанию
ModuleNotFoundError: No module named tasksКонсьюмер запущен не из той папки, где лежит tasks.pyЗапускайте его из этой папки
ResultTimeout, хотя консьюмер запущенСкрипт и консьюмер используют разные файлы huey.dbПередайте SqliteHuey полный путь к файлу
get() сообщает о сбое, а потом задача выполняется успешноstore_intermediate_errors включён, поэтому каждая неудачная попытка сохраняетсяЗадайте store_intermediate_errors=False
Задача, которая должна остановиться, продолжает повторятьсяCancelExecution получил сообщение в качестве аргумента retryВыбрасывайте CancelExecution(False, message)
Воркеры перестают брать задачи и ничего не пишут в логРешение зависло в HTTP-вызове без таймаутаЗадайте таймаут запросам SDK
Каждый запланированный запуск пропускаетсяУстаревшая блокировка от убитого консьюмераПерезапустите консьюмер с -f

FAQ

Можно ли использовать RedisHuey вместо SqliteHuey?

Да. Задачи, повторы и блокировки из этого руководства работают так же. Дополнительно установите пакет redis, а затем создайте очередь через RedisHuey вместо SqliteHuey. Однако RedisHuey нужен сервер Redis, а в составе Windows его нет. На ПК, где уже работает CapSkip, файл SQLite избавляет вас от запуска второго сервиса. Для очереди, распределённой по многим машинам, Redis подходит лучше, как показано в этом руководстве по решению капчи в задачах Celery.

Почему первая задача после простоя запускается с опозданием?

Простаивающий консьюмер проверяет базу данных всё реже и реже, по умолчанию вплоть до одного раза в 10 секунд. С четырьмя воркерами первая задача после простоя в наших запусках ждала от 3 до 7 секунд. Запустите консьюмер с -m 1, чтобы ограничить это ожидание одной секундой; после этого оно не превышало одной секунды.

Работает ли это в проекте Django?

Да, через huey.contrib.djhuey. В настройке HUEY задайте huey_class равным huey.SqliteHuey и укажите filename, а консьюмер запускайте через manage.py run_huey. Одно значение по умолчанию многих удивляет: при включённом DEBUG djhuey выполняет задачи сразу внутри запроса, поэтому решение блокирует страницу, пока CapSkip не ответит. Чтобы протестировать настоящую очередь, задайте immediate значение False в настройке HUEY.

Может ли консьюмер работать не на той машине, где запущен CapSkip?

Да. Переведите CapSkip в режим Server в настройках подключения, чтобы он слушал сетевой адрес, а не локальную петлю, и задайте этот адрес в CAPSKIP_HOST до запуска консьюмера. Тогда задачи обращаются к нему через тот же HTTP API с любой машины, которой доступен порт. Добавьте блокирующее правило Windows Firewall для адресов, у которых не должно быть к нему доступа. Решатель по-прежнему работает на вашем собственном железе, и ни одно решение не проходит через сторонний сервис.

Коротко

Для задачи Huey, которая решает капчу в Windows, закрепите файл SqliteHuey рядом с tasks.py, выключите store_intermediate_errors и повторяйте только те сбои CapSkip, которые может исправить новая попытка, а остальные завершайте через CancelExecution(False, message). Запускайте .venv\Scripts\huey_consumer с потоковыми воркерами из папки, где лежит tasks.py, задайте таймаут HTTP-вызовам SDK, учитывайте, что каждый результат читается только один раз, и ставьте блокировку на запланированные решения. В документации по консьюмеру Huey перечислены все параметры консьюмера, которые здесь использованы.

Очередь в одном файле и решатель на том же ПК держат всю работу на железе, которое вы контролируете. Именно этим и хорош локальный обход капчи: каждый повтор бесплатен, поэтому задача может позволить себе попробовать снова.