import asyncio import time from datetime import datetime def fire_and_forget(client, coro): """后台执行协程,不阻塞接收循环;异常会记录到日志。""" task = asyncio.create_task(coro) def _done(t): if t.cancelled(): return exc = t.exception() if exc: client.logger.exception(f"后台任务异常: {exc}") task.add_done_callback(_done) def prune_seen(seen: dict, now_mono: float, ttl_sec: float = 8.0): if len(seen) <= 2000: return stale = [k for k, t in seen.items() if (now_mono - t) > ttl_sec] for k in stale: seen.pop(k, None) def log_inbound_once(client, data: dict, chat_log_fn): """统一记录入站消息,短窗口去重,避免多分支重复写库。""" try: cid = data.get("from_id", "") if not cid: return msg = client.to_chinese(data.get("msg", "") or "") acc_id = data.get("acc_id", "") mtype = int(data.get("msg_type", 0) or 0) now_mono = time.monotonic() sig = f"{acc_id}|{cid}|{mtype}|{msg}" last = client._inbound_log_seen.get(sig, 0.0) if (now_mono - last) < 2.0: return client._inbound_log_seen[sig] = now_mono prune_seen(client._inbound_log_seen, now_mono, ttl_sec=8.0) chat_log_fn( cid, msg, "in", customer_name=client.to_chinese(data.get("from_name", "") or data.get("cy_name", "")), acc_id=acc_id, platform=data.get("acc_type", ""), msg_type=mtype, ) except Exception: client.logger.debug("入站消息写库失败", exc_info=True) def log_outbound_once(client, original_msg: dict, reply_content: str, chat_log_fn): """统一记录出站消息,短窗口去重,避免重复写库。""" try: cid = original_msg.get("from_id", "") if not cid: return msg = reply_content or "" acc_id = original_msg.get("acc_id", "") now_mono = time.monotonic() sig = f"{acc_id}|{cid}|0|{msg}" last = client._outbound_log_seen.get(sig, 0.0) if (now_mono - last) < 2.0: return client._outbound_log_seen[sig] = now_mono prune_seen(client._outbound_log_seen, now_mono, ttl_sec=8.0) chat_log_fn( cid, msg, "out", customer_name=client.to_chinese(original_msg.get("from_name", "") or original_msg.get("cy_name", "")), acc_id=acc_id, platform=original_msg.get("acc_type", ""), msg_type=0, ) except Exception: client.logger.debug("出站消息写库失败", exc_info=True) def build_customer_message(client, data: dict, customer_message_cls): """把原始消息字典转换为 Agent 输入模型。""" return customer_message_cls( msg_id=data.get("msg_id", ""), acc_id=data.get("acc_id", ""), msg=client.to_chinese(data.get("msg", "")), from_id=data.get("from_id", ""), from_name=client.to_chinese(data.get("from_name", "")), cy_id=data.get("cy_id", ""), acc_type=data.get("acc_type", ""), msg_type=data.get("msg_type", 0), cy_name=client.to_chinese(data.get("cy_name", "")), goods_name=client.to_chinese(data.get("goods_name", "")) if data.get("goods_name") else None, goods_order=client.to_chinese(data.get("goods_order", "")) if data.get("goods_order") else None, ) def touch_customer_last_contact(client, customer_id: str, db): """兜底更新客户最后联系时间。""" if not customer_id: return try: profile = db.get_customer(customer_id) profile.last_contact = datetime.now().isoformat() db.save_customer(profile) except Exception: client.logger.debug("更新客户最后联系时间失败: customer_id=%s", customer_id, exc_info=True) def push_chat_to_wechat_safe(client, *, data: dict, customer_msg: str, reply_msg: str, tag: str, goods_name: str = ""): """异步推送企微聊天日志,失败不影响主流程。""" try: from utils.wechat_chat_log import push_chat_to_wechat asyncio.create_task(push_chat_to_wechat( customer_name=client.to_chinese(data.get("from_name", "") or data.get("cy_name", "")), customer_id=data.get("from_id", ""), acc_id=data.get("acc_id", ""), customer_msg=client.to_chinese(customer_msg or ""), reply_msg=reply_msg or "", goods_name=goods_name or client.to_chinese(data.get("goods_name", "") or ""), )) except Exception: client.logger.debug("推送企微聊天日志失败(%s)", tag, exc_info=True)