diff --git a/astrbot/core/star/filter/command.py b/astrbot/core/star/filter/command.py index 31949b674c..93c7c6a202 100755 --- a/astrbot/core/star/filter/command.py +++ b/astrbot/core/star/filter/command.py @@ -198,10 +198,18 @@ 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() + 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..a1db14b8ae --- /dev/null +++ b/tests/test_command_filter.py @@ -0,0 +1,48 @@ +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"} + + +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"}