"""探测四:验证 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())