彻底优化项目结构,使用 web
This commit is contained in:
+17
-4
@@ -1,4 +1,17 @@
|
|||||||
.venv
|
.venv/
|
||||||
*/__pycache__
|
__pycache__/
|
||||||
/__pycache__
|
*.pyc
|
||||||
data/gui_state.json
|
|
||||||
|
# 运行时数据
|
||||||
|
data/
|
||||||
|
logs/
|
||||||
|
*.db
|
||||||
|
|
||||||
|
# geetest 临时图片
|
||||||
|
bg.jpg
|
||||||
|
fullbg.jpg
|
||||||
|
slice.jpg
|
||||||
|
|
||||||
|
# 前端
|
||||||
|
web/frontend/node_modules/
|
||||||
|
web/frontend/dist/
|
||||||
|
|||||||
@@ -1,144 +1,84 @@
|
|||||||
# 斗鱼自动登录工具 - Python版
|
# 斗鱼批量登录 Web 后台
|
||||||
|
|
||||||
全自动登录斗鱼账号,获取Cookie。
|
全自动登录斗鱼账号、获取 Cookie 的 Web 管理平台。
|
||||||
|
|
||||||
## 功能特性
|
## 功能特性
|
||||||
|
|
||||||
- ✅ 自动过极验v3滑块验证
|
- 极验 v3 滑块自动识别
|
||||||
- ✅ 自动获取邮箱验证码(IMAP)
|
- 邮箱验证码自动获取(IMAP)
|
||||||
- ✅ 批量账号登录
|
- 批量登录 + 实时日志推送(WebSocket)
|
||||||
- ✅ Cookie持久化存储
|
- 代理 & 白名单自动管理
|
||||||
- ✅ 代理支持
|
- 角色权限控制(超级管理员 / 运营 / 客服)
|
||||||
- ✅ GUI导入账号、自动保存配置、代理测试
|
|
||||||
|
|
||||||
## 项目结构
|
## 项目结构
|
||||||
|
|
||||||
```
|
```
|
||||||
douyu_login_py/
|
douyu_login_py/
|
||||||
├── main.py # 主入口
|
├── core/ # 核心业务逻辑
|
||||||
├── config.yaml # 配置文件
|
│ ├── models.py # Account, ProxyConfig 数据类
|
||||||
├── pyproject.toml # uv项目配置
|
│ ├── douyu/ # 斗鱼登录模块
|
||||||
├── douyu/ # 斗鱼登录模块
|
│ │ ├── login.py # 登录流程
|
||||||
│ ├── config.py # 配置管理
|
│ │ ├── crypto.py # 密码加密
|
||||||
│ ├── crypto.py # 加密工具
|
│ │ ├── email_verifier.py # 邮箱验证码
|
||||||
│ ├── email_verifier.py # 邮箱验证
|
│ │ ├── proxy.py # 代理管理
|
||||||
│ └── login.py # 登录核心
|
│ │ └── whitelist.py # 白名单管理
|
||||||
├── geetest/ # 极验滑块破解(复用geetest-v3-silde-crack)
|
│ └── geetest/ # 极验验证码求解
|
||||||
│ ├── solver.py
|
│ ├── common/ # 公共工具(网络/加密/图像/轨迹)
|
||||||
│ ├── network.py
|
│ └── v3_slide/ # v3 滑块求解器
|
||||||
│ ├── crypto.py
|
├── web/ # Web 后台
|
||||||
│ ├── imaging.py
|
│ ├── backend/ # FastAPI 后端
|
||||||
│ └── trajectory.py
|
│ │ ├── main.py # 入口
|
||||||
├── utils/ # 工具模块
|
│ │ ├── routers/ # API 路由
|
||||||
│ ├── logger.py
|
│ │ └── services/ # 业务服务
|
||||||
│ └── helpers.py
|
│ └── frontend/ # React + Ant Design 前端
|
||||||
└── data/
|
├── utils/ # 通用工具
|
||||||
├── accounts.json # 账号配置
|
│ └── logger.py
|
||||||
└── cookies/ # Cookie存储
|
├── data/ # 运行时数据(gitignore)
|
||||||
|
├── start_web.sh # 一键启动
|
||||||
|
└── pyproject.toml
|
||||||
```
|
```
|
||||||
|
|
||||||
## 安装依赖
|
## 快速开始
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uv sync
|
# 一键启动(自动检查依赖)
|
||||||
|
./start_web.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
## GUI使用
|
或手动启动:
|
||||||
|
|
||||||
GUI不再依赖 `config.yaml` 启动,账号数据在界面里导入,代理、极验重试、日志等级和账号列表会自动保存到 `data/gui_state.json`。
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uv run python main.py --gui
|
# 后端
|
||||||
|
uv pip install -e .
|
||||||
|
.venv/bin/python -m uvicorn web.backend.main:app --reload --port 8000
|
||||||
|
|
||||||
|
# 前端
|
||||||
|
cd web/frontend && npm install && npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
支持导入格式:
|
访问 `http://localhost:5173`,默认账号 `admin / admin123`。
|
||||||
|
|
||||||
```text
|
## 角色权限
|
||||||
用户名|密码|邮箱|邮箱密码
|
|
||||||
```
|
|
||||||
|
|
||||||
也可以导入 `txt`、`csv`、`json`、`yaml` 文件。旧版 `config.yaml` 可直接在 GUI 中作为 YAML 文件导入,用来迁移账号数据。
|
| 角色 | 权限 |
|
||||||
|
|---|---|
|
||||||
代理区域支持 API 获取和静态代理,填写后可以点击“测试代理”验证出口连通性。开始批量登录时也会先进行代理预检,只有拿到可用出口后才会继续登录流程。
|
| 超级管理员 | 全部功能 + 用户管理 + 权限分配 |
|
||||||
|
| 运营 | 账号管理 + 批量登录 + 代理配置 + Cookie 导出 |
|
||||||
## 命令行配置
|
| 客服 | 仅查看分配给自己的账号(只显示用户名) |
|
||||||
|
|
||||||
命令行模式仍使用 `config.yaml`:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
accounts:
|
|
||||||
- username: "your_username"
|
|
||||||
password: "your_password"
|
|
||||||
email: "your_email@bdhg.xyz"
|
|
||||||
email_password: "your_email_password"
|
|
||||||
email_imap_server: "mail.bdhg.xyz"
|
|
||||||
email_imap_port: 993
|
|
||||||
```
|
|
||||||
|
|
||||||
## 使用方法
|
|
||||||
|
|
||||||
### 单账号登录
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 登录第一个账号
|
|
||||||
uv run python main.py
|
|
||||||
|
|
||||||
# 登录指定索引的账号
|
|
||||||
uv run python main.py -i 0
|
|
||||||
```
|
|
||||||
|
|
||||||
### 批量登录
|
|
||||||
|
|
||||||
```bash
|
|
||||||
uv run python main.py --batch
|
|
||||||
```
|
|
||||||
|
|
||||||
### 详细日志
|
|
||||||
|
|
||||||
```bash
|
|
||||||
uv run python main.py -v
|
|
||||||
```
|
|
||||||
|
|
||||||
## 登录流程
|
## 登录流程
|
||||||
|
|
||||||
```
|
```
|
||||||
1️⃣ 第一次登录 → 获取极验参数
|
1. 获取极验参数 → 滑块验证 → 自动识别缺口
|
||||||
2️⃣ 极验滑块验证 → 自动识别缺口位置
|
2. 获取邮箱验证 code → 发送验证邮件
|
||||||
3️⃣ 第二次登录 → 获取邮箱验证code
|
3. IMAP 获取验证码 → 提交验证码 → 获取 Cookie
|
||||||
4️⃣ 发送邮箱验证邮件
|
|
||||||
5️⃣ IMAP获取验证码
|
|
||||||
6️⃣ 提交验证码
|
|
||||||
7️⃣ 完成登录获取Cookie
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Cookie使用
|
## 技术栈
|
||||||
|
|
||||||
登录成功后,Cookie会保存到 `data/cookies/` 目录。
|
- 后端:FastAPI + SQLAlchemy + SQLite + JWT
|
||||||
|
- 前端:React + Ant Design + Vite
|
||||||
可以使用以下方式读取Cookie:
|
- 核心:Python + OpenCV + pycryptodome
|
||||||
|
|
||||||
```python
|
|
||||||
from utils.helpers import load_cookie
|
|
||||||
|
|
||||||
cookie = load_cookie("your_username")
|
|
||||||
print(cookie)
|
|
||||||
```
|
|
||||||
|
|
||||||
## 极验滑块破解
|
|
||||||
|
|
||||||
本项目复用了 `geetest-v3-silde-crack` 的滑块破解方案,包含:
|
|
||||||
|
|
||||||
- RSA/AES加密
|
|
||||||
- 轨迹生成
|
|
||||||
- 图像识别(OpenCV)
|
|
||||||
- 性能数据伪造
|
|
||||||
|
|
||||||
## 注意事项
|
|
||||||
|
|
||||||
1. 请确保邮箱支持IMAP协议
|
|
||||||
2. 邮箱需要开启IMAP访问权限
|
|
||||||
3. 部分邮箱需要使用应用专用密码
|
|
||||||
4. 建议使用代理避免IP限制
|
|
||||||
|
|
||||||
## 许可证
|
## 许可证
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -1,14 +0,0 @@
|
|||||||
软软马卡龙(1)
|
|
||||||
|
|
||||||
cvl_csrf_token=62083f188904477e9bc1c5a70537145c; acf_ccn=8b025ffcbf403d5e0d880ad6327bb3cf; PHPSESSID=86k3denvoc1j16rl1fsv5l2mc2; acf_auth=5a8eUzBoDc8uyLasCP7LXaO9a2vUK%2FDaNFwYi0MVdUFMpUcMBfsoZfifJZKCQC8WgJuKq7WB78VZrRlRXnZUSt87TZM0tLNdnplqtuL87c7idAqo%2Fal57ls; acf_jwt_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJtZDUifQ.eyJ1aWQiOjk0MjQxNjQ1MSwiY3QiOjAsInN1YiI6InN0IiwiYXVkIjpbImR5Iiwibm9uZSJdLCJiaXoiOjEsImx0a2lkIjo2OTE2MTY4NSwic3RrIjoiNGZkNDY5ZDE2OWY3NzM0YiIsImV4cCI6MTc4MjMyMjc2OSwiaWF0IjoxNzgxNzE3OTcwLCJrZXkiOiJkeS1qd3QtbWQ1In0.NDc0YzMyNjQ0OGIzNTkxYzkxNGQ1NDk0Y2U0NTk1NTU; acf_dmjwt_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJtZDUifQ.eyJ1aWQiOjk0MjQxNjQ1MSwiY3QiOjAsInN1YiI6InN0IiwiYXVkIjpbImRtIiwibm9uZSJdLCJiaXoiOjEsImx0a2lkIjo2OTE2MTY4NSwic3RrIjoiNGZkNDY5ZDE2OWY3NzM0YiIsImV4cCI6MTc4MjMyMjc2OSwiaWF0IjoxNzgxNzE3OTcwLCJrZXkiOiJkeS1qd3QtbWQ1In0.NWM1ZDgwZjE1N2ZhNTE3NDUxNmQ4ZjljMzI5ODQ0ZjM; dy_auth=29bfEdmmHp50M9Zr2gxQ3%2F2AJD2DAdCK6I%2FoX92U5n13i64R%2B%2FywJZf%2FqG4%2BCJvbUQmFaUSu3dFbJj4%2Be1gU1DpW6ligJYaJLnNuVbmKMeZCtayBmGtns6A; wan_auth37wan=cf514c616a0fzsvhClbvRbju0sSZb4aDqHQQabpd66aLoVky4bvRRXYDfYMX3iSUNiDuh%2BHt%2B20QXW0f5JLZ6lBtOW8LIIwb31iX8OiZurGB0aztSKA; acf_uid=942416451; acf_username=942416451; acf_nickname=%E7%94%A8%E6%88%B74512197651; acf_own_room=0; acf_groupid=1; acf_phonestatus=1; acf_avatar=https%3A%2F%2Fapic.douyucdn.cn%2Fupload%2Favatar%2Fdefault%2F18_; acf_ct=0; acf_ltkid=69161685; acf_biz=1; acf_stk=4fd469d169f7734b; acf_devid=9869cce23d84da0fd8b1cd20162b3def--用户4512197651--
|
|
||||||
|
|
||||||
|
|
||||||
============
|
|
||||||
|
|
||||||
新写的
|
|
||||||
|
|
||||||
dy_accounts_main=; dy_auth=e105BJzhxVWQ0tJKUXiiqN8ky11wgfj7ClW%2F8qo%2B1MPv8%2BDGHBq8VnuaJ1MWnm4EZqgVfLkM6PETWtGpLAHvadcMxCn9HWvTfT8%2BSKZR2dmsvReubJmeqBo; wan_auth37wan=4dc825c71d00LrMvodf%2B7ZrhnyMcsREgwQlgd7Ec06k5%2Bm%2FM0tdv0AJJ8gpoNoinSoPHS3vbJO8iK1vqiXfF2hTOLQqJOW3Wzs19DWLwpJpQgV8J%2BF0; LTP0=eyJhbGciOiJtZDUiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOlsicGFzc3BvcnQiXSwiY3QiOjAsImN0aW1lIjoxNzgyMDU0MDA0LCJleHAiOjE3OTc4MjIwMTYsImtleSI6ImR5LWp3dC1tZDUiLCJsdGsiOiJhZDMxOTliZGRhYmM3ZWRmIiwibHRraWQiOjgwNTYxMzgyLCJzdWIiOiJsdCIsInVpZCI6OTg2Mzg1Mjk2fQ.Y2M0YTQ4YjQwZGYwZWI4YWZiZTllNmVjODNiZmJkMGE; last_login_way=nickname; PHPSESSID=ccr09aa2letju5662gdh5aih62; acf_auth=1a34kGp6rHjazzRy4fVzKXX11qCjC9RuQASDGOEfkRbLjsI3A5PzSCXawo5hZvO4g47LF9ruapNiMpf%2BE3u4TplWPMkW4c4eI0pyG05a%2F9dKr%2BOGM0hAEXk; acf_jwt_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJtZDUifQ.eyJ1aWQiOjk4NjM4NTI5NiwiY3QiOjAsInN1YiI6InN0IiwiYXVkIjpbImR5Iiwibm9uZSJdLCJiaXoiOjEsImx0a2lkIjo4MDU2MTM4Miwic3RrIjoiNjFhODczZjUwZTg3M2Y5MCIsImV4cCI6MTc4MjY1ODgwNCwiaWF0IjoxNzgyMDU0MDA0LCJrZXkiOiJkeS1qd3QtbWQ1In0.OTFlMjIxOGU4MjZkMjcwMWM4NjQ1NTcwNjNmZWIzMDA; acf_dmjwt_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJtZDUifQ.eyJ1aWQiOjk4NjM4NTI5NiwiY3QiOjAsInN1YiI6InN0IiwiYXVkIjpbImRtIiwibm9uZSJdLCJiaXoiOjEsImx0a2lkIjo4MDU2MTM4Miwic3RrIjoiNjFhODczZjUwZTg3M2Y5MCIsImV4cCI6MTc4MjY1ODgwNCwiaWF0IjoxNzgyMDU0MDA0LCJrZXkiOiJkeS1qd3QtbWQ1In0.N2E0NmEyOWVlZDE1NTE4NDg0MTFlMmM5OGY1ZGQzZTk; acf_uid=986385296; acf_username=986385296; acf_nickname=%E7%94%A8%E6%88%B79006787894; acf_own_room=0; acf_groupid=1; acf_phonestatus=1; acf_avatar=https%3A%2F%2Fapic.douyucdn.cn%2Fupload%2Favatar%2Fdefault%2F25_; acf_ct=0; acf_ltkid=80561382; acf_biz=1; acf_stk=61a873f50e873f90; acf_devid=d53498d383f449f4c4ea28c4165aa2b4",
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-32
@@ -1,32 +0,0 @@
|
|||||||
# 斗鱼自动登录配置
|
|
||||||
|
|
||||||
# 账号配置
|
|
||||||
accounts:
|
|
||||||
- username: "用户9006787894"
|
|
||||||
password: "a778899"
|
|
||||||
email: "jtmjcm@bdhg.xyz"
|
|
||||||
email_password: "www123"
|
|
||||||
email_imap_server: "mail.bdhg.xyz"
|
|
||||||
email_imap_port: 993
|
|
||||||
|
|
||||||
# 代理配置
|
|
||||||
proxy:
|
|
||||||
enabled: true
|
|
||||||
# 代理API地址(自动获取代理IP)
|
|
||||||
api_url: "http://api.xiequ.cn/VAD/GetIp.aspx?act=get&uid=106015&vkey=97111DB5379E38E3BC2FF09A1B00A0C7&num=1&time=30&plat=1&re=0&type=0&so=1&ow=1&spl=1&addr=&db=1"
|
|
||||||
# 静态代理(如果不使用API获取)
|
|
||||||
http: ""
|
|
||||||
https: ""
|
|
||||||
|
|
||||||
# 极验配置
|
|
||||||
geetest:
|
|
||||||
# 最大重试次数(遇到点选验证码时重试)
|
|
||||||
max_retries: 10
|
|
||||||
|
|
||||||
# 日志配置
|
|
||||||
log:
|
|
||||||
level: "DEBUG"
|
|
||||||
file: "logs/douyu_login.log"
|
|
||||||
|
|
||||||
# Cookie存储
|
|
||||||
cookie_dir: "data/cookies"
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
"""核心业务逻辑"""
|
||||||
|
|
||||||
|
from .models import Account, ProxyConfig
|
||||||
|
|
||||||
|
__all__ = ["Account", "ProxyConfig"]
|
||||||
@@ -2,8 +2,7 @@
|
|||||||
|
|
||||||
from .login import DouyuLogin
|
from .login import DouyuLogin
|
||||||
from .email_verifier import EmailVerifier
|
from .email_verifier import EmailVerifier
|
||||||
from .config import Config
|
|
||||||
from .proxy import ProxyManager
|
from .proxy import ProxyManager
|
||||||
from .whitelist import WhitelistManager
|
from .whitelist import WhitelistManager
|
||||||
|
|
||||||
__all__ = ["DouyuLogin", "EmailVerifier", "Config", "ProxyManager", "WhitelistManager"]
|
__all__ = ["DouyuLogin", "EmailVerifier", "ProxyManager", "WhitelistManager"]
|
||||||
@@ -5,23 +5,19 @@ import json
|
|||||||
import time
|
import time
|
||||||
import requests
|
import requests
|
||||||
from typing import Mapping, Optional, Tuple
|
from typing import Mapping, Optional, Tuple
|
||||||
from pathlib import Path
|
|
||||||
from urllib.parse import urlsplit, urlunsplit
|
from urllib.parse import urlsplit, urlunsplit
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from .config import Account
|
from core.models import Account
|
||||||
from .crypto import encrypt_password, encrypt_nickname_or_phone
|
from .crypto import encrypt_password, encrypt_nickname_or_phone
|
||||||
from .email_verifier import EmailVerifier
|
from .email_verifier import EmailVerifier
|
||||||
from .proxy import ProxyManager, get_proxy_manager
|
from .proxy import ProxyManager, get_proxy_manager
|
||||||
|
|
||||||
# 导入geetest滑块模块
|
from core.geetest import run_solver
|
||||||
import sys
|
from core.geetest.v3_slide.solver import (
|
||||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
||||||
from geetest import run_solver
|
|
||||||
from geetest.solver import (
|
|
||||||
_generate_seed, get_w1, get_w2,
|
_generate_seed, get_w1, get_w2,
|
||||||
)
|
)
|
||||||
from geetest.network import (
|
from core.geetest.common.network import (
|
||||||
get_js_address,
|
get_js_address,
|
||||||
get_c_s,
|
get_c_s,
|
||||||
req_fullpage_validate,
|
req_fullpage_validate,
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
"""Geetest 验证码求解器"""
|
||||||
|
|
||||||
|
from .v3_slide.solver import run_solver
|
||||||
|
|
||||||
|
__all__ = ["run_solver"]
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""Geetest 公共工具模块"""
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Geetest slider captcha solver package."""
|
"""Geetest v3 滑块验证码求解器"""
|
||||||
|
|
||||||
from .solver import run_solver
|
from .solver import run_solver
|
||||||
|
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
import time
|
import time
|
||||||
import random
|
import random
|
||||||
import json
|
import json
|
||||||
from .trajectory import generate_realistic_trajectory, process_mouse_trajectory, compress_trajectory, TrajectoryEncoder, \
|
from core.geetest.common.trajectory import generate_realistic_trajectory, process_mouse_trajectory, compress_trajectory, TrajectoryEncoder, \
|
||||||
H
|
H
|
||||||
from .crypto import four_random_chart, RSA_jiami_r, AES_O, geetest_base64_encode, encrypt_string, simple_md5
|
from core.geetest.common.crypto import four_random_chart, RSA_jiami_r, AES_O, geetest_base64_encode, encrypt_string, simple_md5
|
||||||
from .imaging import download_picture
|
from core.geetest.common.imaging import download_picture
|
||||||
from .network import get_challenge_gt, get_js_address, get_c_s, req_slide, get_picture, req_end
|
from core.geetest.common.network import get_challenge_gt, get_js_address, get_c_s, req_slide, get_picture, req_end
|
||||||
from .performance import generate_fake_performance_timing, get_slide_track
|
from core.geetest.common.performance import generate_fake_performance_timing, get_slide_track
|
||||||
|
|
||||||
|
|
||||||
def _generate_seed() -> str:
|
def _generate_seed() -> str:
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
"""核心数据模型"""
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Account:
|
||||||
|
"""账号配置"""
|
||||||
|
username: str
|
||||||
|
password: str
|
||||||
|
email: str
|
||||||
|
email_password: str
|
||||||
|
email_imap_server: str
|
||||||
|
email_imap_port: int = 993
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ProxyConfig:
|
||||||
|
"""代理配置"""
|
||||||
|
enabled: bool = False
|
||||||
|
api_url: str = ""
|
||||||
|
http: str = ""
|
||||||
|
https: str = ""
|
||||||
|
# 白名单配置
|
||||||
|
whitelist_enabled: bool = False
|
||||||
|
whitelist_uid: str = ""
|
||||||
|
whitelist_ukey: str = ""
|
||||||
BIN
Binary file not shown.
@@ -1,85 +0,0 @@
|
|||||||
"""配置管理模块"""
|
|
||||||
|
|
||||||
import yaml
|
|
||||||
from pathlib import Path
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from typing import List, Optional
|
|
||||||
|
|
||||||
from .email_verifier import get_email_config_for_account
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Account:
|
|
||||||
"""账号配置"""
|
|
||||||
username: str
|
|
||||||
password: str
|
|
||||||
email: str
|
|
||||||
email_password: str
|
|
||||||
email_imap_server: str
|
|
||||||
email_imap_port: int = 993
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ProxyConfig:
|
|
||||||
"""代理配置"""
|
|
||||||
enabled: bool = False
|
|
||||||
api_url: str = ""
|
|
||||||
http: str = ""
|
|
||||||
https: str = ""
|
|
||||||
# 白名单配置
|
|
||||||
whitelist_enabled: bool = False # 是否启用白名单自动管理
|
|
||||||
whitelist_uid: str = "" # 协固用户ID
|
|
||||||
whitelist_ukey: str = "" # 协固用户密钥
|
|
||||||
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
"""配置管理器"""
|
|
||||||
|
|
||||||
def __init__(self, config_path: str = "config.yaml"):
|
|
||||||
self.config_path = Path(config_path)
|
|
||||||
self._config = self._load_config()
|
|
||||||
|
|
||||||
def _load_config(self) -> dict:
|
|
||||||
"""加载配置文件"""
|
|
||||||
if not self.config_path.exists():
|
|
||||||
raise FileNotFoundError(f"配置文件不存在: {self.config_path}")
|
|
||||||
|
|
||||||
with open(self.config_path, 'r', encoding='utf-8') as f:
|
|
||||||
return yaml.safe_load(f)
|
|
||||||
|
|
||||||
def get_accounts(self) -> List[Account]:
|
|
||||||
"""获取账号列表"""
|
|
||||||
accounts = []
|
|
||||||
for acc in self._config.get('accounts', []):
|
|
||||||
email_config = get_email_config_for_account(acc['email'])
|
|
||||||
accounts.append(Account(
|
|
||||||
username=acc['username'],
|
|
||||||
password=acc['password'],
|
|
||||||
email=acc['email'],
|
|
||||||
email_password=acc['email_password'],
|
|
||||||
email_imap_server=acc.get('email_imap_server') or email_config['server'],
|
|
||||||
email_imap_port=acc.get('email_imap_port') or email_config['port'],
|
|
||||||
))
|
|
||||||
return accounts
|
|
||||||
|
|
||||||
def get_proxy(self) -> ProxyConfig:
|
|
||||||
"""获取代理配置"""
|
|
||||||
proxy = self._config.get('proxy', {})
|
|
||||||
return ProxyConfig(
|
|
||||||
enabled=proxy.get('enabled', False),
|
|
||||||
api_url=proxy.get('api_url', ''),
|
|
||||||
http=proxy.get('http', ''),
|
|
||||||
https=proxy.get('https', ''),
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_geetest_config(self) -> dict:
|
|
||||||
"""获取极验配置"""
|
|
||||||
return self._config.get('geetest', {'max_retries': 5})
|
|
||||||
|
|
||||||
def get_cookie_dir(self) -> str:
|
|
||||||
"""获取Cookie存储目录"""
|
|
||||||
return self._config.get('cookie_dir', 'data/cookies')
|
|
||||||
|
|
||||||
def get_log_config(self) -> dict:
|
|
||||||
"""获取日志配置"""
|
|
||||||
return self._config.get('log', {'level': 'INFO', 'file': 'logs/douyu_login.log'})
|
|
||||||
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 6.2 KiB |
@@ -1,13 +0,0 @@
|
|||||||
"""GUI模块"""
|
|
||||||
|
|
||||||
from .app import DouyuLoginApp
|
|
||||||
from .login_worker import parse_accounts_text, get_imap_server
|
|
||||||
from .widgets import AccountTable, LogPanel
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
'DouyuLoginApp',
|
|
||||||
'parse_accounts_text',
|
|
||||||
'get_imap_server',
|
|
||||||
'AccountTable',
|
|
||||||
'LogPanel',
|
|
||||||
]
|
|
||||||
@@ -1,185 +0,0 @@
|
|||||||
"""账号导入工具。"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import csv
|
|
||||||
import io
|
|
||||||
import json
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Any, Iterable, Mapping
|
|
||||||
|
|
||||||
import yaml
|
|
||||||
from loguru import logger
|
|
||||||
|
|
||||||
from douyu.config import Account
|
|
||||||
from douyu.email_verifier import get_email_config_for_account
|
|
||||||
from .login_worker import EMAIL_PATTERN, _is_ascii, parse_accounts_text
|
|
||||||
|
|
||||||
|
|
||||||
FIELD_ALIASES = {
|
|
||||||
"username": {"username", "user", "account", "账号", "用户名", "斗鱼账号"},
|
|
||||||
"password": {"password", "pass", "pwd", "密码", "登录密码"},
|
|
||||||
"email": {"email", "mail", "邮箱", "邮箱地址"},
|
|
||||||
"email_password": {
|
|
||||||
"email_password",
|
|
||||||
"email_pass",
|
|
||||||
"email_pwd",
|
|
||||||
"mail_password",
|
|
||||||
"mail_pass",
|
|
||||||
"邮箱密码",
|
|
||||||
"邮箱授权码",
|
|
||||||
"授权码",
|
|
||||||
},
|
|
||||||
"email_imap_server": {"email_imap_server", "imap_server", "imap", "imap服务器"},
|
|
||||||
"email_imap_port": {"email_imap_port", "imap_port", "imap端口"},
|
|
||||||
}
|
|
||||||
|
|
||||||
NORMALIZED_ALIASES = {
|
|
||||||
"".join(alias.lower().replace("-", "_").split()): field
|
|
||||||
for field, aliases in FIELD_ALIASES.items()
|
|
||||||
for alias in aliases
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def load_accounts_from_file(filepath: str | Path) -> list[Account]:
|
|
||||||
"""从文件导入账号,支持txt/csv/json/yaml。"""
|
|
||||||
path = Path(filepath)
|
|
||||||
text = _read_text(path)
|
|
||||||
suffix = path.suffix.lower()
|
|
||||||
|
|
||||||
if suffix == ".json":
|
|
||||||
return _load_structured_accounts(text, path.name, "json")
|
|
||||||
|
|
||||||
if suffix in {".yaml", ".yml"}:
|
|
||||||
return _load_structured_accounts(text, path.name, "yaml")
|
|
||||||
|
|
||||||
if suffix == ".csv":
|
|
||||||
accounts = _parse_csv_text(text, path.name)
|
|
||||||
return accounts or parse_accounts_text(text)
|
|
||||||
|
|
||||||
return parse_accounts_text(text)
|
|
||||||
|
|
||||||
|
|
||||||
def _read_text(path: Path) -> str:
|
|
||||||
"""按常见编码读取文本文件。"""
|
|
||||||
last_error: Exception | None = None
|
|
||||||
for encoding in ("utf-8-sig", "utf-8", "gb18030"):
|
|
||||||
try:
|
|
||||||
return path.read_text(encoding=encoding)
|
|
||||||
except UnicodeDecodeError as exc:
|
|
||||||
last_error = exc
|
|
||||||
|
|
||||||
if last_error:
|
|
||||||
raise last_error
|
|
||||||
return path.read_text(encoding="utf-8")
|
|
||||||
|
|
||||||
|
|
||||||
def _load_structured_accounts(text: str, source: str, file_type: str) -> list[Account]:
|
|
||||||
"""读取JSON/YAML中的账号列表。"""
|
|
||||||
try:
|
|
||||||
if file_type == "json":
|
|
||||||
data = json.loads(text)
|
|
||||||
else:
|
|
||||||
data = yaml.safe_load(text)
|
|
||||||
except Exception as exc:
|
|
||||||
logger.warning(f"{source} 结构化解析失败,尝试按文本格式导入: {exc}")
|
|
||||||
return parse_accounts_text(text)
|
|
||||||
|
|
||||||
return accounts_from_payload(data, source)
|
|
||||||
|
|
||||||
|
|
||||||
def accounts_from_payload(data: Any, source: str = "导入数据") -> list[Account]:
|
|
||||||
"""从结构化数据中提取账号列表。"""
|
|
||||||
if isinstance(data, Mapping):
|
|
||||||
rows = data.get("accounts") or data.get("data") or data.get("items") or []
|
|
||||||
else:
|
|
||||||
rows = data
|
|
||||||
|
|
||||||
if not isinstance(rows, list):
|
|
||||||
logger.warning(f"{source} 中未找到账号列表")
|
|
||||||
return []
|
|
||||||
|
|
||||||
return accounts_from_rows(rows, source)
|
|
||||||
|
|
||||||
|
|
||||||
def accounts_from_rows(rows: Iterable[Any], source: str = "导入数据") -> list[Account]:
|
|
||||||
"""从字典行列表转换账号。"""
|
|
||||||
accounts: list[Account] = []
|
|
||||||
for row_index, row in enumerate(rows, 1):
|
|
||||||
if not isinstance(row, Mapping):
|
|
||||||
logger.warning(f"{source} 第{row_index}行不是对象,已跳过")
|
|
||||||
continue
|
|
||||||
|
|
||||||
account = _account_from_mapping(row, f"{source} 第{row_index}行")
|
|
||||||
if account:
|
|
||||||
accounts.append(account)
|
|
||||||
|
|
||||||
return accounts
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_csv_text(text: str, source: str) -> list[Account]:
|
|
||||||
"""解析带表头的CSV文件。"""
|
|
||||||
reader = csv.reader(io.StringIO(text))
|
|
||||||
try:
|
|
||||||
first_row = next(reader)
|
|
||||||
except StopIteration:
|
|
||||||
return []
|
|
||||||
|
|
||||||
if not _looks_like_header(first_row):
|
|
||||||
return []
|
|
||||||
|
|
||||||
dict_reader = csv.DictReader(io.StringIO(text))
|
|
||||||
return accounts_from_rows(dict_reader, source)
|
|
||||||
|
|
||||||
|
|
||||||
def _looks_like_header(row: list[str]) -> bool:
|
|
||||||
"""判断CSV首行是否像账号字段表头。"""
|
|
||||||
normalized = {
|
|
||||||
"".join(str(value).lower().replace("-", "_").split())
|
|
||||||
for value in row
|
|
||||||
}
|
|
||||||
return len(normalized & set(NORMALIZED_ALIASES.keys())) >= 2
|
|
||||||
|
|
||||||
|
|
||||||
def _account_from_mapping(row: Mapping[str, Any], source: str) -> Account | None:
|
|
||||||
"""从字典字段构造账号。"""
|
|
||||||
normalized_row: dict[str, str] = {}
|
|
||||||
for key, value in row.items():
|
|
||||||
normalized_key = "".join(str(key).lower().replace("-", "_").split())
|
|
||||||
field = NORMALIZED_ALIASES.get(normalized_key)
|
|
||||||
if field:
|
|
||||||
normalized_row[field] = "" if value is None else str(value).strip()
|
|
||||||
|
|
||||||
username = normalized_row.get("username", "")
|
|
||||||
password = normalized_row.get("password", "")
|
|
||||||
email = normalized_row.get("email", "")
|
|
||||||
email_password = normalized_row.get("email_password", "")
|
|
||||||
|
|
||||||
if not all([username, password, email, email_password]):
|
|
||||||
logger.warning(f"{source} 存在空字段,已跳过")
|
|
||||||
return None
|
|
||||||
|
|
||||||
if not EMAIL_PATTERN.match(email) or not _is_ascii(email):
|
|
||||||
logger.warning(f"{source} 邮箱格式不正确: {email}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
if not _is_ascii(email_password):
|
|
||||||
logger.warning(f"{source} 邮箱密码/授权码包含非ASCII字符,IMAP可能无法登录")
|
|
||||||
return None
|
|
||||||
|
|
||||||
email_config = get_email_config_for_account(email)
|
|
||||||
imap_server = normalized_row.get("email_imap_server") or email_config["server"]
|
|
||||||
try:
|
|
||||||
imap_port = int(normalized_row.get("email_imap_port") or email_config["port"])
|
|
||||||
except ValueError:
|
|
||||||
logger.warning(f"{source} IMAP端口不正确,已使用默认端口")
|
|
||||||
imap_port = email_config["port"]
|
|
||||||
|
|
||||||
return Account(
|
|
||||||
username=username,
|
|
||||||
password=password,
|
|
||||||
email=email,
|
|
||||||
email_password=email_password,
|
|
||||||
email_imap_server=imap_server,
|
|
||||||
email_imap_port=imap_port,
|
|
||||||
)
|
|
||||||
-1507
File diff suppressed because it is too large
Load Diff
@@ -1,203 +0,0 @@
|
|||||||
"""登录工作线程模块"""
|
|
||||||
|
|
||||||
import csv
|
|
||||||
import re
|
|
||||||
import threading
|
|
||||||
import time
|
|
||||||
from queue import Queue
|
|
||||||
from typing import Optional
|
|
||||||
from loguru import logger
|
|
||||||
|
|
||||||
from douyu import DouyuLogin
|
|
||||||
from douyu.config import Account, ProxyConfig
|
|
||||||
from douyu.email_verifier import get_email_config_for_account
|
|
||||||
|
|
||||||
|
|
||||||
EMAIL_PATTERN = re.compile(r'^[^\s@|]+@[^\s@|]+\.[^\s@|]+$')
|
|
||||||
HEADER_NAMES = {
|
|
||||||
'username', 'user', 'account', '账号', '用户名', '斗鱼账号',
|
|
||||||
'password', 'pass', 'pwd', '密码', '登录密码',
|
|
||||||
'email', 'mail', '邮箱', '邮箱地址',
|
|
||||||
'email_password', 'email_pass', 'email_pwd', 'mail_password',
|
|
||||||
'mail_pass', '邮箱密码', '邮箱授权码', '授权码',
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def get_imap_server(email: str) -> str:
|
|
||||||
"""从邮箱地址推导IMAP服务器"""
|
|
||||||
return get_email_config_for_account(email)['server']
|
|
||||||
|
|
||||||
|
|
||||||
def _is_ascii(value: str) -> bool:
|
|
||||||
"""检查字符串是否为ASCII。"""
|
|
||||||
try:
|
|
||||||
value.encode('ascii')
|
|
||||||
return True
|
|
||||||
except UnicodeEncodeError:
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def _split_account_line(line: str) -> list[str]:
|
|
||||||
"""拆分单行账号数据,支持竖线、Tab和CSV逗号。"""
|
|
||||||
if '|' in line:
|
|
||||||
return line.split('|')
|
|
||||||
if '\t' in line:
|
|
||||||
return line.split('\t')
|
|
||||||
if ',' in line:
|
|
||||||
return next(csv.reader([line]))
|
|
||||||
return line.split()
|
|
||||||
|
|
||||||
|
|
||||||
def _looks_like_header(parts: list[str]) -> bool:
|
|
||||||
"""判断一行是否像表头。"""
|
|
||||||
normalized = {
|
|
||||||
part.strip().lower().replace('-', '_')
|
|
||||||
for part in parts
|
|
||||||
if part.strip()
|
|
||||||
}
|
|
||||||
return len(normalized & HEADER_NAMES) >= 2
|
|
||||||
|
|
||||||
|
|
||||||
def parse_accounts_text(text: str) -> list[Account]:
|
|
||||||
"""
|
|
||||||
解析账号文本
|
|
||||||
|
|
||||||
格式:用户名|密码|邮箱|邮箱密码
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Account列表
|
|
||||||
"""
|
|
||||||
accounts = []
|
|
||||||
|
|
||||||
for line_num, line in enumerate(text.strip().split('\n'), 1):
|
|
||||||
line = line.strip()
|
|
||||||
if not line or line.startswith('#'):
|
|
||||||
continue
|
|
||||||
|
|
||||||
parts = _split_account_line(line)
|
|
||||||
if line_num == 1 and _looks_like_header(parts):
|
|
||||||
continue
|
|
||||||
|
|
||||||
if len(parts) != 4:
|
|
||||||
logger.warning(f"第{line_num}行格式错误,需要4个字段,实际{len(parts)}个: {line}")
|
|
||||||
continue
|
|
||||||
|
|
||||||
username, password, email, email_password = parts[0].strip(), parts[1].strip(), parts[2].strip(), parts[3].strip()
|
|
||||||
|
|
||||||
if not all([username, password, email, email_password]):
|
|
||||||
logger.warning(f"第{line_num}行存在空字段: {line}")
|
|
||||||
continue
|
|
||||||
|
|
||||||
if not EMAIL_PATTERN.match(email) or not _is_ascii(email):
|
|
||||||
logger.warning(
|
|
||||||
f"第{line_num}行邮箱格式不正确: {email},"
|
|
||||||
"请确认格式为:用户名|密码|邮箱|邮箱密码"
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
|
|
||||||
if not _is_ascii(email_password):
|
|
||||||
logger.warning(f"第{line_num}行邮箱密码/授权码包含非ASCII字符,IMAP可能无法登录")
|
|
||||||
continue
|
|
||||||
|
|
||||||
accounts.append(Account(
|
|
||||||
username=username,
|
|
||||||
password=password,
|
|
||||||
email=email,
|
|
||||||
email_password=email_password,
|
|
||||||
email_imap_server=get_imap_server(email),
|
|
||||||
email_imap_port=993,
|
|
||||||
))
|
|
||||||
|
|
||||||
return accounts
|
|
||||||
|
|
||||||
|
|
||||||
class LoginWorker(threading.Thread):
|
|
||||||
"""登录工作线程"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
account: Account,
|
|
||||||
index: int,
|
|
||||||
result_queue: Queue,
|
|
||||||
log_queue: Queue,
|
|
||||||
proxy_config: Optional[ProxyConfig] = None,
|
|
||||||
max_geetest_retries: int = 5,
|
|
||||||
):
|
|
||||||
super().__init__(daemon=True)
|
|
||||||
self.account = account
|
|
||||||
self.index = index
|
|
||||||
self.result_queue = result_queue
|
|
||||||
self.log_queue = log_queue
|
|
||||||
self.proxy_config = proxy_config
|
|
||||||
self.max_geetest_retries = max_geetest_retries
|
|
||||||
self._stop_event = threading.Event()
|
|
||||||
|
|
||||||
def stop(self):
|
|
||||||
"""停止线程"""
|
|
||||||
self._stop_event.set()
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
"""执行登录"""
|
|
||||||
username = self.account.username
|
|
||||||
|
|
||||||
try:
|
|
||||||
# 发送日志
|
|
||||||
self.log_queue.put(('info', f'[{self.index + 1}] 开始登录: {username}'))
|
|
||||||
|
|
||||||
# 配置代理
|
|
||||||
proxy_url = None
|
|
||||||
proxy_api_url = None
|
|
||||||
|
|
||||||
if self.proxy_config and self.proxy_config.enabled:
|
|
||||||
if self.proxy_config.http or self.proxy_config.https:
|
|
||||||
# 优先使用GUI预检通过的代理,避免进入登录流程后再直接获取未验证代理。
|
|
||||||
proxy_url = {
|
|
||||||
'http': self.proxy_config.http or self.proxy_config.https,
|
|
||||||
'https': self.proxy_config.https or self.proxy_config.http,
|
|
||||||
}
|
|
||||||
elif self.proxy_config.api_url:
|
|
||||||
proxy_api_url = self.proxy_config.api_url
|
|
||||||
|
|
||||||
# 创建登录器
|
|
||||||
loginer = DouyuLogin(
|
|
||||||
self.account,
|
|
||||||
proxy=proxy_url,
|
|
||||||
proxy_api_url=proxy_api_url,
|
|
||||||
max_geetest_retries=self.max_geetest_retries,
|
|
||||||
)
|
|
||||||
|
|
||||||
# 执行登录
|
|
||||||
result = loginer.login()
|
|
||||||
|
|
||||||
if result.success:
|
|
||||||
self.log_queue.put(('success', f'[{self.index + 1}] {username} 登录成功'))
|
|
||||||
self.result_queue.put({
|
|
||||||
'index': self.index,
|
|
||||||
'username': username,
|
|
||||||
'email': self.account.email,
|
|
||||||
'status': 'success',
|
|
||||||
'cookie': result.cookie,
|
|
||||||
'message': '登录成功',
|
|
||||||
})
|
|
||||||
else:
|
|
||||||
self.log_queue.put(('error', f'[{self.index + 1}] {username} 登录失败: {result.message}'))
|
|
||||||
self.result_queue.put({
|
|
||||||
'index': self.index,
|
|
||||||
'username': username,
|
|
||||||
'email': self.account.email,
|
|
||||||
'status': 'failed',
|
|
||||||
'cookie': '',
|
|
||||||
'message': result.message,
|
|
||||||
})
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
error_msg = str(e)
|
|
||||||
self.log_queue.put(('error', f'[{self.index + 1}] {username} 登录异常: {error_msg}'))
|
|
||||||
self.result_queue.put({
|
|
||||||
'index': self.index,
|
|
||||||
'username': username,
|
|
||||||
'email': self.account.email,
|
|
||||||
'status': 'error',
|
|
||||||
'cookie': '',
|
|
||||||
'message': error_msg,
|
|
||||||
})
|
|
||||||
-118
@@ -1,118 +0,0 @@
|
|||||||
"""GUI本地状态存储。"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import json
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from loguru import logger
|
|
||||||
|
|
||||||
from douyu.config import Account, ProxyConfig
|
|
||||||
from douyu.email_verifier import get_email_config_for_account
|
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_STATE_PATH = Path("data/gui_state.json")
|
|
||||||
|
|
||||||
|
|
||||||
def account_to_dict(account: Account) -> dict[str, Any]:
|
|
||||||
"""把账号对象转换成可保存的字典。"""
|
|
||||||
return {
|
|
||||||
"username": account.username,
|
|
||||||
"password": account.password,
|
|
||||||
"email": account.email,
|
|
||||||
"email_password": account.email_password,
|
|
||||||
"email_imap_server": account.email_imap_server,
|
|
||||||
"email_imap_port": account.email_imap_port,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def account_from_dict(data: dict[str, Any]) -> Account | None:
|
|
||||||
"""从本地状态恢复账号对象。"""
|
|
||||||
username = str(data.get("username", "")).strip()
|
|
||||||
password = str(data.get("password", "")).strip()
|
|
||||||
email = str(data.get("email", "")).strip()
|
|
||||||
email_password = str(data.get("email_password", "")).strip()
|
|
||||||
|
|
||||||
if not all([username, password, email, email_password]):
|
|
||||||
return None
|
|
||||||
|
|
||||||
email_config = get_email_config_for_account(email)
|
|
||||||
imap_server = str(data.get("email_imap_server") or email_config["server"]).strip()
|
|
||||||
try:
|
|
||||||
imap_port = int(data.get("email_imap_port") or email_config["port"])
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
imap_port = int(email_config["port"])
|
|
||||||
|
|
||||||
return Account(
|
|
||||||
username=username,
|
|
||||||
password=password,
|
|
||||||
email=email,
|
|
||||||
email_password=email_password,
|
|
||||||
email_imap_server=imap_server,
|
|
||||||
email_imap_port=imap_port,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class GuiStateStore:
|
|
||||||
"""负责保存和读取GUI状态。"""
|
|
||||||
|
|
||||||
def __init__(self, path: str | Path = DEFAULT_STATE_PATH):
|
|
||||||
self.path = Path(path)
|
|
||||||
|
|
||||||
def load(self) -> dict[str, Any]:
|
|
||||||
"""读取本地状态文件。"""
|
|
||||||
if not self.path.exists():
|
|
||||||
return {}
|
|
||||||
|
|
||||||
try:
|
|
||||||
with self.path.open("r", encoding="utf-8") as f:
|
|
||||||
data = json.load(f)
|
|
||||||
return data if isinstance(data, dict) else {}
|
|
||||||
except Exception as exc:
|
|
||||||
logger.warning(f"读取GUI状态失败,将使用默认值: {exc}")
|
|
||||||
return {}
|
|
||||||
|
|
||||||
def save(self, state: dict[str, Any]) -> None:
|
|
||||||
"""原子写入本地状态文件。"""
|
|
||||||
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
tmp_path = self.path.with_suffix(f"{self.path.suffix}.tmp")
|
|
||||||
|
|
||||||
with tmp_path.open("w", encoding="utf-8") as f:
|
|
||||||
json.dump(state, f, ensure_ascii=False, indent=2)
|
|
||||||
|
|
||||||
tmp_path.replace(self.path)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def accounts_from_state(state: dict[str, Any]) -> list[Account]:
|
|
||||||
"""从状态字典恢复账号列表。"""
|
|
||||||
accounts = []
|
|
||||||
raw_accounts = state.get("accounts", [])
|
|
||||||
if not isinstance(raw_accounts, list):
|
|
||||||
return accounts
|
|
||||||
|
|
||||||
for raw_account in raw_accounts:
|
|
||||||
if not isinstance(raw_account, dict):
|
|
||||||
continue
|
|
||||||
account = account_from_dict(raw_account)
|
|
||||||
if account:
|
|
||||||
accounts.append(account)
|
|
||||||
|
|
||||||
return accounts
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def proxy_from_state(state: dict[str, Any]) -> ProxyConfig:
|
|
||||||
"""从状态字典恢复代理配置。"""
|
|
||||||
proxy = state.get("proxy", {})
|
|
||||||
if not isinstance(proxy, dict):
|
|
||||||
return ProxyConfig()
|
|
||||||
|
|
||||||
return ProxyConfig(
|
|
||||||
enabled=bool(proxy.get("enabled", False)),
|
|
||||||
api_url=str(proxy.get("api_url", "")).strip(),
|
|
||||||
http=str(proxy.get("http", "")).strip(),
|
|
||||||
https=str(proxy.get("https", "")).strip(),
|
|
||||||
whitelist_enabled=bool(proxy.get("whitelist_enabled", False)),
|
|
||||||
whitelist_uid=str(proxy.get("whitelist_uid", "")).strip(),
|
|
||||||
whitelist_ukey=str(proxy.get("whitelist_ukey", "")).strip(),
|
|
||||||
)
|
|
||||||
-212
@@ -1,212 +0,0 @@
|
|||||||
"""自定义GUI组件"""
|
|
||||||
|
|
||||||
import tkinter as tk
|
|
||||||
from tkinter import ttk
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
|
|
||||||
class AccountTable(ttk.Treeview):
|
|
||||||
"""账号列表表格"""
|
|
||||||
|
|
||||||
COLUMNS = ('index', 'username', 'email', 'status', 'cookie')
|
|
||||||
|
|
||||||
def __init__(self, parent, **kwargs):
|
|
||||||
super().__init__(
|
|
||||||
parent,
|
|
||||||
columns=self.COLUMNS,
|
|
||||||
show='headings',
|
|
||||||
selectmode='extended',
|
|
||||||
**kwargs
|
|
||||||
)
|
|
||||||
|
|
||||||
# 设置列标题
|
|
||||||
self.heading('index', text='序号')
|
|
||||||
self.heading('username', text='用户名')
|
|
||||||
self.heading('email', text='邮箱')
|
|
||||||
self.heading('status', text='状态')
|
|
||||||
self.heading('cookie', text='Cookie')
|
|
||||||
|
|
||||||
# 设置列宽
|
|
||||||
self.column('index', width=50, minwidth=50, anchor='center')
|
|
||||||
self.column('username', width=150, minwidth=100)
|
|
||||||
self.column('email', width=200, minwidth=150)
|
|
||||||
self.column('status', width=80, minwidth=60, anchor='center')
|
|
||||||
self.column('cookie', width=200, minwidth=100)
|
|
||||||
|
|
||||||
# 添加滚动条
|
|
||||||
scrollbar = ttk.Scrollbar(parent, orient='vertical', command=self.yview)
|
|
||||||
self.configure(yscrollcommand=scrollbar.set)
|
|
||||||
scrollbar.pack(side='right', fill='y')
|
|
||||||
|
|
||||||
# 状态标签样式
|
|
||||||
self.tag_configure('pending', foreground='gray')
|
|
||||||
self.tag_configure('running', foreground='blue')
|
|
||||||
self.tag_configure('success', foreground='green')
|
|
||||||
self.tag_configure('failed', foreground='red')
|
|
||||||
self.tag_configure('error', foreground='red')
|
|
||||||
|
|
||||||
def add_account(self, index: int, username: str, email: str):
|
|
||||||
"""添加账号到表格"""
|
|
||||||
self.insert('', 'end', iid=str(index), values=(
|
|
||||||
index + 1,
|
|
||||||
username,
|
|
||||||
email,
|
|
||||||
'待登录',
|
|
||||||
'-'
|
|
||||||
), tags=('pending',))
|
|
||||||
|
|
||||||
def update_status(self, index: int, status: str, cookie: str = ''):
|
|
||||||
"""更新账号状态"""
|
|
||||||
status_map = {
|
|
||||||
'pending': ('待登录', 'pending'),
|
|
||||||
'running': ('登录中...', 'running'),
|
|
||||||
'success': ('成功', 'success'),
|
|
||||||
'failed': ('失败', 'failed'),
|
|
||||||
'error': ('异常', 'error'),
|
|
||||||
}
|
|
||||||
|
|
||||||
text, tag = status_map.get(status, (status, 'pending'))
|
|
||||||
|
|
||||||
item_id = str(index)
|
|
||||||
if self.exists(item_id):
|
|
||||||
values = self.item(item_id, 'values')
|
|
||||||
cookie_display = cookie[:30] + '...' if len(cookie) > 30 else cookie
|
|
||||||
self.item(item_id, values=(
|
|
||||||
values[0],
|
|
||||||
values[1],
|
|
||||||
values[2],
|
|
||||||
text,
|
|
||||||
cookie_display if cookie else '-'
|
|
||||||
), tags=(tag,))
|
|
||||||
|
|
||||||
def clear(self):
|
|
||||||
"""清空表格"""
|
|
||||||
for item in self.get_children():
|
|
||||||
self.delete(item)
|
|
||||||
|
|
||||||
def get_all_items(self):
|
|
||||||
"""获取所有项目"""
|
|
||||||
items = []
|
|
||||||
for item_id in self.get_children():
|
|
||||||
values = self.item(item_id, 'values')
|
|
||||||
items.append({
|
|
||||||
'index': values[0],
|
|
||||||
'username': values[1],
|
|
||||||
'email': values[2],
|
|
||||||
'status': values[3],
|
|
||||||
'cookie': values[4],
|
|
||||||
})
|
|
||||||
return items
|
|
||||||
|
|
||||||
|
|
||||||
class LogPanel(tk.Text):
|
|
||||||
"""日志输出面板"""
|
|
||||||
|
|
||||||
def __init__(self, parent, **kwargs):
|
|
||||||
super().__init__(
|
|
||||||
parent,
|
|
||||||
wrap='word',
|
|
||||||
state='disabled',
|
|
||||||
font=('Consolas', 10),
|
|
||||||
**kwargs
|
|
||||||
)
|
|
||||||
|
|
||||||
# 日志级别颜色
|
|
||||||
self.tag_configure('info', foreground='#333333')
|
|
||||||
self.tag_configure('success', foreground='#008000')
|
|
||||||
self.tag_configure('warning', foreground='#FF8C00')
|
|
||||||
self.tag_configure('error', foreground='#FF0000')
|
|
||||||
self.tag_configure('debug', foreground='#888888')
|
|
||||||
self.tag_configure('timestamp', foreground='#666666')
|
|
||||||
|
|
||||||
# 添加滚动条
|
|
||||||
scrollbar = ttk.Scrollbar(parent, orient='vertical', command=self.yview)
|
|
||||||
self.configure(yscrollcommand=scrollbar.set)
|
|
||||||
scrollbar.pack(side='right', fill='y')
|
|
||||||
|
|
||||||
def append_log(self, level: str, message: str):
|
|
||||||
"""添加日志"""
|
|
||||||
self.configure(state='normal')
|
|
||||||
|
|
||||||
# 添加时间戳
|
|
||||||
timestamp = datetime.now().strftime('%H:%M:%S')
|
|
||||||
self.insert('end', f'[{timestamp}] ', 'timestamp')
|
|
||||||
|
|
||||||
# 添加日志内容
|
|
||||||
self.insert('end', f'{message}\n', level)
|
|
||||||
|
|
||||||
# 滚动到底部
|
|
||||||
self.see('end')
|
|
||||||
|
|
||||||
self.configure(state='disabled')
|
|
||||||
|
|
||||||
def clear(self):
|
|
||||||
"""清空日志"""
|
|
||||||
self.configure(state='normal')
|
|
||||||
self.delete('1.0', 'end')
|
|
||||||
self.configure(state='disabled')
|
|
||||||
|
|
||||||
|
|
||||||
class ImportDialog(tk.Toplevel):
|
|
||||||
"""导入对话框"""
|
|
||||||
|
|
||||||
def __init__(self, parent):
|
|
||||||
super().__init__(parent)
|
|
||||||
|
|
||||||
self.title('从文件导入')
|
|
||||||
self.geometry('400x150')
|
|
||||||
self.resizable(False, False)
|
|
||||||
self.transient(parent)
|
|
||||||
self.grab_set()
|
|
||||||
|
|
||||||
# 居中显示
|
|
||||||
self.update_idletasks()
|
|
||||||
x = (self.winfo_screenwidth() - 400) // 2
|
|
||||||
y = (self.winfo_screenheight() - 150) // 2
|
|
||||||
self.geometry(f'+{x}+{y}')
|
|
||||||
|
|
||||||
self.filepath = None
|
|
||||||
self.result = None
|
|
||||||
|
|
||||||
# 文件路径
|
|
||||||
frame = ttk.Frame(self, padding=20)
|
|
||||||
frame.pack(fill='both', expand=True)
|
|
||||||
|
|
||||||
ttk.Label(frame, text='选择账号文件(每行一个,格式:用户名|密码|邮箱|邮箱密码)').pack(anchor='w')
|
|
||||||
|
|
||||||
path_frame = ttk.Frame(frame)
|
|
||||||
path_frame.pack(fill='x', pady=(10, 0))
|
|
||||||
|
|
||||||
self.path_var = tk.StringVar()
|
|
||||||
ttk.Entry(path_frame, textvariable=self.path_var, state='readonly').pack(side='left', fill='x', expand=True)
|
|
||||||
ttk.Button(path_frame, text='浏览', command=self._browse).pack(side='left', padx=(5, 0))
|
|
||||||
|
|
||||||
# 按钮
|
|
||||||
btn_frame = ttk.Frame(frame)
|
|
||||||
btn_frame.pack(fill='x', pady=(20, 0))
|
|
||||||
|
|
||||||
ttk.Button(btn_frame, text='确定', command=self._confirm).pack(side='right', padx=(5, 0))
|
|
||||||
ttk.Button(btn_frame, text='取消', command=self._cancel).pack(side='right')
|
|
||||||
|
|
||||||
def _browse(self):
|
|
||||||
"""浏览文件"""
|
|
||||||
from tkinter import filedialog
|
|
||||||
filepath = filedialog.askopenfilename(
|
|
||||||
title='选择账号文件',
|
|
||||||
filetypes=[('文本文件', '*.txt'), ('所有文件', '*.*')]
|
|
||||||
)
|
|
||||||
if filepath:
|
|
||||||
self.path_var.set(filepath)
|
|
||||||
self.filepath = filepath
|
|
||||||
|
|
||||||
def _confirm(self):
|
|
||||||
"""确认"""
|
|
||||||
if not self.filepath:
|
|
||||||
tk.messagebox.showwarning('提示', '请选择文件')
|
|
||||||
return
|
|
||||||
self.result = self.filepath
|
|
||||||
self.destroy()
|
|
||||||
|
|
||||||
def _cancel(self):
|
|
||||||
"""取消"""
|
|
||||||
self.destroy()
|
|
||||||
@@ -1,165 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
斗鱼自动登录工具 - Python版
|
|
||||||
|
|
||||||
功能:
|
|
||||||
1. 自动登录斗鱼账号
|
|
||||||
2. 自动过极验滑块验证
|
|
||||||
3. 自动获取邮箱验证码
|
|
||||||
4. 批量获取Cookie
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import argparse
|
|
||||||
from pathlib import Path
|
|
||||||
from loguru import logger
|
|
||||||
|
|
||||||
from douyu import DouyuLogin, Config
|
|
||||||
from utils import setup_logger, save_cookie, load_accounts
|
|
||||||
|
|
||||||
|
|
||||||
def login_single(config: Config, account_index: int = 0) -> str:
|
|
||||||
"""
|
|
||||||
单账号登录
|
|
||||||
|
|
||||||
Args:
|
|
||||||
config: 配置对象
|
|
||||||
account_index: 账号索引
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Cookie字符串
|
|
||||||
"""
|
|
||||||
accounts = config.get_accounts()
|
|
||||||
|
|
||||||
if account_index >= len(accounts):
|
|
||||||
logger.error(f"账号索引 {account_index} 超出范围,共有 {len(accounts)} 个账号")
|
|
||||||
return ""
|
|
||||||
|
|
||||||
account = accounts[account_index]
|
|
||||||
logger.info(f"登录账号: {account.username}")
|
|
||||||
|
|
||||||
# 创建登录器
|
|
||||||
proxy = config.get_proxy()
|
|
||||||
proxy_url = None
|
|
||||||
proxy_api_url = None
|
|
||||||
|
|
||||||
if proxy.enabled:
|
|
||||||
if proxy.api_url:
|
|
||||||
# 使用代理API
|
|
||||||
proxy_api_url = proxy.api_url
|
|
||||||
elif proxy.http or proxy.https:
|
|
||||||
# 使用静态代理
|
|
||||||
proxy_url = {
|
|
||||||
'http': proxy.http or proxy.https,
|
|
||||||
'https': proxy.https or proxy.http,
|
|
||||||
}
|
|
||||||
|
|
||||||
# 获取极验配置
|
|
||||||
geetest_config = config.get_geetest_config()
|
|
||||||
max_retries = geetest_config.get('max_retries', 5)
|
|
||||||
|
|
||||||
loginer = DouyuLogin(
|
|
||||||
account,
|
|
||||||
proxy=proxy_url,
|
|
||||||
proxy_api_url=proxy_api_url,
|
|
||||||
max_geetest_retries=max_retries,
|
|
||||||
)
|
|
||||||
|
|
||||||
# 执行登录
|
|
||||||
result = loginer.login()
|
|
||||||
|
|
||||||
if result.success:
|
|
||||||
# 保存Cookie
|
|
||||||
cookie_dir = config.get_cookie_dir()
|
|
||||||
save_cookie(account.username, result.cookie, cookie_dir)
|
|
||||||
return result.cookie
|
|
||||||
else:
|
|
||||||
logger.error(f"登录失败: {result.message}")
|
|
||||||
return ""
|
|
||||||
|
|
||||||
|
|
||||||
def login_batch(config: Config) -> dict:
|
|
||||||
"""
|
|
||||||
批量登录
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
{username: cookie} 字典
|
|
||||||
"""
|
|
||||||
accounts = config.get_accounts()
|
|
||||||
results = {}
|
|
||||||
|
|
||||||
logger.info(f"开始批量登录,共 {len(accounts)} 个账号")
|
|
||||||
|
|
||||||
for i, account in enumerate(accounts):
|
|
||||||
logger.info(f"[{i+1}/{len(accounts)}] 登录账号: {account.username}")
|
|
||||||
|
|
||||||
try:
|
|
||||||
cookie = login_single(config, i)
|
|
||||||
if cookie:
|
|
||||||
results[account.username] = cookie
|
|
||||||
logger.success(f"账号 {account.username} 登录成功")
|
|
||||||
else:
|
|
||||||
logger.error(f"账号 {account.username} 登录失败")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"账号 {account.username} 登录异常: {e}")
|
|
||||||
|
|
||||||
# 统计结果
|
|
||||||
success_count = len(results)
|
|
||||||
total_count = len(accounts)
|
|
||||||
logger.info(f"批量登录完成: 成功 {success_count}/{total_count}")
|
|
||||||
|
|
||||||
return results
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""主函数"""
|
|
||||||
parser = argparse.ArgumentParser(description='斗鱼自动登录工具')
|
|
||||||
parser.add_argument('-c', '--config', default='config.yaml', help='配置文件路径')
|
|
||||||
parser.add_argument('-i', '--index', type=int, default=0, help='单账号登录时的账号索引')
|
|
||||||
parser.add_argument('-b', '--batch', action='store_true', help='批量登录模式')
|
|
||||||
parser.add_argument('-g', '--gui', action='store_true', help='启动GUI界面')
|
|
||||||
parser.add_argument('-w', '--web', action='store_true', help='启动Web后台')
|
|
||||||
parser.add_argument('-v', '--verbose', action='store_true', help='详细日志')
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
# 配置日志
|
|
||||||
log_level = "DEBUG" if args.verbose else "INFO"
|
|
||||||
setup_logger(level=log_level)
|
|
||||||
|
|
||||||
# 启动GUI
|
|
||||||
if args.gui:
|
|
||||||
from gui import DouyuLoginApp
|
|
||||||
app = DouyuLoginApp()
|
|
||||||
app.run()
|
|
||||||
return
|
|
||||||
|
|
||||||
# 启动Web后台
|
|
||||||
if args.web:
|
|
||||||
from web.backend.main import run
|
|
||||||
run()
|
|
||||||
return
|
|
||||||
|
|
||||||
# 加载配置
|
|
||||||
try:
|
|
||||||
config = Config(args.config)
|
|
||||||
except FileNotFoundError as e:
|
|
||||||
logger.error(str(e))
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# 执行登录
|
|
||||||
if args.batch:
|
|
||||||
results = login_batch(config)
|
|
||||||
print(f"\n登录结果: 成功 {len(results)} 个账号")
|
|
||||||
else:
|
|
||||||
cookie = login_single(config, args.index)
|
|
||||||
if cookie:
|
|
||||||
print(f"\n登录成功!")
|
|
||||||
print(f"Cookie: {cookie[:50]}...")
|
|
||||||
else:
|
|
||||||
print("\n登录失败")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
+3
-7
@@ -1,7 +1,7 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "douyu-login-py"
|
name = "douyu-login-py"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
description = "斗鱼自动登录工具 - Python版"
|
description = "斗鱼批量登录 Web 后台"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.12,<3.13"
|
requires-python = ">=3.12,<3.13"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
@@ -10,7 +10,6 @@ dependencies = [
|
|||||||
"numpy>=1.24.0",
|
"numpy>=1.24.0",
|
||||||
"opencv-python-headless>=4.8.0",
|
"opencv-python-headless>=4.8.0",
|
||||||
"Pillow>=10.0.0",
|
"Pillow>=10.0.0",
|
||||||
"PyYAML>=6.0",
|
|
||||||
"loguru>=0.7.0",
|
"loguru>=0.7.0",
|
||||||
"fastapi>=0.110.0",
|
"fastapi>=0.110.0",
|
||||||
"uvicorn[standard]>=0.27.0",
|
"uvicorn[standard]>=0.27.0",
|
||||||
@@ -21,12 +20,9 @@ dependencies = [
|
|||||||
"python-multipart>=0.0.9",
|
"python-multipart>=0.0.9",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
|
||||||
douyu-login = "main:main"
|
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["hatchling"]
|
requires = ["hatchling"]
|
||||||
build-backend = "hatchling.build"
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
[tool.hatch.build.targets.wheel]
|
[tool.hatch.build.targets.wheel]
|
||||||
packages = ["douyu", "geetest", "utils", "gui", "web"]
|
packages = ["core", "utils", "web"]
|
||||||
|
|||||||
@@ -3,9 +3,7 @@ pycryptodome>=3.19.0
|
|||||||
numpy>=1.24.0
|
numpy>=1.24.0
|
||||||
opencv-python-headless>=4.8.0
|
opencv-python-headless>=4.8.0
|
||||||
Pillow>=10.0.0
|
Pillow>=10.0.0
|
||||||
PyYAML>=6.0
|
|
||||||
loguru>=0.7.0
|
loguru>=0.7.0
|
||||||
# Web 后端
|
|
||||||
fastapi>=0.110.0
|
fastapi>=0.110.0
|
||||||
uvicorn[standard]>=0.27.0
|
uvicorn[standard]>=0.27.0
|
||||||
sqlalchemy>=2.0.0
|
sqlalchemy>=2.0.0
|
||||||
|
|||||||
@@ -1,202 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""邮箱IMAP读取测试脚本"""
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
# 添加项目根目录到path
|
|
||||||
sys.path.insert(0, str(Path(__file__).parent))
|
|
||||||
|
|
||||||
from douyu.config import Config
|
|
||||||
from douyu.email_verifier import EmailVerifier
|
|
||||||
from utils.logger import setup_logger
|
|
||||||
|
|
||||||
|
|
||||||
def mask_email(email_address: str) -> str:
|
|
||||||
"""隐藏邮箱中间部分,避免控制台泄露完整账号。"""
|
|
||||||
if "@" not in email_address:
|
|
||||||
return email_address
|
|
||||||
|
|
||||||
name, domain = email_address.split("@", 1)
|
|
||||||
if len(name) <= 2:
|
|
||||||
masked_name = name[0] + "*"
|
|
||||||
else:
|
|
||||||
masked_name = name[:2] + "*" * max(2, len(name) - 4) + name[-2:]
|
|
||||||
|
|
||||||
return f"{masked_name}@{domain}"
|
|
||||||
|
|
||||||
|
|
||||||
def clean_preview(text: str, max_length: int = 120) -> str:
|
|
||||||
"""清理正文预览,保持输出紧凑。"""
|
|
||||||
preview = " ".join(text.split())
|
|
||||||
preview = re.sub(r"\b\d{6}\b", "******", preview)
|
|
||||||
if len(preview) <= max_length:
|
|
||||||
return preview
|
|
||||||
return preview[:max_length] + "..."
|
|
||||||
|
|
||||||
|
|
||||||
def format_code(code: str, show_code: bool) -> str:
|
|
||||||
"""格式化验证码输出,默认隐藏大部分数字。"""
|
|
||||||
if not code:
|
|
||||||
return "未提取到"
|
|
||||||
if show_code:
|
|
||||||
return code
|
|
||||||
return f"****{code[-2:]}"
|
|
||||||
|
|
||||||
|
|
||||||
def format_message_time(verifier: EmailVerifier, msg) -> str:
|
|
||||||
"""格式化邮件时间。"""
|
|
||||||
message_time = verifier._parse_message_time(msg)
|
|
||||||
if not message_time:
|
|
||||||
return "未知"
|
|
||||||
return message_time.strftime("%Y-%m-%d %H:%M:%S %z")
|
|
||||||
|
|
||||||
|
|
||||||
def load_account(config_path: str, index: int):
|
|
||||||
"""从配置文件加载指定账号。"""
|
|
||||||
config = Config(config_path)
|
|
||||||
accounts = config.get_accounts()
|
|
||||||
if not accounts:
|
|
||||||
raise ValueError("config.yaml 中没有配置账号")
|
|
||||||
|
|
||||||
if index < 0 or index >= len(accounts):
|
|
||||||
raise IndexError(f"账号索引超出范围: {index},当前共有 {len(accounts)} 个账号")
|
|
||||||
|
|
||||||
return accounts[index]
|
|
||||||
|
|
||||||
|
|
||||||
def list_recent_messages(
|
|
||||||
verifier: EmailVerifier,
|
|
||||||
lookback_minutes: int,
|
|
||||||
limit: int,
|
|
||||||
show_code: bool,
|
|
||||||
) -> None:
|
|
||||||
"""列出最近邮件,并测试斗鱼验证码解析。"""
|
|
||||||
verifier.connect()
|
|
||||||
|
|
||||||
try:
|
|
||||||
status, _ = verifier._connection.select(verifier.mailbox, readonly=True)
|
|
||||||
if status != "OK":
|
|
||||||
raise RuntimeError(f"选择邮箱目录失败: {verifier.mailbox}")
|
|
||||||
|
|
||||||
since_dt = datetime.now() - timedelta(minutes=lookback_minutes)
|
|
||||||
since = verifier._format_imap_date(since_dt)
|
|
||||||
status, messages = verifier._connection.search(None, "SINCE", since)
|
|
||||||
if status != "OK":
|
|
||||||
raise RuntimeError(f"搜索邮件失败: status={status}")
|
|
||||||
|
|
||||||
message_ids = messages[0].split() if messages and messages[0] else []
|
|
||||||
if not message_ids:
|
|
||||||
print(f"最近 {lookback_minutes} 分钟没有邮件")
|
|
||||||
return
|
|
||||||
|
|
||||||
recent_ids = list(reversed(message_ids[-limit:]))
|
|
||||||
print(f"搜索范围: 最近 {lookback_minutes} 分钟")
|
|
||||||
print(f"匹配邮件数: {len(message_ids)},展示最新 {len(recent_ids)} 封\n")
|
|
||||||
|
|
||||||
for index, message_id in enumerate(recent_ids, start=1):
|
|
||||||
msg = verifier._fetch_message(message_id)
|
|
||||||
if not msg:
|
|
||||||
print(f"[{index}] 读取失败: id={message_id.decode(errors='ignore')}")
|
|
||||||
continue
|
|
||||||
|
|
||||||
subject = verifier._decode_subject(msg.get("Subject", ""))
|
|
||||||
from_addr = msg.get("From", "")
|
|
||||||
body = verifier._get_email_body(msg)
|
|
||||||
is_douyu = verifier._is_douyu_email(subject, from_addr, body)
|
|
||||||
code = verifier._extract_verification_code(body) if is_douyu else ""
|
|
||||||
|
|
||||||
print(f"[{index}] 邮件ID: {message_id.decode(errors='ignore')}")
|
|
||||||
print(f" 时间: {format_message_time(verifier, msg)}")
|
|
||||||
print(f" 发件人: {from_addr}")
|
|
||||||
print(f" 主题: {subject}")
|
|
||||||
print(f" 斗鱼邮件: {'是' if is_douyu else '否'}")
|
|
||||||
print(f" 验证码: {format_code(code, show_code)}")
|
|
||||||
print(f" 正文预览: {clean_preview(body)}\n")
|
|
||||||
finally:
|
|
||||||
verifier.disconnect()
|
|
||||||
|
|
||||||
|
|
||||||
def wait_verification_code(
|
|
||||||
account,
|
|
||||||
max_wait: int,
|
|
||||||
interval: int,
|
|
||||||
new_only: bool,
|
|
||||||
show_code: bool,
|
|
||||||
) -> None:
|
|
||||||
"""等待并提取斗鱼验证码,不负责发送验证码邮件。"""
|
|
||||||
verifier = EmailVerifier(
|
|
||||||
imap_server=account.email_imap_server,
|
|
||||||
imap_port=account.email_imap_port,
|
|
||||||
username=account.email,
|
|
||||||
password=account.email_password,
|
|
||||||
)
|
|
||||||
|
|
||||||
after_timestamp = time.time() if new_only else None
|
|
||||||
code = verifier.get_verification_code(
|
|
||||||
max_wait=max_wait,
|
|
||||||
interval=interval,
|
|
||||||
after_timestamp=after_timestamp,
|
|
||||||
)
|
|
||||||
print(f"等待验证码结果: {format_code(code, show_code)}")
|
|
||||||
|
|
||||||
|
|
||||||
def parse_args() -> argparse.Namespace:
|
|
||||||
"""解析命令行参数。"""
|
|
||||||
parser = argparse.ArgumentParser(description="测试邮箱IMAP是否能读取邮件和验证码")
|
|
||||||
parser.add_argument("-c", "--config", default="config.yaml", help="配置文件路径")
|
|
||||||
parser.add_argument("-i", "--index", type=int, default=0, help="账号索引")
|
|
||||||
parser.add_argument("--lookback-minutes", type=int, default=30, help="读取最近多少分钟邮件")
|
|
||||||
parser.add_argument("--limit", type=int, default=5, help="展示最近多少封邮件")
|
|
||||||
parser.add_argument("--wait-code", action="store_true", help="额外等待并提取斗鱼验证码")
|
|
||||||
parser.add_argument("--new-only", action="store_true", help="等待验证码时只接受脚本启动后的新邮件")
|
|
||||||
parser.add_argument("--show-code", action="store_true", help="输出完整验证码")
|
|
||||||
parser.add_argument("--max-wait", type=int, default=60, help="等待验证码最大秒数")
|
|
||||||
parser.add_argument("--interval", type=int, default=2, help="验证码轮询间隔秒数")
|
|
||||||
parser.add_argument("--log-level", default="INFO", help="日志级别")
|
|
||||||
return parser.parse_args()
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
"""脚本入口。"""
|
|
||||||
args = parse_args()
|
|
||||||
setup_logger(level=args.log_level)
|
|
||||||
|
|
||||||
account = load_account(args.config, args.index)
|
|
||||||
print("=== 邮箱IMAP读取测试 ===")
|
|
||||||
print(f"账号索引: {args.index}")
|
|
||||||
print(f"邮箱账号: {mask_email(account.email)}")
|
|
||||||
print(f"IMAP服务器: {account.email_imap_server}:{account.email_imap_port}\n")
|
|
||||||
|
|
||||||
verifier = EmailVerifier(
|
|
||||||
imap_server=account.email_imap_server,
|
|
||||||
imap_port=account.email_imap_port,
|
|
||||||
username=account.email,
|
|
||||||
password=account.email_password,
|
|
||||||
lookback_minutes=args.lookback_minutes,
|
|
||||||
max_messages=args.limit,
|
|
||||||
)
|
|
||||||
list_recent_messages(
|
|
||||||
verifier=verifier,
|
|
||||||
lookback_minutes=args.lookback_minutes,
|
|
||||||
limit=args.limit,
|
|
||||||
show_code=args.show_code,
|
|
||||||
)
|
|
||||||
|
|
||||||
if args.wait_code:
|
|
||||||
print("=== 等待斗鱼验证码 ===")
|
|
||||||
wait_verification_code(
|
|
||||||
account=account,
|
|
||||||
max_wait=args.max_wait,
|
|
||||||
interval=args.interval,
|
|
||||||
new_only=args.new_only,
|
|
||||||
show_code=args.show_code,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
测试登录功能
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
# 添加项目根目录到path
|
|
||||||
sys.path.insert(0, str(Path(__file__).parent))
|
|
||||||
|
|
||||||
from douyu.config import Config, Account
|
|
||||||
from douyu.crypto import encrypt_password, encrypt_nickname_or_phone
|
|
||||||
from douyu.email_verifier import EmailVerifier
|
|
||||||
from utils.logger import setup_logger
|
|
||||||
|
|
||||||
|
|
||||||
def test_crypto():
|
|
||||||
"""测试加密功能"""
|
|
||||||
print("=== 测试加密功能 ===")
|
|
||||||
|
|
||||||
# 测试密码加密
|
|
||||||
password = "test_password"
|
|
||||||
encrypted = encrypt_password(password)
|
|
||||||
print(f"密码: {password}")
|
|
||||||
print(f"MD5: {encrypted}")
|
|
||||||
|
|
||||||
# 测试用户名加密
|
|
||||||
username = "test_user"
|
|
||||||
encrypted = encrypt_nickname_or_phone(username)
|
|
||||||
print(f"用户名: {username}")
|
|
||||||
print(f"加密后: {encrypted}")
|
|
||||||
|
|
||||||
print()
|
|
||||||
|
|
||||||
|
|
||||||
def test_config():
|
|
||||||
"""测试配置加载"""
|
|
||||||
print("=== 测试配置加载 ===")
|
|
||||||
|
|
||||||
try:
|
|
||||||
config = Config("config.yaml")
|
|
||||||
accounts = config.get_accounts()
|
|
||||||
|
|
||||||
print(f"加载了 {len(accounts)} 个账号")
|
|
||||||
for i, acc in enumerate(accounts):
|
|
||||||
print(f" [{i}] {acc.username} - {acc.email}")
|
|
||||||
|
|
||||||
proxy = config.get_proxy()
|
|
||||||
print(f"代理: {'启用' if proxy.enabled else '禁用'}")
|
|
||||||
|
|
||||||
except FileNotFoundError as e:
|
|
||||||
print(f"配置文件不存在: {e}")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"加载配置失败: {e}")
|
|
||||||
|
|
||||||
print()
|
|
||||||
|
|
||||||
|
|
||||||
def test_email_verifier():
|
|
||||||
"""测试邮箱验证器(不实际连接)"""
|
|
||||||
print("=== 测试邮箱验证器 ===")
|
|
||||||
|
|
||||||
# 测试验证码提取
|
|
||||||
verifier = EmailVerifier("", "", "", "")
|
|
||||||
|
|
||||||
# 模拟邮件内容
|
|
||||||
test_cases = [
|
|
||||||
"您的验证码是:123456,请在5分钟内完成验证。",
|
|
||||||
"验证码:654321",
|
|
||||||
"Your verification code is 789012",
|
|
||||||
"【斗鱼】安全验证码:345678,切勿泄露给他人!",
|
|
||||||
]
|
|
||||||
|
|
||||||
for text in test_cases:
|
|
||||||
code = verifier._extract_verification_code(text)
|
|
||||||
print(f"文本: {text[:30]}...")
|
|
||||||
print(f"验证码: {code}")
|
|
||||||
print()
|
|
||||||
|
|
||||||
print()
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""运行所有测试"""
|
|
||||||
setup_logger(level="INFO")
|
|
||||||
|
|
||||||
print("斗鱼自动登录工具 - 测试\n")
|
|
||||||
|
|
||||||
test_crypto()
|
|
||||||
test_config()
|
|
||||||
test_email_verifier()
|
|
||||||
|
|
||||||
print("测试完成!")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
+1
-2
@@ -1,6 +1,5 @@
|
|||||||
"""工具模块"""
|
"""工具模块"""
|
||||||
|
|
||||||
from .logger import setup_logger
|
from .logger import setup_logger
|
||||||
from .helpers import load_accounts, save_cookie
|
|
||||||
|
|
||||||
__all__ = ["setup_logger", "load_accounts", "save_cookie"]
|
__all__ = ["setup_logger"]
|
||||||
|
|||||||
@@ -1,95 +0,0 @@
|
|||||||
"""辅助工具函数"""
|
|
||||||
|
|
||||||
import json
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import List
|
|
||||||
from loguru import logger
|
|
||||||
|
|
||||||
from douyu.config import Account
|
|
||||||
|
|
||||||
|
|
||||||
def load_accounts(config_path: str = "config.yaml") -> List[Account]:
|
|
||||||
"""加载账号列表"""
|
|
||||||
from douyu.config import Config
|
|
||||||
|
|
||||||
config = Config(config_path)
|
|
||||||
accounts = config.get_accounts()
|
|
||||||
|
|
||||||
logger.info(f"加载了 {len(accounts)} 个账号")
|
|
||||||
return accounts
|
|
||||||
|
|
||||||
|
|
||||||
def save_cookie(username: str, cookie: str, cookie_dir: str = "data/cookies") -> str:
|
|
||||||
"""
|
|
||||||
保存Cookie到文件
|
|
||||||
|
|
||||||
Args:
|
|
||||||
username: 用户名
|
|
||||||
cookie: Cookie字符串
|
|
||||||
cookie_dir: Cookie存储目录
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
文件路径
|
|
||||||
"""
|
|
||||||
import time
|
|
||||||
|
|
||||||
cookie_path = Path(cookie_dir)
|
|
||||||
cookie_path.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
filepath = cookie_path / f"{username}.json"
|
|
||||||
|
|
||||||
data = {
|
|
||||||
'username': username,
|
|
||||||
'cookie': cookie,
|
|
||||||
'timestamp': int(time.time()),
|
|
||||||
}
|
|
||||||
|
|
||||||
with open(filepath, 'w', encoding='utf-8') as f:
|
|
||||||
json.dump(data, f, ensure_ascii=False, indent=2)
|
|
||||||
|
|
||||||
logger.info(f"Cookie已保存: {filepath}")
|
|
||||||
return str(filepath)
|
|
||||||
|
|
||||||
|
|
||||||
def load_cookie(username: str, cookie_dir: str = "data/cookies") -> str:
|
|
||||||
"""
|
|
||||||
从文件加载Cookie
|
|
||||||
|
|
||||||
Args:
|
|
||||||
username: 用户名
|
|
||||||
cookie_dir: Cookie存储目录
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Cookie字符串,不存在返回空字符串
|
|
||||||
"""
|
|
||||||
filepath = Path(cookie_dir) / f"{username}.json"
|
|
||||||
|
|
||||||
if not filepath.exists():
|
|
||||||
return ""
|
|
||||||
|
|
||||||
try:
|
|
||||||
with open(filepath, 'r', encoding='utf-8') as f:
|
|
||||||
data = json.load(f)
|
|
||||||
return data.get('cookie', '')
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"加载Cookie失败: {e}")
|
|
||||||
return ""
|
|
||||||
|
|
||||||
|
|
||||||
def format_cookie_for_browser(cookie_str: str) -> dict:
|
|
||||||
"""
|
|
||||||
将Cookie字符串转换为浏览器格式
|
|
||||||
|
|
||||||
Args:
|
|
||||||
cookie_str: Cookie字符串
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Cookie字典
|
|
||||||
"""
|
|
||||||
cookies = {}
|
|
||||||
for item in cookie_str.split(';'):
|
|
||||||
item = item.strip()
|
|
||||||
if '=' in item:
|
|
||||||
key, value = item.split('=', 1)
|
|
||||||
cookies[key.strip()] = value.strip()
|
|
||||||
return cookies
|
|
||||||
Binary file not shown.
Binary file not shown.
@@ -70,7 +70,7 @@ def import_accounts(
|
|||||||
current: User = Depends(require_permission("account:import")),
|
current: User = Depends(require_permission("account:import")),
|
||||||
):
|
):
|
||||||
"""批量导入账号。格式:用户名|密码|邮箱|邮箱密码"""
|
"""批量导入账号。格式:用户名|密码|邮箱|邮箱密码"""
|
||||||
from douyu.email_verifier import get_email_config_for_account
|
from core.douyu.email_verifier import get_email_config_for_account
|
||||||
|
|
||||||
accounts = []
|
accounts = []
|
||||||
skipped = 0
|
skipped = 0
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ def test_whitelist(
|
|||||||
current: User = Depends(require_permission("whitelist:test")),
|
current: User = Depends(require_permission("whitelist:test")),
|
||||||
):
|
):
|
||||||
"""测试白名单连接并自动同步出口IP。"""
|
"""测试白名单连接并自动同步出口IP。"""
|
||||||
from douyu.whitelist import WhitelistManager, get_exit_ip_via_proxy
|
from core.douyu.whitelist import WhitelistManager, get_exit_ip_via_proxy
|
||||||
import requests as req_lib
|
import requests as req_lib
|
||||||
|
|
||||||
cfg = _get_or_create(db)
|
cfg = _get_or_create(db)
|
||||||
|
|||||||
Binary file not shown.
@@ -9,8 +9,8 @@ from typing import Optional
|
|||||||
|
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from douyu import DouyuLogin
|
from core.douyu import DouyuLogin
|
||||||
from douyu.config import Account, ProxyConfig as DouyuProxyConfig
|
from core.models import Account, ProxyConfig as DouyuProxyConfig
|
||||||
from ..models import Account as AccountModel, LoginTask, ProxyConfig as ProxyConfigModel
|
from ..models import Account as AccountModel, LoginTask, ProxyConfig as ProxyConfigModel
|
||||||
from ..permissions import has_permission
|
from ..permissions import has_permission
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user