refactor: 将提及检测移动到Adapter端,添加名称和提及兴趣

feature-mention_bot
Oct-autumn 2025-06-04 22:13:44 +08:00
parent d64670a930
commit f1ef511b7c
No known key found for this signature in database
GPG Key ID: 53C510DF2E6149E8
4 changed files with 199 additions and 85 deletions

View File

@ -14,8 +14,9 @@ from src.config.official_configs import (
ChatConfig,
DebugConfig,
MaiBotServerConfig,
MentionInterestConfig,
NapcatServerConfig,
NicknameConfig,
NamesConfig,
VoiceConfig,
)
@ -106,10 +107,11 @@ def update_config():
class Config(ConfigBase):
"""总配置类"""
nickname: NicknameConfig
napcat_server: NapcatServerConfig
maibot_server: MaiBotServerConfig
chat: ChatConfig
names: NamesConfig
mention_interest: MentionInterestConfig
voice: VoiceConfig
debug: DebugConfig

View File

@ -14,12 +14,6 @@ from src.config.config_base import ConfigBase
ADAPTER_PLATFORM = "qq"
@dataclass
class NicknameConfig(ConfigBase):
nickname: str
"""机器人昵称"""
@dataclass
class NapcatServerConfig(ConfigBase):
host: str = "localhost"
@ -61,9 +55,39 @@ class ChatConfig(ConfigBase):
ban_user_id: list[str] = field(default_factory=[])
"""被封禁的用户ID列表封禁后将无法与其进行交互"""
@dataclass
class NamesConfig(ConfigBase):
name: str
"""机器人全名"""
alias_names: list[str] = field(default_factory=[])
"""机器人别名列表,支持多个别名"""
@dataclass
class MentionInterestConfig(ConfigBase):
at: float = 1.0
"""at的兴趣权重"""
reply: float = 1.0
"""回复的兴趣权重"""
enable_poke: bool = True
"""是否启用戳一戳功能"""
poke: float = 1.0
"""戳一戳的兴趣权重(在启用戳一戳时有效)"""
enable_mention_in_text: bool = True
"""是否启用在正文中提及"""
name_in_text: float = 0.4
"""正文中提及全名的兴趣权重在enable_mention_in_text为True时有效"""
alias_in_text: float = 0.2
"""正文中提及别名的兴趣权重在enable_mention_in_text为True时有效"""
@dataclass
class VoiceConfig(ConfigBase):

View File

@ -264,7 +264,7 @@ class RecvHandler:
case RealMessageType.text:
ret_seg = await self.handle_text_message(sub_message)
if ret_seg:
seg_message.append(ret_seg)
seg_message.extend(ret_seg)
else:
logger.warning("text处理失败")
case RealMessageType.face:
@ -274,12 +274,20 @@ class RecvHandler:
else:
logger.warning("face处理失败或不支持")
case RealMessageType.reply:
if not in_reply:
ret_seg = await self.handle_reply_message(sub_message)
if ret_seg:
seg_message += ret_seg
else:
logger.warning("reply处理失败")
if in_reply:
# 如果是回复消息,则不再处理回复消息
logger.debug("取消深层回复解析")
seg_message.append(Seg(type="text", data="[回复消息]"))
continue
ret_seg = await self.handle_reply_message(
sub_message,
raw_message.get("self_id"),
)
if ret_seg:
seg_message.extend(ret_seg)
else:
logger.warning("reply处理失败")
case RealMessageType.image:
ret_seg = await self.handle_image_message(sub_message)
if ret_seg:
@ -297,7 +305,7 @@ class RecvHandler:
raw_message.get("group_id"),
)
if ret_seg:
seg_message.append(ret_seg)
seg_message.extend(ret_seg)
else:
logger.warning("at处理失败")
case RealMessageType.rps:
@ -325,7 +333,7 @@ class RecvHandler:
logger.warning(f"未知消息类型: {sub_message_type}")
return seg_message
async def handle_text_message(self, raw_message: dict) -> Seg:
async def handle_text_message(self, raw_message: dict) -> list[Seg]:
"""
处理纯文本信息
Parameters:
@ -335,7 +343,31 @@ class RecvHandler:
"""
message_data: dict = raw_message.get("data")
plain_text: str = message_data.get("text")
return Seg(type="text", data=plain_text)
seg_list = [
Seg(type="text", data=plain_text),
]
if global_config.mention_interest.enable_mention_in_text:
# 如果启用了正文中提及
if global_config.names.name in plain_text:
# 文本中包含机器人的全名,添加提示
seg_list.append(
Seg(
type="mention_bot",
data=global_config.mention_interest.name_in_text,
)
)
elif any(alias_name in plain_text for alias_name in global_config.names.alias_names):
# 文本中包含机器人的别名,添加提示
seg_list.append(
Seg(
type="mention_bot",
data=global_config.mention_interest.alias_in_text,
)
)
return seg_list
async def handle_face_message(self, raw_message: dict) -> Seg | None:
"""
@ -379,8 +411,7 @@ class RecvHandler:
logger.warning(f"不支持的图片子类型:{image_sub_type}")
return None
async def handle_at_message(self, raw_message: dict, self_id: int, group_id: int) -> Seg | None:
# sourcery skip: use-named-expression
async def handle_at_message(self, raw_message: dict, self_id: int, group_id: int) -> List[Seg] | None:
"""
处理at消息
Parameters:
@ -390,22 +421,31 @@ class RecvHandler:
Returns:
seg_data: Seg: 处理后的消息段
"""
message_data: dict = raw_message.get("data")
if message_data:
qq_id = message_data.get("qq")
if str(self_id) == str(qq_id):
logger.debug("机器人被at")
self_info: dict = await get_self_info(self.server_connection)
if self_info:
return Seg(type="text", data=f"@<{self_info.get('nickname')}:{self_info.get('user_id')}>")
else:
return None
else:
member_info: dict = await get_member_info(self.server_connection, group_id=group_id, user_id=qq_id)
if member_info:
return Seg(type="text", data=f"@<{member_info.get('nickname')}:{member_info.get('user_id')}>")
else:
return None
message_data = raw_message.get("data")
if not message_data:
logger.warning("at消息数据为空")
return None
qq_id = message_data.get("qq")
if str(self_id) == str(qq_id):
logger.debug("机器人被at")
self_target = True
target_user_info: dict = await get_self_info(self.server_connection)
else:
self_target = False
target_user_info: dict = await get_member_info(self.server_connection, group_id=group_id, user_id=qq_id)
seg_list = []
if target_user_info:
seg_list.append(Seg(type="text", data=f"@{target_user_info.get('nickname')}"))
if self_target:
# 如果是at机器人则添加一条提示信息
seg_list.append(Seg(type="mention_bot", data=global_config.mention_interest.at))
return seg_list
async def get_forward_message(self, raw_message: dict) -> Dict[str, Any] | None:
forward_message_data: Dict = raw_message.get("data")
@ -441,37 +481,57 @@ class RecvHandler:
return None
return response_data.get("messages")
async def handle_reply_message(self, raw_message: dict) -> List[Seg] | None:
# sourcery skip: move-assign-in-block, use-named-expression
async def handle_reply_message(self, raw_message: dict, self_id) -> List[Seg] | None:
"""
处理回复消息
"""
raw_message_data: dict = raw_message.get("data")
message_id: int = None
if raw_message_data:
message_id = raw_message_data.get("id")
else:
if not (raw_message_data := raw_message.get("data")):
return None
message_id = raw_message_data.get("id")
message_detail: dict = await get_message_detail(self.server_connection, message_id)
if not message_detail:
logger.warning("获取被引用的消息详情失败")
return None
reply_message = await self.handle_real_message(message_detail, in_reply=True)
if reply_message is None:
reply_message = "(获取发言内容失败)"
# 解析被引消息
replied_message = await self.handle_real_message(message_detail, in_reply=True)
if replied_message is None:
replied_message = Seg(type="text", data="(获取消息内容失败)") # 如果获取被引用消息失败,则返回默认文本
# 尝试获取被引用消息的发送人
sender_info: dict = message_detail.get("sender")
sender_nickname: str = sender_info.get("nickname")
sender_id: str = sender_info.get("user_id")
seg_message: List[Seg] = []
if not sender_nickname:
logger.warning("无法获取被引用的人的昵称,返回默认值")
seg_message.append(Seg(type="text", data="[回复 未知用户:"))
if (
sender_info
and (sender_nickname := sender_info.get("nickname"))
and (sender_id := sender_info.get("user_id"))
):
# 如果获取到昵称和id
self_target = str(self_id) == str(sender_id) # 判断是否是回复自己的消息
else:
seg_message.append(Seg(type="text", data=f"[回复<{sender_nickname}:{sender_id}>"))
seg_message += reply_message
seg_message.append(Seg(type="text", data="],说:"))
return seg_message
# 如果没有获取到昵称和id
logger.warning("无法获取被引用的人的昵称或id使用默认值")
sender_nickname = None
seg_list = []
if sender_nickname:
seg_list.append(Seg(type="text", data=f"[回复<{sender_nickname}:{sender_id}>"))
else:
seg_list.append(Seg(type="text", data="[回复 未知用户:"))
seg_list.extend(replied_message) # 添加被回复的消息体
seg_list.append(Seg(type="text", data="],说:"))
if self_target:
# 如果是回复自己的消息,则添加提示
seg_list.append(Seg(type="mention_bot", data=global_config.mention_interest.reply))
return seg_list
async def handle_notice(self, raw_message: dict) -> None:
notice_type = raw_message.get("notice_type")
@ -500,7 +560,7 @@ class RecvHandler:
sub_type = raw_message.get("sub_type")
match sub_type:
case NoticeType.Notify.poke:
if global_config.chat.enable_poke:
if global_config.mention_interest.enable_poke:
handled_message: Seg = await self.handle_poke_notify(raw_message)
else:
logger.warning("戳一戳消息被禁用,取消戳一戳处理")
@ -580,21 +640,15 @@ class RecvHandler:
target_id = raw_message.get("target_id")
target_name: str = None
raw_info: list = raw_message.get("raw_info")
# 计算Seg
if self_id == target_id:
target_name = self_info.get("nickname")
else:
if self_id != target_id:
return None
try:
first_txt = raw_info[2].get("txt", "戳了戳")
second_txt = raw_info[4].get("txt", "")
except Exception as e:
logger.warning(f"解析戳一戳消息失败: {str(e)},将使用默认文本")
first_txt = "戳了戳"
second_txt = ""
# 如果是戳一戳自己的消息
self_target = True
target_name = self_info.get("nickname")
"""
# 不启用戳其他人的处理
else:
# 不启用戳其他人的处理
self_target = False
# 由于Napcat不支持获取昵称所以需要单独获取
group_id = raw_message.get("group_id")
fetched_member_info: dict = await get_member_info(
@ -603,11 +657,31 @@ class RecvHandler:
if fetched_member_info:
target_name = fetched_member_info.get("nickname")
"""
seg_data: Seg = Seg(
type="text",
data=f"{first_txt}{target_name}{second_txt}这是QQ的一个功能用于提及某人但没那么明显",
)
return seg_data
try:
first_txt = raw_info[2].get("txt", "戳了戳")
second_txt = raw_info[4].get("txt", "")
except Exception as e:
logger.warning(f"解析戳一戳消息失败: {str(e)},将使用默认文本")
first_txt = "戳了戳"
second_txt = ""
seg_list = [
Seg(
type="text",
data=f"{first_txt}{target_name}{second_txt}这是QQ的一个功能用于提及某人但没那么明显",
)
]
if self_target:
seg_list.append(
Seg(
type="mention_bot",
data=global_config.mention_interest.poke,
)
)
return Seg(type="seglist", data=seg_list)
async def handle_forward_message(self, message_list: list) -> Seg | None:
"""

View File

@ -1,10 +1,7 @@
[inner]
version = "0.1.0" # 版本号
version = "0.2.0" # 版本号
# 请勿修改版本号,除非你知道自己在做什么
[nickname] # 现在没用
nickname = ""
[napcat_server] # Napcat连接的ws服务设置
host = "localhost" # Napcat设定的主机地址
port = 8095 # Napcat设定的端口
@ -15,16 +12,33 @@ host = "localhost" # 麦麦在.env文件中设置的主机地址即HOST字段
port = 8000 # 麦麦在.env文件中设置的端口即PORT字段
[chat] # 黑白名单功能
group_list_type = "whitelist" # 群组名单类型可选为whitelist, blacklist
group_list = [] # 群组名单
# 当group_list_type为whitelist时只有群组名单中的群组可以聊天
# 当group_list_type为blacklist时群组名单中的任何群组无法聊天
private_list_type = "whitelist" # 私聊名单类型可选为whitelist, blacklist
private_list = [] # 私聊名单
# 群组名单类型可选为whitelist, blacklist
group_list_type = "whitelist"
group_list = [] # 群组名单
# 私聊名单类型可选为whitelist, blacklist
# 当private_list_type为whitelist时只有私聊名单中的用户可以聊天
# 当private_list_type为blacklist时私聊名单中的任何用户无法聊天
ban_user_id = [] # 全局禁止名单(全局禁止名单中的用户无法进行任何聊天)
enable_poke = true # 是否启用戳一戳功能
private_list_type = "whitelist"
private_list = [] # 私聊名单
# 全局禁止名单(全局禁止名单中的用户无法进行任何聊天)
ban_user_id = []
[names] # 名称设置,用于正文提及检测
name = "麦麦" # bot的全名大名
alias_names = ["牢麦"] # 别名(昵称)列表,支持多个别名
[mention_interest] # 提及兴趣设置0.0-1.0之间的浮点数,越大越感兴趣)
at = 1.0 # at的兴趣权重
reply = 1.0 # 回复的兴趣权重
enable_poke = true # 是否启用戳一戳功能
poke = 1.0 # 戳一戳的兴趣权重
enable_mention_in_text = true # 是否启用“在正文中提及”
name_in_text = 0.4 # 正文中提及全名的兴趣权重
alias_in_text = 0.2 # 正文中提及别名的兴趣权重
[voice] # 发送语音设置
use_tts = false # 是否使用tts语音请确保你配置了tts并有对应的adapter