fix: retry stalled transfers on follow-up messages
This commit is contained in:
@@ -3,6 +3,7 @@ import asyncio
|
|||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
import json
|
import json
|
||||||
|
from datetime import datetime
|
||||||
from typing import Optional, List, Any, Dict
|
from typing import Optional, List, Any, Dict
|
||||||
from collections import deque
|
from collections import deque
|
||||||
from core.schema import StandardMessage, StandardResponse
|
from core.schema import StandardMessage, StandardResponse
|
||||||
@@ -26,6 +27,8 @@ TRANSFER_COOLDOWN_SEC = 120 # 转接冷却时间(秒)—— 转接后2
|
|||||||
DEBOUNCE_SECONDS = 2.0 # 消息防抖延迟(秒)
|
DEBOUNCE_SECONDS = 2.0 # 消息防抖延迟(秒)
|
||||||
PENDING_TRANSFER_POLL_SECONDS = 30
|
PENDING_TRANSFER_POLL_SECONDS = 30
|
||||||
PENDING_TRANSFER_RETRY_SECONDS = 60
|
PENDING_TRANSFER_RETRY_SECONDS = 60
|
||||||
|
TRANSFER_RETRY_WINDOW_SEC = 300
|
||||||
|
TRANSFER_RETRY_GAP_SEC = 45
|
||||||
|
|
||||||
# 转接后安抚话术池(轮换使用,避免复读)
|
# 转接后安抚话术池(轮换使用,避免复读)
|
||||||
_TRANSFER_CALM_REPLIES = [
|
_TRANSFER_CALM_REPLIES = [
|
||||||
@@ -46,6 +49,8 @@ _OUTBOUND_BLOCK_MARKERS = (
|
|||||||
'[{"name":',
|
'[{"name":',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
_TRANSFER_COMMAND_MARKER = "[转移会话]"
|
||||||
|
|
||||||
# 历史记录格式检测模式(AI 转述历史时容易泄露)
|
# 历史记录格式检测模式(AI 转述历史时容易泄露)
|
||||||
_HISTORY_LEAK_PATTERNS = [
|
_HISTORY_LEAK_PATTERNS = [
|
||||||
r'\[\d{4}-\d{2}-\d{2}[^\]]*\]\s*(客户|客服)[::]', # [2026-03-07 12:00:00] 客户:
|
r'\[\d{4}-\d{2}-\d{2}[^\]]*\]\s*(客户|客服)[::]', # [2026-03-07 12:00:00] 客户:
|
||||||
@@ -80,6 +85,7 @@ class SystemOrchestrator:
|
|||||||
self._pending_messages: Dict[str, List[StandardMessage]] = {}
|
self._pending_messages: Dict[str, List[StandardMessage]] = {}
|
||||||
self._user_locks: Dict[str, asyncio.Lock] = {}
|
self._user_locks: Dict[str, asyncio.Lock] = {}
|
||||||
self._pending_transfer_task: Optional[asyncio.Task] = None
|
self._pending_transfer_task: Optional[asyncio.Task] = None
|
||||||
|
self._last_retry_transfer_time: Dict[str, float] = {}
|
||||||
|
|
||||||
bus.subscribe("MESSAGE_OUTBOUND", self.handle_outbound_event)
|
bus.subscribe("MESSAGE_OUTBOUND", self.handle_outbound_event)
|
||||||
|
|
||||||
@@ -107,6 +113,99 @@ class SystemOrchestrator:
|
|||||||
self._pending_transfer_task = asyncio.create_task(self._process_pending_transfers_loop())
|
self._pending_transfer_task = asyncio.create_task(self._process_pending_transfers_loop())
|
||||||
logger.info("[Orchestrator] 待转接轮询任务已启动")
|
logger.info("[Orchestrator] 待转接轮询任务已启动")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _parse_history_ts(ts: Any) -> Optional[datetime]:
|
||||||
|
text = str(ts or "").strip()
|
||||||
|
if not text:
|
||||||
|
return None
|
||||||
|
for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M:%S.%f"):
|
||||||
|
try:
|
||||||
|
return datetime.strptime(text, fmt)
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _find_stalled_transfer(self, history: List[dict]) -> Optional[dict]:
|
||||||
|
if not history:
|
||||||
|
return None
|
||||||
|
|
||||||
|
last_transfer_idx = -1
|
||||||
|
for idx in range(len(history) - 1, -1, -1):
|
||||||
|
item = history[idx]
|
||||||
|
if item.get("role") == "assistant" and _TRANSFER_COMMAND_MARKER in str(item.get("content") or ""):
|
||||||
|
last_transfer_idx = idx
|
||||||
|
break
|
||||||
|
|
||||||
|
if last_transfer_idx < 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
transfer_item = history[last_transfer_idx]
|
||||||
|
transfer_at = self._parse_history_ts(transfer_item.get("timestamp"))
|
||||||
|
if not transfer_at:
|
||||||
|
return None
|
||||||
|
|
||||||
|
elapsed = time.time() - transfer_at.timestamp()
|
||||||
|
if elapsed < 0 or elapsed > TRANSFER_RETRY_WINDOW_SEC:
|
||||||
|
return None
|
||||||
|
|
||||||
|
after_transfer = history[last_transfer_idx + 1:]
|
||||||
|
if not any(item.get("role") == "user" for item in after_transfer):
|
||||||
|
return None
|
||||||
|
|
||||||
|
for item in after_transfer:
|
||||||
|
if item.get("role") != "assistant":
|
||||||
|
continue
|
||||||
|
content = str(item.get("content") or "")
|
||||||
|
if _TRANSFER_COMMAND_MARKER not in content:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return {
|
||||||
|
"timestamp": transfer_at,
|
||||||
|
"elapsed": elapsed,
|
||||||
|
"content": str(transfer_item.get("content") or ""),
|
||||||
|
}
|
||||||
|
|
||||||
|
async def _retry_stalled_transfer_if_needed(
|
||||||
|
self,
|
||||||
|
session_key: str,
|
||||||
|
user_id: str,
|
||||||
|
platform: str,
|
||||||
|
acc_id: str,
|
||||||
|
acc_type: str,
|
||||||
|
history: List[dict],
|
||||||
|
) -> Optional[StandardResponse]:
|
||||||
|
stalled = self._find_stalled_transfer(history)
|
||||||
|
if not stalled:
|
||||||
|
return None
|
||||||
|
|
||||||
|
last_retry_at = self._last_retry_transfer_time.get(session_key, 0.0)
|
||||||
|
if time.time() - last_retry_at < TRANSFER_RETRY_GAP_SEC:
|
||||||
|
logger.info(
|
||||||
|
f"[Orchestrator] 转接补发冷却中,先不重复补转: user={user_id} acc={acc_id}"
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"[Orchestrator] 检测到疑似转接未接上,准备补发转接: "
|
||||||
|
f"user={user_id} acc={acc_id} elapsed={stalled['elapsed']:.0f}s"
|
||||||
|
)
|
||||||
|
designer_name = await dispatch_service.assign_designer(user_id=user_id)
|
||||||
|
if not designer_name:
|
||||||
|
logger.info(f"[Orchestrator] 补发转接失败,当前仍无可用设计师: user={user_id} acc={acc_id}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
self._last_retry_transfer_time[session_key] = time.time()
|
||||||
|
return StandardResponse(
|
||||||
|
reply_content=f"正在为您转接|[转移会话],{designer_name},无原因",
|
||||||
|
need_transfer=True,
|
||||||
|
metadata={
|
||||||
|
"acc_id": acc_id,
|
||||||
|
"acc_type": acc_type,
|
||||||
|
"transfer_prelude": "我再帮您转一下哈",
|
||||||
|
"retry_transfer": True,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _sanitize_outbound_text(text: str) -> str:
|
def _sanitize_outbound_text(text: str) -> str:
|
||||||
if not text:
|
if not text:
|
||||||
@@ -357,12 +456,28 @@ class SystemOrchestrator:
|
|||||||
if all_image_urls:
|
if all_image_urls:
|
||||||
asyncio.create_task(self._analyze_images_background(session_key, all_image_urls))
|
asyncio.create_task(self._analyze_images_background(session_key, all_image_urls))
|
||||||
|
|
||||||
# C. 冷却检查:转接成功后冷却期内,直接回安抚话术,不调AI
|
history_start = time.time()
|
||||||
|
history = await repo.get_chat_history(user_id, limit=12, acc_id=acc_id)
|
||||||
|
history_elapsed = time.time() - history_start
|
||||||
|
logger.info(f"[计时] user={user_id} 查询历史: {history_elapsed:.2f}s (共{len(history)}条)")
|
||||||
|
ai_history = history[:-1] if history and history[-1].get("content") == db_content else history
|
||||||
|
|
||||||
|
# C. 短时间追问且疑似没真正接上人工:优先补发一次转接
|
||||||
|
std_res = await self._retry_stalled_transfer_if_needed(
|
||||||
|
session_key=session_key,
|
||||||
|
user_id=user_id,
|
||||||
|
platform=platform,
|
||||||
|
acc_id=acc_id,
|
||||||
|
acc_type=acc_type,
|
||||||
|
history=history,
|
||||||
|
)
|
||||||
|
|
||||||
|
# D. 冷却检查:转接成功后冷却期内,直接回安抚话术,不调AI
|
||||||
last_transfer = self._last_transfer_time.get(session_key, 0)
|
last_transfer = self._last_transfer_time.get(session_key, 0)
|
||||||
cooldown_elapsed = time.time() - last_transfer
|
cooldown_elapsed = time.time() - last_transfer
|
||||||
is_in_cooldown = cooldown_elapsed < TRANSFER_COOLDOWN_SEC
|
is_in_cooldown = cooldown_elapsed < TRANSFER_COOLDOWN_SEC
|
||||||
|
|
||||||
if is_in_cooldown:
|
if std_res is None and is_in_cooldown:
|
||||||
idx = self._transfer_calm_idx.get(session_key, 0)
|
idx = self._transfer_calm_idx.get(session_key, 0)
|
||||||
calm_reply = _TRANSFER_CALM_REPLIES[idx % len(_TRANSFER_CALM_REPLIES)]
|
calm_reply = _TRANSFER_CALM_REPLIES[idx % len(_TRANSFER_CALM_REPLIES)]
|
||||||
self._transfer_calm_idx[session_key] = idx + 1
|
self._transfer_calm_idx[session_key] = idx + 1
|
||||||
@@ -371,21 +486,16 @@ class SystemOrchestrator:
|
|||||||
reply_content=calm_reply,
|
reply_content=calm_reply,
|
||||||
metadata={"acc_id": acc_id, "acc_type": acc_type}
|
metadata={"acc_id": acc_id, "acc_type": acc_type}
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
# D. 正常流程:调用AI思考
|
|
||||||
history_start = time.time()
|
|
||||||
history = await repo.get_chat_history(user_id, limit=10, acc_id=acc_id)
|
|
||||||
if history and history[-1].get('content') == db_content: history = history[:-1]
|
|
||||||
history_elapsed = time.time() - history_start
|
|
||||||
logger.info(f"[计时] user={user_id} 查询历史: {history_elapsed:.2f}s (共{len(history)}条)")
|
|
||||||
|
|
||||||
|
if std_res is None:
|
||||||
|
# E. 正常流程:调用AI思考
|
||||||
ai_start = time.time()
|
ai_start = time.time()
|
||||||
std_res = await self.brain.think_and_reply(final_msg, history=history)
|
std_res = await self.brain.think_and_reply(final_msg, history=ai_history)
|
||||||
ai_elapsed = time.time() - ai_start
|
ai_elapsed = time.time() - ai_start
|
||||||
total_elapsed = time.time() - process_start
|
total_elapsed = time.time() - process_start
|
||||||
logger.info(f"[计时] user={user_id} AI思考: {ai_elapsed:.1f}s | 总耗时: {total_elapsed:.1f}s")
|
logger.info(f"[计时] user={user_id} AI思考: {ai_elapsed:.1f}s | 总耗时: {total_elapsed:.1f}s")
|
||||||
|
|
||||||
# E. 发送并记录时间
|
# F. 发送并记录时间
|
||||||
if std_res.should_reply:
|
if std_res.should_reply:
|
||||||
std_res.reply_content = self._sanitize_outbound_text(std_res.reply_content)
|
std_res.reply_content = self._sanitize_outbound_text(std_res.reply_content)
|
||||||
meta = dict(std_res.metadata or {})
|
meta = dict(std_res.metadata or {})
|
||||||
@@ -394,8 +504,9 @@ class SystemOrchestrator:
|
|||||||
|
|
||||||
# 转接场景:先发一句安抚话,再发转接指令
|
# 转接场景:先发一句安抚话,再发转接指令
|
||||||
if "[转移会话]" in std_res.reply_content:
|
if "[转移会话]" in std_res.reply_content:
|
||||||
|
transfer_prelude = str(std_res.metadata.get("transfer_prelude") or "").strip()
|
||||||
greet = StandardResponse(
|
greet = StandardResponse(
|
||||||
reply_content="收到,我叫设计师来看下哈",
|
reply_content=transfer_prelude or "收到,我叫设计师来看下哈",
|
||||||
metadata={"acc_id": acc_id, "acc_type": acc_type}
|
metadata={"acc_id": acc_id, "acc_type": acc_type}
|
||||||
)
|
)
|
||||||
await self.qianniu_adapter.translate_outbound(greet, user_id)
|
await self.qianniu_adapter.translate_outbound(greet, user_id)
|
||||||
|
|||||||
Reference in New Issue
Block a user