优化滑块和密码登录
This commit is contained in:
parent
c69840e9c8
commit
6bf2ac43e4
@ -93,6 +93,9 @@ RUN apt-get update && \
|
||||
libxfixes3 \
|
||||
xdg-utils \
|
||||
chromium \
|
||||
xvfb \
|
||||
x11vnc \
|
||||
fluxbox \
|
||||
# OpenCV运行时依赖
|
||||
libgl1 \
|
||||
libglib2.0-0 \
|
||||
|
||||
394
Start.py
394
Start.py
@ -1,9 +1,8 @@
|
||||
"""项目启动入口:
|
||||
|
||||
1. 自动初始化必要的目录和数据库
|
||||
2. 创建 CookieManager,按配置文件 / 环境变量初始化账号任务
|
||||
3. 在后台线程启动 FastAPI (reply_server) 提供管理与自动回复接口
|
||||
4. 主协程保持运行
|
||||
1. 创建 CookieManager,按配置文件 / 环境变量初始化账号任务
|
||||
2. 在后台线程启动 FastAPI (reply_server) 提供管理与自动回复接口
|
||||
3. 主协程保持运行
|
||||
"""
|
||||
|
||||
import os
|
||||
@ -11,51 +10,65 @@ import sys
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
# ==================== 初始化目录和配置 ====================
|
||||
def _init_directories():
|
||||
"""初始化必要的目录结构"""
|
||||
print("=" * 50)
|
||||
print("闲鱼自动回复系统 - 启动中...")
|
||||
print("=" * 50)
|
||||
|
||||
# 创建必要的目录
|
||||
directories = ['data', 'logs', 'backups']
|
||||
for dir_name in directories:
|
||||
dir_path = Path(dir_name)
|
||||
if not dir_path.exists():
|
||||
dir_path.mkdir(parents=True, exist_ok=True)
|
||||
print(f"✓ 创建 {dir_name}/ 目录")
|
||||
else:
|
||||
print(f"✓ {dir_name}/ 目录已存在")
|
||||
|
||||
return True
|
||||
# 设置标准输出编码为UTF-8(Windows兼容)
|
||||
def _setup_console_encoding():
|
||||
"""设置控制台编码为UTF-8,避免Windows GBK编码问题"""
|
||||
if sys.platform == 'win32':
|
||||
try:
|
||||
# 方法1: 设置环境变量
|
||||
os.environ['PYTHONIOENCODING'] = 'utf-8'
|
||||
|
||||
# 方法2: 尝试设置控制台代码页为UTF-8
|
||||
try:
|
||||
import ctypes
|
||||
kernel32 = ctypes.windll.kernel32
|
||||
kernel32.SetConsoleOutputCP(65001) # UTF-8代码页
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 方法3: 重新包装stdout和stderr
|
||||
try:
|
||||
if hasattr(sys.stdout, 'buffer'):
|
||||
import io
|
||||
# 只在编码不是UTF-8时重新包装
|
||||
if sys.stdout.encoding and sys.stdout.encoding.lower() not in ('utf-8', 'utf8'):
|
||||
sys.stdout = io.TextIOWrapper(
|
||||
sys.stdout.buffer,
|
||||
encoding='utf-8',
|
||||
errors='replace',
|
||||
line_buffering=True
|
||||
)
|
||||
if sys.stderr.encoding and sys.stderr.encoding.lower() not in ('utf-8', 'utf8'):
|
||||
sys.stderr = io.TextIOWrapper(
|
||||
sys.stderr.buffer,
|
||||
encoding='utf-8',
|
||||
errors='replace',
|
||||
line_buffering=True
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _init_database():
|
||||
"""初始化数据库(如果不存在)"""
|
||||
db_path = Path("data/xianyu_data.db")
|
||||
|
||||
if not db_path.exists():
|
||||
print("✓ 首次启动,将自动创建数据库...")
|
||||
# 数据库会在 db_manager 导入时自动创建
|
||||
return True
|
||||
else:
|
||||
print(f"✓ 数据库已存在: {db_path}")
|
||||
return False
|
||||
# 在程序启动时设置编码
|
||||
_setup_console_encoding()
|
||||
|
||||
# 在导入任何模块之前先初始化
|
||||
_init_directories()
|
||||
_init_database()
|
||||
# 定义ASCII安全字符(备用方案)
|
||||
_OK = '[OK]'
|
||||
_WARN = '[WARN]'
|
||||
_ERROR = '[ERROR]'
|
||||
_INFO = '[INFO]'
|
||||
|
||||
# ==================== 数据库文件迁移(兼容旧版本) ====================
|
||||
# ==================== 在导入任何模块之前先迁移数据库 ====================
|
||||
def _migrate_database_files_early():
|
||||
"""在启动前检查并迁移数据库文件到data目录(使用print,因为logger还未初始化)"""
|
||||
print("\n检查旧版本数据库文件...")
|
||||
print("检查数据库文件位置...")
|
||||
|
||||
# 确保data目录存在
|
||||
data_dir = Path("data")
|
||||
if not data_dir.exists():
|
||||
data_dir.mkdir(parents=True, exist_ok=True)
|
||||
print("✓ 创建 data 目录")
|
||||
print(f"{_OK} 创建 data 目录")
|
||||
|
||||
# 定义需要迁移的文件
|
||||
files_to_migrate = [
|
||||
@ -75,23 +88,23 @@ def _migrate_database_files_early():
|
||||
# 新位置不存在,移动文件
|
||||
try:
|
||||
shutil.move(str(old_file), str(new_file))
|
||||
print(f"✓ 迁移{description}: {old_path} -> {new_path}")
|
||||
print(f"{_OK} 迁移{description}: {old_path} -> {new_path}")
|
||||
migrated_files.append(description)
|
||||
except Exception as e:
|
||||
print(f"⚠ 无法迁移{description}: {e}")
|
||||
print(f"{_WARN} 无法迁移{description}: {e}")
|
||||
print(f" 尝试复制文件...")
|
||||
try:
|
||||
shutil.copy2(str(old_file), str(new_file))
|
||||
print(f"✓ 已复制{description}到新位置")
|
||||
print(f"{_OK} 已复制{description}到新位置")
|
||||
print(f" 请在确认数据正常后手动删除: {old_path}")
|
||||
migrated_files.append(f"{description}(已复制)")
|
||||
except Exception as e2:
|
||||
print(f"✗ 复制{description}失败: {e2}")
|
||||
print(f"{_ERROR} 复制{description}失败: {e2}")
|
||||
else:
|
||||
# 新位置已存在,检查旧文件大小
|
||||
try:
|
||||
if old_file.stat().st_size > 0:
|
||||
print(f"⚠ 发现旧{description}文件: {old_path}")
|
||||
print(f"{_WARN} 发现旧{description}文件: {old_path}")
|
||||
print(f" 新数据库位于: {new_path}")
|
||||
print(f" 建议备份后删除旧文件")
|
||||
except:
|
||||
@ -107,19 +120,19 @@ def _migrate_database_files_early():
|
||||
if not new_backup_path.exists():
|
||||
try:
|
||||
shutil.move(str(backup_file), str(new_backup_path))
|
||||
print(f"✓ 迁移备份文件: {backup_file.name}")
|
||||
print(f"{_OK} 迁移备份文件: {backup_file.name}")
|
||||
backup_migrated += 1
|
||||
except Exception as e:
|
||||
print(f"⚠ 无法迁移备份文件 {backup_file.name}: {e}")
|
||||
print(f"{_WARN} 无法迁移备份文件 {backup_file.name}: {e}")
|
||||
|
||||
if backup_migrated > 0:
|
||||
migrated_files.append(f"{backup_migrated}个备份文件")
|
||||
|
||||
# 输出迁移总结
|
||||
if migrated_files:
|
||||
print(f"✓ 数据库迁移完成,已迁移: {', '.join(migrated_files)}")
|
||||
print(f"{_OK} 数据库迁移完成,已迁移: {', '.join(migrated_files)}")
|
||||
else:
|
||||
print("✓ 数据库文件检查完成")
|
||||
print(f"{_OK} 数据库文件检查完成")
|
||||
|
||||
return True
|
||||
|
||||
@ -127,9 +140,286 @@ def _migrate_database_files_early():
|
||||
try:
|
||||
_migrate_database_files_early()
|
||||
except Exception as e:
|
||||
print(f"⚠ 数据库迁移检查失败: {e}")
|
||||
print(f"{_WARN} 数据库迁移检查失败: {e}")
|
||||
# 继续启动,因为可能是首次运行
|
||||
|
||||
# ==================== 检查并安装Playwright浏览器 ====================
|
||||
def _check_and_install_playwright():
|
||||
"""检查Playwright浏览器是否存在,如果不存在则自动安装"""
|
||||
print("检查Playwright浏览器...")
|
||||
|
||||
# 检查是否安装了playwright模块
|
||||
try:
|
||||
import playwright
|
||||
except ImportError:
|
||||
print(f"{_WARN} Playwright模块未安装,跳过浏览器检查")
|
||||
return False
|
||||
|
||||
# 检查Playwright浏览器是否存在
|
||||
playwright_installed = False
|
||||
possible_paths = []
|
||||
|
||||
# 如果是打包后的exe,优先检查exe同目录
|
||||
if getattr(sys, 'frozen', False):
|
||||
exe_dir = Path(sys.executable).parent
|
||||
playwright_dir = exe_dir / 'playwright'
|
||||
possible_paths.insert(0, playwright_dir) # 插入到最前面,优先检查
|
||||
|
||||
# 检查exe同目录的浏览器是否完整
|
||||
if playwright_dir.exists():
|
||||
chromium_dirs = list(playwright_dir.glob('chromium-*'))
|
||||
if chromium_dirs:
|
||||
chromium_dir = chromium_dirs[0]
|
||||
chrome_exe = chromium_dir / 'chrome-win' / 'chrome.exe'
|
||||
if chrome_exe.exists() and chrome_exe.stat().st_size > 0:
|
||||
print(f"{_OK} 找到已提取的Playwright浏览器: {chrome_exe}")
|
||||
print(f"{_INFO} 浏览器版本: {chromium_dir.name}")
|
||||
# 清除可能存在的旧环境变量,使用实际存在的浏览器
|
||||
if 'PLAYWRIGHT_BROWSERS_PATH' in os.environ:
|
||||
old_path = os.environ['PLAYWRIGHT_BROWSERS_PATH']
|
||||
if old_path != str(playwright_dir):
|
||||
print(f"{_INFO} 清除旧的环境变量: {old_path}")
|
||||
del os.environ['PLAYWRIGHT_BROWSERS_PATH']
|
||||
# 确保环境变量已设置
|
||||
os.environ['PLAYWRIGHT_BROWSERS_PATH'] = str(playwright_dir)
|
||||
print(f"{_INFO} 已设置PLAYWRIGHT_BROWSERS_PATH: {playwright_dir}")
|
||||
playwright_installed = True
|
||||
return True
|
||||
|
||||
# Windows上的常见位置
|
||||
if sys.platform == 'win32':
|
||||
# 用户缓存目录
|
||||
user_cache = Path.home() / '.cache' / 'ms-playwright'
|
||||
possible_paths.append(user_cache)
|
||||
|
||||
# LocalAppData目录
|
||||
local_appdata = os.getenv('LOCALAPPDATA')
|
||||
if local_appdata:
|
||||
possible_paths.append(Path(local_appdata) / 'ms-playwright')
|
||||
|
||||
# AppData目录
|
||||
appdata = os.getenv('APPDATA')
|
||||
if appdata:
|
||||
possible_paths.append(Path(appdata) / 'ms-playwright')
|
||||
|
||||
# 检查是否存在chromium浏览器
|
||||
for path in possible_paths:
|
||||
if path.exists():
|
||||
# 查找chromium目录
|
||||
chromium_dirs = list(path.glob('chromium-*'))
|
||||
if chromium_dirs:
|
||||
for chromium_dir in chromium_dirs:
|
||||
chrome_win = chromium_dir / 'chrome-win'
|
||||
chrome_exe = chrome_win / 'chrome.exe'
|
||||
if chrome_exe.exists():
|
||||
print(f"{_OK} 找到Playwright浏览器: {chrome_exe}")
|
||||
# 设置环境变量
|
||||
os.environ['PLAYWRIGHT_BROWSERS_PATH'] = str(path)
|
||||
playwright_installed = True
|
||||
break
|
||||
if playwright_installed:
|
||||
break
|
||||
|
||||
# 如果没找到,尝试使用playwright命令检查
|
||||
if not playwright_installed:
|
||||
try:
|
||||
from playwright.sync_api import sync_playwright
|
||||
with sync_playwright() as p:
|
||||
try:
|
||||
browser = p.chromium.launch(headless=True)
|
||||
browser.close()
|
||||
print(f"{_OK} Playwright浏览器已安装(通过API检测)")
|
||||
playwright_installed = True
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 如果没找到,先尝试从临时目录提取(如果是打包的exe)
|
||||
if not playwright_installed and getattr(sys, 'frozen', False):
|
||||
try:
|
||||
exe_dir = Path(sys.executable).parent
|
||||
playwright_dir = exe_dir / 'playwright'
|
||||
|
||||
if hasattr(sys, '_MEIPASS'):
|
||||
temp_dir = Path(sys._MEIPASS)
|
||||
temp_playwright = temp_dir / 'playwright'
|
||||
|
||||
if temp_playwright.exists():
|
||||
# 查找所有 chromium 相关目录(包括 chromium-* 和 chromium_headless_shell-*)
|
||||
temp_chromium_dirs = list(temp_playwright.glob('chromium*'))
|
||||
if temp_chromium_dirs:
|
||||
print(f"{_INFO} 检测到打包的浏览器文件,正在提取...")
|
||||
playwright_dir.mkdir(parents=True, exist_ok=True)
|
||||
extracted_count = 0
|
||||
|
||||
for temp_chromium_dir in temp_chromium_dirs:
|
||||
temp_chrome_win = temp_chromium_dir / 'chrome-win'
|
||||
|
||||
# 检查完整版或 headless_shell 版
|
||||
temp_chrome_exe = temp_chrome_win / 'chrome.exe'
|
||||
temp_headless_exe = temp_chrome_win / 'headless_shell.exe'
|
||||
|
||||
# 验证文件是否存在
|
||||
is_valid = False
|
||||
if temp_chromium_dir.name.startswith('chromium_headless_shell'):
|
||||
is_valid = temp_headless_exe.exists() and temp_headless_exe.stat().st_size > 0
|
||||
else:
|
||||
is_valid = temp_chrome_exe.exists() and temp_chrome_exe.stat().st_size > 0
|
||||
|
||||
if is_valid:
|
||||
target_chromium_dir = playwright_dir / temp_chromium_dir.name
|
||||
|
||||
if not target_chromium_dir.exists():
|
||||
try:
|
||||
shutil.copytree(temp_chromium_dir, target_chromium_dir, dirs_exist_ok=True)
|
||||
|
||||
# 验证提取的文件
|
||||
if temp_chromium_dir.name.startswith('chromium_headless_shell'):
|
||||
target_exe = target_chromium_dir / 'chrome-win' / 'headless_shell.exe'
|
||||
else:
|
||||
target_exe = target_chromium_dir / 'chrome-win' / 'chrome.exe'
|
||||
|
||||
if target_exe.exists() and target_exe.stat().st_size > 0:
|
||||
print(f"{_OK} 浏览器文件提取成功: {target_exe}")
|
||||
print(f"{_INFO} 浏览器版本: {temp_chromium_dir.name}")
|
||||
extracted_count += 1
|
||||
except Exception as e:
|
||||
print(f"{_WARN} 提取 {temp_chromium_dir.name} 失败: {e}")
|
||||
|
||||
if extracted_count > 0:
|
||||
# 清除可能存在的旧环境变量
|
||||
if 'PLAYWRIGHT_BROWSERS_PATH' in os.environ:
|
||||
old_path = os.environ['PLAYWRIGHT_BROWSERS_PATH']
|
||||
print(f"{_INFO} 清除旧的环境变量: {old_path}")
|
||||
del os.environ['PLAYWRIGHT_BROWSERS_PATH']
|
||||
# 设置新的环境变量
|
||||
os.environ['PLAYWRIGHT_BROWSERS_PATH'] = str(playwright_dir)
|
||||
print(f"{_INFO} 已提取 {extracted_count} 个浏览器版本")
|
||||
print(f"{_INFO} 已设置PLAYWRIGHT_BROWSERS_PATH: {playwright_dir}")
|
||||
playwright_installed = True
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"{_WARN} 提取浏览器文件时出错: {e}")
|
||||
|
||||
# 如果没找到,尝试安装
|
||||
if not playwright_installed:
|
||||
print(f"{_WARN} 未找到Playwright浏览器,正在自动安装...")
|
||||
print(" 这可能需要几分钟时间,请耐心等待...")
|
||||
|
||||
try:
|
||||
# 方法1: 尝试使用playwright的Python API安装(推荐,适用于打包后的exe)
|
||||
try:
|
||||
# 直接调用playwright的安装函数
|
||||
from playwright._impl._driver import install_driver, install_browsers
|
||||
print(" 正在安装Playwright驱动...")
|
||||
install_driver()
|
||||
print(" 正在安装Chromium浏览器...")
|
||||
install_browsers(['chromium'])
|
||||
print(f"{_OK} Playwright浏览器安装成功(通过API)")
|
||||
playwright_installed = True
|
||||
except ImportError:
|
||||
# 如果API不可用,使用命令行方式
|
||||
print(" 使用命令行方式安装...")
|
||||
import subprocess
|
||||
|
||||
# 尝试使用playwright的安装命令
|
||||
# 对于打包后的exe,playwright模块应该已经包含
|
||||
creation_flags = 0
|
||||
if sys.platform == 'win32' and hasattr(subprocess, 'CREATE_NO_WINDOW'):
|
||||
creation_flags = subprocess.CREATE_NO_WINDOW
|
||||
|
||||
result = subprocess.run(
|
||||
[sys.executable, '-m', 'playwright', 'install', 'chromium'],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=600, # 10分钟超时
|
||||
creationflags=creation_flags
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
print(f"{_OK} Playwright浏览器安装成功")
|
||||
playwright_installed = True
|
||||
else:
|
||||
print(f"{_WARN} Playwright浏览器安装失败")
|
||||
if result.stdout:
|
||||
print(f" 输出: {result.stdout[-500:]}") # 只显示最后500字符
|
||||
if result.stderr:
|
||||
print(f" 错误: {result.stderr[-500:]}")
|
||||
print(" 您可以稍后手动运行: playwright install chromium")
|
||||
return False
|
||||
except Exception as api_error:
|
||||
# API安装失败,尝试命令行方式
|
||||
print(f" API安装失败,尝试命令行方式: {api_error}")
|
||||
import subprocess
|
||||
|
||||
creation_flags = 0
|
||||
if sys.platform == 'win32' and hasattr(subprocess, 'CREATE_NO_WINDOW'):
|
||||
creation_flags = subprocess.CREATE_NO_WINDOW
|
||||
|
||||
result = subprocess.run(
|
||||
[sys.executable, '-m', 'playwright', 'install', 'chromium'],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=600,
|
||||
creationflags=creation_flags
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
print(f"{_OK} Playwright浏览器安装成功(通过命令行)")
|
||||
playwright_installed = True
|
||||
else:
|
||||
print(f"{_WARN} Playwright浏览器安装失败")
|
||||
if result.stdout:
|
||||
print(f" 输出: {result.stdout[-500:]}")
|
||||
if result.stderr:
|
||||
print(f" 错误: {result.stderr[-500:]}")
|
||||
print(" 您可以稍后手动运行: playwright install chromium")
|
||||
return False
|
||||
except ImportError:
|
||||
# 如果playwright模块不可用,尝试使用subprocess
|
||||
import subprocess
|
||||
result = subprocess.run(
|
||||
[sys.executable, '-m', 'playwright', 'install', 'chromium'],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=600,
|
||||
creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' and hasattr(subprocess, 'CREATE_NO_WINDOW') else 0
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
print(f"{_OK} Playwright浏览器安装成功")
|
||||
playwright_installed = True
|
||||
else:
|
||||
print(f"{_WARN} Playwright浏览器安装失败")
|
||||
if result.stdout:
|
||||
print(f" 输出: {result.stdout}")
|
||||
if result.stderr:
|
||||
print(f" 错误: {result.stderr}")
|
||||
print(" 您可以稍后手动运行: playwright install chromium")
|
||||
return False
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
print(f"{_WARN} Playwright浏览器安装超时(超过10分钟)")
|
||||
print(" 您可以稍后手动运行: playwright install chromium")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"{_WARN} Playwright浏览器安装失败: {e}")
|
||||
import traceback
|
||||
print(f" 详细错误: {traceback.format_exc()}")
|
||||
print(" 您可以稍后手动运行: playwright install chromium")
|
||||
return False
|
||||
|
||||
return playwright_installed
|
||||
|
||||
# 检查并安装Playwright浏览器
|
||||
try:
|
||||
_check_and_install_playwright()
|
||||
except Exception as e:
|
||||
print(f"{_WARN} Playwright浏览器检查失败: {e}")
|
||||
print(" 程序将继续启动,但Playwright功能可能不可用")
|
||||
# 继续启动,不影响主程序运行
|
||||
|
||||
# ==================== 现在可以安全地导入其他模块 ====================
|
||||
import asyncio
|
||||
import threading
|
||||
@ -179,17 +469,13 @@ def _start_api_server():
|
||||
# 在后台线程中创建独立事件循环并直接运行 server.serve()
|
||||
import uvicorn
|
||||
try:
|
||||
# 直接导入 app 对象,避免字符串引用在打包后无法工作
|
||||
from reply_server import app
|
||||
config = uvicorn.Config(app, host=host, port=port, log_level="info")
|
||||
config = uvicorn.Config("reply_server:app", host=host, port=port, log_level="info")
|
||||
server = uvicorn.Server(config)
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
loop.run_until_complete(server.serve())
|
||||
except Exception as e:
|
||||
logger.error(f"uvicorn服务器启动失败: {e}")
|
||||
import traceback
|
||||
logger.error(f"详细错误: {traceback.format_exc()}")
|
||||
try:
|
||||
# 确保线程内事件循环被正确关闭
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
@ -282,21 +282,41 @@ class XianyuLive:
|
||||
try:
|
||||
tasks_to_cancel = []
|
||||
|
||||
# 收集所有需要取消的任务
|
||||
# 收集所有需要取消的任务(只收集未完成的任务)
|
||||
if self.heartbeat_task:
|
||||
tasks_to_cancel.append(("心跳任务", self.heartbeat_task))
|
||||
if not self.heartbeat_task.done():
|
||||
tasks_to_cancel.append(("心跳任务", self.heartbeat_task))
|
||||
else:
|
||||
logger.debug(f"【{self.cookie_id}】心跳任务已完成,跳过")
|
||||
|
||||
if self.token_refresh_task:
|
||||
tasks_to_cancel.append(("Token刷新任务", self.token_refresh_task))
|
||||
if not self.token_refresh_task.done():
|
||||
tasks_to_cancel.append(("Token刷新任务", self.token_refresh_task))
|
||||
else:
|
||||
logger.debug(f"【{self.cookie_id}】Token刷新任务已完成,跳过")
|
||||
|
||||
if self.cleanup_task:
|
||||
tasks_to_cancel.append(("清理任务", self.cleanup_task))
|
||||
if not self.cleanup_task.done():
|
||||
tasks_to_cancel.append(("清理任务", self.cleanup_task))
|
||||
else:
|
||||
logger.debug(f"【{self.cookie_id}】清理任务已完成,跳过")
|
||||
|
||||
if self.cookie_refresh_task:
|
||||
tasks_to_cancel.append(("Cookie刷新任务", self.cookie_refresh_task))
|
||||
if not self.cookie_refresh_task.done():
|
||||
tasks_to_cancel.append(("Cookie刷新任务", self.cookie_refresh_task))
|
||||
else:
|
||||
logger.debug(f"【{self.cookie_id}】Cookie刷新任务已完成,跳过")
|
||||
|
||||
if not tasks_to_cancel:
|
||||
logger.info(f"【{self.cookie_id}】没有后台任务需要取消")
|
||||
logger.info(f"【{self.cookie_id}】没有后台任务需要取消(所有任务已完成或不存在)")
|
||||
# 立即重置任务引用
|
||||
self.heartbeat_task = None
|
||||
self.token_refresh_task = None
|
||||
self.cleanup_task = None
|
||||
self.cookie_refresh_task = None
|
||||
return
|
||||
|
||||
logger.info(f"【{self.cookie_id}】开始取消 {len(tasks_to_cancel)} 个后台任务...")
|
||||
logger.info(f"【{self.cookie_id}】开始取消 {len(tasks_to_cancel)} 个未完成的后台任务...")
|
||||
|
||||
# 取消所有任务
|
||||
for task_name, task in tasks_to_cancel:
|
||||
@ -545,6 +565,66 @@ class XianyuLive:
|
||||
except Exception as e:
|
||||
logger.warning(f"【{self.cookie_id}】清理Playwright缓存时出错: {self._safe_str(e)}")
|
||||
|
||||
async def _cleanup_old_logs(self, retention_days: int = 7):
|
||||
"""清理过期的日志文件
|
||||
|
||||
Args:
|
||||
retention_days: 保留的天数,默认7天
|
||||
|
||||
Returns:
|
||||
清理的文件数量
|
||||
"""
|
||||
try:
|
||||
import glob
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
logs_dir = "logs"
|
||||
if not os.path.exists(logs_dir):
|
||||
logger.warning(f"【{self.cookie_id}】日志目录不存在: {logs_dir}")
|
||||
return 0
|
||||
|
||||
# 计算过期时间点
|
||||
cutoff_time = datetime.now() - timedelta(days=retention_days)
|
||||
|
||||
# 查找所有日志文件(包括.log和.log.zip)
|
||||
log_patterns = [
|
||||
os.path.join(logs_dir, "xianyu_*.log"),
|
||||
os.path.join(logs_dir, "xianyu_*.log.zip"),
|
||||
os.path.join(logs_dir, "app_*.log"),
|
||||
os.path.join(logs_dir, "app_*.log.zip"),
|
||||
]
|
||||
|
||||
total_cleaned = 0
|
||||
total_size_mb = 0
|
||||
|
||||
for pattern in log_patterns:
|
||||
log_files = glob.glob(pattern)
|
||||
for log_file in log_files:
|
||||
try:
|
||||
# 获取文件修改时间
|
||||
file_mtime = datetime.fromtimestamp(os.path.getmtime(log_file))
|
||||
|
||||
# 如果文件早于保留期限,则删除
|
||||
if file_mtime < cutoff_time:
|
||||
file_size = os.path.getsize(log_file)
|
||||
os.remove(log_file)
|
||||
total_size_mb += file_size / (1024 * 1024)
|
||||
total_cleaned += 1
|
||||
logger.debug(f"【{self.cookie_id}】删除过期日志文件: {log_file} (修改时间: {file_mtime})")
|
||||
except Exception as e:
|
||||
logger.warning(f"【{self.cookie_id}】删除日志文件失败 {log_file}: {self._safe_str(e)}")
|
||||
|
||||
if total_cleaned > 0:
|
||||
logger.info(f"【{self.cookie_id}】日志清理完成: 删除了 {total_cleaned} 个日志文件,释放 {total_size_mb:.2f} MB (保留 {retention_days} 天内的日志)")
|
||||
else:
|
||||
logger.debug(f"【{self.cookie_id}】日志清理: 没有需要清理的过期日志文件 (保留 {retention_days} 天)")
|
||||
|
||||
return total_cleaned
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"【{self.cookie_id}】清理日志文件时出错: {self._safe_str(e)}")
|
||||
return 0
|
||||
|
||||
def __init__(self, cookies_str=None, cookie_id: str = "default", user_id: int = None):
|
||||
"""初始化闲鱼直播类"""
|
||||
logger.info(f"【{cookie_id}】开始初始化XianyuLive...")
|
||||
@ -649,13 +729,14 @@ class XianyuLive:
|
||||
# 消息防抖管理器:用于处理用户连续发送消息的情况
|
||||
# {chat_id: {'task': asyncio.Task, 'last_message': dict, 'timer': float}}
|
||||
self.message_debounce_tasks = {} # 存储每个chat_id的防抖任务
|
||||
self.message_debounce_delay = 3 # 防抖延迟时间(秒):用户停止发送消息3秒后才回复
|
||||
self.message_debounce_delay = 1 # 防抖延迟时间(秒):用户停止发送消息1秒后才回复
|
||||
self.message_debounce_lock = asyncio.Lock() # 防抖任务管理的锁
|
||||
|
||||
# 消息去重机制:防止同一条消息被处理多次
|
||||
self.processed_message_ids = set() # 存储已处理的消息ID
|
||||
self.processed_message_ids = {} # 存储已处理的消息ID和时间戳 {message_id: timestamp}
|
||||
self.processed_message_ids_lock = asyncio.Lock() # 消息ID去重的锁
|
||||
self.processed_message_ids_max_size = 10000 # 最大保存10000个消息ID,防止内存泄漏
|
||||
self.message_expire_time = 3600 # 消息过期时间(秒),默认1小时后可以重复回复
|
||||
|
||||
# 初始化订单状态处理器
|
||||
self._init_order_status_handler()
|
||||
@ -1509,136 +1590,17 @@ class XianyuLive:
|
||||
if isinstance(res_json, dict):
|
||||
res_json_str = json.dumps(res_json, ensure_ascii=False, separators=(',', ':'))
|
||||
if '令牌过期' in res_json_str or 'Session过期' in res_json_str:
|
||||
logger.warning(f"【{self.cookie_id}】检测到令牌/Session过期,准备刷新Cookie并重启实例...")
|
||||
|
||||
# 检查是否在密码登录冷却期内,避免重复登录
|
||||
current_time = time.time()
|
||||
last_password_login = XianyuLive._last_password_login_time.get(self.cookie_id, 0)
|
||||
time_since_last_login = current_time - last_password_login
|
||||
# 调用统一的密码登录刷新方法
|
||||
refresh_success = await self._try_password_login_refresh("令牌/Session过期")
|
||||
|
||||
if last_password_login > 0 and time_since_last_login < XianyuLive._password_login_cooldown:
|
||||
remaining_time = XianyuLive._password_login_cooldown - time_since_last_login
|
||||
logger.warning(f"【{self.cookie_id}】距离上次密码登录仅 {time_since_last_login:.1f} 秒,仍在冷却期内(还需等待 {remaining_time:.1f} 秒),跳过密码登录")
|
||||
logger.warning(f"【{self.cookie_id}】提示:如果新Cookie仍然无效,请检查账号状态或手动更新Cookie")
|
||||
if not refresh_success:
|
||||
# 标记已发送通知,避免重复通知
|
||||
notification_sent = True
|
||||
# 返回None,让调用者知道刷新失败
|
||||
return None
|
||||
|
||||
# 记录到日志文件
|
||||
log_captcha_event(self.cookie_id, "令牌/Session过期触发Cookie刷新和实例重启", None,
|
||||
f"检测到令牌/Session过期,准备刷新Cookie并重启实例")
|
||||
|
||||
try:
|
||||
|
||||
# 从数据库获取账号登录信息
|
||||
from db_manager import db_manager
|
||||
account_info = db_manager.get_cookie_details(self.cookie_id)
|
||||
|
||||
if not account_info:
|
||||
logger.error(f"【{self.cookie_id}】无法获取账号信息")
|
||||
raise Exception("无法获取账号信息")
|
||||
|
||||
# 【重要】先检查数据库中的cookie是否已经更新
|
||||
# 如果用户已经手动更新了cookie,就不需要触发密码登录刷新
|
||||
db_cookie_value = account_info.get('cookie_value', '')
|
||||
if db_cookie_value and db_cookie_value != self.cookies_str:
|
||||
logger.info(f"【{self.cookie_id}】检测到数据库中的cookie已更新,重新加载cookie并重试token刷新")
|
||||
self.cookies_str = db_cookie_value
|
||||
self.cookies = trans_cookies(self.cookies_str)
|
||||
logger.info(f"【{self.cookie_id}】Cookie已从数据库重新加载,跳过密码登录刷新")
|
||||
# 重新尝试刷新token(递归调用,但不会无限递归,因为已经重新加载了cookie)
|
||||
return await self.refresh_token(captcha_retry_count)
|
||||
|
||||
username = account_info.get('username', '')
|
||||
password = account_info.get('password', '')
|
||||
show_browser = account_info.get('show_browser', False)
|
||||
|
||||
# 检查是否配置了用户名和密码
|
||||
if not username or not password:
|
||||
logger.warning(f"【{self.cookie_id}】未配置用户名或密码,跳过密码登录刷新")
|
||||
raise Exception("未配置用户名或密码")
|
||||
|
||||
# 使用集成的 Playwright 登录方法(无需猴子补丁)
|
||||
from utils.xianyu_slider_stealth import XianyuSliderStealth
|
||||
browser_mode = "有头" if show_browser else "无头"
|
||||
logger.info(f"【{self.cookie_id}】开始使用{browser_mode}浏览器进行密码登录刷新Cookie...")
|
||||
logger.info(f"【{self.cookie_id}】使用账号: {username}")
|
||||
|
||||
# 创建一个通知回调包装函数,支持接收截图路径和验证链接
|
||||
async def notification_callback_wrapper(message: str, screenshot_path: str = None, verification_url: str = None):
|
||||
"""通知回调包装函数,支持接收截图路径和验证链接"""
|
||||
await self.send_token_refresh_notification(
|
||||
error_message=message,
|
||||
notification_type="token_refresh",
|
||||
chat_id=None,
|
||||
attachment_path=screenshot_path,
|
||||
verification_url=verification_url
|
||||
)
|
||||
|
||||
# 在单独的线程中运行同步的登录方法
|
||||
import asyncio
|
||||
slider = XianyuSliderStealth(user_id=self.cookie_id, enable_learning=False, headless=not show_browser)
|
||||
result = await asyncio.to_thread(
|
||||
slider.login_with_password_playwright,
|
||||
account=username,
|
||||
password=password,
|
||||
show_browser=show_browser,
|
||||
notification_callback=notification_callback_wrapper
|
||||
)
|
||||
|
||||
if result:
|
||||
logger.info(f"【{self.cookie_id}】密码登录成功,获取到Cookie")
|
||||
logger.info(f"【{self.cookie_id}】Cookie内容: {result}")
|
||||
|
||||
# 打印密码登录获取的Cookie字段详情
|
||||
logger.info(f"【{self.cookie_id}】========== 密码登录Cookie字段详情 ==========")
|
||||
logger.info(f"【{self.cookie_id}】Cookie字段数: {len(result)}")
|
||||
logger.info(f"【{self.cookie_id}】Cookie字段列表:")
|
||||
for i, (key, value) in enumerate(result.items(), 1):
|
||||
if len(str(value)) > 50:
|
||||
logger.info(f"【{self.cookie_id}】 {i:2d}. {key}: {str(value)[:30]}...{str(value)[-20:]} (长度: {len(str(value))})")
|
||||
else:
|
||||
logger.info(f"【{self.cookie_id}】 {i:2d}. {key}: {value}")
|
||||
|
||||
# 检查关键字段
|
||||
important_keys = ['unb', '_m_h5_tk', '_m_h5_tk_enc', 'cookie2', 't', 'sgcookie', 'cna']
|
||||
logger.info(f"【{self.cookie_id}】关键字段检查:")
|
||||
for key in important_keys:
|
||||
if key in result:
|
||||
val = result[key]
|
||||
logger.info(f"【{self.cookie_id}】 ✅ {key}: {'存在' if val else '为空'} (长度: {len(str(val)) if val else 0})")
|
||||
else:
|
||||
logger.info(f"【{self.cookie_id}】 ❌ {key}: 缺失")
|
||||
logger.info(f"【{self.cookie_id}】==========================================")
|
||||
|
||||
# 将cookie字典转换为字符串格式
|
||||
new_cookies_str = '; '.join([f"{k}={v}" for k, v in result.items()])
|
||||
logger.info(f"【{self.cookie_id}】Cookie字符串格式: {new_cookies_str[:200]}..." if len(new_cookies_str) > 200 else f"【{self.cookie_id}】Cookie字符串格式: {new_cookies_str}")
|
||||
|
||||
# 更新Cookie并重启任务
|
||||
logger.info(f"【{self.cookie_id}】尝试通过访问指定页面获取真实cookie...")
|
||||
update_success = await self._refresh_cookies_via_browser_page(new_cookies_str)
|
||||
|
||||
if update_success:
|
||||
logger.info(f"【{self.cookie_id}】Cookie更新并重启任务成功")
|
||||
# 记录密码登录时间,防止重复登录
|
||||
XianyuLive._last_password_login_time[self.cookie_id] = time.time()
|
||||
logger.warning(f"【{self.cookie_id}】已记录密码登录时间,冷却期 {XianyuLive._password_login_cooldown} 秒")
|
||||
|
||||
# 发送账号密码登录成功通知
|
||||
await self.send_token_refresh_notification(
|
||||
f"账号密码登录成功,Cookie已更新,任务已重启",
|
||||
"password_login_success"
|
||||
)
|
||||
else:
|
||||
logger.warning(f"【{self.cookie_id}】Cookie更新或重启任务失败")
|
||||
|
||||
else:
|
||||
logger.warning(f"【{self.cookie_id}】密码登录失败,未获取到Cookie")
|
||||
|
||||
except Exception as refresh_e:
|
||||
logger.error(f"【{self.cookie_id}】Cookie刷新或实例重启失败: {self._safe_str(refresh_e)}")
|
||||
else:
|
||||
# 刷新成功后,重新尝试获取token
|
||||
return await self.refresh_token(captcha_retry_count)
|
||||
|
||||
# 刷新失败时继续执行原有的失败处理逻辑
|
||||
|
||||
@ -2035,8 +1997,10 @@ class XianyuLive:
|
||||
# 通过CookieManager重启任务
|
||||
logger.info(f"【{self.cookie_id}】通过CookieManager重启任务...")
|
||||
await self._restart_instance()
|
||||
|
||||
logger.info(f"【{self.cookie_id}】cookies更新和任务重启完成")
|
||||
|
||||
# ⚠️ _restart_instance() 已触发重启,当前任务即将被取消
|
||||
# 立即返回,不执行后续代码
|
||||
logger.info(f"【{self.cookie_id}】cookies更新成功,重启请求已触发")
|
||||
return True
|
||||
|
||||
except Exception as update_e:
|
||||
@ -2096,10 +2060,335 @@ class XianyuLive:
|
||||
# 发送Cookie更新失败通知
|
||||
await self.send_token_refresh_notification(f"Cookie更新失败: {str(e)}", "cookie_update_failed")
|
||||
|
||||
async def _restart_instance(self):
|
||||
"""重启XianyuLive实例"""
|
||||
async def _try_password_login_refresh(self, trigger_reason: str = "令牌/Session过期"):
|
||||
"""尝试通过密码登录刷新Cookie并重启实例
|
||||
|
||||
Args:
|
||||
trigger_reason: 触发原因,用于日志记录
|
||||
|
||||
Returns:
|
||||
bool: 是否成功刷新Cookie
|
||||
"""
|
||||
logger.warning(f"【{self.cookie_id}】检测到{trigger_reason},准备刷新Cookie并重启实例...")
|
||||
|
||||
# 检查是否在密码登录冷却期内,避免重复登录
|
||||
current_time = time.time()
|
||||
last_password_login = XianyuLive._last_password_login_time.get(self.cookie_id, 0)
|
||||
time_since_last_login = current_time - last_password_login
|
||||
|
||||
if last_password_login > 0 and time_since_last_login < XianyuLive._password_login_cooldown:
|
||||
remaining_time = XianyuLive._password_login_cooldown - time_since_last_login
|
||||
logger.warning(f"【{self.cookie_id}】距离上次密码登录仅 {time_since_last_login:.1f} 秒,仍在冷却期内(还需等待 {remaining_time:.1f} 秒),跳过密码登录")
|
||||
logger.warning(f"【{self.cookie_id}】提示:如果新Cookie仍然无效,请检查账号状态或手动更新Cookie")
|
||||
return False
|
||||
|
||||
# 记录到日志文件
|
||||
log_captcha_event(self.cookie_id, f"{trigger_reason}触发Cookie刷新和实例重启", None,
|
||||
f"检测到{trigger_reason},准备刷新Cookie并重启实例")
|
||||
|
||||
try:
|
||||
logger.info(f"【{self.cookie_id}】Token刷新成功,准备重启实例...")
|
||||
# 从数据库获取账号登录信息
|
||||
from db_manager import db_manager
|
||||
account_info = db_manager.get_cookie_details(self.cookie_id)
|
||||
|
||||
if not account_info:
|
||||
logger.error(f"【{self.cookie_id}】无法获取账号信息")
|
||||
return False
|
||||
|
||||
# 【重要】先检查数据库中的cookie是否已经更新
|
||||
# 如果用户已经手动更新了cookie,就不需要触发密码登录刷新
|
||||
db_cookie_value = account_info.get('cookie_value', '')
|
||||
if db_cookie_value and db_cookie_value != self.cookies_str:
|
||||
logger.info(f"【{self.cookie_id}】检测到数据库中的cookie已更新,重新加载cookie")
|
||||
self.cookies_str = db_cookie_value
|
||||
self.cookies = trans_cookies(self.cookies_str)
|
||||
logger.info(f"【{self.cookie_id}】Cookie已从数据库重新加载,跳过密码登录刷新")
|
||||
return True
|
||||
|
||||
username = account_info.get('username', '')
|
||||
password = account_info.get('password', '')
|
||||
show_browser = account_info.get('show_browser', False)
|
||||
|
||||
# 检查是否配置了用户名和密码
|
||||
if not username or not password:
|
||||
logger.warning(f"【{self.cookie_id}】未配置用户名或密码,跳过密码登录刷新")
|
||||
await self.send_token_refresh_notification(
|
||||
f"检测到{trigger_reason},但未配置用户名或密码,无法自动刷新Cookie",
|
||||
"no_credentials"
|
||||
)
|
||||
return False
|
||||
|
||||
# 使用集成的 Playwright 登录方法(无需猴子补丁)
|
||||
from utils.xianyu_slider_stealth import XianyuSliderStealth
|
||||
browser_mode = "有头" if show_browser else "无头"
|
||||
logger.info(f"【{self.cookie_id}】开始使用{browser_mode}浏览器进行密码登录刷新Cookie...")
|
||||
logger.info(f"【{self.cookie_id}】使用账号: {username}")
|
||||
|
||||
# 创建一个通知回调包装函数,支持接收截图路径和验证链接
|
||||
async def notification_callback_wrapper(message: str, screenshot_path: str = None, verification_url: str = None):
|
||||
"""通知回调包装函数,支持接收截图路径和验证链接"""
|
||||
await self.send_token_refresh_notification(
|
||||
error_message=message,
|
||||
notification_type="token_refresh",
|
||||
chat_id=None,
|
||||
attachment_path=screenshot_path,
|
||||
verification_url=verification_url
|
||||
)
|
||||
|
||||
# 在单独的线程中运行同步的登录方法
|
||||
import asyncio
|
||||
slider = XianyuSliderStealth(user_id=self.cookie_id, enable_learning=False, headless=not show_browser)
|
||||
result = await asyncio.to_thread(
|
||||
slider.login_with_password_playwright,
|
||||
account=username,
|
||||
password=password,
|
||||
show_browser=show_browser,
|
||||
notification_callback=notification_callback_wrapper
|
||||
)
|
||||
|
||||
if result:
|
||||
logger.info(f"【{self.cookie_id}】密码登录成功,获取到Cookie")
|
||||
logger.info(f"【{self.cookie_id}】Cookie内容: {result}")
|
||||
|
||||
# 打印密码登录获取的Cookie字段详情
|
||||
logger.info(f"【{self.cookie_id}】========== 密码登录Cookie字段详情 ==========")
|
||||
logger.info(f"【{self.cookie_id}】Cookie字段数: {len(result)}")
|
||||
logger.info(f"【{self.cookie_id}】Cookie字段列表:")
|
||||
for i, (key, value) in enumerate(result.items(), 1):
|
||||
if len(str(value)) > 50:
|
||||
logger.info(f"【{self.cookie_id}】 {i:2d}. {key}: {str(value)[:30]}...{str(value)[-20:]} (长度: {len(str(value))})")
|
||||
else:
|
||||
logger.info(f"【{self.cookie_id}】 {i:2d}. {key}: {value}")
|
||||
|
||||
# 检查关键字段
|
||||
important_keys = ['unb', '_m_h5_tk', '_m_h5_tk_enc', 'cookie2', 't', 'sgcookie', 'cna']
|
||||
logger.info(f"【{self.cookie_id}】关键字段检查:")
|
||||
for key in important_keys:
|
||||
if key in result:
|
||||
val = result[key]
|
||||
logger.info(f"【{self.cookie_id}】 ✅ {key}: {'存在' if val else '为空'} (长度: {len(str(val)) if val else 0})")
|
||||
else:
|
||||
logger.info(f"【{self.cookie_id}】 ❌ {key}: 缺失")
|
||||
logger.info(f"【{self.cookie_id}】==========================================")
|
||||
|
||||
# 将cookie字典转换为字符串格式
|
||||
new_cookies_str = '; '.join([f"{k}={v}" for k, v in result.items()])
|
||||
logger.info(f"【{self.cookie_id}】Cookie字符串格式: {new_cookies_str[:200]}..." if len(new_cookies_str) > 200 else f"【{self.cookie_id}】Cookie字符串格式: {new_cookies_str}")
|
||||
|
||||
# 记录密码登录时间,防止重复登录
|
||||
XianyuLive._last_password_login_time[self.cookie_id] = time.time()
|
||||
logger.warning(f"【{self.cookie_id}】已记录密码登录时间,冷却期 {XianyuLive._password_login_cooldown} 秒")
|
||||
|
||||
# 更新cookies并重启任务
|
||||
update_success = await self._update_cookies_and_restart(new_cookies_str)
|
||||
|
||||
if update_success:
|
||||
logger.info(f"【{self.cookie_id}】Cookie更新并重启任务成功")
|
||||
# 发送账号密码登录成功通知
|
||||
await self.send_token_refresh_notification(
|
||||
f"账号密码登录成功,Cookie已更新,任务已重启",
|
||||
"password_login_success"
|
||||
)
|
||||
return True
|
||||
else:
|
||||
logger.error(f"【{self.cookie_id}】Cookie更新失败")
|
||||
return False
|
||||
|
||||
else:
|
||||
logger.warning(f"【{self.cookie_id}】密码登录失败,未获取到Cookie")
|
||||
return False
|
||||
|
||||
except Exception as refresh_e:
|
||||
logger.error(f"【{self.cookie_id}】Cookie刷新或实例重启失败: {self._safe_str(refresh_e)}")
|
||||
import traceback
|
||||
logger.error(f"【{self.cookie_id}】详细堆栈:\n{traceback.format_exc()}")
|
||||
return False
|
||||
|
||||
async def _verify_cookie_validity(self) -> dict:
|
||||
"""验证Cookie的有效性,通过实际调用API测试
|
||||
|
||||
Returns:
|
||||
dict: {
|
||||
'valid': bool, # 总体是否有效
|
||||
'confirm_api': bool, # 确认发货API是否有效
|
||||
'image_api': bool, # 图片上传API是否有效
|
||||
'details': str # 详细信息
|
||||
}
|
||||
"""
|
||||
logger.info(f"【{self.cookie_id}】开始验证Cookie有效性(使用真实API调用)...")
|
||||
|
||||
result = {
|
||||
'valid': True,
|
||||
'confirm_api': None,
|
||||
'image_api': None,
|
||||
'details': []
|
||||
}
|
||||
|
||||
# 1. 测试确认发货API - 使用测试订单ID实际调用
|
||||
# try:
|
||||
# logger.info(f"【{self.cookie_id}】测试确认发货API(使用测试数据实际调用)...")
|
||||
|
||||
# # 确保session存在
|
||||
# if not self.session:
|
||||
# import aiohttp
|
||||
# connector = aiohttp.TCPConnector(limit=100, limit_per_host=30)
|
||||
# timeout = aiohttp.ClientTimeout(total=30)
|
||||
# self.session = aiohttp.ClientSession(connector=connector, timeout=timeout)
|
||||
|
||||
# # 创建临时的确认发货实例
|
||||
# from secure_confirm_decrypted import SecureConfirm
|
||||
# confirm_tester = SecureConfirm(
|
||||
# session=self.session,
|
||||
# cookies_str=self.cookies_str,
|
||||
# cookie_id=self.cookie_id,
|
||||
# main_instance=self
|
||||
# )
|
||||
|
||||
# # 使用一个测试订单ID(不存在的订单ID)
|
||||
# # 如果Cookie有效,应该返回"订单不存在"类的错误
|
||||
# # 如果Cookie无效,会返回"Session过期"错误
|
||||
# test_order_id = "999999999999999999" # 不存在的测试订单ID
|
||||
|
||||
# # 实际调用API (retry_count=3阻止重试,快速失败)
|
||||
# response = await confirm_tester.auto_confirm(test_order_id, retry_count=3)
|
||||
|
||||
# # 分析响应
|
||||
# if response and isinstance(response, dict):
|
||||
# error_msg = str(response.get('error', ''))
|
||||
# success = response.get('success', False)
|
||||
|
||||
# # 检查是否是Session过期错误
|
||||
# if 'Session过期' in error_msg or 'SESSION_EXPIRED' in error_msg:
|
||||
# logger.warning(f"【{self.cookie_id}】❌ 确认发货API验证失败: Session过期")
|
||||
# result['confirm_api'] = False
|
||||
# result['valid'] = False
|
||||
# result['details'].append("确认发货API: Session过期")
|
||||
# elif '令牌过期' in error_msg:
|
||||
# logger.warning(f"【{self.cookie_id}】❌ 确认发货API验证失败: 令牌过期")
|
||||
# result['confirm_api'] = False
|
||||
# result['valid'] = False
|
||||
# result['details'].append("确认发货API: 令牌过期")
|
||||
# elif success:
|
||||
# # 竟然成功了(不太可能,因为是测试订单ID)
|
||||
# logger.info(f"【{self.cookie_id}】✅ 确认发货API验证通过: API调用成功")
|
||||
# result['confirm_api'] = True
|
||||
# result['details'].append("确认发货API: 通过验证")
|
||||
# elif error_msg and len(error_msg) > 0:
|
||||
# # 有其他错误信息(如订单不存在、重试次数过多等),说明Cookie是有效的
|
||||
# logger.info(f"【{self.cookie_id}】✅ 确认发货API验证通过: Cookie有效(返回业务错误: {error_msg[:50]})")
|
||||
# result['confirm_api'] = True
|
||||
# result['details'].append(f"确认发货API: 通过验证")
|
||||
# else:
|
||||
# # 没有明确信息,保守认为可能有问题
|
||||
# logger.warning(f"【{self.cookie_id}】⚠️ 确认发货API验证警告: 响应不明确")
|
||||
# result['confirm_api'] = False
|
||||
# result['valid'] = False
|
||||
# result['details'].append("确认发货API: 响应不明确")
|
||||
# else:
|
||||
# # 没有响应,可能有问题
|
||||
# logger.warning(f"【{self.cookie_id}】⚠️ 确认发货API验证警告: 无响应")
|
||||
# result['confirm_api'] = False
|
||||
# result['valid'] = False
|
||||
# result['details'].append("确认发货API: 无响应")
|
||||
|
||||
# except Exception as e:
|
||||
# error_str = self._safe_str(e)
|
||||
# # 检查异常信息中是否包含Session过期
|
||||
# if 'Session过期' in error_str or 'SESSION_EXPIRED' in error_str:
|
||||
# logger.warning(f"【{self.cookie_id}】❌ 确认发货API验证失败: Session过期")
|
||||
# result['confirm_api'] = False
|
||||
# result['valid'] = False
|
||||
# result['details'].append("确认发货API: Session过期")
|
||||
# else:
|
||||
# logger.error(f"【{self.cookie_id}】确认发货API验证异常: {error_str}")
|
||||
# # 网络异常等问题,不一定是Cookie问题,暂时标记为通过
|
||||
# result['confirm_api'] = True
|
||||
# result['details'].append(f"确认发货API: 调用异常(可能非Cookie问题)")
|
||||
|
||||
# 2. 测试图片上传API - 创建测试图片并实际上传
|
||||
try:
|
||||
logger.info(f"【{self.cookie_id}】测试图片上传API(使用测试图片实际上传)...")
|
||||
|
||||
# 创建一个最小的测试图片(1x1像素的PNG)
|
||||
import tempfile
|
||||
import os
|
||||
from PIL import Image
|
||||
|
||||
# 创建临时目录
|
||||
temp_dir = tempfile.gettempdir()
|
||||
test_image_path = os.path.join(temp_dir, f'cookie_test_{self.cookie_id}.png')
|
||||
|
||||
try:
|
||||
# 创建1x1像素的白色图片
|
||||
img = Image.new('RGB', (1, 1), color='white')
|
||||
img.save(test_image_path, 'PNG')
|
||||
logger.info(f"【{self.cookie_id}】已创建测试图片: {test_image_path}")
|
||||
|
||||
# 创建图片上传实例
|
||||
from utils.image_uploader import ImageUploader
|
||||
uploader = ImageUploader(cookies_str=self.cookies_str)
|
||||
|
||||
# 创建session
|
||||
await uploader.create_session()
|
||||
|
||||
try:
|
||||
# 实际上传测试图片
|
||||
upload_result = await uploader.upload_image(test_image_path)
|
||||
finally:
|
||||
# 确保关闭session
|
||||
await uploader.close_session()
|
||||
|
||||
# 分析上传结果
|
||||
if upload_result:
|
||||
# 上传成功,Cookie有效
|
||||
logger.info(f"【{self.cookie_id}】✅ 图片上传API验证通过: 上传成功 ({upload_result[:50]}...)")
|
||||
result['image_api'] = True
|
||||
result['details'].append("图片上传API: 通过验证")
|
||||
else:
|
||||
# 上传失败,需要进一步判断原因
|
||||
# 如果是Cookie失效,通常会返回HTML登录页面
|
||||
logger.warning(f"【{self.cookie_id}】❌ 图片上传API验证失败: 上传失败(可能是Cookie失效)")
|
||||
result['image_api'] = False
|
||||
result['valid'] = False
|
||||
result['details'].append("图片上传API: 上传失败,可能Cookie已失效")
|
||||
|
||||
finally:
|
||||
# 清理测试图片
|
||||
if os.path.exists(test_image_path):
|
||||
try:
|
||||
os.remove(test_image_path)
|
||||
logger.debug(f"【{self.cookie_id}】已删除测试图片")
|
||||
except:
|
||||
pass
|
||||
|
||||
except Exception as e:
|
||||
error_str = self._safe_str(e)
|
||||
logger.error(f"【{self.cookie_id}】图片上传API验证异常: {error_str}")
|
||||
# 图片上传异常,标记为失败
|
||||
result['image_api'] = False
|
||||
result['valid'] = False
|
||||
result['details'].append(f"图片上传API: 验证异常 - {error_str[:50]}")
|
||||
|
||||
# 汇总结果
|
||||
if result['valid']:
|
||||
logger.info(f"【{self.cookie_id}】✅ Cookie验证通过: 所有关键API均可用")
|
||||
else:
|
||||
logger.warning(f"【{self.cookie_id}】❌ Cookie验证失败:")
|
||||
for detail in result['details']:
|
||||
logger.warning(f"【{self.cookie_id}】 - {detail}")
|
||||
|
||||
result['details'] = '; '.join(result['details'])
|
||||
return result
|
||||
|
||||
async def _restart_instance(self):
|
||||
"""重启XianyuLive实例
|
||||
|
||||
⚠️ 注意:此方法会触发当前任务被取消!
|
||||
调用此方法后,当前任务会立即被 CookieManager 取消,
|
||||
因此不要在此方法后执行任何重要操作。
|
||||
"""
|
||||
try:
|
||||
logger.info(f"【{self.cookie_id}】准备重启实例...")
|
||||
|
||||
# 导入CookieManager
|
||||
from cookie_manager import manager as cookie_manager
|
||||
@ -2107,29 +2396,48 @@ class XianyuLive:
|
||||
if cookie_manager:
|
||||
# 通过CookieManager重启实例
|
||||
logger.info(f"【{self.cookie_id}】通过CookieManager重启实例...")
|
||||
|
||||
# 使用异步方式调用update_cookie,避免阻塞
|
||||
def restart_task():
|
||||
|
||||
# ⚠️ 重要:不要等待重启完成!
|
||||
# cookie_manager.update_cookie() 会立即取消当前任务
|
||||
# 如果我们等待它完成,会导致 CancelledError 中断等待
|
||||
# 正确的做法是:触发重启后立即返回,让任务自然退出
|
||||
|
||||
import threading
|
||||
|
||||
def trigger_restart():
|
||||
"""在后台线程中触发重启,不阻塞当前任务"""
|
||||
try:
|
||||
# 给当前任务一点时间完成清理(避免竞态条件)
|
||||
import time
|
||||
time.sleep(0.5)
|
||||
|
||||
# save_to_db=False 因为 update_config_cookies 已经保存过了
|
||||
cookie_manager.update_cookie(self.cookie_id, self.cookies_str, save_to_db=False)
|
||||
logger.info(f"【{self.cookie_id}】实例重启请求已发送")
|
||||
logger.info(f"【{self.cookie_id}】实例重启请求已触发")
|
||||
except Exception as e:
|
||||
logger.error(f"【{self.cookie_id}】重启实例失败: {e}")
|
||||
logger.error(f"【{self.cookie_id}】触发实例重启失败: {e}")
|
||||
import traceback
|
||||
logger.error(f"【{self.cookie_id}】重启失败详情:\n{traceback.format_exc()}")
|
||||
|
||||
# 在后台执行重启任务
|
||||
import threading
|
||||
restart_thread = threading.Thread(target=restart_task, daemon=True)
|
||||
# 在后台线程中触发重启
|
||||
restart_thread = threading.Thread(target=trigger_restart, daemon=True)
|
||||
restart_thread.start()
|
||||
|
||||
logger.info(f"【{self.cookie_id}】实例重启已在后台执行")
|
||||
|
||||
logger.info(f"【{self.cookie_id}】实例重启已触发,当前任务即将退出...")
|
||||
logger.warning(f"【{self.cookie_id}】注意:重启请求已发送,CookieManager将在0.5秒后取消当前任务并启动新实例")
|
||||
|
||||
else:
|
||||
logger.warning(f"【{self.cookie_id}】CookieManager不可用,无法重启实例")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"【{self.cookie_id}】重启实例失败: {self._safe_str(e)}")
|
||||
import traceback
|
||||
logger.error(f"【{self.cookie_id}】重启失败堆栈:\n{traceback.format_exc()}")
|
||||
# 发送重启失败通知
|
||||
await self.send_token_refresh_notification(f"实例重启失败: {str(e)}", "instance_restart_failed")
|
||||
try:
|
||||
await self.send_token_refresh_notification(f"实例重启失败: {str(e)}", "instance_restart_failed")
|
||||
except Exception as notify_e:
|
||||
logger.error(f"【{self.cookie_id}】发送重启失败通知时出错: {self._safe_str(notify_e)}")
|
||||
|
||||
async def save_item_info_to_db(self, item_id: str, item_detail: str = None, item_title: str = None):
|
||||
"""保存商品信息到数据库
|
||||
@ -2976,7 +3284,8 @@ class XianyuLive:
|
||||
image_url = cdn_url
|
||||
else:
|
||||
logger.error(f"图片上传失败: {local_image_path}")
|
||||
return f"抱歉,图片发送失败,请稍后重试。"
|
||||
logger.error(f"❌ Cookie可能已失效!请检查配置并更新Cookie")
|
||||
return f"抱歉,图片发送失败(Cookie可能已失效,请检查日志)"
|
||||
else:
|
||||
logger.error(f"本地图片文件不存在: {local_image_path}")
|
||||
return f"抱歉,图片文件不存在。"
|
||||
@ -3995,6 +4304,9 @@ class XianyuLive:
|
||||
case 'bark':
|
||||
await self._send_bark_notification(config_data, notification_message)
|
||||
logger.info(f"已发送自动发货通知到Bark")
|
||||
case 'feishu' | 'lark':
|
||||
await self._send_feishu_notification(config_data, notification_message)
|
||||
logger.info(f"已发送自动发货通知到飞书")
|
||||
case _:
|
||||
logger.warning(f"不支持的通知渠道类型: {channel_type}")
|
||||
|
||||
@ -4706,11 +5018,24 @@ class XianyuLive:
|
||||
logger.info("Token即将过期,准备刷新...")
|
||||
new_token = await self.refresh_token()
|
||||
if new_token:
|
||||
logger.info(f"【{self.cookie_id}】Token刷新成功,准备重启实例...")
|
||||
# 注意:refresh_token方法中已经调用了_restart_instance()
|
||||
# 这里只需要关闭当前连接,让main循环重新开始
|
||||
self.connection_restart_flag = True
|
||||
await self._restart_instance()
|
||||
logger.info(f"【{self.cookie_id}】Token刷新成功,将关闭WebSocket以使用新Token重连")
|
||||
|
||||
# Token刷新成功后,需要关闭WebSocket连接,让它用新Token重新连接
|
||||
# 原因:WebSocket连接建立时使用的是旧Token,新Token需要重新建立连接才能生效
|
||||
# 注意:只关闭WebSocket,不重启整个实例(后台任务继续运行)
|
||||
|
||||
# 关闭当前WebSocket连接
|
||||
if self.ws and not self.ws.closed:
|
||||
try:
|
||||
logger.info(f"【{self.cookie_id}】关闭当前WebSocket连接以使用新Token重连...")
|
||||
await self.ws.close()
|
||||
logger.info(f"【{self.cookie_id}】WebSocket连接已关闭,将自动重连")
|
||||
except Exception as close_e:
|
||||
logger.warning(f"【{self.cookie_id}】关闭WebSocket时出错: {self._safe_str(close_e)}")
|
||||
|
||||
# 退出Token刷新循环,让main循环重新建立连接
|
||||
# 后台任务(心跳、清理等)继续运行
|
||||
logger.info(f"【{self.cookie_id}】Token刷新完成,WebSocket将使用新Token重新连接")
|
||||
break
|
||||
else:
|
||||
# 根据上一次刷新状态决定日志级别(冷却/已重启为正常情况)
|
||||
@ -5006,6 +5331,15 @@ class XianyuLive:
|
||||
except Exception as pw_clean_e:
|
||||
logger.warning(f"【{self.cookie_id}】清理Playwright缓存时出错: {pw_clean_e}")
|
||||
|
||||
# 清理过期的日志文件(每5分钟检查一次,保留7天)
|
||||
try:
|
||||
cleaned_logs = await self._cleanup_old_logs(retention_days=7)
|
||||
await asyncio.sleep(0) # 让出控制权,允许检查取消信号
|
||||
except asyncio.CancelledError:
|
||||
raise
|
||||
except Exception as log_clean_e:
|
||||
logger.warning(f"【{self.cookie_id}】清理日志文件时出错: {log_clean_e}")
|
||||
|
||||
# 清理数据库历史数据(每天一次,保留90天数据)
|
||||
# 为避免所有实例同时执行,只让第一个实例执行
|
||||
try:
|
||||
@ -5144,6 +5478,35 @@ class XianyuLive:
|
||||
if success:
|
||||
self.last_cookie_refresh_time = current_time
|
||||
logger.info(f"【{self.cookie_id}】Cookie刷新任务完成,心跳已恢复")
|
||||
|
||||
# 刷新成功后,验证Cookie有效性
|
||||
logger.info(f"【{self.cookie_id}】开始验证刷新后的Cookie有效性...")
|
||||
try:
|
||||
validation_result = await self._verify_cookie_validity()
|
||||
|
||||
if not validation_result['valid']:
|
||||
logger.warning(f"【{self.cookie_id}】❌ Cookie验证失败: {validation_result['details']}")
|
||||
logger.warning(f"【{self.cookie_id}】检测到Cookie可能无法用于关键API,尝试通过密码登录重新获取...")
|
||||
|
||||
# 触发密码登录刷新
|
||||
password_refresh_success = await self._try_password_login_refresh("Cookie验证失败(关键API不可用)")
|
||||
|
||||
if password_refresh_success:
|
||||
logger.info(f"【{self.cookie_id}】✅ 密码登录刷新成功,Cookie已更新")
|
||||
else:
|
||||
logger.warning(f"【{self.cookie_id}】⚠️ 密码登录刷新失败,Cookie可能仍然无效")
|
||||
# 发送通知
|
||||
await self.send_token_refresh_notification(
|
||||
f"Cookie验证失败且密码登录刷新也失败\n验证详情: {validation_result['details']}",
|
||||
"cookie_validation_failed"
|
||||
)
|
||||
else:
|
||||
logger.info(f"【{self.cookie_id}】✅ Cookie验证通过: {validation_result['details']}")
|
||||
|
||||
except Exception as verify_e:
|
||||
logger.error(f"【{self.cookie_id}】Cookie验证过程异常: {self._safe_str(verify_e)}")
|
||||
import traceback
|
||||
logger.error(f"【{self.cookie_id}】详细堆栈:\n{traceback.format_exc()}")
|
||||
else:
|
||||
logger.warning(f"【{self.cookie_id}】Cookie刷新任务失败")
|
||||
# 即使失败也要更新时间,避免频繁重试
|
||||
@ -6150,13 +6513,16 @@ class XianyuLive:
|
||||
|
||||
# 兜底:直接在此处触发实例重启,避免外层协程在返回后被取消导致未重启
|
||||
try:
|
||||
# 标记“刷新流程内已触发重启”,供外层去重
|
||||
# 标记"刷新流程内已触发重启",供外层去重
|
||||
self.restarted_in_browser_refresh = True
|
||||
|
||||
logger.info(f"【{self.cookie_id}】Cookie刷新成功,准备重启实例...(via _refresh_cookies_via_browser)")
|
||||
await self._restart_instance()
|
||||
logger.info(f"【{self.cookie_id}】实例重启完成(via _refresh_cookies_via_browser)")
|
||||
|
||||
|
||||
# ⚠️ _restart_instance() 已触发重启,当前任务即将被取消
|
||||
# 不要等待或执行耗时操作
|
||||
logger.info(f"【{self.cookie_id}】重启请求已触发(via _refresh_cookies_via_browser)")
|
||||
|
||||
# 标记重启标志(无需主动关闭WS,重启由管理器处理)
|
||||
self.connection_restart_flag = True
|
||||
except Exception as e:
|
||||
@ -6587,19 +6953,45 @@ class XianyuLive:
|
||||
message_id = f"{chat_id}_{send_message}_{int(time.time() * 1000)}"
|
||||
|
||||
async with self.processed_message_ids_lock:
|
||||
current_time = time.time()
|
||||
|
||||
# 检查消息是否已处理且未过期
|
||||
if message_id in self.processed_message_ids:
|
||||
logger.warning(f"【{self.cookie_id}】消息ID {message_id[:50]}... 已处理过,跳过重复处理")
|
||||
return
|
||||
# 标记消息ID为已处理
|
||||
self.processed_message_ids.add(message_id)
|
||||
# 如果集合过大,清理最旧的一半(简单策略)
|
||||
last_process_time = self.processed_message_ids[message_id]
|
||||
time_elapsed = current_time - last_process_time
|
||||
|
||||
# 如果消息处理时间未超过1小时,跳过
|
||||
if time_elapsed < self.message_expire_time:
|
||||
remaining_time = int(self.message_expire_time - time_elapsed)
|
||||
logger.warning(f"【{self.cookie_id}】消息ID {message_id[:50]}... 已处理过,距离可重复回复还需 {remaining_time} 秒")
|
||||
return
|
||||
else:
|
||||
# 超过1小时,可以重新处理
|
||||
logger.info(f"【{self.cookie_id}】消息ID {message_id[:50]}... 已超过 {int(time_elapsed/60)} 分钟,允许重新回复")
|
||||
|
||||
# 标记消息ID为已处理(更新或添加时间戳)
|
||||
self.processed_message_ids[message_id] = current_time
|
||||
|
||||
# 定期清理过期的消息ID
|
||||
if len(self.processed_message_ids) > self.processed_message_ids_max_size:
|
||||
# 转换为列表,删除前一半
|
||||
ids_list = list(self.processed_message_ids)
|
||||
remove_count = len(ids_list) // 2
|
||||
for i in range(remove_count):
|
||||
self.processed_message_ids.discard(ids_list[i])
|
||||
logger.info(f"【{self.cookie_id}】消息ID去重集合过大,已清理 {remove_count} 个旧记录")
|
||||
# 清理超过1小时的旧记录
|
||||
expired_ids = [
|
||||
msg_id for msg_id, timestamp in self.processed_message_ids.items()
|
||||
if current_time - timestamp > self.message_expire_time
|
||||
]
|
||||
|
||||
for msg_id in expired_ids:
|
||||
del self.processed_message_ids[msg_id]
|
||||
|
||||
logger.info(f"【{self.cookie_id}】已清理 {len(expired_ids)} 个过期消息ID")
|
||||
|
||||
# 如果清理后仍然过大,删除最旧的一半
|
||||
if len(self.processed_message_ids) > self.processed_message_ids_max_size:
|
||||
sorted_ids = sorted(self.processed_message_ids.items(), key=lambda x: x[1])
|
||||
remove_count = len(sorted_ids) // 2
|
||||
for msg_id, _ in sorted_ids[:remove_count]:
|
||||
del self.processed_message_ids[msg_id]
|
||||
logger.info(f"【{self.cookie_id}】消息ID去重字典过大,已清理 {remove_count} 个最旧记录")
|
||||
|
||||
async with self.message_debounce_lock:
|
||||
# 如果该chat_id已有防抖任务,取消它
|
||||
@ -7047,8 +7439,14 @@ class XianyuLive:
|
||||
logger.info(f"[{msg_time}] 【收到】用户: {send_user_name} (ID: {send_user_id}), 商品({item_id}): {send_message}")
|
||||
|
||||
# 🔔 立即发送消息通知(独立于自动回复功能)
|
||||
# 检查是否为群组消息,如果是群组消息则跳过通知
|
||||
try:
|
||||
await self.send_notification(send_user_name, send_user_id, send_message, item_id, chat_id)
|
||||
session_type = message_10.get("sessionType", "1") # 默认为个人消息类型
|
||||
if session_type == "30":
|
||||
logger.info(f"📱 检测到群组消息(sessionType=30),跳过消息通知")
|
||||
else:
|
||||
# 只对个人消息发送通知
|
||||
await self.send_notification(send_user_name, send_user_id, send_message, item_id, chat_id)
|
||||
except Exception as notify_error:
|
||||
logger.error(f"📱 发送消息通知失败: {self._safe_str(notify_error)}")
|
||||
|
||||
@ -7128,6 +7526,9 @@ class XianyuLive:
|
||||
elif send_message == '[你已发货]':
|
||||
logger.info(f'[{msg_time}] 【{self.cookie_id}】发货确认消息不处理')
|
||||
return
|
||||
elif send_message == '已发货':
|
||||
logger.info(f'[{msg_time}] 【{self.cookie_id}】发货确认消息不处理')
|
||||
return
|
||||
# 【重要】检查是否为自动发货触发消息 - 即使在人工接入暂停期间也要处理
|
||||
elif self._is_auto_delivery_trigger(send_message):
|
||||
logger.info(f'[{msg_time}] 【{self.cookie_id}】检测到自动发货触发消息,即使在暂停期间也继续处理: {send_message}')
|
||||
@ -7376,10 +7777,51 @@ class XianyuLive:
|
||||
# 检查是否超过最大失败次数
|
||||
if self.connection_failures >= self.max_connection_failures:
|
||||
self._set_connection_state(ConnectionState.FAILED, f"连续失败{self.max_connection_failures}次")
|
||||
logger.warning(f"【{self.cookie_id}】连续失败{self.max_connection_failures}次,尝试通过密码登录刷新Cookie...")
|
||||
|
||||
try:
|
||||
# 调用统一的密码登录刷新方法
|
||||
refresh_success = await self._try_password_login_refresh(f"连续失败{self.max_connection_failures}次")
|
||||
|
||||
if refresh_success:
|
||||
logger.info(f"【{self.cookie_id}】✅ 密码登录刷新成功,将重置失败计数并继续重连")
|
||||
# 重置失败计数,因为已经刷新了Cookie
|
||||
self.connection_failures = 0
|
||||
# 更新连接状态
|
||||
self._set_connection_state(ConnectionState.RECONNECTING, "Cookie已刷新,准备重连")
|
||||
# 短暂等待后继续重连循环
|
||||
await asyncio.sleep(2)
|
||||
continue
|
||||
else:
|
||||
logger.warning(f"【{self.cookie_id}】❌ 密码登录刷新失败,将重启实例...")
|
||||
except Exception as refresh_e:
|
||||
logger.error(f"【{self.cookie_id}】密码登录刷新过程异常: {self._safe_str(refresh_e)}")
|
||||
logger.warning(f"【{self.cookie_id}】将重启实例...")
|
||||
|
||||
# 如果密码登录刷新失败或异常,则重启实例
|
||||
logger.error(f"【{self.cookie_id}】准备重启实例...")
|
||||
self.connection_failures = 0 # 重置失败计数
|
||||
await self._restart_instance() # 重启实例
|
||||
return # 重启后退出当前连接循环
|
||||
|
||||
# 先清理后台任务,避免与重启过程冲突
|
||||
logger.info(f"【{self.cookie_id}】重启前先清理后台任务...")
|
||||
try:
|
||||
await asyncio.wait_for(
|
||||
self._cancel_background_tasks(),
|
||||
timeout=8.0 # 给足够时间让任务响应
|
||||
)
|
||||
logger.info(f"【{self.cookie_id}】后台任务已清理完成")
|
||||
except asyncio.TimeoutError:
|
||||
logger.warning(f"【{self.cookie_id}】后台任务清理超时,强制继续重启")
|
||||
except Exception as cleanup_e:
|
||||
logger.error(f"【{self.cookie_id}】后台任务清理失败: {self._safe_str(cleanup_e)}")
|
||||
|
||||
# 触发重启(不等待完成)
|
||||
await self._restart_instance()
|
||||
|
||||
# ⚠️ 重要:_restart_instance() 已触发重启,0.5秒后当前任务会被取消
|
||||
# 不要在这里等待或执行其他操作,让任务自然退出
|
||||
logger.info(f"【{self.cookie_id}】重启请求已触发,主程序即将退出,新实例将自动启动")
|
||||
return # 退出当前连接循环,等待被取消
|
||||
|
||||
# 计算重试延迟
|
||||
retry_delay = self._calculate_retry_delay(error_msg)
|
||||
@ -7486,17 +7928,34 @@ class XianyuLive:
|
||||
logger.info(f"【{self.cookie_id}】程序退出,清空当前token")
|
||||
self.current_token = None
|
||||
|
||||
# 使用统一的任务清理方法,添加超时保护
|
||||
try:
|
||||
await asyncio.wait_for(
|
||||
self._cancel_background_tasks(),
|
||||
timeout=10.0
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
logger.error(f"【{self.cookie_id}】程序退出时任务取消超时,强制继续")
|
||||
except Exception as e:
|
||||
logger.error(f"【{self.cookie_id}】程序退出时任务取消失败: {self._safe_str(e)}")
|
||||
finally:
|
||||
# 检查是否还有未取消的后台任务,如果有才执行清理
|
||||
has_pending_tasks = any([
|
||||
self.heartbeat_task and not self.heartbeat_task.done(),
|
||||
self.token_refresh_task and not self.token_refresh_task.done(),
|
||||
self.cleanup_task and not self.cleanup_task.done(),
|
||||
self.cookie_refresh_task and not self.cookie_refresh_task.done()
|
||||
])
|
||||
|
||||
if has_pending_tasks:
|
||||
logger.info(f"【{self.cookie_id}】检测到未完成的后台任务,执行清理...")
|
||||
# 使用统一的任务清理方法,添加超时保护
|
||||
try:
|
||||
await asyncio.wait_for(
|
||||
self._cancel_background_tasks(),
|
||||
timeout=10.0
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
logger.error(f"【{self.cookie_id}】程序退出时任务取消超时,强制继续")
|
||||
except Exception as e:
|
||||
logger.error(f"【{self.cookie_id}】程序退出时任务取消失败: {self._safe_str(e)}")
|
||||
finally:
|
||||
# 确保任务引用被重置
|
||||
self.heartbeat_task = None
|
||||
self.token_refresh_task = None
|
||||
self.cleanup_task = None
|
||||
self.cookie_refresh_task = None
|
||||
else:
|
||||
logger.info(f"【{self.cookie_id}】所有后台任务已清理完成,跳过重复清理")
|
||||
# 确保任务引用被重置
|
||||
self.heartbeat_task = None
|
||||
self.token_refresh_task = None
|
||||
@ -7787,7 +8246,8 @@ class XianyuLive:
|
||||
logger.warning(f"【{self.cookie_id}】获取图片尺寸失败,使用默认尺寸: {e}")
|
||||
else:
|
||||
logger.error(f"【{self.cookie_id}】图片上传失败: {local_image_path}")
|
||||
raise Exception(f"图片上传失败: {local_image_path}")
|
||||
logger.error(f"【{self.cookie_id}】❌ Cookie可能已失效!请检查配置并更新Cookie")
|
||||
raise Exception(f"图片上传失败(Cookie可能已失效): {local_image_path}")
|
||||
else:
|
||||
logger.error(f"【{self.cookie_id}】本地图片文件不存在: {local_image_path}")
|
||||
raise Exception(f"本地图片文件不存在: {local_image_path}")
|
||||
@ -7898,6 +8358,7 @@ class XianyuLive:
|
||||
return True
|
||||
else:
|
||||
logger.error(f"【{self.cookie_id}】图片上传失败: {image_path}")
|
||||
logger.error(f"【{self.cookie_id}】❌ Cookie可能已失效!请检查配置并更新Cookie")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
|
||||
@ -309,9 +309,9 @@ class AIReplyEngine:
|
||||
# 使用锁确保同一chat_id的消息串行处理
|
||||
with chat_lock:
|
||||
# 获取最近时间窗口内的所有用户消息
|
||||
# 如果 skip_wait=True(外部防抖),查询窗口为8秒(3秒防抖 + 5秒缓冲)
|
||||
# 如果 skip_wait=True(外部防抖),查询窗口为6秒(1秒防抖 + 5秒缓冲)
|
||||
# 如果 skip_wait=False(内部等待),查询窗口为25秒(10秒等待 + 10秒消息间隔 + 5秒缓冲)
|
||||
query_seconds = 8 if skip_wait else 25
|
||||
query_seconds = 6 if skip_wait else 25
|
||||
recent_messages = self._get_recent_user_messages(chat_id, cookie_id, seconds=query_seconds)
|
||||
logger.info(f"【{cookie_id}】最近{query_seconds}秒内的消息: {[msg['content'][:20] for msg in recent_messages]}")
|
||||
|
||||
|
||||
@ -91,7 +91,7 @@ config = Config()
|
||||
# 导出常用配置项
|
||||
COOKIES_STR = config.get('COOKIES.value', '')
|
||||
COOKIES_LAST_UPDATE = config.get('COOKIES.last_update_time', '')
|
||||
WEBSOCKET_URL = config.get('WEBSOCKET_URL', '://wss-goofish.dingtalk.com/')
|
||||
WEBSOCKET_URL = config.get('WEBSOCKET_URL', 'wss://wss-goofish.dingtalk.com/')
|
||||
HEARTBEAT_INTERVAL = config.get('HEARTBEAT_INTERVAL', 15)
|
||||
HEARTBEAT_TIMEOUT = config.get('HEARTBEAT_TIMEOUT', 30)
|
||||
TOKEN_REFRESH_INTERVAL = config.get('TOKEN_REFRESH_INTERVAL', 72000)
|
||||
|
||||
289
reply_server.py
289
reply_server.py
@ -484,8 +484,44 @@ async def admin_page():
|
||||
index_path = os.path.join(static_dir, 'index.html')
|
||||
if not os.path.exists(index_path):
|
||||
return HTMLResponse('<h3>No front-end found</h3>')
|
||||
with open(index_path, 'r', encoding='utf-8') as f:
|
||||
return HTMLResponse(f.read())
|
||||
|
||||
# 获取静态文件的修改时间作为版本号,解决浏览器缓存问题
|
||||
def get_file_version(file_path, default='1.0.0'):
|
||||
"""获取文件的版本号(基于修改时间)"""
|
||||
if os.path.exists(file_path):
|
||||
try:
|
||||
mtime = os.path.getmtime(file_path)
|
||||
return str(int(mtime))
|
||||
except Exception as e:
|
||||
logger.warning(f"获取文件 {file_path} 修改时间失败: {e}")
|
||||
return default
|
||||
|
||||
app_js_path = os.path.join(static_dir, 'js', 'app.js')
|
||||
app_css_path = os.path.join(static_dir, 'css', 'app.css')
|
||||
|
||||
js_version = get_file_version(app_js_path, '2.2.0')
|
||||
css_version = get_file_version(app_css_path, '1.0.0')
|
||||
|
||||
try:
|
||||
with open(index_path, 'r', encoding='utf-8') as f:
|
||||
html_content = f.read()
|
||||
|
||||
# 替换 app.js 的版本号参数
|
||||
js_pattern = r'/static/js/app\.js\?v=[^"\'\s>]+'
|
||||
js_new_url = f'/static/js/app.js?v={js_version}'
|
||||
if re.search(js_pattern, html_content):
|
||||
html_content = re.sub(js_pattern, js_new_url, html_content)
|
||||
logger.debug(f"已替换 app.js 版本号: {js_version}")
|
||||
|
||||
# 为 app.css 添加或更新版本号参数
|
||||
css_pattern = r'/static/css/app\.css(\?v=[^"\'\s>]+)?'
|
||||
css_new_url = f'/static/css/app.css?v={css_version}'
|
||||
html_content = re.sub(css_pattern, css_new_url, html_content)
|
||||
|
||||
return HTMLResponse(html_content)
|
||||
except Exception as e:
|
||||
logger.error(f"读取或处理 index.html 失败: {e}")
|
||||
return HTMLResponse('<h3>Error loading page</h3>')
|
||||
|
||||
|
||||
|
||||
@ -1316,16 +1352,90 @@ async def _execute_password_login(session_id: str, account_id: str, account: str
|
||||
# 更新会话信息
|
||||
password_login_sessions[session_id]['slider_instance'] = slider_instance
|
||||
|
||||
# 定义通知回调函数,用于检测到人脸认证时返回验证链接(同步函数)
|
||||
def notification_callback(message: str, screenshot_path: str = None, verification_url: str = None):
|
||||
"""人脸认证通知回调(同步)"""
|
||||
# 定义通知回调函数,用于检测到人脸认证时返回验证链接或截图(同步函数)
|
||||
def notification_callback(message: str, screenshot_path: str = None, verification_url: str = None, screenshot_path_new: str = None):
|
||||
"""人脸认证通知回调(同步)
|
||||
|
||||
Args:
|
||||
message: 通知消息
|
||||
screenshot_path: 旧版截图路径(兼容参数)
|
||||
verification_url: 验证链接
|
||||
screenshot_path_new: 新版截图路径(新参数,优先使用)
|
||||
"""
|
||||
try:
|
||||
# 优先使用验证链接
|
||||
if verification_url:
|
||||
# 更新会话状态,保存验证链接
|
||||
# 优先使用新的截图路径参数
|
||||
actual_screenshot_path = screenshot_path_new if screenshot_path_new else screenshot_path
|
||||
|
||||
# 优先使用截图路径,如果没有截图则使用验证链接
|
||||
if actual_screenshot_path and os.path.exists(actual_screenshot_path):
|
||||
# 更新会话状态,保存截图路径
|
||||
password_login_sessions[session_id]['status'] = 'verification_required'
|
||||
password_login_sessions[session_id]['screenshot_path'] = actual_screenshot_path
|
||||
password_login_sessions[session_id]['verification_url'] = None
|
||||
password_login_sessions[session_id]['qr_code_url'] = None
|
||||
log_with_user('info', f"人脸认证截图已保存: {session_id}, 路径: {actual_screenshot_path}", current_user)
|
||||
|
||||
# 发送通知到用户配置的渠道
|
||||
def send_face_verification_notification():
|
||||
"""在后台线程中发送人脸验证通知"""
|
||||
try:
|
||||
from XianyuAutoAsync import XianyuLive
|
||||
log_with_user('info', f"开始尝试发送人脸验证通知: {account_id}", current_user)
|
||||
|
||||
# 尝试获取XianyuLive实例(如果账号已经存在)
|
||||
live_instance = XianyuLive.get_instance(account_id)
|
||||
|
||||
if live_instance:
|
||||
log_with_user('info', f"找到账号实例,准备发送通知: {account_id}", current_user)
|
||||
# 创建新的事件循环来运行异步通知
|
||||
new_loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(new_loop)
|
||||
try:
|
||||
new_loop.run_until_complete(
|
||||
live_instance.send_token_refresh_notification(
|
||||
error_message=message,
|
||||
notification_type="face_verification",
|
||||
verification_url=None,
|
||||
attachment_path=actual_screenshot_path
|
||||
)
|
||||
)
|
||||
log_with_user('info', f"✅ 已发送人脸验证通知: {account_id}", current_user)
|
||||
except Exception as notify_err:
|
||||
log_with_user('error', f"发送人脸验证通知失败: {str(notify_err)}", current_user)
|
||||
import traceback
|
||||
log_with_user('error', f"通知错误详情: {traceback.format_exc()}", current_user)
|
||||
finally:
|
||||
new_loop.close()
|
||||
else:
|
||||
# 如果账号实例不存在,记录警告并尝试从数据库获取通知配置
|
||||
log_with_user('warning', f"账号实例不存在: {account_id},尝试从数据库获取通知配置", current_user)
|
||||
try:
|
||||
# 尝试从数据库获取通知配置
|
||||
notifications = db_manager.get_account_notifications(account_id)
|
||||
if notifications:
|
||||
log_with_user('info', f"找到 {len(notifications)} 个通知配置,但需要账号实例才能发送", current_user)
|
||||
log_with_user('warning', f"账号实例不存在,无法发送通知: {account_id}。请确保账号已登录并运行中。", current_user)
|
||||
else:
|
||||
log_with_user('warning', f"账号 {account_id} 未配置通知渠道", current_user)
|
||||
except Exception as db_err:
|
||||
log_with_user('error', f"获取通知配置失败: {str(db_err)}", current_user)
|
||||
except Exception as notify_err:
|
||||
log_with_user('error', f"发送人脸验证通知时出错: {str(notify_err)}", current_user)
|
||||
import traceback
|
||||
log_with_user('error', f"通知错误详情: {traceback.format_exc()}", current_user)
|
||||
|
||||
# 在后台线程中发送通知,避免阻塞登录流程
|
||||
import threading
|
||||
notification_thread = threading.Thread(target=send_face_verification_notification)
|
||||
notification_thread.daemon = True
|
||||
notification_thread.start()
|
||||
log_with_user('info', f"已启动人脸验证通知发送线程: {account_id}", current_user)
|
||||
elif verification_url:
|
||||
# 如果没有截图,使用验证链接(兼容旧版本)
|
||||
password_login_sessions[session_id]['status'] = 'verification_required'
|
||||
password_login_sessions[session_id]['verification_url'] = verification_url
|
||||
password_login_sessions[session_id]['qr_code_url'] = None # 不再使用截图
|
||||
password_login_sessions[session_id]['screenshot_path'] = None
|
||||
password_login_sessions[session_id]['qr_code_url'] = None
|
||||
log_with_user('info', f"人脸认证验证链接已保存: {session_id}, URL: {verification_url}", current_user)
|
||||
|
||||
# 发送通知到用户配置的渠道
|
||||
@ -1382,9 +1492,6 @@ async def _execute_password_login(session_id: str, account_id: str, account: str
|
||||
notification_thread.daemon = True
|
||||
notification_thread.start()
|
||||
log_with_user('info', f"已启动人脸验证通知发送线程: {account_id}", current_user)
|
||||
elif screenshot_path and os.path.exists(screenshot_path):
|
||||
# 兼容旧版本:如果有截图路径,仍然处理(但不再使用)
|
||||
log_with_user('info', f"收到截图路径(已弃用): {session_id}", current_user)
|
||||
except Exception as e:
|
||||
log_with_user('error', f"处理人脸认证通知失败: {str(e)}", current_user)
|
||||
|
||||
@ -1601,6 +1708,7 @@ async def password_login(
|
||||
'show_browser': show_browser,
|
||||
'status': 'processing',
|
||||
'verification_url': None,
|
||||
'screenshot_path': None,
|
||||
'qr_code_url': None,
|
||||
'slider_instance': None,
|
||||
'task': None,
|
||||
@ -1658,14 +1766,29 @@ async def check_password_login_status(
|
||||
|
||||
if status == 'verification_required':
|
||||
# 需要人脸认证
|
||||
screenshot_path = session.get('screenshot_path')
|
||||
verification_url = session.get('verification_url')
|
||||
return {
|
||||
'status': 'verification_required',
|
||||
'verification_url': session.get('verification_url'),
|
||||
'verification_url': verification_url,
|
||||
'screenshot_path': screenshot_path,
|
||||
'qr_code_url': session.get('qr_code_url'), # 保留兼容性
|
||||
'message': '需要人脸验证,请点击验证链接'
|
||||
'message': '需要人脸验证,请查看验证截图' if screenshot_path else '需要人脸验证,请点击验证链接'
|
||||
}
|
||||
elif status == 'success':
|
||||
# 登录成功
|
||||
# 删除截图(如果存在)
|
||||
screenshot_path = session.get('screenshot_path')
|
||||
if screenshot_path:
|
||||
try:
|
||||
from utils.image_utils import image_manager
|
||||
if image_manager.delete_image(screenshot_path):
|
||||
log_with_user('info', f"验证成功后已删除截图: {screenshot_path}", current_user)
|
||||
else:
|
||||
log_with_user('warning', f"删除截图失败: {screenshot_path}", current_user)
|
||||
except Exception as e:
|
||||
log_with_user('error', f"删除截图时出错: {str(e)}", current_user)
|
||||
|
||||
result = {
|
||||
'status': 'success',
|
||||
'message': f'账号 {session["account_id"]} 登录成功',
|
||||
@ -1678,6 +1801,18 @@ async def check_password_login_status(
|
||||
return result
|
||||
elif status == 'failed':
|
||||
# 登录失败
|
||||
# 删除截图(如果存在)
|
||||
screenshot_path = session.get('screenshot_path')
|
||||
if screenshot_path:
|
||||
try:
|
||||
from utils.image_utils import image_manager
|
||||
if image_manager.delete_image(screenshot_path):
|
||||
log_with_user('info', f"验证失败后已删除截图: {screenshot_path}", current_user)
|
||||
else:
|
||||
log_with_user('warning', f"删除截图失败: {screenshot_path}", current_user)
|
||||
except Exception as e:
|
||||
log_with_user('error', f"删除截图时出错: {str(e)}", current_user)
|
||||
|
||||
error_msg = session.get('error', '登录失败')
|
||||
log_with_user('info', f"返回登录失败状态: {session_id}, 错误消息: {error_msg}", current_user) # 添加日志
|
||||
result = {
|
||||
@ -1700,6 +1835,132 @@ async def check_password_login_status(
|
||||
return {'status': 'error', 'message': str(e)}
|
||||
|
||||
|
||||
# ========================= 人脸验证截图相关接口 =========================
|
||||
|
||||
@app.get("/face-verification/screenshot/{account_id}")
|
||||
async def get_account_face_verification_screenshot(
|
||||
account_id: str,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||
):
|
||||
"""获取指定账号的人脸验证截图"""
|
||||
try:
|
||||
import glob
|
||||
from datetime import datetime
|
||||
|
||||
# 检查账号是否属于当前用户
|
||||
user_id = current_user['user_id']
|
||||
username = current_user['username']
|
||||
|
||||
# 如果是管理员,允许访问所有账号
|
||||
is_admin = username == 'admin'
|
||||
|
||||
if not is_admin:
|
||||
cookie_info = db_manager.get_cookie_details(account_id)
|
||||
if not cookie_info:
|
||||
log_with_user('warning', f"账号 {account_id} 不存在", current_user)
|
||||
return {
|
||||
'success': False,
|
||||
'message': '账号不存在'
|
||||
}
|
||||
|
||||
cookie_user_id = cookie_info.get('user_id')
|
||||
if cookie_user_id != user_id:
|
||||
log_with_user('warning', f"用户 {user_id} 尝试访问账号 {account_id}(归属用户: {cookie_user_id})", current_user)
|
||||
return {
|
||||
'success': False,
|
||||
'message': '无权访问该账号'
|
||||
}
|
||||
|
||||
# 获取该账号的验证截图
|
||||
screenshots_dir = os.path.join(static_dir, 'uploads', 'images')
|
||||
pattern = os.path.join(screenshots_dir, f'face_verify_{account_id}_*.jpg')
|
||||
screenshot_files = glob.glob(pattern)
|
||||
|
||||
log_with_user('debug', f"查找截图: {pattern}, 找到 {len(screenshot_files)} 个文件", current_user)
|
||||
|
||||
if not screenshot_files:
|
||||
log_with_user('warning', f"账号 {account_id} 没有找到验证截图", current_user)
|
||||
return {
|
||||
'success': False,
|
||||
'message': '未找到验证截图'
|
||||
}
|
||||
|
||||
# 获取最新的截图
|
||||
latest_file = max(screenshot_files, key=os.path.getmtime)
|
||||
filename = os.path.basename(latest_file)
|
||||
stat = os.stat(latest_file)
|
||||
|
||||
screenshot_info = {
|
||||
'filename': filename,
|
||||
'account_id': account_id,
|
||||
'path': f'/static/uploads/images/{filename}',
|
||||
'size': stat.st_size,
|
||||
'created_time': stat.st_ctime,
|
||||
'created_time_str': datetime.fromtimestamp(stat.st_ctime).strftime('%Y-%m-%d %H:%M:%S')
|
||||
}
|
||||
|
||||
log_with_user('info', f"获取账号 {account_id} 的验证截图", current_user)
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'screenshot': screenshot_info
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
log_with_user('error', f"获取验证截图失败: {str(e)}", current_user)
|
||||
return {
|
||||
'success': False,
|
||||
'message': str(e)
|
||||
}
|
||||
|
||||
|
||||
@app.delete("/face-verification/screenshot/{account_id}")
|
||||
async def delete_account_face_verification_screenshot(
|
||||
account_id: str,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||
):
|
||||
"""删除指定账号的人脸验证截图"""
|
||||
try:
|
||||
import glob
|
||||
|
||||
# 检查账号是否属于当前用户
|
||||
user_id = current_user['user_id']
|
||||
cookie_info = db_manager.get_cookie_details(account_id)
|
||||
if not cookie_info or cookie_info.get('user_id') != user_id:
|
||||
return {
|
||||
'success': False,
|
||||
'message': '无权访问该账号'
|
||||
}
|
||||
|
||||
# 删除该账号的所有验证截图
|
||||
screenshots_dir = os.path.join(static_dir, 'uploads', 'images')
|
||||
pattern = os.path.join(screenshots_dir, f'face_verify_{account_id}_*.jpg')
|
||||
screenshot_files = glob.glob(pattern)
|
||||
|
||||
deleted_count = 0
|
||||
for file_path in screenshot_files:
|
||||
try:
|
||||
if os.path.exists(file_path):
|
||||
os.remove(file_path)
|
||||
deleted_count += 1
|
||||
log_with_user('info', f"删除账号 {account_id} 的验证截图: {os.path.basename(file_path)}", current_user)
|
||||
except Exception as e:
|
||||
log_with_user('error', f"删除截图失败 {file_path}: {str(e)}", current_user)
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'message': f'已删除 {deleted_count} 个验证截图',
|
||||
'deleted_count': deleted_count
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
log_with_user('error', f"删除验证截图失败: {str(e)}", current_user)
|
||||
return {
|
||||
'success': False,
|
||||
'message': str(e)
|
||||
}
|
||||
|
||||
|
||||
# ========================= 扫码登录相关接口 =========================
|
||||
|
||||
@app.post("/qr-login/generate")
|
||||
|
||||
@ -78,7 +78,7 @@ class SecureConfirm:
|
||||
try:
|
||||
from db_manager import db_manager
|
||||
# 更新数据库中的cookies
|
||||
db_manager.update_cookie_value(self.cookie_id, self.cookies_str)
|
||||
db_manager.update_cookie_account_info(self.cookie_id, cookie_value=self.cookies_str)
|
||||
logger.debug(f"【{self.cookie_id}】已更新数据库中的Cookie")
|
||||
except Exception as e:
|
||||
logger.error(f"【{self.cookie_id}】更新数据库Cookie失败: {self._safe_str(e)}")
|
||||
|
||||
@ -3705,5 +3705,14 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部赞助商信息 -->
|
||||
<footer class="text-center py-3 mt-5" style="background-color: #f8f9fa; border-top: 1px solid #dee2e6;">
|
||||
<div class="container">
|
||||
<p class="mb-0 text-muted">
|
||||
<small>赞助商:划算云服务器 <a href="https://www.hsykj.com" target="_blank" class="text-decoration-none">www.hsykj.com</a></small>
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
199
static/js/app.js
199
static/js/app.js
@ -1307,6 +1307,9 @@ async function loadCookies() {
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
<div class="btn-group" role="group">
|
||||
<button class="btn btn-sm btn-outline-secondary" onclick="showFaceVerification('${cookie.id}')" title="人脸验证">
|
||||
<i class="bi bi-shield-check"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-primary" onclick="editCookieInline('${cookie.id}', '${cookie.value}')" title="修改Cookie" ${!isEnabled ? 'disabled' : ''}>
|
||||
<i class="bi bi-pencil"></i>
|
||||
</button>
|
||||
@ -1319,6 +1322,7 @@ async function loadCookies() {
|
||||
<button class="btn btn-sm btn-outline-info" onclick="copyCookie('${cookie.id}', '${cookie.value}')" title="复制Cookie">
|
||||
<i class="bi bi-clipboard"></i>
|
||||
</button>
|
||||
|
||||
<button class="btn btn-sm btn-outline-danger" onclick="delCookie('${cookie.id}')" title="删除账号">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
@ -7182,8 +7186,8 @@ async function checkPasswordLoginStatus() {
|
||||
// 处理中,继续等待
|
||||
break;
|
||||
case 'verification_required':
|
||||
// 需要人脸认证,显示验证链接
|
||||
showPasswordLoginQRCode(data.verification_url || data.qr_code_url);
|
||||
// 需要人脸认证,显示验证截图或链接
|
||||
showPasswordLoginQRCode(data.screenshot_path || data.verification_url || data.qr_code_url, data.screenshot_path);
|
||||
// 继续监控(人脸认证后需要继续等待登录完成)
|
||||
break;
|
||||
case 'success':
|
||||
@ -7226,8 +7230,8 @@ async function checkPasswordLoginStatus() {
|
||||
}
|
||||
}
|
||||
|
||||
// 显示账号密码登录验证链接(人脸认证)
|
||||
function showPasswordLoginQRCode(verificationUrl) {
|
||||
// 显示账号密码登录验证(人脸认证)
|
||||
function showPasswordLoginQRCode(verificationUrl, screenshotPath) {
|
||||
// 使用现有的二维码登录模态框
|
||||
let modal = document.getElementById('passwordLoginQRModal');
|
||||
if (!modal) {
|
||||
@ -7255,49 +7259,53 @@ function showPasswordLoginQRCode(verificationUrl) {
|
||||
qrContainer.style.display = 'none';
|
||||
}
|
||||
|
||||
// 隐藏二维码图片(不再使用)
|
||||
const qrImg = document.getElementById('passwordLoginQRImg');
|
||||
if (qrImg) {
|
||||
qrImg.style.display = 'none';
|
||||
}
|
||||
// 优先显示截图,如果没有截图则显示链接
|
||||
const screenshotImg = document.getElementById('passwordLoginScreenshotImg');
|
||||
const linkButton = document.getElementById('passwordLoginVerificationLink');
|
||||
const statusText = document.getElementById('passwordLoginQRStatusText');
|
||||
|
||||
// 显示验证链接按钮
|
||||
let linkButton = document.getElementById('passwordLoginVerificationLink');
|
||||
if (!linkButton) {
|
||||
// 如果按钮不存在,创建一个
|
||||
const linkContainer = document.createElement('div');
|
||||
linkContainer.id = 'passwordLoginLinkContainer';
|
||||
linkContainer.className = 'mt-4';
|
||||
linkContainer.innerHTML = `
|
||||
<a id="passwordLoginVerificationLink" href="#" target="_blank" class="btn btn-warning btn-lg">
|
||||
<i class="bi bi-shield-check me-2"></i>
|
||||
跳转闲鱼人脸验证
|
||||
</a>
|
||||
`;
|
||||
const modalBody = modal.querySelector('.modal-body');
|
||||
if (modalBody) {
|
||||
modalBody.appendChild(linkContainer);
|
||||
if (screenshotPath) {
|
||||
// 显示截图
|
||||
if (screenshotImg) {
|
||||
screenshotImg.src = `/${screenshotPath}?t=${new Date().getTime()}`;
|
||||
screenshotImg.style.display = 'block';
|
||||
}
|
||||
linkButton = document.getElementById('passwordLoginVerificationLink');
|
||||
}
|
||||
|
||||
// 更新按钮链接和显示状态
|
||||
if (linkButton) {
|
||||
if (verificationUrl) {
|
||||
linkButton.href = verificationUrl;
|
||||
linkButton.style.display = 'inline-block';
|
||||
} else {
|
||||
|
||||
// 隐藏链接按钮
|
||||
if (linkButton) {
|
||||
linkButton.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// 更新状态文本
|
||||
const statusText = document.getElementById('passwordLoginQRStatusText');
|
||||
if (statusText) {
|
||||
if (verificationUrl) {
|
||||
|
||||
// 更新状态文本
|
||||
if (statusText) {
|
||||
statusText.textContent = '需要闲鱼人脸验证,请使用手机闲鱼APP扫描下方二维码完成验证';
|
||||
}
|
||||
} else if (verificationUrl) {
|
||||
// 隐藏截图
|
||||
if (screenshotImg) {
|
||||
screenshotImg.style.display = 'none';
|
||||
}
|
||||
|
||||
// 显示链接按钮
|
||||
if (linkButton) {
|
||||
linkButton.href = verificationUrl;
|
||||
linkButton.style.display = 'inline-block';
|
||||
}
|
||||
|
||||
// 更新状态文本
|
||||
if (statusText) {
|
||||
statusText.textContent = '需要闲鱼验证,请点击下方按钮跳转到验证页面';
|
||||
} else {
|
||||
statusText.textContent = '需要闲鱼验证,请等待验证链接...';
|
||||
}
|
||||
} else {
|
||||
// 都没有,显示等待
|
||||
if (screenshotImg) {
|
||||
screenshotImg.style.display = 'none';
|
||||
}
|
||||
if (linkButton) {
|
||||
linkButton.style.display = 'none';
|
||||
}
|
||||
if (statusText) {
|
||||
statusText.textContent = '需要闲鱼验证,请等待验证信息...';
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -7315,9 +7323,28 @@ function createPasswordLoginQRModal() {
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body text-center">
|
||||
<p id="passwordLoginQRStatusText" class="text-muted mb-3">
|
||||
需要闲鱼人脸验证,请等待验证信息...
|
||||
</p>
|
||||
|
||||
<!-- 截图显示区域 -->
|
||||
<div id="passwordLoginScreenshotContainer" class="mb-3 d-flex justify-content-center">
|
||||
<img id="passwordLoginScreenshotImg" src="" alt="人脸验证二维码"
|
||||
class="img-fluid" style="display: none; max-width: 400px; height: auto; border: 2px solid #ddd; border-radius: 8px;">
|
||||
</div>
|
||||
|
||||
<!-- 验证链接按钮(回退方案) -->
|
||||
<div id="passwordLoginLinkContainer" class="mt-4">
|
||||
<a id="passwordLoginVerificationLink" href="#" target="_blank"
|
||||
class="btn btn-warning btn-lg" style="display: none;">
|
||||
<i class="bi bi-shield-check me-2"></i>
|
||||
跳转闲鱼人脸验证
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info mt-3">
|
||||
<i class="bi bi-info-circle me-2"></i>
|
||||
<small>请点击下方按钮跳转到验证页面,完成闲鱼人脸验证</small>
|
||||
<small>验证完成后,系统将自动检测并继续登录流程</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -11709,4 +11736,90 @@ async function checkCaptchaCompletion(modal, sessionId) {
|
||||
});
|
||||
}
|
||||
|
||||
// ========================= 人脸验证相关功能 =========================
|
||||
|
||||
// 显示人脸验证截图
|
||||
async function showFaceVerification(accountId) {
|
||||
try {
|
||||
toggleLoading(true);
|
||||
|
||||
// 获取该账号的验证截图
|
||||
const response = await fetch(`${apiBase}/face-verification/screenshot/${accountId}`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${authToken}`
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('获取验证截图失败');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
toggleLoading(false);
|
||||
|
||||
if (!data.success) {
|
||||
showToast(data.message || '未找到验证截图', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// 使用与密码登录相同的弹窗显示验证截图
|
||||
showAccountFaceVerificationModal(accountId, data.screenshot);
|
||||
|
||||
} catch (error) {
|
||||
toggleLoading(false);
|
||||
console.error('获取人脸验证截图失败:', error);
|
||||
showToast('获取验证截图失败: ' + error.message, 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
// 显示账号列表的人脸验证弹窗(使用与密码登录相同的样式)
|
||||
function showAccountFaceVerificationModal(accountId, screenshot) {
|
||||
// 复用密码登录的弹窗
|
||||
let modal = document.getElementById('passwordLoginQRModal');
|
||||
if (!modal) {
|
||||
createPasswordLoginQRModal();
|
||||
modal = document.getElementById('passwordLoginQRModal');
|
||||
}
|
||||
|
||||
// 更新模态框标题
|
||||
const modalTitle = document.getElementById('passwordLoginQRModalLabel');
|
||||
if (modalTitle) {
|
||||
modalTitle.innerHTML = `<i class="bi bi-shield-exclamation text-warning me-2"></i>人脸验证 - 账号 ${accountId}`;
|
||||
}
|
||||
|
||||
// 显示截图
|
||||
const screenshotImg = document.getElementById('passwordLoginScreenshotImg');
|
||||
const linkButton = document.getElementById('passwordLoginVerificationLink');
|
||||
const statusText = document.getElementById('passwordLoginQRStatusText');
|
||||
|
||||
if (screenshotImg) {
|
||||
screenshotImg.src = `${screenshot.path}?t=${new Date().getTime()}`;
|
||||
screenshotImg.style.display = 'block';
|
||||
}
|
||||
|
||||
// 隐藏链接按钮
|
||||
if (linkButton) {
|
||||
linkButton.style.display = 'none';
|
||||
}
|
||||
|
||||
// 更新状态文本
|
||||
if (statusText) {
|
||||
statusText.innerHTML = `需要闲鱼人脸验证,请使用手机闲鱼APP扫描下方二维码完成验证<br><small class="text-muted">创建时间: ${screenshot.created_time_str}</small>`;
|
||||
}
|
||||
|
||||
// 获取或创建模态框实例
|
||||
let modalInstance = bootstrap.Modal.getInstance(modal);
|
||||
if (!modalInstance) {
|
||||
modalInstance = new bootstrap.Modal(modal);
|
||||
}
|
||||
|
||||
// 显示弹窗
|
||||
modalInstance.show();
|
||||
|
||||
// 注意:截图删除由后端在验证完成或失败时自动处理,前端不需要手动删除
|
||||
}
|
||||
|
||||
// 注:人脸验证弹窗已复用密码登录的 passwordLoginQRModal,不再需要单独的弹窗
|
||||
|
||||
|
||||
|
||||
@ -19,8 +19,10 @@
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.login-container {
|
||||
@ -712,5 +714,10 @@
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- 底部赞助商信息 -->
|
||||
<div class="text-center mt-4" style="color: rgba(255,255,255,0.8);">
|
||||
<small>赞助商:划算云服务器 <a href="https://www.hsykj.com" target="_blank" class="text-white text-decoration-none" style="font-weight: 500;">www.hsykj.com</a></small>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -11,9 +11,10 @@
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 10px;
|
||||
padding: 20px 10px;
|
||||
}
|
||||
.register-container {
|
||||
background: white;
|
||||
@ -565,5 +566,10 @@
|
||||
updateSendCodeButton();
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- 底部赞助商信息 -->
|
||||
<div class="text-center mt-4" style="color: rgba(255,255,255,0.8);">
|
||||
<small>赞助商:划算云服务器 <a href="https://www.hsykj.com" target="_blank" class="text-white text-decoration-none" style="font-weight: 500;">www.hsykj.com</a></small>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 173 KiB After Width: | Height: | Size: 214 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 173 KiB After Width: | Height: | Size: 164 KiB |
@ -167,6 +167,20 @@ class ImageUploader:
|
||||
def _parse_upload_response(self, response_text: str) -> Optional[str]:
|
||||
"""解析上传响应获取图片URL"""
|
||||
try:
|
||||
# 检查是否返回了登录页面(Cookie失效的标志)
|
||||
if '<!DOCTYPE html>' in response_text or '<html>' in response_text:
|
||||
if '闲鱼' in response_text and ('login' in response_text.lower() or 'mini-login' in response_text):
|
||||
logger.error("❌ 图片上传失败:Cookie已失效,返回了登录页面!请重新登录获取有效的Cookie")
|
||||
logger.error("💡 解决方法:")
|
||||
logger.error(" 1. 打开浏览器访问 https://www.goofish.com/")
|
||||
logger.error(" 2. 登录您的闲鱼账号")
|
||||
logger.error(" 3. 按F12打开开发者工具,在控制台输入: document.cookie")
|
||||
logger.error(" 4. 复制完整的Cookie字符串,更新配置文件中的Cookie")
|
||||
return None
|
||||
else:
|
||||
logger.error(f"收到HTML响应而非JSON,可能是Cookie失效: {response_text[:500]}")
|
||||
return None
|
||||
|
||||
# 尝试解析JSON响应
|
||||
response_data = json.loads(response_text)
|
||||
|
||||
@ -202,7 +216,7 @@ class ImageUploader:
|
||||
|
||||
except json.JSONDecodeError:
|
||||
# 如果不是JSON格式,尝试其他解析方式
|
||||
logger.error(f"响应不是有效的JSON格式: {response_text}")
|
||||
logger.error(f"响应不是有效的JSON格式,可能是Cookie失效: {response_text[:200]}...")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"解析上传响应异常: {e}")
|
||||
|
||||
@ -1413,19 +1413,60 @@ class XianyuSliderStealth:
|
||||
# 极速模式:不进行页面行为模拟,直接开始滑动
|
||||
pass
|
||||
|
||||
def find_slider_elements(self):
|
||||
"""查找滑块元素(支持在主页面和所有frame中查找)"""
|
||||
def find_slider_elements(self, fast_mode=False):
|
||||
"""查找滑块元素(支持在主页面和所有frame中查找)
|
||||
|
||||
Args:
|
||||
fast_mode: 快速模式,不使用wait_for_selector,减少等待时间(当已确认滑块存在时使用)
|
||||
"""
|
||||
try:
|
||||
# 快速等待页面稳定
|
||||
time.sleep(0.1)
|
||||
# 快速等待页面稳定(快速模式下跳过)
|
||||
if not fast_mode:
|
||||
time.sleep(0.1)
|
||||
|
||||
# ===== 【优化】优先在 frames 中快速查找最常见的滑块组合 =====
|
||||
# 根据实际日志,滑块按钮和轨道通常在同一个 frame 中
|
||||
# 按钮: #nc_1_n1z, 轨道: #nc_1_n1t
|
||||
logger.debug(f"【{self.pure_user_id}】优先在frames中快速查找常见滑块组合...")
|
||||
try:
|
||||
frames = self.page.frames
|
||||
for idx, frame in enumerate(frames):
|
||||
try:
|
||||
# 优先查找最常见的按钮选择器
|
||||
button_element = frame.query_selector("#nc_1_n1z")
|
||||
if button_element and button_element.is_visible():
|
||||
# 在同一个 frame 中查找轨道
|
||||
track_element = frame.query_selector("#nc_1_n1t")
|
||||
if track_element and track_element.is_visible():
|
||||
# 找到容器(可以用按钮或其他选择器)
|
||||
container_element = frame.query_selector("#baxia-dialog-content")
|
||||
if not container_element:
|
||||
container_element = frame.query_selector(".nc-container")
|
||||
if not container_element:
|
||||
# 如果找不到容器,用按钮作为容器标识
|
||||
container_element = button_element
|
||||
|
||||
logger.info(f"【{self.pure_user_id}】✅ 在Frame {idx} 快速找到完整滑块组合!")
|
||||
logger.info(f"【{self.pure_user_id}】 - 按钮: #nc_1_n1z")
|
||||
logger.info(f"【{self.pure_user_id}】 - 轨道: #nc_1_n1t")
|
||||
|
||||
# 保存frame引用
|
||||
self._detected_slider_frame = frame
|
||||
return container_element, button_element, track_element
|
||||
except Exception as e:
|
||||
logger.debug(f"【{self.pure_user_id}】Frame {idx} 快速查找失败: {e}")
|
||||
continue
|
||||
except Exception as e:
|
||||
logger.debug(f"【{self.pure_user_id}】frames 快速查找出错: {e}")
|
||||
|
||||
# ===== 如果快速查找失败,使用原来的完整查找逻辑 =====
|
||||
logger.debug(f"【{self.pure_user_id}】快速查找未成功,使用完整查找逻辑...")
|
||||
|
||||
# 定义滑块容器选择器(支持多种类型)
|
||||
# 注意:将 #nc_1_n1z 放在前面,因为检测时通常先找到这个按钮元素
|
||||
container_selectors = [
|
||||
# nc 系列滑块(优先查找按钮,因为检测时通常先找到这个)
|
||||
"#nc_1_n1z", # 滑块按钮也可以作为容器标识
|
||||
".nc-container",
|
||||
"#baxia-dialog-content",
|
||||
".nc-container",
|
||||
".nc_wrapper",
|
||||
".nc_scale",
|
||||
"[class*='nc-container']",
|
||||
@ -1559,30 +1600,35 @@ class XianyuSliderStealth:
|
||||
# 如果容器是在frame中找到的,按钮也应该在同一个frame中查找
|
||||
for selector in button_selectors:
|
||||
try:
|
||||
if search_frame == self.page:
|
||||
element = self.page.wait_for_selector(selector, timeout=3000)
|
||||
element = None
|
||||
if fast_mode:
|
||||
# 快速模式:直接使用 query_selector,不等待
|
||||
element = search_frame.query_selector(selector)
|
||||
else:
|
||||
# 在frame中先尝试wait_for_selector(如果支持)
|
||||
element = None
|
||||
try:
|
||||
# 尝试使用wait_for_selector(Playwright的frame支持)
|
||||
element = search_frame.wait_for_selector(selector, timeout=3000)
|
||||
except:
|
||||
# 如果不支持wait_for_selector,使用query_selector并等待
|
||||
time.sleep(0.5) # 等待元素加载
|
||||
element = search_frame.query_selector(selector)
|
||||
|
||||
if element:
|
||||
# 检查元素是否可见,但不要因为不可见就放弃
|
||||
# 正常模式:使用 wait_for_selector
|
||||
if search_frame == self.page:
|
||||
element = self.page.wait_for_selector(selector, timeout=3000)
|
||||
else:
|
||||
# 在frame中先尝试wait_for_selector(如果支持)
|
||||
try:
|
||||
is_visible = element.is_visible()
|
||||
if not is_visible:
|
||||
logger.debug(f"【{self.pure_user_id}】找到元素但不可见: {selector},继续尝试其他选择器")
|
||||
element = None
|
||||
except Exception as vis_e:
|
||||
# 如果无法检查可见性,仍然使用该元素
|
||||
logger.debug(f"【{self.pure_user_id}】无法检查元素可见性: {vis_e},继续使用该元素")
|
||||
pass
|
||||
# 尝试使用wait_for_selector(Playwright的frame支持)
|
||||
element = search_frame.wait_for_selector(selector, timeout=3000)
|
||||
except:
|
||||
# 如果不支持wait_for_selector,使用query_selector并等待
|
||||
time.sleep(0.5) # 等待元素加载
|
||||
element = search_frame.query_selector(selector)
|
||||
|
||||
if element:
|
||||
# 检查元素是否可见,但不要因为不可见就放弃
|
||||
try:
|
||||
is_visible = element.is_visible()
|
||||
if not is_visible:
|
||||
logger.debug(f"【{self.pure_user_id}】找到元素但不可见: {selector},继续尝试其他选择器")
|
||||
element = None
|
||||
except Exception as vis_e:
|
||||
# 如果无法检查可见性,仍然使用该元素
|
||||
logger.debug(f"【{self.pure_user_id}】无法检查元素可见性: {vis_e},继续使用该元素")
|
||||
pass
|
||||
|
||||
if element:
|
||||
frame_info = "主页面" if search_frame == self.page else f"Frame"
|
||||
@ -1609,13 +1655,17 @@ class XianyuSliderStealth:
|
||||
|
||||
for selector in button_selectors:
|
||||
try:
|
||||
# 先尝试wait_for_selector
|
||||
element = None
|
||||
try:
|
||||
element = frame.wait_for_selector(selector, timeout=2000)
|
||||
except:
|
||||
time.sleep(0.3) # 等待元素加载
|
||||
if fast_mode:
|
||||
# 快速模式:直接使用 query_selector
|
||||
element = frame.query_selector(selector)
|
||||
else:
|
||||
# 正常模式:先尝试wait_for_selector
|
||||
try:
|
||||
element = frame.wait_for_selector(selector, timeout=2000)
|
||||
except:
|
||||
time.sleep(0.3) # 等待元素加载
|
||||
element = frame.query_selector(selector)
|
||||
|
||||
if element:
|
||||
try:
|
||||
@ -1646,7 +1696,14 @@ class XianyuSliderStealth:
|
||||
logger.warning(f"【{self.pure_user_id}】在所有frame中未找到按钮,尝试在主页面查找...")
|
||||
for selector in button_selectors:
|
||||
try:
|
||||
element = self.page.wait_for_selector(selector, timeout=2000)
|
||||
element = None
|
||||
if fast_mode:
|
||||
# 快速模式:直接使用 query_selector
|
||||
element = self.page.query_selector(selector)
|
||||
else:
|
||||
# 正常模式:使用 wait_for_selector
|
||||
element = self.page.wait_for_selector(selector, timeout=2000)
|
||||
|
||||
if element:
|
||||
try:
|
||||
if element.is_visible():
|
||||
@ -1721,17 +1778,24 @@ class XianyuSliderStealth:
|
||||
|
||||
for selector in track_selectors:
|
||||
try:
|
||||
if track_search_frame == self.page:
|
||||
element = self.page.wait_for_selector(selector, timeout=3000)
|
||||
else:
|
||||
# 在frame中使用query_selector
|
||||
element = None
|
||||
if fast_mode:
|
||||
# 快速模式:直接使用 query_selector
|
||||
element = track_search_frame.query_selector(selector)
|
||||
if element:
|
||||
try:
|
||||
if not element.is_visible():
|
||||
element = None
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
# 正常模式:使用 wait_for_selector
|
||||
if track_search_frame == self.page:
|
||||
element = self.page.wait_for_selector(selector, timeout=3000)
|
||||
else:
|
||||
# 在frame中使用query_selector
|
||||
element = track_search_frame.query_selector(selector)
|
||||
|
||||
if element:
|
||||
try:
|
||||
if not element.is_visible():
|
||||
element = None
|
||||
except:
|
||||
pass
|
||||
|
||||
if element:
|
||||
frame_info = "主页面" if track_search_frame == self.page else f"Frame"
|
||||
@ -2179,8 +2243,13 @@ class XianyuSliderStealth:
|
||||
logger.error(f"【{self.pure_user_id}】分析失败原因时出错: {e}")
|
||||
return {}
|
||||
|
||||
def solve_slider(self, max_retries: int = 2):
|
||||
"""处理滑块验证(极速模式)"""
|
||||
def solve_slider(self, max_retries: int = 3, fast_mode: bool = False):
|
||||
"""处理滑块验证(极速模式)
|
||||
|
||||
Args:
|
||||
max_retries: 最大重试次数(默认3次,因为同一个页面连续失败3次后就不会成功了)
|
||||
fast_mode: 快速查找模式(当已确认滑块存在时使用,减少等待时间)
|
||||
"""
|
||||
failure_records = []
|
||||
current_strategy = 'ultra_fast' # 极速策略
|
||||
|
||||
@ -2202,8 +2271,8 @@ class XianyuSliderStealth:
|
||||
else:
|
||||
logger.info(f"【{self.pure_user_id}】未找到frame引用,将重新检测滑块位置")
|
||||
|
||||
# 1. 查找滑块元素
|
||||
slider_container, slider_button, slider_track = self.find_slider_elements()
|
||||
# 1. 查找滑块元素(使用快速模式)
|
||||
slider_container, slider_button, slider_track = self.find_slider_elements(fast_mode=fast_mode)
|
||||
if not all([slider_container, slider_button, slider_track]):
|
||||
logger.error(f"【{self.pure_user_id}】滑块元素查找失败")
|
||||
continue
|
||||
@ -2502,12 +2571,25 @@ class XianyuSliderStealth:
|
||||
|
||||
# 检测到滑块验证,立即处理
|
||||
logger.warning(f"【{self.pure_user_id}】检测到滑块验证,开始自动处理...")
|
||||
slider_success = self.solve_slider(max_retries=5)
|
||||
slider_success = self.solve_slider(max_retries=3)
|
||||
if slider_success:
|
||||
logger.success(f"【{self.pure_user_id}】✅ 滑块验证成功!")
|
||||
time.sleep(3) # 等待滑块验证后的状态更新
|
||||
else:
|
||||
logger.error(f"【{self.pure_user_id}】❌ 滑块验证失败")
|
||||
# 3次失败后,刷新页面重试
|
||||
logger.warning(f"【{self.pure_user_id}】⚠️ 滑块处理3次都失败,刷新页面后重试...")
|
||||
try:
|
||||
self.page.reload(wait_until="domcontentloaded", timeout=30000)
|
||||
logger.info(f"【{self.pure_user_id}】✅ 页面刷新完成")
|
||||
time.sleep(2)
|
||||
slider_success = self.solve_slider(max_retries=3)
|
||||
if not slider_success:
|
||||
logger.error(f"【{self.pure_user_id}】❌ 刷新后滑块验证仍然失败")
|
||||
else:
|
||||
logger.success(f"【{self.pure_user_id}】✅ 刷新后滑块验证成功!")
|
||||
time.sleep(3)
|
||||
except Exception as e:
|
||||
logger.error(f"【{self.pure_user_id}】❌ 页面刷新失败: {e}")
|
||||
|
||||
# 清理临时变量
|
||||
if hasattr(self, '_detected_slider_frame'):
|
||||
@ -2537,16 +2619,75 @@ class XianyuSliderStealth:
|
||||
face_verify_url = self._get_face_verification_url(frame)
|
||||
if face_verify_url:
|
||||
logger.info(f"【{self.pure_user_id}】✅ 获取到人脸验证链接: {face_verify_url}")
|
||||
# 创建一个特殊的frame对象,包含验证链接
|
||||
|
||||
# 截图并保存
|
||||
screenshot_path = None
|
||||
try:
|
||||
# 等待页面加载完成
|
||||
time.sleep(2)
|
||||
|
||||
# 先删除该账号的旧截图
|
||||
import glob
|
||||
screenshots_dir = "static/uploads/images"
|
||||
os.makedirs(screenshots_dir, exist_ok=True)
|
||||
old_screenshots = glob.glob(os.path.join(screenshots_dir, f"face_verify_{self.pure_user_id}_*.jpg"))
|
||||
for old_file in old_screenshots:
|
||||
try:
|
||||
os.remove(old_file)
|
||||
logger.info(f"【{self.pure_user_id}】删除旧的验证截图: {old_file}")
|
||||
except Exception as e:
|
||||
logger.warning(f"【{self.pure_user_id}】删除旧截图失败: {e}")
|
||||
|
||||
# 尝试截取iframe元素的截图
|
||||
screenshot_bytes = None
|
||||
try:
|
||||
# 获取iframe元素并截图
|
||||
iframe_element = page.query_selector('iframe#alibaba-login-box')
|
||||
if iframe_element:
|
||||
screenshot_bytes = iframe_element.screenshot()
|
||||
logger.info(f"【{self.pure_user_id}】已截取iframe元素")
|
||||
else:
|
||||
# 如果找不到iframe,截取整个页面
|
||||
screenshot_bytes = page.screenshot(full_page=False)
|
||||
logger.info(f"【{self.pure_user_id}】已截取整个页面")
|
||||
except Exception as e:
|
||||
logger.warning(f"【{self.pure_user_id}】截取iframe失败,尝试截取整个页面: {e}")
|
||||
screenshot_bytes = page.screenshot(full_page=False)
|
||||
|
||||
if screenshot_bytes:
|
||||
# 生成带时间戳的文件名并直接保存
|
||||
from datetime import datetime
|
||||
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||
filename = f"face_verify_{self.pure_user_id}_{timestamp}.jpg"
|
||||
file_path = os.path.join(screenshots_dir, filename)
|
||||
|
||||
try:
|
||||
with open(file_path, 'wb') as f:
|
||||
f.write(screenshot_bytes)
|
||||
# 返回相对路径
|
||||
screenshot_path = file_path.replace('\\', '/')
|
||||
logger.info(f"【{self.pure_user_id}】✅ 人脸验证截图已保存: {screenshot_path}")
|
||||
except Exception as e:
|
||||
logger.error(f"【{self.pure_user_id}】保存截图失败: {e}")
|
||||
screenshot_path = None
|
||||
else:
|
||||
logger.warning(f"【{self.pure_user_id}】⚠️ 截图失败,无法获取截图数据")
|
||||
except Exception as e:
|
||||
logger.error(f"【{self.pure_user_id}】截图时出错: {e}")
|
||||
import traceback
|
||||
logger.debug(traceback.format_exc())
|
||||
|
||||
# 创建一个特殊的frame对象,包含截图路径
|
||||
class VerificationFrame:
|
||||
def __init__(self, original_frame, verify_url):
|
||||
def __init__(self, original_frame, verify_url, screenshot_path=None):
|
||||
self._original_frame = original_frame
|
||||
self.verify_url = verify_url
|
||||
self.screenshot_path = screenshot_path
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self._original_frame, name)
|
||||
|
||||
return True, VerificationFrame(frame, face_verify_url)
|
||||
return True, VerificationFrame(frame, face_verify_url, screenshot_path)
|
||||
|
||||
return True, frame
|
||||
except Exception as e:
|
||||
@ -2840,6 +2981,33 @@ class XianyuSliderStealth:
|
||||
'--lang=zh-CN', # 设置浏览器语言为中文
|
||||
]
|
||||
|
||||
# 在启动Playwright之前,重新检查和设置浏览器路径
|
||||
# 确保使用正确的浏览器版本(避免版本不匹配问题)
|
||||
import sys
|
||||
from pathlib import Path
|
||||
if getattr(sys, 'frozen', False):
|
||||
# 如果是打包后的exe,检查exe同目录下的浏览器
|
||||
exe_dir = Path(sys.executable).parent
|
||||
playwright_dir = exe_dir / 'playwright'
|
||||
|
||||
if playwright_dir.exists():
|
||||
chromium_dirs = list(playwright_dir.glob('chromium-*'))
|
||||
# 找到第一个完整的浏览器目录
|
||||
for chromium_dir in chromium_dirs:
|
||||
chrome_exe = chromium_dir / 'chrome-win' / 'chrome.exe'
|
||||
if chrome_exe.exists() and chrome_exe.stat().st_size > 0:
|
||||
# 清除旧的环境变量,使用实际存在的浏览器
|
||||
if 'PLAYWRIGHT_BROWSERS_PATH' in os.environ:
|
||||
old_path = os.environ['PLAYWRIGHT_BROWSERS_PATH']
|
||||
if old_path != str(playwright_dir):
|
||||
logger.info(f"【{self.pure_user_id}】清除旧的环境变量: {old_path}")
|
||||
del os.environ['PLAYWRIGHT_BROWSERS_PATH']
|
||||
# 设置正确的环境变量
|
||||
os.environ['PLAYWRIGHT_BROWSERS_PATH'] = str(playwright_dir)
|
||||
logger.info(f"【{self.pure_user_id}】已设置PLAYWRIGHT_BROWSERS_PATH: {playwright_dir}")
|
||||
logger.info(f"【{self.pure_user_id}】使用浏览器版本: {chromium_dir.name}")
|
||||
break
|
||||
|
||||
# 启动浏览器
|
||||
playwright = sync_playwright().start()
|
||||
context = playwright.chromium.launch_persistent_context(
|
||||
@ -3029,13 +3197,27 @@ class XianyuSliderStealth:
|
||||
self._detected_slider_frame = detected_slider_frame
|
||||
|
||||
logger.warning(f"【{self.pure_user_id}】检测到滑块验证,开始处理...")
|
||||
slider_success = self.solve_slider(max_retries=5)
|
||||
time.sleep(3)
|
||||
slider_success = self.solve_slider(max_retries=3)
|
||||
|
||||
if not slider_success:
|
||||
logger.error(f"【{self.pure_user_id}】❌ 滑块验证失败")
|
||||
return None
|
||||
|
||||
logger.success(f"【{self.pure_user_id}】✅ 滑块验证成功!")
|
||||
# 3次失败后,刷新页面重试
|
||||
logger.warning(f"【{self.pure_user_id}】⚠️ 滑块处理3次都失败,刷新页面后重试...")
|
||||
try:
|
||||
page.reload(wait_until="domcontentloaded", timeout=30000)
|
||||
logger.info(f"【{self.pure_user_id}】✅ 页面刷新完成")
|
||||
time.sleep(2)
|
||||
slider_success = self.solve_slider(max_retries=3)
|
||||
if not slider_success:
|
||||
logger.error(f"【{self.pure_user_id}】❌ 刷新后滑块验证仍然失败")
|
||||
return None
|
||||
else:
|
||||
logger.success(f"【{self.pure_user_id}】✅ 刷新后滑块验证成功!")
|
||||
except Exception as e:
|
||||
logger.error(f"【{self.pure_user_id}】❌ 页面刷新失败: {e}")
|
||||
return None
|
||||
else:
|
||||
logger.success(f"【{self.pure_user_id}】✅ 滑块验证成功!")
|
||||
|
||||
# 等待页面加载和状态更新(第一次等待3秒)
|
||||
logger.info(f"【{self.pure_user_id}】等待3秒,让页面加载完成...")
|
||||
@ -3249,13 +3431,26 @@ class XianyuSliderStealth:
|
||||
logger.warning(f"【{self.pure_user_id}】检测到滑块验证,开始处理...")
|
||||
|
||||
# 【复用】直接调用 solve_slider() 方法处理滑块
|
||||
slider_success = self.solve_slider(max_retries=5)
|
||||
slider_success = self.solve_slider(max_retries=3)
|
||||
|
||||
if slider_success:
|
||||
logger.success(f"【{self.pure_user_id}】✅ 滑块验证成功!")
|
||||
else:
|
||||
logger.error(f"【{self.pure_user_id}】❌ 滑块验证失败")
|
||||
return None
|
||||
# 3次失败后,刷新页面重试
|
||||
logger.warning(f"【{self.pure_user_id}】⚠️ 滑块处理3次都失败,刷新页面后重试...")
|
||||
try:
|
||||
page.reload(wait_until="domcontentloaded", timeout=30000)
|
||||
logger.info(f"【{self.pure_user_id}】✅ 页面刷新完成")
|
||||
time.sleep(2)
|
||||
slider_success = self.solve_slider(max_retries=3)
|
||||
if not slider_success:
|
||||
logger.error(f"【{self.pure_user_id}】❌ 刷新后滑块验证仍然失败")
|
||||
return None
|
||||
else:
|
||||
logger.success(f"【{self.pure_user_id}】✅ 刷新后滑块验证成功!")
|
||||
except Exception as e:
|
||||
logger.error(f"【{self.pure_user_id}】❌ 页面刷新失败: {e}")
|
||||
return None
|
||||
else:
|
||||
logger.info(f"【{self.pure_user_id}】未检测到滑块验证")
|
||||
|
||||
@ -3279,13 +3474,27 @@ class XianyuSliderStealth:
|
||||
|
||||
if has_slider_after_wait:
|
||||
logger.warning(f"【{self.pure_user_id}】检测到滑块验证,开始处理...")
|
||||
slider_success = self.solve_slider(max_retries=5)
|
||||
slider_success = self.solve_slider(max_retries=3)
|
||||
if slider_success:
|
||||
logger.success(f"【{self.pure_user_id}】✅ 滑块验证成功!")
|
||||
time.sleep(3) # 等待滑块验证后的状态更新
|
||||
else:
|
||||
logger.error(f"【{self.pure_user_id}】❌ 滑块验证失败")
|
||||
return None
|
||||
# 3次失败后,刷新页面重试
|
||||
logger.warning(f"【{self.pure_user_id}】⚠️ 滑块处理3次都失败,刷新页面后重试...")
|
||||
try:
|
||||
page.reload(wait_until="domcontentloaded", timeout=30000)
|
||||
logger.info(f"【{self.pure_user_id}】✅ 页面刷新完成")
|
||||
time.sleep(2)
|
||||
slider_success = self.solve_slider(max_retries=3)
|
||||
if not slider_success:
|
||||
logger.error(f"【{self.pure_user_id}】❌ 刷新后滑块验证仍然失败")
|
||||
return None
|
||||
else:
|
||||
logger.success(f"【{self.pure_user_id}】✅ 刷新后滑块验证成功!")
|
||||
time.sleep(3)
|
||||
except Exception as e:
|
||||
logger.error(f"【{self.pure_user_id}】❌ 页面刷新失败: {e}")
|
||||
return None
|
||||
|
||||
# 检查登录状态
|
||||
logger.info(f"【{self.pure_user_id}】等待1秒后检查登录状态...")
|
||||
@ -3331,8 +3540,9 @@ class XianyuSliderStealth:
|
||||
logger.warning(f"【{self.pure_user_id}】⚠️ 检测到二维码/人脸验证")
|
||||
logger.info(f"【{self.pure_user_id}】请在浏览器中完成二维码/人脸验证")
|
||||
|
||||
# 获取验证链接URL
|
||||
# 获取验证链接URL和截图路径
|
||||
frame_url = None
|
||||
screenshot_path = None
|
||||
if qr_frame:
|
||||
try:
|
||||
# 检查是否有验证链接(从VerificationFrame对象)
|
||||
@ -3341,20 +3551,30 @@ class XianyuSliderStealth:
|
||||
logger.info(f"【{self.pure_user_id}】使用获取到的人脸验证链接: {frame_url}")
|
||||
else:
|
||||
frame_url = qr_frame.url if hasattr(qr_frame, 'url') else None
|
||||
|
||||
# 检查是否有截图路径(从VerificationFrame对象)
|
||||
if hasattr(qr_frame, 'screenshot_path') and qr_frame.screenshot_path:
|
||||
screenshot_path = qr_frame.screenshot_path
|
||||
logger.info(f"【{self.pure_user_id}】使用获取到的人脸验证截图: {screenshot_path}")
|
||||
except Exception as e:
|
||||
logger.warning(f"【{self.pure_user_id}】获取frame URL失败: {e}")
|
||||
logger.warning(f"【{self.pure_user_id}】获取frame信息失败: {e}")
|
||||
import traceback
|
||||
logger.debug(traceback.format_exc())
|
||||
|
||||
# 显示验证链接(如果有)
|
||||
if frame_url:
|
||||
# 显示验证信息
|
||||
if screenshot_path:
|
||||
logger.warning(f"【{self.pure_user_id}】" + "=" * 60)
|
||||
logger.warning(f"【{self.pure_user_id}】二维码/人脸验证截图:")
|
||||
logger.warning(f"【{self.pure_user_id}】{screenshot_path}")
|
||||
logger.warning(f"【{self.pure_user_id}】" + "=" * 60)
|
||||
elif frame_url:
|
||||
logger.warning(f"【{self.pure_user_id}】" + "=" * 60)
|
||||
logger.warning(f"【{self.pure_user_id}】二维码/人脸验证链接:")
|
||||
logger.warning(f"【{self.pure_user_id}】{frame_url}")
|
||||
logger.warning(f"【{self.pure_user_id}】" + "=" * 60)
|
||||
else:
|
||||
logger.warning(f"【{self.pure_user_id}】" + "=" * 60)
|
||||
logger.warning(f"【{self.pure_user_id}】二维码/人脸验证已检测到,但无法获取验证链接")
|
||||
logger.warning(f"【{self.pure_user_id}】二维码/人脸验证已检测到,但无法获取验证信息")
|
||||
logger.warning(f"【{self.pure_user_id}】请在浏览器中查看验证页面")
|
||||
logger.warning(f"【{self.pure_user_id}】" + "=" * 60)
|
||||
|
||||
@ -3363,17 +3583,27 @@ class XianyuSliderStealth:
|
||||
# 【重要】发送通知给客户
|
||||
if notification_callback:
|
||||
try:
|
||||
if frame_url:
|
||||
if screenshot_path or frame_url:
|
||||
# 构造清晰的通知消息
|
||||
notification_msg = (
|
||||
f"⚠️ 账号密码登录需要人脸验证\n\n"
|
||||
f"账号: {self.pure_user_id}\n"
|
||||
f"时间: {time.strftime('%Y-%m-%d %H:%M:%S')}\n\n"
|
||||
f"请点击验证链接完成验证:\n{frame_url}\n\n"
|
||||
f"在验证期间,闲鱼自动回复暂时无法使用。"
|
||||
)
|
||||
if screenshot_path:
|
||||
|
||||
notification_msg = (
|
||||
f"⚠️ 账号密码登录需要人脸验证\n\n"
|
||||
f"账号: {self.pure_user_id}\n"
|
||||
f"时间: {time.strftime('%Y-%m-%d %H:%M:%S')}\n\n"
|
||||
f"请登录自动化网站,访问账号管理模块,进行对应账号的人脸验证"
|
||||
f"在验证期间,闲鱼自动回复暂时无法使用。"
|
||||
)
|
||||
else:
|
||||
notification_msg = (
|
||||
f"⚠️ 账号密码登录需要人脸验证\n\n"
|
||||
f"账号: {self.pure_user_id}\n"
|
||||
f"时间: {time.strftime('%Y-%m-%d %H:%M:%S')}\n\n"
|
||||
f"请点击验证链接完成验证:\n{frame_url}\n\n"
|
||||
f"在验证期间,闲鱼自动回复暂时无法使用。"
|
||||
)
|
||||
|
||||
logger.info(f"【{self.pure_user_id}】准备发送人脸验证通知,URL: {frame_url}")
|
||||
logger.info(f"【{self.pure_user_id}】准备发送人脸验证通知,截图路径: {screenshot_path}, URL: {frame_url}")
|
||||
|
||||
# 如果回调是异步函数,使用 asyncio.run 在新的事件循环中运行
|
||||
import asyncio
|
||||
@ -3384,8 +3614,9 @@ class XianyuSliderStealth:
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
try:
|
||||
# 传递通知消息和URL给回调
|
||||
loop.run_until_complete(notification_callback(notification_msg, None, frame_url))
|
||||
# 传递通知消息、截图路径和URL给回调
|
||||
# 参数顺序:message, screenshot_path, verification_url
|
||||
loop.run_until_complete(notification_callback(notification_msg, screenshot_path, frame_url))
|
||||
logger.info(f"【{self.pure_user_id}】✅ 异步通知回调已执行")
|
||||
except Exception as async_err:
|
||||
logger.error(f"【{self.pure_user_id}】异步通知回调执行失败: {async_err}")
|
||||
@ -3400,11 +3631,11 @@ class XianyuSliderStealth:
|
||||
logger.info(f"【{self.pure_user_id}】异步通知线程已启动")
|
||||
# 不等待线程完成,让通知在后台发送
|
||||
else:
|
||||
# 同步回调直接调用(传递通知消息和URL)
|
||||
notification_callback(notification_msg, None, frame_url)
|
||||
# 同步回调直接调用(传递通知消息、截图路径和URL)
|
||||
notification_callback(notification_msg, None, frame_url, screenshot_path)
|
||||
logger.info(f"【{self.pure_user_id}】✅ 同步通知回调已执行")
|
||||
else:
|
||||
logger.warning(f"【{self.pure_user_id}】无法获取验证URL,跳过通知发送")
|
||||
logger.warning(f"【{self.pure_user_id}】无法获取验证信息,跳过通知发送")
|
||||
|
||||
except Exception as notify_err:
|
||||
logger.error(f"【{self.pure_user_id}】发送人脸验证通知失败: {notify_err}")
|
||||
@ -3417,13 +3648,99 @@ class XianyuSliderStealth:
|
||||
# 持续等待用户完成二维码/人脸验证
|
||||
logger.info(f"【{self.pure_user_id}】等待二维码/人脸验证完成...")
|
||||
check_interval = 10 # 每10秒检查一次
|
||||
max_wait_time = 600 # 最多等待10分钟
|
||||
max_wait_time = 450 # 最多等待7.5分钟
|
||||
waited_time = 0
|
||||
|
||||
while waited_time < max_wait_time:
|
||||
time.sleep(check_interval)
|
||||
waited_time += check_interval
|
||||
|
||||
# 先检测是否有滑块,如果有就处理
|
||||
try:
|
||||
logger.debug(f"【{self.pure_user_id}】检测是否存在滑块...")
|
||||
slider_detected = False
|
||||
|
||||
# 快速检测滑块元素(不等待,仅检测)
|
||||
slider_selectors = [
|
||||
"#nc_1_n1z",
|
||||
".nc-container",
|
||||
"#baxia-dialog-content",
|
||||
".nc_wrapper",
|
||||
"#nocaptcha"
|
||||
]
|
||||
|
||||
# 先在主页面检测
|
||||
for selector in slider_selectors:
|
||||
try:
|
||||
element = page.query_selector(selector)
|
||||
if element and element.is_visible():
|
||||
slider_detected = True
|
||||
logger.info(f"【{self.pure_user_id}】🔍 检测到滑块元素: {selector}")
|
||||
break
|
||||
except:
|
||||
pass
|
||||
|
||||
# 如果主页面没找到,检查所有frame
|
||||
if not slider_detected:
|
||||
try:
|
||||
frames = page.frames
|
||||
for frame in frames:
|
||||
for selector in slider_selectors:
|
||||
try:
|
||||
element = frame.query_selector(selector)
|
||||
if element and element.is_visible():
|
||||
slider_detected = True
|
||||
logger.info(f"【{self.pure_user_id}】🔍 在frame中检测到滑块元素: {selector}")
|
||||
break
|
||||
except:
|
||||
pass
|
||||
if slider_detected:
|
||||
break
|
||||
except:
|
||||
pass
|
||||
|
||||
# 如果检测到滑块,尝试处理
|
||||
if slider_detected:
|
||||
logger.info(f"【{self.pure_user_id}】⚡ 检测到滑块,开始自动处理...")
|
||||
time.sleep(3)
|
||||
try:
|
||||
# 调用滑块处理方法(使用快速模式,因为已确认滑块存在)
|
||||
# 最多尝试3次,因为同一个页面连续失败3次后就不会成功了
|
||||
if self.solve_slider(max_retries=3, fast_mode=True):
|
||||
logger.success(f"【{self.pure_user_id}】✅ 滑块处理成功!")
|
||||
|
||||
# 滑块处理成功后,刷新页面
|
||||
try:
|
||||
logger.info(f"【{self.pure_user_id}】🔄 滑块处理成功,刷新页面...")
|
||||
page.reload(wait_until="domcontentloaded", timeout=30000)
|
||||
logger.info(f"【{self.pure_user_id}】✅ 页面刷新完成")
|
||||
# 刷新后短暂等待,让页面稳定
|
||||
time.sleep(2)
|
||||
except Exception as reload_err:
|
||||
logger.warning(f"【{self.pure_user_id}】⚠️ 页面刷新失败: {reload_err}")
|
||||
else:
|
||||
# 3次都失败了,刷新页面后再尝试一次
|
||||
logger.warning(f"【{self.pure_user_id}】⚠️ 滑块处理3次都失败,刷新页面后重试...")
|
||||
try:
|
||||
logger.info(f"【{self.pure_user_id}】🔄 刷新页面以重置滑块状态...")
|
||||
page.reload(wait_until="domcontentloaded", timeout=30000)
|
||||
logger.info(f"【{self.pure_user_id}】✅ 页面刷新完成")
|
||||
time.sleep(2)
|
||||
|
||||
# 刷新后再次尝试处理滑块(给一次机会)
|
||||
logger.info(f"【{self.pure_user_id}】🔄 页面刷新后,再次尝试处理滑块...")
|
||||
if self.solve_slider(max_retries=3, fast_mode=True):
|
||||
logger.success(f"【{self.pure_user_id}】✅ 刷新后滑块处理成功!")
|
||||
else:
|
||||
logger.error(f"【{self.pure_user_id}】❌ 刷新后滑块处理仍然失败,继续等待...")
|
||||
except Exception as reload_err:
|
||||
logger.warning(f"【{self.pure_user_id}】⚠️ 页面刷新失败: {reload_err}")
|
||||
except Exception as slider_err:
|
||||
logger.warning(f"【{self.pure_user_id}】⚠️ 滑块处理出错: {slider_err}")
|
||||
logger.debug(traceback.format_exc())
|
||||
except Exception as e:
|
||||
logger.debug(f"【{self.pure_user_id}】滑块检测时出错: {e}")
|
||||
|
||||
# 检查登录状态(通过页面元素)
|
||||
try:
|
||||
if self._check_login_success_by_element(page):
|
||||
@ -3435,6 +3752,25 @@ class XianyuSliderStealth:
|
||||
except Exception as e:
|
||||
logger.debug(f"【{self.pure_user_id}】检查登录状态时出错: {e}")
|
||||
|
||||
# 删除截图(无论成功或失败)
|
||||
if screenshot_path:
|
||||
try:
|
||||
import glob
|
||||
# 删除该账号的所有验证截图
|
||||
screenshots_dir = "static/uploads/images"
|
||||
all_screenshots = glob.glob(os.path.join(screenshots_dir, f"face_verify_{self.pure_user_id}_*.jpg"))
|
||||
for screenshot_file in all_screenshots:
|
||||
try:
|
||||
if os.path.exists(screenshot_file):
|
||||
os.remove(screenshot_file)
|
||||
logger.info(f"【{self.pure_user_id}】✅ 已删除验证截图: {screenshot_file}")
|
||||
else:
|
||||
logger.warning(f"【{self.pure_user_id}】⚠️ 截图文件不存在: {screenshot_file}")
|
||||
except Exception as e:
|
||||
logger.warning(f"【{self.pure_user_id}】⚠️ 删除截图失败: {e}")
|
||||
except Exception as e:
|
||||
logger.error(f"【{self.pure_user_id}】删除截图时出错: {e}")
|
||||
|
||||
if login_success:
|
||||
logger.info(f"【{self.pure_user_id}】二维码/人脸验证已完成")
|
||||
else:
|
||||
|
||||
5
推荐云服务器CDN-www.hsykj.com.url
Normal file
5
推荐云服务器CDN-www.hsykj.com.url
Normal file
@ -0,0 +1,5 @@
|
||||
[{000214A0-0000-0000-C000-000000000046}]
|
||||
Prop3=19,11
|
||||
[InternetShortcut]
|
||||
IDList=
|
||||
URL=https://www.hsykj.com/
|
||||
Loading…
Reference in New Issue
Block a user