perf: fast-route orchestration and short-reply guard for qingjian
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
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:
@@ -1,12 +1,18 @@
|
||||
import asyncio
|
||||
import json
|
||||
import re
|
||||
import time
|
||||
from collections import defaultdict
|
||||
|
||||
import websockets
|
||||
|
||||
from .callbacks import post_tianwang_callback
|
||||
from .config import AUTO_QUOTE_WAIT_SECONDS, MESSAGE_DEBOUNCE_SECONDS, QINGJIAN_WS_URI
|
||||
from .config import (
|
||||
AUTO_QUOTE_WAIT_SECONDS,
|
||||
MESSAGE_DEBOUNCE_SECONDS,
|
||||
QINGJIAN_WS_URI,
|
||||
SHORT_REPLY_MAX_CHARS,
|
||||
)
|
||||
from .logger import setup_logger
|
||||
from .observability import activity_event, build_trace_id
|
||||
from .orchestrator import Orchestrator
|
||||
@@ -27,7 +33,7 @@ class QingjianClient:
|
||||
self.pending_images: dict[str, list[str]] = defaultdict(list)
|
||||
self.auto_quote_tasks: dict[str, asyncio.Task] = {}
|
||||
self.last_reply_key: dict[str, str] = {}
|
||||
self.recent_outbound: dict[str, tuple[str, float]] = {}
|
||||
self.recent_outbound: list[tuple[str, str, str, float]] = []
|
||||
|
||||
@staticmethod
|
||||
def _customer_key(data: dict) -> str:
|
||||
@@ -57,6 +63,7 @@ class QingjianClient:
|
||||
text = str(text or "").strip()
|
||||
if not text:
|
||||
return
|
||||
text = self._shorten_reply(text)
|
||||
msg = {
|
||||
"msg_id": "",
|
||||
"acc_id": data.get("acc_id", ""),
|
||||
@@ -70,22 +77,47 @@ class QingjianClient:
|
||||
}
|
||||
activity_event(self.logger, "send_reply_attempt", trace_id=trace_id, customer_id=data.get("from_id", "-"), msg=text)
|
||||
await self.send_message(msg)
|
||||
self.recent_outbound[self._customer_key(data)] = (text, time.monotonic())
|
||||
self.recent_outbound.append((str(data.get("acc_id", "")), str(data.get("from_id", "")), text, time.monotonic()))
|
||||
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)
|
||||
|
||||
@staticmethod
|
||||
def _clean_text(text: str) -> str:
|
||||
t = str(text or "").strip()
|
||||
t = re.sub(r"\s+", "", t)
|
||||
return t
|
||||
|
||||
def _shorten_reply(self, text: str) -> str:
|
||||
max_len = max(8, int(SHORT_REPLY_MAX_CHARS))
|
||||
t = str(text or "").strip()
|
||||
if len(t) <= max_len:
|
||||
return t
|
||||
parts = re.split(r"[。!?!?]", t)
|
||||
head = next((p.strip() for p in parts if p and p.strip()), t)
|
||||
if len(head) > max_len:
|
||||
head = head[:max_len].rstrip(",,;;:: ")
|
||||
return head or t[:max_len]
|
||||
|
||||
def _is_outbound_echo(self, data: dict, msg: str) -> bool:
|
||||
"""
|
||||
轻简可能会把我方刚发送文本回推为“收到消息”。
|
||||
对同 customer_key 的“短时间完全相同文本”做回环拦截,避免无限对话。
|
||||
对“短时间完全相同文本”做回环拦截,兼容 acc/from 对调回推,避免无限对话。
|
||||
"""
|
||||
key = self._customer_key(data)
|
||||
last = self.recent_outbound.get(key)
|
||||
if not last:
|
||||
in_acc = str(data.get("acc_id", ""))
|
||||
in_from = str(data.get("from_id", ""))
|
||||
in_msg = self._clean_text(msg)
|
||||
now = time.monotonic()
|
||||
if not in_msg:
|
||||
return False
|
||||
last_msg, ts = last
|
||||
if (time.monotonic() - ts) > 120:
|
||||
return False
|
||||
return str(msg or "").strip() == last_msg
|
||||
for out_acc, out_to, out_msg, ts in reversed(self.recent_outbound):
|
||||
if (now - ts) > 120:
|
||||
break
|
||||
if self._clean_text(out_msg) != in_msg:
|
||||
continue
|
||||
if (out_acc == in_acc and out_to == in_from) or (out_acc == in_from and out_to == in_acc):
|
||||
return True
|
||||
return False
|
||||
|
||||
async def _handle_decision(self, data: dict, merged_msg: str, *, auto_quote: bool = False) -> None:
|
||||
key = self._customer_key(data)
|
||||
|
||||
Reference in New Issue
Block a user