Compare commits

...
15 Commits
Author SHA1 Message Date
UPToZ 56d7cffdaa 修改iKuuu签到脚本 2026-06-06 14:56:09 +08:00
UPToZ cc6429b64b 修改ikuuu签到脚本 2026-06-04 17:02:52 +08:00
UPToZ 53332285f3 修复「剩余流量」查询 2025-07-29 11:37:19 +08:00
UPToZ fbfff5ba21 1. 智能域名发现机制
多模式域名提取:使用多种正则表达式模式从网页内容中提取域名
自动域名发现:当域名不可用时,会从旧域名页面自动抓取新的可用域名
优先级策略:优先使用ikuuu相关域名,其次使用其他发现的域名

2. 增强的域名检测逻辑

内容解析:能识别多种域名变更通知格式
链接提取:从JavaScript跳转、登录链接等多个位置提取域名
智能过滤:过滤掉无效的域名格式

3. 完善的容错机制

逐步回退:当前域名 → 发现的新域名 → 备用域名列表
连接测试:每个域名都会进行可达性测试
异常处理:增加了超时、连接错误等异常处理

4. 改进的用户体验

详细日志:每个步骤都有清晰的状态提示和图标
进度显示:显示账户处理进度
结果统计:提供详细的执行结果汇总

5. 新增功能

域名信息通知:在通知中包含当前使用的域名
更长延迟:账户间延迟增加到2秒,避免请求过快
更好的错误提示:各种异常情况都有对应的错误信息
2025-07-25 10:08:23 +08:00
UPToZ b4b3b29661 更新 iKuuu/ik_signin.py 2025-07-15 09:17:09 +08:00
UPToZ 872f0a5d71 更新 iKuuu/ik_signin.py
增加接口请求的超时时间
2025-07-14 10:30:35 +08:00
UPToZ 8a43d320cf 接口请求统一增加UA标识 2025-07-14 09:37:30 +08:00
UPToZ fc97994ba2 更新 README.md 2025-06-03 22:12:24 +08:00
UPToZ b8d08736b5 增加飞牛论坛签到脚本 2025-06-03 21:09:01 +08:00
UPToZ 2e761c690b 修复通知异常问题 2025-06-03 09:38:46 +08:00
UPToZ 1e248d72df 增加百度贴吧签到脚本 2025-06-02 17:59:24 +08:00
UPToZ be13cc915e 更新定时规则 2025-06-02 11:19:49 +08:00
UPToZ a9ec7ab3b7 更新 README.md 2025-06-01 22:03:23 +08:00
UPToZ d58cc2cd7d 增加脚本注释 2025-06-01 20:57:17 +08:00
UPToZ 245386f46f 增加脚本注释 2025-06-01 20:54:34 +08:00
8 changed files with 2078 additions and 310 deletions
+106
View File
@@ -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. 使用本脚本即视为同意上述条款,请勿在未经允许的情况下将脚本用于他人账号。
+731
View File
@@ -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&paragraph=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())
+60 -2
View File
@@ -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
View File
@@ -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. 多账号处理时,账号间延迟可避免触发风控机制
通过以上步骤配置后,脚本将自动完成百度贴吧的签到任务,并通过通知推送结果,实现无人值守的贴吧签到管理。
+353
View File
@@ -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==== 全部处理完成 ====")
+4 -16
View File
@@ -1,20 +1,8 @@
""" """
描述: 打开并登录“同程旅行”APP,开启抓包,点击APP右下角“我的”-“签到”,进去后抓包域名https://app.17u.cn请求头中的phone、apptoken、device三个参数。 任务名称
环境变量: name: 同程旅行签到
变量名:tongcheng 定时规则
变量格式:phone&apptoken&device cron: 0 0 4 * * ?
多账号之间支持用#或换行分隔:
phone1&apptoken1&device1
phone2&apptoken2&device2
需要依赖:aiohttp
签到奖励:日常任务得里程,签到满360天得价值200元里程
----------------------
版本: 1.6
更新说明:
1. 优化环境变量解析,支持使用换行符分隔多账号
2. 增强环境变量格式验证和错误处理
3. 优化日志输出,明确显示解析到的账号数量
4. 保留之前所有功能和优化
""" """
from datetime import datetime from datetime import datetime
+62
View File
@@ -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
- 增加剩余流量显示 - 增加剩余流量显示
+499 -181
View File
@@ -1,29 +1,34 @@
# -*- coding: utf-8 -* """
''' 任务名称
定时自定义 name: iKuuu签到
0 0 1 * * ? 定时规则
new Env('iKuuu签到'); cron: 0 0 8 * * ?
''' """
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:
@@ -35,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}",
"================================" "================================"
] ]
@@ -192,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()