From a61cfee93284b4584a128bf07eb3666118a745ec Mon Sep 17 00:00:00 2001 From: Wuchang325 Date: Sat, 13 Jun 2026 20:08:29 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat(registry):=20=E4=B8=BA=E5=91=BD?= =?UTF-8?q?=E4=BB=A4=E8=A3=85=E9=A5=B0=E5=99=A8=E6=B7=BB=E5=8A=A0=E8=87=AA?= =?UTF-8?q?=E5=AE=9A=E4=B9=89=E5=89=8D=E7=BC=80=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ncatbot/core/registry/_command_binding.py | 19 ++++++++++--- ncatbot/core/registry/command_hook.py | 16 +++++++++-- ncatbot/core/registry/registrar.py | 33 ++++++++++++++++++++--- 3 files changed, 59 insertions(+), 9 deletions(-) diff --git a/ncatbot/core/registry/_command_binding.py b/ncatbot/core/registry/_command_binding.py index 65199215..ac0e1dee 100644 --- a/ncatbot/core/registry/_command_binding.py +++ b/ncatbot/core/registry/_command_binding.py @@ -359,12 +359,17 @@ def get_param_spec(func: Any) -> _ParamSpec: } -def format_usage(names: tuple, spec: _ParamSpec) -> str: +def format_usage(names: tuple, spec: _ParamSpec, prefix: str = "") -> str: """生成命令用法说明字符串。 示例: ``用法: /ban [duration: 整数 = 60]`` + + Args: + names: 命令名元组 + spec: 参数规格 + prefix: 命令前缀(可选) """ - cmd = names[0] + cmd = f"{prefix}{names[0]}" parts = [f"用法: {cmd}"] for param in spec.params: anno = param.annotation @@ -388,12 +393,18 @@ def format_usage(names: tuple, spec: _ParamSpec) -> str: # ======================= Usage 回复 ======================= -async def reply_usage(ctx: Any, names: tuple, spec: _ParamSpec) -> None: +async def reply_usage(ctx: Any, names: tuple, spec: _ParamSpec, prefix: str = "") -> None: """尝试通过平台 API 回复命令用法说明。 静默处理: api 不可用或调用失败时仅记录 WARNING。 + + Args: + ctx: HookContext + names: 命令名元组 + spec: 参数规格 + prefix: 命令前缀(可选) """ - usage = format_usage(names, spec) + usage = format_usage(names, spec, prefix) api = getattr(ctx, "api", None) if api is None: return diff --git a/ncatbot/core/registry/command_hook.py b/ncatbot/core/registry/command_hook.py index 26ed8921..409905df 100644 --- a/ncatbot/core/registry/command_hook.py +++ b/ncatbot/core/registry/command_hook.py @@ -37,6 +37,7 @@ class CommandHook(Hook): 匹配规则: - 统一前缀匹配 — 对消息预处理后首段 PlainText 分词, 首 token 匹配命令名即触发(无论 handler 有无额外参数) + - 支持自定义命令前缀(如 "/"、"!" 等) 参数绑定规则: - 消息段预处理: 首个 PlainText 移到最前, 解决 Reply 开头的消息 @@ -55,12 +56,14 @@ def __init__( *names: str, ignore_case: bool = False, priority: int = 95, + prefix: Optional[str] = None, ): if not names: raise ValueError("CommandHook 至少需要一个命令名") self.names = names self.ignore_case = ignore_case self.priority = priority + self.prefix = prefix self._sig_cache: Dict[int, Optional[_ParamSpec]] = {} async def execute(self, ctx: HookContext) -> HookAction: @@ -83,7 +86,16 @@ async def execute(self, ctx: HookContext) -> HookAction: if not text: return HookAction.SKIP - # 3) 统一前缀匹配: tokenize 后首 token 匹配命令名 + # 3) 检查前缀(如果配置了前缀) + if self.prefix is not None: + if not text.startswith(self.prefix): + return HookAction.SKIP + # 去除前缀 + text = text[len(self.prefix):].strip() + if not text: + return HookAction.SKIP + + # 4) 统一前缀匹配: tokenize 后首 token 匹配命令名 matched_name = match_command_prefix(text, self.names, self.ignore_case) if matched_name is None: return HookAction.SKIP @@ -119,7 +131,7 @@ async def execute(self, ctx: HookContext) -> HookAction: matched_name, func.__name__, ) - await reply_usage(ctx, self.names, spec) + await reply_usage(ctx, self.names, spec, self.prefix or "") return HookAction.SKIP ctx.kwargs.update(kwargs) diff --git a/ncatbot/core/registry/registrar.py b/ncatbot/core/registry/registrar.py index 382ddb2c..347161f9 100644 --- a/ncatbot/core/registry/registrar.py +++ b/ncatbot/core/registry/registrar.py @@ -206,17 +206,26 @@ def on_command( priority: int = 0, ignore_case: bool = False, platform: Optional[str] = None, + prefix: Optional[str] = None, **metadata: Any, ) -> Callable: """注册命令 handler (群+私聊) 匹配 message.text,支持注解式参数绑定。 + + Args: + *names: 命令名(支持多个别名) + priority: 优先级,越大越先执行 + ignore_case: 是否忽略大小写 + platform: 平台过滤,None 接收所有平台 + prefix: 命令前缀,如 "/"、"!" 等 + **metadata: 附加元信息 """ def decorator(func: Callable) -> Callable: if not hasattr(func, "__hooks__"): func.__hooks__ = [] - func.__hooks__.append(CommandHook(*names, ignore_case=ignore_case)) + func.__hooks__.append(CommandHook(*names, ignore_case=ignore_case, prefix=prefix)) return self.on("message", priority=priority, platform=platform, **metadata)( func ) @@ -229,18 +238,27 @@ def on_group_command( priority: int = 0, ignore_case: bool = False, platform: Optional[str] = None, + prefix: Optional[str] = None, **metadata: Any, ) -> Callable: """注册群命令 handler 组合 MessageTypeFilter("group") + CommandHook 匹配 + 参数绑定。 + + Args: + *names: 命令名(支持多个别名) + priority: 优先级,越大越先执行 + ignore_case: 是否忽略大小写 + platform: 平台过滤,None 接收所有平台 + prefix: 命令前缀,如 "/"、"!" 等 + **metadata: 附加元信息 """ def decorator(func: Callable) -> Callable: if not hasattr(func, "__hooks__"): func.__hooks__ = [] func.__hooks__.append(MessageTypeFilter("group")) - func.__hooks__.append(CommandHook(*names, ignore_case=ignore_case)) + func.__hooks__.append(CommandHook(*names, ignore_case=ignore_case, prefix=prefix)) return self.on("message", priority=priority, platform=platform, **metadata)( func ) @@ -253,18 +271,27 @@ def on_private_command( priority: int = 0, ignore_case: bool = False, platform: Optional[str] = None, + prefix: Optional[str] = None, **metadata: Any, ) -> Callable: """注册私聊命令 handler 组合 MessageTypeFilter("private") + CommandHook 匹配 + 参数绑定。 + + Args: + *names: 命令名(支持多个别名) + priority: 优先级,越大越先执行 + ignore_case: 是否忽略大小写 + platform: 平台过滤,None 接收所有平台 + prefix: 命令前缀,如 "/"、"!" 等 + **metadata: 附加元信息 """ def decorator(func: Callable) -> Callable: if not hasattr(func, "__hooks__"): func.__hooks__ = [] func.__hooks__.append(MessageTypeFilter("private")) - func.__hooks__.append(CommandHook(*names, ignore_case=ignore_case)) + func.__hooks__.append(CommandHook(*names, ignore_case=ignore_case, prefix=prefix)) return self.on("message", priority=priority, platform=platform, **metadata)( func ) From 055e1455467946de4ffd7d4df72bed66ef9d35a1 Mon Sep 17 00:00:00 2001 From: Wuchang325 Date: Sat, 13 Jun 2026 20:16:09 +0800 Subject: [PATCH 2/2] style: format code with ruff --- ncatbot/core/registry/_command_binding.py | 4 +++- ncatbot/core/registry/command_hook.py | 2 +- ncatbot/core/registry/registrar.py | 12 +++++++++--- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/ncatbot/core/registry/_command_binding.py b/ncatbot/core/registry/_command_binding.py index ac0e1dee..d104fd56 100644 --- a/ncatbot/core/registry/_command_binding.py +++ b/ncatbot/core/registry/_command_binding.py @@ -393,7 +393,9 @@ def format_usage(names: tuple, spec: _ParamSpec, prefix: str = "") -> str: # ======================= Usage 回复 ======================= -async def reply_usage(ctx: Any, names: tuple, spec: _ParamSpec, prefix: str = "") -> None: +async def reply_usage( + ctx: Any, names: tuple, spec: _ParamSpec, prefix: str = "" +) -> None: """尝试通过平台 API 回复命令用法说明。 静默处理: api 不可用或调用失败时仅记录 WARNING。 diff --git a/ncatbot/core/registry/command_hook.py b/ncatbot/core/registry/command_hook.py index 409905df..abb562d6 100644 --- a/ncatbot/core/registry/command_hook.py +++ b/ncatbot/core/registry/command_hook.py @@ -91,7 +91,7 @@ async def execute(self, ctx: HookContext) -> HookAction: if not text.startswith(self.prefix): return HookAction.SKIP # 去除前缀 - text = text[len(self.prefix):].strip() + text = text[len(self.prefix) :].strip() if not text: return HookAction.SKIP diff --git a/ncatbot/core/registry/registrar.py b/ncatbot/core/registry/registrar.py index 347161f9..6bd1e7bc 100644 --- a/ncatbot/core/registry/registrar.py +++ b/ncatbot/core/registry/registrar.py @@ -225,7 +225,9 @@ def on_command( def decorator(func: Callable) -> Callable: if not hasattr(func, "__hooks__"): func.__hooks__ = [] - func.__hooks__.append(CommandHook(*names, ignore_case=ignore_case, prefix=prefix)) + func.__hooks__.append( + CommandHook(*names, ignore_case=ignore_case, prefix=prefix) + ) return self.on("message", priority=priority, platform=platform, **metadata)( func ) @@ -258,7 +260,9 @@ def decorator(func: Callable) -> Callable: if not hasattr(func, "__hooks__"): func.__hooks__ = [] func.__hooks__.append(MessageTypeFilter("group")) - func.__hooks__.append(CommandHook(*names, ignore_case=ignore_case, prefix=prefix)) + func.__hooks__.append( + CommandHook(*names, ignore_case=ignore_case, prefix=prefix) + ) return self.on("message", priority=priority, platform=platform, **metadata)( func ) @@ -291,7 +295,9 @@ def decorator(func: Callable) -> Callable: if not hasattr(func, "__hooks__"): func.__hooks__ = [] func.__hooks__.append(MessageTypeFilter("private")) - func.__hooks__.append(CommandHook(*names, ignore_case=ignore_case, prefix=prefix)) + func.__hooks__.append( + CommandHook(*names, ignore_case=ignore_case, prefix=prefix) + ) return self.on("message", priority=priority, platform=platform, **metadata)( func )