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
+13
View File
@@ -0,0 +1,13 @@
checkin.log
chromedriver.exe
captured_captcha.jpg
error_screenshot.png
error_page_source.html
.env
__pycache__/
*.pyc
.vscode/
.idea/
node_modules/
.venv
test/
+42
View File
@@ -0,0 +1,42 @@
FROM python:3.11-slim
ARG TARGETARCH
WORKDIR /app
# Playwright/Chrome runtime deps.
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
ca-certificates curl fonts-liberation gnupg wget \
libasound2 libatk-bridge2.0-0 libatk1.0-0 libcups2 libdbus-1-3 \
libdrm2 libgbm1 libgtk-3-0 libnspr4 libnss3 libx11-xcb1 \
libxcomposite1 libxdamage1 libxfixes3 libxkbcommon0 libxrandr2 \
&& rm -rf /var/lib/apt/lists/*
# Install browser: Google Chrome on amd64, Chromium on arm64/others.
RUN set -eux; \
if [ "${TARGETARCH}" = "amd64" ]; then \
wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | gpg --dearmor -o /usr/share/keyrings/google-linux.gpg; \
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/google-linux.gpg] http://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google-chrome.list; \
apt-get update; \
apt-get install -y --no-install-recommends google-chrome-stable; \
rm -rf /var/lib/apt/lists/*; \
ln -sf /usr/bin/google-chrome /usr/bin/container-browser; \
else \
apt-get update; \
apt-get install -y --no-install-recommends chromium || apt-get install -y --no-install-recommends chromium-browser; \
rm -rf /var/lib/apt/lists/*; \
if command -v chromium >/dev/null 2>&1; then ln -sf "$(command -v chromium)" /usr/bin/container-browser; else ln -sf "$(command -v chromium-browser)" /usr/bin/container-browser; fi; \
fi
COPY requirements.txt ./
RUN pip install --no-cache-dir --upgrade pip \
&& pip install --no-cache-dir -r requirements.txt \
&& python -m playwright install-deps chromium
COPY . .
ENV CI=true \
HEADLESS=true \
CHROME_BINARY_PATH=/usr/bin/container-browser
CMD ["python", "main.py"]
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Yumu_Tudou
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+171
View File
@@ -0,0 +1,171 @@
# SakuraFrp 自动签到脚本
使用 Playwright 驱动浏览器,并通过 AI 视觉识别完成 SakuraFrp 九宫格验证码签到。
## 功能特性
- 自动登录 SakuraFrp 账户
- AI 视觉识别九宫格验证码
- 自动点击匹配的验证码格子
- 失败自动重试,默认最多 10 次
- 支持 GitHub Actions 定时执行
- 支持邮件通知和日志归档
## 本地运行
### 1. 环境准备
```bash
python -m venv .venv
.venv\Scripts\activate
pip install -r requirements.txt
```
本项目默认优先使用本机已安装的 Chrome,不需要下载 ChromeDriver,也不强制执行 `playwright install chromium`。
如果本机没有 Chrome,或希望使用 Playwright 托管浏览器,可以执行:
```bash
python -m playwright install chromium
```
### 2. 配置环境变量
创建 `.env` 文件:
```env
SAKURAFRP_USER=your_username
SAKURAFRP_PASS=your_password
BASE_URL=https://api.example.com/v1
API_KEY=your_api_key
MODEL=your_model_name
MAX_RETRIES=10
HEADLESS=false
CHROME_BINARY_PATH=
# 是否将验证码图片转换为 base64(可选)
# 可接受:true/false, 1/0, yes/no, on/off
# 通过第三方适配器接入的服务可能不支持直接使用图片 URL,此时设为 true。
IMAGE_AS_BASE64=false
```
`CHROME_BINARY_PATH` 可选。留空时会自动查找系统 Chrome;如果你的 Chrome 不在默认位置,可以显式指定:
```env
CHROME_BINARY_PATH=C:\Program Files\Google\Chrome\Application\chrome.exe
```
Playwright 只复用 Chrome 可执行文件,不会读取你日常 Chrome 的用户目录、Cookie、历史记录或扩展。
### 3. 运行脚本
```bash
python main.py
```
## GitHub Actions 部署
工作流文件位于 `.github/workflows/sakurafrp_sign.yml`,默认每天北京时间 9:00 执行,也支持手动触发。
需要配置以下 Secrets:
| 名称 | 说明 |
| --- | --- |
| `SAKURAFRP_USER` | SakuraFrp 用户名 |
| `SAKURAFRP_PASS` | SakuraFrp 密码 |
| `BASE_URL` | OpenAI 兼容 API 地址 |
| `API_KEY` | API 密钥 |
| `MODEL` | 多模态模型名称 |
邮件通知为可选配置:
| 名称 | 说明 |
| --- | --- |
| `EMAIL_USERNAME` | 发件邮箱 |
| `EMAIL_PASSWORD` | 邮箱授权码或应用密码 |
| `RECEIVER_EMAIL` | 收件邮箱,默认发给自己 |
| `SMTP_SERVER` | SMTP 服务器,默认 `smtp.gmail.com` |
| `SMTP_PORT` | SMTP 端口,默认 `587` |
Actions 使用 GitHub runner 自带 Chrome,并执行 `python -m playwright install-deps chromium` 安装 Linux 运行依赖,避免每次下载 Playwright Chromium 大包。
## 项目结构
```text
SakuraFrp-Qiandao/
├── .github/workflows/sakurafrp_sign.yml
├── automation.py # Playwright 签到主流程
├── captcha_handler.py # GeeTest 验证码识别和点击
├── config.py # 环境变量和日志配置
├── human_simulator.py # 简单人类操作模拟
├── main.py # 程序入口
├── send_email.py # 邮件通知
├── requirements.txt # Python 依赖
└── README.md
```
## 配置说明
### 定时任务时间
修改 `.github/workflows/sakurafrp_sign.yml` 中的 cron 表达式:
```yaml
schedule:
- cron: '0 1 * * *' # UTC 1:00 = 北京时间 9:00
```
常用时间对照:
- `0 1 * * *` - 每天 9:00(北京时间)
- `0 13 * * *` - 每天 21:00(北京时间)
- `0 1,13 * * *` - 每天 9:00 和 21:00
### AI 模型推荐
支持任何兼容 OpenAI API 的多模态模型:
- OpenAI: `gpt-4o`
- 阿里通义: `qwen-vl-plus`, `qwen-vl-max`
- 智谱 AI: `glm-4v`
- GitHub Copilot: 可通过第三方 OpenAI 兼容适配层接入
- ModelScope: 各种开源视觉模型
## 故障排查
### 验证码识别失败
尝试更换更强的视觉模型,或增加 `MAX_RETRIES`。模型需要支持图片输入,并兼容 OpenAI Chat Completions API。
### 模型返回非标准响应
脚本会记录完整响应结构。如果响应中包含 `error` 或 `errors`,会直接终止,因为刷新验证码无法修复模型 API 错误。
### Playwright 下载 Chromium 失败
如果出现 `timed out`、`ECONNRESET` 或 `Failed to download Chrome for Testing`,通常是网络到 Playwright/CDN 下载源不稳定。推荐直接使用系统 Chrome,并在 `.env` 中按需设置 `CHROME_BINARY_PATH`。
### 登录失败
检查 `SAKURAFRP_USER` 和 `SAKURAFRP_PASS` 是否正确,必要时关闭无头模式查看页面行为:
```env
HEADLESS=false
```
### 未收到邮件
确认邮箱开启 SMTP,并使用授权码或应用专用密码,不要直接使用普通登录密码。
## 注意事项
请勿频繁触发签到,避免被限流。妥善保管账号密码、API Key 和邮箱授权码。
## 相关链接
- [SakuraFrp 官网](https://www.natfrp.com/)
- [Playwright 文档](https://playwright.dev/python/)
- [GitHub Actions 文档](https://docs.github.com/en/actions)
- [copilot-api](https://github.com/caozhiyuan/copilot-api)
- [LiteLLM](https://github.com/BerriAI/litellm)
+256
View File
@@ -0,0 +1,256 @@
import logging
import os
import shutil
import time
from playwright.sync_api import Error as PlaywrightError
from playwright.sync_api import Page
from playwright.sync_api import TimeoutError as PlaywrightTimeoutError
from playwright.sync_api import sync_playwright
from config import Config
from captcha_handler import ModelApiError
logger = logging.getLogger(__name__)
class CheckInAutomation:
"""签到自动化主类"""
def __init__(self, config: Config):
self.config = config
try:
from captcha_handler import CaptchaHandler
except ImportError:
from captcha_handler import CaptchaHandler # 如果模块名不同
self.captcha_handler = CaptchaHandler(config)
try:
from human_simulator import HumanSimulator
except ImportError:
from human_simulator import HumanSimulator # 如果模块名不同
self.simulator = HumanSimulator()
self.max_retries = config.max_retries
def run(self):
"""执行签到流程"""
# GitHub Actions 环境自动使用 headless 模式
headless = os.getenv('CI') == 'true' or os.getenv('HEADLESS', 'false').lower() == 'true'
with sync_playwright() as playwright:
browser = None
try:
logger.info("正在初始化 Playwright Chromium...")
launch_options = {
"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_path()
if chrome_path:
logger.info(f"使用 Chrome 路径: {chrome_path}")
launch_options["executable_path"] = chrome_path
browser = playwright.chromium.launch(**launch_options)
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)
# 步骤1: 登录
if not self._login(page):
logger.error("登录失败")
return
# 步骤2: 跳转到 处理年龄
if not self._navigate_to_sakurafrp(page):
logger.error("跳转到 SakuraFrp 失败")
return
# 步骤3: 执行签到
if not self._perform_checkin(page):
logger.error("签到失败")
page.screenshot(path="error_screenshot.png", full_page=True)
with open("error_page_source.html", "w", encoding="utf-8") as f:
f.write(page.content())
return
logger.info("✓ 签到流程完成")
except ModelApiError:
logger.error("模型 API 调用失败,终止签到流程")
return
except Exception as e:
logger.error(f"执行过程中发生错误: {e}", exc_info=True)
finally:
if browser and headless:
browser.close()
logger.info("脚本执行完毕")
def _resolve_chrome_path(self):
"""优先使用环境变量指定的 Chrome,其次查找系统 Chrome。"""
if self.config.chrome_binary_path and os.path.exists(self.config.chrome_binary_path):
return self.config.chrome_binary_path
candidate_paths = [
os.path.join(
os.environ.get("PROGRAMFILES", ""),
"Google",
"Chrome",
"Application",
"chrome.exe",
),
os.path.join(
os.environ.get("PROGRAMFILES(X86)", ""),
"Google",
"Chrome",
"Application",
"chrome.exe",
),
os.path.join(
os.environ.get("LOCALAPPDATA", ""),
"Google",
"Chrome",
"Application",
"chrome.exe",
),
]
for executable in ("google-chrome", "google-chrome-stable", "chrome", "chromium"):
found = shutil.which(executable)
if found:
return found
for path in candidate_paths:
if path and os.path.exists(path):
return path
return None
def _login(self, page: Page) -> bool:
"""执行登录"""
login_url = "https://www.natfrp.com/user/"
logger.info(f"导航到登录页面: {login_url}")
page.goto(login_url, wait_until="domcontentloaded")
try:
# 输入用户名和密码
username_input = page.locator("#username")
password_input = page.locator("#password")
username_input.wait_for(state="visible")
password_input.wait_for(state="visible")
logger.info("输入登录凭据...")
username_input.fill("")
self.simulator.type_text(username_input, self.config.sakurafrp_user)
password_input.fill("")
self.simulator.type_text(password_input, self.config.sakurafrp_pass)
# 点击登录按钮
login_button = page.locator("#login")
login_button.wait_for(state="visible")
logger.info("点击登录按钮...")
login_button.click()
self.simulator.random_sleep(3, 5)
logger.info("登录成功")
return True
except PlaywrightTimeoutError:
logger.error("登录页面元素加载超时")
return False
except Exception as e:
logger.error(f"登录过程出错: {e}", exc_info=True)
return False
def _navigate_to_sakurafrp(self, page: Page) -> bool:
"""跳转到 SakuraFrp 仪表板"""
try:
# 点击 SakuraFrp 链接
# sakura_link = page.locator("div.action-list a", has_text="Sakura Frp")
# logger.info("点击 SakuraFrp 跳转链接...")
# sakura_link.click()
# self.simulator.random_sleep(2, 4)
# 处理年龄确认弹窗(如果存在)
try:
age_confirm = page.locator("div.yes a", has_text="是,我已满18岁")
age_confirm.wait_for(state="visible", timeout=5000)
logger.info("处理年龄确认弹窗...")
age_confirm.click()
self.simulator.random_sleep(2, 3)
except PlaywrightTimeoutError:
logger.info("未检测到年龄确认弹窗")
logger.info("成功跳转到 SakuraFrp 仪表板")
return True
except PlaywrightTimeoutError:
logger.warning("SakuraFrp 跳转链接未找到,可能已在目标页面")
return True
except Exception as e:
logger.error(f"跳转过程出错: {e}", exc_info=True)
return False
def _perform_checkin(self, page: Page) -> bool:
"""执行签到操作"""
for attempt in range(1, self.max_retries+1):
logger.info(f"验证码尝试 {attempt}/{self.max_retries}")
try:
# 查找签到按钮
check_in_button = None
try:
check_in_button = page.locator("button", has_text="点击这里签到")
check_in_button.wait_for(state="visible")
logger.info("找到签到按钮")
except PlaywrightTimeoutError:
# 检查是否已签到
try:
page.locator("p", has_text="今天已经签到过啦").wait_for(
state="visible",
timeout=2000,
)
logger.info("今日已签到")
return True
except PlaywrightTimeoutError:
logger.error("未找到签到按钮或已签到标识")
return False
# 点击签到按钮
if check_in_button:
logger.info("点击签到按钮...")
check_in_button.click()
self.simulator.random_sleep(2, 4)
# 处理验证码
self.captcha_handler.handle_geetest_captcha(page)
page.reload(wait_until="domcontentloaded")
time.sleep(5)
continue
return False
except ModelApiError:
raise
except PlaywrightError as e:
logger.error(f"签到过程出错: {e}", exc_info=True)
return False
except Exception as e:
logger.error(f"签到过程出错: {e}", exc_info=True)
return False
logger.info("已达到最大重试次数")
return False
+298
View File
@@ -0,0 +1,298 @@
import logging
import time
import random
import json
import re
from typing import Optional, Dict
from openai import OpenAI
from playwright.sync_api import Page
from playwright.sync_api import TimeoutError as PlaywrightTimeoutError
from config import Config
logger = logging.getLogger(__name__)
class ModelApiError(Exception):
"""模型 API 错误,属于不可重试错误。"""
class CaptchaHandler:
"""验证码处理器"""
def __init__(self, config: Config):
self.config = config
self.client = OpenAI(
base_url=config.base_url,
api_key=config.api_key
)
def get_img(self, page: Page):
try:
# 获取验证码图片
captcha_img_element = page.locator(".geetest_tip_img")
captcha_img_element.wait_for(state="visible", timeout=10000)
# 获取 CSS 属性
bg_style = captcha_img_element.evaluate(
"el => window.getComputedStyle(el).backgroundImage"
)
# 正则匹配
match = re.search(r'url\(["\']?(.*?)["\']?\)', bg_style)
if match:
img_url = match.group(1)
logger.info(f"成功获取验证码图片 URL: {img_url}")
return img_url
else:
logger.error("无法提取验证码图片 URL")
return ""
except PlaywrightTimeoutError:
logger.info("未检测到 GeeTest 验证码窗口")
return False
def handle_geetest_captcha(self, page: Page) -> bool:
"""处理 GeeTest 九宫格验证码(带重试机制)"""
logger.info("开始处理 GeeTest 验证码...")
try:
# 获取验证码图片
img_url = self.get_img(page)
if not img_url:
logger.error("图片获取失败,刷新网页重试...")
time.sleep(2)
return False
# 调用视觉模型识别,imagebase64 由配置控制
recognition_result = self._recognize_captcha(img_url, imagebase64=self.config.image_as_base64)
if not recognition_result:
logger.warning("识别失败,刷新网页重试...")
time.sleep(2)
return False
logger.info(f"验证码识别结果: {recognition_result}")
# 根据识别结果点击相应的九宫格
if not self._click_captcha_items(page, recognition_result):
logger.warning("点击失败,刷新网页重试...")
time.sleep(2)
return False
logger.warning("验证码流程完成,刷新网页验证是否成功...")
time.sleep(2)
return True
except ModelApiError:
raise
except Exception as e:
logger.error(f"处理验证码时发生错误: {e}", exc_info=True)
return False
def _recognize_captcha(self, img_url: str, imagebase64: bool = False) -> Optional[Dict]:
"""使用视觉模型识别验证码,支持 imagebase64 方式"""
try:
prompt = (
'这是一个九宫格验证码,请按从左到右、从上到下的顺序识别每个格子里的物品名称,'
'最后识别左下角的参考图。输出格式为JSON:{"1":"名称", "2":"名称", ..., "10":"参考图名称"}。'
'名称要简洁,参考图名称必须是九宫格里已有的名称。若有类似物品(如气球与热气球),请统一名称。'
)
image_url_value = img_url
if imagebase64:
import requests, base64
try:
resp = requests.get(img_url)
resp.raise_for_status()
img_bytes = resp.content
img_b64 = base64.b64encode(img_bytes).decode('utf-8')
image_url_value = f"data:image/jpeg;base64,{img_b64}"
except Exception as e:
logger.error(f"图片转base64失败: {e}")
raise ModelApiError("图片转base64失败") from e
response = self.client.chat.completions.create(
model=self.config.model,
messages=[{
'role': 'user',
'content': [
{'type': 'text', 'text': prompt},
{'type': 'image_url', 'image_url': {'url': image_url_value}}
]
}],
stream=False
)
response_data = response.model_dump() if hasattr(response, "model_dump") else {}
logger.debug(f"模型完整响应: {response_data}")
for error_key in ("error", "errors"):
if response_data.get(error_key):
logger.error(f"模型 API 返回错误: {response_data[error_key]}")
raise ModelApiError("模型 API 返回错误")
choices = response_data.get("choices") or []
if not choices:
logger.error(f"模型 API 返回非标准成功响应,缺少 choices: {response_data}")
raise ModelApiError("模型 API 返回非标准成功响应")
result_content = choices[0].get("message", {}).get("content")
if not result_content:
logger.error(f"模型 API 响应缺少 message.content: {response_data}")
raise ModelApiError("模型 API 响应缺少 message.content")
logger.info(f"模型原始输出: {result_content}")
json_text = self._extract_json(result_content)
if not json_text:
logger.error("无法从模型输出中提取有效 JSON")
raise ModelApiError("模型输出不包含有效 JSON")
cleaned_str = json_text.replace("'", '"').strip()
result_dict = json.loads(cleaned_str)
if isinstance(result_dict, dict) and result_dict.get("error"):
logger.error(f"模型 API 返回错误: {result_dict['error']}")
raise ModelApiError("模型 API 返回错误")
return result_dict
except json.JSONDecodeError as e:
logger.error(f"JSON 解析失败: {e}")
raise ModelApiError("模型输出 JSON 解析失败") from e
except Exception as e:
if isinstance(e, ModelApiError):
raise
status_code = getattr(e, "status_code", None)
if status_code:
logger.error(f"模型 API 调用失败,HTTP {status_code}: {e}")
else:
logger.error(f"模型 API 调用失败: {e}", exc_info=True)
raise ModelApiError("模型 API 调用失败") from e
@staticmethod
def _extract_json(text: str) -> Optional[str]:
"""从模型输出中提取 JSON,兼容代码块、Qwen box 标记和普通说明文本。"""
if not isinstance(text, str):
text = str(text)
box_match = re.search(r'<\|begin_of_box\|>(.*?)<\|end_of_box\|>', text, flags=re.DOTALL)
if box_match:
return box_match.group(1).strip()
code_block_match = re.search(r'```(?:json)?\s*(\{.*?\})\s*```', text, flags=re.DOTALL)
if code_block_match:
return code_block_match.group(1).strip()
start = text.find('{')
if start == -1:
return None
stack = 0
for i in range(start, len(text)):
if text[i] == '{':
stack += 1
elif text[i] == '}':
stack -= 1
if stack == 0:
return text[start:i + 1].strip()
return None
def _click_captcha_items(self, page: Page, recognition_result: Dict) -> bool:
"""
根据识别结果点击九宫格中匹配的格子
九宫格布局(索引从1开始):
1 2 3
4 5 6
7 8 9
第10个是参考图(左下角)
"""
try:
# 获取参考图名称(第10个元素)
target_name = recognition_result.get("10", "").strip()
if not target_name:
logger.error("未能从识别结果中获取参考图名称")
return False
logger.info(f"目标物品: {target_name}")
# 获取所有九宫格元素(前9个)
grid_items = page.locator(".geetest_item")
item_count = grid_items.count()
# 排除最后一个(参考图),只处理前9个
if item_count < 9:
logger.error(f"九宫格元素数量不足,只找到 {item_count} 个")
return False
# 遍历前9个格子,找到匹配的物品并点击
clicked_count = 0
for i in range(9):
position = i + 1 # 位置索引从1开始
item_name = recognition_result.get(str(position), "").strip()
logger.info(f"位置 {position}: {item_name}")
# 如果当前格子的物品名称匹配参考图
if item_name and item_name == target_name:
logger.info(f"找到匹配项!位置 {position} - {item_name}")
# 点击该格子
try:
grid_items.nth(i).click()
clicked_count += 1
logger.info(f"已点击位置 {position}")
# 点击后短暂等待,模拟人类操作
time.sleep(random.uniform(0.3, 0.6))
except Exception as e:
logger.error(f"点击位置 {position} 时出错: {e}")
if clicked_count == 0:
logger.warning(f"未找到匹配 '{target_name}' 的格子")
return False
logger.info(f"共点击了 {clicked_count} 个匹配的格子")
# 点击完成后,查找并点击确认按钮
try:
# 等待确认按钮变为可用状态(移除 geetest_disable 类)
confirm_button = page.locator(".geetest_commit")
confirm_button.wait_for(state="attached", timeout=5000)
# 检查按钮是否可用(没有 geetest_disable 类)
button_classes = confirm_button.get_attribute("class") or ""
logger.info(f"确认按钮状态: {button_classes}")
# 等待按钮变为可点击状态(最多等待3秒)
max_wait = 3
start = time.time()
while "geetest_disable" in (confirm_button.get_attribute("class") or ""):
if time.time() - start > max_wait:
logger.warning("确认按钮未激活,但仍尝试点击")
break
time.sleep(0.2)
logger.info("找到确认按钮,准备点击...")
confirm_button.click(force=True)
logger.info("已点击确认按钮")
time.sleep(1)
except PlaywrightTimeoutError:
logger.info("未找到确认按钮,可能自动提交")
return True
except Exception as e:
logger.error(f"点击验证码格子时发生错误: {e}", exc_info=True)
return False
def _refresh_captcha(self, page: Page) -> bool:
"""刷新验证码"""
try:
logger.info("正在刷新验证码...")
page.locator(".geetest_refresh").click()
logger.info("已点击刷新按钮")
time.sleep(1.5) # 等待新验证码加载
return True
except Exception as e:
logger.error(f"刷新验证码失败: {e}")
return False
+61
View File
@@ -0,0 +1,61 @@
import logging
import os
from typing import Optional
from dataclasses import dataclass
# 尝试加载 .env 文件
try:
from dotenv import load_dotenv
load_dotenv(override=True)
except ImportError:
pass
# 配置日志
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__)
@dataclass
class Config:
"""配置数据类"""
sakurafrp_user: str
sakurafrp_pass: str
base_url: str
api_key: str
model: str
chrome_binary_path: Optional[str] = None
max_retries: int = 10
image_as_base64: bool = False
@classmethod
def from_env(cls) -> 'Config':
"""从环境变量加载配置"""
def get_env(key: str, required: bool = True) -> str:
value = os.environ.get(key, "").split('\n')[0].strip()
if required and not value:
raise ValueError(f"环境变量 {key} 未设置或为空")
return value
def get_bool_env(key: str, default: bool = False) -> bool:
raw_value = os.environ.get(key, "").strip().lower()
if not raw_value:
return default
return raw_value in {"1", "true", "yes", "on"}
return cls(
sakurafrp_user=get_env("SAKURAFRP_USER"),
sakurafrp_pass=get_env("SAKURAFRP_PASS"),
base_url=get_env("BASE_URL"),
api_key=get_env("API_KEY"),
model=get_env("MODEL"),
chrome_binary_path=get_env("CHROME_BINARY_PATH", required=False),
max_retries=int(get_env("MAX_RETRIES", required=False) or 10),
image_as_base64=get_bool_env("IMAGE_AS_BASE64", default=False)
)
+18
View File
@@ -0,0 +1,18 @@
import time
import random
class HumanSimulator:
"""模拟人类行为"""
@staticmethod
def type_text(element, text: str, min_delay: float = 0.05, max_delay: float = 0.2):
"""模拟人类打字"""
for char in text:
element.type(char, delay=0)
time.sleep(random.uniform(min_delay, max_delay))
@staticmethod
def random_sleep(min_sec: float = 1.0, max_sec: float = 3.0):
"""随机等待"""
time.sleep(random.uniform(min_sec, max_sec))
+26
View File
@@ -0,0 +1,26 @@
from config import Config
from automation import CheckInAutomation
import logging
logger = logging.getLogger(__name__)
def main():
"""主函数"""
try:
# 加载配置
config = Config.from_env()
logger.info(f"使用账户: {config.sakurafrp_user}")
# 执行自动签到
automation = CheckInAutomation(config)
automation.run()
except ValueError as e:
logger.error(f"配置错误: {e}")
except Exception as e:
logger.error(f"程序执行失败: {e}", exc_info=True)
if __name__ == "__main__":
main()
+14
View File
@@ -0,0 +1,14 @@
# Browser automation
playwright>=1.46.0
# API client
openai>=1.0.0
# Environment variables
python-dotenv==1.0.0
# Packaging
setuptools>=65.0.0
# Utilities
requests>=2.31.0
+96
View File
@@ -0,0 +1,96 @@
import smtplib
import os
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.mime.base import MIMEBase
from email import encoders
from datetime import datetime
def send_log_email(log_file='checkin.log'):
"""
发送签到日志邮件
Args:
log_file: 日志文件路径
"""
# 从环境变量读取配置
smtp_server = os.getenv('SMTP_SERVER', 'smtp.gmail.com')
smtp_port = int(os.getenv('SMTP_PORT', '587'))
sender_email = os.getenv('EMAIL_USERNAME', '')
sender_password = os.getenv('EMAIL_PASSWORD', '')
receiver_email = os.getenv('RECEIVER_EMAIL', sender_email)
# 检查配置
if not sender_email or not sender_password:
print("❌ 邮件配置未设置,跳过发送")
return False
try:
# 读取日志内容
log_content = ""
if os.path.exists(log_file):
with open(log_file, 'r', encoding='utf-8') as f:
log_content = f.read()
else:
log_content = "日志文件不存在"
# 判断签到是否成功(根据日志内容)
is_success = "签到流程完成" in log_content or "验证码验证成功" in log_content
status_emoji = "✅" if is_success else "❌"
status_text = "成功" if is_success else "失败"
# 创建邮件
msg = MIMEMultipart()
msg['From'] = sender_email
msg['To'] = receiver_email
msg['Subject'] = f"{status_emoji} SakuraFrp 签到{status_text} - {datetime.now().strftime('%Y-%m-%d')}"
# 邮件正文
body = f"""
SakuraFrp 自动签到报告
状态: {status_emoji} {status_text}
时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
{'='*50}
日志内容:
{'='*50}
{log_content[-2000:] if len(log_content) > 2000 else log_content}
{'='*50}
此邮件由自动签到系统发送
"""
msg.attach(MIMEText(body, 'plain', 'utf-8'))
# 添加日志附件
if os.path.exists(log_file):
with open(log_file, 'rb') as f:
part = MIMEBase('application', 'octet-stream')
part.set_payload(f.read())
encoders.encode_base64(part)
part.add_header('Content-Disposition', f'attachment; filename= {os.path.basename(log_file)}')
msg.attach(part)
if smtp_port == 465:
server = smtplib.SMTP_SSL(smtp_server, smtp_port)
else:
server = smtplib.SMTP(smtp_server, smtp_port)
server.starttls()
with server:
server.login(sender_email, sender_password)
server.send_message(msg)
print(f"✅ 邮件发送成功: {receiver_email}")
return True
except Exception as e:
print(f"❌ 邮件发送失败: {e}")
return False
if __name__ == "__main__":
send_log_email()