From 55f27ede2c60f9587dc8a72840881783351212eb Mon Sep 17 00:00:00 2001 From: Yufeng He <40085740+he-yufeng@users.noreply.github.com> Date: Wed, 15 Jul 2026 23:16:17 +0800 Subject: [PATCH 1/2] fix(command): keep an argument that equals another alias of the same command CommandFilter.filter loops over get_complete_command_names() (the command name plus every alias) and strips the matched prefix from message_str, but it does not break after the first match. When a user's first argument happens to equal another alias of the same command, the loop matches again on the already- stripped string and silently drops that argument. For a `search` command aliased `find`, `search find keyword` parsed to `keyword` instead of `find keyword`; `add` aliased `new` turned `add new` into an empty argument. Break after the first match. Normal arguments are unaffected. --- astrbot/core/star/filter/command.py | 1 + tests/test_command_filter.py | 37 +++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 tests/test_command_filter.py diff --git a/astrbot/core/star/filter/command.py b/astrbot/core/star/filter/command.py index 31949b674c..8e18393beb 100755 --- a/astrbot/core/star/filter/command.py +++ b/astrbot/core/star/filter/command.py @@ -202,6 +202,7 @@ def filter(self, event: AstrMessageEvent, cfg: AstrBotConfig) -> bool: if message_str.startswith(f"{full_cmd} ") or message_str == full_cmd: ok = True message_str = message_str[len(full_cmd) :].strip() + break if not ok: return False diff --git a/tests/test_command_filter.py b/tests/test_command_filter.py new file mode 100644 index 0000000000..fa706b3bae --- /dev/null +++ b/tests/test_command_filter.py @@ -0,0 +1,37 @@ +from types import SimpleNamespace + +from astrbot.core.star.filter.command import CommandFilter, GreedyStr + + +def _run_filter(command_name: str, alias: set, message: str): + cmd_filter = CommandFilter(command_name=command_name, alias=set(alias)) + # A single greedy parameter captures everything after the command name. + cmd_filter.handler_params = {"query": GreedyStr} + extras: dict = {} + event = SimpleNamespace( + is_at_or_wake_command=True, + get_message_str=lambda: message, + set_extra=lambda key, value: extras.__setitem__(key, value), + ) + ok = cmd_filter.filter(event, None) + return ok, extras.get("parsed_params") + + +def test_command_filter_keeps_argument_matching_an_alias(): + # Invoking a command by one name with a first argument that happens to equal + # another alias of the same command must not strip that argument a second time. + ok, params = _run_filter("search", {"find"}, "search find keyword") + assert ok + assert params == {"query": "find keyword"} + + +def test_command_filter_keeps_sole_argument_matching_an_alias(): + ok, params = _run_filter("add", {"new"}, "add new") + assert ok + assert params == {"query": "new"} + + +def test_command_filter_normal_argument_unaffected(): + ok, params = _run_filter("search", {"find"}, "search cat photo") + assert ok + assert params == {"query": "cat photo"} From ab98891238cf7b743848fb89dbfb128e2efce932 Mon Sep 17 00:00:00 2001 From: Yufeng He <40085740+he-yufeng@users.noreply.github.com> Date: Fri, 17 Jul 2026 06:20:19 +0800 Subject: [PATCH 2/2] fix(command): match the longest overlapping command name first Follow-up to the break added for the alias-argument fix: with the break, matching stops at the first hit, so the order of get_complete_command_names() (built from a set) decided which of several prefix-sharing names won -- e.g. an alias 'show' vs 'show all'. Sort the candidates longest-first in the match loop so the most specific name always wins deterministically, without touching get_complete_command_names() itself (dashboard callers rely on its [0] being the primary command name). --- astrbot/core/star/filter/command.py | 9 ++++++++- tests/test_command_filter.py | 11 +++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/astrbot/core/star/filter/command.py b/astrbot/core/star/filter/command.py index 8e18393beb..93c7c6a202 100755 --- a/astrbot/core/star/filter/command.py +++ b/astrbot/core/star/filter/command.py @@ -198,7 +198,14 @@ def filter(self, event: AstrMessageEvent, cfg: AstrBotConfig) -> bool: # 检查是否以指令开头 message_str = re.sub(r"\s+", " ", event.get_message_str().strip()) ok = False - for full_cmd in self.get_complete_command_names(): + # Try the longest name first so that when several complete command names + # share a prefix (e.g. an alias "show" and "show all"), the most specific + # one wins and the match is stable. get_complete_command_names() is built + # from a set, so its own order is not deterministic; sort here rather than + # there because other callers rely on [0] being the primary command name. + for full_cmd in sorted( + self.get_complete_command_names(), key=lambda name: (-len(name), name) + ): if message_str.startswith(f"{full_cmd} ") or message_str == full_cmd: ok = True message_str = message_str[len(full_cmd) :].strip() diff --git a/tests/test_command_filter.py b/tests/test_command_filter.py index fa706b3bae..a1db14b8ae 100644 --- a/tests/test_command_filter.py +++ b/tests/test_command_filter.py @@ -35,3 +35,14 @@ def test_command_filter_normal_argument_unaffected(): ok, params = _run_filter("search", {"find"}, "search cat photo") assert ok assert params == {"query": "cat photo"} + + +def test_command_filter_prefers_longest_overlapping_command_name(): + # When a command name and one of its aliases share a prefix ("show" vs + # "show all"), the most specific one must win regardless of the set's + # iteration order, so only the longer name is stripped and the rest is the + # argument. Without the longest-first ordering, "show" would match first and + # leak "all" into the argument. + ok, params = _run_filter("show", {"show all"}, "show all photos") + assert ok + assert params == {"query": "photos"}