From c24d8c19c08ffc30b4630f05234b1ad137f3772c Mon Sep 17 00:00:00 2001 From: jillion <439912596@qq.com> Date: Mon, 21 Sep 2026 18:16:18 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BA=8B=E4=BB=B6=E5=BC=82=E5=B8=B8?= =?UTF-8?q?=E4=BC=9A=E8=AF=9D=E8=AF=9D=E6=9C=AF=E4=B8=8E=E8=AF=A2=E4=BB=B7?= =?UTF-8?q?=E6=B6=88=E6=81=AF=E6=9C=8D=E5=8A=A1=E5=8D=A1=E7=89=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- inquiry-agent/agent/policy/inquiry_copy.py | 133 ++++++++++++++++++-- inquiry-agent/tests/test_event_exception.py | 61 +++++++++ 2 files changed, 186 insertions(+), 8 deletions(-) diff --git a/inquiry-agent/agent/policy/inquiry_copy.py b/inquiry-agent/agent/policy/inquiry_copy.py index f8ef3a4..f472440 100644 --- a/inquiry-agent/agent/policy/inquiry_copy.py +++ b/inquiry-agent/agent/policy/inquiry_copy.py @@ -2439,6 +2439,17 @@ def air_lock_fail(*, work_order_no: str, operator: str, reason: str) -> str: ) +def air_cabin_fail_reason(*, tms_reason: str = "", flight_date: str = "", today: str = "") -> str: + """ + 锁舱/释放失败原因:原样回 TMS 的 failReason。 + 没有报价也可以锁舱,禁止改写成「报价选项不存在」或「航班已起飞」。 + flight_date / today 只为兼容旧调用,不再用来改写原文。 + """ + _ = flight_date + _ = today + return (tms_reason or "").strip() + + def air_release_ok( *, work_order_no: str, @@ -2459,9 +2470,10 @@ def air_release_ok( def air_release_fail(*, work_order_no: str, operator: str, reason: str) -> str: why = (reason or "").strip() or "TMS未返回原因" return ( - f"工单{work_order_no} 空运释放舱位失败\n" + f"工单{work_order_no} 空运释放舱位失败。\n" f"操作人:{operator}\n" - f"释放舱位失败的原因:{why}" + f"释放舱位失败的原因:{why}\n" + "已通知IT运维排查,请稍等。" ) @@ -2484,6 +2496,8 @@ STEP_LLM_NO_TICKET = "llm_no_ticket" STEP_LLM = "llm" STEP_TMS_QUOTE = "tms_quote" STEP_TMS_CABIN = "tms_cabin" +STEP_TMS_LOCK = "tms_lock" +STEP_TMS_RELEASE = "tms_release" STEP_GROUP = "group" SYS_EXC_LLM_FALLBACK = "大模型服务异常,IT排查中..." @@ -2497,6 +2511,8 @@ IMPACT_LLM_NO_TICKET = "尚未建单,不落后台工单" IMPACT_LLM = "当前识别暂停,已有字段与工单状态保持不变" IMPACT_TMS_QUOTE = "当前询价流程暂停,识别字段与原业务节点保持不变" IMPACT_TMS_CABIN = "当前锁舱/释放暂停,工单状态保持不变" +IMPACT_TMS_LOCK = "锁舱失败,工单状态保持不变" +IMPACT_TMS_RELEASE = "释放舱位失败,工单状态保持不变" IMPACT_GROUP = "当前建群/加人暂停,已建群不新建第二个群" @@ -2519,7 +2535,7 @@ def system_exception_recovery(step: str) -> str: key = (step or "").strip() if key == STEP_LLM or key == STEP_LLM_NO_TICKET: return SYS_EXC_RECOVERY_LLM - if key == STEP_TMS_CABIN: + if key in {STEP_TMS_CABIN, STEP_TMS_LOCK, STEP_TMS_RELEASE}: return SYS_EXC_RECOVERY_CABIN if key == STEP_GROUP: return SYS_EXC_RECOVERY_GROUP @@ -2533,6 +2549,10 @@ def system_exception_impact(step: str) -> str: return IMPACT_LLM_NO_TICKET if key == STEP_LLM: return IMPACT_LLM + if key == STEP_TMS_LOCK: + return IMPACT_TMS_LOCK + if key == STEP_TMS_RELEASE: + return IMPACT_TMS_RELEASE if key == STEP_TMS_CABIN: return IMPACT_TMS_CABIN if key == STEP_GROUP: @@ -2554,11 +2574,11 @@ def system_exception_alert_payload( 系统异常通知卡:走询价消息服务,不走询价小助手。 五行对齐现网示例:异常类型、接口/服务、错误码、影响范围、发生时间。 + 标题不加 [test]:运维要看的就是这张卡本身,test_prefix 仅保留兼容入参。 """ no = (work_order_no or "").strip() or "无" + _ = test_prefix title = "系统异常通知" - if test_prefix: - title = "[test] " + title rows = [ {"keyname": "异常类型", "value": (exception_type or "").strip() or "-"}, {"keyname": "接口/服务", "value": (service or "").strip() or "-"}, @@ -2596,11 +2616,10 @@ def system_exception_alert_text( occurred_at: str, test_prefix: bool = False, ) -> str: - """告警卡纯文本兜底(通道降级时仍能看懂)。""" + """告警卡纯文本兜底(通道降级时仍能看懂)。标题同样不加 [test]。""" no = (work_order_no or "").strip() or "无" + _ = test_prefix head = "系统异常通知" - if test_prefix: - head = "[test] " + head lines = [ "询价机器人系统告警", head, @@ -2612,3 +2631,101 @@ def system_exception_alert_text( f"发生时间:{(occurred_at or '').strip() or '-'}", ] return "\n".join(lines) + + +def event_exception_session_text( + *, + exception_type: str, + work_order_no: str, + names: list[str] | None, + hours: float, + mention: bool, + test_prefix: bool = False, +) -> str: + """ + 事件异常会话两行话术。 + + mention=True 时姓名前加 @,多人空格拼接。小时数用后台规则值。 + """ + from agent.policy.event_exception import format_timeout_hours + + no = (work_order_no or "").strip() or "-" + kind = (exception_type or "").strip() or "事件异常" + hour_text = format_timeout_hours(hours) + people = [str(n).strip() for n in (names or []) if str(n).strip()] + if mention: + tagged = " ".join(f"@{n}" for n in people) if people else "相关人员" + else: + tagged = " ".join(people) if people else "您" + body = ( + f"关联询价工单 {no}。{tagged}已超过 {hour_text} 工作小时 无响应," + f"事件异常已记录工单,请及时处理。" + ) + text = f"{kind}\n{body}" + if test_prefix and not text.startswith("[test]"): + return f"[test] {text}" + return text + + +def event_exception_alert_payload( + *, + card_title: str, + work_order_no: str, + hours: float, + test_prefix: bool = False, +) -> dict[str, Any]: + """ + 事件异常通知卡:走询价消息服务,不走询价小助手。 + + 对齐现网「询价机器人事件告警」:标题用响应异常,正文用「您已超过」。 + """ + from agent.policy.event_exception import format_timeout_hours + + no = (work_order_no or "").strip() or "-" + title = (card_title or "").strip() or "事件响应异常" + if test_prefix and not title.startswith("[test]"): + title = f"[test] {title}" + hour_text = format_timeout_hours(hours) + from agent.config import get_settings + + base = str(getattr(get_settings(), "public_base_url", "") or "").strip().rstrip("/") + action_url = base or "https://work.weixin.qq.com" + return { + "msgtype": "template_card", + "wecom_app": "notify", + "template_card": { + "card_type": "text_notice", + "source": {"desc": "询价机器人事件告警", "desc_color": 0}, + "main_title": { + "title": title, + "desc": f"关联询价工单 {no}", + }, + "sub_title_text": f"您已超过 {hour_text} 工作小时无响应,事件异常已记录工单,请及时处理。", + "card_action": {"type": 1, "url": action_url}, + }, + } + + +def event_exception_alert_text( + *, + card_title: str, + work_order_no: str, + hours: float, + test_prefix: bool = False, +) -> str: + """应用卡片纯文本兜底。""" + payload = event_exception_alert_payload( + card_title=card_title, + work_order_no=work_order_no, + hours=hours, + test_prefix=test_prefix, + ) + card = payload.get("template_card") or {} + main = card.get("main_title") or {} + lines = [ + str((card.get("source") or {}).get("desc") or "询价机器人事件告警"), + str(main.get("title") or ""), + str(main.get("desc") or ""), + str(card.get("sub_title_text") or ""), + ] + return "\n".join(x for x in lines if x) diff --git a/inquiry-agent/tests/test_event_exception.py b/inquiry-agent/tests/test_event_exception.py index f8945d4..7adab43 100644 --- a/inquiry-agent/tests/test_event_exception.py +++ b/inquiry-agent/tests/test_event_exception.py @@ -12,6 +12,7 @@ ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) if ROOT not in sys.path: sys.path.insert(0, ROOT) +from agent.policy import inquiry_copy as copy from agent.policy.event_exception import ( WAIT_ADOPT, WAIT_AIR_QUOTE, @@ -102,5 +103,65 @@ class TestMentions(unittest.TestCase): self.assertEqual(conversation_of(wait_kind=WAIT_DEAL, collab_chat_id=""), "private") +class TestEventExceptionCopy(unittest.TestCase): + def test_group_air(self) -> None: + text = copy.event_exception_session_text( + exception_type="航线响应超时", + work_order_no="WO202606180003", + names=["张三"], + hours=0.25, + mention=True, + ) + self.assertEqual( + text, + "航线响应超时\n关联询价工单 WO202606180003。@张三已超过 0.25 工作小时 无响应,事件异常已记录工单,请及时处理。", + ) + + def test_group_two_names(self) -> None: + text = copy.event_exception_session_text( + exception_type="产品响应超时", + work_order_no="WO1", + names=["张三", "李四"], + hours=2, + mention=True, + ) + self.assertIn("@张三 @李四已超过 2 工作小时", text) + + def test_private_sales_no_at(self) -> None: + text = copy.event_exception_session_text( + exception_type="销售响应超时", + work_order_no="WO1", + names=["王五"], + hours=48, + mention=False, + ) + self.assertIn("王五已超过 48 工作小时", text) + self.assertNotIn("@王五", text) + + def test_test_prefix(self) -> None: + text = copy.event_exception_session_text( + exception_type="销售响应超时", + work_order_no="WO1", + names=["王五"], + hours=1, + mention=False, + test_prefix=True, + ) + self.assertTrue(text.startswith("[test] ")) + + def test_app_card(self) -> None: + payload = copy.event_exception_alert_payload( + card_title="销售响应异常", + work_order_no="WO202609090199", + hours=48, + ) + self.assertEqual(payload["wecom_app"], "notify") + card = payload["template_card"] + self.assertEqual(card["source"]["desc"], "询价机器人事件告警") + self.assertEqual(card["main_title"]["title"], "销售响应异常") + self.assertEqual(card["main_title"]["desc"], "关联询价工单 WO202609090199") + self.assertIn("您已超过 48 工作小时无响应", card["sub_title_text"]) + + if __name__ == "__main__": unittest.main()