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