From 2eeaf1d293394936cbbcf2593d3ac7d2f9533e9d Mon Sep 17 00:00:00 2001 From: WorkBuddy Date: Fri, 10 Jul 2026 08:00:25 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=B8=AA=E4=BA=BA=E5=BE=AE=E4=BF=A1?= =?UTF-8?q?=E5=8F=91=E9=80=81=E5=AF=B9=E9=BD=90iLink=E8=A7=84=E8=8C=83(bas?= =?UTF-8?q?e=5Finfo/client=5Fid/ret=E6=A3=80=E6=9F=A5);=20=E4=BC=9A?= =?UTF-8?q?=E8=AF=9D=E8=BF=87=E6=9C=9F=E6=A3=80=E6=B5=8B+=E9=87=8D?= =?UTF-8?q?=E6=89=ABUI;=20=E4=BB=85=E5=8F=91=E9=80=81=E6=88=90=E5=8A=9F?= =?UTF-8?q?=E6=89=8D=E5=86=99=E5=8E=86=E5=8F=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/python/app/api/im_bot_user.py | 95 +++++++++++++++++++------- frontend/src/pages/front/WechatBot.tsx | 24 +++++-- 2 files changed, 88 insertions(+), 31 deletions(-) diff --git a/backend/python/app/api/im_bot_user.py b/backend/python/app/api/im_bot_user.py index a6bc102..94ca54d 100644 --- a/backend/python/app/api/im_bot_user.py +++ b/backend/python/app/api/im_bot_user.py @@ -164,16 +164,23 @@ def _wechat_get_updates(baseurl: str, bot_token: str, cursor: str): return [], cursor -def _wechat_send_message(baseurl: str, bot_token: str, to_user_id: str, context_token: str, text: str) -> bool: - """发送文本回复(必须带 context_token 关联到正确的对话窗口)。失败重试一次。""" - payload = {"msg": { - "to_user_id": to_user_id, - "message_type": 2, - "message_state": 2, - "context_token": context_token, - "item_list": [{"type": 1, "text_item": {"text": text}}], - }} - last = False +def _wechat_send_message(baseurl: str, bot_token: str, to_user_id: str, context_token: str, text: str): + """发送文本回复(必须带 context_token 关联到正确的对话窗口)。 + 返回 (ok, ret, body)。ret=-14=会话过期需重扫;ret=-2=token 失效。""" + import uuid as _uuid + payload = { + "msg": { + "from_user_id": "", + "to_user_id": to_user_id, + "client_id": "py-" + str(_uuid.uuid4()), + "message_type": 2, + "message_state": 2, + "context_token": context_token, + "item_list": [{"type": 1, "text_item": {"text": text}}], + }, + "base_info": {"channel_version": "1.0.2"}, + } + last_body = {} for _ in range(2): try: resp = requests.post( @@ -182,12 +189,25 @@ def _wechat_send_message(baseurl: str, bot_token: str, to_user_id: str, context_ json=payload, timeout=20, ) - last = resp.ok - if last: - return True + try: + body = resp.json() if resp.content else {} + except Exception: + body = {} + last_body = body + ret = body.get("ret") if isinstance(body, dict) else None + try: + with open("/tmp/wechat_send.log", "a", encoding="utf-8") as _f: + _f.write(datetime.now().isoformat() + " SEND_HTTP=%s ret=%s body=%s\n" + % (resp.status_code, ret, json.dumps(body, ensure_ascii=False)[:300])) + except Exception: + pass + if resp.ok and (ret is None or ret == 0 or not body): + return True, ret, body + if ret in (-2, -14): + return False, ret, body # 鉴权/会话失效,立即停止重试 except Exception: - last = False - return last + pass + return False, (last_body.get("ret") if isinstance(last_body, dict) else None), last_body def _wechat_extract_text(msg: dict) -> str: @@ -247,6 +267,22 @@ def _log_chat(user_id, platform, from_user, context_token, role, content): # ============================================================ # 后台接收线程(每用户独立) # ============================================================ +def _mark_wechat_expired(account_id: int): + """发送会话失效(ret=-14/-2):标记账号需重扫,并清除失效 token。""" + db = None + try: + db = SessionLocal() + _execute_raw(db, text( + "UPDATE im_bot_wechat_account SET status='expired', bot_token=NULL, " + "updated_at=:ts WHERE id=:aid" + ), {"aid": account_id, "ts": datetime.now()}) + except Exception: + pass + finally: + if db: + db.close() + + def _wechat_receiver_loop(account_id: int): """长轮询个人微信消息 → 七虎AI 回复 → 发送。""" cursor = "" @@ -284,17 +320,23 @@ def _wechat_receiver_loop(account_id: int): pass # 先生成回复(此时历史尚未包含本条,避免重复) reply = _qihua_reply(content, owner_uid) - ok = _wechat_send_message(baseurl, bot_token, sender, ctx, reply) - # 持久化对话历史 - if owner_uid: - _log_chat(owner_uid, "personal_wechat", sender, ctx, "user", content) - _log_chat(owner_uid, "personal_wechat", sender, ctx, "assistant", reply) - try: - with open("/tmp/wechat_send.log", "a", encoding="utf-8") as _f: - _f.write(datetime.now().isoformat() + " ok=" + str(ok) - + " sender=" + repr(sender) + " ctx=" + repr(ctx) + "\n") - except Exception: - pass + ok, ret, _ = _wechat_send_message(baseurl, bot_token, sender, ctx, reply) + if ok: + # 仅发送成功才写对话历史(避免“发了但没收到”的误导记录) + if owner_uid: + _log_chat(owner_uid, "personal_wechat", sender, ctx, "user", content) + _log_chat(owner_uid, "personal_wechat", sender, ctx, "assistant", reply) + else: + try: + with open("/tmp/wechat_send.log", "a", encoding="utf-8") as _f: + _f.write(datetime.now().isoformat() + " SEND_FAIL ret=%s sender=%s\n" + % (ret, repr(sender))) + except Exception: + pass + if ret in (-2, -14): + # 会话/鉴权失效:标记需重扫并停止接收线程 + _mark_wechat_expired(d["id"]) + return time.sleep(1) except Exception: time.sleep(5) @@ -324,6 +366,7 @@ async def wechat_status(user: User = Depends(get_current_user)): d = _row_to_dict(row) return {"success": True, "data": { "connected": d.get("status") == "connected", + "status": d.get("status"), "configured": True, "baseurl": d.get("baseurl"), "ilink_bot_id": d.get("ilink_bot_id"), diff --git a/frontend/src/pages/front/WechatBot.tsx b/frontend/src/pages/front/WechatBot.tsx index 6df4391..0718b8e 100644 --- a/frontend/src/pages/front/WechatBot.tsx +++ b/frontend/src/pages/front/WechatBot.tsx @@ -7,6 +7,7 @@ const WechatBot: React.FC = () => { const [status, setStatus] = useState({ connected: false, configured: false }); const [qrcode, setQrcode] = useState(null); const [polling, setPolling] = useState(false); + const [reconnecting, setReconnecting] = useState(false); const [toast, setToast] = useState<{ msg: string; type: 'success' | 'error' } | null>(null); const showToast = (msg: string, type: 'success' | 'error' = 'success') => { @@ -56,6 +57,7 @@ const WechatBot: React.FC = () => { const d = (res as any).data; if (d.status === 'confirmed' || d.connected) { setPolling(false); + setReconnecting(false); setStatus({ ...status, connected: true, configured: true }); showToast('个人微信已连接!'); loadStatus(); @@ -82,6 +84,7 @@ const WechatBot: React.FC = () => { await imUserAPI.disconnect(); setPolling(false); setQrcode(null); + setReconnecting(false); setStatus({ connected: false, configured: false }); showToast('已断开个人微信连接'); loadStatus(); @@ -123,18 +126,29 @@ const WechatBot: React.FC = () => { - {status.connected ? '● 已连接' : status.configured ? '○ 已配置' : '○ 未绑定'} + {status.connected ? '● 已连接' : status.status === 'expired' ? '⚠ 会话已过期' : status.configured ? '○ 已配置' : '○ 未绑定'} {/* 扫码绑定 */}
+ {status.status === 'expired' && ( +
+ ⚠ 微信会话已失效:检测到与微信的连接已过期(通常是服务更新/重启后导致),消息无法再送达。请点击下方「获取扫码二维码」重新扫码授权即可恢复,历史对话记录仍保留。 +
+ )}

扫码授权绑定

- {status.connected ? ( -
已绑定,你的个人微信正在自动收发消息并由七虎AI 应答。
+ {status.connected && !reconnecting ? ( +
+
已绑定,你的个人微信正在自动收发消息并由七虎AI 应答。
+
+ +
+
若发现消息收不到,可点此重新扫码以重建与微信的会话。
+
) : !qrcode ? (