|
|
#!/usr/bin/env python3
|
|
|
"""
|
|
|
Leaflow 多账号自动签到脚本
|
|
|
变量名:LEAFLOW_ACCOUNTS
|
|
|
变量值:邮箱1:密码1,邮箱2:密码2,邮箱3:密码3
|
|
|
"""
|
|
|
|
|
|
import os
|
|
|
import time
|
|
|
import logging
|
|
|
import traceback
|
|
|
from selenium.common.exceptions import TimeoutException
|
|
|
from selenium import webdriver
|
|
|
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.webdriver.chrome.options import Options
|
|
|
from selenium.webdriver.common.action_chains import ActionChains
|
|
|
import requests
|
|
|
from datetime import datetime
|
|
|
|
|
|
# 配置日志
|
|
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class LeaflowAutoCheckin:
|
|
|
def __init__(self, email, password):
|
|
|
self.email = email
|
|
|
self.password = password
|
|
|
|
|
|
if not self.email or not self.password:
|
|
|
raise ValueError("邮箱和密码不能为空")
|
|
|
|
|
|
self.driver = None
|
|
|
self.setup_driver()
|
|
|
|
|
|
def setup_driver(self):
|
|
|
"""设置Chrome驱动选项"""
|
|
|
chrome_options = Options()
|
|
|
|
|
|
# 基础稳定配置(适用于所有环境)
|
|
|
chrome_options.add_argument('--disable-blink-features=AutomationControlled')
|
|
|
chrome_options.add_experimental_option("excludeSwitches", ["enable-automation"])
|
|
|
chrome_options.add_experimental_option('useAutomationExtension', False)
|
|
|
|
|
|
# 增强稳定性的通用选项
|
|
|
chrome_options.add_argument('--disable-gpu') # 禁用GPU加速,减少渲染问题
|
|
|
chrome_options.add_argument('--disable-dev-shm-usage') # 禁用/dev/shm使用,避免内存问题
|
|
|
chrome_options.add_argument('--no-sandbox') # 禁用沙箱,提高兼容性
|
|
|
chrome_options.add_argument('--disable-extensions') # 禁用扩展,减少干扰
|
|
|
chrome_options.add_argument('--disable-plugins') # 禁用插件,减少资源占用
|
|
|
chrome_options.add_argument('--disable-images') # 禁用图片加载,提高页面加载速度
|
|
|
# chrome_options.add_argument('--disable-javascript') # 启用JavaScript,签到页面功能依赖它
|
|
|
chrome_options.add_argument('--window-size=1920,1080') # 设置窗口大小
|
|
|
chrome_options.add_argument('--ignore-certificate-errors') # 忽略证书错误
|
|
|
chrome_options.add_argument('--ignore-ssl-errors') # 忽略SSL错误
|
|
|
chrome_options.add_argument('--allow-insecure-localhost') # 允许不安全的localhost连接
|
|
|
chrome_options.add_argument('--log-level=3') # 减少Chrome日志输出
|
|
|
|
|
|
# 优化资源占用的选项
|
|
|
chrome_options.add_argument('--disable-background-timer-throttling') # 禁用后台定时器节流
|
|
|
chrome_options.add_argument('--disable-backgrounding-occluded-windows') # 禁用后台遮挡窗口
|
|
|
chrome_options.add_argument('--disable-renderer-backgrounding') # 禁用渲染器后台处理
|
|
|
chrome_options.add_argument('--disable-translate') # 禁用翻译
|
|
|
chrome_options.add_argument('--disable-notifications') # 禁用通知
|
|
|
chrome_options.add_argument('--disable-popup-blocking') # 禁用弹窗拦截
|
|
|
chrome_options.add_argument('--disable-default-apps') # 禁用默认应用
|
|
|
chrome_options.add_argument('--disable-sync') # 禁用同步
|
|
|
chrome_options.add_argument('--disable-logging') # 禁用日志
|
|
|
chrome_options.add_argument('--disable-software-rasterizer') # 禁用软件光栅化
|
|
|
chrome_options.add_argument('--disable-features=site-per-process') # 禁用站点隔离
|
|
|
chrome_options.add_argument('--js-flags=--max-old-space-size=256') # 限制JavaScript内存使用
|
|
|
|
|
|
# GitHub Actions环境配置
|
|
|
if os.getenv('GITHUB_ACTIONS'):
|
|
|
chrome_options.add_argument('--headless') # 无头模式
|
|
|
chrome_options.add_argument('--disable-features=VizDisplayCompositor') # 增强无头模式稳定性
|
|
|
chrome_options.add_argument('--headless=new') # 使用新的无头模式
|
|
|
logger.info("已启用GitHub Actions环境配置")
|
|
|
|
|
|
# 使用 webdriver-manager 自动获取匹配的 ChromeDriver
|
|
|
try:
|
|
|
from webdriver_manager.chrome import ChromeDriverManager
|
|
|
from selenium.webdriver.chrome.service import Service
|
|
|
|
|
|
# 尝试获取当前Chrome版本并使用匹配的ChromeDriver
|
|
|
try:
|
|
|
import subprocess
|
|
|
chrome_version = subprocess.check_output(
|
|
|
["google-chrome", "--version"]
|
|
|
).decode("utf-8").strip()
|
|
|
major_version = chrome_version.split(" ")[2].split(".")[0]
|
|
|
logger.info(f"检测到Chrome版本: {chrome_version}")
|
|
|
logger.info(f"使用ChromeDriver主版本: {major_version}")
|
|
|
|
|
|
service = Service(ChromeDriverManager(driver_version=major_version).install())
|
|
|
self.driver = webdriver.Chrome(service=service, options=chrome_options)
|
|
|
|
|
|
# 验证ChromeDriver版本
|
|
|
chromedriver_version = self.driver.capabilities['chrome']['chromedriverVersion'].split(' ')[0]
|
|
|
logger.info(f"已使用ChromeDriver版本: {chromedriver_version}")
|
|
|
|
|
|
except Exception as e:
|
|
|
logger.warning(f"获取Chrome版本失败,使用默认配置: {e}")
|
|
|
service = Service(ChromeDriverManager().install())
|
|
|
self.driver = webdriver.Chrome(service=service, options=chrome_options)
|
|
|
|
|
|
except Exception as e:
|
|
|
logger.warning(f"webdriver-manager 获取 ChromeDriver 失败,使用默认配置: {e}")
|
|
|
self.driver = webdriver.Chrome(options=chrome_options)
|
|
|
|
|
|
self.driver.execute_script("Object.defineProperty(navigator, 'webdriver', {get: () => undefined})")
|
|
|
|
|
|
# 设置默认超时时间
|
|
|
try:
|
|
|
self.driver.set_page_load_timeout(60) # 页面加载超时
|
|
|
self.driver.set_script_timeout(60) # 脚本执行超时
|
|
|
logger.info(f"已设置超时时间: 页面加载60秒,脚本执行60秒")
|
|
|
except Exception as e:
|
|
|
logger.warning(f"设置超时时间时出错: {e}")
|
|
|
|
|
|
# 移除隐式等待,仅使用显式等待
|
|
|
# self.driver.implicitly_wait(5)
|
|
|
logger.info("浏览器驱动初始化完成")
|
|
|
|
|
|
def close_popup(self):
|
|
|
"""关闭初始弹窗"""
|
|
|
try:
|
|
|
logger.info("尝试关闭初始弹窗...")
|
|
|
time.sleep(3) # 等待弹窗加载
|
|
|
|
|
|
# 尝试关闭弹窗
|
|
|
try:
|
|
|
actions = ActionChains(self.driver)
|
|
|
actions.move_by_offset(10, 10).click().perform()
|
|
|
logger.info("已成功关闭弹窗")
|
|
|
time.sleep(2)
|
|
|
return True
|
|
|
except:
|
|
|
pass
|
|
|
return False
|
|
|
|
|
|
except Exception as e:
|
|
|
logger.warning(f"关闭弹窗时出错: {e}")
|
|
|
return False
|
|
|
|
|
|
def wait_for_element_clickable(self, by, value, timeout=10):
|
|
|
"""等待元素可点击"""
|
|
|
return WebDriverWait(self.driver, timeout).until(
|
|
|
EC.element_to_be_clickable((by, value))
|
|
|
)
|
|
|
|
|
|
def wait_for_element_present(self, by, value, timeout=10):
|
|
|
"""等待元素出现"""
|
|
|
return WebDriverWait(self.driver, timeout).until(
|
|
|
EC.presence_of_element_located((by, value))
|
|
|
)
|
|
|
|
|
|
def login(self):
|
|
|
"""执行登录流程"""
|
|
|
logger.info(f"开始登录流程")
|
|
|
|
|
|
# 访问登录页面
|
|
|
self.driver.get("https://leaflow.net/login")
|
|
|
time.sleep(5)
|
|
|
|
|
|
# 关闭弹窗
|
|
|
self.close_popup()
|
|
|
|
|
|
# 输入邮箱
|
|
|
try:
|
|
|
logger.info("查找邮箱输入框...")
|
|
|
|
|
|
# 等待页面稳定
|
|
|
time.sleep(2)
|
|
|
|
|
|
# 尝试多种选择器找到邮箱输入框
|
|
|
email_selectors = [
|
|
|
"input[type='text']",
|
|
|
"input[type='email']",
|
|
|
"input[placeholder*='邮箱']",
|
|
|
"input[placeholder*='邮件']",
|
|
|
"input[placeholder*='email']",
|
|
|
"input[name='email']",
|
|
|
"input[name='username']"
|
|
|
]
|
|
|
|
|
|
email_input = None
|
|
|
for selector in email_selectors:
|
|
|
try:
|
|
|
email_input = self.wait_for_element_clickable(By.CSS_SELECTOR, selector, 5)
|
|
|
logger.info(f"找到邮箱输入框")
|
|
|
break
|
|
|
except:
|
|
|
continue
|
|
|
|
|
|
if not email_input:
|
|
|
raise Exception("找不到邮箱输入框")
|
|
|
|
|
|
# 清除并输入邮箱
|
|
|
email_input.clear()
|
|
|
email_input.send_keys(self.email)
|
|
|
logger.info("邮箱输入完成")
|
|
|
time.sleep(2)
|
|
|
|
|
|
except Exception as e:
|
|
|
logger.error(f"输入邮箱时出错: {e}")
|
|
|
# 尝试使用JavaScript直接设置值
|
|
|
try:
|
|
|
self.driver.execute_script(f"document.querySelector('input[type=\"text\"], input[type=\"email\"]').value = '{self.email}';")
|
|
|
logger.info("通过JavaScript设置邮箱")
|
|
|
time.sleep(2)
|
|
|
except:
|
|
|
raise Exception(f"无法输入邮箱: {e}")
|
|
|
|
|
|
# 等待密码输入框出现并输入密码
|
|
|
try:
|
|
|
logger.info("查找密码输入框...")
|
|
|
|
|
|
# 等待密码框出现
|
|
|
password_input = self.wait_for_element_clickable(
|
|
|
By.CSS_SELECTOR, "input[type='password']", 10
|
|
|
)
|
|
|
|
|
|
password_input.clear()
|
|
|
password_input.send_keys(self.password)
|
|
|
logger.info("密码输入完成")
|
|
|
time.sleep(1)
|
|
|
|
|
|
except TimeoutException:
|
|
|
raise Exception("找不到密码输入框")
|
|
|
|
|
|
# 点击登录按钮
|
|
|
try:
|
|
|
logger.info("查找登录按钮...")
|
|
|
login_btn_selectors = [
|
|
|
"//button[contains(text(), '登录')]",
|
|
|
"//button[contains(text(), 'Login')]",
|
|
|
"//button[@type='submit']",
|
|
|
"//input[@type='submit']",
|
|
|
"button[type='submit']"
|
|
|
]
|
|
|
|
|
|
login_btn = None
|
|
|
for selector in login_btn_selectors:
|
|
|
try:
|
|
|
if selector.startswith("//"):
|
|
|
login_btn = self.wait_for_element_clickable(By.XPATH, selector, 5)
|
|
|
else:
|
|
|
login_btn = self.wait_for_element_clickable(By.CSS_SELECTOR, selector, 5)
|
|
|
logger.info(f"找到登录按钮")
|
|
|
break
|
|
|
except:
|
|
|
continue
|
|
|
|
|
|
if not login_btn:
|
|
|
raise Exception("找不到登录按钮")
|
|
|
|
|
|
login_btn.click()
|
|
|
logger.info("已点击登录按钮")
|
|
|
|
|
|
except Exception as e:
|
|
|
raise Exception(f"点击登录按钮失败: {e}")
|
|
|
|
|
|
# 等待登录完成
|
|
|
try:
|
|
|
WebDriverWait(self.driver, 20).until(
|
|
|
lambda driver: "dashboard" in driver.current_url or "workspaces" in driver.current_url or "login" not in driver.current_url
|
|
|
)
|
|
|
|
|
|
# 检查当前URL确认登录成功
|
|
|
current_url = self.driver.current_url
|
|
|
if "dashboard" in current_url or "workspaces" in current_url or "login" not in current_url:
|
|
|
logger.info(f"登录成功,当前URL: {current_url}")
|
|
|
|
|
|
# 获取并保存登录后的COOKIE
|
|
|
logger.info("获取登录后的COOKIE...")
|
|
|
self.login_cookies = self.driver.get_cookies()
|
|
|
logger.info(f"获取到 {len(self.login_cookies)} 个COOKIE")
|
|
|
for cookie in self.login_cookies:
|
|
|
logger.debug(f"COOKIE: {cookie['name']} -> {cookie['domain']}")
|
|
|
|
|
|
return True
|
|
|
else:
|
|
|
raise Exception("登录后未跳转到正确页面")
|
|
|
|
|
|
except TimeoutException:
|
|
|
# 检查是否登录失败
|
|
|
try:
|
|
|
error_selectors = [".error", ".alert-danger", "[class*='error']", "[class*='danger']"]
|
|
|
for selector in error_selectors:
|
|
|
try:
|
|
|
error_msg = self.driver.find_element(By.CSS_SELECTOR, selector)
|
|
|
if error_msg.is_displayed():
|
|
|
raise Exception(f"登录失败: {error_msg.text}")
|
|
|
except:
|
|
|
continue
|
|
|
raise Exception("登录超时,无法确认登录状态")
|
|
|
except Exception as e:
|
|
|
raise e
|
|
|
|
|
|
def get_balance(self):
|
|
|
"""获取当前账号的总余额"""
|
|
|
try:
|
|
|
logger.info("获取账号余额...")
|
|
|
|
|
|
# 跳转到仪表板页面
|
|
|
self.driver.get("https://leaflow.net/dashboard")
|
|
|
time.sleep(3)
|
|
|
|
|
|
# 等待页面加载
|
|
|
WebDriverWait(self.driver, 10).until(
|
|
|
EC.presence_of_element_located((By.TAG_NAME, "body"))
|
|
|
)
|
|
|
|
|
|
# 尝试多种选择器查找余额元素
|
|
|
balance_selectors = [
|
|
|
"//*[contains(text(), '¥') or contains(text(), '¥') or contains(text(), '元')]",
|
|
|
"//*[contains(@class, 'balance')]",
|
|
|
"//*[contains(@class, 'money')]",
|
|
|
"//*[contains(@class, 'amount')]",
|
|
|
"//button[contains(@class, 'dollar')]",
|
|
|
"//span[contains(@class, 'font-medium')]"
|
|
|
]
|
|
|
|
|
|
for selector in balance_selectors:
|
|
|
try:
|
|
|
elements = self.driver.find_elements(By.XPATH, selector)
|
|
|
for element in elements:
|
|
|
text = element.text.strip()
|
|
|
# 查找包含数字和货币符号的文本
|
|
|
if any(char.isdigit() for char in text) and ('¥' in text or '¥' in text or '元' in text):
|
|
|
# 提取数字部分
|
|
|
import re
|
|
|
numbers = re.findall(r'\d+\.?\d*', text)
|
|
|
if numbers:
|
|
|
balance = numbers[0]
|
|
|
logger.info(f"找到余额: {balance}元")
|
|
|
return f"{balance}元"
|
|
|
except:
|
|
|
continue
|
|
|
|
|
|
logger.warning("未找到余额信息")
|
|
|
return "未知"
|
|
|
|
|
|
except Exception as e:
|
|
|
logger.warning(f"获取余额时出错: {e}")
|
|
|
return "未知"
|
|
|
|
|
|
def wait_for_checkin_page_loaded(self, max_retries=5, wait_time=30):
|
|
|
"""等待签到页面完全加载,支持重试"""
|
|
|
|
|
|
for attempt in range(max_retries):
|
|
|
logger.info(f"等待签到页面加载,尝试 {attempt + 1}/{max_retries}")
|
|
|
|
|
|
# 收集页面基本信息,便于调试
|
|
|
logger.info(f" 当前页面URL: {self.driver.current_url}")
|
|
|
logger.info(f" 当前页面标题: {self.driver.title}")
|
|
|
|
|
|
try:
|
|
|
# 检查页面是否包含签到相关元素
|
|
|
# 使用组合等待条件:DOM就绪 + 核心元素可见
|
|
|
WebDriverWait(self.driver, wait_time).until(
|
|
|
lambda d: d.execute_script("return document.readyState") == "complete"
|
|
|
)
|
|
|
|
|
|
checkin_indicators = [
|
|
|
(By.CSS_SELECTOR, "button.checkin-btn"), # 首选精确选择器
|
|
|
(By.XPATH, "//button[contains(text(), '立即签到')]"),
|
|
|
(By.XPATH, "//button[contains(text(), '已签到')]"),
|
|
|
(By.XPATH, "//button[contains(text(), '已完成')]"),
|
|
|
(By.XPATH, "//*[contains(text(), '每日签到')]"),
|
|
|
(By.XPATH, "//*[contains(text(), '签到')]")
|
|
|
]
|
|
|
|
|
|
for locator_type, selector in checkin_indicators:
|
|
|
try:
|
|
|
# 增加元素等待时间
|
|
|
element = WebDriverWait(self.driver, 15).until(
|
|
|
EC.visibility_of_element_located((locator_type, selector))
|
|
|
)
|
|
|
|
|
|
# 只要找到可见的签到相关元素,不管是否可用,都认为页面已加载成功
|
|
|
# 已签到状态下的按钮可能是禁用的,所以不能用is_enabled()判断
|
|
|
logger.info(f"检测到签到元素: {selector}")
|
|
|
logger.info(f" 元素可见性: {element.is_displayed()}")
|
|
|
logger.info(f" 元素可用性: {'启用' if element.is_enabled() else '禁用'}")
|
|
|
logger.info(f" 元素文本: '{element.text.strip()}'")
|
|
|
return True
|
|
|
except TimeoutException:
|
|
|
logger.debug(f"元素定位失败: {selector},尝试下个策略")
|
|
|
continue
|
|
|
|
|
|
logger.warning(f"第 {attempt + 1} 次尝试未找到签到相关元素")
|
|
|
|
|
|
# 尝试获取页面源代码的前2000个字符,便于调试
|
|
|
try:
|
|
|
page_source = self.driver.page_source[:2000]
|
|
|
logger.debug(f"页面源码片段: {page_source}...")
|
|
|
except Exception as e:
|
|
|
logger.error(f"获取页面源码失败: {e}")
|
|
|
|
|
|
except TimeoutException:
|
|
|
logger.error(f"页面加载超时,重试中... (尝试 {attempt+1})")
|
|
|
except Exception as e:
|
|
|
logger.critical(f"严重错误: {str(e)}")
|
|
|
logger.error(f"错误详情: {traceback.format_exc()}")
|
|
|
if "net::ERR" in str(e):
|
|
|
logger.info("检测到网络错误,立即重试")
|
|
|
continue
|
|
|
|
|
|
return False
|
|
|
|
|
|
def find_and_click_checkin_button(self):
|
|
|
"""查找并点击签到按钮 - 处理已签到状态"""
|
|
|
logger.info("开始查找签到按钮...")
|
|
|
start_time = time.time()
|
|
|
|
|
|
try:
|
|
|
# 收集页面基本信息
|
|
|
logger.info(f"当前页面URL: {self.driver.current_url}")
|
|
|
logger.info(f"当前页面标题: {self.driver.title}")
|
|
|
|
|
|
# 先等待页面可能的重载
|
|
|
logger.info("等待页面稳定...")
|
|
|
time.sleep(5)
|
|
|
|
|
|
# 使用和单账号成功时相同的选择器
|
|
|
checkin_selectors = [
|
|
|
"button.checkin-btn",
|
|
|
"//button[contains(text(), '立即签到')]",
|
|
|
"//button[contains(@class, 'checkin')]",
|
|
|
"button[type='submit']",
|
|
|
"button[name='checkin']"
|
|
|
]
|
|
|
|
|
|
for selector in checkin_selectors:
|
|
|
logger.info(f"尝试使用选择器: {selector}")
|
|
|
try:
|
|
|
if selector.startswith("//"):
|
|
|
checkin_btn = WebDriverWait(self.driver, 15).until(
|
|
|
EC.presence_of_element_located((By.XPATH, selector))
|
|
|
)
|
|
|
else:
|
|
|
checkin_btn = WebDriverWait(self.driver, 15).until(
|
|
|
EC.presence_of_element_located((By.CSS_SELECTOR, selector))
|
|
|
)
|
|
|
|
|
|
# 详细检查按钮状态
|
|
|
logger.info(f"找到按钮,开始检查状态...")
|
|
|
logger.info(f"按钮可见性: {checkin_btn.is_displayed()}")
|
|
|
logger.info(f"按钮可用性: {checkin_btn.is_enabled()}")
|
|
|
logger.info(f"按钮文本: '{checkin_btn.text.strip()}'")
|
|
|
|
|
|
if checkin_btn.is_displayed():
|
|
|
# 检查按钮文本,如果包含"已签到"或"已完成"则说明今天已经签到过了
|
|
|
btn_text = checkin_btn.text.strip()
|
|
|
|
|
|
# 检查页面上是否有"今日已签到"文本
|
|
|
page_text = self.driver.page_source
|
|
|
|
|
|
# 综合判断已签到状态:按钮禁用或按钮文本包含"已完成"或页面包含"今日已签到"
|
|
|
if (not checkin_btn.is_enabled() or
|
|
|
"已完成" in btn_text or
|
|
|
"今日已签到" in page_text or
|
|
|
"已签到" in btn_text):
|
|
|
logger.info(f"今日已签到,状态信息:")
|
|
|
logger.info(f" - 按钮状态: {'禁用' if not checkin_btn.is_enabled() else '可用'}")
|
|
|
logger.info(f" - 按钮文本: '{btn_text}'")
|
|
|
logger.info(f" - 页面包含'今日已签到': {'是' if '今日已签到' in page_text else '否'}")
|
|
|
return "already_checked_in"
|
|
|
|
|
|
# 尝试多种点击方式
|
|
|
clicked = False
|
|
|
|
|
|
# 方式1: JavaScript点击(优先使用,避免页面阻塞)
|
|
|
try:
|
|
|
logger.info("方式1: 尝试JavaScript点击...")
|
|
|
self.driver.execute_script("arguments[0].click();", checkin_btn)
|
|
|
clicked = True
|
|
|
logger.info("方式1: JavaScript点击成功")
|
|
|
except Exception as e:
|
|
|
logger.warning(f"方式1: JavaScript点击失败: {e}")
|
|
|
clicked = False
|
|
|
|
|
|
# 方式2: ActionChains点击
|
|
|
if not clicked:
|
|
|
try:
|
|
|
logger.info("方式2: 尝试ActionChains点击...")
|
|
|
# 设置隐式等待时间,避免点击超时
|
|
|
self.driver.implicitly_wait(5)
|
|
|
actions = ActionChains(self.driver)
|
|
|
actions.move_to_element(checkin_btn).click().perform()
|
|
|
clicked = True
|
|
|
logger.info("方式2: ActionChains点击成功")
|
|
|
except Exception as e:
|
|
|
logger.warning(f"方式2: ActionChains点击失败: {e}")
|
|
|
clicked = False
|
|
|
finally:
|
|
|
# 恢复隐式等待时间
|
|
|
self.driver.implicitly_wait(0)
|
|
|
|
|
|
# 方式3: 直接点击(最后尝试,可能会阻塞)
|
|
|
if not clicked:
|
|
|
try:
|
|
|
logger.info("方式3: 尝试直接点击按钮...")
|
|
|
# 使用WebDriverWait设置点击超时
|
|
|
WebDriverWait(self.driver, 10).until(
|
|
|
EC.element_to_be_clickable((By.CSS_SELECTOR, "button.checkin-btn"))
|
|
|
).click()
|
|
|
clicked = True
|
|
|
logger.info("方式3: 直接点击成功")
|
|
|
except Exception as e:
|
|
|
logger.warning(f"方式3: 直接点击失败: {e}")
|
|
|
clicked = False
|
|
|
|
|
|
if clicked:
|
|
|
logger.info(f"成功点击签到按钮,耗时: {time.time() - start_time:.2f}秒")
|
|
|
# 点击后立即检查页面变化,确认签到是否成功
|
|
|
time.sleep(2)
|
|
|
# 检查按钮状态或页面文本变化
|
|
|
try:
|
|
|
updated_btn = self.driver.find_element(By.CSS_SELECTOR, "button.checkin-btn")
|
|
|
updated_text = updated_btn.text.strip()
|
|
|
page_text = self.driver.page_source
|
|
|
if (not updated_btn.is_enabled() or
|
|
|
"已完成" in updated_text or
|
|
|
"今日已签到" in page_text or
|
|
|
"已签到" in updated_text):
|
|
|
logger.info("签到成功,按钮状态已更新")
|
|
|
except:
|
|
|
pass
|
|
|
return True
|
|
|
else:
|
|
|
logger.error("所有点击方式均失败")
|
|
|
return False
|
|
|
else:
|
|
|
logger.warning("按钮不可见")
|
|
|
continue
|
|
|
|
|
|
except Exception as e:
|
|
|
logger.debug(f"选择器{selector}未找到按钮: {e}")
|
|
|
continue
|
|
|
|
|
|
logger.error("遍历所有选择器后仍未找到可点击的签到按钮")
|
|
|
return False
|
|
|
|
|
|
except Exception as e:
|
|
|
logger.error(f"查找签到按钮时出错: {e}")
|
|
|
logger.error(f"错误详情: {traceback.format_exc()}")
|
|
|
return False
|
|
|
|
|
|
def checkin(self):
|
|
|
"""执行签到流程"""
|
|
|
logger.info("执行签到流程...")
|
|
|
|
|
|
# 跳转到签到页面
|
|
|
logger.info("跳转到签到页面...")
|
|
|
|
|
|
# 只使用明确的签到页面URL,避免跳转到登录页面
|
|
|
target_url = "https://checkin.leaflow.net/index.php"
|
|
|
|
|
|
# 网络状态检查
|
|
|
try:
|
|
|
import socket
|
|
|
socket.create_connection(("checkin.leaflow.net", 443), timeout=10)
|
|
|
logger.info("网络连接正常,可以访问签到服务器")
|
|
|
except Exception as net_e:
|
|
|
logger.warning(f"网络连接检查失败: {net_e},可能网络不稳定")
|
|
|
|
|
|
# 尝试访问签到页面,处理网络超时
|
|
|
max_retries = 8
|
|
|
retry_delay = 3
|
|
|
start_time = time.time()
|
|
|
|
|
|
for attempt in range(1, max_retries + 1):
|
|
|
try:
|
|
|
logger.info(f"尝试第 {attempt}/{max_retries} 次访问签到页面...")
|
|
|
logger.info(f"当前耗时: {time.time() - start_time:.2f} 秒")
|
|
|
|
|
|
# 重置超时设置,使用更长的超时时间
|
|
|
try:
|
|
|
self.driver.set_page_load_timeout(120)
|
|
|
self.driver.set_script_timeout(60)
|
|
|
logger.debug("已重置超时设置: 页面加载120秒,脚本执行60秒")
|
|
|
except Exception as timeout_e:
|
|
|
logger.warning(f"重置超时设置时出错: {timeout_e}")
|
|
|
|
|
|
# 记录开始访问时间
|
|
|
access_start = time.time()
|
|
|
logger.info(f"尝试访问URL: {target_url}")
|
|
|
self.driver.get(target_url)
|
|
|
access_time = time.time() - access_start
|
|
|
logger.info(f"页面访问耗时: {access_time:.2f} 秒")
|
|
|
|
|
|
# 检查当前URL和页面状态
|
|
|
current_url = self.driver.current_url
|
|
|
logger.info(f"当前URL: {current_url}")
|
|
|
|
|
|
# 获取页面标题
|
|
|
try:
|
|
|
page_title = self.driver.title
|
|
|
logger.info(f"当前页面标题: {page_title}")
|
|
|
except Exception as title_e:
|
|
|
logger.warning(f"获取页面标题失败: {title_e}")
|
|
|
|
|
|
# 检查是否跳转到了登录页面
|
|
|
if "login" in current_url and "checkin" not in current_url:
|
|
|
logger.warning(f"访问签到页面时跳转到了登录页面: {current_url}")
|
|
|
logger.info("跳过登录页面,继续执行COOKIE处理...")
|
|
|
else:
|
|
|
logger.info(f"成功访问签到页面,URL: {current_url}")
|
|
|
|
|
|
# 检查页面加载状态
|
|
|
try:
|
|
|
page_state = self.driver.execute_script("return document.readyState")
|
|
|
logger.info(f"页面加载状态: {page_state}")
|
|
|
if page_state != "complete":
|
|
|
logger.warning("页面可能未完全加载,准备继续处理")
|
|
|
except Exception as state_e:
|
|
|
logger.warning(f"获取页面状态失败: {state_e}")
|
|
|
|
|
|
break
|
|
|
|
|
|
except Exception as e:
|
|
|
error_msg = str(e)
|
|
|
logger.warning(f"访问URL {target_url}失败: {error_msg}")
|
|
|
logger.debug(f"错误详情: {traceback.format_exc()}")
|
|
|
|
|
|
# 增强错误分类处理
|
|
|
error_lower = error_msg.lower()
|
|
|
if any(keyword in error_lower for keyword in [
|
|
|
"-0.005", "-0.004", "timed out receiving message from renderer",
|
|
|
"timeout: timed out", "session not created", "chrome not reachable",
|
|
|
"no such session", "session deleted", "connection refused"
|
|
|
]):
|
|
|
logger.error("检测到ChromeDriver兼容性错误,重置浏览器会话...")
|
|
|
try:
|
|
|
self.driver.quit()
|
|
|
logger.info("已关闭旧的浏览器会话")
|
|
|
except Exception as quit_e:
|
|
|
logger.warning(f"关闭浏览器会话失败: {quit_e}")
|
|
|
|
|
|
# 重置浏览器会话
|
|
|
try:
|
|
|
self.setup_driver()
|
|
|
logger.info("浏览器会话已重置,准备重试")
|
|
|
except Exception as setup_e:
|
|
|
logger.error(f"重置浏览器会话失败: {setup_e}")
|
|
|
raise Exception(f"无法重置浏览器会话: {setup_e}")
|
|
|
|
|
|
elif "net::err" in error_lower:
|
|
|
logger.warning(f"网络错误 ({error_msg}),可能需要检查网络连接")
|
|
|
# 网络错误时增加等待时间
|
|
|
retry_delay = 5
|
|
|
elif "connection timed out" in error_lower or "timed out" in error_lower:
|
|
|
logger.warning(f"连接超时 ({error_msg}),可能网络延迟较高")
|
|
|
elif "dns_probe_finished_nxdomain" in error_lower:
|
|
|
logger.warning("DNS解析失败,可能是域名问题")
|
|
|
else:
|
|
|
logger.warning(f"其他错误: {error_msg}")
|
|
|
|
|
|
# 等待后重试,使用指数退避
|
|
|
if attempt < max_retries:
|
|
|
wait_time = retry_delay * (2 ** (attempt - 1)) # 使用2倍指数退避
|
|
|
wait_time = min(wait_time, 60) # 最大等待60秒
|
|
|
logger.info(f"等待 {wait_time:.1f} 秒后重试...")
|
|
|
time.sleep(wait_time)
|
|
|
else:
|
|
|
total_time = time.time() - start_time
|
|
|
logger.error(f"经过 {max_retries} 次重试后仍无法访问签到页面,总耗时: {total_time:.2f} 秒")
|
|
|
raise Exception(f"无法访问签到页面: {error_msg}")
|
|
|
|
|
|
# 添加登录时保存的COOKIE到当前域名
|
|
|
logger.info("添加登录COOKIE到checkin域名...")
|
|
|
if hasattr(self, 'login_cookies') and self.login_cookies:
|
|
|
# 先清除当前页面的COOKIE
|
|
|
self.driver.delete_all_cookies()
|
|
|
|
|
|
# 添加登录时保存的所有COOKIE
|
|
|
for cookie in self.login_cookies:
|
|
|
try:
|
|
|
# 适配不同域名的COOKIE
|
|
|
cookie_copy = cookie.copy()
|
|
|
# 确保COOKIE能被所有子域名使用
|
|
|
if 'domain' not in cookie_copy or not cookie_copy['domain']:
|
|
|
cookie_copy['domain'] = '.leaflow.net'
|
|
|
# 移除可能导致问题的属性
|
|
|
if 'expiry' in cookie_copy and isinstance(cookie_copy['expiry'], float):
|
|
|
cookie_copy['expiry'] = int(cookie_copy['expiry'])
|
|
|
# 添加COOKIE
|
|
|
self.driver.add_cookie(cookie_copy)
|
|
|
logger.debug(f"添加COOKIE成功: {cookie['name']} -> {cookie_copy.get('domain', '无域名')}")
|
|
|
except Exception as e:
|
|
|
logger.debug(f"添加COOKIE失败: {cookie['name']} -> {e}")
|
|
|
|
|
|
# 尝试直接访问签到首页,使用明确的URL
|
|
|
logger.info("COOKIE添加完成,直接访问签到首页...")
|
|
|
try:
|
|
|
# 使用明确的URL,避免重定向,增加超时时间
|
|
|
self.driver.set_page_load_timeout(60)
|
|
|
self.driver.get(target_url)
|
|
|
logger.info(f"成功访问签到首页,URL: {self.driver.current_url}")
|
|
|
except Exception as e:
|
|
|
logger.error(f"访问签到首页时出错: {e}")
|
|
|
# 无论是否超时,都获取当前页面信息
|
|
|
try:
|
|
|
logger.info(f"当前页面URL: {self.driver.current_url}")
|
|
|
logger.info(f"当前页面标题: {self.driver.title}")
|
|
|
# 获取页面源码(最多前2000字符)
|
|
|
page_source = self.driver.page_source[:2000]
|
|
|
logger.info(f"页面源码片段: {page_source}")
|
|
|
except Exception as info_e:
|
|
|
logger.error(f"获取页面信息失败: {info_e}")
|
|
|
|
|
|
# 获取当前页面信息,便于调试
|
|
|
logger.info(f"当前签到页面URL: {self.driver.current_url}")
|
|
|
logger.info(f"当前页面标题: {self.driver.title}")
|
|
|
|
|
|
# 简化重定向处理,直接检查当前URL
|
|
|
logger.info("检查当前页面状态...")
|
|
|
|
|
|
# 检查是否需要进行OAuth授权
|
|
|
if "oauth/authorize" in self.driver.current_url:
|
|
|
logger.info("检测到OAuth授权页面,尝试自动授权...")
|
|
|
# 查找并点击授权按钮
|
|
|
try:
|
|
|
# 尝试多种选择器找到授权按钮
|
|
|
authorize_selectors = [
|
|
|
"button[type='submit']",
|
|
|
"input[type='submit']",
|
|
|
"//button[contains(text(), '授权')]",
|
|
|
"//button[contains(text(), 'Authorize')]"
|
|
|
]
|
|
|
|
|
|
authorize_btn = None
|
|
|
for selector in authorize_selectors:
|
|
|
try:
|
|
|
if selector.startswith("//"):
|
|
|
authorize_btn = WebDriverWait(self.driver, 10).until(
|
|
|
EC.element_to_be_clickable((By.XPATH, selector))
|
|
|
)
|
|
|
else:
|
|
|
authorize_btn = WebDriverWait(self.driver, 10).until(
|
|
|
EC.element_to_be_clickable((By.CSS_SELECTOR, selector))
|
|
|
)
|
|
|
logger.info(f"找到授权按钮")
|
|
|
break
|
|
|
except:
|
|
|
continue
|
|
|
|
|
|
if authorize_btn:
|
|
|
authorize_btn.click()
|
|
|
logger.info("已点击授权按钮")
|
|
|
time.sleep(5)
|
|
|
logger.info(f"授权后URL: {self.driver.current_url}")
|
|
|
else:
|
|
|
logger.warning("未找到授权按钮,尝试等待自动跳转...")
|
|
|
time.sleep(10)
|
|
|
logger.info(f"等待后URL: {self.driver.current_url}")
|
|
|
except Exception as e:
|
|
|
logger.warning(f"自动授权失败,可能需要手动授权: {e}")
|
|
|
|
|
|
# 成功访问并处理完重定向,继续执行后续流程
|
|
|
# 注意:这里不再需要continue或break,因为我们已经在前面的代码中处理了循环退出逻辑
|
|
|
|
|
|
finally:
|
|
|
# 恢复默认页面加载超时
|
|
|
self.driver.set_page_load_timeout(60)
|
|
|
|
|
|
# 等待签到页面加载(最多重试5次,每次等待20秒)
|
|
|
retry_count = 0
|
|
|
max_retries = 5
|
|
|
success = False
|
|
|
|
|
|
while retry_count < max_retries and not success:
|
|
|
retry_count += 1
|
|
|
logger.info(f"等待签到页面加载,尝试 {retry_count}/{max_retries}")
|
|
|
|
|
|
# 检查当前URL和标题,记录详细信息
|
|
|
current_url = self.driver.current_url
|
|
|
current_title = self.driver.title
|
|
|
logger.info(f" 当前URL: {current_url}")
|
|
|
logger.info(f" 当前标题: {current_title}")
|
|
|
|
|
|
# 检查是否是502错误
|
|
|
if "502" in current_title or "Bad Gateway" in current_title:
|
|
|
logger.error(f"第 {retry_count} 次尝试遇到502 Bad Gateway错误")
|
|
|
|
|
|
# 尝试重新访问主站获取有效COOKIE(仅在需要时)
|
|
|
logger.info("尝试重新访问主站获取有效COOKIE...")
|
|
|
self.driver.get("https://leaflow.net/dashboard")
|
|
|
time.sleep(3)
|
|
|
|
|
|
# 重新跳转到签到页面
|
|
|
self.driver.get("https://checkin.leaflow.net")
|
|
|
time.sleep(5)
|
|
|
continue
|
|
|
|
|
|
# 检查是否是重定向到登录页面
|
|
|
if "login" in current_url and "checkin" not in current_url:
|
|
|
logger.error(f"第 {retry_count} 次尝试遇到登录页面,COOKIE可能失效")
|
|
|
|
|
|
# 重新执行登录流程
|
|
|
logger.info("尝试重新登录...")
|
|
|
if self.login():
|
|
|
# 重新跳转到签到页面
|
|
|
self.driver.get("https://checkin.leaflow.net")
|
|
|
time.sleep(5)
|
|
|
else:
|
|
|
raise Exception("重新登录失败")
|
|
|
continue
|
|
|
|
|
|
# 检查是否是OAuth回调页面
|
|
|
if "auth_callback.php" in current_url:
|
|
|
logger.info(f"第 {retry_count} 次尝试遇到OAuth回调页面,等待自动跳转...")
|
|
|
time.sleep(5)
|
|
|
logger.info(f" 自动跳转后URL: {self.driver.current_url}")
|
|
|
logger.info(f" 自动跳转后标题: {self.driver.title}")
|
|
|
|
|
|
# 尝试等待页面加载
|
|
|
if self.wait_for_checkin_page_loaded(max_retries=1, wait_time=15):
|
|
|
success = True
|
|
|
logger.info(f"第 {retry_count} 次尝试成功加载签到页面")
|
|
|
else:
|
|
|
logger.warning(f"第 {retry_count} 次尝试未成功加载签到页面")
|
|
|
|
|
|
# 尝试刷新页面
|
|
|
logger.info("尝试刷新页面...")
|
|
|
self.driver.refresh()
|
|
|
time.sleep(5)
|
|
|
|
|
|
if not success:
|
|
|
raise Exception(f"签到页面加载失败,经过 {max_retries} 次重试后仍无法访问")
|
|
|
|
|
|
# 查找并点击立即签到按钮
|
|
|
checkin_result = self.find_and_click_checkin_button()
|
|
|
|
|
|
if checkin_result == "already_checked_in":
|
|
|
return "今日已签到"
|
|
|
elif checkin_result is True:
|
|
|
logger.info("已点击立即签到按钮")
|
|
|
time.sleep(5) # 等待签到结果
|
|
|
|
|
|
# 获取签到结果
|
|
|
result_message = self.get_checkin_result()
|
|
|
return result_message
|
|
|
else:
|
|
|
raise Exception("找不到立即签到按钮或按钮不可点击")
|
|
|
|
|
|
def get_checkin_result(self):
|
|
|
"""获取签到结果消息"""
|
|
|
try:
|
|
|
# 给页面一些时间显示结果
|
|
|
time.sleep(3)
|
|
|
|
|
|
# 尝试查找各种可能的成功消息元素
|
|
|
success_selectors = [
|
|
|
".alert-success",
|
|
|
".success",
|
|
|
".message",
|
|
|
"[class*='success']",
|
|
|
"[class*='message']",
|
|
|
".modal-content", # 弹窗内容
|
|
|
".ant-message", # Ant Design 消息
|
|
|
".el-message", # Element UI 消息
|
|
|
".toast", # Toast消息
|
|
|
".notification" # 通知
|
|
|
]
|
|
|
|
|
|
for selector in success_selectors:
|
|
|
try:
|
|
|
element = self.driver.find_element(By.CSS_SELECTOR, selector)
|
|
|
if element.is_displayed():
|
|
|
text = element.text.strip()
|
|
|
if text:
|
|
|
return text
|
|
|
except:
|
|
|
continue
|
|
|
|
|
|
# 如果没有找到特定元素,检查页面文本
|
|
|
page_text = self.driver.find_element(By.TAG_NAME, "body").text
|
|
|
important_keywords = ["成功", "签到", "获得", "恭喜", "谢谢", "感谢", "完成", "已签到", "连续签到"]
|
|
|
|
|
|
for keyword in important_keywords:
|
|
|
if keyword in page_text:
|
|
|
# 提取包含关键词的行
|
|
|
lines = page_text.split('\n')
|
|
|
for line in lines:
|
|
|
if keyword in line and len(line.strip()) < 100: # 避免提取过长的文本
|
|
|
return line.strip()
|
|
|
|
|
|
# 检查签到按钮状态变化
|
|
|
try:
|
|
|
checkin_btn = self.driver.find_element(By.CSS_SELECTOR, "button.checkin-btn")
|
|
|
if not checkin_btn.is_enabled() or "已签到" in checkin_btn.text or "disabled" in checkin_btn.get_attribute("class"):
|
|
|
return "今日已签到完成"
|
|
|
except:
|
|
|
pass
|
|
|
|
|
|
return "签到完成,但未找到具体结果消息"
|
|
|
|
|
|
except Exception as e:
|
|
|
return f"获取签到结果时出错: {str(e)}"
|
|
|
|
|
|
def run(self):
|
|
|
"""单个账号执行流程"""
|
|
|
try:
|
|
|
logger.info(f"开始处理账号")
|
|
|
|
|
|
# 登录
|
|
|
if self.login():
|
|
|
# 签到
|
|
|
result = self.checkin()
|
|
|
|
|
|
# 获取余额
|
|
|
balance = self.get_balance()
|
|
|
|
|
|
logger.info(f"签到结果: {result}, 余额: {balance}")
|
|
|
return True, result, balance
|
|
|
else:
|
|
|
raise Exception("登录失败")
|
|
|
|
|
|
except Exception as e:
|
|
|
error_msg = f"自动签到失败: {str(e)}"
|
|
|
logger.error(error_msg)
|
|
|
return False, error_msg, "未知"
|
|
|
|
|
|
finally:
|
|
|
if self.driver:
|
|
|
self.driver.quit()
|
|
|
|
|
|
class MultiAccountManager:
|
|
|
"""多账号管理器 - 简化配置版本"""
|
|
|
|
|
|
def __init__(self):
|
|
|
self.accounts = self.load_accounts()
|
|
|
|
|
|
def load_accounts(self):
|
|
|
"""从环境变量加载多账号信息,支持冒号分隔多账号和单账号"""
|
|
|
accounts = []
|
|
|
|
|
|
logger.info("开始加载账号配置...")
|
|
|
|
|
|
# 方法1: 冒号分隔多账号格式
|
|
|
accounts_str = os.getenv('LEAFLOW_ACCOUNTS', '').strip()
|
|
|
if accounts_str:
|
|
|
try:
|
|
|
logger.info("尝试解析冒号分隔多账号配置")
|
|
|
account_pairs = [pair.strip() for pair in accounts_str.split(',')]
|
|
|
|
|
|
logger.info(f"找到 {len(account_pairs)} 个账号")
|
|
|
|
|
|
for i, pair in enumerate(account_pairs):
|
|
|
if ':' in pair:
|
|
|
email, password, token = pair.split(':', 1)
|
|
|
email = email.strip()
|
|
|
password = password.strip()
|
|
|
token = token.strip()
|
|
|
|
|
|
if email and password:
|
|
|
accounts.append({
|
|
|
'email': email,
|
|
|
'password': password,
|
|
|
'token': token
|
|
|
})
|
|
|
logger.info(f"成功添加第 {i+1} 个账号")
|
|
|
else:
|
|
|
logger.warning(f"账号对格式错误")
|
|
|
else:
|
|
|
logger.warning(f"账号对缺少冒号分隔符")
|
|
|
|
|
|
if accounts:
|
|
|
logger.info(f"从冒号分隔格式成功加载了 {len(accounts)} 个账号")
|
|
|
return accounts
|
|
|
else:
|
|
|
logger.warning("冒号分隔配置中没有找到有效的账号信息")
|
|
|
except Exception as e:
|
|
|
logger.error(f"解析冒号分隔账号配置失败: {e}")
|
|
|
|
|
|
# 方法2: 单账号格式
|
|
|
single_email = os.getenv('LEAFLOW_EMAIL', '').strip()
|
|
|
single_password = os.getenv('LEAFLOW_PASSWORD', '').strip()
|
|
|
single_token = os.getenv('LEAFLOW_TOKEN', '').strip()
|
|
|
if single_email and single_password:
|
|
|
accounts.append({
|
|
|
'email': single_email,
|
|
|
'password': single_password,
|
|
|
'token': single_token
|
|
|
})
|
|
|
logger.info("加载了单个账号配置")
|
|
|
return accounts
|
|
|
|
|
|
# 如果所有方法都失败
|
|
|
logger.error("未找到有效的账号配置")
|
|
|
logger.error("请检查以下环境变量设置:")
|
|
|
logger.error("1. LEAFLOW_ACCOUNTS: 冒号分隔多账号 (email1:pass1,email2:pass2)")
|
|
|
logger.error("2. LEAFLOW_EMAIL 和 LEAFLOW_PASSWORD: 单账号")
|
|
|
|
|
|
raise ValueError("未找到有效的账号配置")
|
|
|
|
|
|
def send_api_notification(self, message):
|
|
|
"""发送API通知"""
|
|
|
try:
|
|
|
url = "http://111.11.107.61:30005/send_private_msg"
|
|
|
# 构建请求数据
|
|
|
data = {
|
|
|
"user_id": "8739050",
|
|
|
"message": [{"type": "text", "data": {"text": message}}]
|
|
|
}
|
|
|
|
|
|
# 从环境变量读取token
|
|
|
token = os.getenv('LEAFLOW_TOKEN', '').strip()
|
|
|
headers = {
|
|
|
"Authorization": f"{token}",
|
|
|
"Content-Type": "application/json"
|
|
|
}
|
|
|
|
|
|
logger.info(f"正在发送API通知到 {url}")
|
|
|
response = requests.post(url, json=data, headers=headers, timeout=10)
|
|
|
|
|
|
logger.info(f"API通知发送结果 - 状态码: {response.status_code}, 响应: {response.text}")
|
|
|
logger.info(f"✅ API通知发送成功") if response.status_code == 200 else logger.error(f"❌ API通知发送失败")
|
|
|
|
|
|
except Exception as e:
|
|
|
logger.error(f"❌ 发送API通知时出错: {e}")
|
|
|
logger.error(f"错误详情: {traceback.format_exc()}")
|
|
|
|
|
|
def send_notification(self, results):
|
|
|
"""发送API通知"""
|
|
|
logger.info("开始发送API通知")
|
|
|
# 确保总是发送API通知,即使发生异常
|
|
|
try:
|
|
|
# 构建通知消息
|
|
|
success_count = sum(1 for _, success, _, _ in results if success)
|
|
|
total_count = len(results)
|
|
|
current_date = datetime.now().strftime("%Y/%m/%d %H:%M:%S")
|
|
|
|
|
|
# 构建API通知消息
|
|
|
api_message = f"🎁 Leaflow自动签到通知\n"
|
|
|
api_message += f"📊 成功: {success_count}/{total_count}\n"
|
|
|
api_message += f"📅 签到时间:{current_date}\n\n"
|
|
|
|
|
|
for email, success, result, balance in results:
|
|
|
# 隐藏邮箱部分字符以保护隐私
|
|
|
masked_email = email[:3] + "***" + email[email.find("@"):]
|
|
|
|
|
|
if success:
|
|
|
status = "✅"
|
|
|
api_message += f"账号:{masked_email}\n"
|
|
|
api_message += f"{status} {result}!\n"
|
|
|
api_message += f"💰 当前总余额:{balance}。\n\n"
|
|
|
else:
|
|
|
status = "❌"
|
|
|
api_message += f"账号:{masked_email}\n"
|
|
|
api_message += f"{status} {result}\n\n"
|
|
|
|
|
|
# 发送API通知
|
|
|
logger.info("准备发送API通知")
|
|
|
self.send_api_notification(api_message)
|
|
|
logger.info("API通知发送完成")
|
|
|
|
|
|
except Exception as e:
|
|
|
logger.error(f"构建API通知消息时出错: {e}")
|
|
|
logger.error(f"错误详情: {traceback.format_exc()}")
|
|
|
# 即使发生异常,也要尝试发送基本的API通知
|
|
|
try:
|
|
|
success_count = sum(1 for _, success, _, _ in results if success)
|
|
|
total_count = len(results)
|
|
|
basic_message = f"签到任务完成,成功{success_count}个,失败{total_count - success_count}个"
|
|
|
logger.info(f"尝试发送基本API通知: {basic_message}")
|
|
|
self.send_api_notification(basic_message)
|
|
|
except Exception as e2:
|
|
|
logger.error(f"发送基本API通知时出错: {e2}")
|
|
|
logger.error(f"错误详情: {traceback.format_exc()}")
|
|
|
|
|
|
def run_all(self):
|
|
|
"""运行所有账号的签到流程"""
|
|
|
logger.info(f"开始执行 {len(self.accounts)} 个账号的签到任务")
|
|
|
|
|
|
results = []
|
|
|
|
|
|
for i, account in enumerate(self.accounts, 1):
|
|
|
logger.info(f"处理第 {i}/{len(self.accounts)} 个账号")
|
|
|
|
|
|
try:
|
|
|
auto_checkin = LeaflowAutoCheckin(account['email'], account['password'])
|
|
|
success, result, balance = auto_checkin.run()
|
|
|
results.append((account['email'], success, result, balance))
|
|
|
|
|
|
# 在账号之间添加间隔,避免请求过于频繁
|
|
|
if i < len(self.accounts):
|
|
|
wait_time = 5
|
|
|
logger.info(f"等待{wait_time}秒后处理下一个账号...")
|
|
|
time.sleep(wait_time)
|
|
|
|
|
|
except Exception as e:
|
|
|
error_msg = f"处理账号时发生异常: {str(e)}"
|
|
|
logger.error(error_msg)
|
|
|
results.append((account['email'], False, error_msg, "未知"))
|
|
|
|
|
|
# 发送第一次汇总通知
|
|
|
self.send_notification(results)
|
|
|
|
|
|
# 暂时关闭30分钟后重试功能
|
|
|
# 检查是否有失败的账号需要重试
|
|
|
# failed_accounts = [account for account, (email, success, _, _) in zip(self.accounts, results) if not success]
|
|
|
# if failed_accounts:
|
|
|
# logger.info(f"发现 {len(failed_accounts)} 个账号签到失败,将在30分钟后重试...")
|
|
|
#
|
|
|
# # 等待30分钟
|
|
|
# retry_wait_time = 30 * 60
|
|
|
# logger.info(f"等待{retry_wait_time}秒后重试失败的账号...")
|
|
|
# time.sleep(retry_wait_time)
|
|
|
#
|
|
|
# # 重试失败的账号
|
|
|
# retry_results = []
|
|
|
# for i, account in enumerate(failed_accounts, 1):
|
|
|
# logger.info(f"重试第 {i}/{len(failed_accounts)} 个失败账号")
|
|
|
#
|
|
|
# try:
|
|
|
# auto_checkin = LeaflowAutoCheckin(account['email'], account['password'])
|
|
|
# success, result, balance = auto_checkin.run()
|
|
|
# retry_results.append((account['email'], success, result, balance))
|
|
|
#
|
|
|
# # 在账号之间添加间隔
|
|
|
# if i < len(failed_accounts):
|
|
|
# wait_time = 5
|
|
|
# logger.info(f"等待{wait_time}秒后处理下一个重试账号...")
|
|
|
# time.sleep(wait_time)
|
|
|
#
|
|
|
# except Exception as e:
|
|
|
# error_msg = f"重试账号时发生异常: {str(e)}"
|
|
|
# logger.error(error_msg)
|
|
|
# retry_results.append((account['email'], False, error_msg, "未知"))
|
|
|
#
|
|
|
# # 发送重试结果通知
|
|
|
# if retry_results:
|
|
|
# # 构建重试通知消息
|
|
|
# retry_success_count = sum(1 for _, success, _, _ in retry_results if success)
|
|
|
# retry_total_count = len(retry_results)
|
|
|
# current_date = datetime.now().strftime("%Y/%m/%d %H:%M:%S")
|
|
|
#
|
|
|
# retry_message = f"🔄 Leaflow自动签到重试通知\n"
|
|
|
# retry_message += f"📊 重试成功: {retry_success_count}/{retry_total_count}\n"
|
|
|
# retry_message += f"📅 重试时间:{current_date}\n\n"
|
|
|
#
|
|
|
# for email, success, result, balance in retry_results:
|
|
|
# masked_email = email[:3] + "***" + email[email.find("@"):]
|
|
|
#
|
|
|
# if success:
|
|
|
# status = "✅"
|
|
|
# retry_message += f"账号:{masked_email}\n"
|
|
|
# retry_message += f"{status} 重试成功!{result}\n"
|
|
|
# retry_message += f"💰 当前总余额:{balance}。\n\n"
|
|
|
# else:
|
|
|
# status = "❌"
|
|
|
# retry_message += f"账号:{masked_email}\n"
|
|
|
# retry_message += f"{status} 重试失败:{result}\n\n"
|
|
|
#
|
|
|
# # 发送重试通知
|
|
|
# logger.info("发送重试结果通知...")
|
|
|
# self.send_api_notification(retry_message)
|
|
|
#
|
|
|
# # 更新原始结果
|
|
|
# for email, success, result, balance in retry_results:
|
|
|
# for i, (orig_email, orig_success, orig_result, orig_balance) in enumerate(results):
|
|
|
# if orig_email == email:
|
|
|
# results[i] = (email, success, result, balance)
|
|
|
# break
|
|
|
|
|
|
# 返回总体结果
|
|
|
success_count = sum(1 for _, success, _, _ in results if success)
|
|
|
return success_count == len(self.accounts), results
|
|
|
|
|
|
def main():
|
|
|
"""主函数"""
|
|
|
try:
|
|
|
manager = MultiAccountManager()
|
|
|
overall_success, detailed_results = manager.run_all()
|
|
|
|
|
|
if overall_success:
|
|
|
logger.info("✅ 所有账号签到成功")
|
|
|
exit(0)
|
|
|
else:
|
|
|
success_count = sum(1 for _, success, _, _ in detailed_results if success)
|
|
|
logger.warning(f"⚠️ 部分账号签到失败: {success_count}/{len(detailed_results)} 成功")
|
|
|
# 即使有失败,也不退出错误状态,因为可能部分成功
|
|
|
exit(0)
|
|
|
|
|
|
except Exception as e:
|
|
|
logger.error(f"❌ 脚本执行出错: {e}")
|
|
|
exit(1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
main()
|
|
|
|