mirror of
https://github.com/zc-zhangchen/any-auto-register.git
synced 2026-05-09 08:44:06 +08:00
Merge pull request #88 from Hoshino-Yumetsuki/main
feat: 支持docker内运行有头/无头浏览器
This commit is contained in:
@@ -37,7 +37,7 @@ COPY scripts/install_camoufox.py /tmp/install_camoufox.py
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
curl ca-certificates \
|
||||
libgtk-3-0 libx11-xcb1 libasound2 \
|
||||
libgtk-3-0 libx11-xcb1 libasound2 xvfb xauth \
|
||||
&& curl -fsSL https://go.dev/dl/go1.24.2.linux-amd64.tar.gz | tar -C /usr/local -xz \
|
||||
&& curl -LsSf https://astral.sh/uv/install.sh | sh \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
64
core/browser_runtime.py
Normal file
64
core/browser_runtime.py
Normal file
@@ -0,0 +1,64 @@
|
||||
"""Browser runtime helpers for headless/headed resolution."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from typing import Iterable
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_TRUE_VALUES = {"1", "true", "yes", "on"}
|
||||
_FALSE_VALUES = {"0", "false", "no", "off"}
|
||||
|
||||
|
||||
def parse_env_bool(name: str) -> bool | None:
|
||||
raw = os.getenv(name)
|
||||
if raw is None:
|
||||
return None
|
||||
|
||||
value = str(raw).strip().lower()
|
||||
if not value:
|
||||
return None
|
||||
if value in _TRUE_VALUES:
|
||||
return True
|
||||
if value in _FALSE_VALUES:
|
||||
return False
|
||||
|
||||
logger.warning("忽略无效布尔环境变量 %s=%r", name, raw)
|
||||
return None
|
||||
|
||||
|
||||
def resolve_browser_headless(
|
||||
requested_headless: bool | None,
|
||||
*,
|
||||
default_headless: bool = True,
|
||||
override_env_names: Iterable[str] = ("PLAYWRIGHT_HEADLESS", "REGISTER_HEADLESS"),
|
||||
) -> tuple[bool, str]:
|
||||
for env_name in override_env_names:
|
||||
override = parse_env_bool(env_name)
|
||||
if override is not None:
|
||||
return override, f"env:{env_name}={str(override).lower()}"
|
||||
|
||||
if requested_headless is not None:
|
||||
return bool(
|
||||
requested_headless
|
||||
), f"requested:{str(bool(requested_headless)).lower()}"
|
||||
|
||||
return bool(default_headless), f"default:{str(bool(default_headless)).lower()}"
|
||||
|
||||
|
||||
def ensure_browser_display_available(headless: bool) -> None:
|
||||
if headless:
|
||||
return
|
||||
if not sys.platform.startswith("linux"):
|
||||
return
|
||||
if os.getenv("DISPLAY"):
|
||||
return
|
||||
|
||||
raise RuntimeError(
|
||||
"当前为 Linux 有头浏览器模式,但未检测到 DISPLAY。"
|
||||
"Docker 内请启用 Xvfb;本地 Linux 请先启动图形环境或改用无头模式。"
|
||||
)
|
||||
@@ -1,47 +1,80 @@
|
||||
"""Playwright 执行器 - 支持 headless/headed 模式"""
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from ..base_executor import BaseExecutor, Response
|
||||
from ..browser_runtime import ensure_browser_display_available, resolve_browser_headless
|
||||
from ..proxy_utils import build_playwright_proxy_config
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PlaywrightExecutor(BaseExecutor):
|
||||
def __init__(self, proxy: str = None, headless: bool = True):
|
||||
super().__init__(proxy)
|
||||
def __init__(self, proxy: str | None = None, headless: bool = True):
|
||||
super().__init__(proxy or "")
|
||||
self.headless = headless
|
||||
self._browser = None
|
||||
self._context = None
|
||||
self._page = None
|
||||
self._pw: Any | None = None
|
||||
self._browser: Any | None = None
|
||||
self._context: Any | None = None
|
||||
self._page: Any | None = None
|
||||
self._init()
|
||||
|
||||
def _init(self):
|
||||
def _init(self) -> None:
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
self._pw = sync_playwright().start()
|
||||
launch_opts = {"headless": self.headless}
|
||||
headless, reason = resolve_browser_headless(self.headless)
|
||||
ensure_browser_display_available(headless)
|
||||
logger.info(
|
||||
"PlaywrightExecutor 浏览器模式: %s (%s)",
|
||||
"headless" if headless else "headed",
|
||||
reason,
|
||||
)
|
||||
|
||||
launch_opts: dict[str, Any] = {"headless": headless}
|
||||
if self.proxy:
|
||||
launch_opts["proxy"] = build_playwright_proxy_config(self.proxy)
|
||||
proxy_cfg = build_playwright_proxy_config(self.proxy)
|
||||
if proxy_cfg:
|
||||
launch_opts["proxy"] = proxy_cfg
|
||||
self._browser = self._pw.chromium.launch(**launch_opts)
|
||||
self._context = self._browser.new_context()
|
||||
self._page = self._context.new_page()
|
||||
|
||||
def _require_page(self) -> Any:
|
||||
if self._page is None:
|
||||
raise RuntimeError("Playwright page 未初始化")
|
||||
return self._page
|
||||
|
||||
def _require_context(self) -> Any:
|
||||
if self._context is None:
|
||||
raise RuntimeError("Playwright context 未初始化")
|
||||
return self._context
|
||||
|
||||
def get(self, url, *, headers=None, params=None) -> Response:
|
||||
import urllib.parse
|
||||
|
||||
page = self._require_page()
|
||||
if params:
|
||||
url = url + "?" + urllib.parse.urlencode(params)
|
||||
if headers:
|
||||
self._page.set_extra_http_headers(headers)
|
||||
resp = self._page.goto(url)
|
||||
page.set_extra_http_headers(headers)
|
||||
resp = page.goto(url)
|
||||
if resp is None:
|
||||
raise RuntimeError(f"Playwright 导航失败: {url}")
|
||||
return Response(
|
||||
status_code=resp.status,
|
||||
text=self._page.content(),
|
||||
text=page.content(),
|
||||
headers=dict(resp.headers),
|
||||
cookies=self.get_cookies(),
|
||||
)
|
||||
|
||||
def post(self, url, *, headers=None, params=None, data=None, json=None) -> Response:
|
||||
import urllib.parse, json as _json
|
||||
import json as _json
|
||||
import urllib.parse
|
||||
|
||||
page = self._require_page()
|
||||
if params:
|
||||
url = url + "?" + urllib.parse.urlencode(params)
|
||||
post_data = None
|
||||
@@ -54,7 +87,7 @@ class PlaywrightExecutor(BaseExecutor):
|
||||
h = {"Content-Type": content_type}
|
||||
if headers:
|
||||
h.update(headers)
|
||||
resp = self._page.request.post(url, headers=h, data=post_data)
|
||||
resp = page.request.post(url, headers=h, data=post_data)
|
||||
return Response(
|
||||
status_code=resp.status,
|
||||
text=resp.text(),
|
||||
@@ -63,16 +96,19 @@ class PlaywrightExecutor(BaseExecutor):
|
||||
)
|
||||
|
||||
def get_cookies(self) -> dict:
|
||||
return {c["name"]: c["value"] for c in self._context.cookies()}
|
||||
context = self._require_context()
|
||||
return {c["name"]: c["value"] for c in context.cookies()}
|
||||
|
||||
def set_cookies(self, cookies: dict, domain: str = ".example.com") -> None:
|
||||
page_url = self._page.url if self._page else None
|
||||
context = self._require_context()
|
||||
page = self._require_page()
|
||||
page_url = page.url
|
||||
if page_url and page_url.startswith("http"):
|
||||
self._context.add_cookies(
|
||||
context.add_cookies(
|
||||
[{"name": k, "value": v, "url": page_url} for k, v in cookies.items()]
|
||||
)
|
||||
else:
|
||||
self._context.add_cookies(
|
||||
context.add_cookies(
|
||||
[
|
||||
{"name": k, "value": v, "domain": domain, "path": "/"}
|
||||
for k, v in cookies.items()
|
||||
|
||||
@@ -20,6 +20,7 @@ services:
|
||||
SOLVER_BIND_HOST: 0.0.0.0
|
||||
LOCAL_SOLVER_URL: http://127.0.0.1:8889
|
||||
SOLVER_BROWSER_TYPE: ${SOLVER_BROWSER_TYPE:-camoufox}
|
||||
PLAYWRIGHT_HEADLESS: ${PLAYWRIGHT_HEADLESS:-}
|
||||
ports:
|
||||
- "8000:8000"
|
||||
- "127.0.0.1:8889:8889"
|
||||
|
||||
@@ -17,4 +17,5 @@ ln -sfn "${RUNTIME_DIR}/smstome_all_numbers.txt" "${APP_DIR}/smstome_all_numbers
|
||||
ln -sfn "${RUNTIME_DIR}/smstome_uk_deep_numbers.txt" "${APP_DIR}/smstome_uk_deep_numbers.txt"
|
||||
ln -sfn "${RUNTIME_DIR}/logs/solver.log" "${APP_DIR}/services/turnstile_solver/solver.log"
|
||||
|
||||
exec python main.py
|
||||
echo "[entrypoint] Starting backend under Xvfb so Docker can handle both headed and headless browser tasks"
|
||||
exec xvfb-run -a --server-args="-screen 0 1920x1080x24" python main.py
|
||||
|
||||
@@ -7,9 +7,10 @@ from __future__ import annotations
|
||||
import logging
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import Optional
|
||||
from typing import Any, Optional
|
||||
|
||||
from curl_cffi import requests as cffi_requests
|
||||
from core.browser_runtime import ensure_browser_display_available
|
||||
from core.proxy_utils import build_requests_proxy_config
|
||||
|
||||
# from ..database.models import Account # removed: external dep
|
||||
@@ -97,7 +98,7 @@ def _open_url_system_browser(url: str) -> bool:
|
||||
|
||||
|
||||
def generate_plus_link(
|
||||
account: Account,
|
||||
account: Any,
|
||||
proxy: Optional[str] = None,
|
||||
country: str = "SG",
|
||||
) -> str:
|
||||
@@ -143,7 +144,7 @@ def generate_plus_link(
|
||||
|
||||
|
||||
def generate_team_link(
|
||||
account: Account,
|
||||
account: Any,
|
||||
workspace_name: str = "MyTeam",
|
||||
price_interval: str = "month",
|
||||
seat_quantity: int = 5,
|
||||
@@ -210,6 +211,7 @@ def open_url_incognito(url: str, cookies_str: Optional[str] = None) -> bool:
|
||||
def _launch():
|
||||
try:
|
||||
with sync_playwright() as p:
|
||||
ensure_browser_display_available(False)
|
||||
browser = p.chromium.launch(headless=False, args=["--incognito"])
|
||||
ctx = browser.new_context()
|
||||
if cookies_str:
|
||||
@@ -225,7 +227,7 @@ def open_url_incognito(url: str, cookies_str: Optional[str] = None) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
def check_subscription_status(account: Account, proxy: Optional[str] = None) -> str:
|
||||
def check_subscription_status(account: Any, proxy: Optional[str] = None) -> str:
|
||||
"""
|
||||
检测账号当前订阅状态。
|
||||
|
||||
|
||||
@@ -3,8 +3,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Callable, Optional
|
||||
from typing import Any, Callable, Optional
|
||||
|
||||
from core.browser_runtime import (
|
||||
ensure_browser_display_available,
|
||||
resolve_browser_headless,
|
||||
)
|
||||
from core.proxy_utils import build_playwright_proxy_config
|
||||
|
||||
|
||||
@@ -40,8 +44,14 @@ def get_sentinel_token_via_browser(
|
||||
return None
|
||||
|
||||
target_url = str(page_url or _flow_page_url(flow)).strip() or _flow_page_url(flow)
|
||||
launch_args = {
|
||||
"headless": bool(headless),
|
||||
effective_headless, reason = resolve_browser_headless(headless)
|
||||
ensure_browser_display_available(effective_headless)
|
||||
logger(
|
||||
f"Sentinel Browser 模式: {'headless' if effective_headless else 'headed'} ({reason})"
|
||||
)
|
||||
|
||||
launch_args: dict[str, Any] = {
|
||||
"headless": effective_headless,
|
||||
"args": [
|
||||
"--no-sandbox",
|
||||
"--disable-blink-features=AutomationControlled",
|
||||
|
||||
@@ -8,11 +8,17 @@ Grok (x.ai) 自动注册
|
||||
4. 完成注册并接受 ToS
|
||||
5. 提取 sso / sso-rw cookie
|
||||
"""
|
||||
|
||||
import ctypes
|
||||
import random
|
||||
import string
|
||||
import time
|
||||
from typing import Callable, Optional, Tuple
|
||||
from typing import Any, Callable, Optional, Tuple
|
||||
|
||||
from core.browser_runtime import (
|
||||
ensure_browser_display_available,
|
||||
resolve_browser_headless,
|
||||
)
|
||||
|
||||
|
||||
UA = (
|
||||
@@ -30,13 +36,27 @@ def _rand_password(n: int = 12) -> str:
|
||||
|
||||
|
||||
class GrokRegister:
|
||||
def __init__(self, captcha_solver=None, yescaptcha_key: str = "", proxy=None, log_fn=print):
|
||||
def __init__(
|
||||
self,
|
||||
captcha_solver=None,
|
||||
yescaptcha_key: str = "",
|
||||
proxy=None,
|
||||
log_fn=print,
|
||||
headless: bool = False,
|
||||
):
|
||||
self.captcha_solver = captcha_solver
|
||||
self.key = yescaptcha_key
|
||||
self.proxy = proxy
|
||||
self.log = log_fn
|
||||
self.headless = headless
|
||||
|
||||
def _wait_until(self, fn: Callable[[], bool], timeout: float = 30.0, interval: float = 0.5, desc: str = ""):
|
||||
def _wait_until(
|
||||
self,
|
||||
fn: Callable[[], bool],
|
||||
timeout: float = 30.0,
|
||||
interval: float = 0.5,
|
||||
desc: str = "",
|
||||
):
|
||||
start = time.time()
|
||||
while time.time() - start < timeout:
|
||||
if fn():
|
||||
@@ -52,8 +72,13 @@ class GrokRegister:
|
||||
from patchright.sync_api import sync_playwright
|
||||
|
||||
playwright = sync_playwright().start()
|
||||
launch_kwargs = {
|
||||
"headless": False,
|
||||
headless, reason = resolve_browser_headless(
|
||||
self.headless, default_headless=False
|
||||
)
|
||||
ensure_browser_display_available(headless)
|
||||
self.log(f"浏览器模式: {'headless' if headless else 'headed'} ({reason})")
|
||||
launch_kwargs: dict[str, Any] = {
|
||||
"headless": headless,
|
||||
"channel": "msedge",
|
||||
}
|
||||
if self.proxy:
|
||||
@@ -98,10 +123,15 @@ class GrokRegister:
|
||||
return page.locator("input[name=code]").count() > 0
|
||||
|
||||
try:
|
||||
self._wait_until(_email_verify_ready, timeout=15, desc="等待邮箱验证码页超时")
|
||||
self._wait_until(
|
||||
_email_verify_ready, timeout=15, desc="等待邮箱验证码页超时"
|
||||
)
|
||||
except Exception:
|
||||
body = page.locator("body").inner_text()
|
||||
if any(x in body for x in ["域名", "已被拒绝", "其他邮箱地址", "disposable", "rejected"]):
|
||||
if any(
|
||||
x in body
|
||||
for x in ["域名", "已被拒绝", "其他邮箱地址", "disposable", "rejected"]
|
||||
):
|
||||
raise RuntimeError(f"邮箱域名被拒绝: {body[:200]}")
|
||||
raise RuntimeError(f"邮箱提交失败: {body[:200]}")
|
||||
|
||||
@@ -129,14 +159,18 @@ class GrokRegister:
|
||||
self._wait_until(_user_form_ready, timeout=20, desc="等待完成注册页超时")
|
||||
self.log(" 已进入完成注册页")
|
||||
|
||||
def _fill_user_form(self, page, given_name: str, family_name: str, password: str) -> None:
|
||||
def _fill_user_form(
|
||||
self, page, given_name: str, family_name: str, password: str
|
||||
) -> None:
|
||||
self.log(f"Step4: 填写用户信息 {given_name} {family_name} ...")
|
||||
page.locator("input[name=givenName]").fill(given_name)
|
||||
page.locator("input[name=familyName]").fill(family_name)
|
||||
page.locator("input[name=password]").fill(password)
|
||||
|
||||
@staticmethod
|
||||
def _find_turnstile_widget(page) -> Tuple[object, Optional[dict]]:
|
||||
def _find_turnstile_widget(
|
||||
page,
|
||||
) -> Tuple[Optional[Any], Optional[dict[str, Any]]]:
|
||||
for frame in page.frames:
|
||||
if "challenges.cloudflare.com" not in frame.url:
|
||||
continue
|
||||
@@ -181,7 +215,13 @@ class GrokRegister:
|
||||
|
||||
@staticmethod
|
||||
def _has_turnstile_error(page) -> bool:
|
||||
keywords = ["验证失败", "故障排除", "verification failed", "troubleshoot", "try again"]
|
||||
keywords = [
|
||||
"验证失败",
|
||||
"故障排除",
|
||||
"verification failed",
|
||||
"troubleshoot",
|
||||
"try again",
|
||||
]
|
||||
texts = []
|
||||
try:
|
||||
texts.append(page.locator("body").inner_text(timeout=800))
|
||||
@@ -233,7 +273,9 @@ class GrokRegister:
|
||||
)
|
||||
)
|
||||
|
||||
def _wait_turnstile_token(self, page, wait_rounds: int = 25, wait_ms: int = 500) -> str:
|
||||
def _wait_turnstile_token(
|
||||
self, page, wait_rounds: int = 25, wait_ms: int = 500
|
||||
) -> str:
|
||||
for _ in range(wait_rounds):
|
||||
token = self._read_turnstile_token(page)
|
||||
if token and len(token) > 20:
|
||||
@@ -323,11 +365,16 @@ class GrokRegister:
|
||||
|
||||
click_x = box["x"] + min(28, max(18, box["width"] * 0.08))
|
||||
click_y = box["y"] + box["height"] / 2
|
||||
self.log(f" Turnstile click #{attempt + 1}: ({click_x:.1f}, {click_y:.1f})")
|
||||
self.log(
|
||||
f" Turnstile click #{attempt + 1}: ({click_x:.1f}, {click_y:.1f})"
|
||||
)
|
||||
try:
|
||||
if frame:
|
||||
frame.locator("body").click(
|
||||
position={"x": min(28, max(18, box["width"] * 0.08)), "y": box["height"] / 2},
|
||||
position={
|
||||
"x": min(28, max(18, box["width"] * 0.08)),
|
||||
"y": box["height"] / 2,
|
||||
},
|
||||
timeout=2500,
|
||||
)
|
||||
page.wait_for_timeout(120)
|
||||
@@ -343,7 +390,9 @@ class GrokRegister:
|
||||
last_error = str(e)
|
||||
|
||||
try:
|
||||
token = self._native_click_turnstile(page, box, min(28, max(18, box["width"] * 0.08)))
|
||||
token = self._native_click_turnstile(
|
||||
page, box, min(28, max(18, box["width"] * 0.08))
|
||||
)
|
||||
if token:
|
||||
self.log(f" Turnstile token: {token[:40]}...")
|
||||
return token
|
||||
@@ -366,6 +415,7 @@ class GrokRegister:
|
||||
|
||||
def _submit_register(self, page) -> None:
|
||||
self.log("Step6: 提交完成注册...")
|
||||
|
||||
def _tos_or_account_ready() -> bool:
|
||||
url = page.url
|
||||
body = page.locator("body").inner_text()
|
||||
@@ -441,14 +491,24 @@ class GrokRegister:
|
||||
def _account_ready() -> bool:
|
||||
url = page.url
|
||||
body = page.locator("body").inner_text()
|
||||
return "/account" in url or "您的账户" in body or self._has_auth_cookies(page.context.cookies())
|
||||
return (
|
||||
"/account" in url
|
||||
or "您的账户" in body
|
||||
or self._has_auth_cookies(page.context.cookies())
|
||||
)
|
||||
|
||||
self._wait_until(_account_ready, timeout=20, desc="等待账户页超时")
|
||||
page.wait_for_timeout(1500)
|
||||
|
||||
@staticmethod
|
||||
def _pick_cookie(cookies: list, name: str) -> str:
|
||||
domains = [".x.ai", "accounts.x.ai", ".grok.com", ".grokusercontent.com", ".grokipedia.com"]
|
||||
domains = [
|
||||
".x.ai",
|
||||
"accounts.x.ai",
|
||||
".grok.com",
|
||||
".grokusercontent.com",
|
||||
".grokipedia.com",
|
||||
]
|
||||
for domain in domains:
|
||||
for cookie in cookies:
|
||||
if cookie.get("name") == name and cookie.get("domain") == domain:
|
||||
@@ -458,7 +518,12 @@ class GrokRegister:
|
||||
return cookie.get("value", "")
|
||||
return ""
|
||||
|
||||
def register(self, email: str, password: str = None, otp_callback: Optional[Callable[[], str]] = None) -> dict:
|
||||
def register(
|
||||
self,
|
||||
email: str,
|
||||
password: Optional[str] = None,
|
||||
otp_callback: Optional[Callable[[], str]] = None,
|
||||
) -> dict:
|
||||
if not password:
|
||||
password = _rand_password()
|
||||
given_name = _rand_name()
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
"""Grok (x.ai) 平台插件"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from core.base_platform import BasePlatform, Account, AccountStatus, RegisterConfig
|
||||
from core.base_mailbox import BaseMailbox
|
||||
from core.registry import register
|
||||
@@ -10,20 +13,36 @@ class GrokPlatform(BasePlatform):
|
||||
display_name = "Grok"
|
||||
version = "1.0.0"
|
||||
|
||||
def __init__(self, config: RegisterConfig = None, mailbox: BaseMailbox = None):
|
||||
super().__init__(config)
|
||||
def __init__(
|
||||
self,
|
||||
config: Optional[RegisterConfig] = None,
|
||||
mailbox: Optional[BaseMailbox] = None,
|
||||
):
|
||||
super().__init__(config or RegisterConfig())
|
||||
self.mailbox = mailbox
|
||||
|
||||
def register(self, email: str, password: str = None) -> Account:
|
||||
def register(self, email: str, password: Optional[str] = None) -> Account:
|
||||
from platforms.grok.core import GrokRegister
|
||||
from core.config_store import config_store
|
||||
log = getattr(self, '_log_fn', print)
|
||||
|
||||
log = getattr(self, "_log_fn", print)
|
||||
|
||||
# 优先从任务配置读取,兜底从全局配置读取
|
||||
yescaptcha_key = self.config.extra.get("yescaptcha_key") or config_store.get("yescaptcha_key", "")
|
||||
yescaptcha_key = self.config.extra.get("yescaptcha_key") or config_store.get(
|
||||
"yescaptcha_key", ""
|
||||
)
|
||||
captcha_solver = self._make_captcha(key=yescaptcha_key)
|
||||
reg = GrokRegister(captcha_solver=captcha_solver, yescaptcha_key=yescaptcha_key, proxy=self.config.proxy, log_fn=log)
|
||||
mailbox_attempts = 1 if email else int(self.config.extra.get("grok_mailbox_attempts", 8))
|
||||
requested_headless = (self.config.executor_type or "protocol") != "headed"
|
||||
reg = GrokRegister(
|
||||
captcha_solver=captcha_solver,
|
||||
yescaptcha_key=yescaptcha_key,
|
||||
proxy=self.config.proxy,
|
||||
log_fn=log,
|
||||
headless=requested_headless,
|
||||
)
|
||||
mailbox_attempts = (
|
||||
1 if email else int(self.config.extra.get("grok_mailbox_attempts", 8))
|
||||
)
|
||||
otp_timeout = self.get_mailbox_otp_timeout()
|
||||
last_error = None
|
||||
|
||||
@@ -34,23 +53,31 @@ class GrokPlatform(BasePlatform):
|
||||
mail_acct = self.mailbox.get_email()
|
||||
current_email = mail_acct.email if mail_acct else None
|
||||
log(f"邮箱: {current_email}")
|
||||
before_ids = self.mailbox.get_current_ids(mail_acct) if (self.mailbox and mail_acct) else set()
|
||||
before_ids = (
|
||||
self.mailbox.get_current_ids(mail_acct)
|
||||
if (self.mailbox and mail_acct)
|
||||
else set()
|
||||
)
|
||||
|
||||
def otp_cb():
|
||||
log("等待验证码...")
|
||||
if not self.mailbox or not mail_acct:
|
||||
return ""
|
||||
code = self.mailbox.wait_for_code(
|
||||
mail_acct,
|
||||
keyword="",
|
||||
timeout=otp_timeout,
|
||||
before_ids=before_ids,
|
||||
code_pattern=r'[A-Z0-9]{3}-[A-Z0-9]{3}',
|
||||
code_pattern=r"[A-Z0-9]{3}-[A-Z0-9]{3}",
|
||||
)
|
||||
if code:
|
||||
code = code.replace('-', '').replace(' ', '')
|
||||
code = code.replace("-", "").replace(" ", "")
|
||||
log(f"验证码: {code}")
|
||||
return code
|
||||
|
||||
try:
|
||||
if not current_email:
|
||||
raise RuntimeError("未获取到可用邮箱")
|
||||
result = reg.register(
|
||||
email=current_email,
|
||||
password=password,
|
||||
@@ -61,7 +88,9 @@ class GrokPlatform(BasePlatform):
|
||||
last_error = e
|
||||
msg = str(e)
|
||||
if attempt < mailbox_attempts and "邮箱域名被拒绝" in msg:
|
||||
log(f"Grok 邮箱域名被拒绝,切换新邮箱重试 {attempt + 1}/{mailbox_attempts}")
|
||||
log(
|
||||
f"Grok 邮箱域名被拒绝,切换新邮箱重试 {attempt + 1}/{mailbox_attempts}"
|
||||
)
|
||||
continue
|
||||
raise
|
||||
else:
|
||||
|
||||
@@ -13,22 +13,27 @@ import hashlib
|
||||
import threading
|
||||
import base64
|
||||
import os
|
||||
import importlib
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from typing import Tuple, Union, Optional
|
||||
from typing import Any, Optional, Tuple, Union
|
||||
from urllib.parse import urlencode, urlparse, parse_qs
|
||||
from core.browser_runtime import (
|
||||
ensure_browser_display_available,
|
||||
resolve_browser_headless,
|
||||
)
|
||||
from urllib.request import Request, build_opener
|
||||
from core.proxy_utils import build_requests_proxy_config
|
||||
|
||||
try:
|
||||
from curl_cffi import requests as cffi_requests
|
||||
except ImportError:
|
||||
cffi_requests = None
|
||||
cffi_requests: Any = None
|
||||
|
||||
from playwright.sync_api import sync_playwright, TimeoutError, Page, Locator
|
||||
|
||||
try:
|
||||
from playwright_stealth import stealth_sync
|
||||
except ImportError:
|
||||
stealth_sync = importlib.import_module("playwright_stealth").stealth_sync
|
||||
except Exception:
|
||||
stealth_sync = None
|
||||
|
||||
# 如果有全局的 turnstile_strategy,可借用,这里留个 stub
|
||||
@@ -177,6 +182,11 @@ class KiroRegister:
|
||||
self._captured_tokens = {}
|
||||
self._network_debug = []
|
||||
|
||||
def _require_context(self):
|
||||
if self.context is None:
|
||||
raise RuntimeError("浏览器上下文未初始化")
|
||||
return self.context
|
||||
|
||||
def log(self, msg):
|
||||
self.log_fn(f"[{self.tag}] {msg}")
|
||||
|
||||
@@ -223,8 +233,12 @@ class KiroRegister:
|
||||
|
||||
def _init_browser(self):
|
||||
self.pw = sync_playwright().start()
|
||||
launch_opts = {
|
||||
"headless": self.headless,
|
||||
headless, reason = resolve_browser_headless(
|
||||
self.headless, default_headless=False
|
||||
)
|
||||
ensure_browser_display_available(headless)
|
||||
launch_opts: dict[str, Any] = {
|
||||
"headless": headless,
|
||||
"args": [
|
||||
"--disable-blink-features=AutomationControlled",
|
||||
"--no-sandbox",
|
||||
@@ -240,20 +254,22 @@ class KiroRegister:
|
||||
env_timezone = os.getenv("KIRO_TIMEZONE", "").strip()
|
||||
locale = env_locale or profile["locale"]
|
||||
timezone_id = env_timezone or profile["timezone_id"]
|
||||
viewport = dict(profile["viewport"])
|
||||
viewport: dict[str, int] = dict(profile["viewport"])
|
||||
|
||||
self.log(f"浏览器模式: {'headless' if headless else 'headed'} ({reason})")
|
||||
self.log(
|
||||
f"浏览器画像: {profile['name']} / {locale} / {timezone_id} / "
|
||||
f"{viewport['width']}x{viewport['height']}"
|
||||
)
|
||||
self.context = self.browser.new_context(
|
||||
user_agent=profile["user_agent"],
|
||||
locale=locale,
|
||||
timezone_id=timezone_id,
|
||||
viewport=viewport,
|
||||
color_scheme=random.choice(["light", "dark"]),
|
||||
reduced_motion=random.choice(["reduce", "no-preference"]),
|
||||
)
|
||||
context_opts: dict[str, Any] = {
|
||||
"user_agent": profile["user_agent"],
|
||||
"locale": locale,
|
||||
"timezone_id": timezone_id,
|
||||
"viewport": viewport,
|
||||
"color_scheme": random.choice(["light", "dark"]),
|
||||
"reduced_motion": random.choice(["reduce", "no-preference"]),
|
||||
}
|
||||
self.context = self.browser.new_context(**context_opts)
|
||||
self.context.set_extra_http_headers({"Accept-Language": f"{locale},en;q=0.9"})
|
||||
|
||||
# 拦截 Kiro 登录成功相关的请求/响应,提取 Token
|
||||
@@ -653,14 +669,16 @@ class KiroRegister:
|
||||
"user-agent": "KiroIDE",
|
||||
}
|
||||
if cffi_requests is not None:
|
||||
kwargs = {
|
||||
kwargs: dict[str, Any] = {
|
||||
"json": payload,
|
||||
"headers": headers,
|
||||
"timeout": 30,
|
||||
"impersonate": "chrome131",
|
||||
}
|
||||
if self.proxy:
|
||||
kwargs["proxies"] = build_requests_proxy_config(self.proxy)
|
||||
proxies = build_requests_proxy_config(self.proxy)
|
||||
if proxies:
|
||||
kwargs["proxies"] = proxies
|
||||
response = cffi_requests.post(url, **kwargs)
|
||||
if response.status_code != 200:
|
||||
raise RuntimeError(
|
||||
@@ -691,7 +709,7 @@ class KiroRegister:
|
||||
try:
|
||||
cookie_map = {
|
||||
c.get("name", ""): c.get("value", "")
|
||||
for c in self.context.cookies()
|
||||
for c in self._require_context().cookies()
|
||||
if c.get("domain", "").endswith("app.kiro.dev")
|
||||
}
|
||||
if cookie_map.get("AccessToken"):
|
||||
@@ -864,7 +882,7 @@ class KiroRegister:
|
||||
)
|
||||
|
||||
self.log("开始桌面端授权跳转 ...")
|
||||
auth_page = self.context.new_page()
|
||||
auth_page = self._require_context().new_page()
|
||||
auth_page.goto(authorize_url, wait_until="domcontentloaded", timeout=60000)
|
||||
|
||||
started = time.time()
|
||||
@@ -928,7 +946,7 @@ class KiroRegister:
|
||||
if not self.context:
|
||||
self._init_browser()
|
||||
created_browser = True
|
||||
page = self.context.new_page()
|
||||
page = self._require_context().new_page()
|
||||
page.goto(KIRO_SIGNIN_URL, wait_until="domcontentloaded")
|
||||
tokens = self._complete_desktop_idc_flow(
|
||||
email=email, pwd=pwd, otp_callback=otp_callback
|
||||
@@ -948,9 +966,9 @@ class KiroRegister:
|
||||
def register(
|
||||
self,
|
||||
email: str,
|
||||
pwd: str = None,
|
||||
pwd: Optional[str] = None,
|
||||
name: str = "Kiro User",
|
||||
mail_token: str = None,
|
||||
mail_token: Optional[str] = None,
|
||||
otp_timeout: int = 120,
|
||||
otp_callback=None,
|
||||
) -> Tuple[bool, dict]:
|
||||
@@ -962,7 +980,7 @@ class KiroRegister:
|
||||
page = None
|
||||
try:
|
||||
self._init_browser()
|
||||
page = self.context.new_page()
|
||||
page = self._require_context().new_page()
|
||||
|
||||
if stealth_sync:
|
||||
stealth_sync(page)
|
||||
@@ -1031,6 +1049,8 @@ class KiroRegister:
|
||||
otp_input = stage_input if stage == "otp" else None
|
||||
if stage == "name":
|
||||
self.log("2. 填写名字 (Your name)...")
|
||||
if stage_input is None:
|
||||
return False, {"error": "未找到姓名输入框"}
|
||||
self._type_like_human(page, stage_input, name)
|
||||
self._click_primary_button(page)
|
||||
self._human_sleep(1.1, 2.4)
|
||||
@@ -1054,6 +1074,8 @@ class KiroRegister:
|
||||
return False, {"error": "未获取到邮箱验证码(OTP Timeout)"}
|
||||
|
||||
self.log(f"获取到验证码: {otp_code},正在填入...")
|
||||
if otp_input is None:
|
||||
return False, {"error": "未找到 OTP 输入框"}
|
||||
self._type_like_human(page, otp_input, otp_code)
|
||||
self._click_primary_button(page)
|
||||
self._human_sleep(1.0, 2.2)
|
||||
@@ -1149,7 +1171,7 @@ class KiroRegister:
|
||||
"domain": c.get("domain", ""),
|
||||
"path": c.get("path", ""),
|
||||
}
|
||||
for c in self.context.cookies()
|
||||
for c in self._require_context().cookies()
|
||||
if "kiro.dev" in c.get("domain", "")
|
||||
or "aws" in c.get("domain", "")
|
||||
]
|
||||
@@ -1205,7 +1227,7 @@ class KiroRegister:
|
||||
with open("kiro_error.html", "w", encoding="utf-8") as f:
|
||||
f.write(page.content())
|
||||
self.log("HTML 已保存为 kiro_error.html")
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
return False, {"error": str(e)}
|
||||
finally:
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
"""Kiro 平台插件 - 基于 AWS Builder ID 注册"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from core.base_platform import BasePlatform, Account, AccountStatus, RegisterConfig
|
||||
from core.base_mailbox import BaseMailbox
|
||||
from core.registry import register
|
||||
@@ -10,37 +13,47 @@ class KiroPlatform(BasePlatform):
|
||||
display_name = "Kiro (AWS Builder ID)"
|
||||
version = "1.0.0"
|
||||
|
||||
def __init__(self, config: RegisterConfig = None, mailbox: BaseMailbox = None):
|
||||
super().__init__(config)
|
||||
def __init__(
|
||||
self,
|
||||
config: Optional[RegisterConfig] = None,
|
||||
mailbox: Optional[BaseMailbox] = None,
|
||||
):
|
||||
super().__init__(config or RegisterConfig())
|
||||
self.mailbox = mailbox
|
||||
|
||||
def register(self, email: str, password: str = None) -> Account:
|
||||
def register(self, email: str, password: Optional[str] = None) -> Account:
|
||||
from platforms.kiro.core import KiroRegister
|
||||
|
||||
proxy = self.config.proxy
|
||||
laoudo_account_id = self.config.extra.get("laoudo_account_id", "")
|
||||
requested_headless = (self.config.executor_type or "protocol") != "headed"
|
||||
|
||||
reg = KiroRegister(proxy=proxy, tag="KIRO")
|
||||
log_fn = getattr(self, '_log_fn', print)
|
||||
reg.log = lambda msg: log_fn(msg)
|
||||
reg = KiroRegister(proxy=proxy, tag="KIRO", headless=requested_headless)
|
||||
log_fn = getattr(self, "_log_fn", print)
|
||||
reg.log_fn = log_fn
|
||||
|
||||
otp_timeout = int(self.config.extra.get("otp_timeout", 120))
|
||||
|
||||
if self.mailbox:
|
||||
mail_acct = self.mailbox.get_email()
|
||||
mailbox = self.mailbox
|
||||
mail_acct = mailbox.get_email()
|
||||
if not mail_acct:
|
||||
raise RuntimeError("未获取到可用邮箱账号")
|
||||
email = email or mail_acct.email
|
||||
log_fn(f"邮箱: {mail_acct.email}")
|
||||
_before = self.mailbox.get_current_ids(mail_acct)
|
||||
_before = mailbox.get_current_ids(mail_acct)
|
||||
|
||||
def otp_cb():
|
||||
log_fn("等待验证码...")
|
||||
code = self.mailbox.wait_for_code(
|
||||
code = mailbox.wait_for_code(
|
||||
mail_acct,
|
||||
keyword="builder id",
|
||||
timeout=otp_timeout,
|
||||
before_ids=_before,
|
||||
code_pattern=r'(?is)(?:verification\s+code|验证码)[^0-9]{0,20}(\d{6})',
|
||||
code_pattern=r"(?is)(?:verification\s+code|验证码)[^0-9]{0,20}(\d{6})",
|
||||
)
|
||||
if code: log_fn(f"验证码: {code}")
|
||||
if code:
|
||||
log_fn(f"验证码: {code}")
|
||||
return code
|
||||
else:
|
||||
otp_cb = None
|
||||
@@ -85,6 +98,7 @@ class KiroPlatform(BasePlatform):
|
||||
return False
|
||||
try:
|
||||
from platforms.kiro.switch import refresh_kiro_token
|
||||
|
||||
ok, _ = refresh_kiro_token(
|
||||
refresh_token,
|
||||
extra.get("clientId", ""),
|
||||
@@ -106,7 +120,9 @@ class KiroPlatform(BasePlatform):
|
||||
|
||||
if action_id == "switch_account":
|
||||
from platforms.kiro.switch import (
|
||||
refresh_kiro_token, switch_kiro_account, restart_kiro_ide,
|
||||
refresh_kiro_token,
|
||||
switch_kiro_account,
|
||||
restart_kiro_ide,
|
||||
)
|
||||
from platforms.kiro.core import KiroRegister
|
||||
from core.base_mailbox import create_mailbox, MailboxAccount
|
||||
@@ -119,11 +135,14 @@ class KiroPlatform(BasePlatform):
|
||||
# Kiro 桌面端需要完整的 Builder ID SSO 缓存。
|
||||
# 只有 accessToken/sessionToken 的网页态账号无法稳定切到桌面应用。
|
||||
if not access_token:
|
||||
return {"ok": False, "error": "当前账号缺少 accessToken,无法切换到桌面应用"}
|
||||
return {
|
||||
"ok": False,
|
||||
"error": "当前账号缺少 accessToken,无法切换到桌面应用",
|
||||
}
|
||||
if not refresh_token or not client_id or not client_secret:
|
||||
if account.email and account.password:
|
||||
reg = KiroRegister(proxy=self.config.proxy, tag="KIRO-SWITCH")
|
||||
reg.log = getattr(self, "_log_fn", print)
|
||||
reg.log_fn = getattr(self, "_log_fn", print)
|
||||
otp_callback = None
|
||||
mailbox_extra = dict(self.config.extra or {})
|
||||
for key in (
|
||||
@@ -142,7 +161,7 @@ class KiroPlatform(BasePlatform):
|
||||
mailbox = create_mailbox(
|
||||
provider=mail_provider,
|
||||
extra=mailbox_extra,
|
||||
proxy=self.config.proxy,
|
||||
proxy=self.config.proxy or "",
|
||||
)
|
||||
mail_account = MailboxAccount(
|
||||
email=account.email,
|
||||
@@ -158,16 +177,18 @@ class KiroPlatform(BasePlatform):
|
||||
keyword="",
|
||||
timeout=45,
|
||||
before_ids=before_ids,
|
||||
code_pattern=r'(?is)(?:verification\s+code|验证码)[^0-9]{0,20}(\d{6})',
|
||||
code_pattern=r"(?is)(?:verification\s+code|验证码)[^0-9]{0,20}(\d{6})",
|
||||
)
|
||||
except Exception:
|
||||
reg.log("未等到新验证码,回退读取最近一封身份验证邮件 ...")
|
||||
reg.log(
|
||||
"未等到新验证码,回退读取最近一封身份验证邮件 ..."
|
||||
)
|
||||
code = mailbox.wait_for_code(
|
||||
mail_account,
|
||||
keyword="",
|
||||
timeout=15,
|
||||
before_ids=None,
|
||||
code_pattern=r'(?is)(?:verification\s+code|验证码)[^0-9]{0,20}(\d{6})',
|
||||
before_ids=set(),
|
||||
code_pattern=r"(?is)(?:verification\s+code|验证码)[^0-9]{0,20}(\d{6})",
|
||||
)
|
||||
if code:
|
||||
reg.log(f"桌面授权验证码: {code}")
|
||||
@@ -219,13 +240,16 @@ class KiroPlatform(BasePlatform):
|
||||
return {"ok": False, "error": msg}
|
||||
|
||||
restart_ok, restart_msg = restart_kiro_ide()
|
||||
return {"ok": True, "data": {
|
||||
"accessToken": access_token,
|
||||
"refreshToken": refresh_token,
|
||||
"clientId": client_id,
|
||||
"clientSecret": client_secret,
|
||||
"message": f"{msg}。{restart_msg}" if restart_ok else msg,
|
||||
}}
|
||||
return {
|
||||
"ok": True,
|
||||
"data": {
|
||||
"accessToken": access_token,
|
||||
"refreshToken": refresh_token,
|
||||
"clientId": client_id,
|
||||
"clientSecret": client_secret,
|
||||
"message": f"{msg}。{restart_msg}" if restart_ok else msg,
|
||||
},
|
||||
}
|
||||
|
||||
elif action_id == "refresh_token":
|
||||
from platforms.kiro.switch import refresh_kiro_token
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
"""Tavily 平台插件"""
|
||||
import random, string
|
||||
|
||||
import random
|
||||
import string
|
||||
from typing import Optional
|
||||
|
||||
from core.base_platform import BasePlatform, Account, AccountStatus, RegisterConfig
|
||||
from core.base_mailbox import BaseMailbox
|
||||
from core.registry import register
|
||||
@@ -12,77 +16,121 @@ class TavilyPlatform(BasePlatform):
|
||||
version = "1.0.0"
|
||||
supported_executors = ["protocol", "headless", "headed"]
|
||||
|
||||
def __init__(self, config: RegisterConfig = None, mailbox: BaseMailbox = None):
|
||||
super().__init__(config)
|
||||
def __init__(
|
||||
self,
|
||||
config: Optional[RegisterConfig] = None,
|
||||
mailbox: Optional[BaseMailbox] = None,
|
||||
):
|
||||
super().__init__(config or RegisterConfig())
|
||||
self.mailbox = mailbox
|
||||
|
||||
def _register_browser(self, email: str, password: str) -> Account:
|
||||
import sys, os, importlib, pathlib
|
||||
import importlib
|
||||
import os
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
extra = self.config.extra or {}
|
||||
os.environ["LOCAL_SOLVER_URL"] = "http://127.0.0.1:8889"
|
||||
os.environ["SOLVER_PORT"] = "8889"
|
||||
os.environ["REGISTER_HEADLESS"] = "true"
|
||||
if extra.get("duckmail_api_key"): os.environ["DUCKMAIL_API_KEY"] = extra["duckmail_api_key"]
|
||||
if extra.get("duckmail_api_url"): os.environ["DUCKMAIL_API_URL"] = extra["duckmail_api_url"]
|
||||
if extra.get("duckmail_domain"): os.environ["DUCKMAIL_DOMAIN"] = extra["duckmail_domain"]
|
||||
tavily_gen_path = str(pathlib.Path(__file__).resolve().parents[3] / "tavily-key-generator")
|
||||
os.environ["REGISTER_HEADLESS"] = (
|
||||
"false" if (self.config.executor_type or "") == "headed" else "true"
|
||||
)
|
||||
if extra.get("duckmail_api_key"):
|
||||
os.environ["DUCKMAIL_API_KEY"] = extra["duckmail_api_key"]
|
||||
if extra.get("duckmail_api_url"):
|
||||
os.environ["DUCKMAIL_API_URL"] = extra["duckmail_api_url"]
|
||||
if extra.get("duckmail_domain"):
|
||||
os.environ["DUCKMAIL_DOMAIN"] = extra["duckmail_domain"]
|
||||
tavily_gen_path = str(
|
||||
pathlib.Path(__file__).resolve().parents[3] / "tavily-key-generator"
|
||||
)
|
||||
if tavily_gen_path not in sys.path:
|
||||
sys.path.insert(0, tavily_gen_path)
|
||||
if "config" in sys.modules: importlib.reload(sys.modules["config"])
|
||||
if "tavily_browser_solver" in sys.modules: importlib.reload(sys.modules["tavily_browser_solver"])
|
||||
from tavily_browser_solver import register_with_browser_solver
|
||||
if "config" in sys.modules:
|
||||
importlib.reload(sys.modules["config"])
|
||||
if "tavily_browser_solver" in sys.modules:
|
||||
importlib.reload(sys.modules["tavily_browser_solver"])
|
||||
solver_mod = importlib.import_module("tavily_browser_solver")
|
||||
register_with_browser_solver = solver_mod.register_with_browser_solver
|
||||
|
||||
api_key = register_with_browser_solver(email, password)
|
||||
if not api_key:
|
||||
raise RuntimeError("浏览器注册失败")
|
||||
return Account(platform="tavily", email=email, password=password,
|
||||
status=AccountStatus.REGISTERED, extra={"api_key": api_key})
|
||||
return Account(
|
||||
platform="tavily",
|
||||
email=email,
|
||||
password=password,
|
||||
status=AccountStatus.REGISTERED,
|
||||
extra={"api_key": api_key},
|
||||
)
|
||||
|
||||
def register(self, email: str, password: str = None) -> Account:
|
||||
def register(self, email: str, password: Optional[str] = None) -> Account:
|
||||
if not password:
|
||||
password = "".join(random.choices(string.ascii_letters + string.digits + "!@#", k=14))
|
||||
log = getattr(self, '_log_fn', print)
|
||||
password = "".join(
|
||||
random.choices(string.ascii_letters + string.digits + "!@#", k=14)
|
||||
)
|
||||
log = getattr(self, "_log_fn", print)
|
||||
|
||||
if (self.config.executor_type or "") in ("headless", "headed"):
|
||||
log(f"使用浏览器模式注册: {email}")
|
||||
return self._register_browser(email, password)
|
||||
|
||||
mail_acct = self.mailbox.get_email() if self.mailbox else None
|
||||
email = email or (mail_acct.email if mail_acct else None)
|
||||
mailbox = self.mailbox
|
||||
mail_acct = mailbox.get_email() if mailbox else None
|
||||
email = email or (mail_acct.email if mail_acct else "")
|
||||
if not email:
|
||||
raise RuntimeError("未获取到可用邮箱")
|
||||
log(f"邮箱: {email}")
|
||||
before_ids = self.mailbox.get_current_ids(mail_acct) if mail_acct else set()
|
||||
before_ids = mailbox.get_current_ids(mail_acct) if (mailbox and mail_acct) else set()
|
||||
otp_timeout = self.get_mailbox_otp_timeout()
|
||||
|
||||
def otp_cb():
|
||||
log("等待验证码邮件...")
|
||||
code = self.mailbox.wait_for_code(
|
||||
if not mailbox or not mail_acct:
|
||||
return ""
|
||||
code = mailbox.wait_for_code(
|
||||
mail_acct,
|
||||
keyword="",
|
||||
timeout=otp_timeout,
|
||||
before_ids=before_ids,
|
||||
)
|
||||
if code: log(f"验证码: {code}")
|
||||
if code:
|
||||
log(f"验证码: {code}")
|
||||
return code
|
||||
|
||||
captcha = self._make_captcha(key=self.config.extra.get("yescaptcha_key", ""))
|
||||
|
||||
from platforms.tavily.core import TavilyRegister
|
||||
|
||||
with self._make_executor() as ex:
|
||||
reg = TavilyRegister(executor=ex, captcha=captcha, log_fn=log)
|
||||
result = reg.register(email=email, password=password,
|
||||
otp_callback=otp_cb if self.mailbox else None)
|
||||
result = reg.register(
|
||||
email=email,
|
||||
password=password,
|
||||
otp_callback=otp_cb if self.mailbox else None,
|
||||
)
|
||||
|
||||
return Account(platform="tavily", email=result["email"], password=result["password"],
|
||||
status=AccountStatus.REGISTERED, extra={"api_key": result["api_key"]})
|
||||
return Account(
|
||||
platform="tavily",
|
||||
email=result["email"],
|
||||
password=result["password"],
|
||||
status=AccountStatus.REGISTERED,
|
||||
extra={"api_key": result["api_key"]},
|
||||
)
|
||||
|
||||
def check_valid(self, account: Account) -> bool:
|
||||
api_key = account.extra.get("api_key", "")
|
||||
if not api_key:
|
||||
return False
|
||||
import requests
|
||||
|
||||
try:
|
||||
r = requests.post("https://api.tavily.com/search",
|
||||
json={"api_key": api_key, "query": "test", "max_results": 1},
|
||||
timeout=10)
|
||||
r = requests.post(
|
||||
"https://api.tavily.com/search",
|
||||
json={"api_key": api_key, "query": "test", "max_results": 1},
|
||||
timeout=10,
|
||||
)
|
||||
return r.status_code != 401
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
Reference in New Issue
Block a user