diff --git a/.env.example b/.env.example index 6e44b74..6a75fca 100644 --- a/.env.example +++ b/.env.example @@ -21,5 +21,8 @@ COOKIE_SECURE=false # CORS 允许的源(逗号分隔,不设则默认开发环境) # CORS_ORIGINS=https://example.com,https://www.example.com +# Roundcube 邮件验证码读取服务地址(可选;不填则使用默认服务) +# MAIL_ROUNDCUBE_URL=http://127.0.0.1:8000/ + # Uvicorn reload(开发模式设为 true,生产环境保持 false) UVICORN_RELOAD=false diff --git a/README.md b/README.md index 88ba1b7..4cbffab 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,10 @@ cd web/frontend && npm install && npm run dev python -c "import secrets; print(secrets.token_urlsafe(32))" ``` +### 邮件验证码服务 + +验证码读取默认使用内置的 Roundcube 服务地址;如部署自己的邮件读取服务,可在 `.env` 中设置 `MAIL_ROUNDCUBE_URL` 覆盖。 + ## 角色权限 | 角色 | 权限 | diff --git a/core/douyu/email_verifier.py b/core/douyu/email_verifier.py index 9345e2e..c3d781f 100644 --- a/core/douyu/email_verifier.py +++ b/core/douyu/email_verifier.py @@ -1,6 +1,7 @@ -"""邮箱验证模块 - 优先通过 Roundcube Webmail API 获取验证码,read.php 作为备用""" +"""邮箱验证模块 - 通过 Roundcube Webmail API 获取验证码。""" import html +import os import re import threading import time @@ -11,15 +12,12 @@ from loguru import logger import requests -# 旧接口(时间解析不可靠,作为备用) -READ_PHP_URL = "http://111.229.206.54:8000/read.php" - -# Roundcube Webmail 地址 -ROUNDCUBE_URL = "http://111.229.206.54:8000/" +# Roundcube Webmail 地址。可通过环境变量覆盖。 +ROUNDCUBE_URL = os.getenv("MAIL_ROUNDCUBE_URL", "http://111.229.206.54:8000/") class EmailVerifier: - """邮箱验证器(Roundcube API 优先,read.php 备用)""" + """邮箱验证器(Roundcube API)。""" def __init__( self, @@ -32,7 +30,7 @@ class EmailVerifier: lookback_minutes: int = 10, max_messages: int = 20, use_ssl: bool = False, - mail_api_url: str = READ_PHP_URL, + roundcube_url: str = "", ): # 兼容旧参数名 self.imap_server = imap_server @@ -40,8 +38,7 @@ class EmailVerifier: self.username = username # 邮箱地址 self.password = password # 邮箱密码 self.timeout = timeout - self.read_php_url = mail_api_url or READ_PHP_URL - self.roundcube_url = ROUNDCUBE_URL + self.roundcube_url = roundcube_url or ROUNDCUBE_URL # Roundcube 会话(懒初始化) self._rc_session: Optional[requests.Session] = None @@ -265,7 +262,7 @@ class EmailVerifier: stop_event: Optional[threading.Event] = None, ) -> str: """ - 轮询获取斗鱼验证码。优先使用 Roundcube API,失败则回退到 read.php。 + 轮询获取斗鱼验证码。 Args: max_wait: 最大等待时间(秒) @@ -281,33 +278,19 @@ class EmailVerifier: deadline = time.monotonic() + max_wait last_error = "" - tried_roundcube = False while time.monotonic() < deadline: if stop_event and stop_event.is_set(): raise InterruptedError("任务已停止") - # 优先尝试 Roundcube - if not tried_roundcube or self._rc_logged_in: - try: - code = self._fetch_code_via_roundcube(after_timestamp, allow_old_seconds) - if code: - logger.success(f"获取到验证码: {code}") - return code - except Exception as e: - last_error = str(e) - logger.warning(f"Roundcube 读邮件异常: {e}") - tried_roundcube = True - - # 回退到 read.php try: - code = self._fetch_code_via_readphp(after_timestamp, allow_old_seconds) + code = self._fetch_code_via_roundcube(after_timestamp, allow_old_seconds) if code: logger.success(f"获取到验证码: {code}") return code except Exception as e: last_error = str(e) - logger.warning(f"read.php 读邮件异常: {e}") + logger.warning(f"Roundcube 读邮件异常: {e}") logger.debug("未找到验证码,等待中...") sleep_deadline = time.monotonic() + interval @@ -354,118 +337,8 @@ class EmailVerifier: return None - # ── read.php 方式获取验证码(备用) ───────────────────── - - def _fetch_code_via_readphp( - self, - after_timestamp: Optional[float] = None, - allow_old_seconds: int = 15, - ) -> Optional[str]: - """通过 read.php HTTP 接口读取最新邮件并提取验证码""" - try: - response = requests.get( - self.read_php_url, - params={ - "yhm": self.username, - "mm": self.password, - }, - timeout=self.timeout, - ) - response.raise_for_status() - content = response.text - - # 检查是否是斗鱼验证码邮件 - if not self._is_douyu_email(content): - logger.debug("read.php: 最新邮件不是斗鱼验证码邮件") - return None - - # 检查邮件时间(read.php 时间不可靠,放宽判断) - if after_timestamp and not self._is_email_recent(content, after_timestamp, allow_old_seconds): - logger.debug("read.php: 邮件时间早于验证码发送时间,跳过") - return None - - # 提取验证码 - code = self._extract_verification_code(content) - if code: - logger.info(f"read.php: 从邮件中提取到验证码: {code}") - return code - - return None - - except requests.RequestException as e: - logger.warning(f"read.php: 请求失败: {e}") - return None - # ── 通用工具方法 ─────────────────────────────────────── - def _is_douyu_email(self, content: str) -> bool: - """判断邮件内容是否是斗鱼验证码邮件""" - douyu_keywords = ['斗鱼', 'douyu', 'douyutv'] - verify_keywords = ['验证码', '安全验证', '登录验证', '动态码', '校验码'] - combined = content.lower() - - return ( - any(keyword in combined for keyword in douyu_keywords) - or any(keyword in content for keyword in verify_keywords) - ) - - def _is_email_recent( - self, - content: str, - after_timestamp: float, - allow_old_seconds: int = 15, - ) -> bool: - """ - 检查 read.php 返回的邮件时间是否晚于验证码发送时间。 - 注意:read.php 的时间解析不可靠,此处仅作参考判断。 - """ - from email.utils import parsedate_to_datetime - - # 先清理 HTML 标签,再提取时间 - clean = re.sub(r'<[^>]+>', ' ', content) - clean = re.sub(r'\s+', ' ', clean) - - # 匹配 RFC2822 格式时间 - time_match = re.search( - r'发信时间[::]?\s*' - r'((?:Mon|Tue|Wed|Thu|Fri|Sat|Sun),\s*\d{1,2}\s+' - r'(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+' - r'\d{4}\s+\d{2}:\d{2}:\d{2}\s*[+-]\d{4})', - clean, - ) - if not time_match: - # 尝试匹配不带星期的时间格式 - time_match2 = re.search( - r'发信时间[::]?\s*(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})', - clean, - ) - if time_match2: - time_str = time_match2.group(1).strip() - try: - from datetime import datetime as dt - msg_time = dt.strptime(time_str, '%Y-%m-%d %H:%M:%S') - if msg_time.timestamp() < after_timestamp - allow_old_seconds: - logger.debug(f"邮件时间 {time_str} 早于发送时间,跳过旧邮件") - return False - return True - except ValueError: - pass - # read.php 时间不可靠时,不再保守拒绝,改为放行 - # 让验证码提取逻辑自行判断 - logger.debug("read.php: 未找到可解析的发信时间,跳过时间检查直接提取验证码") - return True - - time_str = time_match.group(1).strip() - try: - msg_time = parsedate_to_datetime(time_str) - if msg_time and msg_time.timestamp() < after_timestamp - allow_old_seconds: - logger.debug(f"邮件时间 {time_str} 早于发送时间,跳过旧邮件") - return False - return True - except (TypeError, ValueError): - logger.debug(f"无法解析邮件时间: {time_str},跳过时间检查") - return True - def _extract_verification_code(self, text: str) -> Optional[str]: """从文本中提取6位验证码""" # 清理HTML标签和实体 diff --git a/core/douyu/login.py b/core/douyu/login.py index 1eae6d0..f2b21a7 100644 --- a/core/douyu/login.py +++ b/core/douyu/login.py @@ -14,7 +14,6 @@ from .crypto import encrypt_password, encrypt_nickname_or_phone from .email_verifier import EmailVerifier from .proxy import ProxyManager, get_proxy_manager -from core.geetest import run_solver from core.geetest.v3_slide.solver import ( _generate_seed, get_w1, get_w2, ) @@ -53,7 +52,6 @@ class DouyuLogin: LOGIN_API = "https://passport.douyu.com/wgapi/member/passport/login" SEND_EMAIL_API = "https://passport.douyu.com/wgapi/member/passport/remotelogin/sendemail" VERIFY_API = "https://passport.douyu.com/wgapi/member/passport/remotelogin/verify" - LOGIN_CALLBACK_API = "https://www.douyu.com/api/passport/login" WEBLOGIN_API = "https://msg.douyu.com/webLogin" LOGIN_REFERER = ( "https://passport.douyu.com/index/login?" @@ -72,10 +70,9 @@ class DouyuLogin: proxy: Optional[str | Mapping[str, str]] = None, proxy_api_url: Optional[str] = None, timeout: tuple[float, float] = REQUEST_TIMEOUT, - max_geetest_retries: int = 5, max_proxy_retries: int = 0, - max_login_retries: int = 3, - max_total_time: float = 300, + max_login_retries: int = 0, + max_total_time: float = 0, whitelist_uid: str = "", whitelist_ukey: str = "", proxy_manager: Optional[ProxyManager] = None, @@ -84,10 +81,9 @@ class DouyuLogin: self.account = account self.proxy = proxy self.timeout = timeout - self.max_geetest_retries = max_geetest_retries self.max_proxy_retries = max_proxy_retries # 0=无限切换直到成功 - self.max_login_retries = max_login_retries # 登录整体重试次数(换代理从头重跑) - self.max_total_time = max_total_time # 单账号登录总时长上限(秒),超时则放弃 + self.max_login_retries = max_login_retries # 0=无限整体重试直到成功 + self.max_total_time = max_total_time # 0=不限制单账号登录总时长 self.stop_event = stop_event self.session = requests.Session() @@ -308,34 +304,39 @@ class DouyuLogin: """ 完整登录流程(带整体重试)。 - 任何步骤失败时,换新代理从头重跑,最多重试 max_login_retries 次。 - 整体超时 max_total_time 秒后放弃。 + 任何步骤失败时,换新代理从头重跑。 + max_login_retries=0 表示无限重试,max_total_time=0 表示不限制总时长。 Returns: LoginResult: 登录结果,包含cookie """ logger.info(f"开始登录账号: {self.account.username}") start_time = time.monotonic() - deadline = start_time + self.max_total_time + deadline = start_time + self.max_total_time if self.max_total_time > 0 else 0 - for attempt in range(1, self.max_login_retries + 1): + attempt = 0 + while True: + attempt += 1 if self._is_stopped(): logger.warning("登录任务已停止") return LoginResult(success=False, message="任务已停止") elapsed = time.monotonic() - start_time - if elapsed > self.max_total_time: + if self.max_total_time > 0 and elapsed > self.max_total_time: logger.warning(f"登录总耗时 {elapsed:.0f}s 超过上限 {self.max_total_time}s,放弃") return LoginResult(success=False, message=f"登录超时({elapsed:.0f}s > {self.max_total_time}s)") if attempt > 1: - logger.info(f"登录整体重试 {attempt}/{self.max_login_retries},换代理重新开始") + if self.max_login_retries > 0: + logger.info(f"登录整体重试 {attempt}/{self.max_login_retries},换代理重新开始") + else: + logger.info(f"登录整体重试 {attempt} (无限重试),换代理重新开始") # 重试前:换新代理 + 重置 session(清 cookies) self._prepare_retry() try: # 1️⃣ 第一次登录(获取极验参数) logger.info("步骤1: 第一次登录,获取极验参数...") - gt, challenge, code_token, initial_cookies = self._first_login() + gt, challenge, code_token, _ = self._first_login() # 2️⃣ 极验 fullpage 验证 logger.info("步骤2: 极验 fullpage 验证...") @@ -376,8 +377,14 @@ class DouyuLogin: return LoginResult(success=False, message=str(e)) except Exception as e: elapsed = time.monotonic() - start_time - logger.error(f"登录失败(尝试 {attempt}/{self.max_login_retries},已耗时 {elapsed:.0f}s): {e}") - if attempt < self.max_login_retries and elapsed < self.max_total_time: + if self.max_login_retries > 0: + logger.error(f"登录失败(尝试 {attempt}/{self.max_login_retries},已耗时 {elapsed:.0f}s): {e}") + else: + logger.error(f"登录失败(尝试 {attempt},已耗时 {elapsed:.0f}s): {e}") + + has_retry = self.max_login_retries <= 0 or attempt < self.max_login_retries + has_time = self.max_total_time <= 0 or elapsed < self.max_total_time + if has_retry and has_time: # 还有重试机会且未超时:标记当前代理坏,下一轮自动换新代理 if self.proxy_manager and self._current_proxy_url: self.proxy_manager.mark_bad(self._current_proxy_url) @@ -386,8 +393,6 @@ class DouyuLogin: # 所有重试耗尽或超时 return LoginResult(success=False, message=str(e)) - return LoginResult(success=False, message=f"登录失败,已重试 {self.max_login_retries} 次") - def _prepare_retry(self) -> None: """重试前准备:换新代理、重置 session cookies。""" # 重置 session(清掉旧 cookies,避免残留状态干扰) @@ -471,24 +476,35 @@ class DouyuLogin: """ logger.info("开始极验 fullpage 验证...") - # max_proxy_retries=0 表示无限重试直到成功 - max_attempts = self.max_proxy_retries if self.max_proxy_retries > 0 else 999999 + # max_proxy_retries=0 表示不限代理切换次数。 + max_proxy_switches = self.max_proxy_retries if self.max_proxy_retries > 0 else 999999 + proxy_switches = 0 # 连续临时失败计数(同一代理下),超过阈值才换代理 _soft_fail_streak = 0 _SOFT_FAIL_THRESHOLD = 2 # 同一代理连续临时失败2次才换 - for attempt in range(max_attempts): + def refresh_proxy(mark_bad: bool) -> None: + """按代理切换上限刷新代理。""" + nonlocal proxy_switches + if not self.proxy_manager: + return + if proxy_switches >= max_proxy_switches: + raise ValueError(f"极验验证代理切换次数已达上限 {self.max_proxy_retries}") + new_proxy = self._refresh_proxy(mark_bad=mark_bad) + if new_proxy: + proxy_switches += 1 + + attempt = 0 + while True: + attempt += 1 self._ensure_not_stopped() # 超时兜底:极验验证不应超过登录整体时间上限 if deadline and time.monotonic() > deadline: raise ValueError(f"极验验证超时(登录整体时间耗尽)") try: - if self.max_proxy_retries > 0: - logger.info(f"极验验证尝试 {attempt + 1}/{max_attempts}") - else: - logger.info(f"极验验证尝试 {attempt + 1} (无限重试)") + logger.info(f"极验验证尝试 {attempt} (无限重试)") # 按斗鱼登录页 HAR:fullpage 智能检测流程,不进入图片滑块。 str_16 = _generate_seed() @@ -525,7 +541,7 @@ class DouyuLogin: _soft_fail_streak += 1 if _soft_fail_streak >= _SOFT_FAIL_THRESHOLD: logger.warning(f"极验验证失败: {message},同一代理连续 {_soft_fail_streak} 次,换代理") - self._refresh_proxy(mark_bad=False) + refresh_proxy(mark_bad=False) _soft_fail_streak = 0 else: logger.warning(f"极验验证失败: {message},原代理重试 ({_soft_fail_streak}/{_SOFT_FAIL_THRESHOLD})") @@ -540,27 +556,27 @@ class DouyuLogin: except Exception as e: err_str = str(e) + if "代理切换次数已达上限" in err_str: + raise is_proxy_dead = self._is_proxy_connection_error(err_str) if is_proxy_dead: # 代理确实不可用:立即换,标记坏 logger.warning(f"极验验证代理连接失败: {self._truncate_error(err_str)},换代理") - self._refresh_proxy(mark_bad=True) + refresh_proxy(mark_bad=True) _soft_fail_streak = 0 else: # 临时异常(KeyError、网络不给力等):先原代理重试 _soft_fail_streak += 1 if _soft_fail_streak >= _SOFT_FAIL_THRESHOLD: logger.warning(f"极验验证临时异常: {self._truncate_error(err_str)},连续 {_soft_fail_streak} 次,换代理") - self._refresh_proxy(mark_bad=False) + refresh_proxy(mark_bad=False) _soft_fail_streak = 0 else: logger.warning(f"极验验证临时异常: {self._truncate_error(err_str)},原代理重试 ({_soft_fail_streak}/{_SOFT_FAIL_THRESHOLD})") self._sleep_interruptible(2) continue - if self.max_proxy_retries > 0: - raise ValueError(f"极验验证失败,已重试 {max_attempts} 次") raise ValueError("极验验证失败(无限重试模式仍未能通过)") def _second_login(self, gt: str, challenge: str, validate: str, diff --git a/docker-compose.yml b/docker-compose.yml index 99c0067..e9242e7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,6 +13,8 @@ services: - TZ=Asia/Shanghai # JWT 密钥(生产环境务必修改,可用 python -c "import secrets; print(secrets.token_urlsafe(32))" 生成) - JWT_SECRET_KEY=${JWT_SECRET_KEY:-} + # 敏感字段落盘加密密钥(生产环境务必长期保持不变) + - APP_ENCRYPTION_KEY=${APP_ENCRYPTION_KEY:-} # 默认管理员账号密码(仅首次启动建库时生效) - ADMIN_USERNAME=${ADMIN_USERNAME:-admin} - ADMIN_PASSWORD=${ADMIN_PASSWORD:-admin123} @@ -20,6 +22,8 @@ services: - COOKIE_SECURE=${COOKIE_SECURE:-false} # CORS 允许的源(逗号分隔) - CORS_ORIGINS=${CORS_ORIGINS:-} + # Roundcube 邮件验证码读取服务地址(不填则使用代码默认值) + - MAIL_ROUNDCUBE_URL=${MAIL_ROUNDCUBE_URL:-} # Uvicorn reload(生产环境保持 false) - UVICORN_RELOAD=${UVICORN_RELOAD:-false} healthcheck: diff --git a/web/backend/routers/cookies.py b/web/backend/routers/cookies.py index e555924..df970f4 100644 --- a/web/backend/routers/cookies.py +++ b/web/backend/routers/cookies.py @@ -24,21 +24,25 @@ def _fmt_dt(dt) -> str | None: router = APIRouter(prefix="/api/cookies", tags=["Cookie管理"]) +def _visible_cookie_tasks_query(db: Session, current: User): + """返回当前用户可见的成功 Cookie 任务查询。""" + query = db.query(LoginTask).filter(LoginTask.status == "success") + + # 无全量登录任务权限时,只能操作分配给自己的账号 Cookie。 + if not user_has_permission(current, "login:view_all"): + query = query.join(Account, LoginTask.account_id == Account.id).filter( + Account.assigned_to == current.id + ) + return query + + @router.get("") def list_cookies( db: Session = Depends(get_db), current: User = Depends(get_current_user), ): """查看登录成功的 Cookie 列表。""" - query = db.query(LoginTask).filter(LoginTask.status == "success") - - # 客服只能看自己账号的 - if not user_has_permission(current, "login:view_all"): - query = query.join(Account, LoginTask.account_id == Account.id).filter( - Account.assigned_to == current.id - ) - - tasks = query.order_by(LoginTask.finished_at.desc()).all() + tasks = _visible_cookie_tasks_query(db, current).order_by(LoginTask.finished_at.desc()).all() # 批量查账号,避免 N+1 查询 account_ids = [t.account_id for t in tasks] @@ -81,9 +85,7 @@ def export_cookies( csv: 账号, Cookie, 时间 custom: 账号----密码----ck """ - tasks = db.query(LoginTask).filter( - LoginTask.status == "success" - ).order_by(LoginTask.finished_at.desc()).all() + tasks = _visible_cookie_tasks_query(db, current).order_by(LoginTask.finished_at.desc()).all() # 批量查账号,避免 N+1 account_ids = [t.account_id for t in tasks] @@ -136,7 +138,7 @@ def delete_cookies_batch( ids = [int(x) for x in task_ids.split(",") if x.strip().isdigit()] if not ids: raise HTTPException(status_code=400, detail="无效的ID") - tasks = db.query(LoginTask).filter(LoginTask.id.in_(ids)).all() + tasks = _visible_cookie_tasks_query(db, current).filter(LoginTask.id.in_(ids)).all() for t in tasks: t.cookie = "" t.status = "failed" @@ -152,7 +154,7 @@ def delete_cookie( current: User = Depends(require_permission("cookie:export")), ): """删除一条 Cookie 记录。""" - task = db.query(LoginTask).filter(LoginTask.id == task_id).first() + task = _visible_cookie_tasks_query(db, current).filter(LoginTask.id == task_id).first() if not task: raise HTTPException(status_code=404, detail="记录不存在") task.cookie = "" diff --git a/web/backend/routers/login.py b/web/backend/routers/login.py index 2fee33f..05758eb 100644 --- a/web/backend/routers/login.py +++ b/web/backend/routers/login.py @@ -54,7 +54,6 @@ async def create_batch( account_ids=valid_ids, created_by=current.id, creator_permissions=get_user_permissions(current), - max_geetest_retries=req.max_geetest_retries, max_proxy_retries=req.max_proxy_retries, max_login_retries=req.max_login_retries, max_total_time=req.max_total_time, diff --git a/web/backend/schemas.py b/web/backend/schemas.py index ffe57c8..e673829 100644 --- a/web/backend/schemas.py +++ b/web/backend/schemas.py @@ -124,10 +124,9 @@ class AccountOut(BaseModel): # ---- 登录任务 ---- class LoginBatchRequest(BaseModel): account_ids: list[int] - max_geetest_retries: int = 5 max_proxy_retries: int = 0 # 代理切换次数,0=无限切换直到成功 - max_login_retries: int = 3 # 登录整体重试次数(换代理从头重跑) - max_total_time: float = 300 # 单账号登录总时长上限(秒),超时则放弃 + max_login_retries: int = 0 # 登录整体重试次数,0=无限重试直到成功 + max_total_time: float = 0 # 单账号登录总时长上限,0=不限制 concurrency: int = 3 # 并发数,1-10 diff --git a/web/backend/services/login_service.py b/web/backend/services/login_service.py index eccb0d7..c3bcda4 100644 --- a/web/backend/services/login_service.py +++ b/web/backend/services/login_service.py @@ -28,10 +28,9 @@ class LoginBatchRunner: account_ids: list[int], created_by: int, creator_permissions: list[str], - max_geetest_retries: int = 5, max_proxy_retries: int = 0, - max_login_retries: int = 3, - max_total_time: float = 300, + max_login_retries: int = 0, + max_total_time: float = 0, proxy_config: Optional[ProxyConfigModel] = None, log_queue: Optional[asyncio.Queue] = None, loop: Optional[asyncio.AbstractEventLoop] = None, @@ -41,7 +40,6 @@ class LoginBatchRunner: self.account_ids = account_ids self.created_by = created_by self.creator_permissions = creator_permissions - self.max_geetest_retries = max_geetest_retries self.max_proxy_retries = max_proxy_retries self.max_login_retries = max_login_retries self.max_total_time = max_total_time @@ -200,7 +198,6 @@ class LoginBatchRunner: loginer = DouyuLogin( account, proxy=proxy_dict, - max_geetest_retries=self.max_geetest_retries, max_proxy_retries=self.max_proxy_retries, max_login_retries=self.max_login_retries, max_total_time=self.max_total_time, diff --git a/web/frontend/src/App.tsx b/web/frontend/src/App.tsx index ce9926d..640dccc 100644 --- a/web/frontend/src/App.tsx +++ b/web/frontend/src/App.tsx @@ -12,7 +12,8 @@ import ProxyPage from './pages/ProxyPage'; import UsersPage from './pages/UsersPage'; import CookiePage from './pages/CookiePage'; import { getUser } from './store/auth'; -import { ThemeProvider, useTheme } from './store/theme'; +import { ThemeProvider } from './store/theme'; +import { useTheme } from './store/useTheme'; function AppContent() { // 用 state 驱动重渲染,登录/登出时调 refreshAuth() diff --git a/web/frontend/src/api/login.ts b/web/frontend/src/api/login.ts index 5c26c24..e0b10d6 100644 --- a/web/frontend/src/api/login.ts +++ b/web/frontend/src/api/login.ts @@ -3,9 +3,7 @@ import type { BatchLoginResult, LoginTaskItem, MessageDeletedResponse, MessageRe interface CreateBatchParams { account_ids: number[]; - max_geetest_retries?: number; concurrency?: number; - max_proxy_retries?: number; max_login_retries?: number; max_total_time?: number; } diff --git a/web/frontend/src/api/types.ts b/web/frontend/src/api/types.ts index 4a772dd..aff53c3 100644 --- a/web/frontend/src/api/types.ts +++ b/web/frontend/src/api/types.ts @@ -121,33 +121,6 @@ export interface ProxyTestResult { success: boolean; } -// ==================== Log ==================== - -export interface HttpLogEntry { - timestamp: string; - ts: number; - category: string; - tag: string; - method: string; - url: string; - proxy: string | null; - request: { headers: Record; body: string }; - response: { status_code: number | null; headers: Record; body: string }; - duration_ms: number | null; - error: string | null; - level: string; -} - -export interface HttpLogListResult { - items: HttpLogEntry[]; - total: number; -} - -export interface HttpLogClearResult { - success: boolean; - cleared: number; -} - // ==================== Permissions ==================== export interface PermissionsListResult { diff --git a/web/frontend/src/layouts/MainLayout.tsx b/web/frontend/src/layouts/MainLayout.tsx index d20a863..a47de8a 100644 --- a/web/frontend/src/layouts/MainLayout.tsx +++ b/web/frontend/src/layouts/MainLayout.tsx @@ -9,7 +9,8 @@ import { import { useNavigate, useLocation, Outlet } from 'react-router-dom'; import { getUser, clearAuth, type AuthUser } from '../store/auth'; import { authApi } from '../api/modules'; -import { useTheme, type ThemeMode } from '../store/theme'; +import { type ThemeMode } from '../store/theme'; +import { useTheme } from '../store/useTheme'; import { usePermissions } from '../hooks/usePermissions'; const { Sider, Content } = Layout; diff --git a/web/frontend/src/pages/CookiePage.tsx b/web/frontend/src/pages/CookiePage.tsx index 5aa9e57..c578d0b 100644 --- a/web/frontend/src/pages/CookiePage.tsx +++ b/web/frontend/src/pages/CookiePage.tsx @@ -24,7 +24,11 @@ function copyToClipboard(text: string): Promise { try { const ok = document.execCommand('copy'); document.body.removeChild(ta); - ok ? resolve() : reject(new Error('execCommand copy failed')); + if (ok) { + resolve(); + } else { + reject(new Error('execCommand copy failed')); + } } catch (e) { document.body.removeChild(ta); reject(e); diff --git a/web/frontend/src/pages/LoginPage.tsx b/web/frontend/src/pages/LoginPage.tsx index 245864b..d29f250 100644 --- a/web/frontend/src/pages/LoginPage.tsx +++ b/web/frontend/src/pages/LoginPage.tsx @@ -5,7 +5,7 @@ import { useNavigate } from 'react-router-dom'; import { authApi } from '../api/modules'; import { setAuth, type AuthUser } from '../store/auth'; import { getErrorMessage } from '../utils/error'; -import { useTheme } from '../store/theme'; +import { useTheme } from '../store/useTheme'; const { Title } = Typography; diff --git a/web/frontend/src/pages/LoginTasksPage.tsx b/web/frontend/src/pages/LoginTasksPage.tsx index 3e7f79d..4bcfe1c 100644 --- a/web/frontend/src/pages/LoginTasksPage.tsx +++ b/web/frontend/src/pages/LoginTasksPage.tsx @@ -2,7 +2,7 @@ import { useEffect, useState, useMemo, useCallback } from 'react'; import { Table, Button, Select, message, Tag, Space, InputNumber, Tooltip, Popconfirm, theme, } from 'antd'; -import { PlayCircleOutlined, StopOutlined, FilterOutlined, ThunderboltOutlined, ReloadOutlined, DeleteOutlined, SwapOutlined } from '@ant-design/icons'; +import { PlayCircleOutlined, StopOutlined, FilterOutlined, ThunderboltOutlined, ReloadOutlined, DeleteOutlined } from '@ant-design/icons'; import { accountApi, loginApi, type AccountItem, type LoginTaskItem } from '../api/modules'; import RealtimeLogPanel from '../components/RealtimeLogPanel'; import { usePermissions } from '../hooks/usePermissions'; @@ -42,14 +42,9 @@ export default function LoginTasksPage() { const v = localStorage.getItem('login_concurrency'); return v ? Math.max(1, Math.min(10, Number(v) || 3)) : 3; }); - const [maxProxyRetries, setMaxProxyRetries] = useState(() => { - const v = localStorage.getItem('login_max_proxy_retries'); - return v ? Math.max(0, Number(v) || 0) : 0; - }); // 值变化时自动持久化 useEffect(() => { localStorage.setItem('login_concurrency', String(concurrency)); }, [concurrency]); - useEffect(() => { localStorage.setItem('login_max_proxy_retries', String(maxProxyRetries)); }, [maxProxyRetries]); const [selectedRowKeys, setSelectedRowKeys] = useState([]); const { logs, connected: wsConnected, connect: connectLogs } = useWebSocketLogs(); const { can } = usePermissions(); @@ -145,11 +140,7 @@ export default function LoginTasksPage() { try { const result = await loginApi.createBatch({ account_ids: accountIds, - max_geetest_retries: 5, concurrency, - max_proxy_retries: maxProxyRetries, - max_login_retries: 3, - max_total_time: 300, }); setBatchId(result.batch_id); message.success(`已创建登录任务,共 ${result.count} 个账号`); @@ -347,19 +338,6 @@ export default function LoginTasksPage() { /> - - - - setMaxProxyRetries(v ?? 10)} - style={{ width: 55 }} - size="small" - /> - -