From 267b42001ee869a01aad7856ac790e9b0e95883b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=B4=E7=8C=AB?= Date: Sun, 15 Mar 2026 07:22:08 +0900 Subject: [PATCH 1/6] fix: make bot identity platform-aware --- pytests/utils_test/message_utils_test.py | 2 +- pytests/utils_test/test_bot_identity_utils.py | 187 ++++++++++++++++++ src/bw_learner/expression_learner.py | 6 +- src/chat/brain_chat/PFC/message_sender.py | 8 +- src/chat/planner_actions/planner.py | 8 +- src/chat/replyer/group_generator.py | 4 +- src/chat/replyer/private_generator.py | 4 +- src/chat/utils/statistic.py | 9 +- src/chat/utils/utils.py | 124 ++++++------ src/common/message_repository.py | 17 +- src/common/utils/system_utils.py | 12 +- src/common/utils/utils_message.py | 8 +- src/person_info/person_info.py | 46 +---- src/services/send_service.py | 6 +- src/webui/routers/chat/support.py | 11 +- 15 files changed, 311 insertions(+), 141 deletions(-) create mode 100644 pytests/utils_test/test_bot_identity_utils.py diff --git a/pytests/utils_test/message_utils_test.py b/pytests/utils_test/message_utils_test.py index 19ab8ac3..41d5aa0a 100644 --- a/pytests/utils_test/message_utils_test.py +++ b/pytests/utils_test/message_utils_test.py @@ -200,7 +200,7 @@ def dummy_number_to_short_id(original_id: int, salt: str, length: int = 6) -> st return "X" * length # 返回固定的字符串,长度由参数决定,模拟生成短ID的行为 -def dummy_is_bot_self(user_id: str, platform) -> bool: +def dummy_is_bot_self(platform, user_id: str) -> bool: return user_id == "bot_self" diff --git a/pytests/utils_test/test_bot_identity_utils.py b/pytests/utils_test/test_bot_identity_utils.py new file mode 100644 index 00000000..55502a89 --- /dev/null +++ b/pytests/utils_test/test_bot_identity_utils.py @@ -0,0 +1,187 @@ +from pathlib import Path +from types import ModuleType, SimpleNamespace + +import importlib.util +import sys + + +class DummyLogger: + def __init__(self) -> None: + self.warning_messages: list[str] = [] + + def debug(self, _msg: str) -> None: + return + + def info(self, _msg: str) -> None: + return + + def warning(self, msg: str) -> None: + self.warning_messages.append(msg) + + def error(self, _msg: str) -> None: + return + + +def load_utils_module(monkeypatch, qq_account=123456, platforms=None): + logger = DummyLogger() + configured_platforms = platforms or [] + + def _stub_module(name: str) -> ModuleType: + module = ModuleType(name) + monkeypatch.setitem(sys.modules, name, module) + return module + + for package_name in [ + "src", + "src.chat", + "src.chat.message_receive", + "src.chat.utils", + "src.common", + "src.config", + "src.llm_models", + "src.person_info", + ]: + if package_name not in sys.modules: + package_module = ModuleType(package_name) + package_module.__path__ = [] + monkeypatch.setitem(sys.modules, package_name, package_module) + + jieba_module = ModuleType("jieba") + jieba_module.cut = lambda text: list(text) + monkeypatch.setitem(sys.modules, "jieba", jieba_module) + + logger_module = _stub_module("src.common.logger") + logger_module.get_logger = lambda _name: logger + + config_module = _stub_module("src.config.config") + config_module.global_config = SimpleNamespace( + bot=SimpleNamespace( + qq_account=qq_account, + platforms=configured_platforms, + nickname="MaiBot", + alias_names=[], + ), + chat=SimpleNamespace( + at_bot_inevitable_reply=1, + mentioned_bot_reply=1, + ), + ) + config_module.model_config = SimpleNamespace() + + message_module = _stub_module("src.chat.message_receive.message") + + class SessionMessage: + pass + + message_module.SessionMessage = SessionMessage + + chat_manager_module = _stub_module("src.chat.message_receive.chat_manager") + chat_manager_module.chat_manager = SimpleNamespace(get_session_by_session_id=lambda _chat_id: None) + + llm_module = _stub_module("src.llm_models.utils_model") + + class LLMRequest: + def __init__(self, *args, **kwargs) -> None: + del args, kwargs + + llm_module.LLMRequest = LLMRequest + + person_module = _stub_module("src.person_info.person_info") + + class Person: + pass + + person_module.Person = Person + + typo_generator_module = _stub_module("src.chat.utils.typo_generator") + + class ChineseTypoGenerator: + def __init__(self, *args, **kwargs) -> None: + del args, kwargs + + def create_typo_sentence(self, sentence: str): + return sentence, "" + + typo_generator_module.ChineseTypoGenerator = ChineseTypoGenerator + + file_path = Path(__file__).parent.parent.parent / "src" / "chat" / "utils" / "utils.py" + spec = importlib.util.spec_from_file_location("src.chat.utils.utils", file_path) + utils_module = importlib.util.module_from_spec(spec) + utils_module.__package__ = "src.chat.utils" + monkeypatch.setitem(sys.modules, "src.chat.utils.utils", utils_module) + assert spec.loader is not None + spec.loader.exec_module(utils_module) + return utils_module, logger + + +def test_platform_specific_bot_accounts(monkeypatch): + utils_module, _logger = load_utils_module( + monkeypatch, + qq_account=123456, + platforms=[" TG : tg_bot ", "discord: disc_bot"], + ) + + assert utils_module.get_bot_account("qq") == "123456" + assert utils_module.get_bot_account("webui") == "123456" + assert utils_module.get_bot_account("telegram") == "tg_bot" + assert utils_module.get_bot_account("tg") == "tg_bot" + assert utils_module.get_bot_account("discord") == "disc_bot" + + assert utils_module.is_bot_self("qq", "123456") + assert utils_module.is_bot_self("webui", "123456") + assert utils_module.is_bot_self("telegram", "tg_bot") + assert utils_module.is_bot_self(" TG ", "tg_bot") + + +def test_get_all_bot_accounts_includes_runtime_aliases(monkeypatch): + utils_module, _logger = load_utils_module( + monkeypatch, + qq_account=123456, + platforms=["TG:tg_bot", "discord:disc_bot"], + ) + + assert utils_module.get_all_bot_accounts() == { + "qq": "123456", + "webui": "123456", + "telegram": "tg_bot", + "tg": "tg_bot", + "discord": "disc_bot", + } + + +def test_unknown_platform_no_longer_falls_back_to_qq(monkeypatch): + utils_module, logger = load_utils_module(monkeypatch, qq_account=123456, platforms=[]) + + assert utils_module.is_bot_self("unknown_platform", "123456") is False + assert logger.warning_messages + assert "unknown_platform" in logger.warning_messages[-1] + + +def test_unconfigured_qq_account_disables_qq_and_webui_identity(monkeypatch): + utils_module, _logger = load_utils_module(monkeypatch, qq_account=0, platforms=["telegram:tg_bot"]) + + assert utils_module.get_bot_account("qq") == "" + assert utils_module.get_bot_account("webui") == "" + assert utils_module.is_bot_self("qq", "0") is False + assert utils_module.is_bot_self("webui", "0") is False + + +def test_is_mentioned_bot_in_message_uses_platform_account(monkeypatch): + utils_module, _logger = load_utils_module(monkeypatch, qq_account=123456, platforms=["TG:tg_bot"]) + + message = SimpleNamespace( + processed_plain_text="@tg_bot 你好", + platform="telegram", + is_mentioned=False, + message_segment=None, + message_info=SimpleNamespace( + additional_config={}, + user_info=SimpleNamespace(user_id="user_1"), + ), + ) + + is_mentioned, is_at, reply_probability = utils_module.is_mentioned_bot_in_message(message) + + assert is_mentioned is True + assert is_at is True + assert reply_probability == 1.0 diff --git a/src/bw_learner/expression_learner.py b/src/bw_learner/expression_learner.py index ba94d231..1641c0f0 100644 --- a/src/bw_learner/expression_learner.py +++ b/src/bw_learner/expression_learner.py @@ -155,7 +155,7 @@ class ExpressionLearner: for i, msg in enumerate(self._messages_cache): # 跳过机器人自己的消息 - if is_bot_self(msg.message_info.user_info.user_id, msg.platform): + if is_bot_self(msg.platform, msg.message_info.user_info.user_id): continue # 获取消息文本 @@ -238,7 +238,7 @@ class ExpressionLearner: # 检查是否是机器人自己的消息 target_msg = self._messages_cache[line_index] - if is_bot_self(target_msg.message_info.user_info.user_id, target_msg.platform): + if is_bot_self(target_msg.platform, target_msg.message_info.user_info.user_id): logger.info(f"跳过引用机器人自身消息的黑话:content={content}, source_id={source_id}") continue @@ -298,7 +298,7 @@ class ExpressionLearner: # 当前行的原始消息 current_msg = self._messages_cache[line_index] # 过滤掉从 bot 自己发言中提取到的表达方式 - if is_bot_self(current_msg.message_info.user_info.user_id, current_msg.platform): + if is_bot_self(current_msg.platform, current_msg.message_info.user_info.user_id): continue # 过滤掉无上下文的表达方式 context = (current_msg.processed_plain_text or "").strip() diff --git a/src/chat/brain_chat/PFC/message_sender.py b/src/chat/brain_chat/PFC/message_sender.py index 10387319..483a03c8 100644 --- a/src/chat/brain_chat/PFC/message_sender.py +++ b/src/chat/brain_chat/PFC/message_sender.py @@ -1,14 +1,16 @@ import time from typing import Optional -from src.common.logger import get_logger + from maim_message import Seg +from rich.traceback import install from src.common.data_models.mai_message_data_model import MaiMessage, UserInfo from src.chat.message_receive.chat_manager import BotChatSession from src.chat.message_receive.message import MessageSending from src.chat.message_receive.uni_message_sender import UniversalMessageSender +from src.chat.utils.utils import get_bot_account +from src.common.logger import get_logger from src.config.config import global_config -from rich.traceback import install install(extra_lines=3) @@ -41,7 +43,7 @@ class DirectMessageSender: # 获取麦麦的信息 bot_user_info = UserInfo( - user_id=global_config.bot.qq_account, + user_id=get_bot_account(chat_stream.platform), user_nickname=global_config.bot.nickname, ) diff --git a/src/chat/planner_actions/planner.py b/src/chat/planner_actions/planner.py index a2ba4b9d..5184abcb 100644 --- a/src/chat/planner_actions/planner.py +++ b/src/chat/planner_actions/planner.py @@ -122,7 +122,11 @@ class ActionPlanner: msg_text = re.sub(pic_pattern, replace_pic_id, msg_text) # 替换用户引用格式:回复 和 @ - platform = message.platform or "qq" + platform = message.platform or "" + if not platform: + logger.warning( + f"{self.log_prefix}planner: message {message.message_id} has no platform set, bot-self detection will be skipped" + ) msg_text = replace_user_references(msg_text, platform, replace_bot_name=True) # 替换单独的 <用户名:用户ID> 格式(replace_user_references 已处理回复<和@<格式) @@ -135,7 +139,7 @@ class ActionPlanner: user_id = user_match.group(2) try: # 检查是否是机器人自己 - if user_id == global_config.bot.qq_account: + if is_bot_self(platform, str(user_id)): return f"{global_config.bot.nickname}(你)" person = Person(platform=platform, user_id=user_id) return person.person_name or user_name diff --git a/src/chat/replyer/group_generator.py b/src/chat/replyer/group_generator.py index 22ea9228..760cb9ea 100644 --- a/src/chat/replyer/group_generator.py +++ b/src/chat/replyer/group_generator.py @@ -19,7 +19,7 @@ from src.chat.message_receive.message import SessionMessage from src.chat.message_receive.chat_manager import BotChatSession from src.chat.message_receive.uni_message_sender import UniversalMessageSender from src.chat.utils.timer_calculator import Timer # <--- Import Timer -from src.chat.utils.utils import get_chat_type_and_target_info, is_bot_self +from src.chat.utils.utils import get_bot_account, get_chat_type_and_target_info, is_bot_self from src.prompt.prompt_manager import prompt_manager from src.services.message_service import ( build_readable_messages, @@ -1122,7 +1122,7 @@ class DefaultReplyer: message_id=message_id, time=thinking_start_time, user_info=MaimUserInfo( - user_id=str(global_config.bot.qq_account), + user_id=get_bot_account(self.chat_stream.platform), user_nickname=global_config.bot.nickname, ), additional_config={}, diff --git a/src/chat/replyer/private_generator.py b/src/chat/replyer/private_generator.py index 3769dfd2..74049226 100644 --- a/src/chat/replyer/private_generator.py +++ b/src/chat/replyer/private_generator.py @@ -18,7 +18,7 @@ from src.chat.message_receive.message import SessionMessage from src.chat.message_receive.chat_manager import BotChatSession from src.chat.message_receive.uni_message_sender import UniversalMessageSender from src.chat.utils.timer_calculator import Timer -from src.chat.utils.utils import get_chat_type_and_target_info, is_bot_self +from src.chat.utils.utils import get_bot_account, get_chat_type_and_target_info, is_bot_self from src.prompt.prompt_manager import prompt_manager from src.chat.utils.common_utils import TempMethodsExpression from src.services.message_service import ( @@ -962,7 +962,7 @@ class PrivateReplyer: message_id=message_id, time=thinking_start_time, user_info=MaimUserInfo( - user_id=str(global_config.bot.qq_account), + user_id=get_bot_account(self.chat_stream.platform), user_nickname=global_config.bot.nickname, ), group_info=None, diff --git a/src/chat/utils/statistic.py b/src/chat/utils/statistic.py index ba3b5f7d..ede10a41 100644 --- a/src/chat/utils/statistic.py +++ b/src/chat/utils/statistic.py @@ -2106,12 +2106,7 @@ class StatisticOutputTask(AsyncTask): total_replies = [0] * len(time_points) total_online_hours = [0.0] * len(time_points) - # 获取bot的QQ账号 - bot_qq_account = ( - str(global_config.bot.qq_account) - if hasattr(global_config, "bot") and hasattr(global_config.bot, "qq_account") - else "" - ) + from src.chat.utils.utils import is_bot_self interval_seconds = interval_hours * 3600 @@ -2148,7 +2143,7 @@ class StatisticOutputTask(AsyncTask): if 0 <= interval_index < len(time_points): total_messages[interval_index] += 1 # 检查是否是bot发送的消息(回复) - if bot_qq_account and message.user_id == bot_qq_account: + if is_bot_self(message.platform or "", message.user_id or ""): total_replies[interval_index] += 1 # 查询在线时间记录 diff --git a/src/chat/utils/utils.py b/src/chat/utils/utils.py index 8fe1cbef..947d9fc5 100644 --- a/src/chat/utils/utils.py +++ b/src/chat/utils/utils.py @@ -1,18 +1,18 @@ +import ast +import json +import os import random import re import time -import jieba -import json -import ast -import os from datetime import datetime - from typing import Optional, Tuple, List, TYPE_CHECKING +import jieba + +from src.chat.message_receive.chat_manager import chat_manager as _chat_manager +from src.chat.message_receive.message import SessionMessage from src.common.logger import get_logger from src.config.config import global_config, model_config -from src.chat.message_receive.message import SessionMessage -from src.chat.message_receive.chat_manager import chat_manager as _chat_manager from src.llm_models.utils_model import LLMRequest from src.person_info.person_info import Person from .typo_generator import ChineseTypoGenerator @@ -37,33 +37,64 @@ def parse_platform_accounts(platforms: list[str]) -> dict[str, str]: Returns: 字典,键为平台名,值为账号 """ - result = {} + result: dict[str, str] = {} for platform_entry in platforms: if ":" in platform_entry: platform_name, account = platform_entry.split(":", 1) - result[platform_name.strip()] = account.strip() + normalized_platform = platform_name.lower().strip() + account_str = account.strip() + if normalized_platform and account_str: + result[normalized_platform] = account_str return result -def get_current_platform_account(platform: str, platform_accounts: dict[str, str], qq_account: str) -> str: - """根据当前平台获取对应的账号 +def _get_configured_qq_account() -> str: + qq_account = str(getattr(global_config.bot, "qq_account", "")).strip() + if qq_account in {"", "0"}: + return "" + return qq_account - Args: - platform: 当前消息的平台 - platform_accounts: 从 platforms 列表解析的平台账号映射 - qq_account: QQ 账号(兼容旧配置) - Returns: - 当前平台对应的账号 - """ - if platform == "qq": +def get_bot_account(platform: str) -> str: + """根据当前平台获取对应的机器人账号。""" + normalized_platform = str(platform or "").strip().lower() + if not normalized_platform: + return "" + + qq_account = _get_configured_qq_account() + if normalized_platform in {"qq", "webui"}: return qq_account - elif platform == "telegram": - # 优先使用 tg,其次使用 telegram + + platforms_list = getattr(global_config.bot, "platforms", []) or [] + platform_accounts = parse_platform_accounts(platforms_list) + if normalized_platform in {"tg", "telegram"}: return platform_accounts.get("tg", "") or platform_accounts.get("telegram", "") - else: - # 其他平台直接使用平台名作为键 - return platform_accounts.get(platform, "") + + return platform_accounts.get(normalized_platform, "") + + +def get_all_bot_accounts() -> dict[str, str]: + """获取所有已配置的机器人运行时身份。""" + bot_accounts: dict[str, str] = {} + qq_account = _get_configured_qq_account() + if qq_account: + bot_accounts["qq"] = qq_account + bot_accounts["webui"] = qq_account + + platforms_list = getattr(global_config.bot, "platforms", []) or [] + platform_accounts = parse_platform_accounts(platforms_list) + + telegram_account = platform_accounts.get("tg", "") or platform_accounts.get("telegram", "") + if telegram_account: + bot_accounts["telegram"] = telegram_account + bot_accounts["tg"] = telegram_account + + for platform_name, account in platform_accounts.items(): + if platform_name in {"tg", "telegram"}: + continue + bot_accounts[platform_name] = account + + return bot_accounts def is_bot_self(platform: str, user_id: str) -> bool: @@ -78,39 +109,21 @@ def is_bot_self(platform: str, user_id: str) -> bool: Returns: bool: 如果是机器人自己则返回 True,否则返回 False """ - if not platform or not user_id: + normalized_platform = str(platform or "").strip().lower() + if not normalized_platform or not user_id: return False # 将 user_id 转为字符串进行比较 - user_id_str = str(user_id) + user_id_str = str(user_id).strip() + if not user_id_str: + return False - # 获取机器人的 QQ 账号(主账号) - qq_account = str(global_config.bot.qq_account or "") + bot_account = get_bot_account(normalized_platform) + if bot_account: + return user_id_str == bot_account - # QQ 平台:直接比较 QQ 账号 - if platform == "qq": - return user_id_str == qq_account - - # WebUI 平台:机器人回复时使用的是 QQ 账号,所以也比较 QQ 账号 - if platform == "webui": - return user_id_str == qq_account - - # 获取各平台账号映射 - platforms_list = getattr(global_config.bot, "platforms", []) or [] - platform_accounts = parse_platform_accounts(platforms_list) - - # Telegram 平台 - if platform == "telegram": - tg_account = platform_accounts.get("tg", "") or platform_accounts.get("telegram", "") - return user_id_str == tg_account if tg_account else False - - # 其他平台:尝试从 platforms 配置中查找 - platform_account = platform_accounts.get(platform, "") - if platform_account: - return user_id_str == platform_account - - # 默认情况:与主 QQ 账号比较(兼容性) - return user_id_str == qq_account + logger.warning(f"平台 {normalized_platform} 未配置机器人账号,无法判断用户 {user_id_str} 是否为机器人自己") + return False def is_mentioned_bot_in_message(message: SessionMessage) -> tuple[bool, bool, float]: @@ -118,13 +131,8 @@ def is_mentioned_bot_in_message(message: SessionMessage) -> tuple[bool, bool, fl text = message.processed_plain_text or "" platform = message.platform or "" - # 获取各平台账号 - platforms_list = getattr(global_config.bot, "platforms", []) or [] - platform_accounts = parse_platform_accounts(platforms_list) - qq_account = str(getattr(global_config.bot, "qq_account", "") or "") - # 获取当前平台对应的账号 - current_account = get_current_platform_account(platform, platform_accounts, qq_account) + current_account = get_bot_account(platform) nickname = str(global_config.bot.nickname or "") alias_names = list(getattr(global_config.bot, "alias_names", []) or []) diff --git a/src/common/message_repository.py b/src/common/message_repository.py index a89b6f49..79353a9a 100644 --- a/src/common/message_repository.py +++ b/src/common/message_repository.py @@ -1,17 +1,16 @@ -import traceback from datetime import datetime from typing import Any import json +import traceback -from sqlalchemy import func +from sqlalchemy import and_, func, not_, or_ from sqlmodel import col, select from src.common.database.database import get_db_session from src.common.database.database_model import Messages from src.chat.message_receive.message import SessionMessage from src.common.logger import get_logger -from src.config.config import global_config logger = get_logger(__name__) @@ -163,7 +162,17 @@ def find_messages( after_time=after_time, ) if filter_bot: - conditions.append(Messages.user_id != global_config.bot.qq_account) + from src.chat.utils.utils import get_all_bot_accounts + + bot_accounts = get_all_bot_accounts() + if bot_accounts: + bot_identity_predicate = or_( + *[ + and_(Messages.platform == platform_name, Messages.user_id == account) + for platform_name, account in bot_accounts.items() + ] + ) + conditions.append(not_(bot_identity_predicate)) if filter_command: conditions.append(Messages.is_command == False) # noqa: E712 diff --git a/src/common/utils/system_utils.py b/src/common/utils/system_utils.py index bfec3933..6518db42 100644 --- a/src/common/utils/system_utils.py +++ b/src/common/utils/system_utils.py @@ -1,8 +1,10 @@ -# TODO: 这个函数的实现非常临时,后续需要替换为更完善的实现,比如直接从配置文件中读取机器人自己的 ID,或者通过 API 获取机器人自己的信息等 -def is_bot_self(user_id: str, platform: str) -> bool: +# TODO: 这个兼容包装层后续可以删除,统一直接使用 src.chat.utils.utils.is_bot_self +def is_bot_self(platform: str, user_id: str) -> bool: """ - 判断用户 ID 是否是机器人自己 + 判断用户 ID 是否是机器人自己。 - 临时方法,后续会替换为更完善的实现 + 当前仅保留兼容入口,真实实现委托给统一的多平台判断函数。 """ - return user_id == "bot_self" and platform == "test_platform" + from src.chat.utils.utils import is_bot_self as _is_bot_self + + return _is_bot_self(platform, user_id) diff --git a/src/common/utils/utils_message.py b/src/common/utils/utils_message.py index cd46c37e..6b3b5f4e 100644 --- a/src/common/utils/utils_message.py +++ b/src/common/utils/utils_message.py @@ -367,7 +367,7 @@ class MessageUtils: anonymous_name = anonymize_mapping[msg_usr_info.user_id][0] new_message.message_info.user_info.user_nickname = anonymous_name new_message.message_info.user_info.user_cardname = anonymous_name - if replace_bot_name and target_bot_name and is_bot_self(msg_usr_info.user_id, platform): + if replace_bot_name and target_bot_name and is_bot_self(platform, msg_usr_info.user_id): new_message.message_info.user_info.user_nickname = target_bot_name new_message.message_info.user_info.user_cardname = target_bot_name return new_message @@ -437,7 +437,7 @@ class MessageUtils: anonymous_name = anonymize_mapping[user_id][0] component.target_user_nickname = anonymous_name component.target_user_cardname = anonymous_name - if replace_bot_name and target_bot_name and is_bot_self(user_id, platform): + if replace_bot_name and target_bot_name and is_bot_self(platform, user_id): component.target_user_nickname = target_bot_name component.target_user_cardname = target_bot_name return component @@ -473,7 +473,7 @@ class MessageUtils: anonymous_name = anonymize_mapping[user_id][0] comp.user_nickname = anonymous_name comp.user_cardname = anonymous_name - if replace_bot_name and target_bot_name and is_bot_self(user_id, platform): + if replace_bot_name and target_bot_name and is_bot_self(platform, user_id): comp.user_nickname = target_bot_name comp.user_cardname = target_bot_name comp.content = [ # 递归处理转发消息中的组件 @@ -512,7 +512,7 @@ class MessageUtils: anonymous_name = anonymize_mapping[user_id][0] component.target_message_sender_nickname = anonymous_name component.target_message_sender_cardname = anonymous_name - if replace_bot_name and target_bot_name and is_bot_self(user_id, platform): + if replace_bot_name and target_bot_name and is_bot_self(platform, user_id): component.target_message_sender_nickname = target_bot_name component.target_message_sender_cardname = target_bot_name else: diff --git a/src/person_info/person_info.py b/src/person_info/person_info.py index 7f80f496..799f56a0 100644 --- a/src/person_info/person_info.py +++ b/src/person_info/person_info.py @@ -256,43 +256,9 @@ class Person: Returns: bool: 如果是机器人自己则返回 True,否则返回 False """ - if not platform or not user_id: - return False + from src.chat.utils.utils import is_bot_self - # 将 user_id 转为字符串进行比较 - user_id_str = str(user_id) - - # 获取机器人的 QQ 账号(主账号) - qq_account = str(global_config.bot.qq_account or "") - - # QQ 平台:直接比较 QQ 账号 - if platform == "qq": - return user_id_str == qq_account - - # WebUI 平台:机器人回复时使用的是 QQ 账号,所以也比较 QQ 账号 - if platform == "webui": - return user_id_str == qq_account - - # 获取各平台账号映射 - platforms_list = getattr(global_config.bot, "platforms", []) or [] - platform_accounts = {} - for platform_entry in platforms_list: - if ":" in platform_entry: - platform_name, account = platform_entry.split(":", 1) - platform_accounts[platform_name.strip()] = account.strip() - - # Telegram 平台 - if platform == "telegram": - tg_account = platform_accounts.get("tg", "") or platform_accounts.get("telegram", "") - return user_id_str == tg_account if tg_account else False - - # 其他平台:尝试从 platforms 配置中查找 - platform_account = platform_accounts.get(platform, "") - if platform_account: - return user_id_str == platform_account - - # 默认情况:与主 QQ 账号比较(兼容性) - return user_id_str == qq_account + return is_bot_self(platform, user_id) def __init__(self, platform: str = "", user_id: str = "", person_id: str = "", person_name: str = ""): # 使用统一的机器人识别函数(支持多平台,包括 WebUI) @@ -731,15 +697,15 @@ class PersonInfoManager: bot_name = global_config.bot.nickname qv_name_prompt = f"你是{bot_name},一个伪装成人类的AI,你不能让别人发现这一点," - qv_name_prompt += f"现在你想给一个用户取一个昵称,用户的qq昵称是{user_nickname}," - qv_name_prompt += f"用户的qq群昵称名是{user_cardname}," + qv_name_prompt += f"现在你想给一个用户取一个昵称,用户的昵称是{user_nickname}," + qv_name_prompt += f"用户的群昵称名是{user_cardname}," if user_avatar: - qv_name_prompt += f"用户的qq头像是{user_avatar}," + qv_name_prompt += f"用户的头像是{user_avatar}," if old_name: qv_name_prompt += f"你之前叫他{old_name},是因为{old_reason}," qv_name_prompt += f"\n其他取名的要求是:{request},不要太浮夸,简短," - qv_name_prompt += "\n请根据以上用户信息,想想你叫他什么比较好,不要太浮夸,请最好使用用户的qq昵称或群昵称原文,可以稍作修改,优先使用原文。优先使用用户的qq昵称或者群昵称原文。" + qv_name_prompt += "\n请根据以上用户信息,想想你叫他什么比较好,不要太浮夸,请最好使用用户的昵称或群昵称原文,可以稍作修改,优先使用原文。优先使用用户的昵称或者群昵称原文。" if existing_names_str: qv_name_prompt += f"\n请注意,以下名称已被你尝试过或已知存在,请避免:{existing_names_str}。\n" diff --git a/src/services/send_service.py b/src/services/send_service.py index c6885658..ee2a8993 100644 --- a/src/services/send_service.py +++ b/src/services/send_service.py @@ -4,15 +4,17 @@ 提供发送各种类型消息的核心功能。 """ +from typing import Dict, List, Optional, TYPE_CHECKING + import time import traceback -from typing import Dict, List, Optional, TYPE_CHECKING from maim_message import BaseMessageInfo, GroupInfo as MaimGroupInfo, MessageBase, Seg, UserInfo as MaimUserInfo from src.chat.message_receive.chat_manager import chat_manager as _chat_manager from src.chat.message_receive.message import SessionMessage from src.chat.message_receive.uni_message_sender import UniversalMessageSender +from src.chat.utils.utils import get_bot_account from src.common.data_models.mai_message_data_model import MaiMessage from src.common.data_models.message_component_data_model import DictComponent, MessageSequence from src.common.logger import get_logger @@ -88,7 +90,7 @@ async def _send_to_target( message_id=message_id, time=current_time, user_info=MaimUserInfo( - user_id=str(global_config.bot.qq_account), + user_id=get_bot_account(target_stream.platform), user_nickname=global_config.bot.nickname, platform=target_stream.platform, ), diff --git a/src/webui/routers/chat/support.py b/src/webui/routers/chat/support.py index 507abcf5..d9afa474 100644 --- a/src/webui/routers/chat/support.py +++ b/src/webui/routers/chat/support.py @@ -11,11 +11,11 @@ from sqlmodel import col, delete, select from src.chat.message_receive.bot import chat_bot from src.chat.message_receive.message import SessionMessage +from src.chat.utils.utils import is_bot_self from src.common.database.database import get_db_session from src.common.database.database_model import Messages, PersonInfo from src.common.logger import get_logger from src.common.message_repository import find_messages -from src.common.utils.system_utils import is_bot_self from src.common.utils.utils_session import SessionUtils from src.config.config import global_config from src.webui.core import get_token_manager @@ -62,12 +62,7 @@ class ChatHistoryManager: def _message_to_dict(self, msg: SessionMessage, group_id: Optional[str] = None) -> dict[str, Any]: user_info = msg.message_info.user_info user_id = user_info.user_id or "" - is_bot = is_bot_self(user_id, msg.platform) - - if not is_bot and group_id and group_id.startswith(VIRTUAL_GROUP_ID_PREFIX): - is_bot = user_id == str(global_config.bot.qq_account) - elif not is_bot: - is_bot = not user_id.startswith(WEBUI_USER_ID_PREFIX) + is_bot = is_bot_self(msg.platform, user_id) return { "id": msg.message_id, @@ -611,4 +606,4 @@ async def dispatch_chat_event( ) return current_user_name, next_virtual_config - return current_user_name, current_virtual_config \ No newline at end of file + return current_user_name, current_virtual_config From c8dc9ddb60214100f3574c6ba958453e20272dad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=B4=E7=8C=AB?= Date: Sun, 15 Mar 2026 07:29:48 +0900 Subject: [PATCH 2/6] test: add pytest-asyncio dependency group --- pyproject.toml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index e2f97d54..b1f9060d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,6 +42,15 @@ dependencies = [ "maibot-plugin-sdk>=1.2.3", ] +[dependency-groups] +test = [ + "pytest>=8.4.1", + "pytest-asyncio>=1.1.0", +] +dev = [ + { include-group = "test" }, +] + [tool.uv] index-url = "https://pypi.tuna.tsinghua.edu.cn/simple" From 4f8ab0abb1922ced664150c78eacc3c2a976cdb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=B4=E7=8C=AB?= Date: Sun, 15 Mar 2026 07:51:31 +0900 Subject: [PATCH 3/6] fix: address bot identity review regressions --- pytests/utils_test/test_bot_identity_utils.py | 29 +++++++++++++++++++ src/chat/brain_chat/PFC/message_sender.py | 6 +++- src/chat/replyer/group_generator.py | 6 +++- src/chat/replyer/private_generator.py | 6 +++- src/chat/utils/utils.py | 13 ++++++--- src/common/message_repository.py | 25 +++++++++++----- src/common/utils/system_utils.py | 3 +- src/services/send_service.py | 6 +++- 8 files changed, 77 insertions(+), 17 deletions(-) diff --git a/pytests/utils_test/test_bot_identity_utils.py b/pytests/utils_test/test_bot_identity_utils.py index 55502a89..2ba8f35a 100644 --- a/pytests/utils_test/test_bot_identity_utils.py +++ b/pytests/utils_test/test_bot_identity_utils.py @@ -157,6 +157,14 @@ def test_unknown_platform_no_longer_falls_back_to_qq(monkeypatch): assert "unknown_platform" in logger.warning_messages[-1] +def test_unknown_platform_warns_only_once(monkeypatch): + utils_module, logger = load_utils_module(monkeypatch, qq_account=123456, platforms=[]) + + assert utils_module.is_bot_self("unknown_platform", "first") is False + assert utils_module.is_bot_self(" unknown_platform ", "second") is False + assert len(logger.warning_messages) == 1 + + def test_unconfigured_qq_account_disables_qq_and_webui_identity(monkeypatch): utils_module, _logger = load_utils_module(monkeypatch, qq_account=0, platforms=["telegram:tg_bot"]) @@ -185,3 +193,24 @@ def test_is_mentioned_bot_in_message_uses_platform_account(monkeypatch): assert is_mentioned is True assert is_at is True assert reply_probability == 1.0 + + +def test_is_mentioned_bot_in_message_normalizes_qq_platform(monkeypatch): + utils_module, _logger = load_utils_module(monkeypatch, qq_account=123456, platforms=[]) + + message = SimpleNamespace( + processed_plain_text="@ 你好", + platform=" QQ ", + is_mentioned=False, + message_segment=None, + message_info=SimpleNamespace( + additional_config={}, + user_info=SimpleNamespace(user_id="user_1"), + ), + ) + + is_mentioned, is_at, reply_probability = utils_module.is_mentioned_bot_in_message(message) + + assert is_mentioned is True + assert is_at is True + assert reply_probability == 1.0 diff --git a/src/chat/brain_chat/PFC/message_sender.py b/src/chat/brain_chat/PFC/message_sender.py index 483a03c8..a927c413 100644 --- a/src/chat/brain_chat/PFC/message_sender.py +++ b/src/chat/brain_chat/PFC/message_sender.py @@ -42,8 +42,12 @@ class DirectMessageSender: segments = Seg(type="seglist", data=[Seg(type="text", data=content)]) # 获取麦麦的信息 + bot_user_id = get_bot_account(chat_stream.platform) + if not bot_user_id: + logger.warning(f"[私聊][{self.private_name}]平台 {chat_stream.platform} 未配置机器人账号,发送消息时回退到 QQ 账号") + bot_user_id = str(getattr(global_config.bot, "qq_account", "")).strip() bot_user_info = UserInfo( - user_id=get_bot_account(chat_stream.platform), + user_id=bot_user_id, user_nickname=global_config.bot.nickname, ) diff --git a/src/chat/replyer/group_generator.py b/src/chat/replyer/group_generator.py index 760cb9ea..3c41ae2f 100644 --- a/src/chat/replyer/group_generator.py +++ b/src/chat/replyer/group_generator.py @@ -1115,6 +1115,10 @@ class DefaultReplyer: anchor_message: Optional[MaiMessage] = None, ) -> SessionMessage: """构建单个发送消息""" + bot_user_id = get_bot_account(self.chat_stream.platform) + if not bot_user_id: + logger.warning(f"平台 {self.chat_stream.platform} 未配置机器人账号,发送消息时回退到 QQ 账号") + bot_user_id = str(getattr(global_config.bot, "qq_account", "")).strip() maim_message = MessageBase( message_info=BaseMessageInfo( @@ -1122,7 +1126,7 @@ class DefaultReplyer: message_id=message_id, time=thinking_start_time, user_info=MaimUserInfo( - user_id=get_bot_account(self.chat_stream.platform), + user_id=bot_user_id, user_nickname=global_config.bot.nickname, ), additional_config={}, diff --git a/src/chat/replyer/private_generator.py b/src/chat/replyer/private_generator.py index 74049226..008124d4 100644 --- a/src/chat/replyer/private_generator.py +++ b/src/chat/replyer/private_generator.py @@ -955,6 +955,10 @@ class PrivateReplyer: anchor_message: Optional[MaiMessage] = None, ) -> SessionMessage: """构建单个发送消息""" + bot_user_id = get_bot_account(self.chat_stream.platform) + if not bot_user_id: + logger.warning(f"平台 {self.chat_stream.platform} 未配置机器人账号,发送消息时回退到 QQ 账号") + bot_user_id = str(getattr(global_config.bot, "qq_account", "")).strip() maim_message = MessageBase( message_info=BaseMessageInfo( @@ -962,7 +966,7 @@ class PrivateReplyer: message_id=message_id, time=thinking_start_time, user_info=MaimUserInfo( - user_id=get_bot_account(self.chat_stream.platform), + user_id=bot_user_id, user_nickname=global_config.bot.nickname, ), group_info=None, diff --git a/src/chat/utils/utils.py b/src/chat/utils/utils.py index 947d9fc5..44eae4c0 100644 --- a/src/chat/utils/utils.py +++ b/src/chat/utils/utils.py @@ -1,11 +1,12 @@ +from datetime import datetime +from typing import TYPE_CHECKING, List, Optional, Tuple + import ast import json import os import random import re import time -from datetime import datetime -from typing import Optional, Tuple, List, TYPE_CHECKING import jieba @@ -15,12 +16,14 @@ from src.common.logger import get_logger from src.config.config import global_config, model_config from src.llm_models.utils_model import LLMRequest from src.person_info.person_info import Person + from .typo_generator import ChineseTypoGenerator if TYPE_CHECKING: from src.common.data_models.info_data_model import TargetPersonInfo logger = get_logger("chat_utils") +_warned_unconfigured_platforms: set[str] = set() def is_english_letter(char: str) -> bool: @@ -122,14 +125,16 @@ def is_bot_self(platform: str, user_id: str) -> bool: if bot_account: return user_id_str == bot_account - logger.warning(f"平台 {normalized_platform} 未配置机器人账号,无法判断用户 {user_id_str} 是否为机器人自己") + if normalized_platform not in _warned_unconfigured_platforms: + _warned_unconfigured_platforms.add(normalized_platform) + logger.warning(f"平台 {normalized_platform} 未配置机器人账号,无法判断用户 {user_id_str} 是否为机器人自己") return False def is_mentioned_bot_in_message(message: SessionMessage) -> tuple[bool, bool, float]: """检查消息是否提到了机器人(统一多平台实现)""" text = message.processed_plain_text or "" - platform = message.platform or "" + platform = str(message.platform or "").strip().lower() # 获取当前平台对应的账号 current_account = get_bot_account(platform) diff --git a/src/common/message_repository.py b/src/common/message_repository.py index 79353a9a..81ae6360 100644 --- a/src/common/message_repository.py +++ b/src/common/message_repository.py @@ -7,9 +7,9 @@ import traceback from sqlalchemy import and_, func, not_, or_ from sqlmodel import col, select +from src.chat.message_receive.message import SessionMessage from src.common.database.database import get_db_session from src.common.database.database_model import Messages -from src.chat.message_receive.message import SessionMessage from src.common.logger import get_logger logger = get_logger(__name__) @@ -162,17 +162,26 @@ def find_messages( after_time=after_time, ) if filter_bot: - from src.chat.utils.utils import get_all_bot_accounts + from src.chat.utils.utils import _get_configured_qq_account, get_all_bot_accounts bot_accounts = get_all_bot_accounts() + exclusion_conditions: list[Any] = [] if bot_accounts: - bot_identity_predicate = or_( - *[ - and_(Messages.platform == platform_name, Messages.user_id == account) - for platform_name, account in bot_accounts.items() - ] + exclusion_conditions.append( + or_( + *[ + and_(Messages.platform == platform_name, Messages.user_id == account) + for platform_name, account in bot_accounts.items() + ] + ) ) - conditions.append(not_(bot_identity_predicate)) + + # 兼容旧数据:历史机器人消息在所有平台上都使用 QQ 账号进行存储。 + if qq_fallback := _get_configured_qq_account(): + exclusion_conditions.append(Messages.user_id == qq_fallback) + + if exclusion_conditions: + conditions.append(not_(or_(*exclusion_conditions))) if filter_command: conditions.append(Messages.is_command == False) # noqa: E712 diff --git a/src/common/utils/system_utils.py b/src/common/utils/system_utils.py index 6518db42..a5085f19 100644 --- a/src/common/utils/system_utils.py +++ b/src/common/utils/system_utils.py @@ -1,4 +1,5 @@ -# TODO: 这个兼容包装层后续可以删除,统一直接使用 src.chat.utils.utils.is_bot_self +# TODO: 这个包装层后续可以删除,统一直接使用 src.chat.utils.utils.is_bot_self +# 注意:参数顺序已从旧版 (user_id, platform) 变更为 (platform, user_id),与统一接口一致 def is_bot_self(platform: str, user_id: str) -> bool: """ 判断用户 ID 是否是机器人自己。 diff --git a/src/services/send_service.py b/src/services/send_service.py index ee2a8993..6749adfc 100644 --- a/src/services/send_service.py +++ b/src/services/send_service.py @@ -83,6 +83,10 @@ async def _send_to_target( additional_config: dict[str, object] = {} if selected_expressions is not None: additional_config["selected_expressions"] = selected_expressions + bot_user_id = get_bot_account(target_stream.platform) + if not bot_user_id: + logger.warning(f"[SendService] 平台 {target_stream.platform} 未配置机器人账号,发送消息时回退到 QQ 账号") + bot_user_id = str(getattr(global_config.bot, "qq_account", "")).strip() maim_message = MessageBase( message_info=BaseMessageInfo( @@ -90,7 +94,7 @@ async def _send_to_target( message_id=message_id, time=current_time, user_info=MaimUserInfo( - user_id=get_bot_account(target_stream.platform), + user_id=bot_user_id, user_nickname=global_config.bot.nickname, platform=target_stream.platform, ), From d3420bd1b34fcb7adb39833601d3f14447013717 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=B4=E7=8C=AB?= Date: Sun, 15 Mar 2026 08:05:36 +0900 Subject: [PATCH 4/6] fix: address second-round review feedback on bot identity PR --- pytests/utils_test/test_bot_identity_utils.py | 11 +++++++++++ src/bw_learner/expression_learner.py | 2 +- src/chat/brain_chat/PFC/message_sender.py | 6 ++++-- src/chat/replyer/group_generator.py | 6 ++++-- src/chat/replyer/private_generator.py | 6 ++++-- src/chat/utils/utils.py | 2 +- src/common/message_repository.py | 4 ++-- src/services/send_service.py | 6 ++++-- 8 files changed, 31 insertions(+), 12 deletions(-) diff --git a/pytests/utils_test/test_bot_identity_utils.py b/pytests/utils_test/test_bot_identity_utils.py index 2ba8f35a..c345174b 100644 --- a/pytests/utils_test/test_bot_identity_utils.py +++ b/pytests/utils_test/test_bot_identity_utils.py @@ -149,6 +149,17 @@ def test_get_all_bot_accounts_includes_runtime_aliases(monkeypatch): } +def test_get_all_bot_accounts_keeps_canonical_qq_identity(monkeypatch): + utils_module, _logger = load_utils_module( + monkeypatch, + qq_account=123456, + platforms=["qq:999999", "webui:888888", "TG:tg_bot"], + ) + + assert utils_module.get_all_bot_accounts()["qq"] == "123456" + assert utils_module.get_all_bot_accounts()["webui"] == "123456" + + def test_unknown_platform_no_longer_falls_back_to_qq(monkeypatch): utils_module, logger = load_utils_module(monkeypatch, qq_account=123456, platforms=[]) diff --git a/src/bw_learner/expression_learner.py b/src/bw_learner/expression_learner.py index 1641c0f0..43e4ee7d 100644 --- a/src/bw_learner/expression_learner.py +++ b/src/bw_learner/expression_learner.py @@ -14,8 +14,8 @@ from src.common.logger import get_logger from src.common.database.database_model import Expression from src.common.database.database import get_db_session from src.common.data_models.expression_data_model import MaiExpression +from src.chat.utils.utils import is_bot_self from src.common.utils.utils_message import MessageUtils -from src.common.utils.system_utils import is_bot_self from .expression_utils import check_expression_suitability, parse_expression_response diff --git a/src/chat/brain_chat/PFC/message_sender.py b/src/chat/brain_chat/PFC/message_sender.py index a927c413..7cb57d96 100644 --- a/src/chat/brain_chat/PFC/message_sender.py +++ b/src/chat/brain_chat/PFC/message_sender.py @@ -44,8 +44,10 @@ class DirectMessageSender: # 获取麦麦的信息 bot_user_id = get_bot_account(chat_stream.platform) if not bot_user_id: - logger.warning(f"[私聊][{self.private_name}]平台 {chat_stream.platform} 未配置机器人账号,发送消息时回退到 QQ 账号") - bot_user_id = str(getattr(global_config.bot, "qq_account", "")).strip() + bot_user_id = get_bot_account("qq") + if not bot_user_id: + logger.error(f"[私聊][{self.private_name}]平台 {chat_stream.platform} 无可用机器人账号,无法发送消息") + raise RuntimeError("机器人账号未配置") bot_user_info = UserInfo( user_id=bot_user_id, user_nickname=global_config.bot.nickname, diff --git a/src/chat/replyer/group_generator.py b/src/chat/replyer/group_generator.py index 3c41ae2f..a2187be7 100644 --- a/src/chat/replyer/group_generator.py +++ b/src/chat/replyer/group_generator.py @@ -1117,8 +1117,10 @@ class DefaultReplyer: """构建单个发送消息""" bot_user_id = get_bot_account(self.chat_stream.platform) if not bot_user_id: - logger.warning(f"平台 {self.chat_stream.platform} 未配置机器人账号,发送消息时回退到 QQ 账号") - bot_user_id = str(getattr(global_config.bot, "qq_account", "")).strip() + bot_user_id = get_bot_account("qq") + if not bot_user_id: + logger.error(f"平台 {self.chat_stream.platform} 无可用机器人账号,无法构建发送消息") + raise RuntimeError("机器人账号未配置") maim_message = MessageBase( message_info=BaseMessageInfo( diff --git a/src/chat/replyer/private_generator.py b/src/chat/replyer/private_generator.py index 008124d4..e3d5165e 100644 --- a/src/chat/replyer/private_generator.py +++ b/src/chat/replyer/private_generator.py @@ -957,8 +957,10 @@ class PrivateReplyer: """构建单个发送消息""" bot_user_id = get_bot_account(self.chat_stream.platform) if not bot_user_id: - logger.warning(f"平台 {self.chat_stream.platform} 未配置机器人账号,发送消息时回退到 QQ 账号") - bot_user_id = str(getattr(global_config.bot, "qq_account", "")).strip() + bot_user_id = get_bot_account("qq") + if not bot_user_id: + logger.error(f"平台 {self.chat_stream.platform} 无可用机器人账号,无法构建发送消息") + raise RuntimeError("机器人账号未配置") maim_message = MessageBase( message_info=BaseMessageInfo( diff --git a/src/chat/utils/utils.py b/src/chat/utils/utils.py index 44eae4c0..07cec0b4 100644 --- a/src/chat/utils/utils.py +++ b/src/chat/utils/utils.py @@ -93,7 +93,7 @@ def get_all_bot_accounts() -> dict[str, str]: bot_accounts["tg"] = telegram_account for platform_name, account in platform_accounts.items(): - if platform_name in {"tg", "telegram"}: + if platform_name in {"tg", "telegram", "qq", "webui"}: continue bot_accounts[platform_name] = account diff --git a/src/common/message_repository.py b/src/common/message_repository.py index 81ae6360..eac5e837 100644 --- a/src/common/message_repository.py +++ b/src/common/message_repository.py @@ -162,7 +162,7 @@ def find_messages( after_time=after_time, ) if filter_bot: - from src.chat.utils.utils import _get_configured_qq_account, get_all_bot_accounts + from src.chat.utils.utils import get_all_bot_accounts, get_bot_account bot_accounts = get_all_bot_accounts() exclusion_conditions: list[Any] = [] @@ -177,7 +177,7 @@ def find_messages( ) # 兼容旧数据:历史机器人消息在所有平台上都使用 QQ 账号进行存储。 - if qq_fallback := _get_configured_qq_account(): + if qq_fallback := get_bot_account("qq"): exclusion_conditions.append(Messages.user_id == qq_fallback) if exclusion_conditions: diff --git a/src/services/send_service.py b/src/services/send_service.py index 6749adfc..e90009f0 100644 --- a/src/services/send_service.py +++ b/src/services/send_service.py @@ -85,8 +85,10 @@ async def _send_to_target( additional_config["selected_expressions"] = selected_expressions bot_user_id = get_bot_account(target_stream.platform) if not bot_user_id: - logger.warning(f"[SendService] 平台 {target_stream.platform} 未配置机器人账号,发送消息时回退到 QQ 账号") - bot_user_id = str(getattr(global_config.bot, "qq_account", "")).strip() + bot_user_id = get_bot_account("qq") + if not bot_user_id: + logger.error(f"[SendService] 平台 {target_stream.platform} 无可用机器人账号,无法发送消息") + return False maim_message = MessageBase( message_info=BaseMessageInfo( From be047aa2c35465311768244e736522d657f797d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=B4=E7=8C=AB?= Date: Sun, 15 Mar 2026 08:25:56 +0900 Subject: [PATCH 5/6] fix: align sender paths with plan, remove QQ-as-universal fallback - Remove get_bot_account("qq") fallback from all 4 sender paths (plan L108/L208/L449: unknown platform = no account, never substitute QQ) - Sender paths now error immediately if platform bot account is not configured - Add detailed comments on filter_bot legacy fallback explaining why global user_id match is needed (plan contingency L528 insufficient for platform-tagged legacy rows like telegram+qq_account) Co-Authored-By: Claude Opus 4.6 (1M context) --- src/chat/brain_chat/PFC/message_sender.py | 6 ++---- src/chat/replyer/group_generator.py | 6 ++---- src/chat/replyer/private_generator.py | 6 ++---- src/common/message_repository.py | 5 ++++- src/services/send_service.py | 4 +--- 5 files changed, 11 insertions(+), 16 deletions(-) diff --git a/src/chat/brain_chat/PFC/message_sender.py b/src/chat/brain_chat/PFC/message_sender.py index 7cb57d96..ec5fb5ba 100644 --- a/src/chat/brain_chat/PFC/message_sender.py +++ b/src/chat/brain_chat/PFC/message_sender.py @@ -44,10 +44,8 @@ class DirectMessageSender: # 获取麦麦的信息 bot_user_id = get_bot_account(chat_stream.platform) if not bot_user_id: - bot_user_id = get_bot_account("qq") - if not bot_user_id: - logger.error(f"[私聊][{self.private_name}]平台 {chat_stream.platform} 无可用机器人账号,无法发送消息") - raise RuntimeError("机器人账号未配置") + logger.error(f"[私聊][{self.private_name}]平台 {chat_stream.platform} 未配置机器人账号,无法发送消息") + raise RuntimeError(f"平台 {chat_stream.platform} 未配置机器人账号") bot_user_info = UserInfo( user_id=bot_user_id, user_nickname=global_config.bot.nickname, diff --git a/src/chat/replyer/group_generator.py b/src/chat/replyer/group_generator.py index a2187be7..74b324be 100644 --- a/src/chat/replyer/group_generator.py +++ b/src/chat/replyer/group_generator.py @@ -1117,10 +1117,8 @@ class DefaultReplyer: """构建单个发送消息""" bot_user_id = get_bot_account(self.chat_stream.platform) if not bot_user_id: - bot_user_id = get_bot_account("qq") - if not bot_user_id: - logger.error(f"平台 {self.chat_stream.platform} 无可用机器人账号,无法构建发送消息") - raise RuntimeError("机器人账号未配置") + logger.error(f"平台 {self.chat_stream.platform} 未配置机器人账号,无法构建发送消息") + raise RuntimeError(f"平台 {self.chat_stream.platform} 未配置机器人账号") maim_message = MessageBase( message_info=BaseMessageInfo( diff --git a/src/chat/replyer/private_generator.py b/src/chat/replyer/private_generator.py index e3d5165e..3b70bb2c 100644 --- a/src/chat/replyer/private_generator.py +++ b/src/chat/replyer/private_generator.py @@ -957,10 +957,8 @@ class PrivateReplyer: """构建单个发送消息""" bot_user_id = get_bot_account(self.chat_stream.platform) if not bot_user_id: - bot_user_id = get_bot_account("qq") - if not bot_user_id: - logger.error(f"平台 {self.chat_stream.platform} 无可用机器人账号,无法构建发送消息") - raise RuntimeError("机器人账号未配置") + logger.error(f"平台 {self.chat_stream.platform} 未配置机器人账号,无法构建发送消息") + raise RuntimeError(f"平台 {self.chat_stream.platform} 未配置机器人账号") maim_message = MessageBase( message_info=BaseMessageInfo( diff --git a/src/common/message_repository.py b/src/common/message_repository.py index eac5e837..94d7bfea 100644 --- a/src/common/message_repository.py +++ b/src/common/message_repository.py @@ -176,7 +176,10 @@ def find_messages( ) ) - # 兼容旧数据:历史机器人消息在所有平台上都使用 QQ 账号进行存储。 + # 兼容旧数据:历史机器人消息在所有平台上都使用 QQ 账号作为 user_id 存储, + # 例如旧 Telegram bot 消息的 (platform="telegram", user_id=qq_account)。 + # plan 建议的 ("", qq_account) pair 只能覆盖空 platform 行,无法覆盖这种情况。 + # 因此这里使用全局 user_id 匹配作为临时方案,待 DB 迁移后应移除此兜底。 if qq_fallback := get_bot_account("qq"): exclusion_conditions.append(Messages.user_id == qq_fallback) diff --git a/src/services/send_service.py b/src/services/send_service.py index e90009f0..7af55716 100644 --- a/src/services/send_service.py +++ b/src/services/send_service.py @@ -85,9 +85,7 @@ async def _send_to_target( additional_config["selected_expressions"] = selected_expressions bot_user_id = get_bot_account(target_stream.platform) if not bot_user_id: - bot_user_id = get_bot_account("qq") - if not bot_user_id: - logger.error(f"[SendService] 平台 {target_stream.platform} 无可用机器人账号,无法发送消息") + logger.error(f"[SendService] 平台 {target_stream.platform} 未配置机器人账号,无法发送消息") return False maim_message = MessageBase( From 975939535cb18b74e28a90e7cef2f6e9314c5fad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=B4=E7=8C=AB?= Date: Sun, 15 Mar 2026 08:27:32 +0900 Subject: [PATCH 6/6] fix: align dependency-groups with r-dev to avoid TOML duplicate key r-dev already has [dependency-groups] with dev deps including pytest and pytest-asyncio. Our branch had a different structure causing a duplicate key error when merged. Align with r-dev's format. Co-Authored-By: Claude Opus 4.6 (1M context) --- pyproject.toml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b1f9060d..5307f355 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,12 +43,11 @@ dependencies = [ ] [dependency-groups] -test = [ - "pytest>=8.4.1", - "pytest-asyncio>=1.1.0", -] dev = [ - { include-group = "test" }, + "pytest", + "pytest-asyncio", + "ruff>=0.12.2", + "zstandard", ]