落地转人工:三次补问或明确接管后出入口,真正转人工后冻结本票并允许新开询价。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-20 16:42:13 +08:00
co-authored by Cursor
parent 26dd5d72a1
commit 7eedf28e8d
23 changed files with 1499 additions and 132 deletions
@@ -30,6 +30,7 @@ from agent.policy.air_text_flow import get_air_text_flow
from agent.policy.sea_group_ops import (
capture_group_lost_reason,
group_client_of,
is_handoff_ticket,
send_group_text,
ticket_view,
)
@@ -112,6 +113,12 @@ def handle_air_group(
return "duplicate_inbound"
mark_air_text(chat_id=chat_id, sender_id=message.sender_id, text=text)
wo = copy.extract_work_order_no(text)
if wo:
from agent.policy.handoff_human import try_commit_air_group_handoff
handed = try_commit_air_group_handoff(engine, chat_id=chat_id, work_order_no=wo)
if handed:
return handed
intent = classify_air_group_text(text, injected=injected_intent)
logger.info(
"air_group 文字 chat=%s sender=%s intent=%s wo=%s",
@@ -138,6 +145,9 @@ def handle_air_group(
)
if ticket is None:
return phase
if is_handoff_ticket(ticket):
logger.info("air_group 转人工不再跟 chat=%s", chat_id)
return "handoff_ignore"
if intent == INTENT_ACTIVATE:
return phase
else:
@@ -145,6 +155,9 @@ def handle_air_group(
if ticket is None:
send_group_text(group_client_of(engine), chat_id, copy.air_need_work_order())
return "need_work_order"
if is_handoff_ticket(ticket):
logger.info("air_group 转人工不再跟 chat=%s", chat_id)
return "handoff_ignore"
return _handle_bound(
engine,
message,
@@ -177,6 +190,9 @@ def _handle_file(engine, message: InboundMessage) -> str:
if ticket is None:
send_group_text(group_client_of(engine), chat_id, copy.air_need_work_order())
return "need_work_order"
if is_handoff_ticket(ticket):
logger.info("air_group 转人工不再跟 chat=%s", chat_id)
return "handoff_ignore"
return apply_air_file(
flow=engine,
ticket=ticket,
+9 -1
View File
@@ -22,6 +22,7 @@ from agent.policy.sea_group_ops import (
group_client_of,
handle_group_file,
hydrate_ticket,
is_handoff_ticket,
peek_pending_product_quote,
send_group_text,
ticket_no,
@@ -66,7 +67,7 @@ def handle_group_inbound(
群里一条文字或附件。
文字走 DeepSeek A(单测可注入);附件只看扩展名。
协同已结束或终态要回话,不能当没看见。
已成交/未成交/已关闭要回话。真正转人工后群里不再跟、不回。
"""
if (message.chat_type or "") != "group":
return "group_ignore"
@@ -100,6 +101,13 @@ def handle_group_inbound(
return "group_ignore"
sess = _session_of_ticket(engine, ticket)
if is_handoff_ticket(ticket, sess):
logger.info(
"group_inbound 转人工不再跟 wo=%s chat=%s",
ticket_no(ticket),
chat_id,
)
return "handoff_ignore"
view = ticket_view(ticket)
if view.get("collab_ended") or (sess is not None and getattr(sess, "collab_ended", False)):
from agent.policy import inquiry_copy as copy
+56 -11
View File
@@ -22,9 +22,13 @@ logger = logging.getLogger(__name__)
def _front_bookmark(sess) -> bool:
"""补问/核对手稿优先于已出票旧书签。空的 need_mode 不算(那是丢会话后的误伤)。"""
"""补问/核对手稿优先于已出票旧书签。空的 need_mode 不算(那是丢会话后的误伤)。已转人工的票也不再当当前询价。"""
if sess is None:
return False
from agent.policy.handoff_human import is_handoff_frozen
if is_handoff_frozen(sess):
return False
phase = (sess.phase or "").strip()
line = (sess.business_line or "").upper()
if phase == "need_mode" and not line:
@@ -32,16 +36,41 @@ def _front_bookmark(sess) -> bool:
return phase in {"clarify", "need_mode", "need_land_options", "wait_confirm"}
def should_resume_other_as_inquiry(message: InboundMessage) -> bool:
"""
A 把补问短答标成闲聊时,仍续填当前询价。
只拦还在问字段/选项/核对、未成交原因、转人工入口。
已报价协同中不拦,避免「谢谢」被当成新询价。
"""
if not message.has_sender():
return False
engine = _pick_text_flow(
sender_id=message.sender_id, text=message.content or ""
)
sess = engine.session_for_action(
message.sender_id, message.raw, text=message.content or ""
)
if sess is None:
return False
phase = (sess.phase or "").strip()
if phase in {"wait_lost_reason", "handoff_offer"}:
return True
return _front_bookmark(sess)
def _pick_text_flow(*, sender_id: str, text: str, injected_mode: str = "", prefer_quoted: bool = False):
"""
按当前书签或本句运输方式选空运/海运/陆运流程。
陆运核对卡上的确定优先于旧海运书签,避免「确定」被海运/空运吃掉。
A 已判成调价时交给当前已报价工单,避免旧海运书签抢走。
正在补问时,补问书签优先于已报价调价,避免第 3 次反问出不了转人工卡。
A 已判成调价且当前不是补问:交给当前已报价工单,避免旧海运书签抢走。
图片识图在 Worker 写下的补问书签,HTTP 必须从 Redis 读到,否则「托盘」会变成再问运输方式。
"""
from agent.schema.land_options import looks_like_confirm
from agent.policy.quote_adjust_ops import quoted_adjust_flow
from agent.policy.handoff_human import is_handoff_frozen
land = get_land_text_flow()
if looks_like_confirm(text):
@@ -51,21 +80,22 @@ def _pick_text_flow(*, sender_id: str, text: str, injected_mode: str = "", prefe
retry = land.restore_tms_retry_session(sender_id)
if retry:
return land
if prefer_quoted:
quoted = quoted_adjust_flow(sender_id)
if quoted is not None:
return quoted
sea = get_sea_text_flow()
air = get_air_text_flow()
sea_sess = sea.session_of(sender_id)
land_sess = land.session_of(sender_id)
air_sess = air.session_of(sender_id)
# 补问/核对手稿优先于「已报价调价」,避免第 3 次反问被调价路由冲掉、出不了转人工卡。
for sess, flow in ((land_sess, land), (air_sess, air), (sea_sess, sea)):
if _front_bookmark(sess):
return flow
if sea_sess and (sea_sess.business_line or "").upper() == "SEA":
if prefer_quoted:
quoted = quoted_adjust_flow(sender_id)
if quoted is not None:
return quoted
if sea_sess and not is_handoff_frozen(sea_sess) and (sea_sess.business_line or "").upper() == "SEA":
return sea
if land_sess and (land_sess.business_line or "").upper() == "LAND":
if land_sess and not is_handoff_frozen(land_sess) and (land_sess.business_line or "").upper() == "LAND":
return land
mode = (injected_mode or detect_transport_mode(text) or "").upper()
if mode == "SEA":
@@ -140,6 +170,7 @@ def handle_text_inquiry(
flow=None,
store=None,
prefer_quoted: bool = False,
prefer_handoff: bool = False,
) -> str:
"""
处理一条私聊文字询价。
@@ -177,6 +208,15 @@ def handle_text_inquiry(
logger.info("text_inquiry 出站 ok=%s info=%s phase_reply=%s", ok, info, body[:80])
return info if ok else ""
if prefer_handoff:
phase = engine.offer_handoff(
sender_id=message.sender_id,
text=message.content or "",
reply=reply,
)
logger.info("text_inquiry handoff phase=%s sender=%s", phase, message.sender_id)
return phase
phase = engine.on_text(
sender_id=message.sender_id,
text=message.content or "",
@@ -194,21 +234,26 @@ def handle_text_inquiry(
def handle_text_other(message: InboundMessage, *, store=None) -> str:
"""
闲聊/其它:若有待点按钮则当按钮;否则回首次单聊话术。
闲聊/其它:补问短答续填;待点按钮当按钮;否则回首次单聊话术。
"""
from agent.policy import inquiry_copy as copy
if not message.has_sender():
return "reject_no_sender"
msg_store = store or get_message_store()
if should_resume_other_as_inquiry(message):
logger.info(
"text_other 续填当前询价 sender=%s chars=%s",
message.sender_id,
len((message.content or "").strip()),
)
return handle_text_inquiry(message, store=msg_store)
engine = _pick_text_flow(
sender_id=message.sender_id, text=message.content or ""
)
sess = engine.session_for_action(
message.sender_id, message.raw, text=message.content or ""
)
if sess and sess.phase == "wait_lost_reason":
return handle_text_inquiry(message, store=msg_store)
btn = engine.match_button(message.content or "", sess)
if btn:
return handle_text_inquiry(message, store=msg_store)