调价后的新价格写入报价单;多段段前的货物带到各段,协同群不再把体积里的星号吃掉。海运字段齐了先核对再查价。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-03 22:39:54 +08:00
co-authored by Cursor
parent 0a14b4cc15
commit 7c77e45e03
32 changed files with 1384 additions and 75 deletions
+200 -26
View File
@@ -46,6 +46,14 @@ COMPLETE_SEA = {
}
def drive_sea(flow, **kwargs):
"""字段齐时先出核对卡;要看到查价结果的旧用例再自动回一句确定。"""
phase = flow.on_text(**kwargs)
if phase == "wait_confirm":
phase = flow.on_text(sender_id=kwargs["sender_id"], text="确定", reply=kwargs["reply"])
return phase
class SeaCollabContractTests(unittest.TestCase):
def test_collab_no_longer_asks_trade_terms(self):
self.assertNotIn("贸易条款", SEA_COLLAB_FIELDS)
@@ -127,6 +135,80 @@ class SeaTextInquiryTests(unittest.TestCase):
self.replies.append(text)
self.extras.append(dict(extra) if extra else {})
def test_complete_fields_hold_confirm_before_tms(self) -> None:
"""字段齐了只出核对卡,不建单、不查价。"""
phase = self.flow.on_text(
sender_id="s-hold",
text="海运完整",
reply=self._reply,
injected_facts=COMPLETE_SEA,
injected_mode="SEA",
)
self.assertEqual(phase, "wait_confirm")
card = self.replies[-1]
self.assertIn(copy.SEA_CONFIRM_TAIL, card)
self.assertIn("运输方式(必填):海运", card)
self.assertIn("货物数量(必填):20吨", card)
self.assertIn("贸易条款(必填):FOB", card)
self.assertNotIn("工单号", card)
self.assertNotIn("正在查询TMS报价", card)
sess = self.flow.session_of("s-hold")
self.assertEqual(sess.phase, "wait_confirm")
self.assertFalse((sess.work_order_no or "").strip())
self.assertEqual(self.ledger._tickets, {})
def test_confirm_phrases_then_query_tms(self) -> None:
"""确定、确认、查价、可以查了 都建单并查价。"""
for word in ("确定", "确认", "查价", "可以查了"):
with self.subTest(word=word):
self.replies.clear()
sender = f"s-go-{word}"
held = self.flow.on_text(
sender_id=sender,
text="海运完整",
reply=self._reply,
injected_facts=COMPLETE_SEA,
injected_mode="SEA",
)
self.assertEqual(held, "wait_confirm")
self.replies.clear()
phase = self.flow.on_text(sender_id=sender, text=word, reply=self._reply)
self.assertEqual(phase, "wait_collab")
joined = "\n".join(self.replies)
self.assertIn("正在查询TMS报价", joined)
self.assertIn("工单号:", joined)
sess = self.flow.session_of(sender)
self.assertTrue((sess.work_order_no or "").startswith("WO"))
def test_resend_while_waiting_merges_and_does_not_quote(self) -> None:
"""核对卡出来后改一处,并进原字段再出卡,仍不建单。"""
phase = self.flow.on_text(
sender_id="s-fix",
text="海运完整",
reply=self._reply,
injected_facts=COMPLETE_SEA,
injected_mode="SEA",
)
self.assertEqual(phase, "wait_confirm")
self.replies.clear()
fixed = dict(COMPLETE_SEA)
fixed["目的港"] = "纽约"
phase = self.flow.on_text(
sender_id="s-fix",
text="目的港改成纽约",
reply=self._reply,
injected_facts=fixed,
injected_mode="SEA",
)
self.assertEqual(phase, "wait_confirm")
self.assertIn("目的港(必填):纽约", self.replies[-1])
self.assertIn(copy.SEA_CONFIRM_TAIL, self.replies[-1])
sess = self.flow.session_of("s-fix")
self.assertEqual(sess.facts.get("目的港"), "纽约")
self.assertEqual(sess.facts.get("起运港"), "上海")
self.assertFalse((sess.work_order_no or "").strip())
self.assertEqual(self.ledger._tickets, {})
def test_sea_pieces_fill_cargo_qty(self) -> None:
facts = {
"起运港": "上海",
@@ -287,7 +369,7 @@ class SeaTextInquiryTests(unittest.TestCase):
def test_incomplete_without_trade_terms_does_not_create_ticket(self) -> None:
facts = dict(COMPLETE_SEA)
facts.pop("贸易条款")
phase = self.flow.on_text(
phase = drive_sea(self.flow,
sender_id="s-miss",
text="海运缺条款",
reply=self._reply,
@@ -318,7 +400,7 @@ class SeaTextInquiryTests(unittest.TestCase):
facts=leftover,
)
)
phase = self.flow.on_text(
phase = drive_sea(self.flow,
sender_id="s-door",
text="海运、纽约港到巴生西港,机器设备,40HQ*1,整柜,1.5KG,15CBM,15件",
reply=self._reply,
@@ -335,7 +417,7 @@ class SeaTextInquiryTests(unittest.TestCase):
"""补问后只回门到门,要记成贸易条款,不能回参数提取不全。"""
first = dict(COMPLETE_SEA)
first.pop("贸易条款")
phase = self.flow.on_text(
phase = drive_sea(self.flow,
sender_id="s-door-reply",
text="先缺条款",
reply=self._reply,
@@ -344,7 +426,7 @@ class SeaTextInquiryTests(unittest.TestCase):
)
self.assertEqual(phase, "clarify")
self.replies.clear()
phase = self.flow.on_text(
phase = drive_sea(self.flow,
sender_id="s-door-reply",
text="门到门",
reply=self._reply,
@@ -354,10 +436,92 @@ class SeaTextInquiryTests(unittest.TestCase):
self.assertIn("贸易条款:门到门", joined)
self.assertNotIn("参数提取不全", joined)
def test_door_in_sentence_kept_after_choosing_sea(self) -> None:
"""没写海运、句中有门到门,回「2」后贸易条款要带上,不能再追问。"""
from agent.channel.queue import MemoryMessageStore
from agent.policy.air_text_flow import reset_air_text_flow_for_test
from agent.policy.land_text_flow import reset_land_text_flow_for_test
from agent.policy.sea_text_flow import get_sea_text_flow
reset_sea_text_flow_for_test()
reset_land_text_flow_for_test()
reset_air_text_flow_for_test()
store = MemoryMessageStore()
text = "广州港到巴生港,机器设备,40HQ*1,整柜,1.5KG,15CBM,15件,Port to Port,门到门"
facts = {
"起运港": "广州港",
"目的港": "巴生港",
"品名": "机器设备",
"货量": "15件",
"整柜或拼柜": "整柜",
"箱型箱量": "40HQ*1",
"运输分类": "Port to Port",
}
phase = handle_text_inquiry(
InboundMessage(sender_id="s-door-mode", message_id="m-door-1", content=text),
injected_facts=facts,
store=store,
)
self.assertEqual(phase, "need_mode")
phase = handle_text_inquiry(
InboundMessage(sender_id="s-door-mode", message_id="m-door-2", content="2"),
store=store,
)
body = "\n".join(item.content for item in store._outbox.values())
sess = get_sea_text_flow().session_of("s-door-mode")
self.assertEqual(sess.facts.get("贸易条款"), "门到门")
self.assertEqual(sess.facts.get("运输分类"), "Port to Port")
self.assertIn("贸易条款(必填):门到门", body)
self.assertIn(copy.SEA_CONFIRM_TAIL, body)
self.assertEqual(phase, "wait_confirm")
self.assertNotIn("1.贸易条款(必填)", body)
self.assertNotEqual(phase, "need_mode")
def test_port_to_port_phrase_kept_after_choosing_sea(self) -> None:
"""没写海运、句中有港到港,回「2」后贸易条款是港到港,运输分类仍是 Port to Port。"""
from agent.channel.queue import MemoryMessageStore
from agent.policy.air_text_flow import reset_air_text_flow_for_test
from agent.policy.land_text_flow import reset_land_text_flow_for_test
from agent.policy.sea_text_flow import get_sea_text_flow
reset_sea_text_flow_for_test()
reset_land_text_flow_for_test()
reset_air_text_flow_for_test()
store = MemoryMessageStore()
text = "广州港到巴生港,机器设备,40HQ*1,整柜,1.5KG,15CBM,15件,Port to Port,港到港"
facts = {
"起运港": "广州港",
"目的港": "巴生港",
"品名": "机器设备",
"货量": "15件",
"整柜或拼柜": "整柜",
"箱型箱量": "40HQ*1",
"运输分类": "Port to Port",
}
phase = handle_text_inquiry(
InboundMessage(sender_id="s-port-mode", message_id="m-port-1", content=text),
injected_facts=facts,
store=store,
)
self.assertEqual(phase, "need_mode")
phase = handle_text_inquiry(
InboundMessage(sender_id="s-port-mode", message_id="m-port-2", content="2"),
store=store,
)
body = "\n".join(item.content for item in store._outbox.values())
sess = get_sea_text_flow().session_of("s-port-mode")
self.assertEqual(sess.facts.get("贸易条款"), "港到港")
self.assertEqual(sess.facts.get("运输分类"), "Port to Port")
self.assertIn("贸易条款(必填):港到港", body)
self.assertIn(copy.SEA_CONFIRM_TAIL, body)
self.assertEqual(phase, "wait_confirm")
self.assertNotIn("1.贸易条款(必填)", body)
self.assertNotEqual(phase, "need_mode")
def test_harvest_class_from_reply_then_quotes(self) -> None:
first = dict(COMPLETE_SEA)
first.pop("运输分类")
phase = self.flow.on_text(
phase = drive_sea(self.flow,
sender_id="s-class",
text="先缺分类",
reply=self._reply,
@@ -366,7 +530,7 @@ class SeaTextInquiryTests(unittest.TestCase):
)
self.assertEqual(phase, "clarify")
self.replies.clear()
phase = self.flow.on_text(
phase = drive_sea(self.flow,
sender_id="s-class",
text="Port to Port",
reply=self._reply,
@@ -378,7 +542,7 @@ class SeaTextInquiryTests(unittest.TestCase):
self.assertIn("贸易条款:FOB", joined)
def test_complete_sea_inquiry_card_uses_ten_fields(self) -> None:
phase = self.flow.on_text(
phase = drive_sea(self.flow,
sender_id="s1",
text="海运完整",
reply=self._reply,
@@ -404,7 +568,7 @@ class SeaTextInquiryTests(unittest.TestCase):
def test_new_round_after_ticket_does_not_reuse_old_facts(self) -> None:
"""上一单已出号,新一轮不能带上旧贸易条款。"""
first = self.flow.on_text(
first = drive_sea(self.flow,
sender_id="s-round",
text="海运第一轮",
reply=self._reply,
@@ -424,7 +588,7 @@ class SeaTextInquiryTests(unittest.TestCase):
"箱型箱量": "40HQ*1",
"运输分类": "Port to Port",
}
phase = self.flow.on_text(
phase = drive_sea(self.flow,
sender_id="s-round",
text="海运、纽约港到巴生西港,机器设备,40HQ*1,整柜,1.5KG,15CBM,15件,Port to Port",
reply=self._reply,
@@ -457,7 +621,7 @@ class SeaTextInquiryTests(unittest.TestCase):
flow=self.flow,
store=store,
)
self.assertEqual(phase, "wait_collab")
self.assertEqual(phase, "wait_confirm")
self.assertNotEqual(phase, "sea_deferred")
@@ -569,9 +733,19 @@ class SeaTmsCardTests(unittest.TestCase):
self.assertNotIn("时效", text)
self.assertNotIn("报价有效期", text)
card_text = copy.quote_area_safe_text(long_quote, business_line="SEA")
self.assertIn("费用合计:CNY 2287.65", card_text)
self.assertNotIn("海运费:USD 100.00\n燃油附加费", card_text)
self.assertLessEqual(len(card_text), 80)
self.assertEqual(
card_text,
"费用合计:CNY 2287.65\n共7项费用,详见上一条明细",
)
short_quote = {
"total": "100",
"currency": "CNY",
"fee_lines": [{"name": "海运费", "amount": "100", "currency": "CNY"}],
}
self.assertEqual(
copy.quote_area_safe_text(short_quote, business_line="SEA"),
"费用合计:CNY 100\n共1项费用,详见上一条明细",
)
payload = copy.sea_tms_hit_wecom_payload(
work_order_no="WO202609150030", quote=long_quote
)
@@ -614,7 +788,7 @@ class SeaTmsCardTests(unittest.TestCase):
self.assertIn("card", steps)
def test_sea_hit_card_has_price_and_two_buttons(self) -> None:
phase = self.flow.on_text(
phase = drive_sea(self.flow,
sender_id="s1",
text="海运有价",
reply=self._reply,
@@ -639,7 +813,7 @@ class SeaTmsCardTests(unittest.TestCase):
def test_sea_miss_one_card_with_pull_button(self) -> None:
facts = dict(COMPLETE_SEA)
facts["目的港"] = "无价港"
phase = self.flow.on_text(
phase = drive_sea(self.flow,
sender_id="s1",
text="海运无价",
reply=self._reply,
@@ -664,7 +838,7 @@ class SeaTmsCardTests(unittest.TestCase):
def test_sea_tech_fail_is_not_miss(self) -> None:
facts = dict(COMPLETE_SEA)
facts["tms_force"] = "tech"
phase = self.flow.on_text(
phase = drive_sea(self.flow,
sender_id="s1",
text="海运技术失败",
reply=self._reply,
@@ -708,7 +882,7 @@ class SeaSkipAndPullTests(unittest.TestCase):
return ""
def _quote_first(self, sender: str = "s1") -> None:
self.flow.on_text(
drive_sea(self.flow,
sender_id=sender,
text="海运有价",
reply=self._reply,
@@ -820,7 +994,7 @@ class SeaSkipAndPullTests(unittest.TestCase):
)
facts = dict(COMPLETE_SEA)
facts["是否含电"] = "否"
self.flow.on_text(
drive_sea(self.flow,
sender_id="s1",
text="海运不含电",
reply=self._reply,
@@ -857,7 +1031,7 @@ class SeaSkipAndPullTests(unittest.TestCase):
)
with patch("agent.policy.sea_text_flow._default_group_client", return_value=live):
flow = SeaTextInquiryFlow(self.ledger, group_client=None)
flow.on_text(
drive_sea(flow,
sender_id="s9",
text="海运有价",
reply=self._reply,
@@ -964,7 +1138,7 @@ class SeaSkipAndPullTests(unittest.TestCase):
)
facts = dict(COMPLETE_SEA)
facts["目的港"] = "无价港"
self.flow.on_text(
drive_sea(self.flow,
sender_id="s1",
text="海运无价",
reply=self._reply,
@@ -1018,7 +1192,7 @@ class SeaGroupCollabFieldTests(unittest.TestCase):
}
]
)
self.flow.on_text(
drive_sea(self.flow,
sender_id="sales1",
text="海运有价",
reply=self._reply,
@@ -1074,7 +1248,7 @@ class SeaGroupCollabFieldTests(unittest.TestCase):
}
]
)
self.flow.on_text(
drive_sea(self.flow,
sender_id="sales1",
text="海运有价",
reply=self._reply,
@@ -1258,7 +1432,7 @@ class SeaContinueTests(unittest.TestCase):
from agent.handlers.continue_thread import handle_continue_thread
from agent.policy.air_text_flow import AirTextInquiryFlow
self.flow.on_text(
drive_sea(self.flow,
sender_id="c1",
text="海运有价",
reply=self._reply,
@@ -1358,7 +1532,7 @@ class SeaContinueTests(unittest.TestCase):
from agent.handlers.continue_thread import handle_continue_thread
from agent.policy.air_text_flow import AirTextInquiryFlow
self.flow.on_text(
drive_sea(self.flow,
sender_id="c3",
text="海运有价",
reply=self._reply,
@@ -1590,7 +1764,7 @@ class SeaGroupOpsTests(unittest.TestCase):
self.replies.append(text)
def _open_group(self) -> tuple[str, str]:
self.flow.on_text(
drive_sea(self.flow,
sender_id="sales1",
text="海运有价",
reply=self._reply,
@@ -2175,7 +2349,7 @@ class SeaGroupOpsTests(unittest.TestCase):
"""TMS 无价时产品文字报价:摘要 TMS 仍是暂无,人工报价两行齐全。"""
facts = dict(COMPLETE_SEA)
facts["目的港"] = "无价港"
self.flow.on_text(
drive_sea(self.flow,
sender_id="sales1",
text="海运无价",
reply=self._reply,