refactor: migrate workflow to v2 core and archive legacy modules
This commit is contained in:
265
legacy/websocket_debounce_flow.py
Normal file
265
legacy/websocket_debounce_flow.py
Normal file
@@ -0,0 +1,265 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import re
|
||||
import secrets
|
||||
|
||||
logger = logging.getLogger("cs_agent")
|
||||
|
||||
|
||||
async def debounce_agent_reply(client, data: dict):
|
||||
"""
|
||||
消息防抖:同一客户在 _DEBOUNCE_SECONDS 内的连续消息合并后再处理。
|
||||
订单通知、付款相关消息不走防抖,立即处理。
|
||||
"""
|
||||
msg_body = data.get("msg", "")
|
||||
key = f"{data.get('acc_id','')}:{data.get('from_id','')}"
|
||||
client._cancel_auto_quote_task(key, reason="new_inbound")
|
||||
|
||||
# 以下情况跳过防抖,立即处理(后台执行,不阻塞接收循环)
|
||||
immediate_keywords = ["买家已付款", "已付款", "[系统订单信息]"]
|
||||
if any(kw in msg_body for kw in immediate_keywords):
|
||||
client._activity_log(
|
||||
"debounce_bypass_immediate",
|
||||
acc_id=data.get("acc_id", ""),
|
||||
customer_id=data.get("from_id", ""),
|
||||
reason="payment_or_order",
|
||||
msg=msg_body,
|
||||
)
|
||||
client._fire_and_forget(client._agent_reply_serialized(data))
|
||||
return
|
||||
|
||||
# 积攒消息
|
||||
if key not in client._pending_msgs:
|
||||
client._pending_msgs[key] = []
|
||||
client._pending_msgs[key].append(msg_body)
|
||||
client._activity_log(
|
||||
"debounce_enqueue",
|
||||
key=key,
|
||||
queue_size=len(client._pending_msgs[key]),
|
||||
msg=msg_body,
|
||||
)
|
||||
|
||||
# 取消上一个等待任务(如果有)
|
||||
old_task = client._debounce_tasks.get(key)
|
||||
if old_task and not old_task.done():
|
||||
old_task.cancel()
|
||||
|
||||
debounce_seconds = pick_debounce_seconds(client, data, msg_body)
|
||||
|
||||
# 创建新的延迟处理任务
|
||||
async def _delayed(capture_key, capture_data, wait_s: float):
|
||||
await asyncio.sleep(wait_s)
|
||||
msgs = client._pending_msgs.pop(capture_key, [])
|
||||
if not msgs:
|
||||
return
|
||||
if len(msgs) == 1:
|
||||
merged_msg = msgs[0]
|
||||
else:
|
||||
merged_msg = "、".join(m for m in msgs if m.strip())
|
||||
logger.info(f"[{client.get_time()}] 防抖合并 {len(msgs)} 条消息: {merged_msg[:60]}")
|
||||
client._activity_log(
|
||||
"debounce_flush",
|
||||
key=capture_key,
|
||||
merged_count=len(msgs),
|
||||
merged_msg=merged_msg,
|
||||
)
|
||||
merged_data = dict(capture_data)
|
||||
merged_data["msg"] = merged_msg
|
||||
await client._agent_reply_serialized(merged_data)
|
||||
|
||||
task = asyncio.create_task(_delayed(key, data, debounce_seconds))
|
||||
client._debounce_tasks[key] = task
|
||||
|
||||
|
||||
def rand_between(low: float, high: float) -> float:
|
||||
if high <= low:
|
||||
return float(low)
|
||||
# 使用 secrets 增强随机性,避免固定周期导致机械感
|
||||
span = high - low
|
||||
return round(low + span * (secrets.randbelow(1000) / 1000.0), 2)
|
||||
|
||||
|
||||
def guess_intent_for_debounce(client, msg: str) -> str:
|
||||
text = (msg or "").strip()
|
||||
if not text:
|
||||
return "unknown"
|
||||
if msg_has_image_url(text):
|
||||
return "image"
|
||||
try:
|
||||
from utils.intent_analyzer import detect_intent
|
||||
|
||||
decision = detect_intent(text)
|
||||
intent = decision.intent
|
||||
if intent:
|
||||
client._activity_log(
|
||||
"debounce_intent_detected",
|
||||
intent=intent,
|
||||
source=decision.source,
|
||||
score=round(float(decision.score or 0.0), 4),
|
||||
msg=text[:120],
|
||||
)
|
||||
except Exception:
|
||||
intent = ""
|
||||
if intent:
|
||||
return intent
|
||||
lower = text.lower()
|
||||
if any(k in lower for k in ["报价", "多少钱", "价格", "贵", "优惠", "收费", "怎么收费", "咋收费"]):
|
||||
return "询价"
|
||||
if any(k in lower for k in ["做一下", "改一下", "需求", "门头", "上面的字", "处理"]):
|
||||
return "修改"
|
||||
if any(k in lower for k in ["在吗", "你好", "有人"]):
|
||||
return "打招呼"
|
||||
return "unknown"
|
||||
|
||||
|
||||
def looks_like_requirement_text(msg: str) -> bool:
|
||||
text = (msg or "").strip().lower()
|
||||
if not text:
|
||||
return False
|
||||
req_kw = (
|
||||
"做一下",
|
||||
"改一下",
|
||||
"处理一下",
|
||||
"这个字",
|
||||
"上面的字",
|
||||
"门头",
|
||||
"去背景",
|
||||
"抠图",
|
||||
"换色",
|
||||
"调色",
|
||||
"清晰",
|
||||
"高清",
|
||||
"尺寸",
|
||||
"比例",
|
||||
"横版",
|
||||
"竖版",
|
||||
"排版",
|
||||
"改字",
|
||||
"按这个做",
|
||||
"照这个做",
|
||||
"就这张",
|
||||
"看看做",
|
||||
"弄一下",
|
||||
)
|
||||
return any(k in text for k in req_kw)
|
||||
|
||||
|
||||
def pick_debounce_seconds(client, data: dict, msg: str) -> float:
|
||||
"""意图驱动防抖:不同意图不同等待区间,并引入轻微随机。"""
|
||||
base = max(1.0, float(client._DEBOUNCE_SECONDS))
|
||||
if not client._adaptive_debounce_enabled:
|
||||
return base
|
||||
|
||||
intent = guess_intent_for_debounce(client, msg)
|
||||
is_req = looks_like_requirement_text(msg)
|
||||
has_img = msg_has_image_url(msg)
|
||||
|
||||
# 区间策略:越明确、越短消息,等待越短;需求描述类稍长
|
||||
if intent == "打招呼":
|
||||
low, high = 1.0, min(3.0, base)
|
||||
elif intent in ("询价", "砍价"):
|
||||
# 询价先略等一会,给客户补发图片/需求的窗口,减少机械两连回
|
||||
low, high = 4.0, min(7.0, max(base, 7.0))
|
||||
elif intent == "image":
|
||||
# 文本里直接贴图链接:短等合并上下文,避免和上一条询价并发
|
||||
low, high = 2.2, 4.2
|
||||
elif intent in ("修改", "批量"):
|
||||
low, high = max(3.0, base * 0.65), min(18.0, base + 2.0)
|
||||
elif intent == "转接":
|
||||
low, high = 1.0, 2.5
|
||||
else:
|
||||
low, high = max(2.0, base * 0.5), base
|
||||
|
||||
# 发图后的需求描述,优先“多等一点”收集完整需求,减少半句回复
|
||||
# 约束到 12-14s,避免等待过长。
|
||||
if is_req and not has_img:
|
||||
low = max(low, 12.0)
|
||||
high = min(14.0, max(high, 12.6))
|
||||
|
||||
# 短句更快,长句稍慢,避免把连续半句拆开
|
||||
text_len = len((msg or "").strip())
|
||||
if text_len <= 4:
|
||||
high = min(high, max(low + 0.2, 2.5))
|
||||
elif text_len >= 18:
|
||||
low = min(high, low + 0.6)
|
||||
|
||||
wait_s = rand_between(low, high)
|
||||
logger.info(f"防抖等待 {wait_s}s | intent={intent} | len={text_len}")
|
||||
return wait_s
|
||||
|
||||
|
||||
def msg_has_image_url(msg: str) -> bool:
|
||||
"""判断文本消息里是否包含图片URL(客户粘贴了图片链接,可能带前缀文字如 有吗#*#https://...)"""
|
||||
if not msg:
|
||||
return False
|
||||
lower = msg.lower()
|
||||
image_exts = (".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp")
|
||||
image_hosts = ("alicdn.com", "imgextra", "taobao.com", "jd.com", "pinduoduo.com")
|
||||
if "http://" in lower or "https://" in lower:
|
||||
if any(ext in lower for ext in image_exts) or any(h in lower for h in image_hosts):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def msg_refers_images(msg: str) -> bool:
|
||||
"""判断文本是否指代之前的图片(图一/图二/这张/那张/上面那张等)"""
|
||||
if not msg:
|
||||
return False
|
||||
refs = (
|
||||
"图一",
|
||||
"图二",
|
||||
"第一张",
|
||||
"第二张",
|
||||
"这张",
|
||||
"那张",
|
||||
"这图",
|
||||
"那个图",
|
||||
"这个",
|
||||
"这个呢",
|
||||
"上面那张",
|
||||
"下面那张",
|
||||
"刚才那张",
|
||||
"上一张",
|
||||
"下一张",
|
||||
)
|
||||
return any(r in msg for r in refs)
|
||||
|
||||
|
||||
def extract_image_urls(msg: str) -> list:
|
||||
if not msg:
|
||||
return []
|
||||
parts = [p.strip() for p in msg.split("#*#") if p.strip()]
|
||||
urls = []
|
||||
for p in parts:
|
||||
if p.startswith("http://") or p.startswith("https://"):
|
||||
urls.append(p)
|
||||
if not urls and ("http://" in msg or "https://" in msg):
|
||||
tokens = re.findall(r"(https?://\S+)", msg)
|
||||
for t in tokens:
|
||||
if any(ext in t.lower() for ext in [".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp"]):
|
||||
urls.append(t)
|
||||
return urls[:8]
|
||||
|
||||
|
||||
def collect_recent_image_urls(client, customer_id: str, acc_id: str, max_count: int = 6) -> list:
|
||||
"""从最近对话中回溯收集图片URL(优先买家消息),用于慢发或引用图片的场景"""
|
||||
urls, seen = [], set()
|
||||
try:
|
||||
from db.chat_log_db import get_recent_conversation
|
||||
|
||||
recent = get_recent_conversation(customer_id=customer_id, acc_id=acc_id, limit=20)
|
||||
# 从最近到更早遍历,收集买家(in)消息中的图片链接
|
||||
for item in reversed(recent):
|
||||
if item.get("direction") != "in":
|
||||
continue
|
||||
message = item.get("message") or ""
|
||||
found = extract_image_urls(message)
|
||||
for u in found:
|
||||
if u not in seen:
|
||||
seen.add(u)
|
||||
urls.append(u)
|
||||
if len(urls) >= max_count:
|
||||
return urls
|
||||
except Exception:
|
||||
logger.debug("收集近期图片URL失败", exc_info=True)
|
||||
return urls
|
||||
Reference in New Issue
Block a user