diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index 7f7140a9ca..083009b726 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -92,6 +92,7 @@ "empty_mention_waiting": True, "empty_mention_waiting_need_reply": True, "friend_message_needs_wake_prefix": False, + "ignore_unknown_prefix_command": False, "ignore_bot_self_message": False, "ignore_at_all": False, }, @@ -273,6 +274,9 @@ }, }, "wake_prefix": ["/"], + # command_prefix 与 wake_prefix 同层(顶层配置)。 + # 对应的行为开关 ignore_unknown_prefix_command 位于 platform_settings 下。 + "command_prefix": ["/"], "log_level": "INFO", "log_file_enable": False, "log_file_path": "logs/astrbot.log", @@ -2925,6 +2929,10 @@ "type": "list", "items": {"type": "string"}, }, + "command_prefix": { + "type": "list", + "items": {"type": "string"}, + }, "t2i": { "type": "bool", }, @@ -3804,11 +3812,23 @@ "description": "唤醒词", "type": "list", "items": {"type": "string"}, + "hint": "触发 LLM 对话的前缀。", + }, + "command_prefix": { + "description": "指令前缀", + "type": "list", + "items": {"type": "string"}, + "hint": "触发插件指令的前缀(如 /)。设为空时由唤醒词兜底。", }, "platform_settings.friend_message_needs_wake_prefix": { "description": "私聊消息需要唤醒词", "type": "bool", }, + "platform_settings.ignore_unknown_prefix_command": { + "description": "忽略无法识别的指令", + "type": "bool", + "hint": "启用后,以指令前缀开头但不匹配任何已注册指令的消息将被忽略,不触发 LLM。", + }, "platform_settings.reply_prefix": { "description": "回复时的文本前缀", "type": "string", diff --git a/astrbot/core/pipeline/waking_check/stage.py b/astrbot/core/pipeline/waking_check/stage.py index ddc2a6cb83..7a60baae20 100644 --- a/astrbot/core/pipeline/waking_check/stage.py +++ b/astrbot/core/pipeline/waking_check/stage.py @@ -5,6 +5,7 @@ from astrbot.core.message.message_event_result import MessageChain, MessageEventResult from astrbot.core.platform.astr_message_event import AstrMessageEvent from astrbot.core.platform.message_type import MessageType +from astrbot.core.star.filter.command import CommandFilter from astrbot.core.star.filter.command_group import CommandGroupFilter from astrbot.core.star.filter.permission import PermissionTypeFilter from astrbot.core.star.session_plugin_manager import SessionPluginManager @@ -38,7 +39,8 @@ class WakingCheckStage(Stage): 1. 机器人被 @ 了 2. 机器人的消息被提到了 - 3. 以 wake_prefix 前缀开头,并且消息没有以 At 消息段开头 + 3. 以 command_prefix 指令前缀开头(只触发指令),或以 wake_prefix 唤醒词开头(触发 LLM), + 且消息没有以 At 消息段开头 4. 插件(Star)的 handler filter 通过 5. 私聊情况下,位于 admins_id 列表中的管理员的消息(在白名单阶段中) """ @@ -73,6 +75,8 @@ async def initialize(self, ctx: PipelineContext) -> None: ) platform_settings = self.ctx.astrbot_config.get("platform_settings", {}) self.unique_session = platform_settings.get("unique_session", False) + # 以下配置在 process() 中每次读取以支持热更新,此处仅作初始化说明 + # wake_prefix, command_prefix, ignore_unknown_prefix_command 通过 self.ctx.astrbot_config 热读取 async def process( self, @@ -100,24 +104,58 @@ async def process( break # 检查 wake + # command_prefix 用于匹配指令前缀,与唤醒词(wake_prefix)分开配置。 + # 启动时 check_config_integrity 保证 command_prefix 已有默认值,不会为 None。 wake_prefixes = self.ctx.astrbot_config["wake_prefix"] + command_prefixes = self.ctx.astrbot_config.get("command_prefix", wake_prefixes) messages = event.get_messages() is_wake = False - for wake_prefix in wake_prefixes: - if event.message_str.startswith(wake_prefix): - if ( - not event.is_private_chat() - and isinstance(messages[0], At) - and str(messages[0].qq) != str(event.get_self_id()) - and str(messages[0].qq) != "all" - ): - # 如果是群聊,且第一个消息段是 At 消息,但不是 At 机器人或 At 全体成员,则不唤醒 + + # 提取公共的 At 检查逻辑:群聊中首个消息段是 At 他人(非机器人/全体)时不唤醒 + is_at_others = ( + messages + and not event.is_private_chat() + and isinstance(messages[0], At) + and str(messages[0].qq) != str(event.get_self_id()) + and str(messages[0].qq) != "all" + ) + # 预计算前缀差异标记:command_prefix 非空且与 wake_prefix 不同时,唤醒词只触发 LLM。 + # command_prefix=[] 时不标记,避免唤醒词触发的指令全部失效。 + is_different_prefixes = bool( + command_prefixes and set(command_prefixes) != set(wake_prefixes) + ) + + # 先检查是否以指令前缀开头(只匹配指令,不触发 LLM 闲聊) + # command_prefix 与 wake_prefix 相同时,行为与原版一致。 + # command_prefix 与 wake_prefix 不同时,指令前缀只触发指令,唤醒词只触发 LLM。 + is_command_prefix_triggered = False + for cmd_prefix in command_prefixes: + if cmd_prefix and event.message_str.startswith(cmd_prefix): + if is_at_others: break + is_command_prefix_triggered = True + event.message_str = event.message_str[len(cmd_prefix) :].strip() is_wake = True - event.is_at_or_wake_command = True event.is_wake = True - event.message_str = event.message_str[len(wake_prefix) :].strip() + event.is_at_or_wake_command = True break + + # 再检查是否以唤醒词开头(触发 LLM 对话) + # 若 command_prefix 与 wake_prefix 不同(分开配置),唤醒词分支不触发指令匹配, + # 只触发 LLM,CommandFilter 会通过 matched_wake_prefix_only 标记跳过指令检查。 + if not is_wake: + for wake_prefix in wake_prefixes: + if wake_prefix and event.message_str.startswith(wake_prefix): + if is_at_others: + # 如果是群聊,且第一个消息段是 At 消息,但不是 At 机器人或 At 全体成员,则不唤醒 + break + is_wake = True + event.is_wake = True + event.is_at_or_wake_command = True + event.message_str = event.message_str[len(wake_prefix) :].strip() + if is_different_prefixes: + event.set_extra("matched_wake_prefix_only", True) + break if not is_wake: # 检查是否有at消息 / at全体成员消息 / 引用了bot的消息 for message in messages: @@ -234,5 +272,25 @@ async def process( event.set_extra("activated_handlers", activated_handlers) event.set_extra("handlers_parsed_params", handlers_parsed_params) + # 若消息以指令前缀开头,但没有任何「带 CommandFilter 的指令 handler」命中, + # 且 ignore_unknown_prefix_command=True,则静默忽略,不触发 LLM。 + # 默认 False(保持原版行为);设为 True 后可避免误响应其他机器人的指令(如 /grok)。 + # 注意:部分 handler(如 on_message)没有 CommandFilter,不算指令 handler。 + ignore_unknown = self.ctx.astrbot_config.get("platform_settings", {}).get( + "ignore_unknown_prefix_command", False + ) + if is_command_prefix_triggered and ignore_unknown: + # 检查是否有真正的指令 handler 被激活(含 CommandFilter 或 CommandGroupFilter) + has_command_handler = any( + any( + isinstance(f, (CommandFilter, CommandGroupFilter)) + for f in handler.event_filters + ) + for handler in activated_handlers + ) + if not has_command_handler: + event.stop_event() + return + if not is_wake: event.stop_event() diff --git a/astrbot/core/star/filter/command.py b/astrbot/core/star/filter/command.py index 31949b674c..e622932f71 100755 --- a/astrbot/core/star/filter/command.py +++ b/astrbot/core/star/filter/command.py @@ -191,6 +191,10 @@ def equals(self, message_str: str) -> bool: def filter(self, event: AstrMessageEvent, cfg: AstrBotConfig) -> bool: if not event.is_at_or_wake_command: return False + # 若消息仅通过唤醒词触发(非指令前缀),且唤醒词与指令前缀已分开配置, + # 则不匹配指令,只允许 LLM 处理。 + if event.get_extra("matched_wake_prefix_only", default=False): + return False if not self.custom_filter_ok(event, cfg): return False diff --git a/astrbot/core/star/filter/command_group.py b/astrbot/core/star/filter/command_group.py index 52fb6a4521..b359470714 100755 --- a/astrbot/core/star/filter/command_group.py +++ b/astrbot/core/star/filter/command_group.py @@ -117,6 +117,10 @@ def equals(self, message_str: str) -> bool: def filter(self, event: AstrMessageEvent, cfg: AstrBotConfig) -> bool: if not event.is_at_or_wake_command: return False + # 若消息仅通过唤醒词触发(非指令前缀),且唤醒词与指令前缀已分开配置, + # 则不匹配指令,只允许 LLM 处理。 + if event.get_extra("matched_wake_prefix_only", default=False): + return False # 判断当前指令组的自定义过滤器 if not self.custom_filter_ok(event, cfg): diff --git a/dashboard/src/i18n/locales/en-US/features/config-metadata.json b/dashboard/src/i18n/locales/en-US/features/config-metadata.json index 6363b71e31..d68cc5107d 100644 --- a/dashboard/src/i18n/locales/en-US/features/config-metadata.json +++ b/dashboard/src/i18n/locales/en-US/features/config-metadata.json @@ -778,10 +778,19 @@ }, "empty_mention_waiting": { "description": "Trigger Waiting on Mention-only Messages" + }, + "ignore_unknown_prefix_command": { + "description": "Ignore Unrecognized Commands", + "hint": "When enabled, messages starting with the command prefix that do not match any registered command will be silently ignored and will not trigger the LLM." } }, "wake_prefix": { - "description": "Wake Word" + "description": "Wake Word", + "hint": "Prefix to wake the bot for LLM conversation." + }, + "command_prefix": { + "description": "Command Prefix", + "hint": "Prefix for triggering plugin commands (e.g. /). Falls back to the wake word when set to empty." }, "disable_builtin_commands": { "description": "Disable Built-in Commands", @@ -1153,22 +1162,22 @@ "hint": "Only effective for qwen3-rerank models. Recommended to write in English." }, "nvidia_rerank_api_base": { - "description": "API Base URL" + "description": "API Base URL" }, "nvidia_rerank_api_key": { - "description": "API Key" + "description": "API Key" }, "nvidia_rerank_model": { - "description": "Rerank Model Name", - "hint": "Please refer to the NVIDIA Docs for the model name." + "description": "Rerank Model Name", + "hint": "Please refer to the NVIDIA Docs for the model name." }, "nvidia_rerank_model_endpoint": { - "description": "Custom Model Endpoint", - "hint": "Custom URL suffix endpoint, defaults to /reranking." + "description": "Custom Model Endpoint", + "hint": "Custom URL suffix endpoint, defaults to /reranking." }, "nvidia_rerank_truncate": { - "description": "Text Truncation Strategy", - "hint": "Whether to truncate the input to fit the model's maximum context length when the input text is too long." + "description": "Text Truncation Strategy", + "hint": "Whether to truncate the input to fit the model's maximum context length when the input text is too long." }, "launch_model_if_not_running": { "description": "Auto-start model if not running", diff --git a/dashboard/src/i18n/locales/ru-RU/features/config-metadata.json b/dashboard/src/i18n/locales/ru-RU/features/config-metadata.json index 028bff8675..de751d2f42 100644 --- a/dashboard/src/i18n/locales/ru-RU/features/config-metadata.json +++ b/dashboard/src/i18n/locales/ru-RU/features/config-metadata.json @@ -779,10 +779,19 @@ }, "empty_mention_waiting": { "description": "Реагировать на пустое упоминание (@бота)" + }, + "ignore_unknown_prefix_command": { + "description": "Игнорировать нераспознанные команды", + "hint": "При включении сообщения, начинающиеся с префикса команды, но не совпадающие ни с одной зарегистрированной командой, будут молча игнорироваться и не будут запускать LLM." } }, "wake_prefix": { - "description": "Префикс пробуждения" + "description": "Префикс пробуждения", + "hint": "Префикс для пробуждения бота." + }, + "command_prefix": { + "description": "Префикс команды", + "hint": "Префикс для вызова команд плагинов (например /). При пустом значении используется слово пробуждения." }, "disable_builtin_commands": { "description": "Отключить встроенные команды", diff --git a/dashboard/src/i18n/locales/zh-CN/features/config-metadata.json b/dashboard/src/i18n/locales/zh-CN/features/config-metadata.json index 70f4fa5c79..4342fd21b8 100644 --- a/dashboard/src/i18n/locales/zh-CN/features/config-metadata.json +++ b/dashboard/src/i18n/locales/zh-CN/features/config-metadata.json @@ -780,10 +780,19 @@ }, "empty_mention_waiting": { "description": "只 @ 机器人是否触发等待" + }, + "ignore_unknown_prefix_command": { + "description": "忽略无法识别的指令", + "hint": "启用后,以指令前缀开头但不匹配任何已注册指令的消息将被忽略,不触发 LLM。" } }, "wake_prefix": { - "description": "唤醒词" + "description": "唤醒词", + "hint": "触发 LLM 对话的前缀。" + }, + "command_prefix": { + "description": "指令前缀", + "hint": "触发插件指令的前缀(如 /)。设为空时由唤醒词兜底。" }, "disable_builtin_commands": { "description": "禁用自带指令",