zcbot/scripts/probe_clawbot.py

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())