Files
2026-10-07 20:45:48 +08:00

1429 lines
50 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 re
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_task_list(tasks: List[Dict[str, Any]]) -> str:
"""任务列表摘要:X/N 已完成 + 待完成标题(供 run_welfare_tasks 使用,勿与通知版 summarize_tasks 混淆)。"""
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_task_list(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 _compact_balance(balance: str) -> str:
"""精简余额:现金为 0.00 元时省略,只保留鲸币。"""
if not balance or balance == "-":
return balance
if "现金 0.00 元" in balance:
return balance.split(" / 现金 0.00 元")[0]
return balance
def summarize_tasks(res: Dict[str, Any]) -> Tuple[List[str], List[str]]:
"""从各任务消息中提取「已跑/未跑」简要标签(通知用,详情留在运行日志)。"""
done: List[str] = []
pending: List[str] = []
sign = res.get("signMsg") or ""
if "已签到" in sign or "签到成功" in sign:
done.append("签到")
welfare = res.get("welfareMsg") or ""
if "已处理" in welfare and "成功" in welfare:
done.append("收藏")
growth = res.get("growthMsg") or ""
if "发帖" in growth and "成功" in growth:
done.append("发帖")
if "完成旧衣回收" in growth or "完成回收旧书" in growth:
pending.append("回收")
if "邀请新用户" in growth or "购买商品" in growth:
pending.append("邀请/购买")
draw_task = res.get("drawTaskMsg") or ""
if "分享好友" in draw_task and "成功" in draw_task:
done.append("分享好友")
if "待真实动作" in draw_task:
pending.append("预约/捐赠/回复")
lottery = res.get("lotteryMsg") or ""
m = re.search(r"成功 (\d+) 次", lottery)
draws = int(m.group(1)) if m else 0
if draws > 0:
done.append(f"抽奖×{draws}")
else:
pending.append("抽奖(需真实动作)")
return done, pending
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"""♻️ 白鲸鱼旧衣回收 · {now_text()}
✅ {success_count} 成功 / {fail_count} 失败
"""
for res in results:
name = ACCOUNT_NAMES.get(res.get("server") or "", res.get("server") or "-")
if res["success"]:
content += f"""
▸ {name}
✅ {_compact_balance(res.get("balance") or "-")}
"""
done, pending = summarize_tasks(res)
if done:
content += f"已跑: {'、'.join(done)}\n"
if pending:
content += f"未跑: {'、'.join(pending)}\n"
else:
content += f"""
▸ {name}
❌ 失败
原因: {(res.get('error') or '未知错误').strip()}
"""
content += "\n"
return content.strip()
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()