Files
inquiry_robot/inquiry-agent/agent/policy/event_exception.py
T
2026-09-21 18:15:22 +08:00

194 lines
6.0 KiB
Python

"""
事件异常纯规则:等待种类、超时编码、点名对象、卡片接收人。
本文件职责:给 Worker 巡检判定用,不发企微、不写主账。
禁止:在回调线程做 IO;把六态写进本文件;把系统异常混进来。
"""
from __future__ import annotations
from typing import Any
from agent.schema.land_options import canonicalize_land_class
from agent.schema.sea_options import canonicalize_sea_class
WAIT_AIR_QUOTE = "air_quote"
WAIT_PRODUCT_QUOTE = "product_quote"
WAIT_ADOPT = "adopt"
WAIT_DEAL = "deal"
WAIT_LOST_REASON = "lost_reason"
TYPE_AIR = "航线响应超时"
TYPE_PRODUCT = "产品响应超时"
TYPE_SALES = "销售响应超时"
CARD_AIR = "航线响应异常"
CARD_PRODUCT = "产品响应异常"
CARD_SALES = "销售响应异常"
_SALES_CODES = {
"AIR": "air_sales",
"SEA": "sea_sales",
"LAND": "land_sales",
}
_LAND_PRODUCT_CODES = {
"普件(东南亚、中港常用线路/常规车型)": "land_hk_common",
"普件(东南亚、中亚、中欧涉及国内+国外段)": "land_hk_cross",
"普件(东南亚、中亚、中欧涉及境外段清派等特殊要求)": "land_hk_special",
"急件": "land_hk_express",
}
_SALES_WAITS = {WAIT_ADOPT, WAIT_DEAL, WAIT_LOST_REASON}
def timeout_code(*, wait_kind: str, business_line: str, transport_class: str) -> str:
"""
按等待种类 + 运输方式 + 询价分类,对上后台超时规则编码。
对不上返回空串,调用方视为本步不监测。副作用:无。
"""
line = (business_line or "").strip().upper()
kind = (wait_kind or "").strip()
klass = (transport_class or "").strip()
if kind in _SALES_WAITS:
return _SALES_CODES.get(line, "")
if kind == WAIT_AIR_QUOTE:
return "air_route"
if kind == WAIT_PRODUCT_QUOTE:
if line == "SEA":
canon = canonicalize_sea_class(klass) or klass
if canon == "Port to Port":
return "sea_product_ptp"
if canon == "其他":
return "sea_product_other"
return ""
if line == "LAND":
canon = canonicalize_land_class(klass) or klass
return _LAND_PRODUCT_CODES.get(canon, "")
return ""
return ""
def exception_type(wait_kind: str) -> str:
"""工单「事件异常」字段用的类型名。"""
kind = (wait_kind or "").strip()
if kind == WAIT_AIR_QUOTE:
return TYPE_AIR
if kind == WAIT_PRODUCT_QUOTE:
return TYPE_PRODUCT
if kind in _SALES_WAITS:
return TYPE_SALES
return ""
def card_title(wait_kind: str) -> str:
"""询价消息服务卡片标题。"""
kind = (wait_kind or "").strip()
if kind == WAIT_AIR_QUOTE:
return CARD_AIR
if kind == WAIT_PRODUCT_QUOTE:
return CARD_PRODUCT
if kind in _SALES_WAITS:
return CARD_SALES
return ""
def format_timeout_hours(hours: float) -> str:
"""话术里的小时数:整数去小数点,0.25 保持原样。"""
try:
n = float(hours)
except (TypeError, ValueError):
return "0"
if n == int(n):
return str(int(n))
text = f"{n:.4f}".rstrip("0").rstrip(".")
return text or "0"
def pick_mentions(
*,
wait_kind: str,
sales: dict[str, Any] | None,
members: list[dict[str, Any]] | None,
business_line: str,
) -> list[dict[str, Any]]:
"""
本单这次应回而未回、需要被点名的人。
航线只盯群里航线岗;海运/陆运产品盯对应产品岗;销售三类只盯本单销售。
不要询价机器人、不要群里其他人。
"""
kind = (wait_kind or "").strip()
line = (business_line or "").strip().upper()
if kind in _SALES_WAITS:
person = dict(sales or {})
uid = str(person.get("wecomId") or person.get("wecom_id") or "").strip()
if not uid:
return []
person.setdefault("wecomId", uid)
person.setdefault("name", str(person.get("name") or "").strip() or "销售")
return [person]
want = ""
if kind == WAIT_AIR_QUOTE:
want = "air"
elif kind == WAIT_PRODUCT_QUOTE:
if line == "SEA":
want = "sea"
elif line == "LAND":
want = "land"
if not want:
return []
out: list[dict[str, Any]] = []
seen: set[str] = set()
for raw in members or []:
if not isinstance(raw, dict):
continue
role = str(raw.get("roleCode") or raw.get("role_code") or "").strip().lower()
if role != want:
continue
uid = str(raw.get("wecomId") or raw.get("wecom_id") or "").strip()
if not uid or uid in seen:
continue
seen.add(uid)
row = dict(raw)
row.setdefault("wecomId", uid)
row.setdefault("name", str(raw.get("name") or "").strip() or uid)
out.append(row)
return out
def pick_app_targets(people: list[dict[str, Any]] | None) -> list[dict[str, Any]]:
"""勾了「接收事件异常消息提醒」且绑了企微的人。"""
out: list[dict[str, Any]] = []
seen: set[str] = set()
for raw in people or []:
if not isinstance(raw, dict):
continue
flag = raw.get("notifyEventException")
if flag not in (1, True, "1"):
continue
uid = str(raw.get("wecomId") or raw.get("wecom_id") or "").strip()
if not uid or uid in seen:
continue
seen.add(uid)
out.append(raw)
return out
def conversation_of(*, wait_kind: str, collab_chat_id: str) -> str:
"""
告警发在哪个会话:group 或 private。
采用/不采用永远私聊;航线/产品永远群;成交跟进和未成交原因有协同群才群。
"""
kind = (wait_kind or "").strip()
chat = (collab_chat_id or "").strip()
if kind == WAIT_ADOPT:
return "private"
if kind in {WAIT_AIR_QUOTE, WAIT_PRODUCT_QUOTE}:
return "group"
if kind in {WAIT_DEAL, WAIT_LOST_REASON}:
return "group" if chat else "private"
return "private"