From 8fdf8d4b69132fb6454cfb145709b0cdeb920527 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Mon, 22 Jun 2026 22:44:01 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E4=BB=A3=E7=90=86=E8=8E=B7?= =?UTF-8?q?=E5=8F=96=E5=92=8C=E7=99=BB=E5=BD=95=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 每个账号独立获取代理,不再共用一个代理IP - 代理预检失败只标记该账号失败,不终止整个批次 - 代理验证改为并发:verify_proxies_concurrent 多IP同时验证 - resolve_working_proxy 多IP并发验证,找到可用即返回 - parse_proxy_response 改为返回列表,支持多行/逗号分隔 - 邮箱验证码匹配收件人地址,防止多账号并发取错验证码 - 修正 mail.bdhg.xyz 旧数据端口和SSL配置 - bdhg.xyz 默认配置改为 143端口非SSL Co-Authored-By: Claude Fable 5 --- core/douyu/email_verifier.py | 32 +++++++++++++-- core/douyu/proxy.py | 59 +++++++++++++++++++++++---- web/backend/database.py | 6 +++ web/backend/services/login_service.py | 43 ++++++++----------- 4 files changed, 102 insertions(+), 38 deletions(-) diff --git a/core/douyu/email_verifier.py b/core/douyu/email_verifier.py index e1c1ef3..3d25b7b 100644 --- a/core/douyu/email_verifier.py +++ b/core/douyu/email_verifier.py @@ -158,9 +158,10 @@ class EmailVerifier: subject = self._decode_subject(msg.get('Subject', '')) from_addr = msg.get('From', '') + to_addr = msg.get('To', '') or msg.get('Delivered-To', '') or '' body = self._get_email_body(msg) - if not self._is_douyu_email(subject, from_addr, body): + if not self._is_douyu_email(subject, from_addr, body, to_addr=to_addr): continue code = self._extract_verification_code(body) @@ -257,8 +258,20 @@ class EmailVerifier: except (TypeError, ValueError): return None - def _is_douyu_email(self, subject: str, from_addr: str, body: str = "") -> bool: - """判断是否是斗鱼的邮件""" + def _is_douyu_email(self, subject: str, from_addr: str, body: str = "", to_addr: str = "") -> bool: + """判断是否是发给当前账号的斗鱼验证码邮件""" + # 先检查收件人:多个账号共用同一个IMAP时,必须匹配收件人地址 + if to_addr and self.username: + # to_addr 可能是 "name " 格式,提取邮箱 + to_email = re.search(r'[\w.+-]+@[\w.-]+', to_addr) + if to_email: + to_lower = to_email.group(0).lower() + my_lower = self.username.lower() + # 精确匹配,或者 catch-all 前缀匹配 + if to_lower != my_lower and not my_lower.endswith('@' + to_lower.split('@')[-1]): + logger.debug(f"跳过非当前账号邮件: 收件人={to_lower}, 当前={my_lower}") + return False + douyu_keywords = ['斗鱼', 'douyu', 'douyutv'] verify_keywords = ['验证码', '安全验证', '登录验证', '动态码', '校验码'] combined = f"{subject}\n{from_addr}\n{body[:500]}".lower() @@ -354,4 +367,15 @@ def get_email_config_for_account(email_address: str) -> dict: } domain = email_address.split('@')[-1].lower() - return configs.get(domain, {'server': '111.229.206.54', 'port': 143, 'ssl': False}) + configs = { + 'qq.com': {'server': 'imap.qq.com', 'port': 993, 'ssl': True}, + '163.com': {'server': 'imap.163.com', 'port': 993, 'ssl': True}, + '126.com': {'server': 'imap.126.com', 'port': 993, 'ssl': True}, + 'gmail.com': {'server': 'imap.gmail.com', 'port': 993, 'ssl': True}, + 'outlook.com': {'server': 'outlook.office365.com', 'port': 993, 'ssl': True}, + 'hotmail.com': {'server': 'outlook.office365.com', 'port': 993, 'ssl': True}, + 'bdhg.xyz': {'server': 'mail.bdhg.xyz', 'port': 143, 'ssl': False}, + } + + domain = email_address.split('@')[-1].lower() + return configs.get(domain, {'server': domain, 'port': 143, 'ssl': False}) diff --git a/core/douyu/proxy.py b/core/douyu/proxy.py index 254e9e7..e1ba608 100644 --- a/core/douyu/proxy.py +++ b/core/douyu/proxy.py @@ -164,6 +164,51 @@ def verify_proxy_url(proxy_url: str, timeout: tuple = (4, 6)) -> tuple[bool, str return False, '代理验证失败(所有目标不可达)' +def verify_proxies_concurrent(proxy_urls: list[str], timeout: tuple = (4, 6), max_workers: int = 5) -> tuple[Optional[str], str]: + """ + 并发验证多个代理URL,返回第一个可用的。 + + Args: + proxy_urls: 代理URL列表 + timeout: 验证超时 + max_workers: 最大并发数 + + Returns: + (可用的代理URL, 消息) + """ + if not proxy_urls: + return None, '无代理可验证' + + if len(proxy_urls) == 1: + ok, msg = verify_proxy_url(proxy_urls[0], timeout) + if ok: + return proxy_urls[0], msg + return None, msg + + from concurrent.futures import ThreadPoolExecutor, as_completed + + with ThreadPoolExecutor(max_workers=min(max_workers, len(proxy_urls))) as executor: + future_map = { + executor.submit(verify_proxy_url, p, timeout): p + for p in proxy_urls + } + for future in as_completed(future_map): + proxy_url = future_map[future] + try: + ok, msg = future.result() + if ok: + logger.success(f"并发验证找到可用代理: {proxy_url}") + # 取消剩余任务 + for f in future_map: + if f != future: + f.cancel() + return proxy_url, msg + except Exception: + continue + + return None, f'共 {len(proxy_urls)} 个代理均不可用' + + def resolve_working_proxy( api_url: str, whitelist_uid: str = "", @@ -210,14 +255,12 @@ def resolve_working_proxy( proxy_urls, whitelist_ip = parse_proxy_response(text) if proxy_urls: - log('info', f'获取到 {len(proxy_urls)} 个代理,逐一验证') - for proxy_url in proxy_urls: - ok, msg = verify_proxy_url(proxy_url) - if ok: - log('success', f'代理预检成功: {proxy_url}') - return proxy_url, msg - log('warning', f'代理 {proxy_url} 不可用: {msg}') - last_error = f'共 {len(proxy_urls)} 个代理均不可用' + log('info', f'获取到 {len(proxy_urls)} 个代理,并发验证') + proxy_url, msg = verify_proxies_concurrent(proxy_urls) + if proxy_url: + log('success', f'代理预检成功: {proxy_url}') + return proxy_url, msg + last_error = msg log('warning', f'代理预检 {attempt}/{max_attempts}: {last_error}') continue diff --git a/web/backend/database.py b/web/backend/database.py index 05e0c2b..ba1faee 100644 --- a/web/backend/database.py +++ b/web/backend/database.py @@ -64,6 +64,12 @@ def _migrate(): "WHERE email_imap_server = '111.229.206.54'" )) conn.commit() + # 修正旧数据中使用 mail.bdhg.xyz 的账号:993端口不通,改用143非SSL + conn.execute(text( + "UPDATE accounts SET email_imap_port = 143, email_imap_ssl = 0 " + "WHERE email_imap_server = 'mail.bdhg.xyz'" + )) + conn.commit() def _seed(): diff --git a/web/backend/services/login_service.py b/web/backend/services/login_service.py index b36ca1d..febe8de 100644 --- a/web/backend/services/login_service.py +++ b/web/backend/services/login_service.py @@ -93,8 +93,8 @@ class LoginBatchRunner: return None, '' - def _execute_one(self, task_id: int, acc_info: dict, proxy_dict: Optional[dict], total: int): - """在独立线程中执行单个账号登录,使用独立的 DB 会话。""" + def _execute_one(self, task_id: int, acc_info: dict, total: int): + """在独立线程中执行单个账号登录,使用独立的 DB 会话和代理。""" if self._stop.is_set(): self._push_log("warning", f"任务已停止,跳过: {acc_info['username']}") return @@ -114,6 +114,20 @@ class LoginBatchRunner: self._push_log("info", f"[{current}/{total}] 开始登录: {acc_info['username']}") + # 每个账号独立获取代理 + proxy_dict, proxy_msg = self._resolve_proxy() + if proxy_msg: + self._push_log("info", f"[{current}] {proxy_msg}") + + # 代理预检失败时,该账号标记失败但不终止整个批次 + if self.proxy_config and self.proxy_config.enabled and not proxy_dict: + task.status = "error" + task.message = f"代理不可用: {proxy_msg}" + task.finished_at = datetime.now(timezone.utc) + worker_db.commit() + self._push_log("error", f"[{current}] {acc_info['username']} 代理不可用: {proxy_msg}") + return + try: account = Account( username=acc_info["username"], @@ -217,29 +231,7 @@ class LoginBatchRunner: self._push_log("result", "") return - # 代理预检 - proxy_dict, proxy_msg = self._resolve_proxy() - if proxy_msg: - self._push_log("info", proxy_msg) - - # 如果启用了代理但预检失败,终止任务 - if self.proxy_config and self.proxy_config.enabled and not proxy_dict: - self._push_log("error", f"代理不可用,任务终止: {proxy_msg}") - for item in task_infos: - worker_db = SessionLocal() - try: - task = worker_db.query(LoginTask).filter(LoginTask.id == item["task_id"]).first() - if task: - task.status = "error" - task.message = f"代理不可用: {proxy_msg}" - task.finished_at = datetime.now(timezone.utc) - worker_db.commit() - finally: - worker_db.close() - self._push_log("result", "") - return - - # 并发执行登录 + # 并发执行登录,每个账号独立获取代理 with ThreadPoolExecutor(max_workers=concurrency) as executor: futures = [] for item in task_infos: @@ -250,7 +242,6 @@ class LoginBatchRunner: self._execute_one, item["task_id"], item["acc_info"], - proxy_dict, total, ) futures.append(future)