diff --git a/inquiry-agent/agent/policy/air_group_ops.py b/inquiry-agent/agent/policy/air_group_ops.py index c4129c8..422378c 100644 --- a/inquiry-agent/agent/policy/air_group_ops.py +++ b/inquiry-agent/agent/policy/air_group_ops.py @@ -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 diff --git a/inquiry-agent/agent/policy/inquiry_copy.py b/inquiry-agent/agent/policy/inquiry_copy.py index d1bce04..0fa819a 100644 --- a/inquiry-agent/agent/policy/inquiry_copy.py +++ b/inquiry-agent/agent/policy/inquiry_copy.py @@ -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}", diff --git a/inquiry-agent/tests/test_air_group_collab.py b/inquiry-agent/tests/test_air_group_collab.py index 61391e8..c64fea3 100644 --- a/inquiry-agent/tests/test_air_group_collab.py +++ b/inquiry-agent/tests/test_air_group_collab.py @@ -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",