This commit is contained in:
zhinianboke 2025-10-12 22:22:35 +08:00
parent 0109838978
commit cdbb85373f
13 changed files with 1781 additions and 350 deletions

View File

@ -21,29 +21,12 @@ ENV TZ=Asia/Shanghai
ENV DOCKER_ENV=true
ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
# 无头浏览器环境变量
ENV DISPLAY=:99
ENV CHROME_BIN=/usr/bin/chromium
ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=0
ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
# 禁用GPU和图形加速适合容器环境
ENV CHROME_FLAGS="--no-sandbox --disable-dev-shm-usage --disable-gpu --disable-software-rasterizer --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-renderer-backgrounding --disable-features=TranslateUI --disable-extensions --disable-default-apps --disable-sync --disable-translate --hide-scrollbars --mute-audio --no-default-browser-check --no-pings --single-process"
# 安装系统依赖包括Playwright浏览器依赖
RUN apt-get update && \
apt-get install -y --no-install-recommends \
# 基础工具
nodejs \
npm \
build-essential \
gcc \
g++ \
make \
pkg-config \
patchelf \
git \
tzdata \
curl \
ca-certificates \
@ -80,17 +63,15 @@ RUN apt-get update && \
libx11-xcb1 \
libxfixes3 \
xdg-utils \
# DrissionPage需要的Chrome/Chromium浏览器
chromium \
# OpenCV运行时依赖
libgl1 \
libglib2.0-0 \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* \
&& rm -rf /tmp/* \
&& rm -rf /var/tmp/*
# 降低编译内存占用(针对 Nuitka 构建阶段)
ENV CFLAGS="-O1" \
CXXFLAGS="-O1"
# 设置时区
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
@ -127,10 +108,9 @@ EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1
# 复制启动脚本并设置权限
# 复制启动脚本
COPY entrypoint.sh /app/entrypoint.sh
RUN chmod +x /app/entrypoint.sh && \
dos2unix /app/entrypoint.sh 2>/dev/null || true
RUN chmod +x /app/entrypoint.sh
# 启动命令使用ENTRYPOINT确保脚本被执行
ENTRYPOINT ["/bin/bash", "/app/entrypoint.sh"]
# 启动命令
CMD ["/app/entrypoint.sh"]

View File

@ -3,7 +3,7 @@ FROM python:3.11-slim-bookworm
# 设置标签信息
LABEL maintainer="zhinianboke"
LABEL version="2.2.0"
LABEL version="2.1.0"
LABEL description="闲鱼自动回复系统 - 企业级多用户版本,支持自动发货和免拼发货"
LABEL repository="https://github.com/zhinianboke/xianyu-auto-reply"
LABEL license="仅供学习使用,禁止商业用途"
@ -21,15 +21,6 @@ ENV TZ=Asia/Shanghai
ENV DOCKER_ENV=true
ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
# 无头浏览器环境变量
ENV DISPLAY=:99
ENV CHROME_BIN=/usr/bin/chromium
ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=0
ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
# 禁用GPU和图形加速适合容器环境
ENV CHROME_FLAGS="--no-sandbox --disable-dev-shm-usage --disable-gpu --disable-software-rasterizer --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-renderer-backgrounding --disable-features=TranslateUI --disable-extensions --disable-default-apps --disable-sync --disable-translate --hide-scrollbars --mute-audio --no-default-browser-check --no-pings --single-process"
#更换中科大源
RUN sed -i 's/deb.debian.org/mirrors.ustc.edu.cn/g' /etc/apt/sources.list.d/debian.sources
@ -39,14 +30,6 @@ RUN apt-get update && \
# 基础工具
nodejs \
npm \
build-essential \
gcc \
g++ \
make \
pkg-config \
patchelf \
git \
tzdata \
curl \
ca-certificates \
@ -92,21 +75,16 @@ RUN apt-get update && \
&& rm -rf /tmp/* \
&& rm -rf /var/tmp/*
# 降低编译内存占用(针对 Nuitka 构建阶段)
ENV CFLAGS="-O1" \
CXXFLAGS="-O1"
# 设置时区
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
# 验证Node.js安装并设置环境变量
RUN node --version && npm --version
ENV NODE_PATH=/usr/lib/node_modules
# 复制requirements.txt并安装Python依赖
COPY requirements.txt .
RUN pip install --no-cache-dir --upgrade pip -i https://pypi.tuna.tsinghua.edu.cn/simple && \
RUN pip install --no-cache-dir --upgrade pip -i https://pypi.tuna.tsinghua.edu.cn/simple&& \
pip install --no-cache-dir -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
# 复制项目文件
@ -133,10 +111,9 @@ EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1
# 复制启动脚本并设置权限
# 复制启动脚本
COPY entrypoint.sh /app/entrypoint.sh
RUN chmod +x /app/entrypoint.sh && \
dos2unix /app/entrypoint.sh 2>/dev/null || true
RUN chmod +x /app/entrypoint.sh
# 启动命令使用ENTRYPOINT确保脚本被执行
ENTRYPOINT ["/bin/bash", "/app/entrypoint.sh"]
# 启动命令
CMD ["/app/entrypoint.sh"]

View File

@ -4,6 +4,7 @@ import re
import time
import base64
import os
import random
from loguru import logger
import websockets
from utils.xianyu_utils import (
@ -161,7 +162,7 @@ class XianyuLive:
# 类级别的实例管理字典用于API调用
_instances = {} # {cookie_id: XianyuLive实例}
_instances_lock = asyncio.Lock()
def _safe_str(self, e):
"""安全地将异常转换为字符串"""
try:
@ -172,78 +173,6 @@ class XianyuLive:
except:
return "未知错误"
def _get_browser_args(self):
"""获取优化的浏览器启动参数"""
browser_args = [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-accelerated-2d-canvas',
'--no-first-run',
'--no-zygote',
'--disable-gpu',
'--disable-background-timer-throttling',
'--disable-backgrounding-occluded-windows',
'--disable-renderer-backgrounding',
'--disable-features=TranslateUI',
'--disable-ipc-flooding-protection',
'--disable-extensions',
'--disable-default-apps',
'--disable-sync',
'--disable-translate',
'--hide-scrollbars',
'--mute-audio',
'--no-default-browser-check',
'--no-pings'
]
# 在Docker环境中添加额外参数优化容器环境兼容性
if os.getenv('DOCKER_ENV'):
browser_args.extend([
'--disable-background-networking',
'--disable-client-side-phishing-detection',
'--disable-hang-monitor',
'--disable-popup-blocking',
'--disable-prompt-on-repost',
'--disable-web-resources',
'--metrics-recording-only',
'--safebrowsing-disable-auto-update',
'--enable-automation',
'--password-store=basic',
'--use-mock-keychain',
# 容器环境特殊配置
'--disable-software-rasterizer',
'--disable-field-trial-config',
'--disable-back-forward-cache',
'--disable-breakpad',
'--disable-component-extensions-with-background-pages',
'--disable-component-update',
'--disable-domain-reliability',
'--disable-features=VizDisplayCompositor,AudioServiceOutOfProcess,TranslateUI',
'--force-color-profile=srgb',
'--disable-canvas-aa',
'--disable-2d-canvas-clip-aa',
'--disable-gl-drawing-for-tests',
'--disable-threaded-animation',
'--disable-threaded-scrolling',
'--disable-in-process-stack-traces',
'--disable-histogram-customizer',
'--disable-gl-extensions',
'--disable-composited-antialiasing',
# 音频和显示相关
'--disable-audio-output',
'--disable-audio-input',
'--autoplay-policy=no-user-gesture-required',
# 网络和安全相关
'--disable-web-security',
'--disable-ipc-flooding-protection',
# 内存和性能优化
'--memory-pressure-off',
'--max_old_space_size=4096'
])
return browser_args
def __init__(self, cookies_str=None, cookie_id: str = "default", user_id: int = None):
"""初始化闲鱼直播类"""
logger.info(f"{cookie_id}】开始初始化XianyuLive...")
@ -335,9 +264,23 @@ class XianyuLive:
self.max_connection_failures = 5 # 最大连续失败次数
self.last_successful_connection = 0 # 上次成功连接时间
# 初始化订单状态处理器
self._init_order_status_handler()
# 注册实例到类级别字典用于API调用
self._register_instance()
def _init_order_status_handler(self):
"""初始化订单状态处理器"""
try:
# 直接导入订单状态处理器
from order_status_handler import order_status_handler
self.order_status_handler = order_status_handler
logger.info(f"{self.cookie_id}】订单状态处理器已启用")
except Exception as e:
logger.error(f"{self.cookie_id}】初始化订单状态处理器失败: {self._safe_str(e)}")
self.order_status_handler = None
def _register_instance(self):
"""注册当前实例到类级别字典"""
try:
@ -401,6 +344,28 @@ class XianyuLive:
"""标记订单已发货"""
self.delivery_sent_orders.add(order_id)
logger.info(f"{self.cookie_id}】订单 {order_id} 已标记为发货")
# 更新订单状态为已发货
logger.info(f"{self.cookie_id}】检查自动发货订单状态处理器: handler_exists={self.order_status_handler is not None}")
if self.order_status_handler:
logger.info(f"{self.cookie_id}】准备调用订单状态处理器.handle_auto_delivery_order_status: {order_id}")
try:
success = self.order_status_handler.handle_auto_delivery_order_status(
order_id=order_id,
cookie_id=self.cookie_id,
context="自动发货完成"
)
logger.info(f"{self.cookie_id}】订单状态处理器.handle_auto_delivery_order_status返回结果: {success}")
if success:
logger.info(f"{self.cookie_id}】订单 {order_id} 状态已更新为已发货")
else:
logger.warning(f"{self.cookie_id}】订单 {order_id} 状态更新为已发货失败")
except Exception as e:
logger.error(f"{self.cookie_id}】订单状态更新失败: {self._safe_str(e)}")
import traceback
logger.error(f"{self.cookie_id}】详细错误信息: {traceback.format_exc()}")
else:
logger.warning(f"{self.cookie_id}】订单状态处理器为None跳过自动发货状态更新: {order_id}")
async def _delayed_lock_release(self, lock_key: str, delay_minutes: int = 10):
"""
@ -502,7 +467,7 @@ class XianyuLive:
except Exception as e:
logger.error(f"{self.cookie_id}】清理过期锁时发生错误: {self._safe_str(e)}")
def _is_auto_delivery_trigger(self, message: str) -> bool:
"""检查消息是否为自动发货触发关键字"""
@ -842,6 +807,9 @@ class XianyuLive:
Args:
captcha_retry_count: 滑块验证重试次数用于防止无限递归
"""
# 初始化通知发送标志,避免重复发送通知
notification_sent = False
try:
logger.info(f"{self.cookie_id}】开始刷新token... (滑块验证重试次数: {captcha_retry_count})")
# 标记本次刷新状态
@ -856,6 +824,7 @@ class XianyuLive:
f"滑块验证重试次数已达上限,请手动处理",
"captcha_max_retries_exceeded"
)
notification_sent = True
return None
# 【消息接收检查】检查是否在消息接收后的冷却时间内,与 cookie_refresh_loop 保持一致
@ -1038,6 +1007,7 @@ class XianyuLive:
f"滑块验证成功但cookies更新失败",
"captcha_cookies_update_failed"
)
notification_sent = True
else:
logger.error(f"{self.cookie_id}】滑块验证失败")
@ -1060,6 +1030,9 @@ class XianyuLive:
f"滑块验证失败,请检查网络连接或手动处理",
"captcha_verification_failed"
)
# 标记已发送通知,避免后续重复发送
notification_sent = True
except Exception as captcha_e:
logger.error(f"{self.cookie_id}】滑块验证处理异常: {self._safe_str(captcha_e)}")
@ -1084,6 +1057,9 @@ class XianyuLive:
f"滑块验证处理异常: {str(captcha_e)}",
"captcha_verification_exception"
)
# 标记已发送通知,避免后续重复发送
notification_sent = True
# 检查是否包含"令牌过期"或"Session过期"
if isinstance(res_json, dict):
@ -1132,10 +1108,11 @@ class XianyuLive:
# 清空当前token确保下次重试时重新获取
self.current_token = None
# 发送Token刷新失败通知
await self.send_token_refresh_notification(f"Token刷新失败: {res_json}", "token_refresh_failed")
# 标记为失败
self.last_token_refresh_status = "failed"
# 只有在没有发送过通知的情况下才发送Token刷新失败通知
if not notification_sent:
await self.send_token_refresh_notification(f"Token刷新失败: {res_json}", "token_refresh_failed")
else:
logger.info(f"{self.cookie_id}】已发送滑块验证相关通知跳过Token刷新失败通知")
return None
except Exception as e:
@ -1144,10 +1121,11 @@ class XianyuLive:
# 清空当前token确保下次重试时重新获取
self.current_token = None
# 发送Token刷新异常通知
await self.send_token_refresh_notification(f"Token刷新异常: {str(e)}", "token_refresh_exception")
# 标记为异常失败
self.last_token_refresh_status = "failed_exception"
# 只有在没有发送过通知的情况下才发送Token刷新异常通知
if not notification_sent:
await self.send_token_refresh_notification(f"Token刷新异常: {str(e)}", "token_refresh_exception")
else:
logger.info(f"{self.cookie_id}】已发送滑块验证相关通知跳过Token刷新异常通知")
return None
def _need_captcha_verification(self, res_json: dict) -> bool:
@ -1220,10 +1198,10 @@ class XianyuLive:
logger.info(f"{self.cookie_id}】验证URL: {verification_url}")
# 使用增强反检测滑块验证器(独立实例,解决并发冲突)
# 使用滑块验证器(独立实例,解决并发冲突)
try:
from utils.xianyu_slider_stealth import XianyuSliderStealth
logger.info(f"{self.cookie_id}】XianyuSliderStealth导入成功使用增强反检测滑块验证")
logger.info(f"{self.cookie_id}】XianyuSliderStealth导入成功使用滑块验证")
# 创建独立的滑块验证实例(每个用户独立实例,避免并发冲突)
slider_stealth = XianyuSliderStealth(
@ -1245,7 +1223,7 @@ class XianyuLive:
)
if success and cookies:
logger.info(f"{self.cookie_id}增强反检测滑块验证成功获取到新的cookies")
logger.info(f"{self.cookie_id}滑块验证成功获取到新的cookies")
# 只提取x5sec相关的cookie值进行更新
updated_cookies = self.cookies.copy() # 复制现有cookies
@ -1294,7 +1272,7 @@ class XianyuLive:
await self.update_config_cookies()
logger.info(f"{self.cookie_id}】滑块验证成功后数据库cookies已自动更新")
# 记录成功更新到日志文件包含x5相关的cookie信息
x5sec_cookies_str = "; ".join([f"{k}={v}" for k, v in x5sec_cookies.items()]) if x5sec_cookies else ""
log_captcha_event(self.cookie_id, "滑块验证成功并自动更新数据库", True,
@ -1326,15 +1304,15 @@ class XianyuLive:
return cookies_str
else:
logger.error(f"{self.cookie_id}增强反检测滑块验证失败")
logger.error(f"{self.cookie_id}滑块验证失败")
# 记录滑块验证失败到日志文件
log_captcha_event(self.cookie_id, "增强反检测滑块验证失败", False,
log_captcha_event(self.cookie_id, "滑块验证失败", False,
f"XianyuSliderStealth执行失败, 环境: {'Docker' if os.getenv('DOCKER_ENV') else '本地'}")
# 发送通知
await self.send_token_refresh_notification(
f"增强反检测滑块验证失败需要手动处理。验证URL: {verification_url}",
f"滑块验证失败需要手动处理。验证URL: {verification_url}",
"captcha_verification_failed"
)
return None
@ -1355,10 +1333,10 @@ class XianyuLive:
return None
except Exception as stealth_e:
logger.error(f"{self.cookie_id}增强反检测滑块验证异常: {self._safe_str(stealth_e)}")
logger.error(f"{self.cookie_id}滑块验证异常: {self._safe_str(stealth_e)}")
# 记录异常到日志文件
log_captcha_event(self.cookie_id, "增强反检测滑块验证异常", False,
log_captcha_event(self.cookie_id, "滑块验证异常", False,
f"执行异常, 错误: {self._safe_str(stealth_e)[:100]}")
# 发送通知
@ -1666,8 +1644,46 @@ class XianyuLive:
playwright = await async_playwright().start()
# 启动浏览器(使用统一的优化配置)
browser_args = self._get_browser_args()
# 启动浏览器参照order_detail_fetcher的配置
browser_args = [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-accelerated-2d-canvas',
'--no-first-run',
'--no-zygote',
'--disable-gpu',
'--disable-background-timer-throttling',
'--disable-backgrounding-occluded-windows',
'--disable-renderer-backgrounding',
'--disable-features=TranslateUI',
'--disable-ipc-flooding-protection',
'--disable-extensions',
'--disable-default-apps',
'--disable-sync',
'--disable-translate',
'--hide-scrollbars',
'--mute-audio',
'--no-default-browser-check',
'--no-pings'
]
# 在Docker环境中添加额外参数
if os.getenv('DOCKER_ENV'):
browser_args.extend([
# '--single-process', # 注释掉,避免多用户并发时的进程冲突和资源泄漏
'--disable-background-networking',
'--disable-client-side-phishing-detection',
'--disable-hang-monitor',
'--disable-popup-blocking',
'--disable-prompt-on-repost',
'--disable-web-resources',
'--metrics-recording-only',
'--safebrowsing-disable-auto-update',
'--enable-automation',
'--password-store=basic',
'--use-mock-keychain'
])
browser = await playwright.chromium.launch(
headless=True,
@ -3284,7 +3300,7 @@ class XianyuLive:
async with order_detail_lock:
logger.info(f"🔍 【{self.cookie_id}】获取订单详情锁 {order_id},开始处理...")
try:
logger.info(f"{self.cookie_id}】开始获取订单详情: {order_id}")
@ -3329,6 +3345,7 @@ class XianyuLive:
if not cookie_info:
logger.warning(f"Cookie ID {self.cookie_id} 不存在于cookies表中丢弃订单 {order_id}")
else:
# 先保存订单基本信息
success = db_manager.insert_or_update_order(
order_id=order_id,
item_id=item_id,
@ -3337,9 +3354,31 @@ class XianyuLive:
spec_value=spec_value,
quantity=quantity,
amount=amount,
order_status='processed', # 已处理状态
cookie_id=self.cookie_id
)
# 使用订单状态处理器设置状态
logger.info(f"{self.cookie_id}】检查订单状态处理器调用条件: success={success}, handler_exists={self.order_status_handler is not None}")
if success and self.order_status_handler:
logger.info(f"{self.cookie_id}】准备调用订单状态处理器.handle_order_detail_fetched_status: {order_id}")
try:
result = self.order_status_handler.handle_order_detail_fetched_status(
order_id=order_id,
cookie_id=self.cookie_id,
context="订单详情已拉取"
)
logger.info(f"{self.cookie_id}】订单状态处理器.handle_order_detail_fetched_status返回结果: {result}")
# 处理待处理队列
logger.info(f"{self.cookie_id}】准备调用订单状态处理器.on_order_details_fetched: {order_id}")
self.order_status_handler.on_order_details_fetched(order_id)
logger.info(f"{self.cookie_id}】订单状态处理器.on_order_details_fetched调用成功: {order_id}")
except Exception as e:
logger.error(f"{self.cookie_id}】订单状态处理器调用失败: {self._safe_str(e)}")
import traceback
logger.error(f"{self.cookie_id}】详细错误信息: {traceback.format_exc()}")
else:
logger.warning(f"{self.cookie_id}】订单状态处理器调用条件不满足: success={success}, handler_exists={self.order_status_handler is not None}")
if success:
logger.info(f"{self.cookie_id}】订单信息已保存到数据库: {order_id}")
@ -3593,14 +3632,26 @@ class XianyuLive:
existing_order = db_manager.get_order_by_id(order_id)
if not existing_order:
# 插入基本订单信息
db_manager.insert_or_update_order(
success = db_manager.insert_or_update_order(
order_id=order_id,
item_id=item_id,
buyer_id=send_user_id,
order_status='processing', # 处理中状态
cookie_id=self.cookie_id
)
logger.info(f"保存基本订单信息到数据库: {order_id}")
# 使用订单状态处理器设置状态
if success and self.order_status_handler:
try:
self.order_status_handler.handle_order_basic_info_status(
order_id=order_id,
cookie_id=self.cookie_id,
context="自动发货-基本信息"
)
except Exception as e:
logger.error(f"{self.cookie_id}】订单状态处理器调用失败: {self._safe_str(e)}")
if success:
logger.info(f"保存基本订单信息到数据库: {order_id}")
except Exception as db_e:
logger.error(f"保存基本订单信息失败: {self._safe_str(db_e)}")
@ -4332,8 +4383,46 @@ class XianyuLive:
logger.error(f"{target_cookie_id}】Playwright启动超时")
return False
# 启动浏览器(使用统一的优化配置)
browser_args = self._get_browser_args()
# 启动浏览器(参照商品搜索的配置)
browser_args = [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-accelerated-2d-canvas',
'--no-first-run',
'--no-zygote',
'--disable-gpu',
'--disable-background-timer-throttling',
'--disable-backgrounding-occluded-windows',
'--disable-renderer-backgrounding',
'--disable-features=TranslateUI',
'--disable-ipc-flooding-protection',
'--disable-extensions',
'--disable-default-apps',
'--disable-sync',
'--disable-translate',
'--hide-scrollbars',
'--mute-audio',
'--no-default-browser-check',
'--no-pings'
]
# 在Docker环境中添加额外参数
if os.getenv('DOCKER_ENV'):
browser_args.extend([
# '--single-process', # 注释掉,避免多用户并发时的进程冲突和资源泄漏
'--disable-background-networking',
'--disable-client-side-phishing-detection',
'--disable-hang-monitor',
'--disable-popup-blocking',
'--disable-prompt-on-repost',
'--disable-web-resources',
'--metrics-recording-only',
'--safebrowsing-disable-auto-update',
'--enable-automation',
'--password-store=basic',
'--use-mock-keychain'
])
# 使用无头浏览器
browser = await playwright.chromium.launch(
@ -4637,8 +4726,46 @@ class XianyuLive:
logger.error(f"{self.cookie_id}】Playwright启动超时")
return False
# 启动浏览器(使用统一的优化配置)
browser_args = self._get_browser_args()
# 启动浏览器(参照商品搜索的配置)
browser_args = [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-accelerated-2d-canvas',
'--no-first-run',
'--no-zygote',
'--disable-gpu',
'--disable-background-timer-throttling',
'--disable-backgrounding-occluded-windows',
'--disable-renderer-backgrounding',
'--disable-features=TranslateUI',
'--disable-ipc-flooding-protection',
'--disable-extensions',
'--disable-default-apps',
'--disable-sync',
'--disable-translate',
'--hide-scrollbars',
'--mute-audio',
'--no-default-browser-check',
'--no-pings'
]
# 在Docker环境中添加额外参数
if os.getenv('DOCKER_ENV'):
browser_args.extend([
# '--single-process', # 注释掉,避免多用户并发时的进程冲突和资源泄漏
'--disable-background-networking',
'--disable-client-side-phishing-detection',
'--disable-hang-monitor',
'--disable-popup-blocking',
'--disable-prompt-on-repost',
'--disable-web-resources',
'--metrics-recording-only',
'--safebrowsing-disable-auto-update',
'--enable-automation',
'--password-store=basic',
'--use-mock-keychain'
])
# Cookie刷新模式使用无头浏览器
browser = await playwright.chromium.launch(
@ -4816,17 +4943,81 @@ class XianyuLive:
logger.error(f"{self.cookie_id}】通过浏览器刷新Cookie失败: {self._safe_str(e)}")
return False
finally:
# 确保资源清理
# 异步关闭浏览器:创建清理任务,超时后强制关闭
try:
if 'browser' in locals() and browser:
await browser.close()
logger.debug(f"{self.cookie_id}】浏览器已关闭")
if 'playwright' in locals() and playwright:
await playwright.stop()
logger.debug(f"{self.cookie_id}】Playwright已停止")
asyncio.create_task(self._async_close_browser(browser, playwright))
logger.info(f"{self.cookie_id}】浏览器异步关闭任务已启动") # 改为info级别确保能看到
except Exception as cleanup_e:
logger.warning(f"{self.cookie_id}清理浏览器资源时出错: {self._safe_str(cleanup_e)}")
logger.warning(f"{self.cookie_id}】创建浏览器关闭任务时出错: {self._safe_str(cleanup_e)}")
async def _async_close_browser(self, browser, playwright):
"""异步关闭:正常关闭,超时后强制关闭"""
try:
logger.info(f"{self.cookie_id}】开始异步关闭浏览器...") # 改为info级别
# 正常关闭,设置超时
await asyncio.wait_for(
self._normal_close_resources(browser, playwright),
timeout=10.0
)
logger.info(f"{self.cookie_id}】浏览器正常关闭完成") # 改为info级别
except asyncio.TimeoutError:
logger.warning(f"{self.cookie_id}】正常关闭超时,开始强制关闭...")
await self._force_close_resources(browser, playwright)
except Exception as e:
logger.warning(f"{self.cookie_id}】异步关闭时出错,强制关闭: {self._safe_str(e)}")
await self._force_close_resources(browser, playwright)
async def _normal_close_resources(self, browser, playwright):
"""正常关闭资源:浏览器+Playwright短超时关闭"""
try:
# 关闭浏览器
if browser:
try:
await browser.close()
logger.info(f"{self.cookie_id}】浏览器关闭完成")
except Exception as e:
logger.warning(f"{self.cookie_id}】关闭浏览器时出错: {e}")
# 关闭Playwright使用非常短的超时如果超时就放弃
if playwright:
try:
logger.info(f"{self.cookie_id}】正在关闭Playwright...")
await asyncio.wait_for(playwright.stop(), timeout=2.0)
logger.info(f"{self.cookie_id}】Playwright关闭完成")
except asyncio.TimeoutError:
logger.warning(f"{self.cookie_id}】Playwright关闭超时将自动清理")
except Exception as e:
logger.warning(f"{self.cookie_id}】关闭Playwright时出错: {e}")
except Exception as e:
logger.error(f"{self.cookie_id}】正常关闭时出现异常: {e}")
raise
async def _force_close_resources(self, browser, playwright):
"""强制关闭资源:强制关闭浏览器+Playwright超时等待"""
try:
logger.warning(f"{self.cookie_id}】开始强制关闭资源...")
# 强制关闭浏览器+Playwright设置短超时
force_tasks = []
if browser:
force_tasks.append(asyncio.wait_for(browser.close(), timeout=2.0))
if playwright:
force_tasks.append(asyncio.wait_for(playwright.stop(), timeout=2.0))
if force_tasks:
# 使用gather执行所有失败都会被忽略
await asyncio.gather(*force_tasks, return_exceptions=True)
logger.info(f"{self.cookie_id}】强制关闭完成")
else:
logger.info(f"{self.cookie_id}】没有需要强制关闭的资源")
except Exception as e:
logger.warning(f"{self.cookie_id}】强制关闭时出现异常(已忽略): {e}")
async def send_msg_once(self, toid, item_id, text):
headers = {
@ -5111,6 +5302,19 @@ class XianyuLive:
msg_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
logger.info(f'[{msg_time}] 【{self.cookie_id}】✅ 检测到订单ID: {order_id},开始获取订单详情')
# 通知订单状态处理器订单ID已提取
if self.order_status_handler:
logger.info(f"{self.cookie_id}】准备调用订单状态处理器.on_order_id_extracted: {order_id}")
try:
self.order_status_handler.on_order_id_extracted(order_id, self.cookie_id, message)
logger.info(f"{self.cookie_id}】订单状态处理器.on_order_id_extracted调用成功: {order_id}")
except Exception as e:
logger.error(f"{self.cookie_id}】通知订单状态处理器订单ID提取失败: {self._safe_str(e)}")
import traceback
logger.error(f"{self.cookie_id}】详细错误信息: {traceback.format_exc()}")
else:
logger.warning(f"{self.cookie_id}】订单状态处理器为None跳过订单ID提取通知: {order_id}")
# 立即获取订单详情信息
try:
# 先尝试提取用户ID和商品ID用于订单详情获取
@ -5278,6 +5482,45 @@ class XianyuLive:
# 【优先处理】使用订单状态处理器处理系统消息
if self.order_status_handler:
try:
# 处理系统消息的订单状态更新
try:
handled = self.order_status_handler.handle_system_message(
message=message,
send_message=send_message,
cookie_id=self.cookie_id,
msg_time=msg_time
)
except Exception as e:
logger.error(f"{self.cookie_id}】处理系统消息失败: {self._safe_str(e)}")
handled = False
# 处理红色提醒消息
if not handled:
try:
if isinstance(message, dict) and "3" in message and isinstance(message["3"], dict):
red_reminder = message["3"].get("redReminder")
user_id = message["3"].get("userId", "unknown")
if red_reminder:
try:
self.order_status_handler.handle_red_reminder_message(
message=message,
red_reminder=red_reminder,
user_id=user_id,
cookie_id=self.cookie_id,
msg_time=msg_time
)
except Exception as e:
logger.error(f"{self.cookie_id}】处理红色提醒消息失败: {self._safe_str(e)}")
except Exception as red_e:
logger.debug(f"处理红色提醒消息失败: {self._safe_str(red_e)}")
except Exception as e:
logger.error(f"订单状态处理失败: {self._safe_str(e)}")
# 【优先处理】检查系统消息和自动发货触发消息(不受人工接入暂停影响)
if send_message == '[我已拍下,待付款]':
logger.info(f'[{msg_time}] 【{self.cookie_id}】系统消息不处理')

131
check_silent_mode.py Normal file
View File

@ -0,0 +1,131 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
检查静默模式修改
"""
import os
def check_silent_modifications():
"""检查静默模式修改"""
print("🔍 检查静默模式修改...")
refresh_util_path = os.path.join('utils', 'refresh_util.py')
if not os.path.exists(refresh_util_path):
print(f"❌ 文件不存在: {refresh_util_path}")
return False
with open(refresh_util_path, 'r', encoding='utf-8') as f:
content = f.read()
# 检查是否移除了遮挡元素的CSS
removed_css_elements = [
'.slide-info {',
'.test-trace-btn {',
'.trace-status {'
]
css_removed = 0
for element in removed_css_elements:
if element not in content:
css_removed += 1
print(f"✅ 已移除CSS: {element.replace(' {', '')}")
else:
print(f"⚠️ 仍存在CSS: {element.replace(' {', '')}")
# 检查是否保留了必要的CSS
essential_css = [
'.mouse-trace {',
'.mouse-cursor {'
]
css_kept = 0
for element in essential_css:
if element in content:
css_kept += 1
print(f"✅ 保留了CSS: {element.replace(' {', '')}")
else:
print(f"❌ 缺少CSS: {element.replace(' {', '')}")
# 检查静默函数
silent_functions = [
('createStatusIndicator', '静默状态提示'),
('createInfoPanel', '静默信息面板'),
('createTestButton', '静默测试按钮')
]
silent_count = 0
for func_name, description in silent_functions:
if f'function {func_name}()' in content:
# 检查是否包含静默相关的注释或代码
func_start = content.find(f'function {func_name}()')
if func_start != -1:
# 查找函数结束位置(简单查找下一个函数或大段空白)
func_end = content.find('function ', func_start + 1)
if func_end == -1:
func_end = len(content)
func_content = content[func_start:func_end]
if '静默' in func_content or 'console.log' in func_content:
silent_count += 1
print(f"{description}: 已改为静默模式")
else:
print(f"⚠️ {description}: 可能未完全静默化")
else:
print(f"❌ 函数不存在: {func_name}")
# 检查控制台输出
if '静默模式' in content:
print("✅ 控制台输出已改为静默模式")
else:
print("⚠️ 控制台输出可能未修改")
# 总结
print(f"\n📊 修改总结:")
print(f" - CSS移除: {css_removed}/{len(removed_css_elements)}")
print(f" - CSS保留: {css_kept}/{len(essential_css)}")
print(f" - 函数静默化: {silent_count}/{len(silent_functions)}")
success = (css_removed == len(removed_css_elements) and
css_kept == len(essential_css) and
silent_count >= len(silent_functions) - 1) # 允许一个函数的小差异
return success
def show_before_after():
"""显示修改前后对比"""
print("\n📋 修改前后对比")
print("-" * 60)
print("🔴 修改前 - 遮挡页面的元素:")
print(" - 右上角绿色信息面板显示统计信息")
print(" - 左上角橙色测试按钮")
print(" - 页面中央红色状态提示")
print(" - '鼠标轨迹可视化已启用' 大字提示")
print(" - '移动鼠标查看轨迹效果' 说明文字")
print("\n🟢 修改后 - 静默模式:")
print(" - 只显示红色鼠标轨迹点")
print(" - 只显示绿色鼠标光标")
print(" - 所有文字提示和按钮都已移除")
print(" - 不再遮挡页面内容")
print(" - 保持轨迹可视化的核心功能")
if __name__ == "__main__":
print("🚀 静默模式修改检查")
print("=" * 50)
success = check_silent_modifications()
if success:
print("\n🎉 静默模式修改检查通过!")
else:
print("\n⚠️ 静默模式修改可能不完整")
show_before_after()
print("\n" + "=" * 50)
print("✅ 修改完成!现在鼠标轨迹可视化不会再遮挡页面了")

View File

@ -93,10 +93,14 @@ COOKIES_STR = config.get('COOKIES.value', '')
COOKIES_LAST_UPDATE = config.get('COOKIES.last_update_time', '')
WEBSOCKET_URL = config.get('WEBSOCKET_URL', 'wss://wss-goofish.dingtalk.com/')
HEARTBEAT_INTERVAL = config.get('HEARTBEAT_INTERVAL', 15)
HEARTBEAT_TIMEOUT = config.get('HEARTBEAT_TIMEOUT', 5)
TOKEN_REFRESH_INTERVAL = config.get('TOKEN_REFRESH_INTERVAL', 3600)
TOKEN_RETRY_INTERVAL = config.get('TOKEN_RETRY_INTERVAL', 300)
HEARTBEAT_TIMEOUT = config.get('HEARTBEAT_TIMEOUT', 30)
TOKEN_REFRESH_INTERVAL = config.get('TOKEN_REFRESH_INTERVAL', 72000)
TOKEN_RETRY_INTERVAL = config.get('TOKEN_RETRY_INTERVAL', 7200)
MESSAGE_EXPIRE_TIME = config.get('MESSAGE_EXPIRE_TIME', 300000)
SLIDER_VERIFICATION = config.get('SLIDER_VERIFICATION', {
'max_concurrent': 3,
'wait_timeout': 60
})
API_ENDPOINTS = config.get('API_ENDPOINTS', {})
DEFAULT_HEADERS = config.get('DEFAULT_HEADERS', {})
WEBSOCKET_HEADERS = config.get('WEBSOCKET_HEADERS', {})
@ -118,4 +122,4 @@ if isinstance(_cookies_raw, list):
else:
# 兼容旧格式,仅有 value 字段
val = _cookies_raw.get('value') if isinstance(_cookies_raw, dict) else None
COOKIES_LIST = [{'id': 'default', 'value': val}] if val else []
COOKIES_LIST = [{'id': 'default', 'value': val}] if val else []

View File

@ -119,8 +119,8 @@ start_services() {
# 停止服务
stop_services() {
print_info "停止服务..."
docker-compose stop
print_success "服务已停止(未删除容器和镜像)"
docker-compose down
print_success "服务已停止"
}
# 重启服务
@ -249,19 +249,19 @@ update_deployment() {
# 清理环境
cleanup() {
print_warning "这将停止服务并清理数据目录,但不会删除容器和镜像,确定要继续吗?(y/N)"
print_warning "这将删除所有容器、镜像和数据,确定要继续吗?(y/N)"
read -r response
if [[ "$response" =~ ^[Yy]$ ]]; then
print_info "清理环境(保留容器与镜像)..."
# 仅停止容器,保留历史容器与镜像
docker-compose stop || true
# 删除数据目录(如需保留数据,可注释下行)
print_info "清理环境..."
# 停止并删除容器
docker-compose down -v --rmi all
# 删除数据目录
rm -rf data logs backups
print_success "环境清理完成(容器与镜像已保留)"
print_success "环境清理完成"
else
print_info "取消清理操作"
fi

View File

@ -2,6 +2,9 @@
echo "Starting xianyu-auto-reply system..."
# Disable core dumps to prevent core files generation
ulimit -c 0
# Create necessary directories
mkdir -p /app/data /app/logs /app/backups /app/static/uploads/images

1044
order_status_handler.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -642,7 +642,9 @@
<option value="">所有状态</option>
<option value="processing">处理中</option>
<option value="processed">已处理</option>
<option value="shipped">已发货</option>
<option value="completed">已完成</option>
<option value="cancelled">已关闭</option>
<option value="unknown">未知</option>
</select>
</div>

View File

@ -8624,6 +8624,7 @@ function getOrderStatusClass(status) {
'processing': 'bg-warning text-dark',
'processed': 'bg-info text-white',
'completed': 'bg-success text-white',
'cancelled': 'bg-danger text-white',
'unknown': 'bg-secondary text-white'
};
return statusMap[status] || 'bg-secondary text-white';
@ -8634,7 +8635,9 @@ function getOrderStatusText(status) {
const statusMap = {
'processing': '处理中',
'processed': '已处理',
'shipped': '已发货',
'completed': '已完成',
'cancelled': '已关闭',
'unknown': '未知'
};
return statusMap[status] || '未知';

View File

@ -131,7 +131,8 @@ class XianyuSearcher:
if os.getenv('DOCKER_ENV') == 'true':
browser_args.extend([
'--disable-gpu',
'--use-gl=swiftshader'
# 移除--single-process参数使用多进程模式提高稳定性
# '--single-process' # 注释掉,避免崩溃
])
logger.info("正在启动浏览器...")
@ -154,11 +155,19 @@ class XianyuSearcher:
async def close_browser(self):
"""关闭浏览器"""
if self.browser:
await self.browser.close()
self.browser = None
self.context = None
self.page = None
try:
if self.page:
await self.page.close()
self.page = None
if self.context:
await self.context.close()
self.context = None
if self.browser:
await self.browser.close()
self.browser = None
logger.debug("商品搜索器浏览器已关闭")
except Exception as e:
logger.warning(f"关闭商品搜索器浏览器时出错: {e}")
async def search_items(self, keyword: str, page: int = 1, page_size: int = 20) -> Dict[str, Any]:
"""

View File

@ -86,7 +86,6 @@ class OrderDetailFetcher:
'--no-first-run',
'--no-zygote',
'--disable-gpu',
'--use-gl=swiftshader',
'--disable-background-timer-throttling',
'--disable-backgrounding-occluded-windows',
'--disable-renderer-backgrounding',
@ -102,6 +101,10 @@ class OrderDetailFetcher:
'--no-pings'
]
# 移除--single-process参数使用多进程模式提高稳定性
# if os.getenv('DOCKER_ENV'):
# browser_args.append('--single-process') # 注释掉,避免崩溃
# 在Docker环境中添加额外参数
if os.getenv('DOCKER_ENV'):
browser_args.extend([
@ -119,7 +122,18 @@ class OrderDetailFetcher:
'--safebrowsing-disable-auto-update',
'--enable-automation',
'--password-store=basic',
'--use-mock-keychain'
'--use-mock-keychain',
# 添加内存优化和稳定性参数
'--memory-pressure-off',
'--max_old_space_size=512',
'--disable-ipc-flooding-protection',
'--disable-component-extensions-with-background-pages',
'--disable-features=TranslateUI,BlinkGenPropertyTrees',
'--disable-logging',
'--disable-permissions-api',
'--disable-notifications',
'--no-pings',
'--no-zygote'
])
logger.info(f"启动浏览器,参数: {browser_args}")

View File

@ -7,7 +7,6 @@
import time
import random
import logging
import json
import os
import threading
@ -15,24 +14,19 @@ import tempfile
import shutil
from playwright.sync_api import sync_playwright, ElementHandle
from typing import Optional, Tuple, List, Dict, Any
from loguru import logger
# 导入配置
try:
from config import config
SLIDER_MAX_CONCURRENT = config.get('SLIDER_VERIFICATION.max_concurrent', 3)
SLIDER_WAIT_TIMEOUT = config.get('SLIDER_VERIFICATION.wait_timeout', 60)
from config import SLIDER_VERIFICATION
SLIDER_MAX_CONCURRENT = SLIDER_VERIFICATION.get('max_concurrent', 3)
SLIDER_WAIT_TIMEOUT = SLIDER_VERIFICATION.get('wait_timeout', 60)
except ImportError:
# 如果无法导入配置,使用默认值
SLIDER_MAX_CONCURRENT = 3
SLIDER_WAIT_TIMEOUT = 60
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s | %(levelname)s | %(message)s',
datefmt='%Y-%m-%d %H:%M:%S.%f'[:-3]
)
logger = logging.getLogger(__name__)
# 使用loguru日志库与主程序保持一致
# 全局并发控制
class SliderConcurrencyManager:
@ -79,7 +73,9 @@ class SliderConcurrencyManager:
with self.instance_lock:
if user_id not in self.waiting_queue:
self.waiting_queue.append(user_id)
logger.info(f"用户 {user_id} 进入等待队列,当前队列长度: {len(self.waiting_queue)}")
# 提取纯用户ID用于日志显示
pure_user_id = self._extract_pure_user_id(user_id)
logger.info(f"{pure_user_id}】进入等待队列,当前队列长度: {len(self.waiting_queue)}")
# 等待1秒后重试
time.sleep(1)
@ -88,7 +84,9 @@ class SliderConcurrencyManager:
with self.instance_lock:
if user_id in self.waiting_queue:
self.waiting_queue.remove(user_id)
logger.warning(f"用户 {user_id} 等待超时,从队列中移除")
# 提取纯用户ID用于日志显示
pure_user_id = self._extract_pure_user_id(user_id)
logger.warning(f"{pure_user_id}】等待超时,从队列中移除")
return False
@ -108,7 +106,24 @@ class SliderConcurrencyManager:
with self.instance_lock:
if user_id in self.active_instances:
del self.active_instances[user_id]
logger.info(f"用户 {user_id} 实例已注销,当前活跃: {len(self.active_instances)}")
# 提取纯用户ID用于日志显示
pure_user_id = self._extract_pure_user_id(user_id)
logger.info(f"{pure_user_id}】实例已注销,当前活跃: {len(self.active_instances)}")
def _extract_pure_user_id(self, user_id: str) -> str:
"""提取纯用户ID移除时间戳部分"""
if '_' in user_id:
# 检查最后一部分是否为数字(时间戳)
parts = user_id.split('_')
if len(parts) >= 2 and parts[-1].isdigit() and len(parts[-1]) >= 10:
# 最后一部分是时间戳,移除它
return '_'.join(parts[:-1])
else:
# 不是时间戳格式使用原始ID
return user_id
else:
# 没有下划线,直接使用
return user_id
def get_stats(self):
"""获取统计信息"""
@ -134,24 +149,28 @@ class XianyuSliderStealth:
self.context = None
self.playwright = None
# 提取纯用户ID移除时间戳部分
self.pure_user_id = concurrency_manager._extract_pure_user_id(user_id)
# 为每个实例创建独立的临时目录
self.temp_dir = tempfile.mkdtemp(prefix=f"slider_{user_id}_")
logger.debug(f"用户 {self.user_id} 创建临时目录: {self.temp_dir}")
logger.debug(f"{self.pure_user_id}创建临时目录: {self.temp_dir}")
# 等待可用槽位(排队机制)
logger.info(f"用户 {self.user_id} 检查并发限制...")
logger.info(f"{self.pure_user_id}检查并发限制...")
if not concurrency_manager.wait_for_slot(self.user_id):
stats = concurrency_manager.get_stats()
logger.error(f"用户 {self.user_id} 等待槽位超时,当前活跃: {stats['active_count']}/{stats['max_concurrent']}")
logger.error(f"{self.pure_user_id}等待槽位超时,当前活跃: {stats['active_count']}/{stats['max_concurrent']}")
raise Exception(f"滑块验证等待槽位超时,请稍后重试")
# 注册实例
concurrency_manager.register_instance(self.user_id, self)
stats = concurrency_manager.get_stats()
logger.info(f"用户 {self.user_id} 实例已注册,当前并发: {stats['active_count']}/{stats['max_concurrent']}")
logger.info(f"{self.pure_user_id}实例已注册,当前并发: {stats['active_count']}/{stats['max_concurrent']}")
# 轨迹学习相关属性
self.success_history_file = f"trajectory_history/{user_id}_success.json"
self.success_history_file = f"trajectory_history/{self.pure_user_id}_success.json"
self.trajectory_params = {
"total_steps_range": [50, 80],
"base_delay_range": [0.05, 0.12],
@ -252,7 +271,7 @@ class XianyuSliderStealth:
return self.page
except Exception as e:
logger.error(f"用户 {self.user_id} 初始化浏览器失败: {e}")
logger.error(f"{self.pure_user_id}初始化浏览器失败: {e}")
# 确保在异常时也清理已创建的资源
self._cleanup_on_init_failure()
raise
@ -264,28 +283,28 @@ class XianyuSliderStealth:
self.page.close()
self.page = None
except Exception as e:
logger.warning(f"用户 {self.user_id} 清理页面时出错: {e}")
logger.warning(f"{self.pure_user_id}清理页面时出错: {e}")
try:
if hasattr(self, 'context') and self.context:
self.context.close()
self.context = None
except Exception as e:
logger.warning(f"用户 {self.user_id} 清理上下文时出错: {e}")
logger.warning(f"{self.pure_user_id}清理上下文时出错: {e}")
try:
if hasattr(self, 'browser') and self.browser:
self.browser.close()
self.browser = None
except Exception as e:
logger.warning(f"用户 {self.user_id} 清理浏览器时出错: {e}")
logger.warning(f"{self.pure_user_id}清理浏览器时出错: {e}")
try:
if hasattr(self, 'playwright') and self.playwright:
self.playwright.stop()
self.playwright = None
except Exception as e:
logger.warning(f"用户 {self.user_id} 清理Playwright时出错: {e}")
logger.warning(f"{self.pure_user_id}清理Playwright时出错: {e}")
def _load_success_history(self) -> List[Dict[str, Any]]:
"""加载历史成功数据"""
@ -295,10 +314,10 @@ class XianyuSliderStealth:
with open(self.success_history_file, 'r', encoding='utf-8') as f:
history = json.load(f)
logger.info(f"用户 {self.user_id} 加载历史成功数据: {len(history)}条记录")
logger.info(f"{self.pure_user_id}加载历史成功数据: {len(history)}条记录")
return history
except Exception as e:
logger.warning(f"用户 {self.user_id} 加载历史数据失败: {e}")
logger.warning(f"{self.pure_user_id}加载历史数据失败: {e}")
return []
def _save_success_record(self, trajectory_data: Dict[str, Any]):
@ -313,7 +332,7 @@ class XianyuSliderStealth:
# 添加新记录 - 包含完整轨迹数据
record = {
"timestamp": time.time(),
"user_id": self.user_id,
"user_id": self.pure_user_id,
"distance": trajectory_data.get("distance", 0),
"total_steps": trajectory_data.get("total_steps", 0),
"base_delay": trajectory_data.get("base_delay", 0),
@ -340,10 +359,10 @@ class XianyuSliderStealth:
with open(self.success_history_file, 'w', encoding='utf-8') as f:
json.dump(history, f, ensure_ascii=False, indent=2)
logger.info(f"用户 {self.user_id} 保存成功记录: 距离{record['distance']}px, 步数{record['total_steps']}, 轨迹点{len(record['trajectory_points'])}")
logger.info(f"{self.pure_user_id}保存成功记录: 距离{record['distance']}px, 步数{record['total_steps']}, 轨迹点{len(record['trajectory_points'])}")
except Exception as e:
logger.error(f"用户 {self.user_id} 保存成功记录失败: {e}")
logger.error(f"{self.pure_user_id}保存成功记录失败: {e}")
def _optimize_trajectory_params(self) -> Dict[str, Any]:
"""基于历史成功数据优化轨迹参数"""
@ -353,7 +372,7 @@ class XianyuSliderStealth:
history = self._load_success_history()
if len(history) < 3: # 至少需要3条成功记录才开始优化
logger.info(f"用户 {self.user_id} 历史成功数据不足({len(history)}条),使用默认参数")
logger.info(f"{self.pure_user_id}历史成功数据不足({len(history)}条),使用默认参数")
return self.trajectory_params
# 计算成功记录的平均值
@ -420,26 +439,26 @@ class XianyuSliderStealth:
"learning_enabled": True
}
logger.info(f"用户 {self.user_id} 基于{len(history)}条成功记录优化轨迹参数")
logger.info(f"{self.pure_user_id}基于{len(history)}条成功记录优化轨迹参数")
return optimized_params
except Exception as e:
logger.error(f"用户 {self.user_id} 优化轨迹参数失败: {e}")
logger.error(f"{self.pure_user_id}优化轨迹参数失败: {e}")
return self.trajectory_params
def _get_cookies_after_success(self):
"""滑块验证成功后获取cookie"""
try:
logger.info(f"用户 {self.user_id} 开始获取滑块验证成功后的页面cookie...")
logger.info(f"{self.pure_user_id}开始获取滑块验证成功后的页面cookie...")
# 检查当前页面URL
current_url = self.page.url
logger.info(f"用户 {self.user_id} 当前页面URL: {current_url}")
logger.info(f"{self.pure_user_id}当前页面URL: {current_url}")
# 检查页面标题
page_title = self.page.title()
logger.info(f"用户 {self.user_id} 当前页面标题: {page_title}")
logger.info(f"{self.pure_user_id}当前页面标题: {page_title}")
# 等待一下确保cookie完全更新
time.sleep(1)
@ -453,44 +472,35 @@ class XianyuSliderStealth:
for cookie in cookies:
new_cookies[cookie['name']] = cookie['value']
logger.info(f"用户 {self.user_id} 滑块验证成功后已获取cookie{len(new_cookies)}个cookie")
logger.info(f"{self.pure_user_id}滑块验证成功后已获取cookie{len(new_cookies)}个cookie")
# 记录所有cookie的详细信息
logger.info(f"用户 {self.user_id} 获取到的所有cookie: {list(new_cookies.keys())}")
logger.info(f"{self.pure_user_id}获取到的所有cookie: {list(new_cookies.keys())}")
# 只提取指定的cookie: x5sec, _m_h5_tk, _m_h5_tk_enc, cookie2, t, sgcookie, unb, uc1, uc3, uc4
target_cookies = ['x5sec', '_m_h5_tk', '_m_h5_tk_enc', 'cookie2', 't']
# 只提取x5sec相关的cookie
filtered_cookies = {}
for cookie_name in target_cookies:
if cookie_name in new_cookies:
filtered_cookies[cookie_name] = new_cookies[cookie_name]
logger.info(f"用户 {self.user_id} 重要cookie已获取: {cookie_name} = {new_cookies[cookie_name]}")
else:
logger.warning(f"用户 {self.user_id} 重要cookie缺失: {cookie_name}")
# 筛选出x5相关的cookies包括x5sec, x5step等
for cookie_name, cookie_value in new_cookies.items():
cookie_name_lower = cookie_name.lower()
if cookie_name_lower.startswith('x5') or 'x5sec' in cookie_name_lower:
filtered_cookies[cookie_name] = cookie_value
logger.info(f"{self.pure_user_id}】x5相关cookie已获取: {cookie_name} = {cookie_value}")
# 检查是否有新的cookie值
old_cookies = getattr(self, '_old_cookies', {})
new_cookie_count = 0
for name, value in filtered_cookies.items():
if name not in old_cookies or old_cookies[name] != value:
new_cookie_count += 1
logger.info(f"用户 {self.user_id} 发现新/更新的cookie: {name} = {value}")
logger.info(f"用户 {self.user_id} 发现 {new_cookie_count} 个新/更新的目标cookie")
logger.info(f"{self.pure_user_id}】找到{len(filtered_cookies)}个x5相关cookies: {list(filtered_cookies.keys())}")
if filtered_cookies:
logger.info(f"用户 {self.user_id} 返回过滤后的cookie: {list(filtered_cookies.keys())}")
logger.info(f"{self.pure_user_id}】返回过滤后的x5相关cookie: {list(filtered_cookies.keys())}")
return filtered_cookies
else:
logger.warning(f"用户 {self.user_id} 未找到目标cookie (x5sec, _m_h5_tk, _m_h5_tk_enc, cookie2, t)")
logger.warning(f"{self.pure_user_id}】未找到x5相关cookie")
return None
else:
logger.warning(f"用户 {self.user_id} 未获取到任何cookie")
logger.warning(f"{self.pure_user_id}未获取到任何cookie")
return None
except Exception as e:
logger.error(f"用户 {self.user_id} 获取滑块验证成功后的cookie失败: {str(e)}")
logger.error(f"{self.pure_user_id}获取滑块验证成功后的cookie失败: {str(e)}")
return None
def _save_cookies_to_file(self, cookies):
@ -505,10 +515,10 @@ class XianyuSliderStealth:
with open(cookie_file, 'w', encoding='utf-8') as f:
json.dump(cookies, f, ensure_ascii=False, indent=2)
logger.info(f"用户 {self.user_id} Cookie已保存到文件: {cookie_file}")
logger.info(f"{self.pure_user_id}Cookie已保存到文件: {cookie_file}")
except Exception as e:
logger.error(f"用户 {self.user_id} 保存cookie到文件失败: {str(e)}")
logger.error(f"{self.pure_user_id}保存cookie到文件失败: {str(e)}")
def _get_random_browser_features(self):
"""获取随机浏览器特征"""
@ -865,25 +875,25 @@ class XianyuSliderStealth:
# 保存轨迹数据用于后续学习
self.current_trajectory_data = trajectory_data
logger.info(f"用户 {self.user_id} 生成优化轨迹: {len(trajectory)}步, 总距离: {distance}px, 缓慢拖动开始位置: {distance * slow_start_ratio:.1f}px")
logger.info(f"{self.pure_user_id}生成优化轨迹: {len(trajectory)}步, 总距离: {distance}px, 缓慢拖动开始位置: {distance * slow_start_ratio:.1f}px")
if self.enable_learning:
logger.info(f"用户 {self.user_id} 使用历史数据优化参数: 步数{optimized_params['total_steps_range']}, 延迟{optimized_params['base_delay_range']}")
logger.info(f"{self.pure_user_id}使用历史数据优化参数: 步数{optimized_params['total_steps_range']}, 延迟{optimized_params['base_delay_range']}")
return trajectory
except Exception as e:
logger.error(f"用户 {self.user_id} 生成轨迹时出错: {str(e)}")
logger.error(f"{self.pure_user_id}生成轨迹时出错: {str(e)}")
return []
def simulate_slide(self, slider_button: ElementHandle, trajectory):
"""模拟滑动 - 增强人类行为模拟"""
try:
logger.info(f"用户 {self.user_id} 开始模拟滑动...")
logger.info(f"{self.pure_user_id}开始模拟滑动...")
# 获取滑块按钮中心位置
button_box = slider_button.bounding_box()
if not button_box:
logger.error(f"用户 {self.user_id} 无法获取滑块按钮位置")
logger.error(f"{self.pure_user_id}无法获取滑块按钮位置")
return False
start_x = button_box["x"] + button_box["width"] / 2
@ -974,14 +984,14 @@ class XianyuSliderStealth:
except:
pass
logger.info(f"用户 {self.user_id} 滑动进度: {progress:.1f}% ({i+1}/{len(trajectory)}) - left: {left_value}{slow_phase}")
logger.info(f"{self.pure_user_id}滑动进度: {progress:.1f}% ({i+1}/{len(trajectory)}) - left: {left_value}{slow_phase}")
# 检查是否需要补全拖动到258px使用轨迹执行过程中记录的left值
try:
# 如果left值还没到258px需要补全
if final_left_px < 258:
remaining_distance = 258 - final_left_px
logger.info(f"用户 {self.user_id} 检测到需要补全拖动: 当前left={final_left_px}px, 还需{remaining_distance}px")
logger.info(f"{self.pure_user_id}检测到需要补全拖动: 当前left={final_left_px}px, 还需{remaining_distance}px")
# 继续之前的拖动轨迹进行补全
current_x = start_x + trajectory[-1][0] if trajectory else start_x
@ -1016,7 +1026,7 @@ class XianyuSliderStealth:
# 每步记录进度
if step % 2 == 0 or step == completion_steps - 1:
logger.info(f"用户 {self.user_id} 补全拖动进度: {progress*100:.1f}% ({step+1}/{completion_steps}) - 移动{move_distance:.1f}px")
logger.info(f"{self.pure_user_id}补全拖动进度: {progress*100:.1f}% ({step+1}/{completion_steps}) - 移动{move_distance:.1f}px")
# 更新轨迹数据
if hasattr(self, 'current_trajectory_data'):
@ -1026,10 +1036,10 @@ class XianyuSliderStealth:
self.current_trajectory_data["trajectory_points"].extend(completion_trajectory)
self.current_trajectory_data["final_left_px"] = 258 # 更新最终位置
logger.info(f"用户 {self.user_id} 补全拖动完成: 从{final_left_px}px补全到258px")
logger.info(f"{self.pure_user_id}补全拖动完成: 从{final_left_px}px补全到258px")
except Exception as e:
logger.warning(f"用户 {self.user_id} 检查补全拖动时出错: {str(e)}")
logger.warning(f"{self.pure_user_id}检查补全拖动时出错: {str(e)}")
# 稍微回退一点,模拟人类可能的过冲
if len(trajectory) > 0:
@ -1044,12 +1054,12 @@ class XianyuSliderStealth:
# 释放鼠标
self.page.mouse.up()
logger.info(f"用户 {self.user_id} 滑动完成")
logger.info(f"{self.pure_user_id}滑动完成")
return True
except Exception as e:
logger.error(f"用户 {self.user_id} 模拟滑动时出错: {str(e)}")
logger.error(f"{self.pure_user_id}模拟滑动时出错: {str(e)}")
return False
def find_slider_elements(self):
@ -1075,15 +1085,15 @@ class XianyuSliderStealth:
try:
element = self.page.wait_for_selector(selector, timeout=3000)
if element:
logger.info(f"用户 {self.user_id} 找到滑块容器: {selector}")
logger.info(f"{self.pure_user_id}找到滑块容器: {selector}")
slider_container = element
break
except Exception as e:
logger.debug(f"用户 {self.user_id} 选择器 {selector} 未找到: {e}")
logger.debug(f"{self.pure_user_id}选择器 {selector} 未找到: {e}")
continue
if not slider_container:
logger.error(f"用户 {self.user_id} 未找到任何滑块容器")
logger.error(f"{self.pure_user_id}未找到任何滑块容器")
return None, None, None
# 定义滑块按钮选择器
@ -1102,15 +1112,15 @@ class XianyuSliderStealth:
try:
element = self.page.wait_for_selector(selector, timeout=3000)
if element:
logger.info(f"用户 {self.user_id} 找到滑块按钮: {selector}")
logger.info(f"{self.pure_user_id}找到滑块按钮: {selector}")
slider_button = element
break
except Exception as e:
logger.debug(f"用户 {self.user_id} 选择器 {selector} 未找到: {e}")
logger.debug(f"{self.pure_user_id}选择器 {selector} 未找到: {e}")
continue
if not slider_button:
logger.error(f"用户 {self.user_id} 未找到任何滑块按钮")
logger.error(f"{self.pure_user_id}未找到任何滑块按钮")
return slider_container, None, None
# 定义滑块轨道选择器
@ -1128,21 +1138,21 @@ class XianyuSliderStealth:
try:
element = self.page.wait_for_selector(selector, timeout=3000)
if element:
logger.info(f"用户 {self.user_id} 找到滑块轨道: {selector}")
logger.info(f"{self.pure_user_id}找到滑块轨道: {selector}")
slider_track = element
break
except Exception as e:
logger.debug(f"用户 {self.user_id} 选择器 {selector} 未找到: {e}")
logger.debug(f"{self.pure_user_id}选择器 {selector} 未找到: {e}")
continue
if not slider_track:
logger.error(f"用户 {self.user_id} 未找到任何滑块轨道")
logger.error(f"{self.pure_user_id}未找到任何滑块轨道")
return slider_container, slider_button, None
return slider_container, slider_button, slider_track
except Exception as e:
logger.error(f"用户 {self.user_id} 查找滑块元素时出错: {str(e)}")
logger.error(f"{self.pure_user_id}查找滑块元素时出错: {str(e)}")
return None, None, None
def calculate_slide_distance(self, slider_button: ElementHandle, slider_track: ElementHandle):
@ -1151,29 +1161,29 @@ class XianyuSliderStealth:
# 获取滑块按钮位置和大小
button_box = slider_button.bounding_box()
if not button_box:
logger.error(f"用户 {self.user_id} 无法获取滑块按钮位置")
logger.error(f"{self.pure_user_id}无法获取滑块按钮位置")
return 0
# 获取滑块轨道位置和大小
track_box = slider_track.bounding_box()
if not track_box:
logger.error(f"用户 {self.user_id} 无法获取滑块轨道位置")
logger.error(f"{self.pure_user_id}无法获取滑块轨道位置")
return 0
# 计算滑动距离 (轨道宽度 - 滑块宽度)
slide_distance = track_box["width"] - button_box["width"]
logger.info(f"用户 {self.user_id} 计算滑动距离: {slide_distance}px (轨道宽度: {track_box['width']}px, 滑块宽度: {button_box['width']}px)")
logger.info(f"{self.pure_user_id}计算滑动距离: {slide_distance}px (轨道宽度: {track_box['width']}px, 滑块宽度: {button_box['width']}px)")
return slide_distance
except Exception as e:
logger.error(f"用户 {self.user_id} 计算滑动距离时出错: {str(e)}")
logger.error(f"{self.pure_user_id}计算滑动距离时出错: {str(e)}")
return 0
def check_verification_success(self, slider_button: ElementHandle):
"""检查验证结果"""
try:
logger.info(f"用户 {self.user_id} 检查验证结果...")
logger.info(f"{self.pure_user_id}检查验证结果...")
# 等待验证结果
time.sleep(3)
@ -1186,20 +1196,20 @@ class XianyuSliderStealth:
left_match = re.search(r'left:\s*([^;]+)', current_style)
if left_match:
left_value = left_match.group(1).strip()
logger.info(f"用户 {self.user_id} 滑块最终位置: {left_value}")
logger.info(f"{self.pure_user_id}滑块最终位置: {left_value}")
# 如果left值大于0说明滑块被移动了
try:
left_px = float(left_value.replace('px', ''))
if left_px > 0:
logger.info(f"用户 {self.user_id} 滑块已移动,检查页面是否改变...")
logger.info(f"{self.pure_user_id}滑块已移动,检查页面是否改变...")
# 检查页面是否改变
if self.check_page_changed():
logger.info(f"用户 {self.user_id} 页面已改变,验证成功")
logger.info(f"{self.pure_user_id}页面已改变,验证成功")
return True
else:
logger.warning(f"用户 {self.user_id} 页面未改变,检查验证失败提示...")
logger.warning(f"{self.pure_user_id}页面未改变,检查验证失败提示...")
return self.check_verification_failure()
except:
pass
@ -1210,10 +1220,10 @@ class XianyuSliderStealth:
try:
container = self.page.query_selector(".nc-container")
if not container or not container.is_visible():
logger.info(f"用户 {self.user_id} 滑块容器已消失,验证成功")
logger.info(f"{self.pure_user_id}滑块容器已消失,验证成功")
return True
else:
logger.warning(f"用户 {self.user_id} 滑块容器仍存在,验证失败")
logger.warning(f"{self.pure_user_id}滑块容器仍存在,验证失败")
return False
except:
pass
@ -1222,10 +1232,10 @@ class XianyuSliderStealth:
try:
track = self.page.query_selector("#nc_1_n1t")
if not track or not track.is_visible():
logger.info(f"用户 {self.user_id} 滑块轨道已消失,验证成功")
logger.info(f"{self.pure_user_id}滑块轨道已消失,验证成功")
return True
else:
logger.warning(f"用户 {self.user_id} 滑块轨道仍存在,验证失败")
logger.warning(f"{self.pure_user_id}滑块轨道仍存在,验证失败")
return False
except:
pass
@ -1243,16 +1253,16 @@ class XianyuSliderStealth:
try:
element = self.page.query_selector(selector)
if element and element.is_visible():
logger.info(f"用户 {self.user_id} 找到成功提示: {selector}")
logger.info(f"{self.pure_user_id}找到成功提示: {selector}")
return True
except:
continue
logger.warning(f"用户 {self.user_id} 未找到明确的成功或失败提示")
logger.warning(f"{self.pure_user_id}未找到明确的成功或失败提示")
return False
except Exception as e:
logger.error(f"用户 {self.user_id} 检查验证结果时出错: {str(e)}")
logger.error(f"{self.pure_user_id}检查验证结果时出错: {str(e)}")
return False
def check_page_changed(self):
@ -1260,32 +1270,32 @@ class XianyuSliderStealth:
try:
# 检查页面标题是否改变
current_title = self.page.title()
logger.info(f"用户 {self.user_id} 当前页面标题: {current_title}")
logger.info(f"{self.pure_user_id}当前页面标题: {current_title}")
# 如果标题不再是验证码相关,说明页面已改变
if "captcha" not in current_title.lower() and "验证" not in current_title and "拦截" not in current_title:
logger.info(f"用户 {self.user_id} 页面标题已改变,验证成功")
logger.info(f"{self.pure_user_id}页面标题已改变,验证成功")
return True
# 检查URL是否改变
current_url = self.page.url
logger.info(f"用户 {self.user_id} 当前页面URL: {current_url}")
logger.info(f"{self.pure_user_id}当前页面URL: {current_url}")
# 如果URL不再包含验证码相关参数说明页面已改变
if "captcha" not in current_url.lower() and "action=captcha" not in current_url:
logger.info(f"用户 {self.user_id} 页面URL已改变验证成功")
logger.info(f"{self.pure_user_id}页面URL已改变验证成功")
return True
return False
except Exception as e:
logger.warning(f"用户 {self.user_id} 检查页面改变时出错: {e}")
logger.warning(f"{self.pure_user_id}检查页面改变时出错: {e}")
return False
def check_verification_failure(self):
"""检查验证失败提示"""
try:
logger.info(f"用户 {self.user_id} 检查验证失败提示...")
logger.info(f"{self.pure_user_id}检查验证失败提示...")
# 等待一下让失败提示出现
time.sleep(3)
@ -1305,12 +1315,12 @@ class XianyuSliderStealth:
found_failure = False
for keyword in failure_keywords:
if keyword in page_content:
logger.info(f"用户 {self.user_id} 页面内容包含失败关键词: {keyword}")
logger.info(f"{self.pure_user_id}页面内容包含失败关键词: {keyword}")
found_failure = True
break
if not found_failure:
logger.info(f"用户 {self.user_id} 页面内容未包含失败关键词,可能验证真的成功了")
logger.info(f"{self.pure_user_id}页面内容未包含失败关键词,可能验证真的成功了")
return True
# 检查各种可能的验证失败提示元素
@ -1342,126 +1352,148 @@ class XianyuSliderStealth:
except:
pass
logger.info(f"用户 {self.user_id} 找到验证失败提示: {selector}, 文本: {element_text}")
logger.info(f"{self.pure_user_id}找到验证失败提示: {selector}, 文本: {element_text}")
retry_button = element
break
except:
continue
if retry_button:
logger.info(f"用户 {self.user_id} 检测到验证失败提示,但不执行点击操作")
logger.info(f"{self.pure_user_id}检测到验证失败提示,但不执行点击操作")
return False
else:
logger.warning(f"用户 {self.user_id} 未找到验证失败提示元素")
logger.warning(f"{self.pure_user_id}未找到验证失败提示元素")
return False
except Exception as e:
logger.error(f"用户 {self.user_id} 检查验证失败时出错: {e}")
logger.error(f"{self.pure_user_id}检查验证失败时出错: {e}")
return False
def solve_slider(self):
"""处理滑块验证"""
try:
logger.info(f"用户 {self.user_id} 开始处理滑块验证...")
logger.info(f"{self.pure_user_id}开始处理滑块验证...")
# 1. 查找滑块元素
slider_container, slider_button, slider_track = self.find_slider_elements()
if not all([slider_container, slider_button, slider_track]):
logger.error(f"用户 {self.user_id} 滑块元素查找失败")
logger.error(f"{self.pure_user_id}滑块元素查找失败")
return False
# 2. 计算滑动距离
slide_distance = self.calculate_slide_distance(slider_button, slider_track)
if slide_distance <= 0:
logger.error(f"用户 {self.user_id} 滑动距离计算失败")
logger.error(f"{self.pure_user_id}滑动距离计算失败")
return False
# 3. 生成人类化轨迹
trajectory = self.generate_human_trajectory(slide_distance)
if not trajectory:
logger.error(f"用户 {self.user_id} 轨迹生成失败")
logger.error(f"{self.pure_user_id}轨迹生成失败")
return False
# 4. 模拟滑动
if not self.simulate_slide(slider_button, trajectory):
logger.error(f"用户 {self.user_id} 滑动模拟失败")
logger.error(f"{self.pure_user_id}滑动模拟失败")
return False
# 5. 检查验证结果
if self.check_verification_success(slider_button):
logger.info(f"用户 {self.user_id} 滑块验证成功!")
logger.info(f"{self.pure_user_id}滑块验证成功!")
# 保存成功记录用于学习
if self.enable_learning and hasattr(self, 'current_trajectory_data'):
self._save_success_record(self.current_trajectory_data)
logger.info(f"用户 {self.user_id} 已保存成功记录用于参数优化")
logger.info(f"{self.pure_user_id}已保存成功记录用于参数优化")
return True
else:
logger.warning(f"用户 {self.user_id} 滑块验证失败")
logger.warning(f"{self.pure_user_id}滑块验证失败")
return False
except Exception as e:
logger.error(f"用户 {self.user_id} 处理滑块验证时出错: {str(e)}")
logger.error(f"{self.pure_user_id}处理滑块验证时出错: {str(e)}")
return False
def close_browser(self):
"""安全关闭浏览器并清理资源"""
logger.info(f"用户 {self.user_id} 开始清理资源...")
logger.info(f"{self.pure_user_id}开始清理资源...")
# 清理页面
try:
if hasattr(self, 'page') and self.page:
self.page.close()
logger.debug(f"用户 {self.user_id} 页面已关闭")
logger.debug(f"{self.pure_user_id}页面已关闭")
self.page = None
except Exception as e:
logger.warning(f"用户 {self.user_id} 关闭页面时出错: {e}")
logger.warning(f"{self.pure_user_id}关闭页面时出错: {e}")
# 清理上下文
try:
if hasattr(self, 'context') and self.context:
self.context.close()
logger.debug(f"用户 {self.user_id} 上下文已关闭")
logger.debug(f"{self.pure_user_id}上下文已关闭")
self.context = None
except Exception as e:
logger.warning(f"用户 {self.user_id} 关闭上下文时出错: {e}")
logger.warning(f"{self.pure_user_id}关闭上下文时出错: {e}")
# 清理浏览器
# 清理浏览器(异步操作,不等待完成)
try:
if hasattr(self, 'browser') and self.browser:
self.browser.close()
logger.info(f"用户 {self.user_id} 浏览器已关闭")
# 不等待浏览器关闭完成,避免阻塞
import asyncio
asyncio.create_task(self._async_close_browser())
logger.info(f"{self.pure_user_id}】浏览器关闭任务已创建")
self.browser = None
except Exception as e:
logger.warning(f"用户 {self.user_id} 关闭浏览器时出错: {e}")
logger.warning(f"{self.pure_user_id}】创建浏览器关闭任务时出错: {e}")
# 清理Playwright
# 清理Playwright(异步操作,不等待完成)
try:
if hasattr(self, 'playwright') and self.playwright:
self.playwright.stop()
logger.info(f"用户 {self.user_id} Playwright已停止")
# 不等待Playwright停止完成避免阻塞
import asyncio
asyncio.create_task(self._async_stop_playwright())
logger.info(f"{self.pure_user_id}】Playwright停止任务已创建")
self.playwright = None
except Exception as e:
logger.warning(f"用户 {self.user_id} 停止Playwright时出错: {e}")
logger.warning(f"{self.pure_user_id}】创建Playwright停止任务时出错: {e}")
# 清理临时目录
try:
if hasattr(self, 'temp_dir') and self.temp_dir:
shutil.rmtree(self.temp_dir, ignore_errors=True)
logger.debug(f"用户 {self.user_id} 临时目录已清理: {self.temp_dir}")
logger.debug(f"{self.pure_user_id}临时目录已清理: {self.temp_dir}")
except Exception as e:
logger.warning(f"用户 {self.user_id} 清理临时目录时出错: {e}")
logger.warning(f"{self.pure_user_id}清理临时目录时出错: {e}")
# 注销实例(最后执行,确保其他清理完成)
try:
concurrency_manager.unregister_instance(self.user_id)
stats = concurrency_manager.get_stats()
logger.info(f"用户 {self.user_id} 实例已注销,当前并发: {stats['active_count']}/{stats['max_concurrent']},等待队列: {stats['queue_length']}")
logger.info(f"{self.pure_user_id}实例已注销,当前并发: {stats['active_count']}/{stats['max_concurrent']},等待队列: {stats['queue_length']}")
except Exception as e:
logger.warning(f"用户 {self.user_id} 注销实例时出错: {e}")
logger.warning(f"{self.pure_user_id}注销实例时出错: {e}")
logger.info(f"用户 {self.user_id} 资源清理完成")
logger.info(f"{self.pure_user_id}】资源清理完成")
async def _async_close_browser(self):
"""异步关闭浏览器"""
try:
if hasattr(self, 'browser') and self.browser:
await self.browser.close()
logger.info(f"{self.pure_user_id}】浏览器已关闭")
except Exception as e:
logger.warning(f"{self.pure_user_id}】异步关闭浏览器时出错: {e}")
async def _async_stop_playwright(self):
"""异步停止Playwright"""
try:
if hasattr(self, 'playwright') and self.playwright:
await self.playwright.stop()
logger.info(f"{self.pure_user_id}】Playwright已停止")
except Exception as e:
logger.warning(f"{self.pure_user_id}】异步停止Playwright时出错: {e}")
def run(self, url: str):
"""运行主流程,返回(成功状态, cookie数据)"""
@ -1471,23 +1503,12 @@ class XianyuSliderStealth:
self.init_browser()
# 导航到目标URL添加随机延迟
logger.info(f"用户 {self.user_id} 导航到URL: {url}")
logger.info(f"{self.pure_user_id}导航到URL: {url}")
self.page.goto(url, wait_until="networkidle", timeout=60000)
# 保存初始cookie用于比较
try:
initial_cookies = self.context.cookies()
self._old_cookies = {}
for cookie in initial_cookies:
self._old_cookies[cookie['name']] = cookie['value']
logger.info(f"用户 {self.user_id} 已保存初始cookie{len(self._old_cookies)}")
except Exception as e:
logger.warning(f"用户 {self.user_id} 保存初始cookie失败: {str(e)}")
self._old_cookies = {}
# 随机延迟,模拟人类行为
delay = random.uniform(5, 10)
logger.info(f"用户 {self.user_id} 等待页面加载: {delay:.2f}")
logger.info(f"{self.pure_user_id}】等待页面加载: {delay:.2f}")
time.sleep(delay)
# 模拟人类滚动行为
@ -1498,47 +1519,47 @@ class XianyuSliderStealth:
# 检查页面标题
page_title = self.page.title()
logger.info(f"用户 {self.user_id} 页面标题: {page_title}")
logger.info(f"{self.pure_user_id}页面标题: {page_title}")
# 检查页面内容
page_content = self.page.content()
if any(keyword in page_content for keyword in ["验证码", "captcha", "滑块", "slider"]):
logger.info(f"用户 {self.user_id} 页面内容包含验证码相关关键词")
logger.info(f"{self.pure_user_id}页面内容包含验证码相关关键词")
# 处理滑块验证
success = self.solve_slider()
if success:
logger.info(f"用户 {self.user_id} 滑块验证成功")
logger.info(f"{self.pure_user_id}滑块验证成功")
# 等待页面完全加载和跳转让新的cookie生效
try:
logger.info(f"用户 {self.user_id} 等待页面完全加载...")
logger.info(f"{self.pure_user_id}等待页面完全加载...")
time.sleep(3) # 等待页面状态稳定
# 等待页面跳转或刷新
self.page.wait_for_load_state("networkidle", timeout=10000)
time.sleep(2) # 额外等待确保cookie更新
logger.info(f"用户 {self.user_id} 页面加载完成开始获取cookie")
logger.info(f"{self.pure_user_id}页面加载完成开始获取cookie")
except Exception as e:
logger.warning(f"用户 {self.user_id} 等待页面加载时出错: {str(e)}")
logger.warning(f"{self.pure_user_id}等待页面加载时出错: {str(e)}")
# 在关闭浏览器前获取cookie
try:
cookies = self._get_cookies_after_success()
except Exception as e:
logger.warning(f"用户 {self.user_id} 获取cookie时出错: {str(e)}")
logger.warning(f"{self.pure_user_id}获取cookie时出错: {str(e)}")
else:
logger.warning(f"用户 {self.user_id} 滑块验证失败")
logger.warning(f"{self.pure_user_id}滑块验证失败")
return success, cookies
else:
logger.info(f"用户 {self.user_id} 页面内容不包含验证码相关关键词,可能不需要验证")
logger.info(f"{self.pure_user_id}页面内容不包含验证码相关关键词,可能不需要验证")
return True, None
except Exception as e:
logger.error(f"用户 {self.user_id} 执行过程中出错: {str(e)}")
logger.error(f"{self.pure_user_id}执行过程中出错: {str(e)}")
return False, None
finally:
# 关闭浏览器