171 lines
5.8 KiB
Python
171 lines
5.8 KiB
Python
"""
|
||
成交三态落地:改主账六态、推负责人、收下未成交原因、排协商再问。
|
||
|
||
本文件职责:私聊/群聊共用。不在回调线程等 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
|