diff --git a/app/modules/feishu/feishu.py b/app/modules/feishu/feishu.py index e501ec44..ae58aaf4 100644 --- a/app/modules/feishu/feishu.py +++ b/app/modules/feishu/feishu.py @@ -51,6 +51,7 @@ from lark_oapi.event.callback.model.p2_card_action_trigger import ( from app.core.config import settings from app.core.context import Context, MediaInfo +from app.db.user_oper import UserOper from app.log import logger from app.schemas import CommingMessage, Notification from app.schemas.types import MessageChannel, NotificationType @@ -269,6 +270,27 @@ class Feishu: if normalized_user_id: self._user_receive_id_type_mapping[normalized_user_id] = "user_id" + @staticmethod + def _resolve_username( + open_id: Optional[str], + user_id: Optional[str], + fallback: Optional[str], + ) -> Optional[str]: + """根据飞书绑定 ID 映射 MoviePilot 用户名,未绑定时保留渠道名称。""" + binding_ids = {} + if open_id: + binding_ids["feishu_openid"] = open_id + if user_id: + binding_ids["feishu_userid"] = user_id + if binding_ids: + try: + mapped_username = UserOper().get_name(**binding_ids) + if mapped_username: + return mapped_username + except Exception as err: + logger.debug(f"解析飞书用户绑定失败:{err}") + return fallback + def _on_message(self, data: P2ImMessageReceiveV1) -> None: """处理飞书长连接收到的普通消息事件。""" event = getattr(data, "event", None) @@ -454,7 +476,11 @@ class Feishu: sender = message.get("sender") or {} open_id = sender.get("open_id") user_id = sender.get("user_id") - username = sender.get("name") or open_id or user_id + username = self._resolve_username( + open_id=open_id, + user_id=user_id, + fallback=sender.get("name") or open_id or user_id, + ) userid = open_id or user_id if not userid: return None diff --git a/tests/test_agent_add_subscribe_tool.py b/tests/test_agent_add_subscribe_tool.py index 983c836d..ac96ef2d 100644 --- a/tests/test_agent_add_subscribe_tool.py +++ b/tests/test_agent_add_subscribe_tool.py @@ -60,6 +60,32 @@ class TestAgentAddSubscribeTool(unittest.TestCase): self.assertEqual(async_add.await_args.kwargs["username"], "tg_display_name") self.assertIn("成功添加订阅:The Matrix (1999)", result) + def test_feishu_subscription_uses_pre_resolved_username_when_openid_lookup_misses(self): + tool = AddSubscribeTool(session_id="session-1", user_id="ou_feishu_user") + tool.set_message_attr( + channel=MessageChannel.Feishu.value, + source="feishu-main", + username="moviepilot-user", + ) + + with patch( + "app.agent.tools.impl.add_subscribe.SubscribeChain.async_add", + new=AsyncMock(return_value=(1, "")), + ) as async_add, patch( + "app.agent.tools.impl.add_subscribe.UserOper.get_name", + return_value=None, + ): + result = asyncio.run( + tool.run( + title="The Matrix", + year="1999", + media_type="movie", + ) + ) + + self.assertEqual(async_add.await_args.kwargs["username"], "moviepilot-user") + self.assertIn("成功添加订阅:The Matrix (1999)", result) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_feishu.py b/tests/test_feishu.py index 7e662b85..23bcd2a9 100644 --- a/tests/test_feishu.py +++ b/tests/test_feishu.py @@ -123,19 +123,20 @@ class TestFeishu(unittest.TestCase): def test_parse_message_returns_callback_message(self): client = self._build_client() - result = client.parse_message( - { - "type": "cardAction", - "callback_data": "approve", - "message_id": "om_123", - "chat_id": "oc_123", - "sender": { - "open_id": "ou_user_1", - "user_id": "u_user_1", - "name": "tester", - }, - } - ) + with patch("app.modules.feishu.feishu.UserOper.get_name", return_value=None): + result = client.parse_message( + { + "type": "cardAction", + "callback_data": "approve", + "message_id": "om_123", + "chat_id": "oc_123", + "sender": { + "open_id": "ou_user_1", + "user_id": "u_user_1", + "name": "tester", + }, + } + ) self.assertIsNotNone(result) self.assertEqual(result.channel, MessageChannel.Feishu) @@ -195,7 +196,9 @@ class TestFeishu(unittest.TestCase): def test_parse_message_blocks_non_admin_command(self): client = self._build_client(FEISHU_ADMINS="ou_admin") - with patch.object(client, "send_text", return_value={"success": True}) as send_text: + with patch("app.modules.feishu.feishu.UserOper.get_name", return_value=None), patch.object( + client, "send_text", return_value={"success": True} + ) as send_text: result = client.parse_message( { "type": "message", @@ -217,6 +220,30 @@ class TestFeishu(unittest.TestCase): receive_id_type="open_id", ) + def test_parse_message_maps_feishu_ids_to_moviepilot_username(self): + client = self._build_client() + + with patch("app.modules.feishu.feishu.UserOper.get_name", return_value="moviepilot-user") as get_name: + result = client.parse_message( + { + "type": "message", + "text": "/ai 添加黑客帝国订阅", + "sender": { + "open_id": "ou_bound_user", + "user_id": "u_bound_user", + "name": "ou_bound_user", + }, + } + ) + + self.assertIsNotNone(result) + self.assertEqual(result.userid, "ou_bound_user") + self.assertEqual(result.username, "moviepilot-user") + get_name.assert_called_once_with( + feishu_openid="ou_bound_user", + feishu_userid="u_bound_user", + ) + def test_send_notification_uses_direct_card_content(self): client = self._build_client() client._api_client, message_api = self._build_message_api( @@ -499,33 +526,34 @@ class TestFeishu(unittest.TestCase): def test_parse_message_supports_image_and_file_payloads(self): client = self._build_client() - image_message = client.parse_message( - { - "type": "message", - "text": "", - "images": [{"ref": "feishu://image/img_v2_test"}], - "message_id": "om_img", - "chat_id": "oc_chat", - "sender": { - "open_id": "ou_user_5", - "name": "tester", - }, - } - ) + with patch("app.modules.feishu.feishu.UserOper.get_name", return_value=None): + image_message = client.parse_message( + { + "type": "message", + "text": "", + "images": [{"ref": "feishu://image/img_v2_test"}], + "message_id": "om_img", + "chat_id": "oc_chat", + "sender": { + "open_id": "ou_user_5", + "name": "tester", + }, + } + ) - file_message = client.parse_message( - { - "type": "message", - "text": "", - "files": [{"ref": "feishu://file/file_key/report.pdf", "name": "report.pdf"}], - "message_id": "om_file", - "chat_id": "oc_chat", - "sender": { - "open_id": "ou_user_6", - "name": "tester", - }, - } - ) + file_message = client.parse_message( + { + "type": "message", + "text": "", + "files": [{"ref": "feishu://file/file_key/report.pdf", "name": "report.pdf"}], + "message_id": "om_file", + "chat_id": "oc_chat", + "sender": { + "open_id": "ou_user_6", + "name": "tester", + }, + } + ) self.assertEqual(image_message.images[0].ref, "feishu://image/img_v2_test") self.assertEqual(file_message.files[0].ref, "feishu://file/file_key/report.pdf")