彻底优化项目结构,使用 web

This commit is contained in:
yml2213
2026-06-22 13:39:03 +08:00
parent 347edb8103
commit 63c1ffa046
46 changed files with 127 additions and 3077 deletions
+17 -4
View File
@@ -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/
+53 -113
View File
@@ -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.
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

-14
View File
@@ -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
View File
@@ -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"
+5
View File
@@ -0,0 +1,5 @@
"""核心业务逻辑"""
from .models import Account, ProxyConfig
__all__ = ["Account", "ProxyConfig"]
+1 -2
View File
@@ -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"]
+4 -8
View File
@@ -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,
+5
View File
@@ -0,0 +1,5 @@
"""Geetest 验证码求解器"""
from .v3_slide.solver import run_solver
__all__ = ["run_solver"]
+1
View File
@@ -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:
+27
View File
@@ -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
View File
Binary file not shown.
-85
View File
@@ -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
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

-13
View File
@@ -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',
]
-185
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
-203
View File
@@ -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
View File
@@ -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
View File
@@ -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()
-165
View File
@@ -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
View File
@@ -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"]
-2
View File
@@ -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
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

-202
View File
@@ -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()
-98
View File
@@ -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
View File
@@ -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"]
-95
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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)
+2 -2
View File
@@ -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