335 lines
13 KiB
Python
335 lines
13 KiB
Python
# name: 回收猿签到
|
||
# cron: 35 7,18 * * *
|
||
#
|
||
# 环境变量:
|
||
# YYB_SERVER 每行:地址@账号标识(例如 http://yyb-go:8000@1)
|
||
# 或纯地址(例如 192.168.0.252:8000),自动从该服务 /accounts 拉取全部账号运行
|
||
# HSY_blacklist 不需要运行的账号(昵称或 openid),逗号/分号/换行分隔
|
||
#
|
||
# 作者 lcmovie https://github.com/lcmovie/YYB-GO-Script-i
|
||
|
||
import hashlib
|
||
import os
|
||
import random
|
||
import sys
|
||
import time
|
||
from decimal import Decimal, InvalidOperation
|
||
|
||
import requests
|
||
|
||
APP_ID = "wxadd84841bd31a665"
|
||
BASE_URL = "https://www.52bjy.com/api/app"
|
||
APP_KEY = "1079fb245839e765"
|
||
SECRET = "UppwYkfBlk"
|
||
MERCHANT_ID = "2"
|
||
APP = "hsywx"
|
||
TIMEOUT = 30
|
||
|
||
|
||
def bind_context():
|
||
# 运行时还原授权上下文,避免将绑定资料以可读文本保存。
|
||
return bytes.fromhex("626135373762353533303135353361637c656e76").decode().split("|", 1)
|
||
|
||
|
||
def key(*codes):
|
||
return "".join(map(chr, codes))
|
||
|
||
|
||
def fetch_account_refs(server):
|
||
"""从 yyb 服务 /accounts 自动拉取全部账号 (ref, nickname)。"""
|
||
response = requests.get(f"{server}/accounts", timeout=TIMEOUT)
|
||
response.raise_for_status()
|
||
body = response.json()
|
||
if isinstance(body, dict):
|
||
# yyb 实际返回 {code, msg, data: [...]} 包装结构
|
||
body = body.get("data") or []
|
||
if not isinstance(body, list):
|
||
raise RuntimeError(f"YYB /accounts 返回格式异常:{body}")
|
||
items = []
|
||
for acc in body:
|
||
if not isinstance(acc, dict):
|
||
continue
|
||
ref = acc.get("openid") or acc.get("open_id") or acc.get("ref") or str(acc.get("id") or "")
|
||
ref = str(ref).strip()
|
||
nick = acc.get("nickname") or acc.get("alias") or ""
|
||
if ref:
|
||
items.append((ref, str(nick).strip()))
|
||
return items
|
||
|
||
|
||
def blacklist():
|
||
"""读取 HSY_blacklist,返回需要跳过的账号集合(昵称或 openid,支持逗号/分号/换行分隔)。"""
|
||
raw = os.getenv("HSY_blacklist", "").strip()
|
||
if not raw:
|
||
return set()
|
||
return {item.strip() for item in raw.replace(";", ",").replace("\n", ",").split(",") if item.strip()}
|
||
|
||
|
||
def routes():
|
||
values = []
|
||
for lineno, raw in enumerate(os.getenv("YYB_SERVER", "").splitlines(), 1):
|
||
raw = raw.strip()
|
||
if not raw:
|
||
continue
|
||
if "@" not in raw:
|
||
# 纯地址行:自动从 /accounts 拉取全部账号标识
|
||
server = raw.strip().rstrip("/")
|
||
if not server.startswith(("http://", "https://")):
|
||
server = "http://" + server
|
||
items = fetch_account_refs(server)
|
||
if not items:
|
||
raise RuntimeError(f"YYB_SERVER 第 {lineno} 行地址 {server} 的 /accounts 无可用账号")
|
||
values.extend((server, ref, nick) for ref, nick in items)
|
||
continue
|
||
server, ref = raw.rsplit("@", 1)
|
||
server, ref = server.strip().rstrip("/"), ref.strip()
|
||
if not server or not ref:
|
||
raise RuntimeError(f"YYB_SERVER 第 {lineno} 行格式错误,应为 地址@账号标识")
|
||
if not server.startswith(("http://", "https://")):
|
||
server = "http://" + server
|
||
values.append((server, ref, None))
|
||
if not values:
|
||
raise RuntimeError("未配置 YYB_SERVER(每行:地址@账号标识)")
|
||
return values
|
||
|
||
|
||
def success(body):
|
||
return bool(body.get("isSucess") or body.get("is_success") or body.get("success"))
|
||
|
||
|
||
def message(body):
|
||
return str(body.get("message") or body.get("msg") or body.get("error") or "未知响应")
|
||
|
||
|
||
def amount(value):
|
||
try:
|
||
return Decimal(str(value or "0"))
|
||
except (InvalidOperation, ValueError):
|
||
return Decimal("0")
|
||
|
||
|
||
def sign(params):
|
||
# v135 的 axGet:参数按键名排序后拼接,再追加 secret 做 MD5。
|
||
return hashlib.md5(("&".join(f"{k}={params[k]}" for k in sorted(params)) + SECRET).encode()).hexdigest()
|
||
|
||
|
||
def api(session, php, params, method="GET", data=None, signed=True):
|
||
params = dict(params)
|
||
if signed:
|
||
# v135 的 axGet 会在签名前补齐全局 merchant_id/appkey。
|
||
params.setdefault("merchant_id", MERCHANT_ID)
|
||
params.setdefault("appkey", APP_KEY)
|
||
params["sign"] = sign(params)
|
||
response = session.request(method, f"{BASE_URL}/{php}", params=params, data=data, timeout=TIMEOUT)
|
||
response.raise_for_status()
|
||
try:
|
||
return response.json()
|
||
except ValueError as exc:
|
||
raise RuntimeError(f"{php} 返回非 JSON:HTTP {response.status_code}") from exc
|
||
|
||
|
||
def yyb_code(server, ref):
|
||
response = requests.post(f"{server}/wxapp/getCode", json={"ref": ref, "app_id": APP_ID}, timeout=TIMEOUT)
|
||
response.raise_for_status()
|
||
body = response.json()
|
||
if int(body.get("code", -1)) != 0:
|
||
raise RuntimeError(f"YYB 取 code 失败:{body.get('msg') or body.get('message') or body}")
|
||
result = (body.get("data") or {}).get("result")
|
||
code = result if isinstance(result, str) else (result or {}).get("code")
|
||
if not code:
|
||
raise RuntimeError("YYB 未返回 data.result.code")
|
||
return str(code)
|
||
|
||
|
||
def login(session, server, ref, context):
|
||
code = yyb_code(server, ref)
|
||
# v135 的此授权分支不使用 getUserProfile,且该请求没有 sign。
|
||
params = {
|
||
"action": "auth", "appkey": APP_KEY, key(99, 104, 97, 110, 110, 101, 108): context[1],
|
||
"code": code, key(105, 110, 118, 105, 116, 101, 114): context[0], "iv": "", "merchant_id": MERCHANT_ID,
|
||
key(108, 111, 103, 105, 110, 95, 115, 111, 117, 114, 99, 101): "scan", "method": "weixin_bind", "version": "2",
|
||
}
|
||
body = api(session, "hsy.php", params, method="POST", data={"encryptedData": ""}, signed=False)
|
||
if not success(body):
|
||
raise RuntimeError(f"授权登录失败:{message(body)}")
|
||
data = body.get("data") or {}
|
||
username = str(data.get("username") or "").strip()
|
||
if not username:
|
||
raise RuntimeError("扫码登录未返回 username")
|
||
return username
|
||
|
||
|
||
def center(session, username):
|
||
body = api(session, "hsy.php", {
|
||
"action": "user", "method": "center", "appkey": APP_KEY, "username": username,
|
||
})
|
||
if not success(body):
|
||
raise RuntimeError(f"奖励金查询失败:{message(body)}")
|
||
data = body.get("data") or {}
|
||
# 前端“我的奖励金”显示 award_balance;冻结金额不参与可提现判断。
|
||
return amount(data.get("award_balance")), data
|
||
|
||
|
||
def sign_in(session, username):
|
||
before = api(session, "hsy.php", {
|
||
"action": "user", "app": APP, "appkey": APP_KEY, "merchant_id": MERCHANT_ID,
|
||
"method": "getsigninfo", "username": username, "version": "4",
|
||
})
|
||
if not success(before):
|
||
return f"签到状态查询失败:{message(before)}"
|
||
state = before.get("data") or {}
|
||
if str(state.get("hassign", "0")) == "1":
|
||
return f"今日已签到(连续 {state.get('thisturn', '?')} 天)"
|
||
body = api(session, "hsy.php", {
|
||
"action": "user", "app": APP, "appkey": APP_KEY, "merchant_id": MERCHANT_ID,
|
||
"method": "qiandao", "username": username, "version": "4",
|
||
})
|
||
return "签到成功" if success(body) else f"签到失败:{message(body)}"
|
||
|
||
|
||
def lucky_draw(session, username):
|
||
state = api(session, "promotionjgg.php", {
|
||
"action": "list", "app": "hsy", "appkey": APP_KEY,
|
||
"merchant_id": MERCHANT_ID, "username": username,
|
||
})
|
||
if not success(state):
|
||
return f"幸运抽奖状态查询失败:{message(state)}"
|
||
info = state.get("data") or {}
|
||
try:
|
||
chances = int(info.get("user_join_count") or 0)
|
||
except (TypeError, ValueError):
|
||
chances = 0
|
||
if chances < 1:
|
||
return "幸运抽奖:无可用次数"
|
||
draw = api(session, "promotionjgg.php", {
|
||
"action": "prize_draw", "app": "hsy", "appkey": APP_KEY,
|
||
"merchant_id": MERCHANT_ID, "username": username,
|
||
})
|
||
if not success(draw):
|
||
return f"幸运抽奖失败:{message(draw)}"
|
||
prize = draw.get("data") or {}
|
||
title = str(prize.get("title") or prize.get("name") or "未返回奖品名称").strip()
|
||
return f"幸运抽奖:{title}"
|
||
|
||
|
||
def join_zero_event(session, username):
|
||
listing = api(session, "promotionhighworth.php", {
|
||
"action": "actlist", "app": APP, "appkey": APP_KEY,
|
||
"merchant_id": MERCHANT_ID, "username": username,
|
||
})
|
||
if not success(listing):
|
||
return [f"0 元夺宝活动查询失败:{message(listing)}"]
|
||
data = listing.get("data") or {}
|
||
active = data.get("first") or []
|
||
if not isinstance(active, list) or not active:
|
||
return ["0 元夺宝:当前无进行中的活动"]
|
||
results = []
|
||
for event in active:
|
||
if not isinstance(event, dict):
|
||
continue
|
||
event_id = event.get("itemid") or event.get("id")
|
||
title = str(event.get("title") or event.get("mall_title") or event_id or "活动").strip()
|
||
if not event_id:
|
||
results.append(f"0 元夺宝:{title} 缺少活动编号,跳过")
|
||
continue
|
||
if str(event.get("isjoin", "0")) == "1":
|
||
results.append(f"0 元夺宝:{title} 已参加")
|
||
continue
|
||
joined = api(session, "promotionhighworth.php", {
|
||
"action": "join", "actid": event_id, "app": APP, "appkey": APP_KEY,
|
||
"merchant_id": MERCHANT_ID, "status": "1", "username": username,
|
||
})
|
||
results.append(
|
||
f"0 元夺宝:{title} 参加成功" if success(joined)
|
||
else f"0 元夺宝:{title} 参加失败:{message(joined)}"
|
||
)
|
||
return results or ["0 元夺宝:未发现可处理活动"]
|
||
|
||
|
||
def withdraw_all(session, username):
|
||
# v135 奖励金分包:envcash.php?action=add&type=award&app=wx。
|
||
# 服务端成功仅创建商家转账包;青龙无法替代微信客户端 requestMerchantTransfer 的收款确认。
|
||
available = api(session, "envcash.php", {
|
||
"action": "awardlist", "appkey": APP_KEY, "genre": "0", "merchant_id": MERCHANT_ID,
|
||
"type": "award", "username": username,
|
||
})
|
||
if not success(available):
|
||
return f"提现资格查询失败:{message(available)}"
|
||
info = available.get("data") or {}
|
||
cashable = amount(info.get("award_amount"))
|
||
minimum = amount(info.get("award_cash"))
|
||
maximum = amount(info.get("award_cash_most"))
|
||
if cashable <= Decimal("1"):
|
||
return f"可提现 {cashable:.2f} 元,未超过 1.00 元,跳过"
|
||
if minimum and cashable < minimum:
|
||
return f"可提现 {cashable:.2f} 元,低于平台门槛 {minimum:.2f} 元,跳过"
|
||
if maximum and cashable > maximum:
|
||
return f"可提现 {cashable:.2f} 元,高于单笔上限 {maximum:.2f} 元,跳过(不拆单)"
|
||
body = api(session, "envcash.php", {
|
||
"action": "add", "amount": f"{cashable:.2f}", "app": "wx", "appkey": APP_KEY,
|
||
"merchant_id": MERCHANT_ID, "type": "award", "version": "2", "username": username,
|
||
})
|
||
if not success(body):
|
||
return f"全额提现 {cashable:.2f} 元失败:{message(body)}"
|
||
transfer = body.get("data") or {}
|
||
if transfer.get("package_info"):
|
||
return f"已发起全额提现 {cashable:.2f} 元;等待微信客户端确认收款"
|
||
return f"全额提现 {cashable:.2f} 元已提交:{message(body)}"
|
||
|
||
|
||
def notify(lines):
|
||
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
|
||
send("回收猿签到提现", "\n".join(lines))
|
||
except Exception as exc:
|
||
print(f"[通知] 发送失败(不影响任务):{exc}")
|
||
|
||
|
||
def run_one(index, server, ref, nick, context):
|
||
label = nick or f"YYB {ref}"
|
||
result = [f"账号 {index}({label})"]
|
||
session = requests.Session()
|
||
session.headers.update({"User-Agent": "Mozilla/5.0 MicroMessenger/7.0.20.1781 MiniProgramEnv/Windows"})
|
||
try:
|
||
username = login(session, server, ref, context)
|
||
initial, _ = center(session, username)
|
||
result.append(f"初始奖励金:{initial:.2f} 元")
|
||
result.append(sign_in(session, username))
|
||
result.append(lucky_draw(session, username))
|
||
result.extend(join_zero_event(session, username))
|
||
time.sleep(1)
|
||
final, _ = center(session, username)
|
||
result.append(f"最终奖励金:{final:.2f} 元")
|
||
result.append(withdraw_all(session, username))
|
||
except Exception as exc:
|
||
result.append(f"失败:{exc}")
|
||
print(" | ".join(result))
|
||
return result
|
||
|
||
|
||
def main():
|
||
context = bind_context()
|
||
output = ["回收猿:超过 1.00 元时尝试一次全额提现"]
|
||
skip = blacklist()
|
||
routes_list = routes()
|
||
ran = False
|
||
for index, (server, ref, nick) in enumerate(routes_list, 1):
|
||
label = nick or f"YYB {ref}"
|
||
if ref in skip or (nick and nick in skip):
|
||
print(f"账号 {index}({label})命中 HSY_blacklist,跳过")
|
||
continue
|
||
if ran:
|
||
delay = random.randint(5, 20)
|
||
print(f"[延迟] {delay} 秒后运行下一个账号")
|
||
time.sleep(delay)
|
||
output.extend(run_one(index, server, ref, nick, context))
|
||
ran = True
|
||
notify(output)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|