Files
tw/core/websocket_auto_quote_flow.py

101 lines
4.6 KiB
Python

import asyncio
import os
from typing import Any
def cancel_auto_quote_task(client, key: str, reason: str = ""):
task = client._auto_quote_tasks.get(key)
if task and not task.done():
task.cancel()
client._activity_log("auto_quote_cancel", key=key, reason=reason or "unknown")
def build_auto_quote_signature(state: Any) -> str:
"""为待报价内容生成稳定签名,用于避免同一批内容反复自动触发。"""
urls = list(getattr(state, "pending_image_urls", []) or [])
reqs = list(getattr(state, "pending_requirements", []) or [])
req_tail = reqs[-6:] if len(reqs) > 6 else reqs
return "||".join(urls) + "##" + "||".join(req_tail)
async def schedule_auto_quote(client, data: dict, *, shop_type_resolver):
"""
智能兜底:客户发图后若长时间不再补充消息,自动触发一次报价,避免会话卡住。
"""
if not client.enable_agent or not client.agent:
return
try:
shop_type = shop_type_resolver(data.get('acc_id', ''), client.to_chinese(data.get('goods_name', '') or ''))
if shop_type != "find_image":
return
cid = data.get('from_id', '')
key = client._customer_key(data)
state = client.agent._get_conversation_state(cid)
if not state or not getattr(state, "pending_image_urls", None):
cancel_auto_quote_task(client, key, reason="no_pending_images")
client._auto_quote_done_sig.pop(key, None)
return
if state.quote_phase not in {"collecting", "waiting_result"}:
return
current_sig = build_auto_quote_signature(state)
if current_sig and client._auto_quote_done_sig.get(key) == current_sig:
client._activity_log(
"auto_quote_skip_duplicate",
key=key,
pending_count=len(state.pending_image_urls),
)
return
try:
idle_seconds = max(8, int(os.getenv("AUTO_QUOTE_IDLE_SECONDS", "18")))
except Exception:
idle_seconds = 18
cancel_auto_quote_task(client, key, reason="reschedule")
async def _delayed_auto_quote(capture_key: str, capture_data: dict, wait_s: int, capture_sig: str):
await asyncio.sleep(wait_s)
async with client._get_customer_lock(capture_key):
capture_cid = capture_data.get('from_id', '')
st = client.agent._get_conversation_state(capture_cid)
if not st or not st.pending_image_urls:
client._auto_quote_done_sig.pop(capture_key, None)
return
# 内容变化时,放弃旧触发(会在新一轮消息后重新调度)。
if build_auto_quote_signature(st) != capture_sig:
return
# 标记本批次已自动触发,避免同内容循环“马上报价”。
client._auto_quote_done_sig[capture_key] = capture_sig
# 直接置为可报价,走内部自动报价入口(不伪造客户语句)。
client.agent._mark_quote_ready(st)
client.agent._sync_pending_quote_state(capture_cid, st)
client._activity_log(
"auto_quote_trigger",
key=capture_key,
pending_count=len(st.pending_image_urls),
wait_s=wait_s,
)
notify_data = dict(capture_data)
notify_data["msg_id"] = "auto_quote_idle_trigger"
notify_data["msg"] = "__AUTO_QUOTE_INTERNAL_TRIGGER__"
notify_msg = client._build_customer_message(notify_data)
response = await client.agent.build_auto_quote_reply(st, notify_msg)
if response.should_reply and response.reply and not response.need_transfer:
await client.send_reply(capture_data, response.reply)
client._activity_log(
"auto_quote_sent",
key=capture_key,
reply=response.reply,
)
task = asyncio.create_task(_delayed_auto_quote(key, dict(data), idle_seconds, current_sig))
client._auto_quote_tasks[key] = task
client._activity_log(
"auto_quote_scheduled",
key=key,
pending_count=len(state.pending_image_urls),
phase=state.quote_phase,
wait_s=idle_seconds,
)
except Exception as e:
client._activity_log("auto_quote_schedule_error", error=str(e), key=client._customer_key(data))