Compare commits
19
Commits
e7378139ea
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
56d7cffdaa | ||
|
|
cc6429b64b | ||
|
|
53332285f3 | ||
|
|
fbfff5ba21 | ||
|
|
b4b3b29661 | ||
|
|
872f0a5d71 | ||
|
|
8a43d320cf | ||
|
|
fc97994ba2 | ||
|
|
b8d08736b5 | ||
|
|
2e761c690b | ||
|
|
1e248d72df | ||
|
|
be13cc915e | ||
|
|
a9ec7ab3b7 | ||
|
|
d58cc2cd7d | ||
|
|
245386f46f | ||
|
|
19a69c4204 | ||
|
|
ac2529f718 | ||
|
|
71d0873821 | ||
|
|
35e3a329c0 |
@@ -0,0 +1,106 @@
|
|||||||
|
# 飞牛论坛自动签到脚本
|
||||||
|
|
||||||
|
## 脚本功能
|
||||||
|
|
||||||
|
飞牛论坛自动签到脚本用于自动完成飞牛论坛的每日签到任务,支持以下功能:
|
||||||
|
|
||||||
|
- 自动登录飞牛论坛账号
|
||||||
|
- 自动识别验证码(通过百度OCR API)
|
||||||
|
- 每日自动签到
|
||||||
|
- 签到结果通过通知功能发送
|
||||||
|
|
||||||
|
## 依赖要求
|
||||||
|
|
||||||
|
### 软件依赖
|
||||||
|
- Python 3.6+
|
||||||
|
- 以下Python库:
|
||||||
|
- requests
|
||||||
|
- beautifulsoup4
|
||||||
|
|
||||||
|
### 安装命令
|
||||||
|
```bash
|
||||||
|
pip install requests beautifulsoup4
|
||||||
|
```
|
||||||
|
|
||||||
|
## 配置方法
|
||||||
|
|
||||||
|
### 1. 环境变量配置
|
||||||
|
|
||||||
|
#### (1) 账号配置 - FN_CONFIG
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"USERNAME": "你的飞牛论坛用户名",
|
||||||
|
"PASSWORD": "你的飞牛论坛密码",
|
||||||
|
"BASE_URL": "飞牛论坛基础URL(默认:https://club.fnnas.com/)"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### (2) 百度OCR API配置 - BAIDU_API_CONFIG
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"API_KEY": "你的百度OCR API Key",
|
||||||
|
"SECRET_KEY": "你的百度OCR Secret Key"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 百度OCR API申请
|
||||||
|
1. 注册百度智能云账号:https://cloud.baidu.com
|
||||||
|
2. 创建文字识别应用,获取API Key和Secret Key
|
||||||
|
3. 将获取的密钥填入环境变量
|
||||||
|
|
||||||
|
## 使用说明
|
||||||
|
|
||||||
|
### 运行方式
|
||||||
|
```bash
|
||||||
|
python3 sign_with_qinglong.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### 定时任务示例
|
||||||
|
```bash
|
||||||
|
0 8 * * * python3 sign_with_qinglong.py # 每天8点自动签到
|
||||||
|
```
|
||||||
|
|
||||||
|
## 通知功能
|
||||||
|
|
||||||
|
签到结果将通过通知功能发送,示例内容:
|
||||||
|
```
|
||||||
|
📅 签到日期: 2023-05-10 08:30:45
|
||||||
|
|
||||||
|
👤 账号: your_username
|
||||||
|
--------------------------------
|
||||||
|
✅ 签到成功!
|
||||||
|
|
||||||
|
📊 签到数据:
|
||||||
|
• 连续签到: 5天
|
||||||
|
• 累计签到: 30天
|
||||||
|
• 今日获得积分: 100
|
||||||
|
|
||||||
|
--------------------------------
|
||||||
|
💻 脚本运行完成!
|
||||||
|
🚀 下次见!
|
||||||
|
```
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
### 1. 验证码识别失败
|
||||||
|
- 确认百度OCR API密钥正确
|
||||||
|
- 检查验证码图片清晰度,可尝试更换OCR识别类型
|
||||||
|
- 避免频繁请求触发API频率限制
|
||||||
|
|
||||||
|
### 2. 登录失败
|
||||||
|
- 检查账号密码是否正确
|
||||||
|
- 确认论坛登录页面结构是否变更
|
||||||
|
- 尝试删除cookies.json后重新登录
|
||||||
|
|
||||||
|
### 3. 通知未收到
|
||||||
|
- 确保通知模块路径正确
|
||||||
|
- 手动测试通知功能:`python notify.py "测试通知" "测试内容"`
|
||||||
|
|
||||||
|
## 免责协议
|
||||||
|
|
||||||
|
1. 本脚本仅供个人学习和研究使用,请勿用于商业用途或其他非法目的。
|
||||||
|
2. 脚本运行可能涉及论坛账号登录和数据获取,请严格遵守飞牛论坛的用户协议和相关法律法规。
|
||||||
|
3. 作者不对脚本的可用性、准确性负责,使用本脚本产生的一切后果由使用者自行承担。
|
||||||
|
4. 如因使用本脚本导致账号异常、封禁或其他损失,作者不承担任何责任。
|
||||||
|
5. 若论坛调整页面结构或策略导致脚本失效,作者无义务持续维护更新。
|
||||||
|
6. 使用本脚本即视为同意上述条款,请勿在未经允许的情况下将脚本用于他人账号。
|
||||||
@@ -0,0 +1,731 @@
|
|||||||
|
"""
|
||||||
|
任务名称
|
||||||
|
name: 飞牛论坛签到
|
||||||
|
定时规则
|
||||||
|
cron: 0 0 8 * * ?
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import re
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import requests
|
||||||
|
import base64
|
||||||
|
import urllib.parse
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# 添加青龙脚本根目录到Python路径
|
||||||
|
QL_SCRIPTS_DIR = '/ql/scripts' # 青龙脚本默认目录
|
||||||
|
sys.path.append(QL_SCRIPTS_DIR)
|
||||||
|
|
||||||
|
# 添加notify可能存在的其他路径
|
||||||
|
POSSIBLE_PATHS = [
|
||||||
|
'/ql', # 青龙根目录
|
||||||
|
'/ql/data/scripts', # 新版青龙数据目录
|
||||||
|
'/ql/scripts/notify', # 自定义通知目录
|
||||||
|
os.path.dirname(__file__) # 当前脚本目录
|
||||||
|
]
|
||||||
|
|
||||||
|
for path in POSSIBLE_PATHS:
|
||||||
|
if os.path.exists(os.path.join(path, 'notify.py')):
|
||||||
|
sys.path.append(path)
|
||||||
|
break
|
||||||
|
|
||||||
|
try:
|
||||||
|
from notify import send
|
||||||
|
except ImportError:
|
||||||
|
print("⚠️ 无法加载通知模块,请检查路径配置")
|
||||||
|
send = lambda title, content: None # 创建空函数防止报错
|
||||||
|
|
||||||
|
class ConfigLoader:
|
||||||
|
"""从青龙环境变量加载配置"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_env_config():
|
||||||
|
"""获取环境变量配置"""
|
||||||
|
config = {}
|
||||||
|
|
||||||
|
# 从青龙环境变量获取配置
|
||||||
|
fn_config = os.getenv('FN_CONFIG', '')
|
||||||
|
baidu_api_config = os.getenv('BAIDU_API_CONFIG', '')
|
||||||
|
|
||||||
|
# 检查必要配置是否存在
|
||||||
|
if not fn_config or not baidu_api_config:
|
||||||
|
print("错误: 缺少必要的配置 - FN_CONFIG 和 BAIDU_API_CONFIG")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 解析配置
|
||||||
|
try:
|
||||||
|
fn_config = json.loads(fn_config)
|
||||||
|
baidu_api_config = json.loads(baidu_api_config)
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
print(f"配置解析失败: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 检查必要字段
|
||||||
|
required_fn_fields = ['USERNAME', 'PASSWORD', 'BASE_URL']
|
||||||
|
required_baidu_fields = ['API_KEY', 'SECRET_KEY']
|
||||||
|
|
||||||
|
missing_fn_fields = [field for field in required_fn_fields if field not in fn_config]
|
||||||
|
missing_baidu_fields = [field for field in required_baidu_fields if field not in baidu_api_config]
|
||||||
|
|
||||||
|
if missing_fn_fields or missing_baidu_fields:
|
||||||
|
missing_fields = missing_fn_fields + missing_baidu_fields
|
||||||
|
print(f"错误: 缺少必要的配置字段: {', '.join(missing_fields)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 设置配置
|
||||||
|
config.update(fn_config)
|
||||||
|
config.update(baidu_api_config)
|
||||||
|
|
||||||
|
# 设置其他配置
|
||||||
|
config['LOGIN_URL'] = config['BASE_URL'] + 'member.php?mod=logging&action=login'
|
||||||
|
config['SIGN_URL'] = config['BASE_URL'] + 'plugin.php?id=zqlj_sign'
|
||||||
|
config['COOKIE_FILE'] = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'cookies.json')
|
||||||
|
config['CAPTCHA_API_URL'] = "https://aip.baidubce.com/rest/2.0/ocr/v1/accurate_basic"
|
||||||
|
config['MAX_RETRIES'] = int(os.getenv('FN_MAX_RETRIES', 3))
|
||||||
|
config['RETRY_DELAY'] = int(os.getenv('FN_RETRY_DELAY', 2))
|
||||||
|
config['TOKEN_CACHE_FILE'] = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'token_cache.json')
|
||||||
|
|
||||||
|
return config
|
||||||
|
|
||||||
|
# 配置信息
|
||||||
|
class Config:
|
||||||
|
# 从环境变量加载配置
|
||||||
|
env_config = ConfigLoader.get_env_config()
|
||||||
|
|
||||||
|
if env_config:
|
||||||
|
# 账号信息
|
||||||
|
USERNAME = env_config['USERNAME']
|
||||||
|
PASSWORD = env_config['PASSWORD']
|
||||||
|
|
||||||
|
# 网站URL
|
||||||
|
BASE_URL = env_config['BASE_URL']
|
||||||
|
LOGIN_URL = env_config['LOGIN_URL']
|
||||||
|
SIGN_URL = env_config['SIGN_URL']
|
||||||
|
|
||||||
|
# Cookie文件路径
|
||||||
|
COOKIE_FILE = env_config['COOKIE_FILE']
|
||||||
|
|
||||||
|
# 验证码识别API (百度OCR API)
|
||||||
|
CAPTCHA_API_URL = env_config['CAPTCHA_API_URL']
|
||||||
|
API_KEY = env_config['API_KEY']
|
||||||
|
SECRET_KEY = env_config['SECRET_KEY']
|
||||||
|
|
||||||
|
# 重试设置
|
||||||
|
MAX_RETRIES = env_config['MAX_RETRIES']
|
||||||
|
RETRY_DELAY = env_config['RETRY_DELAY']
|
||||||
|
|
||||||
|
# Token缓存文件
|
||||||
|
TOKEN_CACHE_FILE = env_config['TOKEN_CACHE_FILE']
|
||||||
|
else:
|
||||||
|
# 如果缺少必要配置,设置默认值(将导致运行失败)
|
||||||
|
USERNAME = 'your_username'
|
||||||
|
PASSWORD = 'your_password'
|
||||||
|
API_KEY = 'your_api_key'
|
||||||
|
SECRET_KEY = 'your_secret_key'
|
||||||
|
BASE_URL = 'https://club.fnnas.com/'
|
||||||
|
|
||||||
|
class FNSignIn:
|
||||||
|
def __init__(self):
|
||||||
|
self.session = requests.Session()
|
||||||
|
self.session.headers.update({
|
||||||
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||||||
|
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
||||||
|
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8'
|
||||||
|
})
|
||||||
|
self.load_cookies()
|
||||||
|
|
||||||
|
def load_cookies(self):
|
||||||
|
"""从文件加载Cookie"""
|
||||||
|
if os.path.exists(Config.COOKIE_FILE):
|
||||||
|
try:
|
||||||
|
with open(Config.COOKIE_FILE, 'r') as f:
|
||||||
|
cookies_list = json.load(f)
|
||||||
|
|
||||||
|
# 检查是否为新格式的Cookie列表
|
||||||
|
if isinstance(cookies_list, list) and len(cookies_list) > 0 and 'name' in cookies_list[0]:
|
||||||
|
# 新格式:包含完整Cookie属性的列表
|
||||||
|
for cookie_dict in cookies_list:
|
||||||
|
self.session.cookies.set(
|
||||||
|
cookie_dict['name'],
|
||||||
|
cookie_dict['value'],
|
||||||
|
domain=cookie_dict.get('domain'),
|
||||||
|
path=cookie_dict.get('path')
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# 旧格式:简单的名称-值字典
|
||||||
|
self.session.cookies.update(cookies_list)
|
||||||
|
|
||||||
|
print("已从文件加载Cookie")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"加载Cookie失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def save_cookies(self):
|
||||||
|
"""保存Cookie到文件"""
|
||||||
|
try:
|
||||||
|
# 保存完整的Cookie信息,包括域名、路径等属性
|
||||||
|
cookies_list = []
|
||||||
|
for cookie in self.session.cookies:
|
||||||
|
cookie_dict = {
|
||||||
|
'name': cookie.name,
|
||||||
|
'value': cookie.value,
|
||||||
|
'domain': cookie.domain,
|
||||||
|
'path': cookie.path,
|
||||||
|
'expires': cookie.expires,
|
||||||
|
'secure': cookie.secure
|
||||||
|
}
|
||||||
|
cookies_list.append(cookie_dict)
|
||||||
|
|
||||||
|
with open(Config.COOKIE_FILE, 'w') as f:
|
||||||
|
json.dump(cookies_list, f)
|
||||||
|
print("Cookie已保存到文件")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"保存Cookie失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def check_login_status(self):
|
||||||
|
"""检查登录状态"""
|
||||||
|
try:
|
||||||
|
response = self.session.get(Config.BASE_URL)
|
||||||
|
soup = BeautifulSoup(response.text, 'html.parser')
|
||||||
|
|
||||||
|
# 检查是否存在登录链接,如果存在则表示未登录
|
||||||
|
login_links = soup.select('a[href*="member.php?mod=logging&action=login"]')
|
||||||
|
|
||||||
|
# 检查页面内容是否包含用户名
|
||||||
|
username_in_page = Config.USERNAME in response.text
|
||||||
|
|
||||||
|
# 检查是否有个人中心链接
|
||||||
|
user_center_links = soup.select('a[href*="home.php?mod=space"]')
|
||||||
|
|
||||||
|
# 如果没有登录链接或者页面中包含用户名,则认为已登录
|
||||||
|
if (len(login_links) == 0 or username_in_page) and len(user_center_links) > 0:
|
||||||
|
print("Cookie有效,已登录状态")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print("Cookie无效或已过期,需要重新登录")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
print(f"检查登录状态失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_access_token(self):
|
||||||
|
"""获取百度API的access_token,带缓存功能"""
|
||||||
|
try:
|
||||||
|
# 检查是否有缓存的token
|
||||||
|
if os.path.exists(Config.TOKEN_CACHE_FILE):
|
||||||
|
try:
|
||||||
|
with open(Config.TOKEN_CACHE_FILE, 'r') as f:
|
||||||
|
token_data = json.load(f)
|
||||||
|
# 检查token是否过期(百度token有效期为30天)
|
||||||
|
if token_data.get('expires_time', 0) > time.time():
|
||||||
|
print("使用缓存的access_token")
|
||||||
|
return token_data.get('access_token')
|
||||||
|
else:
|
||||||
|
print("缓存的access_token已过期,重新获取")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"读取token缓存文件失败: {e}")
|
||||||
|
|
||||||
|
# 获取新token
|
||||||
|
url = "https://aip.baidubce.com/oauth/2.0/token"
|
||||||
|
params = {
|
||||||
|
"grant_type": "client_credentials",
|
||||||
|
"client_id": Config.API_KEY,
|
||||||
|
"client_secret": Config.SECRET_KEY
|
||||||
|
}
|
||||||
|
|
||||||
|
# 添加重试机制
|
||||||
|
for retry in range(Config.MAX_RETRIES):
|
||||||
|
try:
|
||||||
|
response = requests.post(url, params=params)
|
||||||
|
if response.status_code == 200:
|
||||||
|
result = response.json()
|
||||||
|
access_token = str(result.get("access_token"))
|
||||||
|
expires_in = result.get("expires_in", 2592000) # 默认30天
|
||||||
|
|
||||||
|
# 缓存token
|
||||||
|
token_cache = {
|
||||||
|
'access_token': access_token,
|
||||||
|
'expires_time': time.time() + expires_in - 86400 # 提前一天过期
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
with open(Config.TOKEN_CACHE_FILE, 'w') as f:
|
||||||
|
json.dump(token_cache, f)
|
||||||
|
print("access_token已缓存")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"缓存access_token失败: {e}")
|
||||||
|
|
||||||
|
return access_token
|
||||||
|
else:
|
||||||
|
print(f"获取access_token失败,状态码: {response.status_code},重试({retry+1}/{Config.MAX_RETRIES})")
|
||||||
|
if retry < Config.MAX_RETRIES - 1:
|
||||||
|
time.sleep(Config.RETRY_DELAY)
|
||||||
|
continue
|
||||||
|
except Exception as e:
|
||||||
|
print(f"获取access_token请求异常: {e},重试({retry+1}/{Config.MAX_RETRIES})")
|
||||||
|
if retry < Config.MAX_RETRIES - 1:
|
||||||
|
time.sleep(Config.RETRY_DELAY)
|
||||||
|
continue
|
||||||
|
|
||||||
|
print(f"获取access_token失败,已达到最大重试次数({Config.MAX_RETRIES})")
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
print(f"获取access_token过程发生错误: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def recognize_captcha(self, captcha_url):
|
||||||
|
"""识别验证码,带重试机制"""
|
||||||
|
for retry in range(Config.MAX_RETRIES):
|
||||||
|
try:
|
||||||
|
# 下载验证码图片
|
||||||
|
captcha_response = self.session.get(captcha_url)
|
||||||
|
if captcha_response.status_code != 200:
|
||||||
|
print(f"下载验证码图片失败,状态码: {captcha_response.status_code},重试({retry+1}/{Config.MAX_RETRIES})")
|
||||||
|
if retry < Config.MAX_RETRIES - 1:
|
||||||
|
time.sleep(Config.RETRY_DELAY)
|
||||||
|
continue
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 将图片转换为Base64编码
|
||||||
|
captcha_base64 = base64.b64encode(captcha_response.content).decode('utf-8')
|
||||||
|
|
||||||
|
# 获取access_token
|
||||||
|
access_token = self.get_access_token()
|
||||||
|
if not access_token:
|
||||||
|
print(f"获取百度API access_token失败,重试({retry+1}/{Config.MAX_RETRIES})")
|
||||||
|
if retry < Config.MAX_RETRIES - 1:
|
||||||
|
time.sleep(Config.RETRY_DELAY)
|
||||||
|
continue
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 构建API请求URL
|
||||||
|
url = f"{Config.CAPTCHA_API_URL}?access_token={access_token}"
|
||||||
|
|
||||||
|
# 构建请求参数
|
||||||
|
payload = f'image={urllib.parse.quote_plus(captcha_base64)}&detect_direction=false¶graph=false&probability=false'
|
||||||
|
|
||||||
|
# 设置请求头
|
||||||
|
headers = {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'Accept': 'application/json'
|
||||||
|
}
|
||||||
|
|
||||||
|
# 发送请求
|
||||||
|
api_response = requests.request("POST", url, headers=headers, data=payload.encode("utf-8"))
|
||||||
|
|
||||||
|
if api_response.status_code != 200:
|
||||||
|
print(f"验证码识别API请求失败,状态码: {api_response.status_code},重试({retry+1}/{Config.MAX_RETRIES})")
|
||||||
|
if retry < Config.MAX_RETRIES - 1:
|
||||||
|
time.sleep(Config.RETRY_DELAY)
|
||||||
|
continue
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 解析API响应
|
||||||
|
result = api_response.json()
|
||||||
|
if 'words_result' in result and len(result['words_result']) > 0:
|
||||||
|
captcha_text = result['words_result'][0]['words']
|
||||||
|
# 清理验证码文本,移除空格和特殊字符
|
||||||
|
captcha_text = re.sub(r'[\s\W]+', '', captcha_text)
|
||||||
|
print(f"验证码识别成功: {captcha_text}")
|
||||||
|
return captcha_text
|
||||||
|
elif 'error_code' in result:
|
||||||
|
print(f"验证码识别API返回错误: {result.get('error_code')}, {result.get('error_msg')},重试({retry+1}/{Config.MAX_RETRIES})")
|
||||||
|
if retry < Config.MAX_RETRIES - 1:
|
||||||
|
time.sleep(Config.RETRY_DELAY)
|
||||||
|
continue
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
print(f"验证码识别API返回格式异常: {result},重试({retry+1}/{Config.MAX_RETRIES})")
|
||||||
|
if retry < Config.MAX_RETRIES - 1:
|
||||||
|
time.sleep(Config.RETRY_DELAY)
|
||||||
|
continue
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
print(f"验证码识别过程发生错误: {e},重试({retry+1}/{Config.MAX_RETRIES})")
|
||||||
|
if retry < Config.MAX_RETRIES - 1:
|
||||||
|
time.sleep(Config.RETRY_DELAY)
|
||||||
|
continue
|
||||||
|
return None
|
||||||
|
|
||||||
|
print(f"验证码识别失败,已达到最大重试次数({Config.MAX_RETRIES})")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def login(self):
|
||||||
|
"""使用账号密码登录,带重试机制"""
|
||||||
|
for retry in range(Config.MAX_RETRIES):
|
||||||
|
try:
|
||||||
|
# 获取登录页面
|
||||||
|
response = self.session.get(Config.LOGIN_URL)
|
||||||
|
soup = BeautifulSoup(response.text, 'html.parser')
|
||||||
|
|
||||||
|
# 获取登录表单信息
|
||||||
|
login_form = None
|
||||||
|
for form in soup.find_all('form'):
|
||||||
|
form_id = form.get('id', '')
|
||||||
|
if form_id and ('loginform' in form_id or 'lsform' in form_id):
|
||||||
|
login_form = form
|
||||||
|
break
|
||||||
|
elif form.get('name') == 'login':
|
||||||
|
login_form = form
|
||||||
|
break
|
||||||
|
elif form.get('action') and 'logging' in form.get('action'):
|
||||||
|
login_form = form
|
||||||
|
break
|
||||||
|
|
||||||
|
if not login_form:
|
||||||
|
# 尝试查找任何表单,可能是登录表单
|
||||||
|
all_forms = soup.find_all('form')
|
||||||
|
if all_forms:
|
||||||
|
login_form = all_forms[0] # 使用第一个表单
|
||||||
|
print(f"使用备选表单: ID={login_form.get('id')}, Action={login_form.get('action')}")
|
||||||
|
|
||||||
|
if not login_form:
|
||||||
|
print(f"未找到登录表单,重试({retry+1}/{Config.MAX_RETRIES})")
|
||||||
|
if retry < Config.MAX_RETRIES - 1:
|
||||||
|
time.sleep(Config.RETRY_DELAY)
|
||||||
|
continue
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 提取登录表单ID中的随机部分
|
||||||
|
form_id = login_form.get('id', '')
|
||||||
|
login_hash = form_id.split('_')[-1] if '_' in form_id else ''
|
||||||
|
|
||||||
|
# 获取登录表单的action属性
|
||||||
|
form_action = login_form.get('action', '')
|
||||||
|
print(f"找到登录表单: ID={form_id}, Action={form_action}")
|
||||||
|
|
||||||
|
# 获取表单字段
|
||||||
|
formhash = soup.find('input', {'name': 'formhash'})
|
||||||
|
if not formhash:
|
||||||
|
print(f"未找到登录表单的formhash字段,重试({retry+1}/{Config.MAX_RETRIES})")
|
||||||
|
if retry < Config.MAX_RETRIES - 1:
|
||||||
|
time.sleep(Config.RETRY_DELAY)
|
||||||
|
continue
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 获取表单字段
|
||||||
|
formhash = formhash['value']
|
||||||
|
|
||||||
|
# 获取用户名输入框ID
|
||||||
|
username_input = soup.find('input', {'name': 'username'})
|
||||||
|
username_id = username_input.get('id', '') if username_input else ''
|
||||||
|
|
||||||
|
# 获取密码输入框ID
|
||||||
|
password_input = soup.find('input', {'name': 'password'})
|
||||||
|
password_id = password_input.get('id', '') if password_input else ''
|
||||||
|
|
||||||
|
print(f"找到用户名输入框ID: {username_id}")
|
||||||
|
print(f"找到密码输入框ID: {password_id}")
|
||||||
|
|
||||||
|
# 构建登录数据
|
||||||
|
login_data = {
|
||||||
|
'formhash': formhash,
|
||||||
|
'referer': Config.BASE_URL,
|
||||||
|
'loginfield': 'username',
|
||||||
|
'username': Config.USERNAME,
|
||||||
|
'password': Config.PASSWORD,
|
||||||
|
'questionid': '0',
|
||||||
|
'answer': '',
|
||||||
|
'cookietime': '2592000', # 保持登录状态30天
|
||||||
|
'loginsubmit': 'true'
|
||||||
|
}
|
||||||
|
|
||||||
|
# 添加特定的表单字段
|
||||||
|
if username_id:
|
||||||
|
login_data[username_id] = Config.USERNAME
|
||||||
|
if password_id:
|
||||||
|
login_data[password_id] = Config.PASSWORD
|
||||||
|
|
||||||
|
# 检查是否需要验证码
|
||||||
|
seccodeverify = soup.find('input', {'name': 'seccodeverify'})
|
||||||
|
if seccodeverify:
|
||||||
|
print("检测到需要验证码,尝试自动识别验证码")
|
||||||
|
|
||||||
|
# 获取验证码ID
|
||||||
|
seccode_id = seccodeverify.get('id', '').replace('seccodeverify_', '')
|
||||||
|
|
||||||
|
# 获取验证码图片URL
|
||||||
|
captcha_img = soup.find('img', {'src': re.compile(r'misc\.php\?mod=seccode')})
|
||||||
|
if not captcha_img:
|
||||||
|
print(f"未找到验证码图片,重试({retry+1}/{Config.MAX_RETRIES})")
|
||||||
|
if retry < Config.MAX_RETRIES - 1:
|
||||||
|
time.sleep(Config.RETRY_DELAY)
|
||||||
|
continue
|
||||||
|
return False
|
||||||
|
|
||||||
|
captcha_url = Config.BASE_URL + captcha_img['src']
|
||||||
|
print(f"验证码图片URL: {captcha_url}")
|
||||||
|
|
||||||
|
# 识别验证码
|
||||||
|
captcha_text = self.recognize_captcha(captcha_url)
|
||||||
|
if not captcha_text:
|
||||||
|
print(f"验证码识别失败,重试({retry+1}/{Config.MAX_RETRIES})")
|
||||||
|
if retry < Config.MAX_RETRIES - 1:
|
||||||
|
time.sleep(Config.RETRY_DELAY)
|
||||||
|
continue
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 添加验证码到登录数据
|
||||||
|
login_data['seccodeverify'] = captcha_text
|
||||||
|
login_data['seccodehash'] = seccode_id
|
||||||
|
|
||||||
|
# 更新请求头,模拟真实浏览器
|
||||||
|
self.session.headers.update({
|
||||||
|
'Origin': Config.BASE_URL.rstrip('/'),
|
||||||
|
'Referer': Config.LOGIN_URL,
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'Upgrade-Insecure-Requests': '1'
|
||||||
|
})
|
||||||
|
|
||||||
|
# 构建登录URL
|
||||||
|
login_url = f"{Config.LOGIN_URL}&loginsubmit=yes&inajax=1"
|
||||||
|
|
||||||
|
# 发送登录请求
|
||||||
|
login_response = self.session.post(login_url, data=login_data, allow_redirects=True)
|
||||||
|
|
||||||
|
# 检查登录结果
|
||||||
|
if '验证码' in login_response.text and '验证码错误' in login_response.text:
|
||||||
|
print(f"验证码错误,登录失败,重试({retry+1}/{Config.MAX_RETRIES})")
|
||||||
|
if retry < Config.MAX_RETRIES - 1:
|
||||||
|
time.sleep(Config.RETRY_DELAY)
|
||||||
|
continue
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 检查登录是否成功
|
||||||
|
if 'succeedhandle_' in login_response.text or self.check_login_status():
|
||||||
|
print(f"账号 {Config.USERNAME} 登录成功")
|
||||||
|
self.save_cookies()
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print(f"登录失败,请检查账号密码,重试({retry+1}/{Config.MAX_RETRIES})")
|
||||||
|
if retry < Config.MAX_RETRIES - 1:
|
||||||
|
time.sleep(Config.RETRY_DELAY)
|
||||||
|
continue
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
print(f"登录过程发生错误: {e},重试({retry+1}/{Config.MAX_RETRIES})")
|
||||||
|
if retry < Config.MAX_RETRIES - 1:
|
||||||
|
time.sleep(Config.RETRY_DELAY)
|
||||||
|
continue
|
||||||
|
return False
|
||||||
|
|
||||||
|
print(f"登录失败,已达到最大重试次数({Config.MAX_RETRIES})")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def check_sign_status(self):
|
||||||
|
"""检查签到状态,带重试机制"""
|
||||||
|
for retry in range(Config.MAX_RETRIES):
|
||||||
|
try:
|
||||||
|
response = self.session.get(Config.SIGN_URL)
|
||||||
|
soup = BeautifulSoup(response.text, 'html.parser')
|
||||||
|
|
||||||
|
# 查找签到按钮
|
||||||
|
sign_btn = soup.select_one('.signbtn .btna')
|
||||||
|
if not sign_btn:
|
||||||
|
print(f"未找到签到按钮,重试({retry+1}/{Config.MAX_RETRIES})")
|
||||||
|
if retry < Config.MAX_RETRIES - 1:
|
||||||
|
time.sleep(Config.RETRY_DELAY)
|
||||||
|
continue
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
# 获取签到链接和状态
|
||||||
|
sign_text = sign_btn.text.strip()
|
||||||
|
sign_link = sign_btn.get('href')
|
||||||
|
|
||||||
|
# 提取sign参数
|
||||||
|
sign_param = None
|
||||||
|
if sign_link:
|
||||||
|
match = re.search(r'sign=([^&]+)', sign_link)
|
||||||
|
if match:
|
||||||
|
sign_param = match.group(1)
|
||||||
|
|
||||||
|
return sign_text, sign_param
|
||||||
|
except Exception as e:
|
||||||
|
print(f"检查签到状态失败: {e},重试({retry+1}/{Config.MAX_RETRIES})")
|
||||||
|
if retry < Config.MAX_RETRIES - 1:
|
||||||
|
time.sleep(Config.RETRY_DELAY)
|
||||||
|
continue
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
def do_sign(self, sign_param):
|
||||||
|
"""执行签到,带重试机制"""
|
||||||
|
for retry in range(Config.MAX_RETRIES):
|
||||||
|
try:
|
||||||
|
sign_url = f"{Config.SIGN_URL}&sign={sign_param}"
|
||||||
|
response = self.session.get(sign_url)
|
||||||
|
|
||||||
|
# 检查签到结果
|
||||||
|
if response.status_code == 200:
|
||||||
|
# 再次检查签到状态
|
||||||
|
sign_text, _ = self.check_sign_status()
|
||||||
|
if sign_text == "今日已打卡":
|
||||||
|
print("签到成功")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print(f"签到请求已发送,但状态未更新,重试({retry+1}/{Config.MAX_RETRIES})")
|
||||||
|
if retry < Config.MAX_RETRIES - 1:
|
||||||
|
time.sleep(Config.RETRY_DELAY)
|
||||||
|
continue
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
print(f"签到请求失败,状态码: {response.status_code},重试({retry+1}/{Config.MAX_RETRIES})")
|
||||||
|
if retry < Config.MAX_RETRIES - 1:
|
||||||
|
time.sleep(Config.RETRY_DELAY)
|
||||||
|
continue
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
print(f"签到过程发生错误: {e},重试({retry+1}/{Config.MAX_RETRIES})")
|
||||||
|
if retry < Config.MAX_RETRIES - 1:
|
||||||
|
time.sleep(Config.RETRY_DELAY)
|
||||||
|
continue
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_sign_info(self):
|
||||||
|
"""获取签到信息,带重试机制"""
|
||||||
|
for retry in range(Config.MAX_RETRIES):
|
||||||
|
try:
|
||||||
|
response = self.session.get(Config.SIGN_URL)
|
||||||
|
soup = BeautifulSoup(response.text, 'html.parser')
|
||||||
|
|
||||||
|
# 查找签到信息区域
|
||||||
|
sign_info_divs = soup.find_all('div', class_='bm')
|
||||||
|
sign_info_div = None
|
||||||
|
for div in sign_info_divs:
|
||||||
|
header = div.find('div', class_='bm_h')
|
||||||
|
if header and '我的打卡动态' in header.get_text():
|
||||||
|
sign_info_div = div
|
||||||
|
break
|
||||||
|
|
||||||
|
if not sign_info_div:
|
||||||
|
print(f"未找到签到信息区域,重试({retry+1}/{Config.MAX_RETRIES})")
|
||||||
|
if retry < Config.MAX_RETRIES - 1:
|
||||||
|
time.sleep(Config.RETRY_DELAY)
|
||||||
|
continue
|
||||||
|
return {}
|
||||||
|
|
||||||
|
# 查找签到信息列表
|
||||||
|
info_list = sign_info_div.find('div', class_='bm_c').find_all('li')
|
||||||
|
|
||||||
|
# 解析签到信息
|
||||||
|
sign_info = {}
|
||||||
|
for item in info_list:
|
||||||
|
text = item.get_text(strip=True)
|
||||||
|
if ':' in text:
|
||||||
|
key, value = text.split(':', 1)
|
||||||
|
sign_info[key] = value
|
||||||
|
|
||||||
|
return sign_info
|
||||||
|
except Exception as e:
|
||||||
|
print(f"获取签到信息失败: {e},重试({retry+1}/{Config.MAX_RETRIES})")
|
||||||
|
if retry < Config.MAX_RETRIES - 1:
|
||||||
|
time.sleep(Config.RETRY_DELAY)
|
||||||
|
continue
|
||||||
|
return {}
|
||||||
|
|
||||||
|
print(f"获取签到信息失败,已达到最大重试次数({Config.MAX_RETRIES})")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"""运行签到流程,带重试机制"""
|
||||||
|
print("===== 开始运行签到脚本 =====")
|
||||||
|
|
||||||
|
# 检查登录状态
|
||||||
|
if not self.check_login_status():
|
||||||
|
# 如果未登录,尝试登录
|
||||||
|
if not self.login():
|
||||||
|
print("登录失败,签到流程终止")
|
||||||
|
return False, "登录失败"
|
||||||
|
|
||||||
|
# 检查签到状态
|
||||||
|
sign_text, sign_param = self.check_sign_status()
|
||||||
|
if sign_text is None or sign_param is None:
|
||||||
|
print("获取签到状态失败,签到流程终止")
|
||||||
|
return False, "获取签到状态失败"
|
||||||
|
|
||||||
|
print(f"当前签到状态: {sign_text}")
|
||||||
|
|
||||||
|
# 初始化通知内容
|
||||||
|
notify_title = f"飞牛论坛签到通知"
|
||||||
|
notify_content = f"📅 签到日期: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n"
|
||||||
|
notify_content += f"👤 账号: {Config.USERNAME}\n"
|
||||||
|
notify_content += "--------------------------------\n"
|
||||||
|
|
||||||
|
# 如果未签到,执行签到
|
||||||
|
if sign_text == "点击打卡":
|
||||||
|
print("开始执行签到...")
|
||||||
|
if self.do_sign(sign_param):
|
||||||
|
# 获取并记录签到信息
|
||||||
|
sign_info = self.get_sign_info()
|
||||||
|
if sign_info:
|
||||||
|
print("===== 签到信息 =====")
|
||||||
|
notify_content += "✅ 签到成功!\n\n"
|
||||||
|
notify_content += "📊 签到数据:\n"
|
||||||
|
for key, value in sign_info.items():
|
||||||
|
print(f"{key}: {value}")
|
||||||
|
notify_content += f" • {key}: {value}\n"
|
||||||
|
else:
|
||||||
|
notify_content += "✅ 签到成功!\n"
|
||||||
|
notify_content += " • 未能获取详细签到数据\n"
|
||||||
|
else:
|
||||||
|
notify_content += "❌ 签到失败!\n"
|
||||||
|
notify_content += " • 请检查日志查看详细原因\n"
|
||||||
|
print("签到失败")
|
||||||
|
# 发送通知
|
||||||
|
send(notify_title, notify_content)
|
||||||
|
return False, "签到失败"
|
||||||
|
elif sign_text == "今日已打卡":
|
||||||
|
print("今日已签到,无需重复签到")
|
||||||
|
# 获取并记录签到信息
|
||||||
|
sign_info = self.get_sign_info()
|
||||||
|
if sign_info:
|
||||||
|
print("===== 签到信息 =====")
|
||||||
|
notify_content += "✅ 今日已签到!\n\n"
|
||||||
|
notify_content += "📊 签到数据:\n"
|
||||||
|
for key, value in sign_info.items():
|
||||||
|
print(f"{key}: {value}")
|
||||||
|
notify_content += f" • {key}: {value}\n"
|
||||||
|
else:
|
||||||
|
notify_content += "✅ 今日已签到!\n"
|
||||||
|
notify_content += " • 未能获取详细签到数据\n"
|
||||||
|
else:
|
||||||
|
notify_content += f"⚠️ 未知的签到状态: {sign_text},签到流程终止!\n"
|
||||||
|
print(f"未知的签到状态: {sign_text},签到流程终止")
|
||||||
|
# 发送通知
|
||||||
|
send(notify_title, notify_content)
|
||||||
|
return False, f"未知的签到状态: {sign_text}"
|
||||||
|
|
||||||
|
# 添加底部信息
|
||||||
|
notify_content += "\n--------------------------------\n"
|
||||||
|
notify_content += "💻 脚本运行完成!\n"
|
||||||
|
notify_content += "🚀 下次见!"
|
||||||
|
|
||||||
|
# 发送通知
|
||||||
|
send(notify_title, notify_content)
|
||||||
|
|
||||||
|
return True, "签到成功" if sign_text == "点击打卡" else "今日已签到"
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
# 创建签到实例并运行
|
||||||
|
sign = FNSignIn()
|
||||||
|
result, message = sign.run()
|
||||||
|
|
||||||
|
# 输出最终结果
|
||||||
|
if result:
|
||||||
|
print("===== 签到脚本执行成功 =====")
|
||||||
|
else:
|
||||||
|
print("===== 签到脚本执行失败 =====")
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("脚本被用户中断")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"脚本运行出错: {e}")
|
||||||
|
# 输出详细的异常堆栈信息
|
||||||
|
import traceback
|
||||||
|
print(traceback.format_exc())
|
||||||
@@ -1,3 +1,61 @@
|
|||||||
# qinglong-scripts
|
# 青龙脚本仓库 | QingLong Scripts
|
||||||
|
|
||||||
青龙脚本
|
青龙面板(QL)自动化脚本集合,包含各类签到、任务及实用工具。定期维护更新,确保稳定运行。
|
||||||
|
|
||||||
|
## 📖 脚本列表
|
||||||
|
|
||||||
|
| 脚本名称 | 功能描述 | 配置说明 | 依赖库 | 可用状态 |
|
||||||
|
|------------------------|-----------------------------------|-----------------------------------|----------------------|-----------------------------------|
|
||||||
|
| `ik_signin.py` | iKuuu签到 | 需配置 `IKUUU_ACCOUNTS` 环境变量 | `requests`、`beautifulsoup4` | ✅ |
|
||||||
|
| `tc_signin.py` | 同程旅行签到 | 需配置 `tongcheng` 环境变量 | `aiohttp`、`requests` | ✅ |
|
||||||
|
| `tb_signin` | 百度贴吧签到 | 需配置 `TIEBA` 环境变量 | `requests` | ✅ |
|
||||||
|
| `fn_signin` | 飞牛论坛签到 | 需配置 `FN_CONFIG`、`BAIDU_API_CONFIG` 环境变量 | `requests`、`beautifulsoup4` | ✅ |
|
||||||
|
|
||||||
|
## 🚀 使用说明
|
||||||
|
|
||||||
|
### 1. 安装青龙面板
|
||||||
|
推荐使用Docker部署:
|
||||||
|
```bash
|
||||||
|
docker run -dit \
|
||||||
|
-v $PWD/ql/config:/ql/config \
|
||||||
|
-v $PWD/ql/log:/ql/log \
|
||||||
|
-v $PWD/ql/db:/ql/db \
|
||||||
|
-v $PWD/ql/repo:/ql/repo \
|
||||||
|
-v $PWD/ql/raw:/ql/raw \
|
||||||
|
-v $PWD/ql/scripts:/ql/scripts \
|
||||||
|
-p 5700:5700 \
|
||||||
|
--name qinglong \
|
||||||
|
--hostname qinglong \
|
||||||
|
--restart always \
|
||||||
|
whyour/qinglong:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 添加仓库
|
||||||
|
1. 打开青龙面板,进入「依赖管理」安装必要依赖
|
||||||
|
2. 进入「定时任务」,点击「新建任务」
|
||||||
|
3. 填写以下信息:
|
||||||
|
- 名称:自定义(如"拉取脚本")
|
||||||
|
- 命令:`ql repo https://gitea.uptoz.cn/UPToZ/qinglong-scripts.git
|
||||||
|
- 定时规则:`0 0 * * *`(每天0点执行)
|
||||||
|
|
||||||
|
### 3. 配置环境变量
|
||||||
|
1. 进入青龙面板「环境变量」
|
||||||
|
2. 按需添加脚本所需的Cookie或配置项
|
||||||
|
3. 格式参考各脚本配置说明
|
||||||
|
|
||||||
|
### 消息推送配置
|
||||||
|
修改`utils/notify.js`中的推送参数,支持多种推送方式:
|
||||||
|
```javascript
|
||||||
|
// 示例:配置Telegram推送
|
||||||
|
const TG_BOT_TOKEN = '你的BotToken';
|
||||||
|
const TG_USER_ID = '你的用户ID';
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📌 注意事项
|
||||||
|
- 部分脚本需要特定依赖,请确保在青龙面板中安装
|
||||||
|
- 请勿频繁运行脚本,避免触发平台风控
|
||||||
|
- 本仓库脚本仅供学习交流,请勿用于商业用途
|
||||||
|
|
||||||
|
|
||||||
|
## 📄 许可证
|
||||||
|
本仓库代码采用 [GPL-3.0](https://www.gnu.org/licenses/gpl-3.0.en.html) 许可证开源。使用时请遵守开源协议。
|
||||||
+152
@@ -0,0 +1,152 @@
|
|||||||
|
# 百度贴吧自动签到脚本
|
||||||
|
|
||||||
|
## 项目简介
|
||||||
|
|
||||||
|
这是一个用于百度贴吧自动签到的青龙面板脚本,支持多账号管理、自动获取关注的贴吧列表并执行签到操作,同时提供签到结果通知功能。脚本通过读取青龙环境变量或配置文件中的Cookie信息,实现无人值守的贴吧签到任务。
|
||||||
|
|
||||||
|
## 功能特点
|
||||||
|
|
||||||
|
- 自动获取所有关注的百度贴吧并执行签到
|
||||||
|
- 支持多账号同时管理和签到
|
||||||
|
- 智能处理已签到、被屏蔽的贴吧
|
||||||
|
- 详细的签到结果统计和日志输出
|
||||||
|
- 支持青龙面板通知功能,实时推送签到结果
|
||||||
|
- 自动处理网络请求异常,包含重试机制
|
||||||
|
|
||||||
|
## 前提条件
|
||||||
|
|
||||||
|
1. 已部署青龙面板环境(v2或v2.1+)
|
||||||
|
2. 拥有有效的百度账号Cookie(包含BDUSS字段)
|
||||||
|
3. 青龙面板已安装通知模块(可选,用于推送签到结果)
|
||||||
|
|
||||||
|
## 配置步骤
|
||||||
|
|
||||||
|
### 1. 获取百度贴吧Cookie
|
||||||
|
|
||||||
|
#### 获取BDUSS的方法:
|
||||||
|
|
||||||
|
1. 使用浏览器登录百度账号
|
||||||
|
2. 访问百度贴吧任意页面(如 https://tieba.baidu.com)
|
||||||
|
3. 按F12打开开发者工具,切换到"应用程序"(Application)选项卡
|
||||||
|
4. 在左侧菜单中选择"Cookie",找到`BDUSS`字段
|
||||||
|
5. 复制`BDUSS`的值,格式类似:`xxx&yyy=zzz; ...`
|
||||||
|
|
||||||
|
#### 完整Cookie获取方法:
|
||||||
|
|
||||||
|
1. 登录百度贴吧后,右键网页空白处选择"检查"
|
||||||
|
2. 在开发者工具中切换到"网络"(Network)选项卡
|
||||||
|
3. 刷新页面,找到任意请求(如`tbs`或`like`)
|
||||||
|
4. 在请求详情中找到"请求标头"(Request Headers)
|
||||||
|
5. 复制整个`Cookie`字段的值
|
||||||
|
|
||||||
|
### 2. 配置青龙环境变量
|
||||||
|
|
||||||
|
在青龙面板中添加环境变量:
|
||||||
|
|
||||||
|
1. 进入青龙面板 -> 环境变量 -> 新增
|
||||||
|
2. 变量名:`TIEBA`
|
||||||
|
3. 变量值:粘贴获取的Cookie(多账号用换行分隔)
|
||||||
|
|
||||||
|
**多账号示例:**
|
||||||
|
```
|
||||||
|
BDUSS=xxx1; STOKEN=yyy1; ...
|
||||||
|
BDUSS=xxx2; STOKEN=yyy2; ...
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 配置文件方式(可选)
|
||||||
|
|
||||||
|
如果不想使用环境变量,也可以在青龙中创建配置文件:
|
||||||
|
|
||||||
|
1. 在青龙脚本目录下创建`config.json`文件
|
||||||
|
2. 内容格式:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"TIEBA": [
|
||||||
|
{"cookie": "BDUSS=xxx1; STOKEN=yyy1; ..."},
|
||||||
|
{"cookie": "BDUSS=xxx2; STOKEN=yyy2; ..."}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
3. 脚本会优先读取环境变量,若不存在则尝试读取配置文件
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
1. 将脚本文件上传到青龙面板的`scripts`目录
|
||||||
|
2. 在青龙面板中手动运行脚本,或设置定时任务
|
||||||
|
3. 查看脚本运行日志或接收通知获取签到结果
|
||||||
|
|
||||||
|
## 定时任务设置
|
||||||
|
|
||||||
|
在青龙面板中设置定时任务:
|
||||||
|
|
||||||
|
1. 进入青龙面板 -> 定时任务 -> 新增
|
||||||
|
2. 任务名称:自定义(如"百度贴吧签到")
|
||||||
|
3. 命令:`python3 /ql/scripts/tieba_sign.py`(根据实际路径调整)
|
||||||
|
4. Cron表达式:`0 0 1 * * ?`(每天凌晨1点执行,可自定义)
|
||||||
|
|
||||||
|
**常用Cron示例:**
|
||||||
|
- 每天0点执行:`0 0 * * * ?`
|
||||||
|
- 每周一0点执行:`0 0 * * 1 ?`
|
||||||
|
- 每月1-5号0点执行:`0 0 1-5 * * ?`
|
||||||
|
|
||||||
|
## 通知功能配置
|
||||||
|
|
||||||
|
脚本支持青龙面板的通知功能,需确保青龙已安装`notify.py`通知模块:
|
||||||
|
|
||||||
|
1. 确保青龙面板中存在`notify.py`文件
|
||||||
|
2. 脚本会自动调用`notify.send`方法发送通知
|
||||||
|
3. 签到完成后会汇总所有账号结果并发送通知
|
||||||
|
|
||||||
|
## 脚本日志说明
|
||||||
|
|
||||||
|
脚本运行时会输出详细日志:
|
||||||
|
|
||||||
|
- 成功签到:显示贴吧名称和签到排名
|
||||||
|
- 已签到:提示今日已完成签到
|
||||||
|
- 被屏蔽:显示贴吧已被屏蔽
|
||||||
|
- 错误信息:包含具体错误码和原因
|
||||||
|
- 账号间延迟:处理多账号时的等待时间
|
||||||
|
- 汇总统计:显示各账号签到结果和总体统计
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
### 1. 签到失败,提示"登录失败,Cookie异常"
|
||||||
|
|
||||||
|
- 原因:Cookie已过期或缺少必要字段
|
||||||
|
- 解决方法:重新获取最新Cookie,确保包含`BDUSS`字段
|
||||||
|
|
||||||
|
### 2. 无法获取贴吧列表
|
||||||
|
|
||||||
|
- 原因:网络请求异常或账号权限问题
|
||||||
|
- 解决方法:检查网络连接,尝试更换Cookie
|
||||||
|
|
||||||
|
### 3. 提示"今日已签到"
|
||||||
|
|
||||||
|
- 原因:该贴吧当天已完成签到
|
||||||
|
- 解决方法:无需处理,脚本会自动跳过已签到的贴吧
|
||||||
|
|
||||||
|
### 4. 通知发送失败
|
||||||
|
|
||||||
|
- 原因:青龙未安装通知模块或模块路径错误
|
||||||
|
- 解决方法:安装`notify.py`通知模块,或检查模块路径
|
||||||
|
|
||||||
|
### 5. 多账号处理时速度过慢
|
||||||
|
|
||||||
|
- 原因:脚本为避免频繁请求设置了延迟
|
||||||
|
- 解决方法:如需加快速度,可调整`sign_forums`方法中的延迟参数
|
||||||
|
|
||||||
|
## 脚本更新
|
||||||
|
|
||||||
|
1. 如需更新脚本,直接替换青龙面板中的脚本文件
|
||||||
|
2. 建议定期更新以适应百度贴吧接口变化
|
||||||
|
3. 若签到失败率突然升高,可能是接口变动,需检查脚本是否需要更新
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. 请勿频繁使用同一Cookie大量签到,避免账号异常
|
||||||
|
2. Cookie有效期通常为7-15天,建议定期更新
|
||||||
|
3. 脚本仅供个人使用,请勿用于商业用途
|
||||||
|
4. 百度可能随时变更接口,脚本可能需要不定期维护
|
||||||
|
5. 多账号处理时,账号间延迟可避免触发风控机制
|
||||||
|
|
||||||
|
通过以上步骤配置后,脚本将自动完成百度贴吧的签到任务,并通过通知推送结果,实现无人值守的贴吧签到管理。
|
||||||
@@ -0,0 +1,353 @@
|
|||||||
|
"""
|
||||||
|
任务名称
|
||||||
|
name: 百度贴吧签到
|
||||||
|
定时规则
|
||||||
|
cron: 0 0 1 * * ?
|
||||||
|
"""
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from typing import Optional, Union
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
class Tieba(object):
|
||||||
|
name = "百度贴吧"
|
||||||
|
|
||||||
|
def __init__(self, check_item: dict):
|
||||||
|
self.TBS_URL = "http://tieba.baidu.com/dc/common/tbs"
|
||||||
|
self.LIKE_URL = "http://c.tieba.baidu.com/c/f/forum/like"
|
||||||
|
self.SIGN_URL = "http://c.tieba.baidu.com/c/c/forum/sign"
|
||||||
|
self.LOGIN_INFO_URL = "https://zhidao.baidu.com/api/loginInfo"
|
||||||
|
self.SIGN_KEY = "tiebaclient!!!"
|
||||||
|
|
||||||
|
self.HEADERS = {
|
||||||
|
"Host": "tieba.baidu.com",
|
||||||
|
"User-Agent": "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.71 Safari/537.36",
|
||||||
|
"Connection": "keep-alive",
|
||||||
|
"Accept-Encoding": "gzip, deflate",
|
||||||
|
"Cache-Control": "no-cache",
|
||||||
|
}
|
||||||
|
|
||||||
|
self.SIGN_DATA = {
|
||||||
|
"_client_type": "2",
|
||||||
|
"_client_version": "9.7.8.0",
|
||||||
|
"_phone_imei": "000000000000000",
|
||||||
|
"model": "MI+5",
|
||||||
|
"net_type": "1",
|
||||||
|
}
|
||||||
|
|
||||||
|
self.session = requests.Session()
|
||||||
|
self.session.headers.update(self.HEADERS)
|
||||||
|
|
||||||
|
cookie = check_item.get("cookie")
|
||||||
|
if not cookie:
|
||||||
|
raise ValueError("必须提供 BDUSS 或完整 Cookie")
|
||||||
|
|
||||||
|
cookie_dict = {
|
||||||
|
item.split("=")[0]: item.split("=")[1]
|
||||||
|
for item in cookie.split("; ")
|
||||||
|
if "=" in item
|
||||||
|
}
|
||||||
|
requests.utils.add_dict_to_cookiejar(self.session.cookies, cookie_dict)
|
||||||
|
self.bduss = cookie_dict.get("BDUSS", "")
|
||||||
|
if not self.bduss:
|
||||||
|
raise ValueError("Cookie 中未找到 BDUSS")
|
||||||
|
|
||||||
|
def request(
|
||||||
|
self, url: str, method: str = "get", data: Optional[dict] = None, retry: int = 3
|
||||||
|
) -> dict:
|
||||||
|
for i in range(retry):
|
||||||
|
try:
|
||||||
|
if method.lower() == "get":
|
||||||
|
response = self.session.get(url, timeout=10)
|
||||||
|
else:
|
||||||
|
response = self.session.post(url, data=data, timeout=10)
|
||||||
|
|
||||||
|
response.raise_for_status()
|
||||||
|
if not response.text.strip():
|
||||||
|
raise ValueError("空响应内容")
|
||||||
|
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
if i == retry - 1:
|
||||||
|
raise Exception(f"请求失败: {str(e)}")
|
||||||
|
|
||||||
|
wait_time = 1.5 * (2**i) + random.uniform(0, 1)
|
||||||
|
time.sleep(wait_time)
|
||||||
|
|
||||||
|
raise Exception(f"请求失败,已达最大重试次数 {retry}")
|
||||||
|
|
||||||
|
def encode_data(self, data: dict) -> dict:
|
||||||
|
s = ""
|
||||||
|
for key in sorted(data.keys()):
|
||||||
|
s += f"{key}={data[key]}"
|
||||||
|
sign = hashlib.md5((s + self.SIGN_KEY).encode("utf-8")).hexdigest().upper()
|
||||||
|
data.update({"sign": sign})
|
||||||
|
return data
|
||||||
|
|
||||||
|
def get_user_info(self) -> tuple[Union[str, bool], str]:
|
||||||
|
try:
|
||||||
|
result = self.request(self.TBS_URL)
|
||||||
|
if result.get("is_login", 0) == 0:
|
||||||
|
return False, "登录失败,Cookie 异常"
|
||||||
|
tbs = result.get("tbs", "")
|
||||||
|
try:
|
||||||
|
user_info = self.request(self.LOGIN_INFO_URL)
|
||||||
|
user_name = user_info.get("userName", "未知用户")
|
||||||
|
except Exception:
|
||||||
|
user_name = "未知用户"
|
||||||
|
return tbs, user_name
|
||||||
|
except Exception as e:
|
||||||
|
return False, f"登录验证异常: {e}"
|
||||||
|
|
||||||
|
def get_favorite(self) -> list[dict]:
|
||||||
|
forums = []
|
||||||
|
page_no = 1
|
||||||
|
|
||||||
|
while True:
|
||||||
|
data = {
|
||||||
|
"BDUSS": self.bduss,
|
||||||
|
"_client_type": "2",
|
||||||
|
"_client_id": "wappc_1534235498291_488",
|
||||||
|
"_client_version": "9.7.8.0",
|
||||||
|
"_phone_imei": "000000000000000",
|
||||||
|
"from": "1008621y",
|
||||||
|
"page_no": str(page_no),
|
||||||
|
"page_size": "200",
|
||||||
|
"model": "MI+5",
|
||||||
|
"net_type": "1",
|
||||||
|
"timestamp": str(int(time.time())),
|
||||||
|
"vcode_tag": "11",
|
||||||
|
}
|
||||||
|
data = self.encode_data(data)
|
||||||
|
|
||||||
|
try:
|
||||||
|
res = self.request(self.LIKE_URL, "post", data)
|
||||||
|
|
||||||
|
if "forum_list" in res:
|
||||||
|
for forum_type in ["non-gconforum", "gconforum"]:
|
||||||
|
if forum_type in res["forum_list"]:
|
||||||
|
items = res["forum_list"][forum_type]
|
||||||
|
if isinstance(items, list):
|
||||||
|
forums.extend(items)
|
||||||
|
elif isinstance(items, dict):
|
||||||
|
forums.append(items)
|
||||||
|
|
||||||
|
if res.get("has_more") != "1":
|
||||||
|
break
|
||||||
|
|
||||||
|
page_no += 1
|
||||||
|
time.sleep(random.uniform(1, 2))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"获取贴吧列表出错: {e}")
|
||||||
|
break
|
||||||
|
|
||||||
|
print(f"共获取到 {len(forums)} 个关注的贴吧")
|
||||||
|
return forums
|
||||||
|
|
||||||
|
def sign_forums(self, forums, tbs: str) -> dict:
|
||||||
|
success_count, error_count, exist_count, shield_count = 0, 0, 0, 0
|
||||||
|
total = len(forums)
|
||||||
|
print(f"开始签到 {total} 个贴吧")
|
||||||
|
last_request_time = time.time()
|
||||||
|
for idx, forum in enumerate(forums):
|
||||||
|
elapsed = time.time() - last_request_time
|
||||||
|
delay = max(0, 1.0 + random.uniform(0.5, 1.5) - elapsed)
|
||||||
|
time.sleep(delay)
|
||||||
|
last_request_time = time.time()
|
||||||
|
if (idx + 1) % 10 == 0:
|
||||||
|
extra_delay = random.uniform(5, 10)
|
||||||
|
print(f"已签到 {idx + 1}/{total} 个贴吧,休息 {extra_delay:.2f} 秒")
|
||||||
|
time.sleep(extra_delay)
|
||||||
|
|
||||||
|
forum_name = forum.get("name", "")
|
||||||
|
forum_id = forum.get("id", "")
|
||||||
|
log_prefix = f"【{forum_name}】吧({idx + 1}/{total})"
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = self.SIGN_DATA.copy()
|
||||||
|
data.update(
|
||||||
|
{
|
||||||
|
"BDUSS": self.bduss,
|
||||||
|
"fid": forum_id,
|
||||||
|
"kw": forum_name,
|
||||||
|
"tbs": tbs,
|
||||||
|
"timestamp": str(int(time.time())),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
data = self.encode_data(data)
|
||||||
|
result = self.request(self.SIGN_URL, "post", data)
|
||||||
|
error_code = result.get("error_code", "")
|
||||||
|
if error_code == "0":
|
||||||
|
success_count += 1
|
||||||
|
if "user_info" in result:
|
||||||
|
rank = result["user_info"]["user_sign_rank"]
|
||||||
|
print(f"{log_prefix} 签到成功,第{rank}个签到")
|
||||||
|
else:
|
||||||
|
print(f"{log_prefix} 签到成功")
|
||||||
|
elif error_code == "160002":
|
||||||
|
exist_count += 1
|
||||||
|
print(f"{log_prefix} {result.get('error_msg', '今日已签到')}")
|
||||||
|
elif error_code == "340006":
|
||||||
|
shield_count += 1
|
||||||
|
print(f"{log_prefix} 贴吧已被屏蔽")
|
||||||
|
else:
|
||||||
|
error_count += 1
|
||||||
|
print(
|
||||||
|
f"{log_prefix} 签到失败,错误: {result.get('error_msg', '未知错误')}"
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_count += 1
|
||||||
|
print(f"{log_prefix} 签到异常: {str(e)}")
|
||||||
|
return {
|
||||||
|
"total": total,
|
||||||
|
"success": success_count,
|
||||||
|
"exist": exist_count,
|
||||||
|
"shield": shield_count,
|
||||||
|
"error": error_count,
|
||||||
|
}
|
||||||
|
|
||||||
|
def main(self) -> str:
|
||||||
|
try:
|
||||||
|
tbs, user_name = self.get_user_info()
|
||||||
|
if not tbs:
|
||||||
|
return f"账号: {user_name}\n登录状态: Cookie可能已过期"
|
||||||
|
forums = self.get_favorite()
|
||||||
|
|
||||||
|
if forums:
|
||||||
|
stats = self.sign_forums(forums, tbs)
|
||||||
|
msg = [
|
||||||
|
{"name": "帐号信息", "value": user_name},
|
||||||
|
{"name": "贴吧总数", "value": stats["total"]},
|
||||||
|
{"name": "签到成功", "value": stats["success"]},
|
||||||
|
{"name": "已经签到", "value": stats["exist"]},
|
||||||
|
{"name": "被屏蔽的", "value": stats["shield"]},
|
||||||
|
{"name": "签到失败", "value": stats["error"]},
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
msg = [
|
||||||
|
{"name": "帐号信息", "value": user_name},
|
||||||
|
{"name": "获取贴吧列表失败,无法完成签到", "value": ""},
|
||||||
|
]
|
||||||
|
except Exception as e:
|
||||||
|
msg = [
|
||||||
|
{"name": "帐号信息", "value": "未知用户"},
|
||||||
|
{"name": "签到失败", "value": str(e)},
|
||||||
|
]
|
||||||
|
msg = "\n".join([f"{one.get('name')}: {one.get('value')}" for one in msg])
|
||||||
|
return msg
|
||||||
|
|
||||||
|
|
||||||
|
# 从青龙环境变量读取Cookie并处理多账户
|
||||||
|
def get_tieba_cookies():
|
||||||
|
cookies = []
|
||||||
|
# 尝试从环境变量读取
|
||||||
|
tieba_env = os.getenv('TIEBA', '')
|
||||||
|
|
||||||
|
if not tieba_env:
|
||||||
|
print("未找到环境变量 TIEBA")
|
||||||
|
# 尝试从配置文件读取
|
||||||
|
try:
|
||||||
|
with open(
|
||||||
|
os.path.join(os.path.dirname(os.path.dirname(__file__)), "config.json"),
|
||||||
|
encoding="utf-8",
|
||||||
|
) as f:
|
||||||
|
datas = json.loads(f.read())
|
||||||
|
config_cookies = datas.get("TIEBA", [])
|
||||||
|
for item in config_cookies:
|
||||||
|
if isinstance(item, dict) and 'cookie' in item:
|
||||||
|
cookies.append(item['cookie'])
|
||||||
|
if cookies:
|
||||||
|
print(f"从配置文件读取到 {len(cookies)} 个账号")
|
||||||
|
return cookies
|
||||||
|
except Exception as e:
|
||||||
|
print(f"读取配置文件失败: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
# 处理环境变量中的多账户(用换行分隔)
|
||||||
|
lines = tieba_env.strip().split('\n')
|
||||||
|
for line in lines:
|
||||||
|
line = line.strip()
|
||||||
|
if line:
|
||||||
|
cookies.append(line)
|
||||||
|
|
||||||
|
print(f"从环境变量读取到 {len(cookies)} 个账号")
|
||||||
|
return cookies
|
||||||
|
|
||||||
|
# 添加青龙脚本根目录到Python路径
|
||||||
|
QL_SCRIPTS_DIR = '/ql/scripts' # 青龙脚本默认目录
|
||||||
|
sys.path.append(QL_SCRIPTS_DIR)
|
||||||
|
|
||||||
|
# 添加notify可能存在的其他路径
|
||||||
|
POSSIBLE_PATHS = [
|
||||||
|
'/ql', # 青龙根目录
|
||||||
|
'/ql/data/scripts', # 新版青龙数据目录
|
||||||
|
'/ql/scripts/notify', # 自定义通知目录
|
||||||
|
os.path.dirname(__file__) # 当前脚本目录
|
||||||
|
]
|
||||||
|
|
||||||
|
for path in POSSIBLE_PATHS:
|
||||||
|
if os.path.exists(os.path.join(path, 'notify.py')):
|
||||||
|
sys.path.append(path)
|
||||||
|
break
|
||||||
|
|
||||||
|
# 发送青龙面板通知
|
||||||
|
def send_notification(title, content):
|
||||||
|
try:
|
||||||
|
# 尝试导入青龙的通知模块
|
||||||
|
from notify import send
|
||||||
|
return send(title, content)
|
||||||
|
except ImportError:
|
||||||
|
print("⚠️ 无法加载通知模块,请检查路径配置")
|
||||||
|
return lambda title, content: None # 创建空函数防止报错
|
||||||
|
except Exception as e:
|
||||||
|
print(f"通知发送失败: {e}")
|
||||||
|
print(f"通知内容: {title}\n{content}")
|
||||||
|
return "通知发送失败"
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
cookies = get_tieba_cookies()
|
||||||
|
if not cookies:
|
||||||
|
print("没有找到有效的百度贴吧Cookie")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
results = []
|
||||||
|
for idx, cookie in enumerate(cookies, 1):
|
||||||
|
print(f"\n==== 处理第 {idx}/{len(cookies)} 个账号 ====")
|
||||||
|
try:
|
||||||
|
check_item = {"cookie": cookie}
|
||||||
|
tieba = Tieba(check_item)
|
||||||
|
result = tieba.main()
|
||||||
|
results.append(result)
|
||||||
|
print(f"账号 {idx} 处理完成")
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"账号 {idx} 处理失败: {str(e)}"
|
||||||
|
results.append(error_msg)
|
||||||
|
print(error_msg)
|
||||||
|
|
||||||
|
# 账号间随机延迟
|
||||||
|
if idx < len(cookies):
|
||||||
|
delay = random.uniform(3, 8)
|
||||||
|
print(f"等待 {delay:.2f} 秒后处理下一个账号...")
|
||||||
|
time.sleep(delay)
|
||||||
|
|
||||||
|
# 汇总所有结果并发送通知
|
||||||
|
final_title = "百度贴吧签到结果汇总"
|
||||||
|
final_content = "\n\n".join([f"【账号 {i+1}】\n{result}" for i, result in enumerate(results)])
|
||||||
|
|
||||||
|
# 添加统计信息
|
||||||
|
success_count = sum(1 for r in results if "签到成功" in r)
|
||||||
|
total_count = len(results)
|
||||||
|
final_content += f"\n\n📊 统计信息:\n成功: {success_count}/{total_count}\n失败: {total_count-success_count}/{total_count}"
|
||||||
|
|
||||||
|
print("\n==== 开始发送通知 ====")
|
||||||
|
notify_result = send_notification(final_title, final_content)
|
||||||
|
print(f"通知发送结果: {notify_result}")
|
||||||
|
print("\n==== 全部处理完成 ====")
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
# 同程旅行自动签到脚本
|
||||||
|
|
||||||
|
这是一个基于Python的自动化脚本,用于自动完成同程旅行APP的每日签到和任务,获取里程奖励。脚本支持多账号配置,自动处理日常任务,并通过青龙面板提供通知功能。
|
||||||
|
|
||||||
|
## 功能特性
|
||||||
|
|
||||||
|
- **自动签到**:每日自动完成签到任务
|
||||||
|
- **任务处理**:自动识别并完成各类日常任务
|
||||||
|
- **多账号支持**:通过环境变量配置多个账号,支持使用换行符或`#`分隔
|
||||||
|
- **通知功能**:支持通过青龙面板发送执行结果通知
|
||||||
|
- **智能重试**:任务失败时自动重试,提高成功率
|
||||||
|
- **数据统计**:记录签到天数、里程变化等信息
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
### 1. 环境准备
|
||||||
|
|
||||||
|
首先需要准备好运行环境:
|
||||||
|
|
||||||
|
1. 安装青龙面板(建议版本2.10.0以上)
|
||||||
|
2. 确保青龙面板已安装以下依赖:
|
||||||
|
- aiohttp
|
||||||
|
- requests
|
||||||
|
|
||||||
|
### 2. 获取抓包信息
|
||||||
|
|
||||||
|
1. 打开并登录“同程旅行”APP
|
||||||
|
2. 开启抓包工具(如Charles、Fiddler等)
|
||||||
|
3. 点击APP右下角“我的”-“签到”
|
||||||
|
4. 抓取请求域名`https://app.17u.cn`请求头中的三个参数:
|
||||||
|
- `phone`:手机号
|
||||||
|
- `apptoken`:认证令牌
|
||||||
|
- `device`:设备标识
|
||||||
|
|
||||||
|
### 3. 配置环境变量
|
||||||
|
|
||||||
|
在青龙面板中添加环境变量:
|
||||||
|
|
||||||
|
1. 打开青龙面板,进入「环境变量」页面
|
||||||
|
2. 点击「添加变量」按钮
|
||||||
|
3. 变量名:`tongcheng`
|
||||||
|
4. 变量值格式:
|
||||||
|
```
|
||||||
|
phone&apptoken&device
|
||||||
|
```
|
||||||
|
多账号之间支持用换行符或`#`分隔,例如:
|
||||||
|
```
|
||||||
|
# 使用换行符分隔(推荐)
|
||||||
|
13800138000&abcdef123456&device1
|
||||||
|
13900139000&abcdef789012&device2
|
||||||
|
|
||||||
|
# 或使用#分隔
|
||||||
|
13800138000&abcdef123456&device1#13900139000&abcdef789012&device2
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 添加脚本
|
||||||
|
|
||||||
|
1. 在青龙面板中,进入「脚本管理」页面
|
||||||
|
2. 点击「添加脚本」按钮
|
||||||
|
3. 输入脚本名称(如`tc_signin.py`)
|
||||||
|
4. 将脚本代码复制到编辑器中
|
||||||
|
5. 点击「保存」按钮
|
||||||
|
|
||||||
|
### 5. 设置定时任务
|
||||||
|
|
||||||
|
1. 在青龙面板中,进入「定时任务」页面
|
||||||
|
2. 点击「添加任务」按钮
|
||||||
|
3. 任务名称:自定义(如`同程旅行签到`)
|
||||||
|
4. 命令:
|
||||||
|
```bash
|
||||||
|
task tc_signin.py
|
||||||
|
```
|
||||||
|
5. 定时规则:设置执行频率(如`0 0 * * *`表示每天0点执行)
|
||||||
|
6. 点击「保存」按钮
|
||||||
|
|
||||||
|
## 通知配置
|
||||||
|
|
||||||
|
脚本支持通过青龙面板的通知功能发送执行结果,需要确保青龙面板已正确配置通知方式(如Server酱、Telegram等)。
|
||||||
|
|
||||||
|
如果通知功能无法正常工作,脚本会在日志中记录详细的错误信息,帮助排查问题。
|
||||||
|
|
||||||
|
## 脚本更新说明
|
||||||
|
|
||||||
|
### 版本1.6更新内容
|
||||||
|
|
||||||
|
- 优化环境变量解析,支持使用换行符分隔多账号
|
||||||
|
- 增强环境变量格式验证和错误处理
|
||||||
|
- 优化日志输出,明确显示解析到的账号数量
|
||||||
|
- 保留之前所有功能和优化
|
||||||
|
|
||||||
|
### 版本1.5更新内容
|
||||||
|
|
||||||
|
- 优化通知模块,解决异步环境下通知失败问题
|
||||||
|
- 使用线程池执行同步通知函数,避免阻塞事件循环
|
||||||
|
- 增强错误处理,提供更详细的通知失败信息
|
||||||
|
- 保留之前的所有功能和优化
|
||||||
|
|
||||||
|
### 版本1.4更新内容
|
||||||
|
|
||||||
|
- 修复推送服务失败的问题
|
||||||
|
- 优化notify模块导入逻辑
|
||||||
|
- 添加详细的路径查找和错误日志
|
||||||
|
- 保留之前的所有功能和优化
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
1. **脚本执行失败**:
|
||||||
|
- 检查环境变量是否正确配置
|
||||||
|
- 确认抓包获取的参数是否有效
|
||||||
|
- 查看脚本执行日志,定位具体错误原因
|
||||||
|
|
||||||
|
2. **通知未收到**:
|
||||||
|
- 检查青龙面板通知配置是否正确
|
||||||
|
- 查看脚本日志,确认通知是否发送成功
|
||||||
|
- 检查通知服务提供商的状态
|
||||||
|
|
||||||
|
3. **任务执行不完全**:
|
||||||
|
- 脚本有内置的重试机制,但部分任务可能需要手动完成
|
||||||
|
- 检查任务列表中是否有特殊任务需要额外操作
|
||||||
|
|
||||||
|
## 免责声明
|
||||||
|
|
||||||
|
本脚本仅供学习交流使用,请勿用于商业用途。使用本脚本可能违反同程旅行的用户协议,请谨慎使用。开发者不对因使用本脚本而导致的任何问题负责。
|
||||||
@@ -0,0 +1,461 @@
|
|||||||
|
"""
|
||||||
|
任务名称
|
||||||
|
name: 同程旅行签到
|
||||||
|
定时规则
|
||||||
|
cron: 0 0 4 * * ?
|
||||||
|
"""
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import asyncio
|
||||||
|
import aiohttp
|
||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
import random
|
||||||
|
import sys
|
||||||
|
import traceback # 用于获取详细的错误堆栈信息
|
||||||
|
|
||||||
|
# 手机号脱敏处理
|
||||||
|
def mask_phone(phone):
|
||||||
|
if len(phone) == 11:
|
||||||
|
return phone[:3] + '****' + phone[7:]
|
||||||
|
return phone
|
||||||
|
|
||||||
|
# 从环境变量获取账号信息
|
||||||
|
def get_accounts():
|
||||||
|
accounts = []
|
||||||
|
tongcheng_env = os.getenv('tongcheng', '')
|
||||||
|
|
||||||
|
if not tongcheng_env:
|
||||||
|
print("未找到环境变量 tongcheng")
|
||||||
|
return accounts
|
||||||
|
|
||||||
|
# 支持使用#或换行符分隔多账号
|
||||||
|
# 先尝试按换行符分割
|
||||||
|
lines = tongcheng_env.strip().split('\n')
|
||||||
|
if len(lines) > 1:
|
||||||
|
print(f"发现{len(lines)}行配置,尝试按行解析...")
|
||||||
|
account_list = lines
|
||||||
|
else:
|
||||||
|
# 尝试按#分割
|
||||||
|
print("未发现换行符,尝试按#分割...")
|
||||||
|
account_list = tongcheng_env.split('#')
|
||||||
|
|
||||||
|
valid_accounts = 0
|
||||||
|
invalid_accounts = 0
|
||||||
|
|
||||||
|
for account in account_list:
|
||||||
|
account = account.strip()
|
||||||
|
if not account:
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
phone, apptoken, device = account.split('&')
|
||||||
|
if not phone or not apptoken or not device:
|
||||||
|
raise ValueError("参数为空")
|
||||||
|
|
||||||
|
accounts.append({
|
||||||
|
'phone': phone,
|
||||||
|
'apptoken': apptoken,
|
||||||
|
'device': device
|
||||||
|
})
|
||||||
|
valid_accounts += 1
|
||||||
|
print(f" ✅ 解析账号: {mask_phone(phone)}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
invalid_accounts += 1
|
||||||
|
print(f" ❌ 无效账号配置: {account} - 错误: {str(e)}")
|
||||||
|
|
||||||
|
print(f"🔍 共解析出{valid_accounts}个有效账号,{invalid_accounts}个无效配置")
|
||||||
|
return accounts
|
||||||
|
|
||||||
|
# 获取请求头
|
||||||
|
def get_headers(phone, apptoken, device, payload):
|
||||||
|
payload_str = json.dumps(payload)
|
||||||
|
headers = {
|
||||||
|
'accept': 'application/json, text/plain, */*',
|
||||||
|
'phone': phone,
|
||||||
|
'channel': '1',
|
||||||
|
'apptoken': apptoken,
|
||||||
|
'sec-fetch-site': 'same-site',
|
||||||
|
'accept-language': 'zh-CN,zh-Hans;q=0.9',
|
||||||
|
'accept-encoding': 'gzip, deflate, br',
|
||||||
|
'sec-fetch-mode': 'cors',
|
||||||
|
'origin': 'https://m.17u.cn',
|
||||||
|
'user-agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 TcTravel/11.0.0 tctype/wk',
|
||||||
|
'referer': 'https://m.17u.cn/',
|
||||||
|
'content-length': str(len(payload_str.encode('utf-8'))),
|
||||||
|
'device': device,
|
||||||
|
'sec-fetch-dest': 'empty'
|
||||||
|
}
|
||||||
|
return headers
|
||||||
|
|
||||||
|
# 获取当前日期
|
||||||
|
def get_today_date():
|
||||||
|
return datetime.now().strftime('%Y-%m-%d')
|
||||||
|
|
||||||
|
# 异步执行任务
|
||||||
|
async def execute_task(session, phone, apptoken, device, task_code, title, browser_time):
|
||||||
|
# 开始任务
|
||||||
|
start_url = "https://app.17u.cn/welfarecenter/task/start"
|
||||||
|
start_payload = {"taskCode": task_code}
|
||||||
|
start_headers = get_headers(phone, apptoken, device, start_payload)
|
||||||
|
|
||||||
|
task_id = None
|
||||||
|
async with session.post(start_url, json=start_payload, headers=start_headers) as response:
|
||||||
|
data = await response.json()
|
||||||
|
if data['code'] != 2200:
|
||||||
|
print(f" - ❌ 做任务【{title}】失败,跳过当前任务")
|
||||||
|
# 即使任务开始失败,也尝试获取task_id
|
||||||
|
task_id = data.get('data')
|
||||||
|
if not task_id:
|
||||||
|
return False, f"任务【{title}】开始失败"
|
||||||
|
else:
|
||||||
|
task_id = data['data']
|
||||||
|
print(f" - 🚀 开始执行任务【{title}】,等待{browser_time}秒...")
|
||||||
|
# 等待浏览时间
|
||||||
|
await asyncio.sleep(browser_time)
|
||||||
|
|
||||||
|
if task_id:
|
||||||
|
# 完成任务
|
||||||
|
finish_url = "https://app.17u.cn/welfarecenter/task/finish"
|
||||||
|
finish_payload = {"id": task_id}
|
||||||
|
finish_headers = get_headers(phone, apptoken, device, finish_payload)
|
||||||
|
|
||||||
|
async with session.post(finish_url, json=finish_payload, headers=finish_headers) as response:
|
||||||
|
data = await response.json()
|
||||||
|
if data['code'] != 2200:
|
||||||
|
print(f" - ⚠️ 任务【{title}】完成失败,尝试重新提交...")
|
||||||
|
await asyncio.sleep(2)
|
||||||
|
async with session.post(finish_url, json=finish_payload, headers=finish_headers) as retry_response:
|
||||||
|
data = await retry_response.json()
|
||||||
|
if data['code'] != 2200:
|
||||||
|
print(f" - ❌ 任务【{title}】完成失败,继续尝试领取奖励")
|
||||||
|
|
||||||
|
print(f" - ✅ 任务【{title}】完成成功,开始领取奖励")
|
||||||
|
|
||||||
|
# 领取奖励
|
||||||
|
reward_url = "https://app.17u.cn/welfarecenter/task/receive"
|
||||||
|
reward_payload = {"id": task_id}
|
||||||
|
reward_headers = get_headers(phone, apptoken, device, reward_payload)
|
||||||
|
|
||||||
|
# 添加奖励领取重试逻辑
|
||||||
|
max_retries = 2
|
||||||
|
for retry in range(max_retries + 1):
|
||||||
|
async with session.post(reward_url, json=reward_payload, headers=reward_headers) as response:
|
||||||
|
data = await response.json()
|
||||||
|
if data['code'] == 2200:
|
||||||
|
print(f" - 🎁 任务【{title}】领取奖励成功")
|
||||||
|
return True, f"任务【{title}】领取奖励成功"
|
||||||
|
elif retry < max_retries:
|
||||||
|
print(f" - ⚠️ 任务【{title}】领取奖励失败,正在进行第{retry + 1}次重试...")
|
||||||
|
await asyncio.sleep(2)
|
||||||
|
else:
|
||||||
|
print(f" - ❌ 任务【{title}】领取奖励失败,已重试{max_retries}次,请尝试手动领取")
|
||||||
|
return False, f"任务【{title}】领取奖励失败"
|
||||||
|
return False, f"任务【{title}】开始失败"
|
||||||
|
|
||||||
|
# 异步处理单个账号的所有任务
|
||||||
|
async def process_account(session, account):
|
||||||
|
phone = account['phone']
|
||||||
|
apptoken = account['apptoken']
|
||||||
|
device = account['device']
|
||||||
|
|
||||||
|
# 存储当前账号的通知内容
|
||||||
|
account_notify_content = []
|
||||||
|
|
||||||
|
print(f"\n🔄 开始处理用户{mask_phone(phone)}的任务")
|
||||||
|
account_notify_content.append(f"🔄 开始处理用户{mask_phone(phone)}的任务")
|
||||||
|
|
||||||
|
# 获取签到状态
|
||||||
|
sign_url = "https://app.17u.cn/welfarecenter/index/signIndex"
|
||||||
|
sign_headers = get_headers(phone, apptoken, device, {})
|
||||||
|
|
||||||
|
async with session.post(sign_url, json={}, headers=sign_headers) as response:
|
||||||
|
data = await response.json()
|
||||||
|
|
||||||
|
if data['data'] is None:
|
||||||
|
print(" ❌ Token失效,请更新")
|
||||||
|
account_notify_content.append(" ❌ Token失效,请更新")
|
||||||
|
return False, account_notify_content
|
||||||
|
|
||||||
|
today_sign = data['data']['todaySign']
|
||||||
|
mileage = data['data']['mileageBalance']['mileage']
|
||||||
|
print(f" 今日{'✅ 已' if today_sign else '⏳ 未'}签到,当前剩余里程{mileage}")
|
||||||
|
account_notify_content.append(f" 今日{'✅ 已' if today_sign else '⏳ 未'}签到,当前剩余里程{mileage}")
|
||||||
|
|
||||||
|
if not today_sign:
|
||||||
|
# 执行签到
|
||||||
|
sign_in_url = "https://app.17u.cn/welfarecenter/index/sign"
|
||||||
|
sign_in_payload = {"type": 1, "day": get_today_date()}
|
||||||
|
sign_in_headers = get_headers(phone, apptoken, device, sign_in_payload)
|
||||||
|
|
||||||
|
async with session.post(sign_in_url, json=sign_in_payload, headers=sign_in_headers) as response:
|
||||||
|
data = await response.json()
|
||||||
|
if data['code'] != 2200:
|
||||||
|
print(" ❌ 签到失败")
|
||||||
|
account_notify_content.append(" ❌ 签到失败")
|
||||||
|
return False, account_notify_content
|
||||||
|
print(" ✅ 签到成功")
|
||||||
|
account_notify_content.append(" ✅ 签到成功")
|
||||||
|
|
||||||
|
# 获取任务列表
|
||||||
|
task_list_url = "https://app.17u.cn/welfarecenter/task/taskList?version=11.1.2.1"
|
||||||
|
task_list_headers = get_headers(phone, apptoken, device, {})
|
||||||
|
|
||||||
|
async with session.post(task_list_url, json={}, headers=task_list_headers) as response:
|
||||||
|
data = await response.json()
|
||||||
|
if data['code'] != 2200:
|
||||||
|
print(" ❌ 获取任务列表失败,跳过当前账号")
|
||||||
|
account_notify_content.append(" ❌ 获取任务列表失败,跳过当前账号")
|
||||||
|
return False, account_notify_content
|
||||||
|
|
||||||
|
tasks = []
|
||||||
|
for task in data['data']:
|
||||||
|
if task['state'] == 1 and task['browserTime'] != 0:
|
||||||
|
tasks.append({
|
||||||
|
'taskCode': task['taskCode'],
|
||||||
|
'title': task['title'],
|
||||||
|
'browserTime': task['browserTime']
|
||||||
|
})
|
||||||
|
|
||||||
|
if not tasks:
|
||||||
|
print(" ✅ 没有待完成的任务")
|
||||||
|
account_notify_content.append(" ✅ 没有待完成的任务")
|
||||||
|
return True, account_notify_content
|
||||||
|
|
||||||
|
print(f"\n 📋 本次共有{len(tasks)}个待做任务")
|
||||||
|
account_notify_content.append(f"\n 📋 本次共有{len(tasks)}个待做任务")
|
||||||
|
|
||||||
|
# 记录任务完成情况
|
||||||
|
success_tasks = []
|
||||||
|
failed_tasks = []
|
||||||
|
|
||||||
|
# 将任务分批处理,每批3个
|
||||||
|
for i in range(0, len(tasks), 3):
|
||||||
|
batch = tasks[i:i+3]
|
||||||
|
batch_num = i // 3 + 1
|
||||||
|
total_batches = (len(tasks) + 2) // 3
|
||||||
|
|
||||||
|
print(f"\n 🔄 开始执行第{batch_num}/{total_batches}批任务:")
|
||||||
|
account_notify_content.append(f"\n 🔄 开始执行第{batch_num}/{total_batches}批任务:")
|
||||||
|
for task in batch:
|
||||||
|
print(f" - 📌 {task['title']} (需要浏览{task['browserTime']}秒)")
|
||||||
|
account_notify_content.append(f" - 📌 {task['title']} (需要浏览{task['browserTime']}秒)")
|
||||||
|
|
||||||
|
# 创建当前批次任务的协程
|
||||||
|
batch_coroutines = [
|
||||||
|
execute_task(session, phone, apptoken, device, task['taskCode'], task['title'], task['browserTime'])
|
||||||
|
for task in batch
|
||||||
|
]
|
||||||
|
# 并发执行当前批次的任务
|
||||||
|
results = await asyncio.gather(*batch_coroutines)
|
||||||
|
|
||||||
|
# 处理任务结果
|
||||||
|
for success, message in results:
|
||||||
|
if success:
|
||||||
|
success_tasks.append(message)
|
||||||
|
else:
|
||||||
|
failed_tasks.append(message)
|
||||||
|
|
||||||
|
# 批次间隔
|
||||||
|
if batch_num < total_batches:
|
||||||
|
wait_time = random.uniform(1, 2)
|
||||||
|
print(f"\n ⏳ 等待{wait_time:.1f}秒后执行下一批任务...")
|
||||||
|
account_notify_content.append(f"\n ⏳ 等待{wait_time:.1f}秒后执行下一批任务...")
|
||||||
|
await asyncio.sleep(wait_time)
|
||||||
|
|
||||||
|
# 添加任务结果统计
|
||||||
|
account_notify_content.append("\n 📊 任务执行统计:")
|
||||||
|
account_notify_content.append(f" - ✅ 成功完成 {len(success_tasks)} 个任务")
|
||||||
|
for msg in success_tasks:
|
||||||
|
account_notify_content.append(f" - {msg}")
|
||||||
|
|
||||||
|
if failed_tasks:
|
||||||
|
account_notify_content.append(f" - ❌ 失败 {len(failed_tasks)} 个任务")
|
||||||
|
for msg in failed_tasks:
|
||||||
|
account_notify_content.append(f" - {msg}")
|
||||||
|
|
||||||
|
# 获取积分信息
|
||||||
|
info_url = "https://app.17u.cn/welfarecenter/index/signIndex"
|
||||||
|
info_headers = get_headers(phone, apptoken, device, {})
|
||||||
|
|
||||||
|
async with session.post(info_url, json={}, headers=info_headers) as response:
|
||||||
|
data = await response.json()
|
||||||
|
if data['code'] == 2200:
|
||||||
|
cycle_sign_num = data['data']['cycleSighNum']
|
||||||
|
continuous_history = data['data']['continuousHistory']
|
||||||
|
mileage = data['data']['mileageBalance']['mileage']
|
||||||
|
today_mileage = data['data']['mileageBalance']['todayMileage']
|
||||||
|
|
||||||
|
# 获取特殊连续签到信息
|
||||||
|
special_info = data['data'].get('specialContinuousInfo', {})
|
||||||
|
next_reward_mileage = special_info.get('nextRewardMileage', 0)
|
||||||
|
need_sign = special_info.get('needSign', 0)
|
||||||
|
next_reward_day = special_info.get('nextRewardDay', 0)
|
||||||
|
|
||||||
|
print(f"\n 📊 任务完成情况:")
|
||||||
|
account_notify_content.append(f"\n 📊 任务完成情况:")
|
||||||
|
print(f" - 📅 周期内连续签到{cycle_sign_num}天,已连续签到{continuous_history}天")
|
||||||
|
account_notify_content.append(f" - 📅 周期内连续签到{cycle_sign_num}天,已连续签到{continuous_history}天")
|
||||||
|
if need_sign > 0 and next_reward_day > 0:
|
||||||
|
print(f" - 🎯 再签{need_sign}天,可获得「{next_reward_day}日奖励」(最高{next_reward_mileage}里程)")
|
||||||
|
account_notify_content.append(f" - 🎯 再签{need_sign}天,可获得「{next_reward_day}日奖励」(最高{next_reward_mileage}里程)")
|
||||||
|
print(f" - 💰 今日获取{today_mileage}里程,当前剩余{mileage}里程")
|
||||||
|
account_notify_content.append(f" - 💰 今日获取{today_mileage}里程,当前剩余{mileage}里程")
|
||||||
|
|
||||||
|
return True, account_notify_content
|
||||||
|
|
||||||
|
# 异步发送通知
|
||||||
|
async def async_send_notification(send_func, title, content, loop=None):
|
||||||
|
if not loop:
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
|
||||||
|
# 创建线程池执行器
|
||||||
|
with ThreadPoolExecutor(max_workers=1) as executor:
|
||||||
|
try:
|
||||||
|
# 在单独的线程中执行同步的通知函数
|
||||||
|
result = await loop.run_in_executor(executor, lambda: send_func(title, content))
|
||||||
|
print("✅ 通知发送成功")
|
||||||
|
return True, result
|
||||||
|
except Exception as e:
|
||||||
|
# 获取详细的错误堆栈信息
|
||||||
|
error_stack = traceback.format_exc()
|
||||||
|
print(f"❌ 通知发送失败: {str(e)}")
|
||||||
|
print(f"错误堆栈信息:\n{error_stack}")
|
||||||
|
return False, str(e)
|
||||||
|
|
||||||
|
# 主函数
|
||||||
|
async def main():
|
||||||
|
# 存储所有账号的通知内容
|
||||||
|
all_notify_title = "同程旅行任务执行结果"
|
||||||
|
all_notify_content = []
|
||||||
|
|
||||||
|
# 尝试导入通知模块
|
||||||
|
send = None
|
||||||
|
notify_found = False
|
||||||
|
print("\n🔍 正在查找青龙面板通知模块...")
|
||||||
|
|
||||||
|
# 青龙面板默认路径
|
||||||
|
QL_SCRIPTS_DIR = '/ql/scripts'
|
||||||
|
sys.path.append(QL_SCRIPTS_DIR)
|
||||||
|
|
||||||
|
# 添加notify可能存在的其他路径
|
||||||
|
POSSIBLE_PATHS = [
|
||||||
|
'/ql', # 青龙根目录
|
||||||
|
'/ql/data/scripts', # 新版青龙数据目录
|
||||||
|
'/ql/repo', # 青龙脚本仓库目录
|
||||||
|
'/ql/repo/QLDependency', # QLDependency仓库目录
|
||||||
|
'/ql/config', # 配置目录
|
||||||
|
os.path.dirname(__file__), # 当前脚本目录
|
||||||
|
os.path.join(os.path.dirname(__file__), 'notify') # 当前目录下的notify文件夹
|
||||||
|
]
|
||||||
|
|
||||||
|
# 检查每个路径是否存在并尝试导入
|
||||||
|
for path in POSSIBLE_PATHS:
|
||||||
|
notify_path = os.path.join(path, 'notify.py')
|
||||||
|
print(f" - 检查路径: {notify_path}")
|
||||||
|
if os.path.exists(notify_path):
|
||||||
|
print(f" ✅ 找到通知模块文件")
|
||||||
|
try:
|
||||||
|
# 临时添加路径到sys.path
|
||||||
|
if path not in sys.path:
|
||||||
|
sys.path.append(path)
|
||||||
|
from notify import send
|
||||||
|
print(f" ✅ 成功导入通知模块")
|
||||||
|
notify_found = True
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ❌ 导入失败: {str(e)}")
|
||||||
|
else:
|
||||||
|
print(f" ❌ 文件不存在")
|
||||||
|
|
||||||
|
# 如果未找到通知模块,创建一个模拟函数
|
||||||
|
if not notify_found:
|
||||||
|
print("❌ 未找到青龙面板通知模块,请确保notify.py存在于正确的路径")
|
||||||
|
print("❌ 脚本将继续执行,但无法发送通知")
|
||||||
|
def dummy_send(title, content):
|
||||||
|
print(f"\n📧 通知内容 (未发送):\n{title}\n\n{content}")
|
||||||
|
return "Notification not sent - notify.py not found"
|
||||||
|
send = dummy_send
|
||||||
|
|
||||||
|
# 继续执行主程序
|
||||||
|
accounts = get_accounts()
|
||||||
|
if not accounts:
|
||||||
|
print("❌ 没有找到有效的账号信息,请添加环境变量")
|
||||||
|
print("❌ 环境变量名称tongcheng 格式为:phone&apptoken&device")
|
||||||
|
all_notify_content.append("❌ 没有找到有效的账号信息,请添加环境变量")
|
||||||
|
all_notify_content.append("❌ 环境变量名称tongcheng 格式为:phone&apptoken&device")
|
||||||
|
# 异步发送通知
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
await async_send_notification(send, all_notify_title, "\n".join(all_notify_content), loop)
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"🔍 本次共获取到 {len(accounts)} 个账号")
|
||||||
|
all_notify_content.append(f"🔍 本次共获取到 {len(accounts)} 个账号")
|
||||||
|
|
||||||
|
# 统计信息
|
||||||
|
total_accounts = len(accounts)
|
||||||
|
success_accounts = 0
|
||||||
|
failed_accounts = 0
|
||||||
|
|
||||||
|
# 存储每个账号的结果
|
||||||
|
account_results = []
|
||||||
|
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
# 串行处理每个账号
|
||||||
|
for i, account in enumerate(accounts, 1):
|
||||||
|
print(f"\n📱 开始处理第 {i}/{len(accounts)} 个账号")
|
||||||
|
account_result = {
|
||||||
|
'phone': mask_phone(account['phone']),
|
||||||
|
'success': False,
|
||||||
|
'content': []
|
||||||
|
}
|
||||||
|
|
||||||
|
success, content = await process_account(session, account)
|
||||||
|
|
||||||
|
account_result['success'] = success
|
||||||
|
account_result['content'] = content
|
||||||
|
|
||||||
|
account_results.append(account_result)
|
||||||
|
|
||||||
|
# 统计账号执行情况
|
||||||
|
if success:
|
||||||
|
success_accounts += 1
|
||||||
|
else:
|
||||||
|
failed_accounts += 1
|
||||||
|
|
||||||
|
if i < len(accounts):
|
||||||
|
wait_time = random.uniform(2, 3)
|
||||||
|
print(f"\n⏳ 等待 {wait_time:.1f} 秒后处理下一个账号...")
|
||||||
|
await asyncio.sleep(wait_time)
|
||||||
|
|
||||||
|
# 构建聚合通知
|
||||||
|
all_notify_content.append("\n\n📊 总体执行统计:")
|
||||||
|
all_notify_content.append(f" - 账号总数:{total_accounts}")
|
||||||
|
all_notify_content.append(f" - 成功:{success_accounts}")
|
||||||
|
all_notify_content.append(f" - 失败:{failed_accounts}")
|
||||||
|
|
||||||
|
all_notify_content.append("\n\n===== 详细结果 =====")
|
||||||
|
|
||||||
|
# 添加每个账号的结果
|
||||||
|
for result in account_results:
|
||||||
|
all_notify_content.append(f"\n\n📱 账号:{result['phone']}")
|
||||||
|
all_notify_content.append(f"{'✅ 成功' if result['success'] else '❌ 失败'}")
|
||||||
|
all_notify_content.extend(result['content'])
|
||||||
|
|
||||||
|
# 发送聚合通知
|
||||||
|
print("\n📤 正在发送通知...")
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
notify_success, notify_result = await async_send_notification(send, all_notify_title, "\n".join(all_notify_content), loop)
|
||||||
|
|
||||||
|
# 记录通知发送结果
|
||||||
|
if not notify_success:
|
||||||
|
all_notify_content.append("\n\n⚠️ 通知发送失败,请检查notify.py配置")
|
||||||
|
all_notify_content.append(f"错误信息:{notify_result}")
|
||||||
|
|
||||||
|
print("✅ 脚本执行完成")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
@@ -38,8 +38,70 @@ task ik_signin 0 0 1 * * ?
|
|||||||
3. 依次对配置的所有账号进行签到操作
|
3. 依次对配置的所有账号进行签到操作
|
||||||
4. 签到结果会通过青龙面板的通知系统发送
|
4. 签到结果会通过青龙面板的通知系统发送
|
||||||
|
|
||||||
|
### 运行依赖
|
||||||
|
|
||||||
|
当前版本使用真实浏览器页面登录并点击Geetest验证按钮。Python依赖只需要安装:
|
||||||
|
|
||||||
|
```
|
||||||
|
DrissionPage
|
||||||
|
```
|
||||||
|
|
||||||
|
脚本不需要`chromium-chromedriver`,也不需要`xvfb`。DrissionPage使用浏览器调试协议,不走ChromeDriver;脚本已启用headless模式。
|
||||||
|
|
||||||
|
青龙容器内需要有可用的Chrome或Chromium。如果脚本没有自动找到浏览器,可以额外设置:
|
||||||
|
|
||||||
|
| 变量名 | 说明 | 示例 |
|
||||||
|
| ---- | ---- | ---- |
|
||||||
|
| IKUUU_BROWSER_PATH | Chrome/Chromium可执行文件路径 | `/usr/bin/chromium` |
|
||||||
|
|
||||||
|
浏览器不是Python依赖,不能在青龙Python依赖页面安装。需要进入青龙容器安装或确认已有路径:
|
||||||
|
|
||||||
|
```
|
||||||
|
which chromium chromium-browser google-chrome google-chrome-stable chrome
|
||||||
|
```
|
||||||
|
|
||||||
|
Debian/Ubuntu容器可尝试:
|
||||||
|
|
||||||
|
```
|
||||||
|
apt-get update && apt-get install -y chromium
|
||||||
|
```
|
||||||
|
|
||||||
|
Alpine容器可尝试:
|
||||||
|
|
||||||
|
```
|
||||||
|
apk add --no-cache chromium
|
||||||
|
```
|
||||||
|
|
||||||
|
安装后如果脚本仍未自动识别,在青龙环境变量中增加:
|
||||||
|
|
||||||
|
```
|
||||||
|
IKUUU_BROWSER_PATH=/usr/bin/chromium
|
||||||
|
```
|
||||||
|
|
||||||
## 更新日志
|
## 更新日志
|
||||||
|
|
||||||
|
### 2026-06-04
|
||||||
|
- 参照浏览器自动化逻辑,恢复`IKUUU_ACCOUNTS`账号密码登录
|
||||||
|
- 移除Cookie登录路径,改为使用DrissionPage打开页面、点击Geetest并调用页面签到函数
|
||||||
|
- 移除`xvfb`和`chromium-chromedriver`依赖要求,支持通过`IKUUU_BROWSER_PATH`指定浏览器路径
|
||||||
|
|
||||||
|
### 2025-07-29
|
||||||
|
- 修复「剩余流量」查询
|
||||||
|
|
||||||
|
### 2025-07-25
|
||||||
|
- 智能域名发现机制。使用多种正则表达式模式从网页内容中提取域名,当域名不可用时,会从旧域名页面自动抓取新的可用域名,优先使用ikuuu相关域名,其次使用其他发现的域名
|
||||||
|
- 增强的域名检测逻辑。能识别多种域名变更通知格式,从JavaScript跳转、登录链接等多个位置提取域名,过滤掉无效的域名格式
|
||||||
|
- 完善的容错机制。当前域名 → 发现的新域名 → 备用域名列表,每个域名都会进行可达性测试,增加了超时、连接错误等异常处理
|
||||||
|
- 改进的用户体验。每个步骤都有清晰的状态提示和图标,显示账户处理进度,提供详细的执行结果汇总
|
||||||
|
- 新增功能。在通知中包含当前使用的域名,账户间延迟增加到2秒,避免请求过快,各种异常情况都有对应的错误信息
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2025-01-14
|
||||||
|
- 接口请求统一增加UA标识
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### 2025-06-01
|
### 2025-06-01
|
||||||
- 增加剩余流量显示
|
- 增加剩余流量显示
|
||||||
|
|
||||||
|
|||||||
+498
-184
@@ -1,33 +1,34 @@
|
|||||||
# -*- coding: utf-8 -*
|
"""
|
||||||
'''
|
任务名称
|
||||||
定时自定义
|
name: iKuuu签到
|
||||||
<<<<<<< .mine
|
定时规则
|
||||||
0 0 1 * * ?
|
cron: 0 0 8 * * ?
|
||||||
=======
|
"""
|
||||||
0 0 1 * * ? ik_signin.py
|
|
||||||
>>>>>>> .theirs
|
|
||||||
new Env('iKuuu签到');
|
|
||||||
'''
|
|
||||||
import requests
|
|
||||||
import re
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import datetime
|
import datetime
|
||||||
import urllib.parse
|
import os
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
from bs4 import BeautifulSoup
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
try:
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
# 添加青龙脚本根目录到Python路径
|
# 添加青龙脚本根目录到Python路径
|
||||||
QL_SCRIPTS_DIR = '/ql/scripts' # 青龙脚本默认目录
|
QL_SCRIPTS_DIR = '/ql/scripts'
|
||||||
sys.path.append(QL_SCRIPTS_DIR)
|
sys.path.append(QL_SCRIPTS_DIR)
|
||||||
|
|
||||||
# 添加notify可能存在的其他路径
|
# 添加notify可能存在的其他路径
|
||||||
POSSIBLE_PATHS = [
|
POSSIBLE_PATHS = [
|
||||||
'/ql', # 青龙根目录
|
'/ql',
|
||||||
'/ql/data/scripts', # 新版青龙数据目录
|
'/ql/data/scripts',
|
||||||
'/ql/scripts/notify', # 自定义通知目录
|
'/ql/scripts/notify',
|
||||||
os.path.dirname(__file__) # 当前脚本目录
|
os.path.dirname(__file__)
|
||||||
]
|
]
|
||||||
|
|
||||||
for path in POSSIBLE_PATHS:
|
for path in POSSIBLE_PATHS:
|
||||||
@@ -39,155 +40,449 @@ try:
|
|||||||
from notify import send
|
from notify import send
|
||||||
except ImportError:
|
except ImportError:
|
||||||
print("⚠️ 无法加载通知模块,请检查路径配置")
|
print("⚠️ 无法加载通知模块,请检查路径配置")
|
||||||
send = lambda title, content: None # 创建空函数防止报错
|
send = lambda title, content: None
|
||||||
|
|
||||||
|
|
||||||
|
ikun_host = "ikuuu.win" # 自动更新于2026-01-17 08:00:07
|
||||||
|
backup_hosts = ["ikuuu.win", "ikuuu.fyi", "ikuuu.one", "ikuuu.pw", "ikuuu.me", "ikuuu.cc"]
|
||||||
|
|
||||||
|
USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
||||||
|
|
||||||
|
|
||||||
|
def mask_email(email):
|
||||||
|
if '@' not in email:
|
||||||
|
return email
|
||||||
|
local, domain = email.split('@', 1)
|
||||||
|
if len(local) <= 2:
|
||||||
|
return f"{local[0]}***@{domain}"
|
||||||
|
return f"{local[0]}***{local[-1]}@{domain}"
|
||||||
|
|
||||||
|
|
||||||
|
def extract_domains_from_content(content):
|
||||||
|
"""从网页内容中提取可用域名"""
|
||||||
|
domains = []
|
||||||
|
patterns = [
|
||||||
|
r'<h[1-6][^>]*>.*?(?:域名|domain|新域名|最新域名)[::]\s*([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})',
|
||||||
|
r'(?:location\.href|window\.location)\s*=\s*["\']https?://([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})',
|
||||||
|
r'https?://([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/auth/login',
|
||||||
|
r'https?://([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})',
|
||||||
|
r'(?:域名|domain|网址|地址)[::\s]*([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})',
|
||||||
|
r'(ikuuu\.[a-zA-Z0-9.-]+)',
|
||||||
|
]
|
||||||
|
|
||||||
|
for pattern in patterns:
|
||||||
|
matches = re.findall(pattern, content, re.IGNORECASE | re.MULTILINE)
|
||||||
|
for match in matches:
|
||||||
|
domain = match.strip().lower()
|
||||||
|
if (domain and '.' in domain and not domain.startswith('.') and
|
||||||
|
not domain.endswith('.') and 3 < len(domain) < 50 and
|
||||||
|
not any(char in domain for char in [' ', '\n', '\t', '<', '>', '"', "'"])):
|
||||||
|
domains.append(domain)
|
||||||
|
|
||||||
|
return list(set(domains))
|
||||||
|
|
||||||
|
|
||||||
|
def get_available_domains_from_old_domain(old_domain):
|
||||||
|
"""从旧域名页面获取新的可用域名"""
|
||||||
|
available_domains = []
|
||||||
|
try:
|
||||||
|
print(f"🔍 从域名 {old_domain} 获取新域名信息...")
|
||||||
|
response = requests.get(
|
||||||
|
f"https://{old_domain}/",
|
||||||
|
headers={"User-Agent": USER_AGENT},
|
||||||
|
timeout=15,
|
||||||
|
allow_redirects=True
|
||||||
|
)
|
||||||
|
if response.status_code == 200:
|
||||||
|
domains = extract_domains_from_content(response.text)
|
||||||
|
available_domains.extend([d for d in domains if 'ikuuu' in d])
|
||||||
|
else:
|
||||||
|
print(f"⚠️ 域名 {old_domain} 返回状态码: {response.status_code}")
|
||||||
|
except requests.exceptions.Timeout:
|
||||||
|
print(f"⏰ 域名 {old_domain} 请求超时")
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
print(f"🔌 域名 {old_domain} 连接失败")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 检查域名 {old_domain} 时出错: {e}")
|
||||||
|
return available_domains
|
||||||
|
|
||||||
# 初始域名
|
|
||||||
ikun_host = "ikuuu.one" # 自动更新于2025-04-29 13:08:20
|
|
||||||
backup_hosts = ["ikuuu.one", "ikuuu.pw", "ikuuu.me"] # 备用域名列表
|
|
||||||
|
|
||||||
def get_latest_ikun_host():
|
def get_latest_ikun_host():
|
||||||
test_url = f"https://{ikun_host}/"
|
"""获取最新可用域名"""
|
||||||
try:
|
try:
|
||||||
response = requests.get(test_url, timeout=10)
|
response = requests.get(f"https://{ikun_host}/", headers={"User-Agent": USER_AGENT}, timeout=10)
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
if "官网域名已更改" in response.text or "Domain deprecated" in response.text:
|
change_indicators = ['官网域名已更改', 'Domain deprecated', '域名已更新', '新域名', '最新域名', '域名变更']
|
||||||
print("检测到域名变更通知,正在提取新域名...")
|
if any(indicator in response.text for indicator in change_indicators):
|
||||||
h2_matches = re.findall(r'<h2>.*?(?:域名|domain)[::]\s*([a-zA-Z0-9.-]+)</h2>', response.text)
|
print("🔄 检测到域名变更通知,正在提取新域名...")
|
||||||
if h2_matches:
|
domains = extract_domains_from_content(response.text)
|
||||||
return h2_matches[0]
|
for domain in domains:
|
||||||
js_matches = re.findall(r'https?://([a-zA-Z0-9.-]+)/auth/login', response.text)
|
if 'ikuuu' in domain and domain != ikun_host:
|
||||||
if js_matches:
|
print(f"🎯 找到新域名: {domain}")
|
||||||
return js_matches[0]
|
return domain
|
||||||
fallback_match = re.search(r'(?:域名|domain)[::]\s*([a-zA-Z0-9.-]+)', response.text)
|
|
||||||
if fallback_match:
|
|
||||||
return fallback_match.group(1)
|
|
||||||
print("⚠️ 检测到域名变更但无法提取新域名")
|
|
||||||
return None
|
|
||||||
else:
|
else:
|
||||||
print("✅ 当前域名正常")
|
print("✅ 当前域名正常")
|
||||||
return None
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"域名检测异常: {e}")
|
print(f"🔍 当前域名检测异常: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def update_self_host(new_host):
|
def update_self_host(new_host):
|
||||||
|
"""更新脚本中的域名"""
|
||||||
script_path = os.path.abspath(__file__)
|
script_path = os.path.abspath(__file__)
|
||||||
|
try:
|
||||||
with open(script_path, "r", encoding="utf-8") as f:
|
with open(script_path, "r", encoding="utf-8") as f:
|
||||||
lines = f.readlines()
|
lines = f.readlines()
|
||||||
|
|
||||||
updated = False
|
updated = False
|
||||||
for i, line in enumerate(lines):
|
for i, line in enumerate(lines):
|
||||||
if line.strip().startswith("ikun_host = "):
|
if line.strip().startswith("ikun_host = "):
|
||||||
lines[i] = f'ikun_host = "{new_host}" # 自动更新于{datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")}\n'
|
now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
lines[i] = f'ikun_host = "{new_host}" # 自动更新于{now}\n'
|
||||||
updated = True
|
updated = True
|
||||||
break
|
break
|
||||||
|
|
||||||
if updated:
|
if updated:
|
||||||
with open(script_path, "w", encoding="utf-8") as f:
|
with open(script_path, "w", encoding="utf-8") as f:
|
||||||
f.writelines(lines)
|
f.writelines(lines)
|
||||||
print(f"✅ 脚本已更新至域名: {new_host}")
|
print(f"✅ 脚本已更新至域名: {new_host}")
|
||||||
return True
|
return True
|
||||||
else:
|
|
||||||
print("⚠️ 域名更新失败")
|
print("⚠️ 未找到域名配置行,无法自动更新")
|
||||||
return False
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ 域名更新失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def test_host_reachable(host):
|
def test_host_reachable(host):
|
||||||
|
"""测试域名是否可达"""
|
||||||
try:
|
try:
|
||||||
response = requests.get(f"https://{host}/", timeout=10)
|
print(f"🔗 测试域名: {host}")
|
||||||
return response.status_code == 200
|
response = requests.get(f"https://{host}/", headers={"User-Agent": USER_AGENT}, timeout=10)
|
||||||
except:
|
if response.status_code == 200:
|
||||||
|
print(f"✅ 域名 {host} 可用")
|
||||||
|
return True
|
||||||
|
print(f"⚠️ 域名 {host} 返回状态码: {response.status_code}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 域名 {host} 不可用: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get_remaining_flow(cookies):
|
|
||||||
"""获取用户剩余流量信息"""
|
|
||||||
user_url = f'https://{ikun_host}/user'
|
|
||||||
try:
|
|
||||||
# 获取用户页面
|
|
||||||
user_page = requests.get(user_url, cookies=cookies, timeout=15)
|
|
||||||
if user_page.status_code != 200:
|
|
||||||
return "获取流量失败", "状态码: " + str(user_page.status_code)
|
|
||||||
|
|
||||||
# 使用BeautifulSoup解析HTML
|
def find_working_domain():
|
||||||
soup = BeautifulSoup(user_page.text, 'html.parser')
|
"""寻找可用的域名"""
|
||||||
|
global ikun_host
|
||||||
|
print(f"🏠 当前域名: {ikun_host}")
|
||||||
|
if test_host_reachable(ikun_host):
|
||||||
|
return ikun_host
|
||||||
|
|
||||||
# 查找包含剩余流量的卡片
|
discovered_domains = []
|
||||||
flow_cards = soup.find_all('div', class_='card card-statistic-2')
|
for domain in [ikun_host] + backup_hosts:
|
||||||
for card in flow_cards:
|
discovered_domains.extend(get_available_domains_from_old_domain(domain))
|
||||||
h4_tag = card.find('h4')
|
|
||||||
if h4_tag and '剩余流量' in h4_tag.text:
|
|
||||||
# 查找流量数值
|
|
||||||
counter_span = card.find('span', class_='counter')
|
|
||||||
if counter_span:
|
|
||||||
flow_value = counter_span.text.strip()
|
|
||||||
|
|
||||||
# 查找流量单位
|
discovered_domains = list(set(discovered_domains))
|
||||||
unit_text = ""
|
print(f"🔍 发现的域名: {discovered_domains}")
|
||||||
next_sibling = counter_span.next_sibling
|
for domain in discovered_domains:
|
||||||
if next_sibling:
|
if domain != ikun_host and test_host_reachable(domain):
|
||||||
unit_text = next_sibling.strip()
|
ikun_host = domain
|
||||||
|
update_self_host(domain)
|
||||||
|
return domain
|
||||||
|
|
||||||
return flow_value, unit_text
|
print("🔄 测试备用域名列表...")
|
||||||
|
for host in backup_hosts:
|
||||||
|
if host != ikun_host and test_host_reachable(host):
|
||||||
|
ikun_host = host
|
||||||
|
return host
|
||||||
|
|
||||||
# 如果没有找到,尝试其他方式
|
print("❌ 所有域名均不可用")
|
||||||
flow_div = soup.find('div', string='剩余流量')
|
return None
|
||||||
if flow_div:
|
|
||||||
parent_div = flow_div.find_parent('div', class_='card-body')
|
|
||||||
if parent_div:
|
|
||||||
flow_text = parent_div.get_text(strip=True).replace('剩余流量', '')
|
|
||||||
return flow_text.split()[0], flow_text.split()[1] if len(flow_text.split()) > 1 else ""
|
|
||||||
|
|
||||||
return "未找到", "流量信息"
|
|
||||||
|
|
||||||
except Exception as e:
|
def parse_accounts():
|
||||||
return "流量获取异常", str(e)
|
account_str = os.getenv('IKUUU_ACCOUNTS')
|
||||||
|
if not account_str:
|
||||||
|
print("❌ 未找到环境变量 IKUUU_ACCOUNTS")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
def ikuuu_signin(email, password):
|
accounts = []
|
||||||
params = {'email': email, 'passwd': password, 'code': ''}
|
for line in account_str.strip().splitlines():
|
||||||
login_url = f'https://{ikun_host}/auth/login'
|
if ':' not in line:
|
||||||
try:
|
print(f"⚠️ 忽略无效账户行: {line}")
|
||||||
# 登录请求
|
continue
|
||||||
login_res = requests.post(login_url, data=params, timeout=15)
|
email, password = line.split(':', 1)
|
||||||
if login_res.status_code != 200:
|
email = email.strip()
|
||||||
flow_value, flow_unit = "登录失败", "无法获取"
|
password = password.strip()
|
||||||
return False, f"登录失败(状态码{login_res.status_code})", flow_value, flow_unit
|
if email and password:
|
||||||
|
accounts.append((email, password))
|
||||||
login_data = login_res.json()
|
|
||||||
if login_data.get('ret') != 1:
|
|
||||||
flow_value, flow_unit = "登录失败", "无法获取"
|
|
||||||
return False, f"登录失败:{login_data.get('msg', '未知错误')}", flow_value, flow_unit
|
|
||||||
|
|
||||||
# 获取用户剩余流量
|
|
||||||
cookies = login_res.cookies
|
|
||||||
flow_value, flow_unit = get_remaining_flow(cookies)
|
|
||||||
|
|
||||||
# 执行签到
|
|
||||||
checkin_res = requests.post(f'https://{ikun_host}/user/checkin', cookies=cookies, timeout=15)
|
|
||||||
if checkin_res.status_code != 200:
|
|
||||||
return False, f"签到失败(状态码{checkin_res.status_code})", flow_value, flow_unit
|
|
||||||
|
|
||||||
checkin_data = checkin_res.json()
|
|
||||||
if checkin_data.get('ret') == 1:
|
|
||||||
return True, f"成功 | {checkin_data.get('msg', '')}", flow_value, flow_unit
|
|
||||||
else:
|
else:
|
||||||
return False, f"签到失败:{checkin_data.get('msg', '未知错误')}", flow_value, flow_unit
|
print(f"⚠️ 忽略无效账户行: {line}")
|
||||||
except json.JSONDecodeError:
|
|
||||||
return False, "响应解析失败", "未知", "未知"
|
if not accounts:
|
||||||
except requests.exceptions.Timeout:
|
print("❌ 未找到有效账户")
|
||||||
return False, "请求超时", "未知", "未知"
|
sys.exit(1)
|
||||||
|
|
||||||
|
print(f"📋 找到 {len(accounts)} 个账户")
|
||||||
|
return accounts
|
||||||
|
|
||||||
|
|
||||||
|
def find_browser_path():
|
||||||
|
env_names = ['IKUUU_BROWSER_PATH', 'CHROME_PATH', 'CHROMIUM_PATH', 'BROWSER_PATH']
|
||||||
|
for env_name in env_names:
|
||||||
|
env_path = os.getenv(env_name)
|
||||||
|
if env_path:
|
||||||
|
if os.path.exists(env_path):
|
||||||
|
return env_path
|
||||||
|
print(f"⚠️ {env_name} 指定的浏览器不存在: {env_path}")
|
||||||
|
|
||||||
|
executable_names = ['chromium-browser', 'chromium', 'google-chrome', 'google-chrome-stable', 'chrome']
|
||||||
|
for name in executable_names:
|
||||||
|
path = shutil.which(name)
|
||||||
|
if path:
|
||||||
|
return path
|
||||||
|
|
||||||
|
common_paths = [
|
||||||
|
'/usr/bin/chromium-browser',
|
||||||
|
'/usr/bin/chromium',
|
||||||
|
'/usr/bin/google-chrome',
|
||||||
|
'/usr/bin/google-chrome-stable',
|
||||||
|
'/opt/google/chrome/chrome',
|
||||||
|
r'C:\Program Files\Google\Chrome\Application\chrome.exe',
|
||||||
|
r'C:\Program Files (x86)\Google\Chrome\Application\chrome.exe',
|
||||||
|
]
|
||||||
|
for path in common_paths:
|
||||||
|
if os.path.exists(path):
|
||||||
|
return path
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def print_browser_install_help():
|
||||||
|
print("❌ 未找到Chrome/Chromium浏览器可执行文件")
|
||||||
|
print("请进入青龙容器安装浏览器,或设置 IKUUU_BROWSER_PATH 指向已有浏览器路径。")
|
||||||
|
print("常用检查命令:")
|
||||||
|
print(" which chromium chromium-browser google-chrome google-chrome-stable chrome")
|
||||||
|
print("Debian/Ubuntu容器可尝试:")
|
||||||
|
print(" apt-get update && apt-get install -y chromium")
|
||||||
|
print("Alpine容器可尝试:")
|
||||||
|
print(" apk add --no-cache chromium")
|
||||||
|
print("安装后在青龙环境变量中设置,例如:")
|
||||||
|
print(" IKUUU_BROWSER_PATH=/usr/bin/chromium")
|
||||||
|
|
||||||
|
|
||||||
|
def init_browser():
|
||||||
|
try:
|
||||||
|
from DrissionPage import ChromiumOptions, ChromiumPage
|
||||||
|
except ImportError:
|
||||||
|
print("❌ 缺少依赖 DrissionPage,请在青龙依赖管理中安装")
|
||||||
|
raise
|
||||||
|
|
||||||
|
co = ChromiumOptions()
|
||||||
|
browser_path = find_browser_path()
|
||||||
|
if not browser_path:
|
||||||
|
print_browser_install_help()
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
co.set_browser_path(browser_path)
|
||||||
|
print(f"✅ 使用浏览器: {browser_path}")
|
||||||
|
|
||||||
|
co.set_argument('--no-sandbox')
|
||||||
|
co.set_argument('--disable-dev-shm-usage')
|
||||||
|
co.set_argument('--disable-gpu')
|
||||||
|
co.set_argument('--headless=new')
|
||||||
|
co.set_argument('--window-size=1920,1080')
|
||||||
|
co.set_argument('--disable-blink-features=AutomationControlled')
|
||||||
|
co.set_argument('--disable-infobars')
|
||||||
|
co.set_user_agent(USER_AGENT)
|
||||||
|
|
||||||
|
page = ChromiumPage(co)
|
||||||
|
print("✅ 浏览器初始化成功")
|
||||||
|
return page
|
||||||
|
|
||||||
|
|
||||||
|
def click_geetest(page):
|
||||||
|
"""点击极验验证按钮。该步骤依赖站点当前是否允许无交互验证通过。"""
|
||||||
|
try:
|
||||||
|
print("⏳ 等待验证码加载...")
|
||||||
|
time.sleep(8)
|
||||||
|
selectors = [
|
||||||
|
'.geetest_btn_click',
|
||||||
|
'@aria-label=点击按钮开始验证',
|
||||||
|
'.geetest_btn',
|
||||||
|
'.embed-captcha',
|
||||||
|
]
|
||||||
|
for selector in selectors:
|
||||||
|
try:
|
||||||
|
btn = page.ele(selector, timeout=3)
|
||||||
|
if btn and btn.states.is_displayed:
|
||||||
|
print(f"✅ 找到验证按钮: {selector}")
|
||||||
|
btn.click()
|
||||||
|
time.sleep(5)
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
for div in page.eles('tag:div')[:120]:
|
||||||
|
cls = div.attr('class') or ''
|
||||||
|
if 'geetest' in cls.lower() and div.states.is_displayed:
|
||||||
|
div.click()
|
||||||
|
time.sleep(5)
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
print("⚠️ 未找到验证按钮")
|
||||||
|
return False
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return False, f"请求异常:{str(e)}", "未知", "未知"
|
print(f"⚠️ 点击验证按钮失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
def send_qinglong_notification(results):
|
|
||||||
"""
|
def logout(page, site_url):
|
||||||
使用青龙面板内置通知系统发送通知
|
try:
|
||||||
需要青龙面板已配置通知渠道(如钉钉、企业微信等)
|
page.get(f'{site_url}/user/logout')
|
||||||
"""
|
time.sleep(3)
|
||||||
|
page.run_js('document.cookie.split(";").forEach(c => document.cookie = c.trim().split("=")[0] + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/")')
|
||||||
|
print("✅ 已退出登录")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ 退出登录失败: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def get_logged_email(page, site_url):
|
||||||
|
try:
|
||||||
|
page.get(f'{site_url}/user/profile')
|
||||||
|
time.sleep(3)
|
||||||
|
email_ele = page.ele('tag:input@@id():email', timeout=5)
|
||||||
|
if email_ele:
|
||||||
|
return email_ele.attr('value') or email_ele.text
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def login(page, site_url, email, password):
|
||||||
|
try:
|
||||||
|
masked = mask_email(email)
|
||||||
|
print(f"🔐 正在登录: {masked}")
|
||||||
|
page.get(f'{site_url}/user')
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
|
if '/auth/login' not in page.url:
|
||||||
|
logged_email = get_logged_email(page, site_url)
|
||||||
|
if logged_email and logged_email.lower() == email.lower():
|
||||||
|
print(f"✅ 当前已登录目标账号: {masked}")
|
||||||
|
return True
|
||||||
|
print("ℹ️ 当前登录账号不一致,正在退出...")
|
||||||
|
logout(page, site_url)
|
||||||
|
page.get(f'{site_url}/user')
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
|
email_input = page.ele('#email', timeout=20)
|
||||||
|
if not email_input:
|
||||||
|
print("❌ 未找到邮箱输入框")
|
||||||
|
return False
|
||||||
|
email_input.clear()
|
||||||
|
email_input.input(email)
|
||||||
|
|
||||||
|
password_input = page.ele('#password', timeout=10)
|
||||||
|
if not password_input:
|
||||||
|
print("❌ 未找到密码输入框")
|
||||||
|
return False
|
||||||
|
password_input.clear()
|
||||||
|
password_input.input(password)
|
||||||
|
|
||||||
|
click_geetest(page)
|
||||||
|
|
||||||
|
login_btn = page.ele('tag:button@@text():Login', timeout=5) or page.ele('.login', timeout=5)
|
||||||
|
if not login_btn:
|
||||||
|
print("❌ 未找到登录按钮")
|
||||||
|
return False
|
||||||
|
login_btn.click()
|
||||||
|
time.sleep(6)
|
||||||
|
|
||||||
|
if '/user' in page.url and '/auth/login' not in page.url:
|
||||||
|
print("✅ 登录成功")
|
||||||
|
return True
|
||||||
|
|
||||||
|
error_msg = page.ele('.swal2-html-container', timeout=3) or page.ele('.swal-text', timeout=1)
|
||||||
|
if error_msg:
|
||||||
|
print(f"❌ 登录失败: {error_msg.text}")
|
||||||
|
else:
|
||||||
|
print("❌ 登录失败,请检查账号密码或验证码状态")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 登录过程出错: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_traffic_info(page):
|
||||||
|
try:
|
||||||
|
traffic_card = page.ele('tag:h4@@text():剩余流量', timeout=5)
|
||||||
|
if traffic_card:
|
||||||
|
current = traffic_card.parent()
|
||||||
|
for _ in range(5):
|
||||||
|
if not current:
|
||||||
|
break
|
||||||
|
text = current.text or ''
|
||||||
|
remaining_match = re.search(r'(\d+(?:\.\d+)?)\s*(B|KB|MB|GB|TB|PB)', text, re.IGNORECASE)
|
||||||
|
today_match = re.search(r'今日[^\d]*(\d+(?:\.\d+)?)\s*(B|KB|MB|GB|TB|PB)', text, re.IGNORECASE)
|
||||||
|
if remaining_match:
|
||||||
|
remaining = f"{remaining_match.group(1)} {remaining_match.group(2).upper()}"
|
||||||
|
today_used = '未知'
|
||||||
|
if today_match:
|
||||||
|
today_used = f"{today_match.group(1)} {today_match.group(2).upper()}"
|
||||||
|
return remaining, today_used
|
||||||
|
current = current.parent()
|
||||||
|
|
||||||
|
page_text = page.ele('tag:body', timeout=5).text
|
||||||
|
remaining_match = re.search(r'(?:剩余流量|可用流量|流量余额)[^\d]{0,80}(\d+(?:\.\d+)?)\s*(B|KB|MB|GB|TB|PB)', page_text, re.IGNORECASE)
|
||||||
|
today_match = re.search(r'(?:今日已用|今日使用|已用流量)[^\d]{0,80}(\d+(?:\.\d+)?)\s*(B|KB|MB|GB|TB|PB)', page_text, re.IGNORECASE)
|
||||||
|
remaining = f"{remaining_match.group(1)} {remaining_match.group(2).upper()}" if remaining_match else '未知'
|
||||||
|
today_used = f"{today_match.group(1)} {today_match.group(2).upper()}" if today_match else '未知'
|
||||||
|
return remaining, today_used
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ 获取流量信息失败: {e}")
|
||||||
|
return '未知', '未知'
|
||||||
|
|
||||||
|
|
||||||
|
def checkin(page, site_url):
|
||||||
|
try:
|
||||||
|
if '/user' not in page.url:
|
||||||
|
page.get(f'{site_url}/user')
|
||||||
|
time.sleep(3)
|
||||||
|
|
||||||
|
checkin_div = page.ele('#checkin-div', timeout=5)
|
||||||
|
if checkin_div and ('已签到' in checkin_div.text or '明日再来' in checkin_div.text):
|
||||||
|
print("✅ 今日已签到")
|
||||||
|
return True, '今日已签到'
|
||||||
|
|
||||||
|
try:
|
||||||
|
page.run_js('checkin()')
|
||||||
|
time.sleep(5)
|
||||||
|
msg = page.ele('.swal2-html-container', timeout=10) or page.ele('.swal-text', timeout=1)
|
||||||
|
if msg:
|
||||||
|
print(f"✅ 签到结果: {msg.text}")
|
||||||
|
return True, msg.text
|
||||||
|
|
||||||
|
checkin_div = page.ele('#checkin-div', timeout=3)
|
||||||
|
if checkin_div and ('已签到' in checkin_div.text or '已签' in checkin_div.text):
|
||||||
|
return True, '签到成功'
|
||||||
|
return True, '签到成功'
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ JS签到失败: {e}")
|
||||||
|
return False, '签到失败'
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 签到过程出错: {e}")
|
||||||
|
return False, str(e)
|
||||||
|
|
||||||
|
|
||||||
|
def send_qinglong_notification(results, current_domain):
|
||||||
title = "iKuuu签到通知"
|
title = "iKuuu签到通知"
|
||||||
|
|
||||||
# 构建消息内容
|
|
||||||
success_count = sum(1 for res in results if res['success'])
|
success_count = sum(1 for res in results if res['success'])
|
||||||
failure_count = len(results) - success_count
|
failure_count = len(results) - success_count
|
||||||
|
|
||||||
message = [
|
message = [
|
||||||
f"🔔 签到完成 | 成功:{success_count} 失败:{failure_count}",
|
f"🔔 签到完成 | 成功:{success_count} 失败:{failure_count}",
|
||||||
|
f"🌐 当前域名:{current_domain}",
|
||||||
"================================"
|
"================================"
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -196,86 +491,105 @@ def send_qinglong_notification(results):
|
|||||||
message.append(f"{index}. {res['email']}")
|
message.append(f"{index}. {res['email']}")
|
||||||
message.append(f" 状态:{status}")
|
message.append(f" 状态:{status}")
|
||||||
message.append(f" 详情:{res['message']}")
|
message.append(f" 详情:{res['message']}")
|
||||||
message.append(f" 剩余流量:{res['flow_value']} {res['flow_unit']}")
|
message.append(f" 剩余流量:{res['remaining_flow']}")
|
||||||
|
message.append(f" 今日已用:{res['today_used']}")
|
||||||
message.append("--------------------------------")
|
message.append("--------------------------------")
|
||||||
|
|
||||||
# 添加统计信息
|
|
||||||
message.append("\n🕒 执行时间:" + datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
|
message.append("\n🕒 执行时间:" + datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 发送通知(青龙自动处理多通知渠道)
|
|
||||||
send(title, "\n".join(message))
|
send(title, "\n".join(message))
|
||||||
print("✅ 通知已发送")
|
print("✅ 通知已发送")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"⚠️ 通知发送失败,请检查通知配置: {str(e)}")
|
print(f"⚠️ 通知发送失败,请检查通知配置: {str(e)}")
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
# ==================== 域名更新逻辑 ====================
|
def main():
|
||||||
print(f"当前域名: {ikun_host}")
|
print("🚀 iKuuu签到脚本启动")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
latest_host = get_latest_ikun_host()
|
latest_host = get_latest_ikun_host()
|
||||||
if latest_host:
|
if latest_host and latest_host != ikun_host:
|
||||||
print(f"检测到新域名: {latest_host}")
|
print(f"🔄 检测到新域名: {latest_host}")
|
||||||
if update_self_host(latest_host):
|
if update_self_host(latest_host):
|
||||||
ikun_host = latest_host
|
globals()['ikun_host'] = latest_host
|
||||||
|
|
||||||
# ==================== 域名可用性检查 ====================
|
working_domain = find_working_domain()
|
||||||
if not test_host_reachable(ikun_host):
|
if not working_domain:
|
||||||
print("主域名不可用,尝试备用域名...")
|
print("💥 无法找到可用域名,脚本退出")
|
||||||
found = False
|
sys.exit(1)
|
||||||
for host in backup_hosts:
|
|
||||||
if test_host_reachable(host):
|
|
||||||
ikun_host = host
|
|
||||||
print(f"切换到备用域名: {ikun_host}")
|
|
||||||
found = True
|
|
||||||
break
|
|
||||||
if not found:
|
|
||||||
print("❌ 所有域名均不可用")
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
# ==================== 账户处理 ====================
|
site_url = f"https://{working_domain}"
|
||||||
accounts = []
|
print(f"🎯 使用域名: {working_domain}")
|
||||||
account_str = os.getenv('IKUUU_ACCOUNTS')
|
print("=" * 50)
|
||||||
if not account_str:
|
|
||||||
print("❌ 未找到环境变量 IKUUU_ACCOUNTS")
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
for line in account_str.strip().splitlines():
|
accounts = parse_accounts()
|
||||||
if ':' in line:
|
page = None
|
||||||
email, pwd = line.split(':', 1)
|
|
||||||
accounts.append((email.strip(), pwd.strip()))
|
|
||||||
else:
|
|
||||||
print(f"⚠️ 忽略无效账户行: {line}")
|
|
||||||
|
|
||||||
if not accounts:
|
|
||||||
print("❌ 未找到有效账户")
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
# ==================== 执行签到 ====================
|
|
||||||
results = []
|
results = []
|
||||||
for email, pwd in accounts:
|
|
||||||
print(f"\n处理账户: {email}")
|
try:
|
||||||
success, msg, flow_value, flow_unit = ikuuu_signin(email, pwd)
|
page = init_browser()
|
||||||
results.append({
|
for index, (email, password) in enumerate(accounts, 1):
|
||||||
'email': email,
|
masked_email = mask_email(email)
|
||||||
|
print(f"\n👤 [{index}/{len(accounts)}] 处理账户: {masked_email}")
|
||||||
|
result = {
|
||||||
|
'email': masked_email,
|
||||||
|
'success': False,
|
||||||
|
'message': '登录失败',
|
||||||
|
'remaining_flow': '未知',
|
||||||
|
'today_used': '未知'
|
||||||
|
}
|
||||||
|
|
||||||
|
if login(page, site_url, email, password):
|
||||||
|
success, msg = checkin(page, site_url)
|
||||||
|
remaining, today_used = get_traffic_info(page)
|
||||||
|
result.update({
|
||||||
'success': success,
|
'success': success,
|
||||||
'message': msg,
|
'message': msg,
|
||||||
'flow_value': flow_value,
|
'remaining_flow': remaining,
|
||||||
'flow_unit': flow_unit
|
'today_used': today_used
|
||||||
})
|
})
|
||||||
print(f"结果: {'成功' if success else '失败'} - {msg}")
|
print(f" 📊 剩余流量: {remaining}")
|
||||||
print(f"剩余流量: {flow_value} {flow_unit}")
|
print(f" 📈 今日已用: {today_used}")
|
||||||
|
logout(page, site_url)
|
||||||
|
|
||||||
# 账户间延迟防止请求过快
|
results.append(result)
|
||||||
time.sleep(1)
|
status_icon = "✅" if result['success'] else "❌"
|
||||||
|
print(f" {status_icon} 结果: {result['message']}")
|
||||||
|
|
||||||
# ==================== 结果通知 ====================
|
if index < len(accounts):
|
||||||
print("\n正在发送通知...")
|
time.sleep(2)
|
||||||
send_qinglong_notification(results)
|
finally:
|
||||||
|
if page:
|
||||||
|
try:
|
||||||
|
page.quit()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
print("\n📢 正在发送通知...")
|
||||||
|
send_qinglong_notification(results, working_domain)
|
||||||
|
|
||||||
|
print("\n📊 签到结果汇总:")
|
||||||
|
print("=" * 50)
|
||||||
|
success_count = sum(1 for res in results if res['success'])
|
||||||
|
print(f"🎯 总账户数: {len(results)}")
|
||||||
|
print(f"✅ 成功: {success_count}")
|
||||||
|
print(f"❌ 失败: {len(results) - success_count}")
|
||||||
|
print(f"🌐 使用域名: {working_domain}")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
# ==================== 本地结果输出 ====================
|
|
||||||
print("\n签到结果汇总:")
|
|
||||||
for res in results:
|
for res in results:
|
||||||
print(f"邮箱: {res['email']}")
|
status_icon = "✅" if res['success'] else "❌"
|
||||||
print(f"状态: {'成功' if res['success'] else '失败'}")
|
print(f"{status_icon} {res['email']}")
|
||||||
print(f" 详情: {res['message']}")
|
print(f" 详情: {res['message']}")
|
||||||
print(f"剩余流量: {res['flow_value']} {res['flow_unit']}\n{'-'*40}")
|
print(f" 剩余流量: {res['remaining_flow']}")
|
||||||
|
print(f" 今日已用: {res['today_used']}")
|
||||||
|
|
||||||
|
print("=" * 50)
|
||||||
|
print("🏁 脚本执行完成")
|
||||||
|
|
||||||
|
if any(not res['success'] for res in results):
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|||||||
Reference in New Issue
Block a user