看板平均响应按协同分发统计;查价失败可从主账恢复;报价模板匹配与填表更稳,后台改名运营后台。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -118,15 +118,28 @@ class HttpLedger:
|
||||
facts: dict[str, str],
|
||||
business_line: str = "",
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
打主账查 TMS。查价可能慢于默认 10s:单独放宽,仍禁止无限等。
|
||||
|
||||
若仍超时,上层 query_tms_resilient 会回读主账:已 persistHit 有价则按成功续办。
|
||||
"""
|
||||
body: dict[str, Any] = {"work_order_no": work_order_no, "facts": facts}
|
||||
line = (business_line or str((facts or {}).get("业务线") or "")).strip()
|
||||
if line:
|
||||
body["businessLine"] = line
|
||||
out = self._post(
|
||||
"/inquiry/agent/tms/query",
|
||||
body,
|
||||
idem=f"tms:{work_order_no}",
|
||||
)
|
||||
# TMS 出站 + persistHit 常超过默认 10s,超时会被误判成接口异常。
|
||||
prev_timeout = self._client.timeout_seconds
|
||||
self._client.timeout_seconds = max(float(prev_timeout or 10), 45.0)
|
||||
self._client.close()
|
||||
try:
|
||||
out = self._post(
|
||||
"/inquiry/agent/tms/query",
|
||||
body,
|
||||
idem=f"tms:{work_order_no}",
|
||||
)
|
||||
finally:
|
||||
self._client.timeout_seconds = prev_timeout
|
||||
self._client.close()
|
||||
if "classification" not in out and not out.get("ok"):
|
||||
return {
|
||||
"ok": False,
|
||||
|
||||
@@ -1212,6 +1212,7 @@ class AirTextInquiryFlow:
|
||||
from agent.policy.system_exception import (
|
||||
confirm_tms_quote_if_tech,
|
||||
query_tms_resilient,
|
||||
recover_tms_query_from_ledger,
|
||||
)
|
||||
|
||||
tms = query_tms_resilient(self._ledger, work_order_no=no, facts=facts)
|
||||
@@ -1238,12 +1239,17 @@ class AirTextInquiryFlow:
|
||||
):
|
||||
self.drop_current_bookmark(sender_id)
|
||||
return "system_exception"
|
||||
if tms.get("classification") == "TMS_QUERY_INCOMPLETE":
|
||||
logger.warning("tms.query 未出站 wo=%s class=%s", no, tms.get("classification"))
|
||||
reply("TMS 查询失败,请稍后重试。工单已创建:" + no)
|
||||
sess.phase = "tms_tech"
|
||||
self._save(sess)
|
||||
return "tms_tech"
|
||||
# 主账已落价但回包超时:按成功续办,禁止再报接口异常
|
||||
recovered = recover_tms_query_from_ledger(self._ledger, work_order_no=no)
|
||||
if recovered is not None:
|
||||
tms = recovered
|
||||
else:
|
||||
if tms.get("classification") == "TMS_QUERY_INCOMPLETE":
|
||||
logger.warning("tms.query 未出站 wo=%s class=%s", no, tms.get("classification"))
|
||||
reply("TMS 查询失败,请稍后重试。工单已创建:" + no)
|
||||
sess.phase = "tms_tech"
|
||||
self._save(sess)
|
||||
return "tms_tech"
|
||||
if not tms.get("has_price"):
|
||||
# ok=true 才是 TMS 已查过、确认没价
|
||||
self._emit_tms_miss(reply, no)
|
||||
|
||||
@@ -390,7 +390,10 @@ class LandTextInquiryFlow(SeaTextInquiryFlow):
|
||||
_ = retrying
|
||||
no = (sess.work_order_no or "").strip()
|
||||
if not tms.get("ok"):
|
||||
from agent.policy.system_exception import confirm_tms_quote_if_tech
|
||||
from agent.policy.system_exception import (
|
||||
confirm_tms_quote_if_tech,
|
||||
recover_tms_query_from_ledger,
|
||||
)
|
||||
|
||||
if confirm_tms_quote_if_tech(
|
||||
ledger=self._ledger,
|
||||
@@ -402,11 +405,15 @@ class LandTextInquiryFlow(SeaTextInquiryFlow):
|
||||
):
|
||||
self.drop_current_bookmark(sess.sender_id)
|
||||
return "system_exception"
|
||||
reply("TMS 查询失败,请稍后重试。工单已创建:" + no)
|
||||
sess.phase = "tms_tech"
|
||||
self._save(sess)
|
||||
self._park_tms_retry(sess)
|
||||
return "tms_tech"
|
||||
recovered = recover_tms_query_from_ledger(self._ledger, work_order_no=no)
|
||||
if recovered is not None:
|
||||
tms = recovered
|
||||
else:
|
||||
reply("TMS 查询失败,请稍后重试。工单已创建:" + no)
|
||||
sess.phase = "tms_tech"
|
||||
self._save(sess)
|
||||
self._park_tms_retry(sess)
|
||||
return "tms_tech"
|
||||
self._clear_parked_confirm(sess.sender_id)
|
||||
if not tms.get("has_price"):
|
||||
self._emit_tms_miss(reply, no)
|
||||
|
||||
@@ -416,6 +416,7 @@ class SeaTextInquiryFlow(AirTextInquiryFlow):
|
||||
from agent.policy.system_exception import (
|
||||
confirm_tms_quote_if_tech,
|
||||
query_tms_resilient,
|
||||
recover_tms_query_from_ledger,
|
||||
)
|
||||
|
||||
tms = query_tms_resilient(
|
||||
@@ -443,12 +444,16 @@ class SeaTextInquiryFlow(AirTextInquiryFlow):
|
||||
):
|
||||
self.drop_current_bookmark(sender_id)
|
||||
return "system_exception"
|
||||
if tms.get("classification") == "TMS_QUERY_INCOMPLETE":
|
||||
logger.warning("tms.query 未出站 wo=%s class=%s", no, tms.get("classification"))
|
||||
reply("TMS 查询失败,请稍后重试。工单已创建:" + no)
|
||||
sess.phase = "tms_tech"
|
||||
self._save(sess)
|
||||
return "tms_tech"
|
||||
recovered = recover_tms_query_from_ledger(self._ledger, work_order_no=no)
|
||||
if recovered is not None:
|
||||
tms = recovered
|
||||
else:
|
||||
if tms.get("classification") == "TMS_QUERY_INCOMPLETE":
|
||||
logger.warning("tms.query 未出站 wo=%s class=%s", no, tms.get("classification"))
|
||||
reply("TMS 查询失败,请稍后重试。工单已创建:" + no)
|
||||
sess.phase = "tms_tech"
|
||||
self._save(sess)
|
||||
return "tms_tech"
|
||||
if not tms.get("has_price"):
|
||||
self._emit_tms_miss(reply, no)
|
||||
sess.phase = "tms_miss"
|
||||
|
||||
@@ -310,8 +310,85 @@ def reply_paused_ticket(*, ticket: Any, reply: ReplyFn) -> None:
|
||||
reply(copy.system_exception_fallback(kind, names=names))
|
||||
|
||||
|
||||
def _quote_has_usable_total(quote: dict[str, Any] | None) -> bool:
|
||||
"""报价包是否已有可用于出卡的总价。"""
|
||||
src = dict(quote or {})
|
||||
total = str(src.get("total") or src.get("salePrice") or src.get("total_amount") or "").strip()
|
||||
return bool(total and total != "-")
|
||||
|
||||
|
||||
def recover_tms_query_from_ledger(ledger: Any, *, work_order_no: str) -> dict[str, Any] | None:
|
||||
"""
|
||||
查价超时/失败后回读主账:若已 persistHit 落成已报价+销售价,按查价成功续办。
|
||||
|
||||
产品口径:主账已经查到价,只是智能体等回包超时,不能标 TMS异常、不能给销售发接口异常,
|
||||
应直接推当前工单报价。无价才走真正的系统异常。
|
||||
"""
|
||||
no = (work_order_no or "").strip()
|
||||
if not no or ledger is None:
|
||||
return None
|
||||
getter = getattr(ledger, "get_for_agent", None)
|
||||
snap: Any = None
|
||||
if callable(getter):
|
||||
try:
|
||||
snap = getter(work_order_no=no, sender_id="")
|
||||
except TypeError:
|
||||
try:
|
||||
snap = getter(work_order_no=no)
|
||||
except Exception: # noqa: BLE001
|
||||
snap = None
|
||||
except Exception: # noqa: BLE001
|
||||
snap = None
|
||||
if not isinstance(snap, dict) or not snap:
|
||||
getter2 = getattr(ledger, "get_ticket", None)
|
||||
if callable(getter2):
|
||||
try:
|
||||
snap = getter2(work_order_no=no)
|
||||
except Exception: # noqa: BLE001
|
||||
snap = None
|
||||
if not isinstance(snap, dict) or not snap:
|
||||
return None
|
||||
status = str(snap.get("status") or "").strip()
|
||||
tms_hit = snap.get("tms_hit") in (1, "1", True, "true")
|
||||
sale = str(snap.get("sale_price") or snap.get("salePrice") or "").strip()
|
||||
quote = dict(snap.get("quote") or {})
|
||||
tms_quote = dict(snap.get("tms_quote") or snap.get("tmsQuote") or {})
|
||||
if not _quote_has_usable_total(quote) and _quote_has_usable_total(tms_quote):
|
||||
quote = dict(tms_quote)
|
||||
if not _quote_has_usable_total(quote) and sale and sale != "-":
|
||||
quote = dict(quote)
|
||||
quote["total"] = sale
|
||||
quote.setdefault("source", "TMS")
|
||||
quote.setdefault("source_label", "TMS标准报价")
|
||||
priced = status == "已报价" and (
|
||||
tms_hit or (sale and sale != "-") or _quote_has_usable_total(quote)
|
||||
)
|
||||
if not priced:
|
||||
return None
|
||||
if not _quote_has_usable_total(quote):
|
||||
return None
|
||||
logger.warning(
|
||||
"tms.query 超时/失败但主账已有价,按成功续办 wo=%s total=%s",
|
||||
no,
|
||||
quote.get("total"),
|
||||
)
|
||||
return {
|
||||
"ok": True,
|
||||
"has_price": True,
|
||||
"classification": "HAS_PRICE",
|
||||
"quote": quote,
|
||||
"recovered": True,
|
||||
"source": "LEDGER_AFTER_TIMEOUT",
|
||||
}
|
||||
|
||||
|
||||
def query_tms_resilient(ledger: Any, **kwargs: Any) -> dict[str, Any]:
|
||||
"""TMS 查价:最多 3 次,ok=true(含无价)即成功。"""
|
||||
"""
|
||||
TMS 查价:最多 3 次,ok=true(含无价)即成功。
|
||||
|
||||
三次仍失败时,再读主账:若查价过程中已落库有价(常见于智能体超时、主账已 persistHit),
|
||||
按成功回包返回,避免误标 TMS异常。
|
||||
"""
|
||||
getter = getattr(ledger, "query_tms", None)
|
||||
if not callable(getter):
|
||||
return {"ok": False, "classification": "TMS_TECH_FAILURE", "has_price": False, "quote": {}}
|
||||
@@ -319,7 +396,14 @@ def query_tms_resilient(ledger: Any, **kwargs: Any) -> dict[str, Any]:
|
||||
lambda: getter(**kwargs),
|
||||
is_ok=lambda x: bool((x or {}).get("ok")),
|
||||
)
|
||||
return out if isinstance(out, dict) else {"ok": False, "classification": "TMS_TECH_FAILURE"}
|
||||
result = out if isinstance(out, dict) else {"ok": False, "classification": "TMS_TECH_FAILURE"}
|
||||
if result.get("ok"):
|
||||
return result
|
||||
no = str(kwargs.get("work_order_no") or "").strip()
|
||||
recovered = recover_tms_query_from_ledger(ledger, work_order_no=no) if no else None
|
||||
if recovered is not None:
|
||||
return recovered
|
||||
return result
|
||||
|
||||
|
||||
def confirm_tms_quote_if_tech(
|
||||
@@ -332,9 +416,20 @@ def confirm_tms_quote_if_tech(
|
||||
ticket_status: str = "询价中",
|
||||
facts: dict[str, Any] | None = None,
|
||||
) -> bool:
|
||||
"""技术失败则标 TMS异常并回话告警。返回是否已按系统异常处理。"""
|
||||
"""
|
||||
技术失败则标 TMS异常并回话告警。返回是否已按系统异常处理。
|
||||
|
||||
主账其实已有价时不再标异常(兜底:调用方应优先走 query_tms_resilient 恢复)。
|
||||
"""
|
||||
if not is_tms_tech_failure(tms):
|
||||
return False
|
||||
# 最后一道闸:主账其实已有价时禁止再打 TMS异常(正常应已在 query_tms_resilient 恢复)。
|
||||
if recover_tms_query_from_ledger(ledger, work_order_no=work_order_no) is not None:
|
||||
logger.warning(
|
||||
"confirm_tms_quote_if_tech 跳过标异常:主账已有价 wo=%s",
|
||||
work_order_no,
|
||||
)
|
||||
return False
|
||||
err = str(tms.get("classification") or tms.get("error") or "TMS_TECH_FAILURE").strip()
|
||||
confirm_system_exception(
|
||||
event=SystemExceptionEvent(
|
||||
|
||||
@@ -178,7 +178,8 @@ _GENERIC_KEYWORDS = frozenset(
|
||||
_LAND_KEYWORD_ALIASES = {
|
||||
"国内拼车": ("国内运输拼车", "拼车"),
|
||||
"国内零担": ("国内运输拼车", "国内长途/零担", "零担"),
|
||||
"国内整车": ("国内运输整车", "整车"),
|
||||
# 「整车」不能当国内整车别名:跨境整车 / 中港整车都含这两个字。
|
||||
"国内整车": ("国内运输整车",),
|
||||
"跨境整车": ("跨境整车",),
|
||||
"跨境集拼": ("跨境集拼",),
|
||||
"跨境零担": ("跨境集拼",),
|
||||
@@ -198,6 +199,19 @@ def _keyword_needles(kw: str) -> list[str]:
|
||||
return out
|
||||
|
||||
|
||||
def _needle_in(text: str, needle: str) -> bool:
|
||||
"""
|
||||
陆运针:普通包含。单独「整车」不命中跨境/中港,避免国内整车模板抢走跨境单。
|
||||
"""
|
||||
hay = (text or "").lower()
|
||||
n = (needle or "").strip().lower()
|
||||
if not hay or not n or n not in hay:
|
||||
return False
|
||||
if n == "整车" and ("跨境" in hay or "中港" in hay):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _keyword_score(raw: Any, haystack: str, type_val: str) -> tuple[int, int]:
|
||||
"""
|
||||
运输类型上的命中优先,其次全文词长。
|
||||
@@ -212,11 +226,11 @@ def _keyword_score(raw: Any, haystack: str, type_val: str) -> tuple[int, int]:
|
||||
if k in _GENERIC_KEYWORDS:
|
||||
continue
|
||||
needles = _keyword_needles(kw)
|
||||
if type_val and any(n in type_val for n in needles):
|
||||
if type_val and any(_needle_in(type_val, n) for n in needles):
|
||||
type_hits += 1
|
||||
length += len(k)
|
||||
any_hit = True
|
||||
elif any(n in haystack for n in needles):
|
||||
elif any(_needle_in(haystack, n) for n in needles):
|
||||
length += len(k)
|
||||
any_hit = True
|
||||
if not any_hit:
|
||||
|
||||
Reference in New Issue
Block a user