zcbot/scripts/probe_clawbot_stream.py

148 lines
5.3 KiB
Python

"""探测四:验证 ClawBot 流式/多条回复(message_state 非 FINISH 是关键)。
上轮发现:message_state=2 = FINISH,会"封口"本轮,故第二条被丢。
本轮:同一 context_token 连发三段——前两段 state=1(未结束),末段 state=2(FINISH),
看手机收到的形态:
- 三条独立气泡 AAA / BBB / CCC -> 支持多条独立消息
- 一条气泡里 AAABBBCCC(增长) -> 流式增量(delta),拼成一条
- 只剩 CCC -> 流式覆盖(cumulative,末值胜)
据此定长简报的发法。需要你发【一条】消息触发。bot_token 不打印。ASCII-only。
"""
from __future__ import annotations
import base64
import os
import random
import sys
import time
import httpx
import segno
BASE = "https://ilinkai.weixin.qq.com"
QR_DIR = os.path.dirname(os.path.abspath(__file__))
CHANNEL_VER = "1.0.2"
def _uin() -> str:
return base64.b64encode(str(random.randint(0, 2**32 - 1)).encode()).decode()
def _headers(token: str | None = None) -> dict:
h = {"Content-Type": "application/json",
"AuthorizationType": "ilink_bot_token", "X-WECHAT-UIN": _uin()}
if token:
h["Authorization"] = f"Bearer {token}"
return h
def _new_qr() -> str | None:
with httpx.Client(timeout=20) as c:
r = c.get(f"{BASE}/ilink/bot/get_bot_qrcode",
params={"bot_type": "3"}, headers=_headers())
if r.status_code != 200:
print(f"[FAIL] http {r.status_code}: {r.text[:200]}"); return None
d = r.json()
uniq = os.path.join(QR_DIR, f"clawbot_qr_{int(time.time())}.png")
segno.make(d.get("qrcode_img_content", ""), error="m").save(uniq, scale=8, border=3)
try:
os.startfile(uniq)
except Exception:
pass
print(f"[bind] FRESH QR -> {uniq}")
return d.get("qrcode", "")
def bind() -> tuple[str, str] | None:
print("[bind] auto-refresh on expiry; scan whenever ready.")
qid = _new_qr()
if not qid:
return None
deadline = time.time() + 300
with httpx.Client(timeout=40) as c:
last = ""
while time.time() < deadline:
try:
j = c.get(f"{BASE}/ilink/bot/get_qrcode_status",
params={"qrcode": qid}, headers=_headers()).json()
st = j.get("status", "")
if st != last:
print(f"[bind] status={st!r}"); last = st
if st == "confirmed":
return j.get("bot_token", ""), (j.get("baseurl") or BASE)
if st == "expired":
print("[bind] expired -> new QR");
nq = _new_qr()
if not nq:
return None
qid, last = nq, ""
continue
except Exception as e:
print(f"[bind] err {type(e).__name__}: {e}")
time.sleep(2)
return None
def send(c, token, to_user, text, ctx, state):
body = {"msg": {"to_user_id": to_user, "message_type": 2, "message_state": state,
"context_token": ctx,
"item_list": [{"type": 1, "text_item": {"text": text}}]}}
r = c.post(f"{BASE}/ilink/bot/sendmessage", json=body, headers=_headers(token))
try:
j = r.json()
except Exception:
j = r.text[:200]
print(f"[send] state={state} text={text!r} -> http={r.status_code} body={j}")
def wait_msg(c, token):
deadline = time.time() + 150
buf = ""
while time.time() < deadline:
try:
j = c.post(f"{BASE}/ilink/bot/getupdates",
json={"get_updates_buf": buf,
"base_info": {"channel_version": CHANNEL_VER}},
headers=_headers(token)).json()
buf = j.get("get_updates_buf", buf)
for m in j.get("msgs", []) or []:
txt = "".join((it.get("text_item", {}) or {}).get("text", "")
for it in m.get("item_list", []) or [])
print(f"[recv] <- {txt!r}")
return m
except Exception as e:
print(f"[recv] err {type(e).__name__}: {e}"); time.sleep(2)
return None
def main() -> int:
b = bind()
if not b:
return 2
token, base_url = b
global BASE
BASE = base_url or BASE
print("[bind] confirmed.\n[stream] SEND one message now (e.g. 'go') ...")
with httpx.Client(timeout=30) as c:
m = wait_msg(c, token)
if not m:
print("[stream] no msg; abort."); return 1
to_user, ctx = m.get("from_user_id", ""), m.get("context_token", "")
print("[stream] sending 3 parts with same token (state 1,1,2)...")
send(c, token, to_user, "AAA-第一段(state=1)", ctx, 1)
time.sleep(1)
send(c, token, to_user, "BBB-第二段(state=1)", ctx, 1)
time.sleep(1)
send(c, token, to_user, "CCC-第三段(state=2,FINISH)", ctx, 2)
print("\n========== CHECK YOUR PHONE ==========")
print("Which form did you get?")
print(" (a) three separate bubbles: AAA / BBB / CCC -> multi-message OK")
print(" (b) one bubble growing: AAABBBCCC -> streaming delta-append")
print(" (c) one bubble only: CCC -> streaming cumulative(last wins)")
print(" (d) only AAA / nothing else -> still single")
return 0
if __name__ == "__main__":
sys.exit(main())