后台成本价、销售价、利润点按费用逐行展示,费用合计用报价官方总价。选完线路只关页面。报价单文件名带上模板名,空运起运地和转人工跟对应工单走。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-05 12:33:34 +08:00
co-authored by Cursor
parent 7c77e45e03
commit e95e8869a6
30 changed files with 1926 additions and 75 deletions
+133 -6
View File
@@ -337,13 +337,134 @@ class AirTextInquiryFlow:
self.drop_current_bookmark(sender_id)
return None
def _handoff_flows(self) -> list[Any]:
"""陆运、海运、空运各查自己的工单索引。当前流程放前面,避免单测实例对不上全局。"""
from agent.policy.land_text_flow import get_land_text_flow
from agent.policy.sea_text_flow import get_sea_text_flow
flows: list[Any] = [self]
for flow in (get_land_text_flow(), get_sea_text_flow(), get_air_text_flow()):
if all(flow is not item for item in flows):
flows.append(flow)
return flows
def _handoff_owner_for_line(self, line: str):
"""
转人工入口跟工单运输方式走,不跟当前私聊里的另一张单走。
空运、海运、陆运各用自己的流程收书签。对不上就返回空,不能默认成空运。
"""
from agent.policy.land_text_flow import LandTextInquiryFlow, get_land_text_flow
from agent.policy.sea_text_flow import SeaTextInquiryFlow, get_sea_text_flow
code = (line or "").strip().upper()
if code == "LAND":
return self if isinstance(self, LandTextInquiryFlow) else get_land_text_flow()
if code == "SEA":
return (
self
if isinstance(self, SeaTextInquiryFlow)
and not isinstance(self, LandTextInquiryFlow)
else get_sea_text_flow()
)
if code == "AIR":
return self if type(self) is AirTextInquiryFlow else get_air_text_flow()
return None
def _offer_handoff_for_named_ticket(
self,
*,
sender_id: str,
work_order_no: str,
text: str,
reply: ReplyFn,
) -> str:
"""
按原话里的工单号转人工。
有没有协同群只决定拦不拦。话术只看这张单是空运、海运还是陆运。
销售私聊里另一张已建群的单,不能拿来拒绝,也不能拿来决定话术。
副作用:命中后把这张单收到对应运输方式的转人工入口;读主账。不建群。
"""
from agent.policy.handoff_human import is_closed_for_handoff
no = (work_order_no or "").strip().upper()
hit = None
hit_flow = None
for flow in self._handoff_flows():
found = flow.session_by_ticket(sender_id, no)
if found is None:
continue
hit = found
hit_flow = flow
break
getter = getattr((hit_flow or self)._ledger, "get_for_agent", None)
view: dict[str, Any] = {}
if callable(getter):
try:
view = getter(work_order_no=no, sender_id=sender_id) or {}
except Exception:
logger.exception("转人工读主账失败 wo=%s", no)
view = {}
if view.get("found") and view.get("owned") is False:
reply("这张工单不属于当前账号,不能转人工。")
return "ticket_forbidden"
if hit is not None and view.get("found"):
# 主账上的运输方式和群号盖过进程里的旧书签。
hit.collab_chat_id = str(view.get("collab_chat_id") or "").strip()
if str(view.get("status") or "").strip():
hit.status = str(view.get("status") or "").strip()
if str(view.get("business_line") or "").strip():
hit.business_line = str(view.get("business_line") or "").strip().upper()
if hit is None:
if not callable(getter):
reply(f"找不到工单 {no},请核对工单号后再试。")
return "ticket_missing"
if not view.get("found"):
reply(f"找不到工单 {no},请核对工单号后再试。")
return "ticket_missing"
line = str(view.get("business_line") or "").strip().upper()
owner = self._handoff_owner_for_line(line)
if owner is None:
reply(copy.ASK_TRANSPORT)
return "need_mode"
hit = owner._session_from_ledger(sender_id=sender_id, view=view)
line = (hit.business_line or "").strip().upper()
owner = self._handoff_owner_for_line(line)
if owner is None:
reply(copy.ASK_TRANSPORT)
return "need_mode"
logger.info(
"handoff.named wo=%s line=%s phase=%s chat=%s",
no,
line or "-",
hit.phase or "-",
"yes" if (hit.collab_chat_id or "").strip() else "no",
)
if is_closed_for_handoff(hit.status):
reply("该工单已结束,不能再转人工。")
return "handoff_closed"
if owner._block_if_handoff_committed(hit, reply):
return "handoff_done"
return owner._offer_handoff_now(
sender_id=sender_id, reason="intent", reply=reply, text=text, sess=hit
)
def offer_handoff(self, *, sender_id: str, text: str, reply: ReplyFn) -> str:
"""
路由判定销售明确要求人来接管。运输方式不明则继续补问运输方式。
原话里写了工单号时,只处理那一张。
有没有协同群只决定拦不拦;空运、海运、陆运各走各的转人工。
"""
from agent.llm.extract_text import detect_transport_mode
from agent.policy.handoff_human import is_closed_for_handoff
named = copy.extract_work_order_no(text)
if named:
return self._offer_handoff_for_named_ticket(
sender_id=sender_id, work_order_no=named, text=text, reply=reply
)
sess = self.session_of(sender_id)
if sess and is_closed_for_handoff(sess.status):
reply("该工单已结束,不能再转人工。")
@@ -1572,7 +1693,15 @@ class AirTextInquiryFlow:
if not result.get("ok"):
reply(str(result.get("message") or copy.quote_template_missing()))
return "wait_adopt"
filename = str(result.get("fileName") or f"{sess.work_order_no}_quote.{fmt}")
from agent.quote_templates.filename import filename_from_render
# 私聊直接生成与群里同一套名字:工单号 + 模板名 + 后缀。PDF 用 .pdf,表格用 .xlsx。
xlsx_name = filename_from_render(sess.work_order_no, result, suffix=".xlsx")
filename = (
filename_from_render(sess.work_order_no, result, suffix=".pdf")
if kind == "PDF"
else xlsx_name
)
from agent.policy.event_exception import clear_wait
clear_wait(self._ledger, work_order_no=sess.work_order_no)
@@ -1582,20 +1711,18 @@ class AirTextInquiryFlow:
result,
work_order_no=sess.work_order_no,
sender_id=sess.sender_id,
filename=filename if filename.lower().endswith(".pdf") else f"{sess.work_order_no}_quote.pdf",
filename=filename,
quote=dict(sess.quote or {}),
facts=dict(sess.facts or {}),
)
if not queued:
fallback = str(result.get("fileName") or f"{sess.work_order_no}_quote.xlsx")
if not fallback.lower().endswith(".xlsx"):
fallback = f"{sess.work_order_no}_quote.xlsx"
reply("PDF 转换排队失败,已按后台模板发送 Excel 报价单。")
file_extra = self._quote_file_payload(result, filename=xlsx_name)
return self._emit_file_then_deal(
sess,
reply,
kind="Excel",
filename=fallback,
filename=xlsx_name,
file_extra=file_extra,
)
# 已入队:等 Worker 出文件后再出成交卡,这里不提前发卡