fix: 个人微信发送对齐iLink规范(base_info/client_id/ret检查); 会话过期检测+重扫UI; 仅发送成功才写历史

This commit is contained in:
WorkBuddy committed 2026-07-10 08:00:25 +08:00
1 parent 62f3986e04
commit 2eeaf1d293
2 files changed
+88 -31

No files matched your search

+69 -26
View File
@@ -164,16 +164,23 @@ def _wechat_get_updates(baseurl: str, bot_token: str, cursor: str):
return [], cursor return [], cursor
def _wechat_send_message(baseurl: str, bot_token: str, to_user_id: str, context_token: str, text: str) -> bool: def _wechat_send_message(baseurl: str, bot_token: str, to_user_id: str, context_token: str, text: str):
"""发送文本回复(必须带 context_token 关联到正确的对话窗口)。失败重试一次。""" """发送文本回复(必须带 context_token 关联到正确的对话窗口)。
payload = {"msg": { 返回 (ok, ret, body)。ret=-14=会话过期需重扫;ret=-2=token 失效。"""
"to_user_id": to_user_id, import uuid as _uuid
"message_type": 2, payload = {
"message_state": 2, "msg": {
"context_token": context_token, "from_user_id": "",
"item_list": [{"type": 1, "text_item": {"text": text}}], "to_user_id": to_user_id,
}} "client_id": "py-" + str(_uuid.uuid4()),
last = False "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): for _ in range(2):
try: try:
resp = requests.post( resp = requests.post(
@@ -182,12 +189,25 @@ def _wechat_send_message(baseurl: str, bot_token: str, to_user_id: str, context_
json=payload, json=payload,
timeout=20, timeout=20,
) )
last = resp.ok try:
if last: body = resp.json() if resp.content else {}
return True 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: except Exception:
last = False pass
return last return False, (last_body.get("ret") if isinstance(last_body, dict) else None), last_body
def _wechat_extract_text(msg: dict) -> str: 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): def _wechat_receiver_loop(account_id: int):
"""长轮询个人微信消息 → 七虎AI 回复 → 发送。""" """长轮询个人微信消息 → 七虎AI 回复 → 发送。"""
cursor = "" cursor = ""
@@ -284,17 +320,23 @@ def _wechat_receiver_loop(account_id: int):
pass pass
# 先生成回复(此时历史尚未包含本条,避免重复) # 先生成回复(此时历史尚未包含本条,避免重复)
reply = _qihua_reply(content, owner_uid) reply = _qihua_reply(content, owner_uid)
ok = _wechat_send_message(baseurl, bot_token, sender, ctx, reply) 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) if owner_uid:
_log_chat(owner_uid, "personal_wechat", sender, ctx, "assistant", reply) _log_chat(owner_uid, "personal_wechat", sender, ctx, "user", content)
try: _log_chat(owner_uid, "personal_wechat", sender, ctx, "assistant", reply)
with open("/tmp/wechat_send.log", "a", encoding="utf-8") as _f: else:
_f.write(datetime.now().isoformat() + " ok=" + str(ok) try:
+ " sender=" + repr(sender) + " ctx=" + repr(ctx) + "\n") with open("/tmp/wechat_send.log", "a", encoding="utf-8") as _f:
except Exception: _f.write(datetime.now().isoformat() + " SEND_FAIL ret=%s sender=%s\n"
pass % (ret, repr(sender)))
except Exception:
pass
if ret in (-2, -14):
# 会话/鉴权失效:标记需重扫并停止接收线程
_mark_wechat_expired(d["id"])
return
time.sleep(1) time.sleep(1)
except Exception: except Exception:
time.sleep(5) time.sleep(5)
@@ -324,6 +366,7 @@ async def wechat_status(user: User = Depends(get_current_user)):
d = _row_to_dict(row) d = _row_to_dict(row)
return {"success": True, "data": { return {"success": True, "data": {
"connected": d.get("status") == "connected", "connected": d.get("status") == "connected",
"status": d.get("status"),
"configured": True, "configured": True,
"baseurl": d.get("baseurl"), "baseurl": d.get("baseurl"),
"ilink_bot_id": d.get("ilink_bot_id"), "ilink_bot_id": d.get("ilink_bot_id"),
+19 -5
View File
@@ -7,6 +7,7 @@ const WechatBot: React.FC = () => {
const [status, setStatus] = useState<any>({ connected: false, configured: false }); const [status, setStatus] = useState<any>({ connected: false, configured: false });
const [qrcode, setQrcode] = useState<any>(null); const [qrcode, setQrcode] = useState<any>(null);
const [polling, setPolling] = useState(false); const [polling, setPolling] = useState(false);
const [reconnecting, setReconnecting] = useState(false);
const [toast, setToast] = useState<{ msg: string; type: 'success' | 'error' } | null>(null); const [toast, setToast] = useState<{ msg: string; type: 'success' | 'error' } | null>(null);
const showToast = (msg: string, type: 'success' | 'error' = 'success') => { const showToast = (msg: string, type: 'success' | 'error' = 'success') => {
@@ -56,6 +57,7 @@ const WechatBot: React.FC = () => {
const d = (res as any).data; const d = (res as any).data;
if (d.status === 'confirmed' || d.connected) { if (d.status === 'confirmed' || d.connected) {
setPolling(false); setPolling(false);
setReconnecting(false);
setStatus({ ...status, connected: true, configured: true }); setStatus({ ...status, connected: true, configured: true });
showToast('个人微信已连接!'); showToast('个人微信已连接!');
loadStatus(); loadStatus();
@@ -82,6 +84,7 @@ const WechatBot: React.FC = () => {
await imUserAPI.disconnect(); await imUserAPI.disconnect();
setPolling(false); setPolling(false);
setQrcode(null); setQrcode(null);
setReconnecting(false);
setStatus({ connected: false, configured: false }); setStatus({ connected: false, configured: false });
showToast('已断开个人微信连接'); showToast('已断开个人微信连接');
loadStatus(); loadStatus();
@@ -123,18 +126,29 @@ const WechatBot: React.FC = () => {
</div> </div>
<span style={{ <span style={{
padding: '4px 12px', borderRadius: '4px', fontSize: '12px', padding: '4px 12px', borderRadius: '4px', fontSize: '12px',
background: status.connected ? '#10b98120' : '#6b728020', background: status.connected ? '#10b98120' : status.status === 'expired' ? '#ef444420' : '#6b728020',
color: status.connected ? '#10b981' : '#6b7280', color: status.connected ? '#10b981' : status.status === 'expired' ? '#ef4444' : '#6b7280',
}}> }}>
{status.connected ? '● 已连接' : status.configured ? '○ 已配置' : '○ 未绑定'} {status.connected ? '● 已连接' : status.status === 'expired' ? '⚠ 会话已过期' : status.configured ? '○ 已配置' : '○ 未绑定'}
</span> </span>
</div> </div>
{/* 扫码绑定 */} {/* 扫码绑定 */}
<div style={{ background: '#F8F9FC', borderRadius: '12px', padding: '24px', marginBottom: '16px' }}> <div style={{ background: '#F8F9FC', borderRadius: '12px', padding: '24px', marginBottom: '16px' }}>
{status.status === 'expired' && (
<div style={{ background: '#fef2f2', border: '1px solid #fecaca', color: '#b91c1c', borderRadius: '8px', padding: '12px 14px', marginBottom: '16px', fontSize: '13px', lineHeight: '1.7' }}>
⚠ <b>微信会话已失效</b>:检测到与微信的连接已过期(通常是服务更新/重启后导致),消息无法再送达。请点击下方「获取扫码二维码」<b>重新扫码授权</b>即可恢复,历史对话记录仍保留。
</div>
)}
<h4 style={{ marginTop: 0, fontSize: '15px', color: '#1A1F36', marginBottom: '12px' }}>扫码授权绑定</h4> <h4 style={{ marginTop: 0, fontSize: '15px', color: '#1A1F36', marginBottom: '12px' }}>扫码授权绑定</h4>
{status.connected ? ( {status.connected && !reconnecting ? (
<div style={{ fontSize: '13px', color: '#10b981' }}>已绑定,你的个人微信正在自动收发消息并由七虎AI 应答。</div> <div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
<div style={{ fontSize: '13px', color: '#10b981' }}>已绑定,你的个人微信正在自动收发消息并由七虎AI 应答。</div>
<div>
<button onClick={() => { setReconnecting(true); handleQrcode(); }} style={secondaryBtnStyle}>重新连接(重扫二维码)</button>
</div>
<div style={{ fontSize: '12px', color: '#9CA3AF' }}>若发现消息收不到,可点此重新扫码以重建与微信的会话。</div>
</div>
) : !qrcode ? ( ) : !qrcode ? (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start', gap: '12px' }}> <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start', gap: '12px' }}>
<div style={{ fontSize: '13px', color: '#6B7280' }}> <div style={{ fontSize: '13px', color: '#6B7280' }}>