Files
hello_js_reverse_skill/scripts/rainyun_checkin.py
T
yml2213 5835d7cb15 feat(rainyun): 完成雨云纯协议自动登录签到脚本及青龙单目录部署包
- 逆向分析雨云登录与签到接口,打通腾讯天御点选验证码全协议过码闭环
- 支持 RAINYUN_ACCOUNT='账号#密码' 极简配置 (多账号支持 & 与换行分割)
- 增加积分提现金额换算展示 (2000积分 = 1元现金) 及青龙系统通知推送
- 新增 dist/rainyun_qinglong 单层平铺部署目录与专属 README
- 增强 solver.py 在模板缺失时的 ResNet18 分类器双模降级能力
2026-09-12 10:56:25 +08:00

493 lines
18 KiB
Python

# -*- coding: utf-8 -*-
"""
cron: 30 8 * * *
new Env('雨云每日签到');
雨云 (Rainyun) 自动登录与每日签到脚本 (适用于青龙面板)
账号配置规范:
环境变量 / 本地 .env 统一使用 RAINYUN_ACCOUNT="账号#密码"
多账号分割:
- 青龙环境变量: 使用 & 分割 (如 RAINYUN_ACCOUNT="13754650804#pwd1&13800000000#pwd2")
- 本地 .env 文件: 支持 & 分割或换行分割
"""
import json
import logging
import os
import random
import re
import sys
import time
import uuid
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple
import httpx
# 将项目根目录加入 sys.path 以便加载本地模块
CURRENT_DIR = Path(__file__).resolve().parent
if str(CURRENT_DIR) not in sys.path:
sys.path.insert(0, str(CURRENT_DIR))
# 配置日志格式
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
datefmt="%H:%M:%S",
)
logger = logging.getLogger("rainyun_checkin")
# 青龙通知模块导入
try:
from sendNotify import send
except Exception:
try:
from notify import send
except Exception:
def send(title: str, content: str):
pass
# 默认配置
RAINYUN_API_BASE = "https://api.v2.rainyun.com"
TENCENT_AID = "2039519451"
DEFAULT_UA = (
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
)
# 积分提现比例: 2000 积分 = 1 元现金 (支持直接提现)
POINTS_PER_CNY = 2000.0
def format_points(points: int) -> str:
"""格式化积分及等值提现金额 (2000积分 = 1元)"""
cny = points / POINTS_PER_CNY
return f"{points} 积分 (≈ {cny:.2f} 元)"
def mask_field(val: str) -> str:
"""脱敏账号信息"""
if not val:
return ""
if len(val) <= 4:
return "***"
if len(val) == 11 and val.isdigit():
return f"{val[:3]}****{val[7:]}"
return f"{val[:2]}****{val[-2:]}"
class CaptchaManager:
"""验证码求解调度器:支持本地 tcaptcha 模块或远程 HTTP 接口"""
def __init__(self, api_url: Optional[str] = None):
self.api_url = api_url or os.getenv("TCAPTCHA_API_URL", "").strip()
self.local_client = None
# 尝试初始化本地客户端
if not self.api_url:
try:
from tcaptcha.client import TencentCaptchaClient
self.local_client = TencentCaptchaClient(aid=TENCENT_AID)
logger.info("✓ 成功加载本地 tcaptcha 纯算法求解引擎")
except Exception as e:
logger.warning(f"未能加载本地 tcaptcha 求解引擎 ({e}),将尝试远程接口或环境变量配置")
def get_ticket(self) -> Tuple[Optional[str], Optional[str]]:
"""获取腾讯天御点选验证码票据 (ticket, randstr)"""
# 1. 优先使用远程 API (若已配置)
if self.api_url:
logger.info(f"正在调用远程验证码求解接口: {self.api_url} ...")
try:
with httpx.Client(timeout=30.0, trust_env=False) as client:
resp = client.post(self.api_url, json={"aid": TENCENT_AID})
if resp.status_code == 200:
data = resp.json()
# 兼容直接返回或嵌套在 data 中
res_data = data.get("data", data)
ticket = res_data.get("ticket")
randstr = res_data.get("randstr")
if ticket and randstr:
logger.info(f"✓ 远程验证码求解成功 (randstr: {randstr})")
return ticket, randstr
else:
logger.error(f"远程验证码接口未返回有效票据: {data}")
else:
logger.error(f"远程验证码接口响应异常: {resp.status_code} {resp.text}")
except Exception as e:
logger.error(f"调用远程验证码接口失败: {e}")
# 2. 本地求解引擎
if self.local_client:
logger.info("正在调用本地 tcaptcha AI 纯协议求解器...")
try:
res = self.local_client.solve()
if res.get("success"):
logger.info(f"✓ 本地求解成功 (耗时: {res.get('elapsed_ms')}ms, randstr: {res.get('randstr')})")
return res.get("ticket"), res.get("randstr")
else:
logger.error(f"本地求解失败: {res.get('error')}")
except Exception as e:
logger.error(f"本地求解器执行异常: {e}")
logger.error("未找到可用的验证码求解方案!请确认部署了 tcaptcha 本地模型或配置了 TCAPTCHA_API_URL 环境变量")
return None, None
class RainyunClient:
"""雨云 API 客户端"""
def __init__(
self,
user: str,
password: str,
captcha_mgr: Optional[CaptchaManager] = None,
):
self.user = user
self.password = password
self.captcha_mgr = captcha_mgr or CaptchaManager()
self.account_name = mask_field(user) if user else "未知账号"
self.points_before = 0
self.points_after = 0
# 初始化 HTTP 客户端
self.session = httpx.Client(
base_url=RAINYUN_API_BASE,
timeout=15.0,
follow_redirects=True,
trust_env=False, # 屏蔽环境代理干扰以兼容各类青龙网络
)
self._update_common_headers()
def _update_common_headers(self):
rys = f"{uuid.uuid4().hex}{time.time():.3f}"
self.session.headers.update({
"User-Agent": DEFAULT_UA,
"Origin": "https://app.rainyun.com",
"Referer": "https://app.rainyun.com/",
"Accept": "application/json, text/plain, */*",
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
"rys": rys,
})
def login(self) -> bool:
"""执行登录并获取凭据"""
if not self.user or not self.password:
logger.error("缺少账号或密码!")
return False
logger.info(f"正在登录雨云账号: {self.account_name} ...")
self._update_common_headers()
payload = {
"field": self.user,
"password": self.password,
"verifyCode": "",
}
resp = self.session.post("/user/login", json=payload)
res_json = {}
try:
res_json = resp.json()
except Exception:
pass
# 检查是否需要验证码
if resp.status_code == 400 and res_json.get("code") in [10004, 10005]:
logger.info("雨云登录要求完成人机验证,正在调用验证码求解器...")
ticket, randstr = self.captcha_mgr.get_ticket()
if not ticket or not randstr:
logger.error("登录验证码获取失败,无法继续登录")
return False
payload["vticket"] = ticket
payload["vrandstr"] = randstr
resp = self.session.post("/user/login", json=payload)
try:
res_json = resp.json()
except Exception:
pass
if resp.status_code == 200 and res_json.get("code") == 200:
# 同步更新 CSRF Token
csrf = self.session.cookies.get("X-CSRF-Token")
if csrf:
self.session.headers["x-csrf-token"] = csrf
logger.info(f"✓ 账号 {self.account_name} 登录成功!")
return True
err_msg = res_json.get("message") or resp.text
logger.error(f"❌ 账号 {self.account_name} 登录失败: {err_msg}")
return False
def fetch_user_info(self) -> Optional[Dict[str, Any]]:
"""获取当前用户信息(积分、用户名等)"""
self._update_common_headers()
try:
resp = self.session.get("/user/?no_cache=true")
if resp.status_code == 200:
data = resp.json()
if data.get("code") == 200:
info = data.get("data", {})
self.account_name = info.get("Name") or self.account_name
self.points_before = info.get("Points", 0)
return info
except Exception as e:
logger.warning(f"获取用户信息异常: {e}")
return None
def fetch_tasks(self) -> List[Dict[str, Any]]:
"""获取任务列表"""
self._update_common_headers()
try:
resp = self.session.get("/user/reward/tasks")
if resp.status_code == 200:
data = resp.json()
if data.get("code") == 200:
return data.get("data", [])
except Exception as e:
logger.error(f"获取任务列表失败: {e}")
return []
def claim_daily_checkin(self) -> Dict[str, Any]:
"""
执行每日签到核心业务逻辑:
1. 检查任务列表与签到状态
2. 若 Status == 2: 今日已完成,直接跳过
3. 若 Status == 1: 待领取,获取验证码并提交
"""
user_info = self.fetch_user_info()
user_display = f"{self.account_name} (ID: {user_info.get('ID') if user_info else '未知'})"
tasks = self.fetch_tasks()
checkin_task = None
for t in tasks:
if t.get("Name") == "每日签到":
checkin_task = t
break
if not checkin_task:
return {
"user": user_display,
"status": "error",
"message": "未在任务列表中找到【每日签到】任务",
"points_before": self.points_before,
"points_after": self.points_before,
}
task_status = checkin_task.get("Status")
# Status 2: 今日已领取
if task_status == 2:
logger.info(f"✨ 账号【{user_display}】今日已完成每日签到,当前总资产: {format_points(self.points_before)}。")
return {
"user": user_display,
"status": "already_done",
"message": "今日已签到 (无需重复领取)",
"points_before": self.points_before,
"points_after": self.points_before,
}
# Status 1: 待签到
logger.info(f"🎯 账号【{user_display}】今日尚未签到,开始启动签到流程...")
# 1. 求解验证码
ticket, randstr = self.captcha_mgr.get_ticket()
if not ticket or not randstr:
return {
"user": user_display,
"status": "fail",
"message": "验证码求解失败,签到中止",
"points_before": self.points_before,
"points_after": self.points_before,
}
# 2. 提交签到请求
self._update_common_headers()
payload = {
"task_name": "每日签到",
"verifyCode": "",
"vticket": ticket,
"vrandstr": randstr,
}
resp = self.session.post("/user/reward/tasks", json=payload)
res_json = {}
try:
res_json = resp.json()
except Exception:
pass
if resp.status_code == 200 and res_json.get("code") == 200:
reward_data = res_json.get("data", {})
earned_points = reward_data.get("points", 500)
earned_cny = earned_points / POINTS_PER_CNY
# 刷新最新积分
latest_info = self.fetch_user_info()
self.points_after = latest_info.get("Points", self.points_before + earned_points) if latest_info else self.points_before + earned_points
msg = f"签到成功!获得 +{earned_points} 积分 (≈ {earned_cny:.2f} 元)"
logger.info(f"🎉 账号【{user_display}】{msg},当前总资产: {format_points(self.points_after)}")
return {
"user": user_display,
"status": "success",
"message": msg,
"earned_points": earned_points,
"points_before": self.points_before,
"points_after": self.points_after,
}
else:
err_msg = res_json.get("message") or resp.text
logger.error(f"❌ 账号【{user_display}】签到失败: {err_msg}")
return {
"user": user_display,
"status": "fail",
"message": f"签到请求失败: {err_msg}",
"points_before": self.points_before,
"points_after": self.points_before,
}
def load_dotenv(env_path: Optional[Path] = None):
"""自动加载本地 .env 文件到 os.environ,方便本地直接调试运行"""
search_paths = [
env_path,
CURRENT_DIR / ".env",
Path.cwd() / ".env",
]
for p in search_paths:
if p and p.is_file():
try:
with open(p, "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line or line.startswith("#"):
continue
if "=" in line:
k, v = line.split("=", 1)
k = k.strip()
v = v.strip()
if (v.startswith('"') and v.endswith('"')) or (v.startswith("'") and v.endswith("'")):
v = v[1:-1]
if k not in os.environ or not os.environ[k]:
os.environ[k] = v
logger.info(f"✓ 成功加载本地环境变量: {p}")
break
except Exception as e:
logger.warning(f"读取 .env 异常: {e}")
def parse_accounts() -> List[Dict[str, str]]:
"""
解析雨云账号配置 (优先加载本地 .env):
仅支持 RAINYUN_ACCOUNT="账号#密码" 格式
多账号使用 & 分割 (青龙面板推荐) 或 换行分割 (本地 .env 推荐)
例如:
RAINYUN_ACCOUNT="13754650804#pwd1&13800000000#pwd2"
"""
load_dotenv()
accounts = []
acc_env = os.getenv("RAINYUN_ACCOUNT", "").strip()
if not acc_env:
return []
# 去除外层引号
if (acc_env.startswith('"') and acc_env.endswith('"')) or (acc_env.startswith("'") and acc_env.endswith("'")):
acc_env = acc_env[1:-1].strip()
# 按照 & 或 换行 分割多账号
raw_list = re.split(r"[&\n]", acc_env)
for item in raw_list:
item = item.strip()
if not item or "#" not in item:
continue
u, p = item.split("#", 1)
u, p = u.strip(), p.strip()
if u and p and not any(a.get("user") == u for a in accounts):
accounts.append({"user": u, "password": p})
return accounts
def main():
print("=" * 60)
print(" 🚀 雨云 (Rainyun) 自动登录与每日签到任务启动")
print("=" * 60)
accounts = parse_accounts()
if not accounts:
msg = (
"未检测到雨云账号配置!请在青龙环境变量或本地 .env 中配置 RAINYUN_ACCOUNT\n"
"格式: 账号#密码 (多账号用 & 或换行分割)\n"
"例如: RAINYUN_ACCOUNT=\"13754650804#pwd1&13800000000#pwd2\"\n"
"如需远程验证码服务,可添加 TCAPTCHA_API_URL (如 http://x.x.x.x:8000/solve)"
)
logger.warning(msg)
send("雨云每日签到异常", msg)
return
logger.info(f"已成功加载 {len(accounts)} 个雨云账号配置")
captcha_mgr = CaptchaManager()
results = []
for idx, acc in enumerate(accounts, 1):
logger.info(f"\n--- [正在处理账号 {idx}/{len(accounts)}] ---")
client = RainyunClient(
user=acc["user"],
password=acc["password"],
captcha_mgr=captcha_mgr,
)
login_ok = client.login()
if not login_ok:
results.append({
"user": client.account_name,
"status": "login_failed",
"message": "登录失败",
"points_before": 0,
"points_after": 0,
})
continue
res = client.claim_daily_checkin()
results.append(res)
# 多账号之间随机休眠 2~4 秒
if idx < len(accounts):
sleep_sec = random.uniform(2.0, 4.0)
time.sleep(sleep_sec)
# 统计汇总
print("\n" + "=" * 60)
print(" 📊 任务执行结果统计报表")
print("=" * 60)
success_cnt = sum(1 for r in results if r["status"] in ["success", "already_done"])
summary_lines = []
total_points = 0
for idx, r in enumerate(results, 1):
status_icon = "✅" if r["status"] in ["success", "already_done"] else "❌"
pts = r.get("points_after", 0)
total_points += pts
cny_str = format_points(pts)
line = f"[{idx}] {status_icon} 账号: {r['user']} | 结果: {r['message']} | 当前账户: {cny_str}"
summary_lines.append(line)
print(line)
total_cny = total_points / POINTS_PER_CNY
total_summary = f"💰 全部账号总资产: {total_points} 积分 (可提现 ≈ {total_cny:.2f} 元)"
summary_lines.append(total_summary)
print(total_summary)
print("=" * 60)
# 推送青龙系统通知
notify_title = f"雨云签到: 成功 {success_cnt}/{len(results)} | 总资产 ≈ {total_cny:.2f}元"
notify_content = "\n".join(summary_lines)
send(notify_title, notify_content)
if __name__ == "__main__":
main()