diff --git a/frontends/wechatapp.py b/frontends/wechatapp.py index aff57bf1..301f10c6 100644 --- a/frontends/wechatapp.py +++ b/frontends/wechatapp.py @@ -63,20 +63,42 @@ def _post(self, ep, body, timeout=15): return r.json() def login_qr(self, poll_interval=2): - r = requests.get(f'{API}/ilink/bot/get_bot_qrcode', params={'bot_type': 3}, headers={'User-Agent': UA}, timeout=10) - r.raise_for_status() - d = r.json() + # 获取二维码:对限流/缺字段(无 'qrcode')退避重试,避免崩溃→重启→更狠地打接口的死亡螺旋 + d = {} + for attempt in range(6): + try: + r = requests.get(f'{API}/ilink/bot/get_bot_qrcode', params={'bot_type': 3}, headers={'User-Agent': UA}, timeout=10) + r.raise_for_status() + d = r.json() + except requests.exceptions.RequestException as e: + print(f'[QR登录] 获取二维码失败({e}),{2 ** attempt}s 后重试...'); time.sleep(2 ** attempt); continue + if d.get('qrcode') and d.get('qrcode_img_content'): # 二维码 ID + 可扫图都就绪才算成功 + break + print(f'[QR登录] 二维码未就绪(可能被限流,ret={d.get("ret")}),{2 ** attempt}s 后重试...') + time.sleep(2 ** attempt) + if not (d.get('qrcode') and d.get('qrcode_img_content')): + raise RuntimeError('多次重试仍未获取到可扫二维码(疑似限流),请稍后重试') qr_id, url = d['qrcode'], d.get('qrcode_img_content', '') print(f'[QR登录] ID: {qr_id}') if url: - img = self._tf.parent / 'wx_qr.png' - qrcode.make(url).save(str(img)) # 保存到文件,不弹浏览器 + # 先打 ASCII 二维码(纯文本,无需 PIL;容器/无头环境靠它扫码) qr = qrcode.QRCode(border=1); qr.add_data(url); qr.make(fit=True); qr.print_ascii(invert=True) + # 再尝试存 PNG 兜底——依赖 PIL,缺失/失败不应让登录崩溃 + try: + qrcode.make(url).save(str(self._tf.parent / 'wx_qr.png')) + except Exception as e: + print(f'[QR登录] PNG 兜底保存失败({e}),用上方 ASCII 二维码扫码即可') last = '' while True: time.sleep(poll_interval) - try: s = requests.get(f'{API}/ilink/bot/get_qrcode_status', params={'qrcode': qr_id}, headers={'User-Agent': UA}, timeout=60).json() - except requests.exceptions.ReadTimeout: continue + # 轮询状态:对所有网络异常 / 非 JSON(被限流时常返回 HTML)容错重试, + # 不让单次抖动把进程打崩——配合 restart:unless-stopped 否则会死亡螺旋 + try: + s = requests.get(f'{API}/ilink/bot/get_qrcode_status', params={'qrcode': qr_id}, headers={'User-Agent': UA}, timeout=60).json() + except requests.exceptions.RequestException: + continue + except ValueError: # 响应非 JSON(限流/网关页) + time.sleep(poll_interval); continue st = s.get('status', '') if st != last: print(f' 状态: {st}'); last = st if st == 'confirmed': @@ -415,11 +437,14 @@ def _send(show): print(f'[NEW] Process starting {time.strftime("%m-%d %H:%M")}') bot = WxBotClient() if _do_relogin or not bot.token: - if not sys.stdout.isatty(): - print('[Bot] no token and not interactive, exit.'); sys.exit(1) - sys.stdout = sys.stderr = sys.__stdout__ # restore for QR display - bot.login_qr() - sys.stdout = sys.stderr = _logf + # QR 登录在无 TTY 的容器里也可用:把二维码打到真实 stdout(docker logs + # 可见),而不是日志文件——之前在重定向后才判 isatty(),文件句柄恒 false + # 导致容器内必然退出,无法首次登录。PNG 仍存 ~/.wxbot/wx_qr.png 作兜底。 + sys.stdout = sys.stderr = sys.__stdout__ # restore for QR display (real stdout / container log) + try: + bot.login_qr() + finally: + sys.stdout = sys.stderr = _logf threading.Thread(target=agent.run, daemon=True).start() print(f'WeChat Bot 已启动 (bot_id={bot.bot_id})', file=sys.__stdout__) try: