Skip to content
20 changes: 20 additions & 0 deletions astrbot/core/config/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -2925,6 +2929,10 @@
"type": "list",
"items": {"type": "string"},
},
"command_prefix": {
"type": "list",
"items": {"type": "string"},
},
"t2i": {
"type": "bool",
},
Expand Down Expand Up @@ -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",
Expand Down
82 changes: 70 additions & 12 deletions astrbot/core/pipeline/waking_check/stage.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 列表中的管理员的消息(在白名单阶段中)
"""
Expand Down Expand Up @@ -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(
Comment thread
lingyun14beta marked this conversation as resolved.
self,
Expand Down Expand Up @@ -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)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The retrieval of command_prefix should be more defensive. If the configuration key exists in the user's config file but is set to null, the current get("command_prefix", wake_prefixes) call will return None instead of the default wake_prefixes. This would cause a TypeError later when the code attempts to iterate over command_prefixes or convert it to a set. Using the or operator ensures that any falsy value (including None or an empty list) correctly falls back to wake_prefixes, which also aligns with the "fallback to wake word" behavior mentioned in the documentation.

Suggested change
command_prefixes = self.ctx.astrbot_config.get("command_prefix", wake_prefixes)
command_prefixes = self.ctx.astrbot_config.get("command_prefix") or wake_prefixes

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

启动时会将None替换为默认值,运行时不会出现None。

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 他人(非机器人/全体)时不唤醒
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
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
Comment thread
lingyun14beta marked this conversation as resolved.
if not is_wake:
# 检查是否有at消息 / at全体成员消息 / 引用了bot的消息
for message in messages:
Expand Down Expand Up @@ -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
)
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
for handler in activated_handlers
)
if not has_command_handler:
event.stop_event()
return

if not is_wake:
event.stop_event()
4 changes: 4 additions & 0 deletions astrbot/core/star/filter/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions astrbot/core/star/filter/command_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
27 changes: 18 additions & 9 deletions dashboard/src/i18n/locales/en-US/features/config-metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
11 changes: 10 additions & 1 deletion dashboard/src/i18n/locales/ru-RU/features/config-metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "Отключить встроенные команды",
Expand Down
11 changes: 10 additions & 1 deletion dashboard/src/i18n/locales/zh-CN/features/config-metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "禁用自带指令",
Expand Down
Loading