mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-08-14 06:30:57 +00:00
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:
@@ -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': '@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
|
||||||
Reference in New Issue
Block a user