后台成本价、销售价、利润点按费用逐行展示,费用合计用报价官方总价。选完线路只关页面。报价单文件名带上模板名,空运起运地和转人工跟对应工单走。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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 出文件后再出成交卡,这里不提前发卡
|
||||
|
||||
Reference in New Issue
Block a user