This commit is contained in:
sjk
2026-01-23 16:27:47 +08:00
parent 213229953b
commit e8e6d913df
26 changed files with 4294 additions and 431 deletions

View File

@@ -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)