add new tool: to_captcha
This commit is contained in:
parent
8f86dbbdac
commit
4852345bf6
12 changed files with 716 additions and 35 deletions
|
|
@ -10,8 +10,16 @@ class BrowserRpcClient:
|
|||
self._rpc_url = rpc_url
|
||||
self._session = session
|
||||
|
||||
async def run(self, task: str, timeout_sec: float, rpc_url: str | None = None) -> dict[str, Any]:
|
||||
payload = {"task": task}
|
||||
async def run(
|
||||
self,
|
||||
task: str,
|
||||
timeout_sec: float,
|
||||
rpc_url: str | None = None,
|
||||
task_id: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
payload: dict[str, Any] = {"task": task}
|
||||
if task_id:
|
||||
payload["task_id"] = task_id
|
||||
timeout = aiohttp.ClientTimeout(total=timeout_sec)
|
||||
target_url = rpc_url or self._rpc_url
|
||||
|
||||
|
|
@ -34,6 +42,15 @@ class BrowserRpcClient:
|
|||
return data
|
||||
|
||||
|
||||
async def run_browser_task(rpc_url: str, task: str, timeout_sec: float) -> dict[str, Any]:
|
||||
async def run_browser_task(
|
||||
rpc_url: str,
|
||||
task: str,
|
||||
timeout_sec: float,
|
||||
task_id: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
return await BrowserRpcClient(rpc_url, session=session).run(task=task, timeout_sec=timeout_sec)
|
||||
return await BrowserRpcClient(rpc_url, session=session).run(
|
||||
task=task,
|
||||
timeout_sec=timeout_sec,
|
||||
task_id=task_id,
|
||||
)
|
||||
|
|
|
|||
54
api/contracts/captcha_schemas.py
Normal file
54
api/contracts/captcha_schemas.py
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
from pydantic import BaseModel, Field
|
||||
|
||||
from api.domain.captcha_state import CaptchaState
|
||||
|
||||
|
||||
class CaptchaNotifyRequest(BaseModel):
|
||||
"""Запрос от browser-use tool о том, что задача упёрлась в капчу."""
|
||||
|
||||
browser_view_url: str | None = Field(default=None, description="URL noVNC/прокси для ручного решения")
|
||||
captcha_kind: str | None = Field(default=None, description="Тип капчи (recaptcha_v2/hcaptcha/turnstile/unknown)")
|
||||
reason: str | None = Field(default=None, description="Свободное описание ситуации от агента")
|
||||
timeout_seconds: int = Field(default=300, ge=1, le=3600, description="Сколько ждать решения до timeout-prompt")
|
||||
|
||||
|
||||
class CaptchaSolvedRequest(BaseModel):
|
||||
"""Уведомление от детектора, что капча больше не блокирует страницу."""
|
||||
|
||||
detector: str | None = Field(default=None, description="Имя детектора (dom_poller/2captcha/user)")
|
||||
|
||||
|
||||
class CaptchaExtendRequest(BaseModel):
|
||||
"""Пользовательский ответ «продлить» на timeout-prompt."""
|
||||
|
||||
extra_seconds: int = Field(default=300, ge=1, le=3600, description="На сколько ещё ждать решения")
|
||||
|
||||
|
||||
class CaptchaAbortRequest(BaseModel):
|
||||
"""Пользовательский ответ «отменить» на timeout-prompt."""
|
||||
|
||||
reason: str | None = Field(default=None, description="Свободный текст причины")
|
||||
|
||||
|
||||
class CaptchaStatusResponse(BaseModel):
|
||||
"""Снимок captcha-состояния задачи."""
|
||||
|
||||
task_id: str
|
||||
state: CaptchaState
|
||||
captcha_kind: str | None = None
|
||||
reason: str | None = None
|
||||
browser_view_url: str | None = None
|
||||
notified_at: float | None = None
|
||||
solved_at: float | None = None
|
||||
deadline: float | None = None
|
||||
extra_seconds: int = 0
|
||||
remaining_seconds: float | None = None
|
||||
|
||||
|
||||
class CaptchaActionResponse(BaseModel):
|
||||
"""Универсальный ответ на действие над captcha-flow."""
|
||||
|
||||
task_id: str
|
||||
state: CaptchaState
|
||||
accepted: bool = True
|
||||
message: str | None = None
|
||||
13
api/domain/captcha_state.py
Normal file
13
api/domain/captcha_state.py
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
from enum import Enum
|
||||
|
||||
|
||||
class CaptchaState(str, Enum):
|
||||
"""Состояние captcha-флоу для задачи."""
|
||||
|
||||
none = "none"
|
||||
awaiting = "awaiting"
|
||||
solved = "solved"
|
||||
timeout_prompt = "timeout_prompt"
|
||||
extended = "extended"
|
||||
aborted = "aborted"
|
||||
failed = "failed"
|
||||
|
|
@ -6,6 +6,7 @@ from fastapi import FastAPI
|
|||
from api.clients.browser_rpc_client import BrowserRpcClient
|
||||
from api.core.settings import settings
|
||||
from api.repositories.task_store import TaskStore
|
||||
from api.routes.captcha import router as captcha_router
|
||||
from api.routes.runs import router as runs_router
|
||||
from api.routes.tasks import router as tasks_router
|
||||
from api.services.task_service import TaskService
|
||||
|
|
@ -37,6 +38,7 @@ def create_app() -> FastAPI:
|
|||
)
|
||||
app.include_router(tasks_router)
|
||||
app.include_router(runs_router)
|
||||
app.include_router(captcha_router)
|
||||
|
||||
@app.get("/health")
|
||||
async def health() -> dict:
|
||||
|
|
|
|||
35
api/mappers/captcha_mapper.py
Normal file
35
api/mappers/captcha_mapper.py
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
|
||||
from api.contracts.captcha_schemas import CaptchaActionResponse, CaptchaStatusResponse
|
||||
from api.repositories.task_store import TaskRecord
|
||||
|
||||
|
||||
class CaptchaMapper:
|
||||
@staticmethod
|
||||
def to_status(rec: TaskRecord) -> CaptchaStatusResponse:
|
||||
remaining: float | None = None
|
||||
if rec.captcha_deadline is not None:
|
||||
remaining = max(0.0, rec.captcha_deadline - time.time())
|
||||
return CaptchaStatusResponse(
|
||||
task_id=rec.task_id,
|
||||
state=rec.captcha_state,
|
||||
captcha_kind=rec.captcha_kind,
|
||||
reason=rec.captcha_reason,
|
||||
browser_view_url=rec.captcha_view_url,
|
||||
notified_at=rec.captcha_notified_at,
|
||||
solved_at=rec.captcha_solved_at,
|
||||
deadline=rec.captcha_deadline,
|
||||
extra_seconds=rec.captcha_extra_seconds,
|
||||
remaining_seconds=remaining,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def to_action(rec: TaskRecord, message: str | None = None, accepted: bool = True) -> CaptchaActionResponse:
|
||||
return CaptchaActionResponse(
|
||||
task_id=rec.task_id,
|
||||
state=rec.captcha_state,
|
||||
accepted=accepted,
|
||||
message=message,
|
||||
)
|
||||
|
|
@ -4,6 +4,7 @@ from asyncio import Event, Lock, Queue
|
|||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
from api.domain.captcha_state import CaptchaState
|
||||
from api.domain.task_status import TaskStatus
|
||||
|
||||
|
||||
|
|
@ -25,6 +26,16 @@ class TaskRecord:
|
|||
cancel_requested: bool = False
|
||||
done_event: Event = field(default_factory=Event)
|
||||
|
||||
captcha_state: CaptchaState = CaptchaState.none
|
||||
captcha_kind: str | None = None
|
||||
captcha_reason: str | None = None
|
||||
captcha_view_url: str | None = None
|
||||
captcha_notified_at: float | None = None
|
||||
captcha_solved_at: float | None = None
|
||||
captcha_deadline: float | None = None
|
||||
captcha_extra_seconds: int = 0
|
||||
captcha_event: Event = field(default_factory=Event)
|
||||
|
||||
@property
|
||||
def execution_time(self) -> float:
|
||||
if self.started_at is None:
|
||||
|
|
@ -40,13 +51,7 @@ class TaskStore:
|
|||
self._thread_index: dict[str, list[str]] = {}
|
||||
self._subscribers: dict[str, set[Queue[dict[str, Any]]]] = {}
|
||||
|
||||
async def create(
|
||||
self,
|
||||
task: str,
|
||||
timeout: int,
|
||||
metadata: dict[str, Any] | None,
|
||||
thread_id: str = "default",
|
||||
) -> TaskRecord:
|
||||
async def create(self, task: str, timeout: int, metadata: dict[str, Any] | None, thread_id: str = "default") -> TaskRecord:
|
||||
task_id = uuid.uuid4().hex
|
||||
rec = TaskRecord(task_id=task_id, thread_id=thread_id, task=task, timeout=timeout, metadata=metadata)
|
||||
async with self._lock:
|
||||
|
|
@ -75,25 +80,15 @@ class TaskStore:
|
|||
rec.started_at = time.time()
|
||||
return rec
|
||||
|
||||
async def set_done(
|
||||
self,
|
||||
task_id: str,
|
||||
success: bool,
|
||||
raw_response: dict[str, Any] | None,
|
||||
error: str | None,
|
||||
result: str | None = None,
|
||||
history: list[dict[str, Any]] | None = None,
|
||||
) -> TaskRecord | None:
|
||||
async def set_done(self, task_id: str, success: bool, raw_response: dict[str, Any] | None, error: str | None, result: str | None = None, history: list[dict[str, Any]] | None = None) -> TaskRecord | None:
|
||||
async with self._lock:
|
||||
rec = self._tasks.get(task_id)
|
||||
if rec is None:
|
||||
return None
|
||||
rec.finished_at = time.time()
|
||||
rec.raw_response = raw_response
|
||||
rec.error = error if error is not None else (
|
||||
raw_response.get("error") if isinstance(raw_response, dict) else None)
|
||||
rec.result = result if result is not None else (
|
||||
raw_response.get("result") if isinstance(raw_response, dict) else None)
|
||||
rec.error = error if error is not None else (raw_response.get("error") if isinstance(raw_response, dict) else None)
|
||||
rec.result = result if result is not None else (raw_response.get("result") if isinstance(raw_response, dict) else None)
|
||||
rec.history = list(history or [])
|
||||
rec.status = TaskStatus.succeeded if success else TaskStatus.failed
|
||||
rec.done_event.set()
|
||||
|
|
@ -132,7 +127,6 @@ class TaskStore:
|
|||
return False, False
|
||||
if rec.status in (TaskStatus.queued, TaskStatus.running):
|
||||
return True, False
|
||||
|
||||
del self._tasks[task_id]
|
||||
thread_list = self._thread_index.get(rec.thread_id, [])
|
||||
if task_id in thread_list:
|
||||
|
|
@ -140,6 +134,93 @@ class TaskStore:
|
|||
self._subscribers.pop(task_id, None)
|
||||
return True, True
|
||||
|
||||
async def set_captcha_awaiting(self, task_id: str, kind: str | None, reason: str | None, view_url: str | None, timeout_seconds: int) -> TaskRecord | None:
|
||||
async with self._lock:
|
||||
rec = self._tasks.get(task_id)
|
||||
if rec is None:
|
||||
return None
|
||||
now = time.time()
|
||||
rec.captcha_state = CaptchaState.awaiting
|
||||
rec.captcha_kind = kind
|
||||
rec.captcha_reason = reason
|
||||
rec.captcha_view_url = view_url
|
||||
rec.captcha_notified_at = now
|
||||
rec.captcha_solved_at = None
|
||||
rec.captcha_extra_seconds = 0
|
||||
rec.captcha_deadline = now + max(1, int(timeout_seconds))
|
||||
rec.captcha_event.clear()
|
||||
return rec
|
||||
|
||||
async def set_captcha_solved(self, task_id: str) -> TaskRecord | None:
|
||||
async with self._lock:
|
||||
rec = self._tasks.get(task_id)
|
||||
if rec is None:
|
||||
return None
|
||||
if rec.captcha_state in (CaptchaState.none, CaptchaState.solved):
|
||||
return rec
|
||||
rec.captcha_state = CaptchaState.solved
|
||||
rec.captcha_solved_at = time.time()
|
||||
rec.captcha_event.set()
|
||||
return rec
|
||||
|
||||
async def set_captcha_timeout_prompt(self, task_id: str) -> TaskRecord | None:
|
||||
async with self._lock:
|
||||
rec = self._tasks.get(task_id)
|
||||
if rec is None:
|
||||
return None
|
||||
if rec.captcha_state != CaptchaState.awaiting:
|
||||
return rec
|
||||
rec.captcha_state = CaptchaState.timeout_prompt
|
||||
rec.captcha_event.set()
|
||||
return rec
|
||||
|
||||
async def set_captcha_extended(self, task_id: str, extra_seconds: int) -> TaskRecord | None:
|
||||
async with self._lock:
|
||||
rec = self._tasks.get(task_id)
|
||||
if rec is None:
|
||||
return None
|
||||
if rec.captcha_state not in (CaptchaState.timeout_prompt, CaptchaState.awaiting):
|
||||
return rec
|
||||
extra = max(1, int(extra_seconds))
|
||||
rec.captcha_extra_seconds += extra
|
||||
base = rec.captcha_deadline or time.time()
|
||||
rec.captcha_deadline = max(base, time.time()) + extra
|
||||
rec.captcha_state = CaptchaState.extended
|
||||
rec.captcha_event.set()
|
||||
return rec
|
||||
|
||||
async def set_captcha_aborted(self, task_id: str, reason: str | None = None) -> TaskRecord | None:
|
||||
async with self._lock:
|
||||
rec = self._tasks.get(task_id)
|
||||
if rec is None:
|
||||
return None
|
||||
if rec.captcha_state in (CaptchaState.none, CaptchaState.aborted, CaptchaState.failed):
|
||||
return rec
|
||||
rec.captcha_state = CaptchaState.aborted
|
||||
rec.captcha_reason = reason or rec.captcha_reason
|
||||
rec.captcha_event.set()
|
||||
return rec
|
||||
|
||||
async def set_captcha_failed(self, task_id: str, error: str | None = None) -> TaskRecord | None:
|
||||
async with self._lock:
|
||||
rec = self._tasks.get(task_id)
|
||||
if rec is None:
|
||||
return None
|
||||
rec.captcha_state = CaptchaState.failed
|
||||
if error:
|
||||
rec.captcha_reason = error
|
||||
rec.captcha_event.set()
|
||||
return rec
|
||||
|
||||
async def reset_captcha(self, task_id: str) -> TaskRecord | None:
|
||||
async with self._lock:
|
||||
rec = self._tasks.get(task_id)
|
||||
if rec is None:
|
||||
return None
|
||||
rec.captcha_state = CaptchaState.none
|
||||
rec.captcha_event.clear()
|
||||
return rec
|
||||
|
||||
async def subscribe(self, task_id: str) -> Queue[dict[str, Any]] | None:
|
||||
queue: Queue[dict[str, Any]] = Queue()
|
||||
async with self._lock:
|
||||
|
|
|
|||
105
api/routes/captcha.py
Normal file
105
api/routes/captcha.py
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
import asyncio
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
|
||||
from api.contracts.captcha_schemas import (
|
||||
CaptchaAbortRequest,
|
||||
CaptchaActionResponse,
|
||||
CaptchaExtendRequest,
|
||||
CaptchaNotifyRequest,
|
||||
CaptchaSolvedRequest,
|
||||
CaptchaStatusResponse,
|
||||
)
|
||||
from api.mappers.captcha_mapper import CaptchaMapper
|
||||
from api.routes.dependencies import get_task_service
|
||||
from api.services.protocols import TaskServiceProtocol
|
||||
|
||||
router = APIRouter(prefix="/api/browser", tags=["browser-captcha"])
|
||||
|
||||
|
||||
@router.post("/tasks/{task_id}/captcha/notify", response_model=CaptchaActionResponse, status_code=202)
|
||||
async def captcha_notify(
|
||||
task_id: str,
|
||||
payload: CaptchaNotifyRequest,
|
||||
service: TaskServiceProtocol = Depends(get_task_service),
|
||||
) -> CaptchaActionResponse:
|
||||
rec = await service.notify_captcha(
|
||||
task_id=task_id,
|
||||
kind=payload.captcha_kind,
|
||||
reason=payload.reason,
|
||||
view_url=payload.browser_view_url,
|
||||
timeout_seconds=payload.timeout_seconds,
|
||||
)
|
||||
if rec is None:
|
||||
raise HTTPException(status_code=404, detail="Task not found")
|
||||
return CaptchaMapper.to_action(rec, message="captcha awaiting solution")
|
||||
|
||||
|
||||
@router.get("/tasks/{task_id}/captcha", response_model=CaptchaStatusResponse)
|
||||
async def captcha_status(
|
||||
task_id: str,
|
||||
service: TaskServiceProtocol = Depends(get_task_service),
|
||||
) -> CaptchaStatusResponse:
|
||||
rec = await service.get_task(task_id)
|
||||
if rec is None:
|
||||
raise HTTPException(status_code=404, detail="Task not found")
|
||||
return CaptchaMapper.to_status(rec)
|
||||
|
||||
|
||||
@router.get("/tasks/{task_id}/captcha/wait", response_model=CaptchaStatusResponse)
|
||||
async def captcha_wait(
|
||||
task_id: str,
|
||||
timeout: float = Query(default=30.0, ge=0.1, le=600.0, description="long-poll cap, seconds"),
|
||||
service: TaskServiceProtocol = Depends(get_task_service),
|
||||
) -> CaptchaStatusResponse:
|
||||
rec = await service.wait_captcha(task_id=task_id, timeout=timeout)
|
||||
if rec is None:
|
||||
raise HTTPException(status_code=404, detail="Task not found")
|
||||
return CaptchaMapper.to_status(rec)
|
||||
|
||||
|
||||
@router.post("/tasks/{task_id}/captcha/solved", response_model=CaptchaActionResponse)
|
||||
async def captcha_solved(
|
||||
task_id: str,
|
||||
payload: CaptchaSolvedRequest | None = None,
|
||||
service: TaskServiceProtocol = Depends(get_task_service),
|
||||
) -> CaptchaActionResponse:
|
||||
rec = await service.mark_captcha_solved(task_id=task_id, detector=(payload.detector if payload else None))
|
||||
if rec is None:
|
||||
raise HTTPException(status_code=404, detail="Task not found")
|
||||
return CaptchaMapper.to_action(rec, message="captcha marked as solved")
|
||||
|
||||
|
||||
@router.post("/tasks/{task_id}/captcha/timeout-prompt", response_model=CaptchaActionResponse)
|
||||
async def captcha_timeout_prompt(
|
||||
task_id: str,
|
||||
service: TaskServiceProtocol = Depends(get_task_service),
|
||||
) -> CaptchaActionResponse:
|
||||
rec = await service.prompt_captcha_timeout(task_id=task_id)
|
||||
if rec is None:
|
||||
raise HTTPException(status_code=404, detail="Task not found")
|
||||
return CaptchaMapper.to_action(rec, message="user must choose: extend or abort")
|
||||
|
||||
|
||||
@router.post("/tasks/{task_id}/captcha/extend", response_model=CaptchaActionResponse)
|
||||
async def captcha_extend(
|
||||
task_id: str,
|
||||
payload: CaptchaExtendRequest,
|
||||
service: TaskServiceProtocol = Depends(get_task_service),
|
||||
) -> CaptchaActionResponse:
|
||||
rec = await service.extend_captcha(task_id=task_id, extra_seconds=payload.extra_seconds)
|
||||
if rec is None:
|
||||
raise HTTPException(status_code=404, detail="Task not found")
|
||||
return CaptchaMapper.to_action(rec, message=f"captcha extended by {payload.extra_seconds}s")
|
||||
|
||||
|
||||
@router.post("/tasks/{task_id}/captcha/abort", response_model=CaptchaActionResponse)
|
||||
async def captcha_abort(
|
||||
task_id: str,
|
||||
payload: CaptchaAbortRequest | None = None,
|
||||
service: TaskServiceProtocol = Depends(get_task_service),
|
||||
) -> CaptchaActionResponse:
|
||||
rec = await service.abort_captcha(task_id=task_id, reason=(payload.reason if payload else None))
|
||||
if rec is None:
|
||||
raise HTTPException(status_code=404, detail="Task not found")
|
||||
return CaptchaMapper.to_action(rec, message="captcha aborted by user")
|
||||
|
|
@ -26,3 +26,22 @@ class TaskServiceProtocol(Protocol):
|
|||
async def subscribe_run_stream(self, run_id: str) -> Queue[dict[str, Any]] | None: ...
|
||||
|
||||
async def unsubscribe_run_stream(self, run_id: str, queue: Queue[dict[str, Any]]) -> None: ...
|
||||
|
||||
async def notify_captcha(
|
||||
self,
|
||||
task_id: str,
|
||||
kind: str | None,
|
||||
reason: str | None,
|
||||
view_url: str | None,
|
||||
timeout_seconds: int,
|
||||
) -> TaskRecord | None: ...
|
||||
|
||||
async def mark_captcha_solved(self, task_id: str, detector: str | None = None) -> TaskRecord | None: ...
|
||||
|
||||
async def extend_captcha(self, task_id: str, extra_seconds: int) -> TaskRecord | None: ...
|
||||
|
||||
async def abort_captcha(self, task_id: str, reason: str | None = None) -> TaskRecord | None: ...
|
||||
|
||||
async def prompt_captcha_timeout(self, task_id: str) -> TaskRecord | None: ...
|
||||
|
||||
async def wait_captcha(self, task_id: str, timeout: float) -> TaskRecord | None: ...
|
||||
|
|
|
|||
|
|
@ -89,6 +89,93 @@ class TaskService:
|
|||
async def unsubscribe_run_stream(self, run_id: str, queue) -> None:
|
||||
await self._store.unsubscribe(run_id, queue)
|
||||
|
||||
async def notify_captcha(
|
||||
self,
|
||||
task_id: str,
|
||||
kind: str | None,
|
||||
reason: str | None,
|
||||
view_url: str | None,
|
||||
timeout_seconds: int,
|
||||
):
|
||||
rec = await self._store.set_captcha_awaiting(
|
||||
task_id=task_id,
|
||||
kind=kind,
|
||||
reason=reason,
|
||||
view_url=view_url,
|
||||
timeout_seconds=timeout_seconds,
|
||||
)
|
||||
if rec is not None:
|
||||
await self._store.publish(
|
||||
task_id,
|
||||
self._event(task_id, "captcha_required", {
|
||||
"captcha_kind": rec.captcha_kind,
|
||||
"reason": rec.captcha_reason,
|
||||
"browser_view_url": rec.captcha_view_url,
|
||||
"deadline": rec.captcha_deadline,
|
||||
"timeout_seconds": timeout_seconds,
|
||||
}),
|
||||
)
|
||||
return rec
|
||||
|
||||
async def mark_captcha_solved(self, task_id: str, detector: str | None = None):
|
||||
rec = await self._store.set_captcha_solved(task_id)
|
||||
if rec is not None:
|
||||
await self._store.publish(
|
||||
task_id,
|
||||
self._event(task_id, "captcha_solved", {
|
||||
"detector": detector or "unknown",
|
||||
"solved_at": rec.captcha_solved_at,
|
||||
}),
|
||||
)
|
||||
return rec
|
||||
|
||||
async def extend_captcha(self, task_id: str, extra_seconds: int):
|
||||
rec = await self._store.set_captcha_extended(task_id=task_id, extra_seconds=extra_seconds)
|
||||
if rec is not None:
|
||||
await self._store.publish(
|
||||
task_id,
|
||||
self._event(task_id, "captcha_extended", {
|
||||
"extra_seconds": extra_seconds,
|
||||
"deadline": rec.captcha_deadline,
|
||||
}),
|
||||
)
|
||||
return rec
|
||||
|
||||
async def prompt_captcha_timeout(self, task_id: str):
|
||||
rec = await self._store.set_captcha_timeout_prompt(task_id)
|
||||
if rec is not None:
|
||||
await self._store.publish(
|
||||
task_id,
|
||||
self._event(task_id, "captcha_timeout_prompt", {
|
||||
"captcha_kind": rec.captcha_kind,
|
||||
"browser_view_url": rec.captcha_view_url,
|
||||
"deadline": rec.captcha_deadline,
|
||||
"actions": ["extend", "abort"],
|
||||
}),
|
||||
)
|
||||
return rec
|
||||
|
||||
async def abort_captcha(self, task_id: str, reason: str | None = None):
|
||||
rec = await self._store.set_captcha_aborted(task_id=task_id, reason=reason)
|
||||
if rec is not None:
|
||||
await self._store.publish(
|
||||
task_id,
|
||||
self._event(task_id, "captcha_aborted", {
|
||||
"reason": rec.captcha_reason,
|
||||
}),
|
||||
)
|
||||
return rec
|
||||
|
||||
async def wait_captcha(self, task_id: str, timeout: float):
|
||||
rec = await self._store.get(task_id)
|
||||
if rec is None:
|
||||
return None
|
||||
try:
|
||||
await asyncio.wait_for(rec.captcha_event.wait(), timeout=timeout)
|
||||
except asyncio.TimeoutError:
|
||||
pass
|
||||
return await self._store.get(task_id)
|
||||
|
||||
async def close(self) -> None:
|
||||
if not self._background_tasks:
|
||||
return
|
||||
|
|
@ -127,7 +214,12 @@ class TaskService:
|
|||
rpc_timeout = min(rpc_timeout, self._rpc_timeout_cap)
|
||||
|
||||
raw = await asyncio.wait_for(
|
||||
self._rpc_client.run(task=rec.task, timeout_sec=rpc_timeout, rpc_url=runtime.get("rpc_url")),
|
||||
self._rpc_client.run(
|
||||
task=rec.task,
|
||||
timeout_sec=rpc_timeout,
|
||||
rpc_url=runtime.get("rpc_url"),
|
||||
task_id=task_id,
|
||||
),
|
||||
timeout=float(rec.timeout) + 5,
|
||||
)
|
||||
raw = self._with_runtime_metadata(raw, runtime)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue