This commit is contained in:
sjk
2026-01-07 22:55:12 +08:00
parent cb267e8d5e
commit 4720ab2a15
76 changed files with 3110 additions and 7168 deletions

View File

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