Files
natfrp-auto-check-in/main.py
T

359 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import logging
import os
import sys
import time
import random
import shutil
import cv2
import numpy as np
from playwright.sync_api import sync_playwright, Page
from playwright.sync_api import TimeoutError as PlaywrightTimeoutError
from playwright.sync_api import Error as PlaywrightError
# 将 SakuraFrpQiandao 加入搜索路径,复用其模块
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "SakuraFrpQiandao"))
from config import Config
from captcha_handler import CaptchaHandler, ModelApiError
from human_simulator import HumanSimulator
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.StreamHandler(),
logging.FileHandler('checkin.log', encoding='utf-8'),
],
)
logger = logging.getLogger(__name__)
def get_gap_offset(bg_path: str, fullbg_path: str) -> int:
"""OpenCV 滑块缺口检测"""
bg = cv2.imread(bg_path, 0)
fullbg = cv2.imread(fullbg_path, 0)
diff = cv2.absdiff(fullbg, bg)
_, thresh = cv2.threshold(diff, 25, 255, cv2.THRESH_BINARY)
contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
if len(contours) < 2:
raise ValueError("轮廓数量不足,滑块识别失败")
sorted_contours = sorted(contours, key=cv2.contourArea, reverse=True)[:2]
rects = [cv2.boundingRect(c) for c in sorted_contours]
target = max(rects, key=lambda r: r[0])
x, y, w, h = target
logger.info(f"滑块缺口坐标: x={x}, y={y}, w={w}, h={h}")
return x
def drag_slider(page: Page, slider, canvas_bg, gap_x: int):
"""模拟人类拖动滑块,根据 canvas 缩放比例校正距离"""
# 计算 canvas 显示宽度与实际截图宽度的比例
canvas_box = canvas_bg.bounding_box()
bg_img = cv2.imread("bg.png", 0)
img_w = bg_img.shape[1] # 截图宽度(像素)
display_w = canvas_box['width'] # CSS 显示宽度
scale = display_w / img_w if img_w > 0 else 1.0
distance = int(gap_x * scale)
logger.info(f"Canvas 缩放比例: {scale:.3f} (img={img_w}px, display={display_w:.1f}px), 校正距离: {distance}px")
slider_box = slider.bounding_box()
start_x = slider_box['x'] + slider_box['width'] / 2
start_y = slider_box['y'] + slider_box['height'] / 2
logger.info(f"滑块起点: ({start_x:.0f}, {start_y:.0f})")
page.mouse.move(start_x, start_y)
page.mouse.down()
page.wait_for_timeout(random.randint(200, 400))
# 分段拖动到目标位置过一点,再回退微调(模拟人类)
page.mouse.move(start_x + distance + random.randint(2, 8), start_y + random.uniform(-1, 1))
page.wait_for_timeout(random.randint(300, 600))
page.mouse.move(start_x + distance - random.randint(2, 5), start_y)
page.wait_for_timeout(random.randint(200, 400))
page.mouse.move(start_x + distance - random.randint(5, 8), start_y)
page.mouse.up()
logger.info("滑块拖动完成")
def solve_slider_captcha(page: Page) -> bool:
"""处理滑块验证码(OpenCV 方案)"""
logger.info("检测到滑块验证码,使用 OpenCV 方案处理...")
try:
# 截图含缺口的背景
bg = page.query_selector('.geetest_canvas_bg')
if not bg:
logger.error("未找到滑块背景 canvas")
return False
bg.screenshot(path="bg.png")
# 显示完整背景并截图
page.evaluate(
"document.querySelector('.geetest_canvas_fullbg').style.display = 'block';"
)
fullbg = page.query_selector('.geetest_canvas_fullbg')
fullbg.screenshot(path="fullbg.png")
page.evaluate(
"document.querySelector('.geetest_canvas_fullbg').style.display = 'none';"
)
gap_x = get_gap_offset("bg.png", "fullbg.png")
slider = page.query_selector('.geetest_slider_button')
if not slider:
logger.error("未找到滑块按钮")
return False
drag_slider(page, slider, bg, gap_x)
time.sleep(2)
# 检查成功文案或 slider 消失
try:
page.wait_for_selector('.geetest_slider_button', state='detached', timeout=3000)
logger.info("滑块已消失,验证通过")
return True
except PlaywrightTimeoutError:
logger.warning("滑块未消失,可能未通过")
return False
except Exception as e:
logger.error(f"滑块验证处理出错: {e}", exc_info=True)
return False
def detect_captcha_type(page: Page) -> str:
"""检测验证码类型: 'slider' | 'grid' | 'none'"""
try:
slider = page.query_selector('.geetest_slider_button')
if slider and slider.is_visible():
return 'slider'
except Exception:
pass
try:
grid = page.query_selector('.geetest_tip_img')
if grid and grid.is_visible():
return 'grid'
except Exception:
pass
return 'none'
class CheckInBot:
def __init__(self, config: Config):
self.config = config
self.captcha_handler = CaptchaHandler(config)
self.simulator = HumanSimulator()
self.max_retries = config.max_retries
def run(self):
headless = os.getenv('CI') == 'true' or os.getenv('HEADLESS', 'false').lower() == 'true'
with sync_playwright() as playwright:
browser = None
try:
launch_opts = {
"headless": headless,
"args": [
"--window-size=1280,800",
"--disable-blink-features=AutomationControlled",
"--no-proxy-server",
"--lang=zh-CN",
"--disable-gpu",
"--no-sandbox",
"--disable-dev-shm-usage",
],
}
chrome_path = self._resolve_chrome()
if chrome_path:
launch_opts["executable_path"] = chrome_path
browser = playwright.chromium.launch(**launch_opts)
context = browser.new_context(
viewport={"width": 1280, "height": 800},
locale="zh-CN",
user_agent=(
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/120.0.0.0 Safari/537.36"
),
)
context.add_init_script(
"Object.defineProperty(navigator, 'webdriver', { get: () => undefined });"
)
page = context.new_page()
page.set_default_timeout(20000)
self._login(page)
self._navigate_dashboard(page)
self._do_checkin(page)
logger.info("签到流程完成")
except ModelApiError:
logger.error("模型 API 调用失败,终止")
except Exception as e:
logger.error(f"执行出错: {e}", exc_info=True)
finally:
if browser and headless:
browser.close()
def _resolve_chrome(self):
if self.config.chrome_binary_path and os.path.exists(self.config.chrome_binary_path):
return self.config.chrome_binary_path
for exe in ("google-chrome", "google-chrome-stable", "chrome", "chromium"):
found = shutil.which(exe)
if found:
return found
return None
def _login(self, page: Page):
url = "https://www.natfrp.com/user/"
logger.info(f"导航到登录页: {url}")
page.goto(url, wait_until="domcontentloaded")
user_input = page.locator("#username")
pass_input = page.locator("#password")
user_input.wait_for(state="visible")
pass_input.wait_for(state="visible")
logger.info("输入登录凭据...")
user_input.fill("")
self.simulator.type_text(user_input, self.config.sakurafrp_user)
pass_input.fill("")
self.simulator.type_text(pass_input, self.config.sakurafrp_pass)
login_btn = page.locator("#login")
login_btn.wait_for(state="visible")
login_btn.click()
logger.info("登录成功")
def _navigate_dashboard(self, page: Page):
self.simulator.random_sleep(3, 5)
self._dismiss_age_check(page)
def _dismiss_age_check(self, page: Page) -> bool:
"""尝试多种方式解除年龄确认弹窗"""
# 方式1: 检测 adult-check 弹窗,点击"是"按钮
try:
adult_check = page.locator(".adult-check")
if adult_check.count() > 0 and adult_check.first.is_visible():
logger.info("检测到年龄确认弹窗...")
yes_btn = page.locator(".adult-check button").first
yes_btn.wait_for(state="visible", timeout=3000)
yes_btn.click(force=True)
logger.info("已点击年龄确认按钮(force click)")
time.sleep(1)
return True
except Exception:
pass
# 方式2: 匹配文本"是,我已满18岁"
try:
age = page.locator("div.yes a", has_text="是,我已满18岁")
age.wait_for(state="visible", timeout=3000)
age.click()
logger.info("已处理年龄确认(文本匹配)")
time.sleep(1)
return True
except PlaywrightTimeoutError:
pass
logger.info("无需年龄确认")
return False
def _already_signed(self, page: Page) -> bool:
try:
page.wait_for_selector("text=今天已经签到过啦", timeout=3000)
return True
except PlaywrightTimeoutError:
return False
def _do_checkin(self, page: Page):
for attempt in range(1, self.max_retries + 1):
logger.info(f"签到尝试 {attempt}/{self.max_retries}")
# 先检查是否已签到
if self._already_signed(page):
logger.info("今日已签到")
return
btn = None
try:
btn = page.locator("button", has_text="点击这里签到")
btn.wait_for(state="visible", timeout=5000)
except PlaywrightTimeoutError:
# 等久一点再检查一次
time.sleep(2)
if self._already_signed(page):
logger.info("今日已签到(延迟检测)")
return
logger.error("未找到签到按钮,也未检测到已签到标识")
return
logger.info("点击签到按钮...")
self._dismiss_age_check(page) # 二次确认,防止弹窗阻挡点击
btn.click(force=True)
self.simulator.random_sleep(2, 4)
captcha_type = detect_captcha_type(page)
logger.info(f"验证码类型: {captcha_type}")
if captcha_type == 'slider':
if solve_slider_captcha(page):
page.reload(wait_until="domcontentloaded")
time.sleep(3)
if self._already_signed(page):
logger.info("滑块验证 + 签到成功")
return
else:
page.reload(wait_until="domcontentloaded")
time.sleep(3)
continue
elif captcha_type == 'grid':
try:
self.captcha_handler.handle_geetest_captcha(page)
page.reload(wait_until="domcontentloaded")
time.sleep(3)
if self._already_signed(page):
logger.info("九宫格验证 + 签到成功")
return
except ModelApiError:
raise
except Exception as e:
logger.error(f"九宫格处理出错: {e}")
elif captcha_type == 'none':
time.sleep(3)
if self._already_signed(page):
logger.info("签到成功(无验证码)")
return
page.reload(wait_until="domcontentloaded")
time.sleep(3)
logger.warning("已达最大重试次数")
def main():
try:
config = Config.from_env()
logger.info(f"使用账户: {config.sakurafrp_user}")
bot = CheckInBot(config)
bot.run()
except ValueError as e:
logger.error(f"配置错误: {e}")
except Exception as e:
logger.error(f"程序执行失败: {e}", exc_info=True)
if __name__ == "__main__":
main()