feat: 事件异常会话话术与询价消息服务卡片

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-21 18:16:18 +08:00
co-authored by Cursor
parent 2527901efd
commit c24d8c19c0
2 changed files with 186 additions and 8 deletions
+125 -8
View File
@@ -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)
@@ -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()