Compare commits

..
19 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
UPToZ 19a69c4204 Merge branch 'master' of https://gitea.uptoz.cn/UPToZ/qinglong-scripts
# Conflicts:
#	iKuuu/ik_signin.py
2025-06-01 20:37:34 +08:00
UPToZ ac2529f718 增加同城旅行每日签到脚本 2025-06-01 20:32:37 +08:00
UPToZ 71d0873821 更新 iKuuu/ik_signin.py 2025-06-01 17:31:43 +08:00
UPToZ 35e3a329c0 Merge branch 'master' of https://gitea.uptoz.cn/UPToZ/qinglong-scripts
# Conflicts:
#	iKuuu/ik_signin.py
2025-06-01 17:29:43 +08:00
9 changed files with 2658 additions and 298 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==== 全部处理完成 ====")
+123
View File
@@ -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. **任务执行不完全**
- 脚本有内置的重试机制,但部分任务可能需要手动完成
- 检查任务列表中是否有特殊任务需要额外操作
## 免责声明
本脚本仅供学习交流使用,请勿用于商业用途。使用本脚本可能违反同程旅行的用户协议,请谨慎使用。开发者不对因使用本脚本而导致的任何问题负责。
+461
View File
@@ -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())
+62
View File
@@ -38,8 +38,70 @@ task ik_signin 0 0 1 * * ?
3. 依次对配置的所有账号进行签到操作
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
- 增加剩余流量显示
+498 -184
View File
@@ -1,33 +1,34 @@
# -*- coding: utf-8 -*
'''
定时自定义
<<<<<<< .mine
0 0 1 * * ?
=======
0 0 1 * * ? ik_signin.py
>>>>>>> .theirs
new Env('iKuuu签到');
'''
import requests
import re
import json
import os
"""
任务名称
name: iKuuu签到
定时规则
cron: 0 0 8 * * ?
"""
import datetime
import urllib.parse
import os
import re
import shutil
import sys
import time
from bs4 import BeautifulSoup
import requests
try:
sys.stdout.reconfigure(encoding="utf-8")
except Exception:
pass
# 添加青龙脚本根目录到Python路径
QL_SCRIPTS_DIR = '/ql/scripts' # 青龙脚本默认目录
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__) # 当前脚本目录
'/ql',
'/ql/data/scripts',
'/ql/scripts/notify',
os.path.dirname(__file__)
]
for path in POSSIBLE_PATHS:
@@ -39,155 +40,449 @@ try:
from notify import send
except ImportError:
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():
test_url = f"https://{ikun_host}/"
"""获取最新可用域名"""
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 "官网域名已更改" in response.text or "Domain deprecated" in response.text:
print("检测到域名变更通知,正在提取新域名...")
h2_matches = re.findall(r'<h2>.*?(?:域名|domain)[:]\s*([a-zA-Z0-9.-]+)</h2>', response.text)
if h2_matches:
return h2_matches[0]
js_matches = re.findall(r'https?://([a-zA-Z0-9.-]+)/auth/login', response.text)
if js_matches:
return js_matches[0]
fallback_match = re.search(r'(?:域名|domain)[:]\s*([a-zA-Z0-9.-]+)', response.text)
if fallback_match:
return fallback_match.group(1)
print("⚠️ 检测到域名变更但无法提取新域名")
return None
change_indicators = ['官网域名已更改', 'Domain deprecated', '域名已更新', '新域名', '最新域名', '域名变更']
if any(indicator in response.text for indicator in change_indicators):
print("🔄 检测到域名变更通知,正在提取新域名...")
domains = extract_domains_from_content(response.text)
for domain in domains:
if 'ikuuu' in domain and domain != ikun_host:
print(f"🎯 找到新域名: {domain}")
return domain
else:
print("✅ 当前域名正常")
return None
except Exception as e:
print(f"域名检测异常: {e}")
print(f"🔍 当前域名检测异常: {e}")
return None
def update_self_host(new_host):
"""更新脚本中的域名"""
script_path = os.path.abspath(__file__)
try:
with open(script_path, "r", encoding="utf-8") as f:
lines = f.readlines()
updated = False
for i, line in enumerate(lines):
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
break
if updated:
with open(script_path, "w", encoding="utf-8") as f:
f.writelines(lines)
print(f"✅ 脚本已更新至域名: {new_host}")
return True
else:
print("⚠️ 域名更新失败")
print("⚠️ 未找到域名配置行,无法自动更新")
return False
except Exception as e:
print(f"⚠️ 域名更新失败: {e}")
return False
def test_host_reachable(host):
"""测试域名是否可达"""
try:
response = requests.get(f"https://{host}/", timeout=10)
return response.status_code == 200
except:
print(f"🔗 测试域名: {host}")
response = requests.get(f"https://{host}/", headers={"User-Agent": USER_AGENT}, timeout=10)
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
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
soup = BeautifulSoup(user_page.text, 'html.parser')
def find_working_domain():
"""寻找可用的域名"""
global ikun_host
print(f"🏠 当前域名: {ikun_host}")
if test_host_reachable(ikun_host):
return ikun_host
# 查找包含剩余流量的卡片
flow_cards = soup.find_all('div', class_='card card-statistic-2')
for card in flow_cards:
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 = []
for domain in [ikun_host] + backup_hosts:
discovered_domains.extend(get_available_domains_from_old_domain(domain))
# 查找流量单位
unit_text = ""
next_sibling = counter_span.next_sibling
if next_sibling:
unit_text = next_sibling.strip()
discovered_domains = list(set(discovered_domains))
print(f"🔍 发现的域名: {discovered_domains}")
for domain in discovered_domains:
if domain != ikun_host and test_host_reachable(domain):
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
# 如果没有找到,尝试其他方式
flow_div = soup.find('div', string='剩余流量')
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 ""
print("❌ 所有域名均不可用")
return None
return "未找到", "流量信息"
except Exception as e:
return "流量获取异常", str(e)
def parse_accounts():
account_str = os.getenv('IKUUU_ACCOUNTS')
if not account_str:
print("❌ 未找到环境变量 IKUUU_ACCOUNTS")
sys.exit(1)
def ikuuu_signin(email, password):
params = {'email': email, 'passwd': password, 'code': ''}
login_url = f'https://{ikun_host}/auth/login'
try:
# 登录请求
login_res = requests.post(login_url, data=params, timeout=15)
if login_res.status_code != 200:
flow_value, flow_unit = "登录失败", "无法获取"
return False, f"登录失败(状态码{login_res.status_code}", flow_value, flow_unit
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
accounts = []
for line in account_str.strip().splitlines():
if ':' not in line:
print(f"⚠️ 忽略无效账户行: {line}")
continue
email, password = line.split(':', 1)
email = email.strip()
password = password.strip()
if email and password:
accounts.append((email, password))
else:
return False, f"签到失败:{checkin_data.get('msg', '未知错误')}", flow_value, flow_unit
except json.JSONDecodeError:
return False, "响应解析失败", "未知", "未知"
except requests.exceptions.Timeout:
return False, "请求超时", "未知", "未知"
print(f"⚠️ 忽略无效账户行: {line}")
if not accounts:
print("❌ 未找到有效账户")
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:
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签到通知"
# 构建消息内容
success_count = sum(1 for res in results if res['success'])
failure_count = len(results) - success_count
message = [
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" 状态:{status}")
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("\n🕒 执行时间:" + datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
try:
# 发送通知(青龙自动处理多通知渠道)
send(title, "\n".join(message))
print("✅ 通知已发送")
except Exception as e:
print(f"⚠️ 通知发送失败,请检查通知配置: {str(e)}")
if __name__ == "__main__":
# ==================== 域名更新逻辑 ====================
print(f"当前域名: {ikun_host}")
def main():
print("🚀 iKuuu签到脚本启动")
print("=" * 50)
latest_host = get_latest_ikun_host()
if latest_host:
print(f"检测到新域名: {latest_host}")
if latest_host and latest_host != ikun_host:
print(f"🔄 检测到新域名: {latest_host}")
if update_self_host(latest_host):
ikun_host = latest_host
globals()['ikun_host'] = latest_host
# ==================== 域名可用性检查 ====================
if not test_host_reachable(ikun_host):
print("主域名不可用,尝试备用域名...")
found = False
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)
working_domain = find_working_domain()
if not working_domain:
print("💥 无法找到可用域名,脚本退出")
sys.exit(1)
# ==================== 账户处理 ====================
accounts = []
account_str = os.getenv('IKUUU_ACCOUNTS')
if not account_str:
print("❌ 未找到环境变量 IKUUU_ACCOUNTS")
exit(1)
site_url = f"https://{working_domain}"
print(f"🎯 使用域名: {working_domain}")
print("=" * 50)
for line in account_str.strip().splitlines():
if ':' in line:
email, pwd = line.split(':', 1)
accounts.append((email.strip(), pwd.strip()))
else:
print(f"⚠️ 忽略无效账户行: {line}")
if not accounts:
print("❌ 未找到有效账户")
exit(1)
# ==================== 执行签到 ====================
accounts = parse_accounts()
page = None
results = []
for email, pwd in accounts:
print(f"\n处理账户: {email}")
success, msg, flow_value, flow_unit = ikuuu_signin(email, pwd)
results.append({
'email': email,
try:
page = init_browser()
for index, (email, password) in enumerate(accounts, 1):
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,
'message': msg,
'flow_value': flow_value,
'flow_unit': flow_unit
'remaining_flow': remaining,
'today_used': today_used
})
print(f"结果: {'成功' if success else '失败'} - {msg}")
print(f"剩余流量: {flow_value} {flow_unit}")
print(f" 📊 剩余流量: {remaining}")
print(f" 📈 今日已用: {today_used}")
logout(page, site_url)
# 账户间延迟防止请求过快
time.sleep(1)
results.append(result)
status_icon = "" if result['success'] else ""
print(f" {status_icon} 结果: {result['message']}")
# ==================== 结果通知 ====================
print("\n正在发送通知...")
send_qinglong_notification(results)
if index < len(accounts):
time.sleep(2)
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:
print(f"邮箱: {res['email']}")
print(f"状态: {'成功' if res['success'] else '失败'}")
status_icon = "" if res['success'] else ""
print(f"{status_icon} {res['email']}")
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()