提交
This commit is contained in:
parent
0109838978
commit
cdbb85373f
34
Dockerfile
34
Dockerfile
@ -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"]
|
||||
@ -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"]
|
||||
|
||||
@ -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
131
check_silent_mode.py
Normal 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("✅ 修改完成!现在鼠标轨迹可视化不会再遮挡页面了")
|
||||
12
config.py
12
config.py
@ -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 []
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
1044
order_status_handler.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -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>
|
||||
|
||||
@ -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] || '未知';
|
||||
|
||||
@ -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]:
|
||||
"""
|
||||
|
||||
@ -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}")
|
||||
|
||||
@ -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:
|
||||
# 关闭浏览器
|
||||
|
||||
Loading…
Reference in New Issue
Block a user