fix: map feishu user identity for agent subscribe

This commit is contained in:
jxxghp
2026-05-13 12:14:05 +08:00
parent 35eb8c51a9
commit b24127e66f
3 changed files with 121 additions and 41 deletions

View File

@@ -51,6 +51,7 @@ from lark_oapi.event.callback.model.p2_card_action_trigger import (
from app.core.config import settings from app.core.config import settings
from app.core.context import Context, MediaInfo from app.core.context import Context, MediaInfo
from app.db.user_oper import UserOper
from app.log import logger from app.log import logger
from app.schemas import CommingMessage, Notification from app.schemas import CommingMessage, Notification
from app.schemas.types import MessageChannel, NotificationType from app.schemas.types import MessageChannel, NotificationType
@@ -269,6 +270,27 @@ class Feishu:
if normalized_user_id: if normalized_user_id:
self._user_receive_id_type_mapping[normalized_user_id] = "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: def _on_message(self, data: P2ImMessageReceiveV1) -> None:
"""处理飞书长连接收到的普通消息事件。""" """处理飞书长连接收到的普通消息事件。"""
event = getattr(data, "event", None) event = getattr(data, "event", None)
@@ -454,7 +476,11 @@ class Feishu:
sender = message.get("sender") or {} sender = message.get("sender") or {}
open_id = sender.get("open_id") open_id = sender.get("open_id")
user_id = sender.get("user_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 userid = open_id or user_id
if not userid: if not userid:
return None return None

View File

@@ -60,6 +60,32 @@ class TestAgentAddSubscribeTool(unittest.TestCase):
self.assertEqual(async_add.await_args.kwargs["username"], "tg_display_name") self.assertEqual(async_add.await_args.kwargs["username"], "tg_display_name")
self.assertIn("成功添加订阅The Matrix (1999)", result) 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__": if __name__ == "__main__":
unittest.main() unittest.main()

View File

@@ -123,19 +123,20 @@ class TestFeishu(unittest.TestCase):
def test_parse_message_returns_callback_message(self): def test_parse_message_returns_callback_message(self):
client = self._build_client() client = self._build_client()
result = client.parse_message( with patch("app.modules.feishu.feishu.UserOper.get_name", return_value=None):
{ result = client.parse_message(
"type": "cardAction", {
"callback_data": "approve", "type": "cardAction",
"message_id": "om_123", "callback_data": "approve",
"chat_id": "oc_123", "message_id": "om_123",
"sender": { "chat_id": "oc_123",
"open_id": "ou_user_1", "sender": {
"user_id": "u_user_1", "open_id": "ou_user_1",
"name": "tester", "user_id": "u_user_1",
}, "name": "tester",
} },
) }
)
self.assertIsNotNone(result) self.assertIsNotNone(result)
self.assertEqual(result.channel, MessageChannel.Feishu) self.assertEqual(result.channel, MessageChannel.Feishu)
@@ -195,7 +196,9 @@ class TestFeishu(unittest.TestCase):
def test_parse_message_blocks_non_admin_command(self): def test_parse_message_blocks_non_admin_command(self):
client = self._build_client(FEISHU_ADMINS="ou_admin") 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( result = client.parse_message(
{ {
"type": "message", "type": "message",
@@ -217,6 +220,30 @@ class TestFeishu(unittest.TestCase):
receive_id_type="open_id", 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): def test_send_notification_uses_direct_card_content(self):
client = self._build_client() client = self._build_client()
client._api_client, message_api = self._build_message_api( 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): def test_parse_message_supports_image_and_file_payloads(self):
client = self._build_client() client = self._build_client()
image_message = client.parse_message( with patch("app.modules.feishu.feishu.UserOper.get_name", return_value=None):
{ image_message = client.parse_message(
"type": "message", {
"text": "", "type": "message",
"images": [{"ref": "feishu://image/img_v2_test"}], "text": "",
"message_id": "om_img", "images": [{"ref": "feishu://image/img_v2_test"}],
"chat_id": "oc_chat", "message_id": "om_img",
"sender": { "chat_id": "oc_chat",
"open_id": "ou_user_5", "sender": {
"name": "tester", "open_id": "ou_user_5",
}, "name": "tester",
} },
) }
)
file_message = client.parse_message( file_message = client.parse_message(
{ {
"type": "message", "type": "message",
"text": "", "text": "",
"files": [{"ref": "feishu://file/file_key/report.pdf", "name": "report.pdf"}], "files": [{"ref": "feishu://file/file_key/report.pdf", "name": "report.pdf"}],
"message_id": "om_file", "message_id": "om_file",
"chat_id": "oc_chat", "chat_id": "oc_chat",
"sender": { "sender": {
"open_id": "ou_user_6", "open_id": "ou_user_6",
"name": "tester", "name": "tester",
}, },
} }
) )
self.assertEqual(image_message.images[0].ref, "feishu://image/img_v2_test") 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") self.assertEqual(file_message.files[0].ref, "feishu://file/file_key/report.pdf")