1.语音消息ASR: 提取voice_item.text(微信自带转文字) 2.sendtyping正在输入状态: getconfig拿typing_ticket缓存24h + sendtyping发送 3.Session Guard增强: 会话过期记录告警日志供前台提示重扫
This commit is contained in:
1 parent
46e9f1e1e6
commit
e284d43cc7
1 file changed
+66
-2
@@ -54,6 +54,9 @@ WECHAT_TEXT_MAX_LEN = 1500
|
|||||||
# 每个已连接账号一个后台接收线程,key = 账号表主键 id
|
# 每个已连接账号一个后台接收线程,key = 账号表主键 id
|
||||||
_wechat_receiver_threads: dict[int, threading.Thread] = {}
|
_wechat_receiver_threads: dict[int, threading.Thread] = {}
|
||||||
|
|
||||||
|
# typing_ticket 缓存:{ilink_user_id: (ticket, 写入时间戳)},24h 有效
|
||||||
|
_typing_ticket_cache: dict[str, tuple[str, float]] = {}
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# 通用 DB 工具
|
# 通用 DB 工具
|
||||||
@@ -307,6 +310,45 @@ def _wechat_send_message(baseurl: str, bot_token: str, to_user_id: str, context_
|
|||||||
return all_ok, last_ret, {}
|
return all_ok, last_ret, {}
|
||||||
|
|
||||||
|
|
||||||
|
def _wechat_get_typing_ticket(baseurl: str, bot_token: str, ilink_user_id: str, context_token: str) -> str:
|
||||||
|
"""getconfig 获取 typing_ticket(按 ilink_user_id 缓存 24h)。"""
|
||||||
|
global _typing_ticket_cache
|
||||||
|
_now = time.time()
|
||||||
|
_hit = _typing_ticket_cache.get(ilink_user_id)
|
||||||
|
if _hit and (_now - _hit[1]) < 86400:
|
||||||
|
return _hit[0]
|
||||||
|
try:
|
||||||
|
resp = requests.post(
|
||||||
|
baseurl.rstrip("/") + "/ilink/bot/getconfig",
|
||||||
|
headers=_wechat_headers(bot_token),
|
||||||
|
json={"ilink_user_id": ilink_user_id, "context_token": context_token},
|
||||||
|
timeout=10,
|
||||||
|
)
|
||||||
|
body = resp.json() if resp.content else {}
|
||||||
|
ticket = body.get("typing_ticket", "") if isinstance(body, dict) else ""
|
||||||
|
if ticket:
|
||||||
|
_typing_ticket_cache[ilink_user_id] = (ticket, _now)
|
||||||
|
return ticket
|
||||||
|
except Exception:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _wechat_send_typing(baseurl: str, bot_token: str, ilink_user_id: str, typing_ticket: str, status: int = 1) -> bool:
|
||||||
|
"""发送/取消"正在输入"状态。status=1 typing,2 cancel。失败静默(不阻塞主流程)。"""
|
||||||
|
if not ilink_user_id or not typing_ticket:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
resp = requests.post(
|
||||||
|
baseurl.rstrip("/") + "/ilink/bot/sendtyping",
|
||||||
|
headers=_wechat_headers(bot_token),
|
||||||
|
json={"ilink_user_id": ilink_user_id, "typing_ticket": typing_ticket, "status": status},
|
||||||
|
timeout=10,
|
||||||
|
)
|
||||||
|
return resp.ok
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _platform_relay_url(raw_url: str) -> str:
|
def _platform_relay_url(raw_url: str) -> str:
|
||||||
"""把 AGNES 等境外媒体 URL 改写为平台中转地址,使微信/iLink 可内联渲染。
|
"""把 AGNES 等境外媒体 URL 改写为平台中转地址,使微信/iLink 可内联渲染。
|
||||||
|
|
||||||
@@ -367,8 +409,14 @@ def _wechat_send_media(baseurl: str, bot_token: str, to_user_id: str, context_to
|
|||||||
def _wechat_extract_text(msg: dict) -> str:
|
def _wechat_extract_text(msg: dict) -> str:
|
||||||
parts = []
|
parts = []
|
||||||
for it in (msg.get("item_list") or []):
|
for it in (msg.get("item_list") or []):
|
||||||
if it.get("type") == 1 and it.get("text_item", {}).get("text"):
|
_t = it.get("type")
|
||||||
|
if _t == 1 and it.get("text_item", {}).get("text"):
|
||||||
parts.append(it["text_item"]["text"])
|
parts.append(it["text_item"]["text"])
|
||||||
|
elif _t == 3:
|
||||||
|
# 语音消息:微信已自带 ASR 转文字(voice_item.text),直接复用
|
||||||
|
voice = it.get("voice_item") or {}
|
||||||
|
if voice.get("text"):
|
||||||
|
parts.append("[语音] " + str(voice["text"]))
|
||||||
if not parts:
|
if not parts:
|
||||||
for k in ("text", "content", "message", "msg"):
|
for k in ("text", "content", "message", "msg"):
|
||||||
v = msg.get(k)
|
v = msg.get(k)
|
||||||
@@ -480,7 +528,7 @@ def _mirror_to_ai_chat(user_id: str, user_content: str, assistant_content: str):
|
|||||||
# 后台接收线程(每用户独立)
|
# 后台接收线程(每用户独立)
|
||||||
# ============================================================
|
# ============================================================
|
||||||
def _mark_wechat_expired(account_id: int):
|
def _mark_wechat_expired(account_id: int):
|
||||||
"""发送会话失效(ret=-14/-2):标记账号需重扫,并清除失效 token。"""
|
"""发送会话失效(ret=-14/-2):标记账号需重扫,清除失效 token,并记录告警日志。"""
|
||||||
db = None
|
db = None
|
||||||
try:
|
try:
|
||||||
db = SessionLocal()
|
db = SessionLocal()
|
||||||
@@ -488,6 +536,14 @@ def _mark_wechat_expired(account_id: int):
|
|||||||
"UPDATE im_bot_wechat_account SET status='expired', bot_token=NULL, "
|
"UPDATE im_bot_wechat_account SET status='expired', bot_token=NULL, "
|
||||||
"updated_at=:ts WHERE id=:aid"
|
"updated_at=:ts WHERE id=:aid"
|
||||||
), {"aid": account_id, "ts": _beijing_now()})
|
), {"aid": account_id, "ts": _beijing_now()})
|
||||||
|
# Session Guard:记录账号信息,供前台提示用户重扫
|
||||||
|
row = _execute_raw(db, text("SELECT user_id, username FROM im_bot_wechat_account WHERE id=:aid"), {"aid": account_id})
|
||||||
|
_uid = row[0][0] if row else "?"
|
||||||
|
try:
|
||||||
|
with open("/tmp/wechat_session_expired.log", "a", encoding="utf-8") as _f:
|
||||||
|
_f.write(_beijing_now().isoformat() + " SESSION_EXPIRED account_id=%s user_id=%s\n" % (account_id, _uid))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
finally:
|
finally:
|
||||||
@@ -530,6 +586,14 @@ def _wechat_receiver_loop(account_id: int):
|
|||||||
+ " ctx=" + repr(ctx) + " raw=" + json.dumps(m, ensure_ascii=False) + "\n")
|
+ " ctx=" + repr(ctx) + " raw=" + json.dumps(m, ensure_ascii=False) + "\n")
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
# 发送"正在输入"状态(尽力而为,失败静默)
|
||||||
|
try:
|
||||||
|
_bot_id = m.get("to_user_id") or d.get("ilink_bot_id") or ""
|
||||||
|
_ticket = _wechat_get_typing_ticket(baseurl, bot_token, _bot_id, ctx)
|
||||||
|
if _bot_id and _ticket:
|
||||||
|
_wechat_send_typing(baseurl, bot_token, _bot_id, _ticket, 1)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
# 先生成回复(此时历史尚未包含本条,避免重复)
|
# 先生成回复(此时历史尚未包含本条,避免重复)
|
||||||
import logging as _lrx, inspect as _insp
|
import logging as _lrx, inspect as _insp
|
||||||
_lrx.warning(f'[receiver] calling _qihua_reply({repr(content)}, {repr(owner_uid)})')
|
_lrx.warning(f'[receiver] calling _qihua_reply({repr(content)}, {repr(owner_uid)})')
|
||||||
|
|||||||
Reference in new issue
Block a user