落地空运单聊询价字段与线路选择。
必填核对后再建单;多条线路走 H5 点选;费用只展示该线路 TMS 回包,不套用整单空运费。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -49,6 +49,18 @@ _LOCK_QUOTE = "__air_lock_quote"
|
||||
_LOCK_EXEC = "__air_lock_exec"
|
||||
|
||||
|
||||
def _ticket_flight_date(ticket: object) -> str:
|
||||
"""锁舱 flightDate 用工单报价日期,和主账出站一致。"""
|
||||
view = ticket_view(ticket)
|
||||
facts = dict(view.get("facts") or {})
|
||||
return str(
|
||||
facts.get("报价日期")
|
||||
or view.get("quoteDate")
|
||||
or view.get("quote_date")
|
||||
or ""
|
||||
).strip()
|
||||
|
||||
|
||||
def is_airline(flow: Any, sender_id: str) -> bool:
|
||||
"""员工管理岗位=航线·空运且启用。"""
|
||||
uid = (sender_id or "").strip()
|
||||
@@ -595,21 +607,13 @@ def apply_air_lock(
|
||||
if not is_airline(flow, sender_id):
|
||||
send_group_text(group_client_of(flow), chat_id, copy.air_airline_only_cabin())
|
||||
return "not_airline"
|
||||
from agent.policy.system_exception import reply_paused_ticket, ticket_is_paused
|
||||
|
||||
if ticket_is_paused(ticket):
|
||||
reply_paused_ticket(
|
||||
ticket=ticket,
|
||||
reply=lambda text: send_group_text(group_client_of(flow), chat_id, text),
|
||||
)
|
||||
return "system_exception_paused"
|
||||
# 锁舱是航线主动再试:即使工单因上次失败标了系统异常,也要再打 TMS,
|
||||
# 不能只回「TMS接口异常,IT运维排查中」。
|
||||
no = ticket_no(ticket)
|
||||
name = operator_name(flow, sender_id)
|
||||
quote = ticket_view(ticket).get("quote") or {}
|
||||
options = _air_options(quote)
|
||||
quote_id = _quote_id(quote) or f"QT-{no}"
|
||||
if not options:
|
||||
options = [{"optionNo": "AIR-OPT-01"}]
|
||||
quote_id = _quote_id(quote)
|
||||
picked = (option_no or "").strip()
|
||||
if not picked and len(options) > 1:
|
||||
facts = dict(ticket_view(ticket).get("collab_facts") or {})
|
||||
@@ -619,8 +623,10 @@ def apply_air_lock(
|
||||
)
|
||||
send_group_text(group_client_of(flow), chat_id, copy.air_ask_lock_option(options))
|
||||
return "wait_lock_option"
|
||||
if not picked:
|
||||
if not picked and options:
|
||||
picked = str(options[0].get("optionNo") or options[0].get("option_no") or "").strip()
|
||||
if not picked:
|
||||
picked = "AIR-OPT-01"
|
||||
locker = getattr(flow.ledger, "lock_cabin", None)
|
||||
if not callable(locker):
|
||||
send_group_text(
|
||||
@@ -630,7 +636,7 @@ def apply_air_lock(
|
||||
)
|
||||
return "lock_unavailable"
|
||||
from agent.policy.system_exception import (
|
||||
STEP_TMS_CABIN,
|
||||
STEP_TMS_LOCK,
|
||||
TYPE_TMS,
|
||||
SystemExceptionEvent,
|
||||
call_with_retries,
|
||||
@@ -649,21 +655,35 @@ def apply_air_lock(
|
||||
is_ok=lambda x: bool((x or {}).get("ok")),
|
||||
)
|
||||
if not out.get("ok"):
|
||||
why = copy.air_cabin_fail_reason(
|
||||
tms_reason=str(out.get("failReason") or out.get("error") or ""),
|
||||
flight_date=_ticket_flight_date(ticket),
|
||||
)
|
||||
send_group_text(
|
||||
group_client_of(flow),
|
||||
chat_id,
|
||||
copy.air_lock_fail(work_order_no=no, operator=name, reason=why),
|
||||
)
|
||||
confirm_system_exception(
|
||||
event=SystemExceptionEvent(
|
||||
exception_type=TYPE_TMS,
|
||||
step=STEP_TMS_CABIN,
|
||||
step=STEP_TMS_LOCK,
|
||||
reason="空运锁舱接口调用失败",
|
||||
service="空运锁舱",
|
||||
error_code=str(out.get("failReason") or out.get("error") or "LOCK_FAIL"),
|
||||
error_code=why or "LOCK_FAIL",
|
||||
work_order_no=no,
|
||||
ticket_status=ticket_status(ticket),
|
||||
conversation_kind="group",
|
||||
conversation_target=chat_id,
|
||||
extra={"option_no": picked, "instruction": instruction, "sender_id": sender_id},
|
||||
extra={
|
||||
"option_no": picked,
|
||||
"instruction": instruction,
|
||||
"sender_id": sender_id,
|
||||
"operator_name": name,
|
||||
"flight_date": _ticket_flight_date(ticket),
|
||||
},
|
||||
),
|
||||
ledger=flow.ledger,
|
||||
reply=lambda text: send_group_text(group_client_of(flow), chat_id, text),
|
||||
)
|
||||
return "system_exception"
|
||||
exec_no = str(out.get("lockId") or out.get("tmsRequestId") or "").strip()
|
||||
@@ -690,6 +710,10 @@ def apply_air_lock(
|
||||
quote_no=quote_no,
|
||||
),
|
||||
)
|
||||
from agent.policy.system_exception import clear_system_exception, ticket_is_paused
|
||||
|
||||
if ticket_is_paused(ticket):
|
||||
clear_system_exception(ledger=flow.ledger, work_order_no=no)
|
||||
view = ticket_view(ticket)
|
||||
view["collab_chat_id"] = chat_id
|
||||
emit_group_deal(flow=flow, ticket=view, quote=dict(view.get("quote") or {}))
|
||||
@@ -707,14 +731,7 @@ def apply_air_release(
|
||||
if not is_airline(flow, sender_id):
|
||||
send_group_text(group_client_of(flow), chat_id, copy.air_airline_only_cabin())
|
||||
return "not_airline"
|
||||
from agent.policy.system_exception import reply_paused_ticket, ticket_is_paused
|
||||
|
||||
if ticket_is_paused(ticket):
|
||||
reply_paused_ticket(
|
||||
ticket=ticket,
|
||||
reply=lambda text: send_group_text(group_client_of(flow), chat_id, text),
|
||||
)
|
||||
return "system_exception_paused"
|
||||
# 释放同理:暂停单也允许再打 TMS,回话用释放失败原因而不是接口异常兜底。
|
||||
no = ticket_no(ticket)
|
||||
if ticket_status(ticket) == "已成交":
|
||||
send_group_text(group_client_of(flow), chat_id, copy.air_release_deal_blocked(no))
|
||||
@@ -734,7 +751,7 @@ def apply_air_release(
|
||||
)
|
||||
return "release_unavailable"
|
||||
from agent.policy.system_exception import (
|
||||
STEP_TMS_CABIN,
|
||||
STEP_TMS_RELEASE,
|
||||
TYPE_TMS,
|
||||
SystemExceptionEvent,
|
||||
call_with_retries,
|
||||
@@ -753,21 +770,35 @@ def apply_air_release(
|
||||
is_ok=lambda x: bool((x or {}).get("ok")),
|
||||
)
|
||||
if not out.get("ok"):
|
||||
why = copy.air_cabin_fail_reason(
|
||||
tms_reason=str(out.get("failReason") or out.get("error") or ""),
|
||||
flight_date=_ticket_flight_date(ticket),
|
||||
)
|
||||
send_group_text(
|
||||
group_client_of(flow),
|
||||
chat_id,
|
||||
copy.air_release_fail(work_order_no=no, operator=name, reason=why),
|
||||
)
|
||||
confirm_system_exception(
|
||||
event=SystemExceptionEvent(
|
||||
exception_type=TYPE_TMS,
|
||||
step=STEP_TMS_CABIN,
|
||||
step=STEP_TMS_RELEASE,
|
||||
reason="空运释放舱位接口调用失败",
|
||||
service="空运舱位",
|
||||
error_code=str(out.get("failReason") or out.get("error") or "RELEASE_FAIL"),
|
||||
error_code=why or "RELEASE_FAIL",
|
||||
work_order_no=no,
|
||||
ticket_status=ticket_status(ticket),
|
||||
conversation_kind="group",
|
||||
conversation_target=chat_id,
|
||||
extra={"lock_id": lock_id, "instruction": instruction, "sender_id": sender_id},
|
||||
extra={
|
||||
"lock_id": lock_id,
|
||||
"instruction": instruction,
|
||||
"sender_id": sender_id,
|
||||
"operator_name": name,
|
||||
"flight_date": _ticket_flight_date(ticket),
|
||||
},
|
||||
),
|
||||
ledger=flow.ledger,
|
||||
reply=lambda text: send_group_text(group_client_of(flow), chat_id, text),
|
||||
)
|
||||
return "system_exception"
|
||||
exec_no = str(out.get("releaseId") or out.get("tmsRequestId") or lock_id).strip()
|
||||
@@ -782,6 +813,10 @@ def apply_air_release(
|
||||
quote_no=str(facts.get(_LOCK_QUOTE) or ""),
|
||||
),
|
||||
)
|
||||
from agent.policy.system_exception import clear_system_exception, ticket_is_paused
|
||||
|
||||
if ticket_is_paused(ticket):
|
||||
clear_system_exception(ledger=flow.ledger, work_order_no=no)
|
||||
view = ticket_view(ticket)
|
||||
view["collab_chat_id"] = chat_id
|
||||
emit_group_deal(flow=flow, ticket=view, quote=dict(view.get("quote") or {}))
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
空运文字询价确定性流程(模型只负责抽字段,本文件只编排与执行)。
|
||||
|
||||
本文件职责:补问到询价必填齐全 → 建单出卡片 → 必须打主账查 TMS(有价/暂无报价)。
|
||||
本文件职责:补问到必填齐 → 核对确定后建单 → 查 TMS(一条出卡,多条 H5 选线路)。
|
||||
对不上机场也不能跳过查价、本地说暂无报价。
|
||||
调用:handler 在 Worker/inbox 线程;禁止回调线程同步跑。
|
||||
禁止:直连 TMS;用图状态冒充主账成功;用户文字冒充文件已生成。
|
||||
@@ -11,6 +11,7 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import threading
|
||||
import time
|
||||
import uuid
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from typing import Any, Callable, Optional
|
||||
@@ -20,6 +21,7 @@ from agent.ledger.memory_ledger import MemoryLedger
|
||||
from agent.llm.extract_text import extract_inquiry_snapshot
|
||||
from agent.policy import inquiry_copy as copy
|
||||
from agent.schema.field_validate import validate_required_fields
|
||||
from agent.schema.land_options import looks_like_confirm
|
||||
from agent.schema.tms_air_query import assemble_air_query
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -114,6 +116,8 @@ class AirTextInquiryFlow:
|
||||
"""
|
||||
|
||||
bookmark_kind = "air"
|
||||
# 明细文本真正发出后再发卡,避免销售先看到半截引用条。
|
||||
_detail_before_card_gap_sec = 0.8
|
||||
|
||||
def __init__(self, ledger: Any = None, group_client: Any = None, *, bookmark_store: Any = None) -> None:
|
||||
self._ledger = ledger or build_ledger()
|
||||
@@ -410,7 +414,7 @@ class AirTextInquiryFlow:
|
||||
phase = (sess.phase or "").strip()
|
||||
if not no or is_virtual_no(no):
|
||||
return sess
|
||||
if phase in {"clarify", "need_mode", "handoff_offer", "wait_lost_reason"}:
|
||||
if phase in {"clarify", "need_mode", "handoff_offer", "wait_lost_reason", "wait_air_route"}:
|
||||
return sess
|
||||
logger.info(
|
||||
"%s.new_over_old phase=%s wo=%s",
|
||||
@@ -621,21 +625,13 @@ class AirTextInquiryFlow:
|
||||
)
|
||||
return "wait_adopt"
|
||||
if sess.phase == "wait_collab":
|
||||
reply(
|
||||
copy.tms_hit_card(
|
||||
work_order_no=sess.work_order_no,
|
||||
quote=sess.quote,
|
||||
first_or_same=sess.first_or_same,
|
||||
history_work_order_no=sess.history_work_order_no,
|
||||
status=sess.status or "已报价",
|
||||
),
|
||||
copy.tms_hit_wecom_payload(
|
||||
work_order_no=sess.work_order_no,
|
||||
quote=sess.quote,
|
||||
first_or_same=sess.first_or_same,
|
||||
history_work_order_no=sess.history_work_order_no,
|
||||
status=sess.status or "已报价",
|
||||
),
|
||||
self._emit_air_hit(
|
||||
reply,
|
||||
work_order_no=sess.work_order_no,
|
||||
quote=sess.quote,
|
||||
first_or_same=sess.first_or_same,
|
||||
history_work_order_no=sess.history_work_order_no,
|
||||
status=sess.status or "已报价",
|
||||
)
|
||||
return "wait_collab"
|
||||
if sess.phase == "tms_miss":
|
||||
@@ -716,6 +712,19 @@ class AirTextInquiryFlow:
|
||||
adjusted = try_private_adjust(self, sess, text, reply)
|
||||
if adjusted:
|
||||
return adjusted
|
||||
if sess and sess.phase == "wait_confirm" and looks_like_confirm(text):
|
||||
# 确定查价不再抽字段,避免「确定」被模型改成空 facts。
|
||||
logger.info("air.confirm sender=%s", sender_id)
|
||||
return self._confirm_or_hold(
|
||||
sender_id=sender_id,
|
||||
facts=dict(sess.facts),
|
||||
text=sess.immutable_text or text,
|
||||
reply=reply,
|
||||
sess=sess,
|
||||
)
|
||||
if (not sess or sess.phase != "wait_confirm") and looks_like_confirm(text) and not sess:
|
||||
reply(copy.AIR_CONFIRM_EXPIRED)
|
||||
return "confirm_expired"
|
||||
sess = self._detach_ticketed_for_new_inquiry(sess)
|
||||
|
||||
snap = extract_inquiry_snapshot(
|
||||
@@ -739,7 +748,7 @@ class AirTextInquiryFlow:
|
||||
return llm_phase
|
||||
mode = (snap.get("business_line") or "").upper()
|
||||
facts = dict(snap.get("facts") or {})
|
||||
if sess and sess.phase in {"clarify", "need_mode"}:
|
||||
if sess and sess.phase in {"clarify", "need_mode", "wait_confirm"}:
|
||||
# 补问轮:旧字段打底;运输方式沿用上一句(飞=空运)
|
||||
from agent.llm.extract_text import detect_transport_mode
|
||||
from agent.schema.field_validate import harvest_oral_measures
|
||||
@@ -833,9 +842,78 @@ class AirTextInquiryFlow:
|
||||
),
|
||||
)
|
||||
|
||||
return self._create_and_quote(
|
||||
return self._hold_for_confirm(
|
||||
sender_id=sender_id,
|
||||
facts=facts,
|
||||
text=sess.immutable_text if sess and sess.immutable_text else text,
|
||||
reply=reply,
|
||||
sess=sess,
|
||||
)
|
||||
|
||||
def _hold_for_confirm(
|
||||
self,
|
||||
*,
|
||||
sender_id: str,
|
||||
facts: dict[str, str],
|
||||
text: str,
|
||||
reply: ReplyFn,
|
||||
sess: Optional[FlowSession],
|
||||
) -> str:
|
||||
"""必填已齐:出核对卡,等确定再建单。"""
|
||||
reply(copy.air_confirm_card(facts))
|
||||
self._save(
|
||||
FlowSession(
|
||||
sender_id=sender_id,
|
||||
thread_id=sender_id,
|
||||
phase="wait_confirm",
|
||||
business_line="AIR",
|
||||
facts=facts,
|
||||
immutable_text=text,
|
||||
wait_version=(sess.wait_version + 1) if sess else 1,
|
||||
)
|
||||
)
|
||||
return "wait_confirm"
|
||||
|
||||
def _confirm_or_hold(
|
||||
self,
|
||||
*,
|
||||
sender_id: str,
|
||||
facts: dict[str, str],
|
||||
text: str,
|
||||
reply: ReplyFn,
|
||||
sess: FlowSession,
|
||||
) -> str:
|
||||
"""确定意图:字段仍齐则建单查价。"""
|
||||
check = validate_required_fields(facts=facts, business_line="AIR")
|
||||
tms_ready = assemble_air_query(check["facts"])
|
||||
if not check["ok"] or not tms_ready.get("ok"):
|
||||
keys = list(check["missing"] or [])
|
||||
for key in tms_ready.get("missing") or []:
|
||||
if key not in keys:
|
||||
keys.append(key)
|
||||
new_sess = FlowSession(
|
||||
sender_id=sender_id,
|
||||
thread_id=sender_id,
|
||||
phase="clarify",
|
||||
business_line="AIR",
|
||||
facts=dict(tms_ready.get("facts") or facts),
|
||||
immutable_text=sess.immutable_text or text,
|
||||
wait_version=sess.wait_version + 1,
|
||||
)
|
||||
return self._clarify_or_handoff(
|
||||
sess=sess,
|
||||
keys=keys,
|
||||
new_sess=new_sess,
|
||||
reply=reply,
|
||||
clarify_text=copy.ask_clarify(
|
||||
facts=new_sess.facts,
|
||||
missing_keys=keys,
|
||||
transport_mode="空运",
|
||||
),
|
||||
)
|
||||
return self._create_and_quote(
|
||||
sender_id=sender_id,
|
||||
facts=dict(tms_ready["facts"]),
|
||||
text=text,
|
||||
reply=reply,
|
||||
)
|
||||
@@ -925,6 +1003,15 @@ class AirTextInquiryFlow:
|
||||
return "tms_miss"
|
||||
|
||||
quote = tms["quote"]
|
||||
from agent.schema.air_route_options import (
|
||||
needs_route_pick,
|
||||
parse_air_options,
|
||||
project_selected_quote,
|
||||
)
|
||||
|
||||
options = parse_air_options(quote)
|
||||
if options and not needs_route_pick(quote):
|
||||
quote = project_selected_quote(quote, options[0])
|
||||
up = self._ledger.upsert_quote(work_order_no=no, quote=quote, to_status="已报价")
|
||||
if not up.get("ok"):
|
||||
reply("主账写入报价失败,不能当作已报价。")
|
||||
@@ -933,29 +1020,193 @@ class AirTextInquiryFlow:
|
||||
return "ledger_fail"
|
||||
sess.quote = quote
|
||||
copy.remember_tms_quote(sess, quote)
|
||||
sess.tms_quote = dict(quote)
|
||||
sess.status = "已报价"
|
||||
if needs_route_pick(quote):
|
||||
return self._offer_air_routes(
|
||||
sess=sess,
|
||||
work_order_no=no,
|
||||
quote=quote,
|
||||
reply=reply,
|
||||
)
|
||||
sess.phase = "wait_collab"
|
||||
sess.allowed = (copy.BTN_SKIP_COLLAB,)
|
||||
sess.wait_version += 1
|
||||
self._save(sess)
|
||||
reply(
|
||||
copy.tms_hit_card(
|
||||
work_order_no=no,
|
||||
quote=quote,
|
||||
first_or_same=first,
|
||||
history_work_order_no=history_wo,
|
||||
status="已报价",
|
||||
),
|
||||
copy.tms_hit_wecom_payload(
|
||||
work_order_no=no,
|
||||
quote=quote,
|
||||
first_or_same=first,
|
||||
history_work_order_no=history_wo,
|
||||
status="已报价",
|
||||
),
|
||||
self._emit_air_hit(
|
||||
reply,
|
||||
work_order_no=no,
|
||||
quote=quote,
|
||||
first_or_same=first,
|
||||
history_work_order_no=history_wo,
|
||||
status="已报价",
|
||||
)
|
||||
return "wait_collab"
|
||||
|
||||
def _offer_air_routes(
|
||||
self,
|
||||
*,
|
||||
sess: FlowSession,
|
||||
work_order_no: str,
|
||||
quote: dict[str, Any],
|
||||
reply: ReplyFn,
|
||||
) -> str:
|
||||
"""多条线路:企微只发入口卡,选线在 H5。"""
|
||||
from agent.channel.h5.air_route_store import build_air_route_url, issue_air_route_token
|
||||
from agent.schema.air_route_options import parse_air_options
|
||||
|
||||
token = issue_air_route_token(
|
||||
sender_id=sess.sender_id,
|
||||
work_order_no=work_order_no,
|
||||
quote=quote,
|
||||
facts=dict(sess.facts),
|
||||
)
|
||||
url = build_air_route_url(token)
|
||||
count = len(parse_air_options(quote))
|
||||
sess.phase = "wait_air_route"
|
||||
sess.allowed = ()
|
||||
sess.wait_version += 1
|
||||
self._save(sess)
|
||||
reply(
|
||||
copy.air_route_entry_text(work_order_no=work_order_no, count=count),
|
||||
copy.air_route_entry_card(work_order_no=work_order_no, count=count, url=url),
|
||||
)
|
||||
return "wait_air_route"
|
||||
|
||||
def select_air_route(
|
||||
self,
|
||||
*,
|
||||
sender_id: str,
|
||||
index: int,
|
||||
reply: ReplyFn,
|
||||
) -> str:
|
||||
"""
|
||||
H5 选定一条线路:写回起运港,出价格明细卡。
|
||||
|
||||
已采用过则作废旧报价单后再出新卡。
|
||||
"""
|
||||
sess = self.session_of(sender_id)
|
||||
if sess is None or not (sess.work_order_no or "").strip():
|
||||
reply("线路选择已过期,请回企微重新询价。")
|
||||
return "route_expired"
|
||||
from agent.schema.air_route_options import (
|
||||
apply_selected_option,
|
||||
parse_air_options,
|
||||
project_selected_quote,
|
||||
)
|
||||
|
||||
raw_quote = dict(sess.tms_quote or sess.quote or {})
|
||||
options = parse_air_options(raw_quote)
|
||||
if index < 0 or index >= len(options):
|
||||
reply("没有这条线路,请重新打开页面选择。")
|
||||
return "route_invalid"
|
||||
option = options[index]
|
||||
facts = apply_selected_option(dict(sess.facts), option)
|
||||
sess.facts = facts
|
||||
quote = project_selected_quote(raw_quote, option)
|
||||
prev_phase = (sess.phase or "").strip()
|
||||
already_adopted = prev_phase in {"wait_deal", "wait_file", "wait_adopt"}
|
||||
if already_adopted:
|
||||
clearer = getattr(self._ledger, "void_quote_files", None)
|
||||
if callable(clearer):
|
||||
clearer(work_order_no=sess.work_order_no)
|
||||
from agent.redis_coord.quote_file import clear_quote_file_ready
|
||||
|
||||
clear_quote_file_ready(sess.work_order_no)
|
||||
patcher = getattr(self._ledger, "update_facts", None)
|
||||
if callable(patcher):
|
||||
patcher(work_order_no=sess.work_order_no, facts=facts)
|
||||
self._ledger.upsert_quote(work_order_no=sess.work_order_no, quote=quote, to_status="已报价")
|
||||
sess.quote = quote
|
||||
copy.remember_tms_quote(sess, quote)
|
||||
sess.status = "已报价"
|
||||
sess.wait_version += 1
|
||||
if already_adopted:
|
||||
# 旧报价单已作废,直接出方案卡让销售重新采用。
|
||||
sess.phase = "wait_adopt"
|
||||
sess.allowed = (copy.BTN_ADOPT_EXCEL, copy.BTN_ADOPT_PDF, copy.BTN_REJECT)
|
||||
self._save(sess)
|
||||
mode = copy.scheme_transport_mode(sess.business_line)
|
||||
reply(
|
||||
copy.scheme_card(
|
||||
work_order_no=sess.work_order_no,
|
||||
status="已报价",
|
||||
facts=facts,
|
||||
quote=quote,
|
||||
transport_mode=mode,
|
||||
),
|
||||
copy.scheme_wecom_payload(
|
||||
work_order_no=sess.work_order_no,
|
||||
facts=facts,
|
||||
quote=quote,
|
||||
status="已报价",
|
||||
transport_mode=mode,
|
||||
),
|
||||
)
|
||||
return "wait_adopt"
|
||||
sess.phase = "wait_collab"
|
||||
sess.allowed = (copy.BTN_SKIP_COLLAB,)
|
||||
self._save(sess)
|
||||
self._emit_air_hit(
|
||||
reply,
|
||||
work_order_no=sess.work_order_no,
|
||||
quote=quote,
|
||||
first_or_same=sess.first_or_same,
|
||||
history_work_order_no=sess.history_work_order_no,
|
||||
status="已报价",
|
||||
)
|
||||
return "wait_collab"
|
||||
|
||||
def _emit_air_hit(
|
||||
self,
|
||||
reply: ReplyFn,
|
||||
*,
|
||||
work_order_no: str,
|
||||
quote: dict[str, Any],
|
||||
first_or_same: str = "first",
|
||||
history_work_order_no: str = "",
|
||||
status: str = "已报价",
|
||||
) -> None:
|
||||
"""
|
||||
先发完整价格明细文本,等它真正发出,再发带按钮的卡。
|
||||
|
||||
企微引用条超长会截成省略号。海运已走这条;空运多项费用同样不能塞进卡。
|
||||
只等这一条出站,不锁其它工单。
|
||||
"""
|
||||
src = dict(quote or {})
|
||||
out_id = reply(
|
||||
copy.sea_quote_detail_text(
|
||||
work_order_no=work_order_no,
|
||||
quote=src,
|
||||
first_or_same=first_or_same,
|
||||
history_work_order_no=history_work_order_no,
|
||||
status=status,
|
||||
business_line="AIR",
|
||||
)
|
||||
) or ""
|
||||
self._wait_outbound_sent(
|
||||
str(out_id), work_order_no=work_order_no, what="价格明细文本"
|
||||
)
|
||||
gap = float(getattr(self, "_detail_before_card_gap_sec", 0.8) or 0)
|
||||
if out_id and gap > 0:
|
||||
time.sleep(gap)
|
||||
reply(
|
||||
copy.tms_hit_card(
|
||||
work_order_no=work_order_no,
|
||||
quote=src,
|
||||
first_or_same=first_or_same,
|
||||
history_work_order_no=history_work_order_no,
|
||||
status=status,
|
||||
),
|
||||
copy.tms_hit_wecom_payload(
|
||||
work_order_no=work_order_no,
|
||||
quote=src,
|
||||
first_or_same=first_or_same,
|
||||
history_work_order_no=history_work_order_no,
|
||||
status=status,
|
||||
),
|
||||
)
|
||||
|
||||
def _wait_inquiry_sent(self, out_id: str, *, work_order_no: str) -> None:
|
||||
"""等询价确认出站完成后再查价。"""
|
||||
self._wait_outbound_sent(out_id, work_order_no=work_order_no, what="询价确认")
|
||||
|
||||
@@ -15,6 +15,7 @@ from typing import Any, Iterable
|
||||
|
||||
from agent.schema.field_validate import (
|
||||
AIR_COLLAB_FIELDS,
|
||||
AIR_OPTIONAL_FIELDS,
|
||||
AIR_REQUIRED_FIELDS,
|
||||
LAND_DISPLAY,
|
||||
SEA_COLLAB_FIELDS,
|
||||
@@ -27,7 +28,7 @@ from agent.schema.land_options import option_prompt as _land_option_prompt
|
||||
from agent.schema.quote_adjust import total_price_label
|
||||
from agent.schema.sea_options import sea_class_prompt
|
||||
|
||||
# 空运补问:与合同 8 项一致。报价日期默认当天,不进待补充。
|
||||
# 空运补问:必填 7 项 + 非必填。报价日期默认当天,不进待补充。
|
||||
_AIR_EXAMPLES = {
|
||||
"起运港": "南京",
|
||||
"目的港": "吉隆坡",
|
||||
@@ -37,10 +38,20 @@ _AIR_EXAMPLES = {
|
||||
"体积": "2CBM",
|
||||
"包装方式": "托盘",
|
||||
"报价日期": "2026-09-15",
|
||||
"货源地": "东莞",
|
||||
"货好时间": "随时可提",
|
||||
"提货地址": "东莞仓库",
|
||||
"收货地址": "马尼拉",
|
||||
"贸易条款": "FOB",
|
||||
}
|
||||
_AIR_CLARIFY = tuple(
|
||||
(key, label, _AIR_EXAMPLES[key]) for key, label in AIR_REQUIRED_FIELDS
|
||||
)
|
||||
_AIR_OPTIONAL_CLARIFY = tuple(
|
||||
(key, label, _AIR_EXAMPLES.get(key, "")) for key, label in AIR_OPTIONAL_FIELDS
|
||||
)
|
||||
AIR_CONFIRM_TAIL = "请回复“确定”进行TMS查价,内容有误时,请重新发内容。"
|
||||
AIR_CONFIRM_EXPIRED = "刚才的空运核对已过期,请把空运需求再发一次,出核对卡后再回复确定。"
|
||||
|
||||
# 海运补问:与合同 10 项一致。报价日期默认当天,不进待补充。
|
||||
_SEA_EXAMPLES = {
|
||||
@@ -373,6 +384,46 @@ def _sea_known_value(facts: dict[str, str], key: str) -> str:
|
||||
return (src.get(key) or "").strip()
|
||||
|
||||
|
||||
def _air_field_line(key: str, value: str, *, required: bool, example: str = "") -> str:
|
||||
"""空运销售侧一行:名称(必填/非必填):值或示例。"""
|
||||
label = AIR_DISPLAY_LABEL(key)
|
||||
mark = "必填" if required else "非必填"
|
||||
text = (value or "").strip()
|
||||
if text:
|
||||
return f"{label}({mark}):{text}"
|
||||
if example:
|
||||
return f"{label}({mark}):(如:{example})"
|
||||
return f"{label}({mark}):"
|
||||
|
||||
|
||||
def AIR_DISPLAY_LABEL(key: str) -> str:
|
||||
"""核对/补问对外名。起运港按产品口径写起运港。"""
|
||||
if key == "起运港":
|
||||
return "起运港"
|
||||
return display_name(key, "AIR")
|
||||
|
||||
|
||||
def air_confirm_card(facts: dict[str, str] | None) -> str:
|
||||
"""
|
||||
确定查价前的核对卡。没有工单号。
|
||||
|
||||
必填 7 项 + 6 个非必填都列出;空着的非必填留空行。
|
||||
"""
|
||||
src = dict(facts or {})
|
||||
lines = ["运输方式(必填):空运"]
|
||||
for key, _label in AIR_REQUIRED_FIELDS:
|
||||
if key == "报价日期":
|
||||
val = _air_known_value(src, key) or (src.get("报价日期") or "").strip()
|
||||
lines.append(_air_field_line(key, val, required=True))
|
||||
continue
|
||||
lines.append(_air_field_line(key, _air_known_value(src, key), required=True))
|
||||
for key, _label in AIR_OPTIONAL_FIELDS:
|
||||
lines.append(_air_field_line(key, _air_known_value(src, key), required=False))
|
||||
lines.append("")
|
||||
lines.append(AIR_CONFIRM_TAIL)
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def ask_clarify(
|
||||
*,
|
||||
facts: dict[str, str] | None = None,
|
||||
@@ -383,6 +434,7 @@ def ask_clarify(
|
||||
缺项补问:先回显已识别,再列待补充并给示例。
|
||||
|
||||
报价日期写进已识别(销售说了用原话,没说用当天),不出现在待补充。
|
||||
空运非必填一并列出,没填进待补充,不挡查价。
|
||||
"""
|
||||
src = dict(facts or {})
|
||||
skip = {"报价日期"}
|
||||
@@ -398,6 +450,16 @@ def ask_clarify(
|
||||
val = known(src, key)
|
||||
if val:
|
||||
lines.append(f"{label}:{val}")
|
||||
if not sea:
|
||||
origin = known(src, "起运港")
|
||||
if origin:
|
||||
lines.append(f"起运地:{origin}")
|
||||
for key, label, _example in _AIR_OPTIONAL_CLARIFY:
|
||||
if key == "起运港":
|
||||
continue
|
||||
val = known(src, key)
|
||||
if val:
|
||||
lines.append(f"{label}:{val}")
|
||||
lines.append("")
|
||||
lines.append("待补充:")
|
||||
any_missing = False
|
||||
@@ -411,6 +473,16 @@ def ask_clarify(
|
||||
lines.append(sea_class_prompt())
|
||||
else:
|
||||
lines.append(f"{label}:(如:{example})")
|
||||
if not sea:
|
||||
optional_pending = [
|
||||
(key, label, example)
|
||||
for key, label, example in _AIR_OPTIONAL_CLARIFY
|
||||
if not known(src, key)
|
||||
]
|
||||
for key, label, example in optional_pending:
|
||||
any_missing = True
|
||||
shown = "起运港" if key == "起运港" else label
|
||||
lines.append(f"{shown}(非必填):(如:{example})")
|
||||
if not any_missing:
|
||||
return ""
|
||||
return "\n".join(lines)
|
||||
@@ -861,18 +933,24 @@ def quote_area_safe_text(
|
||||
transport_mode: str = "",
|
||||
) -> str:
|
||||
"""
|
||||
卡片引用条:短报价原样;超长只留总价名称,避免半截费用名。
|
||||
卡片引用条:空运有费用行就只留总价,禁止再塞费用。
|
||||
|
||||
完整明细由 sea_quote_detail_text 另发一条文本。空运卡仍用 quote_area_fee_text。
|
||||
三项短费用也会被企微裁成「人工费:CNY 123...」。完整明细走文本。
|
||||
海运/陆运仍按字数:短的原样,超长同样只留总价。
|
||||
"""
|
||||
full = quote_area_fee_text(quote, business_line=business_line, transport_mode=transport_mode)
|
||||
if len(full) <= _QUOTE_AREA_SAFE_CHARS:
|
||||
return full
|
||||
src = dict(quote or {})
|
||||
header_ccy = str(src.get("currency") or src.get("currencyCode") or "").strip()
|
||||
total = money_text(str(src.get("total") or "-").strip() or "-", header_ccy)
|
||||
n = len(quote_fee_lines(src))
|
||||
label = _total_label(src, business_line, transport_mode)
|
||||
line = (business_line or "").upper()
|
||||
mode = (transport_mode or "").strip()
|
||||
is_air = line == "AIR" or mode == "空运"
|
||||
if is_air and n:
|
||||
return f"{label}:{total}\n共{n}项费用,详见上一条明细"
|
||||
full = quote_area_fee_text(quote, business_line=business_line, transport_mode=transport_mode)
|
||||
if len(full) <= _QUOTE_AREA_SAFE_CHARS:
|
||||
return full
|
||||
if n:
|
||||
return f"{label}:{total}\n共{n}项费用,详见上一条明细"
|
||||
return f"{label}:{total}"
|
||||
@@ -888,7 +966,7 @@ def sea_quote_detail_text(
|
||||
business_line: str = "SEA",
|
||||
) -> str:
|
||||
"""
|
||||
海运/陆运完整价格明细文本:来源、工单号、费用行、总销售价。
|
||||
完整价格明细文本:来源、工单号、费用行、总价。海运/陆运/空运共用。
|
||||
|
||||
时效、报价有效期只在卡片上出现,这里不写,避免和卡重复。
|
||||
不受卡片引用条字数限制。first_or_same 只兼容调用方。
|
||||
@@ -896,13 +974,15 @@ def sea_quote_detail_text(
|
||||
_ = first_or_same, history_work_order_no, status
|
||||
src = dict(quote or {})
|
||||
source = str(src.get("source_label") or "TMS标准报价").replace(" ", "")
|
||||
return "\n".join(
|
||||
[
|
||||
source,
|
||||
f"{work_order_no}价格明细",
|
||||
quote_area_fee_text(src, business_line=business_line or "SEA"),
|
||||
]
|
||||
)
|
||||
lines = [source, f"{work_order_no}价格明细"]
|
||||
if (business_line or "").upper() == "AIR":
|
||||
from agent.schema.air_route_options import route_codes
|
||||
|
||||
route = route_codes(None, {}, src)
|
||||
if route:
|
||||
lines.append(route)
|
||||
lines.append(quote_area_fee_text(src, business_line=business_line or "SEA"))
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def collab_hint(work_order_no: str) -> str:
|
||||
@@ -1007,6 +1087,11 @@ def tms_hit_card(
|
||||
src = dict(quote or {})
|
||||
source = str(src.get("source_label") or "TMS标准报价").replace(" ", "")
|
||||
lines = [source, f"{work_order_no}价格明细"]
|
||||
from agent.schema.air_route_options import route_codes
|
||||
|
||||
route = route_codes(None, {}, src) or str(src.get("route_line") or "").strip()
|
||||
if route:
|
||||
lines.append(route)
|
||||
lines.extend(
|
||||
quote_detail_lines(
|
||||
src,
|
||||
@@ -1022,6 +1107,32 @@ def tms_hit_card(
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def air_route_entry_card(*, work_order_no: str, count: int, url: str) -> dict[str, Any]:
|
||||
"""
|
||||
多条线路入口卡:短文案跳转 H5,避免企微截断。
|
||||
"""
|
||||
n = max(int(count or 0), 2)
|
||||
fallback = "https://ai.ytd-scm.com/inquiry-form"
|
||||
action_url = (url or "").strip() or fallback
|
||||
return {
|
||||
"msgtype": "template_card",
|
||||
"template_card": {
|
||||
"card_type": "text_notice",
|
||||
"source": {"desc": "TMS线路报价", "desc_color": 0},
|
||||
"main_title": {"title": f"{work_order_no}已查到{n}条线路"},
|
||||
"sub_title_text": "点击查看并选择一条线路,选定后回企微看价格明细。",
|
||||
"card_action": {"type": 1, "url": action_url},
|
||||
"jump_list": [{"type": 1, "title": "查看并选择线路", "url": action_url}],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def air_route_entry_text(*, work_order_no: str, count: int) -> str:
|
||||
"""入口卡失败时的文字兜底。"""
|
||||
n = max(int(count or 0), 2)
|
||||
return f"{work_order_no}已查到{n}条线路,请点击卡片查看并选择。"
|
||||
|
||||
|
||||
def tms_hit_wecom_payload(
|
||||
*,
|
||||
work_order_no: str,
|
||||
@@ -1062,7 +1173,7 @@ def tms_hit_wecom_payload(
|
||||
"main_title": {"title": f"{work_order_no}价格明细"},
|
||||
"quote_area": {
|
||||
"type": 0,
|
||||
"quote_text": quote_area_fee_text(src, business_line="AIR"),
|
||||
"quote_text": quote_area_safe_text(src, business_line="AIR"),
|
||||
},
|
||||
"sub_title_text": meta,
|
||||
"task_id": card_task_id("quote", work_order_no),
|
||||
@@ -1675,6 +1786,13 @@ def _clip(raw: str, limit: int) -> str:
|
||||
return text if len(text) <= limit else text[: limit - 1] + "…"
|
||||
|
||||
|
||||
def _air_scheme_route(quote: dict[str, Any], facts: dict[str, str]) -> str:
|
||||
"""方案卡航线:空运ZUH→中转→CRK。没有三字码才退回地名。"""
|
||||
from agent.schema.air_route_options import scheme_route_codes
|
||||
|
||||
return scheme_route_codes(quote, facts)
|
||||
|
||||
|
||||
def scheme_transport_mode(business_line: str = "", fallback: str = "空运") -> str:
|
||||
"""会话业务线 → 方案卡运输方式。海运不得再写成空运。"""
|
||||
raw = (business_line or "").strip()
|
||||
@@ -1736,7 +1854,7 @@ def scheme_horizontal_rows(
|
||||
pack = (src.get("包装方式") or src.get("包装类型") or "-").strip() or "-"
|
||||
return [
|
||||
{"keyname": "工单号", "value": _clip(work_order_no, 26)},
|
||||
{"keyname": "航线", "value": _clip(f"空运 {origin}→{dest}", 26)},
|
||||
{"keyname": "航线", "value": _clip(_air_scheme_route(q, src), 26)},
|
||||
{"keyname": "货物", "value": _clip(f"{cargo};{pieces};{weight};{volume}", 26)},
|
||||
{"keyname": "包装", "value": _clip(pack, 26)},
|
||||
{"keyname": "时效", "value": _clip(tms_eta_display(q), 26)},
|
||||
@@ -1798,11 +1916,8 @@ def scheme_wecom_payload(
|
||||
"main_title": {"title": f"{work_order_no}报价方案"},
|
||||
"quote_area": {
|
||||
"type": 0,
|
||||
"quote_text": (
|
||||
quote_area_safe_text(src, transport_mode=transport_mode)
|
||||
if (transport_mode or "").strip() in {"海运", "陆运"}
|
||||
else quote_area_fee_text(src, transport_mode=transport_mode)
|
||||
),
|
||||
# 空运/海运/陆运引用条一律只留总价,费用在上一条明细。
|
||||
"quote_text": quote_area_safe_text(src, transport_mode=transport_mode),
|
||||
},
|
||||
"horizontal_content_list": scheme_horizontal_rows(
|
||||
work_order_no=work_order_no,
|
||||
|
||||
@@ -107,6 +107,17 @@ def _emit_private_card(flow: Any, sess: Any, reply) -> None:
|
||||
)
|
||||
return
|
||||
sess.allowed = (copy.BTN_SKIP_COLLAB,)
|
||||
emit_air = getattr(flow, "_emit_air_hit", None)
|
||||
if callable(emit_air):
|
||||
emit_air(
|
||||
reply,
|
||||
work_order_no=no,
|
||||
quote=quote,
|
||||
first_or_same=first,
|
||||
history_work_order_no=history,
|
||||
status="已报价",
|
||||
)
|
||||
return
|
||||
reply(
|
||||
copy.tms_hit_card(
|
||||
work_order_no=no,
|
||||
|
||||
@@ -95,6 +95,12 @@ def ticket_view(ticket: object) -> dict[str, Any]:
|
||||
"product_names": list(getattr(ticket, "product_names", []) or []),
|
||||
"sales_name": str(getattr(ticket, "sales_name", "") or ""),
|
||||
"lost_reason": str(getattr(ticket, "lost_reason", "") or ""),
|
||||
"system_exception": str(getattr(ticket, "system_exception", "") or ""),
|
||||
"systemException": str(getattr(ticket, "system_exception", "") or ""),
|
||||
"system_exception_reason": str(getattr(ticket, "system_exception_reason", "") or ""),
|
||||
"systemExceptionReason": str(getattr(ticket, "system_exception_reason", "") or ""),
|
||||
"system_exception_step": str(getattr(ticket, "system_exception_step", "") or ""),
|
||||
"system_exception_payload": dict(getattr(ticket, "system_exception_payload", {}) or {}),
|
||||
}
|
||||
|
||||
|
||||
@@ -418,6 +424,16 @@ def hydrate_ticket(flow: Any, ticket: object) -> object:
|
||||
merged["collab_facts"] = collab
|
||||
if view.get("collab_chat_id"):
|
||||
merged["collab_chat_id"] = view.get("collab_chat_id")
|
||||
for key in (
|
||||
"system_exception",
|
||||
"systemException",
|
||||
"system_exception_reason",
|
||||
"systemExceptionReason",
|
||||
"system_exception_step",
|
||||
"system_exception_payload",
|
||||
):
|
||||
if full_view.get(key) and not merged.get(key):
|
||||
merged[key] = full_view.get(key)
|
||||
return merged
|
||||
|
||||
|
||||
|
||||
@@ -27,6 +27,8 @@ STEP_LLM_NO_TICKET = copy.STEP_LLM_NO_TICKET
|
||||
STEP_LLM = copy.STEP_LLM
|
||||
STEP_TMS_QUOTE = copy.STEP_TMS_QUOTE
|
||||
STEP_TMS_CABIN = copy.STEP_TMS_CABIN
|
||||
STEP_TMS_LOCK = copy.STEP_TMS_LOCK
|
||||
STEP_TMS_RELEASE = copy.STEP_TMS_RELEASE
|
||||
STEP_GROUP = copy.STEP_GROUP
|
||||
|
||||
MAX_AUTO_RETRIES = 3
|
||||
@@ -140,15 +142,6 @@ def _now_text(now: datetime | None) -> str:
|
||||
return stamp.strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
|
||||
def _test_prefix() -> bool:
|
||||
try:
|
||||
from agent.config import get_settings
|
||||
|
||||
return (get_settings().ytd_env or "").strip().lower() == "test"
|
||||
except Exception: # noqa: BLE001
|
||||
return False
|
||||
|
||||
|
||||
def notify_app_ready() -> bool:
|
||||
"""询价消息服务已配才推告警。"""
|
||||
from agent.policy.deal_outcome import notify_app_ready as deal_ready
|
||||
@@ -244,7 +237,6 @@ def confirm_system_exception(
|
||||
users = _list_notify_users(ledger) if ledger is not None else []
|
||||
if ready and users:
|
||||
occurred = _now_text(now)
|
||||
test_prefix = _test_prefix()
|
||||
payload = copy.system_exception_alert_payload(
|
||||
work_order_no=no,
|
||||
exception_type=event.exception_type,
|
||||
@@ -252,7 +244,6 @@ def confirm_system_exception(
|
||||
error_code=event.error_code,
|
||||
step=event.step,
|
||||
occurred_at=occurred,
|
||||
test_prefix=test_prefix,
|
||||
)
|
||||
text = copy.system_exception_alert_text(
|
||||
work_order_no=no,
|
||||
@@ -261,7 +252,6 @@ def confirm_system_exception(
|
||||
error_code=event.error_code,
|
||||
step=event.step,
|
||||
occurred_at=occurred,
|
||||
test_prefix=test_prefix,
|
||||
)
|
||||
sender = enqueue or _enqueue_default
|
||||
for row in users:
|
||||
@@ -626,7 +616,7 @@ def run_admin_retry(work_order_no: str, *, ledger: Any = None) -> dict[str, Any]
|
||||
reply("请把刚才那句话再发一次。")
|
||||
return {"ok": True, "step": step}
|
||||
|
||||
if step == STEP_TMS_CABIN:
|
||||
if step in {STEP_TMS_CABIN, STEP_TMS_LOCK, STEP_TMS_RELEASE}:
|
||||
return _resume_cabin_step(
|
||||
ledger=book,
|
||||
work_order_no=no,
|
||||
@@ -634,6 +624,7 @@ def run_admin_retry(work_order_no: str, *, ledger: Any = None) -> dict[str, Any]
|
||||
target=target,
|
||||
status=status,
|
||||
reply=reply,
|
||||
step=step,
|
||||
)
|
||||
|
||||
return {"ok": False, "error": "unknown_step", "step": step}
|
||||
@@ -716,10 +707,14 @@ def _resume_tms_quote(
|
||||
copy.remember_tms_quote(sess, quote)
|
||||
flow._save(sess)
|
||||
if biz == "AIR":
|
||||
reply(
|
||||
copy.tms_hit_card(work_order_no=no, quote=quote, status="已报价"),
|
||||
copy.tms_hit_wecom_payload(work_order_no=no, quote=quote, status="已报价"),
|
||||
)
|
||||
emit_air = getattr(flow, "_emit_air_hit", None)
|
||||
if callable(emit_air):
|
||||
emit_air(reply, work_order_no=no, quote=quote, status="已报价")
|
||||
else:
|
||||
reply(
|
||||
copy.tms_hit_card(work_order_no=no, quote=quote, status="已报价"),
|
||||
copy.tms_hit_wecom_payload(work_order_no=no, quote=quote, status="已报价"),
|
||||
)
|
||||
return
|
||||
emit = getattr(flow, "_emit_sea_hit", None)
|
||||
if callable(emit):
|
||||
@@ -742,13 +737,17 @@ def _resume_cabin_step(
|
||||
target: str,
|
||||
status: str,
|
||||
reply: ReplyFn,
|
||||
step: str = "",
|
||||
) -> dict[str, Any]:
|
||||
"""重试空运锁舱或释放:payload 里有 lock_id 就是释放。"""
|
||||
no = (work_order_no or "").strip()
|
||||
lock_id = str(extra.get("lock_id") or "").strip()
|
||||
option_no = str(extra.get("option_no") or "").strip() or "AIR-OPT-01"
|
||||
option_no = str(extra.get("option_no") or "").strip()
|
||||
instruction = str(extra.get("instruction") or "")
|
||||
sender = str(extra.get("sender_id") or target or "").strip()
|
||||
cabin_step = STEP_TMS_RELEASE if lock_id else STEP_TMS_LOCK
|
||||
if (step or "").strip() in {STEP_TMS_LOCK, STEP_TMS_RELEASE}:
|
||||
cabin_step = (step or "").strip()
|
||||
if lock_id:
|
||||
releaser = getattr(ledger, "release_cabin", None)
|
||||
out = call_with_retries(
|
||||
@@ -782,10 +781,19 @@ def _resume_cabin_step(
|
||||
service = "空运锁舱"
|
||||
reason = "空运锁舱接口调用失败"
|
||||
if not (out or {}).get("ok"):
|
||||
why = copy.air_cabin_fail_reason(
|
||||
tms_reason=str((out or {}).get("failReason") or (out or {}).get("error") or ""),
|
||||
flight_date=str(extra.get("flight_date") or ""),
|
||||
)
|
||||
operator = str(extra.get("operator_name") or "同事").strip() or "同事"
|
||||
if cabin_step == STEP_TMS_RELEASE or lock_id:
|
||||
reply(copy.air_release_fail(work_order_no=no, operator=operator, reason=why))
|
||||
else:
|
||||
reply(copy.air_lock_fail(work_order_no=no, operator=operator, reason=why))
|
||||
confirm_system_exception(
|
||||
event=SystemExceptionEvent(
|
||||
exception_type=TYPE_TMS,
|
||||
step=STEP_TMS_CABIN,
|
||||
step=cabin_step,
|
||||
reason=reason,
|
||||
service=service,
|
||||
error_code=str((out or {}).get("failReason") or (out or {}).get("error") or "CABIN_FAIL"),
|
||||
@@ -796,10 +804,9 @@ def _resume_cabin_step(
|
||||
extra=dict(extra),
|
||||
),
|
||||
ledger=ledger,
|
||||
reply=reply,
|
||||
)
|
||||
return {"ok": False, "error": "still_fail", "step": STEP_TMS_CABIN}
|
||||
reply(copy.system_exception_recovery(STEP_TMS_CABIN))
|
||||
return {"ok": False, "error": "still_fail", "step": cabin_step}
|
||||
reply(copy.system_exception_recovery(cabin_step))
|
||||
clear_system_exception(ledger=ledger, work_order_no=no)
|
||||
if not lock_id:
|
||||
exec_no = str((out or {}).get("lockId") or (out or {}).get("tmsRequestId") or "").strip()
|
||||
@@ -813,4 +820,4 @@ def _resume_cabin_step(
|
||||
reply(copy.air_lock_ok(work_order_no=no, operator="同事", option_no=option_no, exec_no=exec_no, quote_no=""))
|
||||
else:
|
||||
reply(copy.air_release_ok(work_order_no=no, operator="同事", option_no="", exec_no=str((out or {}).get("releaseId") or lock_id), quote_no=""))
|
||||
return {"ok": True, "step": STEP_TMS_CABIN}
|
||||
return {"ok": True, "step": cabin_step}
|
||||
|
||||
Reference in New Issue
Block a user