From e211d3aae60d53a80acc7aa821af2b7b20afd218 Mon Sep 17 00:00:00 2001 From: fdc310 <2213070223@qq.com> Date: Thu, 13 Aug 2026 11:18:09 +0800 Subject: [PATCH] fix(itchat): correct group/private message handling and sender resolution - filter the bot's own messages to prevent reply loops - use startswith('@@') for group detection - use ActualUserName as stable GroupMember id - fill Friend.remark from RemarkName - strip @mention prefix with a regex --- src/langbot/pkg/platform/sources/itchat.py | 33 +++-- .../platform/test_itchat_adapter.py | 130 ++++++++++++++++++ 2 files changed, 146 insertions(+), 17 deletions(-) create mode 100644 tests/unit_tests/platform/test_itchat_adapter.py diff --git a/src/langbot/pkg/platform/sources/itchat.py b/src/langbot/pkg/platform/sources/itchat.py index 0edc7f011..2356a7b8c 100644 --- a/src/langbot/pkg/platform/sources/itchat.py +++ b/src/langbot/pkg/platform/sources/itchat.py @@ -187,34 +187,36 @@ class ItchatEventConverter(abstract_platform_adapter.AbstractEventConverter): bot_account_id = adapter.bot_account_id bot_nickname = adapter._bot_nickname + # Ignore the bot's own messages to avoid reply loops + bot_user_name = getattr(adapter._core.storageClass, 'userName', '') + if from_user == bot_account_id or (bot_user_name and from_user == bot_user_name): + return None + message_chain = ItchatMessageConverter.target2yiri(msg) if not message_chain: return None # Determine if this is a group message # itchat uses '@@' prefix for chatroom IDs (not '@chatroom' suffix) - is_group = '@@' in from_user + is_group = from_user.startswith('@@') timestamp = msg.get('CreateTime', 0) if is_group: # Actual sender within the group actual_user = msg.get('ActualUserName', '') - actual_nick = msg.get('ActualNickName', '') - if not actual_nick: - actual_nick = actual_user + actual_nick = msg.get('ActualNickName', '') or actual_user + if not actual_user: + return None # Prepend @bot if the bot was mentioned # itchat uses 'IsAt' (capital I, capital A) in produce_group_chat if msg.get('IsAt', False): # Strip @bot_nickname from the text content to avoid LLM confusion if bot_nickname: - at_pattern = '@' + bot_nickname + (' ' if ' ' in msg.get('Content', '') else ' ') + at_re = re.compile(re.escape('@' + bot_nickname) + r'[  ]?') for component in message_chain: if isinstance(component, platform_message.Plain): - if component.text.startswith(at_pattern): - component.text = component.text[len(at_pattern) :] - elif at_pattern in component.text: - component.text = component.text.replace(at_pattern, '') + component.text = at_re.sub('', component.text, count=1) break message_chain = platform_message.MessageChain( [platform_message.At(target=bot_account_id)] + list(message_chain) @@ -230,8 +232,8 @@ class ItchatEventConverter(abstract_platform_adapter.AbstractEventConverter): return platform_events.GroupMessage( sender=platform_entities.GroupMember( - id=actual_user or actual_nick, - member_name=actual_nick or actual_user, + id=actual_user, + member_name=actual_nick, permission=platform_entities.Permission.Member, group=platform_entities.Group( id=from_user, @@ -246,18 +248,15 @@ class ItchatEventConverter(abstract_platform_adapter.AbstractEventConverter): ) else: # Private / friend message - sender_nick = '' user_obj = msg.get('User', {}) - if hasattr(user_obj, 'NickName'): - sender_nick = user_obj.NickName - elif isinstance(user_obj, dict): - sender_nick = user_obj.get('NickName', '') + sender_nick = adapter._get_obj_value(user_obj, 'NickName') + sender_remark = adapter._get_obj_value(user_obj, 'RemarkName') return platform_events.FriendMessage( sender=platform_entities.Friend( id=from_user, nickname=sender_nick or from_user, - remark='', + remark=sender_remark, ), message_chain=message_chain, time=timestamp, diff --git a/tests/unit_tests/platform/test_itchat_adapter.py b/tests/unit_tests/platform/test_itchat_adapter.py new file mode 100644 index 000000000..0d2ca85b6 --- /dev/null +++ b/tests/unit_tests/platform/test_itchat_adapter.py @@ -0,0 +1,130 @@ +"""Tests for itchat adapter group/private message conversion.""" + +from __future__ import annotations + +from types import SimpleNamespace + +import langbot_plugin.api.entities.builtin.platform.entities as platform_entities +import langbot_plugin.api.entities.builtin.platform.events as platform_events +import langbot_plugin.api.entities.builtin.platform.message as platform_message + +from langbot.pkg.platform import botmgr as _botmgr # noqa: F401 + +from langbot.pkg.platform.sources.itchat import ItchatAdapter, ItchatEventConverter + + +def _make_adapter(bot_account_id: str = '@bot_wxid', bot_nickname: str = 'MyBot'): + adapter = SimpleNamespace( + bot_account_id=bot_account_id, + _bot_nickname=bot_nickname, + _core=SimpleNamespace(storageClass=SimpleNamespace(userName=bot_account_id)), + _get_obj_value=ItchatAdapter._get_obj_value, + ) + return adapter + + +def _make_converter(adapter) -> ItchatEventConverter: + return ItchatEventConverter(adapter_ref=lambda: adapter) + + +def test_group_text_becomes_group_message(): + converter = _make_converter(_make_adapter()) + + msg = { + 'FromUserName': '@@group_wxid', + 'Type': 'Text', + 'Text': 'hello', + 'ActualUserName': '@member_wxid', + 'ActualNickName': 'MemberNick', + 'IsAt': False, + 'CreateTime': 123456, + 'User': SimpleNamespace(NickName='Group Name'), + } + + event = converter.target2yiri(msg) + + assert isinstance(event, platform_events.GroupMessage) + assert isinstance(event.sender, platform_entities.GroupMember) + assert event.sender.id == '@member_wxid' + assert event.sender.member_name == 'MemberNick' + assert event.sender.group.id == '@@group_wxid' + assert event.sender.group.name == 'Group Name' + + components = list(event.message_chain) + assert len(components) == 1 + assert isinstance(components[0], platform_message.Plain) + assert components[0].text == 'hello' + + +def test_private_text_becomes_friend_message(): + converter = _make_converter(_make_adapter()) + + msg = { + 'FromUserName': '@friend_wxid', + 'Type': 'Text', + 'Text': 'hi', + 'CreateTime': 123456, + 'User': SimpleNamespace(NickName='FriendNick', RemarkName='FriendRemark'), + } + + event = converter.target2yiri(msg) + + assert isinstance(event, platform_events.FriendMessage) + assert isinstance(event.sender, platform_entities.Friend) + assert event.sender.id == '@friend_wxid' + assert event.sender.nickname == 'FriendNick' + assert event.sender.remark == 'FriendRemark' + + +def test_bot_own_message_is_ignored(): + converter = _make_converter(_make_adapter(bot_account_id='@bot_wxid')) + + msg = { + 'FromUserName': '@bot_wxid', + 'Type': 'Text', + 'Text': 'self echo', + } + + assert converter.target2yiri(msg) is None + + +def test_group_at_bot_strips_prefix_and_adds_at(): + converter = _make_converter(_make_adapter(bot_account_id='@bot_wxid', bot_nickname='MyBot')) + + msg = { + 'FromUserName': '@@group_wxid', + 'Type': 'Text', + 'Text': '@MyBot hello world', + 'ActualUserName': '@member_wxid', + 'ActualNickName': 'MemberNick', + 'IsAt': True, + 'CreateTime': 123456, + 'User': SimpleNamespace(NickName='Group Name'), + } + + event = converter.target2yiri(msg) + + assert isinstance(event, platform_events.GroupMessage) + components = list(event.message_chain) + assert len(components) == 2 + assert isinstance(components[0], platform_message.At) + assert components[0].target == '@bot_wxid' + assert isinstance(components[1], platform_message.Plain) + assert components[1].text == 'hello world' + + +def test_group_message_without_sender_is_ignored(): + converter = _make_converter(_make_adapter()) + + msg = { + 'FromUserName': '@@group_wxid', + 'Type': 'Text', + 'Text': 'system note', + 'ActualUserName': '', + 'ActualNickName': '', + 'IsAt': False, + 'CreateTime': 123456, + 'User': SimpleNamespace(NickName='Group Name'), + } + + assert converter.target2yiri(msg) is None