refactor: v2

backend & frontend
This commit is contained in:
2026-01-01 18:38:21 +08:00
parent 3d201bc497
commit fdc725b893
109 changed files with 22918 additions and 1135 deletions
+275
View File
@@ -0,0 +1,275 @@
import requests
import json
import time
import os
import logging
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options
from typing import Dict, Any
from backend.config import settings
from backend.workers.email_notifier import send_success_notification, send_failure_notification
logger = logging.getLogger(__name__)
# Chrome 配置路径 - 从设置中读取
CHROME_BINARY_PATH = settings.CHROME_BINARY_PATH
CHROMEDRIVER_PATH = settings.CHROMEDRIVER_PATH
def get_live_x_api_payload(auth_token: str) -> str:
"""
启动一个临时的无头浏览器会话,获取新鲜的 x-api-request-payload
Args:
auth_token: 用户的 Authorization Token
Returns:
x-api-request-payload 值,失败返回 None
"""
logger.info("正在启动临时浏览器会话以监听网络日志...")
# 根据配置创建 Service
if CHROMEDRIVER_PATH:
service = Service(executable_path=CHROMEDRIVER_PATH)
else:
service = Service() # 使用 Selenium Manager 自动管理
chrome_options = Options()
# 如果配置了 Chrome 路径,则使用配置的路径
if CHROME_BINARY_PATH:
chrome_options.binary_location = CHROME_BINARY_PATH
# 开启性能日志记录功能
logging_prefs = {'performance': 'ALL'}
chrome_options.set_capability('goog:loggingPrefs', logging_prefs)
# Headless 模式配置
user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36"
chrome_options.add_argument(f'user-agent={user_agent}')
chrome_options.add_argument("--headless")
chrome_options.add_argument("--no-sandbox")
chrome_options.add_argument("--disable-dev-shm-usage")
chrome_options.add_argument("--window-size=1920,1080")
chrome_options.add_argument('--ignore-certificate-errors')
chrome_options.add_experimental_option("excludeSwitches", ["enable-automation"])
driver = webdriver.Chrome(service=service, options=chrome_options)
payload_signature = None
try:
# 导航到同源空白页,用于设置 Cookie
driver.get("https://i.jielong.com/my-class")
# 注入长期 Token
driver.add_cookie({
'name': 'token',
'value': auth_token,
'domain': '.jielong.com'
})
# 导航到触发 API 的页面
driver.get("https://i.jielong.com/my-form")
# 等待并捕获 x-api-request-payload
max_wait_time = 20 # 最多等待20秒
start_time = time.time()
found = False
while time.time() - start_time < max_wait_time:
logs = driver.get_log('performance')
for entry in logs:
log = json.loads(entry['message'])['message']
if log['method'] == 'Network.requestWillBeSent':
headers = log.get('params', {}).get('request', {}).get('headers', {})
headers_lower = {k.lower(): v for k, v in headers.items()}
if 'x-api-request-payload' in headers_lower:
payload_signature = headers_lower['x-api-request-payload']
logger.info("成功通过网络日志捕获到现场的 x-api-request-payload")
found = True
break
if found:
break
time.sleep(1)
if not payload_signature:
raise Exception(f"{max_wait_time} 秒内未能通过网络日志捕获到 x-api-request-payload。")
except Exception as e:
logger.error(f"获取现场 x-api-request-payload 时失败: {e}")
debug_screenshot = os.path.join(settings.BASE_DIR, 'payload_debug.png')
driver.save_screenshot(debug_screenshot)
finally:
driver.quit()
return payload_signature
def perform_check_in(task, user_token: str) -> Dict[str, Any]:
"""
执行打卡任务
Args:
task: CheckInTask 对象,包含打卡任务配置
user_token: 用户的 Authorization Token(从 task.user.authorization 获取)
Returns:
打卡结果字典:
- success: 是否成功
- status: 状态 (success/failure)
- response_text: 响应文本
- error_message: 错误信息
"""
# 从 payload_config 中提取 Signature 用于日志
try:
payload_dict = json.loads(task.payload_config) if task.payload_config else {}
signature = payload_dict.get('Signature', 'Unknown')
except:
signature = 'Unknown'
logger.info(f"Selenium打卡: 正在为任务 ID: {task.id} (Signature: {signature}) 执行打卡...")
if not user_token:
error_msg = f"任务 ID: {task.id} (Signature: {signature}) 的 Token 为空,跳过。"
logger.error(error_msg)
return {
"success": False,
"status": "failure",
"response_text": "",
"error_message": error_msg
}
# 获取 x-api-request-payload
payload_signature = get_live_x_api_payload(user_token)
if not payload_signature:
error_msg = f"任务 ID: {task.id} (Signature: {signature}) 未能获取到现场签名,打卡中止。"
logger.error(error_msg)
return {
"success": False,
"status": "failure",
"response_text": "",
"error_message": error_msg
}
try:
# 使用任务的 payload_config(从模板生成的完整配置,包含 ThreadId)
payload = json.loads(task.payload_config) if task.payload_config else {}
if not payload.get('ThreadId'):
error_msg = f"任务 ID: {task.id} 的 payload_config 缺少 ThreadId"
logger.error(error_msg)
return {
"success": False,
"status": "failure",
"response_text": "",
"error_message": error_msg
}
headers = {
'User-Agent': "Mozilla%2f5.0+(Linux%3b+Android+16%3b+wv)+AppleWebKit%2f537.36+(KHTML%2c+like+Gecko)+Chrome%2f142.0.0.0+Safari%2f537.36+QQ%2f9.2.30.31620+QQ%2fMiniApp",
'Accept-Encoding': "gzip",
'Content-Type': "application/json",
'authorization': f"Bearer {user_token}",
'x-api-request-referer': "https://appservice.qq.com/1110276759",
'x-api-request-payload': payload_signature,
'referer': "https://appservice.qq.com/1110276759/8.10.1.7/page-frame.html",
'platform': "qq",
'x-api-request-mode': "cors",
}
url = "https://api.jielong.com/api/CheckIn/EditRecord"
# 打印请求详情用于调试
payload_json = json.dumps(payload, ensure_ascii=False)
logger.info(f"📤 打卡请求详情 - 任务 ID: {task.id} (Signature: {signature})")
logger.info(f"📍 URL: {url}")
logger.info(f"📦 Payload: {payload_json}")
logger.info(f"🔑 x-api-request-payload: {payload_signature[:50]}...")
response = requests.post(url, data=payload_json, headers=headers)
response.raise_for_status()
response_text = response.text
logger.info(f"✉️ 任务 ID: {task.id} (Signature: {signature}) 打卡请求完成!响应: {response_text}")
# 判断响应内容(参考 V1 实现逻辑)
# 使用用户账户的邮箱,而不是任务的邮箱
email = task.user.email if task.user else None
# 情况1: 明确包含"打卡成功" → 成功
if "打卡成功" in response_text:
logger.info(f"✅ 检测到成功关键字 '打卡成功',打卡成功")
if email:
send_success_notification(email)
return {
"success": True,
"status": "success",
"response_text": response_text,
"error_message": ""
}
# 情况2: 不在打卡时间范围 → 标记为时间范围外
# 支持多种匹配方式:直接文本匹配、JSON Data 字段、Description 字段
elif ("不在打卡时间范围" in response_text or
"不在打卡时间" in response_text or
'"Data":"不在打卡时间范围"' in response_text or
'"Description":"不在打卡时间范围"' in response_text):
logger.warning(f"⏰ 检测到'不在打卡时间范围',打卡时间不符")
return {
"success": False,
"status": "out_of_time",
"response_text": response_text,
"error_message": "不在打卡时间范围内"
}
# 情况3: Token 失效的特征标识 → 失败
elif ("登录" in response_text):
logger.warning(f"⚠️ 检测到登录失败关键字,Token 可能已失效")
if email:
send_failure_notification(email)
return {
"success": False,
"status": "failure",
"response_text": response_text,
"error_message": "Token 已失效,需要重新授权"
}
# 情况4: 其他响应 → 需要人工确认(标记为异常)
else:
logger.warning(f"⚠️ 未识别的响应内容,请检查: {response_text[:200]}...")
# 标记为未知状态,记录完整响应供后续分析
return {
"success": False,
"status": "unknown",
"response_text": response_text,
"error_message": "未识别的响应,请人工确认"
}
except requests.exceptions.RequestException as e:
error_msg = f"为任务 ID: {task.id} (Signature: {signature}) 打卡时请求失败: {e}"
logger.error(error_msg)
response_text = ""
if e.response is not None:
response_text = e.response.text
logger.error(f"响应状态码: {e.response.status_code}, 响应内容: {response_text}")
return {
"success": False,
"status": "failure",
"response_text": response_text,
"error_message": str(e)
}
except Exception as e:
error_msg = f"为任务 ID: {task.id} (Signature: {signature}) 打卡时发生未知错误: {e}"
logger.error(error_msg)
return {
"success": False,
"status": "failure",
"response_text": "",
"error_message": str(e)
}
+240
View File
@@ -0,0 +1,240 @@
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
import time
import logging
import configparser
from pathlib import Path
from backend.config import settings
logger = logging.getLogger(__name__)
# --- 邮件模板 ---
EXPIRATION_HTML_TEMPLATE = """
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>Token 到期通知</title>
<style>
body {{ font-family: Arial, sans-serif; background-color: #f4f4f4; color: #333; margin: 20px; padding: 20px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); }}
h1 {{ color: #d9534f; }}
.message {{ background-color: #fff; padding: 15px; border: 1px solid #ddd; border-radius: 5px; margin-bottom: 20px; }}
.important {{ font-weight: bold; color: #d9534f; }}
.footer {{ font-size: 0.9em; color: #666; }}
</style>
</head>
<body>
<h1>注意!</h1>
<div class="message">
<p>{name},请注意!</p>
<p>您的 <span class="important">token</span> 已经到期,请尽快重新刷新您的 token,否则您的自动打卡功能将会失效。</p>
<p><strong>到期时间:</strong> {exp_time}</p>
</div>
<p class="footer">邮件发送时间: {send_time}</p>
</body>
</html>
"""
SUCCESS_HTML_TEMPLATE = """
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>打卡成功通知</title>
<style>
body {{ font-family: Arial, sans-serif; background-color: #f4f4f4; color: #333; margin: 20px; padding: 20px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); }}
h1 {{ color: #5cb85c; }}
.message {{ background-color: #fff; padding: 15px; border: 1px solid #ddd; border-radius: 5px; margin-bottom: 20px; }}
.important {{ font-weight: bold; color: #5cb85c; }}
.footer {{ font-size: 0.9em; color: #666; }}
</style>
</head>
<body>
<h1>打卡成功!</h1>
<div class="message">
<p>{name},您好!</p>
<p>系统已于 <span class="important">{send_time}</span> 成功为您完成自动打卡。</p>
<p>您无需进行任何操作,此邮件仅作通知。</p>
</div>
<p class="footer">感谢您的使用!</p>
</body>
</html>
"""
FAILURE_HTML_TEMPLATE = """
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>打卡失败通知</title>
<style>
body {{ font-family: Arial, sans-serif; background-color: #f4f4f4; color: #333; margin: 20px; padding: 20px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); }}
h1 {{ color: #d9534f; }}
.message {{ background-color: #fff; padding: 15px; border: 1px solid #ddd; border-radius: 5px; margin-bottom: 20px; }}
.important {{ font-weight: bold; color: #d9534f; }}
.footer {{ font-size: 0.9em; color: #666; }}
</style>
</head>
<body>
<h1>通知:自动打卡失败!</h1>
<div class="message">
<p>{name},您好!</p>
<p>系统于 <span class="important">{send_time}</span> 尝试为您自动打卡时失败。</p>
<p><strong>失败原因:</strong> 服务器返回 "需要登录",这通常意味着您的 <span class="important">Token 已失效</span>。</p>
<p><strong>请您立即刷新您的 Token,以确保后续打卡能够成功。</strong></p>
</div>
<p class="footer">感谢您的使用!</p>
</body>
</html>
"""
def get_email_settings():
"""
从 config.ini 读取邮件配置
Returns:
dict: 邮件配置,如果配置文件不存在则返回 None
"""
if not settings.EMAIL_CONFIG_FILE.exists():
logger.warning("找不到 config.ini,无法发送邮件")
return None
try:
config_parser = configparser.ConfigParser()
config_parser.read(settings.EMAIL_CONFIG_FILE, encoding='utf-8')
if 'Email' not in config_parser:
logger.warning("config.ini 中缺少 [Email] 配置段")
return None
return config_parser['Email']
except Exception as e:
logger.error(f"读取邮件配置失败: {e}")
return None
def _send_email(to_email: str, subject: str, html_content: str, email_settings: dict) -> bool:
"""
发送邮件
Args:
to_email: 收件人邮箱
subject: 邮件主题
html_content: HTML 邮件内容
email_settings: 邮件配置
Returns:
是否发送成功
"""
try:
msg = MIMEMultipart()
msg["From"] = email_settings['senderemail']
msg["To"] = to_email
msg["Subject"] = subject
msg.attach(MIMEText(html_content, 'html', 'utf-8'))
with smtplib.SMTP_SSL(email_settings['smtpserver'], int(email_settings['smtpport'])) as server:
server.login(email_settings['senderemail'], email_settings['senderpassword'])
server.sendmail(msg["From"], msg["To"], msg.as_string())
logger.info(f"已成功向 {to_email} 发送邮件,主题: {subject}")
return True
except Exception as e:
logger.error(f"{to_email} 发送邮件时失败: {e}")
return False
def send_expiration_notification(email: str, jwt_exp: str) -> bool:
"""
发送 Token 到期提醒邮件
Args:
email: 收件人邮箱
jwt_exp: Token 过期时间戳
Returns:
是否发送成功
"""
email_settings = get_email_settings()
if not email_settings:
return False
try:
exp_time = time.strftime("%Y年%m月%d%H:%M:%S", time.localtime(float(jwt_exp)))
send_time = time.strftime("%Y年%m月%d%H:%M:%S", time.localtime())
html = EXPIRATION_HTML_TEMPLATE.format(
name=email,
exp_time=exp_time,
send_time=send_time
)
return _send_email(email, "接龙管家Token到期通知", html, email_settings)
except Exception as e:
logger.error(f"发送过期通知邮件失败: {e}")
return False
def send_success_notification(email: str) -> bool:
"""
发送打卡成功通知邮件
Args:
email: 收件人邮箱
Returns:
是否发送成功
"""
email_settings = get_email_settings()
if not email_settings:
return False
try:
send_time = time.strftime("%Y年%m月%d%H:%M:%S", time.localtime())
html = SUCCESS_HTML_TEMPLATE.format(
name=email,
send_time=send_time
)
return _send_email(email, "自动打卡成功通知", html, email_settings)
except Exception as e:
logger.error(f"发送成功通知邮件失败: {e}")
return False
def send_failure_notification(email: str) -> bool:
"""
发送打卡失败通知邮件
Args:
email: 收件人邮箱
Returns:
是否发送成功
"""
email_settings = get_email_settings()
if not email_settings:
return False
try:
send_time = time.strftime("%Y年%m月%d%H:%M:%S", time.localtime())
html = FAILURE_HTML_TEMPLATE.format(
name=email,
send_time=send_time
)
return _send_email(email, "打卡失败 - 需要刷新Token", html, email_settings)
except Exception as e:
logger.error(f"发送失败通知邮件失败: {e}")
return False
+262
View File
@@ -0,0 +1,262 @@
import os
import logging
import json
from pathlib import Path
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import TimeoutException
from filelock import FileLock
from backend.config import settings
logger = logging.getLogger(__name__)
# Chrome 配置路径
BASE_DIR = settings.BASE_DIR
# 调试文件路径
DEBUG_SCREENSHOT_PATH = os.path.join(BASE_DIR, "debug_screenshot.png")
DEBUG_PAGE_SOURCE_PATH = os.path.join(BASE_DIR, "debug_page_source.html")
def get_chrome_config():
"""获取 Chrome 配置(从 settings 读取)"""
return {
"chrome_binary": settings.CHROME_BINARY_PATH,
"chromedriver": settings.CHROMEDRIVER_PATH
}
def update_session_file(session_id: str, data: dict) -> None:
"""线程安全地写入会话文件"""
filepath = settings.SESSION_DIR / f"{session_id}.json"
lock_path = settings.SESSION_DIR / f"{session_id}.json.lock"
try:
with FileLock(lock_path, timeout=5):
with open(filepath, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=2)
except Exception as e:
logger.error(f"写入会话文件 {filepath} 失败: {e}")
def get_session_status(session_id: str) -> str:
"""安全地读取会话文件的状态"""
filepath = settings.SESSION_DIR / f"{session_id}.json"
lock_path = settings.SESSION_DIR / f"{session_id}.json.lock"
if not filepath.exists():
return None
try:
with FileLock(lock_path, timeout=5):
with open(filepath, 'r', encoding='utf-8') as f:
content = f.read()
if not content:
return None
data = json.loads(content)
return data.get('status')
except (IOError, json.JSONDecodeError) as e:
logger.error(f"读取会话文件 {filepath} 失败: {e}")
return None
def get_session_data(session_id: str) -> dict:
"""读取完整的会话数据"""
filepath = settings.SESSION_DIR / f"{session_id}.json"
lock_path = settings.SESSION_DIR / f"{session_id}.json.lock"
if not filepath.exists():
return None
try:
with FileLock(lock_path, timeout=5):
with open(filepath, 'r', encoding='utf-8') as f:
content = f.read()
if not content:
return None
return json.loads(content)
except (IOError, json.JSONDecodeError) as e:
logger.error(f"读取会话文件 {filepath} 失败: {e}")
return None
def get_token_headless(session_id: str, jwt_sub: str = None, alias: str = None, client_ip: str = "") -> None:
"""
使用 Selenium 获取 QQ 扫码登录的 Token
Args:
session_id: 会话 ID
jwt_sub: QQ 用户标识(老用户刷新 Token 时提供,新用户为 None)
alias: 用户别名(用于新用户注册)
client_ip: 客户端 IP 地址
"""
driver = None
current_step = "初始化"
try:
# 获取 Chrome 配置
chrome_config = get_chrome_config()
chrome_binary_path = chrome_config["chrome_binary"]
chromedriver_path = chrome_config["chromedriver"]
# 配置 Chrome 选项
current_step = "配置 ChromeDriver"
logger.info(f"Selenium ({session_id}): {current_step}...")
chrome_options = Options()
# 如果指定了自定义 Chrome 路径,则使用
if chrome_binary_path:
chrome_options.binary_location = chrome_binary_path
logger.info(f"Selenium ({session_id}): 使用自定义 Chrome 路径: {chrome_binary_path}")
else:
logger.info(f"Selenium ({session_id}): 使用系统默认 Chrome")
# Headless 模式配置
user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36"
chrome_options.add_argument(f'user-agent={user_agent}')
chrome_options.add_argument("--headless")
chrome_options.add_argument("--no-sandbox")
chrome_options.add_argument("--disable-dev-shm-usage")
chrome_options.add_argument("--window-size=1920,1080")
chrome_options.add_argument('--ignore-certificate-errors')
chrome_options.add_experimental_option("excludeSwitches", ["enable-automation"])
# 启动浏览器
current_step = "启动 Chrome 浏览器"
logger.info(f"Selenium ({session_id}): {current_step}...")
# 如果指定了 ChromeDriver 路径,则使用 Service;否则让 Selenium 自动管理
if chromedriver_path:
service = Service(executable_path=chromedriver_path)
driver = webdriver.Chrome(service=service, options=chrome_options)
logger.info(f"Selenium ({session_id}): 使用自定义 ChromeDriver: {chromedriver_path}")
else:
driver = webdriver.Chrome(options=chrome_options)
logger.info(f"Selenium ({session_id}): 使用 Selenium Manager 自动管理 ChromeDriver")
logger.info(f"Selenium ({session_id}): Chrome 浏览器启动成功")
current_step = "导航到登录页面"
logger.info(f"Selenium ({session_id}): {current_step}...")
driver.get("https://i.jielong.com/login?redirectTo=https%3A%2F%2Fi.jielong.com%2F")
wait = WebDriverWait(driver, 60)
# --- 步骤 1: 点击切换到 QQ 登录 ---
current_step = "查找并点击切换按钮"
toggle_button_selector = "div.login-wrap .toggle"
logger.info(f"Selenium ({session_id}): {current_step} ({toggle_button_selector})...")
toggle_button = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, toggle_button_selector)))
toggle_button.click()
# --- 步骤 2: 勾选同意服务协议 ---
current_step = "勾选同意服务协议"
checkbox_selector = "input.ant-checkbox-input[type='checkbox']"
logger.info(f"Selenium ({session_id}): {current_step} ({checkbox_selector})...")
checkbox = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, checkbox_selector)))
if not checkbox.is_selected():
checkbox.click()
logger.info(f"Selenium ({session_id}): 已勾选服务协议")
# --- 步骤 3: 点击"立即登录"按钮 ---
current_step = "点击立即登录按钮"
login_button_selector = "button.css-1wli0ry.ant-btn.ant-btn-default.login-btn"
logger.info(f"Selenium ({session_id}): {current_step} ({login_button_selector})...")
login_button = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, login_button_selector)))
login_button.click()
# --- 步骤 4: 等待二维码加载 ---
import time
time.sleep(3) # 等待几秒让二维码刷新出来
current_step = "等待QQ二维码图片加载"
qq_qr_image_selector = "#login_container img"
logger.info(f"Selenium ({session_id}): {current_step} ({qq_qr_image_selector})...")
qr_element = wait.until(EC.visibility_of_element_located((By.CSS_SELECTOR, qq_qr_image_selector)))
logger.info(f"Selenium ({session_id}): 成功找到QQ二维码元素,正在截图...")
qr_base64 = qr_element.screenshot_as_base64
update_session_file(session_id, {
'status': 'waiting_scan',
'qr_image_data': qr_base64,
'jwt_sub': jwt_sub,
'alias': alias, # 新增:保存 alias
'client_ip': client_ip # 新增:保存 IP
})
current_step = "等待用户扫描登录 (Cookie 'token' 出现)"
cookie_name_to_find = "token"
logger.info(f"Selenium ({session_id}): {current_step}...")
WebDriverWait(driver, 120, 1).until(lambda d: d.get_cookie(cookie_name_to_find) is not None) # 改为 120 秒(2分钟)
cookie = driver.get_cookie(cookie_name_to_find)
if cookie:
logger.info(f"Selenium ({session_id}): 成功在Cookie中捕获到Token")
update_session_file(session_id, {
'status': 'success',
'token': cookie['value'],
'alias': alias, # 保存 alias
'client_ip': client_ip # 保存 IP
})
else:
raise Exception("等待Cookie成功但获取失败")
except TimeoutException:
if get_session_status(session_id) == 'success':
logger.warning(f"Selenium ({session_id}): 一个并发线程超时,但会话已成功,将忽略此超时。")
else:
# 释放预占的用户名
if alias:
from backend.services.registration_manager import registration_manager
registration_manager.release_alias(alias, session_id)
logger.info(f"超时释放用户名预占: {alias}")
error_message = f"操作超时!卡在了步骤: '{current_step}'。请检查CSS选择器或网络。"
logger.error(f"Selenium ({session_id}): {error_message}")
# 保存调试信息(仅当 driver 已创建时)
if driver:
try:
driver.save_screenshot(DEBUG_SCREENSHOT_PATH)
with open(DEBUG_PAGE_SOURCE_PATH, 'w', encoding='utf-8') as f:
f.write(driver.page_source)
logger.error(f"Selenium ({session_id}): 调试截图和源码已保存。当前URL: {driver.current_url}")
except Exception as debug_error:
logger.error(f"Selenium ({session_id}): 保存调试信息失败: {debug_error}")
update_session_file(session_id, {
'status': 'error',
'message': error_message,
'jwt_sub': jwt_sub
})
except Exception as e:
if get_session_status(session_id) == 'success':
logger.warning(f"Selenium ({session_id}): 一个并发线程出错 ({e}),但会话已成功,将忽略此错误。")
else:
# 释放预占的用户名
if alias:
from backend.services.registration_manager import registration_manager
registration_manager.release_alias(alias, session_id)
logger.info(f"异常释放用户名预占: {alias}")
logger.error(f"Selenium ({session_id}): 发生未知错误: {e}", exc_info=True)
update_session_file(session_id, {
'status': 'error',
'message': str(e),
'jwt_sub': jwt_sub
})
finally:
if driver:
try:
driver.quit()
logger.info(f"Selenium ({session_id}): 浏览器已关闭")
except Exception as quit_error:
logger.error(f"Selenium ({session_id}): 关闭浏览器失败: {quit_error}")