空运已有 TMS 选定舱位时直接锁舱,不再追问三项;纯文字报价提示只留给附件报价。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
linmianxun
2026-10-13 22:51:58 +08:00
co-authored by Cursor
parent 7ef4a1b1cf
commit 92bdf56a4d
3 changed files with 117 additions and 39 deletions
+64 -22
View File
@@ -968,24 +968,12 @@ def apply_air_lock(
)
send_group_text(group_client_of(flow), chat_id, copy.air_lock_need_params(labels))
return "wait_lock_params"
# 附件/手工报价、运价库无舱位:引导纯文字报价,不算系统异常、不通知 IT。
# 仅「航线附件报价」引导纯文字价。有 TMS / 文字手工价失败不能误进这条
# (旧逻辑用「无匹配报价」关键字,会把 TMS 锁舱失败也拐进文字报价提示)。
src_label = str((quote or {}).get("source_label") or (quote or {}).get("source") or "")
is_attach_quote = "航线附件报价" in src_label
need_text_quote = (
err_code in {"lock_incomplete", "lock_fail"}
and (
"航线附件报价" in src_label
or "附件" in src_label
or any(
x in reason_text
for x in (
"手工报价",
"无匹配舱位",
"无匹配报价",
"运价库无匹配",
"标准运价库无匹配",
)
)
)
is_attach_quote and err_code in {"lock_incomplete", "lock_fail"}
)
if need_text_quote:
send_group_text(
@@ -1555,6 +1543,8 @@ def _resolve_lock_params(
拼出锁舱三项。
这一句里写的优先;没写的用上一句暂存的;再没有才用报价上已有的舱位。
TMS 已有舱位明细时(selected_air_option / airOptions):句中未点名也自动带出,
禁止再追问舱位编号/航司/航班号(WO014:航线确认TMS 后直接「锁舱」仍被补问)。
航司写成南航、东方航空这类名字时归一成二字码;归一不了也算已经补充,不再追问。
"""
said_opt, said_air, said_flight = lock_params_from_text(instruction)
@@ -1562,16 +1552,18 @@ def _resolve_lock_params(
air = _normalize_airline(airline or said_air or str(facts.get(_PENDING_LOCK_AIRLINE) or ""))
flight = _normalize_flight(flight_no or said_flight or str(facts.get(_PENDING_LOCK_FLIGHT) or ""))
chosen = _option_row(quote, option)
single_auto = False
if chosen is None and not option:
options = _air_options(quote)
if len(options) == 1:
chosen = options[0]
chosen = _default_cabin_row(quote)
if chosen is not None:
option = str(chosen.get("optionNo") or chosen.get("option_no") or "").strip()
single_auto = True
if not _airline_ready(air) and chosen is not None:
air = _normalize_airline(
str(chosen.get("airlineCode") or chosen.get("airline") or "")
str(
chosen.get("airlineCode")
or chosen.get("airline")
or chosen.get("airlineName")
or ""
)
)
if not flight and chosen is not None:
flight = _normalize_flight(str(chosen.get("flightNo") or chosen.get("flight_no") or ""))
@@ -1584,6 +1576,51 @@ def _resolve_lock_params(
return option, air, flight
def _selected_cabin_row(quote: dict[str, Any]) -> Optional[dict[str, Any]]:
"""报价上已选定的舱位(JSON 字符串或 dict)。"""
selected = (quote or {}).get("selected_air_option") or (quote or {}).get("selectedAirOption")
if isinstance(selected, str) and selected.strip().startswith("{"):
try:
selected = json.loads(selected)
except json.JSONDecodeError:
selected = None
if isinstance(selected, dict) and (
selected.get("optionNo") or selected.get("option_no") or selected.get("flightNo")
):
return selected
return None
def _default_cabin_row(quote: dict[str, Any]) -> Optional[dict[str, Any]]:
"""
句中未点名舱位时的默认行。
优先 selected_air_option;否则单条 airOptions;TMS 多条时取第一条,
避免「已有 TMS 舱位仍追问三项」(WO014)。
"""
selected = _selected_cabin_row(quote)
if selected is not None:
return selected
options = _air_options(quote)
if len(options) == 1:
return options[0]
if len(options) > 1 and _quote_has_tms_cabin(quote):
return options[0]
return None
def _quote_has_tms_cabin(quote: dict[str, Any]) -> bool:
"""本单报价里已有可引用的 TMS 舱位明细。"""
src = dict(quote or {})
qid = str(src.get("quoteId") or src.get("quote_id") or "").strip()
if qid and not qid.upper().startswith("QT-"):
return bool(_air_options(src) or _selected_cabin_row(src))
label = str(src.get("source_label") or src.get("source") or "").upper()
if "TMS" in label:
return bool(_air_options(src) or _selected_cabin_row(src))
return False
def _option_row(quote: dict[str, Any], option_no: str) -> Optional[dict[str, Any]]:
"""按舱位编号找报价里的那一条;对不上返回空。"""
want = (option_no or "").strip().upper()
@@ -1593,6 +1630,11 @@ def _option_row(quote: dict[str, Any], option_no: str) -> Optional[dict[str, Any
no = str(row.get("optionNo") or row.get("option_no") or "").strip().upper()
if no == want:
return row
selected = _selected_cabin_row(quote)
if selected is not None:
no = str(selected.get("optionNo") or selected.get("option_no") or "").strip().upper()
if no == want:
return selected
return None
+3 -16
View File
@@ -3347,24 +3347,11 @@ def air_lock_incomplete(*, work_order_no: str, operator: str, reason: str = "")
"""
锁舱参数不齐(纯手工价、运价库无舱位等):业务补问,不算系统异常,不惊动 IT。
附件/无舱位场景改引导纯文字报价;有 TMS 舱位时仍可补 AIR-OPT 三项。
有 TMS / 非附件报价:仍补舱位三项。附件报价的「请发纯文字」只由
air_group_ops 在 source_label=航线附件报价 时走 air_lock_need_text_quote,
禁止按「无匹配报价」关键字在这里拐过去(会误伤 TMS 锁舱)。
"""
why = (reason or "").strip()
blob = why
if any(
x in blob
for x in (
"手工报价",
"附件",
"无匹配舱位",
"无匹配报价",
"运价库无匹配",
"标准运价库无匹配",
)
):
return air_lock_need_text_quote(
work_order_no=work_order_no, operator=operator, reason=why
)
lines = [
f"工单{work_order_no} 还不能锁舱。",
f"操作人:{operator}",
+50 -1
View File
@@ -1110,8 +1110,57 @@ class AirGroupCollabTests(unittest.TestCase):
self.assertEqual(phase, "lock_ok")
self.assertTrue(any("执行成功" in t for t in self._texts()))
def test_tms_quote_with_selected_cabin_locks_without_asking_params(self) -> None:
"""TMS 已有 selected_air_option:直接锁,不追问舱位/航司/航班(WO014)。"""
wo = self._create_air(
quote={
"total": "183370",
"source": "TMS",
"source_label": "航线确认TMS",
"quoteId": "AIR-2094721692181016578-2094721692701110273",
"selected_air_option": {
"optionNo": "AIR-OPT01",
"airlineCode": "JG",
"flightNo": "JG2719",
},
"airOptions": [
{
"optionNo": "AIR-OPT01",
"airlineCode": "JG",
"flightNo": "JG2719",
},
{
"optionNo": "AIR-OPT02",
"airlineCode": "RW",
"flightNo": "RW633",
},
],
},
status="已报价",
)
calls: list[dict] = []
orig = self.ledger.lock_cabin
def spy(**kwargs):
calls.append(kwargs)
return orig(**kwargs)
self.ledger.lock_cabin = spy # type: ignore
phase = handle_air_group(
self._msg(sender="air1", text=f"{wo}锁舱", chat="wr_air_lock_tms_auto", mid="tmsauto1"),
flow=self.flow,
ledger=self.ledger,
injected_intent="air_lock",
)
self.assertEqual(phase, "lock_ok")
self.assertEqual(len(calls), 1)
self.assertEqual(calls[0].get("option_no"), "AIR-OPT01")
self.assertEqual(calls[0].get("airline"), "JG")
self.assertEqual(calls[0].get("flight_no"), "JG2719")
self.assertFalse(any("锁舱参数不完整" in t for t in self._texts()))
def test_lock_missing_params_asks_and_does_not_call_tms(self) -> None:
"""缺舱位、航司、航班号时只请补,不锁舱、不报 TMS 失败、不通知 IT。"""
"""无舱位明细时缺三项才请补,不锁舱、不报 TMS 失败、不通知 IT。"""
wo = self._create_air(
quote={
"total": "CNY 1800",