101 lines
4.6 KiB
Python
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))
|