""" 成交三态落地:改主账六态、推负责人、收下未成交原因、排协商再问。 本文件职责:私聊/群聊共用。不在回调线程等 LLM。推送只入 outbox。 禁止:把图成功当主账成功;成交失败还去推负责人;未成交推负责人。 """ from __future__ import annotations import logging from typing import Any, Callable, Optional from agent.config import get_settings from agent.policy import inquiry_copy as copy from agent.policy.deal_followup import next_followup_at, pick_leaders logger = logging.getLogger(__name__) EnqueueFn = Callable[..., tuple[bool, str]] def notify_app_ready() -> bool: """询价消息服务已配 AgentId+Secret 才推。空则只成交不推。""" settings = get_settings() return bool( str(getattr(settings, "wecom_notify_agent_id", "") or "").strip() and str(getattr(settings, "wecom_notify_secret", "") or "").strip() ) def _role_of(business_line: str) -> str: line = (business_line or "").strip().upper() if line == "SEA": return "sea" if line == "LAND": return "land" return "air" def _enqueue_default( *, touser: str, content: str, dedupe_key: str, payload: dict[str, Any], ) -> tuple[bool, str]: from agent.channel.queue import get_message_store return get_message_store().enqueue_outbound( touser=touser, content=content, dedupe_key=dedupe_key, payload=payload, ) def finish_deal_outcome( *, ledger: Any, work_order_no: str, outcome: str, facts: dict[str, str] | None, quote: dict[str, Any] | None, business_line: str, sales_name: str = "", enqueue: Optional[EnqueueFn] = None, notify_ready: Optional[bool] = None, ) -> dict[str, Any]: """ 先入主账,再按结果推人或排再问。 返回 ok / phase / notify_summary。phase: 已成交 done;未成交 wait_lost_reason;协商中 wait_deal。 """ no = (work_order_no or "").strip() status = (outcome or "").strip() extra: dict[str, Any] = {} summary = "" if status == "协商中": extra["next_followup_at"] = next_followup_at().strftime("%Y-%m-%d %H:%M:%S") if status == "已成交": staff: list[dict[str, Any]] = [] lister = getattr(ledger, "list_staff_by_role", None) if callable(lister): try: staff = list(lister(role_code=_role_of(business_line)) or []) except Exception: logger.exception("成交匹配负责人失败 wo=%s", no) leaders = pick_leaders(staff, business_line=business_line, facts=facts) ready = notify_app_ready() if notify_ready is None else bool(notify_ready) if not leaders: summary = "无匹配负责人" elif not ready: summary = "通知未发出" else: names = [str(x.get("name") or x.get("wecomId") or "").strip() for x in leaders] summary = "已推:" + "、".join(n for n in names if n) sender = enqueue or _enqueue_default payload = copy.deal_notify_wecom_payload( work_order_no=no, sales_name=sales_name, facts=facts or {}, quote=quote or {}, business_line=business_line, ) text = copy.deal_notify_card( work_order_no=no, sales_name=sales_name, facts=facts or {}, quote=quote or {}, business_line=business_line, ) for row in leaders: uid = str(row.get("wecomId") or "").strip() if not uid: continue try: ok, info = sender( touser=uid, content=text, dedupe_key=f"deal-notify:{no}:{uid}", payload=dict(payload), ) if not ok: logger.warning("成交通知入队失败 wo=%s uid=%s info=%s", no, uid, info) except Exception: logger.exception("成交通知入队异常 wo=%s uid=%s", no, uid) extra["deal_notify_summary"] = summary extra["remark"] = summary writer = getattr(ledger, "transition", None) if not callable(writer): return {"ok": False, "error": "no_transition", "phase": "ledger_fail"} result = writer(work_order_no=no, to_status=status, **extra) or {} if not result.get("ok"): return {"ok": False, "error": result.get("error") or "transition_failed", "phase": "ledger_fail"} from agent.policy.event_exception import WAIT_LOST_REASON, clear_wait, start_wait if status == "未成交": phase = "wait_lost_reason" start_wait(ledger, work_order_no=no, wait_kind=WAIT_LOST_REASON) elif status == "协商中": phase = "wait_deal" clear_wait(ledger, work_order_no=no) else: phase = "done" clear_wait(ledger, work_order_no=no) return { "ok": True, "phase": phase, "notify_summary": summary, "status": status, "next_followup_at": str(extra.get("next_followup_at") or ""), } def capture_lost_reason(*, ledger: Any, work_order_no: str, reason: str) -> dict[str, Any]: """把销售下一句写进未成交原因。""" text = (reason or "").strip() no = (work_order_no or "").strip() if not no or not text: return {"ok": False, "error": "blank"} patcher = getattr(ledger, "patch_lost_reason", None) if not callable(patcher): return {"ok": False, "error": "no_patch"} out = patcher(work_order_no=no, lost_reason=text) or {"ok": False} if out.get("ok"): from agent.policy.event_exception import clear_wait clear_wait(ledger, work_order_no=no) return out