落地多段联运私聊:齐了才建单拉群,附件只收格子值,核对改正不再整单重开。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-23 15:08:15 +08:00
co-authored by Cursor
parent 6e3e09a316
commit 2334524bb8
30 changed files with 4033 additions and 40 deletions
@@ -35,6 +35,7 @@ from agent.policy.air_text_flow import reset_air_text_flow_for_test
from agent.policy.attachment_wave import get_attach_wave_book, reset_attach_wave_book_for_test
from agent.policy.image_wave import reset_image_wave_book_for_test
from agent.policy.land_text_flow import reset_land_text_flow_for_test
from agent.policy.multi_text_flow import get_multi_text_flow, reset_multi_text_flow_for_test
from agent.policy.sea_text_flow import get_sea_text_flow, reset_sea_text_flow_for_test
from agent.routing.decision import RouteDecision
from agent.routing.dispatch import dispatch_inbound
@@ -51,6 +52,23 @@ SEA_ORAL = """海运
运输分类:Port to Port
"""
LAND_ORAL = """运输类型:国内运输整车
线路类别:国内长途/零担
运输分类:普件(东南亚、中港常用线路/常规车型)
始发站:东莞
目的地:上海
货物品名:普货
重量:1000
体积:10
"""
# 测服陆运报价单抽出约 997 字、整理后只剩 48 字。
_SEA_SHEET_RAW = (
"起运港:上海\n目的港:洛杉矶\n品名:普货\n整柜或拼柜:整柜\n"
"箱型箱量:1x40HQ\n贸易条款:FOB\n货物数量:20吨\n运输分类:Port to Port\n"
) * 16
_SEA_SHEET_TINY = "货物数量:100\n运输分类:其他\n报价日期:2026-09-23\n备注:报价单整理"
def _extract(text: str):
def _fn(_name: str, _raw: bytes):
@@ -83,6 +101,7 @@ class AttachmentInquiryTests(unittest.TestCase):
reset_sea_text_flow_for_test()
reset_land_text_flow_for_test()
reset_air_text_flow_for_test()
reset_multi_text_flow_for_test()
reset_attach_wave_book_for_test()
reset_image_wave_book_for_test()
self.store = MemoryMessageStore()
@@ -93,6 +112,69 @@ class AttachmentInquiryTests(unittest.TestCase):
def _extras(self) -> list[dict]:
return [i.payload for i in self.store._outbox.values()]
def test_tiny_organize_keeps_extracted_text(self) -> None:
"""报价单被整理成一个字时,改用抽出的原文,否则多段开单看不到附件内容。"""
from agent.handlers.attachment_inquiry import prefer_extracted_oral
raw = "起运港\t纽约港\n目的港\t巴生西港\n品名\t机器设备\n" * 8
kept = "运输方式:海运\n起运港:纽约港\n目的港:巴生西港\n品名:机器设备\n箱型箱量:40HQ*1"
self.assertEqual(prefer_extracted_oral(raw, "。"), raw.strip())
self.assertEqual(prefer_extracted_oral(raw, kept), kept)
self.assertEqual(prefer_extracted_oral("", ""), "")
self.assertGreaterEqual(len(_SEA_SHEET_RAW), 200)
self.assertLess(len(_SEA_SHEET_TINY), 80)
self.assertEqual(prefer_extracted_oral(_SEA_SHEET_RAW, _SEA_SHEET_TINY), _SEA_SHEET_RAW.strip())
def test_leftover_multi_plus_shrunk_sheet_still_keeps_sea_ports(self) -> None:
"""
旧多段补问草稿 + 千问把报价单压成几十个字时,
新附件必须当新询价,并把单元格原文里的起运港/目的港留下。
"""
leftover = get_multi_text_flow()
leftover.on_text(
sender_id="s1",
text="段1:陆运\n段2:海运\n货物数量:100\n运输分类:其他",
reply=lambda t, extra=None: None,
)
self.assertEqual((leftover.session_of("s1").phase or ""), "clarify")
handle_attachment_inquiry(
InboundMessage(sender_id="s1", message_id="m-attach", content="附件询价"),
store=self.store,
)
self.assertIsNone(get_multi_text_flow().session_of("s1"))
def _extract_two(name: str, _raw: bytes):
if "陆运" in name:
return {"ok": True, "text": LAND_ORAL, "empty": False, "error": ""}
return {"ok": True, "text": _SEA_SHEET_RAW, "empty": False, "error": ""}
def _shrink_qianwen(**kwargs):
raw = str((kwargs.get("payload") or {}).get("raw_text") or "")
if "起运港" in raw:
return {"ok": True, "text": _SEA_SHEET_TINY, "empty": False, "error": ""}
return {"ok": True, "text": "运输类型:国内运输整车", "empty": False, "error": ""}
accept_h5_uploads(
sender_id="s1",
files=[("WO陆运报价单.xlsx", b"land"), ("手工报价海运.xlsx", b"sea")],
store=self.store,
extract_fn=_extract_two,
attach_text_fn=_shrink_qianwen,
)
bodies = self._bodies()
joined = "\n".join(bodies)
self.assertNotIn("起运港:(如:上海)", joined)
sess = get_multi_text_flow().session_of("s1")
self.assertIsNotNone(sess)
segs = (sess.quote or {}).get("multi_segments") or []
sea = next((row for row in segs if str(row.get("mode") or "") == "SEA"), None)
self.assertIsNotNone(sea)
sea_facts = dict((sea or {}).get("facts") or {})
self.assertEqual(str(sea_facts.get("起运港") or ""), "上海")
self.assertEqual(str(sea_facts.get("目的港") or ""), "洛杉矶")
self.assertEqual(str(sea_facts.get("品名") or ""), "普货")
def test_intent_sends_upload_card_not_ticket(self) -> None:
phase = handle_attachment_inquiry(
InboundMessage(sender_id="s1", message_id="m1", content="帮我传个Excel询价,海运上海到纽约"),