feat: add email notification channel for chat replies
Some checks failed
Pre-commit / run (ubuntu-latest) (push) Has been cancelled
Deploy Sphinx documentation to Pages / build_en (ubuntu-latest, 3.10) (push) Has been cancelled
Deploy Sphinx documentation to Pages / build_zh (ubuntu-latest, 3.10) (push) Has been cancelled
Python Unittest Coverage / test (macos-15, 3.10) (push) Has been cancelled
Python Unittest Coverage / test (macos-15, 3.11) (push) Has been cancelled
Python Unittest Coverage / test (macos-15, 3.12) (push) Has been cancelled
Python Unittest Coverage / test (ubuntu-latest, 3.10) (push) Has been cancelled
Python Unittest Coverage / test (ubuntu-latest, 3.11) (push) Has been cancelled
Python Unittest Coverage / test (ubuntu-latest, 3.12) (push) Has been cancelled
Python Unittest Coverage / test (windows-latest, 3.10) (push) Has been cancelled
Python Unittest Coverage / test (windows-latest, 3.11) (push) Has been cancelled
Python Unittest Coverage / test (windows-latest, 3.12) (push) Has been cancelled

This commit is contained in:
2026-03-03 14:22:56 +08:00
parent 484f1f6be4
commit 31c74e661e
3 changed files with 171 additions and 13 deletions

View File

@@ -26,6 +26,8 @@ from .rules import extract_image_urls, prefilter_message
from .runtime_switch import is_listen_only
from .store import ConversationStore
from .transfer_flow import transfer_to_human_flow
from .email_push import push_chat_to_email
from .wechat_push import push_chat_to_wechat
class QingjianClient:
@@ -144,15 +146,15 @@ class QingjianClient:
await self.websocket.send(json.dumps(message, ensure_ascii=False))
self.logger.info("[发送] %s", message.get("msg", ""))
async def send_reply(self, data: dict, text: str, trace_id: str = "-", turn_version: int | None = None) -> None:
async def send_reply(self, data: dict, text: str, trace_id: str = "-", turn_version: int | None = None) -> bool:
text = str(text or "").strip()
if not text:
return
return False
text = self._shorten_reply(text)
key = self._customer_key(data)
if turn_version is not None and self.turn_versions.get(key, 0) != turn_version:
activity_event(self.logger, "send_reply_skipped", trace_id=trace_id, customer_id=data.get("from_id", "-"), reason="stale_turn")
return
return False
msg = {
"msg_id": "",
"acc_id": data.get("acc_id", ""),
@@ -185,6 +187,47 @@ class QingjianClient:
if len(self.recent_outbound) > 200:
self.recent_outbound = self.recent_outbound[-200:]
activity_event(self.logger, "send_reply_success", trace_id=trace_id, customer_id=data.get("from_id", "-"), msg=text)
return True
async def _push_wechat_pair(self, data: dict, customer_msg: str, reply_msg: str, recent_dialogue: list[dict] | None = None) -> None:
try:
ok, reason = await push_chat_to_wechat(
customer_name=str(data.get("from_name", "") or data.get("cy_name", "") or ""),
customer_id=str(data.get("from_id", "") or ""),
acc_id=str(data.get("acc_id", "") or ""),
customer_msg=str(customer_msg or ""),
reply_msg=str(reply_msg or ""),
goods_name=str(data.get("goods_name", "") or ""),
recent_dialogue=recent_dialogue or [],
)
activity_event(
self.logger,
"wechat_push",
customer_id=data.get("from_id", "-"),
result="ok" if ok else "skip",
reason=reason,
)
except Exception as e:
activity_event(self.logger, "wechat_push", customer_id=data.get("from_id", "-"), result="error", reason=str(e))
try:
ok, reason = await push_chat_to_email(
customer_name=str(data.get("from_name", "") or data.get("cy_name", "") or ""),
customer_id=str(data.get("from_id", "") or ""),
acc_id=str(data.get("acc_id", "") or ""),
customer_msg=str(customer_msg or ""),
reply_msg=str(reply_msg or ""),
goods_name=str(data.get("goods_name", "") or ""),
recent_dialogue=recent_dialogue or [],
)
activity_event(
self.logger,
"email_push",
customer_id=data.get("from_id", "-"),
result="ok" if ok else "skip",
reason=reason,
)
except Exception as e:
activity_event(self.logger, "email_push", customer_id=data.get("from_id", "-"), result="error", reason=str(e))
async def send_image(self, data: dict, image_url: str, trace_id: str = "-", turn_version: int | None = None) -> None:
image_url = str(image_url or "").strip()
@@ -387,8 +430,10 @@ class QingjianClient:
ok_transfer, reason = await transfer_to_human_flow(self, data, transfer_msg=text, trace_id=trace_id)
if not ok_transfer:
self.logger.error("[转人工] 指令失败: %s", reason)
await self.send_reply(data, text, trace_id=trace_id, turn_version=turn_version)
self.last_reply_key[key] = text
sent = await self.send_reply(data, text, trace_id=trace_id, turn_version=turn_version)
if sent:
self.last_reply_key[key] = text
await self._push_wechat_pair(data, merged_msg, text, recent_dialogue=recent_dialogue[-12:])
await post_tianwang_callback("message_processed", data, extra={"trace_id": trace_id, "route": route, "action": "transfer", "reply": text})
return
@@ -442,8 +487,10 @@ class QingjianClient:
ok_transfer, reason = await transfer_to_human_flow(self, data, transfer_msg=tmsg, trace_id=trace_id)
if not ok_transfer:
self.logger.error("[转人工] 指令失败: %s", reason)
await self.send_reply(data, tmsg, trace_id=trace_id, turn_version=turn_version)
self.last_reply_key[key] = tmsg
sent = await self.send_reply(data, tmsg, trace_id=trace_id, turn_version=turn_version)
if sent:
self.last_reply_key[key] = tmsg
await self._push_wechat_pair(data, merged_msg, tmsg, recent_dialogue=recent_dialogue[-12:])
await post_tianwang_callback(
"message_processed",
data,
@@ -457,8 +504,10 @@ class QingjianClient:
if self._is_invalid_ai_reply(text):
text = self._fallback_reply("quote")
if self.last_reply_key.get(key) != text:
await self.send_reply(data, text, trace_id=trace_id, turn_version=turn_version)
self.last_reply_key[key] = text
sent = await self.send_reply(data, text, trace_id=trace_id, turn_version=turn_version)
if sent:
self.last_reply_key[key] = text
await self._push_wechat_pair(data, merged_msg, text, recent_dialogue=recent_dialogue[-12:])
await post_tianwang_callback("message_processed", data, extra={"trace_id": trace_id, "route": route, "action": "quote", "reply": text})
return
@@ -470,8 +519,10 @@ class QingjianClient:
if self._is_invalid_ai_reply(text):
text = self._fallback_reply("reply")
if self.last_reply_key.get(key) != text:
await self.send_reply(data, text, trace_id=trace_id, turn_version=turn_version)
self.last_reply_key[key] = text
sent = await self.send_reply(data, text, trace_id=trace_id, turn_version=turn_version)
if sent:
self.last_reply_key[key] = text
await self._push_wechat_pair(data, merged_msg, text, recent_dialogue=recent_dialogue[-12:])
await post_tianwang_callback("message_processed", data, extra={"trace_id": trace_id, "route": route, "action": "reply", "reply": text})
@@ -632,8 +683,10 @@ class QingjianClient:
# 硬编码:每个客户首条消息先快速回复“在的”
if key not in self.first_msg_replied:
await self.send_reply(patched, "在的")
self.last_reply_key[key] = "在的"
sent = await self.send_reply(patched, "在的")
if sent:
self.last_reply_key[key] = "在的"
await self._push_wechat_pair(patched, patched["msg"], "在的", recent_dialogue=self.recent_dialogue.get(key, [])[-8:])
self.first_msg_replied.add(key)
if msg_type == 1: