落地多段联运私聊:齐了才建单拉群,附件只收格子值,核对改正不再整单重开。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -116,6 +116,19 @@ def handle_air_group(
|
||||
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.multi_group_ops import handle_multi_inbound, ticket_is_multi
|
||||
|
||||
getter = getattr(engine.ledger, "get_ticket", None)
|
||||
raw_ticket = getter(work_order_no=wo) if callable(getter) else None
|
||||
if raw_ticket is not None and ticket_is_multi(raw_ticket):
|
||||
return handle_multi_inbound(
|
||||
flow=engine,
|
||||
ticket=raw_ticket,
|
||||
chat_id=chat_id,
|
||||
sender_id=message.sender_id,
|
||||
text=text,
|
||||
injected_quote=injected_quote,
|
||||
)
|
||||
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)
|
||||
|
||||
@@ -30,6 +30,30 @@ VisionFn = Callable[..., dict[str, Any]]
|
||||
DownloadFn = Callable[[str], tuple[bytes, str]]
|
||||
PutFn = Callable[..., dict[str, Any]]
|
||||
|
||||
|
||||
def prefer_extracted_oral(raw_text: str, organized: str) -> str:
|
||||
"""
|
||||
千问把报价单整理成空、几个字或几十个字时,改用抽出的单元格原文。
|
||||
|
||||
测服复现:陆运 997 字被压成 48 字、海运 1325 字被压成 52 字,
|
||||
多段补问就只剩货量/运输分类,起运港目的港整段待补充。
|
||||
整理结果够长、没有被大幅压短时仍用整理后的,方便人读。
|
||||
"""
|
||||
raw = (raw_text or "").strip()
|
||||
org = (organized or "").strip()
|
||||
if not raw:
|
||||
return org
|
||||
if not org:
|
||||
return raw
|
||||
# 报价单原文很长、整理后只剩几分之一:用原文。997→48、1325→52 都走这里。
|
||||
if len(raw) >= 200 and len(org) * 8 < len(raw):
|
||||
return raw
|
||||
if len(org) < 8 and len(raw) >= 40:
|
||||
return raw
|
||||
if len(org) < 40 and len(org) * 5 < len(raw):
|
||||
return raw
|
||||
return org
|
||||
|
||||
_STASH: dict[str, bytes] = {}
|
||||
_STASH_LOCK = threading.Lock()
|
||||
_PRECEDE_NOISE = (
|
||||
@@ -68,10 +92,17 @@ def _stash_get(key: str) -> bytes:
|
||||
|
||||
|
||||
def drop_all_private_bookmarks(sender_id: str) -> None:
|
||||
"""三线当前书签都丢掉;已出号仍能按工单号找回。"""
|
||||
"""
|
||||
海运/空运/陆运/多段当前书签都丢掉;已出号仍能按工单号找回。
|
||||
|
||||
附件发卡或认完必须连多段一起清:否则旧补问草稿会把新附件口播当成补字段,
|
||||
海运只剩货量/运输分类,陆运再问一遍运输类型。
|
||||
"""
|
||||
from agent.handlers.image_inquiry import drop_all_private_bookmarks as _drop
|
||||
from agent.policy.multi_text_flow import get_multi_text_flow
|
||||
|
||||
_drop(sender_id)
|
||||
get_multi_text_flow().drop_current_bookmark(sender_id)
|
||||
|
||||
|
||||
def is_active_attachment_wait(sender_id: str, *, now: Optional[float] = None) -> bool:
|
||||
@@ -404,6 +435,8 @@ def finish_attach_wave(
|
||||
media=dict(seed_message.media) if seed_message else {},
|
||||
raw=dict(seed_message.raw) if seed_message else {},
|
||||
)
|
||||
# 认完附件一律新开:不能粘在发卡前留下的多段/单段补问草稿上。
|
||||
drop_all_private_bookmarks(sender_id)
|
||||
phase = handle_text_inquiry(inbound, store=_StoreProxy())
|
||||
if reason and not prefix_used["v"]:
|
||||
msg_store.enqueue_outbound(
|
||||
@@ -612,9 +645,18 @@ def _recognize_file(
|
||||
allow_network=bool(settings.llm_allow_network and settings.llm_data_usage_confirmed),
|
||||
)
|
||||
if packed.get("ok") and str(packed.get("text") or "").strip():
|
||||
organized = str(packed.get("text") or "")
|
||||
organized = prefer_extracted_oral(raw_text, str(packed.get("text") or ""))
|
||||
elif not packed.get("ok"):
|
||||
org_err = str(packed.get("error") or org_err or "attachment_text_failed")
|
||||
logger.info(
|
||||
"attachment 认完 sender=%s idx=%s name=%s raw_len=%s oral_len=%s err=%s",
|
||||
sender_id,
|
||||
index,
|
||||
filename,
|
||||
len(raw_text),
|
||||
len((organized or "").strip()),
|
||||
org_err or "-",
|
||||
)
|
||||
book.complete_material(
|
||||
sender_id,
|
||||
index,
|
||||
|
||||
@@ -20,7 +20,17 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _pick_card_flow(sender_id: str, payload: dict | None):
|
||||
"""点卡:海运/陆运书签能找回工单就走对应流程,否则空运。"""
|
||||
"""
|
||||
点卡:先认多段联运,再认海运/陆运,最后才空运。
|
||||
|
||||
多段工单主账停点常被收成 tms_miss。若先走空运,「拉产品进群」会被丢掉、销售点了没回声。
|
||||
"""
|
||||
from agent.policy.multi_text_flow import get_multi_text_flow
|
||||
|
||||
multi = get_multi_text_flow()
|
||||
multi_sess = multi.session_for_action(sender_id, payload)
|
||||
if multi_sess and (multi_sess.business_line or "").upper() == "MULTI":
|
||||
return multi
|
||||
sea = get_sea_text_flow()
|
||||
sess = sea.session_for_action(sender_id, payload)
|
||||
if sess and (sess.business_line or "").upper() == "SEA":
|
||||
|
||||
@@ -95,6 +95,30 @@ def handle_group_inbound(
|
||||
from agent.policy.land_text_flow import get_land_text_flow
|
||||
|
||||
engine = get_land_text_flow()
|
||||
view0 = ticket_view(ticket)
|
||||
line = str(
|
||||
view0.get("business_line")
|
||||
or view0.get("businessLine")
|
||||
or getattr(ticket, "business_line", "")
|
||||
or ""
|
||||
).upper()
|
||||
if line == "MULTI":
|
||||
from agent.policy.multi_group_ops import handle_multi_inbound, sender_mode
|
||||
|
||||
if not (
|
||||
_sender_allowed(ticket, message.sender_id)
|
||||
or sender_mode(engine, message.sender_id)
|
||||
):
|
||||
logger.info("group_inbound 多段忽略非成员 sender=%s", message.sender_id)
|
||||
return "group_ignore"
|
||||
return handle_multi_inbound(
|
||||
flow=engine,
|
||||
ticket=ticket,
|
||||
chat_id=chat_id,
|
||||
sender_id=message.sender_id,
|
||||
text=message.content or "",
|
||||
injected_quote=injected_quote,
|
||||
)
|
||||
ticket = hydrate_ticket(engine, ticket)
|
||||
if not _sender_allowed(ticket, message.sender_id):
|
||||
logger.info("group_inbound 忽略非成员 sender=%s chat=%s", message.sender_id, chat_id)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
意图:普通文字询价(空运 / 海运闭环)。
|
||||
意图:普通文字询价(空运 / 海运 / 陆运 / 多段联运)。
|
||||
|
||||
本文件职责:入站文字 → 流程编排;回复只写入 outbox。
|
||||
禁止:在本文件抽字段语义(交给 extract);禁止直连 TMS;禁止回调线程外的额外阻塞。
|
||||
@@ -45,7 +45,7 @@ def _drop_other_mode_drafts(*, owner: str, sender_id: str) -> None:
|
||||
"""
|
||||
kind = (owner or "").upper()
|
||||
sid = (sender_id or "").strip()
|
||||
if not sid or kind not in {"SEA", "AIR", "LAND"}:
|
||||
if not sid or kind not in {"SEA", "AIR", "LAND", "MULTI"}:
|
||||
return
|
||||
land = get_land_text_flow()
|
||||
air = get_air_text_flow()
|
||||
@@ -112,6 +112,19 @@ def _pick_text_flow(*, sender_id: str, text: str, injected_mode: str = "", prefe
|
||||
from agent.policy.quote_adjust_ops import quoted_adjust_flow
|
||||
from agent.policy.handoff_human import is_handoff_frozen
|
||||
|
||||
from agent.policy.multi_segments import classify_multi_text
|
||||
from agent.policy.multi_text_flow import get_multi_text_flow
|
||||
|
||||
multi = get_multi_text_flow()
|
||||
# 多段核对上的「确定」优先于陆运旧核对,避免建出海运/陆运单段工单。
|
||||
if looks_like_confirm(text):
|
||||
multi_hit = multi.session_of(sender_id)
|
||||
if (
|
||||
multi_hit
|
||||
and (multi_hit.business_line or "").upper() == "MULTI"
|
||||
and (multi_hit.phase or "") == "wait_confirm"
|
||||
):
|
||||
return multi
|
||||
land = get_land_text_flow()
|
||||
if looks_like_confirm(text):
|
||||
restored = land.restore_confirm_session(sender_id)
|
||||
@@ -123,14 +136,42 @@ def _pick_text_flow(*, sender_id: str, text: str, injected_mode: str = "", prefe
|
||||
sea = get_sea_text_flow()
|
||||
air = get_air_text_flow()
|
||||
spoken = (injected_mode or detect_transport_mode(text) or "").upper()
|
||||
multi_kind = classify_multi_text(text) if not injected_mode else "single"
|
||||
if multi_kind in {"multi", "same_mode"}:
|
||||
_drop_other_mode_drafts(owner="MULTI", sender_id=sender_id)
|
||||
return multi
|
||||
# 本句点名运输方式:盖掉其它方式的旧补问,避免海运补字段走陆运样例图。
|
||||
# 多段正在补问时例外:销售会写「段2:运输方式:空运」,不能把多段草稿丢掉改走单段。
|
||||
# 已出号等拉群不算补问:再发陆运附件要走陆运,不能粘在旧工单的手动拉群提示上。
|
||||
if spoken in {"SEA", "AIR", "LAND"}:
|
||||
multi_draft = multi.session_of(sender_id)
|
||||
if (
|
||||
multi_draft
|
||||
and (multi_draft.business_line or "").upper() == "MULTI"
|
||||
and (multi_draft.phase or "") == "clarify"
|
||||
):
|
||||
return multi
|
||||
_drop_other_mode_drafts(owner=spoken, sender_id=sender_id)
|
||||
stale_handoff = (multi_draft.phase or "") in {
|
||||
"wait_collab",
|
||||
"wait_manual_group",
|
||||
"collab_group",
|
||||
} if multi_draft else False
|
||||
if _front_bookmark(multi_draft) or stale_handoff:
|
||||
multi.drop_current_bookmark(sender_id)
|
||||
if spoken == "SEA":
|
||||
return sea
|
||||
if spoken == "LAND":
|
||||
return land
|
||||
return air
|
||||
multi_sess = multi.session_of(sender_id)
|
||||
if (
|
||||
multi_sess
|
||||
and (multi_sess.business_line or "").upper() == "MULTI"
|
||||
and (multi_sess.phase or "") in {"clarify", "wait_confirm"}
|
||||
and not (looks_like_confirm(text) and (multi_sess.phase or "") != "wait_confirm")
|
||||
):
|
||||
return multi
|
||||
sea_sess = sea.session_of(sender_id)
|
||||
land_sess = land.session_of(sender_id)
|
||||
air_sess = air.session_of(sender_id)
|
||||
|
||||
Reference in New Issue
Block a user