空运协同里手工改费用、有起运地、锁舱失败或缺参会被误判,报价单已出后再激活又重发模板。按销售侧口径收紧这些分支,并让价格卡和未成交回执带上真实工单号。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-08 18:05:29 +08:00
co-authored by Cursor
parent 43e633f359
commit 6141978a9e
25 changed files with 1347 additions and 98 deletions
@@ -22,6 +22,8 @@ from agent.policy.air_group_ops import (
apply_air_file,
apply_air_lock,
apply_air_quote,
lock_params_from_text,
text_has_lock_supplement,
apply_air_release,
apply_air_tms_choice,
current_ticket,
@@ -229,10 +231,18 @@ def handle_air_group(
if is_handoff_ticket(ticket):
logger.info("air_group 转人工不再跟 chat=%s", chat_id)
return "handoff_ignore"
from agent.policy.system_exception import reply_paused_ticket, ticket_is_paused
from agent.policy.system_exception import (
cabin_failure_pause,
reply_paused_ticket,
ticket_is_paused,
)
# 锁舱失败留下的暂停只拦还要打 TMS 的操作。成交、未成交和补原因不调用 TMS。
if ticket_is_paused(ticket) and not _deal_ignores_tms_pause(ticket, intent):
# 查价失败的暂停仍要拦住。锁舱/释放失败已经告诉过原因,不能挡住后面的报价和成交。
if (
ticket_is_paused(ticket)
and not cabin_failure_pause(ticket)
and not _deal_ignores_tms_pause(ticket, intent)
):
reply_paused_ticket(
ticket=ticket,
reply=lambda text: send_group_text(group_client_of(engine), chat_id, text),
@@ -310,6 +320,29 @@ def _handle_cabin(
_ = injected_quote
chat_id = message.chat_id
if not work_order_no:
# 本群已经在锁这张单时,补舱位不用再报工单号。
bound = current_ticket(engine, chat_id)
if bound is not None and is_air_ticket(bound):
ticket = bound
if intent == INTENT_RELEASE:
return apply_air_release(
flow=engine,
ticket=ticket,
sender_id=message.sender_id,
chat_id=chat_id,
instruction=message.content or "",
)
option_no, airline, flight_no = lock_params_from_text(message.content or "")
return apply_air_lock(
flow=engine,
ticket=ticket,
sender_id=message.sender_id,
chat_id=chat_id,
instruction=message.content or "",
option_no=option_no,
airline=airline,
flight_no=flight_no,
)
send_group_text(group_client_of(engine), chat_id, copy.air_lock_need_work_order())
return "cabin_need_work_order"
ticket, phase = resolve_cabin_ticket(
@@ -328,15 +361,17 @@ def _handle_cabin(
chat_id=chat_id,
instruction=message.content or "",
)
# 「WO…锁舱,锁AIR-OPT01 TH」要带上句中的舱位和航司,不能只靠报价里碰巧只有一条。
# 「锁 AIR-OPT01 CZ 航班CZ327」要带上舱位、航司、航班号。缺了先补,不能直接锁。
option_no, airline, flight_no = lock_params_from_text(message.content or "")
return apply_air_lock(
flow=engine,
ticket=ticket,
sender_id=message.sender_id,
chat_id=chat_id,
instruction=message.content or "",
option_no=_option_from_text(message.content or ""),
airline=_airline_from_text(message.content or ""),
option_no=option_no,
airline=airline,
flight_no=flight_no,
)
@@ -373,19 +408,20 @@ def _handle_bound(
if lost:
return lost
facts = dict(ticket_view(ticket).get("collab_facts") or {})
if intent == INTENT_PICK_OPTION or (
intent == INTENT_OTHER and str(facts.get("__pending_air_lock") or "") == "1"
):
option = _option_from_text(message.content or "")
if option:
pending_lock = str(facts.get("__pending_air_lock") or "") == "1"
# 等补舱位时,报价/选线路等意图让路:话里带了舱位、航司或航班号就继续锁。
if intent == INTENT_PICK_OPTION or pending_lock or text_has_lock_supplement(message.content or ""):
option_no, airline, flight_no = lock_params_from_text(message.content or "")
if option_no or airline or flight_no:
return apply_air_lock(
flow=engine,
ticket=ticket,
sender_id=message.sender_id,
chat_id=chat_id,
instruction=message.content or "",
option_no=option,
airline=_airline_from_text(message.content or ""),
option_no=option_no,
airline=airline,
flight_no=flight_no,
)
if intent in {INTENT_DEAL, INTENT_LOST, INTENT_NEGOTIATE}:
outcome = {
@@ -421,6 +457,18 @@ def _handle_bound(
sender_id=message.sender_id,
chat_id=chat_id,
)
from agent.policy.sea_group_ops import is_sales
from agent.routing.group_intent import resolve_price_intent
# 航线写出「空运费改为900」是报价。模型若判成调价,这里按说话人改回来。
priced = resolve_price_intent(
intent,
message.content or "",
sales=is_sales(ticket, message.sender_id),
)
if priced != intent:
logger.info("air_group 报价/调价改判 %s -> %s chat=%s", intent, priced, chat_id)
intent = priced
if intent == INTENT_ADJUST:
from agent.policy.quote_adjust_ops import apply_group_adjust
@@ -448,26 +496,3 @@ def _handle_bound(
return "group_ignore"
def _option_from_text(text: str) -> str:
"""从「锁AIR-OPT01 TH」抽出舱位编号;不要把后面的航司码拼进 optionNo。"""
import re
m = re.search(r"AIR-?OPT-?\d+", (text or "").upper())
if not m:
return ""
raw = m.group(0)
# 统一成 TMS 常见写法 AIR-OPT01(中间可保留已有的第二根横杠)
if raw.startswith("AIROPT"):
return "AIR-OPT" + raw[6:]
return raw
def _airline_from_text(text: str) -> str:
"""从「AIR-OPT01 TH」抽出两位航司码;没有就不猜。"""
import re
blob = (text or "").upper()
m = re.search(r"AIR-?OPT-?\d+\s*([A-Z]{2})(?![A-Z0-9])", blob)
if m:
return m.group(1)
return ""