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
This commit is contained in:
fdc310
2026-08-13 11:18:09 +08:00
parent 42f1f00772
commit e211d3aae6
2 changed files with 146 additions and 17 deletions
+16 -17
View File
@@ -187,34 +187,36 @@ class ItchatEventConverter(abstract_platform_adapter.AbstractEventConverter):
bot_account_id = adapter.bot_account_id bot_account_id = adapter.bot_account_id
bot_nickname = adapter._bot_nickname 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) message_chain = ItchatMessageConverter.target2yiri(msg)
if not message_chain: if not message_chain:
return None return None
# Determine if this is a group message # Determine if this is a group message
# itchat uses '@@' prefix for chatroom IDs (not '@chatroom' suffix) # itchat uses '@@' prefix for chatroom IDs (not '@chatroom' suffix)
is_group = '@@' in from_user is_group = from_user.startswith('@@')
timestamp = msg.get('CreateTime', 0) timestamp = msg.get('CreateTime', 0)
if is_group: if is_group:
# Actual sender within the group # Actual sender within the group
actual_user = msg.get('ActualUserName', '') actual_user = msg.get('ActualUserName', '')
actual_nick = msg.get('ActualNickName', '') actual_nick = msg.get('ActualNickName', '') or actual_user
if not actual_nick: if not actual_user:
actual_nick = actual_user return None
# Prepend @bot if the bot was mentioned # Prepend @bot if the bot was mentioned
# itchat uses 'IsAt' (capital I, capital A) in produce_group_chat # itchat uses 'IsAt' (capital I, capital A) in produce_group_chat
if msg.get('IsAt', False): if msg.get('IsAt', False):
# Strip @bot_nickname from the text content to avoid LLM confusion # Strip @bot_nickname from the text content to avoid LLM confusion
if bot_nickname: 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: for component in message_chain:
if isinstance(component, platform_message.Plain): if isinstance(component, platform_message.Plain):
if component.text.startswith(at_pattern): component.text = at_re.sub('', component.text, count=1)
component.text = component.text[len(at_pattern) :]
elif at_pattern in component.text:
component.text = component.text.replace(at_pattern, '')
break break
message_chain = platform_message.MessageChain( message_chain = platform_message.MessageChain(
[platform_message.At(target=bot_account_id)] + list(message_chain) [platform_message.At(target=bot_account_id)] + list(message_chain)
@@ -230,8 +232,8 @@ class ItchatEventConverter(abstract_platform_adapter.AbstractEventConverter):
return platform_events.GroupMessage( return platform_events.GroupMessage(
sender=platform_entities.GroupMember( sender=platform_entities.GroupMember(
id=actual_user or actual_nick, id=actual_user,
member_name=actual_nick or actual_user, member_name=actual_nick,
permission=platform_entities.Permission.Member, permission=platform_entities.Permission.Member,
group=platform_entities.Group( group=platform_entities.Group(
id=from_user, id=from_user,
@@ -246,18 +248,15 @@ class ItchatEventConverter(abstract_platform_adapter.AbstractEventConverter):
) )
else: else:
# Private / friend message # Private / friend message
sender_nick = ''
user_obj = msg.get('User', {}) user_obj = msg.get('User', {})
if hasattr(user_obj, 'NickName'): sender_nick = adapter._get_obj_value(user_obj, 'NickName')
sender_nick = user_obj.NickName sender_remark = adapter._get_obj_value(user_obj, 'RemarkName')
elif isinstance(user_obj, dict):
sender_nick = user_obj.get('NickName', '')
return platform_events.FriendMessage( return platform_events.FriendMessage(
sender=platform_entities.Friend( sender=platform_entities.Friend(
id=from_user, id=from_user,
nickname=sender_nick or from_user, nickname=sender_nick or from_user,
remark='', remark=sender_remark,
), ),
message_chain=message_chain, message_chain=message_chain,
time=timestamp, time=timestamp,
@@ -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': '@MyBothello 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