SakuraFrp 自动签到(main + sakura_frp_auto_check + SakuraFrpQiandao),已排除 .env 与日志
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
/bg.png
|
||||
/cookies.txt
|
||||
/debug_01_bg_gray.png
|
||||
/debug_02_fullbg_gray.png
|
||||
/debug_03_diff.png
|
||||
/debug_04_thresh.png
|
||||
/debug_05_with_box.png
|
||||
/fullbg.png
|
||||
/state.json
|
||||
/account.txt
|
||||
@@ -0,0 +1,77 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
This is an automated FRP (NATFRP) check-in bot built with Python and Playwright. The application automates daily sign-in on www.natfrp.com with CAPTCHA solving capabilities.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Core Components
|
||||
|
||||
- **main.py**: Single-file application containing the entire automation logic
|
||||
- **Computer Vision Module** (`get_gap_offset`): Uses OpenCV to solve sliding CAPTCHA puzzles by detecting gaps in background images
|
||||
- **Browser Automation**: Uses Playwright to interact with the NATFRP website
|
||||
- **State Management**: Preserves login sessions using `state.json` to avoid repeated logins
|
||||
- **Retry Logic**: Built-in retry mechanism with configurable maximum attempts
|
||||
|
||||
### Key Functions
|
||||
|
||||
- `get_gap_offset()`: CAPTCHA solver using image difference detection in main.py:7-51
|
||||
- `main()`: Main automation workflow in main.py:251-324
|
||||
- `solve_geetest_puzzle()`: Handles GeeTest CAPTCHA solving in main.py:220-235
|
||||
- `wait_signed_text_and_shoot()`: Success detection and screenshot capture in main.py:238-248
|
||||
|
||||
## Running the Application
|
||||
|
||||
### Basic Execution
|
||||
```bash
|
||||
python main.py
|
||||
```
|
||||
|
||||
### Dependencies
|
||||
The project requires:
|
||||
- playwright
|
||||
- opencv-python (cv2)
|
||||
- pathlib (built-in)
|
||||
|
||||
Install Playwright browsers after installing the package:
|
||||
```bash
|
||||
playwright install chromium
|
||||
```
|
||||
|
||||
## Configuration Files
|
||||
|
||||
- **account.txt**: Contains username (line 1) and password (line 2) for NATFRP login
|
||||
- **state.json**: Browser session state for maintaining login sessions
|
||||
- **.gitignore**: Excludes sensitive files and debug images
|
||||
|
||||
## Generated Files
|
||||
|
||||
- **bg.png, fullbg.png**: CAPTCHA background images extracted during solving
|
||||
- **debug_*.png**: Debug images for CAPTCHA solving analysis (when debug=True)
|
||||
- **checkin.png**: Screenshot saved upon successful sign-in
|
||||
|
||||
## Configuration Variables
|
||||
|
||||
Key settings in main.py:53-64:
|
||||
- `MAX_RETRY`: Maximum retry attempts (0 for infinite)
|
||||
- `ALREADY_SIGNED_TEXT`: Success detection text ("今天已经签到过啦")
|
||||
- `SIGNED_ANCESTOR_LEVELS`: Screenshot capture depth (3 parent levels)
|
||||
- `SIGNED_ANCESTOR_SCREENSHOT`: Success screenshot filename
|
||||
|
||||
## CAPTCHA Solving Process
|
||||
|
||||
1. Extract background image with gap (`bg.png`)
|
||||
2. Extract complete background image (`fullbg.png`)
|
||||
3. Calculate absolute difference between images
|
||||
4. Apply binary threshold and find contours
|
||||
5. Select gap location based on largest contours
|
||||
6. Simulate human-like mouse dragging to solve
|
||||
|
||||
## Browser Configuration
|
||||
|
||||
- Runs in non-headless mode by default (`headless=False`)
|
||||
- Uses `slow_mo=200` for human-like interaction timing
|
||||
- Maintains session state between runs via `state.json`
|
||||
@@ -0,0 +1,85 @@
|
||||
# CLAUDE_CN.md
|
||||
|
||||
此文件为 Claude Code (claude.ai/code) 在此代码仓库中工作时提供中文指导。
|
||||
|
||||
## 项目概述
|
||||
|
||||
这是一个使用 Python 和 Playwright 构建的自动化 FRP (NATFRP) 签到机器人。该应用程序可以自动在 www.natfrp.com 上进行每日签到,具备验证码破解能力。
|
||||
|
||||
## 架构设计
|
||||
|
||||
### 核心组件
|
||||
|
||||
- **main.py**: 包含所有自动化逻辑的单文件应用程序
|
||||
- **计算机视觉模块** (`get_gap_offset`): 使用 OpenCV 通过检测背景图像缺口来破解滑动验证码
|
||||
- **浏览器自动化**: 使用 Playwright 与 NATFRP 网站交互
|
||||
- **状态管理**: 使用 `state.json` 保存登录会话以避免重复登录
|
||||
- **重试机制**: 内置重试逻辑,支持配置最大重试次数
|
||||
|
||||
### 关键函数
|
||||
|
||||
- `get_gap_offset()`: 验证码破解器,使用图像差异检测 (main.py:7-51)
|
||||
- `main()`: 主要自动化工作流程 (main.py:251-324)
|
||||
- `solve_geetest_puzzle()`: 处理极验验证码破解 (main.py:220-235)
|
||||
- `wait_signed_text_and_shoot()`: 成功检测和截图保存 (main.py:238-248)
|
||||
|
||||
## 运行应用程序
|
||||
|
||||
### 基本执行
|
||||
```bash
|
||||
python main.py
|
||||
```
|
||||
|
||||
### 依赖项
|
||||
项目需要以下依赖:
|
||||
- playwright
|
||||
- opencv-python (cv2)
|
||||
- pathlib (内置模块)
|
||||
|
||||
安装 Playwright 浏览器:
|
||||
```bash
|
||||
playwright install chromium
|
||||
```
|
||||
|
||||
## 配置文件
|
||||
|
||||
- **account.txt**: 包含 NATFRP 登录用户名(第1行)和密码(第2行)
|
||||
- **state.json**: 浏览器会话状态,用于保持登录状态
|
||||
- **.gitignore**: 排除敏感文件和调试图像
|
||||
|
||||
## 生成文件
|
||||
|
||||
- **bg.png, fullbg.png**: 验证码破解过程中提取的背景图像
|
||||
- **debug_*.png**: 验证码破解分析的调试图像(当 debug=True 时)
|
||||
- **checkin.png**: 签到成功时保存的截图
|
||||
|
||||
## 配置变量
|
||||
|
||||
main.py:53-64 中的关键设置:
|
||||
- `MAX_RETRY`: 最大重试次数(0表示无限重试)
|
||||
- `ALREADY_SIGNED_TEXT`: 成功检测文本("今天已经签到过啦")
|
||||
- `SIGNED_ANCESTOR_LEVELS`: 截图捕获深度(3层父级元素)
|
||||
- `SIGNED_ANCESTOR_SCREENSHOT`: 成功截图文件名
|
||||
|
||||
## 验证码破解流程
|
||||
|
||||
1. 提取带缺口的背景图像(`bg.png`)
|
||||
2. 提取完整背景图像(`fullbg.png`)
|
||||
3. 计算两张图像的绝对差异
|
||||
4. 应用二值化阈值并查找轮廓
|
||||
5. 根据最大轮廓选择缺口位置
|
||||
6. 模拟人类般的鼠标拖拽来破解验证码
|
||||
|
||||
## 浏览器配置
|
||||
|
||||
- 默认以非无头模式运行(`headless=False`)
|
||||
- 使用 `slow_mo=200` 实现类人交互时序
|
||||
- 通过 `state.json` 在运行间保持会话状态
|
||||
|
||||
## 使用说明
|
||||
|
||||
1. 确保 `account.txt` 文件包含正确的用户名和密码
|
||||
2. 首次运行会创建 `state.json` 保存登录状态
|
||||
3. 程序会自动检测是否已签到,避免重复操作
|
||||
4. 遇到验证码时会自动进行图像识别和破解
|
||||
5. 签到成功后会保存截图作为凭证
|
||||
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,116 @@
|
||||
# SAKURA FRP 自动签到脚本
|
||||
|
||||
> 自动访问 `https://www.natfrp.com/user/` 完成每日签到;支持在本地直接运行,也支持在 **QingLong(青龙)** 定时任务中运行。
|
||||
> 成功后会自动保存签到结果截图(默认:`checkin.png`),并在必要时自动处理滑块验证。
|
||||
|
||||
## 一、目录结构
|
||||
|
||||
```
|
||||
.
|
||||
├─ main.py # 主程序(无需改动即可用)
|
||||
├─ account.txt # 账号文件(必填:第1行用户名;第2行密码)
|
||||
├─ state.json # 登录状态缓存文件(自动生成与更新)
|
||||
└─ checkin.png # 成功时保存的签到区域截图(自动生成)
|
||||
```
|
||||
|
||||
> 说明:程序会优先尝试复用 `state.json` 中的登录状态;状态失效时会自动回退为账号密码登录。
|
||||
|
||||
## 二、准备工作
|
||||
|
||||
1. **创建账号文件 `account.txt`**
|
||||
- 第 1 行:用户名
|
||||
- 第 2 行:密码
|
||||
- 文件编码建议使用 `UTF-8`,不要多余空行或空格。
|
||||
示例:
|
||||
```
|
||||
your_username
|
||||
your_password
|
||||
```
|
||||
|
||||
2. **(可选)检查/调整配置** — 打开 `main.py` 顶部“配置”区域,可按需修改:
|
||||
- `ACCOUNT_FILE`:账号文件路径(默认 `account.txt`)
|
||||
- `STATE_FILE`:状态缓存文件(默认 `state.json`)
|
||||
- `MAX_RETRY`:失败重试次数(`0` 表示无限重试)
|
||||
- `SIGNED_ANCESTOR_SCREENSHOT`:成功截图文件名(默认 `checkin.png`)
|
||||
|
||||
> **无需修改网站域名与地址**,默认已指向 `https://www.natfrp.com/user/`。
|
||||
|
||||
## 三、环境要求
|
||||
|
||||
需要安装Playwright及其依赖,推荐使用 Python 3.7 及以上版本。
|
||||
|
||||
## 四、在 QingLong(青龙)中使用
|
||||
|
||||
> 适用于青龙面板 v2.XX 及以上版本(路径请以你面板的实际目录为准)。
|
||||
|
||||
### 1)放置脚本与账号文件
|
||||
|
||||
方式 A:在面板「文件管理」中将 `main.py` 和 `account.txt` 上传到脚本目录,例如:
|
||||
```
|
||||
/ql/data/scripts/natfrp_checkin/
|
||||
```
|
||||
|
||||
方式 B:通过仓库/订阅拉取(如你有自建仓库),确保最终能在脚本目录看到 `main.py` 与 `account.txt`。
|
||||
|
||||
> **注意**:`account.txt` 一定要跟 `main.py` 放在同一目录(或同步修改 `ACCOUNT_FILE` 为你的自定义路径)。
|
||||
|
||||
### 2)创建定时任务
|
||||
|
||||
在青龙面板 ->「定时任务」->「新建任务」:
|
||||
|
||||
- **名称**:NATFRP 自动签到
|
||||
- **命令**(按你的实际路径替换):
|
||||
```bash
|
||||
python3 /ql/data/scripts/natfrp_checkin/main.py
|
||||
```
|
||||
- **定时规则(CRON)**:建议每日固定时间运行一次,例如每天 8:10:
|
||||
```
|
||||
10 8 * * *
|
||||
```
|
||||
|
||||
保存后即可生效。你也可以在任务列表中手动点击「运行」测试是否正常。
|
||||
|
||||
### 3)查看运行日志与产物
|
||||
|
||||
- 在任务详情页可查看实时日志;
|
||||
- 成功后会生成 `checkin.png`(截图)与 `state.json`(状态);
|
||||
- 若失败,会打印原因并根据 `MAX_RETRY` 自动重试。
|
||||
|
||||
## 五、常见问题
|
||||
|
||||
1. **提示账号文件不存在或格式错误**
|
||||
- 确认 `account.txt` 与 `main.py` 在同一目录;
|
||||
- 确认文件编码为 `UTF-8`,且只有两行:第一行用户名,第二行密码;
|
||||
- 不要包含多余空行或空格。
|
||||
|
||||
2. **一直提示登录状态过期**
|
||||
- 删除同目录下旧的 `state.json` 后重跑;
|
||||
- 确认账号密码正确;
|
||||
- 网络/地区问题可能导致访问异常,可稍后再试。
|
||||
|
||||
3. **出现滑块但始终无法通过**
|
||||
- 目标站点的风控策略可能调整;
|
||||
- 可适当增加重试次数(`MAX_RETRY`),或更换执行时间段;
|
||||
- 若长时间失败,建议手动登录一次,让站点信任度恢复。
|
||||
|
||||
4. **成功了但没有截图**
|
||||
- 程序以出现“今天已经签到过啦”的提示为成功判据;
|
||||
- 如果站点文案发生变化,可修改 `ALREADY_SIGNED_TEXT`;
|
||||
- 截图保存路径由 `SIGNED_ANCESTOR_SCREENSHOT` 控制(默认 `checkin.png`)。
|
||||
|
||||
5. **青龙里命令执行不动或报路径错误**
|
||||
- 确认命令中的绝对路径与实际文件路径一致;
|
||||
- 如果你把文件放在其它目录,务必同步修改命令与 `ACCOUNT_FILE` 路径。
|
||||
|
||||
## 六、日志与产物
|
||||
|
||||
- **日志输出**:执行时会输出每一步进度与判定结果,便于排查。
|
||||
- **截图与状态**:
|
||||
- `checkin.png`:成功时的页面关键区域截图;
|
||||
- `state.json`:登录状态缓存(自动生成/刷新);
|
||||
- 若启用内部调试流程,可能在同目录产生若干 `debug_*.png` 用于图像比对,这些文件可忽略或自行清理。
|
||||
|
||||
---
|
||||
|
||||
**至此即可完成配置与使用。**
|
||||
如需嵌入到你现有的青龙仓库,只要保证 `main.py` 与 `account.txt` 的相对位置一致,并按上文的命令与定时规则召唤即可。祝签到愉快 🎯
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 4.1 KiB |
@@ -0,0 +1,346 @@
|
||||
from playwright.sync_api import sync_playwright
|
||||
from pathlib import Path
|
||||
import random
|
||||
import time
|
||||
import cv2
|
||||
|
||||
def get_gap_offset(bg_path, fullbg_path, debug=False):
|
||||
"""比较有缺口背景图和完整背景图,找到缺口 x 坐标(取前两个最大轮廓,选x更大者)"""
|
||||
# 1. 灰度读取
|
||||
bg_img = cv2.imread(bg_path, 0)
|
||||
fullbg_img = cv2.imread(fullbg_path, 0)
|
||||
if debug:
|
||||
cv2.imwrite("debug_01_bg_gray.png", bg_img)
|
||||
cv2.imwrite("debug_02_fullbg_gray.png", fullbg_img)
|
||||
|
||||
# 2. 计算绝对差
|
||||
diff = cv2.absdiff(fullbg_img, bg_img)
|
||||
if debug:
|
||||
cv2.imwrite("debug_03_diff.png", diff)
|
||||
|
||||
# 3. 二值化
|
||||
_, thresh = cv2.threshold(diff, 25, 255, cv2.THRESH_BINARY)
|
||||
if debug:
|
||||
cv2.imwrite("debug_04_thresh.png", thresh)
|
||||
|
||||
# 4. 找轮廓
|
||||
contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
||||
|
||||
if len(contours) < 2:
|
||||
raise ValueError("[ERROR] 找到的轮廓少于两个,可能识别失败")
|
||||
|
||||
# 5. 按面积排序,取前两个最大的
|
||||
contours_sorted = sorted(contours, key=cv2.contourArea, reverse=True)[:2]
|
||||
|
||||
# 6. 获取它们的 bounding box
|
||||
rects = [cv2.boundingRect(c) for c in contours_sorted]
|
||||
|
||||
# 7. 选择 x 坐标更大的那个
|
||||
target_rect = max(rects, key=lambda r: r[0])
|
||||
x, y, w, h = target_rect
|
||||
|
||||
# 8. 调试输出
|
||||
if debug:
|
||||
debug_img = cv2.cvtColor(bg_img, cv2.COLOR_GRAY2BGR)
|
||||
for rect in rects:
|
||||
cx, cy, cw, ch = rect
|
||||
cv2.rectangle(debug_img, (cx, cy), (cx+cw, cy+ch), (0,255,0), 2)
|
||||
cv2.rectangle(debug_img, (x, y), (x+w, y+h), (0,0,255), 2)
|
||||
cv2.imwrite("debug_05_with_box.png", debug_img)
|
||||
|
||||
return x
|
||||
|
||||
# ========= 配置 =========
|
||||
domain = "www.natfrp.com"
|
||||
target_url = f"https://{domain}/user/"
|
||||
ACCOUNT_FILE = Path("account.txt") # 第一行用户名,第二行密码
|
||||
STATE_FILE = "state.json"
|
||||
MAX_RETRY = 5 # 0 表示无限重试,>0 表示最大重试次数
|
||||
|
||||
# --- 新增/修改 ---
|
||||
ALREADY_SIGNED_TEXT = "今天已经签到过啦" # 用于判定成功的文案(包含即可)
|
||||
SIGNED_ANCESTOR_LEVELS = 3 # 向上取第 3 层父级
|
||||
SIGNED_ANCESTOR_SCREENSHOT = "checkin.png" # 成功时保存截图的文件名
|
||||
# ========================
|
||||
|
||||
# ---------------- 读取账号密码 ----------------
|
||||
def load_username_password(path: Path):
|
||||
if not path.exists():
|
||||
raise FileNotFoundError(f"{path} 文件不存在")
|
||||
lines = [l.strip() for l in path.read_text(encoding="utf-8").strip().splitlines() if l.strip()]
|
||||
if len(lines) < 2:
|
||||
raise ValueError("account.txt 必须至少有两行:用户名、密码")
|
||||
return lines[0], lines[1]
|
||||
|
||||
# ---------------- 检测函数 ----------------
|
||||
def is_cookie_expired(page, timeout=6000):
|
||||
try:
|
||||
# 页面出现“Nyatwork 登录”说明需要重新登录
|
||||
page.wait_for_selector("text=Nyatwork 登录", timeout=timeout)
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
def confirm_age_if_needed(page, timeout=6000):
|
||||
try:
|
||||
page.wait_for_selector('text=是,我已满18岁', timeout=timeout)
|
||||
page.click('text=是,我已满18岁')
|
||||
print("[INFO] 已点击满18岁确认按钮")
|
||||
except:
|
||||
print("[INFO] 无需进行年龄确认")
|
||||
|
||||
def is_logged_in(page, timeout=6000):
|
||||
try:
|
||||
page.wait_for_selector("text=账号信息", timeout=timeout)
|
||||
print("[INFO] 检测到已登录状态")
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
def is_sign_button_visible(page, timeout=6000):
|
||||
try:
|
||||
page.wait_for_selector("text=点击这里签到", timeout=timeout)
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
def is_captcha_visible(page, timeout=6000):
|
||||
try:
|
||||
page.wait_for_selector(".geetest_slider_button", timeout=timeout)
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
def wait_captcha_disappear(page, timeout=10000):
|
||||
try:
|
||||
page.wait_for_selector("text=签到成功", timeout=timeout)
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
# --- 新增/修改:查找“今天已经签到过啦”的元素(包含匹配),并取其第 N 层父级 ---
|
||||
def find_signed_text_locator(page, timeout=6000):
|
||||
"""
|
||||
返回包含“今天已经签到过啦”的元素 locator(取第一个可见)。
|
||||
使用 normalize-space 规避前后空格影响,并做“包含”匹配。
|
||||
"""
|
||||
selector = "text=%s" % ALREADY_SIGNED_TEXT
|
||||
try:
|
||||
page.wait_for_selector(selector, timeout=timeout)
|
||||
return page.locator(selector).first
|
||||
except Exception as e:
|
||||
return None
|
||||
|
||||
def screenshot_signed_text_ancestor(page, levels: int = SIGNED_ANCESTOR_LEVELS,
|
||||
path: str = SIGNED_ANCESTOR_SCREENSHOT,
|
||||
timeout: int = 6000) -> bool:
|
||||
"""
|
||||
截图“包含『今天已经签到过啦』的元素”的第 levels 层父级。
|
||||
若未找到该元素或父级,兜底整页截图。返回 True 表示截到了祖先元素,False 表示做了兜底。
|
||||
"""
|
||||
base = find_signed_text_locator(page, timeout=timeout)
|
||||
if base is None:
|
||||
# 没找到文本元素,整页兜底
|
||||
try:
|
||||
page.screenshot(path=path, full_page=True)
|
||||
print(f"[WARN] 未找到包含『{ALREADY_SIGNED_TEXT}』的元素,已兜底整页截图:{path}")
|
||||
except Exception as e:
|
||||
print(f"[ERROR] 兜底整页截图失败:{e}")
|
||||
return False
|
||||
return False
|
||||
|
||||
try:
|
||||
# 取第 N 层父级
|
||||
ancestor = base.locator(f"xpath=ancestor::*[{levels}]").first
|
||||
# 有些页面结构不够深,尝试逐级回退
|
||||
for lvl in range(levels, 0, -1):
|
||||
candidate = base.locator(f"xpath=ancestor::*[{lvl}]").first
|
||||
try:
|
||||
candidate.wait_for(state="visible", timeout=1200)
|
||||
try:
|
||||
candidate.scroll_into_view_if_needed()
|
||||
except:
|
||||
pass
|
||||
candidate.screenshot(path=path)
|
||||
print(f"[INFO] 已保存签到截图:{path}")
|
||||
return True
|
||||
except:
|
||||
continue
|
||||
|
||||
# 如果所有层级都没成功,整页兜底
|
||||
page.screenshot(path=path, full_page=True)
|
||||
print(f"[WARN] 祖先节点截图未成功,已兜底整页截图:{path}")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"[ERROR] 祖先截图异常:{e}")
|
||||
try:
|
||||
page.screenshot(path=path, full_page=True)
|
||||
print(f"[WARN] 异常时已兜底整页截图:{path}")
|
||||
except:
|
||||
return False
|
||||
return False
|
||||
|
||||
# ---------------- 操作函数 ----------------
|
||||
def login_with_user_pass(page, username, password):
|
||||
page.fill("#username", username)
|
||||
page.fill("#password", password)
|
||||
page.click("button[id=login]")
|
||||
print("[INFO] 已提交登录表单")
|
||||
|
||||
def click_sign_in_button(page):
|
||||
page.click("text=点击这里签到")
|
||||
print("[INFO] 已点击签到按钮")
|
||||
|
||||
def drag_slider_fixed_steps(page, slider_element, distance, button_type="middle", debug_pause=False):
|
||||
box = slider_element.bounding_box()
|
||||
start_x = int(box['x'])
|
||||
start_y = int(box['y'] + box['height'] / 2)
|
||||
|
||||
print(f"[INFO] 滑块初始坐标: ({start_x}, {start_y})")
|
||||
print(f"[INFO] 目标滑动距离: {distance} 像素")
|
||||
|
||||
page.mouse.move(start_x, start_y)
|
||||
page.mouse.down(button=button_type)
|
||||
page.wait_for_timeout(300)
|
||||
|
||||
page.mouse.move(start_x + distance + random.randint(2, 8), start_y)
|
||||
page.wait_for_timeout(500)
|
||||
|
||||
page.mouse.move(start_x + distance - 2, start_y)
|
||||
page.mouse.move(start_x + distance - 6, start_y)
|
||||
page.wait_for_timeout(400)
|
||||
page.mouse.move(start_x + distance - 8, start_y)
|
||||
|
||||
page.mouse.up(button=button_type)
|
||||
print("[INFO] 滑动完成")
|
||||
|
||||
if debug_pause:
|
||||
page.pause()
|
||||
|
||||
def solve_geetest_puzzle(page):
|
||||
bg_path = "bg.png"
|
||||
fullbg_path = "fullbg.png"
|
||||
page.query_selector('.geetest_canvas_bg').screenshot(path=bg_path)
|
||||
page.evaluate("""document.querySelector('.geetest_canvas_fullbg').style.display = 'block';""")
|
||||
page.query_selector('.geetest_canvas_fullbg').screenshot(path=fullbg_path)
|
||||
page.evaluate("""document.querySelector('.geetest_canvas_fullbg').style.display = 'none';""")
|
||||
|
||||
gap_x = get_gap_offset(bg_path, fullbg_path, debug=True)
|
||||
print(f"[INFO] 缺口坐标: {gap_x}")
|
||||
|
||||
slider = page.query_selector('.geetest_slider_button')
|
||||
distance = gap_x
|
||||
|
||||
drag_slider_fixed_steps(page, slider, distance, button_type="left", debug_pause=False)
|
||||
print("[INFO] 验证码滑动完成")
|
||||
|
||||
# --- 新增/修改:以“今天已经签到过啦”判定成功 ---
|
||||
def wait_signed_text_and_shoot(page, timeout=8000) -> bool:
|
||||
"""
|
||||
等待出现包含『今天已经签到过啦』的元素;出现则截图其第 3 层父级并返回 True。
|
||||
若未出现则返回 False。
|
||||
"""
|
||||
loc = find_signed_text_locator(page, timeout=timeout)
|
||||
if loc is None:
|
||||
return False
|
||||
# 找到了就截图祖先
|
||||
screenshot_signed_text_ancestor(page, SIGNED_ANCESTOR_LEVELS, SIGNED_ANCESTOR_SCREENSHOT)
|
||||
return True
|
||||
|
||||
# ---------------- 主逻辑 ----------------
|
||||
def main() -> bool:
|
||||
"""
|
||||
主流程
|
||||
:return: True 表示成功,False 表示失败需要重试
|
||||
"""
|
||||
username, password = load_username_password(ACCOUNT_FILE)
|
||||
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(headless=True, slow_mo=200)
|
||||
|
||||
# 先尝试从 state.json 加载状态(cookie/session)
|
||||
try:
|
||||
context = browser.new_context(storage_state=STATE_FILE)
|
||||
print("[INFO] 已加载保存的状态文件")
|
||||
except:
|
||||
print("[WARN] 未找到状态文件,将新建上下文")
|
||||
context = browser.new_context()
|
||||
|
||||
page = context.new_page()
|
||||
page.goto(target_url, timeout=8000)
|
||||
|
||||
# 登录后再检查 18 岁确认(若页面已处于登录态也会直接检测)
|
||||
if is_logged_in(page):
|
||||
confirm_age_if_needed(page)
|
||||
else:
|
||||
# 检查状态文件里的 cookie 是否失效
|
||||
if is_cookie_expired(page):
|
||||
print("[WARN] Cookie 已过期,使用账号密码登录")
|
||||
login_with_user_pass(page, username, password)
|
||||
if is_logged_in(page):
|
||||
print("[INFO] 登录成功,更新状态文件")
|
||||
context.storage_state(path=STATE_FILE)
|
||||
# 登录成功后再检查一次 18 岁确认
|
||||
confirm_age_if_needed(page)
|
||||
else:
|
||||
print("[ERROR] 登录失败")
|
||||
browser.close()
|
||||
return False
|
||||
|
||||
# --- 新增/修改:页面若已出现“今天已经签到过啦”,直接判定成功并截图祖先 ---
|
||||
if wait_signed_text_and_shoot(page, timeout=2000):
|
||||
print("[INFO] 已经签到过啦(页面已有提示)")
|
||||
browser.close()
|
||||
return True
|
||||
|
||||
# 正常签到流程
|
||||
if is_sign_button_visible(page):
|
||||
click_sign_in_button(page)
|
||||
if is_captcha_visible(page):
|
||||
solve_geetest_puzzle(page)
|
||||
# 成功判定:等待出现“今天已经签到过啦”
|
||||
if wait_signed_text_and_shoot(page, timeout=10000):
|
||||
print("[INFO] 签到成功(出现『今天已经签到过啦』)")
|
||||
browser.close()
|
||||
return True
|
||||
else:
|
||||
print("[ERROR] 签到失败:未出现『今天已经签到过啦』提示")
|
||||
browser.close()
|
||||
return False
|
||||
else:
|
||||
# 无验证码路径:同样等待出现成功提示
|
||||
if wait_signed_text_and_shoot(page, timeout=8000):
|
||||
print("[INFO] 签到成功(无需验证码)")
|
||||
browser.close()
|
||||
return True
|
||||
else:
|
||||
print("[ERROR] 签到失败:未出现『今天已经签到过啦』提示(无需验证码路径)")
|
||||
browser.close()
|
||||
return False
|
||||
else:
|
||||
# 不再以“找不到按钮”作为成功判据;此分支仅作为提示与失败返回
|
||||
print("[ERROR] 未发现签到按钮,且未检测到『今天已经签到过啦』提示")
|
||||
browser.close()
|
||||
return False
|
||||
|
||||
# ---------------- 带重试启动 ----------------
|
||||
if __name__ == "__main__":
|
||||
attempt = 0
|
||||
while True:
|
||||
attempt += 1
|
||||
print(f"[INFO] 第 {attempt} 次执行签到")
|
||||
try:
|
||||
success = main()
|
||||
except Exception as e:
|
||||
print(f"[ERROR] 程序异常: {e}")
|
||||
success = False
|
||||
|
||||
if success:
|
||||
print("[INFO] 本次执行成功,退出程序")
|
||||
break
|
||||
else:
|
||||
if MAX_RETRY > 0 and attempt >= MAX_RETRY:
|
||||
print("[ERROR] 超过最大重试次数,退出程序")
|
||||
break
|
||||
print("[WARN] 本次失败,10秒后重试")
|
||||
time.sleep(10)
|
||||
Reference in New Issue
Block a user