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))