commit
This commit is contained in:
384
backend/main.py
384
backend/main.py
@@ -288,9 +288,10 @@ async def send_code(request: SendCodeRequest):
|
||||
支持选择从创作者中心或小红书首页登录
|
||||
并发支持:为每个请求分配独立的浏览器实例
|
||||
"""
|
||||
# 使用手机号作为session_id,确保发送验证码和登录验证使用同一个浏览器
|
||||
session_id = f"xhs_login_{request.phone}"
|
||||
print(f"[发送验证码] session_id={session_id}, phone={request.phone}", file=sys.stderr)
|
||||
# 使用随机UUID作为session_id,确保每次都创建全新浏览器,完全不复用
|
||||
import uuid
|
||||
session_id = f"xhs_login_{uuid.uuid4().hex}"
|
||||
print(f"[发送验证码] 创建全新浏览器实例 session_id={session_id}, phone={request.phone}", file=sys.stderr)
|
||||
|
||||
# 获取配置中的默认login_page,如果API传入了则优先使用API参数
|
||||
config = get_config()
|
||||
@@ -315,6 +316,14 @@ 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)
|
||||
|
||||
return BaseResponse(
|
||||
code=0,
|
||||
message="验证码已发送,请在小红书APP中查看",
|
||||
@@ -421,6 +430,346 @@ async def verify_phone_code(request: VerifyCodeRequest):
|
||||
data=None
|
||||
)
|
||||
|
||||
@app.post("/api/xhs/qrcode/start", response_model=BaseResponse)
|
||||
async def start_qrcode_login():
|
||||
"""
|
||||
启动小红书扫码登录,返回二维码图片和状态
|
||||
每个用户必须使用独立的浏览器实例,不能共享Context
|
||||
"""
|
||||
try:
|
||||
print("[扫码登录] 启动扫码登录流程", file=sys.stderr)
|
||||
|
||||
# 使用随机UUID创建临时的登录服务实例,完全不复用
|
||||
import uuid
|
||||
session_id = f"qrcode_login_{uuid.uuid4().hex}"
|
||||
print(f"[扫码登录] 创建全新浏览器实例 session_id={session_id}", file=sys.stderr)
|
||||
|
||||
qrcode_service = XHSLoginService(
|
||||
use_pool=True,
|
||||
headless=login_service.headless,
|
||||
session_id=session_id,
|
||||
use_page_isolation=False # 小红书不支持页面隔离,必须独立浏览器
|
||||
)
|
||||
|
||||
# 初始化浏览器
|
||||
await qrcode_service.init_browser()
|
||||
|
||||
# 启动扫码登录
|
||||
result = await qrcode_service.start_qrcode_login()
|
||||
|
||||
if result["success"]:
|
||||
return BaseResponse(
|
||||
code=0,
|
||||
message="二维码获取成功",
|
||||
data={
|
||||
"session_id": session_id,
|
||||
"qrcode_image": result["qrcode_image"],
|
||||
"status_text": result.get("status_text", ""),
|
||||
"status_desc": result.get("status_desc", ""),
|
||||
"is_expired": result.get("is_expired", False),
|
||||
# 添加二维码创建信息
|
||||
"qr_url": result.get("qr_url", ""),
|
||||
"qr_id": result.get("qr_id", ""),
|
||||
"qr_code": result.get("qr_code", ""),
|
||||
"multi_flag": result.get("multi_flag", 0)
|
||||
}
|
||||
)
|
||||
else:
|
||||
# 失败后释放临时浏览器
|
||||
if browser_pool and session_id:
|
||||
try:
|
||||
await browser_pool.release_temp_browser(session_id)
|
||||
print(f"[扫码登录] 已释放失败的session: {session_id}", file=sys.stderr)
|
||||
except Exception as release_error:
|
||||
print(f"[扫码登录] 释放浏览器失败: {str(release_error)}", file=sys.stderr)
|
||||
|
||||
return BaseResponse(
|
||||
code=1,
|
||||
message=result.get("error", "获取二维码失败"),
|
||||
data=None
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"[扫码登录] 异常: {str(e)}", file=sys.stderr)
|
||||
|
||||
# 异常后释放临时浏览器
|
||||
if browser_pool and 'session_id' in locals():
|
||||
try:
|
||||
await browser_pool.release_temp_browser(session_id)
|
||||
print(f"[扫码登录] 已释放异常的session: {session_id}", file=sys.stderr)
|
||||
except Exception as release_error:
|
||||
print(f"[扫码登录] 释放浏览器失败: {str(release_error)}", file=sys.stderr)
|
||||
|
||||
return BaseResponse(
|
||||
code=1,
|
||||
message=f"启动扫码登录失败: {str(e)}",
|
||||
data=None
|
||||
)
|
||||
|
||||
@app.post("/api/xhs/qrcode/status")
|
||||
async def get_qrcode_status(request: dict):
|
||||
"""
|
||||
轮询获取扫码状态和最新的二维码图片
|
||||
"""
|
||||
try:
|
||||
session_id = request.get('session_id')
|
||||
if not session_id:
|
||||
return BaseResponse(
|
||||
code=1,
|
||||
message="session_id不能为空",
|
||||
data=None
|
||||
)
|
||||
|
||||
# 检查session是否存在于浏览器池中
|
||||
if browser_pool and session_id not in browser_pool.temp_browsers:
|
||||
print(f"[扫码状态] session_id={session_id} 已失效,要求重新创建二维码", file=sys.stderr)
|
||||
return BaseResponse(
|
||||
code=2, # 特殊错误码,表示session失效
|
||||
message="会话已失效,请刷新二维码重新开始",
|
||||
data={
|
||||
"session_expired": True
|
||||
}
|
||||
)
|
||||
|
||||
# 使用session_id获取浏览器实例
|
||||
qrcode_service = XHSLoginService(
|
||||
use_pool=True,
|
||||
headless=login_service.headless,
|
||||
session_id=session_id
|
||||
)
|
||||
|
||||
# 初始化浏览器(会复用已有的)
|
||||
await qrcode_service.init_browser()
|
||||
|
||||
# 提取当前二维码状态
|
||||
result = await qrcode_service.extract_qrcode_with_status()
|
||||
|
||||
if result["success"]:
|
||||
# 如果登录成功,返回登录信息
|
||||
if result.get("login_success"):
|
||||
return BaseResponse(
|
||||
code=0,
|
||||
message="扫码登录成功",
|
||||
data={
|
||||
"login_success": True,
|
||||
"user_info": result.get("user_info"),
|
||||
"cookies": result.get("cookies"),
|
||||
"cookies_full": result.get("cookies_full"),
|
||||
"login_state": result.get("login_state")
|
||||
}
|
||||
)
|
||||
else:
|
||||
# 还未登录,返回二维码状态
|
||||
return BaseResponse(
|
||||
code=0,
|
||||
message="获取状态成功",
|
||||
data={
|
||||
"login_success": False,
|
||||
"qrcode_image": result["qrcode_image"],
|
||||
"status_text": result.get("status_text", ""),
|
||||
"status_desc": result.get("status_desc", ""),
|
||||
"is_expired": result.get("is_expired", False)
|
||||
}
|
||||
)
|
||||
else:
|
||||
return BaseResponse(
|
||||
code=1,
|
||||
message=result.get("error", "获取状态失败"),
|
||||
data=None
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"[扫码状态] 异常: {str(e)}", file=sys.stderr)
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return BaseResponse(
|
||||
code=1,
|
||||
message=f"获取状态失败: {str(e)}",
|
||||
data=None
|
||||
)
|
||||
|
||||
@app.post("/api/xhs/qrcode/refresh")
|
||||
async def refresh_qrcode(request: dict):
|
||||
"""
|
||||
刷新过期的二维码
|
||||
"""
|
||||
try:
|
||||
session_id = request.get('session_id')
|
||||
if not session_id:
|
||||
return BaseResponse(
|
||||
code=1,
|
||||
message="session_id不能为空",
|
||||
data=None
|
||||
)
|
||||
|
||||
# 使用session_id获取浏览器实例
|
||||
qrcode_service = XHSLoginService(
|
||||
use_pool=True,
|
||||
headless=login_service.headless,
|
||||
session_id=session_id
|
||||
)
|
||||
|
||||
# 初始化浏览器
|
||||
await qrcode_service.init_browser()
|
||||
|
||||
# 刷新二维码
|
||||
result = await qrcode_service.refresh_qrcode()
|
||||
|
||||
if result["success"]:
|
||||
return BaseResponse(
|
||||
code=0,
|
||||
message="二维码刷新成功",
|
||||
data={
|
||||
"qrcode_image": result["qrcode_image"],
|
||||
"status_text": result.get("status_text", ""),
|
||||
"status_desc": result.get("status_desc", ""),
|
||||
"is_expired": result.get("is_expired", False),
|
||||
# 添加二维码创建信息
|
||||
"qr_url": result.get("qr_url", ""),
|
||||
"qr_id": result.get("qr_id", ""),
|
||||
"qr_code": result.get("qr_code", ""),
|
||||
"multi_flag": result.get("multi_flag", 0)
|
||||
}
|
||||
)
|
||||
else:
|
||||
# 检查是否需要重启
|
||||
if result.get("need_restart"):
|
||||
return BaseResponse(
|
||||
code=3, # 特殊错误码,表示需要重启
|
||||
message="页面已失效,请重新启动扫码登录",
|
||||
data={
|
||||
"need_restart": True
|
||||
}
|
||||
)
|
||||
return BaseResponse(
|
||||
code=1,
|
||||
message=result.get("error", "刷新失败"),
|
||||
data=None
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"[刷新二维码] 异常: {str(e)}", file=sys.stderr)
|
||||
return BaseResponse(
|
||||
code=1,
|
||||
message=f"刷新二维码失败: {str(e)}",
|
||||
data=None
|
||||
)
|
||||
|
||||
@app.post("/api/xhs/save-bind-info")
|
||||
async def save_bind_info(request: dict):
|
||||
"""
|
||||
保存扫码登录的绑定信息到Go后端
|
||||
与验证码登录不同,扫码登录直接返回了完整数据,需要由Python转发给Go后端保存
|
||||
"""
|
||||
try:
|
||||
employee_id = request.get('employee_id')
|
||||
cookies_full = request.get('cookies_full', [])
|
||||
user_info = request.get('user_info', {})
|
||||
login_state = request.get('login_state', {})
|
||||
|
||||
if not employee_id:
|
||||
return BaseResponse(
|
||||
code=1,
|
||||
message="employee_id不能为空",
|
||||
data=None
|
||||
)
|
||||
|
||||
# 调用Go后端API保存
|
||||
config = get_config()
|
||||
go_backend_url = config.get_str('go_backend.url', 'http://localhost:8080')
|
||||
|
||||
# 构造请求数据,模仏bind-xhs接口的返回格式
|
||||
# Go后端期望接收的是验证码登录的结果
|
||||
save_data = {
|
||||
"employee_id": employee_id,
|
||||
"cookies_full": cookies_full,
|
||||
"user_info": user_info,
|
||||
"login_state": login_state
|
||||
}
|
||||
|
||||
import aiohttp
|
||||
async with aiohttp.ClientSession() as session:
|
||||
# 获取小程序传来的token
|
||||
auth_header = request.get('Authorization', '')
|
||||
|
||||
async with session.post(
|
||||
f"{go_backend_url}/api/xhs/save-qrcode-login",
|
||||
json=save_data,
|
||||
headers={'Authorization': auth_header} if auth_header else {}
|
||||
) as resp:
|
||||
result = await resp.json()
|
||||
|
||||
if resp.status == 200 and result.get('code') == 200:
|
||||
return BaseResponse(
|
||||
code=0,
|
||||
message="保存成功",
|
||||
data=result.get('data')
|
||||
)
|
||||
else:
|
||||
return BaseResponse(
|
||||
code=1,
|
||||
message=result.get('message', '保存失败'),
|
||||
data=None
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"[保存绑定信息] 异常: {str(e)}", file=sys.stderr)
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return BaseResponse(
|
||||
code=1,
|
||||
message=f"保存失败: {str(e)}",
|
||||
data=None
|
||||
)
|
||||
|
||||
@app.post("/api/xhs/qrcode/cancel")
|
||||
async def cancel_qrcode_login(request: dict):
|
||||
"""
|
||||
取消扫码登录,释放浏览器资源
|
||||
用于用户切换登录方式或关闭页面时
|
||||
"""
|
||||
try:
|
||||
session_id = request.get('session_id')
|
||||
if not session_id:
|
||||
return BaseResponse(
|
||||
code=1,
|
||||
message="session_id不能为空",
|
||||
data=None
|
||||
)
|
||||
|
||||
# 释放临时浏览器
|
||||
if browser_pool:
|
||||
try:
|
||||
await browser_pool.release_temp_browser(session_id)
|
||||
print(f"[取消扫码] 已释放 session: {session_id}", file=sys.stderr)
|
||||
return BaseResponse(
|
||||
code=0,
|
||||
message="已取消扫码登录",
|
||||
data=None
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"[取消扫码] 释放浏览器失败: {str(e)}", file=sys.stderr)
|
||||
# 即使失败也返回成功,不影响用户体验
|
||||
return BaseResponse(
|
||||
code=0,
|
||||
message="已取消扫码登录",
|
||||
data=None
|
||||
)
|
||||
else:
|
||||
return BaseResponse(
|
||||
code=0,
|
||||
message="浏览器池未初始化",
|
||||
data=None
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"[取消扫码] 异常: {str(e)}", file=sys.stderr)
|
||||
return BaseResponse(
|
||||
code=0, # 即使异常也返回成功
|
||||
message="已取消扫码登录",
|
||||
data=None
|
||||
)
|
||||
|
||||
@app.post("/api/xhs/login", response_model=BaseResponse)
|
||||
async def login(request: LoginRequest):
|
||||
"""
|
||||
@@ -429,13 +778,16 @@ async def login(request: LoginRequest):
|
||||
支持选择从创作者中心或小红书首页登录
|
||||
并发支持:可复用send-code接口的session_id
|
||||
"""
|
||||
# 使用手机号作为session_id,复用发送验证码时的浏览器
|
||||
# 如果前端传了session_id就使用前端的,否则根据手机号生成
|
||||
# 必须使用前端传递的session_id来复用浏览器
|
||||
# 如果前端没有传session_id,说明前端实现有问题
|
||||
if not request.session_id:
|
||||
session_id = f"xhs_login_{request.phone}"
|
||||
else:
|
||||
session_id = request.session_id
|
||||
return BaseResponse(
|
||||
code=1,
|
||||
message="缺少session_id参数,无法复用浏览器实例,请重新发送验证码",
|
||||
data=None
|
||||
)
|
||||
|
||||
session_id = request.session_id
|
||||
print(f"[登录验证] session_id={session_id}, phone={request.phone}", file=sys.stderr)
|
||||
|
||||
# 获取配置中的默认login_page,如果API传入了则优先使用API参数
|
||||
@@ -448,7 +800,15 @@ async def login(request: LoginRequest):
|
||||
try:
|
||||
# 如果有session_id,复用send-code的浏览器;否则创建新的
|
||||
if session_id:
|
||||
print(f"[登录验证] 复用send-code的浏览器: {session_id}", file=sys.stderr)
|
||||
print(f"[登录验证] 尝试复用send-code的浏览器: {session_id}", file=sys.stderr)
|
||||
|
||||
# 先检查浏览器池中是否存在该session
|
||||
if browser_pool and session_id in browser_pool.temp_browsers:
|
||||
print(f"[登录验证] ✅ 在浏览器池中找到session: {session_id}", file=sys.stderr)
|
||||
else:
|
||||
print(f"[登录验证] ⚠️ 浏览器池中未找到session: {session_id}", file=sys.stderr)
|
||||
print(f"[登录验证] 当前池中的session列表: {list(browser_pool.temp_browsers.keys()) if browser_pool else 'None'}", file=sys.stderr)
|
||||
|
||||
request_login_service = XHSLoginService(
|
||||
use_pool=True,
|
||||
headless=login_service.headless, # 使用配置文件中的login.headless配置
|
||||
@@ -456,6 +816,12 @@ async def login(request: LoginRequest):
|
||||
)
|
||||
# 初始化浏览器,以便从浏览器池获取临时浏览器
|
||||
await request_login_service.init_browser()
|
||||
|
||||
# 再次验证浏览器是否正常初始化
|
||||
if request_login_service.page:
|
||||
print(f"[登录验证] ✅ 浏览器初始化成功,当前URL: {request_login_service.page.url}", file=sys.stderr)
|
||||
else:
|
||||
print(f"[登录验证] ❌ 浏览器初始化失败,page为None", file=sys.stderr)
|
||||
else:
|
||||
# 旧逻辑:不传session_id,使用全局登录服务
|
||||
print(f"[登录验证] 使用全局登录服务(旧逻辑)", file=sys.stderr)
|
||||
|
||||
Reference in New Issue
Block a user