xianyu-backend-java/api_captcha_remote.py
2025-11-24 16:24:09 +08:00

319 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
刮刮乐远程控制 API 路由
提供 WebSocket 和 HTTP 接口用于远程操作滑块验证
"""
from fastapi import APIRouter, WebSocket, WebSocketDisconnect, HTTPException
from fastapi.responses import HTMLResponse, FileResponse
from pydantic import BaseModel
from typing import Optional, List
import asyncio
import os
from loguru import logger
from utils.captcha_remote_control import captcha_controller
# 创建路由器
router = APIRouter(prefix="/api/captcha", tags=["captcha"])
class MouseEvent(BaseModel):
"""鼠标事件模型"""
session_id: str
event_type: str # down, move, up
x: int
y: int
class SessionCheckRequest(BaseModel):
"""会话检查请求"""
session_id: str
# =============================================================================
# WebSocket 端点 - 实时通信
# =============================================================================
@router.websocket("/ws/{session_id}")
async def websocket_endpoint(websocket: WebSocket, session_id: str):
"""
WebSocket 连接用于实时传输截图和接收鼠标事件
"""
await websocket.accept()
logger.info(f"🔌 WebSocket 连接建立: {session_id}")
# 注册 WebSocket 连接
captcha_controller.websocket_connections[session_id] = websocket
try:
# 发送初始会话信息
if session_id in captcha_controller.active_sessions:
session_data = captcha_controller.active_sessions[session_id]
await websocket.send_json({
'type': 'session_info',
'screenshot': session_data['screenshot'],
'captcha_info': session_data['captcha_info'],
'viewport': session_data['viewport']
})
# 不启动自动刷新,改为只在操作时更新(极速优化)
# refresh_task = asyncio.create_task(
# captcha_controller.auto_refresh_screenshot(session_id, interval=1.5)
# )
else:
await websocket.send_json({
'type': 'error',
'message': '会话不存在'
})
await websocket.close()
return
# 持续接收客户端消息
while True:
data = await websocket.receive_json()
msg_type = data.get('type')
if msg_type == 'mouse_event':
# 处理鼠标事件
event_type = data.get('event_type')
x = data.get('x')
y = data.get('y')
success = await captcha_controller.handle_mouse_event(
session_id, event_type, x, y
)
if success:
# 只在鼠标释放后才检查完成状态
if event_type == 'up':
# 等待页面更新(给验证码一些反应时间)
await asyncio.sleep(1.0)
# 多次确认滑块确实消失
completed = await captcha_controller.check_completion(session_id)
if completed:
# 再次确认(避免误判)
await asyncio.sleep(0.5)
completed = await captcha_controller.check_completion(session_id)
if completed:
await websocket.send_json({
'type': 'completed',
'message': '验证成功!'
})
logger.success(f"✅ 验证完成: {session_id}")
break
else:
# 更新截图显示验证结果
screenshot = await captcha_controller.update_screenshot(session_id)
if screenshot:
await websocket.send_json({
'type': 'screenshot_update',
'screenshot': screenshot
})
else:
# 按下或移动时,实时更新截图(截取整个验证码容器)
if event_type in ['down', 'move']:
# 截取整个验证码容器,降低质量换取速度
screenshot = await captcha_controller.update_screenshot(session_id, quality=30)
if screenshot:
await websocket.send_json({
'type': 'screenshot_update',
'screenshot': screenshot
})
elif msg_type == 'check_completion':
# 手动检查完成状态
completed = await captcha_controller.check_completion(session_id)
await websocket.send_json({
'type': 'completion_status',
'completed': completed
})
if completed:
break
elif msg_type == 'ping':
# 心跳
await websocket.send_json({'type': 'pong'})
except WebSocketDisconnect:
logger.info(f"🔌 WebSocket 连接断开: {session_id}")
except Exception as e:
logger.error(f"❌ WebSocket 错误: {e}")
import traceback
logger.error(traceback.format_exc())
finally:
# 清理
if session_id in captcha_controller.websocket_connections:
del captcha_controller.websocket_connections[session_id]
logger.info(f"🔒 WebSocket 会话结束: {session_id}")
# =============================================================================
# HTTP 端点 - REST API
# =============================================================================
@router.get("/sessions")
async def get_active_sessions():
"""获取所有活跃的验证会话"""
sessions = []
for session_id, data in captcha_controller.active_sessions.items():
sessions.append({
'session_id': session_id,
'completed': data.get('completed', False),
'has_websocket': session_id in captcha_controller.websocket_connections
})
return {
'count': len(sessions),
'sessions': sessions
}
@router.get("/session/{session_id}")
async def get_session_info(session_id: str):
"""获取指定会话的信息"""
if session_id not in captcha_controller.active_sessions:
raise HTTPException(status_code=404, detail="会话不存在")
session_data = captcha_controller.active_sessions[session_id]
return {
'session_id': session_id,
'screenshot': session_data['screenshot'],
'captcha_info': session_data['captcha_info'],
'viewport': session_data['viewport'],
'completed': session_data.get('completed', False)
}
@router.get("/screenshot/{session_id}")
async def get_screenshot(session_id: str):
"""获取最新截图"""
screenshot = await captcha_controller.update_screenshot(session_id)
if not screenshot:
raise HTTPException(status_code=404, detail="无法获取截图")
return {'screenshot': screenshot}
@router.post("/mouse_event")
async def handle_mouse_event(event: MouseEvent):
"""处理鼠标事件HTTP方式不推荐建议使用WebSocket"""
success = await captcha_controller.handle_mouse_event(
event.session_id,
event.event_type,
event.x,
event.y
)
if not success:
raise HTTPException(status_code=400, detail="处理失败")
# 检查是否完成
completed = await captcha_controller.check_completion(event.session_id)
return {
'success': True,
'completed': completed
}
@router.post("/check_completion")
async def check_completion(request: SessionCheckRequest):
"""检查验证是否完成"""
completed = await captcha_controller.check_completion(request.session_id)
return {
'session_id': request.session_id,
'completed': completed
}
@router.delete("/session/{session_id}")
async def close_session(session_id: str):
"""关闭会话"""
await captcha_controller.close_session(session_id)
return {'success': True}
# =============================================================================
# 前端页面
# =============================================================================
@router.get("/status/{session_id}")
async def get_captcha_status(session_id: str):
"""
获取验证状态
用于前端轮询检查验证是否完成
"""
try:
is_completed = captcha_controller.is_completed(session_id)
session_exists = captcha_controller.session_exists(session_id)
return {
"success": True,
"completed": is_completed,
"session_exists": session_exists,
"session_id": session_id
}
except Exception as e:
logger.error(f"获取验证状态失败: {e}")
return {
"success": False,
"completed": False,
"session_exists": False,
"session_id": session_id,
"error": str(e)
}
@router.get("/control", response_class=HTMLResponse)
async def captcha_control_page():
"""返回滑块控制页面"""
html_file = "captcha_control.html"
if os.path.exists(html_file):
return FileResponse(html_file, media_type="text/html")
else:
# 返回简单的提示页面
return HTMLResponse(content="""
<!DOCTYPE html>
<html>
<head>
<title>验证码控制面板</title>
</head>
<body>
<h1>验证码控制面板</h1>
<p>前端页面文件 captcha_control.html 不存在</p>
<p>请查看文档了解如何创建前端页面</p>
</body>
</html>
""")
@router.get("/control/{session_id}", response_class=HTMLResponse)
async def captcha_control_page_with_session(session_id: str):
"""返回带会话ID的滑块控制页面"""
html_file = "captcha_control.html"
if os.path.exists(html_file):
with open(html_file, 'r', encoding='utf-8') as f:
html_content = f.read()
# 注入会话ID
html_content = html_content.replace(
'</body>',
f'<script>window.INITIAL_SESSION_ID = "{session_id}";</script></body>'
)
return HTMLResponse(content=html_content)
else:
raise HTTPException(status_code=404, detail="前端页面不存在")