SakuraFrp 自动签到(main + sakura_frp_auto_check + SakuraFrpQiandao),已排除 .env 与日志

This commit is contained in:
RRRosmontis
2026-08-27 21:43:38 +08:00
commit 2a9f96fb18
21 changed files with 2209 additions and 0 deletions
+358
View File
@@ -0,0 +1,358 @@
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()