# -*- 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()