From e00e7d09a14f83ea5e6792ba2e711ebbab0d35cf Mon Sep 17 00:00:00 2001 From: zhinianboke <115088296+zhinianboke@users.noreply.github.com> Date: Sun, 24 Aug 2025 11:04:45 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E6=89=AB=E7=A0=81=E7=99=BB?= =?UTF-8?q?=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- XianyuAutoAsync.py | 360 ++++++++++++++++++++++++++++++++++++++ reply_server.py | 355 ++++++++++++++++++++++++++++++++----- static/css/components.css | 10 ++ static/index.html | 6 +- static/js/app.js | 194 +++++++++++++++++++- 5 files changed, 878 insertions(+), 47 deletions(-) diff --git a/XianyuAutoAsync.py b/XianyuAutoAsync.py index c317e58..af33f9c 100644 --- a/XianyuAutoAsync.py +++ b/XianyuAutoAsync.py @@ -202,6 +202,10 @@ class XianyuLive: self.cookie_refresh_running = False # 防止重复执行Cookie刷新 self.cookie_refresh_enabled = True # 是否启用Cookie刷新功能 + # 扫码登录Cookie刷新标志 + self.last_qr_cookie_refresh_time = 0 # 记录上次扫码登录Cookie刷新时间 + self.qr_cookie_refresh_cooldown = 600 # 扫码登录Cookie刷新后的冷却时间:10分钟 + # WebSocket连接监控 @@ -3485,6 +3489,7 @@ class XianyuLive: logger.error(f"【{self.cookie_id}】清理任务失败: {self._safe_str(e)}") await asyncio.sleep(300) # 出错后也等待5分钟再重试 + async def cookie_refresh_loop(self): """Cookie刷新定时任务 - 每小时执行一次""" while True: @@ -3578,6 +3583,347 @@ class XianyuLive: status = "启用" if enabled else "禁用" logger.info(f"【{self.cookie_id}】Cookie刷新功能已{status}") + + async def refresh_cookies_from_qr_login(self, qr_cookies_str: str, cookie_id: str = None, user_id: int = None): + """使用扫码登录获取的cookie访问指定界面获取真实cookie并存入数据库 + + Args: + qr_cookies_str: 扫码登录获取的cookie字符串 + cookie_id: 可选的cookie ID,如果不提供则使用当前实例的cookie_id + user_id: 可选的用户ID,如果不提供则使用当前实例的user_id + + Returns: + bool: 成功返回True,失败返回False + """ + playwright = None + browser = None + target_cookie_id = cookie_id or self.cookie_id + target_user_id = user_id or self.user_id + + try: + import asyncio + from playwright.async_api import async_playwright + from utils.xianyu_utils import trans_cookies + + logger.info(f"【{target_cookie_id}】开始使用扫码登录cookie获取真实cookie...") + logger.info(f"【{target_cookie_id}】扫码cookie长度: {len(qr_cookies_str)}") + + # 解析扫码登录的cookie + qr_cookies_dict = trans_cookies(qr_cookies_str) + logger.info(f"【{target_cookie_id}】扫码cookie字段数: {len(qr_cookies_dict)}") + + # Docker环境下修复asyncio子进程问题 + is_docker = os.getenv('DOCKER_ENV') or os.path.exists('/.dockerenv') + + if is_docker: + logger.debug(f"【{target_cookie_id}】检测到Docker环境,应用asyncio修复") + + # 创建一个完整的虚拟子进程监视器 + class DummyChildWatcher: + def __enter__(self): + return self + def __exit__(self, *args): + pass + def is_active(self): + return True + def add_child_handler(self, *args, **kwargs): + pass + def remove_child_handler(self, *args, **kwargs): + pass + def attach_loop(self, *args, **kwargs): + pass + def close(self): + pass + def __del__(self): + pass + + # 创建自定义事件循环策略 + class DockerEventLoopPolicy(asyncio.DefaultEventLoopPolicy): + def get_child_watcher(self): + return DummyChildWatcher() + + # 临时设置策略 + old_policy = asyncio.get_event_loop_policy() + asyncio.set_event_loop_policy(DockerEventLoopPolicy()) + + try: + # 添加超时机制,避免无限等待 + playwright = await asyncio.wait_for( + async_playwright().start(), + timeout=30.0 # 30秒超时 + ) + logger.debug(f"【{target_cookie_id}】Docker环境下Playwright启动成功") + except asyncio.TimeoutError: + logger.error(f"【{target_cookie_id}】Docker环境下Playwright启动超时") + return False + finally: + # 恢复原策略 + asyncio.set_event_loop_policy(old_policy) + else: + # 非Docker环境,正常启动(也添加超时保护) + try: + playwright = await asyncio.wait_for( + async_playwright().start(), + timeout=30.0 # 30秒超时 + ) + except asyncio.TimeoutError: + logger.error(f"【{target_cookie_id}】Playwright启动超时") + return False + + # 启动浏览器(参照商品搜索的配置) + browser_args = [ + '--no-sandbox', + '--disable-setuid-sandbox', + '--disable-dev-shm-usage', + '--disable-accelerated-2d-canvas', + '--no-first-run', + '--no-zygote', + '--disable-gpu', + '--disable-background-timer-throttling', + '--disable-backgrounding-occluded-windows', + '--disable-renderer-backgrounding', + '--disable-features=TranslateUI', + '--disable-ipc-flooding-protection', + '--disable-extensions', + '--disable-default-apps', + '--disable-sync', + '--disable-translate', + '--hide-scrollbars', + '--mute-audio', + '--no-default-browser-check', + '--no-pings' + ] + + # 在Docker环境中添加额外参数 + if os.getenv('DOCKER_ENV'): + browser_args.extend([ + '--single-process', + '--disable-background-networking', + '--disable-client-side-phishing-detection', + '--disable-hang-monitor', + '--disable-popup-blocking', + '--disable-prompt-on-repost', + '--disable-web-resources', + '--metrics-recording-only', + '--safebrowsing-disable-auto-update', + '--enable-automation', + '--password-store=basic', + '--use-mock-keychain' + ]) + + # 使用无头浏览器 + browser = await playwright.chromium.launch( + headless=True, # 改回无头模式 + args=browser_args + ) + + # 创建浏览器上下文 + context_options = { + 'user_agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36' + } + + # 使用标准窗口大小 + context_options['viewport'] = {'width': 1920, 'height': 1080} + + context = await browser.new_context(**context_options) + + # 设置扫码登录获取的Cookie + cookies = [] + for cookie_pair in qr_cookies_str.split('; '): + if '=' in cookie_pair: + name, value = cookie_pair.split('=', 1) + cookies.append({ + 'name': name.strip(), + 'value': value.strip(), + 'domain': '.goofish.com', + 'path': '/' + }) + + await context.add_cookies(cookies) + logger.info(f"【{target_cookie_id}】已设置 {len(cookies)} 个扫码Cookie到浏览器") + + # 打印设置的扫码Cookie详情 + logger.info(f"【{target_cookie_id}】=== 设置到浏览器的扫码Cookie ===") + for i, cookie in enumerate(cookies, 1): + logger.info(f"【{target_cookie_id}】{i:2d}. {cookie['name']}: {cookie['value'][:50]}{'...' if len(cookie['value']) > 50 else ''}") + + # 创建页面 + page = await context.new_page() + + # 等待页面准备 + await asyncio.sleep(0.1) + + # 访问指定页面获取真实cookie + target_url = "https://www.goofish.com/im?spm=a21ybx.home.sidebar.1.4c053da6vYwnmf" + logger.info(f"【{target_cookie_id}】访问页面获取真实cookie: {target_url}") + + # 使用更灵活的页面访问策略 + try: + # 首先尝试较短超时 + await page.goto(target_url, wait_until='domcontentloaded', timeout=15000) + logger.info(f"【{target_cookie_id}】页面访问成功") + except Exception as e: + if 'timeout' in str(e).lower(): + logger.warning(f"【{target_cookie_id}】页面访问超时,尝试降级策略...") + try: + # 降级策略:只等待基本加载 + await page.goto(target_url, wait_until='load', timeout=20000) + logger.info(f"【{target_cookie_id}】页面访问成功(降级策略)") + except Exception as e2: + logger.warning(f"【{target_cookie_id}】降级策略也失败,尝试最基本访问...") + # 最后尝试:不等待任何加载完成 + await page.goto(target_url, timeout=25000) + logger.info(f"【{target_cookie_id}】页面访问成功(最基本策略)") + else: + raise e + + # 等待页面完全加载并获取真实cookie + logger.info(f"【{target_cookie_id}】页面加载完成,等待获取真实cookie...") + await asyncio.sleep(2) + + # 执行一次刷新以确保获取最新的cookie + logger.info(f"【{target_cookie_id}】执行页面刷新获取最新cookie...") + try: + await page.reload(wait_until='domcontentloaded', timeout=12000) + logger.info(f"【{target_cookie_id}】页面刷新成功") + except Exception as e: + if 'timeout' in str(e).lower(): + logger.warning(f"【{target_cookie_id}】页面刷新超时,使用降级策略...") + await page.reload(wait_until='load', timeout=15000) + logger.info(f"【{target_cookie_id}】页面刷新成功(降级策略)") + else: + raise e + await asyncio.sleep(1) + + # 获取更新后的真实Cookie + logger.info(f"【{target_cookie_id}】获取真实Cookie...") + updated_cookies = await context.cookies() + + # 构造新的Cookie字典 + real_cookies_dict = {} + for cookie in updated_cookies: + real_cookies_dict[cookie['name']] = cookie['value'] + + # 生成真实cookie字符串 + real_cookies_str = '; '.join([f"{k}={v}" for k, v in real_cookies_dict.items()]) + + logger.info(f"【{target_cookie_id}】真实Cookie已获取,包含 {len(real_cookies_dict)} 个字段") + + # 打印完整的真实Cookie内容 + logger.info(f"【{target_cookie_id}】=== 完整真实Cookie内容 ===") + logger.info(f"【{target_cookie_id}】Cookie字符串长度: {len(real_cookies_str)}") + logger.info(f"【{target_cookie_id}】Cookie完整内容:") + logger.info(f"【{target_cookie_id}】{real_cookies_str}") + + # 打印所有Cookie字段的详细信息 + logger.info(f"【{target_cookie_id}】=== Cookie字段详细信息 ===") + for i, (name, value) in enumerate(real_cookies_dict.items(), 1): + # 对于长值,显示前后部分 + if len(value) > 50: + display_value = f"{value[:20]}...{value[-20:]}" + else: + display_value = value + logger.info(f"【{target_cookie_id}】{i:2d}. {name}: {display_value}") + + # 打印原始扫码Cookie对比 + logger.info(f"【{target_cookie_id}】=== 扫码Cookie对比 ===") + logger.info(f"【{target_cookie_id}】扫码Cookie长度: {len(qr_cookies_str)}") + logger.info(f"【{target_cookie_id}】扫码Cookie字段数: {len(qr_cookies_dict)}") + logger.info(f"【{target_cookie_id}】真实Cookie长度: {len(real_cookies_str)}") + logger.info(f"【{target_cookie_id}】真实Cookie字段数: {len(real_cookies_dict)}") + logger.info(f"【{target_cookie_id}】长度增加: {len(real_cookies_str) - len(qr_cookies_str)} 字符") + logger.info(f"【{target_cookie_id}】字段增加: {len(real_cookies_dict) - len(qr_cookies_dict)} 个") + + # 检查Cookie变化 + changed_cookies = [] + new_cookies = [] + for name, new_value in real_cookies_dict.items(): + old_value = qr_cookies_dict.get(name) + if old_value is None: + new_cookies.append(name) + elif old_value != new_value: + changed_cookies.append(name) + + # 显示Cookie变化统计 + if changed_cookies: + logger.info(f"【{target_cookie_id}】发生变化的Cookie字段 ({len(changed_cookies)}个): {', '.join(changed_cookies)}") + if new_cookies: + logger.info(f"【{target_cookie_id}】新增的Cookie字段 ({len(new_cookies)}个): {', '.join(new_cookies)}") + if not changed_cookies and not new_cookies: + logger.info(f"【{target_cookie_id}】Cookie无变化") + + # 打印重要Cookie字段的完整详情 + important_cookies = ['_m_h5_tk', '_m_h5_tk_enc', 'cookie2', 't', 'sgcookie', 'unb', 'uc1', 'uc3', 'uc4'] + logger.info(f"【{target_cookie_id}】=== 重要Cookie字段完整详情 ===") + for cookie_name in important_cookies: + if cookie_name in real_cookies_dict: + cookie_value = real_cookies_dict[cookie_name] + + # 标记是否发生了变化 + change_mark = " [已变化]" if cookie_name in changed_cookies else " [新增]" if cookie_name in new_cookies else " [无变化]" + + # 显示完整的cookie值 + logger.info(f"【{target_cookie_id}】{cookie_name}{change_mark}:") + logger.info(f"【{target_cookie_id}】 值: {cookie_value}") + logger.info(f"【{target_cookie_id}】 长度: {len(cookie_value)}") + + # 如果有对应的扫码cookie值,显示对比 + if cookie_name in qr_cookies_dict: + old_value = qr_cookies_dict[cookie_name] + if old_value != cookie_value: + logger.info(f"【{target_cookie_id}】 原值: {old_value}") + logger.info(f"【{target_cookie_id}】 原长度: {len(old_value)}") + logger.info(f"【{target_cookie_id}】 ---") + else: + logger.info(f"【{target_cookie_id}】{cookie_name}: [不存在]") + + # 保存真实Cookie到数据库 + from db_manager import db_manager + success = db_manager.save_cookie(target_cookie_id, real_cookies_str, target_user_id) + + if success: + logger.info(f"【{target_cookie_id}】真实Cookie已成功保存到数据库") + + # 如果当前实例的cookie_id匹配,更新实例的cookie信息 + if target_cookie_id == self.cookie_id: + self.cookies = real_cookies_dict + self.cookies_str = real_cookies_str + logger.info(f"【{target_cookie_id}】已更新当前实例的Cookie信息") + + # 更新扫码登录Cookie刷新时间标志 + self.last_qr_cookie_refresh_time = time.time() + logger.info(f"【{target_cookie_id}】已更新扫码登录Cookie刷新时间标志,_refresh_cookies_via_browser将等待{self.qr_cookie_refresh_cooldown//60}分钟后执行") + + return True + else: + logger.error(f"【{target_cookie_id}】保存真实Cookie到数据库失败") + return False + + except Exception as e: + logger.error(f"【{target_cookie_id}】使用扫码cookie获取真实cookie失败: {self._safe_str(e)}") + return False + finally: + # 确保资源清理 + try: + if browser: + await browser.close() + if playwright: + await playwright.stop() + except Exception as cleanup_e: + logger.warning(f"【{target_cookie_id}】清理浏览器资源时出错: {self._safe_str(cleanup_e)}") + + def reset_qr_cookie_refresh_flag(self): + """重置扫码登录Cookie刷新标志,允许立即执行_refresh_cookies_via_browser""" + self.last_qr_cookie_refresh_time = 0 + logger.info(f"【{self.cookie_id}】已重置扫码登录Cookie刷新标志") + + def get_qr_cookie_refresh_remaining_time(self) -> int: + """获取扫码登录Cookie刷新剩余冷却时间(秒)""" + current_time = time.time() + time_since_qr_refresh = current_time - self.last_qr_cookie_refresh_time + remaining_time = max(0, self.qr_cookie_refresh_cooldown - time_since_qr_refresh) + return int(remaining_time) + async def _refresh_cookies_via_browser(self): """通过浏览器访问指定页面刷新Cookie""" @@ -3588,6 +3934,19 @@ class XianyuLive: import asyncio from playwright.async_api import async_playwright + # 检查是否需要等待扫码登录Cookie刷新的冷却时间 + current_time = time.time() + time_since_qr_refresh = current_time - self.last_qr_cookie_refresh_time + + if time_since_qr_refresh < self.qr_cookie_refresh_cooldown: + remaining_time = self.qr_cookie_refresh_cooldown - time_since_qr_refresh + remaining_minutes = int(remaining_time // 60) + remaining_seconds = int(remaining_time % 60) + + logger.info(f"【{self.cookie_id}】扫码登录Cookie刷新冷却中,还需等待 {remaining_minutes}分{remaining_seconds}秒") + logger.info(f"【{self.cookie_id}】跳过本次浏览器Cookie刷新") + return False + logger.info(f"【{self.cookie_id}】开始通过浏览器刷新Cookie...") logger.info(f"【{self.cookie_id}】刷新前Cookie长度: {len(self.cookies_str)}") logger.info(f"【{self.cookie_id}】刷新前Cookie字段数: {len(self.cookies)}") @@ -3855,6 +4214,7 @@ class XianyuLive: except Exception as cleanup_e: logger.warning(f"【{self.cookie_id}】清理浏览器资源时出错: {self._safe_str(cleanup_e)}") + async def send_msg_once(self, toid, item_id, text): headers = { "Cookie": self.cookies_str, diff --git a/reply_server.py b/reply_server.py index 1c9df63..0bdd996 100644 --- a/reply_server.py +++ b/reply_server.py @@ -14,6 +14,8 @@ import os import uvicorn import pandas as pd import io +import asyncio +from collections import defaultdict import cookie_manager from db_manager import db_manager @@ -36,9 +38,30 @@ TOKEN_EXPIRE_TIME = 24 * 60 * 60 # token过期时间:24小时 # HTTP Bearer认证 security = HTTPBearer(auto_error=False) +# 扫码登录检查锁 - 防止并发处理同一个session +qr_check_locks = defaultdict(lambda: asyncio.Lock()) +qr_check_processed = {} # 记录已处理的session: {session_id: {'processed': bool, 'timestamp': float}} + # 不再需要单独的密码初始化,由数据库初始化时处理 +def cleanup_qr_check_records(): + """清理过期的扫码检查记录""" + current_time = time.time() + expired_sessions = [] + + for session_id, record in qr_check_processed.items(): + # 清理超过1小时的记录 + if current_time - record['timestamp'] > 3600: + expired_sessions.append(session_id) + + for session_id in expired_sessions: + if session_id in qr_check_processed: + del qr_check_processed[session_id] + if session_id in qr_check_locks: + del qr_check_locks[session_id] + + def load_keywords() -> List[Tuple[str, str]]: """读取关键字→回复映射表 @@ -1038,26 +1061,57 @@ async def generate_qr_code(current_user: Dict[str, Any] = Depends(get_current_us async def check_qr_code_status(session_id: str, current_user: Dict[str, Any] = Depends(get_current_user)): """检查扫码登录状态""" try: - # 清理过期会话 - qr_login_manager.cleanup_expired_sessions() + # 清理过期记录 + cleanup_qr_check_records() - # 获取会话状态 - status_info = qr_login_manager.get_session_status(session_id) + # 检查是否已经处理过 + if session_id in qr_check_processed: + record = qr_check_processed[session_id] + if record['processed']: + log_with_user('debug', f"扫码登录session {session_id} 已处理过,直接返回", current_user) + # 返回简单的成功状态,避免重复处理 + return {'status': 'already_processed', 'message': '该会话已处理完成'} - if status_info['status'] == 'success': - # 登录成功,处理Cookie - cookies_info = qr_login_manager.get_session_cookies(session_id) - if cookies_info: - account_info = await process_qr_login_cookies( - cookies_info['cookies'], - cookies_info['unb'], - current_user - ) - status_info['account_info'] = account_info + # 获取该session的锁 + session_lock = qr_check_locks[session_id] - log_with_user('info', f"扫码登录成功处理完成: {session_id}, 账号: {account_info.get('account_id', 'unknown')}", current_user) + # 使用非阻塞方式尝试获取锁 + if session_lock.locked(): + log_with_user('debug', f"扫码登录session {session_id} 正在被其他请求处理,跳过", current_user) + return {'status': 'processing', 'message': '正在处理中,请稍候...'} - return status_info + async with session_lock: + # 再次检查是否已处理(双重检查) + if session_id in qr_check_processed and qr_check_processed[session_id]['processed']: + log_with_user('debug', f"扫码登录session {session_id} 在获取锁后发现已处理,直接返回", current_user) + return {'status': 'already_processed', 'message': '该会话已处理完成'} + + # 清理过期会话 + qr_login_manager.cleanup_expired_sessions() + + # 获取会话状态 + status_info = qr_login_manager.get_session_status(session_id) + + if status_info['status'] == 'success': + # 登录成功,处理Cookie(现在包含获取真实cookie的逻辑) + cookies_info = qr_login_manager.get_session_cookies(session_id) + if cookies_info: + account_info = await process_qr_login_cookies( + cookies_info['cookies'], + cookies_info['unb'], + current_user + ) + status_info['account_info'] = account_info + + log_with_user('info', f"扫码登录处理完成: {session_id}, 账号: {account_info.get('account_id', 'unknown')}", current_user) + + # 标记该session已处理 + qr_check_processed[session_id] = { + 'processed': True, + 'timestamp': time.time() + } + + return status_info except Exception as e: log_with_user('error', f"检查扫码登录状态异常: {str(e)}", current_user) @@ -1065,7 +1119,7 @@ async def check_qr_code_status(session_id: str, current_user: Dict[str, Any] = D async def process_qr_login_cookies(cookies: str, unb: str, current_user: Dict[str, Any]) -> Dict[str, Any]: - """处理扫码登录获取的Cookie""" + """处理扫码登录获取的Cookie - 先获取真实cookie再保存到数据库""" try: user_id = current_user['user_id'] @@ -1083,20 +1137,11 @@ async def process_qr_login_cookies(cookies: str, unb: str, current_user: Dict[st except: continue + # 确定账号ID if existing_account_id: - # 更新现有账号的Cookie - db_manager.save_cookie(existing_account_id, cookies, user_id) - - # 更新cookie_manager中的Cookie - if cookie_manager.manager: - cookie_manager.manager.update_cookie(existing_account_id, cookies) - - log_with_user('info', f"扫码登录更新现有账号Cookie: {existing_account_id}, UNB: {unb}", current_user) - - return { - 'account_id': existing_account_id, - 'is_new_account': False - } + account_id = existing_account_id + is_new_account = False + log_with_user('info', f"扫码登录找到现有账号: {account_id}, UNB: {unb}", current_user) else: # 创建新账号,使用unb作为账号ID account_id = unb @@ -1108,25 +1153,255 @@ async def process_qr_login_cookies(cookies: str, unb: str, current_user: Dict[st account_id = f"{original_account_id}_{counter}" counter += 1 - # 保存新账号 - db_manager.save_cookie(account_id, cookies, user_id) + is_new_account = True + log_with_user('info', f"扫码登录准备创建新账号: {account_id}, UNB: {unb}", current_user) - # 添加到cookie_manager - if cookie_manager.manager: - cookie_manager.manager.add_cookie(account_id, cookies) + # 第一步:使用扫码cookie获取真实cookie + log_with_user('info', f"开始使用扫码cookie获取真实cookie: {account_id}", current_user) - log_with_user('info', f"扫码登录创建新账号: {account_id}, UNB: {unb}", current_user) + try: + # 创建一个临时的XianyuLive实例来执行cookie刷新 + from XianyuAutoAsync import XianyuLive - return { - 'account_id': account_id, - 'is_new_account': True - } + # 使用扫码登录的cookie创建临时实例 + temp_instance = XianyuLive( + cookies_str=cookies, + cookie_id=account_id, + user_id=user_id + ) + + # 执行cookie刷新获取真实cookie + refresh_success = await temp_instance.refresh_cookies_from_qr_login( + qr_cookies_str=cookies, + cookie_id=account_id, + user_id=user_id + ) + + if refresh_success: + log_with_user('info', f"扫码登录真实cookie获取成功: {account_id}", current_user) + + # 从数据库获取刚刚保存的真实cookie + updated_cookie_info = db_manager.get_cookie_by_id(account_id) + if updated_cookie_info: + real_cookies = updated_cookie_info['cookies_str'] + log_with_user('info', f"已获取真实cookie,长度: {len(real_cookies)}", current_user) + + # 第二步:将真实cookie添加到cookie_manager(如果是新账号)或更新现有账号 + if cookie_manager.manager: + if is_new_account: + cookie_manager.manager.add_cookie(account_id, real_cookies) + log_with_user('info', f"已将真实cookie添加到cookie_manager: {account_id}", current_user) + else: + cookie_manager.manager.update_cookie(account_id, real_cookies) + log_with_user('info', f"已更新cookie_manager中的真实cookie: {account_id}", current_user) + + return { + 'account_id': account_id, + 'is_new_account': is_new_account, + 'real_cookie_refreshed': True, + 'cookie_length': len(real_cookies) + } + else: + log_with_user('error', f"无法从数据库获取真实cookie: {account_id}", current_user) + # 降级处理:使用原始扫码cookie + return await _fallback_save_qr_cookie(account_id, cookies, user_id, is_new_account, current_user, "无法从数据库获取真实cookie") + else: + log_with_user('warning', f"扫码登录真实cookie获取失败: {account_id}", current_user) + # 降级处理:使用原始扫码cookie + return await _fallback_save_qr_cookie(account_id, cookies, user_id, is_new_account, current_user, "真实cookie获取失败") + + except Exception as refresh_e: + log_with_user('error', f"扫码登录真实cookie获取异常: {str(refresh_e)}", current_user) + # 降级处理:使用原始扫码cookie + return await _fallback_save_qr_cookie(account_id, cookies, user_id, is_new_account, current_user, f"获取真实cookie异常: {str(refresh_e)}") except Exception as e: log_with_user('error', f"处理扫码登录Cookie失败: {str(e)}", current_user) raise e +async def _fallback_save_qr_cookie(account_id: str, cookies: str, user_id: int, is_new_account: bool, current_user: Dict[str, Any], error_reason: str) -> Dict[str, Any]: + """降级处理:当无法获取真实cookie时,保存原始扫码cookie""" + try: + log_with_user('warning', f"降级处理 - 保存原始扫码cookie: {account_id}, 原因: {error_reason}", current_user) + + # 保存原始扫码cookie到数据库 + if is_new_account: + db_manager.save_cookie(account_id, cookies, user_id) + log_with_user('info', f"降级处理 - 新账号原始cookie已保存: {account_id}", current_user) + else: + db_manager.save_cookie(account_id, cookies, user_id) + log_with_user('info', f"降级处理 - 现有账号原始cookie已更新: {account_id}", current_user) + + # 添加到或更新cookie_manager + if cookie_manager.manager: + if is_new_account: + cookie_manager.manager.add_cookie(account_id, cookies) + log_with_user('info', f"降级处理 - 已将原始cookie添加到cookie_manager: {account_id}", current_user) + else: + cookie_manager.manager.update_cookie(account_id, cookies) + log_with_user('info', f"降级处理 - 已更新cookie_manager中的原始cookie: {account_id}", current_user) + + return { + 'account_id': account_id, + 'is_new_account': is_new_account, + 'real_cookie_refreshed': False, + 'fallback_reason': error_reason, + 'cookie_length': len(cookies) + } + + except Exception as fallback_e: + log_with_user('error', f"降级处理失败: {str(fallback_e)}", current_user) + raise fallback_e + + +@app.post("/qr-login/refresh-cookies") +async def refresh_cookies_from_qr_login( + request: Dict[str, Any], + current_user: Dict[str, Any] = Depends(get_current_user) +): + """使用扫码登录获取的cookie访问指定界面获取真实cookie并存入数据库""" + try: + qr_cookies = request.get('qr_cookies') + cookie_id = request.get('cookie_id') + + if not qr_cookies: + return {'success': False, 'message': '缺少扫码登录cookie'} + + if not cookie_id: + return {'success': False, 'message': '缺少cookie_id'} + + log_with_user('info', f"开始使用扫码cookie刷新真实cookie: {cookie_id}", current_user) + + # 创建一个临时的XianyuLive实例来执行cookie刷新 + from XianyuAutoAsync import XianyuLive + + # 使用扫码登录的cookie创建临时实例 + temp_instance = XianyuLive( + cookies_str=qr_cookies, + cookie_id=cookie_id, + user_id=current_user['user_id'] + ) + + # 执行cookie刷新 + success = await temp_instance.refresh_cookies_from_qr_login( + qr_cookies_str=qr_cookies, + cookie_id=cookie_id, + user_id=current_user['user_id'] + ) + + if success: + log_with_user('info', f"扫码cookie刷新成功: {cookie_id}", current_user) + + # 如果cookie_manager存在,更新其中的cookie + if cookie_manager.manager: + # 从数据库获取更新后的cookie + updated_cookie_info = db_manager.get_cookie_by_id(cookie_id) + if updated_cookie_info: + cookie_manager.manager.update_cookie(cookie_id, updated_cookie_info['cookies_str']) + log_with_user('info', f"已更新cookie_manager中的cookie: {cookie_id}", current_user) + + return { + 'success': True, + 'message': '真实cookie获取并保存成功', + 'cookie_id': cookie_id + } + else: + log_with_user('error', f"扫码cookie刷新失败: {cookie_id}", current_user) + return {'success': False, 'message': '获取真实cookie失败'} + + except Exception as e: + log_with_user('error', f"扫码cookie刷新异常: {str(e)}", current_user) + return {'success': False, 'message': f'刷新cookie失败: {str(e)}'} + + +@app.post("/qr-login/reset-cooldown/{cookie_id}") +async def reset_qr_cookie_refresh_cooldown( + cookie_id: str, + current_user: Dict[str, Any] = Depends(get_current_user) +): + """重置指定账号的扫码登录Cookie刷新冷却时间""" + try: + log_with_user('info', f"重置扫码登录Cookie刷新冷却时间: {cookie_id}", current_user) + + # 检查cookie是否存在 + cookie_info = db_manager.get_cookie_by_id(cookie_id) + if not cookie_info: + return {'success': False, 'message': '账号不存在'} + + # 如果cookie_manager中有对应的实例,直接重置 + if cookie_manager.manager and cookie_id in cookie_manager.manager.instances: + instance = cookie_manager.manager.instances[cookie_id] + remaining_time_before = instance.get_qr_cookie_refresh_remaining_time() + instance.reset_qr_cookie_refresh_flag() + + log_with_user('info', f"已重置账号 {cookie_id} 的扫码登录冷却时间,原剩余时间: {remaining_time_before}秒", current_user) + + return { + 'success': True, + 'message': '扫码登录Cookie刷新冷却时间已重置', + 'cookie_id': cookie_id, + 'previous_remaining_time': remaining_time_before + } + else: + # 如果没有活跃实例,返回成功(因为没有冷却时间需要重置) + log_with_user('info', f"账号 {cookie_id} 没有活跃实例,无需重置冷却时间", current_user) + return { + 'success': True, + 'message': '账号没有活跃实例,无需重置冷却时间', + 'cookie_id': cookie_id + } + + except Exception as e: + log_with_user('error', f"重置扫码登录冷却时间异常: {str(e)}", current_user) + return {'success': False, 'message': f'重置冷却时间失败: {str(e)}'} + + +@app.get("/qr-login/cooldown-status/{cookie_id}") +async def get_qr_cookie_refresh_cooldown_status( + cookie_id: str, + current_user: Dict[str, Any] = Depends(get_current_user) +): + """获取指定账号的扫码登录Cookie刷新冷却状态""" + try: + # 检查cookie是否存在 + cookie_info = db_manager.get_cookie_by_id(cookie_id) + if not cookie_info: + return {'success': False, 'message': '账号不存在'} + + # 如果cookie_manager中有对应的实例,获取冷却状态 + if cookie_manager.manager and cookie_id in cookie_manager.manager.instances: + instance = cookie_manager.manager.instances[cookie_id] + remaining_time = instance.get_qr_cookie_refresh_remaining_time() + cooldown_duration = instance.qr_cookie_refresh_cooldown + last_refresh_time = instance.last_qr_cookie_refresh_time + + return { + 'success': True, + 'cookie_id': cookie_id, + 'remaining_time': remaining_time, + 'cooldown_duration': cooldown_duration, + 'last_refresh_time': last_refresh_time, + 'is_in_cooldown': remaining_time > 0, + 'remaining_minutes': remaining_time // 60, + 'remaining_seconds': remaining_time % 60 + } + else: + return { + 'success': True, + 'cookie_id': cookie_id, + 'remaining_time': 0, + 'cooldown_duration': 600, # 默认10分钟 + 'last_refresh_time': 0, + 'is_in_cooldown': False, + 'message': '账号没有活跃实例' + } + + except Exception as e: + log_with_user('error', f"获取扫码登录冷却状态异常: {str(e)}", current_user) + return {'success': False, 'message': f'获取冷却状态失败: {str(e)}'} + + @app.put('/cookies/{cid}/status') def update_cookie_status(cid: str, status_data: CookieStatusIn, current_user: Dict[str, Any] = Depends(get_current_user)): """更新账号的启用/禁用状态""" diff --git a/static/css/components.css b/static/css/components.css index 2d286c0..10feca8 100644 --- a/static/css/components.css +++ b/static/css/components.css @@ -1,6 +1,16 @@ /* ================================ 通用卡片样式 - 适用于所有菜单的卡片 ================================ */ + +/* 旋转动画 */ +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.spin { + animation: spin 1s linear infinite; +} .card { background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(10px); diff --git a/static/index.html b/static/index.html index 494e5ed..a7e4ac6 100644 --- a/static/index.html +++ b/static/index.html @@ -244,17 +244,17 @@
-
diff --git a/static/js/app.js b/static/js/app.js index 9e38b55..26e4a81 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -1347,6 +1347,157 @@ function copyCookie(id, value) { }); } +// 刷新真实Cookie +async function refreshRealCookie(cookieId) { + if (!cookieId) { + showToast('缺少账号ID', 'warning'); + return; + } + + // 获取当前cookie值 + try { + const cookieDetails = await fetchJSON(`${apiBase}/cookies/details`); + const currentCookie = cookieDetails.find(c => c.id === cookieId); + + if (!currentCookie || !currentCookie.value) { + showToast('未找到有效的Cookie信息', 'warning'); + return; + } + + // 确认操作 + if (!confirm(`确定要刷新账号 "${cookieId}" 的真实Cookie吗?\n\n此操作将使用当前Cookie访问闲鱼IM界面获取最新的真实Cookie。`)) { + return; + } + + // 显示加载状态 + const button = event.target.closest('button'); + const originalContent = button.innerHTML; + button.disabled = true; + button.innerHTML = ''; + + // 调用刷新API + const response = await fetch(`${apiBase}/qr-login/refresh-cookies`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${authToken}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + qr_cookies: currentCookie.value, + cookie_id: cookieId + }) + }); + + const result = await response.json(); + + if (result.success) { + showToast(`账号 "${cookieId}" 真实Cookie刷新成功`, 'success'); + // 刷新账号列表以显示更新后的cookie + loadCookies(); + } else { + showToast(`真实Cookie刷新失败: ${result.message}`, 'danger'); + } + + } catch (error) { + console.error('刷新真实Cookie失败:', error); + showToast(`刷新真实Cookie失败: ${error.message || '未知错误'}`, 'danger'); + } finally { + // 恢复按钮状态 + const button = event.target.closest('button'); + if (button) { + button.disabled = false; + button.innerHTML = ''; + } + } +} + +// 显示冷却状态 +async function showCooldownStatus(cookieId) { + if (!cookieId) { + showToast('缺少账号ID', 'warning'); + return; + } + + try { + const response = await fetch(`${apiBase}/qr-login/cooldown-status/${cookieId}`, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${authToken}`, + 'Content-Type': 'application/json' + } + }); + + const result = await response.json(); + + if (result.success) { + const { remaining_time, cooldown_duration, is_in_cooldown, remaining_minutes, remaining_seconds } = result; + + let statusMessage = `账号: ${cookieId}\n`; + statusMessage += `冷却时长: ${cooldown_duration / 60}分钟\n`; + + if (is_in_cooldown) { + statusMessage += `冷却状态: 进行中\n`; + statusMessage += `剩余时间: ${remaining_minutes}分${remaining_seconds}秒\n\n`; + statusMessage += `在冷却期间,_refresh_cookies_via_browser 方法将被跳过。\n\n`; + statusMessage += `是否要重置冷却时间?`; + + if (confirm(statusMessage)) { + await resetCooldownTime(cookieId); + } + } else { + statusMessage += `冷却状态: 无冷却\n`; + statusMessage += `可以正常执行 _refresh_cookies_via_browser 方法`; + alert(statusMessage); + } + } else { + showToast(`获取冷却状态失败: ${result.message}`, 'danger'); + } + + } catch (error) { + console.error('获取冷却状态失败:', error); + showToast(`获取冷却状态失败: ${error.message || '未知错误'}`, 'danger'); + } +} + +// 重置冷却时间 +async function resetCooldownTime(cookieId) { + if (!cookieId) { + showToast('缺少账号ID', 'warning'); + return; + } + + try { + const response = await fetch(`${apiBase}/qr-login/reset-cooldown/${cookieId}`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${authToken}`, + 'Content-Type': 'application/json' + } + }); + + const result = await response.json(); + + if (result.success) { + const previousTime = result.previous_remaining_time || 0; + const previousMinutes = Math.floor(previousTime / 60); + const previousSeconds = previousTime % 60; + + let message = `账号 "${cookieId}" 的扫码登录冷却时间已重置`; + if (previousTime > 0) { + message += `\n原剩余时间: ${previousMinutes}分${previousSeconds}秒`; + } + + showToast(message, 'success'); + } else { + showToast(`重置冷却时间失败: ${result.message}`, 'danger'); + } + + } catch (error) { + console.error('重置冷却时间失败:', error); + showToast(`重置冷却时间失败: ${error.message || '未知错误'}`, 'danger'); + } +} + // 删除Cookie async function delCookie(id) { if (!confirm(`确定要删除账号 "${id}" 吗?此操作不可恢复。`)) return; @@ -7015,6 +7166,16 @@ async function checkQRCodeStatus() { clearQRCodeCheck(); showVerificationRequired(data); break; + case 'processing': + document.getElementById('statusText').textContent = '正在处理中...'; + // 继续轮询,不清理检查 + break; + case 'already_processed': + document.getElementById('statusText').textContent = '登录已完成'; + document.getElementById('statusSpinner').style.display = 'none'; + clearQRCodeCheck(); + showToast('该扫码会话已处理完成', 'info'); + break; } } } catch (error) { @@ -7078,12 +7239,37 @@ function showVerificationRequired(data) { // 处理扫码成功 function handleQRCodeSuccess(data) { if (data.account_info) { - const { account_id, is_new_account } = data.account_info; + const { account_id, is_new_account, real_cookie_refreshed, fallback_reason, cookie_length } = data.account_info; + // 构建成功消息 + let successMessage = ''; if (is_new_account) { - showToast(`新账号添加成功!账号ID: ${account_id}`, 'success'); + successMessage = `新账号添加成功!账号ID: ${account_id}`; } else { - showToast(`账号Cookie已更新!账号ID: ${account_id}`, 'success'); + successMessage = `账号Cookie已更新!账号ID: ${account_id}`; + } + + // 添加cookie长度信息 + if (cookie_length) { + successMessage += `\nCookie长度: ${cookie_length}`; + } + + // 添加真实cookie获取状态信息 + if (real_cookie_refreshed === true) { + successMessage += '\n✅ 真实Cookie获取并保存成功'; + document.getElementById('statusText').textContent = '登录成功!真实Cookie已获取并保存'; + showToast(successMessage, 'success'); + } else if (real_cookie_refreshed === false) { + successMessage += '\n⚠️ 真实Cookie获取失败,已保存原始扫码Cookie'; + if (fallback_reason) { + successMessage += `\n原因: ${fallback_reason}`; + } + document.getElementById('statusText').textContent = '登录成功,但使用原始Cookie'; + showToast(successMessage, 'warning'); + } else { + // 兼容旧版本,没有真实cookie刷新信息 + document.getElementById('statusText').textContent = '登录成功!'; + showToast(successMessage, 'success'); } // 关闭模态框 @@ -7093,7 +7279,7 @@ function handleQRCodeSuccess(data) { // 刷新账号列表 loadCookies(); - }, 2000); + }, 3000); // 延长显示时间以便用户看到详细信息 } }