diff --git a/YYB/YYB_BJY.py b/YYB/YYB_BJY.py new file mode 100644 index 0000000..4ef7c70 --- /dev/null +++ b/YYB/YYB_BJY.py @@ -0,0 +1,1387 @@ +# YYB-Go-Enhanced 适配说明:配置多行 YYB_SERVER=地址@账号标识 或 纯地址(自动拉取 /accounts);通知兼容青龙/呆呆面板 notify.py。 + +# ========================================================== +# 功能说明:code 换 token(含缓存与自动刷新) +# 机制:yyb_go 自动获取存活账号并换取微信 code → 换取 token → 缓存到本地 JSON; +# 下次运行先读取缓存 token,并调用用户信息接口验证是否仍有效; +# 有效则直接复用(无需再获取 code);失效或过期则重新获取 code 自动刷新。 +# ========================================================== +# name: 白鲸鱼旧衣服回收 +# cron: 5 9,20 * * * + +""" +白鲸鱼旧衣服回收动态 code 版 + +功能: + 1. yyb_go 自动获取存活账号并换取微信 code + 2. /api/app/weapp.php 使用 code 换 token + 3. 每日签到 + 4. 九宫格抽奖(任务卡动态领取) + 5. 福利任务自动处理(收藏小程序等) + 6. 成长值任务汇总 + 7. 查询积分与余额 + 8. 品赞代理,业务请求优先代理,失败直连兜底 + +环境变量: + YYB_SERVER YYB-Go-Enhanced 路由,每行:地址@账号标识 或 纯地址(自动请求 /accounts 拉取全部账号) + 账号直接来自 YYB_SERVER;无需配置 WX_ID + BJY_blacklist 黑名单账号(昵称或 openid),逗号/分号/换行分隔,命中的跳过 + PROXY_API 品赞代理提取 API,可选 + PROXY_TYPE http / socks5,默认 http + BJY_CHANNEL_ID 渠道 ID,默认 wx1008 + BJY_OAID 设备 oaid,默认使用抓包中的值 + BJY_DRAW_MODE task=任务卡动态抽奖,fixed=固定次数,默认 task + BJY_DRAW_TIMES 固定模式抽奖次数,默认 1 + +依赖: + pip install requests + socks5 代理需: + pip install requests[socks] +""" + +import hashlib +import json +import os +import random +import sys +import time +import traceback +from datetime import datetime +from typing import Any, Dict, List, Tuple +from urllib.parse import quote + +import requests + +# 将脚本所在目录加入搜索路径(确保能找到 yyb.py 等同目录模块) +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +# ===== YYB-Go-Enhanced + QingLong standalone adapter ===== +def _yyb_fetch_accounts(server: str) -> List[Dict[str, str]]: + """请求 GET /accounts 拉取全部账号,兼容 {code,msg,data} 包装与直接数组返回。""" + import requests as _requests + response = _requests.get(server.rstrip("/") + "/accounts", timeout=15) + response.raise_for_status() + body = response.json() + if isinstance(body, dict): + if int(body.get("code", -1)) != 0: + raise RuntimeError(f"/accounts 返回失败:{body.get('msg') or body}") + data = body.get("data") + else: + data = body + if not isinstance(data, list): + raise RuntimeError("/accounts 未返回账号数组") + accounts: List[Dict[str, str]] = [] + for item in data: + if not isinstance(item, dict): + continue + ref = str(item.get("openid") or item.get("id") or "").strip() + if not ref: + continue + nickname = str(item.get("nickname") or item.get("alias") or ref).strip() + accounts.append({"server": server, "ref": ref, "nickname": nickname}) + if not accounts: + raise RuntimeError(f"/accounts 未返回任何有效账号({server})") + return accounts + + +def _yyb_routes(): + routes = [] + for number, line in enumerate(os.getenv("YYB_SERVER", "").splitlines(), 1): + line = line.strip() + if not line: + continue + if "@" in line: + server, ref = line.rsplit("@", 1) + if not server.strip() or not ref.strip(): + raise RuntimeError(f"YYB_SERVER 第 {number} 行格式错误,应为 地址@账号标识") + server = server.strip().rstrip("/") + if not server.lower().startswith(("http://", "https://")): + server = "http://" + server + routes.append({"server": server, "ref": ref.strip(), "nickname": ref.strip()}) + else: + server = line.rstrip("/") + if not server.lower().startswith(("http://", "https://")): + server = "http://" + server + try: + routes.extend(_yyb_fetch_accounts(server)) + except Exception as exc: + raise RuntimeError(f"YYB_SERVER 第 {number} 行(纯地址)拉取账号失败:{exc}") + if not routes: + raise RuntimeError("未配置 YYB_SERVER(每行:地址@账号标识 或 纯地址)") + return routes + + +def _yyb_clean_ref(value): + text = str(value or "").split("#", 1)[0].strip() + for prefix in ("wx:", "yyb:", "wmpf:", "syzs:"): + if text.lower().startswith(prefix): + return text[len(prefix):].strip() + return text + + +def _yyb_route(identifier): + routes = _yyb_routes() + wanted = _yyb_clean_ref(identifier) + for route in routes: + if _yyb_clean_ref(route["ref"]) == wanted: + return route + if wanted.isdigit() and 0 < int(wanted) <= len(routes): + return routes[int(wanted) - 1] + if len(routes) == 1: + return routes[0] + raise RuntimeError(f"YYB_SERVER 中找不到账号标识:{wanted or '(空)'}") + + +def _yyb_call(endpoint, app_id, identifier): + import requests as _requests + route = _yyb_route(identifier) + response = _requests.post(route["server"] + endpoint, + json={"ref": route["ref"], "app_id": app_id}, timeout=30) + response.raise_for_status() + body = response.json() + if not isinstance(body, dict) or int(body.get("code", -1)) != 0: + raise RuntimeError(f"{endpoint} 返回失败:{body.get('msg') or body.get('message') or body}") + result = (body.get("data") or {}).get("result") + if result is None: + raise RuntimeError(f"{endpoint} 未返回 data.result") + return result + + +class _YybClient: + def get_online_accounts(self): + return [{"id": x["ref"], "openid": x["ref"], "wxid": x["ref"], + "remark": x.get("nickname") or x["ref"], "nickname": x.get("nickname") or x["ref"], + "status": "online", "login_type": "WX"} + for x in _yyb_routes()] + + def get_accounts(self, force_refresh=False): + return self.get_online_accounts() + + +class _YybCompat: + YYBClient = _YybClient + + @staticmethod + def load_accounts(*_args, **_kwargs): + return _YybClient().get_online_accounts() + + @staticmethod + def resolve_accounts(env_name=""): + return [x["ref"] for x in _yyb_routes()] + + @staticmethod + def get_global_server_url(): + return _yyb_routes()[0]["server"] + + @staticmethod + def get_single_code(app_id, identifier): + result = _yyb_call("/wxapp/getCode", app_id, identifier) + code = result if isinstance(result, str) else result.get("code") + if not code: + raise RuntimeError("/wxapp/getCode 未返回 data.result.code") + return str(code) + + @staticmethod + def get_single_phone_code(app_id, identifier, login_type=None): + result = _yyb_call("/wxapp/getPhoneNumber", app_id, identifier) + return str(result if isinstance(result, str) else result.get("code") or "") + + @staticmethod + def normalize_login_type(value): + return str(value or "WX").upper() + + @staticmethod + def login_type_label(value): + return {"WX": "应用宝", "WMPF": "微信小程序", "SYZS": "手游助手"}.get( + _YybCompat.normalize_login_type(value), "应用宝") + + @staticmethod + def _cache_path(name): + import pathlib + safe = "".join(ch if ch.isalnum() or ch in "-_" else "_" for ch in name) + return pathlib.Path(os.getenv("QL_DIR", "/ql/data")) / "config" / "yyb_token_caches" / f"{safe}.json" + + @classmethod + def _read_cache(cls, name): + try: + return json.loads(cls._cache_path(name).read_text(encoding="utf-8")) + except Exception: + return {} + + @classmethod + def get_cached_token(cls, name, identifier, options=None): + item = cls._read_cache(name).get(_yyb_clean_ref(identifier)) + if not item: + return None + max_age = int((options or {}).get("max_age_ms", 0)) + if max_age and int(time.time() * 1000) - int(item.get("_saved_at", 0)) > max_age: + return None + return item + + @classmethod + def save_cached_token(cls, name, identifier, value): + data = cls._read_cache(name) + data[_yyb_clean_ref(identifier)] = dict(value, _saved_at=int(time.time() * 1000)) + path = cls._cache_path(name) + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8") + try: + path.chmod(0o600) + except OSError: + pass + + @classmethod + def remove_cached_token(cls, name, identifier): + data = cls._read_cache(name) + data.pop(_yyb_clean_ref(identifier), None) + path = cls._cache_path(name) + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8") + + +yyb = _YybCompat() + + +def _send_qinglong_notify(title, content): + import sys as _sys + for path in (os.path.dirname(os.path.abspath(__file__)), "/ql/data/scripts", "/ql/scripts"): + if path not in _sys.path: + _sys.path.insert(0, path) + try: + from notify import send as _ql_send + _ql_send(title, content) + return True + except Exception as exc: + print(f"青龙通知失败(不影响任务结果):{exc}") + return False +# ===== adapter end ===== + +ql_send = _send_qinglong_notify + + +APP_NAME = "白鲸鱼旧衣服回收小程序" +APPID = "wxc525caf8e3a9e434" +APP_PLATFORM = "wx" + +# 账号列表:自动从 yyb_go 同步存活账号(与小米社区一致,无需单独配置服务与账号) +ACCOUNT_NAMES: Dict[str, str] = {} + + +def load_accounts() -> List[str]: + """从 yyb_go 拉取存活账号,返回账号标识列表(可用 WX_ID 指定白名单)""" + try: + accounts = yyb.load_accounts() + except Exception as exc: + print(f"❌ 拉取 yyb_go 账号失败:{exc}") + return [] + refs: List[str] = [] + for account in accounts or []: + ref = str(account.get("id") or account.get("openid") or account.get("wxid") or "").strip() + if not ref: + continue + ACCOUNT_NAMES[ref] = str(account.get("remark") or account.get("nickname") or account.get("alias") or ref) + refs.append(ref) + if refs: + print(f"✅ 已从 yyb_go 同步 {len(refs)} 个存活账号") + return refs + + +SERVERS = load_accounts() + + +CHANNEL_ID = os.getenv("BJY_CHANNEL_ID", "wx1008") +OAID = os.getenv("BJY_OAID", "260827105351e14208") +DRAW_MODE = os.getenv("BJY_DRAW_MODE", "task").lower() +DRAW_TIMES = int(os.getenv("BJY_DRAW_TIMES", "1")) +CLUB_GID = os.getenv("BJY_CLUB_GID", "304") +AUTO_GROWTH_TYPES = ("weixin",) +CLUB_POST_CONTENTS = [ + "今天把家里的旧衣服整理了一大袋,预约了白鲸鱼上门回收,环保又省心,给平台点赞!", + "换季整理出一堆旧衣服和旧书,交给白鲸鱼回收,还能攒成长值,一举两得。", + "旧衣回收又一次打卡,快递小哥准时上门,流程很顺畅,支持环保事业!", + "把闲置的衣物都回收了,白鲸鱼的处理方式很规范,推荐给大家。", + "断舍离成功!旧衣服旧书统统交给白鲸鱼,为地球减负一点点。", +] + +PROXY_API = os.getenv("PROXY_API", "") +PROXY_TYPE = os.getenv("PROXY_TYPE", "http").lower() + +PROXY_RETRY_TIMES = 3 +PROXY_VALIDATE_URL = "http://httpbin.org/ip" +PROXY_FETCH_INTERVAL = 3 +ENABLE_DIRECT_FALLBACK = True +REQUEST_TIMEOUT = 30 + +BASE_URL = "https://www.52bjy.com" +APPKEY = "1f70a57fdf4061a7" +SECRET = "eBRaFLkuJ5" +LOGIN_URL = f"{BASE_URL}/api/app/weapp.php" + +SIGN_INFO_URL = f"{BASE_URL}/api/app/user.php" +SIGN_URL = f"{BASE_URL}/api/app/user.php" +USERINFO_URL = f"{BASE_URL}/api/app/user.php" +DRAW_LIST_URL = f"{BASE_URL}/api/app/promotionjgg.php" +DRAW_RESULT_URL = f"{BASE_URL}/api/app/promotionjgg.php" +TASK_LIST_URL = f"{BASE_URL}/api/app/promotion.php" +GROWTH_URL = f"{BASE_URL}/api/app/membervip.php" + +COOKIE_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "bjycookie.json") + +USER_AGENT = ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " + "(KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36 " + "MicroMessenger/7.0.20.1781(0x6700143B) NetType/WIFI " + "MiniProgramEnv/Windows WindowsWechat/WMPF WindowsWechat(0x63090a13) " + "UnifiedPCWindowsWechat(0xf2541923) XWEB/25364" +) + + +def now_text() -> str: + return datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + +def sleep(seconds: float) -> None: + time.sleep(seconds) + + +def mask(value: Any) -> str: + value = str(value or "") + if len(value) <= 12: + return value + return f"{value[:6]}...{value[-6:]}" + + +def json_preview(data: Any, limit: int = 800) -> str: + try: + return json.dumps(data, ensure_ascii=False)[:limit] + except Exception: + return str(data)[:limit] + + +def to_float(value: Any) -> float: + try: + return float(value or 0) + except (TypeError, ValueError): + return 0.0 + + +def to_int(value: Any, default: int = 0) -> int: + try: + return int(float(value or default)) + except (TypeError, ValueError): + return default + + +def safe_data(resp: Dict[str, Any]) -> Dict[str, Any]: + """Safely extract 'data' from an API response, handling null/missing.""" + return resp.get("data") or {} + + +def log_title() -> None: + print() + print("╔" + "═" * 50 + "╗") + print("║ ♻️ 白鲸鱼旧衣服回收动态 code 版 ║") + print(f"║ 🕒 启动时间: {now_text():<32}║") + print(f"║ 🔢 账号数量: {len(SERVERS):<34}║") + print("╚" + "═" * 50 + "╝") + + +def log_account_header(index: int, total: int, server: str) -> None: + print() + print("┌" + "─" * 50 + "┐") + print(f"│ 🧩 账号 {index} / {total:<37}│") + print(f"│ 🌍 来源 {ACCOUNT_NAMES.get(server, server):<40}│") + print("└" + "─" * 50 + "┘") + + +def direct_session() -> requests.Session: + session = requests.Session() + session.trust_env = False + return session + + +def parse_proxy_response(text: Any) -> Dict[str, Any] | None: + if not isinstance(text, str): + text = json.dumps(text, ensure_ascii=False) + + text = text.strip() + if not text: + return None + + try: + data = json.loads(text) + proxy_obj = None + + if isinstance(data.get("data"), list) and data["data"]: + proxy_obj = data["data"][0] + elif isinstance(data.get("data"), dict): + proxy_obj = data["data"] + elif data.get("ip") and data.get("port"): + proxy_obj = data + elif isinstance(data.get("result"), dict): + proxy_obj = data["result"] + + if proxy_obj: + host = proxy_obj.get("ip") or proxy_obj.get("host") + port = proxy_obj.get("port") + if host and port: + return { + "host": str(host), + "port": int(port), + "username": proxy_obj.get("user") or proxy_obj.get("username") or "", + "password": proxy_obj.get("pass") or proxy_obj.get("password") or "", + } + except Exception: + pass + + if ":" in text: + parts = text.split(":") + if len(parts) >= 2: + return { + "host": parts[0], + "port": int(parts[1]), + "username": parts[2] if len(parts) > 2 else "", + "password": parts[3] if len(parts) > 3 else "", + } + + return None + + +def build_proxy_dict(proxy_info: Dict[str, Any] | None) -> Dict[str, str] | None: + if not proxy_info: + return None + + host = proxy_info["host"] + port = proxy_info["port"] + username = proxy_info.get("username", "") + password = proxy_info.get("password", "") + + auth = "" + if username and password: + auth = f"{quote(username)}:{quote(password)}@" + + scheme = "socks5" if PROXY_TYPE == "socks5" else "http" + proxy_url = f"{scheme}://{auth}{host}:{port}" + + print(f"🛠️ [代理] 生成 {scheme.upper()} 代理 {host}:{port}") + + return { + "http": proxy_url, + "https": proxy_url, + } + + +def validate_proxy(proxies: Dict[str, str] | None) -> Tuple[bool, str]: + if not proxies: + return False, "" + + try: + response = requests.get(PROXY_VALIDATE_URL, proxies=proxies, timeout=15) + if response.status_code == 200: + try: + ip = response.json().get("origin", "未知") + except Exception: + ip = "未知" + print(f"✅ [代理] 验证通过,出口 IP: {ip}") + return True, ip + except Exception as exc: + print(f"⚠️ [代理] 验证失败: {exc}") + + return False, "" + + +def get_valid_proxy(account_name: str) -> Tuple[Dict[str, str] | None, str]: + if not PROXY_API: + print(f"⚠️ [代理] {account_name} 未配置 PROXY_API,使用直连") + return None, "" + + print(f"🌐 [代理] {account_name} 正在获取品赞代理...") + + for index in range(1, PROXY_RETRY_TIMES + 1): + try: + response = direct_session().get(PROXY_API, timeout=15) + proxy_info = parse_proxy_response(response.text) + + if not proxy_info: + print(f"⚠️ [代理] 第 {index} 次代理解析失败") + continue + + print(f"✅ [代理] 提取到 {proxy_info['host']}:{proxy_info['port']}") + proxies = build_proxy_dict(proxy_info) + + ok, ip = validate_proxy(proxies) + if ok: + return proxies, ip + + print(f"⚠️ [代理] 第 {index} 次代理不可用") + except Exception as exc: + print(f"⚠️ [代理] 第 {index} 次获取代理异常: {exc}") + + if index < PROXY_RETRY_TIMES: + sleep(2) + + print("⚠️ [代理] 获取失败,使用直连") + return None, "" + + +def request_with_proxy( + method: str, + url: str, + *, + proxies: Dict[str, str] | None = None, + server: str = "", + **kwargs, +) -> requests.Response: + kwargs.setdefault("timeout", REQUEST_TIMEOUT) + + if proxies: + try: + return requests.request(method, url, proxies=proxies, **kwargs) + except Exception as exc: + print(f"⚠️ [代理] {server} 代理请求失败: {exc}") + if not ENABLE_DIRECT_FALLBACK: + raise + print("🔁 [兜底] 切换直连重试") + + session = direct_session() + return session.request(method, url, **kwargs) + + +def get_code(server: str) -> str | None: + """从 yyb_go 获取一次性微信 code(server 为账号标识)""" + try: + print(f"🔐 [授权] YYB 获取 code(账号 {ACCOUNT_NAMES.get(server, mask(server))})") + code = yyb.get_single_code(APPID, server) + if not code: + print("❌ [授权] code 获取失败") + return None + print("✅ [授权] code 获取成功") + return str(code) + except Exception as exc: + print(f"❌ [授权] code 获取异常: {exc}") + return None + + +def js_encode(value: str) -> str: + """Encode a value like JS encodeURIComponent with Destoon's extra escaping.""" + encoded = quote(str(value), safe="-_.~") + encoded = encoded.replace("!", "%21").replace("'", "%27") + encoded = encoded.replace("(", "%28").replace(")", "%29") + encoded = encoded.replace("*", "%2A") + return encoded + + +def bjy_query_value(value: Any) -> str: + text = str(value) + if any("\u4e00" <= ch <= "\u9fff" for ch in text): + return js_encode(text) + return text + + +def bjy_sign(params: Dict[str, Any]) -> str: + """小程序 sign 算法:参数按键名排序,拼接 key=value,再追加 secret 取 md5。""" + qs = "&".join(f"{key}={bjy_query_value(value)}" for key, value in sorted(params.items())) + return hashlib.md5((qs + SECRET).encode("utf-8")).hexdigest() + + +def build_api_url(php: str, params: Dict[str, Any]) -> str: + """构造 /api/app/{php}.php 的签名 URL。""" + data = dict(params) + data.setdefault("sign_type", "md5") + data.pop("php", None) + data.setdefault("appkey", APPKEY) + query = "&".join(f"{key}={bjy_query_value(value)}" for key, value in sorted(data.items())) + sign = bjy_sign(data) + return f"{BASE_URL}/api/app/{php}.php?{query}&sign={sign}" + +def bjy_sign_raw(params: Dict[str, Any]) -> str: + """POST 表单签名:原始值按键名排序拼接后追加 secret 取 md5(与小程序 axPost 一致)。""" + qs = "&".join(f"{key}={params[key]}" for key in sorted(params)) + return hashlib.md5((qs + SECRET).encode("utf-8")).hexdigest() + + +def common_headers(token: str | None = None) -> Dict[str, str]: + headers = { + "User-Agent": USER_AGENT, + "Accept": "*/*", + "Content-Type": "application/json", + "EnvConnection": "test", + "Referer": f"https://servicewechat.com/{APPID}/392/page-frame.html", + "Accept-Language": "zh-CN,zh;q=0.9", + } + if token: + headers["auth"] = token + return headers + + +def extract_auth(data: Any) -> Tuple[str | None, str | None]: + if not isinstance(data, dict): + return None, None + + inner = data.get("data") + if isinstance(inner, dict): + token = inner.get("jiufy_auth") or inner.get("token") or inner.get("accessToken") + username = inner.get("username") + if token and token != "null": + return str(token), str(username or "") + return None, None + + +def login_by_code(server: str, code: str, proxies: Dict[str, str] | None) -> Tuple[str | None, str | None, Dict[str, Any] | None]: + try: + print("🔐 [登录] 使用 code 换 token") + url = ( + f"{LOGIN_URL}?action=bind&app={APP_PLATFORM}&appkey={APPKEY}" + f"&channel={CHANNEL_ID}&inviter=&merchant_id=1&login_source=scan" + ) + response = request_with_proxy( + "POST", + url, + data={ + "encryptedData": "", + "iv": "", + "code": code, + }, + headers={ + "User-Agent": USER_AGENT, + "Content-Type": "application/x-www-form-urlencoded", + "EnvConnection": "test", + "Referer": f"https://servicewechat.com/{APPID}/392/page-frame.html", + }, + proxies=proxies, + server=server, + ) + response.encoding = "utf-8" + try: + data = response.json() + except Exception: + data = {"raw": response.text[:800]} + + token, username = extract_auth(data) + if token: + print(f"✅ [登录] token 获取成功: {mask(token)}") + return token, username, data + + print(f"❌ [登录] 未识别 token 字段: {json_preview(data)}") + return None, None, data + except Exception as exc: + print(f"❌ [登录] 请求异常: {exc}") + return None, None, None + + +def api_get(server: str, url: str, proxies: Dict[str, str] | None) -> Dict[str, Any]: + response = request_with_proxy( + "GET", + url, + headers=common_headers(), + proxies=proxies, + server=server, + ) + response.encoding = "utf-8" + try: + return response.json() + except Exception: + return { + "code": -1, + "msg": f"JSON解析失败: {response.text[:300]}", + } + +def api_post(server: str, php: str, params: Dict[str, Any], proxies: Dict[str, str] | None) -> Dict[str, Any]: + """POST 表单请求(小程序 axPost 风格):参数放 body,sign 用原始值计算。""" + data = dict(params) + data.pop("php", None) + data.pop("sign_type", None) + data.setdefault("appkey", APPKEY) + data["sign"] = bjy_sign_raw(data) + response = request_with_proxy( + "POST", + f"{BASE_URL}/api/app/{php}.php", + data=data, + headers={**common_headers(), "Content-Type": "application/x-www-form-urlencoded"}, + proxies=proxies, + server=server, + ) + response.encoding = "utf-8" + try: + return response.json() + except Exception: + return { + "code": -1, + "msg": f"JSON解析失败: {response.text[:300]}", + } + + +# ====================== Token缓存管理 ====================== +def load_token_cache() -> Dict[str, Any]: + try: + if os.path.exists(COOKIE_FILE): + with open(COOKIE_FILE, "r", encoding="utf-8") as f: + return json.load(f) + except Exception as exc: + print(f"⚠️ [缓存] 读取失败: {exc}") + return {} + + +def save_token_cache(cache: Dict[str, Any]) -> None: + try: + with open(COOKIE_FILE, "w", encoding="utf-8") as f: + json.dump(cache, f, ensure_ascii=False, indent=2) + print("✅ [缓存] Token保存成功") + except Exception as exc: + print(f"❌ [缓存] 保存失败: {exc}") + + +def get_cached_token(server: str) -> Tuple[str | None, str | None]: + cache = load_token_cache() + data = cache.get(server) + if data and data.get("token") and data.get("expireTime"): + try: + expire = datetime.fromisoformat(data["expireTime"]).timestamp() * 1000 + if time.time() * 1000 < expire - 3600 * 1000: + print(f"✅ [缓存] 使用 {server} token") + return data["token"], data.get("username") or "" + except Exception as exc: + print(f"⚠️ [缓存] 过期时间解析异常: {exc}") + return None, None + + +def set_cached_token(server: str, token: str, expire_time: str, username: str) -> None: + cache = load_token_cache() + cache[server] = { + "token": token, + "username": username, + "expireTime": expire_time, + "updateTime": datetime.now().isoformat(), + } + save_token_cache(cache) + + +def login_with_cache(server: str, proxies: Dict[str, str] | None) -> Tuple[str | None, str | None, Dict[str, Any] | None]: + """优先使用缓存 token(用户信息接口验证),失效自动 code 刷新""" + cache_token, cache_username = get_cached_token(server) + if cache_token and cache_username: + print("🔍 [缓存] 验证 token") + try: + url = build_api_url( + "user", + { + "action": "userinfo", + "auth": cache_token, + "username": cache_username, + "merchant_id": 1, + }, + ) + account_resp = api_get(server, url, proxies) + if account_resp.get("isSucess") or account_resp.get("is_success"): + print("✅ [缓存] token 有效") + return cache_token, cache_username, None + except Exception as exc: + print(f"⚠️ [缓存] 验证异常: {exc}") + print("⚠️ [缓存] token 已失效,重新登录") + + code = get_code(server) + if not code: + return None, None, None + + token, username, raw_login = login_by_code(server, code, proxies) + if not token: + return None, None, raw_login + + expire_time = datetime.fromtimestamp(time.time() + 7 * 24 * 3600).isoformat() + set_cached_token(server, token, expire_time, username) + return token, username, raw_login + + +# ====================== 任务辅助 ====================== +def fetch_task_list(server: str, task_type: str, username: str, proxies) -> List[Dict[str, Any]]: + resp = api_get( + server, + build_api_url( + "promotion", + { + "action": "tasklist", + "app": APP_PLATFORM, + "merchant_id": 1, + "type": task_type, + "username": username, + }, + ), + proxies, + ) + data = resp.get("data") or [] + return [item for item in data if isinstance(item, dict)] + + +def summarize_tasks(tasks: List[Dict[str, Any]]) -> str: + done = sum(1 for item in tasks if to_int(item.get("is_done")) == 1) + pending = [str(item.get("title") or item.get("type")) for item in tasks if to_int(item.get("is_done")) != 1] + text = f"{done}/{len(tasks)} 已完成" + if pending: + text += ",待完成:" + "、".join(pending[:4]) + ("..." if len(pending) > 4 else "") + return text + + +def direct_task_action(server: str, task: Dict[str, Any], token: str, username: str, proxies) -> Tuple[Dict[str, Any] | None, str]: + task_type = task.get("type") or "" + if task_type == "fav": + url = build_api_url("weapp", {"action": "addmyapp", "username": username}) + result = api_get(server, url, proxies) + return result, result.get("message") or "收藏请求已提交" + if task_type in ("sharefriends", "browse", "myminiprogram"): + url = build_api_url( + "user", + { + "action": "task", + "app": APP_PLATFORM, + "auth": token, + "type": task_type, + "username": username, + }, + ) + result = api_get(server, url, proxies) + return result, result.get("message") or f"{task.get('title') or task_type} 上报成功" + return None, "需要真实客户端/业务动作" + + +def run_welfare_tasks(server: str, token: str, username: str, proxies) -> str: + tasks = fetch_task_list(server, "welfare", username, proxies) + action_messages: List[str] = [] + for task in tasks: + if to_int(task.get("is_done")) == 1: + continue + action_resp, action_msg = direct_task_action(server, task, token, username, proxies) + ok = bool(action_resp is None or action_resp.get("isSucess") or action_resp.get("is_success")) + name = task.get("title") or task.get("type") or "未命名任务" + if action_resp is not None and not ok: + action_msg = action_resp.get("message") or action_msg + action_messages.append(f"{name}: {'成功' if ok else '失败'} {action_msg}") + sleep(random.randint(1, 2)) + + refreshed = fetch_task_list(server, "welfare", username, proxies) + profile = safe_data(api_get( + server, + build_api_url( + "user", + { + "action": "userinfo", + "auth": token, + "username": username, + }, + ), + proxies, + )) + fav_flag = "" + if isinstance(profile, dict) and profile.get("weapp_fav") is not None: + fav_flag = f",资料 weapp_fav={profile.get('weapp_fav', '?')}" + result = summarize_tasks(refreshed) + if action_messages: + result += ";已处理:" + ";".join(action_messages[:5]) + result += fav_flag + return result + + +def run_growth_summary(server: str, username: str, proxies) -> str: + resp = api_get( + server, + build_api_url( + "membervip", + { + "action": "growth", + "app": APP_PLATFORM, + "merchant_id": 1, + "username": username, + }, + ), + proxies, + ) + if not (resp.get("isSucess") or resp.get("is_success")): + return resp.get("message") or "成长值查询失败" + + data = safe_data(resp) + growth_task = data.get("growth_task") or {} + daily_tasks = list(growth_task.get("day") or []) + list(growth_task.get("new") or []) + daily_done = sum(1 for item in daily_tasks if to_int(item.get("is_done")) == 1) + pending = [str(item.get("title")) for item in daily_tasks if to_int(item.get("is_done")) != 1] + base_task = data.get("base_task") or {} + brand_orders = (base_task.get("brandorder") or {}).get("list") or [] + brand_done = sum(1 for item in brand_orders if to_int(item.get("is_done")) == 1) + info = base_task.get("information") or {} + + parts = [ + f"累计 {data.get('growths', 0)}(今日 +{data.get('growth', 0)})", + f"每日任务 {daily_done}/{len(daily_tasks)}", + f"基础资料 {'已完成' if to_int(info.get('is_done')) == 1 else '未完成'}", + f"回收成长任务 {brand_done}/{len(brand_orders)}", + ] + if pending: + parts.append("待完成:" + "、".join(pending[:5]) + ("..." if len(pending) > 5 else "")) + return ";".join(parts) + +def fetch_club_topics(server: str, username: str, proxies) -> List[Dict[str, Any]]: + resp = api_get(server, build_api_url("club", {"action": "list", "username": username}), proxies) + data = resp.get("data") or [] + return [item for item in data if isinstance(item, dict) and str(item.get("catid")) != "507"] + +def count_today_posts(server: str, token: str, username: str, proxies) -> int: + resp = api_get( + server, + build_api_url( + "club", + {"action": "mypublish", "auth": token, "username": username}, + ), + proxies, + ) + today = datetime.now().strftime("%Y-%m-%d") + posts = resp.get("data") or [] + return sum(1 for item in posts if isinstance(item, dict) and str(item.get("addtime", "")).startswith(today)) + +def run_club_post(server: str, token: str, username: str, proxies, limit: int = 3) -> str: + ban = api_get(server, build_api_url("club", {"action": "ban_status", "username": username}), proxies) + if to_int(safe_data(ban).get("status")) == 1: + return f"账号被禁言:{safe_data(ban).get('tip') or '无法发帖'}" + + posted_today = count_today_posts(server, token, username, proxies) + if posted_today >= limit: + return f"今日已发帖 {posted_today}/{limit} 篇,达到上限" + + topics = fetch_club_topics(server, username, proxies) + gid = CLUB_GID + title = "" + for item in topics: + if str(item.get("gid")) == str(CLUB_GID): + title = str(item.get("title") or "") + break + if not title and topics: + gid = str(topics[0].get("gid")) + title = str(topics[0].get("title") or "") + if not title: + return "未找到可发布的话题" + + content = CLUB_POST_CONTENTS[(datetime.now().toordinal() + posted_today) % len(CLUB_POST_CONTENTS)] + sec_response = request_with_proxy( + "POST", + f"{BASE_URL}/api/app/weapp.php?action=msgseccheck", + data={"content": content}, + headers={**common_headers(), "Content-Type": "application/x-www-form-urlencoded"}, + proxies=proxies, + server=server, + ) + try: + sec_data = sec_response.json() + except Exception: + sec_data = {} + errcode = (sec_data.get("data") or {}).get("errcode") + if str(errcode) != "0": + return f"内容安全检测未通过({errcode}),跳过发帖" + + add_resp = api_post( + server, + "club", + { + "action": "add", + "auth": token, + "content": content, + "gid": gid, + "thumb": "", + "title": title, + "username": username, + "version": "2", + }, + proxies, + ) + if add_resp.get("isSucess") or add_resp.get("is_success"): + return f"发帖成功(话题:{title}),成长值待审核后发放" + return f"发帖失败:{add_resp.get('message') or '未知错误'}" + +def run_growth_tasks(server: str, token: str, username: str, proxies) -> str: + messages: List[str] = [] + resp = api_get( + server, + build_api_url( + "membervip", + {"action": "growth", "app": APP_PLATFORM, "merchant_id": 1, "username": username}, + ), + proxies, + ) + if resp.get("isSucess") or resp.get("is_success"): + growth_task = safe_data(resp).get("growth_task") or {} + for task in list(growth_task.get("day") or []) + list(growth_task.get("new") or []): + if to_int(task.get("is_done")) == 1: + continue + task_type = task.get("type") or "" + name = task.get("title") or task_type + if task_type in AUTO_GROWTH_TYPES: + report = api_get( + server, + build_api_url( + "user", + {"action": "task", "app": APP_PLATFORM, "auth": token, "type": task_type, "username": username}, + ), + proxies, + ) + ok = bool(report.get("isSucess") or report.get("is_success")) + messages.append(f"{name}: {'上报成功' if ok else '上报失败 ' + str(report.get('message') or '')}") + sleep(random.randint(1, 2)) + elif task_type == "club": + limit = to_int(task.get("period_num"), 3) or 3 + if to_int(task.get("counter")) < limit: + messages.append("发帖: " + run_club_post(server, token, username, proxies, limit=limit)) + sleep(random.randint(1, 2)) + + summary = run_growth_summary(server, username, proxies) + if messages: + summary += ";已处理:" + ";".join(messages[:6]) + return summary + + +def get_draw_cards(server: str, username: str, proxies) -> int: + resp = api_get( + server, + build_api_url( + "promotionjgg", + { + "action": "count", + "channel": "promotion_jgg", + "merchant_id": 1, + "username": username, + }, + ), + proxies, + ) + return max(0, to_int(safe_data(resp).get("new_count"))) + + +def run_jgg_tasks(server: str, token: str, username: str, proxies) -> Tuple[str, List[str]]: + tasks = fetch_task_list(server, "jgg", username, proxies) + completed: List[str] = [] + skipped: List[str] = [] + for task in tasks: + if to_int(task.get("is_done")) == 1: + continue + task_type = task.get("type") or "" + name = task.get("title") or task_type + if task_type == "qiandao": + skipped.append(f"{name} 由签到流程处理") + continue + if task_type in ("sharefriends", "browse", "myminiprogram"): + action_resp, action_msg = direct_task_action(server, task, token, username, proxies) + ok = bool(action_resp is None or action_resp.get("isSucess") or action_resp.get("is_success")) + completed.append(f"{name}: {'成功' if ok else '失败'} {action_msg}") + sleep(random.randint(1, 2)) + else: + skipped.append(f"{name} 需真实预约/捐赠/回复") + text = ";".join(completed) if completed else "无可自动上报任务" + if skipped: + text += ";待真实动作:" + "、".join(skipped[:6]) + return text, completed + + +# ====================== 账号流程 ====================== +def run_account(index: int, total: int, server: str) -> Dict[str, Any]: + result = { + "server": server, + "success": False, + "proxyStatus": "未使用代理", + "proxyIp": "-", + "token": "-", + "username": "-", + "signMsg": "-", + "welfareMsg": "-", + "growthMsg": "-", + "drawTaskMsg": "-", + "lotteryMsg": "-", + "balance": "-", + "error": "", + } + + log_account_header(index, total, server) + + proxies, proxy_ip = get_valid_proxy(server) + result["proxyStatus"] = "使用专属代理" if proxies else "使用直连" + result["proxyIp"] = proxy_ip or "-" + + sleep(PROXY_FETCH_INTERVAL) + + delay = random.randint(2, 6) + print(f"⏳ [延迟] 启动延迟 {delay}s") + sleep(delay) + + token, username, raw_login = login_with_cache(server, proxies) + if not token: + result["error"] = f"登录失败: {json_preview(raw_login)}" + return result + + result["token"] = mask(token) + result["username"] = username + + try: + # ====================== 每日签到 ====================== + sign_info = api_get( + server, + build_api_url( + "user", + { + "action": "getsigninfo", + "app": APP_PLATFORM, + "auth": token, + "username": username, + }, + ), + proxies, + ) + if not (sign_info.get("isSucess") or sign_info.get("is_success")): + result["signMsg"] = sign_info.get("message") or "签到状态查询失败" + print(f"⚠️ [签到] {result['signMsg']}") + else: + sign_data = safe_data(sign_info) + has_sign = str(sign_data.get("hassign", "0")) + days = to_int(sign_data.get("thisturn")) + if has_sign == "1": + result["signMsg"] = f"今日已签到,连续 {days} 天" + print(f"✅ [签到] {result['signMsg']}") + else: + sign_resp = api_get( + server, + build_api_url( + "user", + { + "action": "qiandao", + "app": APP_PLATFORM, + "auth": token, + "username": username, + }, + ), + proxies, + ) + if sign_resp.get("isSucess") or sign_resp.get("is_success"): + result["signMsg"] = f"每日签到成功,连续 {days + 1} 天" + print(f"✅ [签到] {result['signMsg']}") + else: + result["signMsg"] = sign_resp.get("message") or "签到失败" + print(f"⚠️ [签到] {result['signMsg']}") + + # ====================== 福利任务 ====================== + result["welfareMsg"] = run_welfare_tasks(server, token, username, proxies) + print(f"🧧 [福利] {result['welfareMsg']}") + + # ====================== 成长值任务 ====================== + result["growthMsg"] = run_growth_tasks(server, token, username, proxies) + print(f"🌱 [成长] {result['growthMsg']}") + + # ====================== 九宫格任务卡与抽奖 ====================== + jgg_task_msg, _ = run_jgg_tasks(server, token, username, proxies) + result["drawTaskMsg"] = jgg_task_msg + print(f"🎟️ [抽奖任务] {jgg_task_msg}") + + cards = get_draw_cards(server, username, proxies) + if DRAW_MODE == "fixed": + cards = max(cards, DRAW_TIMES) + print(f"🎰 [抽奖] 当前任务卡 {cards} 张") + + draw_config = api_get( + server, + build_api_url( + "promotionjgg", + { + "action": "list", + "app": APP_PLATFORM, + "merchant_id": "1", + "username": username, + }, + ), + proxies, + ) + prize_count = len(safe_data(draw_config).get("items") or []) if (draw_config.get("isSucess") or draw_config.get("is_success")) else 0 + + prizes: List[str] = [] + success_draws = 0 + for draw_index in range(1, cards + 1): + wait_time = random.randint(2, 5) + print(f"⏳ [抽奖] 第 {draw_index} 次抽奖前等待 {wait_time}s") + sleep(wait_time) + + draw_resp = api_get( + server, + build_api_url( + "promotionjgg", + { + "action": "prize_draw", + "app": APP_PLATFORM, + "merchant_id": 1, + "username": username, + }, + ), + proxies, + ) + if not (draw_resp.get("isSucess") or draw_resp.get("is_success")): + msg = draw_resp.get("message") or "抽奖失败" + prizes.append(f"第{draw_index}次: {msg}") + print(f"❌ [抽奖] {msg}") + if "用完" in str(msg): + break + continue + + prize_data = draw_resp.get("data") or {} + prize_name = prize_data.get("title") or prize_data.get("goodName") or "未知奖品" + prizes.append(prize_name) + success_draws += 1 + print(f"✅ [抽奖] 第 {draw_index} 次获得: {prize_name}") + + cards_after = get_draw_cards(server, username, proxies) + result["lotteryMsg"] = ( + f"奖品格 {prize_count} 个;任务卡后剩余 {cards_after} 张;" + f"成功 {success_draws} 次;" + ("、".join(prizes) if prizes else "无奖品") + ) + + # ====================== 余额查询 ====================== + user_resp = api_get( + server, + build_api_url( + "user", + { + "action": "userinfo", + "auth": token, + "username": username, + "merchant_id": 1, + }, + ), + proxies, + ) + if not (user_resp.get("isSucess") or user_resp.get("is_success")): + result["balance"] = user_resp.get("message") or "余额获取失败" + print(f"⚠️ [余额] {result['balance']}") + else: + user_data = safe_data(user_resp) + credit = user_data.get("credit", 0) + money = user_data.get("money", "0.00") + result["balance"] = f"鲸币 {credit} / 现金 {money} 元" + print(f"💰 [余额] {result['balance']}") + + result["success"] = True + return result + + except Exception as exc: + result["error"] = traceback.format_exc().strip() + print(f"❌ [账号] 执行失败: {exc}") + return result + + +def build_notify(results: List[Dict[str, Any]]) -> str: + success_count = sum(1 for item in results if item["success"]) + fail_count = len(results) - success_count + + content = f"""♻️ 白鲸鱼旧衣服回收任务结果 + +━━━━━━━━━━━━━━━━━━━━ +🏁 总结:{success_count} 成功 / {fail_count} 失败 +🕒 时间:{now_text()} +━━━━━━━━━━━━━━━━━━━━ +""" + + for idx, res in enumerate(results, 1): + icon = "✅" if res["success"] else "❌" + + content += f""" +🧩 账号 {idx} +🌍 来源:{res["server"]} +🌐 代理:{res["proxyStatus"]} +📡 出口IP:{res["proxyIp"]} +🔐 Token:{res["token"]} +👤 用户:{res["username"]} +📝 签到:{res["signMsg"]} +🧧 福利:{res["welfareMsg"]} +🌱 成长:{res["growthMsg"]} +🎟️ 抽奖任务:{res["drawTaskMsg"]} +🎰 抽奖:{res["lotteryMsg"]} +💰 余额:{res["balance"]} +{icon} 结果:{"成功" if res["success"] else "失败"} +""" + + if not res["success"]: + content += f"❌ 原因:{res['error']}\n" + + content += "━━━━━━━━━━━━━━━━━━━━\n" + + return content + + +def parse_blacklist(value: str) -> set: + """解析 BJY_blacklist:支持逗号/分号/换行分隔,自动去空白,空值返回空集合。""" + items = set() + for part in (value or "").replace(",", "\n").replace(";", "\n").splitlines(): + part = part.strip() + if part: + items.add(part) + return items + + +def main() -> None: + log_title() + + if not SERVERS: + print("❌ 未从 yyb_go 获取到存活账号,请确认 YYB_SERVER 已配置且服务内有在线账号") + ql_send( + "♻️ 白鲸鱼旧衣服回收任务失败", + "未从 yyb_go 获取到存活账号,请确认 YYB_SERVER 已配置且服务内有在线账号", + ) + return + + blacklist = parse_blacklist(os.getenv("BJY_blacklist", "")) + if blacklist: + print(f"⏭️ [黑名单] BJY_blacklist 共 {len(blacklist)} 项,命中账号将跳过") + + results: List[Dict[str, Any]] = [] + active_index = 0 + prev_ran = False + + for server in SERVERS: + display_name = ACCOUNT_NAMES.get(server, server) + if server in blacklist or display_name in blacklist: + print(f"⏭️ 命中 BJY_blacklist,跳过:{display_name}") + continue + + if prev_ran: + delay = random.randint(5, 20) + print(f"⏳ [间隔] 等待 {delay}s 后处理下一个账号") + sleep(delay) + + active_index += 1 + try: + result = run_account(active_index, len(SERVERS), server) + results.append(result) + except Exception as exc: + print(f"❌ [主程序] {server} 执行异常: {exc}") + results.append({ + "server": server, + "success": False, + "proxyStatus": "-", + "proxyIp": "-", + "token": "-", + "username": "-", + "signMsg": "-", + "welfareMsg": "-", + "growthMsg": "-", + "drawTaskMsg": "-", + "lotteryMsg": "-", + "balance": "-", + "error": traceback.format_exc().strip(), + }) + + prev_ran = True + + success_count = sum(1 for item in results if item["success"]) + fail_count = len(results) - success_count + + print() + print("╔" + "═" * 50 + "╗") + print("║ 🏁 白鲸鱼旧衣服回收任务执行完成 ║") + print(f"║ ✅ 成功: {success_count:<39}║") + print(f"║ ❌ 失败: {fail_count:<39}║") + print(f"║ 🕒 结束时间: {now_text():<32}║") + print("╚" + "═" * 50 + "╝") + + ql_send("♻️ 白鲸鱼旧衣服回收任务完成", build_notify(results)) + + +if __name__ == "__main__": + main()