151 lines
5.5 KiB
Python
151 lines
5.5 KiB
Python
"""一次性探测:微信 ClawBot 灰度是否覆盖某个微信号。
|
|
|
|
只做两件事(不碰 zcbot 主体、不落库):
|
|
1. GET get_bot_qrcode 拿二维码 -> 存 qr.png 并自动打开
|
|
2. 轮询 get_qrcode_status 等扫码确认 -> 报告 status
|
|
|
|
判读:
|
|
- 接口连不通 / 非 200 -> 本机到 ilinkai 网络不通,换网或在有网机器跑
|
|
- 出码成功、手机扫得动确认 -> 该微信号在灰度内,ClawBot 可用
|
|
- 出码成功、扫了报"不支持" -> 版本不够或未灰度到该号
|
|
|
|
ASCII-only 输出(Windows GBK 控制台)。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import base64
|
|
import os
|
|
import random
|
|
import sys
|
|
import time
|
|
import webbrowser
|
|
|
|
import httpx
|
|
|
|
BASE = "https://ilinkai.weixin.qq.com"
|
|
QR_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "clawbot_qr.png")
|
|
|
|
|
|
def _uin_header() -> str:
|
|
# X-WECHAT-UIN: base64(String(randomUint32()))
|
|
return base64.b64encode(str(random.randint(0, 2**32 - 1)).encode()).decode()
|
|
|
|
|
|
def _headers() -> dict:
|
|
return {
|
|
"Content-Type": "application/json",
|
|
"AuthorizationType": "ilink_bot_token",
|
|
"X-WECHAT-UIN": _uin_header(),
|
|
}
|
|
|
|
|
|
def _save_qr(img_content: str, qrcode_id: str) -> bool:
|
|
"""实测:qrcode_img_content 是微信深链(https://liteapp.weixin.qq.com/q/...),
|
|
需把该 URL **编码成二维码** 让微信扫,而非当图片下载。
|
|
兜底:若哪天返回的是真图片字节(data-uri / base64 PNG)则直接存。
|
|
"""
|
|
try:
|
|
if not img_content:
|
|
print(f"[hint] no img content; encode this id manually: {qrcode_id}")
|
|
return False
|
|
# 情况 A:真图片字节
|
|
if img_content.startswith("data:image"):
|
|
data = base64.b64decode(img_content.split(",", 1)[1])
|
|
with open(QR_PATH, "wb") as f:
|
|
f.write(data)
|
|
print(f"[ok] QR (image) saved -> {QR_PATH}")
|
|
return True
|
|
# 情况 B(实测):深链 / 任意字符串 -> 自己渲染成二维码
|
|
import segno
|
|
print(f"[info] encoding deep-link into QR: {img_content}")
|
|
segno.make(img_content, error="m").save(QR_PATH, scale=8, border=3)
|
|
print(f"[ok] QR (rendered from deep-link) saved -> {QR_PATH}")
|
|
return True
|
|
except Exception as e:
|
|
print(f"[warn] could not build QR: {type(e).__name__}: {e}")
|
|
print(f"[hint] deep-link to scan manually: {img_content}")
|
|
return False
|
|
|
|
|
|
def main() -> int:
|
|
print("[step1] GET get_bot_qrcode ...")
|
|
try:
|
|
with httpx.Client(timeout=20) as c:
|
|
r = c.get(
|
|
f"{BASE}/ilink/bot/get_bot_qrcode",
|
|
params={"bot_type": "3"},
|
|
headers=_headers(),
|
|
)
|
|
except Exception as e:
|
|
print(f"[FAIL] network error to {BASE}: {type(e).__name__}: {e}")
|
|
print("[judge] host cannot reach ilinkai.weixin.qq.com -> try another network.")
|
|
return 2
|
|
|
|
print(f"[http] status={r.status_code}")
|
|
body_preview = r.text[:600]
|
|
print(f"[body] {body_preview}")
|
|
if r.status_code != 200:
|
|
print("[judge] non-200 from get_bot_qrcode -> endpoint/params may be wrong or blocked.")
|
|
return 3
|
|
|
|
try:
|
|
data = r.json()
|
|
except Exception:
|
|
print("[FAIL] response not JSON; see body above.")
|
|
return 3
|
|
|
|
qrcode_id = data.get("qrcode") or data.get("qrcode_id") or ""
|
|
img = data.get("qrcode_img_content") or data.get("qrcode_img") or ""
|
|
if not qrcode_id:
|
|
print("[FAIL] no 'qrcode' field in response; field names differ -> inspect body above.")
|
|
return 3
|
|
|
|
if _save_qr(img, qrcode_id):
|
|
try:
|
|
webbrowser.open("file://" + QR_PATH.replace("\\", "/"))
|
|
except Exception:
|
|
pass
|
|
print("[action] QR opened. Scan it with your phone WeChat NOW.")
|
|
else:
|
|
print("[action] QR image unavailable; cannot open. See hint above.")
|
|
|
|
poll_secs = int(sys.argv[1]) if len(sys.argv) > 1 else 100
|
|
print(f"[step2] polling get_qrcode_status (up to ~{poll_secs}s; Ctrl-C to stop)...")
|
|
deadline = time.time() + poll_secs
|
|
last = ""
|
|
with httpx.Client(timeout=40) as c:
|
|
while time.time() < deadline:
|
|
try:
|
|
r = c.get(
|
|
f"{BASE}/ilink/bot/get_qrcode_status",
|
|
params={"qrcode": qrcode_id},
|
|
headers=_headers(),
|
|
)
|
|
st = ""
|
|
try:
|
|
st = (r.json() or {}).get("status", "")
|
|
except Exception:
|
|
st = f"(non-json http {r.status_code})"
|
|
if st != last:
|
|
print(f"[poll] status={st!r}")
|
|
last = st
|
|
if st == "confirmed":
|
|
j = r.json()
|
|
tok = j.get("bot_token", "")
|
|
base_url = j.get("baseurl") or j.get("base_url") or ""
|
|
masked = (tok[:6] + "..." + tok[-4:]) if len(tok) > 12 else "(short)"
|
|
print("[SUCCESS] scan confirmed -> this WeChat account IS in the ClawBot rollout.")
|
|
print(f"[SUCCESS] bot_token={masked} baseurl={base_url}")
|
|
print("[note] token masked on purpose; it is a per-user credential.")
|
|
return 0
|
|
except Exception as e:
|
|
print(f"[poll] error: {type(e).__name__}: {e}")
|
|
time.sleep(2)
|
|
print("[timeout] no confirmation within window. Either not scanned in time, or")
|
|
print("[timeout] your WeChat lacks the ClawBot entry (version <8.0.70 or not gray-rolled).")
|
|
return 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|