机器人显示名与站点标题可配;多段联运列表投影;协同群监听对账;工单附件落存;模板与工单后台补强
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -835,9 +835,16 @@ class AirTextInquiryFlow:
|
||||
facts = dict(view.get("facts") or {})
|
||||
quote = dict(view.get("quote") or {})
|
||||
status = str(view.get("status") or "").strip()
|
||||
lost_reason = str(view.get("lost_reason") or view.get("lostReason") or "").strip()
|
||||
tms_quote = dict(view.get("tms_quote") or view.get("tmsQuote") or {})
|
||||
phase = str(view.get("wait_phase") or "").strip() or self._infer_wait_phase(
|
||||
status=status, quote=quote
|
||||
status=status, quote=quote, lost_reason=lost_reason
|
||||
)
|
||||
# 主账询价中+舱位备选时 wait_phase 常被收成 tms_miss;续办必须回到选舱。
|
||||
if self._quote_pending_cabin(quote) or self._quote_pending_cabin(tms_quote):
|
||||
phase = "wait_air_route"
|
||||
if not quote and tms_quote:
|
||||
quote = dict(tms_quote)
|
||||
allowed: tuple[str, ...] = ()
|
||||
if phase == "wait_collab":
|
||||
allowed = (copy.BTN_SKIP_COLLAB,)
|
||||
@@ -855,6 +862,8 @@ class AirTextInquiryFlow:
|
||||
allowed = (copy.BTN_DEAL, copy.BTN_LOST, copy.BTN_NEGOTIATE)
|
||||
elif phase == "wait_lost_reason":
|
||||
allowed = ()
|
||||
elif phase == "wait_air_route":
|
||||
allowed = ()
|
||||
return FlowSession(
|
||||
sender_id=sender_id,
|
||||
thread_id=sender_id,
|
||||
@@ -863,6 +872,7 @@ class AirTextInquiryFlow:
|
||||
facts=facts,
|
||||
work_order_no=no,
|
||||
quote=quote,
|
||||
tms_quote=tms_quote or dict(quote),
|
||||
status=status,
|
||||
allowed=allowed,
|
||||
wait_version=1,
|
||||
@@ -870,17 +880,50 @@ class AirTextInquiryFlow:
|
||||
collab_chat_id=str(view.get("collab_chat_id") or ""),
|
||||
)
|
||||
|
||||
def _infer_wait_phase(self, *, status: str, quote: dict[str, Any]) -> str:
|
||||
def _infer_wait_phase(
|
||||
self, *, status: str, quote: dict[str, Any], lost_reason: str = ""
|
||||
) -> str:
|
||||
if status in {"已成交", "已关闭"}:
|
||||
return "done"
|
||||
if status == "未成交":
|
||||
return "wait_lost_reason"
|
||||
# 原因已记则终态回放;缺原因才继续要原因。
|
||||
return "wait_lost_reason" if not (lost_reason or "").strip() else "done"
|
||||
if status == "协商中":
|
||||
return "wait_deal"
|
||||
if status == "已报价" and quote:
|
||||
return "wait_adopt"
|
||||
return "tms_miss"
|
||||
|
||||
def _lost_reason_of(self, sess: FlowSession) -> str:
|
||||
"""从主账读未成交原因;续办终态回放要用,不能靠进程内字段。"""
|
||||
no = (sess.work_order_no or "").strip()
|
||||
if not no:
|
||||
return ""
|
||||
getter = getattr(self._ledger, "get_for_agent", None)
|
||||
if not callable(getter):
|
||||
return ""
|
||||
try:
|
||||
view = getter(work_order_no=no, sender_id=sess.sender_id) or {}
|
||||
except Exception:
|
||||
logger.warning("读未成交原因失败 wo=%s", no)
|
||||
return ""
|
||||
return str(view.get("lost_reason") or view.get("lostReason") or "").strip()
|
||||
|
||||
@staticmethod
|
||||
def _quote_pending_cabin(quote: dict[str, Any] | None) -> bool:
|
||||
"""报价快照是否仍停在空运选舱(pending_cabin_pick=1 或未选多舱位)。"""
|
||||
src = dict(quote or {})
|
||||
flag = str(src.get("pending_cabin_pick") or src.get("pendingCabinPick") or "").strip()
|
||||
if flag in {"1", "true", "True", "yes"}:
|
||||
return True
|
||||
from agent.schema.air_route_options import parse_air_options
|
||||
|
||||
if src.get("selected_air_option") or src.get("selectedAirOption"):
|
||||
return False
|
||||
return len(parse_air_options(src)) > 0 and not (
|
||||
str(src.get("total") or "").strip()
|
||||
)
|
||||
|
||||
def _align_resume_phase(self, sess: FlowSession) -> None:
|
||||
"""
|
||||
报价文件已经发给销售,续办必须回到成交跟进。
|
||||
@@ -971,8 +1014,23 @@ class AirTextInquiryFlow:
|
||||
if sess.phase == "wait_lost_reason":
|
||||
reply(copy.deal_ack("未成交", sess.work_order_no))
|
||||
return "wait_lost_reason"
|
||||
if sess.phase == "wait_air_route":
|
||||
quote = dict(sess.tms_quote or sess.quote or {})
|
||||
return self._offer_air_routes(
|
||||
sess=sess,
|
||||
work_order_no=sess.work_order_no,
|
||||
quote=quote,
|
||||
reply=reply,
|
||||
cabin_pick=self._quote_pending_cabin(quote),
|
||||
)
|
||||
if sess.phase == "done":
|
||||
reply(copy.deal_ack(sess.status or "已关闭", sess.work_order_no))
|
||||
status = (sess.status or "").strip() or "已关闭"
|
||||
lost = self._lost_reason_of(sess) if status == "未成交" else ""
|
||||
# 未成交且原因已记:续办只重播「原因已记、工单已关」,禁止再要原因。
|
||||
if status == "未成交" and lost:
|
||||
reply(copy.lost_reason_ack(lost, work_order_no=sess.work_order_no))
|
||||
return "done"
|
||||
reply(copy.deal_ack(status, sess.work_order_no, lost_reason=lost))
|
||||
return "done"
|
||||
reply(copy.inquiry_card(work_order_no=sess.work_order_no, facts=sess.facts))
|
||||
return sess.phase or "replay"
|
||||
@@ -1393,9 +1451,13 @@ class AirTextInquiryFlow:
|
||||
)
|
||||
|
||||
options = parse_air_options(quote)
|
||||
from agent.schema.field_validate import is_blank_quote_date
|
||||
|
||||
|
||||
# 没填报价日期:不按起运地收口,TMS 返回的舱位全部给销售点。1 条也要点。
|
||||
# 未选定前六态保持询价中,禁止把备选舱位费写进后台「已报价」。
|
||||
if not str((sess.facts or {}).get("报价日期") or "").strip() and options:
|
||||
# 「(非必填):」占位文案也当没填,必须进选舱。
|
||||
if is_blank_quote_date((sess.facts or {}).get("报价日期")) and options:
|
||||
quote = dict(quote)
|
||||
quote["airOptions"] = sort_cabin_options(options)
|
||||
ledger_quote = _ledger_quote_pending_cabin(quote)
|
||||
@@ -1894,6 +1956,7 @@ class AirTextInquiryFlow:
|
||||
if not callable(renderer):
|
||||
reply(copy.quote_template_missing())
|
||||
return "wait_adopt"
|
||||
# 私聊采用出单:填好的报价单要进后台附件;激活群发模版另走 persist=False。
|
||||
result = renderer(
|
||||
work_order_no=sess.work_order_no,
|
||||
business_line=sess.business_line or "AIR",
|
||||
@@ -1901,6 +1964,7 @@ class AirTextInquiryFlow:
|
||||
quote=dict(sess.quote or {}),
|
||||
format=fmt,
|
||||
sales_wecom_id=sess.sender_id,
|
||||
persist_attachment=True,
|
||||
) or {}
|
||||
if not result.get("ok"):
|
||||
reply(str(result.get("message") or copy.quote_template_missing()))
|
||||
@@ -2298,7 +2362,7 @@ _FLOW_GUARD = threading.Lock()
|
||||
|
||||
|
||||
def build_ledger() -> Any:
|
||||
"""生产打 8180;单测 LEDGER_BACKEND=memory。"""
|
||||
"""生产打 8080;单测 LEDGER_BACKEND=memory。"""
|
||||
from agent.config import get_settings
|
||||
|
||||
backend = (get_settings().ledger_backend or "http").strip().lower()
|
||||
|
||||
Reference in New Issue
Block a user