看板平均响应按协同分发统计;查价失败可从主账恢复;报价模板匹配与填表更稳,后台改名运营后台。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
linmianxun
2026-10-05 18:00:08 +08:00
co-authored by Cursor
parent e95e8869a6
commit 58df358577
28 changed files with 1493 additions and 245 deletions
+18 -5
View File
@@ -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,
+12 -6
View File
@@ -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)
+13 -6
View File
@@ -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)
+11 -6
View File
@@ -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"
+98 -3
View File
@@ -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(
+17 -3
View File
@@ -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: