commit
This commit is contained in:
503
backend/main.py
503
backend/main.py
@@ -51,6 +51,9 @@ scheduler = None
|
||||
# 全局阿里云短信服务实例
|
||||
sms_service = None
|
||||
|
||||
# 由于禁用了浏览器池,使用一个简单的字典来存储session
|
||||
temp_sessions = {}
|
||||
|
||||
# WebSocket连接管理器
|
||||
class ConnectionManager:
|
||||
def __init__(self):
|
||||
@@ -58,6 +61,10 @@ class ConnectionManager:
|
||||
self.active_connections: Dict[str, WebSocket] = {}
|
||||
# session_id -> 消息队列(用于缓存连接建立前的消息)
|
||||
self.pending_messages: Dict[str, list] = {}
|
||||
# 未确认消息:{session_id: {message_id: {message, timestamp, retry_count}}}
|
||||
self.unconfirmed_messages: Dict[str, Dict[str, dict]] = {}
|
||||
# 消息重发定时器
|
||||
self.retry_tasks: Dict[str, asyncio.Task] = {}
|
||||
|
||||
async def connect(self, session_id: str, websocket: WebSocket):
|
||||
await websocket.accept()
|
||||
@@ -67,12 +74,34 @@ class ConnectionManager:
|
||||
print(f"[WebSocket] 当前活跃连接数: {len(self.active_connections)}", file=sys.stderr)
|
||||
print(f"[WebSocket] 连接时间: {__import__('datetime').datetime.now()}", file=sys.stderr)
|
||||
print(f"[WebSocket] ===================================", file=sys.stderr)
|
||||
|
||||
# 检查未确认消息(断线期间的消息)
|
||||
if session_id in self.unconfirmed_messages:
|
||||
unconfirmed_count = len(self.unconfirmed_messages[session_id])
|
||||
print(f"[WebSocket] 发现 {unconfirmed_count} 条未确认消息(断线期间)", file=sys.stderr)
|
||||
|
||||
# 立即检查缓存消息(不等待)
|
||||
# 等待100ms让前端监听器就绪
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
# 立即重发所有未确认消息
|
||||
for idx, (message_id, msg_data) in enumerate(self.unconfirmed_messages[session_id].items()):
|
||||
try:
|
||||
print(f"[WebSocket] 重发未确认消息 [{idx+1}/{unconfirmed_count}]: {msg_data['message'].get('type')}", file=sys.stderr)
|
||||
await websocket.send_json(msg_data['message'])
|
||||
# 每条消息间隔1100ms
|
||||
await asyncio.sleep(0.1)
|
||||
except Exception as e:
|
||||
print(f"[WebSocket] 重发未确认消息失败: {str(e)}", file=sys.stderr)
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
print(f"[WebSocket] 已重发所有未确认消息", file=sys.stderr)
|
||||
|
||||
# 检查pending消息(连接建立前的消息)
|
||||
if session_id in self.pending_messages:
|
||||
pending_count = len(self.pending_messages[session_id])
|
||||
print(f"[WebSocket] 发现缓存消息: {pending_count} 条", file=sys.stderr)
|
||||
print(f"[WebSocket] 缓存消息内容: {self.pending_messages[session_id]}", file=sys.stderr)
|
||||
print(f"[WebSocket] 发现 {pending_count} 条pending消息", file=sys.stderr)
|
||||
print(f"[WebSocket] pending消息内容: {self.pending_messages[session_id]}", file=sys.stderr)
|
||||
|
||||
# 等待100ms让前端监听器就绪
|
||||
await asyncio.sleep(0.1)
|
||||
@@ -81,18 +110,18 @@ class ConnectionManager:
|
||||
try:
|
||||
print(f"[WebSocket] 准备发送第{idx+1}条消息...", file=sys.stderr)
|
||||
await websocket.send_json(message)
|
||||
print(f"[WebSocket] 已发送缓存消息 [{idx+1}/{pending_count}]: {message.get('type')}", file=sys.stderr)
|
||||
print(f"[WebSocket] 已发送pending消息 [{idx+1}/{pending_count}]: {message.get('type')}", file=sys.stderr)
|
||||
# 每条消息间隔100ms
|
||||
await asyncio.sleep(0.1)
|
||||
except Exception as e:
|
||||
print(f"[WebSocket] 发送缓存消息失败: {str(e)}", file=sys.stderr)
|
||||
print(f"[WebSocket] 发送pending消息失败: {str(e)}", file=sys.stderr)
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
del self.pending_messages[session_id]
|
||||
print(f"[WebSocket] 缓存消息已清空: {session_id}", file=sys.stderr)
|
||||
print(f"[WebSocket] pending消息已清空: {session_id}", file=sys.stderr)
|
||||
else:
|
||||
print(f"[WebSocket] 没有缓存消息: {session_id}", file=sys.stderr)
|
||||
print(f"[WebSocket] 没有pending消息: {session_id}", file=sys.stderr)
|
||||
|
||||
def disconnect(self, session_id: str, reason: str = "未知原因"):
|
||||
"""断开WebSocket连接并记录原因"""
|
||||
@@ -102,18 +131,41 @@ class ConnectionManager:
|
||||
print(f"[WebSocket] Session ID: {session_id}", file=sys.stderr)
|
||||
print(f"[WebSocket] 断开原因: {reason}", file=sys.stderr)
|
||||
print(f"[WebSocket] 剩余活跃连接数: {len(self.active_connections)}", file=sys.stderr)
|
||||
|
||||
# 检查是否有未确认消息
|
||||
if session_id in self.unconfirmed_messages:
|
||||
unconfirmed_count = len(self.unconfirmed_messages[session_id])
|
||||
print(f"[WebSocket] 保留 {unconfirmed_count} 条未确认消息,等待重连后重发", file=sys.stderr)
|
||||
|
||||
print(f"[WebSocket] ===================================", file=sys.stderr)
|
||||
# 清理缓存消息
|
||||
|
||||
# 清理pending_messages(这些是连接建立前的消息,已经过时)
|
||||
if session_id in self.pending_messages:
|
||||
pending_count = len(self.pending_messages[session_id])
|
||||
if pending_count > 0:
|
||||
print(f"[WebSocket] 清理 {pending_count} 条未发送的缓存消息", file=sys.stderr)
|
||||
print(f"[WebSocket] 清理 {pending_count} 条过时的pending消息", file=sys.stderr)
|
||||
del self.pending_messages[session_id]
|
||||
|
||||
# 不清理unconfirmed_messages和retry_tasks,让它们在重连后继续工作
|
||||
# 这样就能实现断线重连后的消息恢复
|
||||
|
||||
async def send_message(self, session_id: str, message: dict):
|
||||
"""发送消息并等待确认"""
|
||||
import uuid
|
||||
import time
|
||||
|
||||
# 为消息添加唯一ID和时间戳
|
||||
if 'message_id' not in message:
|
||||
message['message_id'] = str(uuid.uuid4())
|
||||
if 'timestamp' not in message:
|
||||
message['timestamp'] = time.time()
|
||||
|
||||
message_id = message['message_id']
|
||||
|
||||
print(f"[WebSocket] ========== 尝试发送消息 ==========", file=sys.stderr)
|
||||
print(f"[WebSocket] Session ID: {session_id}", file=sys.stderr)
|
||||
print(f"[WebSocket] 消息类型: {message.get('type')}", file=sys.stderr)
|
||||
print(f"[WebSocket] 消息ID: {message_id}", file=sys.stderr)
|
||||
print(f"[WebSocket] 当前活跃连接数: {len(self.active_connections)}", file=sys.stderr)
|
||||
print(f"[WebSocket] 活跃连接session_ids: {list(self.active_connections.keys())}", file=sys.stderr)
|
||||
print(f"[WebSocket] session_id在连接中: {session_id in self.active_connections}", file=sys.stderr)
|
||||
@@ -122,7 +174,25 @@ class ConnectionManager:
|
||||
if session_id in self.active_connections:
|
||||
try:
|
||||
await self.active_connections[session_id].send_json(message)
|
||||
print(f"[WebSocket] 发送消息到 {session_id}: {message.get('type')}", file=sys.stderr)
|
||||
print(f"[WebSocket] 发送消息到 {session_id}: {message.get('type')}, message_id={message_id}", file=sys.stderr)
|
||||
|
||||
# 存储未确认消息
|
||||
if session_id not in self.unconfirmed_messages:
|
||||
self.unconfirmed_messages[session_id] = {}
|
||||
|
||||
self.unconfirmed_messages[session_id][message_id] = {
|
||||
'message': message,
|
||||
'timestamp': time.time(),
|
||||
'retry_count': 0
|
||||
}
|
||||
|
||||
# 启动重发任务
|
||||
if session_id not in self.retry_tasks:
|
||||
self.retry_tasks[session_id] = asyncio.create_task(
|
||||
self._retry_unconfirmed_messages(session_id)
|
||||
)
|
||||
print(f"[WebSocket] 已启动消息重发任务: {session_id}", file=sys.stderr)
|
||||
|
||||
except Exception as e:
|
||||
print(f"[WebSocket] ========== 发送消息失败 ==========", file=sys.stderr)
|
||||
print(f"[WebSocket] Session ID: {session_id}", file=sys.stderr)
|
||||
@@ -139,6 +209,83 @@ class ConnectionManager:
|
||||
# 最多缓存10条消息
|
||||
if len(self.pending_messages[session_id]) > 10:
|
||||
self.pending_messages[session_id].pop(0)
|
||||
|
||||
async def _retry_unconfirmed_messages(self, session_id: str):
|
||||
"""定期检查并重发未确认的消息"""
|
||||
import time
|
||||
|
||||
while True:
|
||||
try:
|
||||
await asyncio.sleep(3) # 每3秒检查一次
|
||||
|
||||
if session_id not in self.unconfirmed_messages:
|
||||
continue
|
||||
|
||||
current_time = time.time()
|
||||
messages_to_retry = []
|
||||
messages_to_remove = []
|
||||
|
||||
for message_id, msg_data in self.unconfirmed_messages[session_id].items():
|
||||
elapsed = current_time - msg_data['timestamp']
|
||||
|
||||
# 超过60秒未确认,标记为失败并移除(给重连更多时间)
|
||||
if elapsed > 60:
|
||||
print(f"[WebSocket] 消息超时未确认: {message_id}, 已等待{elapsed:.1f}秒", file=sys.stderr)
|
||||
messages_to_remove.append(message_id)
|
||||
continue
|
||||
|
||||
# 超过3秒未确认且重试次数<5,重发(增加重试次数)
|
||||
if elapsed > 3 and msg_data['retry_count'] < 5:
|
||||
messages_to_retry.append((message_id, msg_data))
|
||||
|
||||
# 移除超时消息
|
||||
for message_id in messages_to_remove:
|
||||
del self.unconfirmed_messages[session_id][message_id]
|
||||
print(f"[WebSocket] 已移除超时消息: {message_id}", file=sys.stderr)
|
||||
|
||||
# 重发消息(只有在连接活跃时才发送)
|
||||
for message_id, msg_data in messages_to_retry:
|
||||
if session_id in self.active_connections:
|
||||
try:
|
||||
await self.active_connections[session_id].send_json(msg_data['message'])
|
||||
msg_data['retry_count'] += 1
|
||||
msg_data['timestamp'] = current_time
|
||||
print(f"[WebSocket] 重发消息: {message_id}, 第{msg_data['retry_count']}次重试", file=sys.stderr)
|
||||
except Exception as e:
|
||||
print(f"[WebSocket] 重发消息失败: {message_id}, {str(e)}", file=sys.stderr)
|
||||
# 不移除,等待下次重试
|
||||
else:
|
||||
# 连接未建立,等待重连
|
||||
print(f"[WebSocket] 连接未建立,等待重连后重发: {message_id}", file=sys.stderr)
|
||||
|
||||
except asyncio.CancelledError:
|
||||
print(f"[WebSocket] 重发任务被取消: {session_id}", file=sys.stderr)
|
||||
break
|
||||
except Exception as e:
|
||||
print(f"[WebSocket] 重发任务异常: {session_id}, {str(e)}", file=sys.stderr)
|
||||
|
||||
def confirm_message(self, session_id: str, message_id: str):
|
||||
"""确认收到消息"""
|
||||
if session_id in self.unconfirmed_messages:
|
||||
if message_id in self.unconfirmed_messages[session_id]:
|
||||
del self.unconfirmed_messages[session_id][message_id]
|
||||
print(f"[WebSocket] 消息已确认: {session_id}, message_id={message_id}", file=sys.stderr)
|
||||
print(f"[WebSocket] 剩余未确认消息: {len(self.unconfirmed_messages[session_id])}", file=sys.stderr)
|
||||
|
||||
# 如果没有未确认消息了,取消重发任务
|
||||
if len(self.unconfirmed_messages[session_id]) == 0:
|
||||
if session_id in self.retry_tasks:
|
||||
self.retry_tasks[session_id].cancel()
|
||||
del self.retry_tasks[session_id]
|
||||
print(f"[WebSocket] 所有消息已确认,取消重发任务: {session_id}", file=sys.stderr)
|
||||
|
||||
def get_unconfirmed_messages(self, session_id: str) -> list:
|
||||
"""获取未确认的消息列表"""
|
||||
if session_id in self.unconfirmed_messages:
|
||||
messages = [msg_data['message'] for msg_data in self.unconfirmed_messages[session_id].values()]
|
||||
print(f"[WebSocket] 获取未确认消息: {session_id}, 共{len(messages)}条", file=sys.stderr)
|
||||
return messages
|
||||
return []
|
||||
|
||||
# 全局WebSocket管理器
|
||||
ws_manager = ConnectionManager()
|
||||
@@ -256,12 +403,15 @@ async def startup_event():
|
||||
preheat_url = "https://creator.xiaohongshu.com/login"
|
||||
|
||||
# 初始化全局浏览器池(使用配置的headless参数)
|
||||
# 已禁用:使用AdsPower管理浏览器,不需要浏览器池
|
||||
global browser_pool, login_service, sms_service
|
||||
browser_pool = get_browser_pool(idle_timeout=1800, headless=headless)
|
||||
print(f"[服务启动] 浏览器池模式: {'headless(无头模式)' if headless else 'headed(有头模式)'}")
|
||||
browser_pool = None # 使用AdsPower,不创建浏览器池
|
||||
# browser_pool = get_browser_pool(idle_timeout=1800, headless=headless)
|
||||
# print(f"[服务启动] 浏览器池模式: {'headless(无头模式)' if headless else 'headed(有头模式)'}")
|
||||
print(f"[服务启动] 浏览器管理: 使用AdsPower")
|
||||
|
||||
# 初始化登录服务(使用独立的login.headless配置)
|
||||
login_service = XHSLoginService(use_pool=True, headless=login_headless)
|
||||
login_service = XHSLoginService(use_pool=False, headless=login_headless) # 使用AdsPower,不需要浏览器池
|
||||
print(f"[服务启动] 登录服务模式: {'headless(无头模式)' if login_headless else 'headed(有头模式)'}")
|
||||
|
||||
# 初始化阿里云短信服务
|
||||
@@ -275,7 +425,8 @@ async def startup_event():
|
||||
print("[服务启动] 阿里云短信服务已初始化")
|
||||
|
||||
# 启动浏览器池清理任务
|
||||
asyncio.create_task(browser_cleanup_task())
|
||||
# 已禁用:使用AdsPower,不需要浏览器池清理
|
||||
# asyncio.create_task(browser_cleanup_task())
|
||||
|
||||
# 已禁用预热功能,避免干扰正常业务流程
|
||||
# asyncio.create_task(browser_preheat_task())
|
||||
@@ -298,12 +449,17 @@ async def startup_event():
|
||||
scheduler_enabled = config.get_bool('scheduler.enabled', False)
|
||||
proxy_pool_enabled = config.get_bool('proxy_pool.enabled', False)
|
||||
proxy_pool_api_url = config.get_str('proxy_pool.api_url', '')
|
||||
proxy_username = config.get_str('proxy_pool.username', '') # 新增:代理用户名
|
||||
proxy_password = config.get_str('proxy_pool.password', '') # 新增:代理密码
|
||||
enable_random_ua = config.get_bool('scheduler.enable_random_ua', True)
|
||||
min_publish_interval = config.get_int('scheduler.min_publish_interval', 30)
|
||||
max_publish_interval = config.get_int('scheduler.max_publish_interval', 120)
|
||||
# headless已经在上面读取了
|
||||
|
||||
if scheduler_enabled:
|
||||
# 从配置读取是否启用AdsPower
|
||||
use_adspower_scheduler = config.get_bool('adspower.enabled', False)
|
||||
|
||||
scheduler = XHSScheduler(
|
||||
db_config=db_config,
|
||||
max_concurrent=config.get_int('scheduler.max_concurrent', 2),
|
||||
@@ -314,17 +470,20 @@ async def startup_event():
|
||||
max_hourly_articles_per_user=config.get_int('scheduler.max_hourly_articles_per_user', 2),
|
||||
proxy_pool_enabled=proxy_pool_enabled,
|
||||
proxy_pool_api_url=proxy_pool_api_url,
|
||||
proxy_username=proxy_username, # 新增:传递代理用户名
|
||||
proxy_password=proxy_password, # 新增:传递代理密码
|
||||
enable_random_ua=enable_random_ua,
|
||||
min_publish_interval=min_publish_interval,
|
||||
max_publish_interval=max_publish_interval,
|
||||
headless=headless, # 新增: 传递headless参数
|
||||
use_adspower=use_adspower_scheduler # 新增:是否使用AdsPower
|
||||
)
|
||||
|
||||
cron_expr = config.get_str('scheduler.cron', '*/5 * * * * *')
|
||||
scheduler.start(cron_expr)
|
||||
print(f"[服务启动] 定时发布任务已启动,Cron: {cron_expr}")
|
||||
print(f"[服务启动] 定时发布任务已启动,Cron: {cron_expr}", file=sys.stderr)
|
||||
else:
|
||||
print("[服务启动] 定时发布任务未启用")
|
||||
print("[服务启动] 定时发布任务未启用", file=sys.stderr)
|
||||
|
||||
async def browser_cleanup_task():
|
||||
"""后台任务:定期清理空闲浏览器"""
|
||||
@@ -370,8 +529,11 @@ async def shutdown_event():
|
||||
print("[服务关闭] 调度器已停止")
|
||||
|
||||
# 关闭浏览器池
|
||||
await browser_pool.close()
|
||||
print("[服务关闭] 浏览器池已关闭")
|
||||
# 已禁用:使用AdsPower,不需要浏览器池
|
||||
# if browser_pool:
|
||||
# await browser_pool.close()
|
||||
# print("[服务关闭] 浏览器池已关闭")
|
||||
print("[服务关闭] 服务关闭完成")
|
||||
|
||||
@app.post("/api/xhs/send-code", response_model=BaseResponse)
|
||||
async def send_code(request: SendCodeRequest):
|
||||
@@ -398,11 +560,16 @@ async def send_code(request: SendCodeRequest):
|
||||
print(f"[发送验证码] 使用登录页面: {login_page} (配置默认={default_login_page}, API参数={request.login_page})", file=sys.stderr)
|
||||
|
||||
try:
|
||||
# 为此请求创建独立的登录服务实例,使用session_id实现并发隔离
|
||||
# 从配置读取是否启用AdsPower
|
||||
use_adspower = config.get_bool('adspower.enabled', False)
|
||||
print(f"[发送验证码] AdsPower模式: {'[开启]' if use_adspower else '[关闭]'}", file=sys.stderr)
|
||||
|
||||
# 每次都创建全新的登录服务实例(不使用浏览器池,确保每次获取新代理IP)
|
||||
request_login_service = XHSLoginService(
|
||||
use_pool=True,
|
||||
headless=login_service.headless, # 使用配置文件中的login.headless配置
|
||||
session_id=session_id # 关键:传递session_id
|
||||
use_pool=False, # 关键:不使用浏览器池,每次创建新实例
|
||||
headless=login_service.headless,
|
||||
session_id=session_id,
|
||||
use_adspower=use_adspower
|
||||
)
|
||||
|
||||
# 调用登录服务发送验证码
|
||||
@@ -428,13 +595,12 @@ async def send_code(request: SendCodeRequest):
|
||||
)
|
||||
|
||||
if result["success"]:
|
||||
# 验证浏览器是否已保存到池中
|
||||
if browser_pool and session_id in browser_pool.temp_browsers:
|
||||
print(f"[发送验证码] ✅ 浏览器实例已保存到池中: {session_id}", file=sys.stderr)
|
||||
print(f"[发送验证码] 当前池中共有 {len(browser_pool.temp_browsers)} 个临时浏览器", file=sys.stderr)
|
||||
else:
|
||||
print(f"[发送验证码] ⚠️ 浏览器实例未保存到池中: {session_id}", file=sys.stderr)
|
||||
print(f"[发送验证码] 池中的session列表: {list(browser_pool.temp_browsers.keys()) if browser_pool else 'None'}", file=sys.stderr)
|
||||
# 验证码发送成功,关闭浏览器(下次重新创建)
|
||||
try:
|
||||
await request_login_service.close()
|
||||
print(f"[发送验证码] ✅ 已关闭浏览器实例: {session_id}", file=sys.stderr)
|
||||
except Exception as e:
|
||||
print(f"[发送验证码] 关闭浏览器失败: {str(e)}", file=sys.stderr)
|
||||
|
||||
return BaseResponse(
|
||||
code=0,
|
||||
@@ -445,13 +611,12 @@ async def send_code(request: SendCodeRequest):
|
||||
}
|
||||
)
|
||||
else:
|
||||
# 发送失败,释放临时浏览器
|
||||
if session_id and browser_pool:
|
||||
try:
|
||||
await browser_pool.release_temp_browser(session_id)
|
||||
print(f"[发送验证码] 已释放临时浏览器: {session_id}", file=sys.stderr)
|
||||
except Exception as e:
|
||||
print(f"[发送验证码] 释放临时浏览器失败: {str(e)}", file=sys.stderr)
|
||||
# 发送失败,关闭浏览器
|
||||
try:
|
||||
await request_login_service.close()
|
||||
print(f"[发送验证码] 已关闭失败的浏览器: {session_id}", file=sys.stderr)
|
||||
except Exception as e:
|
||||
print(f"[发送验证码] 关闭浏览器失败: {str(e)}", file=sys.stderr)
|
||||
|
||||
return BaseResponse(
|
||||
code=1,
|
||||
@@ -462,13 +627,13 @@ async def send_code(request: SendCodeRequest):
|
||||
except Exception as e:
|
||||
print(f"发送验证码异常: {str(e)}", file=sys.stderr)
|
||||
|
||||
# 异常情况,释放临时浏览器
|
||||
if session_id and browser_pool:
|
||||
try:
|
||||
await browser_pool.release_temp_browser(session_id)
|
||||
print(f"[发送验证码] 已释放临时浏览器: {session_id}", file=sys.stderr)
|
||||
except Exception as release_error:
|
||||
print(f"[发送验证码] 释放临时浏览器失败: {str(release_error)}", file=sys.stderr)
|
||||
# 异常情况,关闭浏览器
|
||||
try:
|
||||
if 'request_login_service' in locals():
|
||||
await request_login_service.close()
|
||||
print(f"[发送验证码] 已关闭异常的浏览器: {session_id}", file=sys.stderr)
|
||||
except Exception as close_error:
|
||||
print(f"[发送验证码] 关闭浏览器失败: {str(close_error)}", file=sys.stderr)
|
||||
|
||||
return BaseResponse(
|
||||
code=1,
|
||||
@@ -557,7 +722,7 @@ async def start_qrcode_login():
|
||||
print(f"[扫码登录] 创建全新浏览器实例 session_id={session_id}", file=sys.stderr)
|
||||
|
||||
qrcode_service = XHSLoginService(
|
||||
use_pool=True,
|
||||
use_pool=False, # 使用AdsPower,不需要浏览器池
|
||||
headless=login_service.headless,
|
||||
session_id=session_id,
|
||||
use_page_isolation=False # 小红书不支持页面隔离,必须独立浏览器
|
||||
@@ -645,7 +810,7 @@ async def get_qrcode_status(request: dict):
|
||||
|
||||
# 使用session_id获取浏览器实例
|
||||
qrcode_service = XHSLoginService(
|
||||
use_pool=True,
|
||||
use_pool=False, # 使用AdsPower,不需要浏览器池
|
||||
headless=login_service.headless,
|
||||
session_id=session_id
|
||||
)
|
||||
@@ -716,7 +881,7 @@ async def refresh_qrcode(request: dict):
|
||||
|
||||
# 使用session_id获取浏览器实例
|
||||
qrcode_service = XHSLoginService(
|
||||
use_pool=True,
|
||||
use_pool=False, # 使用AdsPower,不需要浏览器池
|
||||
headless=login_service.headless,
|
||||
session_id=session_id
|
||||
)
|
||||
@@ -1433,11 +1598,17 @@ async def handle_send_code_ws(session_id: str, phone: str, country_code: str, lo
|
||||
try:
|
||||
print(f"[WebSocket-SendCode] 开始处理: session={session_id}, phone={phone}", file=sys.stderr)
|
||||
|
||||
# 创建登录服务实例
|
||||
# 从配置读取是否启用AdsPower
|
||||
config = get_config()
|
||||
use_adspower = config.get_bool('adspower.enabled', False)
|
||||
print(f"[WebSocket-SendCode] AdsPower模式: {'[开启]' if use_adspower else '[关闭]'}", file=sys.stderr)
|
||||
|
||||
# 每次都创建全新的登录服务实例(不使用浏览器池,确保每次获取新代理IP)
|
||||
request_login_service = XHSLoginService(
|
||||
use_pool=True,
|
||||
use_pool=False, # 不使用浏览器池,每次创建新实例
|
||||
headless=login_service.headless,
|
||||
session_id=session_id
|
||||
session_id=session_id,
|
||||
use_adspower=use_adspower
|
||||
)
|
||||
|
||||
# 调用登录服务发送验证码
|
||||
@@ -1448,12 +1619,22 @@ async def handle_send_code_ws(session_id: str, phone: str, country_code: str, lo
|
||||
session_id=session_id
|
||||
)
|
||||
|
||||
# 将service实例存储到浏览器池,供后续验证码验证使用
|
||||
if session_id in browser_pool.temp_browsers:
|
||||
browser_pool.temp_browsers[session_id]['service'] = request_login_service
|
||||
print(f"[WebSocket-SendCode] 已存储service实例: {session_id}", file=sys.stderr)
|
||||
# 将service实例存储,供后续验证码验证使用
|
||||
# 注意:由于browser_pool已禁用,使用temp_sessions字典存储
|
||||
if session_id not in temp_sessions:
|
||||
# 手动创建session记录
|
||||
temp_sessions[session_id] = {
|
||||
'browser': request_login_service.browser,
|
||||
'context': request_login_service.context,
|
||||
'page': request_login_service.page,
|
||||
'service': request_login_service,
|
||||
'created_at': datetime.now()
|
||||
}
|
||||
print(f"[WebSocket-SendCode] 已手动创建session记录: {session_id}", file=sys.stderr)
|
||||
else:
|
||||
print(f"[WebSocket-SendCode] 警告: session_id {session_id} 不在temp_browsers中", file=sys.stderr)
|
||||
# 更新现有session的service实例
|
||||
temp_sessions[session_id]['service'] = request_login_service
|
||||
print(f"[WebSocket-SendCode] 已更新service实例: {session_id}", file=sys.stderr)
|
||||
|
||||
# 检查是否需要验证(发送验证码时触发风控)
|
||||
if result.get("need_captcha"):
|
||||
@@ -1506,8 +1687,8 @@ async def handle_verify_code_ws(session_id: str, phone: str, code: str, country_
|
||||
try:
|
||||
print(f"[WebSocket-VerifyCode] 开始验证: session={session_id}, phone={phone}, code={code}", file=sys.stderr)
|
||||
|
||||
# 从浏览器池中获取之前的浏览器实例
|
||||
if session_id not in browser_pool.temp_browsers:
|
||||
# 从temp_sessions中获取之前的浏览器实例
|
||||
if session_id not in temp_sessions:
|
||||
print(f"[WebSocket-VerifyCode] 未找到session: {session_id}", file=sys.stderr)
|
||||
await websocket.send_json({
|
||||
"type": "login_result",
|
||||
@@ -1517,7 +1698,7 @@ async def handle_verify_code_ws(session_id: str, phone: str, code: str, country_
|
||||
return
|
||||
|
||||
# 获取浏览器实例
|
||||
browser_data = browser_pool.temp_browsers[session_id]
|
||||
browser_data = temp_sessions[session_id]
|
||||
request_login_service = browser_data['service']
|
||||
|
||||
# 调用登录服务验证登录
|
||||
@@ -1561,6 +1742,10 @@ async def handle_verify_code_ws(session_id: str, phone: str, code: str, country_
|
||||
await websocket.send_json({
|
||||
"type": "login_success",
|
||||
"success": True,
|
||||
"cookies": result.get("cookies"), # 键值对格式
|
||||
"cookies_full": result.get("cookies_full"), # Playwright完整格式(你需要的格式)
|
||||
"user_info": result.get("user_info"),
|
||||
"login_state": result.get("login_state"),
|
||||
"storage_state": storage_state,
|
||||
"storage_state_path": storage_state_path,
|
||||
"message": "登录成功"
|
||||
@@ -1568,8 +1753,62 @@ async def handle_verify_code_ws(session_id: str, phone: str, code: str, country_
|
||||
|
||||
# 释放浏览器
|
||||
try:
|
||||
await browser_pool.release_temp_browser(session_id)
|
||||
print(f"[WebSocket-VerifyCode] 已释放浏览器: {session_id}", file=sys.stderr)
|
||||
# 使用temp_sessions的自定义清理逻辑
|
||||
if session_id in temp_sessions:
|
||||
try:
|
||||
service = temp_sessions[session_id].get('service')
|
||||
if service:
|
||||
# 关闭浏览器
|
||||
await service.close_browser() # 使用正确的方法名
|
||||
print(f"[WebSocket-VerifyCode] 浏览器已关闭: {session_id}", file=sys.stderr)
|
||||
|
||||
# 关闭后查询AdsPower Cookie
|
||||
adspower_cookies = await service.get_adspower_cookies_after_close()
|
||||
if adspower_cookies:
|
||||
print(f"[WebSocket-VerifyCode] 获取到AdsPower Cookie: {len(adspower_cookies)}个", file=sys.stderr)
|
||||
|
||||
# 更新返回给前端的cookies_full
|
||||
result['cookies_full'] = adspower_cookies
|
||||
result['cookies'] = {cookie['name']: cookie['value'] for cookie in adspower_cookies}
|
||||
|
||||
# 更新storage_state中的cookies
|
||||
if storage_state:
|
||||
storage_state['cookies'] = adspower_cookies
|
||||
result['storage_state'] = storage_state
|
||||
|
||||
# 重新保存storage_state文件
|
||||
try:
|
||||
with open(storage_state_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(storage_state, f, ensure_ascii=False, indent=2)
|
||||
print(f"[WebSocket-VerifyCode] 已更新storage_state文件中的Cookie", file=sys.stderr)
|
||||
except Exception as save_err:
|
||||
print(f"[WebSocket-VerifyCode] 更新storage_state失败: {str(save_err)}", file=sys.stderr)
|
||||
|
||||
# 检查WebSocket连接状态后再推送消息
|
||||
try:
|
||||
# 重新推送登录成功消息,包含更新后的Cookie
|
||||
await websocket.send_json({
|
||||
"type": "login_success",
|
||||
"success": True,
|
||||
"cookies": result.get("cookies"),
|
||||
"cookies_full": adspower_cookies, # 使用AdsPower Cookie
|
||||
"user_info": result.get("user_info"),
|
||||
"login_state": result.get("login_state"),
|
||||
"storage_state": storage_state,
|
||||
"storage_state_path": storage_state_path,
|
||||
"message": "登录成功(已同步AdsPower Cookie)"
|
||||
})
|
||||
print(f"[WebSocket-VerifyCode] 已重新推送包含AdsPower Cookie的登录成功消息", file=sys.stderr)
|
||||
except Exception as send_err:
|
||||
# WebSocket已断开,不记录错误(客户端可能已主动断开)
|
||||
print(f"[WebSocket-VerifyCode] WebSocket已断开,跳过消息推送", file=sys.stderr)
|
||||
else:
|
||||
print(f"[WebSocket-VerifyCode] 未获取到AdsPower Cookie,使用Playwright Cookie", file=sys.stderr)
|
||||
|
||||
del temp_sessions[session_id]
|
||||
print(f"[WebSocket-VerifyCode] 已释放session: {session_id}", file=sys.stderr)
|
||||
except Exception as e:
|
||||
print(f"[WebSocket-VerifyCode] 释放浏览器失败: {str(e)}", file=sys.stderr)
|
||||
except Exception as e:
|
||||
print(f"[WebSocket-VerifyCode] 释放浏览器失败: {str(e)}", file=sys.stderr)
|
||||
else:
|
||||
@@ -1665,6 +1904,26 @@ async def websocket_login(websocket: WebSocket, session_id: str):
|
||||
})
|
||||
print(f"[WebSocket] 已回复测试消息", file=sys.stderr)
|
||||
|
||||
# 处理消息ACK确认
|
||||
elif msg_type == 'ack':
|
||||
message_id = msg.get('message_id')
|
||||
if message_id:
|
||||
ws_manager.confirm_message(session_id, message_id)
|
||||
print(f"[WebSocket] 收到ACK确认: {message_id}", file=sys.stderr)
|
||||
|
||||
# 处理拉取未确认消息请求
|
||||
elif msg_type == 'pull_unconfirmed':
|
||||
unconfirmed = ws_manager.get_unconfirmed_messages(session_id)
|
||||
if unconfirmed:
|
||||
print(f"[WebSocket] 前端请求拉取未确认消息: {len(unconfirmed)}条", file=sys.stderr)
|
||||
# 逐条重发
|
||||
for msg in unconfirmed:
|
||||
await websocket.send_json(msg)
|
||||
await asyncio.sleep(0.1) # 间陑00ms
|
||||
print(f"[WebSocket] 已重发所有未确认消息", file=sys.stderr)
|
||||
else:
|
||||
print(f"[WebSocket] 没有未确认消息", file=sys.stderr)
|
||||
|
||||
# 处理发送验证码消息
|
||||
elif msg_type == 'send_code':
|
||||
phone = msg.get('phone')
|
||||
@@ -1675,11 +1934,66 @@ async def websocket_login(websocket: WebSocket, session_id: str):
|
||||
# 直接处理发送验证码(不使用create_task)
|
||||
result, service_instance = await handle_send_code_ws(session_id, phone, country_code, login_page, websocket)
|
||||
|
||||
# 如果需要扫码,在当前协程中启动监听
|
||||
# 如果需要扫码,在当前协程中启动监听,并在扫码成功后自动继续发送验证码
|
||||
if result.get("need_captcha") and service_instance:
|
||||
print(f"[WebSocket] 在主协程中启动扫码监听: {session_id}", file=sys.stderr)
|
||||
|
||||
# 创建一个包装函数,在扫码完成后自动继续发送验证码
|
||||
async def monitor_and_continue():
|
||||
try:
|
||||
# 等待扫码完成
|
||||
print(f"[WebSocket] 开始监听扫码...", file=sys.stderr)
|
||||
await service_instance._monitor_qrcode_scan(session_id)
|
||||
print(f"[WebSocket] 扫码监听已返回,说明扫码已完成", file=sys.stderr)
|
||||
|
||||
# 扫码成功,自动重新发送验证码
|
||||
print(f"[WebSocket] 扫码成功,自动重新发送验证码...", file=sys.stderr)
|
||||
|
||||
# 从之前的result中获取参数
|
||||
retry_phone = result.get('phone', phone)
|
||||
retry_country_code = result.get('country_code', country_code)
|
||||
retry_login_page = result.get('login_page', login_page)
|
||||
|
||||
# 重新调用send_verification_code
|
||||
retry_result = await service_instance.send_verification_code(
|
||||
phone=retry_phone,
|
||||
country_code=retry_country_code,
|
||||
login_page=retry_login_page,
|
||||
session_id=session_id
|
||||
)
|
||||
|
||||
# 检查是否成功
|
||||
if retry_result.get("success"):
|
||||
print(f"[WebSocket] 扫码后自动发送验证码成功", file=sys.stderr)
|
||||
# 推送成功消息给前端
|
||||
await websocket.send_json({
|
||||
"type": "code_sent",
|
||||
"success": True,
|
||||
"message": "扫码验证完成,验证码已自动发送"
|
||||
})
|
||||
else:
|
||||
print(f"[WebSocket] 扫码后自动发送验证码失败: {retry_result.get('error')}", file=sys.stderr)
|
||||
# 推送失败消息给前端
|
||||
await websocket.send_json({
|
||||
"type": "code_sent",
|
||||
"success": False,
|
||||
"message": retry_result.get("error", "自动发送验证码失败")
|
||||
})
|
||||
except Exception as e:
|
||||
print(f"[WebSocket] 扫码后自动继续流程异常: {str(e)}", file=sys.stderr)
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
try:
|
||||
await websocket.send_json({
|
||||
"type": "code_sent",
|
||||
"success": False,
|
||||
"message": f"自动继续流程异常: {str(e)}"
|
||||
})
|
||||
except:
|
||||
pass
|
||||
|
||||
# 使用create_task在后台监听,但不阻塞当前消息循环
|
||||
asyncio.create_task(service_instance._monitor_qrcode_scan(session_id))
|
||||
asyncio.create_task(monitor_and_continue())
|
||||
print(f"[WebSocket] 已启动扫码监听任务", file=sys.stderr)
|
||||
|
||||
# 处理验证码验证消息
|
||||
@@ -1693,6 +2007,51 @@ async def websocket_login(websocket: WebSocket, session_id: str):
|
||||
# 启动异步任务处理验证码验证
|
||||
asyncio.create_task(handle_verify_code_ws(session_id, phone, code, country_code, login_page, websocket))
|
||||
|
||||
# 处理刷新二维码消息
|
||||
elif msg_type == 'refresh_qrcode':
|
||||
print(f"[WebSocket] 收到刷新二维码请求: session_id={session_id}", file=sys.stderr)
|
||||
|
||||
# 从temp_sessions中获取service实例
|
||||
if session_id in temp_sessions:
|
||||
browser_data = temp_sessions[session_id]
|
||||
service_instance = browser_data.get('service')
|
||||
|
||||
if service_instance:
|
||||
print(f"[WebSocket] 开始刷新二维码...", file=sys.stderr)
|
||||
result = await service_instance.refresh_qrcode()
|
||||
|
||||
if result['success']:
|
||||
# 刷新成功,推送新二维码
|
||||
print(f"[WebSocket] 二维码刷新成功", file=sys.stderr)
|
||||
await websocket.send_json({
|
||||
"type": "qrcode_refreshed",
|
||||
"success": True,
|
||||
"qrcode_image": result['qrcode_image'],
|
||||
"message": result.get('message', '二维码已刷新')
|
||||
})
|
||||
else:
|
||||
# 刷新失败
|
||||
print(f"[WebSocket] 二维码刷新失败: {result.get('message', '未知错误')}", file=sys.stderr)
|
||||
await websocket.send_json({
|
||||
"type": "qrcode_refreshed",
|
||||
"success": False,
|
||||
"message": result.get('message', '刷新失败')
|
||||
})
|
||||
else:
|
||||
print(f"[WebSocket] 错误: 未找到service实例", file=sys.stderr)
|
||||
await websocket.send_json({
|
||||
"type": "qrcode_refreshed",
|
||||
"success": False,
|
||||
"message": "内部错误: 服务实例不存在"
|
||||
})
|
||||
else:
|
||||
print(f"[WebSocket] 错误: session_id {session_id} 不在temp_sessions中", file=sys.stderr)
|
||||
await websocket.send_json({
|
||||
"type": "qrcode_refreshed",
|
||||
"success": False,
|
||||
"message": "会话已过期,请重新发送验证码"
|
||||
})
|
||||
|
||||
except json.JSONDecodeError:
|
||||
print(f"[WebSocket] 无法解析为JSON: {data}", file=sys.stderr)
|
||||
|
||||
@@ -1707,8 +2066,8 @@ async def websocket_login(websocket: WebSocket, session_id: str):
|
||||
try:
|
||||
redis_task.cancel()
|
||||
await pubsub.unsubscribe(channel)
|
||||
await pubsub.close()
|
||||
await redis_client.close()
|
||||
await pubsub.aclose()
|
||||
await redis_client.aclose() # 使用aclose()而不是close()
|
||||
print(f"[WebSocket] 已取消Redis订阅: {channel}", file=sys.stderr)
|
||||
except:
|
||||
pass
|
||||
@@ -1716,16 +2075,16 @@ async def websocket_login(websocket: WebSocket, session_id: str):
|
||||
# 释放浏览器实例
|
||||
try:
|
||||
# 检查是否有临时浏览器需要释放
|
||||
if session_id in browser_pool.temp_browsers:
|
||||
if session_id in temp_sessions:
|
||||
print(f"[WebSocket] 检测到未释放的临时浏览器,开始清理: {session_id}", file=sys.stderr)
|
||||
await browser_pool.release_temp_browser(session_id)
|
||||
print(f"[WebSocket] 已释放临时浏览器: {session_id}", file=sys.stderr)
|
||||
|
||||
# 检查是否有扫码页面需要释放
|
||||
if session_id in browser_pool.qrcode_pages:
|
||||
print(f"[WebSocket] 检测到未释放的扫码页面,开始清理: {session_id}", file=sys.stderr)
|
||||
await browser_pool.release_qrcode_page(session_id)
|
||||
print(f"[WebSocket] 已释放扫码页面: {session_id}", file=sys.stderr)
|
||||
try:
|
||||
service = temp_sessions[session_id].get('service')
|
||||
if service:
|
||||
await service.close_browser() # 使用正确的方法名
|
||||
del temp_sessions[session_id]
|
||||
print(f"[WebSocket] 已释放临时浏览器: {session_id}", file=sys.stderr)
|
||||
except Exception as e:
|
||||
print(f"[WebSocket] 释放临时浏览器失败: {str(e)}", file=sys.stderr)
|
||||
except Exception as e:
|
||||
print(f"[WebSocket] 释放浏览器异常: {str(e)}", file=sys.stderr)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user