Repository: KroMiose/nekro-agent
Pull request #84: Adapter feature 001
Author: login: zxjwzn
Created: 2025-05-24T21:28:00Z | Merged: 2025-05-25T08:00:02Z

# 🌟 PR 提交说明

## 📋 许可确认

- [x] 我已仔细阅读并同意 [NekroAgent 开源协议](../../LICENSE) 中的所有条款并理解我的贡献将受到该协议的约束，特别是"贡献者条款"部分
- [x] 我确认我提交的代码不会侵犯任何第三方的知识产权

## 💯 代码质量确认

- [x] 我已执行静态类型检查，确保代码不含基本类型错误：
  - 后端：Pylance (basic+) + ruff
  - 前端：TypeScript 严格模式
- [x] 对于难以解决的必要类型忽略标记，我已添加注释说明原因：
  <!-- 若有类型忽略标记，请简述原因 -->

## 🔍 变更类型 (勾选所有适用项)

- [x] 🐛 Bug 修复 (修复一个问题)
- [x] ✨ 新功能 (添加新功能)
- [ ] 🔄 重构 (不改变功能的代码调整)
- [ ] 📝 文档更新
- [ ] 🔧 配置变更
- [ ] 🎨 代码风格优化
- [ ] 💅 样式调整 (UI/UX外观变化)
- [ ] 🚀 性能提升
- [ ] ✅ 测试相关
- [ ] 🌐 国际化/本地化
- [x] 🔗 依赖更新
- [x] 🧩 插件系统相关
- [ ] 🤖 AI 模型/提示词相关
- [ ] 🔄 其他...

## 📄 PR 描述

1.新增了minecraft适配器 支持监听MC服务器消息以及消息发送
2.将notice通知器细分到每个适配器中,由适配器单独实现
3.修复了ob11适配器中bot.py可能获取到非ob11的Bot实例的问题

## 🔗 相关 Issue

<!-- 如果有相关Issue，请在此处引用 (例如: Closes #123, Fixes #456) -->

## 📸 修改效果截图

<!-- 如果你的PR包含UI变更或功能改进，请提供修改前后的对比截图 -->

## 🛠️ 实现复杂度

<!-- 请选择一项 -->
- [ ] 简单 (小改动，影响有限)
- [ ] 中等 (需要一些技术考量)
- [x] 复杂 (涉及多个组件或系统)

## 📋 实现细节 (可选)

<!-- 如果实现方式比较复杂或特殊，可以在这里详细说明技术实现 -->

## 🧪 测试步骤 (可选)

<!-- 说明如何测试这个PR的功能，例如:
1. 进入...页面
2. 点击...按钮
3. 观察...结果
-->

## Sourcery 总结

通过引入统一的适配器接口和通用聊天服务，将核心代理逻辑与平台协议分离。将现有的 OneBot V11 集成迁移到新的 BaseAdapter 系统，并添加具有专用匹配器和沙盒方法的 Minecraft 适配器。重构数据库模型、上下文、API 和插件，以支持通过 adapter_key 和 platform_userid 进行多平台用户和频道识别，并相应地更新前端。

新特性：
- 引入正式的适配器接口和通用聊天服务，以将核心逻辑与协议实现分离
- 添加实现新 BaseAdapter 接口的 OneBot V11 适配器
- 添加具有匹配器、通知处理程序、富文本消息传递和 RCON 命令执行支持的 Minecraft 适配器

Bug 修复：
- 修复 OneBot V11 适配器命令守卫检索非 ob11 Bot 实例的问题

增强功能：
- 重构数据库模型（DBUser、DBChatChannel、DBChatMessage）以包含 adapter_key 和 platform_userid，以实现多平台支持
- 更新 AgentCtx、API 层和内置插件以使用 adapter_key 和 platform_userid 而不是 bind_qq
- 迁移所有消息发送和预处理以使用 universal_chat_service 和 adapter_utils
- 修改前端用户管理器以显示 adapter_key 和 platform_userid 列
- 简化命令和通知匹配器以使用新的基于适配器的 collect_message 流程

构建：
- 将 nonebot-adapter-minecraft 和插件依赖项添加到 pyproject.toml

部署：
- 在启动时和 run_bot 中注册并加载适配器（OnebotV11 和 Minecraft）

文档：
- 添加广泛的跨平台适配器架构文档和示例
- 更新系统提示和模板以反映新的 chat_key 格式和适配器用法
- 包括插件 Alconna 文档

<details>
<summary>Original summary in English</summary>

## Summary by Sourcery

Decouple the core agent logic from platform protocols by introducing a unified adapter interface and a universal chat service. Migrate existing OneBot V11 integration to the new BaseAdapter system, and add a Minecraft adapter with specialized matchers and sandbox methods. Refactor database models, context, API, and plugins to support multi-platform user and channel identification via adapter_key and platform_userid, and update the frontend accordingly.

New Features:
- Introduce a formal adapter interface and universal chat service to decouple core logic from protocol implementations
- Add OneBot V11 adapter implementing the new BaseAdapter interface
- Add Minecraft adapter with matchers, notice handlers, rich text messaging and RCON command execution support

Bug Fixes:
- Fix OneBot V11 adapter command guard retrieving a non-ob11 Bot instance

Enhancements:
- Refactor database models (DBUser, DBChatChannel, DBChatMessage) to include adapter_key and platform_userid for multi-platform support
- Update AgentCtx, API layers and built-in plugins to use adapter_key and platform_userid instead of bind_qq
- Migrate all message sending and preprocessing to use universal_chat_service and adapter_utils
- Revise frontend user manager to display adapter_key and platform_userid columns
- Simplify command and notice matchers to use new adapter-based collect_message flow

Build:
- Add nonebot-adapter-minecraft and plugin dependencies to pyproject.toml

Deployment:
- Register and load adapters (OnebotV11 and Minecraft) on startup and in run_bot

Documentation:
- Add extensive cross-platform adapter architecture documentation and examples
- Update system prompts and templates to reflect new chat_key format and adapter usage
- Include plugin Alconna documentation

</details>

Linked issue #123: [Bug]: 添加Bot好友时出错
### 版本信息

v2.0.0-beta.17

### 部署方式

其他

### 运行环境

其他

### 系统架构

amd64 (x86_64)

### Python 版本

3.11.4

### 发生了什么？

<img width="1920" height="1032" alt="Image" src="https://github.com/user-attachments/assets/229b08d6-99a7-4ff3-9a5c-689e09fd8ff2" />
这里应该在加个判断，添加好友属于通用通知事件，所以才会别归类为群组,但添加好友的那个临时会应该算是私聊

### 相关信息

```shell

```

### 贡献意向

愿意协助该改动影响的相关功能测试

### 补充信息

_No response_

Diff:
diff --git a/.env.prod b/.env.prod
index cb44b60f..89a9d96a 100644
--- a/.env.prod
+++ b/.env.prod
@@ -1,4 +1,7 @@
-DRIVER=~fastapi
+DRIVER=~fastapi+~websockets
 HOST=0.0.0.0
 PORT=8021
 ONEBOT_ACCESS_TOKEN=
+minecraft_ws_urls=
+minecraft_access_token=
+minecraft_server_rcon=
\ No newline at end of file
diff --git a/nekro_agent/adapters/__init__.py b/nekro_agent/adapters/__init__.py
index f79d30d9..b1997002 100644
--- a/nekro_agent/adapters/__init__.py
+++ b/nekro_agent/adapters/__init__.py
@@ -3,10 +3,12 @@
 from fastapi import FastAPI
 
 from .interface.base import BaseAdapter
+from .minecraft.adapter import MinecraftAdapter
 from .onebot_v11.adapter import OnebotV11Adapter
 
 ALL_ADAPTERS: List[Type[BaseAdapter]] = [
     OnebotV11Adapter,
+    MinecraftAdapter,
 ]
 
 
diff --git a/nekro_agent/adapters/minecraft/__init__.py b/nekro_agent/adapters/minecraft/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/nekro_agent/adapters/minecraft/adapter.py b/nekro_agent/adapters/minecraft/adapter.py
new file mode 100644
index 00000000..6622096a
--- /dev/null
+++ b/nekro_agent/adapters/minecraft/adapter.py
@@ -0,0 +1,103 @@
+import re
+from typing import List, Optional
+
+from fastapi import APIRouter
+from nonebot.adapters.minecraft import Bot, Message, MessageSegment
+from nonebot.adapters.minecraft.model import ClickEvent, HoverEvent, TextColor
+
+from nekro_agent.adapters.interface.base import BaseAdapter
+from nekro_agent.adapters.interface.schemas.platform import (
+    PlatformChannel,
+    PlatformSendRequest,
+    PlatformSendResponse,
+    PlatformSendSegmentType,
+    PlatformUser,
+)
+from nekro_agent.core import logger
+from nekro_agent.models.db_chat_channel import DBChatChannel
+from nekro_agent.schemas.chat_message import ChatType
+
+from .core.bot import get_bot
+from .matchers.message import register_matcher
+from .matchers.notice import notice_manager
+
+
+class MinecraftAdapter(BaseAdapter):
+    """Minecraft 适配器"""
+
+    @property
+    def key(self) -> str:
+        return "minecraft"
+
+    @property
+    def chat_key_rules(self) -> List[str]:
+        # TODO: 根据 Minecraft 的实际情况修改
+        return [
+            "Server chat: `minecraft-servername` (where servername is the name of the Minecraft server)",
+            "YOU CAN NOT USE ANY AT IN MINECRAFT SERVER CHAT!",
+        ]
+
+    async def init(self) -> None:
+        """初始化适配器"""
+        logger.info(f"Minecraft adapter [{self.key}] initialized.")
+        register_matcher(self)
+
+    def _remove_at_mentions(self, text: str) -> str:
+        """移除文本中的特定格式的 @ 提及 (例如 [@id:123;nickname:test@] 或 [@id:123@])"""
+        processed_text = re.sub(r"\[@(?:id:[^;@]+(?:;nickname:[^@]+)?|[^@\]]+)@\]", "", text)
+        # 将多个空格替换为单个空格，并去除首尾空格
+        return re.sub(r"\\s+", " ", processed_text).strip()
+
+    async def _send_text(self, text: str, chat_key: str):
+        """将文本消息通过 Bot 发送到 Minecraft 服务器"""
+        try:
+            bot_instance:Optional[Bot] = get_bot(chat_key)
+            if not bot_instance:
+                logger.error(f"没有找到对应的 Minecraft 服务器: {chat_key}")
+                return
+
+            cleaned_text = self._remove_at_mentions(text)
+            channel = await DBChatChannel.get_channel(chat_key)
+            preset = await channel.get_preset()
+
+            message_to_send = Message([
+                MessageSegment.text(f"<{preset.name}>", color=TextColor.GREEN),
+                MessageSegment.text(
+                    cleaned_text,
+                ),
+            ])
+            await bot_instance.send_msg(message=message_to_send)
+
+        except Exception as e:
+            logger.error(f"发送消息时错误 ({chat_key}): {e}", exc_info=True)
+    
+    async def forward_message(self, request: PlatformSendRequest) -> PlatformSendResponse:
+        """推送消息到 Minecraft 协议端"""
+        for seg in request.segments:
+            if seg.type == PlatformSendSegmentType.TEXT:
+                await self._send_text(seg.content, request.chat_key)
+        return PlatformSendResponse(success=True)
+
+    async def get_self_info(self) -> PlatformUser:
+        """获取自身信息"""
+        return PlatformUser(user_id="Minecraft Server Bot", user_name="Minecraft Server Bot")
+
+    async def get_user_info(self, user_id: str) -> PlatformUser:
+        """获取用户信息"""
+        # TODO: 实现获取 Minecraft 用户信息的逻辑
+        raise NotImplementedError
+
+    async def get_channel_info(self, channel_id: str) -> PlatformChannel:
+        """获取频道信息"""
+        return PlatformChannel(channel_id=channel_id, channel_name=channel_id, channel_type=ChatType.GROUP)
+
+    async def set_message_reaction(self, message_id: str, status: bool = True) -> bool:
+        """设置消息反应（可选实现）"""
+        return True
+
+    async def get_adapter_router(self) -> APIRouter:
+        """获取适配器路由"""
+        # TODO: 如果 Minecraft 适配器需要特定的 API 路由，在此实现
+        # 默认为基础路由
+        return await super().get_adapter_router()
+
diff --git a/nekro_agent/adapters/minecraft/core/bot.py b/nekro_agent/adapters/minecraft/core/bot.py
new file mode 100644
index 00000000..cd15278e
--- /dev/null
+++ b/nekro_agent/adapters/minecraft/core/bot.py
@@ -0,0 +1,32 @@
+from typing import Optional
+
+from nonebot import get_bots
+from nonebot.adapters.minecraft import Bot
+
+from nekro_agent.core import logger
+
+
+def get_bot(identifier: Optional[str] = None) -> Optional[Bot]:
+    """
+    获取 Minecraft Bot 实例。
+
+    Args:
+        identifier: Bot 的标识符。
+                    来自 nekro-agent 的 chat_key (例如 'minecraft-servername')，
+                    其中 'servername' 应该对应 NoneBot 中 Bot 的 self_id。
+                    如果为 None 或无法从中解析出 servername，则尝试返回第一个可用的 Minecraft Bot。
+    """
+    bots = get_bots()  # get_bots() 返回一个字典 {bot_id: Bot_instance}
+
+    target_bot_id: Optional[str] = None
+    if identifier and identifier.startswith("minecraft-"):
+        parts = identifier.split("-", 1)
+        if len(parts) > 1 and parts[1]:
+            target_bot_id = parts[1]
+
+    if target_bot_id:
+        bot = bots.get(target_bot_id)
+        if bot and isinstance(bot, Bot):
+            return bot
+
+    return None
diff --git a/nekro_agent/adapters/minecraft/matchers/message.py b/nekro_agent/adapters/minecraft/matchers/message.py
new file mode 100644
index 00000000..0fa002f1
--- /dev/null
+++ b/nekro_agent/adapters/minecraft/matchers/message.py
@@ -0,0 +1,82 @@
+import hashlib
+import time
+from typing import Optional, Union
+
+from arclet.alconna import Alconna, Args
+from nonebot import on_message
+from nonebot.adapters.minecraft import Bot, Event
+from nonebot.adapters.minecraft.event.base import (
+    BaseChatEvent,
+    BaseDeathEvent,
+    BasePlayerCommandEvent,
+    MessageEvent,
+)
+from nonebot.matcher import Matcher
+from nonebot_plugin_alconna import AlconnaMatches, Arparma, on_alconna
+from nonebot_plugin_alconna.uniseg import UniMessage, get_target
+
+from nekro_agent.adapters.interface import (
+    BaseAdapter,
+    PlatformChannel,
+    PlatformMessage,
+    PlatformUser,
+    collect_message,
+)
+from nekro_agent.adapters.interface.schemas.platform import (
+    PlatformChannel as PlatformChannelSchema,
+)
+from nekro_agent.adapters.interface.schemas.platform import (
+    PlatformMessage as PlatformMessageSchema,
+)
+from nekro_agent.adapters.interface.schemas.platform import (
+    PlatformUser as PlatformUserSchema,
+)
+from nekro_agent.core import config, logger
+from nekro_agent.models.db_chat_channel import DBChatChannel
+from nekro_agent.schemas.chat_message import (
+    ChatMessageSegment,
+    ChatMessageSegmentType,
+    ChatType,
+)
+
+
+def register_matcher(adapter:BaseAdapter):
+    @on_message(priority=999, block=False).handle()
+    async def _(_matcher: Matcher, event: Union[BaseChatEvent,BasePlayerCommandEvent,BaseDeathEvent], _bot: Bot):
+        """Minecraft 消息收集与预处理"""
+        
+        plt_channel: PlatformChannel = PlatformChannel(channel_id=event.server_name, channel_name=event.server_name, channel_type=ChatType.GROUP)
+
+        sender_name: Optional[str] = event.player.nickname
+        original_player_uuid = str(event.player.uuid)
+        short_player_id = hashlib.sha1(original_player_uuid.encode("utf-8")).hexdigest()[:10]
+
+        plt_user: PlatformUser = PlatformUser(user_id=short_player_id, user_name=sender_name, user_avatar="")
+
+        msg_list = []
+        plain_text_content = event.message.extract_plain_text()
+        msg_list.append(
+            ChatMessageSegment(
+                type=ChatMessageSegmentType.TEXT,
+                text=plain_text_content,
+            ),
+        )
+        logger.info(f"Minecraft Message:{plain_text_content}")
+        msg_id_from_event = event.message_id
+        if msg_id_from_event and msg_id_from_event.strip():
+            msg_id = msg_id_from_event
+        else:
+            msg_id = f"mc_{event.server_name}_{short_player_id}_{int(event.timestamp)}"
+
+        plt_msg: PlatformMessage = PlatformMessage(
+            message_id=msg_id,
+            sender_id=short_player_id,
+            sender_name=sender_name,
+            sender_nickname=sender_name,
+            content_data=msg_list,
+            content_text=event.message.extract_plain_text(),
+            is_tome=False,
+            timestamp=int(event.timestamp),
+        )
+
+        await collect_message(adapter, plt_channel, plt_user, plt_msg)
\ No newline at end of file
diff --git a/nekro_agent/adapters/minecraft/matchers/notice.py b/nekro_agent/adapters/minecraft/matchers/notice.py
new file mode 100644
index 00000000..6a6d6ebb
--- /dev/null
+++ b/nekro_agent/adapters/minecraft/matchers/notice.py
@@ -0,0 +1,157 @@
+import time
+from datetime import datetime
+from typing import Any, Dict, Optional, Type
+
+from nonebot import on_notice
+from nonebot.adapters.minecraft import Bot, NoticeEvent
+from nonebot.matcher import Matcher
+
+from nekro_agent.core.config import config
+from nekro_agent.core.logger import logger
+from nekro_agent.models.db_chat_channel import DBChatChannel
+from nekro_agent.models.db_user import DBUser
+from nekro_agent.schemas.chat_message import ChatMessage, ChatType
+from nekro_agent.services.message_service import message_service
+from nekro_agent.services.notice_service import (
+    BaseNoticeHandler,
+    NoticeConfig,
+    NoticeResult,
+)
+from nekro_agent.tools.time_util import format_duration
+
+
+class PlayerJoinNoticeHandler(BaseNoticeHandler):
+    """玩家加入通知处理器"""
+
+    def get_notice_config(self) -> NoticeConfig:
+        return NoticeConfig(force_tome=True, use_system_sender=True)
+
+    def match(self, _db_chat_channel: DBChatChannel, event_dict: Dict[str, Any]) -> Optional[Dict[str, str]]:
+        if event_dict["sub_type"] != "join":
+            return None
+        return {
+            "user_id": str(event_dict["player"].nickname),
+        }
+
+    async def format_message(self, _db_chat_channel: DBChatChannel, info: Dict[str, str]) -> str:
+        return f"(玩家 {info['user_id']} 加入了服务器)"
+
+class PlayerQuitNoticeHandler(BaseNoticeHandler):
+    """玩家离开通知处理器"""
+
+    def get_notice_config(self) -> NoticeConfig:
+        return NoticeConfig(force_tome=True, use_system_sender=True)
+
+    def match(self, _db_chat_channel: DBChatChannel, event_dict: Dict[str, Any]) -> Optional[Dict[str, str]]:
+        if event_dict["sub_type"] != "quit":
+            return None
+        return {
+            "user_id": str(event_dict["player"].nickname),
+        }
+
+    async def format_message(self, _db_chat_channel: DBChatChannel, info: Dict[str, str]) -> str:
+        return f"(玩家 {info['user_id']} 离开了服务器)"
+
+class NoticeHandlerManager:
+    """通知处理器管理器"""
+
+    def __init__(self):
+        self._handlers: list[BaseNoticeHandler] = []
+
+    def register(self, handler: BaseNoticeHandler):
+        """注册处理器"""
+        self._handlers.append(handler)
+
+    async def handle(self, event: NoticeEvent, _bot: Bot, db_chat_channel: DBChatChannel) -> Optional[NoticeResult]:
+        """处理通知事件
+
+        Args:
+            event (NoticeEvent): 通知事件
+            _bot (Bot): 机器人实例
+
+        Returns:
+            Optional[NoticeResult]: 处理结果，包含处理器实例和通知信息
+        """
+        # 预处理事件为字典
+        event_dict = dict(event)
+
+        for handler in self._handlers:
+            if info := handler.match(db_chat_channel, event_dict):
+                return NoticeResult(
+                    handler=handler,
+                    info=info,
+                )
+        return None
+
+    def get_sender_platform_userid(self, info: Dict[str, str]) -> str:
+        """获取发送者平台用户ID"""
+        return info["user_id"]
+
+notice_manager = NoticeHandlerManager()
+
+notice_manager.register(PlayerJoinNoticeHandler())
+notice_manager.register(PlayerQuitNoticeHandler())
+
+"""通用通知匹配器"""
+notice_matcher: Type[Matcher] = on_notice(priority=99999, block=False)
+
+
+@notice_matcher.handle()
+async def _(_: Matcher, event: NoticeEvent, bot: Bot):
+    # 处理通知事件
+    chat_key = "minecraft-" + event.server_name
+    db_chat_channel: DBChatChannel = await DBChatChannel.get_channel(chat_key=chat_key)
+    result = await notice_manager.handle(event, bot, db_chat_channel)
+    if not result:
+        event_dict = dict(event)
+        logger.debug(
+            f"收到未处理的通知类型: {event_dict}\n"
+            f"notice_type: {event_dict.get('notice_type')}\n"
+            f"sub_type: {event_dict.get('sub_type')}\n",
+        )
+        return
+
+    handler = result["handler"]
+    info = result["info"]
+
+    # 格式化消息
+    content_text = await handler.format_message(db_chat_channel, info)
+
+    if handler.config.use_system_sender:
+        # 使用系统消息
+        await message_service.push_system_message(
+            chat_key=chat_key,
+            agent_messages=content_text,
+            trigger_agent=handler.config.force_tome,
+        )
+    else:
+        # 使用普通消息
+        platform_userid: str = handler.get_sender_platform_userid(info)
+        user: Optional[DBUser] = await DBUser.get_or_none(
+            adapter_key=db_chat_channel.adapter_key,
+            platform_userid=platform_userid,
+        )
+        sender_nickname = event.player.nickname
+
+        if user and not user.is_active:
+            logger.info(f"用户 {platform_userid} 被封禁，封禁结束时间: {user.ban_until}")
+            return
+
+        chat_message: ChatMessage = ChatMessage(
+            message_id="",
+            sender_id=str(user.id) if user else platform_userid,
+            sender_name=user.username if user else sender_nickname,
+            sender_nickname=sender_nickname,
+            adapter_key=db_chat_channel.adapter_key,
+            platform_userid=platform_userid,
+            is_tome=1 if handler.config.force_tome else 0,
+            is_recalled=False,
+            chat_key=chat_key,
+            chat_type=ChatType.GROUP,
+            content_text=content_text,
+            content_data=[],
+            raw_cq_code="",
+            ext_data={},
+            send_timestamp=int(time.time()),
+        )
+        await message_service.push_human_message(message=chat_message, user=user)
\ No newline at end of file
diff --git a/nekro_agent/adapters/minecraft/tools/__init__.py b/nekro_agent/adapters/minecraft/tools/__init__.py
new file mode 100644
index 00000000..0519ecba
--- /dev/null
+++ b/nekro_agent/adapters/minecraft/tools/__init__.py
@@ -0,0 +1 @@
+ 
\ No newline at end of file
diff --git a/nekro_agent/adapters/onebot_v11/adapter.py b/nekro_agent/adapters/onebot_v11/adapter.py
index ee7fe9f1..744b9f53 100644
--- a/nekro_agent/adapters/onebot_v11/adapter.py
+++ b/nekro_agent/adapters/onebot_v11/adapter.py
@@ -160,7 +160,11 @@ async def _send_to_chat(self, chat_key: str, message: Message) -> None:
 
     async def get_self_info(self) -> PlatformUser:
         """获取自身信息"""
-        return PlatformUser(user_id=str(get_bot().self_id), user_name=get_bot().self_id)
+        bot: Bot = get_bot()
+        if bot:
+            logger.info(f"Self_id:{bot.self_id} user_name:{bot.self_id}")
+            return PlatformUser(user_id=str(bot.self_id), user_name=bot.self_id)
+        raise ValueError("No bot found")
 
     async def get_user_info(self, user_id: str) -> PlatformUser:
         """获取用户信息"""
diff --git a/nekro_agent/adapters/onebot_v11/core/bot.py b/nekro_agent/adapters/onebot_v11/core/bot.py
index e8a0bf10..4e56d4af 100644
--- a/nekro_agent/adapters/onebot_v11/core/bot.py
+++ b/nekro_agent/adapters/onebot_v11/core/bot.py
@@ -1,8 +1,11 @@
-from nonebot import get_bot as _get_bot
-from nonebot.adapters.onebot.v11 import Bot
+from nonebot import get_bots
+from nonebot.adapters.onebot.v11 import Bot as OneBotV11Bot
 
 
-def get_bot() -> Bot:
-    """获取当前 Bot 实例"""
-
-    return _get_bot()  # type: ignore
+def get_bot() -> OneBotV11Bot:
+    """获取当前 OneBot V11 Bot 实例"""
+    all_bots = get_bots()
+    for bot_instance in all_bots.values():
+        if isinstance(bot_instance, OneBotV11Bot):
+            return bot_instance
+    raise RuntimeError("No OneBot V11 bot instance found.")
diff --git a/nekro_agent/adapters/onebot_v11/matchers/notice.py b/nekro_agent/adapters/onebot_v11/matchers/notice.py
index 26d0eea6..3363498e 100644
--- a/nekro_agent/adapters/onebot_v11/matchers/notice.py
+++ b/nekro_agent/adapters/onebot_v11/matchers/notice.py
@@ -20,7 +20,7 @@
 from nekro_agent.services.notice_service import (
     BaseNoticeHandler,
     NoticeConfig,
-    notice_manager,
+    NoticeResult,
 )
 from nekro_agent.tools.time_util import format_duration
 
@@ -149,7 +149,40 @@ async def format_message(self, _db_chat_channel: DBChatChannel, info: Dict[str,
         }
         return f"(成员 (qq:{info['user_id']}) {action_map[info['action']]})"
 
+class NoticeHandlerManager:
+    """通知处理器管理器"""
 
+    def __init__(self):
+        self._handlers: list[BaseNoticeHandler] = []
+
+    def register(self, handler: BaseNoticeHandler):
+        """注册处理器"""
+        self._handlers.append(handler)
+
+    async def handle(self, event: NoticeEvent, _bot: Bot, db_chat_channel: DBChatChannel) -> Optional[NoticeResult]:
+        """处理通知事件
+
+        Args:
+            event (NoticeEvent): 通知事件
+            _bot (Bot): 机器人实例
+
+        Returns:
+            Optional[NoticeResult]: 处理结果，包含处理器实例和通知信息
+        """
+        # 预处理事件为字典
+        event_dict = dict(event)
+
+        for handler in self._handlers:
+            if info := handler.match(db_chat_channel, event_dict):
+                return NoticeResult(
+                    handler=handler,
+                    info=info,
+                )
+        return None
+
+
+# 全局通知处理器管理器
+notice_manager = NoticeHandlerManager()
 # 注册所有通知处理器
 notice_manager.register(GroupAdminNoticeHandler())
 notice_manager.register(PokeNoticeHandler())
diff --git a/nekro_agent/api/plugin.py b/nekro_agent/api/plugin.py
index d0105ed1..d3a4ee02 100644
--- a/nekro_agent/api/plugin.py
+++ b/nekro_agent/api/plugin.py
@@ -4,10 +4,11 @@
 """
 
 from nekro_agent.services.plugin.base import ConfigBase, NekroPlugin
-from nekro_agent.services.plugin.schema import SandboxMethodType
+from nekro_agent.services.plugin.schema import SandboxMethod, SandboxMethodType
 
 __all__ = [
     "ConfigBase",
     "NekroPlugin",
+    "SandboxMethod",
     "SandboxMethodType",
 ]
diff --git a/nekro_agent/services/notice_service.py b/nekro_agent/services/notice_service.py
index fbd710c7..2ab05c51 100644
--- a/nekro_agent/services/notice_service.py
+++ b/nekro_agent/services/notice_service.py
@@ -70,39 +70,3 @@ class NoticeResult(TypedDict):
 
     handler: BaseNoticeHandler
     info: Dict[str, str]
-
-
-class NoticeHandlerManager:
-    """通知处理器管理器"""
-
-    def __init__(self):
-        self._handlers: list[BaseNoticeHandler] = []
-
-    def register(self, handler: BaseNoticeHandler):
-        """注册处理器"""
-        self._handlers.append(handler)
-
-    async def handle(self, event: NoticeEvent, _bot: Bot, db_chat_channel: DBChatChannel) -> Optional[NoticeResult]:
-        """处理通知事件
-
-        Args:
-            event (NoticeEvent): 通知事件
-            _bot (Bot): 机器人实例
-
-        Returns:
-            Optional[NoticeResult]: 处理结果，包含处理器实例和通知信息
-        """
-        # 预处理事件为字典
-        event_dict = dict(event)
-
-        for handler in self._handlers:
-            if info := handler.match(db_chat_channel, event_dict):
-                return NoticeResult(
-                    handler=handler,
-                    info=info,
-                )
-        return None
-
-
-# 全局通知处理器管理器
-notice_manager = NoticeHandlerManager()
diff --git a/plugins/builtin/basic.py b/plugins/builtin/basic.py
index 694736a1..b832b188 100644
--- a/plugins/builtin/basic.py
+++ b/plugins/builtin/basic.py
@@ -10,7 +10,12 @@
 
 from nekro_agent.adapters.onebot_v11.tools import user
 from nekro_agent.api import core, message
-from nekro_agent.api.plugin import ConfigBase, NekroPlugin, SandboxMethodType
+from nekro_agent.api.plugin import (
+    ConfigBase,
+    NekroPlugin,
+    SandboxMethod,
+    SandboxMethodType,
+)
 from nekro_agent.api.schemas import AgentCtx
 from nekro_agent.services.message_service import message_service
 from nekro_agent.tools.common_util import (
@@ -252,6 +257,43 @@ async def get_user_avatar(_ctx: AgentCtx, user_qq: str) -> str:
     except Exception as e:
         raise Exception(f"Error getting user avatar: {e}") from e
 
+@plugin.mount_collect_methods()
+async def collect_available_methods(_ctx: AgentCtx) -> List[SandboxMethod]:
+    """根据适配器收集可用方法"""
+
+    method_send_msg_text = SandboxMethod(
+        method_type=SandboxMethodType.TOOL,
+        name="发送聊天消息文本",
+        description="发送聊天消息文本，附带缓存消息重复检查",
+        func=send_msg_text,
+    )
+
+    method_send_msg_file = SandboxMethod(
+        method_type=SandboxMethodType.TOOL,
+        name="发送聊天消息图片/文件资源",
+        description="发送聊天消息图片/文件资源，附带缓存文件重复检查",
+        func=send_msg_file,
+    )
+
+    method_get_user_avatar = SandboxMethod(
+        method_type=SandboxMethodType.TOOL,
+        name="获取用户头像",
+        description="获取用户头像",
+        func=get_user_avatar,
+    )
+
+    all_methods = [
+        method_send_msg_text,
+        method_send_msg_file,
+        method_get_user_avatar,
+    ]
+
+    if _ctx.adapter_key == "minecraft":
+        # 如果是 Minecraft 适配器，只保留 send_msg_text
+        # 通过列表推导式筛选，只保留 name 为 "发送聊天消息文本" 的方法
+        return [m for m in all_methods if m.name == "发送聊天消息文本"]
+    
+    return all_methods
 
 @plugin.mount_cleanup_method()
 async def clean_up():
diff --git a/plugins/builtin/minecraft_utils.py b/plugins/builtin/minecraft_utils.py
new file mode 100644
index 00000000..aaa1202d
--- /dev/null
+++ b/plugins/builtin/minecraft_utils.py
@@ -0,0 +1,388 @@
+import json
+from typing import Any, Dict, List, Optional, Union
+
+from nonebot.adapters.minecraft import Bot, Message, MessageSegment
+from nonebot.adapters.minecraft.model import (
+    BaseComponent,
+    ClickAction,
+    ClickEvent,
+    HoverAction,
+    HoverEvent,
+    TextColor,
+    TextComponent,
+)
+
+from nekro_agent.adapters.minecraft.core.bot import get_bot
+from nekro_agent.api import core
+from nekro_agent.api.core import logger
+from nekro_agent.api.plugin import NekroPlugin, SandboxMethod, SandboxMethodType
+from nekro_agent.api.schemas import AgentCtx
+from nekro_agent.models.db_chat_channel import DBChatChannel
+from nekro_agent.services.message_service import message_service
+
+plugin = NekroPlugin(
+    name="Minecraft 工具插件",
+    module_name="minecraft_utils",
+    description="提供 Minecraft 适配器专用的消息发送和 RCON 命令执行功能。",
+    version="0.1.0",
+    author="Zaxpris",
+    url="https://github.com/KroMiose/nekro-agent",
+    support_adapter=["minecraft"],
+)
+
+
+def _llm_segment_dict_to_text_component(segment_dict: Dict[str, Any]) -> TextComponent:
+    """
+    将 LLM 输出的单个富文本段字典转换为 Minecraft TextComponent。
+    主要用于构建 HoverEvent.text 中的组件。
+    """
+    text = segment_dict.get("text", "")
+    component_data: Dict[str, Any] = {"text": text}
+
+    if "color" in segment_dict:
+        try:
+            component_data["color"] = TextColor(str(segment_dict["color"]).lower())
+        except ValueError:
+            logger.warning(
+                f"[MinecraftUtils] 无效的悬停文本颜色: {segment_dict['color']}",
+            )
+
+    for style_attr in ["bold", "italic", "underlined", "strikethrough", "obfuscated"]:
+        if style_attr in segment_dict:
+            component_data[style_attr] = segment_dict[style_attr]
+
+    if "insertion" in segment_dict:
+        component_data["insertion"] = segment_dict["insertion"]
+
+    click_action_str = segment_dict.get("click_event_action")
+    click_value = segment_dict.get("click_event_value")
+    if click_action_str and click_value:
+        try:
+            action = ClickAction(str(click_action_str).lower())
+            component_data["click_event"] = ClickEvent(
+                action=action, value=str(click_value),
+            )
+        except ValueError:
+            logger.warning(
+                f"[MinecraftUtils] 无效的悬停点击事件动作: {click_action_str}",
+            )
+
+    hover_action_str = segment_dict.get("hover_event_action")
+    if hover_action_str:
+        try:
+            hover_action = HoverAction(str(hover_action_str).lower())
+            if hover_action == HoverAction.SHOW_TEXT:
+                hover_text_segments_data = segment_dict.get("hover_event_text_segments")
+                if isinstance(hover_text_segments_data, list):
+                    nested_hover_components: List[BaseComponent] = [
+                        _llm_segment_dict_to_text_component(s)
+                        for s in hover_text_segments_data
+                    ]
+                    component_data["hover_event"] = HoverEvent(
+                        action=hover_action, text=nested_hover_components,
+                    )
+                else:
+                    logger.warning(
+                        "[MinecraftUtils] hover_event_text_segments 在悬停事件中缺失或非列表",
+                    )
+            # TODO: 支持其他 HoverAction (show_item, show_entity) 如果需要
+        except ValueError:
+            logger.warning(
+                f"[MinecraftUtils] 无效的悬停事件动作: {hover_action_str}",
+            )
+    return TextComponent(**{k: v for k, v in component_data.items() if v is not None})
+
+
+def _llm_segment_to_mc_message_segment(
+    segment_dict: Dict[str, Any],
+) -> Optional[MessageSegment]:
+    """
+    将 LLM 输出的单个富文本段字典转换为 Minecraft MessageSegment。
+    """
+    text = segment_dict.get("text")
+    if text is None:  # 每个段必须有文本
+        logger.warning("[MinecraftUtils] 富文本段缺少 'text' 键。")
+        return None
+
+    segment_args: Dict[str, Any] = {}
+
+    if "color" in segment_dict:
+        try:
+            segment_args["color"] = TextColor(str(segment_dict["color"]).lower())
+        except ValueError:
+            logger.warning(
+                f"[MinecraftUtils] 无效的文本颜色: {segment_dict['color']}",
+            )
+
+    for style_attr in ["bold", "italic", "underlined", "strikethrough", "obfuscated"]:
+        if style_attr in segment_dict:
+            segment_args[style_attr] = segment_dict[style_attr]
+
+    if "insertion" in segment_dict:
+        segment_args["insertion"] = segment_dict["insertion"]
+
+    click_action_str = segment_dict.get("click_event_action")
+    click_value = segment_dict.get("click_event_value")
+    if click_action_str and click_value:
+        try:
+            action = ClickAction(str(click_action_str).lower())
+            segment_args["click_event"] = ClickEvent(
+                action=action, value=str(click_value),
+            )
+        except ValueError:
+            logger.warning(
+                f"[MinecraftUtils] 无效的点击事件动作: {click_action_str}",
+            )
+
+    hover_action_str = segment_dict.get("hover_event_action")
+    if hover_action_str:
+        try:
+            hover_action = HoverAction(str(hover_action_str).lower())
+            if hover_action == HoverAction.SHOW_TEXT:
+                hover_text_segments_data = segment_dict.get("hover_event_text_segments")
+                if isinstance(hover_text_segments_data, list):
+                    hover_components: List[BaseComponent] = [
+                        _llm_segment_dict_to_text_component(s_dict)
+                        for s_dict in hover_text_segments_data
+                    ]
+                    segment_args["hover_event"] = HoverEvent(
+                        action=hover_action, text=hover_components,
+                    )
+                else:
+                    logger.warning(
+                        "[MinecraftUtils] hover_event_text_segments 缺失或非列表",
+                    )
+            # TODO: 支持其他 HoverAction (show_item, show_entity) 如果需要
+        except ValueError:
+            logger.warning(
+                f"[MinecraftUtils] 无效的悬停事件动作: {hover_action_str}",
+            )
+
+    return MessageSegment.text(str(text), **segment_args)
+
+@plugin.mount_sandbox_method(
+    SandboxMethodType.TOOL,
+    name="发送Minecraft富文本消息",
+    description="向指定的Minecraft聊天发送富文本消息。LLM需要提供特定格式的JSON字符串。",
+)
+async def send_rich_text(_ctx: AgentCtx, chat_key: str, rich_text_json: str):
+    """发送Minecraft富文本消息
+    CAUTION: THIS METHOD IS USED FOR SENDING MESSAGES VIA MINECRAFT ADAPTER, NOT A MODEL CONTEXT PROTOCOL (MCP) SERVICE.
+    Args:
+        chat_key (str): Minecraft 服务器的会话标识 (例如 'minecraft-servername')。
+        rich_text_json (str): 一个 JSON 字符串，表示一个消息段(segment)的列表。
+            每个 segment 都是一个 JSON 对象，用于描述一段具有特定样式的文本及其交互行为。
+
+    富文本 Segment JSON 对象支持的键:
+        - `text` (str, 必选): 该段的文本内容。
+        - `color` (str, 可选): 文本颜色。有效值是 TextColor 枚举的小写形式，例如:
+          `"black"`, `"dark_blue"`, `"dark_green"`, `"dark_aqua"`, `"dark_red"`,
+          `"dark_purple"`, `"gold"`, `"gray"`, `"dark_gray"`, `"blue"`, `"green"`,
+          `"aqua"`, `"red"`, `"light_purple"`, `"yellow"`, `"white"`.
+          默认为服务器的默认文本颜色。
+        - `bold` (bool, 可选): 文本是否加粗。默认为 `false`。
+        - `italic` (bool, 可选): 文本是否斜体。默认为 `false`。
+        - `underlined` (bool, 可选): 文本是否有下划线。默认为 `false`。
+        - `strikethrough` (bool, 可选): 文本是否有删除线。默认为 `false`。
+        - `obfuscated` (bool, 可选): 文本是否混淆显示 (随机字符)。默认为 `false`。
+        - `insertion` (str, 可选): 当玩家在聊天中点击这段文本时，插入到玩家聊天输入框的文本。
+        - `click_event_action` (str, 可选): 点击事件的动作。必须与 `click_event_value` 一起使用。
+          有效值 (ClickAction 枚举的小写形式):
+            - `"open_url"`: 打开一个 URL。
+            - `"open_file"`: 打开一个文件 (通常在客户端上，不推荐用于服务器消息)。
+            - `"run_command"`: 以玩家身份执行一条命令。
+            - `"suggest_command"`: 在玩家聊天输入框中建议一条命令 (玩家需手动发送)。
+            - `"change_page"`: (仅用于书本中) 改变书本的页面。
+            - `"copy_to_clipboard"`: 将指定文本复制到玩家的剪贴板。
+        - `click_event_value` (str, 可选): 点击事件的值。根据 `click_event_action` 的不同而不同:
+            - 对于 `"open_url"`, `"suggest_command"`, `"copy_to_clipboard"`: 对应的 URL、命令字符串或要复制的文本。
+            - 对于 `"run_command"`: 要执行的命令 (通常需要以 `/` 开头)。
+            - 对于 `"change_page"`: 页码。
+        - `hover_event_action` (str, 可选): 鼠标悬停事件的动作。目前主要支持 `"show_text"`。
+          必须与 `hover_event_text_segments` 一起使用。
+          有效值 (HoverAction 枚举的小写形式):
+            - `"show_text"`: 悬停时显示一段富文本。
+            - `"show_item"`: (暂未完全支持通过此JSON格式配置) 悬停时显示物品信息。
+            - `"show_entity"`: (暂未完全支持通过此JSON格式配置) 悬停时显示实体信息。
+        - `hover_event_text_segments` (List[Dict], 可选): 当 `hover_event_action` 为 `"show_text"` 时使用。
+          它本身是一个消息段 (segment) 对象的列表 (与顶层结构相同，但不建议嵌套过深的悬停事件)，
+          用于定义鼠标悬停时显示的富文本内容。
+
+    示例 JSON 字符串:
+    ```json
+    [
+        {"text": "欢迎来到 "},
+        {"text": "我的世界!", "color": "gold", "bold": true},
+        {
+            "text": " 点击这里访问官网", 
+            "color": "aqua", 
+            "underlined": true, 
+            "click_event_action": "open_url", 
+            "click_event_value": "https://www.minecraft.net"
+        },
+        {
+            "text": " 或 ", 
+            "color": "gray"
+        },
+        {
+            "text": "执行命令", 
+            "color": "light_purple", 
+            "italic": true,
+            "click_event_action": "suggest_command", 
+            "click_event_value": "/help",
+            "hover_event_action": "show_text",
+            "hover_event_text_segments": [
+                {"text": "点击后会在你的聊天框输入 ", "color": "yellow"},
+                {"text": "/help", "color": "red", "bold": true}
+            ]
+        }
+    ]
+    ```
+    注意: JSON 字符串在作为参数传递时，内部的双引号需要被正确转义，例如在Python字符串中表示为 `\"`。
+    示例参数: `rich_text_json='[{"text": "你好", "color": "red"}, {"text": " 世界!", "bold": true}]'`
+    """
+
+    def _parse_and_validate_segments(json_str: str) -> List[MessageSegment]:
+        """解析并验证富文本JSON字符串，返回MessageSegment列表。"""
+        try:
+            segments_data = json.loads(json_str)
+        except json.JSONDecodeError as e:
+            logger.error(f"解析富文本 JSON 失败: {e}", exc_info=True)
+            raise ValueError(f"富文本 JSON 格式错误: {e}") from e
+
+        if not isinstance(segments_data, list):
+            raise ValueError("富文本 JSON 解析后必须是一个列表。")  # noqa: TRY004
+        
+        parsed_message_segments: List[MessageSegment] = []
+        for seg_dict in segments_data:
+            if isinstance(seg_dict, dict):
+                mc_segment = _llm_segment_to_mc_message_segment(seg_dict)
+                if mc_segment:
+                    parsed_message_segments.append(mc_segment)
+            else:
+                logger.warning(
+                    f"[MinecraftUtils] 富文本列表中的元素不是字典: {seg_dict}",
+                )
+        
+        if not parsed_message_segments:
+            raise ValueError("没有可发送的有效富文本段。")
+        return parsed_message_segments
+
+    if not isinstance(chat_key, str) or not chat_key.startswith("minecraft-"):
+        raise ValueError("chat_key 无效，必须是 'minecraft-servername' 格式。")
+    if not isinstance(rich_text_json, str) or not rich_text_json.strip():
+        raise ValueError("富文本 JSON 字符串不能为空。")
+
+    bot: Optional[Bot] = get_bot(chat_key)
+    if not bot:
+        raise ConnectionError(f"未能找到或连接到 Minecraft 服务器: {chat_key}")
+
+    try:
+        message_segments = _parse_and_validate_segments(rich_text_json)
+
+        channel = await DBChatChannel.get_channel(chat_key)
+        preset = await channel.get_preset()
+
+        plain_text_parts = []
+        for seg in message_segments: # message_segments 是 _parse_and_validate_segments 的结果
+            if hasattr(seg, "data") and isinstance(seg.data, dict) and "text" in seg.data:
+                plain_text_parts.append(str(seg.data["text"]))
+        plain_text_content = "".join(plain_text_parts)
+        
+        await message_service.push_bot_message(
+            chat_key=chat_key,
+            agent_messages=plain_text_content, # 使用提取的纯文本
+            db_chat_channel=channel, 
+        )
+        message_segments.insert(0,MessageSegment.text(f"<{preset.name}>", color=TextColor.GREEN))
+        message_to_send = Message(message_segments)
+        await bot.send_msg(message=message_to_send)
+        logger.info(f"Minecraft 富文本消息已发送到 {chat_key}")
+    except ValueError as e:
+        logger.error(f"处理富文本消息失败: {e}", exc_info=True)
+        raise
+    except Exception as e:
+        logger.error(
+            f"发送 Minecraft 富文本消息到 {chat_key} 失败: {e}", exc_info=True,
+        )
+        raise Exception(f"发送 Minecraft 富文本消息失败: {e}") from e
+
+
+@plugin.mount_sandbox_method(
+    SandboxMethodType.AGENT,
+    name="批量执行Minecraft RCON命令",
+    description="在指定的Minecraft服务器上批量执行RCON命令。",
+)
+async def execute_rcon_commands(
+    _ctx: AgentCtx,
+    chat_key: str,
+    commands: List[str],
+    continue_on_error: bool = False,
+) -> str:
+    """批量执行Minecraft RCON命令
+    CAUTION: THIS METHOD IS USED FOR EXECUTING RCON COMMANDS VIA MINECRAFT ADAPTER, NOT A MODEL CONTEXT PROTOCOL (MCP) SERVICE.
+    Args:
+        chat_key (str): Minecraft 服务器的会话标识 (例如 'minecraft-servername')。
+        commands (List[str]): 要执行的 RCON 命令列表。
+        continue_on_error (bool, optional): 如果为 True，则在单个命令出错时继续执行后续命令，
+                                        并将错误信息记录在对应命令的结果中。
+                                        如果为 False (默认)，则在任何命令出错时立即停止，
+                                        并返回错误信息及之前已成功执行的命令结果。
+
+    Returns:
+        str:
+            RCON 命令的执行结果。
+            如果 continue_on_error 为 True，返回所有命令结果的拼接字符串，错误会明确指出。
+            如果 continue_on_error 为 False 且发生错误，则返回一个描述错误的字符串，包含出错命令和已成功的结果。
+            如果 continue_on_error 为 False 且所有命令成功，则返回所有成功结果的拼接字符串。
+
+    Examples:
+        execute_rcon_commands(chat_key="minecraft-myserver", commands=["say Hello", "time set day"], continue_on_error=False)
+        -> "Command 'say Hello': Hello\nCommand 'time set day': Set the time to 24000"
+
+        execute_rcon_commands(chat_key="minecraft-myserver", commands=["say Hello", "invalid_command", "say World"], continue_on_error=False)
+        -> "Error executing command 'invalid_command': <具体错误>\nPrevious results:\nCommand 'say Hello': Hello"
+
+        execute_rcon_commands(chat_key="minecraft-myserver", commands=["say Hello", "invalid_command", "say World"], continue_on_error=True)
+        -> "Command 'say Hello': Hello\nCommand 'invalid_command': Error - <具体错误>\nCommand 'say World': World"
+    """
+    if not isinstance(chat_key, str) or not chat_key.startswith("minecraft-"):
+        raise ValueError("chat_key 无效，必须是 'minecraft-servername' 格式。")
+    if not isinstance(commands, list) or not all(
+        isinstance(cmd, str) and cmd.strip() for cmd in commands
+    ):
+        raise ValueError("RCON 命令列表无效，必须是非空字符串的列表。")
+    if not commands:
+        raise ValueError("RCON 命令列表不能为空。")
+
+    bot: Optional[Bot] = get_bot(chat_key)
+    if not bot:
+        # 以字符串形式返回错误，因为这是 Agent 类型方法
+        return f"错误：未能找到或连接到 Minecraft 服务器: {chat_key}"
+
+    if not bot.rcon:
+        return f"错误：Minecraft 服务器 {chat_key} 未配置或未连接 RCON。"
+
+    results_log: List[str] = [] # 用于记录每个命令的文本结果或错误
+
+    for command in commands:
+        try:
+            response = await bot.call_api("send_rcon_cmd", command=command)
+            result_str = str(response) if response is not None else "指令无返回内容"
+            log_entry = f"Command '{command}': {result_str}"
+            results_log.append(log_entry)
+            logger.info(
+                f"RCON 命令 '{command}' 在 {chat_key} 上执行成功，响应: {result_str}",
+            )
+        except Exception as e:
+            error_message = f"在 {chat_key} 上执行 RCON 命令 '{command}' 失败: {e}"
+            logger.error(error_message, exc_info=True)
+            if not continue_on_error:
+                error_response = f"执行命令 '{command}' 时出错: {e}"
+                if results_log: # 如果之前有成功的结果
+                    error_response += "\n先前成功的结果:\n" + "\n".join(results_log)
+                return error_response
+            # 如果 continue_on_error 为 True，则记录错误并继续
+            results_log.append(f"Command '{command}': Error - {e}")
+
+    return "\n".join(results_log)
diff --git a/poetry.lock b/poetry.lock
index ca368498..6302aeb4 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,5 +1,16 @@
 # This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand.
 
+[[package]]
+name = "aio-mc-rcon"
+version = "3.4.1"
+description = "An async library for utilizing remote console on Minecraft Java Edition servers"
+optional = false
+python-versions = "<4.0,>=3.9"
+files = [
+    {file = "aio_mc_rcon-3.4.1-py3-none-any.whl", hash = "sha256:b8cb967505b3801d341e949a36afa6ea62ffe6f999cfa9fe72108378347ef440"},
+    {file = "aio_mc_rcon-3.4.1.tar.gz", hash = "sha256:c1623ed54a56b302abd76a7be9fbeab1275285bf58f8109a78169f82c01cdd13"},
+]
+
 [[package]]
 name = "aiodocker"
 version = "0.22.2"
@@ -224,6 +235,40 @@ doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)",
 test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"]
 trio = ["trio (>=0.26.1)"]
 
+[[package]]
+name = "arclet-alconna"
+version = "1.8.37"
+description = "A High-performance, Generality, Humane Command Line Arguments Parser Library."
+optional = false
+python-versions = ">=3.9"
+files = [
+    {file = "arclet_alconna-1.8.37-py3-none-any.whl", hash = "sha256:d247b2959a0d2c4b7ea5c8e446c5e09cc05a6dbaaf3fb776abc43ad2dd147e5d"},
+    {file = "arclet_alconna-1.8.37.tar.gz", hash = "sha256:3cc53a5b7413194d26cd67fb49a92a121de356cfcca767f2cedc9ff57b76dea4"},
+]
+
+[package.dependencies]
+nepattern = ">=0.7.7,<1.0.0"
+tarina = ">=0.6.1,<0.7.0"
+typing-extensions = ">=4.5.0"
+
+[package.extras]
+full = ["arclet-alconna-tools (>=0.2.0)"]
+
+[[package]]
+name = "arclet-alconna-tools"
+version = "0.7.10"
+description = "Builtin Tools for Alconna"
+optional = false
+python-versions = ">=3.9"
+files = [
+    {file = "arclet_alconna_tools-0.7.10-py3-none-any.whl", hash = "sha256:50e8b2f433fbc612dc8b99f4f5410006dcb1ef406c971c795071117a4eab8e20"},
+    {file = "arclet_alconna_tools-0.7.10.tar.gz", hash = "sha256:446a63a9c56886c23fb44548bb9a18655e0ba5b5dd80cc87915b858dfb02554c"},
+]
+
+[package.dependencies]
+arclet-alconna = ">=1.8.31"
+nepattern = ">=0.7.3,<1.0.0"
+
 [[package]]
 name = "arrow"
 version = "1.3.0"
@@ -2396,6 +2441,37 @@ virtualenv = ">=20.21,<21.0"
 watchfiles = ">=0.16,<1.0"
 wcwidth = ">=0.2,<1.0"
 
+[[package]]
+name = "nepattern"
+version = "0.7.7"
+description = "a complex pattern, support typing"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "nepattern-0.7.7-py3-none-any.whl", hash = "sha256:2d66f964333f42df7971390da4fb98dfed1e8b769236f305c28a83c0bcda849a"},
+    {file = "nepattern-0.7.7.tar.gz", hash = "sha256:6667f888457e78937998f9412eb70ad16d220464d2d77850dd2b05e9ecfb3207"},
+]
+
+[package.dependencies]
+tarina = ">=0.5.1"
+typing-extensions = ">=4.5.0"
+
+[[package]]
+name = "nonebot-adapter-minecraft"
+version = "1.4.0"
+description = "NoneBot2与MineCraft Server互通的适配器。"
+optional = false
+python-versions = "<4.0,>=3.9"
+files = [
+    {file = "nonebot_adapter_minecraft-1.4.0-py3-none-any.whl", hash = "sha256:b890ae5bf446c130d9a5a3942c7927f18cbf3fc976acc3fa44bd9d948cb34491"},
+    {file = "nonebot_adapter_minecraft-1.4.0.tar.gz", hash = "sha256:77bfe23a6e187677fafb865215be969d5eb2b0e651e9ed498db629afc06ffe43"},
+]
+
+[package.dependencies]
+aio-mc-rcon = ">=3.2.2,<4.0.0"
+nonebot2 = {version = ">=2.2.0,<3.0.0", extras = ["fastapi"]}
+pydantic = ">=1.10.0,<2.5.0 || >2.5.0,<2.5.1 || >2.5.1,<3.0.0"
+
 [[package]]
 name = "nonebot-adapter-onebot"
 version = "2.4.6"
@@ -2413,15 +2489,67 @@ nonebot2 = ">=2.2.0,<3.0.0"
 pydantic = ">=1.10.0,<2.5.0 || >2.5.0,<2.5.1 || >2.5.1,<3.0.0"
 typing-extensions = ">=4.0.0,<5.0.0"
 
+[[package]]
+name = "nonebot-plugin-alconna"
+version = "0.57.6"
+description = "Alconna Adapter for Nonebot"
+optional = false
+python-versions = ">=3.9"
+files = [
+    {file = "nonebot_plugin_alconna-0.57.6-py3-none-any.whl", hash = "sha256:7690f4f1bfc1be8755080eea3554582cafe9cb44c1a1f270e30922d3d1f8358d"},
+    {file = "nonebot_plugin_alconna-0.57.6.tar.gz", hash = "sha256:f22357cf784358205b8ae76707627fd28d1a73b594e38b239619be4eea9fe6ae"},
+]
+
+[package.dependencies]
+arclet-alconna = ">=1.8.35,<2.0"
+arclet-alconna-tools = ">=0.7.10"
+importlib-metadata = ">=4.13.0"
+nepattern = ">=0.7.7,<1.0"
+nonebot-plugin-waiter = ">=0.6.0"
+nonebot2 = ">=2.3.0"
+tarina = ">=0.6.8,<0.7"
+
+[[package]]
+name = "nonebot-plugin-uninfo"
+version = "0.7.4"
+description = "Universal Information Model for Nonebot2"
+optional = false
+python-versions = ">=3.9"
+files = [
+    {file = "nonebot_plugin_uninfo-0.7.4-py3-none-any.whl", hash = "sha256:673b2f3c0c0a6049eed3ca66de6992793c7a85ff831223c2ef920b532b29a244"},
+    {file = "nonebot_plugin_uninfo-0.7.4.tar.gz", hash = "sha256:91a461f39ffd30629bfcf32c6f262ded322c5651d6cfbf7ced4f6a5c251afee3"},
+]
+
+[package.dependencies]
+importlib-metadata = ">=7.2.1"
+nonebot2 = ">=2.4.2"
+
+[[package]]
+name = "nonebot-plugin-waiter"
+version = "0.8.1"
+description = "An alternative for got-and-reject in Nonebot"
+optional = false
+python-versions = ">=3.9"
+files = [
+    {file = "nonebot_plugin_waiter-0.8.1-py3-none-any.whl", hash = "sha256:3e1afc8f134496d3a4ecefd9c3a2a98d6ef28a5318268cb22b99a0ef61a44080"},
+    {file = "nonebot_plugin_waiter-0.8.1.tar.gz", hash = "sha256:5e54213dfea1fd8a1e20dbe6d93b7881f35cbeedf80005148cdc39c1fd2ccc0f"},
+]
+
+[package.dependencies]
+nonebot2 = ">=2.3.0"
+
+[package.extras]
+unimsg = ["nonebot-plugin-alconna (>=0.52.2)"]
+
 [[package]]
 name = "nonebot2"
-version = "2.4.1"
+version = "2.4.2"
 description = "An asynchronous python bot framework."
 optional = false
 python-versions = "<4.0,>=3.9"
 files = [
-    {file = "nonebot2-2.4.1-py3-none-any.whl", hash = "sha256:fec95f075efc89dbe9ce148618b413b02f46ba284200367749b035e794695111"},
-    {file = "nonebot2-2.4.1.tar.gz", hash = "sha256:8fea364318501ed79721403a8ecd76587bc884d58c356260f691a8bbda9b05e6"},
+    {file = "nonebot2-2.4.2-py3-none-any.whl", hash = "sha256:ed3e970cdb6c885fb23349b65a045c08cf3ac7f43e28564ae0c72d3671ecda74"},
+    {file = "nonebot2-2.4.2.tar.gz", hash = "sha256:cf72d5920503ff373ba1d7963f3ddf573db913eb504e3b68ee347efb937db27d"},
 ]
 
 [package.dependencies]
@@ -4319,6 +4447,98 @@ anyio = ">=3.6.2,<5"
 [package.extras]
 full = ["httpx (>=0.27.0,<0.29.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"]
 
+[[package]]
+name = "tarina"
+version = "0.6.8"
+description = "A collection of common utils for Arclet"
+optional = false
+python-versions = ">=3.9"
+files = [
+    {file = "tarina-0.6.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a2f7b7e61912a020d6ba3c591c4edbc31bb468544640bd814470c69a07dcc4cd"},
+    {file = "tarina-0.6.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1cac7cbd49317b8e63eba7d0ce0ba11e1218ab51c9d6ee9df8404b5e226db15b"},
+    {file = "tarina-0.6.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:69d9923d888948ccc9d20a191ab1f6ff2bc097f4154b6a3780c45dc78ae047cd"},
+    {file = "tarina-0.6.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b0f12395301584e0d56c7edd6118c5b5732b456067735ee7f0800575d5d490f"},
+    {file = "tarina-0.6.8-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84bd95b18c7f6a55606b0071654137b98f70faf5d12343d361377cf0d81ecde6"},
+    {file = "tarina-0.6.8-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94e6ee4df273e1fd3eaf36de7c78a93dde08bf2ad67526122afab1b077e344d9"},
+    {file = "tarina-0.6.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7afc1a8a11983f39cce18ad7f8d27601c4d34b1ffe1a1aca2a59b4111d181d01"},
+    {file = "tarina-0.6.8-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e0f74dc96979b774d7e23b964886137f7cf5f58cb2d652c23c3e2d42c1ad39a9"},
+    {file = "tarina-0.6.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3493d59ef19d1ded232e7a33e52c7d6ebfff66f30ac4c3649ae765b14dacc393"},
+    {file = "tarina-0.6.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:323351247817dac174f609104c54fcdc4923aaf3ac5e4a4a81c3efaa007d5778"},
+    {file = "tarina-0.6.8-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:eb7407046d8051b062f71440b90a11be3ef1a64e99b1c6be157312fe2889e4f4"},
+    {file = "tarina-0.6.8-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:50bbbb249d0849855cf43f4fa09c4eecac53e76a54c876ef1a7fb967f17d35b6"},
+    {file = "tarina-0.6.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8a05063e41f01f64c3c6e87b740c1e2cebffdcae20b9d89014a934bb2707a2b7"},
+    {file = "tarina-0.6.8-cp310-cp310-win32.whl", hash = "sha256:162756b5c0872856c11cc97ed1a618a9118fef3fb6999edc917578b56f3eb67f"},
+    {file = "tarina-0.6.8-cp310-cp310-win_amd64.whl", hash = "sha256:a58a8a6e36e43de8f3a0561cb264abe300ec6bb4a4215f16c316f1db564de6fb"},
+    {file = "tarina-0.6.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c05d38546712014aafcf123e89d90ad75f08ebd40c5a7e4c76db40d2ae11b387"},
+    {file = "tarina-0.6.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b036d53d2f8ec46fff9f649b5ec70431cdee3a2b99c8706f67d8d55ea4d4e493"},
+    {file = "tarina-0.6.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:195d2b4a8a8ec2e946ecd8abab70cf3065fbba67e18e971e0412a74a8417a656"},
+    {file = "tarina-0.6.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9bbe85a83904eb29143a43511c04a3fea9a7392c70a6216fcec3ba06cf1b83a"},
+    {file = "tarina-0.6.8-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d6b5893b735d24d2a57f6dbb26de92bb5e09e476077a16408e2054f06263530"},
+    {file = "tarina-0.6.8-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a74cc68f4678d37d517a2d661184712f15da608510de6b1bb9103d85d6014409"},
+    {file = "tarina-0.6.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6dd0f0d769e503fb81d07bd2d342585347f5149803bd7b4b233deaff1833451f"},
+    {file = "tarina-0.6.8-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d3b7c91a488aa616e141c337cddce1ba1e0bdac6b98362bc15ce76bd3cd71e17"},
+    {file = "tarina-0.6.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4f0b91da85a816e34a3eb4670d21c26a3909872b1f5525797bedc32a45efd98d"},
+    {file = "tarina-0.6.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bf3a5a8fe912d5984627f64cdd187c8a0801c6f3b2f7f12c68cdb5babf691155"},
+    {file = "tarina-0.6.8-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:622e226553453a9a873fca5a2d495d2d28003a23834503147954d651186146c4"},
+    {file = "tarina-0.6.8-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:198102e526d096f37bfb868f815cb3bfc864e136aa8b70e90a107a38ffb82c15"},
+    {file = "tarina-0.6.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:07b252f686410fbf1fe64b3b6705dd40ff218bc1d6a67efac640a0a246f66da3"},
+    {file = "tarina-0.6.8-cp311-cp311-win32.whl", hash = "sha256:9e4f56478c58706548e0f08bd277666bac783d918257950be49c4a5d8a35e7b0"},
+    {file = "tarina-0.6.8-cp311-cp311-win_amd64.whl", hash = "sha256:d0a92b5368a8a1700f0482d0bc3891dccdfc6dc61f7e66101f68544fca10f096"},
+    {file = "tarina-0.6.8-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e8e4bf8ed36c92af7b2a973905f5f4f58dabf7490c5dda695534bf5e3b20d92e"},
+    {file = "tarina-0.6.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:175754132a107c2830a341d3efcae60f44bfd8e0eab56d92537cc6f849ad44e3"},
+    {file = "tarina-0.6.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c5f983f0c84959589f884777c7ac0b22786d6d183ae367e53265f09e9ed7dc9f"},
+    {file = "tarina-0.6.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c8cc5f1bf3c2099377b5de76b0e192bb4fae3dcfcc245be9ed14315b29c8c75"},
+    {file = "tarina-0.6.8-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e7067131af4081d8ca3d4e70c96e8903c9e483e4d5af4910b93051602f7e8b34"},
+    {file = "tarina-0.6.8-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a66ea0778dcbd9e9b9b226529ed1822261722956468de026655e09b41feee42b"},
+    {file = "tarina-0.6.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd8501b0cd51ad8c0e294b46614ef27ef6773324099b5a24711534db55ded44d"},
+    {file = "tarina-0.6.8-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5812e903c79c02029bfe61fd0ff30bf2a5ac58e7c5b1ab3abfb0f0b57260c5b8"},
+    {file = "tarina-0.6.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:60ea05cb8e07a629096d9abbd11258df2bcea03168bc8ef8f369ca0dddfc504a"},
+    {file = "tarina-0.6.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b2635d27c34353e9431cb995acece3a0b607cde0934ebf8df35f0e4ae70a0335"},
+    {file = "tarina-0.6.8-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:542b5359d23f969f275f9e80960ce7f19a2684e93cd3ab659afaeb7ee06de038"},
+    {file = "tarina-0.6.8-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4ba2018a855f59c28f087ec1ec1d892c6afccdd0da8f4347fbf8e3219145d66c"},
+    {file = "tarina-0.6.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fdf9c5c44d402041df79e747ff9f82c6794875febb053376acdb724043770975"},
+    {file = "tarina-0.6.8-cp312-cp312-win32.whl", hash = "sha256:cf44ca009d79d0ecc8541b9067a3b7d086d1939de3bc1b04c164286b0fcf2112"},
+    {file = "tarina-0.6.8-cp312-cp312-win_amd64.whl", hash = "sha256:8cd9ff00e2eb9ed6c42b80504005c9b6de4a9afe9ef36771f63b8f9492e38a90"},
+    {file = "tarina-0.6.8-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:af3ff4742b0324d590298812997d028a6d6892bfab5680cd266bed0c1bbf90b0"},
+    {file = "tarina-0.6.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e7919d53f20e7d780b44e1804f7c9006bcf5832f87cfeb8d5432f1c832953ed4"},
+    {file = "tarina-0.6.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:878f8d9411ed36cf4408751e209a09c5e2363fc06b6074e966995cdbdc365a9d"},
+    {file = "tarina-0.6.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa166fe58b65be6aed113fca54851b236cfd5c23e33f5e61c403f7b71eda17a"},
+    {file = "tarina-0.6.8-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:04ea6b8f4568fe5b4f167d002fbe05879bfa04a4121b4ca3d294580cfc86b85d"},
+    {file = "tarina-0.6.8-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cbdf37600789f34c1eba6bf05e246f8e9a5b46cf93f9652e92d355e767bbc430"},
+    {file = "tarina-0.6.8-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb748ad914898c293771ccdb32c73b3117a15a3234c9d5eb001f834533cf67ef"},
+    {file = "tarina-0.6.8-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:55ca473eb8f3bbd70af71a29d59c6c0951e19ae24f3be86259b884fc097028a0"},
+    {file = "tarina-0.6.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4b124eee1ce8e25f1c1aba0464c95226ce5ed1b35f079f0fce0f993f38d3393b"},
+    {file = "tarina-0.6.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4295448f4dd9f54703adde2683cf6a515f0944687868da4ec500d595e6a9122d"},
+    {file = "tarina-0.6.8-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:77392b3e53e9e1069b9131b1eb2037c53b360e4d7f3cfa8ae04277fe4e785eb1"},
+    {file = "tarina-0.6.8-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:da9771f40ddcd9da087a4fc2d472b548c0da2bb61ad642b7e186ea7098120be6"},
+    {file = "tarina-0.6.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fac01686a3f9d4461b8ce6ba3afadb5b24138e27f725ee11efc4b057d98d4e4a"},
+    {file = "tarina-0.6.8-cp313-cp313-win32.whl", hash = "sha256:a977e3e38535455b38e018ec345f9982c4d2b40945b330aa5c70c1a24a2f8f5b"},
+    {file = "tarina-0.6.8-cp313-cp313-win_amd64.whl", hash = "sha256:62445e9a7c0ab7ac8f9c316836ade382f4b9780492f0a8a35a658f827bcb1d7f"},
+    {file = "tarina-0.6.8-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:491b46bfd030f64edc8daac99051e9e1a82d4592aaa9d91c86d18da7f1816a8d"},
+    {file = "tarina-0.6.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8dab1c7ff584f427ae671ecd0379b5795f3ddfee867c5c8997fb110c35b72901"},
+    {file = "tarina-0.6.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9f998c32df568da30f236876118f5c359caf00d4935249c78c2e6e15caa1a72f"},
+    {file = "tarina-0.6.8-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b427b03be5e9d19882558dafd8cae6decc4406f1511741409f1b75a5ce0bf447"},
+    {file = "tarina-0.6.8-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ec0bb5da7bbea3df20750acab45403c5fbd5a0b5c4dd7267e6d0cb3669935db4"},
+    {file = "tarina-0.6.8-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7acbc4b547fd623beb174431e2d01b2bc2a5d7451fe3d0e9ae3e0118d862d375"},
+    {file = "tarina-0.6.8-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed03a67986b00411aa5d3cd02896e5daf0e5af44247fd0ef79484b06780a2462"},
+    {file = "tarina-0.6.8-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:637740055c0e7da77793e0f2d56189f4620e9db31578c590234f85240364e80d"},
+    {file = "tarina-0.6.8-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9df3d4441b36b7e6ac290b05dd9a1e6193ff0acc2d970be2b855283b76d0ad40"},
+    {file = "tarina-0.6.8-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ff583c3579e5ea939da67c6a32ebced76b270c884d9f686a003f66f32a0cecdc"},
+    {file = "tarina-0.6.8-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:91589c96904f6a07ccf7fec977c403bab52aad63bd3a801a32e11d2ee85e1d46"},
+    {file = "tarina-0.6.8-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4a7448cacff18d536c3cf4453282ee29cf401442cba1a50f02981e5d4c374d7f"},
+    {file = "tarina-0.6.8-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:96fe4a3a33fa20cc3bd996ef9e4a909b00c37d89bd24d594a6024ca59a26c6c4"},
+    {file = "tarina-0.6.8-cp39-cp39-win32.whl", hash = "sha256:cbff457659568f46fbbb8e594644278b51efddb532890117b4965b8c059d4d4a"},
+    {file = "tarina-0.6.8-cp39-cp39-win_amd64.whl", hash = "sha256:bace5eeaed47048e9a8e9dd7953aadc11aace8c8a5db8457906d8f9c52604fc5"},
+    {file = "tarina-0.6.8-py3-none-any.whl", hash = "sha256:0e8a04eecb741937244d97da54f8cd36995819fc12a6b60c9b3f3f3d2860ebe6"},
+    {file = "tarina-0.6.8.tar.gz", hash = "sha256:d77d68948967a130066b61e85b21189259030f80ad19295bd81ff6b52f348316"},
+]
+
+[package.dependencies]
+typing-extensions = ">=4.4.0"
+
+[package.extras]
+yaml = ["pyyaml (>=6.0.1)"]
+
 [[package]]
 name = "tenacity"
 version = "9.0.0"
@@ -5230,4 +5450,4 @@ type = ["pytest-mypy"]
 [metadata]
 lock-version = "2.0"
 python-versions = ">=3.10,<3.12"
-content-hash = "2a646a02ecfeb787cdbab0719508e1e827430000c76c75c8c0bc80cf62562777"
+content-hash = "55358c396f014000aa179df7a6eabbb7ac6c79da6ad5de989e802a818fdea81f"
diff --git a/pyproject.toml b/pyproject.toml
index a3470a14..53141c7c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -46,6 +46,9 @@ mem0ai = "^0.1.79"
 gitpython = "^3.1.44"
 qdrant-client = "^1.13.3"
 mcp = "^1.7.0"
+nonebot-adapter-minecraft = "^1.4.0"
+nonebot-plugin-alconna = "^0.57.6"
+nonebot-plugin-uninfo = "^0.7.4"
 
 [tool.poetry.group.dev.dependencies]
 uvicorn = "^0.23.2"
@@ -58,8 +61,9 @@ build-backend = "poetry.core.masonry.api"
 [tool.nonebot]
 adapters = [
     { name = "OneBot V11", module_name = "nonebot.adapters.onebot.v11" },
+    {name = "Minecraft", module_name = "nonebot.adapters.minecraft"},
 ]
-plugins = ["nekro_agent"]
+plugins = ["nekro_agent", "nonebot_plugin_alconna", "nonebot_plugin_uninfo"]
 plugin_dirs = []
 builtin_plugins = []
 
diff --git a/run_bot.py b/run_bot.py
index 0570b3a1..09983ce5 100644
--- a/run_bot.py
+++ b/run_bot.py
@@ -1,12 +1,12 @@
 try:
     import nonebot
+    from nonebot.adapters.minecraft import Adapter as MinecraftAdapter
     from nonebot.adapters.onebot.v11 import Adapter as ONEBOT_V11Adapter
-
     nonebot.init()
 
     driver = nonebot.get_driver()
     driver.register_adapter(ONEBOT_V11Adapter)
-
+    driver.register_adapter(MinecraftAdapter)
     nonebot.load_from_toml("pyproject.toml")
 except Exception as e:
     import traceback


Reviews:
user: login: sourcery-ai[bot]
body: 嘿 @zxjwzn - 我已经查看了你的更改 - 这里有一些反馈：

**阻塞问题**：
- 看起来无论如何评估 isSmall，此表达式都返回 1。这可能是复制粘贴错误。([链接](https://github.com/KroMiose/nekro-agent/pull/84/files#diff-cdc691275171f675ae358b7d68e61f9fee3548f2bf04bf04e88159351f81b372R545))

**一般评论**：

- 对于高风险命令（`docker_restart`、`docker_logs`、`sh`、`nekro_db_reset`），有两个几乎相同的块 - 你应该删除重复的定义并将它们集中起来，以避免差异。
- 两个函数 `get_chat_info` 和 `get_chat_info_old` 重叠且令人困惑；合并为一个清晰的辅助函数（并弃用旧版本）以简化通道 ID 解析。
- 由于你已更改 chat_key 格式以包含适配器前缀（例如 `adapter‐group_1234`），请添加验证层或迁移助手来捕获旧式键，并为下游代码显示清晰的错误或转换。

<details>
<summary>以下是我在审查期间查看的内容</summary>

- 🟡 **一般问题**：发现 9 个问题
- 🔴 **安全**：1 个阻塞问题
- 🟢 **测试**：一切看起来都很好
- 🟡 **复杂性**：发现 2 个问题
- 🟢 **文档**：一切看起来都很好
</details>

***

<details>
<summary>Sourcery 对开源是免费的 - 如果你喜欢我们的评论，请考虑分享它们 ✨</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https://sourcery.ai)
- [Facebook](https://www.facebook.com/sharer/sharer.php?u=https://sourcery.ai)

</details>

<sub>
帮助我更有用！请点击每个评论上的 👍 或 👎，我将使用反馈来改进你的评论。
</sub>

<details>
<summary>Original comment in English</summary>

Hey @zxjwzn - I've reviewed your changes - here's some feedback:

**Blocking issues**:
- It looks like no matter how isSmall is evaluated, this expression returns 1. This is probably a copy-paste error. ([link](https://github.com/KroMiose/nekro-agent/pull/84/files#diff-cdc691275171f675ae358b7d68e61f9fee3548f2bf04bf04e88159351f81b372R545))

**General comments**:

- There are two nearly identical blocks for the high-risk commands (`docker_restart`, `docker_logs`, `sh`, `nekro_db_reset`)—you should remove the duplicate definitions and centralize them to avoid divergence.
- The two functions `get_chat_info` and `get_chat_info_old` are overlapping and confusing; consolidate into one clear helper (and deprecate the legacy variant) to simplify channel‐ID parsing.
- Since you’ve changed the chat_key format to include the adapter prefix (e.g. `adapter‐group_1234`), please add a validation layer or migration helper to catch old‐style keys and surface a clear error or conversion for downstream code.

<details>
<summary>Here's what I looked at during the review</summary>

- 🟡 **General issues**: 9 issues found
- 🔴 **Security**: 1 blocking issue
- 🟢 **Testing**: all looks good
- 🟡 **Complexity**: 2 issues found
- 🟢 **Documentation**: all looks good
</details>

***

<details>
<summary>Sourcery is free for open source - if you like our reviews please consider sharing them ✨</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https://sourcery.ai)
- [Facebook](https://www.facebook.com/sharer/sharer.php?u=https://sourcery.ai)

</details>

<sub>
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
</sub>

</details>
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-24T21:36:16Z

Review comments:
diff_hunk: @@ -50,5 +52,15 @@ def is_prevent_trigger(self) -> bool:
         prevent_until = prevent_until.astimezone(ZoneInfo("Asia/Shanghai"))
         return now < prevent_until  # 检查是否在禁止触发期间（现在时间小于结束时间）
 
+    @property
+    def unique_id(self) -> str:
path: nekro_agent/models/db_user.py
user: login: sourcery-ai[bot]
body: **suggestion (bug_risk):** 考虑在数据库级别对 unique_id 强制执行唯一性。

由于 unique_id 结合了 adapter_key 和 platform_userid，因此在数据库中对这些字段添加唯一约束将有助于防止重复的用户条目。

<details>
<summary>Original comment in English</summary>

**suggestion (bug_risk):** Consider enforcing uniqueness at the database level for unique_id.

Since unique_id combines adapter_key and platform_userid, adding a unique constraint on these fields in the database will help prevent duplicate user entries.

</details>
created_at: 2025-05-24T21:36:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -95,11 +116,20 @@ async def set_active(self, is_active: bool):
     @property
path: nekro_agent/models/db_chat_channel.py
user: login: sourcery-ai[bot]
body: **suggestion (bug_risk):** adapter 属性假定 adapter_utils.get_adapter 总是成功。

如果 get_adapter 可能失败或返回 None，请添加错误处理以防止在使用未知 adapter_key 时出现运行时错误。

<details>
<summary>Original comment in English</summary>

**suggestion (bug_risk):** adapter property assumes adapter_utils.get_adapter always succeeds.

If get_adapter can fail or return None, add error handling to prevent runtime errors when an unknown adapter_key is used.

</details>
created_at: 2025-05-24T21:36:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -42,6 +42,7 @@ def __init__(
         version: str,
         author: str,
         url: str,
+        support_adapter: Optional[List[str]] = None,
path: nekro_agent/services/plugin/base.py
user: login: sourcery-ai[bot]
body: **suggestion (bug_risk):** 添加了 support_adapter，但未对其内容或类型进行验证。

验证 'support_adapter' 是一个仅包含已识别适配器键的字符串列表，以防止配置错误或运行时错误。

建议的实现方式：

```python
        support_adapter: Optional[List[str]] = None,
        is_builtin: bool = False,
        is_package: bool = False,
    ):
        RECOGNIZED_ADAPTER_KEYS = {"adapter1", "adapter2", "adapter3"}  # TODO: 替换为实际的适配器键

        self.version = version.strip()
        self.author = _validate_name(author, "Author")
        self.url = url.strip()

        if support_adapter is None:
            self.support_adapter = []
        else:
            if not isinstance(support_adapter, list) or not all(isinstance(a, str) for a in support_adapter):
                raise ValueError("support_adapter 必须是字符串列表")
            unrecognized = [a for a in support_adapter if a not in RECOGNIZED_ADAPTER_KEYS]
            if unrecognized:
                raise ValueError(f"support_adapter 中无法识别的适配器键：{unrecognized}")
            self.support_adapter = support_adapter

        self._is_enabled = True
        self._key = f"{self.author}.{self.module_name}"
        self._collect_methods_func: Optional[CollectMethodsFunc] = None

```

- 将占位符 `RECOGNIZED_ADAPTER_KEYS` 集合替换为应用程序的实际有效适配器键集合。
- 如果 `RECOGNIZED_ADAPTER_KEYS` 应该在模块或类中共享，请考虑将其移动到模块或类作用域。

<details>
<summary>Original comment in English</summary>

**suggestion (bug_risk):** support_adapter is added but not validated for content or type.

Validate that 'support_adapter' is a list of strings with only recognized adapter keys to prevent misconfiguration or runtime errors.

Suggested implementation:

```python
        support_adapter: Optional[List[str]] = None,
        is_builtin: bool = False,
        is_package: bool = False,
    ):
        RECOGNIZED_ADAPTER_KEYS = {"adapter1", "adapter2", "adapter3"}  # TODO: Replace with actual adapter keys

        self.version = version.strip()
        self.author = _validate_name(author, "Author")
        self.url = url.strip()

        if support_adapter is None:
            self.support_adapter = []
        else:
            if not isinstance(support_adapter, list) or not all(isinstance(a, str) for a in support_adapter):
                raise ValueError("support_adapter must be a list of strings")
            unrecognized = [a for a in support_adapter if a not in RECOGNIZED_ADAPTER_KEYS]
            if unrecognized:
                raise ValueError(f"Unrecognized adapter keys in support_adapter: {unrecognized}")
            self.support_adapter = support_adapter

        self._is_enabled = True
        self._key = f"{self.author}.{self.module_name}"
        self._collect_methods_func: Optional[CollectMethodsFunc] = None

```

- Replace the placeholder `RECOGNIZED_ADAPTER_KEYS` set with the actual set of valid adapter keys for your application.
- If `RECOGNIZED_ADAPTER_KEYS` should be shared across the module or class, consider moving it to the module or class scope.

</details>
created_at: 2025-05-24T21:36:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,1451 @@
+# Nonebot Plugin Alconna 文档
+
+本文分为三部分:
+- [`nonebot_plugin_alconna` 的介绍与使用](#插件)
+- [`Alconna` 本体的介绍与使用](#本体)(请优先阅读本体)
+- [外部参考](#references)
+
+## 插件
+### 安装
+
+```shell
+pip install nonebot-plugin-alconna
+```
+
+或
+
+```shell
+nb plugin install nonebot-plugin-alconna
+```
+
+### 展示
+
+```python
+from nonebot.adapters.onebot.v12 import Message
+from nonebot_plugin_alconna import on_alconna, AlconnaMatches, At
+from nonebot_plugin_alconna.adapters.onebot12 import Image
+from arclet.alconna import Alconna, Args, Option, Arparma
+
+
+alc = Alconna("Hello!", Option("--spec", Args["target", At]))
+hello = on_alconna(alc, auto_send_output=True)
+
+@hello.handle()
+async def _(result: Arparma = AlconnaMatches()):
+    if result.find("spec"):
+        target = result.query[At]("spec.target")
+        seed = target.target
+        await hello.finish(Message(Image(await gen_image(seed))))
+    else:
+        await hello.finish("Hello!")
+```
+
+### 响应器使用
+
+本插件基于 **Alconna** , 为 **Nonebot** 提供了一类新的事件响应器辅助函数 `on_alconna`.
+```python
+def on_alconna(
+    command: Alconna | str,
+    skip_for_unmatch: bool = True,
+    auto_send_output: bool = True,
+    aliases: set[str | tuple[str, ...]] | None = None,
+    comp_config: CompConfig | None = None,
+    extensions: list[type[Extension] | Extension] | None = None,
+    exclude_ext: list[type[Extension] | str] | None = None,
+    use_origin: bool = False,
+    use_cmd_start: bool = False,
+    use_cmd_sep: bool = False,
+    **kwargs,
+    ...,
+):
+```
+- `command`: Alconna 命令或字符串，字符串将通过 `AlconnaFormat` 转换为 Alconna 命令
+- `skip_for_unmatch`: 是否在命令不匹配时跳过该响应
+- `auto_send_output`: 是否自动发送输出信息并跳过响应
+- `aliases`: 命令别名， 作用类似于 `on_command` 中的 aliases
+- `comp_config`: 补全会话配置， 不传入则不启用补全会话
+- `extensions`: 需要加载的匹配扩展, 可以是扩展类或扩展实例
+- `exclude_ext`: 需要排除的匹配扩展, 可以是扩展类或扩展的id
+- `use_origin`: 是否使用未经 to_me 等处理过的消息
+- `use_cmd_start`: 是否使用 COMMAND_START 作为命令前缀
+- `use_cmd_sep`: 是否使用 COMMAND_SEP 作为命令分隔符
+
+其中对于 `skip_for_unmatch` 和 `auto_send_output` 的效果如下:
+
+| skip \ auto | True           | False                         |
+|-------------|----------------|-------------------------------|
+| True        | 帮助文本等自动发送      | 帮助文本等不发送                      |
+| False       | 帮助文本等和错误信息自动发送 | 帮助文本等和错误信息设置到 `CommandResult` |
+
+
+`on_alconna` 返回的是 `Matcher` 的子类 `AlconnaMatcher`，其拓展了如下方法:
+
+- `.assign(path, value, or_not)`: 用于对包含多个选项/子命令的命令的分派处理(具体请看条件控制)
+- `.got_path(path, prompt, middleware)`: 在 `got` 方法的基础上，会以 path 对应的参数为准，读取传入 message 的最后一个消息段并验证转换
+- `.set_path_arg(key, value)`, `.get_path_arg(key)`: 类似 `set_arg` 和 `got_arg`，为 `got_path` 的特化版本
+- `.reject_path(path[, prompt, fallback])`: 类似于 `reject_arg`，对应 `got_path`
+- `.dispatch`: 同样的分派处理，但是是类似 `CommandGroup` 一样返回新的 `AlconnaMatcher`
+- `.got`, `send`, `reject`, ...: 拓展了 prompt 类型，即支持使用 `UniMessage` 作为 prompt
+
+`assign`实例:
+
+```python
+from nonebot import require
+require("nonebot_plugin_alconna")
+
+from arclet.alconna import Alconna, Option, Args
+from nonebot_plugin_alconna import on_alconna, AlconnaMatch, Match, UniMessage
+
+  
+login = on_alconna(Alconna(["/"], "login", Args["password?", str], Option("-r|--recall"))) # 这里["/"]指命令前缀必须是/
+
+@login.assign("recall") # /login -r
+async def login_exit():
+    await login.finish("已退出")
+
+@login.assign("password") # /login xxx
+async def login_handle(pw: Match[str] = AlconnaMatch("password")):
+    if pw.available:
+        login.set_path_arg("password", pw.result)
+```
+
+`dispatch`每个分发设置独立的 matcher:
+
+```python
+update_cmd = pip_cmd.dispatch("install.pak", "pip")
+
+@update_cmd.handle()
+async def update(arp: CommandResult = AlconnaResult()):
+    ...
+```
+
+`got_path`类似 Nonebot2 的got, 它与 `assign`，`Match`，`Query` 等地方一样，都需要指明 `path` 参数 (即对应 Arg 验证的路径)
+
+`got_path` 会获取消息的最后一个消息段并转为 path 对应的类型，例如示例中 `target` 对应的 Arg 里要求 str 或 At，则 got 后用户输入的消息只有为 text 或 at 才能进入处理函数.
+
+实例:
+
+```python
+from nonebot_plugin_alconna import At, Match, UniMessage, on_alconna
+
+
+test_cmd = on_alconna(Alconna("test", Args["target?", Union[str, At]]))
+
+@test_cmd.handle()
+async def tt_h(target: Match[Union[str, At]]):
+    if target.available:
+        test_cmd.set_path_arg("target", target.result)
+
+@test_cmd.got_path("target", prompt="请输入目标")
+async def tt(target: Union[str, At]):
+    await test_cmd.send(UniMessage(["ok\n", target]))
+```
+
+`path`支持 ~XXX 语法，其会把 ~ 替换为可能的父级路径:
+
+```python
+ pip = Alconna(
+     "pip",
+     Subcommand(
+         "install",
+         Args["pak", str],
+         Option("--upgrade|-U"),
+         Option("--force-reinstall"),
+     ),
+     Subcommand("list", Option("--out-dated")),
+ )
+
+ pipcmd = on_alconna(pip)
+ pip_install_cmd = pipcmd.dispatch("install")
+
+
+ @pip_install_cmd.assign("~upgrade")
+ async def pip1_u(pak: Query[str] = Query("~pak")):
+     await pip_install_cmd.finish(f"pip upgrading {pak.result}...")
+```
+
+
+### Alconna的依赖注入
+
+本插件提供了一系列依赖注入函数，便于在响应函数中获取解析结果:
+
+- `AlconnaResult`: `CommandResult` 类型的依赖注入函数
+- `AlconnaMatches`: `Arparma` 类型的依赖注入函数
+- `AlconnaDuplication`: `Duplication` 类型的依赖注入函数
+- `AlconnaMatch`: `Match` 类型的依赖注入函数
+- `AlconnaQuery`: `Query` 类型的依赖注入函数
+
+同时，基于 [`Annotated` 支持](https://github.com/nonebot/nonebot2/pull/1832), 添加了两类注解:
+
+- `AlcMatches`：同 `AlconnaMatches`
+- `AlcResult`：同 `AlconnaResult`
+
+可以看到，本插件提供了几类额外的模型:
+
+- `CommandResult`: 解析结果，包括了源命令 `source: Alconna` ，解析结果 `result: Arparma`，以及可能的输出信息 `output: str | None` 字段
+- `Match`: 匹配项，表示参数是否存在于 `all_matched_args` 内，可用 `Match.available` 判断是否匹配，`Match.result` 获取匹配的值
+- `Query`: 查询项，表示参数是否可由 `Arparma.query` 查询并获得结果，可用 `Query.available` 判断是否查询成功，`Query.result` 获取查询结果
+
+**Alconna** 默认依赖注入的目标参数皆不需要使用依赖注入函数， 该效果对于 `AlconnaMatcher.got_path` 下的 Arg 同样有效:
+
+```python
+async def handle(
+    result: CommandResult,
+    arp: Arparma,
+    dup: Duplication,
+    source: Alconna,
+    abc: str,  # 类似 Match, 但是若匹配结果不存在对应字段则跳过该 handler
+    foo: Match[str],
+    bar: Query[int] = Query("ttt.bar", 0)  # Query 仍然需要一个默认值来传递 path 参数
+):
+    ...
+```
+
+如果你更喜欢 Depends 式的依赖注入，`nonebot_plugin_alconna` 同时提供了一系列的依赖注入函数，他们包括：
+
+- `AlconnaResult`: `CommandResult` 类型的依赖注入函数
+- `AlconnaMatches`: `Arparma` 类型的依赖注入函数
+- `AlconnaDuplication`: `Duplication` 类型的依赖注入函数
+- `AlconnaMatch`: `Match` 类型的依赖注入函数，其能够额外传入一个 middleware 函数来处理得到的参数
+- `AlconnaQuery`: `Query` 类型的依赖注入函数，其能够额外传入一个 middleware 函数来处理得到的参数
+- `AlconnaExecResult`: 提供挂载在命令上的 callback 的返回结果 (`Dict[str, Any]`) 的依赖注入函数
+- `AlconnaExtension`: 提供指定类型的 `Extension` 的依赖注入函数
+
+实例:
+
+```python
+from nonebot import require
+require("nonebot_plugin_alconna")
+
+from nonebot_plugin_alconna import (
+    on_alconna, 
+    Match,
+    Query,
+    AlconnaMatch,
+    AlcResult
+)
+from arclet.alconna import Alconna, Args, Option, Arparma
+
+
+test = on_alconna(
+    Alconna(
+        "test",
+        Option("foo", Args["bar", int]),
+        Option("baz", Args["qux", bool, False])
+    ),
+    auto_send_output=True
+)
+
+@test.handle()
+async def handle_test1(result: AlcResult):
+    await test.send(f"matched: {result.matched}")
+    await test.send(f"maybe output: {result.output}")
+
+@test.handle()
+async def handle_test2(result: Arparma):
+    await test.send(f"head result: {result.header_result}")
+    await test.send(f"args: {result.all_matched_args}")
+
+@test.handle()
+async def handle_test3(bar: Match[int] = AlconnaMatch("bar")):
+    if bar.available:    
+        await test.send(f"foo={bar.result}")
+
+@test.handle()
+async def handle_test4(qux: Query[bool] = Query("baz.qux", False)):
+    if qux.available:
+        await test.send(f"baz.qux={qux.result}")
+```
+
+
+### 跨平台适配
+
+`uniseg` 模块属于 `nonebot-plugin-alconna` 的子插件，其提供了一套通用的消息组件，用于在 `nonebot-plugin-alconna` 下构建通用消息.
+
+#### 通用消息段
+
+适配器下的消息段标注会匹配适配器特定的 `MessageSegment`, 而通用消息段与适配器消息段的区别在于:  
+
+通用消息段会匹配多个适配器中相似类型的消息段，并返回 `uniseg` 模块中定义的 [`Segment` 模型](https://nonebot.dev/docs/next/best-practice/alconna/utils#%E9%80%9A%E7%94%A8%E6%B6%88%E6%81%AF%E6%AE%B5), 以达到**跨平台接收消息**的作用
+
+`uniseg` 模块提供了类似 `MessageSegment` 的通用消息段，并可在 `Alconna` 下直接标注使用：
+
+```python
+class Segment:
+    """基类标注"""
+
+class Text(Segment):
+    """Text对象, 表示一类文本元素"""
+    text: str
+    style: Optional[str]
+
+class At(Segment):
+    """At对象, 表示一类提醒某用户的元素"""
+    type: Literal["user", "role", "channel"]
+    target: str
+
+class AtAll(Segment):
+    """AtAll对象, 表示一类提醒所有人的元素"""
+
+class Emoji(Segment):
+    """Emoji对象, 表示一类表情元素"""
+    id: str
+    name: Optional[str]
+
+class Media(Segment):
+    url: Optional[str]
+    id: Optional[str]
+    path: Optional[str]
+    raw: Optional[bytes]
+
+class Image(Media):
+    """Image对象, 表示一类图片元素"""
+
+class Audio(Media):
+    """Audio对象, 表示一类音频元素"""
+
+class Voice(Media):
+    """Voice对象, 表示一类语音元素"""
+
+class Video(Media):
+    """Video对象, 表示一类视频元素"""
+
+class File(Segment):
+    """File对象, 表示一类文件元素"""
+    id: str
+    name: Optional[str]
+
+class Reply(Segment):
+    """Reply对象，表示一类回复消息"""
+    id: str
+    """此处不一定是消息ID，可能是其他ID，如消息序号等"""
+    msg: Optional[Union[Message, str]]
+    origin: Optional[Any]
+
+class Reference(Segment):
+    """Reference对象，表示一类引用消息。转发消息 (Forward) 也属于此类"""
+    id: Optional[str]
+    """此处不一定是消息ID，可能是其他ID，如消息序号等"""
+    content: Optional[Union[Message, str, List[Union[RefNode, CustomNode]]]]
+
+class Card(Segment):
+    type: Literal["xml", "json"]
+    raw: str
+
+class Other(Segment):
+    """其他 Segment"""
+```
+此类消息段通过 `UniMessage.export` 可以转为特定的 `MessageSegment`.
+
+
+#### 通用信息序列
+
+`uniseg` 模块还提供了一个类似于 `Message` 的 `UniMessage` 类型，其元素为经过通用标注转换后的通用消息段.
+
+你可以通过提供的 `UniversalMessage` 或 `UniMsg` 依赖注入器来获取 `UniMessage`.
+
+```python
+from nonebot_plugin_alconna.uniseg import UniMsg, At, Reply
+
+
+matcher = on_xxx(...)
+
+@matcher.handle()
+async def _(msg: UniMsg):
+    reply = msg[Reply, 0]
+    print(reply.origin)
+    if msg.has(At):
+        ats = msg.get(At)
+        print(ats)
+    ...
+```
+
+还可以通过 `UniMessage` 的 `export` 与 `send` 方法来**跨平台发送消息**.
+
+```python
+from nonebot import Bot, on_command
+from nonebot_plugin_alconna.uniseg import Image, UniMessage
+
+
+test = on_command("test")
+
+@test.handle()
+async def handle_test():
+    await test.send(await UniMessage(Image(path="path/to/img")).export())
+```
+`UniMessage.export` 会通过传入的 `bot: Bot` 参数，或上下文中的 `Bot` 对象读取适配器信息，并使用对应的生成方法把通用消息转为适配器对应的消息序列.
+
+而在 `AlconnaMatcher` 下，`got`, `send`, `reject` 等可以发送消息的方法皆支持使用 `UniMessage`，不需要手动调用 export 方法：
+
+```python
+from arclet.alconna import Alconna, Args
+from nonebot_plugin_alconna import Match, AlconnaMatcher, on_alconna
+from nonebot_plugin_alconna.uniseg import At,  UniMessage
+
+
+test_cmd = on_alconna(Alconna("test", Args["target?", At]))
+
+@test_cmd.handle()
+async def tt_h(matcher: AlconnaMatcher, target: Match[At]):
+    if target.available:
+        matcher.set_path_arg("target", target.result)
+
+@test_cmd.got_path("target", prompt="请输入目标")
+async def tt(target: At):
+    await test_cmd.send(UniMessage([target, "\ndone."]))
+```
+
+除此之外 `UniMessage.send` 方法基于 `UniMessage.export` 并调用各适配器下的发送消息方法，返回一个 `Receipt` 对象，用于修改/撤回消息：
+
+```python
+from nonebot import Bot, on_command
+from nonebot_plugin_alconna.uniseg import UniMessage
+
+
+test = on_command("test")
+
+@test.handle()
+async def handle():
+    receipt = await UniMessage.text("hello!").send(at_sender=True, reply_to=True)
+    await receipt.recall(delay=1)
+```
+**在响应器以外的地方，`bot` 参数必须手动传入**
+
+本插件为以下设配器提供了 **Segment** 标注，可用于匹配各适配器的 `MessageSegment`，也可用于创建 `MessageSegment`:
path: nekro_agent/adapters/alconna.md
user: login: sourcery-ai[bot]
body: **issue (typo):** 错别字：'设配器' 应该是 '适配器'。

请更新文本以使用正确的术语。

```suggestion
本插件为以下适配器提供了 **Segment** 标注，可用于匹配各适配器的 `MessageSegment`，也可用于创建 `MessageSegment`:
```

<details>
<summary>Original comment in English</summary>

**issue (typo):** Typo: '设配器' should be '适配器'.

Please update the text to use the correct term.

```suggestion
本插件为以下适配器提供了 **Segment** 标注，可用于匹配各适配器的 `MessageSegment`，也可用于创建 `MessageSegment`:
```

</details>
created_at: 2025-05-24T21:36:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,1451 @@
+# Nonebot Plugin Alconna 文档
+
+本文分为三部分:
+- [`nonebot_plugin_alconna` 的介绍与使用](#插件)
+- [`Alconna` 本体的介绍与使用](#本体)(请优先阅读本体)
+- [外部参考](#references)
+
+## 插件
+### 安装
+
+```shell
+pip install nonebot-plugin-alconna
+```
+
+或
+
+```shell
+nb plugin install nonebot-plugin-alconna
+```
+
+### 展示
+
+```python
+from nonebot.adapters.onebot.v12 import Message
+from nonebot_plugin_alconna import on_alconna, AlconnaMatches, At
+from nonebot_plugin_alconna.adapters.onebot12 import Image
+from arclet.alconna import Alconna, Args, Option, Arparma
+
+
+alc = Alconna("Hello!", Option("--spec", Args["target", At]))
+hello = on_alconna(alc, auto_send_output=True)
+
+@hello.handle()
+async def _(result: Arparma = AlconnaMatches()):
+    if result.find("spec"):
+        target = result.query[At]("spec.target")
+        seed = target.target
+        await hello.finish(Message(Image(await gen_image(seed))))
+    else:
+        await hello.finish("Hello!")
+```
+
+### 响应器使用
+
+本插件基于 **Alconna** , 为 **Nonebot** 提供了一类新的事件响应器辅助函数 `on_alconna`.
+```python
+def on_alconna(
+    command: Alconna | str,
+    skip_for_unmatch: bool = True,
+    auto_send_output: bool = True,
+    aliases: set[str | tuple[str, ...]] | None = None,
+    comp_config: CompConfig | None = None,
+    extensions: list[type[Extension] | Extension] | None = None,
+    exclude_ext: list[type[Extension] | str] | None = None,
+    use_origin: bool = False,
+    use_cmd_start: bool = False,
+    use_cmd_sep: bool = False,
+    **kwargs,
+    ...,
+):
+```
+- `command`: Alconna 命令或字符串，字符串将通过 `AlconnaFormat` 转换为 Alconna 命令
+- `skip_for_unmatch`: 是否在命令不匹配时跳过该响应
+- `auto_send_output`: 是否自动发送输出信息并跳过响应
+- `aliases`: 命令别名， 作用类似于 `on_command` 中的 aliases
+- `comp_config`: 补全会话配置， 不传入则不启用补全会话
+- `extensions`: 需要加载的匹配扩展, 可以是扩展类或扩展实例
+- `exclude_ext`: 需要排除的匹配扩展, 可以是扩展类或扩展的id
+- `use_origin`: 是否使用未经 to_me 等处理过的消息
+- `use_cmd_start`: 是否使用 COMMAND_START 作为命令前缀
+- `use_cmd_sep`: 是否使用 COMMAND_SEP 作为命令分隔符
+
+其中对于 `skip_for_unmatch` 和 `auto_send_output` 的效果如下:
+
+| skip \ auto | True           | False                         |
+|-------------|----------------|-------------------------------|
+| True        | 帮助文本等自动发送      | 帮助文本等不发送                      |
+| False       | 帮助文本等和错误信息自动发送 | 帮助文本等和错误信息设置到 `CommandResult` |
+
+
+`on_alconna` 返回的是 `Matcher` 的子类 `AlconnaMatcher`，其拓展了如下方法:
+
+- `.assign(path, value, or_not)`: 用于对包含多个选项/子命令的命令的分派处理(具体请看条件控制)
+- `.got_path(path, prompt, middleware)`: 在 `got` 方法的基础上，会以 path 对应的参数为准，读取传入 message 的最后一个消息段并验证转换
+- `.set_path_arg(key, value)`, `.get_path_arg(key)`: 类似 `set_arg` 和 `got_arg`，为 `got_path` 的特化版本
+- `.reject_path(path[, prompt, fallback])`: 类似于 `reject_arg`，对应 `got_path`
+- `.dispatch`: 同样的分派处理，但是是类似 `CommandGroup` 一样返回新的 `AlconnaMatcher`
+- `.got`, `send`, `reject`, ...: 拓展了 prompt 类型，即支持使用 `UniMessage` 作为 prompt
+
+`assign`实例:
+
+```python
+from nonebot import require
+require("nonebot_plugin_alconna")
+
+from arclet.alconna import Alconna, Option, Args
+from nonebot_plugin_alconna import on_alconna, AlconnaMatch, Match, UniMessage
+
+  
+login = on_alconna(Alconna(["/"], "login", Args["password?", str], Option("-r|--recall"))) # 这里["/"]指命令前缀必须是/
+
+@login.assign("recall") # /login -r
+async def login_exit():
+    await login.finish("已退出")
+
+@login.assign("password") # /login xxx
+async def login_handle(pw: Match[str] = AlconnaMatch("password")):
+    if pw.available:
+        login.set_path_arg("password", pw.result)
+```
+
+`dispatch`每个分发设置独立的 matcher:
+
+```python
+update_cmd = pip_cmd.dispatch("install.pak", "pip")
+
+@update_cmd.handle()
+async def update(arp: CommandResult = AlconnaResult()):
+    ...
+```
+
+`got_path`类似 Nonebot2 的got, 它与 `assign`，`Match`，`Query` 等地方一样，都需要指明 `path` 参数 (即对应 Arg 验证的路径)
+
+`got_path` 会获取消息的最后一个消息段并转为 path 对应的类型，例如示例中 `target` 对应的 Arg 里要求 str 或 At，则 got 后用户输入的消息只有为 text 或 at 才能进入处理函数.
+
+实例:
+
+```python
+from nonebot_plugin_alconna import At, Match, UniMessage, on_alconna
+
+
+test_cmd = on_alconna(Alconna("test", Args["target?", Union[str, At]]))
+
+@test_cmd.handle()
+async def tt_h(target: Match[Union[str, At]]):
+    if target.available:
+        test_cmd.set_path_arg("target", target.result)
+
+@test_cmd.got_path("target", prompt="请输入目标")
+async def tt(target: Union[str, At]):
+    await test_cmd.send(UniMessage(["ok\n", target]))
+```
+
+`path`支持 ~XXX 语法，其会把 ~ 替换为可能的父级路径:
+
+```python
+ pip = Alconna(
+     "pip",
+     Subcommand(
+         "install",
+         Args["pak", str],
+         Option("--upgrade|-U"),
+         Option("--force-reinstall"),
+     ),
+     Subcommand("list", Option("--out-dated")),
+ )
+
+ pipcmd = on_alconna(pip)
+ pip_install_cmd = pipcmd.dispatch("install")
+
+
+ @pip_install_cmd.assign("~upgrade")
+ async def pip1_u(pak: Query[str] = Query("~pak")):
+     await pip_install_cmd.finish(f"pip upgrading {pak.result}...")
+```
+
+
+### Alconna的依赖注入
+
+本插件提供了一系列依赖注入函数，便于在响应函数中获取解析结果:
+
+- `AlconnaResult`: `CommandResult` 类型的依赖注入函数
+- `AlconnaMatches`: `Arparma` 类型的依赖注入函数
+- `AlconnaDuplication`: `Duplication` 类型的依赖注入函数
+- `AlconnaMatch`: `Match` 类型的依赖注入函数
+- `AlconnaQuery`: `Query` 类型的依赖注入函数
+
+同时，基于 [`Annotated` 支持](https://github.com/nonebot/nonebot2/pull/1832), 添加了两类注解:
+
+- `AlcMatches`：同 `AlconnaMatches`
+- `AlcResult`：同 `AlconnaResult`
+
+可以看到，本插件提供了几类额外的模型:
+
+- `CommandResult`: 解析结果，包括了源命令 `source: Alconna` ，解析结果 `result: Arparma`，以及可能的输出信息 `output: str | None` 字段
+- `Match`: 匹配项，表示参数是否存在于 `all_matched_args` 内，可用 `Match.available` 判断是否匹配，`Match.result` 获取匹配的值
+- `Query`: 查询项，表示参数是否可由 `Arparma.query` 查询并获得结果，可用 `Query.available` 判断是否查询成功，`Query.result` 获取查询结果
+
+**Alconna** 默认依赖注入的目标参数皆不需要使用依赖注入函数， 该效果对于 `AlconnaMatcher.got_path` 下的 Arg 同样有效:
+
+```python
+async def handle(
+    result: CommandResult,
+    arp: Arparma,
+    dup: Duplication,
+    source: Alconna,
+    abc: str,  # 类似 Match, 但是若匹配结果不存在对应字段则跳过该 handler
+    foo: Match[str],
+    bar: Query[int] = Query("ttt.bar", 0)  # Query 仍然需要一个默认值来传递 path 参数
+):
+    ...
+```
+
+如果你更喜欢 Depends 式的依赖注入，`nonebot_plugin_alconna` 同时提供了一系列的依赖注入函数，他们包括：
+
+- `AlconnaResult`: `CommandResult` 类型的依赖注入函数
+- `AlconnaMatches`: `Arparma` 类型的依赖注入函数
+- `AlconnaDuplication`: `Duplication` 类型的依赖注入函数
+- `AlconnaMatch`: `Match` 类型的依赖注入函数，其能够额外传入一个 middleware 函数来处理得到的参数
+- `AlconnaQuery`: `Query` 类型的依赖注入函数，其能够额外传入一个 middleware 函数来处理得到的参数
+- `AlconnaExecResult`: 提供挂载在命令上的 callback 的返回结果 (`Dict[str, Any]`) 的依赖注入函数
+- `AlconnaExtension`: 提供指定类型的 `Extension` 的依赖注入函数
+
+实例:
+
+```python
+from nonebot import require
+require("nonebot_plugin_alconna")
+
+from nonebot_plugin_alconna import (
+    on_alconna, 
+    Match,
+    Query,
+    AlconnaMatch,
+    AlcResult
+)
+from arclet.alconna import Alconna, Args, Option, Arparma
+
+
+test = on_alconna(
+    Alconna(
+        "test",
+        Option("foo", Args["bar", int]),
+        Option("baz", Args["qux", bool, False])
+    ),
+    auto_send_output=True
+)
+
+@test.handle()
+async def handle_test1(result: AlcResult):
+    await test.send(f"matched: {result.matched}")
+    await test.send(f"maybe output: {result.output}")
+
+@test.handle()
+async def handle_test2(result: Arparma):
+    await test.send(f"head result: {result.header_result}")
+    await test.send(f"args: {result.all_matched_args}")
+
+@test.handle()
+async def handle_test3(bar: Match[int] = AlconnaMatch("bar")):
+    if bar.available:    
+        await test.send(f"foo={bar.result}")
+
+@test.handle()
+async def handle_test4(qux: Query[bool] = Query("baz.qux", False)):
+    if qux.available:
+        await test.send(f"baz.qux={qux.result}")
+```
+
+
+### 跨平台适配
+
+`uniseg` 模块属于 `nonebot-plugin-alconna` 的子插件，其提供了一套通用的消息组件，用于在 `nonebot-plugin-alconna` 下构建通用消息.
+
+#### 通用消息段
+
+适配器下的消息段标注会匹配适配器特定的 `MessageSegment`, 而通用消息段与适配器消息段的区别在于:  
+
+通用消息段会匹配多个适配器中相似类型的消息段，并返回 `uniseg` 模块中定义的 [`Segment` 模型](https://nonebot.dev/docs/next/best-practice/alconna/utils#%E9%80%9A%E7%94%A8%E6%B6%88%E6%81%AF%E6%AE%B5), 以达到**跨平台接收消息**的作用
+
+`uniseg` 模块提供了类似 `MessageSegment` 的通用消息段，并可在 `Alconna` 下直接标注使用：
+
+```python
+class Segment:
+    """基类标注"""
+
+class Text(Segment):
+    """Text对象, 表示一类文本元素"""
+    text: str
+    style: Optional[str]
+
+class At(Segment):
+    """At对象, 表示一类提醒某用户的元素"""
+    type: Literal["user", "role", "channel"]
+    target: str
+
+class AtAll(Segment):
+    """AtAll对象, 表示一类提醒所有人的元素"""
+
+class Emoji(Segment):
+    """Emoji对象, 表示一类表情元素"""
+    id: str
+    name: Optional[str]
+
+class Media(Segment):
+    url: Optional[str]
+    id: Optional[str]
+    path: Optional[str]
+    raw: Optional[bytes]
+
+class Image(Media):
+    """Image对象, 表示一类图片元素"""
+
+class Audio(Media):
+    """Audio对象, 表示一类音频元素"""
+
+class Voice(Media):
+    """Voice对象, 表示一类语音元素"""
+
+class Video(Media):
+    """Video对象, 表示一类视频元素"""
+
+class File(Segment):
+    """File对象, 表示一类文件元素"""
+    id: str
+    name: Optional[str]
+
+class Reply(Segment):
+    """Reply对象，表示一类回复消息"""
+    id: str
+    """此处不一定是消息ID，可能是其他ID，如消息序号等"""
+    msg: Optional[Union[Message, str]]
+    origin: Optional[Any]
+
+class Reference(Segment):
+    """Reference对象，表示一类引用消息。转发消息 (Forward) 也属于此类"""
+    id: Optional[str]
+    """此处不一定是消息ID，可能是其他ID，如消息序号等"""
+    content: Optional[Union[Message, str, List[Union[RefNode, CustomNode]]]]
+
+class Card(Segment):
+    type: Literal["xml", "json"]
+    raw: str
+
+class Other(Segment):
+    """其他 Segment"""
+```
+此类消息段通过 `UniMessage.export` 可以转为特定的 `MessageSegment`.
+
+
+#### 通用信息序列
+
+`uniseg` 模块还提供了一个类似于 `Message` 的 `UniMessage` 类型，其元素为经过通用标注转换后的通用消息段.
+
+你可以通过提供的 `UniversalMessage` 或 `UniMsg` 依赖注入器来获取 `UniMessage`.
+
+```python
+from nonebot_plugin_alconna.uniseg import UniMsg, At, Reply
+
+
+matcher = on_xxx(...)
+
+@matcher.handle()
+async def _(msg: UniMsg):
+    reply = msg[Reply, 0]
+    print(reply.origin)
+    if msg.has(At):
+        ats = msg.get(At)
+        print(ats)
+    ...
+```
+
+还可以通过 `UniMessage` 的 `export` 与 `send` 方法来**跨平台发送消息**.
+
+```python
+from nonebot import Bot, on_command
+from nonebot_plugin_alconna.uniseg import Image, UniMessage
+
+
+test = on_command("test")
+
+@test.handle()
+async def handle_test():
+    await test.send(await UniMessage(Image(path="path/to/img")).export())
+```
+`UniMessage.export` 会通过传入的 `bot: Bot` 参数，或上下文中的 `Bot` 对象读取适配器信息，并使用对应的生成方法把通用消息转为适配器对应的消息序列.
+
+而在 `AlconnaMatcher` 下，`got`, `send`, `reject` 等可以发送消息的方法皆支持使用 `UniMessage`，不需要手动调用 export 方法：
+
+```python
+from arclet.alconna import Alconna, Args
+from nonebot_plugin_alconna import Match, AlconnaMatcher, on_alconna
+from nonebot_plugin_alconna.uniseg import At,  UniMessage
+
+
+test_cmd = on_alconna(Alconna("test", Args["target?", At]))
+
+@test_cmd.handle()
+async def tt_h(matcher: AlconnaMatcher, target: Match[At]):
+    if target.available:
+        matcher.set_path_arg("target", target.result)
+
+@test_cmd.got_path("target", prompt="请输入目标")
+async def tt(target: At):
+    await test_cmd.send(UniMessage([target, "\ndone."]))
+```
+
+除此之外 `UniMessage.send` 方法基于 `UniMessage.export` 并调用各适配器下的发送消息方法，返回一个 `Receipt` 对象，用于修改/撤回消息：
+
+```python
+from nonebot import Bot, on_command
+from nonebot_plugin_alconna.uniseg import UniMessage
+
+
+test = on_command("test")
+
+@test.handle()
+async def handle():
+    receipt = await UniMessage.text("hello!").send(at_sender=True, reply_to=True)
+    await receipt.recall(delay=1)
+```
+**在响应器以外的地方，`bot` 参数必须手动传入**
+
+本插件为以下设配器提供了 **Segment** 标注，可用于匹配各适配器的 `MessageSegment`，也可用于创建 `MessageSegment`:
+
+| 协议名称                                                                | 路径                                   |
+|---------------------------------------------------------------------|--------------------------------------|
+| [OneBot 协议](https://github.com/nonebot/adapter-onebot)              | adapters.onebot11, adapters.onebot12 |
+| [Telegram](https://github.com/nonebot/adapter-telegram)             | adapters.telegram                    |
+| [飞书](https://github.com/nonebot/adapter-feishu)                     | adapters.feishu                      |
+| [GitHub](https://github.com/nonebot/adapter-github)                 | adapters.github                      |
+| [QQ bot](https://github.com/nonebot/adapter-qq)                     | adapters.qq                          |
+| [钉钉](https://github.com/nonebot/adapter-ding)                       | adapters.ding                        |
+| [Console](https://github.com/nonebot/adapter-console)               | adapters.console                     |
+| [开黑啦](https://github.com/Tian-que/nonebot-adapter-kaiheila)         | adapters.kook                        |
+| [Mirai](https://github.com/nonebot/adapter-mirai)                   | adapters.mirai, adapters.mirai2      |
+| [Ntchat](https://github.com/JustUndertaker/adapter-ntchat)          | adapters.ntchat                      |
+| [MineCraft](https://github.com/17TheWord/nonebot-adapter-minecraft) | adapters.minecraft                   |
+| [Walle-Q](https://github.com/onebot-walle/nonebot_adapter_walleq)   | adapters.onebot12                    |
+| [Discord](https://github.com/nonebot/adapter-discord)               | adapters.discord                     |
+| [Red 协议](https://github.com/nonebot/adapter-red)                    | adapters.red                         |
+| [Satori](https://github.com/nonebot/adapter-satori)                 | adapters.satori                      |
+| [Dodo IM](https://github.com/nonebot/adapter-dodo)                  | adapters.dodo                        |
+| [Kritor](https://github.com/nonebot/adapter-kritor)                 | adapters.kritor                      |  
+| [Tailchat](https://github.com/eya46/nonebot-adapter-tailchat)       | adapters.tailchat                    |
+
+
+##### 构造
+
+类比 `Message`, `UniMessage` 可以传入单个字符串/消息段或可迭代的字符串/消息段：
+
+```python
+from nonebot_plugin_alconna.uniseg import UniMessage, At
+
+
+msg = UniMessage("Hello")
+msg1 = UniMessage(At("user", "124"))
+msg2 = UniMessage(["Hello", At("user", "124")])
+```
+
+`UniMessage` 上同时存在便捷方法，令其可以链式地添加消息段：
+
+```python
+from nonebot_plugin_alconna.uniseg import UniMessage, At, Image
+
+
+msg = UniMessage.text("Hello").at("124").image(path="/path/to/img")
+assert msg == UniMessage(
+    ["Hello", At("user", "124"), Image(path="/path/to/img")]
+)
+```
+
+##### 拼接消息
+
+`str`、`UniMessage`、`Segment` 对象之间可以直接相加，相加均会返回一个新的 `UniMessage` 对象.
+
+```python
+# 消息序列与消息段相加
+UniMessage("text") + Text("text")
+# 消息序列与字符串相加
+UniMessage([Text("text")]) + "text"
+# 消息序列与消息序列相加
+UniMessage("text") + UniMessage([Text("text")])
+# 字符串与消息序列相加
+"text" + UniMessage([Text("text")])
+# 消息段与消息段相加
+Text("text") + Text("text")
+# 消息段与字符串相加
+Text("text") + "text"
+# 消息段与消息序列相加
+Text("text") + UniMessage([Text("text")])
+# 字符串与消息段相加
+"text" + Text("text")
+```
+
+如果需要在当前消息序列后直接拼接新的消息段，可以使用 `Message.append`、`Message.extend` 方法，或者使用自加.
+
+```python
+msg = UniMessage([Text("text")])
+# 自加
+msg += "text"
+msg += Text("text")
+msg += UniMessage([Text("text")])
+# 附加
+msg.append(Text("text"))
+# 扩展
+msg.extend([Text("text")])
+```
+
+##### 使用消息模板
+
+`UniMessage.template` 同样类似于 `Message.template`，可以用于格式化消息。大体用法参考 [消息模板](https://nonebot.dev/docs/next/tutorial/message#%E4%BD%BF%E7%94%A8%E6%B6%88%E6%81%AF%E6%A8%A1%E6%9D%BF).
+
+这里额外说明 `UniMessage.template` 的拓展控制符.
+
+相比 `Message`，UniMessage 对于 {:XXX} 做了另一类拓展。其能够识别例如 At(xxx, yyy) 或 Emoji(aaa, bbb)的字符串并执行.
+
+以 At(...) 为例使用通用消息段的拓展控制符：
+
+```python
+>>> from nonebot_plugin_alconna.uniseg import UniMessage
+>>>  UniMessage.template("{:At(user, target)}").format(target="123")
+UniMessage(At("user", "123"))
+>>> UniMessage.template("{:At(type=user, target=id)}").format(id="123")
+UniMessage(At("user", "123"))
+>>> UniMessage.template("{:At(type=user, target=123)}").format()
+UniMessage(At("user", "123"))
+```
+
+而在 `AlconnaMatcher` 中，{:XXX} 更进一步地提供了获取 `event` 和 `bot` 中的属性的功能.
+
+在AlconnaMatcher中使用通用消息段的拓展控制符:
+
+```python
+from arclet.alconna import Alconna, Args
+from nonebot_plugin_alconna import At, Match, UniMessage, AlconnaMatcher, on_alconna
+
+
+test_cmd = on_alconna(Alconna("test", Args["target?", At]))
+
+@test_cmd.handle()
+async def tt_h(matcher: AlconnaMatcher, target: Match[At]):
+    if target.available:
+        matcher.set_path_arg("target", target.result)
+
+@test_cmd.got_path(
+    "target",
+    prompt=UniMessage.template("{:At(user, $event.get_user_id())} 请确认目标")
+)
+async def tt():
+    await test_cmd.send(
+      UniMessage.template("{:At(user, $event.get_user_id())} 已确认目标为 {target}")
+    )
+```
+另外也有 `$message_id` 与 `$target` 两个特殊值
+
+##### 检查消息段
+
+我们可以通过 `in` 运算符或消息序列的 `has` 方法来：
+
+```python
+# 是否存在消息段
+At("user", "1234") in message
+# 是否存在指定类型的消息段
+At in message
+```
+
+我们还可以使用 `only` 方法来检查消息中是否仅包含指定的消息段。
+
+```python
+# 是否都为 "test"
+message.only("test")
+# 是否仅包含指定类型的消息段
+message.only(Text)
+```
+
+##### 获取消息纯文本
+
+类似于 `Message.extract_plain_text()`，用于获取通用消息的纯文本.
+```python 
+from nonebot_plugin_alconna.uniseg import UniMessage, At
+
+
+# 提取消息纯文本字符串
+assert UniMessage(
+    [At("user", "1234"), "text"]
+).extract_plain_text() == "text"
+```
+
+##### 遍历
+
+通用消息序列继承自 `List[Segment]` ，因此可以使用 `for` 循环遍历消息段。
+
+```python
+for segment in message:  # type: Segment
+	...
+```
+
+##### 过滤、索引与切片
+
+消息序列对列表的索引与切片进行了增强，在原有列表 `int` 索引与 `slice` 切片的基础上，支持 `type` 过滤索引与切片.
+
+```python
+from nonebot_plugin_alconna.uniseg import UniMessage, At, Text, Reply
+
+
+message = UniMessage(
+    [
+        Reply(...),
+        "text1",
+        At("user", "1234"),
+        "text2"
+    ]
+)
+# 索引
+message[0] == Reply(...)
+# 切片
+message[0:2] == UniMessage([Reply(...), Text("text1")])
+# 类型过滤
+message[At] == Message([At("user", "1234")])
+# 类型索引
+message[At, 0] == At("user", "1234")
+# 类型切片
+message[Text, 0:2] == UniMessage([Text("text1"), Text("text2")])
+```
+
+我们也可以通过消息序列的 `include`、`exclude` 方法进行类型过滤.
+
+```python 
+message.include(Text, At)  
+message.exclude(Reply)
+```
+
+同样的，消息序列对列表的 `index`、`count` 方法也进行了增强，可以用于索引指定类型的消息段.
+
+```python
+# 指定类型首个消息段索引
+message.index(Text) == 1
+# 指定类型消息段数量
+message.count(Text) == 2
+```
+
+此外，消息序列添加了一个 `get` 方法，可以用于获取指定类型指定个数的消息段.
+
+```python
+# 获取指定类型指定个数的消息段
+message.get(Text, 1) == UniMessage([Text("test1")])
+```
+
+##### 消息发送
+
+通用消息可用 `UniMessage.send` 发送自身：
+
+```python
+async def send(
+    self,
+    target: Union[Event, Target, None] = None,
+    bot: Optional[Bot] = None,
+    fallback: bool = True,
+    at_sender: Union[str, bool] = False,
+    reply_to: Union[str, bool] = False,
+) -> Receipt:
+```
+
+实际上，`UniMessage` 同时提供了获取消息事件 id 与消息发送对象的方法:
+
+```python
+from nonebot import Event, Bot
+from nonebot_plugin_alconna.uniseg import Target, get_message_id, get_target
+
+
+matcher = on_xxx(...)
+
+@matcher.handle()
+async def _(bot: Bot, event: Event):
+    target: Target = get_target(event, bot)
+    msg_id: str = get_message_id(event, bot)
+
+```
+
+`send`, `get_target`, `get_message_id` 中与 `event`, `bot` 相关的参数都会尝试从上下文中获取对象.
+
+其中，`Target`:
+
+```python
+class Target:
+    id: str
+    """目标id；若为群聊则为group_id或者channel_id，若为私聊则为user_id"""
+    parent_id: str = ""
+    """父级id；若为频道则为guild_id，其他情况为空字符串"""
+    channel: bool = False
+    """是否为频道，仅当目标平台同时支持群聊和频道时有效"""
+    private: bool = False
+    """是否为私聊"""
+    source: str = ""
+    """可能的事件id"""
+```
+是用来描述响应消息时的发送对象.
+
+同样的，你可以通过依赖注入的方式在响应器中直接获取它们.
+
+### 条件控制
+
+本插件可以通过 `assign` 来控制一个具体的响应函数是否在不满足条件时跳过响应
+
+```python
+from nonebot import require  
+require("nonebot_plugin_alconna")  
+...  
+  
+from arclet.alconna import Alconna, Subcommand, Option, Args  
+from nonebot_plugin_alconna import on_alconna, CommandResult  
+  
+pip = Alconna(  
+"pip",  
+Subcommand(  
+"install", Args["pak", str],  
+Option("--upgrade"),  
+Option("--force-reinstall")  
+),  
+Subcommand("list", Option("--out-dated"))  
+)  
+  
+pip_cmd = on_alconna(pip)  
+  
+# 仅在命令为 `pip install pip` 时响应  
+@pip_cmd.assign("install.pak", "pip")  
+async def update(res: CommandResult):  
+...  
+  
+# 仅在命令为 `pip list` 时响应  
+@pip_cmd.assign("list")  
+async def list_(res: CommandResult):  
+...  
+  
+# 在命令为 `pip install xxx` 时响应  
+@pip_cmd.assign("install")  
+async def install(res: CommandResult):  
+...
+```
+
+
+### 响应器创建装饰
+
+本插件提供了一个 `funcommand` 装饰器, 其用于将一个接受任意参数， 返回 `str` 或 `Message` 或 `MessageSegment` 的函数转换为命令响应器.
+
+```python
+from nonebot_plugin_alconna import funcommand
+
+
+@funcommand()
+async def echo(msg: str):
+    return msg
+```
+
+其等同于
+
+```python
+from arclet.alconna import Alconna, Args
+from nonebot_plugin_alconna import on_alconna, AlconnaMatch, Match
+
+
+echo = on_alconna(Alconna("echo", Args["msg", str]))
+
+@echo.handle()
+async def echo_exit(msg: Match[str] = AlconnaMatch("msg")):
+    await echo.finish(msg.result)
+
+```
+
+### 类Koishi构造器
+
+本插件提供了一个 `Command` 构造器，其基于 `arclet.alconna.tools` 中的 `AlconnaString`， 以类似 `Koishi` 中注册命令的方式来构建一个 **AlconnaMatcher** :
+
+```python
+from nonebot_plugin_alconna import Command, Arparma
+
+
+book = (
+    Command("book", "测试")
+    .option("writer", "-w <id:int>")
+    .option("writer", "--anonymous", {"id": 0})
+    .usage("book [-w <id:int> | --anonymous]")
+    .shortcut("测试", {"args": ["--anonymous"]})
+    .build()
+)
+
+@book.handle()
+async def _(arp: Arparma):
+    await book.send(str(arp.options))
+```
+
+甚至，你可以设置 `action` 来设定响应行为：
+
+```python
+book = (
+    Command("book", "测试")
+    .option("writer", "-w <id:int>")
+    .option("writer", "--anonymous", {"id": 0})
+    .usage("book [-w <id:int> | --anonymous]")
+    .shortcut("测试", {"args": ["--anonymous"]})
+    .action(lambda options: str(options))  # 会自动通过 bot.send 发送
+    .build()
+)
+```
+
+### 返回值回调
+
+在 `AlconnaMatch`, `AlconnaQuery` 或 `got_path` 中，你可以使用 `middleware` 参数来传入一个对返回值进行处理的函数:
+
+```python
+from nonebot_plugin_alconna import image_fetch
+
+
+mask_cmd = on_alconna(
+    Alconna("search", Args["img?", Image]),
+)
+
+
+@mask_cmd.handle()
+async def mask_h(matcher: AlconnaMatcher, img: Match[bytes] = AlconnaMatch("img", image_fetch)):
+    result = await search_img(img.result)
+    await matcher.send(result.content)
+```
+
+其中，`image_fetch` 是一个中间件，其接受一个 `Image` 对象，并提取图片的二进制数据返回.
+
+
+### 匹配拓展
+
+本插件提供了一个 `Extension` 类，其用于自定义 AlconnaMatcher 的部分行为.
+
+例如 `LLMExtension` (仅举例)：
+
+```python
+from nonebot_plugin_alconna import Extension, Alconna, on_alconna, Interface
+
+
+class LLMExtension(Extension):
+    @property
+    def priority(self) -> int:
+        return 10
+
+    @property
+    def id(self) -> str:
+        return "LLMExtension"
+
+    def __init__(self, llm):
+      self.llm = llm
+
+    def post_init(self, alc: Alconna) -> None:
+        self.llm.add_context(alc.command, alc.meta.description)
+
+    async def receive_wrapper(self, bot, event, receive):
+        resp = await self.llm.input(str(receive))
+        return receive.__class__(resp.content)
+
+    def before_catch(self, name, annotation, default):
+        return name == "llm"
+
+    def catch(self, interface: Interface):
+        if interface.name == "llm":
+            return self.llm
+
+matcher = on_alconna(
+    Alconna(...),
+    extensions=[LLMExtension(LLM)]
+)
+...
+```
+
+那么添加了 `LLMExtension` 的响应器便能接受任何能通过 llm 翻译为具体命令的自然语言消息，同时可以在响应器中为所有 `llm` 参数注入模型变量
+
+目前 `Extension` 的功能有:
+
+- `validate`: 对于事件的来源适配器或 bot 选择是否接受响应
+- `output_converter`: 输出信息的自定义转换方法
+- `message_provider`: 从传入事件中自定义提取消息的方法
+- `receive_provider`: 对传入的消息 (Message 或 UniMessage) 的额外处理
+- `permission_check`: 命令对消息解析并确认头部匹配（即确认选择响应）时对发送者的权限判断
+- `parse_wrapper`: 对命令解析结果的额外处理
+- `send_wrapper`: 对发送的消息 (Message 或 UniMessage) 的额外处理
+- `before_catch`: 自定义依赖注入的绑定确认函数
+- `catch`: 自定义依赖注入处理函数
+- `post_init`: 响应器创建后对命令对象的额外处理
+
+例如内置的 `DiscordSlashExtension`，其可自动将 Alconna 对象翻译成 slash 指令并注册，且将收到的指令交互事件转为指令供命令解析:
+
+```python
+from nonebot_plugin_alconna import Match, on_alconna
+from nonebot_plugin_alconna.adapters.discord import DiscordSlashExtension
+
+
+alc = Alconna(
+    ["/"],
+    "permission",
+    Subcommand("add", Args["plugin", str]["priority?", int]),
+    Option("remove", Args["plugin", str]["time?", int]),
+    meta=CommandMeta(description="权限管理"),
+)
+
+matcher = on_alconna(alc, extensions=[DiscordSlashExtension()])
+
+@matcher.assign("add")
+async def add(plugin: Match[str], priority: Match[int]):
+    await matcher.finish(f"added {plugin.result} with {priority.result if priority.available else 0}")
+
+@matcher.assign("remove")
+async def remove(plugin: Match[str], time: Match[int]):
+    await matcher.finish(f"removed {plugin.result} with {time.result if time.available else -1}")
+```
+
+TIP:  
+全局的 Extension 可延迟加载 (即若有全局拓展加载于部分 AlconnaMatcher 之后，这部分响应器会被追加拓展)
+
+
+## 本体
+[`Alconna`](https://github.com/ArcletProject/Alconna) 隶属于 `ArcletProject`，是一个简单、灵活、高效的命令参数解析器, 并且不局限于解析命令式字符串.
+
+我们通过一个例子来讲解 **Alconna** 的核心: `Args`, `Subcommand`, `Option`:
+
+```python
+from arclet.alconna import Alconna, Args, Subcommand, Option
+
+
+alc = Alconna(
+    "pip",
+    Subcommand(
+        "install",
+        Args["package", str],
+        Option("-r|--requirement", Args["file", str]),
+        Option("-i|--index-url", Args["url", str]),
+    )
+)
+
+res = alc.parse("pip install nonebot2 -i URL")
+
+print(res)
+# matched=True, header_match=(origin='pip' result='pip' matched=True groups={}), subcommands={'install': (value=Ellipsis args={'package': 'nonebot2'} options={'index-url': (value=None args={'url': 'URL'})} subcommands={})}, other_args={'package': 'nonebot2', 'url': 'URL'} 
+
+print(res.all_matched_args)
+# {'package': 'nonebot2', 'url': 'URL'}
+```
+
+这段代码通过`Alconna`创捷了一个接受主命令名为`pip`, 子命令为`install`且子命令接受一个 **Args** 参数`package`和二个 **Option** 参数`-r`和`-i`的命令参数解析器, 通过`parse`方法返回解析结果 **Arparma** 的实例.
path: nekro_agent/adapters/alconna.md
user: login: sourcery-ai[bot]
body: **issue (typo):** 错别字：'创捷' 应该是 '创建'。

请更新文本以使用 '创建' 而不是 '创捷'。

```suggestion
+这段代码通过`Alconna`创建了一个接受主命令名为`pip`, 子命令为`install`且子命令接受一个 **Args** 参数`package`和二个 **Option** 参数`-r`和`-i`的命令参数解析器, 通过`parse`方法返回解析结果 **Arparma** 的实例.
```

<details>
<summary>Original comment in English</summary>

**issue (typo):** Typo: '创捷' should be '创建'.

Please update the text to use '创建' instead of '创捷'.

```suggestion
+这段代码通过`Alconna`创建了一个接受主命令名为`pip`, 子命令为`install`且子命令接受一个 **Args** 参数`package`和二个 **Option** 参数`-r`和`-i`的命令参数解析器, 通过`parse`方法返回解析结果 **Arparma** 的实例.
```

</details>
created_at: 2025-05-24T21:36:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,1451 @@
+# Nonebot Plugin Alconna 文档
+
+本文分为三部分:
+- [`nonebot_plugin_alconna` 的介绍与使用](#插件)
+- [`Alconna` 本体的介绍与使用](#本体)(请优先阅读本体)
+- [外部参考](#references)
+
+## 插件
+### 安装
+
+```shell
+pip install nonebot-plugin-alconna
+```
+
+或
+
+```shell
+nb plugin install nonebot-plugin-alconna
+```
+
+### 展示
+
+```python
+from nonebot.adapters.onebot.v12 import Message
+from nonebot_plugin_alconna import on_alconna, AlconnaMatches, At
+from nonebot_plugin_alconna.adapters.onebot12 import Image
+from arclet.alconna import Alconna, Args, Option, Arparma
+
+
+alc = Alconna("Hello!", Option("--spec", Args["target", At]))
+hello = on_alconna(alc, auto_send_output=True)
+
+@hello.handle()
+async def _(result: Arparma = AlconnaMatches()):
+    if result.find("spec"):
+        target = result.query[At]("spec.target")
+        seed = target.target
+        await hello.finish(Message(Image(await gen_image(seed))))
+    else:
+        await hello.finish("Hello!")
+```
+
+### 响应器使用
+
+本插件基于 **Alconna** , 为 **Nonebot** 提供了一类新的事件响应器辅助函数 `on_alconna`.
+```python
+def on_alconna(
+    command: Alconna | str,
+    skip_for_unmatch: bool = True,
+    auto_send_output: bool = True,
+    aliases: set[str | tuple[str, ...]] | None = None,
+    comp_config: CompConfig | None = None,
+    extensions: list[type[Extension] | Extension] | None = None,
+    exclude_ext: list[type[Extension] | str] | None = None,
+    use_origin: bool = False,
+    use_cmd_start: bool = False,
+    use_cmd_sep: bool = False,
+    **kwargs,
+    ...,
+):
+```
+- `command`: Alconna 命令或字符串，字符串将通过 `AlconnaFormat` 转换为 Alconna 命令
+- `skip_for_unmatch`: 是否在命令不匹配时跳过该响应
+- `auto_send_output`: 是否自动发送输出信息并跳过响应
+- `aliases`: 命令别名， 作用类似于 `on_command` 中的 aliases
+- `comp_config`: 补全会话配置， 不传入则不启用补全会话
+- `extensions`: 需要加载的匹配扩展, 可以是扩展类或扩展实例
+- `exclude_ext`: 需要排除的匹配扩展, 可以是扩展类或扩展的id
+- `use_origin`: 是否使用未经 to_me 等处理过的消息
+- `use_cmd_start`: 是否使用 COMMAND_START 作为命令前缀
+- `use_cmd_sep`: 是否使用 COMMAND_SEP 作为命令分隔符
+
+其中对于 `skip_for_unmatch` 和 `auto_send_output` 的效果如下:
+
+| skip \ auto | True           | False                         |
+|-------------|----------------|-------------------------------|
+| True        | 帮助文本等自动发送      | 帮助文本等不发送                      |
+| False       | 帮助文本等和错误信息自动发送 | 帮助文本等和错误信息设置到 `CommandResult` |
+
+
+`on_alconna` 返回的是 `Matcher` 的子类 `AlconnaMatcher`，其拓展了如下方法:
+
+- `.assign(path, value, or_not)`: 用于对包含多个选项/子命令的命令的分派处理(具体请看条件控制)
+- `.got_path(path, prompt, middleware)`: 在 `got` 方法的基础上，会以 path 对应的参数为准，读取传入 message 的最后一个消息段并验证转换
+- `.set_path_arg(key, value)`, `.get_path_arg(key)`: 类似 `set_arg` 和 `got_arg`，为 `got_path` 的特化版本
+- `.reject_path(path[, prompt, fallback])`: 类似于 `reject_arg`，对应 `got_path`
+- `.dispatch`: 同样的分派处理，但是是类似 `CommandGroup` 一样返回新的 `AlconnaMatcher`
+- `.got`, `send`, `reject`, ...: 拓展了 prompt 类型，即支持使用 `UniMessage` 作为 prompt
+
+`assign`实例:
+
+```python
+from nonebot import require
+require("nonebot_plugin_alconna")
+
+from arclet.alconna import Alconna, Option, Args
+from nonebot_plugin_alconna import on_alconna, AlconnaMatch, Match, UniMessage
+
+  
+login = on_alconna(Alconna(["/"], "login", Args["password?", str], Option("-r|--recall"))) # 这里["/"]指命令前缀必须是/
+
+@login.assign("recall") # /login -r
+async def login_exit():
+    await login.finish("已退出")
+
+@login.assign("password") # /login xxx
+async def login_handle(pw: Match[str] = AlconnaMatch("password")):
+    if pw.available:
+        login.set_path_arg("password", pw.result)
+```
+
+`dispatch`每个分发设置独立的 matcher:
+
+```python
+update_cmd = pip_cmd.dispatch("install.pak", "pip")
+
+@update_cmd.handle()
+async def update(arp: CommandResult = AlconnaResult()):
+    ...
+```
+
+`got_path`类似 Nonebot2 的got, 它与 `assign`，`Match`，`Query` 等地方一样，都需要指明 `path` 参数 (即对应 Arg 验证的路径)
+
+`got_path` 会获取消息的最后一个消息段并转为 path 对应的类型，例如示例中 `target` 对应的 Arg 里要求 str 或 At，则 got 后用户输入的消息只有为 text 或 at 才能进入处理函数.
+
+实例:
+
+```python
+from nonebot_plugin_alconna import At, Match, UniMessage, on_alconna
+
+
+test_cmd = on_alconna(Alconna("test", Args["target?", Union[str, At]]))
+
+@test_cmd.handle()
+async def tt_h(target: Match[Union[str, At]]):
+    if target.available:
+        test_cmd.set_path_arg("target", target.result)
+
+@test_cmd.got_path("target", prompt="请输入目标")
+async def tt(target: Union[str, At]):
+    await test_cmd.send(UniMessage(["ok\n", target]))
+```
+
+`path`支持 ~XXX 语法，其会把 ~ 替换为可能的父级路径:
+
+```python
+ pip = Alconna(
+     "pip",
+     Subcommand(
+         "install",
+         Args["pak", str],
+         Option("--upgrade|-U"),
+         Option("--force-reinstall"),
+     ),
+     Subcommand("list", Option("--out-dated")),
+ )
+
+ pipcmd = on_alconna(pip)
+ pip_install_cmd = pipcmd.dispatch("install")
+
+
+ @pip_install_cmd.assign("~upgrade")
+ async def pip1_u(pak: Query[str] = Query("~pak")):
+     await pip_install_cmd.finish(f"pip upgrading {pak.result}...")
+```
+
+
+### Alconna的依赖注入
+
+本插件提供了一系列依赖注入函数，便于在响应函数中获取解析结果:
+
+- `AlconnaResult`: `CommandResult` 类型的依赖注入函数
+- `AlconnaMatches`: `Arparma` 类型的依赖注入函数
+- `AlconnaDuplication`: `Duplication` 类型的依赖注入函数
+- `AlconnaMatch`: `Match` 类型的依赖注入函数
+- `AlconnaQuery`: `Query` 类型的依赖注入函数
+
+同时，基于 [`Annotated` 支持](https://github.com/nonebot/nonebot2/pull/1832), 添加了两类注解:
+
+- `AlcMatches`：同 `AlconnaMatches`
+- `AlcResult`：同 `AlconnaResult`
+
+可以看到，本插件提供了几类额外的模型:
+
+- `CommandResult`: 解析结果，包括了源命令 `source: Alconna` ，解析结果 `result: Arparma`，以及可能的输出信息 `output: str | None` 字段
+- `Match`: 匹配项，表示参数是否存在于 `all_matched_args` 内，可用 `Match.available` 判断是否匹配，`Match.result` 获取匹配的值
+- `Query`: 查询项，表示参数是否可由 `Arparma.query` 查询并获得结果，可用 `Query.available` 判断是否查询成功，`Query.result` 获取查询结果
+
+**Alconna** 默认依赖注入的目标参数皆不需要使用依赖注入函数， 该效果对于 `AlconnaMatcher.got_path` 下的 Arg 同样有效:
+
+```python
+async def handle(
+    result: CommandResult,
+    arp: Arparma,
+    dup: Duplication,
+    source: Alconna,
+    abc: str,  # 类似 Match, 但是若匹配结果不存在对应字段则跳过该 handler
+    foo: Match[str],
+    bar: Query[int] = Query("ttt.bar", 0)  # Query 仍然需要一个默认值来传递 path 参数
+):
+    ...
+```
+
+如果你更喜欢 Depends 式的依赖注入，`nonebot_plugin_alconna` 同时提供了一系列的依赖注入函数，他们包括：
+
+- `AlconnaResult`: `CommandResult` 类型的依赖注入函数
+- `AlconnaMatches`: `Arparma` 类型的依赖注入函数
+- `AlconnaDuplication`: `Duplication` 类型的依赖注入函数
+- `AlconnaMatch`: `Match` 类型的依赖注入函数，其能够额外传入一个 middleware 函数来处理得到的参数
+- `AlconnaQuery`: `Query` 类型的依赖注入函数，其能够额外传入一个 middleware 函数来处理得到的参数
+- `AlconnaExecResult`: 提供挂载在命令上的 callback 的返回结果 (`Dict[str, Any]`) 的依赖注入函数
+- `AlconnaExtension`: 提供指定类型的 `Extension` 的依赖注入函数
+
+实例:
+
+```python
+from nonebot import require
+require("nonebot_plugin_alconna")
+
+from nonebot_plugin_alconna import (
+    on_alconna, 
+    Match,
+    Query,
+    AlconnaMatch,
+    AlcResult
+)
+from arclet.alconna import Alconna, Args, Option, Arparma
+
+
+test = on_alconna(
+    Alconna(
+        "test",
+        Option("foo", Args["bar", int]),
+        Option("baz", Args["qux", bool, False])
+    ),
+    auto_send_output=True
+)
+
+@test.handle()
+async def handle_test1(result: AlcResult):
+    await test.send(f"matched: {result.matched}")
+    await test.send(f"maybe output: {result.output}")
+
+@test.handle()
+async def handle_test2(result: Arparma):
+    await test.send(f"head result: {result.header_result}")
+    await test.send(f"args: {result.all_matched_args}")
+
+@test.handle()
+async def handle_test3(bar: Match[int] = AlconnaMatch("bar")):
+    if bar.available:    
+        await test.send(f"foo={bar.result}")
+
+@test.handle()
+async def handle_test4(qux: Query[bool] = Query("baz.qux", False)):
+    if qux.available:
+        await test.send(f"baz.qux={qux.result}")
+```
+
+
+### 跨平台适配
+
+`uniseg` 模块属于 `nonebot-plugin-alconna` 的子插件，其提供了一套通用的消息组件，用于在 `nonebot-plugin-alconna` 下构建通用消息.
+
+#### 通用消息段
+
+适配器下的消息段标注会匹配适配器特定的 `MessageSegment`, 而通用消息段与适配器消息段的区别在于:  
+
+通用消息段会匹配多个适配器中相似类型的消息段，并返回 `uniseg` 模块中定义的 [`Segment` 模型](https://nonebot.dev/docs/next/best-practice/alconna/utils#%E9%80%9A%E7%94%A8%E6%B6%88%E6%81%AF%E6%AE%B5), 以达到**跨平台接收消息**的作用
+
+`uniseg` 模块提供了类似 `MessageSegment` 的通用消息段，并可在 `Alconna` 下直接标注使用：
+
+```python
+class Segment:
+    """基类标注"""
+
+class Text(Segment):
+    """Text对象, 表示一类文本元素"""
+    text: str
+    style: Optional[str]
+
+class At(Segment):
+    """At对象, 表示一类提醒某用户的元素"""
+    type: Literal["user", "role", "channel"]
+    target: str
+
+class AtAll(Segment):
+    """AtAll对象, 表示一类提醒所有人的元素"""
+
+class Emoji(Segment):
+    """Emoji对象, 表示一类表情元素"""
+    id: str
+    name: Optional[str]
+
+class Media(Segment):
+    url: Optional[str]
+    id: Optional[str]
+    path: Optional[str]
+    raw: Optional[bytes]
+
+class Image(Media):
+    """Image对象, 表示一类图片元素"""
+
+class Audio(Media):
+    """Audio对象, 表示一类音频元素"""
+
+class Voice(Media):
+    """Voice对象, 表示一类语音元素"""
+
+class Video(Media):
+    """Video对象, 表示一类视频元素"""
+
+class File(Segment):
+    """File对象, 表示一类文件元素"""
+    id: str
+    name: Optional[str]
+
+class Reply(Segment):
+    """Reply对象，表示一类回复消息"""
+    id: str
+    """此处不一定是消息ID，可能是其他ID，如消息序号等"""
+    msg: Optional[Union[Message, str]]
+    origin: Optional[Any]
+
+class Reference(Segment):
+    """Reference对象，表示一类引用消息。转发消息 (Forward) 也属于此类"""
+    id: Optional[str]
+    """此处不一定是消息ID，可能是其他ID，如消息序号等"""
+    content: Optional[Union[Message, str, List[Union[RefNode, CustomNode]]]]
+
+class Card(Segment):
+    type: Literal["xml", "json"]
+    raw: str
+
+class Other(Segment):
+    """其他 Segment"""
+```
+此类消息段通过 `UniMessage.export` 可以转为特定的 `MessageSegment`.
+
+
+#### 通用信息序列
+
+`uniseg` 模块还提供了一个类似于 `Message` 的 `UniMessage` 类型，其元素为经过通用标注转换后的通用消息段.
+
+你可以通过提供的 `UniversalMessage` 或 `UniMsg` 依赖注入器来获取 `UniMessage`.
+
+```python
+from nonebot_plugin_alconna.uniseg import UniMsg, At, Reply
+
+
+matcher = on_xxx(...)
+
+@matcher.handle()
+async def _(msg: UniMsg):
+    reply = msg[Reply, 0]
+    print(reply.origin)
+    if msg.has(At):
+        ats = msg.get(At)
+        print(ats)
+    ...
+```
+
+还可以通过 `UniMessage` 的 `export` 与 `send` 方法来**跨平台发送消息**.
+
+```python
+from nonebot import Bot, on_command
+from nonebot_plugin_alconna.uniseg import Image, UniMessage
+
+
+test = on_command("test")
+
+@test.handle()
+async def handle_test():
+    await test.send(await UniMessage(Image(path="path/to/img")).export())
+```
+`UniMessage.export` 会通过传入的 `bot: Bot` 参数，或上下文中的 `Bot` 对象读取适配器信息，并使用对应的生成方法把通用消息转为适配器对应的消息序列.
+
+而在 `AlconnaMatcher` 下，`got`, `send`, `reject` 等可以发送消息的方法皆支持使用 `UniMessage`，不需要手动调用 export 方法：
+
+```python
+from arclet.alconna import Alconna, Args
+from nonebot_plugin_alconna import Match, AlconnaMatcher, on_alconna
+from nonebot_plugin_alconna.uniseg import At,  UniMessage
+
+
+test_cmd = on_alconna(Alconna("test", Args["target?", At]))
+
+@test_cmd.handle()
+async def tt_h(matcher: AlconnaMatcher, target: Match[At]):
+    if target.available:
+        matcher.set_path_arg("target", target.result)
+
+@test_cmd.got_path("target", prompt="请输入目标")
+async def tt(target: At):
+    await test_cmd.send(UniMessage([target, "\ndone."]))
+```
+
+除此之外 `UniMessage.send` 方法基于 `UniMessage.export` 并调用各适配器下的发送消息方法，返回一个 `Receipt` 对象，用于修改/撤回消息：
+
+```python
+from nonebot import Bot, on_command
+from nonebot_plugin_alconna.uniseg import UniMessage
+
+
+test = on_command("test")
+
+@test.handle()
+async def handle():
+    receipt = await UniMessage.text("hello!").send(at_sender=True, reply_to=True)
+    await receipt.recall(delay=1)
+```
+**在响应器以外的地方，`bot` 参数必须手动传入**
+
+本插件为以下设配器提供了 **Segment** 标注，可用于匹配各适配器的 `MessageSegment`，也可用于创建 `MessageSegment`:
+
+| 协议名称                                                                | 路径                                   |
+|---------------------------------------------------------------------|--------------------------------------|
+| [OneBot 协议](https://github.com/nonebot/adapter-onebot)              | adapters.onebot11, adapters.onebot12 |
+| [Telegram](https://github.com/nonebot/adapter-telegram)             | adapters.telegram                    |
+| [飞书](https://github.com/nonebot/adapter-feishu)                     | adapters.feishu                      |
+| [GitHub](https://github.com/nonebot/adapter-github)                 | adapters.github                      |
+| [QQ bot](https://github.com/nonebot/adapter-qq)                     | adapters.qq                          |
+| [钉钉](https://github.com/nonebot/adapter-ding)                       | adapters.ding                        |
+| [Console](https://github.com/nonebot/adapter-console)               | adapters.console                     |
+| [开黑啦](https://github.com/Tian-que/nonebot-adapter-kaiheila)         | adapters.kook                        |
+| [Mirai](https://github.com/nonebot/adapter-mirai)                   | adapters.mirai, adapters.mirai2      |
+| [Ntchat](https://github.com/JustUndertaker/adapter-ntchat)          | adapters.ntchat                      |
+| [MineCraft](https://github.com/17TheWord/nonebot-adapter-minecraft) | adapters.minecraft                   |
+| [Walle-Q](https://github.com/onebot-walle/nonebot_adapter_walleq)   | adapters.onebot12                    |
+| [Discord](https://github.com/nonebot/adapter-discord)               | adapters.discord                     |
+| [Red 协议](https://github.com/nonebot/adapter-red)                    | adapters.red                         |
+| [Satori](https://github.com/nonebot/adapter-satori)                 | adapters.satori                      |
+| [Dodo IM](https://github.com/nonebot/adapter-dodo)                  | adapters.dodo                        |
+| [Kritor](https://github.com/nonebot/adapter-kritor)                 | adapters.kritor                      |  
+| [Tailchat](https://github.com/eya46/nonebot-adapter-tailchat)       | adapters.tailchat                    |
+
+
+##### 构造
+
+类比 `Message`, `UniMessage` 可以传入单个字符串/消息段或可迭代的字符串/消息段：
+
+```python
+from nonebot_plugin_alconna.uniseg import UniMessage, At
+
+
+msg = UniMessage("Hello")
+msg1 = UniMessage(At("user", "124"))
+msg2 = UniMessage(["Hello", At("user", "124")])
+```
+
+`UniMessage` 上同时存在便捷方法，令其可以链式地添加消息段：
+
+```python
+from nonebot_plugin_alconna.uniseg import UniMessage, At, Image
+
+
+msg = UniMessage.text("Hello").at("124").image(path="/path/to/img")
+assert msg == UniMessage(
+    ["Hello", At("user", "124"), Image(path="/path/to/img")]
+)
+```
+
+##### 拼接消息
+
+`str`、`UniMessage`、`Segment` 对象之间可以直接相加，相加均会返回一个新的 `UniMessage` 对象.
+
+```python
+# 消息序列与消息段相加
+UniMessage("text") + Text("text")
+# 消息序列与字符串相加
+UniMessage([Text("text")]) + "text"
+# 消息序列与消息序列相加
+UniMessage("text") + UniMessage([Text("text")])
+# 字符串与消息序列相加
+"text" + UniMessage([Text("text")])
+# 消息段与消息段相加
+Text("text") + Text("text")
+# 消息段与字符串相加
+Text("text") + "text"
+# 消息段与消息序列相加
+Text("text") + UniMessage([Text("text")])
+# 字符串与消息段相加
+"text" + Text("text")
+```
+
+如果需要在当前消息序列后直接拼接新的消息段，可以使用 `Message.append`、`Message.extend` 方法，或者使用自加.
+
+```python
+msg = UniMessage([Text("text")])
+# 自加
+msg += "text"
+msg += Text("text")
+msg += UniMessage([Text("text")])
+# 附加
+msg.append(Text("text"))
+# 扩展
+msg.extend([Text("text")])
+```
+
+##### 使用消息模板
+
+`UniMessage.template` 同样类似于 `Message.template`，可以用于格式化消息。大体用法参考 [消息模板](https://nonebot.dev/docs/next/tutorial/message#%E4%BD%BF%E7%94%A8%E6%B6%88%E6%81%AF%E6%A8%A1%E6%9D%BF).
+
+这里额外说明 `UniMessage.template` 的拓展控制符.
+
+相比 `Message`，UniMessage 对于 {:XXX} 做了另一类拓展。其能够识别例如 At(xxx, yyy) 或 Emoji(aaa, bbb)的字符串并执行.
+
+以 At(...) 为例使用通用消息段的拓展控制符：
+
+```python
+>>> from nonebot_plugin_alconna.uniseg import UniMessage
+>>>  UniMessage.template("{:At(user, target)}").format(target="123")
+UniMessage(At("user", "123"))
+>>> UniMessage.template("{:At(type=user, target=id)}").format(id="123")
+UniMessage(At("user", "123"))
+>>> UniMessage.template("{:At(type=user, target=123)}").format()
+UniMessage(At("user", "123"))
+```
+
+而在 `AlconnaMatcher` 中，{:XXX} 更进一步地提供了获取 `event` 和 `bot` 中的属性的功能.
+
+在AlconnaMatcher中使用通用消息段的拓展控制符:
+
+```python
+from arclet.alconna import Alconna, Args
+from nonebot_plugin_alconna import At, Match, UniMessage, AlconnaMatcher, on_alconna
+
+
+test_cmd = on_alconna(Alconna("test", Args["target?", At]))
+
+@test_cmd.handle()
+async def tt_h(matcher: AlconnaMatcher, target: Match[At]):
+    if target.available:
+        matcher.set_path_arg("target", target.result)
+
+@test_cmd.got_path(
+    "target",
+    prompt=UniMessage.template("{:At(user, $event.get_user_id())} 请确认目标")
+)
+async def tt():
+    await test_cmd.send(
+      UniMessage.template("{:At(user, $event.get_user_id())} 已确认目标为 {target}")
+    )
+```
+另外也有 `$message_id` 与 `$target` 两个特殊值
+
+##### 检查消息段
+
+我们可以通过 `in` 运算符或消息序列的 `has` 方法来：
+
+```python
+# 是否存在消息段
+At("user", "1234") in message
+# 是否存在指定类型的消息段
+At in message
+```
+
+我们还可以使用 `only` 方法来检查消息中是否仅包含指定的消息段。
+
+```python
+# 是否都为 "test"
+message.only("test")
+# 是否仅包含指定类型的消息段
+message.only(Text)
+```
+
+##### 获取消息纯文本
+
+类似于 `Message.extract_plain_text()`，用于获取通用消息的纯文本.
+```python 
+from nonebot_plugin_alconna.uniseg import UniMessage, At
+
+
+# 提取消息纯文本字符串
+assert UniMessage(
+    [At("user", "1234"), "text"]
+).extract_plain_text() == "text"
+```
+
+##### 遍历
+
+通用消息序列继承自 `List[Segment]` ，因此可以使用 `for` 循环遍历消息段。
+
+```python
+for segment in message:  # type: Segment
+	...
+```
+
+##### 过滤、索引与切片
+
+消息序列对列表的索引与切片进行了增强，在原有列表 `int` 索引与 `slice` 切片的基础上，支持 `type` 过滤索引与切片.
+
+```python
+from nonebot_plugin_alconna.uniseg import UniMessage, At, Text, Reply
+
+
+message = UniMessage(
+    [
+        Reply(...),
+        "text1",
+        At("user", "1234"),
+        "text2"
+    ]
+)
+# 索引
+message[0] == Reply(...)
+# 切片
+message[0:2] == UniMessage([Reply(...), Text("text1")])
+# 类型过滤
+message[At] == Message([At("user", "1234")])
+# 类型索引
+message[At, 0] == At("user", "1234")
+# 类型切片
+message[Text, 0:2] == UniMessage([Text("text1"), Text("text2")])
+```
+
+我们也可以通过消息序列的 `include`、`exclude` 方法进行类型过滤.
+
+```python 
+message.include(Text, At)  
+message.exclude(Reply)
+```
+
+同样的，消息序列对列表的 `index`、`count` 方法也进行了增强，可以用于索引指定类型的消息段.
+
+```python
+# 指定类型首个消息段索引
+message.index(Text) == 1
+# 指定类型消息段数量
+message.count(Text) == 2
+```
+
+此外，消息序列添加了一个 `get` 方法，可以用于获取指定类型指定个数的消息段.
+
+```python
+# 获取指定类型指定个数的消息段
+message.get(Text, 1) == UniMessage([Text("test1")])
+```
+
+##### 消息发送
+
+通用消息可用 `UniMessage.send` 发送自身：
+
+```python
+async def send(
+    self,
+    target: Union[Event, Target, None] = None,
+    bot: Optional[Bot] = None,
+    fallback: bool = True,
+    at_sender: Union[str, bool] = False,
+    reply_to: Union[str, bool] = False,
+) -> Receipt:
+```
+
+实际上，`UniMessage` 同时提供了获取消息事件 id 与消息发送对象的方法:
+
+```python
+from nonebot import Event, Bot
+from nonebot_plugin_alconna.uniseg import Target, get_message_id, get_target
+
+
+matcher = on_xxx(...)
+
+@matcher.handle()
+async def _(bot: Bot, event: Event):
+    target: Target = get_target(event, bot)
+    msg_id: str = get_message_id(event, bot)
+
+```
+
+`send`, `get_target`, `get_message_id` 中与 `event`, `bot` 相关的参数都会尝试从上下文中获取对象.
+
+其中，`Target`:
+
+```python
+class Target:
+    id: str
+    """目标id；若为群聊则为group_id或者channel_id，若为私聊则为user_id"""
+    parent_id: str = ""
+    """父级id；若为频道则为guild_id，其他情况为空字符串"""
+    channel: bool = False
+    """是否为频道，仅当目标平台同时支持群聊和频道时有效"""
+    private: bool = False
+    """是否为私聊"""
+    source: str = ""
+    """可能的事件id"""
+```
+是用来描述响应消息时的发送对象.
+
+同样的，你可以通过依赖注入的方式在响应器中直接获取它们.
+
+### 条件控制
+
+本插件可以通过 `assign` 来控制一个具体的响应函数是否在不满足条件时跳过响应
+
+```python
+from nonebot import require  
+require("nonebot_plugin_alconna")  
+...  
+  
+from arclet.alconna import Alconna, Subcommand, Option, Args  
+from nonebot_plugin_alconna import on_alconna, CommandResult  
+  
+pip = Alconna(  
+"pip",  
+Subcommand(  
+"install", Args["pak", str],  
+Option("--upgrade"),  
+Option("--force-reinstall")  
+),  
+Subcommand("list", Option("--out-dated"))  
+)  
+  
+pip_cmd = on_alconna(pip)  
+  
+# 仅在命令为 `pip install pip` 时响应  
+@pip_cmd.assign("install.pak", "pip")  
+async def update(res: CommandResult):  
+...  
+  
+# 仅在命令为 `pip list` 时响应  
+@pip_cmd.assign("list")  
+async def list_(res: CommandResult):  
+...  
+  
+# 在命令为 `pip install xxx` 时响应  
+@pip_cmd.assign("install")  
+async def install(res: CommandResult):  
+...
+```
+
+
+### 响应器创建装饰
+
+本插件提供了一个 `funcommand` 装饰器, 其用于将一个接受任意参数， 返回 `str` 或 `Message` 或 `MessageSegment` 的函数转换为命令响应器.
+
+```python
+from nonebot_plugin_alconna import funcommand
+
+
+@funcommand()
+async def echo(msg: str):
+    return msg
+```
+
+其等同于
+
+```python
+from arclet.alconna import Alconna, Args
+from nonebot_plugin_alconna import on_alconna, AlconnaMatch, Match
+
+
+echo = on_alconna(Alconna("echo", Args["msg", str]))
+
+@echo.handle()
+async def echo_exit(msg: Match[str] = AlconnaMatch("msg")):
+    await echo.finish(msg.result)
+
+```
+
+### 类Koishi构造器
+
+本插件提供了一个 `Command` 构造器，其基于 `arclet.alconna.tools` 中的 `AlconnaString`， 以类似 `Koishi` 中注册命令的方式来构建一个 **AlconnaMatcher** :
+
+```python
+from nonebot_plugin_alconna import Command, Arparma
+
+
+book = (
+    Command("book", "测试")
+    .option("writer", "-w <id:int>")
+    .option("writer", "--anonymous", {"id": 0})
+    .usage("book [-w <id:int> | --anonymous]")
+    .shortcut("测试", {"args": ["--anonymous"]})
+    .build()
+)
+
+@book.handle()
+async def _(arp: Arparma):
+    await book.send(str(arp.options))
+```
+
+甚至，你可以设置 `action` 来设定响应行为：
+
+```python
+book = (
+    Command("book", "测试")
+    .option("writer", "-w <id:int>")
+    .option("writer", "--anonymous", {"id": 0})
+    .usage("book [-w <id:int> | --anonymous]")
+    .shortcut("测试", {"args": ["--anonymous"]})
+    .action(lambda options: str(options))  # 会自动通过 bot.send 发送
+    .build()
+)
+```
+
+### 返回值回调
+
+在 `AlconnaMatch`, `AlconnaQuery` 或 `got_path` 中，你可以使用 `middleware` 参数来传入一个对返回值进行处理的函数:
+
+```python
+from nonebot_plugin_alconna import image_fetch
+
+
+mask_cmd = on_alconna(
+    Alconna("search", Args["img?", Image]),
+)
+
+
+@mask_cmd.handle()
+async def mask_h(matcher: AlconnaMatcher, img: Match[bytes] = AlconnaMatch("img", image_fetch)):
+    result = await search_img(img.result)
+    await matcher.send(result.content)
+```
+
+其中，`image_fetch` 是一个中间件，其接受一个 `Image` 对象，并提取图片的二进制数据返回.
+
+
+### 匹配拓展
+
+本插件提供了一个 `Extension` 类，其用于自定义 AlconnaMatcher 的部分行为.
+
+例如 `LLMExtension` (仅举例)：
+
+```python
+from nonebot_plugin_alconna import Extension, Alconna, on_alconna, Interface
+
+
+class LLMExtension(Extension):
+    @property
+    def priority(self) -> int:
+        return 10
+
+    @property
+    def id(self) -> str:
+        return "LLMExtension"
+
+    def __init__(self, llm):
+      self.llm = llm
+
+    def post_init(self, alc: Alconna) -> None:
+        self.llm.add_context(alc.command, alc.meta.description)
+
+    async def receive_wrapper(self, bot, event, receive):
+        resp = await self.llm.input(str(receive))
+        return receive.__class__(resp.content)
+
+    def before_catch(self, name, annotation, default):
+        return name == "llm"
+
+    def catch(self, interface: Interface):
+        if interface.name == "llm":
+            return self.llm
+
+matcher = on_alconna(
+    Alconna(...),
+    extensions=[LLMExtension(LLM)]
+)
+...
+```
+
+那么添加了 `LLMExtension` 的响应器便能接受任何能通过 llm 翻译为具体命令的自然语言消息，同时可以在响应器中为所有 `llm` 参数注入模型变量
+
+目前 `Extension` 的功能有:
+
+- `validate`: 对于事件的来源适配器或 bot 选择是否接受响应
+- `output_converter`: 输出信息的自定义转换方法
+- `message_provider`: 从传入事件中自定义提取消息的方法
+- `receive_provider`: 对传入的消息 (Message 或 UniMessage) 的额外处理
+- `permission_check`: 命令对消息解析并确认头部匹配（即确认选择响应）时对发送者的权限判断
+- `parse_wrapper`: 对命令解析结果的额外处理
+- `send_wrapper`: 对发送的消息 (Message 或 UniMessage) 的额外处理
+- `before_catch`: 自定义依赖注入的绑定确认函数
+- `catch`: 自定义依赖注入处理函数
+- `post_init`: 响应器创建后对命令对象的额外处理
+
+例如内置的 `DiscordSlashExtension`，其可自动将 Alconna 对象翻译成 slash 指令并注册，且将收到的指令交互事件转为指令供命令解析:
+
+```python
+from nonebot_plugin_alconna import Match, on_alconna
+from nonebot_plugin_alconna.adapters.discord import DiscordSlashExtension
+
+
+alc = Alconna(
+    ["/"],
+    "permission",
+    Subcommand("add", Args["plugin", str]["priority?", int]),
+    Option("remove", Args["plugin", str]["time?", int]),
+    meta=CommandMeta(description="权限管理"),
+)
+
+matcher = on_alconna(alc, extensions=[DiscordSlashExtension()])
+
+@matcher.assign("add")
+async def add(plugin: Match[str], priority: Match[int]):
+    await matcher.finish(f"added {plugin.result} with {priority.result if priority.available else 0}")
+
+@matcher.assign("remove")
+async def remove(plugin: Match[str], time: Match[int]):
+    await matcher.finish(f"removed {plugin.result} with {time.result if time.available else -1}")
+```
+
+TIP:  
+全局的 Extension 可延迟加载 (即若有全局拓展加载于部分 AlconnaMatcher 之后，这部分响应器会被追加拓展)
+
+
+## 本体
+[`Alconna`](https://github.com/ArcletProject/Alconna) 隶属于 `ArcletProject`，是一个简单、灵活、高效的命令参数解析器, 并且不局限于解析命令式字符串.
+
+我们通过一个例子来讲解 **Alconna** 的核心: `Args`, `Subcommand`, `Option`:
+
+```python
+from arclet.alconna import Alconna, Args, Subcommand, Option
+
+
+alc = Alconna(
+    "pip",
+    Subcommand(
+        "install",
+        Args["package", str],
+        Option("-r|--requirement", Args["file", str]),
+        Option("-i|--index-url", Args["url", str]),
+    )
+)
+
+res = alc.parse("pip install nonebot2 -i URL")
+
+print(res)
+# matched=True, header_match=(origin='pip' result='pip' matched=True groups={}), subcommands={'install': (value=Ellipsis args={'package': 'nonebot2'} options={'index-url': (value=None args={'url': 'URL'})} subcommands={})}, other_args={'package': 'nonebot2', 'url': 'URL'} 
+
+print(res.all_matched_args)
+# {'package': 'nonebot2', 'url': 'URL'}
+```
+
+这段代码通过`Alconna`创捷了一个接受主命令名为`pip`, 子命令为`install`且子命令接受一个 **Args** 参数`package`和二个 **Option** 参数`-r`和`-i`的命令参数解析器, 通过`parse`方法返回解析结果 **Arparma** 的实例.
+
+
+### 组成
+
+#### 命令头
+命令头是指命令的前缀 (Prefix) 与命令名 (Command) 的组合，例如 !help 中的 ! 与 help.
+|前缀|命令名|匹配内容|说明|
+|:-:|:-:|:-:|:-:|
+|-|"foo"|`"foo"`|无前缀的纯文字头|
+|-|123|`123`|无前缀的元素头|
+|-|"re:\d{2}"|`"32"`|无前缀的正则头|
+|-|int|`123` 或 `"456"`|无前缀的类型头|
+|[int, bool]|-|`True` 或 `123`|无名的元素类头|
+|["foo", "bar"]|-|`"foo"` 或 `"bar"`|无名的纯文字头|
+|["foo", "bar"]|"baz"|`"foobaz"` 或 `"barbaz"`|纯文字头|
+|[int, bool]|"foo"|`[123, "foo"]` 或 `[False, "foo"]`|类型头|
+|[123, 4567]|"foo"|`[123, "foo"]` 或 `[4567, "foo"]`|元素头|
+|[nepattern.NUMBER]|"bar"|`[123, "bar"]` 或 `[123.456, "bar"]`|表达式头|
+|[123, "foo"]|"bar"|`[123, "bar"]` 或 `"foobar"` 或 `["foo", "bar"]`|混合头|
+|[(int, "foo"), (456, "bar")]|"baz"|`[123, "foobaz"]` 或 `[456, "foobaz"]` 或 `[456, "barbaz"]`|对头|
+
+无前缀的类型头：此时会将传入的值尝试转为 BasePattern，例如 `int` 会转为 `nepattern.INTEGER`。此时命令头会匹配对应的类型， 例如 `int` 会匹配 `123` 或 `"456"`，但不会匹配 `"foo"`。同时，Alconna 会将命令头匹配到的值转为对应的类型，例如 `int` 会将 `"123"` 转为 `123`。
+
+**正则只在命令名上生效，命令前缀中的正则会被转义**  
+除了通过传入 `re:xxx` 来使用正则表达式外，Alconna 还提供了一种更加简洁的方式来使用正则表达式，那就是 Bracket Header。
+
+```python
+from alconna import Alconna
+
+
+alc = Alconna(".rd{roll:int}")
+assert alc.parse(".rd123").header["roll"] == 123
+```
+
+Bracket Header 类似 python 里的 f-string 写法，通过 "{}" 声明匹配类型
+
+"{}" 中的内容为 "name:type or pat"：
+- "{}", "{:}" **⇔** "(.+)", 占位符
+- "{foo}" **⇔** "(?P&lt;foo&gt;.+)"
+- "{:\d+}" **⇔** "(\d+)"
+- "{foo:int}" **⇔** "(?P&lt;foo&gt;\d+)"，其中 "int" 部分若能转为 `BasePattern` 则读取里面的表达式
+
+
+#### 参数声明(Args)
+
+`Args` 是用于声明命令参数的组件, 可以通过以下几种方式构造 **Args** :
+
+ - `Args[key, var, default][key1, var1, default1][...]` 
+ - `Args[(key, var, default)]`
+ - `Args.key[var, default]`
+ 
+其中，key **一定**是字符串，而 var 一般为参数的类型，default 为具体的值或者 **arclet.alconna.args.Field**.
+
+其与函数签名类似，但是允许含有默认值的参数在前；同时支持 keyword-only 参数不依照构造顺序传入 （但是仍需要在非 keyword-only 参数之后）.
+
+##### key
+`key` 的作用是用以标记解析出来的参数并存放于 **Arparma** 中，以方便用户调用.
+
+其有三种为 Args 注解的标识符:  `?`、`/`、 `!`, 标识符与 key 之间建议以 `;` 分隔:
+
+- `!` 标识符表示该处传入的参数应**不是**规定的类型，或**不在**指定的值中。
+- `?` 标识符表示该参数为**可选**参数，会在无参数匹配时跳过。
+- `/` 标识符表示该参数的类型注解需要隐藏。
+
+另外，对于参数的注释也可以标记在 `key` 中，其与 key 或者标识符 以 `#` 分割：  
+`foo#这是注释;?` 或 `foo?#这是注释`
path: nekro_agent/adapters/alconna.md
user: login: sourcery-ai[bot]
body: **issue (typo):** 错别字：'分割' 应该是 '分隔'。

将 '分割' 更新为 '分隔'，以匹配 '分隔符' 中使用的术语。

```suggestion
另外，对于参数的注释也可以标记在 `key` 中，其与 key 或者标识符 以 `#` 分隔：  
`foo#这是注释;?` 或 `foo?#这是注释`
```

<details>
<summary>Original comment in English</summary>

**issue (typo):** Typo: '分割' should be '分隔'.

Update '分割' to '分隔' to match the terminology used in '分隔符'.

```suggestion
另外，对于参数的注释也可以标记在 `key` 中，其与 key 或者标识符 以 `#` 分隔：  
`foo#这是注释;?` 或 `foo?#这是注释`
```

</details>
created_at: 2025-05-24T21:36:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,1451 @@
+# Nonebot Plugin Alconna 文档
+
+本文分为三部分:
+- [`nonebot_plugin_alconna` 的介绍与使用](#插件)
+- [`Alconna` 本体的介绍与使用](#本体)(请优先阅读本体)
+- [外部参考](#references)
+
+## 插件
+### 安装
+
+```shell
+pip install nonebot-plugin-alconna
+```
+
+或
+
+```shell
+nb plugin install nonebot-plugin-alconna
+```
+
+### 展示
+
+```python
+from nonebot.adapters.onebot.v12 import Message
+from nonebot_plugin_alconna import on_alconna, AlconnaMatches, At
+from nonebot_plugin_alconna.adapters.onebot12 import Image
+from arclet.alconna import Alconna, Args, Option, Arparma
+
+
+alc = Alconna("Hello!", Option("--spec", Args["target", At]))
+hello = on_alconna(alc, auto_send_output=True)
+
+@hello.handle()
+async def _(result: Arparma = AlconnaMatches()):
+    if result.find("spec"):
+        target = result.query[At]("spec.target")
+        seed = target.target
+        await hello.finish(Message(Image(await gen_image(seed))))
+    else:
+        await hello.finish("Hello!")
+```
+
+### 响应器使用
+
+本插件基于 **Alconna** , 为 **Nonebot** 提供了一类新的事件响应器辅助函数 `on_alconna`.
+```python
+def on_alconna(
+    command: Alconna | str,
+    skip_for_unmatch: bool = True,
+    auto_send_output: bool = True,
+    aliases: set[str | tuple[str, ...]] | None = None,
+    comp_config: CompConfig | None = None,
+    extensions: list[type[Extension] | Extension] | None = None,
+    exclude_ext: list[type[Extension] | str] | None = None,
+    use_origin: bool = False,
+    use_cmd_start: bool = False,
+    use_cmd_sep: bool = False,
+    **kwargs,
+    ...,
+):
+```
+- `command`: Alconna 命令或字符串，字符串将通过 `AlconnaFormat` 转换为 Alconna 命令
+- `skip_for_unmatch`: 是否在命令不匹配时跳过该响应
+- `auto_send_output`: 是否自动发送输出信息并跳过响应
+- `aliases`: 命令别名， 作用类似于 `on_command` 中的 aliases
+- `comp_config`: 补全会话配置， 不传入则不启用补全会话
+- `extensions`: 需要加载的匹配扩展, 可以是扩展类或扩展实例
+- `exclude_ext`: 需要排除的匹配扩展, 可以是扩展类或扩展的id
+- `use_origin`: 是否使用未经 to_me 等处理过的消息
+- `use_cmd_start`: 是否使用 COMMAND_START 作为命令前缀
+- `use_cmd_sep`: 是否使用 COMMAND_SEP 作为命令分隔符
+
+其中对于 `skip_for_unmatch` 和 `auto_send_output` 的效果如下:
+
+| skip \ auto | True           | False                         |
+|-------------|----------------|-------------------------------|
+| True        | 帮助文本等自动发送      | 帮助文本等不发送                      |
+| False       | 帮助文本等和错误信息自动发送 | 帮助文本等和错误信息设置到 `CommandResult` |
+
+
+`on_alconna` 返回的是 `Matcher` 的子类 `AlconnaMatcher`，其拓展了如下方法:
+
+- `.assign(path, value, or_not)`: 用于对包含多个选项/子命令的命令的分派处理(具体请看条件控制)
+- `.got_path(path, prompt, middleware)`: 在 `got` 方法的基础上，会以 path 对应的参数为准，读取传入 message 的最后一个消息段并验证转换
+- `.set_path_arg(key, value)`, `.get_path_arg(key)`: 类似 `set_arg` 和 `got_arg`，为 `got_path` 的特化版本
+- `.reject_path(path[, prompt, fallback])`: 类似于 `reject_arg`，对应 `got_path`
+- `.dispatch`: 同样的分派处理，但是是类似 `CommandGroup` 一样返回新的 `AlconnaMatcher`
+- `.got`, `send`, `reject`, ...: 拓展了 prompt 类型，即支持使用 `UniMessage` 作为 prompt
+
+`assign`实例:
+
+```python
+from nonebot import require
+require("nonebot_plugin_alconna")
+
+from arclet.alconna import Alconna, Option, Args
+from nonebot_plugin_alconna import on_alconna, AlconnaMatch, Match, UniMessage
+
+  
+login = on_alconna(Alconna(["/"], "login", Args["password?", str], Option("-r|--recall"))) # 这里["/"]指命令前缀必须是/
+
+@login.assign("recall") # /login -r
+async def login_exit():
+    await login.finish("已退出")
+
+@login.assign("password") # /login xxx
+async def login_handle(pw: Match[str] = AlconnaMatch("password")):
+    if pw.available:
+        login.set_path_arg("password", pw.result)
+```
+
+`dispatch`每个分发设置独立的 matcher:
+
+```python
+update_cmd = pip_cmd.dispatch("install.pak", "pip")
+
+@update_cmd.handle()
+async def update(arp: CommandResult = AlconnaResult()):
+    ...
+```
+
+`got_path`类似 Nonebot2 的got, 它与 `assign`，`Match`，`Query` 等地方一样，都需要指明 `path` 参数 (即对应 Arg 验证的路径)
+
+`got_path` 会获取消息的最后一个消息段并转为 path 对应的类型，例如示例中 `target` 对应的 Arg 里要求 str 或 At，则 got 后用户输入的消息只有为 text 或 at 才能进入处理函数.
+
+实例:
+
+```python
+from nonebot_plugin_alconna import At, Match, UniMessage, on_alconna
+
+
+test_cmd = on_alconna(Alconna("test", Args["target?", Union[str, At]]))
+
+@test_cmd.handle()
+async def tt_h(target: Match[Union[str, At]]):
+    if target.available:
+        test_cmd.set_path_arg("target", target.result)
+
+@test_cmd.got_path("target", prompt="请输入目标")
+async def tt(target: Union[str, At]):
+    await test_cmd.send(UniMessage(["ok\n", target]))
+```
+
+`path`支持 ~XXX 语法，其会把 ~ 替换为可能的父级路径:
+
+```python
+ pip = Alconna(
+     "pip",
+     Subcommand(
+         "install",
+         Args["pak", str],
+         Option("--upgrade|-U"),
+         Option("--force-reinstall"),
+     ),
+     Subcommand("list", Option("--out-dated")),
+ )
+
+ pipcmd = on_alconna(pip)
+ pip_install_cmd = pipcmd.dispatch("install")
+
+
+ @pip_install_cmd.assign("~upgrade")
+ async def pip1_u(pak: Query[str] = Query("~pak")):
+     await pip_install_cmd.finish(f"pip upgrading {pak.result}...")
+```
+
+
+### Alconna的依赖注入
+
+本插件提供了一系列依赖注入函数，便于在响应函数中获取解析结果:
+
+- `AlconnaResult`: `CommandResult` 类型的依赖注入函数
+- `AlconnaMatches`: `Arparma` 类型的依赖注入函数
+- `AlconnaDuplication`: `Duplication` 类型的依赖注入函数
+- `AlconnaMatch`: `Match` 类型的依赖注入函数
+- `AlconnaQuery`: `Query` 类型的依赖注入函数
+
+同时，基于 [`Annotated` 支持](https://github.com/nonebot/nonebot2/pull/1832), 添加了两类注解:
+
+- `AlcMatches`：同 `AlconnaMatches`
+- `AlcResult`：同 `AlconnaResult`
+
+可以看到，本插件提供了几类额外的模型:
+
+- `CommandResult`: 解析结果，包括了源命令 `source: Alconna` ，解析结果 `result: Arparma`，以及可能的输出信息 `output: str | None` 字段
+- `Match`: 匹配项，表示参数是否存在于 `all_matched_args` 内，可用 `Match.available` 判断是否匹配，`Match.result` 获取匹配的值
+- `Query`: 查询项，表示参数是否可由 `Arparma.query` 查询并获得结果，可用 `Query.available` 判断是否查询成功，`Query.result` 获取查询结果
+
+**Alconna** 默认依赖注入的目标参数皆不需要使用依赖注入函数， 该效果对于 `AlconnaMatcher.got_path` 下的 Arg 同样有效:
+
+```python
+async def handle(
+    result: CommandResult,
+    arp: Arparma,
+    dup: Duplication,
+    source: Alconna,
+    abc: str,  # 类似 Match, 但是若匹配结果不存在对应字段则跳过该 handler
+    foo: Match[str],
+    bar: Query[int] = Query("ttt.bar", 0)  # Query 仍然需要一个默认值来传递 path 参数
+):
+    ...
+```
+
+如果你更喜欢 Depends 式的依赖注入，`nonebot_plugin_alconna` 同时提供了一系列的依赖注入函数，他们包括：
+
+- `AlconnaResult`: `CommandResult` 类型的依赖注入函数
+- `AlconnaMatches`: `Arparma` 类型的依赖注入函数
+- `AlconnaDuplication`: `Duplication` 类型的依赖注入函数
+- `AlconnaMatch`: `Match` 类型的依赖注入函数，其能够额外传入一个 middleware 函数来处理得到的参数
+- `AlconnaQuery`: `Query` 类型的依赖注入函数，其能够额外传入一个 middleware 函数来处理得到的参数
+- `AlconnaExecResult`: 提供挂载在命令上的 callback 的返回结果 (`Dict[str, Any]`) 的依赖注入函数
+- `AlconnaExtension`: 提供指定类型的 `Extension` 的依赖注入函数
+
+实例:
+
+```python
+from nonebot import require
+require("nonebot_plugin_alconna")
+
+from nonebot_plugin_alconna import (
+    on_alconna, 
+    Match,
+    Query,
+    AlconnaMatch,
+    AlcResult
+)
+from arclet.alconna import Alconna, Args, Option, Arparma
+
+
+test = on_alconna(
+    Alconna(
+        "test",
+        Option("foo", Args["bar", int]),
+        Option("baz", Args["qux", bool, False])
+    ),
+    auto_send_output=True
+)
+
+@test.handle()
+async def handle_test1(result: AlcResult):
+    await test.send(f"matched: {result.matched}")
+    await test.send(f"maybe output: {result.output}")
+
+@test.handle()
+async def handle_test2(result: Arparma):
+    await test.send(f"head result: {result.header_result}")
+    await test.send(f"args: {result.all_matched_args}")
+
+@test.handle()
+async def handle_test3(bar: Match[int] = AlconnaMatch("bar")):
+    if bar.available:    
+        await test.send(f"foo={bar.result}")
+
+@test.handle()
+async def handle_test4(qux: Query[bool] = Query("baz.qux", False)):
+    if qux.available:
+        await test.send(f"baz.qux={qux.result}")
+```
+
+
+### 跨平台适配
+
+`uniseg` 模块属于 `nonebot-plugin-alconna` 的子插件，其提供了一套通用的消息组件，用于在 `nonebot-plugin-alconna` 下构建通用消息.
+
+#### 通用消息段
+
+适配器下的消息段标注会匹配适配器特定的 `MessageSegment`, 而通用消息段与适配器消息段的区别在于:  
+
+通用消息段会匹配多个适配器中相似类型的消息段，并返回 `uniseg` 模块中定义的 [`Segment` 模型](https://nonebot.dev/docs/next/best-practice/alconna/utils#%E9%80%9A%E7%94%A8%E6%B6%88%E6%81%AF%E6%AE%B5), 以达到**跨平台接收消息**的作用
+
+`uniseg` 模块提供了类似 `MessageSegment` 的通用消息段，并可在 `Alconna` 下直接标注使用：
+
+```python
+class Segment:
+    """基类标注"""
+
+class Text(Segment):
+    """Text对象, 表示一类文本元素"""
+    text: str
+    style: Optional[str]
+
+class At(Segment):
+    """At对象, 表示一类提醒某用户的元素"""
+    type: Literal["user", "role", "channel"]
+    target: str
+
+class AtAll(Segment):
+    """AtAll对象, 表示一类提醒所有人的元素"""
+
+class Emoji(Segment):
+    """Emoji对象, 表示一类表情元素"""
+    id: str
+    name: Optional[str]
+
+class Media(Segment):
+    url: Optional[str]
+    id: Optional[str]
+    path: Optional[str]
+    raw: Optional[bytes]
+
+class Image(Media):
+    """Image对象, 表示一类图片元素"""
+
+class Audio(Media):
+    """Audio对象, 表示一类音频元素"""
+
+class Voice(Media):
+    """Voice对象, 表示一类语音元素"""
+
+class Video(Media):
+    """Video对象, 表示一类视频元素"""
+
+class File(Segment):
+    """File对象, 表示一类文件元素"""
+    id: str
+    name: Optional[str]
+
+class Reply(Segment):
+    """Reply对象，表示一类回复消息"""
+    id: str
+    """此处不一定是消息ID，可能是其他ID，如消息序号等"""
+    msg: Optional[Union[Message, str]]
+    origin: Optional[Any]
+
+class Reference(Segment):
+    """Reference对象，表示一类引用消息。转发消息 (Forward) 也属于此类"""
+    id: Optional[str]
+    """此处不一定是消息ID，可能是其他ID，如消息序号等"""
+    content: Optional[Union[Message, str, List[Union[RefNode, CustomNode]]]]
+
+class Card(Segment):
+    type: Literal["xml", "json"]
+    raw: str
+
+class Other(Segment):
+    """其他 Segment"""
+```
+此类消息段通过 `UniMessage.export` 可以转为特定的 `MessageSegment`.
+
+
+#### 通用信息序列
+
+`uniseg` 模块还提供了一个类似于 `Message` 的 `UniMessage` 类型，其元素为经过通用标注转换后的通用消息段.
+
+你可以通过提供的 `UniversalMessage` 或 `UniMsg` 依赖注入器来获取 `UniMessage`.
+
+```python
+from nonebot_plugin_alconna.uniseg import UniMsg, At, Reply
+
+
+matcher = on_xxx(...)
+
+@matcher.handle()
+async def _(msg: UniMsg):
+    reply = msg[Reply, 0]
+    print(reply.origin)
+    if msg.has(At):
+        ats = msg.get(At)
+        print(ats)
+    ...
+```
+
+还可以通过 `UniMessage` 的 `export` 与 `send` 方法来**跨平台发送消息**.
+
+```python
+from nonebot import Bot, on_command
+from nonebot_plugin_alconna.uniseg import Image, UniMessage
+
+
+test = on_command("test")
+
+@test.handle()
+async def handle_test():
+    await test.send(await UniMessage(Image(path="path/to/img")).export())
+```
+`UniMessage.export` 会通过传入的 `bot: Bot` 参数，或上下文中的 `Bot` 对象读取适配器信息，并使用对应的生成方法把通用消息转为适配器对应的消息序列.
+
+而在 `AlconnaMatcher` 下，`got`, `send`, `reject` 等可以发送消息的方法皆支持使用 `UniMessage`，不需要手动调用 export 方法：
+
+```python
+from arclet.alconna import Alconna, Args
+from nonebot_plugin_alconna import Match, AlconnaMatcher, on_alconna
+from nonebot_plugin_alconna.uniseg import At,  UniMessage
+
+
+test_cmd = on_alconna(Alconna("test", Args["target?", At]))
+
+@test_cmd.handle()
+async def tt_h(matcher: AlconnaMatcher, target: Match[At]):
+    if target.available:
+        matcher.set_path_arg("target", target.result)
+
+@test_cmd.got_path("target", prompt="请输入目标")
+async def tt(target: At):
+    await test_cmd.send(UniMessage([target, "\ndone."]))
+```
+
+除此之外 `UniMessage.send` 方法基于 `UniMessage.export` 并调用各适配器下的发送消息方法，返回一个 `Receipt` 对象，用于修改/撤回消息：
+
+```python
+from nonebot import Bot, on_command
+from nonebot_plugin_alconna.uniseg import UniMessage
+
+
+test = on_command("test")
+
+@test.handle()
+async def handle():
+    receipt = await UniMessage.text("hello!").send(at_sender=True, reply_to=True)
+    await receipt.recall(delay=1)
+```
+**在响应器以外的地方，`bot` 参数必须手动传入**
+
+本插件为以下设配器提供了 **Segment** 标注，可用于匹配各适配器的 `MessageSegment`，也可用于创建 `MessageSegment`:
+
+| 协议名称                                                                | 路径                                   |
+|---------------------------------------------------------------------|--------------------------------------|
+| [OneBot 协议](https://github.com/nonebot/adapter-onebot)              | adapters.onebot11, adapters.onebot12 |
+| [Telegram](https://github.com/nonebot/adapter-telegram)             | adapters.telegram                    |
+| [飞书](https://github.com/nonebot/adapter-feishu)                     | adapters.feishu                      |
+| [GitHub](https://github.com/nonebot/adapter-github)                 | adapters.github                      |
+| [QQ bot](https://github.com/nonebot/adapter-qq)                     | adapters.qq                          |
+| [钉钉](https://github.com/nonebot/adapter-ding)                       | adapters.ding                        |
+| [Console](https://github.com/nonebot/adapter-console)               | adapters.console                     |
+| [开黑啦](https://github.com/Tian-que/nonebot-adapter-kaiheila)         | adapters.kook                        |
+| [Mirai](https://github.com/nonebot/adapter-mirai)                   | adapters.mirai, adapters.mirai2      |
+| [Ntchat](https://github.com/JustUndertaker/adapter-ntchat)          | adapters.ntchat                      |
+| [MineCraft](https://github.com/17TheWord/nonebot-adapter-minecraft) | adapters.minecraft                   |
+| [Walle-Q](https://github.com/onebot-walle/nonebot_adapter_walleq)   | adapters.onebot12                    |
+| [Discord](https://github.com/nonebot/adapter-discord)               | adapters.discord                     |
+| [Red 协议](https://github.com/nonebot/adapter-red)                    | adapters.red                         |
+| [Satori](https://github.com/nonebot/adapter-satori)                 | adapters.satori                      |
+| [Dodo IM](https://github.com/nonebot/adapter-dodo)                  | adapters.dodo                        |
+| [Kritor](https://github.com/nonebot/adapter-kritor)                 | adapters.kritor                      |  
+| [Tailchat](https://github.com/eya46/nonebot-adapter-tailchat)       | adapters.tailchat                    |
+
+
+##### 构造
+
+类比 `Message`, `UniMessage` 可以传入单个字符串/消息段或可迭代的字符串/消息段：
+
+```python
+from nonebot_plugin_alconna.uniseg import UniMessage, At
+
+
+msg = UniMessage("Hello")
+msg1 = UniMessage(At("user", "124"))
+msg2 = UniMessage(["Hello", At("user", "124")])
+```
+
+`UniMessage` 上同时存在便捷方法，令其可以链式地添加消息段：
+
+```python
+from nonebot_plugin_alconna.uniseg import UniMessage, At, Image
+
+
+msg = UniMessage.text("Hello").at("124").image(path="/path/to/img")
+assert msg == UniMessage(
+    ["Hello", At("user", "124"), Image(path="/path/to/img")]
+)
+```
+
+##### 拼接消息
+
+`str`、`UniMessage`、`Segment` 对象之间可以直接相加，相加均会返回一个新的 `UniMessage` 对象.
+
+```python
+# 消息序列与消息段相加
+UniMessage("text") + Text("text")
+# 消息序列与字符串相加
+UniMessage([Text("text")]) + "text"
+# 消息序列与消息序列相加
+UniMessage("text") + UniMessage([Text("text")])
+# 字符串与消息序列相加
+"text" + UniMessage([Text("text")])
+# 消息段与消息段相加
+Text("text") + Text("text")
+# 消息段与字符串相加
+Text("text") + "text"
+# 消息段与消息序列相加
+Text("text") + UniMessage([Text("text")])
+# 字符串与消息段相加
+"text" + Text("text")
+```
+
+如果需要在当前消息序列后直接拼接新的消息段，可以使用 `Message.append`、`Message.extend` 方法，或者使用自加.
+
+```python
+msg = UniMessage([Text("text")])
+# 自加
+msg += "text"
+msg += Text("text")
+msg += UniMessage([Text("text")])
+# 附加
+msg.append(Text("text"))
+# 扩展
+msg.extend([Text("text")])
+```
+
+##### 使用消息模板
+
+`UniMessage.template` 同样类似于 `Message.template`，可以用于格式化消息。大体用法参考 [消息模板](https://nonebot.dev/docs/next/tutorial/message#%E4%BD%BF%E7%94%A8%E6%B6%88%E6%81%AF%E6%A8%A1%E6%9D%BF).
+
+这里额外说明 `UniMessage.template` 的拓展控制符.
+
+相比 `Message`，UniMessage 对于 {:XXX} 做了另一类拓展。其能够识别例如 At(xxx, yyy) 或 Emoji(aaa, bbb)的字符串并执行.
+
+以 At(...) 为例使用通用消息段的拓展控制符：
+
+```python
+>>> from nonebot_plugin_alconna.uniseg import UniMessage
+>>>  UniMessage.template("{:At(user, target)}").format(target="123")
+UniMessage(At("user", "123"))
+>>> UniMessage.template("{:At(type=user, target=id)}").format(id="123")
+UniMessage(At("user", "123"))
+>>> UniMessage.template("{:At(type=user, target=123)}").format()
+UniMessage(At("user", "123"))
+```
+
+而在 `AlconnaMatcher` 中，{:XXX} 更进一步地提供了获取 `event` 和 `bot` 中的属性的功能.
+
+在AlconnaMatcher中使用通用消息段的拓展控制符:
+
+```python
+from arclet.alconna import Alconna, Args
+from nonebot_plugin_alconna import At, Match, UniMessage, AlconnaMatcher, on_alconna
+
+
+test_cmd = on_alconna(Alconna("test", Args["target?", At]))
+
+@test_cmd.handle()
+async def tt_h(matcher: AlconnaMatcher, target: Match[At]):
+    if target.available:
+        matcher.set_path_arg("target", target.result)
+
+@test_cmd.got_path(
+    "target",
+    prompt=UniMessage.template("{:At(user, $event.get_user_id())} 请确认目标")
+)
+async def tt():
+    await test_cmd.send(
+      UniMessage.template("{:At(user, $event.get_user_id())} 已确认目标为 {target}")
+    )
+```
+另外也有 `$message_id` 与 `$target` 两个特殊值
+
+##### 检查消息段
+
+我们可以通过 `in` 运算符或消息序列的 `has` 方法来：
+
+```python
+# 是否存在消息段
+At("user", "1234") in message
+# 是否存在指定类型的消息段
+At in message
+```
+
+我们还可以使用 `only` 方法来检查消息中是否仅包含指定的消息段。
+
+```python
+# 是否都为 "test"
+message.only("test")
+# 是否仅包含指定类型的消息段
+message.only(Text)
+```
+
+##### 获取消息纯文本
+
+类似于 `Message.extract_plain_text()`，用于获取通用消息的纯文本.
+```python 
+from nonebot_plugin_alconna.uniseg import UniMessage, At
+
+
+# 提取消息纯文本字符串
+assert UniMessage(
+    [At("user", "1234"), "text"]
+).extract_plain_text() == "text"
+```
+
+##### 遍历
+
+通用消息序列继承自 `List[Segment]` ，因此可以使用 `for` 循环遍历消息段。
+
+```python
+for segment in message:  # type: Segment
+	...
+```
+
+##### 过滤、索引与切片
+
+消息序列对列表的索引与切片进行了增强，在原有列表 `int` 索引与 `slice` 切片的基础上，支持 `type` 过滤索引与切片.
+
+```python
+from nonebot_plugin_alconna.uniseg import UniMessage, At, Text, Reply
+
+
+message = UniMessage(
+    [
+        Reply(...),
+        "text1",
+        At("user", "1234"),
+        "text2"
+    ]
+)
+# 索引
+message[0] == Reply(...)
+# 切片
+message[0:2] == UniMessage([Reply(...), Text("text1")])
+# 类型过滤
+message[At] == Message([At("user", "1234")])
+# 类型索引
+message[At, 0] == At("user", "1234")
+# 类型切片
+message[Text, 0:2] == UniMessage([Text("text1"), Text("text2")])
+```
+
+我们也可以通过消息序列的 `include`、`exclude` 方法进行类型过滤.
+
+```python 
+message.include(Text, At)  
+message.exclude(Reply)
+```
+
+同样的，消息序列对列表的 `index`、`count` 方法也进行了增强，可以用于索引指定类型的消息段.
+
+```python
+# 指定类型首个消息段索引
+message.index(Text) == 1
+# 指定类型消息段数量
+message.count(Text) == 2
+```
+
+此外，消息序列添加了一个 `get` 方法，可以用于获取指定类型指定个数的消息段.
+
+```python
+# 获取指定类型指定个数的消息段
+message.get(Text, 1) == UniMessage([Text("test1")])
+```
+
+##### 消息发送
+
+通用消息可用 `UniMessage.send` 发送自身：
+
+```python
+async def send(
+    self,
+    target: Union[Event, Target, None] = None,
+    bot: Optional[Bot] = None,
+    fallback: bool = True,
+    at_sender: Union[str, bool] = False,
+    reply_to: Union[str, bool] = False,
+) -> Receipt:
+```
+
+实际上，`UniMessage` 同时提供了获取消息事件 id 与消息发送对象的方法:
+
+```python
+from nonebot import Event, Bot
+from nonebot_plugin_alconna.uniseg import Target, get_message_id, get_target
+
+
+matcher = on_xxx(...)
+
+@matcher.handle()
+async def _(bot: Bot, event: Event):
+    target: Target = get_target(event, bot)
+    msg_id: str = get_message_id(event, bot)
+
+```
+
+`send`, `get_target`, `get_message_id` 中与 `event`, `bot` 相关的参数都会尝试从上下文中获取对象.
+
+其中，`Target`:
+
+```python
+class Target:
+    id: str
+    """目标id；若为群聊则为group_id或者channel_id，若为私聊则为user_id"""
+    parent_id: str = ""
+    """父级id；若为频道则为guild_id，其他情况为空字符串"""
+    channel: bool = False
+    """是否为频道，仅当目标平台同时支持群聊和频道时有效"""
+    private: bool = False
+    """是否为私聊"""
+    source: str = ""
+    """可能的事件id"""
+```
+是用来描述响应消息时的发送对象.
+
+同样的，你可以通过依赖注入的方式在响应器中直接获取它们.
+
+### 条件控制
+
+本插件可以通过 `assign` 来控制一个具体的响应函数是否在不满足条件时跳过响应
+
+```python
+from nonebot import require  
+require("nonebot_plugin_alconna")  
+...  
+  
+from arclet.alconna import Alconna, Subcommand, Option, Args  
+from nonebot_plugin_alconna import on_alconna, CommandResult  
+  
+pip = Alconna(  
+"pip",  
+Subcommand(  
+"install", Args["pak", str],  
+Option("--upgrade"),  
+Option("--force-reinstall")  
+),  
+Subcommand("list", Option("--out-dated"))  
+)  
+  
+pip_cmd = on_alconna(pip)  
+  
+# 仅在命令为 `pip install pip` 时响应  
+@pip_cmd.assign("install.pak", "pip")  
+async def update(res: CommandResult):  
+...  
+  
+# 仅在命令为 `pip list` 时响应  
+@pip_cmd.assign("list")  
+async def list_(res: CommandResult):  
+...  
+  
+# 在命令为 `pip install xxx` 时响应  
+@pip_cmd.assign("install")  
+async def install(res: CommandResult):  
+...
+```
+
+
+### 响应器创建装饰
+
+本插件提供了一个 `funcommand` 装饰器, 其用于将一个接受任意参数， 返回 `str` 或 `Message` 或 `MessageSegment` 的函数转换为命令响应器.
+
+```python
+from nonebot_plugin_alconna import funcommand
+
+
+@funcommand()
+async def echo(msg: str):
+    return msg
+```
+
+其等同于
+
+```python
+from arclet.alconna import Alconna, Args
+from nonebot_plugin_alconna import on_alconna, AlconnaMatch, Match
+
+
+echo = on_alconna(Alconna("echo", Args["msg", str]))
+
+@echo.handle()
+async def echo_exit(msg: Match[str] = AlconnaMatch("msg")):
+    await echo.finish(msg.result)
+
+```
+
+### 类Koishi构造器
+
+本插件提供了一个 `Command` 构造器，其基于 `arclet.alconna.tools` 中的 `AlconnaString`， 以类似 `Koishi` 中注册命令的方式来构建一个 **AlconnaMatcher** :
+
+```python
+from nonebot_plugin_alconna import Command, Arparma
+
+
+book = (
+    Command("book", "测试")
+    .option("writer", "-w <id:int>")
+    .option("writer", "--anonymous", {"id": 0})
+    .usage("book [-w <id:int> | --anonymous]")
+    .shortcut("测试", {"args": ["--anonymous"]})
+    .build()
+)
+
+@book.handle()
+async def _(arp: Arparma):
+    await book.send(str(arp.options))
+```
+
+甚至，你可以设置 `action` 来设定响应行为：
+
+```python
+book = (
+    Command("book", "测试")
+    .option("writer", "-w <id:int>")
+    .option("writer", "--anonymous", {"id": 0})
+    .usage("book [-w <id:int> | --anonymous]")
+    .shortcut("测试", {"args": ["--anonymous"]})
+    .action(lambda options: str(options))  # 会自动通过 bot.send 发送
+    .build()
+)
+```
+
+### 返回值回调
+
+在 `AlconnaMatch`, `AlconnaQuery` 或 `got_path` 中，你可以使用 `middleware` 参数来传入一个对返回值进行处理的函数:
+
+```python
+from nonebot_plugin_alconna import image_fetch
+
+
+mask_cmd = on_alconna(
+    Alconna("search", Args["img?", Image]),
+)
+
+
+@mask_cmd.handle()
+async def mask_h(matcher: AlconnaMatcher, img: Match[bytes] = AlconnaMatch("img", image_fetch)):
+    result = await search_img(img.result)
+    await matcher.send(result.content)
+```
+
+其中，`image_fetch` 是一个中间件，其接受一个 `Image` 对象，并提取图片的二进制数据返回.
+
+
+### 匹配拓展
+
+本插件提供了一个 `Extension` 类，其用于自定义 AlconnaMatcher 的部分行为.
+
+例如 `LLMExtension` (仅举例)：
+
+```python
+from nonebot_plugin_alconna import Extension, Alconna, on_alconna, Interface
+
+
+class LLMExtension(Extension):
+    @property
+    def priority(self) -> int:
+        return 10
+
+    @property
+    def id(self) -> str:
+        return "LLMExtension"
+
+    def __init__(self, llm):
+      self.llm = llm
+
+    def post_init(self, alc: Alconna) -> None:
+        self.llm.add_context(alc.command, alc.meta.description)
+
+    async def receive_wrapper(self, bot, event, receive):
+        resp = await self.llm.input(str(receive))
+        return receive.__class__(resp.content)
+
+    def before_catch(self, name, annotation, default):
+        return name == "llm"
+
+    def catch(self, interface: Interface):
+        if interface.name == "llm":
+            return self.llm
+
+matcher = on_alconna(
+    Alconna(...),
+    extensions=[LLMExtension(LLM)]
+)
+...
+```
+
+那么添加了 `LLMExtension` 的响应器便能接受任何能通过 llm 翻译为具体命令的自然语言消息，同时可以在响应器中为所有 `llm` 参数注入模型变量
+
+目前 `Extension` 的功能有:
+
+- `validate`: 对于事件的来源适配器或 bot 选择是否接受响应
+- `output_converter`: 输出信息的自定义转换方法
+- `message_provider`: 从传入事件中自定义提取消息的方法
+- `receive_provider`: 对传入的消息 (Message 或 UniMessage) 的额外处理
+- `permission_check`: 命令对消息解析并确认头部匹配（即确认选择响应）时对发送者的权限判断
+- `parse_wrapper`: 对命令解析结果的额外处理
+- `send_wrapper`: 对发送的消息 (Message 或 UniMessage) 的额外处理
+- `before_catch`: 自定义依赖注入的绑定确认函数
+- `catch`: 自定义依赖注入处理函数
+- `post_init`: 响应器创建后对命令对象的额外处理
+
+例如内置的 `DiscordSlashExtension`，其可自动将 Alconna 对象翻译成 slash 指令并注册，且将收到的指令交互事件转为指令供命令解析:
+
+```python
+from nonebot_plugin_alconna import Match, on_alconna
+from nonebot_plugin_alconna.adapters.discord import DiscordSlashExtension
+
+
+alc = Alconna(
+    ["/"],
+    "permission",
+    Subcommand("add", Args["plugin", str]["priority?", int]),
+    Option("remove", Args["plugin", str]["time?", int]),
+    meta=CommandMeta(description="权限管理"),
+)
+
+matcher = on_alconna(alc, extensions=[DiscordSlashExtension()])
+
+@matcher.assign("add")
+async def add(plugin: Match[str], priority: Match[int]):
+    await matcher.finish(f"added {plugin.result} with {priority.result if priority.available else 0}")
+
+@matcher.assign("remove")
+async def remove(plugin: Match[str], time: Match[int]):
+    await matcher.finish(f"removed {plugin.result} with {time.result if time.available else -1}")
+```
+
+TIP:  
+全局的 Extension 可延迟加载 (即若有全局拓展加载于部分 AlconnaMatcher 之后，这部分响应器会被追加拓展)
+
+
+## 本体
+[`Alconna`](https://github.com/ArcletProject/Alconna) 隶属于 `ArcletProject`，是一个简单、灵活、高效的命令参数解析器, 并且不局限于解析命令式字符串.
+
+我们通过一个例子来讲解 **Alconna** 的核心: `Args`, `Subcommand`, `Option`:
+
+```python
+from arclet.alconna import Alconna, Args, Subcommand, Option
+
+
+alc = Alconna(
+    "pip",
+    Subcommand(
+        "install",
+        Args["package", str],
+        Option("-r|--requirement", Args["file", str]),
+        Option("-i|--index-url", Args["url", str]),
+    )
+)
+
+res = alc.parse("pip install nonebot2 -i URL")
+
+print(res)
+# matched=True, header_match=(origin='pip' result='pip' matched=True groups={}), subcommands={'install': (value=Ellipsis args={'package': 'nonebot2'} options={'index-url': (value=None args={'url': 'URL'})} subcommands={})}, other_args={'package': 'nonebot2', 'url': 'URL'} 
+
+print(res.all_matched_args)
+# {'package': 'nonebot2', 'url': 'URL'}
+```
+
+这段代码通过`Alconna`创捷了一个接受主命令名为`pip`, 子命令为`install`且子命令接受一个 **Args** 参数`package`和二个 **Option** 参数`-r`和`-i`的命令参数解析器, 通过`parse`方法返回解析结果 **Arparma** 的实例.
+
+
+### 组成
+
+#### 命令头
+命令头是指命令的前缀 (Prefix) 与命令名 (Command) 的组合，例如 !help 中的 ! 与 help.
+|前缀|命令名|匹配内容|说明|
+|:-:|:-:|:-:|:-:|
+|-|"foo"|`"foo"`|无前缀的纯文字头|
+|-|123|`123`|无前缀的元素头|
+|-|"re:\d{2}"|`"32"`|无前缀的正则头|
+|-|int|`123` 或 `"456"`|无前缀的类型头|
+|[int, bool]|-|`True` 或 `123`|无名的元素类头|
+|["foo", "bar"]|-|`"foo"` 或 `"bar"`|无名的纯文字头|
+|["foo", "bar"]|"baz"|`"foobaz"` 或 `"barbaz"`|纯文字头|
+|[int, bool]|"foo"|`[123, "foo"]` 或 `[False, "foo"]`|类型头|
+|[123, 4567]|"foo"|`[123, "foo"]` 或 `[4567, "foo"]`|元素头|
+|[nepattern.NUMBER]|"bar"|`[123, "bar"]` 或 `[123.456, "bar"]`|表达式头|
+|[123, "foo"]|"bar"|`[123, "bar"]` 或 `"foobar"` 或 `["foo", "bar"]`|混合头|
+|[(int, "foo"), (456, "bar")]|"baz"|`[123, "foobaz"]` 或 `[456, "foobaz"]` 或 `[456, "barbaz"]`|对头|
+
+无前缀的类型头：此时会将传入的值尝试转为 BasePattern，例如 `int` 会转为 `nepattern.INTEGER`。此时命令头会匹配对应的类型， 例如 `int` 会匹配 `123` 或 `"456"`，但不会匹配 `"foo"`。同时，Alconna 会将命令头匹配到的值转为对应的类型，例如 `int` 会将 `"123"` 转为 `123`。
+
+**正则只在命令名上生效，命令前缀中的正则会被转义**  
+除了通过传入 `re:xxx` 来使用正则表达式外，Alconna 还提供了一种更加简洁的方式来使用正则表达式，那就是 Bracket Header。
+
+```python
+from alconna import Alconna
+
+
+alc = Alconna(".rd{roll:int}")
+assert alc.parse(".rd123").header["roll"] == 123
+```
+
+Bracket Header 类似 python 里的 f-string 写法，通过 "{}" 声明匹配类型
+
+"{}" 中的内容为 "name:type or pat"：
+- "{}", "{:}" **⇔** "(.+)", 占位符
+- "{foo}" **⇔** "(?P&lt;foo&gt;.+)"
+- "{:\d+}" **⇔** "(\d+)"
+- "{foo:int}" **⇔** "(?P&lt;foo&gt;\d+)"，其中 "int" 部分若能转为 `BasePattern` 则读取里面的表达式
+
+
+#### 参数声明(Args)
+
+`Args` 是用于声明命令参数的组件, 可以通过以下几种方式构造 **Args** :
+
+ - `Args[key, var, default][key1, var1, default1][...]` 
+ - `Args[(key, var, default)]`
+ - `Args.key[var, default]`
+ 
+其中，key **一定**是字符串，而 var 一般为参数的类型，default 为具体的值或者 **arclet.alconna.args.Field**.
+
+其与函数签名类似，但是允许含有默认值的参数在前；同时支持 keyword-only 参数不依照构造顺序传入 （但是仍需要在非 keyword-only 参数之后）.
+
+##### key
+`key` 的作用是用以标记解析出来的参数并存放于 **Arparma** 中，以方便用户调用.
+
+其有三种为 Args 注解的标识符:  `?`、`/`、 `!`, 标识符与 key 之间建议以 `;` 分隔:
+
+- `!` 标识符表示该处传入的参数应**不是**规定的类型，或**不在**指定的值中。
+- `?` 标识符表示该参数为**可选**参数，会在无参数匹配时跳过。
+- `/` 标识符表示该参数的类型注解需要隐藏。
+
+另外，对于参数的注释也可以标记在 `key` 中，其与 key 或者标识符 以 `#` 分割：  
+`foo#这是注释;?` 或 `foo?#这是注释`
+
+`Args` 中的 `key` 在实际命令中并不需要传入（keyword 参数除外）：
+
+```python
+from arclet.alconna import Alconna, Args  
+
+
+alc = Alconna("test", Args["foo", str])  
+alc.parse("test --foo abc") # 错误  
+alc.parse("test abc") # 正确
+```
+
+若需要 `test --foo abc`，你应该使用 `Option`：
+
+```python
+from arclet.alconna import Alconna, Args, Option
+
+
+alc = Alconna("test", Option("--foo", Args["foo", str]))
+```
+##### var
+var 负责命令参数的**类型检查**与**类型转化**.
path: nekro_agent/adapters/alconna.md
user: login: sourcery-ai[bot]
body: **issue (typo):** 错别字：'类型转化' 应该是 '类型转换'。

'类型转换' 是 'type conversion' 的标准术语。

```suggestion
var 负责命令参数的**类型检查**与**类型转换**.
```

<details>
<summary>Original comment in English</summary>

**issue (typo):** Typo: '类型转化' should be '类型转换'.

'类型转换' is the standard term for 'type conversion'.

```suggestion
var 负责命令参数的**类型检查**与**类型转换**.
```

</details>
created_at: 2025-05-24T21:36:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,1451 @@
+# Nonebot Plugin Alconna 文档
+
+本文分为三部分:
+- [`nonebot_plugin_alconna` 的介绍与使用](#插件)
+- [`Alconna` 本体的介绍与使用](#本体)(请优先阅读本体)
+- [外部参考](#references)
+
+## 插件
+### 安装
+
+```shell
+pip install nonebot-plugin-alconna
+```
+
+或
+
+```shell
+nb plugin install nonebot-plugin-alconna
+```
+
+### 展示
+
+```python
+from nonebot.adapters.onebot.v12 import Message
+from nonebot_plugin_alconna import on_alconna, AlconnaMatches, At
+from nonebot_plugin_alconna.adapters.onebot12 import Image
+from arclet.alconna import Alconna, Args, Option, Arparma
+
+
+alc = Alconna("Hello!", Option("--spec", Args["target", At]))
+hello = on_alconna(alc, auto_send_output=True)
+
+@hello.handle()
+async def _(result: Arparma = AlconnaMatches()):
+    if result.find("spec"):
+        target = result.query[At]("spec.target")
+        seed = target.target
+        await hello.finish(Message(Image(await gen_image(seed))))
+    else:
+        await hello.finish("Hello!")
+```
+
+### 响应器使用
+
+本插件基于 **Alconna** , 为 **Nonebot** 提供了一类新的事件响应器辅助函数 `on_alconna`.
+```python
+def on_alconna(
+    command: Alconna | str,
+    skip_for_unmatch: bool = True,
+    auto_send_output: bool = True,
+    aliases: set[str | tuple[str, ...]] | None = None,
+    comp_config: CompConfig | None = None,
+    extensions: list[type[Extension] | Extension] | None = None,
+    exclude_ext: list[type[Extension] | str] | None = None,
+    use_origin: bool = False,
+    use_cmd_start: bool = False,
+    use_cmd_sep: bool = False,
+    **kwargs,
+    ...,
+):
+```
+- `command`: Alconna 命令或字符串，字符串将通过 `AlconnaFormat` 转换为 Alconna 命令
+- `skip_for_unmatch`: 是否在命令不匹配时跳过该响应
+- `auto_send_output`: 是否自动发送输出信息并跳过响应
+- `aliases`: 命令别名， 作用类似于 `on_command` 中的 aliases
+- `comp_config`: 补全会话配置， 不传入则不启用补全会话
+- `extensions`: 需要加载的匹配扩展, 可以是扩展类或扩展实例
+- `exclude_ext`: 需要排除的匹配扩展, 可以是扩展类或扩展的id
+- `use_origin`: 是否使用未经 to_me 等处理过的消息
+- `use_cmd_start`: 是否使用 COMMAND_START 作为命令前缀
+- `use_cmd_sep`: 是否使用 COMMAND_SEP 作为命令分隔符
+
+其中对于 `skip_for_unmatch` 和 `auto_send_output` 的效果如下:
+
+| skip \ auto | True           | False                         |
+|-------------|----------------|-------------------------------|
+| True        | 帮助文本等自动发送      | 帮助文本等不发送                      |
+| False       | 帮助文本等和错误信息自动发送 | 帮助文本等和错误信息设置到 `CommandResult` |
+
+
+`on_alconna` 返回的是 `Matcher` 的子类 `AlconnaMatcher`，其拓展了如下方法:
+
+- `.assign(path, value, or_not)`: 用于对包含多个选项/子命令的命令的分派处理(具体请看条件控制)
+- `.got_path(path, prompt, middleware)`: 在 `got` 方法的基础上，会以 path 对应的参数为准，读取传入 message 的最后一个消息段并验证转换
+- `.set_path_arg(key, value)`, `.get_path_arg(key)`: 类似 `set_arg` 和 `got_arg`，为 `got_path` 的特化版本
+- `.reject_path(path[, prompt, fallback])`: 类似于 `reject_arg`，对应 `got_path`
+- `.dispatch`: 同样的分派处理，但是是类似 `CommandGroup` 一样返回新的 `AlconnaMatcher`
+- `.got`, `send`, `reject`, ...: 拓展了 prompt 类型，即支持使用 `UniMessage` 作为 prompt
+
+`assign`实例:
+
+```python
+from nonebot import require
+require("nonebot_plugin_alconna")
+
+from arclet.alconna import Alconna, Option, Args
+from nonebot_plugin_alconna import on_alconna, AlconnaMatch, Match, UniMessage
+
+  
+login = on_alconna(Alconna(["/"], "login", Args["password?", str], Option("-r|--recall"))) # 这里["/"]指命令前缀必须是/
+
+@login.assign("recall") # /login -r
+async def login_exit():
+    await login.finish("已退出")
+
+@login.assign("password") # /login xxx
+async def login_handle(pw: Match[str] = AlconnaMatch("password")):
+    if pw.available:
+        login.set_path_arg("password", pw.result)
+```
+
+`dispatch`每个分发设置独立的 matcher:
+
+```python
+update_cmd = pip_cmd.dispatch("install.pak", "pip")
+
+@update_cmd.handle()
+async def update(arp: CommandResult = AlconnaResult()):
+    ...
+```
+
+`got_path`类似 Nonebot2 的got, 它与 `assign`，`Match`，`Query` 等地方一样，都需要指明 `path` 参数 (即对应 Arg 验证的路径)
+
+`got_path` 会获取消息的最后一个消息段并转为 path 对应的类型，例如示例中 `target` 对应的 Arg 里要求 str 或 At，则 got 后用户输入的消息只有为 text 或 at 才能进入处理函数.
+
+实例:
+
+```python
+from nonebot_plugin_alconna import At, Match, UniMessage, on_alconna
+
+
+test_cmd = on_alconna(Alconna("test", Args["target?", Union[str, At]]))
+
+@test_cmd.handle()
+async def tt_h(target: Match[Union[str, At]]):
+    if target.available:
+        test_cmd.set_path_arg("target", target.result)
+
+@test_cmd.got_path("target", prompt="请输入目标")
+async def tt(target: Union[str, At]):
+    await test_cmd.send(UniMessage(["ok\n", target]))
+```
+
+`path`支持 ~XXX 语法，其会把 ~ 替换为可能的父级路径:
+
+```python
+ pip = Alconna(
+     "pip",
+     Subcommand(
+         "install",
+         Args["pak", str],
+         Option("--upgrade|-U"),
+         Option("--force-reinstall"),
+     ),
+     Subcommand("list", Option("--out-dated")),
+ )
+
+ pipcmd = on_alconna(pip)
+ pip_install_cmd = pipcmd.dispatch("install")
+
+
+ @pip_install_cmd.assign("~upgrade")
+ async def pip1_u(pak: Query[str] = Query("~pak")):
+     await pip_install_cmd.finish(f"pip upgrading {pak.result}...")
+```
+
+
+### Alconna的依赖注入
+
+本插件提供了一系列依赖注入函数，便于在响应函数中获取解析结果:
+
+- `AlconnaResult`: `CommandResult` 类型的依赖注入函数
+- `AlconnaMatches`: `Arparma` 类型的依赖注入函数
+- `AlconnaDuplication`: `Duplication` 类型的依赖注入函数
+- `AlconnaMatch`: `Match` 类型的依赖注入函数
+- `AlconnaQuery`: `Query` 类型的依赖注入函数
+
+同时，基于 [`Annotated` 支持](https://github.com/nonebot/nonebot2/pull/1832), 添加了两类注解:
+
+- `AlcMatches`：同 `AlconnaMatches`
+- `AlcResult`：同 `AlconnaResult`
+
+可以看到，本插件提供了几类额外的模型:
+
+- `CommandResult`: 解析结果，包括了源命令 `source: Alconna` ，解析结果 `result: Arparma`，以及可能的输出信息 `output: str | None` 字段
+- `Match`: 匹配项，表示参数是否存在于 `all_matched_args` 内，可用 `Match.available` 判断是否匹配，`Match.result` 获取匹配的值
+- `Query`: 查询项，表示参数是否可由 `Arparma.query` 查询并获得结果，可用 `Query.available` 判断是否查询成功，`Query.result` 获取查询结果
+
+**Alconna** 默认依赖注入的目标参数皆不需要使用依赖注入函数， 该效果对于 `AlconnaMatcher.got_path` 下的 Arg 同样有效:
+
+```python
+async def handle(
+    result: CommandResult,
+    arp: Arparma,
+    dup: Duplication,
+    source: Alconna,
+    abc: str,  # 类似 Match, 但是若匹配结果不存在对应字段则跳过该 handler
+    foo: Match[str],
+    bar: Query[int] = Query("ttt.bar", 0)  # Query 仍然需要一个默认值来传递 path 参数
+):
+    ...
+```
+
+如果你更喜欢 Depends 式的依赖注入，`nonebot_plugin_alconna` 同时提供了一系列的依赖注入函数，他们包括：
+
+- `AlconnaResult`: `CommandResult` 类型的依赖注入函数
+- `AlconnaMatches`: `Arparma` 类型的依赖注入函数
+- `AlconnaDuplication`: `Duplication` 类型的依赖注入函数
+- `AlconnaMatch`: `Match` 类型的依赖注入函数，其能够额外传入一个 middleware 函数来处理得到的参数
+- `AlconnaQuery`: `Query` 类型的依赖注入函数，其能够额外传入一个 middleware 函数来处理得到的参数
+- `AlconnaExecResult`: 提供挂载在命令上的 callback 的返回结果 (`Dict[str, Any]`) 的依赖注入函数
+- `AlconnaExtension`: 提供指定类型的 `Extension` 的依赖注入函数
+
+实例:
+
+```python
+from nonebot import require
+require("nonebot_plugin_alconna")
+
+from nonebot_plugin_alconna import (
+    on_alconna, 
+    Match,
+    Query,
+    AlconnaMatch,
+    AlcResult
+)
+from arclet.alconna import Alconna, Args, Option, Arparma
+
+
+test = on_alconna(
+    Alconna(
+        "test",
+        Option("foo", Args["bar", int]),
+        Option("baz", Args["qux", bool, False])
+    ),
+    auto_send_output=True
+)
+
+@test.handle()
+async def handle_test1(result: AlcResult):
+    await test.send(f"matched: {result.matched}")
+    await test.send(f"maybe output: {result.output}")
+
+@test.handle()
+async def handle_test2(result: Arparma):
+    await test.send(f"head result: {result.header_result}")
+    await test.send(f"args: {result.all_matched_args}")
+
+@test.handle()
+async def handle_test3(bar: Match[int] = AlconnaMatch("bar")):
+    if bar.available:    
+        await test.send(f"foo={bar.result}")
+
+@test.handle()
+async def handle_test4(qux: Query[bool] = Query("baz.qux", False)):
+    if qux.available:
+        await test.send(f"baz.qux={qux.result}")
+```
+
+
+### 跨平台适配
+
+`uniseg` 模块属于 `nonebot-plugin-alconna` 的子插件，其提供了一套通用的消息组件，用于在 `nonebot-plugin-alconna` 下构建通用消息.
+
+#### 通用消息段
+
+适配器下的消息段标注会匹配适配器特定的 `MessageSegment`, 而通用消息段与适配器消息段的区别在于:  
+
+通用消息段会匹配多个适配器中相似类型的消息段，并返回 `uniseg` 模块中定义的 [`Segment` 模型](https://nonebot.dev/docs/next/best-practice/alconna/utils#%E9%80%9A%E7%94%A8%E6%B6%88%E6%81%AF%E6%AE%B5), 以达到**跨平台接收消息**的作用
+
+`uniseg` 模块提供了类似 `MessageSegment` 的通用消息段，并可在 `Alconna` 下直接标注使用：
+
+```python
+class Segment:
+    """基类标注"""
+
+class Text(Segment):
+    """Text对象, 表示一类文本元素"""
+    text: str
+    style: Optional[str]
+
+class At(Segment):
+    """At对象, 表示一类提醒某用户的元素"""
+    type: Literal["user", "role", "channel"]
+    target: str
+
+class AtAll(Segment):
+    """AtAll对象, 表示一类提醒所有人的元素"""
+
+class Emoji(Segment):
+    """Emoji对象, 表示一类表情元素"""
+    id: str
+    name: Optional[str]
+
+class Media(Segment):
+    url: Optional[str]
+    id: Optional[str]
+    path: Optional[str]
+    raw: Optional[bytes]
+
+class Image(Media):
+    """Image对象, 表示一类图片元素"""
+
+class Audio(Media):
+    """Audio对象, 表示一类音频元素"""
+
+class Voice(Media):
+    """Voice对象, 表示一类语音元素"""
+
+class Video(Media):
+    """Video对象, 表示一类视频元素"""
+
+class File(Segment):
+    """File对象, 表示一类文件元素"""
+    id: str
+    name: Optional[str]
+
+class Reply(Segment):
+    """Reply对象，表示一类回复消息"""
+    id: str
+    """此处不一定是消息ID，可能是其他ID，如消息序号等"""
+    msg: Optional[Union[Message, str]]
+    origin: Optional[Any]
+
+class Reference(Segment):
+    """Reference对象，表示一类引用消息。转发消息 (Forward) 也属于此类"""
+    id: Optional[str]
+    """此处不一定是消息ID，可能是其他ID，如消息序号等"""
+    content: Optional[Union[Message, str, List[Union[RefNode, CustomNode]]]]
+
+class Card(Segment):
+    type: Literal["xml", "json"]
+    raw: str
+
+class Other(Segment):
+    """其他 Segment"""
+```
+此类消息段通过 `UniMessage.export` 可以转为特定的 `MessageSegment`.
+
+
+#### 通用信息序列
+
+`uniseg` 模块还提供了一个类似于 `Message` 的 `UniMessage` 类型，其元素为经过通用标注转换后的通用消息段.
+
+你可以通过提供的 `UniversalMessage` 或 `UniMsg` 依赖注入器来获取 `UniMessage`.
+
+```python
+from nonebot_plugin_alconna.uniseg import UniMsg, At, Reply
+
+
+matcher = on_xxx(...)
+
+@matcher.handle()
+async def _(msg: UniMsg):
+    reply = msg[Reply, 0]
+    print(reply.origin)
+    if msg.has(At):
+        ats = msg.get(At)
+        print(ats)
+    ...
+```
+
+还可以通过 `UniMessage` 的 `export` 与 `send` 方法来**跨平台发送消息**.
+
+```python
+from nonebot import Bot, on_command
+from nonebot_plugin_alconna.uniseg import Image, UniMessage
+
+
+test = on_command("test")
+
+@test.handle()
+async def handle_test():
+    await test.send(await UniMessage(Image(path="path/to/img")).export())
+```
+`UniMessage.export` 会通过传入的 `bot: Bot` 参数，或上下文中的 `Bot` 对象读取适配器信息，并使用对应的生成方法把通用消息转为适配器对应的消息序列.
+
+而在 `AlconnaMatcher` 下，`got`, `send`, `reject` 等可以发送消息的方法皆支持使用 `UniMessage`，不需要手动调用 export 方法：
+
+```python
+from arclet.alconna import Alconna, Args
+from nonebot_plugin_alconna import Match, AlconnaMatcher, on_alconna
+from nonebot_plugin_alconna.uniseg import At,  UniMessage
+
+
+test_cmd = on_alconna(Alconna("test", Args["target?", At]))
+
+@test_cmd.handle()
+async def tt_h(matcher: AlconnaMatcher, target: Match[At]):
+    if target.available:
+        matcher.set_path_arg("target", target.result)
+
+@test_cmd.got_path("target", prompt="请输入目标")
+async def tt(target: At):
+    await test_cmd.send(UniMessage([target, "\ndone."]))
+```
+
+除此之外 `UniMessage.send` 方法基于 `UniMessage.export` 并调用各适配器下的发送消息方法，返回一个 `Receipt` 对象，用于修改/撤回消息：
+
+```python
+from nonebot import Bot, on_command
+from nonebot_plugin_alconna.uniseg import UniMessage
+
+
+test = on_command("test")
+
+@test.handle()
+async def handle():
+    receipt = await UniMessage.text("hello!").send(at_sender=True, reply_to=True)
+    await receipt.recall(delay=1)
+```
+**在响应器以外的地方，`bot` 参数必须手动传入**
+
+本插件为以下设配器提供了 **Segment** 标注，可用于匹配各适配器的 `MessageSegment`，也可用于创建 `MessageSegment`:
+
+| 协议名称                                                                | 路径                                   |
+|---------------------------------------------------------------------|--------------------------------------|
+| [OneBot 协议](https://github.com/nonebot/adapter-onebot)              | adapters.onebot11, adapters.onebot12 |
+| [Telegram](https://github.com/nonebot/adapter-telegram)             | adapters.telegram                    |
+| [飞书](https://github.com/nonebot/adapter-feishu)                     | adapters.feishu                      |
+| [GitHub](https://github.com/nonebot/adapter-github)                 | adapters.github                      |
+| [QQ bot](https://github.com/nonebot/adapter-qq)                     | adapters.qq                          |
+| [钉钉](https://github.com/nonebot/adapter-ding)                       | adapters.ding                        |
+| [Console](https://github.com/nonebot/adapter-console)               | adapters.console                     |
+| [开黑啦](https://github.com/Tian-que/nonebot-adapter-kaiheila)         | adapters.kook                        |
+| [Mirai](https://github.com/nonebot/adapter-mirai)                   | adapters.mirai, adapters.mirai2      |
+| [Ntchat](https://github.com/JustUndertaker/adapter-ntchat)          | adapters.ntchat                      |
+| [MineCraft](https://github.com/17TheWord/nonebot-adapter-minecraft) | adapters.minecraft                   |
+| [Walle-Q](https://github.com/onebot-walle/nonebot_adapter_walleq)   | adapters.onebot12                    |
+| [Discord](https://github.com/nonebot/adapter-discord)               | adapters.discord                     |
+| [Red 协议](https://github.com/nonebot/adapter-red)                    | adapters.red                         |
+| [Satori](https://github.com/nonebot/adapter-satori)                 | adapters.satori                      |
+| [Dodo IM](https://github.com/nonebot/adapter-dodo)                  | adapters.dodo                        |
+| [Kritor](https://github.com/nonebot/adapter-kritor)                 | adapters.kritor                      |  
+| [Tailchat](https://github.com/eya46/nonebot-adapter-tailchat)       | adapters.tailchat                    |
+
+
+##### 构造
+
+类比 `Message`, `UniMessage` 可以传入单个字符串/消息段或可迭代的字符串/消息段：
+
+```python
+from nonebot_plugin_alconna.uniseg import UniMessage, At
+
+
+msg = UniMessage("Hello")
+msg1 = UniMessage(At("user", "124"))
+msg2 = UniMessage(["Hello", At("user", "124")])
+```
+
+`UniMessage` 上同时存在便捷方法，令其可以链式地添加消息段：
+
+```python
+from nonebot_plugin_alconna.uniseg import UniMessage, At, Image
+
+
+msg = UniMessage.text("Hello").at("124").image(path="/path/to/img")
+assert msg == UniMessage(
+    ["Hello", At("user", "124"), Image(path="/path/to/img")]
+)
+```
+
+##### 拼接消息
+
+`str`、`UniMessage`、`Segment` 对象之间可以直接相加，相加均会返回一个新的 `UniMessage` 对象.
+
+```python
+# 消息序列与消息段相加
+UniMessage("text") + Text("text")
+# 消息序列与字符串相加
+UniMessage([Text("text")]) + "text"
+# 消息序列与消息序列相加
+UniMessage("text") + UniMessage([Text("text")])
+# 字符串与消息序列相加
+"text" + UniMessage([Text("text")])
+# 消息段与消息段相加
+Text("text") + Text("text")
+# 消息段与字符串相加
+Text("text") + "text"
+# 消息段与消息序列相加
+Text("text") + UniMessage([Text("text")])
+# 字符串与消息段相加
+"text" + Text("text")
+```
+
+如果需要在当前消息序列后直接拼接新的消息段，可以使用 `Message.append`、`Message.extend` 方法，或者使用自加.
+
+```python
+msg = UniMessage([Text("text")])
+# 自加
+msg += "text"
+msg += Text("text")
+msg += UniMessage([Text("text")])
+# 附加
+msg.append(Text("text"))
+# 扩展
+msg.extend([Text("text")])
+```
+
+##### 使用消息模板
+
+`UniMessage.template` 同样类似于 `Message.template`，可以用于格式化消息。大体用法参考 [消息模板](https://nonebot.dev/docs/next/tutorial/message#%E4%BD%BF%E7%94%A8%E6%B6%88%E6%81%AF%E6%A8%A1%E6%9D%BF).
+
+这里额外说明 `UniMessage.template` 的拓展控制符.
+
+相比 `Message`，UniMessage 对于 {:XXX} 做了另一类拓展。其能够识别例如 At(xxx, yyy) 或 Emoji(aaa, bbb)的字符串并执行.
+
+以 At(...) 为例使用通用消息段的拓展控制符：
+
+```python
+>>> from nonebot_plugin_alconna.uniseg import UniMessage
+>>>  UniMessage.template("{:At(user, target)}").format(target="123")
+UniMessage(At("user", "123"))
+>>> UniMessage.template("{:At(type=user, target=id)}").format(id="123")
+UniMessage(At("user", "123"))
+>>> UniMessage.template("{:At(type=user, target=123)}").format()
+UniMessage(At("user", "123"))
+```
+
+而在 `AlconnaMatcher` 中，{:XXX} 更进一步地提供了获取 `event` 和 `bot` 中的属性的功能.
+
+在AlconnaMatcher中使用通用消息段的拓展控制符:
+
+```python
+from arclet.alconna import Alconna, Args
+from nonebot_plugin_alconna import At, Match, UniMessage, AlconnaMatcher, on_alconna
+
+
+test_cmd = on_alconna(Alconna("test", Args["target?", At]))
+
+@test_cmd.handle()
+async def tt_h(matcher: AlconnaMatcher, target: Match[At]):
+    if target.available:
+        matcher.set_path_arg("target", target.result)
+
+@test_cmd.got_path(
+    "target",
+    prompt=UniMessage.template("{:At(user, $event.get_user_id())} 请确认目标")
+)
+async def tt():
+    await test_cmd.send(
+      UniMessage.template("{:At(user, $event.get_user_id())} 已确认目标为 {target}")
+    )
+```
+另外也有 `$message_id` 与 `$target` 两个特殊值
+
+##### 检查消息段
+
+我们可以通过 `in` 运算符或消息序列的 `has` 方法来：
+
+```python
+# 是否存在消息段
+At("user", "1234") in message
+# 是否存在指定类型的消息段
+At in message
+```
+
+我们还可以使用 `only` 方法来检查消息中是否仅包含指定的消息段。
+
+```python
+# 是否都为 "test"
+message.only("test")
+# 是否仅包含指定类型的消息段
+message.only(Text)
+```
+
+##### 获取消息纯文本
+
+类似于 `Message.extract_plain_text()`，用于获取通用消息的纯文本.
+```python 
+from nonebot_plugin_alconna.uniseg import UniMessage, At
+
+
+# 提取消息纯文本字符串
+assert UniMessage(
+    [At("user", "1234"), "text"]
+).extract_plain_text() == "text"
+```
+
+##### 遍历
+
+通用消息序列继承自 `List[Segment]` ，因此可以使用 `for` 循环遍历消息段。
+
+```python
+for segment in message:  # type: Segment
+	...
+```
+
+##### 过滤、索引与切片
+
+消息序列对列表的索引与切片进行了增强，在原有列表 `int` 索引与 `slice` 切片的基础上，支持 `type` 过滤索引与切片.
+
+```python
+from nonebot_plugin_alconna.uniseg import UniMessage, At, Text, Reply
+
+
+message = UniMessage(
+    [
+        Reply(...),
+        "text1",
+        At("user", "1234"),
+        "text2"
+    ]
+)
+# 索引
+message[0] == Reply(...)
+# 切片
+message[0:2] == UniMessage([Reply(...), Text("text1")])
+# 类型过滤
+message[At] == Message([At("user", "1234")])
+# 类型索引
+message[At, 0] == At("user", "1234")
+# 类型切片
+message[Text, 0:2] == UniMessage([Text("text1"), Text("text2")])
+```
+
+我们也可以通过消息序列的 `include`、`exclude` 方法进行类型过滤.
+
+```python 
+message.include(Text, At)  
+message.exclude(Reply)
+```
+
+同样的，消息序列对列表的 `index`、`count` 方法也进行了增强，可以用于索引指定类型的消息段.
+
+```python
+# 指定类型首个消息段索引
+message.index(Text) == 1
+# 指定类型消息段数量
+message.count(Text) == 2
+```
+
+此外，消息序列添加了一个 `get` 方法，可以用于获取指定类型指定个数的消息段.
+
+```python
+# 获取指定类型指定个数的消息段
+message.get(Text, 1) == UniMessage([Text("test1")])
+```
+
+##### 消息发送
+
+通用消息可用 `UniMessage.send` 发送自身：
+
+```python
+async def send(
+    self,
+    target: Union[Event, Target, None] = None,
+    bot: Optional[Bot] = None,
+    fallback: bool = True,
+    at_sender: Union[str, bool] = False,
+    reply_to: Union[str, bool] = False,
+) -> Receipt:
+```
+
+实际上，`UniMessage` 同时提供了获取消息事件 id 与消息发送对象的方法:
+
+```python
+from nonebot import Event, Bot
+from nonebot_plugin_alconna.uniseg import Target, get_message_id, get_target
+
+
+matcher = on_xxx(...)
+
+@matcher.handle()
+async def _(bot: Bot, event: Event):
+    target: Target = get_target(event, bot)
+    msg_id: str = get_message_id(event, bot)
+
+```
+
+`send`, `get_target`, `get_message_id` 中与 `event`, `bot` 相关的参数都会尝试从上下文中获取对象.
+
+其中，`Target`:
+
+```python
+class Target:
+    id: str
+    """目标id；若为群聊则为group_id或者channel_id，若为私聊则为user_id"""
+    parent_id: str = ""
+    """父级id；若为频道则为guild_id，其他情况为空字符串"""
+    channel: bool = False
+    """是否为频道，仅当目标平台同时支持群聊和频道时有效"""
+    private: bool = False
+    """是否为私聊"""
+    source: str = ""
+    """可能的事件id"""
+```
+是用来描述响应消息时的发送对象.
+
+同样的，你可以通过依赖注入的方式在响应器中直接获取它们.
+
+### 条件控制
+
+本插件可以通过 `assign` 来控制一个具体的响应函数是否在不满足条件时跳过响应
+
+```python
+from nonebot import require  
+require("nonebot_plugin_alconna")  
+...  
+  
+from arclet.alconna import Alconna, Subcommand, Option, Args  
+from nonebot_plugin_alconna import on_alconna, CommandResult  
+  
+pip = Alconna(  
+"pip",  
+Subcommand(  
+"install", Args["pak", str],  
+Option("--upgrade"),  
+Option("--force-reinstall")  
+),  
+Subcommand("list", Option("--out-dated"))  
+)  
+  
+pip_cmd = on_alconna(pip)  
+  
+# 仅在命令为 `pip install pip` 时响应  
+@pip_cmd.assign("install.pak", "pip")  
+async def update(res: CommandResult):  
+...  
+  
+# 仅在命令为 `pip list` 时响应  
+@pip_cmd.assign("list")  
+async def list_(res: CommandResult):  
+...  
+  
+# 在命令为 `pip install xxx` 时响应  
+@pip_cmd.assign("install")  
+async def install(res: CommandResult):  
+...
+```
+
+
+### 响应器创建装饰
+
+本插件提供了一个 `funcommand` 装饰器, 其用于将一个接受任意参数， 返回 `str` 或 `Message` 或 `MessageSegment` 的函数转换为命令响应器.
+
+```python
+from nonebot_plugin_alconna import funcommand
+
+
+@funcommand()
+async def echo(msg: str):
+    return msg
+```
+
+其等同于
+
+```python
+from arclet.alconna import Alconna, Args
+from nonebot_plugin_alconna import on_alconna, AlconnaMatch, Match
+
+
+echo = on_alconna(Alconna("echo", Args["msg", str]))
+
+@echo.handle()
+async def echo_exit(msg: Match[str] = AlconnaMatch("msg")):
+    await echo.finish(msg.result)
+
+```
+
+### 类Koishi构造器
+
+本插件提供了一个 `Command` 构造器，其基于 `arclet.alconna.tools` 中的 `AlconnaString`， 以类似 `Koishi` 中注册命令的方式来构建一个 **AlconnaMatcher** :
+
+```python
+from nonebot_plugin_alconna import Command, Arparma
+
+
+book = (
+    Command("book", "测试")
+    .option("writer", "-w <id:int>")
+    .option("writer", "--anonymous", {"id": 0})
+    .usage("book [-w <id:int> | --anonymous]")
+    .shortcut("测试", {"args": ["--anonymous"]})
+    .build()
+)
+
+@book.handle()
+async def _(arp: Arparma):
+    await book.send(str(arp.options))
+```
+
+甚至，你可以设置 `action` 来设定响应行为：
+
+```python
+book = (
+    Command("book", "测试")
+    .option("writer", "-w <id:int>")
+    .option("writer", "--anonymous", {"id": 0})
+    .usage("book [-w <id:int> | --anonymous]")
+    .shortcut("测试", {"args": ["--anonymous"]})
+    .action(lambda options: str(options))  # 会自动通过 bot.send 发送
+    .build()
+)
+```
+
+### 返回值回调
+
+在 `AlconnaMatch`, `AlconnaQuery` 或 `got_path` 中，你可以使用 `middleware` 参数来传入一个对返回值进行处理的函数:
+
+```python
+from nonebot_plugin_alconna import image_fetch
+
+
+mask_cmd = on_alconna(
+    Alconna("search", Args["img?", Image]),
+)
+
+
+@mask_cmd.handle()
+async def mask_h(matcher: AlconnaMatcher, img: Match[bytes] = AlconnaMatch("img", image_fetch)):
+    result = await search_img(img.result)
+    await matcher.send(result.content)
+```
+
+其中，`image_fetch` 是一个中间件，其接受一个 `Image` 对象，并提取图片的二进制数据返回.
+
+
+### 匹配拓展
+
+本插件提供了一个 `Extension` 类，其用于自定义 AlconnaMatcher 的部分行为.
+
+例如 `LLMExtension` (仅举例)：
+
+```python
+from nonebot_plugin_alconna import Extension, Alconna, on_alconna, Interface
+
+
+class LLMExtension(Extension):
+    @property
+    def priority(self) -> int:
+        return 10
+
+    @property
+    def id(self) -> str:
+        return "LLMExtension"
+
+    def __init__(self, llm):
+      self.llm = llm
+
+    def post_init(self, alc: Alconna) -> None:
+        self.llm.add_context(alc.command, alc.meta.description)
+
+    async def receive_wrapper(self, bot, event, receive):
+        resp = await self.llm.input(str(receive))
+        return receive.__class__(resp.content)
+
+    def before_catch(self, name, annotation, default):
+        return name == "llm"
+
+    def catch(self, interface: Interface):
+        if interface.name == "llm":
+            return self.llm
+
+matcher = on_alconna(
+    Alconna(...),
+    extensions=[LLMExtension(LLM)]
+)
+...
+```
+
+那么添加了 `LLMExtension` 的响应器便能接受任何能通过 llm 翻译为具体命令的自然语言消息，同时可以在响应器中为所有 `llm` 参数注入模型变量
+
+目前 `Extension` 的功能有:
+
+- `validate`: 对于事件的来源适配器或 bot 选择是否接受响应
+- `output_converter`: 输出信息的自定义转换方法
+- `message_provider`: 从传入事件中自定义提取消息的方法
+- `receive_provider`: 对传入的消息 (Message 或 UniMessage) 的额外处理
+- `permission_check`: 命令对消息解析并确认头部匹配（即确认选择响应）时对发送者的权限判断
+- `parse_wrapper`: 对命令解析结果的额外处理
+- `send_wrapper`: 对发送的消息 (Message 或 UniMessage) 的额外处理
+- `before_catch`: 自定义依赖注入的绑定确认函数
+- `catch`: 自定义依赖注入处理函数
+- `post_init`: 响应器创建后对命令对象的额外处理
+
+例如内置的 `DiscordSlashExtension`，其可自动将 Alconna 对象翻译成 slash 指令并注册，且将收到的指令交互事件转为指令供命令解析:
+
+```python
+from nonebot_plugin_alconna import Match, on_alconna
+from nonebot_plugin_alconna.adapters.discord import DiscordSlashExtension
+
+
+alc = Alconna(
+    ["/"],
+    "permission",
+    Subcommand("add", Args["plugin", str]["priority?", int]),
+    Option("remove", Args["plugin", str]["time?", int]),
+    meta=CommandMeta(description="权限管理"),
+)
+
+matcher = on_alconna(alc, extensions=[DiscordSlashExtension()])
+
+@matcher.assign("add")
+async def add(plugin: Match[str], priority: Match[int]):
+    await matcher.finish(f"added {plugin.result} with {priority.result if priority.available else 0}")
+
+@matcher.assign("remove")
+async def remove(plugin: Match[str], time: Match[int]):
+    await matcher.finish(f"removed {plugin.result} with {time.result if time.available else -1}")
+```
+
+TIP:  
+全局的 Extension 可延迟加载 (即若有全局拓展加载于部分 AlconnaMatcher 之后，这部分响应器会被追加拓展)
+
+
+## 本体
+[`Alconna`](https://github.com/ArcletProject/Alconna) 隶属于 `ArcletProject`，是一个简单、灵活、高效的命令参数解析器, 并且不局限于解析命令式字符串.
+
+我们通过一个例子来讲解 **Alconna** 的核心: `Args`, `Subcommand`, `Option`:
+
+```python
+from arclet.alconna import Alconna, Args, Subcommand, Option
+
+
+alc = Alconna(
+    "pip",
+    Subcommand(
+        "install",
+        Args["package", str],
+        Option("-r|--requirement", Args["file", str]),
+        Option("-i|--index-url", Args["url", str]),
+    )
+)
+
+res = alc.parse("pip install nonebot2 -i URL")
+
+print(res)
+# matched=True, header_match=(origin='pip' result='pip' matched=True groups={}), subcommands={'install': (value=Ellipsis args={'package': 'nonebot2'} options={'index-url': (value=None args={'url': 'URL'})} subcommands={})}, other_args={'package': 'nonebot2', 'url': 'URL'} 
+
+print(res.all_matched_args)
+# {'package': 'nonebot2', 'url': 'URL'}
+```
+
+这段代码通过`Alconna`创捷了一个接受主命令名为`pip`, 子命令为`install`且子命令接受一个 **Args** 参数`package`和二个 **Option** 参数`-r`和`-i`的命令参数解析器, 通过`parse`方法返回解析结果 **Arparma** 的实例.
+
+
+### 组成
+
+#### 命令头
+命令头是指命令的前缀 (Prefix) 与命令名 (Command) 的组合，例如 !help 中的 ! 与 help.
+|前缀|命令名|匹配内容|说明|
+|:-:|:-:|:-:|:-:|
+|-|"foo"|`"foo"`|无前缀的纯文字头|
+|-|123|`123`|无前缀的元素头|
+|-|"re:\d{2}"|`"32"`|无前缀的正则头|
+|-|int|`123` 或 `"456"`|无前缀的类型头|
+|[int, bool]|-|`True` 或 `123`|无名的元素类头|
+|["foo", "bar"]|-|`"foo"` 或 `"bar"`|无名的纯文字头|
+|["foo", "bar"]|"baz"|`"foobaz"` 或 `"barbaz"`|纯文字头|
+|[int, bool]|"foo"|`[123, "foo"]` 或 `[False, "foo"]`|类型头|
+|[123, 4567]|"foo"|`[123, "foo"]` 或 `[4567, "foo"]`|元素头|
+|[nepattern.NUMBER]|"bar"|`[123, "bar"]` 或 `[123.456, "bar"]`|表达式头|
+|[123, "foo"]|"bar"|`[123, "bar"]` 或 `"foobar"` 或 `["foo", "bar"]`|混合头|
+|[(int, "foo"), (456, "bar")]|"baz"|`[123, "foobaz"]` 或 `[456, "foobaz"]` 或 `[456, "barbaz"]`|对头|
+
+无前缀的类型头：此时会将传入的值尝试转为 BasePattern，例如 `int` 会转为 `nepattern.INTEGER`。此时命令头会匹配对应的类型， 例如 `int` 会匹配 `123` 或 `"456"`，但不会匹配 `"foo"`。同时，Alconna 会将命令头匹配到的值转为对应的类型，例如 `int` 会将 `"123"` 转为 `123`。
+
+**正则只在命令名上生效，命令前缀中的正则会被转义**  
+除了通过传入 `re:xxx` 来使用正则表达式外，Alconna 还提供了一种更加简洁的方式来使用正则表达式，那就是 Bracket Header。
+
+```python
+from alconna import Alconna
+
+
+alc = Alconna(".rd{roll:int}")
+assert alc.parse(".rd123").header["roll"] == 123
+```
+
+Bracket Header 类似 python 里的 f-string 写法，通过 "{}" 声明匹配类型
+
+"{}" 中的内容为 "name:type or pat"：
+- "{}", "{:}" **⇔** "(.+)", 占位符
+- "{foo}" **⇔** "(?P&lt;foo&gt;.+)"
+- "{:\d+}" **⇔** "(\d+)"
+- "{foo:int}" **⇔** "(?P&lt;foo&gt;\d+)"，其中 "int" 部分若能转为 `BasePattern` 则读取里面的表达式
+
+
+#### 参数声明(Args)
+
+`Args` 是用于声明命令参数的组件, 可以通过以下几种方式构造 **Args** :
+
+ - `Args[key, var, default][key1, var1, default1][...]` 
+ - `Args[(key, var, default)]`
+ - `Args.key[var, default]`
+ 
+其中，key **一定**是字符串，而 var 一般为参数的类型，default 为具体的值或者 **arclet.alconna.args.Field**.
+
+其与函数签名类似，但是允许含有默认值的参数在前；同时支持 keyword-only 参数不依照构造顺序传入 （但是仍需要在非 keyword-only 参数之后）.
+
+##### key
+`key` 的作用是用以标记解析出来的参数并存放于 **Arparma** 中，以方便用户调用.
+
+其有三种为 Args 注解的标识符:  `?`、`/`、 `!`, 标识符与 key 之间建议以 `;` 分隔:
+
+- `!` 标识符表示该处传入的参数应**不是**规定的类型，或**不在**指定的值中。
+- `?` 标识符表示该参数为**可选**参数，会在无参数匹配时跳过。
+- `/` 标识符表示该参数的类型注解需要隐藏。
+
+另外，对于参数的注释也可以标记在 `key` 中，其与 key 或者标识符 以 `#` 分割：  
+`foo#这是注释;?` 或 `foo?#这是注释`
+
+`Args` 中的 `key` 在实际命令中并不需要传入（keyword 参数除外）：
+
+```python
+from arclet.alconna import Alconna, Args  
+
+
+alc = Alconna("test", Args["foo", str])  
+alc.parse("test --foo abc") # 错误  
+alc.parse("test abc") # 正确
+```
+
+若需要 `test --foo abc`，你应该使用 `Option`：
+
+```python
+from arclet.alconna import Alconna, Args, Option
+
+
+alc = Alconna("test", Option("--foo", Args["foo", str]))
+```
+##### var
+var 负责命令参数的**类型检查**与**类型转化**.
+
+`Args` 的`var`表面上看需要传入一个 `type`，但实际上它需要的是一个 `nepattern.BasePattern` 的实例.
+
+```python
+from arclet.alconna import Args  
+from nepattern import BasePattern  
+
+
+# 表示 foo 参数需要匹配一个 @number 样式的字符串  
+args = Args["foo", BasePattern("@\d+")]
+```
+
+示例中可以传入 `str` 是因为 `str` 已经注册在了 `nepattern.global_patterns` 中，因此会替换为 `nepattern.global_patterns[str]`.
+
+`nepattern.global_patterns`默认支持的类型有：
+
+- `str`: 匹配任意字符串
+- `int`: 匹配整数
+- `float`: 匹配浮点数
+- `bool`: 匹配 `True` 与 `False` 以及他们小写形式
+- `hex`: 匹配 `0x` 开头的十六进制字符串
+- `url`: 匹配网址
+- `email`: 匹配 `xxxx@xxx` 的字符串
+- `ipv4`: 匹配 `xxx.xxx.xxx.xxx` 的字符串
+- `list`: 匹配类似 `["foo","bar","baz"]` 的字符串
+- `dict`: 匹配类似 `{"foo":"bar","baz":"qux"}` 的字符串
+- `datetime`: 传入一个 `datetime` 支持的格式字符串，或时间戳
+- `Any`: 匹配任意类型
+- `AnyString`: 匹配任意类型，转为 `str`
+- `Number`: 匹配 `int` 与 `float`，转为 `int`
+
+同时可以使用 typing 中的类型：
+
+- `Literal[X]`: 匹配其中的任意一个值
+- `Union[X, Y]`: 匹配其中的任意一个类型
+- `Optional[xxx]`: 会自动将默认值设为 `None`，并在解析失败时使用默认值
+- `List[X]`: 匹配一个列表，其中的元素为 `X` 类型
+- `Dict[X, Y]`: 匹配一个字典，其中的 key 为 `X` 类型，value 为 `Y` 类型
+- ...
+
+几类特殊的传入标记：
+
+- `"foo"`: 匹配字符串 "foo" (若没有某个 `BasePattern` 与之关联)
+- `RawStr("foo")`: 匹配字符串 "foo" (不会被 `BasePattern` 替换)
+- `"foo|bar|baz"`: 匹配 "foo" 或 "bar" 或 "baz"
+- `[foo, bar, Baz, ...]`: 匹配其中的任意一个值或类型
+- `Callable[[X], Y]`: 匹配一个参数为 `X` 类型的值，并返回通过该函数调用得到的 `Y` 类型的值
+- `"re:xxx"`: 匹配一个正则表达式 `xxx`，会返回 Match[0]
+- `"rep:xxx"`: 匹配一个正则表达式 `xxx`，会返回 `re.Match` 对象
+- `{foo: bar, baz: qux}`: 匹配字典中的任意一个键, 并返回对应的值 (特殊的键 ... 会匹配任意的值)
+- ...
+
+`MultiVar` 则是一个特殊的标注，用于告知解析器该参数可以接受多个值，其构造方法形如 `MultiVar(str)`。 同样的还有 `KeyWordVar`，其构造方法形如 `KeyWordVar(str)`，用于告知解析器该参数为一个 keyword-only 参数.
+
+TIPS:
+
+`MultiVar` 与 `KeyWordVar` 组合时，代表该参数为一个可接受多个 key-value 的参数，其构造方法形如 `MultiVar(KeyWordVar(str))`.
+
+`MultiVar` 与 `KeyWordVar` 也可以传入 `default` 参数，用于指定默认值.
+
+`MultiVar` 不能在 `KeyWordVar` 之后传入.
+
+
+#### **Option** 和 **Subcommand**
+`Option` 可以传入一组 `alias`，如 `Option("--foo|-F|--FOO|-f")` 或 `Option("--foo", alias=["-F"]`.  
+
+传入别名后，`option` 会选择其中长度最长的作为选项名称。若传入为 "--foo|-f"，则命令名称为 "--foo".  
+
+**在 Alconna 中 Option 的名字或别名**没有要求**必须在前面写上 `-`.**  
+
+`Subcommand` 可以传入自己的 **Option** 与 **Subcommand**.  
+
+他们拥有如下共同参数:
+
+- `help_text`: 传入该组件的帮助信息
+- `dest`: 被指定为解析完成时标注匹配结果的标识符，不传入时默认为选项或子命令的名称 (name)
+- `requires`: 一段指定顺序的字符串列表，作为唯一的前置序列与命令嵌套替换
+对于命令 `test foo bar baz qux <a:int>` 来讲，因为`foo bar baz` 仅需要判断是否相等, 所以可以这么编写：
+```python
+Alconna("test", Option("qux", Args.a[int], requires=["foo", "bar", "baz"]))
+```
+
+- `default`: 默认值，在该组件未被解析时使用使用该值替换。
path: nekro_agent/adapters/alconna.md
user: login: sourcery-ai[bot]
body: **issue (typo):** 错别字：重复的词 '使用使用'。

将 '使用使用' 替换为单个 '使用' 以获得正确的措辞。

```suggestion
- `default`: 默认值，在该组件未被解析时使用该值替换。
```

<details>
<summary>Original comment in English</summary>

**issue (typo):** Typo: Repeated word '使用使用'.

Replace '使用使用' with a single '使用' for correct phrasing.

```suggestion
- `default`: 默认值，在该组件未被解析时使用该值替换。
```

</details>
created_at: 2025-05-24T21:36:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,1451 @@
+# Nonebot Plugin Alconna 文档
+
+本文分为三部分:
+- [`nonebot_plugin_alconna` 的介绍与使用](#插件)
+- [`Alconna` 本体的介绍与使用](#本体)(请优先阅读本体)
+- [外部参考](#references)
+
+## 插件
+### 安装
+
+```shell
+pip install nonebot-plugin-alconna
+```
+
+或
+
+```shell
+nb plugin install nonebot-plugin-alconna
+```
+
+### 展示
+
+```python
+from nonebot.adapters.onebot.v12 import Message
+from nonebot_plugin_alconna import on_alconna, AlconnaMatches, At
+from nonebot_plugin_alconna.adapters.onebot12 import Image
+from arclet.alconna import Alconna, Args, Option, Arparma
+
+
+alc = Alconna("Hello!", Option("--spec", Args["target", At]))
+hello = on_alconna(alc, auto_send_output=True)
+
+@hello.handle()
+async def _(result: Arparma = AlconnaMatches()):
+    if result.find("spec"):
+        target = result.query[At]("spec.target")
+        seed = target.target
+        await hello.finish(Message(Image(await gen_image(seed))))
+    else:
+        await hello.finish("Hello!")
+```
+
+### 响应器使用
+
+本插件基于 **Alconna** , 为 **Nonebot** 提供了一类新的事件响应器辅助函数 `on_alconna`.
+```python
+def on_alconna(
+    command: Alconna | str,
+    skip_for_unmatch: bool = True,
+    auto_send_output: bool = True,
+    aliases: set[str | tuple[str, ...]] | None = None,
+    comp_config: CompConfig | None = None,
+    extensions: list[type[Extension] | Extension] | None = None,
+    exclude_ext: list[type[Extension] | str] | None = None,
+    use_origin: bool = False,
+    use_cmd_start: bool = False,
+    use_cmd_sep: bool = False,
+    **kwargs,
+    ...,
+):
+```
+- `command`: Alconna 命令或字符串，字符串将通过 `AlconnaFormat` 转换为 Alconna 命令
+- `skip_for_unmatch`: 是否在命令不匹配时跳过该响应
+- `auto_send_output`: 是否自动发送输出信息并跳过响应
+- `aliases`: 命令别名， 作用类似于 `on_command` 中的 aliases
+- `comp_config`: 补全会话配置， 不传入则不启用补全会话
+- `extensions`: 需要加载的匹配扩展, 可以是扩展类或扩展实例
+- `exclude_ext`: 需要排除的匹配扩展, 可以是扩展类或扩展的id
+- `use_origin`: 是否使用未经 to_me 等处理过的消息
+- `use_cmd_start`: 是否使用 COMMAND_START 作为命令前缀
+- `use_cmd_sep`: 是否使用 COMMAND_SEP 作为命令分隔符
+
+其中对于 `skip_for_unmatch` 和 `auto_send_output` 的效果如下:
+
+| skip \ auto | True           | False                         |
+|-------------|----------------|-------------------------------|
+| True        | 帮助文本等自动发送      | 帮助文本等不发送                      |
+| False       | 帮助文本等和错误信息自动发送 | 帮助文本等和错误信息设置到 `CommandResult` |
+
+
+`on_alconna` 返回的是 `Matcher` 的子类 `AlconnaMatcher`，其拓展了如下方法:
+
+- `.assign(path, value, or_not)`: 用于对包含多个选项/子命令的命令的分派处理(具体请看条件控制)
+- `.got_path(path, prompt, middleware)`: 在 `got` 方法的基础上，会以 path 对应的参数为准，读取传入 message 的最后一个消息段并验证转换
+- `.set_path_arg(key, value)`, `.get_path_arg(key)`: 类似 `set_arg` 和 `got_arg`，为 `got_path` 的特化版本
+- `.reject_path(path[, prompt, fallback])`: 类似于 `reject_arg`，对应 `got_path`
+- `.dispatch`: 同样的分派处理，但是是类似 `CommandGroup` 一样返回新的 `AlconnaMatcher`
+- `.got`, `send`, `reject`, ...: 拓展了 prompt 类型，即支持使用 `UniMessage` 作为 prompt
+
+`assign`实例:
+
+```python
+from nonebot import require
+require("nonebot_plugin_alconna")
+
+from arclet.alconna import Alconna, Option, Args
+from nonebot_plugin_alconna import on_alconna, AlconnaMatch, Match, UniMessage
+
+  
+login = on_alconna(Alconna(["/"], "login", Args["password?", str], Option("-r|--recall"))) # 这里["/"]指命令前缀必须是/
+
+@login.assign("recall") # /login -r
+async def login_exit():
+    await login.finish("已退出")
+
+@login.assign("password") # /login xxx
+async def login_handle(pw: Match[str] = AlconnaMatch("password")):
+    if pw.available:
+        login.set_path_arg("password", pw.result)
+```
+
+`dispatch`每个分发设置独立的 matcher:
+
+```python
+update_cmd = pip_cmd.dispatch("install.pak", "pip")
+
+@update_cmd.handle()
+async def update(arp: CommandResult = AlconnaResult()):
+    ...
+```
+
+`got_path`类似 Nonebot2 的got, 它与 `assign`，`Match`，`Query` 等地方一样，都需要指明 `path` 参数 (即对应 Arg 验证的路径)
+
+`got_path` 会获取消息的最后一个消息段并转为 path 对应的类型，例如示例中 `target` 对应的 Arg 里要求 str 或 At，则 got 后用户输入的消息只有为 text 或 at 才能进入处理函数.
+
+实例:
+
+```python
+from nonebot_plugin_alconna import At, Match, UniMessage, on_alconna
+
+
+test_cmd = on_alconna(Alconna("test", Args["target?", Union[str, At]]))
+
+@test_cmd.handle()
+async def tt_h(target: Match[Union[str, At]]):
+    if target.available:
+        test_cmd.set_path_arg("target", target.result)
+
+@test_cmd.got_path("target", prompt="请输入目标")
+async def tt(target: Union[str, At]):
+    await test_cmd.send(UniMessage(["ok\n", target]))
+```
+
+`path`支持 ~XXX 语法，其会把 ~ 替换为可能的父级路径:
+
+```python
+ pip = Alconna(
+     "pip",
+     Subcommand(
+         "install",
+         Args["pak", str],
+         Option("--upgrade|-U"),
+         Option("--force-reinstall"),
+     ),
+     Subcommand("list", Option("--out-dated")),
+ )
+
+ pipcmd = on_alconna(pip)
+ pip_install_cmd = pipcmd.dispatch("install")
+
+
+ @pip_install_cmd.assign("~upgrade")
+ async def pip1_u(pak: Query[str] = Query("~pak")):
+     await pip_install_cmd.finish(f"pip upgrading {pak.result}...")
+```
+
+
+### Alconna的依赖注入
+
+本插件提供了一系列依赖注入函数，便于在响应函数中获取解析结果:
+
+- `AlconnaResult`: `CommandResult` 类型的依赖注入函数
+- `AlconnaMatches`: `Arparma` 类型的依赖注入函数
+- `AlconnaDuplication`: `Duplication` 类型的依赖注入函数
+- `AlconnaMatch`: `Match` 类型的依赖注入函数
+- `AlconnaQuery`: `Query` 类型的依赖注入函数
+
+同时，基于 [`Annotated` 支持](https://github.com/nonebot/nonebot2/pull/1832), 添加了两类注解:
+
+- `AlcMatches`：同 `AlconnaMatches`
+- `AlcResult`：同 `AlconnaResult`
+
+可以看到，本插件提供了几类额外的模型:
+
+- `CommandResult`: 解析结果，包括了源命令 `source: Alconna` ，解析结果 `result: Arparma`，以及可能的输出信息 `output: str | None` 字段
+- `Match`: 匹配项，表示参数是否存在于 `all_matched_args` 内，可用 `Match.available` 判断是否匹配，`Match.result` 获取匹配的值
+- `Query`: 查询项，表示参数是否可由 `Arparma.query` 查询并获得结果，可用 `Query.available` 判断是否查询成功，`Query.result` 获取查询结果
+
+**Alconna** 默认依赖注入的目标参数皆不需要使用依赖注入函数， 该效果对于 `AlconnaMatcher.got_path` 下的 Arg 同样有效:
+
+```python
+async def handle(
+    result: CommandResult,
+    arp: Arparma,
+    dup: Duplication,
+    source: Alconna,
+    abc: str,  # 类似 Match, 但是若匹配结果不存在对应字段则跳过该 handler
+    foo: Match[str],
+    bar: Query[int] = Query("ttt.bar", 0)  # Query 仍然需要一个默认值来传递 path 参数
+):
+    ...
+```
+
+如果你更喜欢 Depends 式的依赖注入，`nonebot_plugin_alconna` 同时提供了一系列的依赖注入函数，他们包括：
+
+- `AlconnaResult`: `CommandResult` 类型的依赖注入函数
+- `AlconnaMatches`: `Arparma` 类型的依赖注入函数
+- `AlconnaDuplication`: `Duplication` 类型的依赖注入函数
+- `AlconnaMatch`: `Match` 类型的依赖注入函数，其能够额外传入一个 middleware 函数来处理得到的参数
+- `AlconnaQuery`: `Query` 类型的依赖注入函数，其能够额外传入一个 middleware 函数来处理得到的参数
+- `AlconnaExecResult`: 提供挂载在命令上的 callback 的返回结果 (`Dict[str, Any]`) 的依赖注入函数
+- `AlconnaExtension`: 提供指定类型的 `Extension` 的依赖注入函数
+
+实例:
+
+```python
+from nonebot import require
+require("nonebot_plugin_alconna")
+
+from nonebot_plugin_alconna import (
+    on_alconna, 
+    Match,
+    Query,
+    AlconnaMatch,
+    AlcResult
+)
+from arclet.alconna import Alconna, Args, Option, Arparma
+
+
+test = on_alconna(
+    Alconna(
+        "test",
+        Option("foo", Args["bar", int]),
+        Option("baz", Args["qux", bool, False])
+    ),
+    auto_send_output=True
+)
+
+@test.handle()
+async def handle_test1(result: AlcResult):
+    await test.send(f"matched: {result.matched}")
+    await test.send(f"maybe output: {result.output}")
+
+@test.handle()
+async def handle_test2(result: Arparma):
+    await test.send(f"head result: {result.header_result}")
+    await test.send(f"args: {result.all_matched_args}")
+
+@test.handle()
+async def handle_test3(bar: Match[int] = AlconnaMatch("bar")):
+    if bar.available:    
+        await test.send(f"foo={bar.result}")
+
+@test.handle()
+async def handle_test4(qux: Query[bool] = Query("baz.qux", False)):
+    if qux.available:
+        await test.send(f"baz.qux={qux.result}")
+```
+
+
+### 跨平台适配
+
+`uniseg` 模块属于 `nonebot-plugin-alconna` 的子插件，其提供了一套通用的消息组件，用于在 `nonebot-plugin-alconna` 下构建通用消息.
+
+#### 通用消息段
+
+适配器下的消息段标注会匹配适配器特定的 `MessageSegment`, 而通用消息段与适配器消息段的区别在于:  
+
+通用消息段会匹配多个适配器中相似类型的消息段，并返回 `uniseg` 模块中定义的 [`Segment` 模型](https://nonebot.dev/docs/next/best-practice/alconna/utils#%E9%80%9A%E7%94%A8%E6%B6%88%E6%81%AF%E6%AE%B5), 以达到**跨平台接收消息**的作用
+
+`uniseg` 模块提供了类似 `MessageSegment` 的通用消息段，并可在 `Alconna` 下直接标注使用：
+
+```python
+class Segment:
+    """基类标注"""
+
+class Text(Segment):
+    """Text对象, 表示一类文本元素"""
+    text: str
+    style: Optional[str]
+
+class At(Segment):
+    """At对象, 表示一类提醒某用户的元素"""
+    type: Literal["user", "role", "channel"]
+    target: str
+
+class AtAll(Segment):
+    """AtAll对象, 表示一类提醒所有人的元素"""
+
+class Emoji(Segment):
+    """Emoji对象, 表示一类表情元素"""
+    id: str
+    name: Optional[str]
+
+class Media(Segment):
+    url: Optional[str]
+    id: Optional[str]
+    path: Optional[str]
+    raw: Optional[bytes]
+
+class Image(Media):
+    """Image对象, 表示一类图片元素"""
+
+class Audio(Media):
+    """Audio对象, 表示一类音频元素"""
+
+class Voice(Media):
+    """Voice对象, 表示一类语音元素"""
+
+class Video(Media):
+    """Video对象, 表示一类视频元素"""
+
+class File(Segment):
+    """File对象, 表示一类文件元素"""
+    id: str
+    name: Optional[str]
+
+class Reply(Segment):
+    """Reply对象，表示一类回复消息"""
+    id: str
+    """此处不一定是消息ID，可能是其他ID，如消息序号等"""
+    msg: Optional[Union[Message, str]]
+    origin: Optional[Any]
+
+class Reference(Segment):
+    """Reference对象，表示一类引用消息。转发消息 (Forward) 也属于此类"""
+    id: Optional[str]
+    """此处不一定是消息ID，可能是其他ID，如消息序号等"""
+    content: Optional[Union[Message, str, List[Union[RefNode, CustomNode]]]]
+
+class Card(Segment):
+    type: Literal["xml", "json"]
+    raw: str
+
+class Other(Segment):
+    """其他 Segment"""
+```
+此类消息段通过 `UniMessage.export` 可以转为特定的 `MessageSegment`.
+
+
+#### 通用信息序列
+
+`uniseg` 模块还提供了一个类似于 `Message` 的 `UniMessage` 类型，其元素为经过通用标注转换后的通用消息段.
+
+你可以通过提供的 `UniversalMessage` 或 `UniMsg` 依赖注入器来获取 `UniMessage`.
+
+```python
+from nonebot_plugin_alconna.uniseg import UniMsg, At, Reply
+
+
+matcher = on_xxx(...)
+
+@matcher.handle()
+async def _(msg: UniMsg):
+    reply = msg[Reply, 0]
+    print(reply.origin)
+    if msg.has(At):
+        ats = msg.get(At)
+        print(ats)
+    ...
+```
+
+还可以通过 `UniMessage` 的 `export` 与 `send` 方法来**跨平台发送消息**.
+
+```python
+from nonebot import Bot, on_command
+from nonebot_plugin_alconna.uniseg import Image, UniMessage
+
+
+test = on_command("test")
+
+@test.handle()
+async def handle_test():
+    await test.send(await UniMessage(Image(path="path/to/img")).export())
+```
+`UniMessage.export` 会通过传入的 `bot: Bot` 参数，或上下文中的 `Bot` 对象读取适配器信息，并使用对应的生成方法把通用消息转为适配器对应的消息序列.
+
+而在 `AlconnaMatcher` 下，`got`, `send`, `reject` 等可以发送消息的方法皆支持使用 `UniMessage`，不需要手动调用 export 方法：
+
+```python
+from arclet.alconna import Alconna, Args
+from nonebot_plugin_alconna import Match, AlconnaMatcher, on_alconna
+from nonebot_plugin_alconna.uniseg import At,  UniMessage
+
+
+test_cmd = on_alconna(Alconna("test", Args["target?", At]))
+
+@test_cmd.handle()
+async def tt_h(matcher: AlconnaMatcher, target: Match[At]):
+    if target.available:
+        matcher.set_path_arg("target", target.result)
+
+@test_cmd.got_path("target", prompt="请输入目标")
+async def tt(target: At):
+    await test_cmd.send(UniMessage([target, "\ndone."]))
+```
+
+除此之外 `UniMessage.send` 方法基于 `UniMessage.export` 并调用各适配器下的发送消息方法，返回一个 `Receipt` 对象，用于修改/撤回消息：
+
+```python
+from nonebot import Bot, on_command
+from nonebot_plugin_alconna.uniseg import UniMessage
+
+
+test = on_command("test")
+
+@test.handle()
+async def handle():
+    receipt = await UniMessage.text("hello!").send(at_sender=True, reply_to=True)
+    await receipt.recall(delay=1)
+```
+**在响应器以外的地方，`bot` 参数必须手动传入**
+
+本插件为以下设配器提供了 **Segment** 标注，可用于匹配各适配器的 `MessageSegment`，也可用于创建 `MessageSegment`:
+
+| 协议名称                                                                | 路径                                   |
+|---------------------------------------------------------------------|--------------------------------------|
+| [OneBot 协议](https://github.com/nonebot/adapter-onebot)              | adapters.onebot11, adapters.onebot12 |
+| [Telegram](https://github.com/nonebot/adapter-telegram)             | adapters.telegram                    |
+| [飞书](https://github.com/nonebot/adapter-feishu)                     | adapters.feishu                      |
+| [GitHub](https://github.com/nonebot/adapter-github)                 | adapters.github                      |
+| [QQ bot](https://github.com/nonebot/adapter-qq)                     | adapters.qq                          |
+| [钉钉](https://github.com/nonebot/adapter-ding)                       | adapters.ding                        |
+| [Console](https://github.com/nonebot/adapter-console)               | adapters.console                     |
+| [开黑啦](https://github.com/Tian-que/nonebot-adapter-kaiheila)         | adapters.kook                        |
+| [Mirai](https://github.com/nonebot/adapter-mirai)                   | adapters.mirai, adapters.mirai2      |
+| [Ntchat](https://github.com/JustUndertaker/adapter-ntchat)          | adapters.ntchat                      |
+| [MineCraft](https://github.com/17TheWord/nonebot-adapter-minecraft) | adapters.minecraft                   |
+| [Walle-Q](https://github.com/onebot-walle/nonebot_adapter_walleq)   | adapters.onebot12                    |
+| [Discord](https://github.com/nonebot/adapter-discord)               | adapters.discord                     |
+| [Red 协议](https://github.com/nonebot/adapter-red)                    | adapters.red                         |
+| [Satori](https://github.com/nonebot/adapter-satori)                 | adapters.satori                      |
+| [Dodo IM](https://github.com/nonebot/adapter-dodo)                  | adapters.dodo                        |
+| [Kritor](https://github.com/nonebot/adapter-kritor)                 | adapters.kritor                      |  
+| [Tailchat](https://github.com/eya46/nonebot-adapter-tailchat)       | adapters.tailchat                    |
+
+
+##### 构造
+
+类比 `Message`, `UniMessage` 可以传入单个字符串/消息段或可迭代的字符串/消息段：
+
+```python
+from nonebot_plugin_alconna.uniseg import UniMessage, At
+
+
+msg = UniMessage("Hello")
+msg1 = UniMessage(At("user", "124"))
+msg2 = UniMessage(["Hello", At("user", "124")])
+```
+
+`UniMessage` 上同时存在便捷方法，令其可以链式地添加消息段：
+
+```python
+from nonebot_plugin_alconna.uniseg import UniMessage, At, Image
+
+
+msg = UniMessage.text("Hello").at("124").image(path="/path/to/img")
+assert msg == UniMessage(
+    ["Hello", At("user", "124"), Image(path="/path/to/img")]
+)
+```
+
+##### 拼接消息
+
+`str`、`UniMessage`、`Segment` 对象之间可以直接相加，相加均会返回一个新的 `UniMessage` 对象.
+
+```python
+# 消息序列与消息段相加
+UniMessage("text") + Text("text")
+# 消息序列与字符串相加
+UniMessage([Text("text")]) + "text"
+# 消息序列与消息序列相加
+UniMessage("text") + UniMessage([Text("text")])
+# 字符串与消息序列相加
+"text" + UniMessage([Text("text")])
+# 消息段与消息段相加
+Text("text") + Text("text")
+# 消息段与字符串相加
+Text("text") + "text"
+# 消息段与消息序列相加
+Text("text") + UniMessage([Text("text")])
+# 字符串与消息段相加
+"text" + Text("text")
+```
+
+如果需要在当前消息序列后直接拼接新的消息段，可以使用 `Message.append`、`Message.extend` 方法，或者使用自加.
+
+```python
+msg = UniMessage([Text("text")])
+# 自加
+msg += "text"
+msg += Text("text")
+msg += UniMessage([Text("text")])
+# 附加
+msg.append(Text("text"))
+# 扩展
+msg.extend([Text("text")])
+```
+
+##### 使用消息模板
+
+`UniMessage.template` 同样类似于 `Message.template`，可以用于格式化消息。大体用法参考 [消息模板](https://nonebot.dev/docs/next/tutorial/message#%E4%BD%BF%E7%94%A8%E6%B6%88%E6%81%AF%E6%A8%A1%E6%9D%BF).
+
+这里额外说明 `UniMessage.template` 的拓展控制符.
+
+相比 `Message`，UniMessage 对于 {:XXX} 做了另一类拓展。其能够识别例如 At(xxx, yyy) 或 Emoji(aaa, bbb)的字符串并执行.
+
+以 At(...) 为例使用通用消息段的拓展控制符：
+
+```python
+>>> from nonebot_plugin_alconna.uniseg import UniMessage
+>>>  UniMessage.template("{:At(user, target)}").format(target="123")
+UniMessage(At("user", "123"))
+>>> UniMessage.template("{:At(type=user, target=id)}").format(id="123")
+UniMessage(At("user", "123"))
+>>> UniMessage.template("{:At(type=user, target=123)}").format()
+UniMessage(At("user", "123"))
+```
+
+而在 `AlconnaMatcher` 中，{:XXX} 更进一步地提供了获取 `event` 和 `bot` 中的属性的功能.
+
+在AlconnaMatcher中使用通用消息段的拓展控制符:
+
+```python
+from arclet.alconna import Alconna, Args
+from nonebot_plugin_alconna import At, Match, UniMessage, AlconnaMatcher, on_alconna
+
+
+test_cmd = on_alconna(Alconna("test", Args["target?", At]))
+
+@test_cmd.handle()
+async def tt_h(matcher: AlconnaMatcher, target: Match[At]):
+    if target.available:
+        matcher.set_path_arg("target", target.result)
+
+@test_cmd.got_path(
+    "target",
+    prompt=UniMessage.template("{:At(user, $event.get_user_id())} 请确认目标")
+)
+async def tt():
+    await test_cmd.send(
+      UniMessage.template("{:At(user, $event.get_user_id())} 已确认目标为 {target}")
+    )
+```
+另外也有 `$message_id` 与 `$target` 两个特殊值
+
+##### 检查消息段
+
+我们可以通过 `in` 运算符或消息序列的 `has` 方法来：
+
+```python
+# 是否存在消息段
+At("user", "1234") in message
+# 是否存在指定类型的消息段
+At in message
+```
+
+我们还可以使用 `only` 方法来检查消息中是否仅包含指定的消息段。
+
+```python
+# 是否都为 "test"
+message.only("test")
+# 是否仅包含指定类型的消息段
+message.only(Text)
+```
+
+##### 获取消息纯文本
+
+类似于 `Message.extract_plain_text()`，用于获取通用消息的纯文本.
+```python 
+from nonebot_plugin_alconna.uniseg import UniMessage, At
+
+
+# 提取消息纯文本字符串
+assert UniMessage(
+    [At("user", "1234"), "text"]
+).extract_plain_text() == "text"
+```
+
+##### 遍历
+
+通用消息序列继承自 `List[Segment]` ，因此可以使用 `for` 循环遍历消息段。
+
+```python
+for segment in message:  # type: Segment
+	...
+```
+
+##### 过滤、索引与切片
+
+消息序列对列表的索引与切片进行了增强，在原有列表 `int` 索引与 `slice` 切片的基础上，支持 `type` 过滤索引与切片.
+
+```python
+from nonebot_plugin_alconna.uniseg import UniMessage, At, Text, Reply
+
+
+message = UniMessage(
+    [
+        Reply(...),
+        "text1",
+        At("user", "1234"),
+        "text2"
+    ]
+)
+# 索引
+message[0] == Reply(...)
+# 切片
+message[0:2] == UniMessage([Reply(...), Text("text1")])
+# 类型过滤
+message[At] == Message([At("user", "1234")])
+# 类型索引
+message[At, 0] == At("user", "1234")
+# 类型切片
+message[Text, 0:2] == UniMessage([Text("text1"), Text("text2")])
+```
+
+我们也可以通过消息序列的 `include`、`exclude` 方法进行类型过滤.
+
+```python 
+message.include(Text, At)  
+message.exclude(Reply)
+```
+
+同样的，消息序列对列表的 `index`、`count` 方法也进行了增强，可以用于索引指定类型的消息段.
+
+```python
+# 指定类型首个消息段索引
+message.index(Text) == 1
+# 指定类型消息段数量
+message.count(Text) == 2
+```
+
+此外，消息序列添加了一个 `get` 方法，可以用于获取指定类型指定个数的消息段.
+
+```python
+# 获取指定类型指定个数的消息段
+message.get(Text, 1) == UniMessage([Text("test1")])
+```
+
+##### 消息发送
+
+通用消息可用 `UniMessage.send` 发送自身：
+
+```python
+async def send(
+    self,
+    target: Union[Event, Target, None] = None,
+    bot: Optional[Bot] = None,
+    fallback: bool = True,
+    at_sender: Union[str, bool] = False,
+    reply_to: Union[str, bool] = False,
+) -> Receipt:
+```
+
+实际上，`UniMessage` 同时提供了获取消息事件 id 与消息发送对象的方法:
+
+```python
+from nonebot import Event, Bot
+from nonebot_plugin_alconna.uniseg import Target, get_message_id, get_target
+
+
+matcher = on_xxx(...)
+
+@matcher.handle()
+async def _(bot: Bot, event: Event):
+    target: Target = get_target(event, bot)
+    msg_id: str = get_message_id(event, bot)
+
+```
+
+`send`, `get_target`, `get_message_id` 中与 `event`, `bot` 相关的参数都会尝试从上下文中获取对象.
+
+其中，`Target`:
+
+```python
+class Target:
+    id: str
+    """目标id；若为群聊则为group_id或者channel_id，若为私聊则为user_id"""
+    parent_id: str = ""
+    """父级id；若为频道则为guild_id，其他情况为空字符串"""
+    channel: bool = False
+    """是否为频道，仅当目标平台同时支持群聊和频道时有效"""
+    private: bool = False
+    """是否为私聊"""
+    source: str = ""
+    """可能的事件id"""
+```
+是用来描述响应消息时的发送对象.
+
+同样的，你可以通过依赖注入的方式在响应器中直接获取它们.
+
+### 条件控制
+
+本插件可以通过 `assign` 来控制一个具体的响应函数是否在不满足条件时跳过响应
+
+```python
+from nonebot import require  
+require("nonebot_plugin_alconna")  
+...  
+  
+from arclet.alconna import Alconna, Subcommand, Option, Args  
+from nonebot_plugin_alconna import on_alconna, CommandResult  
+  
+pip = Alconna(  
+"pip",  
+Subcommand(  
+"install", Args["pak", str],  
+Option("--upgrade"),  
+Option("--force-reinstall")  
+),  
+Subcommand("list", Option("--out-dated"))  
+)  
+  
+pip_cmd = on_alconna(pip)  
+  
+# 仅在命令为 `pip install pip` 时响应  
+@pip_cmd.assign("install.pak", "pip")  
+async def update(res: CommandResult):  
+...  
+  
+# 仅在命令为 `pip list` 时响应  
+@pip_cmd.assign("list")  
+async def list_(res: CommandResult):  
+...  
+  
+# 在命令为 `pip install xxx` 时响应  
+@pip_cmd.assign("install")  
+async def install(res: CommandResult):  
+...
+```
+
+
+### 响应器创建装饰
+
+本插件提供了一个 `funcommand` 装饰器, 其用于将一个接受任意参数， 返回 `str` 或 `Message` 或 `MessageSegment` 的函数转换为命令响应器.
+
+```python
+from nonebot_plugin_alconna import funcommand
+
+
+@funcommand()
+async def echo(msg: str):
+    return msg
+```
+
+其等同于
+
+```python
+from arclet.alconna import Alconna, Args
+from nonebot_plugin_alconna import on_alconna, AlconnaMatch, Match
+
+
+echo = on_alconna(Alconna("echo", Args["msg", str]))
+
+@echo.handle()
+async def echo_exit(msg: Match[str] = AlconnaMatch("msg")):
+    await echo.finish(msg.result)
+
+```
+
+### 类Koishi构造器
+
+本插件提供了一个 `Command` 构造器，其基于 `arclet.alconna.tools` 中的 `AlconnaString`， 以类似 `Koishi` 中注册命令的方式来构建一个 **AlconnaMatcher** :
+
+```python
+from nonebot_plugin_alconna import Command, Arparma
+
+
+book = (
+    Command("book", "测试")
+    .option("writer", "-w <id:int>")
+    .option("writer", "--anonymous", {"id": 0})
+    .usage("book [-w <id:int> | --anonymous]")
+    .shortcut("测试", {"args": ["--anonymous"]})
+    .build()
+)
+
+@book.handle()
+async def _(arp: Arparma):
+    await book.send(str(arp.options))
+```
+
+甚至，你可以设置 `action` 来设定响应行为：
+
+```python
+book = (
+    Command("book", "测试")
+    .option("writer", "-w <id:int>")
+    .option("writer", "--anonymous", {"id": 0})
+    .usage("book [-w <id:int> | --anonymous]")
+    .shortcut("测试", {"args": ["--anonymous"]})
+    .action(lambda options: str(options))  # 会自动通过 bot.send 发送
+    .build()
+)
+```
+
+### 返回值回调
+
+在 `AlconnaMatch`, `AlconnaQuery` 或 `got_path` 中，你可以使用 `middleware` 参数来传入一个对返回值进行处理的函数:
+
+```python
+from nonebot_plugin_alconna import image_fetch
+
+
+mask_cmd = on_alconna(
+    Alconna("search", Args["img?", Image]),
+)
+
+
+@mask_cmd.handle()
+async def mask_h(matcher: AlconnaMatcher, img: Match[bytes] = AlconnaMatch("img", image_fetch)):
+    result = await search_img(img.result)
+    await matcher.send(result.content)
+```
+
+其中，`image_fetch` 是一个中间件，其接受一个 `Image` 对象，并提取图片的二进制数据返回.
+
+
+### 匹配拓展
+
+本插件提供了一个 `Extension` 类，其用于自定义 AlconnaMatcher 的部分行为.
+
+例如 `LLMExtension` (仅举例)：
+
+```python
+from nonebot_plugin_alconna import Extension, Alconna, on_alconna, Interface
+
+
+class LLMExtension(Extension):
+    @property
+    def priority(self) -> int:
+        return 10
+
+    @property
+    def id(self) -> str:
+        return "LLMExtension"
+
+    def __init__(self, llm):
+      self.llm = llm
+
+    def post_init(self, alc: Alconna) -> None:
+        self.llm.add_context(alc.command, alc.meta.description)
+
+    async def receive_wrapper(self, bot, event, receive):
+        resp = await self.llm.input(str(receive))
+        return receive.__class__(resp.content)
+
+    def before_catch(self, name, annotation, default):
+        return name == "llm"
+
+    def catch(self, interface: Interface):
+        if interface.name == "llm":
+            return self.llm
+
+matcher = on_alconna(
+    Alconna(...),
+    extensions=[LLMExtension(LLM)]
+)
+...
+```
+
+那么添加了 `LLMExtension` 的响应器便能接受任何能通过 llm 翻译为具体命令的自然语言消息，同时可以在响应器中为所有 `llm` 参数注入模型变量
+
+目前 `Extension` 的功能有:
+
+- `validate`: 对于事件的来源适配器或 bot 选择是否接受响应
+- `output_converter`: 输出信息的自定义转换方法
+- `message_provider`: 从传入事件中自定义提取消息的方法
+- `receive_provider`: 对传入的消息 (Message 或 UniMessage) 的额外处理
+- `permission_check`: 命令对消息解析并确认头部匹配（即确认选择响应）时对发送者的权限判断
+- `parse_wrapper`: 对命令解析结果的额外处理
+- `send_wrapper`: 对发送的消息 (Message 或 UniMessage) 的额外处理
+- `before_catch`: 自定义依赖注入的绑定确认函数
+- `catch`: 自定义依赖注入处理函数
+- `post_init`: 响应器创建后对命令对象的额外处理
+
+例如内置的 `DiscordSlashExtension`，其可自动将 Alconna 对象翻译成 slash 指令并注册，且将收到的指令交互事件转为指令供命令解析:
+
+```python
+from nonebot_plugin_alconna import Match, on_alconna
+from nonebot_plugin_alconna.adapters.discord import DiscordSlashExtension
+
+
+alc = Alconna(
+    ["/"],
+    "permission",
+    Subcommand("add", Args["plugin", str]["priority?", int]),
+    Option("remove", Args["plugin", str]["time?", int]),
+    meta=CommandMeta(description="权限管理"),
+)
+
+matcher = on_alconna(alc, extensions=[DiscordSlashExtension()])
+
+@matcher.assign("add")
+async def add(plugin: Match[str], priority: Match[int]):
+    await matcher.finish(f"added {plugin.result} with {priority.result if priority.available else 0}")
+
+@matcher.assign("remove")
+async def remove(plugin: Match[str], time: Match[int]):
+    await matcher.finish(f"removed {plugin.result} with {time.result if time.available else -1}")
+```
+
+TIP:  
+全局的 Extension 可延迟加载 (即若有全局拓展加载于部分 AlconnaMatcher 之后，这部分响应器会被追加拓展)
+
+
+## 本体
+[`Alconna`](https://github.com/ArcletProject/Alconna) 隶属于 `ArcletProject`，是一个简单、灵活、高效的命令参数解析器, 并且不局限于解析命令式字符串.
+
+我们通过一个例子来讲解 **Alconna** 的核心: `Args`, `Subcommand`, `Option`:
+
+```python
+from arclet.alconna import Alconna, Args, Subcommand, Option
+
+
+alc = Alconna(
+    "pip",
+    Subcommand(
+        "install",
+        Args["package", str],
+        Option("-r|--requirement", Args["file", str]),
+        Option("-i|--index-url", Args["url", str]),
+    )
+)
+
+res = alc.parse("pip install nonebot2 -i URL")
+
+print(res)
+# matched=True, header_match=(origin='pip' result='pip' matched=True groups={}), subcommands={'install': (value=Ellipsis args={'package': 'nonebot2'} options={'index-url': (value=None args={'url': 'URL'})} subcommands={})}, other_args={'package': 'nonebot2', 'url': 'URL'} 
+
+print(res.all_matched_args)
+# {'package': 'nonebot2', 'url': 'URL'}
+```
+
+这段代码通过`Alconna`创捷了一个接受主命令名为`pip`, 子命令为`install`且子命令接受一个 **Args** 参数`package`和二个 **Option** 参数`-r`和`-i`的命令参数解析器, 通过`parse`方法返回解析结果 **Arparma** 的实例.
+
+
+### 组成
+
+#### 命令头
+命令头是指命令的前缀 (Prefix) 与命令名 (Command) 的组合，例如 !help 中的 ! 与 help.
+|前缀|命令名|匹配内容|说明|
+|:-:|:-:|:-:|:-:|
+|-|"foo"|`"foo"`|无前缀的纯文字头|
+|-|123|`123`|无前缀的元素头|
+|-|"re:\d{2}"|`"32"`|无前缀的正则头|
+|-|int|`123` 或 `"456"`|无前缀的类型头|
+|[int, bool]|-|`True` 或 `123`|无名的元素类头|
+|["foo", "bar"]|-|`"foo"` 或 `"bar"`|无名的纯文字头|
+|["foo", "bar"]|"baz"|`"foobaz"` 或 `"barbaz"`|纯文字头|
+|[int, bool]|"foo"|`[123, "foo"]` 或 `[False, "foo"]`|类型头|
+|[123, 4567]|"foo"|`[123, "foo"]` 或 `[4567, "foo"]`|元素头|
+|[nepattern.NUMBER]|"bar"|`[123, "bar"]` 或 `[123.456, "bar"]`|表达式头|
+|[123, "foo"]|"bar"|`[123, "bar"]` 或 `"foobar"` 或 `["foo", "bar"]`|混合头|
+|[(int, "foo"), (456, "bar")]|"baz"|`[123, "foobaz"]` 或 `[456, "foobaz"]` 或 `[456, "barbaz"]`|对头|
+
+无前缀的类型头：此时会将传入的值尝试转为 BasePattern，例如 `int` 会转为 `nepattern.INTEGER`。此时命令头会匹配对应的类型， 例如 `int` 会匹配 `123` 或 `"456"`，但不会匹配 `"foo"`。同时，Alconna 会将命令头匹配到的值转为对应的类型，例如 `int` 会将 `"123"` 转为 `123`。
+
+**正则只在命令名上生效，命令前缀中的正则会被转义**  
+除了通过传入 `re:xxx` 来使用正则表达式外，Alconna 还提供了一种更加简洁的方式来使用正则表达式，那就是 Bracket Header。
+
+```python
+from alconna import Alconna
+
+
+alc = Alconna(".rd{roll:int}")
+assert alc.parse(".rd123").header["roll"] == 123
+```
+
+Bracket Header 类似 python 里的 f-string 写法，通过 "{}" 声明匹配类型
+
+"{}" 中的内容为 "name:type or pat"：
+- "{}", "{:}" **⇔** "(.+)", 占位符
+- "{foo}" **⇔** "(?P&lt;foo&gt;.+)"
+- "{:\d+}" **⇔** "(\d+)"
+- "{foo:int}" **⇔** "(?P&lt;foo&gt;\d+)"，其中 "int" 部分若能转为 `BasePattern` 则读取里面的表达式
+
+
+#### 参数声明(Args)
+
+`Args` 是用于声明命令参数的组件, 可以通过以下几种方式构造 **Args** :
+
+ - `Args[key, var, default][key1, var1, default1][...]` 
+ - `Args[(key, var, default)]`
+ - `Args.key[var, default]`
+ 
+其中，key **一定**是字符串，而 var 一般为参数的类型，default 为具体的值或者 **arclet.alconna.args.Field**.
+
+其与函数签名类似，但是允许含有默认值的参数在前；同时支持 keyword-only 参数不依照构造顺序传入 （但是仍需要在非 keyword-only 参数之后）.
+
+##### key
+`key` 的作用是用以标记解析出来的参数并存放于 **Arparma** 中，以方便用户调用.
+
+其有三种为 Args 注解的标识符:  `?`、`/`、 `!`, 标识符与 key 之间建议以 `;` 分隔:
+
+- `!` 标识符表示该处传入的参数应**不是**规定的类型，或**不在**指定的值中。
+- `?` 标识符表示该参数为**可选**参数，会在无参数匹配时跳过。
+- `/` 标识符表示该参数的类型注解需要隐藏。
+
+另外，对于参数的注释也可以标记在 `key` 中，其与 key 或者标识符 以 `#` 分割：  
+`foo#这是注释;?` 或 `foo?#这是注释`
+
+`Args` 中的 `key` 在实际命令中并不需要传入（keyword 参数除外）：
+
+```python
+from arclet.alconna import Alconna, Args  
+
+
+alc = Alconna("test", Args["foo", str])  
+alc.parse("test --foo abc") # 错误  
+alc.parse("test abc") # 正确
+```
+
+若需要 `test --foo abc`，你应该使用 `Option`：
+
+```python
+from arclet.alconna import Alconna, Args, Option
+
+
+alc = Alconna("test", Option("--foo", Args["foo", str]))
+```
+##### var
+var 负责命令参数的**类型检查**与**类型转化**.
+
+`Args` 的`var`表面上看需要传入一个 `type`，但实际上它需要的是一个 `nepattern.BasePattern` 的实例.
+
+```python
+from arclet.alconna import Args  
+from nepattern import BasePattern  
+
+
+# 表示 foo 参数需要匹配一个 @number 样式的字符串  
+args = Args["foo", BasePattern("@\d+")]
+```
+
+示例中可以传入 `str` 是因为 `str` 已经注册在了 `nepattern.global_patterns` 中，因此会替换为 `nepattern.global_patterns[str]`.
+
+`nepattern.global_patterns`默认支持的类型有：
+
+- `str`: 匹配任意字符串
+- `int`: 匹配整数
+- `float`: 匹配浮点数
+- `bool`: 匹配 `True` 与 `False` 以及他们小写形式
+- `hex`: 匹配 `0x` 开头的十六进制字符串
+- `url`: 匹配网址
+- `email`: 匹配 `xxxx@xxx` 的字符串
+- `ipv4`: 匹配 `xxx.xxx.xxx.xxx` 的字符串
+- `list`: 匹配类似 `["foo","bar","baz"]` 的字符串
+- `dict`: 匹配类似 `{"foo":"bar","baz":"qux"}` 的字符串
+- `datetime`: 传入一个 `datetime` 支持的格式字符串，或时间戳
+- `Any`: 匹配任意类型
+- `AnyString`: 匹配任意类型，转为 `str`
+- `Number`: 匹配 `int` 与 `float`，转为 `int`
+
+同时可以使用 typing 中的类型：
+
+- `Literal[X]`: 匹配其中的任意一个值
+- `Union[X, Y]`: 匹配其中的任意一个类型
+- `Optional[xxx]`: 会自动将默认值设为 `None`，并在解析失败时使用默认值
+- `List[X]`: 匹配一个列表，其中的元素为 `X` 类型
+- `Dict[X, Y]`: 匹配一个字典，其中的 key 为 `X` 类型，value 为 `Y` 类型
+- ...
+
+几类特殊的传入标记：
+
+- `"foo"`: 匹配字符串 "foo" (若没有某个 `BasePattern` 与之关联)
+- `RawStr("foo")`: 匹配字符串 "foo" (不会被 `BasePattern` 替换)
+- `"foo|bar|baz"`: 匹配 "foo" 或 "bar" 或 "baz"
+- `[foo, bar, Baz, ...]`: 匹配其中的任意一个值或类型
+- `Callable[[X], Y]`: 匹配一个参数为 `X` 类型的值，并返回通过该函数调用得到的 `Y` 类型的值
+- `"re:xxx"`: 匹配一个正则表达式 `xxx`，会返回 Match[0]
+- `"rep:xxx"`: 匹配一个正则表达式 `xxx`，会返回 `re.Match` 对象
+- `{foo: bar, baz: qux}`: 匹配字典中的任意一个键, 并返回对应的值 (特殊的键 ... 会匹配任意的值)
+- ...
+
+`MultiVar` 则是一个特殊的标注，用于告知解析器该参数可以接受多个值，其构造方法形如 `MultiVar(str)`。 同样的还有 `KeyWordVar`，其构造方法形如 `KeyWordVar(str)`，用于告知解析器该参数为一个 keyword-only 参数.
+
+TIPS:
+
+`MultiVar` 与 `KeyWordVar` 组合时，代表该参数为一个可接受多个 key-value 的参数，其构造方法形如 `MultiVar(KeyWordVar(str))`.
+
+`MultiVar` 与 `KeyWordVar` 也可以传入 `default` 参数，用于指定默认值.
+
+`MultiVar` 不能在 `KeyWordVar` 之后传入.
+
+
+#### **Option** 和 **Subcommand**
+`Option` 可以传入一组 `alias`，如 `Option("--foo|-F|--FOO|-f")` 或 `Option("--foo", alias=["-F"]`.  
+
+传入别名后，`option` 会选择其中长度最长的作为选项名称。若传入为 "--foo|-f"，则命令名称为 "--foo".  
+
+**在 Alconna 中 Option 的名字或别名**没有要求**必须在前面写上 `-`.**  
+
+`Subcommand` 可以传入自己的 **Option** 与 **Subcommand**.  
+
+他们拥有如下共同参数:
+
+- `help_text`: 传入该组件的帮助信息
+- `dest`: 被指定为解析完成时标注匹配结果的标识符，不传入时默认为选项或子命令的名称 (name)
+- `requires`: 一段指定顺序的字符串列表，作为唯一的前置序列与命令嵌套替换
+对于命令 `test foo bar baz qux <a:int>` 来讲，因为`foo bar baz` 仅需要判断是否相等, 所以可以这么编写：
+```python
+Alconna("test", Option("qux", Args.a[int], requires=["foo", "bar", "baz"]))
+```
+
+- `default`: 默认值，在该组件未被解析时使用使用该值替换。
+特别的，使用 `OptionResult` 或 `SubcomanndResult` 可以设置包括参数字典在内的默认值：
+```python
+from arclet.alconna import Option, OptionResult
path: nekro_agent/adapters/alconna.md
user: login: sourcery-ai[bot]
body: **issue (typo):** 错别字：'SubcomanndResult' 应该是 'SubcommandResult'。

```suggestion
特别的，使用 `OptionResult` 或 `SubcommandResult` 可以设置包括参数字典在内的默认值：
```python
from arclet.alconna import Option, OptionResult
```

<details>
<summary>Original comment in English</summary>

**issue (typo):** Typo: 'SubcomanndResult' should be 'SubcommandResult'.

```suggestion
特别的，使用 `OptionResult` 或 `SubcommandResult` 可以设置包括参数字典在内的默认值：
```python
from arclet.alconna import Option, OptionResult
```

</details>
created_at: 2025-05-24T21:36:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,388 @@
+import json
+from typing import Any, Dict, List, Optional, Union
+
+from nonebot.adapters.minecraft import Bot, Message, MessageSegment
+from nonebot.adapters.minecraft.model import (
+    BaseComponent,
+    ClickAction,
+    ClickEvent,
+    HoverAction,
+    HoverEvent,
+    TextColor,
+    TextComponent,
+)
+
+from nekro_agent.adapters.minecraft.core.bot import get_bot
+from nekro_agent.api import core
+from nekro_agent.api.core import logger
+from nekro_agent.api.plugin import NekroPlugin, SandboxMethod, SandboxMethodType
+from nekro_agent.api.schemas import AgentCtx
+from nekro_agent.models.db_chat_channel import DBChatChannel
+from nekro_agent.services.message_service import message_service
+
+plugin = NekroPlugin(
+    name="Minecraft 工具插件",
+    module_name="minecraft_utils",
+    description="提供 Minecraft 适配器专用的消息发送和 RCON 命令执行功能。",
+    version="0.1.0",
+    author="Zaxpris",
+    url="https://github.com/KroMiose/nekro-agent",
+    support_adapter=["minecraft"],
+)
+
+
+def _llm_segment_dict_to_text_component(segment_dict: Dict[str, Any]) -> TextComponent:
path: plugins/builtin/minecraft_utils.py
user: login: sourcery-ai[bot]
body: **issue (complexity):** 考虑通过将共享属性和事件处理提取到辅助函数中来重构 segment-to-args 转换逻辑，以消除代码重复。

```markdown
您可以通过将公共属性和事件逻辑提取到小型辅助函数中来减少两个“segment → args”方法中的大部分重复。例如：

```python
# 顶级常量
_STYLE_ATTRS = ["bold", "italic", "underlined", "strikethrough", "obfuscated"]

def _populate_base_args(data: Dict[str, Any], args: Dict[str, Any]):
    # 颜色
    if "color" in data:
        try:
            args["color"] = TextColor(str(data["color"]).lower())
        except ValueError:
            logger.warning(f"无效的颜色: {data['color']}")
    # 样式
    for attr in _STYLE_ATTRS:
        if data.get(attr) is not None:
            args[attr] = data[attr]
    # 插入
    if "insertion" in data:
        args["insertion"] = data["insertion"]

def _make_click_event(data: Dict[str, Any]) -> Optional[ClickEvent]:
    act = data.get("click_event_action")
    val = data.get("click_event_value")
    if act and val:
        try:
            return ClickEvent(
                action=ClickAction(str(act).lower()),
                value=str(val)
            )
        except ValueError:
            logger.warning(f"无效的点击动作: {act}")
    return None

def _make_hover_event(data: Dict[str, Any]) -> Optional[HoverEvent]:
    act = data.get("hover_event_action")
    if not act:
        return None
    try:
        hover = HoverAction(str(act).lower())
    except ValueError:
        logger.warning(f"无效的悬停动作: {act}")
        return None

    if hover == HoverAction.SHOW_TEXT:
        segs = data.get("hover_event_text_segments", [])
        if isinstance(segs, list):
            comps = [_llm_segment_dict_to_text_component(s) for s in segs]
            return HoverEvent(action=hover, text=comps)
        logger.warning("hover_event_text_segments 必须是一个列表")
    # TODO: 其他悬停类型...
    return None
```

然后，您的两个转换器缩小为：

```python
def _llm_segment_dict_to_text_component(data):
    args = {"text": data.get("text", "")}
    _populate_base_args(data, args)
    click = _make_click_event(data)
    if click: args["click_event"] = click
    hover = _make_hover_event(data)
    if hover: args["hover_event"] = hover
    return TextComponent(**{k: v for k, v in args.items() if v is not None})

def _llm_segment_to_mc_message_segment(data):
    text = data.get("text")
    if text is None:
        logger.warning("段落缺少文本")
        return None

    args: Dict[str, Any] = {}
    _populate_base_args(data, args)
    click = _make_click_event(data)
    if click: args["click_event"] = click
    hover = _make_hover_event(data)
    if hover: args["hover_event"] = hover
    return MessageSegment.text(str(text), **args)
```

这保留了所有行为，但将大约 100 行的重复减少到几个简洁的辅助函数。

<details>
<summary>Original comment in English</summary>

**issue (complexity):** Consider refactoring the segment-to-args conversion logic by extracting shared attribute and event handling into helper functions to eliminate code duplication.

```markdown
You can collapse most of the duplication in your two “segment → args” methods by extracting the common attribute‐and‐event logic into small helpers. For example:

```python
# top‐level constants
_STYLE_ATTRS = ["bold", "italic", "underlined", "strikethrough", "obfuscated"]

def _populate_base_args(data: Dict[str, Any], args: Dict[str, Any]):
    # color
    if "color" in data:
        try:
            args["color"] = TextColor(str(data["color"]).lower())
        except ValueError:
            logger.warning(f"Invalid color: {data['color']}")
    # styles
    for attr in _STYLE_ATTRS:
        if data.get(attr) is not None:
            args[attr] = data[attr]
    # insertion
    if "insertion" in data:
        args["insertion"] = data["insertion"]

def _make_click_event(data: Dict[str, Any]) -> Optional[ClickEvent]:
    act = data.get("click_event_action")
    val = data.get("click_event_value")
    if act and val:
        try:
            return ClickEvent(
                action=ClickAction(str(act).lower()),
                value=str(val)
            )
        except ValueError:
            logger.warning(f"Invalid click action: {act}")
    return None

def _make_hover_event(data: Dict[str, Any]) -> Optional[HoverEvent]:
    act = data.get("hover_event_action")
    if not act:
        return None
    try:
        hover = HoverAction(str(act).lower())
    except ValueError:
        logger.warning(f"Invalid hover action: {act}")
        return None

    if hover == HoverAction.SHOW_TEXT:
        segs = data.get("hover_event_text_segments", [])
        if isinstance(segs, list):
            comps = [_llm_segment_dict_to_text_component(s) for s in segs]
            return HoverEvent(action=hover, text=comps)
        logger.warning("hover_event_text_segments must be a list")
    # TODO: other hover types...
    return None
```

Then your two converters shrink to:

```python
def _llm_segment_dict_to_text_component(data):
    args = {"text": data.get("text", "")}
    _populate_base_args(data, args)
    click = _make_click_event(data)
    if click: args["click_event"] = click
    hover = _make_hover_event(data)
    if hover: args["hover_event"] = hover
    return TextComponent(**{k: v for k, v in args.items() if v is not None})

def _llm_segment_to_mc_message_segment(data):
    text = data.get("text")
    if text is None:
        logger.warning("Segment missing text")
        return None

    args: Dict[str, Any] = {}
    _populate_base_args(data, args)
    click = _make_click_event(data)
    if click: args["click_event"] = click
    hover = _make_hover_event(data)
    if hover: args["hover_event"] = hover
    return MessageSegment.text(str(text), **args)
```

This preserves all behavior but reduces ~100s of lines of repetition to a few concise helpers.

</details>
created_at: 2025-05-24T21:36:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,81 @@
+"""NoneBot @ 解析工具
+
+这个模块包含 NoneBot 平台特定的 @ 解析功能。
+"""
+
+from typing import List, Optional, Union
+
+from pydantic import BaseModel
+
+from nekro_agent.adapters.onebot_v11.tools.onebot_util import get_user_group_card_name
+from nekro_agent.core.config import config
+from nekro_agent.models.db_chat_channel import DBChatChannel
+
+
+class SegAt(BaseModel):
+    """@ 消息段"""
+
+    platform_user_id: str
+    nickname: Optional[str]
+
+
+async def parse_at_from_text(text: str, db_chat_channel: DBChatChannel) -> List[Union[str, SegAt]]:
path: nekro_agent/adapters/onebot_v11/tools/at_parser.py
user: login: sourcery-ai[bot]
body: **issue (complexity):** 考虑通过提取用于组检测、段解析和渲染的辅助函数来重构该函数，以减少嵌套并阐明逻辑。

```markdown
您可以将大多数嵌套的 `if/else` 折叠为三个小型辅助函数：
1) `is_group_chat` – 检测群组与私聊
2) `parse_seg` – 拉取 `uid` 和可选的原始 `nickname`
3) `render_at` – 决定是返回 `SegAt` 还是纯字符串

这保留了所有行为，但消除了深度嵌套：

```python
async def parse_at_from_text(text: str, db_chat_channel: DBChatChannel) -> List[Union[str, SegAt]]:
    result = []
    start = 0
    while True:
        at_i = text.find("[@", start)
        if at_i < 0:
            result.append(text[start:])
            break
        result.append(text[start:at_i])
        end_i = text.find("@]", at_i)
        if end_i < 0:
            result.append(text[at_i:])
            break

        seg = text[at_i + 2 : end_i]
        uid, raw_nick = await parse_seg(seg, db_chat_channel)
        result.append(render_at(uid, raw_nick, db_chat_channel))
        start = end_i + 2
    return result


def is_group_chat(db_chat_channel: DBChatChannel) -> bool:
    return "group" in db_chat_channel.chat_key


async def parse_seg(seg: str, db_chat_channel: DBChatChannel) -> Tuple[str, Optional[str]]:
    if "nickname:" in seg:
        parts = {k: v for k, v in (p.split(":", 1) for p in seg.split(";"))}
        return parts["id"].strip(), parts["nickname"].strip()
    # 源中没有昵称 → 可能需要获取群名片名称
    uid = seg.replace("id:", "").strip()
    if is_group_chat(db_chat_channel):
        group_id = db_chat_channel.chat_key.replace("group_", "")
        nick = await get_user_group_card_name(
            group_id=group_id, user_id=uid, db_chat_channel=db_chat_channel
        )
        return uid, nick
    return uid, None


def render_at(uid: str, nickname: Optional[str], db_chat_channel: DBChatChannel) -> Union[str, SegAt]:
    if not config.SESSION_ENABLE_AT:
        # 禁用 at 时，仅在群组中显示昵称
        return nickname or "" if is_group_chat(db_chat_channel) else ""
    # 启用 at → 仅在群组中显示 SegAt
    return SegAt(platform_user_id=uid, nickname=None) if is_group_chat(db_chat_channel) else ""
```

此重构：

- 将“群组与私聊”检查拉入 `is_group_chat`
- 将解析 (`parse_seg`) 与格式化 (`render_at`) 分开
- 保持主循环简单且线性
- 保留所有现有功能（包括异步获取）
```

<details>
<summary>Original comment in English</summary>

**issue (complexity):** Consider refactoring the function by extracting helpers for group detection, segment parsing, and rendering to reduce nesting and clarify logic.

```markdown
You can collapse most of the nested `if/else` into three small helpers:  
1) `is_group_chat` – detect group vs private  
2) `parse_seg` – pull out `uid` and optional raw `nickname`  
3) `render_at` – decide whether to return a `SegAt` or a plain string  

This preserves all behavior but removes the deep nesting:

```python
async def parse_at_from_text(text: str, db_chat_channel: DBChatChannel) -> List[Union[str, SegAt]]:
    result = []
    start = 0
    while True:
        at_i = text.find("[@", start)
        if at_i < 0:
            result.append(text[start:])
            break
        result.append(text[start:at_i])
        end_i = text.find("@]", at_i)
        if end_i < 0:
            result.append(text[at_i:])
            break

        seg = text[at_i + 2 : end_i]
        uid, raw_nick = await parse_seg(seg, db_chat_channel)
        result.append(render_at(uid, raw_nick, db_chat_channel))
        start = end_i + 2
    return result


def is_group_chat(db_chat_channel: DBChatChannel) -> bool:
    return "group" in db_chat_channel.chat_key


async def parse_seg(seg: str, db_chat_channel: DBChatChannel) -> Tuple[str, Optional[str]]:
    if "nickname:" in seg:
        parts = {k: v for k, v in (p.split(":", 1) for p in seg.split(";"))}
        return parts["id"].strip(), parts["nickname"].strip()
    # no nickname in source → may need to fetch group card name
    uid = seg.replace("id:", "").strip()
    if is_group_chat(db_chat_channel):
        group_id = db_chat_channel.chat_key.replace("group_", "")
        nick = await get_user_group_card_name(
            group_id=group_id, user_id=uid, db_chat_channel=db_chat_channel
        )
        return uid, nick
    return uid, None


def render_at(uid: str, nickname: Optional[str], db_chat_channel: DBChatChannel) -> Union[str, SegAt]:
    if not config.SESSION_ENABLE_AT:
        # when at’s disabled, only show nickname in group
        return nickname or "" if is_group_chat(db_chat_channel) else ""
    # at’s enabled → only SegAt in group
    return SegAt(platform_user_id=uid, nickname=None) if is_group_chat(db_chat_channel) else ""
```

This refactoring:

- Pulls the “group vs private” check into `is_group_chat`
- Splits parsing (`parse_seg`) from formatting (`render_at`)
- Keeps the main loop trivial and linear
- Preserves all existing functionality (including async fetch)
```

</details>
created_at: 2025-05-24T21:36:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -540,9 +540,9 @@ const UserTable: React.FC<UserTableProps> = ({
               <TableRow>
                 <TableCell
                   sx={{
-                    width: '60px',
-                    minWidth: '60px',
-                    py: isSmall ? 1 : 1.5,
+                    width: '32px',
+                    minWidth: '32px',
+                    py: isSmall ? 1 : 1,
path: frontend/src/pages/user-manager/components/UserTable.tsx
user: login: sourcery-ai[bot]
body: **security (useless-ternary):** 看起来无论如何评估 isSmall，此表达式都返回 1。这可能是复制粘贴错误。

*来源：opengrep*

<details>
<summary>Original comment in English</summary>

**security (useless-ternary):** It looks like no matter how isSmall is evaluated, this expression returns 1. This is probably a copy-paste error.

*Source: opengrep*

</details>
created_at: 2025-05-24T21:36:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,388 @@
+import json
+from typing import Any, Dict, List, Optional, Union
+
+from nonebot.adapters.minecraft import Bot, Message, MessageSegment
+from nonebot.adapters.minecraft.model import (
+    BaseComponent,
+    ClickAction,
+    ClickEvent,
+    HoverAction,
+    HoverEvent,
+    TextColor,
+    TextComponent,
+)
+
+from nekro_agent.adapters.minecraft.core.bot import get_bot
+from nekro_agent.api import core
+from nekro_agent.api.core import logger
+from nekro_agent.api.plugin import NekroPlugin, SandboxMethod, SandboxMethodType
+from nekro_agent.api.schemas import AgentCtx
+from nekro_agent.models.db_chat_channel import DBChatChannel
+from nekro_agent.services.message_service import message_service
+
+plugin = NekroPlugin(
+    name="Minecraft 工具插件",
+    module_name="minecraft_utils",
+    description="提供 Minecraft 适配器专用的消息发送和 RCON 命令执行功能。",
+    version="0.1.0",
+    author="Zaxpris",
+    url="https://github.com/KroMiose/nekro-agent",
+    support_adapter=["minecraft"],
+)
+
+
+def _llm_segment_dict_to_text_component(segment_dict: Dict[str, Any]) -> TextComponent:
+    """
+    将 LLM 输出的单个富文本段字典转换为 Minecraft TextComponent。
+    主要用于构建 HoverEvent.text 中的组件。
+    """
+    text = segment_dict.get("text", "")
+    component_data: Dict[str, Any] = {"text": text}
+
+    if "color" in segment_dict:
+        try:
+            component_data["color"] = TextColor(str(segment_dict["color"]).lower())
+        except ValueError:
+            logger.warning(
+                f"[MinecraftUtils] 无效的悬停文本颜色: {segment_dict['color']}",
+            )
+
+    for style_attr in ["bold", "italic", "underlined", "strikethrough", "obfuscated"]:
+        if style_attr in segment_dict:
+            component_data[style_attr] = segment_dict[style_attr]
+
+    if "insertion" in segment_dict:
+        component_data["insertion"] = segment_dict["insertion"]
+
+    click_action_str = segment_dict.get("click_event_action")
+    click_value = segment_dict.get("click_event_value")
+    if click_action_str and click_value:
+        try:
+            action = ClickAction(str(click_action_str).lower())
+            component_data["click_event"] = ClickEvent(
+                action=action, value=str(click_value),
+            )
+        except ValueError:
+            logger.warning(
+                f"[MinecraftUtils] 无效的悬停点击事件动作: {click_action_str}",
+            )
+
+    hover_action_str = segment_dict.get("hover_event_action")
+    if hover_action_str:
+        try:
+            hover_action = HoverAction(str(hover_action_str).lower())
+            if hover_action == HoverAction.SHOW_TEXT:
+                hover_text_segments_data = segment_dict.get("hover_event_text_segments")
+                if isinstance(hover_text_segments_data, list):
+                    nested_hover_components: List[BaseComponent] = [
+                        _llm_segment_dict_to_text_component(s)
+                        for s in hover_text_segments_data
+                    ]
+                    component_data["hover_event"] = HoverEvent(
+                        action=hover_action, text=nested_hover_components,
+                    )
+                else:
+                    logger.warning(
+                        "[MinecraftUtils] hover_event_text_segments 在悬停事件中缺失或非列表",
+                    )
+            # TODO: 支持其他 HoverAction (show_item, show_entity) 如果需要
+        except ValueError:
+            logger.warning(
+                f"[MinecraftUtils] 无效的悬停事件动作: {hover_action_str}",
+            )
+    return TextComponent(**{k: v for k, v in component_data.items() if v is not None})
+
+
+def _llm_segment_to_mc_message_segment(
+    segment_dict: Dict[str, Any],
+) -> Optional[MessageSegment]:
+    """
+    将 LLM 输出的单个富文本段字典转换为 Minecraft MessageSegment。
+    """
+    text = segment_dict.get("text")
+    if text is None:  # 每个段必须有文本
+        logger.warning("[MinecraftUtils] 富文本段缺少 'text' 键。")
+        return None
+
+    segment_args: Dict[str, Any] = {}
+
+    if "color" in segment_dict:
+        try:
+            segment_args["color"] = TextColor(str(segment_dict["color"]).lower())
+        except ValueError:
+            logger.warning(
+                f"[MinecraftUtils] 无效的文本颜色: {segment_dict['color']}",
+            )
+
+    for style_attr in ["bold", "italic", "underlined", "strikethrough", "obfuscated"]:
+        if style_attr in segment_dict:
+            segment_args[style_attr] = segment_dict[style_attr]
+
+    if "insertion" in segment_dict:
+        segment_args["insertion"] = segment_dict["insertion"]
+
+    click_action_str = segment_dict.get("click_event_action")
+    click_value = segment_dict.get("click_event_value")
+    if click_action_str and click_value:
+        try:
+            action = ClickAction(str(click_action_str).lower())
+            segment_args["click_event"] = ClickEvent(
+                action=action, value=str(click_value),
+            )
+        except ValueError:
+            logger.warning(
+                f"[MinecraftUtils] 无效的点击事件动作: {click_action_str}",
+            )
+
+    hover_action_str = segment_dict.get("hover_event_action")
+    if hover_action_str:
+        try:
+            hover_action = HoverAction(str(hover_action_str).lower())
+            if hover_action == HoverAction.SHOW_TEXT:
+                hover_text_segments_data = segment_dict.get("hover_event_text_segments")
+                if isinstance(hover_text_segments_data, list):
+                    hover_components: List[BaseComponent] = [
+                        _llm_segment_dict_to_text_component(s_dict)
+                        for s_dict in hover_text_segments_data
+                    ]
+                    segment_args["hover_event"] = HoverEvent(
+                        action=hover_action, text=hover_components,
+                    )
+                else:
+                    logger.warning(
+                        "[MinecraftUtils] hover_event_text_segments 缺失或非列表",
+                    )
+            # TODO: 支持其他 HoverAction (show_item, show_entity) 如果需要
+        except ValueError:
+            logger.warning(
+                f"[MinecraftUtils] 无效的悬停事件动作: {hover_action_str}",
+            )
+
+    return MessageSegment.text(str(text), **segment_args)
+
+@plugin.mount_sandbox_method(
+    SandboxMethodType.TOOL,
+    name="发送Minecraft富文本消息",
+    description="向指定的Minecraft聊天发送富文本消息。LLM需要提供特定格式的JSON字符串。",
+)
+async def send_rich_text(_ctx: AgentCtx, chat_key: str, rich_text_json: str):
+    """发送Minecraft富文本消息
+    CAUTION: THIS METHOD IS USED FOR SENDING MESSAGES VIA MINECRAFT ADAPTER, NOT A MODEL CONTEXT PROTOCOL (MCP) SERVICE.
+    Args:
+        chat_key (str): Minecraft 服务器的会话标识 (例如 'minecraft-servername')。
+        rich_text_json (str): 一个 JSON 字符串，表示一个消息段(segment)的列表。
+            每个 segment 都是一个 JSON 对象，用于描述一段具有特定样式的文本及其交互行为。
+
+    富文本 Segment JSON 对象支持的键:
+        - `text` (str, 必选): 该段的文本内容。
+        - `color` (str, 可选): 文本颜色。有效值是 TextColor 枚举的小写形式，例如:
+          `"black"`, `"dark_blue"`, `"dark_green"`, `"dark_aqua"`, `"dark_red"`,
+          `"dark_purple"`, `"gold"`, `"gray"`, `"dark_gray"`, `"blue"`, `"green"`,
+          `"aqua"`, `"red"`, `"light_purple"`, `"yellow"`, `"white"`.
+          默认为服务器的默认文本颜色。
+        - `bold` (bool, 可选): 文本是否加粗。默认为 `false`。
+        - `italic` (bool, 可选): 文本是否斜体。默认为 `false`。
+        - `underlined` (bool, 可选): 文本是否有下划线。默认为 `false`。
+        - `strikethrough` (bool, 可选): 文本是否有删除线。默认为 `false`。
+        - `obfuscated` (bool, 可选): 文本是否混淆显示 (随机字符)。默认为 `false`。
+        - `insertion` (str, 可选): 当玩家在聊天中点击这段文本时，插入到玩家聊天输入框的文本。
+        - `click_event_action` (str, 可选): 点击事件的动作。必须与 `click_event_value` 一起使用。
+          有效值 (ClickAction 枚举的小写形式):
+            - `"open_url"`: 打开一个 URL。
+            - `"open_file"`: 打开一个文件 (通常在客户端上，不推荐用于服务器消息)。
+            - `"run_command"`: 以玩家身份执行一条命令。
+            - `"suggest_command"`: 在玩家聊天输入框中建议一条命令 (玩家需手动发送)。
+            - `"change_page"`: (仅用于书本中) 改变书本的页面。
+            - `"copy_to_clipboard"`: 将指定文本复制到玩家的剪贴板。
+        - `click_event_value` (str, 可选): 点击事件的值。根据 `click_event_action` 的不同而不同:
+            - 对于 `"open_url"`, `"suggest_command"`, `"copy_to_clipboard"`: 对应的 URL、命令字符串或要复制的文本。
+            - 对于 `"run_command"`: 要执行的命令 (通常需要以 `/` 开头)。
+            - 对于 `"change_page"`: 页码。
+        - `hover_event_action` (str, 可选): 鼠标悬停事件的动作。目前主要支持 `"show_text"`。
+          必须与 `hover_event_text_segments` 一起使用。
+          有效值 (HoverAction 枚举的小写形式):
+            - `"show_text"`: 悬停时显示一段富文本。
+            - `"show_item"`: (暂未完全支持通过此JSON格式配置) 悬停时显示物品信息。
+            - `"show_entity"`: (暂未完全支持通过此JSON格式配置) 悬停时显示实体信息。
+        - `hover_event_text_segments` (List[Dict], 可选): 当 `hover_event_action` 为 `"show_text"` 时使用。
+          它本身是一个消息段 (segment) 对象的列表 (与顶层结构相同，但不建议嵌套过深的悬停事件)，
+          用于定义鼠标悬停时显示的富文本内容。
+
+    示例 JSON 字符串:
+    ```json
+    [
+        {"text": "欢迎来到 "},
+        {"text": "我的世界!", "color": "gold", "bold": true},
+        {
+            "text": " 点击这里访问官网", 
+            "color": "aqua", 
+            "underlined": true, 
+            "click_event_action": "open_url", 
+            "click_event_value": "https://www.minecraft.net"
+        },
+        {
+            "text": " 或 ", 
+            "color": "gray"
+        },
+        {
+            "text": "执行命令", 
+            "color": "light_purple", 
+            "italic": true,
+            "click_event_action": "suggest_command", 
+            "click_event_value": "/help",
+            "hover_event_action": "show_text",
+            "hover_event_text_segments": [
+                {"text": "点击后会在你的聊天框输入 ", "color": "yellow"},
+                {"text": "/help", "color": "red", "bold": true}
+            ]
+        }
+    ]
+    ```
+    注意: JSON 字符串在作为参数传递时，内部的双引号需要被正确转义，例如在Python字符串中表示为 `\"`。
+    示例参数: `rich_text_json='[{"text": "你好", "color": "red"}, {"text": " 世界!", "bold": true}]'`
+    """
+
+    def _parse_and_validate_segments(json_str: str) -> List[MessageSegment]:
+        """解析并验证富文本JSON字符串，返回MessageSegment列表。"""
+        try:
+            segments_data = json.loads(json_str)
+        except json.JSONDecodeError as e:
+            logger.error(f"解析富文本 JSON 失败: {e}", exc_info=True)
+            raise ValueError(f"富文本 JSON 格式错误: {e}") from e
+
+        if not isinstance(segments_data, list):
+            raise ValueError("富文本 JSON 解析后必须是一个列表。")  # noqa: TRY004
+        
+        parsed_message_segments: List[MessageSegment] = []
+        for seg_dict in segments_data:
+            if isinstance(seg_dict, dict):
+                mc_segment = _llm_segment_to_mc_message_segment(seg_dict)
+                if mc_segment:
+                    parsed_message_segments.append(mc_segment)
+            else:
+                logger.warning(
+                    f"[MinecraftUtils] 富文本列表中的元素不是字典: {seg_dict}",
+                )
+        
+        if not parsed_message_segments:
+            raise ValueError("没有可发送的有效富文本段。")
+        return parsed_message_segments
+
+    if not isinstance(chat_key, str) or not chat_key.startswith("minecraft-"):
+        raise ValueError("chat_key 无效，必须是 'minecraft-servername' 格式。")
+    if not isinstance(rich_text_json, str) or not rich_text_json.strip():
+        raise ValueError("富文本 JSON 字符串不能为空。")
+
+    bot: Optional[Bot] = get_bot(chat_key)
+    if not bot:
+        raise ConnectionError(f"未能找到或连接到 Minecraft 服务器: {chat_key}")
+
+    try:
+        message_segments = _parse_and_validate_segments(rich_text_json)
+
+        channel = await DBChatChannel.get_channel(chat_key)
+        preset = await channel.get_preset()
+
+        plain_text_parts = []
+        for seg in message_segments: # message_segments 是 _parse_and_validate_segments 的结果
+            if hasattr(seg, "data") and isinstance(seg.data, dict) and "text" in seg.data:
+                plain_text_parts.append(str(seg.data["text"]))
+        plain_text_content = "".join(plain_text_parts)
+        
+        await message_service.push_bot_message(
+            chat_key=chat_key,
+            agent_messages=plain_text_content, # 使用提取的纯文本
+            db_chat_channel=channel, 
+        )
+        message_segments.insert(0,MessageSegment.text(f"<{preset.name}>", color=TextColor.GREEN))
+        message_to_send = Message(message_segments)
+        await bot.send_msg(message=message_to_send)
+        logger.info(f"Minecraft 富文本消息已发送到 {chat_key}")
+    except ValueError as e:
+        logger.error(f"处理富文本消息失败: {e}", exc_info=True)
+        raise
+    except Exception as e:
+        logger.error(
+            f"发送 Minecraft 富文本消息到 {chat_key} 失败: {e}", exc_info=True,
+        )
+        raise Exception(f"发送 Minecraft 富文本消息失败: {e}") from e
path: plugins/builtin/minecraft_utils.py
user: login: sourcery-ai[bot]
body: **issue (code-quality):** 引发特定错误，而不是通用的 `Exception` 或 `BaseException` ([`raise-specific-error`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/Python/Default-Rules/raise-specific-error))

<details><summary>解释</summary>如果一段代码引发特定异常类型，
而不是通用的
[`BaseException`](https://docs.python.org/3/library/exceptions.html#BaseException)
或 [`Exception`](https://docs.python.org/3/library/exceptions.html#Exception)，
则调用代码可以：

- 获取有关错误类型的更多信息
- 为其定义特定的异常处理

这样，代码的调用者可以适当地处理错误。

您如何解决这个问题？

- 使用标准库的[内置异常](https://docs.python.org/3/library/exceptions.html)之一。
- [定义您自己的错误类](https://docs.python.org/3/tutorial/errors.html#tut-userexceptions)，该类是 `Exception` 的子类。

因此，与其让代码引发 `Exception` 或 `BaseException`，例如

```python
if incorrect_input(value):
    raise Exception("输入不正确")
```

您可以让代码引发特定错误，例如

```python
if incorrect_input(value):
    raise ValueError("输入不正确")
```

或

```python
class IncorrectInputError(Exception):
    pass


if incorrect_input(value):
    raise IncorrectInputError("输入不正确")
```
</details>

<details>
<summary>Original comment in English</summary>

**issue (code-quality):** Raise a specific error instead of the general `Exception` or `BaseException` ([`raise-specific-error`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/Python/Default-Rules/raise-specific-error))

<details><summary>Explanation</summary>If a piece of code raises a specific exception type
rather than the generic
[`BaseException`](https://docs.python.org/3/library/exceptions.html#BaseException)
or [`Exception`](https://docs.python.org/3/library/exceptions.html#Exception),
the calling code can:

- get more information about what type of error it is
- define specific exception handling for it

This way, callers of the code can handle the error appropriately.

How can you solve this?

- Use one of the [built-in exceptions](https://docs.python.org/3/library/exceptions.html) of the standard library.
- [Define your own error class](https://docs.python.org/3/tutorial/errors.html#tut-userexceptions) that subclasses `Exception`.

So instead of having code raising `Exception` or `BaseException` like

```python
if incorrect_input(value):
    raise Exception("The input is incorrect")
```

you can have code raising a specific error like

```python
if incorrect_input(value):
    raise ValueError("The input is incorrect")
```

or

```python
class IncorrectInputError(Exception):
    pass


if incorrect_input(value):
    raise IncorrectInputError("The input is incorrect")
```
</details>

</details>
created_at: 2025-05-24T21:36:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,158 @@
+import time
+from datetime import datetime
+from typing import Any, Dict, Optional, Type
+
+from nonebot import on_notice
+from nonebot.adapters.minecraft import Bot, NoticeEvent
+from nonebot.matcher import Matcher
+
+from nekro_agent.core.config import config
+from nekro_agent.core.logger import logger
+from nekro_agent.models.db_chat_channel import DBChatChannel
+from nekro_agent.models.db_user import DBUser
+from nekro_agent.schemas.chat_message import ChatMessage, ChatType
+from nekro_agent.services.message_service import message_service
+from nekro_agent.services.notice_service import (
+    BaseNoticeHandler,
+    NoticeConfig,
+    NoticeResult,
+)
+from nekro_agent.tools.time_util import format_duration
+
+
+class PlayerJoinNoticeHandler(BaseNoticeHandler):
+    """玩家加入通知处理器"""
+
+    def get_notice_config(self) -> NoticeConfig:
+        return NoticeConfig(force_tome=True, use_system_sender=True)
+
+    def match(self, _db_chat_channel: DBChatChannel, event_dict: Dict[str, Any]) -> Optional[Dict[str, str]]:
+        if event_dict["sub_type"] != "join":
+            return None
+        return {
+            "user_id": str(event_dict["player"].nickname),
+        }
+
+    async def format_message(self, _db_chat_channel: DBChatChannel, info: Dict[str, str]) -> str:
+        return f"(玩家 {info['user_id']} 加入服务器)"
+
+class PlayerQuitNoticeHandler(BaseNoticeHandler):
+    """玩家离开通知处理器"""
+
+    def get_notice_config(self) -> NoticeConfig:
+        return NoticeConfig(force_tome=True, use_system_sender=True)
+
+    def match(self, _db_chat_channel: DBChatChannel, event_dict: Dict[str, Any]) -> Optional[Dict[str, str]]:
+        if event_dict["sub_type"] != "quit":
+            return None
+        return {
+            "user_id": str(event_dict["player"].nickname),
+        }
+
+    async def format_message(self, _db_chat_channel: DBChatChannel, info: Dict[str, str]) -> str:
+        return f"(玩家 {info['user_id']} 离开服务器)"
+
+
+class NoticeHandlerManager:
+    """通知处理器管理器"""
+
+    def __init__(self):
+        self._handlers: list[BaseNoticeHandler] = []
+
+    def register(self, handler: BaseNoticeHandler):
+        """注册处理器"""
+        self._handlers.append(handler)
+
+    async def handle(self, event: NoticeEvent, _bot: Bot, db_chat_channel: DBChatChannel) -> Optional[NoticeResult]:
+        """处理通知事件
+
+        Args:
+            event (NoticeEvent): 通知事件
+            _bot (Bot): 机器人实例
+
+        Returns:
+            Optional[NoticeResult]: 处理结果，包含处理器实例和通知信息
+        """
+        # 预处理事件为字典
+        event_dict = dict(event)
+
+        for handler in self._handlers:
+            if info := handler.match(db_chat_channel, event_dict):
+                return NoticeResult(
+                    handler=handler,
+                    info=info,
+                )
+        return None
+
+    def get_sender_platform_userid(self, info: Dict[str, str]) -> str:
+        """获取发送者平台用户ID"""
+        return info["user_id"]
+
+notice_manager = NoticeHandlerManager()
+
+notice_manager.register(PlayerJoinNoticeHandler())
+notice_manager.register(PlayerQuitNoticeHandler())
+
+"""通用通知匹配器"""
+notice_matcher: Type[Matcher] = on_notice(priority=99999, block=False)
+
+
+@notice_matcher.handle()
+async def _(_: Matcher, event: NoticeEvent, bot: Bot):
+    # 处理通知事件
+    chat_key = "minecraft-" + event.server_name
path: nekro_agent/adapters/minecraft/matchers/notice.py
user: login: sourcery-ai[bot]
body: **suggestion (code-quality):** 使用 f-string 而不是字符串连接 ([`use-fstring-for-concatenation`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/use-fstring-for-concatenation/))

```suggestion
    chat_key = f"minecraft-{event.server_name}"
```

<details>
<summary>Original comment in English</summary>

**suggestion (code-quality):** Use f-string instead of string concatenation ([`use-fstring-for-concatenation`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/use-fstring-for-concatenation/))

```suggestion
    chat_key = f"minecraft-{event.server_name}"
```

</details>
created_at: 2025-05-24T21:36:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,211 @@
+from pathlib import Path
+from typing import List
+
+from nonebot.adapters.onebot.v11 import Bot, Message, MessageSegment
+
+from nekro_agent.adapters.interface.schemas.platform import (
+    PlatformChannel,
+    PlatformSendRequest,
+    PlatformSendResponse,
+    PlatformSendSegmentType,
+    PlatformUser,
+)
+from nekro_agent.adapters.onebot_v11.matchers.message import register_matcher
+from nekro_agent.core import config, logger
+from nekro_agent.core.os_env import OsEnv
+from nekro_agent.models.db_chat_channel import DBChatChannel
+from nekro_agent.schemas.chat_message import ChatType
+
+from ..interface.base import BaseAdapter
+from .core.bot import get_bot
+from .tools.at_parser import SegAt, parse_at_from_text
+from .tools.convertor import get_channel_type
+
+
+class OnebotV11Adapter(BaseAdapter):
+    """OneBot V11 适配器"""
+
+    @property
+    def key(self) -> str:
+        return "onebot_v11"
+
+    async def init(self) -> None:
+        """初始化适配器"""
+        from . import matchers
+
+        register_matcher(self)
+
+    async def forward_message(self, request: PlatformSendRequest) -> PlatformSendResponse:
+        """推送消息到 OneBot V11 协议端"""
+        # 分离文件类型和其他类型的消息段
+        file_segments = [seg for seg in request.segments if seg.type == PlatformSendSegmentType.FILE]
+        other_segments = [seg for seg in request.segments if seg.type != PlatformSendSegmentType.FILE]
+
+        # 先发送文件（如果有）
+        if file_segments:
+            await self._send_files(request.chat_key, file_segments)
+
+        # 再发送其他类型消息（如果有）
+        if other_segments:
+            modified_request = PlatformSendRequest(chat_key=request.chat_key, segments=other_segments)
+            await self._send_message(modified_request)
+
+        return PlatformSendResponse(success=True)
+
+    async def _send_message(self, request: PlatformSendRequest) -> None:
+        """发送普通消息（文本、@、图片等）"""
+        message = Message()
+
+        # 获取聊天频道信息用于 @ 解析
+        db_chat_channel = await DBChatChannel.get_channel(chat_key=request.chat_key)
+
+        for segment in request.segments:
+            if segment.type == PlatformSendSegmentType.TEXT:
+                if segment.content.strip():
+                    # NoneBot 特有功能：解析文本中的 @ 信息
+                    seg_data = await parse_at_from_text(segment.content, db_chat_channel)
+
+                    for seg in seg_data:
+                        if isinstance(seg, str):
+                            if seg.strip():
+                                message.append(MessageSegment.text(seg))
+                        elif isinstance(seg, SegAt):  # SegAt 对象
+                            message.append(MessageSegment.at(user_id=seg.platform_user_id))
+
+            elif segment.type == PlatformSendSegmentType.AT:
+                if segment.at_info:
+                    message.append(MessageSegment.at(user_id=segment.at_info.platform_user_id))
+            elif segment.type == PlatformSendSegmentType.IMAGE:
+                # 图片以富文本形式发送
+                if segment.file_path:
+                    file_path = Path(segment.file_path)
+                    if file_path.exists():
+                        message.append(MessageSegment.image(file=file_path.read_bytes()))
+                    else:
+                        message.append(MessageSegment.text(f"Image file not found: {segment.file_path}"))
+            else:
+                logger.warning(f"Unsupported segment type in normal mode: {segment.type}")
+
+        if message:
+            await self._send_to_chat(request.chat_key, message)
+
+    async def _send_files(self, chat_key: str, file_segments: List) -> None:
+        """发送文件（文件上传模式）"""
+        bot: Bot = get_bot()
+        files: List[Path] = []
+
+        # 收集所有文件路径
+        for segment in file_segments:
+            if segment.file_path:
+                file_path = Path(segment.file_path)
+                if file_path.exists():
+                    files.append(file_path)
+                else:
+                    logger.warning(f"File not found: {segment.file_path}")
+
+        if not files:
+            logger.warning("No valid files to send")
+            return
+
+        # 获取聊天频道信息
+        db_chat_channel = await DBChatChannel.get_channel(chat_key=chat_key)
+        chat_type = db_chat_channel.chat_type
+
+        # 从channel_id中提取真实的ID
+        chat_id = int(db_chat_channel.channel_id.split("_")[1])
+
+        # 如果配置了 OneBot 服务器挂载目录，需要转换路径
+        def get_onebot_path(file_path: Path) -> Path:
+            if config.SANDBOX_ONEBOT_SERVER_MOUNT_DIR:
+                return Path(config.SANDBOX_ONEBOT_SERVER_MOUNT_DIR) / file_path.relative_to(Path(OsEnv.DATA_DIR))
+            return file_path
+
+        if chat_type is ChatType.GROUP:
+            for file in files:
+                logger.info(f"Sending file: {file}")
+                onebot_path = get_onebot_path(file)
+                await bot.upload_group_file(
+                    group_id=chat_id,
+                    file=str(onebot_path),
+                    name=file.name,
+                )
+        elif chat_type is ChatType.PRIVATE:
+            for file in files:
+                onebot_path = get_onebot_path(file)
+                await bot.upload_private_file(
+                    user_id=chat_id,
+                    file=str(onebot_path),
+                    name=file.name,
+                )
+        else:
+            raise ValueError("Invalid chat type")
+
+    async def _send_to_chat(self, chat_key: str, message: Message) -> None:
+        """发送消息到指定聊天"""
+        bot: Bot = get_bot()
+
+        # 获取聊天频道信息
+        db_chat_channel = await DBChatChannel.get_channel(chat_key=chat_key)
+        chat_type = db_chat_channel.chat_type
+
+        # 从channel_id中提取真实的ID
+        chat_id = int(db_chat_channel.channel_id.split("_")[1])
+
+        if chat_type is ChatType.GROUP:
+            await bot.send_group_msg(group_id=chat_id, message=message)
+        elif chat_type is ChatType.PRIVATE:
+            await bot.send_private_msg(user_id=chat_id, message=message)
+        else:
+            raise ValueError("Invalid chat type")
+
+    async def get_self_info(self) -> PlatformUser:
+        """获取自身信息"""
+        bot: Bot = get_bot()
+        if bot:
path: nekro_agent/adapters/onebot_v11/adapter.py
user: login: sourcery-ai[bot]
body: **suggestion (code-quality):** 使用命名表达式来简化赋值和条件 ([`use-named-expression`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/use-named-expression/))

```suggestion
        if bot := get_bot():
```

<details>
<summary>Original comment in English</summary>

**suggestion (code-quality):** Use named expression to simplify assignment and conditional ([`use-named-expression`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/use-named-expression/))

```suggestion
        if bot := get_bot():
```

</details>
created_at: 2025-05-24T21:36:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,81 @@
+"""NoneBot @ 解析工具
+
+这个模块包含 NoneBot 平台特定的 @ 解析功能。
+"""
+
+from typing import List, Optional, Union
+
+from pydantic import BaseModel
+
+from nekro_agent.adapters.onebot_v11.tools.onebot_util import get_user_group_card_name
+from nekro_agent.core.config import config
+from nekro_agent.models.db_chat_channel import DBChatChannel
+
+
+class SegAt(BaseModel):
+    """@ 消息段"""
+
+    platform_user_id: str
+    nickname: Optional[str]
+
+
+async def parse_at_from_text(text: str, db_chat_channel: DBChatChannel) -> List[Union[str, SegAt]]:
+    """从文本中解析@信息 (NoneBot 平台特定功能)
+    需要提取 '[@id:123456;nickname:用户名@]' 或 '[@id:123456@]' 这样的格式，其余的文本不变
+
+    Args:
+        text (str): 文本
+        db_chat_channel (DBChatChannel): 聊天频道信息
+        adapter (BaseAdapter): 适配器实例
+
+    Returns:
+        List[Union[str, SegAt]]: 解析结果 (原始文本或SegAt对象)
+
+    Examples:
+        >>> parse_at_from_text("hello [@id:123456;nickname:用户名@]")
+        ['hello ', SegAt(platform_user_id='123456', nickname='用户名')]
+        >>> parse_at_from_text("hello [@id:123456@]")
+        ['hello ', SegAt(platform_user_id='123456', nickname=None)]
+        >>> parse_at_from_text("hello world")
+        ['hello world']
+    """
+    result = []
+    start = 0
+    while True:
+        at_index = text.find("[@", start)
+        if at_index == -1:
+            result.append(text[start:])
+            break
+        result.append(text[start:at_index])
+        end_index = text.find("@]", at_index)
+        if end_index == -1:
+            result.append(text[at_index:])
+            break
+        seg = text[at_index + 2 : end_index]
+        if "nickname:" in seg:
+            parts = seg.split(";")
+            uid = parts[0].replace("id:", "").strip()
+            nickname = parts[1].replace("nickname:", "").strip()
+            if not config.SESSION_ENABLE_AT:
+                result.append(f"{nickname}")
+            else:
+                if "group" in db_chat_channel.chat_key:
+                    result.append(SegAt(platform_user_id=uid, nickname=None))
+                else:
+                    result.append("")  # 私聊无法@
+        else:
+            uid = seg.replace("id:", "").strip()
+            if not config.SESSION_ENABLE_AT:
path: nekro_agent/adapters/onebot_v11/tools/at_parser.py
user: login: sourcery-ai[bot]
body: **issue (code-quality):** 将嵌套的重复代码提升到条件语句之外 ([`hoist-similar-statement-from-if`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/hoist-similar-statement-from-if/))

<details>
<summary>Original comment in English</summary>

**issue (code-quality):** Hoist nested repeated code outside conditional statements ([`hoist-similar-statement-from-if`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/hoist-similar-statement-from-if/))

</details>
created_at: 2025-05-24T21:36:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,203 @@
+"""通用消息服务
+
+这个服务负责处理跨平台的消息发送逻辑，包含所有与具体协议端无关的通用处理。
+协议端特定的逻辑通过 adapter.forward_message 接口委托给各自的适配器实现。
+"""
+
+from pathlib import Path
+from typing import List, Optional, Union
+
+from nekro_agent.adapters.interface.base import BaseAdapter
+from nekro_agent.adapters.interface.schemas.platform import (
+    PlatformSendRequest,
+    PlatformSendSegment,
+    PlatformSendSegmentType,
+)
+from nekro_agent.adapters.utils import adapter_utils
+from nekro_agent.api.schemas import AgentCtx
+from nekro_agent.core.config import config
+from nekro_agent.core.logger import logger
+from nekro_agent.models.db_chat_channel import DBChatChannel
+from nekro_agent.schemas.agent_message import (
+    AgentMessageSegment,
+    AgentMessageSegmentType,
+)
+from nekro_agent.services.agent.resolver import fix_raw_response
+from nekro_agent.tools.common_util import download_file
+from nekro_agent.tools.path_convertor import (
+    convert_to_host_path,
+    is_url_path,
+)
+
+
+class UniversalChatService:
+    """通用消息服务 - 处理跨平台的消息发送逻辑"""
+
+    def __init__(self):
+        pass
+
+    async def send_operation_message(self, chat_key: str, message: str):
+        """发送操作消息（通用方法）
+
+        Args:
+            chat_key (str): 会话标识
+            message (str): 操作消息内容
+        """
+
+        adapter = await adapter_utils.get_adapter_for_chat(chat_key)
+        await self.send_agent_message(
+            chat_key=chat_key,
+            messages="[Opt Output] " + message,
+            adapter=adapter,
path: nekro_agent/services/chat/universal_chat_service.py
user: login: sourcery-ai[bot]
body: **suggestion (code-quality):** 使用 f-string 而不是字符串连接 ([`use-fstring-for-concatenation`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/use-fstring-for-concatenation/))

```suggestion
            chat_key=chat_key, messages=f"[Opt Output] {message}", adapter=adapter
```

<details>
<summary>Original comment in English</summary>

**suggestion (code-quality):** Use f-string instead of string concatenation ([`use-fstring-for-concatenation`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/use-fstring-for-concatenation/))

```suggestion
            chat_key=chat_key, messages=f"[Opt Output] {message}", adapter=adapter
```

</details>
created_at: 2025-05-24T21:36:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,203 @@
+"""通用消息服务
+
+这个服务负责处理跨平台的消息发送逻辑，包含所有与具体协议端无关的通用处理。
+协议端特定的逻辑通过 adapter.forward_message 接口委托给各自的适配器实现。
+"""
+
+from pathlib import Path
+from typing import List, Optional, Union
+
+from nekro_agent.adapters.interface.base import BaseAdapter
+from nekro_agent.adapters.interface.schemas.platform import (
+    PlatformSendRequest,
+    PlatformSendSegment,
+    PlatformSendSegmentType,
+)
+from nekro_agent.adapters.utils import adapter_utils
+from nekro_agent.api.schemas import AgentCtx
+from nekro_agent.core.config import config
+from nekro_agent.core.logger import logger
+from nekro_agent.models.db_chat_channel import DBChatChannel
+from nekro_agent.schemas.agent_message import (
+    AgentMessageSegment,
+    AgentMessageSegmentType,
+)
+from nekro_agent.services.agent.resolver import fix_raw_response
+from nekro_agent.tools.common_util import download_file
+from nekro_agent.tools.path_convertor import (
+    convert_to_host_path,
+    is_url_path,
+)
+
+
+class UniversalChatService:
+    """通用消息服务 - 处理跨平台的消息发送逻辑"""
+
+    def __init__(self):
+        pass
+
+    async def send_operation_message(self, chat_key: str, message: str):
+        """发送操作消息（通用方法）
+
+        Args:
+            chat_key (str): 会话标识
+            message (str): 操作消息内容
+        """
+
+        adapter = await adapter_utils.get_adapter_for_chat(chat_key)
+        await self.send_agent_message(
+            chat_key=chat_key,
+            messages="[Opt Output] " + message,
+            adapter=adapter,
+        )
+
+    async def send_agent_message(
+        self,
+        chat_key: str,
+        messages: Union[List[AgentMessageSegment], str],
+        adapter: BaseAdapter,
+        ctx: Optional[AgentCtx] = None,
+        file_mode: bool = False,
+        record: bool = False,
+    ):
+        """发送机器人消息
+
+        Args:
+            chat_key (str): 聊天的唯一标识
+            messages (Union[List[AgentMessageSegment], str]): 机器人消息
+            adapter (BaseAdapter): 适配器实例
+            ctx (Optional[AgentCtx], optional): 机器人上下文. Defaults to None.
+            file_mode (bool, optional): 是否为文件发送模式（影响文件类型的消息段生成）. Defaults to False.
+            record (bool, optional): 是否记录聊天记录. Defaults to False.
+
+        Raises:
+            ValueError: 聊天类型错误
+        """
+        if isinstance(messages, str):
+            messages = [AgentMessageSegment(type=AgentMessageSegmentType.TEXT, content=messages)]
+
+        # 预处理消息段
+        processed_segments = await self._preprocess_messages(messages, chat_key, ctx, file_mode)
+
+        # 检查是否有有效的消息段
+        if not processed_segments:
+            logger.warning("Empty Message, skip sending")
+            if config.DEBUG_IN_CHAT:
+                # 发送调试消息
+                debug_request = PlatformSendRequest(
+                    chat_key=chat_key,
+                    segments=[PlatformSendSegment(type=PlatformSendSegmentType.TEXT, content="[Debug] 无消息回复")],
+                )
+                await adapter.forward_message(debug_request)
+            return
+
+        # 构建协议端发送请求
+        send_request = PlatformSendRequest(
+            chat_key=chat_key,
+            segments=processed_segments,
+        )
+
+        # 发送消息
+        try:
+            await adapter.forward_message(send_request)
+        except Exception as e:
+            logger.exception(f"发送消息失败: {e}")
+            if config.DEBUG_IN_CHAT:
+                debug_request = PlatformSendRequest(
+                    chat_key=chat_key,
+                    segments=[
+                        PlatformSendSegment(
+                            type=PlatformSendSegmentType.TEXT,
+                            content="[Debug] 发送消息失败，请检查协议端状态",
+                        ),
+                    ],
+                )
+                await adapter.forward_message(debug_request)
+            else:
+                raise
+
+        # 记录聊天记录
+        if record:
+            from nekro_agent.services.message_service import message_service
+
+            await message_service.push_bot_message(chat_key, messages)
+
+    async def _preprocess_messages(
path: nekro_agent/services/chat/universal_chat_service.py
user: login: sourcery-ai[bot]
body: **issue (code-quality):** 在 UniversalChatService.\_preprocess\_messages 中发现低代码质量 - 23% ([`low-code-quality`](https://docs.sourcery.ai/Reference/Default-Rules/comments/low-code-quality/))

<br/><details><summary>解释</summary>此函数的质量分数低于 25% 的质量阈值。
此分数是方法长度、认知复杂性和工作记忆的组合。

您如何解决这个问题？

可能值得重构此函数，使其更短且更易于阅读。

- 通过将部分功能提取到它们自己的函数中来减少函数长度。这是您可以做的最重要的事情 - 理想情况下，一个函数应该少于 10 行。
- 减少嵌套，也许可以通过引入保护子句来提前返回。
- 确保变量的作用域很小，以便使用相关概念的代码位于函数内部，而不是分散开来。</details>

<details>
<summary>Original comment in English</summary>

**issue (code-quality):** Low code quality found in UniversalChatService.\_preprocess\_messages - 23% ([`low-code-quality`](https://docs.sourcery.ai/Reference/Default-Rules/comments/low-code-quality/))

<br/><details><summary>Explanation</summary>The quality score for this function is below the quality threshold of 25%.
This score is a combination of the method length, cognitive complexity and working memory.

How can you solve this?

It might be worth refactoring this function to make it shorter and more readable.

- Reduce the function length by extracting pieces of functionality out into
  their own functions. This is the most important thing you can do - ideally a
  function should be less than 10 lines.
- Reduce nesting, perhaps by introducing guard clauses to return early.
- Ensure that variables are tightly scoped, so that code using related concepts
  sits together within the function rather than being scattered.</details>

</details>
created_at: 2025-05-24T21:36:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,388 @@
+import json
+from typing import Any, Dict, List, Optional, Union
+
+from nonebot.adapters.minecraft import Bot, Message, MessageSegment
+from nonebot.adapters.minecraft.model import (
+    BaseComponent,
+    ClickAction,
+    ClickEvent,
+    HoverAction,
+    HoverEvent,
+    TextColor,
+    TextComponent,
+)
+
+from nekro_agent.adapters.minecraft.core.bot import get_bot
+from nekro_agent.api import core
+from nekro_agent.api.core import logger
+from nekro_agent.api.plugin import NekroPlugin, SandboxMethod, SandboxMethodType
+from nekro_agent.api.schemas import AgentCtx
+from nekro_agent.models.db_chat_channel import DBChatChannel
+from nekro_agent.services.message_service import message_service
+
+plugin = NekroPlugin(
+    name="Minecraft 工具插件",
+    module_name="minecraft_utils",
+    description="提供 Minecraft 适配器专用的消息发送和 RCON 命令执行功能。",
+    version="0.1.0",
+    author="Zaxpris",
+    url="https://github.com/KroMiose/nekro-agent",
+    support_adapter=["minecraft"],
+)
+
+
+def _llm_segment_dict_to_text_component(segment_dict: Dict[str, Any]) -> TextComponent:
+    """
+    将 LLM 输出的单个富文本段字典转换为 Minecraft TextComponent。
+    主要用于构建 HoverEvent.text 中的组件。
+    """
+    text = segment_dict.get("text", "")
+    component_data: Dict[str, Any] = {"text": text}
+
+    if "color" in segment_dict:
+        try:
+            component_data["color"] = TextColor(str(segment_dict["color"]).lower())
+        except ValueError:
+            logger.warning(
+                f"[MinecraftUtils] 无效的悬停文本颜色: {segment_dict['color']}",
+            )
+
+    for style_attr in ["bold", "italic", "underlined", "strikethrough", "obfuscated"]:
+        if style_attr in segment_dict:
+            component_data[style_attr] = segment_dict[style_attr]
+
+    if "insertion" in segment_dict:
+        component_data["insertion"] = segment_dict["insertion"]
+
+    click_action_str = segment_dict.get("click_event_action")
+    click_value = segment_dict.get("click_event_value")
+    if click_action_str and click_value:
+        try:
+            action = ClickAction(str(click_action_str).lower())
+            component_data["click_event"] = ClickEvent(
+                action=action, value=str(click_value),
+            )
+        except ValueError:
+            logger.warning(
+                f"[MinecraftUtils] 无效的悬停点击事件动作: {click_action_str}",
+            )
+
+    hover_action_str = segment_dict.get("hover_event_action")
+    if hover_action_str:
path: plugins/builtin/minecraft_utils.py
user: login: sourcery-ai[bot]
body: **suggestion (code-quality):** 使用命名表达式来简化赋值和条件 ([`use-named-expression`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/use-named-expression/))

```suggestion
    if hover_action_str := segment_dict.get("hover_event_action"):
```

<details>
<summary>Original comment in English</summary>

**suggestion (code-quality):** Use named expression to simplify assignment and conditional ([`use-named-expression`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/use-named-expression/))

```suggestion
    if hover_action_str := segment_dict.get("hover_event_action"):
```

</details>
created_at: 2025-05-24T21:36:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,388 @@
+import json
+from typing import Any, Dict, List, Optional, Union
+
+from nonebot.adapters.minecraft import Bot, Message, MessageSegment
+from nonebot.adapters.minecraft.model import (
+    BaseComponent,
+    ClickAction,
+    ClickEvent,
+    HoverAction,
+    HoverEvent,
+    TextColor,
+    TextComponent,
+)
+
+from nekro_agent.adapters.minecraft.core.bot import get_bot
+from nekro_agent.api import core
+from nekro_agent.api.core import logger
+from nekro_agent.api.plugin import NekroPlugin, SandboxMethod, SandboxMethodType
+from nekro_agent.api.schemas import AgentCtx
+from nekro_agent.models.db_chat_channel import DBChatChannel
+from nekro_agent.services.message_service import message_service
+
+plugin = NekroPlugin(
+    name="Minecraft 工具插件",
+    module_name="minecraft_utils",
+    description="提供 Minecraft 适配器专用的消息发送和 RCON 命令执行功能。",
+    version="0.1.0",
+    author="Zaxpris",
+    url="https://github.com/KroMiose/nekro-agent",
+    support_adapter=["minecraft"],
+)
+
+
+def _llm_segment_dict_to_text_component(segment_dict: Dict[str, Any]) -> TextComponent:
+    """
+    将 LLM 输出的单个富文本段字典转换为 Minecraft TextComponent。
+    主要用于构建 HoverEvent.text 中的组件。
+    """
+    text = segment_dict.get("text", "")
+    component_data: Dict[str, Any] = {"text": text}
+
+    if "color" in segment_dict:
+        try:
+            component_data["color"] = TextColor(str(segment_dict["color"]).lower())
+        except ValueError:
+            logger.warning(
+                f"[MinecraftUtils] 无效的悬停文本颜色: {segment_dict['color']}",
+            )
+
+    for style_attr in ["bold", "italic", "underlined", "strikethrough", "obfuscated"]:
+        if style_attr in segment_dict:
+            component_data[style_attr] = segment_dict[style_attr]
+
+    if "insertion" in segment_dict:
+        component_data["insertion"] = segment_dict["insertion"]
+
+    click_action_str = segment_dict.get("click_event_action")
+    click_value = segment_dict.get("click_event_value")
+    if click_action_str and click_value:
+        try:
+            action = ClickAction(str(click_action_str).lower())
+            component_data["click_event"] = ClickEvent(
+                action=action, value=str(click_value),
+            )
+        except ValueError:
+            logger.warning(
+                f"[MinecraftUtils] 无效的悬停点击事件动作: {click_action_str}",
+            )
+
+    hover_action_str = segment_dict.get("hover_event_action")
+    if hover_action_str:
+        try:
+            hover_action = HoverAction(str(hover_action_str).lower())
+            if hover_action == HoverAction.SHOW_TEXT:
+                hover_text_segments_data = segment_dict.get("hover_event_text_segments")
+                if isinstance(hover_text_segments_data, list):
+                    nested_hover_components: List[BaseComponent] = [
+                        _llm_segment_dict_to_text_component(s)
+                        for s in hover_text_segments_data
+                    ]
+                    component_data["hover_event"] = HoverEvent(
+                        action=hover_action, text=nested_hover_components,
+                    )
+                else:
+                    logger.warning(
+                        "[MinecraftUtils] hover_event_text_segments 在悬停事件中缺失或非列表",
+                    )
+            # TODO: 支持其他 HoverAction (show_item, show_entity) 如果需要
+        except ValueError:
+            logger.warning(
+                f"[MinecraftUtils] 无效的悬停事件动作: {hover_action_str}",
+            )
+    return TextComponent(**{k: v for k, v in component_data.items() if v is not None})
+
+
+def _llm_segment_to_mc_message_segment(
+    segment_dict: Dict[str, Any],
+) -> Optional[MessageSegment]:
+    """
+    将 LLM 输出的单个富文本段字典转换为 Minecraft MessageSegment。
+    """
+    text = segment_dict.get("text")
+    if text is None:  # 每个段必须有文本
+        logger.warning("[MinecraftUtils] 富文本段缺少 'text' 键。")
+        return None
+
+    segment_args: Dict[str, Any] = {}
+
+    if "color" in segment_dict:
+        try:
+            segment_args["color"] = TextColor(str(segment_dict["color"]).lower())
+        except ValueError:
+            logger.warning(
+                f"[MinecraftUtils] 无效的文本颜色: {segment_dict['color']}",
+            )
+
+    for style_attr in ["bold", "italic", "underlined", "strikethrough", "obfuscated"]:
+        if style_attr in segment_dict:
+            segment_args[style_attr] = segment_dict[style_attr]
+
+    if "insertion" in segment_dict:
+        segment_args["insertion"] = segment_dict["insertion"]
+
+    click_action_str = segment_dict.get("click_event_action")
+    click_value = segment_dict.get("click_event_value")
+    if click_action_str and click_value:
+        try:
+            action = ClickAction(str(click_action_str).lower())
+            segment_args["click_event"] = ClickEvent(
+                action=action, value=str(click_value),
+            )
+        except ValueError:
+            logger.warning(
+                f"[MinecraftUtils] 无效的点击事件动作: {click_action_str}",
+            )
+
+    hover_action_str = segment_dict.get("hover_event_action")
+    if hover_action_str:
path: plugins/builtin/minecraft_utils.py
user: login: sourcery-ai[bot]
body: **suggestion (code-quality):** 使用命名表达式来简化赋值和条件 ([`use-named-expression`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/use-named-expression/))

```suggestion
    if hover_action_str := segment_dict.get("hover_event_action"):
```

<details>
<summary>Original comment in English</summary>

**suggestion (code-quality):** Use named expression to simplify assignment and conditional ([`use-named-expression`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/use-named-expression/))

```suggestion
    if hover_action_str := segment_dict.get("hover_event_action"):
```

</details>
created_at: 2025-05-24T21:36:16Z
author_association: CONTRIBUTOR

PR comments:
user: login: sourcery-ai[bot]
created_at: 2025-05-24T21:28:10Z
body: <!-- Generated by sourcery-ai[bot]: start review_guide -->

```
## Reviewer's Guide (审查指南)

实现了多平台适配器层，并将消息流重构为统一服务，将核心数据模型和上下文更新为新的 chat_key 格式，用 adapter_key+platform_userid 替换了单平台 QQ 绑定，并相应地对齐了前端用户管理 UI。

#### Sequence Diagram: Incoming Message Processing with Adapters (时序图：使用适配器的传入消息处理)

```mermaid
sequenceDiagram
    actor User
    participant PlatformClient as Platform Client (e.g., Minecraft, QQ)
    participant Adapter as Specific Adapter (e.g., MinecraftAdapter)
    participant UniversalMessageService as Universal Message Service
    participant DBChatChannelModel as DBChatChannel
    participant AgentCtxFactory as AgentCtx Factory
    participant AgentRunner

    User->>PlatformClient: Sends message (发送消息)
    PlatformClient->>Adapter: Raw Event (原始事件)
    Adapter->>Adapter: Convert to internal ChatMessage (set adapter_key, platform_userid, channel_id) (转换为内部 ChatMessage（设置 adapter_key、platform_userid、channel_id）)
    Adapter->>DBChatChannelModel: get_or_create(adapter_key, channel_id, type, name)
    DBChatChannelModel-->>Adapter: db_chat_channel
    Adapter->>AgentCtxFactory: create_by_db_chat_channel(db_chat_channel)
    AgentCtxFactory-->>Adapter: agent_ctx
    Adapter->>UniversalMessageService: push_human_message(ChatMessage, db_chat_channel)
    activate UniversalMessageService
    UniversalMessageService->>DBChatMessageModel: Create message record (创建消息记录)
    deactivate UniversalMessageService
    alt Message is for Bot (消息是给 Bot 的)
        UniversalMessageService->>AgentRunner: run_agent(chat_key, ChatMessage, agent_ctx)
    end
```

#### Sequence Diagram: Outgoing Message Sending with Adapters (时序图：使用适配器的传出消息发送)

```mermaid
sequenceDiagram
    participant AgentLogic as Agent Logic / Plugin
    participant MessageAPI as nekro_agent.api.message
    participant AdapterUtils as nekro_agent.adapters.utils
    participant UniversalChatService as Universal Chat Service
    participant SpecificAdapter as Specific Adapter (e.g., MinecraftAdapter)
    participant PlatformClient as Platform Client
    actor User

    AgentLogic->>MessageAPI: send_text(chat_key, text, ctx)
    MessageAPI->>AdapterUtils: get_adapter_for_ctx(ctx)
    AdapterUtils-->>MessageAPI: adapter_instance
    MessageAPI->>UniversalChatService: send_agent_message(chat_key, segments, adapter_instance, ctx)
    activate UniversalChatService
    UniversalChatService->>DBChatMessageModel: Create bot message record (with adapter_key, platform_userid from adapter) (创建 Bot 消息记录（带有来自适配器的 adapter_key、platform_userid）)
    UniversalChatService->>SpecificAdapter: send_to_platform(chat_key, segments)
    deactivate UniversalChatService
    SpecificAdapter->>PlatformClient: Formatted Message (格式化消息)
    PlatformClient->>User: Receives Message (接收消息)
```

#### Entity Relationship Diagram: Database Schema Changes (实体关系图：数据库模式更改)

```mermaid
erDiagram
    DBChatChannel {
        string adapter_key PK
        string channel_id PK
        string chat_key "derived (adapter_key-channel_id)" (派生，adapter_key-channel_id)
        string channel_name
        string channel_type
        bool is_active
        int preset_id
    }
    DBChatMessage {
        int id PK
        string adapter_key FK
        string channel_id FK "Refers to DBChatChannel(channel_id) via chat_key logic" (通过 chat_key 逻辑引用 DBChatChannel(channel_id))
        string chat_key "Refers to DBChatChannel(chat_key)" (引用 DBChatChannel(chat_key))
        string platform_userid "User's ID on the platform" (用户在平台上的 ID)
        string sender_id "Internal DBUser.id or platform_userid" (内部 DBUser.id 或 platform_userid)
        string sender_name
        string content_text
        datetime send_timestamp
    }
    DBUser {
        int id PK
        string adapter_key PK "Composite logical key with platform_userid" (与 platform_userid 的复合逻辑键)
        string platform_userid PK "Composite logical key with adapter_key" (与 adapter_key 的复合逻辑键)
        string username
        string unique_id "derived (adapter_key:platform_userid)" (派生，adapter_key:platform_userid)
        int perm_level
    }
    DBChatChannel ||--o{ DBChatMessage : has (拥有)
    DBUser ||--o{ DBChatMessage : sent_by (发送者)
```

#### Class Diagram: Adapter and Context Propagation Changes (类图：适配器和上下文传播更改)

```mermaid
classDiagram
    direction LR
    class BaseAdapter {
        <<Interface>>
        +adapter_key: string
        +chat_key_rules: List~string~
        +get_self_info(): AdapterSelfInfo
        +get_channel_info(channel_id: string): AdapterChannelInfo
        +set_message_reaction(message_id: string, reaction_state: bool)
        +send_message(chat_key: string, segments: List~Segment~)
        +convert_event_to_chat_message(event: Any): ChatMessage
    }
    class OneBotV11Adapter {
        <<Adapter>>
    }
    class MinecraftAdapter {
        <<Adapter>>
    }
    BaseAdapter <|-- OneBotV11Adapter
    BaseAdapter <|-- MinecraftAdapter

    class AgentCtx {
      -from_chat_key: string (optional)
      +adapter_key: string (new) (新)
      +channel_id: string (new) (新)
      +channel_name: string (new) (新)
      +channel_type: string (new) (新)
      +webhook_request: WebhookRequest
      +container_key: string
      +chat_key: string (property)
      +adapter: BaseAdapter (property)
      +create_by_db_chat_channel(db_chat_channel: DBChatChannel)
      +create_by_chat_key(chat_key: string)
      +create_by_webhook(webhook_request: WebhookRequest)
    }
    AgentCtx ..> BaseAdapter : uses (使用)

    class NekroPlugin {
        +name: string
        +support_adapter: List~string~ (usage emphasized) (强调用法)
        +get_sandbox_methods(): List~SandboxMethod~
        +get_prompt_inject_methods(): List~PromptInjectMethod~
    }
    class PluginManager {
        +render_plugins_prompt(plugins: List~NekroPlugin~, ctx: AgentCtx): string
    }
    PluginManager ..> NekroPlugin : uses (使用)
    PluginManager ..> AgentCtx : uses for filtering (用于过滤)
```

#### Class Diagram: Core Data Model Changes (DBChatChannel, DBChatMessage, DBUser) (类图：核心数据模型更改（DBChatChannel、DBChatMessage、DBUser）)

```mermaid
classDiagram
    class DBChatChannel {
      -chat_key: string
      +adapter_key: string (new) (新)
      +channel_id: string (new) (新)
      +channel_name: string
      +channel_type: string (new) (新)
      +is_active: bool
      +preset_id: int
      +data: string
      +chat_key: string (property, new format: adapter_key-channel_id) (属性，新格式：adapter_key-channel_id)
      +get_or_create(adapter_key, channel_id, channel_type, channel_name) (new) (新)
      +get_channel(chat_key) (modified) (修改)
      -sync_channel_name() (removed) (移除)
      +get_channel_name() (modified) (修改)
      +adapter: BaseAdapter (property, new) (属性，新)
    }
    class DBChatMessage {
      -sender_bind_qq: string (removed) (移除)
      -sender_real_nickname: string (removed) (移除)
      +adapter_key: string (new) (新)
      +platform_userid: string (new) (新)
      +sender_name: string (new) (新)
      +message_id: string
      +sender_id: string
      +sender_nickname: string
      +is_tome: int
      +is_recalled: bool
      +chat_key: string
      +chat_type: string
      +content_text: string
      +content_data: string
      +parse_chat_history_prompt() (modified) (修改)
      +convert_raw_msg_data_json_to_msg_prompt() (moved & modified) (移动并修改)
    }
    class DBUser {
      -bind_qq: string (removed) (移除)
      +adapter_key: string (new) (新)
      +platform_userid: string (new) (新)
      +id: int
      +username: string
      +password: string
      +perm_level: int
      +unique_id: string (property, new) (属性，新)
      +get_by_union_id(adapter_key, platform_userid) (new) (新)
    }
    DBChatChannel "1" -- "*" DBChatMessage : contains (包含)
    DBUser "1" -- "*" DBChatMessage : sent by (发送者)
```

#### Class Diagram: Chat Message Schema Changes (类图：聊天消息模式更改)

```mermaid
classDiagram
    class ChatMessage {
        <<Schema>>
        -sender_bind_qq: string (removed) (移除)
        -sender_real_nickname: string (removed) (移除)
        +adapter_key: string (new) (新)
        +platform_userid: string (new) (新)
        +sender_name: string (new) (新)
        +message_id: string
        +sender_id: string
        +sender_nickname: string
        +chat_key: string
        +content_data: List~ChatMessageSegment~
    }
    class ChatMessageSegmentAt {
        <<Schema>>
        -target_qq: string (removed) (移除)
        +target_platform_userid: string (new) (新)
        +target_nickname: string
    }
    ChatMessage *-- ChatMessageSegmentAt : contains (包含)
```

### File-Level Changes (文件级别更改)

| Change (变更) | Details (详情) | Files (文件) |
| ------ | ------- | ----- |
| Introduced a cross‐platform adapter abstraction and centralized routing via a universal chat service (引入了跨平台适配器抽象，并通过通用聊天服务集中路由) | <ul><li>Defined BaseAdapter interface and PlatformSend schemas (定义了 BaseAdapter 接口和 PlatformSend 模式)</li><li>Created OneBot V11 and Minecraft adapter implementations (创建了 OneBot V11 和 Minecraft 适配器实现)</li><li>Added adapter loader in run_bot/startup and adapter_utils helper (在 run_bot/startup 中添加了适配器加载器和 adapter_utils 助手)</li><li>Built universal_chat_service to preprocess AgentMessageSegment into PlatformSendRequest (构建了 universal_chat_service 以将 AgentMessageSegment 预处理为 PlatformSendRequest)</li></ul> | `nekro_agent/adapters/interface/base.py`<br/>`nekro_agent/adapters/onebot_v11/adapter.py`<br/>`nekro_agent/services/chat/universal_chat_service.py`<br/>`run_bot.py`<br/>`nekro_agent/adapters/utils.py` |
| Unified and refactored chat_key, context and message models for cross‐platform support (统一并重构了 chat_key、上下文和消息模型，以实现跨平台支持) | <ul><li>Extended DBChatChannel with adapter_key, channel_id/type and get_or_create (使用 adapter_key、channel_id/type 和 get_or_create 扩展了 DBChatChannel)</li><li>Updated DBChatMessage, AgentCtx and chat_message schema to use chat_key rules (更新了 DBChatMessage、AgentCtx 和 chat_message 模式以使用 chat_key 规则)</li><li>Rewrote run_agent and services to leverage ctx.adapter and new chat_key format (重写了 run_agent 和服务以利用 ctx.adapter 和新的 chat_key 格式)</li></ul> | `nekro_agent/models/db_chat_channel.py`<br/>`nekro_agent/schemas/agent_ctx.py`<br/>`nekro_agent/models/db_chat_message.py`<br/>`nekro_agent/schemas/chat_message.py`<br/>`nekro_agent/services/agent/run_agent.py` |
| Transformed user identity from bind_qq to multi‐platform adapter_key+platform_userid (将用户身份从 bind_qq 转换为多平台 adapter_key+platform_userid) | <ul><li>Added adapter_key and platform_userid fields on DBUser and unique_id property (在 DBUser 和 unique_id 属性上添加了 adapter_key 和 platform_userid 字段)</li><li>Refactored user_register, user_login and deps to use new identifiers (重构了 user_register、user_login 和 deps 以使用新的标识符)</li><li>Updated services and routers to query by adapter_key+platform_userid (更新了服务和路由器以按 adapter_key+platform_userid 查询)</li></ul> | `nekro_agent/models/db_user.py`<br/>`nekro_agent/services/user/util.py`<br/>`nekro_agent/services/user/deps.py` |
| Aligned front-end user management to reflect multi-platform identity (对齐前端用户管理以反映多平台身份) | <ul><li>Replaced bind_qq with adapter_key and platform_userid in table, form and API interfaces (在表、表单和 API 接口中，用 adapter_key 和 platform_userid 替换了 bind_qq)</li><li>Adjusted search, display columns and form validation (调整了搜索、显示列和表单验证)</li><li>Updated user-manager API types and responses (更新了 user-manager API 类型和响应)</li></ul> | `frontend/src/pages/user-manager/components/UserTable.tsx`<br/>`frontend/src/pages/user-manager/components/UserForm.tsx`<br/>`frontend/src/services/api/user-manager.ts` |

---

<details>
<summary>Tips and commands (提示和命令)</summary>

#### Interacting with Sourcery (与 Sourcery 互动)

- **Trigger a new review:** Comment `@sourcery-ai review` on the pull request. (触发新的审查：在拉取请求上评论 `@sourcery-ai review`。)
- **Continue discussions:** Reply directly to Sourcery's review comments. (继续讨论：直接回复 Sourcery 的审查评论。)
- **Generate a GitHub issue from a review comment:** Ask Sourcery to create an
  issue from a review comment by replying to it. You can also reply to a
  review comment with `@sourcery-ai issue` to create an issue from it. (从审查评论生成 GitHub 问题：通过回复审查评论，要求 Sourcery 从审查评论创建一个问题。您也可以回复审查评论并使用 `@sourcery-ai issue` 从中创建一个问题。)
- **Generate a pull request title:** Write `@sourcery-ai` anywhere in the pull
  request title to generate a title at any time. You can also comment
  `@sourcery-ai title` on the pull request to (re-)generate the title at any time. (生成拉取请求标题：在拉取请求标题中的任何位置写入 `@sourcery-ai` 以随时生成标题。您也可以在拉取请求上评论 `@sourcery-ai title` 以随时（重新）生成标题。)
- **Generate a pull request summary:** Write `@sourcery-ai summary` anywhere in
  the pull request body to generate a PR summary at any time exactly where you
  want it. You can also comment `@sourcery-ai summary` on the pull request to
  (re-)generate the summary at any time. (生成拉取请求摘要：在拉取请求正文中的任何位置写入 `@sourcery-ai summary` 以随时在您想要的位置生成 PR 摘要。您也可以在拉取请求上评论 `@sourcery-ai summary` 以随时（重新）生成摘要。)
- **Generate reviewer's guide:** Comment `@sourcery-ai guide` on the pull
  request to (re-)generate the reviewer's guide at any time. (生成审查指南：在拉取请求上评论 `@sourcery-ai guide` 以随时（重新）生成审查指南。)
- **Resolve all Sourcery comments:** Comment `@sourcery-ai resolve` on the
  pull request to resolve all Sourcery comments. Useful if you've already
  addressed all the comments and don't want to see them anymore. (解决所有 Sourcery 评论：在拉取请求上评论 `@sourcery-ai resolve` 以解决所有 Sourcery 评论。如果您已经解决了所有评论并且不想再看到它们，这将非常有用。)
- **Dismiss all Sourcery reviews:** Comment `@sourcery-ai dismiss` on the pull
  request to dismiss all existing Sourcery reviews. Especially useful if you
  want to start fresh with a new review - don't forget to comment
  `@sourcery-ai review` to trigger a new review! (驳回所有 Sourcery 审查：在拉取请求上评论 `@sourcery-ai dismiss` 以驳回所有现有的 Sourcery 审查。如果您想从新的审查开始，这将特别有用 - 不要忘记评论 `@sourcery-ai review` 以触发新的审查！)

#### Customizing Your Experience (自定义您的体验)

Access your [dashboard](https://app.sourcery.ai) to: (访问您的 [仪表板](https://app.sourcery.ai) 以：)
- Enable or disable review features such as the Sourcery-generated pull request
  summary, the reviewer's guide, and others. (启用或禁用审查功能，例如 Sourcery 生成的拉取请求摘要、审查指南等。)
- Change the review language. (更改审查语言。)
- Add, remove or edit custom review instructions. (添加、删除或编辑自定义审查说明。)
- Adjust other review settings. (调整其他审查设置。)

#### Getting Help (获取帮助)

- [Contact our support team](mailto:support@sourcery.ai) for questions or feedback. (如有问题或反馈，请[联系我们的支持团队](mailto:support@sourcery.ai)。)
- Visit our [documentation](https://docs.sourcery.ai) for detailed guides and information. (访问我们的[文档](https://docs.sourcery.ai)以获取详细的指南和信息。)
- Keep in touch with the Sourcery team by following us on [X/Twitter](https://x.com/SourceryAI), [LinkedIn](https://www.linkedin.com/company/sourcery-ai/) or [GitHub](https://github.com/sourcery-ai). (通过在 [X/Twitter](https://x.com/SourceryAI)、[LinkedIn](https://www.linkedin.com/company/sourcery-ai/) 或 [GitHub](https://github.com/sourcery-ai) 上关注我们，与 Sourcery 团队保持联系。)

</details>
```

<details>
<summary>Original review guide in English</summary>

## Reviewer's Guide

Implements a multi‐platform adapter layer and refactors message flow into a unified service, updates core data models and context to a new chat_key format, replaces single‐platform QQ binding with adapter_key+platform_userid, and aligns front‐end user management UI accordingly.

#### Sequence Diagram: Incoming Message Processing with Adapters

```mermaid
sequenceDiagram
    actor User
    participant PlatformClient as Platform Client (e.g., Minecraft, QQ)
    participant Adapter as Specific Adapter (e.g., MinecraftAdapter)
    participant UniversalMessageService as Universal Message Service
    participant DBChatChannelModel as DBChatChannel
    participant AgentCtxFactory as AgentCtx Factory
    participant AgentRunner

    User->>PlatformClient: Sends message
    PlatformClient->>Adapter: Raw Event
    Adapter->>Adapter: Convert to internal ChatMessage (set adapter_key, platform_userid, channel_id)
    Adapter->>DBChatChannelModel: get_or_create(adapter_key, channel_id, type, name)
    DBChatChannelModel-->>Adapter: db_chat_channel
    Adapter->>AgentCtxFactory: create_by_db_chat_channel(db_chat_channel)
    AgentCtxFactory-->>Adapter: agent_ctx
    Adapter->>UniversalMessageService: push_human_message(ChatMessage, db_chat_channel)
    activate UniversalMessageService
    UniversalMessageService->>DBChatMessageModel: Create message record
    deactivate UniversalMessageService
    alt Message is for Bot
        UniversalMessageService->>AgentRunner: run_agent(chat_key, ChatMessage, agent_ctx)
    end
```

#### Sequence Diagram: Outgoing Message Sending with Adapters

```mermaid
sequenceDiagram
    participant AgentLogic as Agent Logic / Plugin
    participant MessageAPI as nekro_agent.api.message
    participant AdapterUtils as nekro_agent.adapters.utils
    participant UniversalChatService as Universal Chat Service
    participant SpecificAdapter as Specific Adapter (e.g., MinecraftAdapter)
    participant PlatformClient as Platform Client
    actor User

    AgentLogic->>MessageAPI: send_text(chat_key, text, ctx)
    MessageAPI->>AdapterUtils: get_adapter_for_ctx(ctx)
    AdapterUtils-->>MessageAPI: adapter_instance
    MessageAPI->>UniversalChatService: send_agent_message(chat_key, segments, adapter_instance, ctx)
    activate UniversalChatService
    UniversalChatService->>DBChatMessageModel: Create bot message record (with adapter_key, platform_userid from adapter)
    UniversalChatService->>SpecificAdapter: send_to_platform(chat_key, segments)
    deactivate UniversalChatService
    SpecificAdapter->>PlatformClient: Formatted Message
    PlatformClient->>User: Receives Message
```

#### Entity Relationship Diagram: Database Schema Changes

```mermaid
erDiagram
    DBChatChannel {
        string adapter_key PK
        string channel_id PK
        string chat_key "derived (adapter_key-channel_id)"
        string channel_name
        string channel_type
        bool is_active
        int preset_id
    }
    DBChatMessage {
        int id PK
        string adapter_key FK
        string channel_id FK "Refers to DBChatChannel(channel_id) via chat_key logic"
        string chat_key "Refers to DBChatChannel(chat_key)"
        string platform_userid "User's ID on the platform"
        string sender_id "Internal DBUser.id or platform_userid"
        string sender_name
        string content_text
        datetime send_timestamp
    }
    DBUser {
        int id PK
        string adapter_key PK "Composite logical key with platform_userid"
        string platform_userid PK "Composite logical key with adapter_key"
        string username
        string unique_id "derived (adapter_key:platform_userid)"
        int perm_level
    }
    DBChatChannel ||--o{ DBChatMessage : has
    DBUser ||--o{ DBChatMessage : sent_by
```

#### Class Diagram: Adapter and Context Propagation Changes

```mermaid
classDiagram
    direction LR
    class BaseAdapter {
        <<Interface>>
        +adapter_key: string
        +chat_key_rules: List~string~
        +get_self_info(): AdapterSelfInfo
        +get_channel_info(channel_id: string): AdapterChannelInfo
        +set_message_reaction(message_id: string, reaction_state: bool)
        +send_message(chat_key: string, segments: List~Segment~)
        +convert_event_to_chat_message(event: Any): ChatMessage
    }
    class OneBotV11Adapter {
        <<Adapter>>
    }
    class MinecraftAdapter {
        <<Adapter>>
    }
    BaseAdapter <|-- OneBotV11Adapter
    BaseAdapter <|-- MinecraftAdapter

    class AgentCtx {
      -from_chat_key: string (optional)
      +adapter_key: string (new)
      +channel_id: string (new)
      +channel_name: string (new)
      +channel_type: string (new)
      +webhook_request: WebhookRequest
      +container_key: string
      +chat_key: string (property)
      +adapter: BaseAdapter (property)
      +create_by_db_chat_channel(db_chat_channel: DBChatChannel)
      +create_by_chat_key(chat_key: string)
      +create_by_webhook(webhook_request: WebhookRequest)
    }
    AgentCtx ..> BaseAdapter : uses

    class NekroPlugin {
        +name: string
        +support_adapter: List~string~ (usage emphasized)
        +get_sandbox_methods(): List~SandboxMethod~
        +get_prompt_inject_methods(): List~PromptInjectMethod~
    }
    class PluginManager {
        +render_plugins_prompt(plugins: List~NekroPlugin~, ctx: AgentCtx): string
    }
    PluginManager ..> NekroPlugin : uses
    PluginManager ..> AgentCtx : uses for filtering
```

#### Class Diagram: Core Data Model Changes (DBChatChannel, DBChatMessage, DBUser)

```mermaid
classDiagram
    class DBChatChannel {
      -chat_key: string
      +adapter_key: string (new)
      +channel_id: string (new)
      +channel_name: string
      +channel_type: string (new)
      +is_active: bool
      +preset_id: int
      +data: string
      +chat_key: string (property, new format: adapter_key-channel_id)
      +get_or_create(adapter_key, channel_id, channel_type, channel_name) (new)
      +get_channel(chat_key) (modified)
      -sync_channel_name() (removed)
      +get_channel_name() (modified)
      +adapter: BaseAdapter (property, new)
    }
    class DBChatMessage {
      -sender_bind_qq: string (removed)
      -sender_real_nickname: string (removed)
      +adapter_key: string (new)
      +platform_userid: string (new)
      +sender_name: string (new)
      +message_id: string
      +sender_id: string
      +sender_nickname: string
      +is_tome: int
      +is_recalled: bool
      +chat_key: string
      +chat_type: string
      +content_text: string
      +content_data: string
      +parse_chat_history_prompt() (modified)
      +convert_raw_msg_data_json_to_msg_prompt() (moved & modified)
    }
    class DBUser {
      -bind_qq: string (removed)
      +adapter_key: string (new)
      +platform_userid: string (new)
      +id: int
      +username: string
      +password: string
      +perm_level: int
      +unique_id: string (property, new)
      +get_by_union_id(adapter_key, platform_userid) (new)
    }
    DBChatChannel "1" -- "*" DBChatMessage : contains
    DBUser "1" -- "*" DBChatMessage : sent by
```

#### Class Diagram: Chat Message Schema Changes

```mermaid
classDiagram
    class ChatMessage {
        <<Schema>>
        -sender_bind_qq: string (removed)
        -sender_real_nickname: string (removed)
        +adapter_key: string (new)
        +platform_userid: string (new)
        +sender_name: string (new)
        +message_id: string
        +sender_id: string
        +sender_nickname: string
        +chat_key: string
        +content_data: List~ChatMessageSegment~
    }
    class ChatMessageSegmentAt {
        <<Schema>>
        -target_qq: string (removed)
        +target_platform_userid: string (new)
        +target_nickname: string
    }
    ChatMessage *-- ChatMessageSegmentAt : contains
```

### File-Level Changes

| Change | Details | Files |
| ------ | ------- | ----- |
| Introduced a cross‐platform adapter abstraction and centralized routing via a universal chat service | <ul><li>Defined BaseAdapter interface and PlatformSend schemas</li><li>Created OneBot V11 and Minecraft adapter implementations</li><li>Added adapter loader in run_bot/startup and adapter_utils helper</li><li>Built universal_chat_service to preprocess AgentMessageSegment into PlatformSendRequest</li></ul> | `nekro_agent/adapters/interface/base.py`<br/>`nekro_agent/adapters/onebot_v11/adapter.py`<br/>`nekro_agent/services/chat/universal_chat_service.py`<br/>`run_bot.py`<br/>`nekro_agent/adapters/utils.py` |
| Unified and refactored chat_key, context and message models for cross‐platform support | <ul><li>Extended DBChatChannel with adapter_key, channel_id/type and get_or_create</li><li>Updated DBChatMessage, AgentCtx and chat_message schema to use chat_key rules</li><li>Rewrote run_agent and services to leverage ctx.adapter and new chat_key format</li></ul> | `nekro_agent/models/db_chat_channel.py`<br/>`nekro_agent/schemas/agent_ctx.py`<br/>`nekro_agent/models/db_chat_message.py`<br/>`nekro_agent/schemas/chat_message.py`<br/>`nekro_agent/services/agent/run_agent.py` |
| Transformed user identity from bind_qq to multi‐platform adapter_key+platform_userid | <ul><li>Added adapter_key and platform_userid fields on DBUser and unique_id property</li><li>Refactored user_register, user_login and deps to use new identifiers</li><li>Updated services and routers to query by adapter_key+platform_userid</li></ul> | `nekro_agent/models/db_user.py`<br/>`nekro_agent/services/user/util.py`<br/>`nekro_agent/services/user/deps.py` |
| Aligned front-end user management to reflect multi-platform identity | <ul><li>Replaced bind_qq with adapter_key and platform_userid in table, form and API interfaces</li><li>Adjusted search, display columns and form validation</li><li>Updated user-manager API types and responses</li></ul> | `frontend/src/pages/user-manager/components/UserTable.tsx`<br/>`frontend/src/pages/user-manager/components/UserForm.tsx`<br/>`frontend/src/services/api/user-manager.ts` |

---

<details>
<summary>Tips and commands</summary>

#### Interacting with Sourcery

- **Trigger a new review:** Comment `@sourcery-ai review` on the pull request.
- **Continue discussions:** Reply directly to Sourcery's review comments.
- **Generate a GitHub issue from a review comment:** Ask Sourcery to create an
  issue from a review comment by replying to it. You can also reply to a
  review comment with `@sourcery-ai issue` to create an issue from it.
- **Generate a pull request title:** Write `@sourcery-ai` anywhere in the pull
  request title to generate a title at any time. You can also comment
  `@sourcery-ai title` on the pull request to (re-)generate the title at any time.
- **Generate a pull request summary:** Write `@sourcery-ai summary` anywhere in
  the pull request body to generate a PR summary at any time exactly where you
  want it. You can also comment `@sourcery-ai summary` on the pull request to
  (re-)generate the summary at any time.
- **Generate reviewer's guide:** Comment `@sourcery-ai guide` on the pull
  request to (re-)generate the reviewer's guide at any time.
- **Resolve all Sourcery comments:** Comment `@sourcery-ai resolve` on the
  pull request to resolve all Sourcery comments. Useful if you've already
  addressed all the comments and don't want to see them anymore.
- **Dismiss all Sourcery reviews:** Comment `@sourcery-ai dismiss` on the pull
  request to dismiss all existing Sourcery reviews. Especially useful if you
  want to start fresh with a new review - don't forget to comment
  `@sourcery-ai review` to trigger a new review!

#### Customizing Your Experience

Access your [dashboard](https://app.sourcery.ai) to:
- Enable or disable review features such as the Sourcery-generated pull request
  summary, the reviewer's guide, and others.
- Change the review language.
- Add, remove or edit custom review instructions.
- Adjust other review settings.

#### Getting Help

- [Contact our support team](mailto:support@sourcery.ai) for questions or feedback.
- Visit our [documentation](https://docs.sourcery.ai) for detailed guides and information.
- Keep in touch with the Sourcery team by following us on [X/Twitter](https://x.com/SourceryAI), [LinkedIn](https://www.linkedin.com/company/sourcery-ai/) or [GitHub](https://github.com/sourcery-ai).

</details>

</details>

<!-- Generated by sourcery-ai[bot]: end review_guide -->
author_association: CONTRIBUTOR
user: login: zxjwzn
created_at: 2025-05-24T21:36:06Z
body: mc服务器列表需从环境变量中加载
我在上传时填写的值为空
因此plugin load test出现了2 validation errors for Config错误
author_association: COLLABORATOR