fix: 个人微信发送对齐iLink规范(base_info/client_id/ret检查); 会话过期检测+重扫UI; 仅发送成功才写历史
This commit is contained in:
1 parent
62f3986e04
commit
2eeaf1d293
2 files changed
+75
-18
No files matched your search
@@ -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 失效。"""
|
||||||
|
import uuid as _uuid
|
||||||
|
payload = {
|
||||||
|
"msg": {
|
||||||
|
"from_user_id": "",
|
||||||
"to_user_id": to_user_id,
|
"to_user_id": to_user_id,
|
||||||
|
"client_id": "py-" + str(_uuid.uuid4()),
|
||||||
"message_type": 2,
|
"message_type": 2,
|
||||||
"message_state": 2,
|
"message_state": 2,
|
||||||
"context_token": context_token,
|
"context_token": context_token,
|
||||||
"item_list": [{"type": 1, "text_item": {"text": text}}],
|
"item_list": [{"type": 1, "text_item": {"text": text}}],
|
||||||
}}
|
},
|
||||||
last = False
|
"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:
|
except Exception:
|
||||||
last = False
|
body = {}
|
||||||
return last
|
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:
|
||||||
|
pass
|
||||||
|
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:
|
if owner_uid:
|
||||||
_log_chat(owner_uid, "personal_wechat", sender, ctx, "user", content)
|
_log_chat(owner_uid, "personal_wechat", sender, ctx, "user", content)
|
||||||
_log_chat(owner_uid, "personal_wechat", sender, ctx, "assistant", reply)
|
_log_chat(owner_uid, "personal_wechat", sender, ctx, "assistant", reply)
|
||||||
|
else:
|
||||||
try:
|
try:
|
||||||
with open("/tmp/wechat_send.log", "a", encoding="utf-8") as _f:
|
with open("/tmp/wechat_send.log", "a", encoding="utf-8") as _f:
|
||||||
_f.write(datetime.now().isoformat() + " ok=" + str(ok)
|
_f.write(datetime.now().isoformat() + " SEND_FAIL ret=%s sender=%s\n"
|
||||||
+ " sender=" + repr(sender) + " ctx=" + repr(ctx) + "\n")
|
% (ret, repr(sender)))
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
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"),
|
||||||
|
|||||||
@@ -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={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
||||||
<div style={{ fontSize: '13px', color: '#10b981' }}>已绑定,你的个人微信正在自动收发消息并由七虎AI 应答。</div>
|
<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' }}>
|
||||||
|
|||||||
Reference in new issue
Block a user