重启后仍能跳过协同出方案,未成交可同一句带原因;海陆有价卡改为直接生成报价单。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-23 18:45:28 +08:00
co-authored by Cursor
parent 378bb892b3
commit 84207bb4e5
18 changed files with 414 additions and 32 deletions
@@ -182,6 +182,9 @@ class AirGroupCollabTests(unittest.TestCase):
self.assertIn("是否含磁:否", general)
self.assertNotIn("是否含电:-", general)
self.assertIn("\n\nTMS报价:\nTMS 暂无匹配报价", text)
self.assertIn("航线人员可通过“纯文字”告知新报价,或直接将新报价excel表发给智能体。", text)
self.assertIn("附件报价单仅作为文件存储于后台,智能体不读取且不将报价内容写进系统", text)
self.assertNotIn("产品人员可通过", text)
self.assertNotIn("当前工单:", text)
self.assertNotIn("贸易条款", text)
@@ -211,6 +214,10 @@ class AirGroupCollabTests(unittest.TestCase):
self.assertIn("总销售价:CNY 183370.0", tms_block)
self.assertNotIn("时效", tms_block)
self.assertIn("报价有效期:2026-09-30", tms_block)
self.assertIn("请航线人员回复“确认报价”确定采用TMS报价。", text)
self.assertIn("亦可通过“纯文字”告知新报价,或直接将新报价excel表发给智能体。", text)
self.assertIn("附件报价单仅作为文件存储于后台,智能体不读取且不将报价内容写进系统", text)
self.assertNotIn("请产品人员回复", text)
def test_air_group_brief_uses_route_fees_not_air_freight_total(self) -> None:
"""
+1 -1
View File
@@ -1123,7 +1123,7 @@ class AirTextInquiryTests(unittest.TestCase):
text = copy.deal_card(work_order_no="WO202608210017", quote=quote, business_line="AIR")
self.assertIn("询价工单 WO202608210017", text)
self.assertIn("报价单已发送,请跟进成交状态", text)
self.assertIn("咨询成交状态:请选择成交 / 未成交 / 协商中(未成交需补充原因)。", text)
self.assertIn("咨询成交状态:请输入成交 / 未成交 (未成交需补充原因)/ 协商中。", text)
self.assertIn("总销售价:CNY 722.5", text)
self.assertNotIn("时效", text)
self.assertNotIn("- / -", text)
@@ -269,6 +269,54 @@ class ContinueTicketTests(unittest.TestCase):
self.assertEqual(decision.intent, "continue_thread")
_ = dispatch_inbound
def test_skip_collab_after_restart_from_ledger(self) -> None:
"""
测服 WO202609230031:进程重启后主账把空运已报价收成 wait_adopt,
点「不需要协同,直接生成报价单」不能再静默丢掉。
"""
ledger = MemoryLedger()
live = AirTextInquiryFlow(ledger)
sink: list[str] = []
def reply(text: str, extra=None) -> None:
sink.append(text)
_ = extra
created = ledger.create_ticket(
sender_id="u-skip-restart", business_line="AIR", facts=COMPLETE_AIR
)
wo = created["work_order_no"]
ledger.upsert_quote(
work_order_no=wo,
quote={
"total": "USD 1,280",
"eta": "3-5 天",
"valid_until": "2026-09-20",
"source_label": "TMS 标准报价",
},
to_status="已报价",
)
view = ledger.get_for_agent(work_order_no=wo, sender_id="u-skip-restart")
self.assertEqual(view.get("wait_phase"), "wait_adopt")
restarted = AirTextInquiryFlow(ledger)
phase = restarted.on_button(
sender_id="u-skip-restart",
action=copy.BTN_SKIP_COLLAB,
reply=reply,
card_meta={
"event_key": f"skip_collab:{wo}",
"task_id": f"price-{wo}-deadbeef",
"response_code": "rc-skip-restart",
},
)
self.assertEqual(phase, "wait_adopt")
joined = "\n".join(sink)
self.assertIn("已跳过内部协同", joined)
self.assertIn(f"{wo}报价方案", joined)
self.assertEqual(restarted.session_of("u-skip-restart").phase, "wait_adopt")
self.assertNotIn(copy.BTN_SKIP_COLLAB, restarted.session_of("u-skip-restart").allowed)
def test_adopt_excel_after_restart_stays_on_card_ticket(self) -> None:
"""点 0040 方案卡不能落到刚续办的 0037:进程无书签时按卡上工单号从主账收回。"""
ledger = MemoryLedger()
+60 -1
View File
@@ -9,7 +9,11 @@ import unittest
from agent.ledger.memory_ledger import MemoryLedger
from agent.policy import inquiry_copy as copy
from agent.policy.air_text_flow import AirTextInquiryFlow, FlowSession
from agent.policy.deal_outcome import capture_lost_reason, finish_deal_outcome
from agent.policy.deal_outcome import (
capture_lost_reason,
finish_deal_outcome,
harvest_inline_lost_reason,
)
from agent.policy.sea_text_flow import SeaTextInquiryFlow
@@ -144,6 +148,61 @@ class TestDealOutcome(unittest.TestCase):
self.assertEqual(copy.lost_reason_ack("客户不要了"), "已记下未成交原因:客户不要了。")
self.assertEqual(copy.lost_reason_ack("价格高。"), "已记下未成交原因:价格高。")
def test_harvest_inline_lost_reason(self) -> None:
self.assertEqual(harvest_inline_lost_reason("未成交,原因客户不想要了"), "客户不想要了")
self.assertEqual(harvest_inline_lost_reason("未成交,客户不想要了"), "客户不想要了")
self.assertEqual(harvest_inline_lost_reason("未成交:价格太高"), "价格太高")
self.assertEqual(harvest_inline_lost_reason("未成交原因客户不想要了"), "客户不想要了")
self.assertEqual(harvest_inline_lost_reason("未成交"), "")
self.assertEqual(harvest_inline_lost_reason("未成交吗"), "")
self.assertEqual(harvest_inline_lost_reason("成交"), "")
def test_lost_with_inline_reason_finishes(self) -> None:
out = finish_deal_outcome(
ledger=self.ledger,
work_order_no=self.wo,
outcome="未成交",
facts={},
quote={},
business_line="SEA",
lost_reason="客户不想要了",
)
self.assertEqual(out.get("phase"), "done")
ticket = self.ledger.get_ticket(work_order_no=self.wo)
self.assertEqual(ticket.status, "未成交")
self.assertEqual(ticket.lost_reason, "客户不想要了")
self.assertEqual(ticket.event_wait_kind, "")
self.assertEqual(
copy.deal_ack("未成交", self.wo, lost_reason="客户不想要了"),
"已记录为「未成交」。已记下未成交原因:客户不想要了。",
)
def test_private_lost_and_reason_same_message(self) -> None:
flow = AirTextInquiryFlow(self.ledger)
flow._save(
FlowSession(
sender_id="sales1",
thread_id="sales1",
phase="wait_deal",
business_line="SEA",
work_order_no=self.wo,
allowed=(copy.BTN_DEAL, copy.BTN_LOST, copy.BTN_NEGOTIATE),
)
)
replies: list[str] = []
phase = flow.on_text(
sender_id="sales1",
text="未成交,原因客户不想要了",
reply=lambda t, extra=None: replies.append(t),
)
self.assertEqual(phase, "done")
self.assertEqual(
self.ledger.get_ticket(work_order_no=self.wo).lost_reason, "客户不想要了"
)
joined = "\n".join(replies)
self.assertIn("客户不想要了", joined)
self.assertNotIn("请补充未成交原因", joined)
def test_negotiate_ack_names_followup_day(self) -> None:
text = copy.deal_ack("协商中", "WO202609190007", "2026-09-24 09:00:00")
self.assertEqual(
@@ -338,7 +338,7 @@ class LandRouteOptionsTests(unittest.TestCase):
tms_quote=raw,
transport_mode="陆运",
)
tms_block = brief.split("TMS报价:", 1)[1].split("有TMS报价时", 1)[0]
tms_block = brief.split("TMS报价:", 1)[1].split("请产品人员回复", 1)[0]
self.assertIn("上海-广西壮族自治区-胡志明", tms_block)
self.assertIn("上海 → 广西壮族自治区", tms_block)
self.assertIn("运输费:CNY 12500.00", tms_block)
@@ -350,6 +350,8 @@ class LandRouteOptionsTests(unittest.TestCase):
self.assertNotIn("总成本价:-", tms_block)
self.assertNotIn("时效:-", tms_block)
self.assertNotIn("时效:N+3 / N+3", tms_block)
self.assertIn("请产品人员回复“确认报价”确定采用TMS报价。", brief)
self.assertNotIn("请航线人员回复", brief)
def test_h5_lists_segment_costs(self) -> None:
from agent.channel.h5.land_route_page import render_land_route_page
@@ -659,5 +659,57 @@ class LandLostReasonTests(unittest.TestCase):
self.assertTrue(any(t == "已记下未成交原因:价格高。" for t in replies))
class LandSkipAfterRestartTests(unittest.TestCase):
def test_skip_collab_after_restart_from_ledger(self) -> None:
"""
陆运已报价主账也会收成 wait_adopt。进程无书签时点「不需要协同」仍须出方案卡。
"""
ledger = MemoryLedger()
facts = {
"起运港": "广州",
"目的港": "深圳",
"品名": "衣服",
"体积": "100CBM",
"毛重": "50kg",
"运输类型": "国内运输拼车",
"线路类别": "国内长途/零担",
"运输分类": "急件",
}
created = ledger.create_ticket(
sender_id="u-land-skip", business_line="LAND", facts=facts
)
wo = created["work_order_no"]
ledger.upsert_quote(
work_order_no=wo,
quote={"total": "CNY 800", "source_label": "TMS 标准报价"},
to_status="已报价",
)
view = ledger.get_for_agent(work_order_no=wo, sender_id="u-land-skip")
self.assertEqual(view.get("wait_phase"), "wait_adopt")
restarted = LandTextInquiryFlow(ledger)
replies: list[str] = []
def reply(text: str, extra=None) -> str:
replies.append(text)
_ = extra
return ""
phase = restarted.on_button(
sender_id="u-land-skip",
action=copy.BTN_SKIP_COLLAB,
reply=reply,
card_meta={
"event_key": f"skip_collab:{wo}",
"task_id": f"price-{wo}-land",
"response_code": "rc-land-skip",
},
)
self.assertEqual(phase, "wait_adopt")
joined = "\n".join(replies)
self.assertIn("已跳过内部协同", joined)
self.assertIn(f"{wo}报价方案", joined)
if __name__ == "__main__":
unittest.main()
+1
View File
@@ -490,6 +490,7 @@ class QuoteAdjustTests(unittest.TestCase):
transport_mode="陆运",
)
self.assertIn("TMS报价:\nTMS 暂无匹配报价", text)
self.assertIn("产品人员可通过“纯文字”告知新报价,或直接将新报价excel表发给智能体。", text)
self.assertIn("销售调价:", text)
self.assertIn("总销售价:CNY 100.00", text.split("销售调价:", 1)[1])
+32 -5
View File
@@ -579,13 +579,15 @@ class SeaTmsCardTests(unittest.TestCase):
self.assertIn("价格明细", joined)
self.assertIn("是否拉产品人员进群协同", joined)
self.assertIn(copy.BTN_PULL_COLLAB, joined)
self.assertIn(copy.BTN_SKIP_COLLAB, joined)
self.assertIn(copy.BTN_SKIP_COLLAB_TMS, joined)
self.assertNotIn(copy.BTN_SKIP_COLLAB, joined)
self.assertEqual(copy.canonical_button(copy.BTN_SKIP_COLLAB_TMS), copy.BTN_SKIP_COLLAB)
self.assertNotIn("请手动拉相关人员", joined)
self.assertNotIn("若需要咨询内部航线人员", joined)
cards = [x for x in self.extras if x.get("msgtype") == "template_card"]
self.assertTrue(cards)
buttons = [b["text"] for b in cards[-1]["template_card"]["button_list"]]
self.assertEqual(buttons, [copy.BTN_PULL_COLLAB, copy.BTN_SKIP_COLLAB])
self.assertEqual(buttons, [copy.BTN_PULL_COLLAB, copy.BTN_SKIP_COLLAB_TMS])
def test_sea_miss_one_card_with_pull_button(self) -> None:
facts = dict(COMPLETE_SEA)
@@ -1374,6 +1376,9 @@ class SeaGroupCopyTests(unittest.TestCase):
self.assertNotIn("贸易条款", collab_block)
self.assertNotIn("HS编码", text)
self.assertIn("\n\nTMS报价:\nTMS 暂无匹配报价", text)
self.assertIn("产品人员可通过“纯文字”告知新报价,或直接将新报价excel表发给智能体。", text)
self.assertIn("附件报价单仅作为文件存储于后台,智能体不读取且不将报价内容写进系统", text)
self.assertNotIn("航线人员可通过", text)
self.assertNotIn("未查到合适报价", text)
self.assertNotIn("确认报价", text)
@@ -1388,8 +1393,10 @@ class SeaGroupCopyTests(unittest.TestCase):
has_price=True,
)
self.assertIn("报价有效期:2026-09-30", text)
self.assertIn("有TMS报价时可回复“确认报价”沿用该价", text)
self.assertIn("也可直接回复协同报价或上传报价附件", text)
self.assertIn("请产品人员回复“确认报价”确定采用TMS报价。", text)
self.assertIn("亦可通过“纯文字”告知新报价,或直接将新报价excel表发给智能体。", text)
self.assertIn("附件报价单仅作为文件存储于后台,智能体不读取且不将报价内容写进系统", text)
self.assertNotIn("请航线人员回复", text)
self.assertNotIn("人工报价:", text)
def test_group_brief_manual_quote_stays_off_tms(self) -> None:
@@ -1419,6 +1426,7 @@ class SeaGroupCopyTests(unittest.TestCase):
},
)
self.assertIn("TMS报价:\nTMS 暂无匹配报价", text)
self.assertIn("产品人员可通过“纯文字”告知新报价,或直接将新报价excel表发给智能体。", text)
self.assertIn("人工报价:\n海运费:USD 100.00\n燃油附加费:USD 15.00", text)
self.assertNotIn("确认报价", text)
self.assertNotIn("总销售价合计:USD 100.00", text)
@@ -1795,7 +1803,7 @@ class SeaGroupOpsTests(unittest.TestCase):
"@吴季林\n"
"询价工单 WO202609160001\n"
"报价单已发送,请确认成交状态\n"
"咨询成交状态:请输入成交 / 未成交 / 协商中(未成交需补充原因)"
"咨询成交状态:请输入成交 / 未成交 (未成交需补充原因)/ 协商中"
),
)
wo, chat = self._open_group()
@@ -1903,6 +1911,25 @@ class SeaGroupOpsTests(unittest.TestCase):
self.assertEqual(phase, "done")
self.assertEqual(self.ledger.get_ticket(work_order_no=wo).lost_reason, "客人比价了")
def test_sales_lost_and_reason_same_message(self) -> None:
"""群里一句同时说未成交和原因,不再追问。"""
wo, chat = self._open_group()
phase = handle_group_collab(
InboundMessage(
sender_id="sales1",
message_id="lost-inline-1",
content="未成交,原因客户不想要了",
chat_id=chat,
chat_type="group",
),
flow=self.flow,
ledger=self.ledger,
)
self.assertEqual(phase, "done")
ticket = self.ledger.get_ticket(work_order_no=wo)
self.assertEqual(ticket.status, "未成交")
self.assertEqual(ticket.lost_reason, "客户不想要了")
def test_sales_can_close_deal_by_text(self) -> None:
"""群里销售说成交,要入账并回话,不能装没听见。相近说法也认。"""
wo, chat = self._open_group()