Переглянути джерело

feat: link Feishu alerts to Telegram chats

AI-Co-Authored-By: Codex
chendeben 1 місяць тому
батько
коміт
a1edffe3b7
2 змінених файлів з 128 додано та 11 видалено
  1. 67 2
      tests/test_business_assistant.py
  2. 61 9
      wbb/services/business_assistant.py

+ 67 - 2
tests/test_business_assistant.py

@@ -128,6 +128,27 @@ class FakeFeishuWebhook:
             }
         )
 
+    async def send_post(
+        self,
+        webhook_url: str,
+        *,
+        title: str,
+        content: list[list[dict[str, str]]],
+        signing_secret: str = "",
+    ) -> None:
+        self.sent.append(
+            {
+                "webhook_url": webhook_url,
+                "title": title,
+                "content": content,
+                "text": "\n".join(
+                    "".join(segment.get("text", "") for segment in line)
+                    for line in content
+                ),
+                "signing_secret": signing_secret,
+            }
+        )
+
 
 async def prepare_runtime(app_modules, *, provider: FakeProvider | None = None):
     dbassistant = app_modules.load("wbb.utils.dbassistant")
@@ -625,10 +646,14 @@ async def test_every_user_message_notifies_feishu_once(app_modules):
     assert feishu.sent[0]["webhook_url"] == webhook_url
     assert feishu.sent[0]["signing_secret"] == "secret"
     assert "营业时间是什么" not in feishu.sent[0]["text"]
-    assert feishu.sent[0]["text"].startswith("消息提醒\n")
+    assert feishu.sent[0]["title"] == "消息提醒"
     assert "用户:" in feishu.sent[0]["text"]
     assert "客户:" not in feishu.sent[0]["text"]
     assert "消息标识:" not in feishu.sent[0]["text"]
+    assert feishu.sent[0]["content"][1] == [
+        {"tag": "text", "text": "用户:"},
+        {"tag": "a", "text": "客户", "href": "https://t.me/user501"},
+    ]
 
     await dbassistant.update_account_settings(
         "conn-a", {"feishu_message_preview_enabled": True}
@@ -659,6 +684,23 @@ async def test_every_user_message_notifies_feishu_once(app_modules):
     assert "类型:非文本" in feishu.sent[2]["text"]
     assert "内容:图片说明" in feishu.sent[2]["text"]
 
+    await runtime.process_update(
+        {
+            "update_id": 1031,
+            "business_message": customer_message(
+                message_id=21,
+                chat_id=502,
+                sender_id=502,
+                **{"from": {"id": 502, "first_name": "无用户名用户"}},
+            ),
+        }
+    )
+    assert len(feishu.sent) == 4
+    assert feishu.sent[3]["content"][1] == [
+        {"tag": "text", "text": "用户:"},
+        {"tag": "text", "text": "无用户名用户"},
+    ]
+
     await runtime.process_update(
         {
             "update_id": 104,
@@ -681,7 +723,7 @@ async def test_every_user_message_notifies_feishu_once(app_modules):
             ),
         }
     )
-    assert len(feishu.sent) == 3
+    assert len(feishu.sent) == 4
     conversation = await dbassistant.get_conversation_by_chat("conn-a", 501)
     messages = await dbassistant.recent_conversation_messages(
         conversation["conversation_id"], limit=10
@@ -946,6 +988,29 @@ async def test_feishu_webhook_signature_and_success_contract(app_modules):
     assert session.payload["timestamp"]
     assert session.payload["sign"]
 
+    post_content = [
+        [
+            {"tag": "text", "text": "用户:"},
+            {"tag": "a", "text": "测试用户", "href": "https://t.me/test_user"},
+        ]
+    ]
+    await client.send_post(
+        "https://open.feishu.cn/open-apis/bot/v2/hook/12345678-abcd-4321-abcd-123456789abc",
+        title="消息提醒",
+        content=post_content,
+    )
+    assert session.payload == {
+        "msg_type": "post",
+        "content": {
+            "post": {
+                "zh_cn": {
+                    "title": "消息提醒",
+                    "content": post_content,
+                }
+            }
+        },
+    }
+
 
 async def _login_and_change_password(client: TestClient) -> str:
     login = await client.post(

+ 61 - 9
wbb/services/business_assistant.py

@@ -104,6 +104,40 @@ class FeishuWebhookClient:
             "msg_type": "text",
             "content": {"text": str(text)[:4000]},
         }
+        await self._send_payload(
+            webhook_url, payload, signing_secret=signing_secret
+        )
+
+    async def send_post(
+        self,
+        webhook_url: str,
+        *,
+        title: str,
+        content: list[list[dict[str, str]]],
+        signing_secret: str = "",
+    ) -> None:
+        payload: dict[str, Any] = {
+            "msg_type": "post",
+            "content": {
+                "post": {
+                    "zh_cn": {
+                        "title": str(title)[:100],
+                        "content": content,
+                    }
+                }
+            },
+        }
+        await self._send_payload(
+            webhook_url, payload, signing_secret=signing_secret
+        )
+
+    async def _send_payload(
+        self,
+        webhook_url: str,
+        payload: dict[str, Any],
+        *,
+        signing_secret: str = "",
+    ) -> None:
         if signing_secret:
             timestamp = int(utc_now().timestamp())
             payload.update(
@@ -1113,25 +1147,43 @@ class BusinessAssistantRuntime:
         preview_text = str(
             message.get("text") or message.get("caption") or ""
         ).strip()
-        lines = [
-            "消息提醒",
-            f"账号:{account_name}",
-            f"用户:{user_name}",
-            f"类型:{message_type}",
+        content: list[list[dict[str, str]]] = [
+            [{"tag": "text", "text": f"账号:{account_name}"}],
+            [{"tag": "text", "text": "用户:"}],
+            [{"tag": "text", "text": f"类型:{message_type}"}],
         ]
+        telegram_username = str(sender.get("username") or "").strip().lstrip("@")
+        if re.fullmatch(r"[A-Za-z0-9_]{1,64}", telegram_username):
+            content[1].append(
+                {
+                    "tag": "a",
+                    "text": user_name,
+                    "href": f"https://t.me/{telegram_username}",
+                }
+            )
+        else:
+            content[1].append({"tag": "text", "text": user_name})
         if settings.get("feishu_message_preview_enabled"):
-            lines.append(f"内容:{preview_text[:500] if preview_text else '[非文本消息]'}")
+            content.append(
+                [
+                    {
+                        "tag": "text",
+                        "text": f"内容:{preview_text[:500] if preview_text else '[非文本消息]'}",
+                    }
+                ]
+            )
         try:
             message_time = datetime.fromtimestamp(
                 int(message.get("date") or 0), UTC
             ).strftime("%Y-%m-%d %H:%M:%S UTC")
         except (OSError, OverflowError, TypeError, ValueError):
             message_time = "未知"
-        lines.append(f"时间:{message_time}")
+        content.append([{"tag": "text", "text": f"时间:{message_time}"}])
         try:
-            await self.feishu.send(
+            await self.feishu.send_post(
                 webhook_url,
-                "\n".join(lines),
+                title="消息提醒",
+                content=content,
                 signing_secret=str(settings.get("feishu_webhook_signing_secret") or ""),
             )
         except FeishuWebhookError as exc: