From b6b13cc8cf356f316a056fbee87c7bd7663ba3e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E6=96=87=E5=BD=AC?= <1473157698@qq.com> Date: Thu, 11 Jun 2026 09:15:55 +0800 Subject: [PATCH] =?UTF-8?q?fix(wechat):=20=E4=BF=AE=E5=A4=8D=E4=B8=AA?= =?UTF-8?q?=E4=BA=BA=E5=BE=AE=E4=BF=A1=E5=89=8D=E7=AB=AF=E5=9C=A8=20headle?= =?UTF-8?q?ss=20=E5=AE=B9=E5=99=A8=E5=86=85=E6=97=A0=E6=B3=95=E7=99=BB?= =?UTF-8?q?=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 容器化部署时个人微信前端无法首次扫码登录,定位并修复 4 处问题: 1. isatty 闸门顺序错误:__main__ 先把 stdout 重定向到日志文件,再判 sys.stdout.isatty()——文件句柄恒为 false,导致无 token 时必然退出。 改为始终走 QR 登录、二维码打到真实 stdout(docker logs 可见), 无需 TTY 即可扫码。 2. get_bot_qrcode 无容错:被限流时响应缺 'qrcode' 字段直接 KeyError 崩溃, 配合 restart 策略形成死亡螺旋。改为退避重试,等 qrcode + img_content 都就绪。 3. get_qrcode_status 轮询只 catch ReadTimeout:限流返回非 JSON 时 .json() 抛错崩溃。改为捕获所有 RequestException + ValueError 容错重试。 4. PNG 兜底依赖 PIL,缺失时 qrcode.make().save() 抛 ModuleNotFoundError, 且 traceback 被 finally 重定向到日志文件而难以发现。改为先打 ASCII 二维码(扫码真正所需,无依赖),PNG 保存包 try 失败不崩。 均为通用的容器化健壮性改进。 Co-Authored-By: Claude Opus 4.8 --- frontends/wechatapp.py | 49 +++++++++++++++++++++++++++++++----------- 1 file changed, 37 insertions(+), 12 deletions(-) diff --git a/frontends/wechatapp.py b/frontends/wechatapp.py index aff57bf16..301f10c62 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: