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

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
+151 -15
View File
@@ -77,6 +77,7 @@ class SeaTextInquiryFlow(AirTextInquiryFlow):
if not (sender_id or "").strip():
return "reject_no_sender"
sess = self.session_for_action(sender_id, inbound_raw, text=text)
sess = self._detach_frozen_bookmark(sess, sender_id)
btn = self.match_button(text, sess)
if btn:
return self.on_button(
@@ -173,25 +174,26 @@ class SeaTextInquiryFlow(AirTextInquiryFlow):
for key in tms_ready.get("missing") or []:
if key not in keys:
keys.append(key)
reply(
copy.ask_clarify(
new_sess = FlowSession(
sender_id=sender_id,
thread_id=sender_id,
phase="clarify",
business_line="SEA",
facts=facts,
immutable_text=sess.immutable_text if sess and sess.immutable_text else text,
wait_version=(sess.wait_version + 1) if sess else 1,
)
return self._clarify_or_handoff(
sess=sess,
keys=keys,
new_sess=new_sess,
reply=reply,
clarify_text=copy.ask_clarify(
facts=facts,
missing_keys=keys,
transport_mode="海运",
)
),
)
self._save(
FlowSession(
sender_id=sender_id,
thread_id=sender_id,
phase="clarify",
business_line="SEA",
facts=facts,
immutable_text=sess.immutable_text if sess and sess.immutable_text else text,
wait_version=(sess.wait_version + 1) if sess else 1,
)
)
return "clarify"
return self._create_and_quote(
sender_id=sender_id,
@@ -211,6 +213,11 @@ class SeaTextInquiryFlow(AirTextInquiryFlow):
"""建海运工单、先出确认卡、再查 TMS。"""
import uuid
from agent.policy.handoff_human import void_virtual_no
old = self.session_of(sender_id)
if old:
void_virtual_no(old)
self._save(
FlowSession(
sender_id=sender_id,
@@ -423,6 +430,8 @@ class SeaTextInquiryFlow(AirTextInquiryFlow):
) -> str:
sess = self.session_for_action(sender_id, card_meta)
if sess and action == copy.BTN_PULL_COLLAB and action in sess.allowed:
if sess.handoff_offer and not sess.handoff_committed:
return self._commit_handoff_pull(sess, reply, card_meta=card_meta)
return self._pull_collab_group(sess, reply, card_meta=card_meta)
if action in {copy.BTN_DEAL, copy.BTN_LOST, copy.BTN_NEGOTIATE}:
from agent.policy.sea_group_ops import explain_deal_click
@@ -600,6 +609,133 @@ class SeaTextInquiryFlow(AirTextInquiryFlow):
)
return "collab_group"
def _match_handoff_staff(self, sess: FlowSession) -> list[dict[str, Any]]:
"""
转人工拉人:线路/港口已齐走原匹配;未齐则拉该运输方式全部启用产品。
"""
line = (sess.business_line or "SEA").upper()
facts = dict(sess.facts or {})
if line == "LAND":
if str(facts.get("线路类别") or "").strip():
return self._match_sea_staff(sess)
lister = getattr(self._ledger, "list_staff_by_role", None)
if not callable(lister):
return []
try:
rows = lister(role_code="land") or []
except Exception:
logger.exception("转人工拉全部陆运产品失败")
return []
return [dict(x) for x in rows if isinstance(x, dict)]
origin = str(facts.get("起运港") or facts.get("起运地") or "").strip()
dest = str(facts.get("目的港") or facts.get("目的地") or "").strip()
if origin or dest:
return self._match_sea_staff(sess)
lister = getattr(self._ledger, "list_staff_by_role", None)
if not callable(lister):
return []
try:
rows = lister(role_code="sea") or []
except Exception:
logger.exception("转人工拉全部海运产品失败")
return []
return [dict(x) for x in rows if isinstance(x, dict)]
def _commit_handoff_pull(
self,
sess: FlowSession,
reply: ReplyFn,
card_meta: Optional[dict[str, Any]] = None,
) -> str:
"""
点转人工「拉产品进群」:建群、发转人工摘要、正式单改「转人工」。
虚拟单不写主账。不发 TMS、不发模板、不跟成交。
"""
from agent.policy.handoff_human import (
HANDOFF_STATUS,
ensure_virtual_no,
is_virtual_no,
)
if sess.collab_ended or (sess.collab_chat_id or "").strip():
reply(copy.HANDOFF_ALREADY_COLLAB)
return "handoff_already"
display_no = ensure_virtual_no(sess) if not (sess.work_order_no or "").strip() else sess.work_order_no
staff = self._match_handoff_staff(sess)
if not staff:
reply(self._group_no_staff_copy())
return sess.phase or "handoff_offer"
userids = [sess.sender_id]
product_names: list[str] = []
product_ids: list[str] = []
for row in staff:
uid = str(row.get("wecomId") or row.get("wecom_id") or "").strip()
name = str(row.get("name") or "").strip() or uid
if not uid:
continue
if uid not in userids:
userids.append(uid)
if uid not in product_ids:
product_ids.append(uid)
product_names.append(name)
if not product_ids:
reply(self._group_no_staff_copy())
return sess.phase or "handoff_offer"
for uid in self._archive_seat_userids():
if uid not in userids:
userids.append(uid)
client = self.group_client()
created = client.create_group(name=display_no, userids=userids) or {}
if not created.get("ok") or not created.get("chat_id"):
reply(copy.sea_group_create_fail(reason=str(created.get("error") or "")))
return sess.phase or "handoff_offer"
chat_id = str(created.get("chat_id") or "").strip()
brief = copy.handoff_group_brief(
work_order_no=display_no,
facts=sess.facts,
business_line=sess.business_line,
product_names=product_names,
)
client.send_group(chat_id=chat_id, content=brief)
real = (sess.work_order_no or "").strip()
if real and not is_virtual_no(real):
trans = getattr(self._ledger, "transition", None)
if callable(trans):
trans(
work_order_no=real,
to_status=HANDOFF_STATUS,
remark=copy.HANDOFF_CLARIFY_REASON
if sess.handoff_offer == "clarify"
else copy.HANDOFF_INTENT_REASON,
)
binder = getattr(self._ledger, "bind_collab_group", None)
if callable(binder):
binder(
work_order_no=real,
chat_id=chat_id,
member_ids=userids,
product_names=product_names,
product_ids=product_ids,
)
sess.status = HANDOFF_STATUS
sess.collab_chat_id = chat_id
sess.handoff_committed = True
sess.handoff_offer = ""
sess.phase = "handoff_done"
sess.allowed = ()
sess.invited_product_names = tuple(product_names)
self._save(sess)
self._grey_clicked_card(reply, card_meta)
sales_name = self._sales_display_name(sess.sender_id)
reply(
copy.sea_group_created(
work_order_no=display_no,
sales_name=sales_name,
product_names=product_names,
)
)
return "handoff_done"
def _match_sea_staff(self, sess: FlowSession) -> list[dict[str, Any]]:
"""主账按起运/目的匹配启用的海运产品岗。"""
matcher = getattr(self._ledger, "match_sea_staff", None)