diff --git a/YYB/YYB_HSY.py b/YYB/YYB_HSY.py new file mode 100644 index 0000000..612d023 --- /dev/null +++ b/YYB/YYB_HSY.py @@ -0,0 +1,334 @@ +# 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()