194 lines
6.0 KiB
Python
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"
|