Repository: AstrBotDevs/AstrBot
Pull request #1179: feat: 添加插件能针对不同消息平台开启关闭的功能
Author: login: zhx8702
Created: 2025-04-08T00:49:51Z | Merged: 2025-04-10T03:02:09Z

修复了 #914

### Motivation
为每一个平台都添加可以对插件单独启用的功能

### Modifications
 所有平台适配器meta初始化添加平台id 作为后面校验唯一键，在命令执行前根据平台配置判断是否启用插件

![WeChat469493e0d5a05ce52b68fba03c7978bc](https://github.com/user-attachments/assets/82a02945-f448-40ce-b20b-fedfbc1fae42)

## Sourcery 总结

添加特定于平台的插件启用/禁用功能，以实现对不同消息传递平台上的插件可用性的精细控制

新功能：
- 实现一个配置界面，用于启用或禁用特定平台的插件
- 添加平台级别的插件管理，支持系统插件和自定义插件

增强功能：
- 扩展平台元数据以包含唯一的平台 ID
- 在命令处理程序中添加特定于平台的插件过滤
- 改进插件平台配置的日志记录

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

## Summary by Sourcery

Add platform-specific plugin enable/disable functionality to allow granular control over plugin availability across different messaging platforms

New Features:
- Implement a configuration interface to enable or disable plugins for specific platforms
- Add platform-level plugin management with support for system and custom plugins

Enhancements:
- Extend platform metadata to include a unique platform ID
- Add platform-specific plugin filtering in command handlers
- Improve logging for plugin platform configuration

</details>


Diff:
diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py
index 13be0b4985..4a63649798 100644
--- a/astrbot/core/config/default.py
+++ b/astrbot/core/config/default.py
@@ -248,6 +248,9 @@
                 "description": "平台设置",
                 "type": "object",
                 "items": {
+                    "plugin_enable": {
+                        "invisible": True,  # 隐藏插件启用配置
+                    },
                     "unique_session": {
                         "description": "会话隔离",
                         "type": "bool",
diff --git a/astrbot/core/pipeline/__init__.py b/astrbot/core/pipeline/__init__.py
index b97fc0f120..406fcc7967 100644
--- a/astrbot/core/pipeline/__init__.py
+++ b/astrbot/core/pipeline/__init__.py
@@ -7,6 +7,7 @@
 from .whitelist_check.stage import WhitelistCheckStage
 from .rate_limit_check.stage import RateLimitStage
 from .content_safety_check.stage import ContentSafetyCheckStage
+from .platform_compatibility.stage import PlatformCompatibilityStage
 from .preprocess_stage.stage import PreProcessStage
 from .process_stage.stage import ProcessStage
 from .result_decorate.stage import ResultDecorateStage
@@ -18,6 +19,7 @@
     "WhitelistCheckStage",  # 检查是否在群聊/私聊白名单
     "RateLimitStage",  # 检查会话是否超过频率限制
     "ContentSafetyCheckStage",  # 检查内容安全
+    "PlatformCompatibilityStage",  # 检查所有处理器的平台兼容性
     "PreProcessStage",  # 预处理
     "ProcessStage",  # 交由 Stars 处理（a.k.a 插件），或者 LLM 调用
     "ResultDecorateStage",  # 处理结果，比如添加回复前缀、t2i、转换为语音 等
@@ -29,6 +31,7 @@
     "WhitelistCheckStage",
     "RateLimitStage",
     "ContentSafetyCheckStage",
+    "PlatformCompatibilityStage",
     "PreProcessStage",
     "ProcessStage",
     "ResultDecorateStage",
diff --git a/astrbot/core/pipeline/platform_compatibility/stage.py b/astrbot/core/pipeline/platform_compatibility/stage.py
new file mode 100644
index 0000000000..644912c26d
--- /dev/null
+++ b/astrbot/core/pipeline/platform_compatibility/stage.py
@@ -0,0 +1,56 @@
+from ..stage import Stage, register_stage
+from ..context import PipelineContext
+from typing import Union, AsyncGenerator
+from astrbot.core.platform.astr_message_event import AstrMessageEvent
+from astrbot.core.star.star import star_map
+from astrbot.core.star.star_handler import StarHandlerMetadata
+from astrbot.core import logger
+
+
+@register_stage
+class PlatformCompatibilityStage(Stage):
+    """检查所有处理器的平台兼容性。
+
+    这个阶段会检查所有处理器是否在当前平台启用，如果未启用则设置platform_compatible属性为False。
+    """
+
+    async def initialize(self, ctx: PipelineContext) -> None:
+        """初始化平台兼容性检查阶段
+
+        Args:
+            ctx (PipelineContext): 消息管道上下文对象, 包括配置和插件管理器
+        """
+        self.ctx = ctx
+
+    async def process(
+        self, event: AstrMessageEvent
+    ) -> Union[None, AsyncGenerator[None, None]]:
+        # 获取当前平台ID
+        platform_id = event.get_platform_id()
+
+        # 获取已激活的处理器
+        activated_handlers = event.get_extra("activated_handlers")
+        if activated_handlers is None:
+            activated_handlers = []
+
+        # 标记不兼容的处理器
+        for handler in activated_handlers:
+            if not isinstance(handler, StarHandlerMetadata):
+                continue
+            # 检查处理器是否在当前平台启用
+            enabled = handler.is_enabled_for_platform(platform_id)
+            if not enabled:
+                if handler.handler_module_path in star_map:
+                    plugin_name = star_map[handler.handler_module_path].name
+                logger.debug(
+                    f"[PlatformCompatibilityStage] 插件 {plugin_name} 在平台 {platform_id} 未启用，标记处理器 {handler.handler_name} 为平台不兼容"
+                )
+                # 设置处理器为平台不兼容状态
+                # TODO: 更好的标记方式
+                handler.platform_compatible = False
+            else:
+                # 确保处理器为平台兼容状态
+                handler.platform_compatible = True
+
+        # 更新已激活的处理器列表
+        event.set_extra("activated_handlers", activated_handlers)
diff --git a/astrbot/core/pipeline/process_stage/method/llm_request.py b/astrbot/core/pipeline/process_stage/method/llm_request.py
index c6a87b37c1..a9f793626a 100644
--- a/astrbot/core/pipeline/process_stage/method/llm_request.py
+++ b/astrbot/core/pipeline/process_stage/method/llm_request.py
@@ -108,8 +108,10 @@ async def process(
 
         # 执行请求 LLM 前事件钩子。
         # 装饰 system_prompt 等功能
+        # 获取当前平台ID
+        platform_id = event.get_platform_id()
         handlers = star_handlers_registry.get_handlers_by_event_type(
-            EventType.OnLLMRequestEvent
+            EventType.OnLLMRequestEvent, platform_id=platform_id
         )
         for handler in handlers:
             try:
@@ -368,6 +370,15 @@ async def _handle_function_tools(
                             )
                         )
                 else:
+                    # 获取处理器，过滤掉平台不兼容的处理器
+                    platform_id = event.get_platform_id()
+                    if not func_tool.handler.is_enabled_for_platform(platform_id):
+                        logger.debug(
+                            f"处理器 {func_tool_name} 在当前平台不兼容，跳过执行"
+                        )
+                        # 直接跳过，不添加任何消息到tool_call_result
+                        continue
+
                     logger.info(
                         f"调用工具函数：{func_tool_name}，参数：{func_tool_args}"
                     )
diff --git a/astrbot/core/pipeline/process_stage/method/star_request.py b/astrbot/core/pipeline/process_stage/method/star_request.py
index d369e53ed9..c7817e49ca 100644
--- a/astrbot/core/pipeline/process_stage/method/star_request.py
+++ b/astrbot/core/pipeline/process_stage/method/star_request.py
@@ -31,7 +31,18 @@ async def process(
         )
         if not handlers_parsed_params:
             handlers_parsed_params = {}
+
         for handler in activated_handlers:
+            # 检查处理器是否在当前平台兼容
+            if (
+                hasattr(handler, "platform_compatible")
+                and handler.platform_compatible is False
+            ):
+                logger.debug(
+                    f"处理器 {handler.handler_name} 在当前平台不兼容，跳过执行"
+                )
+                continue
+
             params = handlers_parsed_params.get(handler.handler_full_name, {})
             try:
                 if handler.handler_module_path not in star_map:
diff --git a/astrbot/core/pipeline/respond/stage.py b/astrbot/core/pipeline/respond/stage.py
index 0d50440544..60a0524547 100644
--- a/astrbot/core/pipeline/respond/stage.py
+++ b/astrbot/core/pipeline/respond/stage.py
@@ -198,7 +198,7 @@ async def process(
             )
 
         handlers = star_handlers_registry.get_handlers_by_event_type(
-            EventType.OnAfterMessageSentEvent
+            EventType.OnAfterMessageSentEvent, platform_id=event.get_platform_id()
         )
         for handler in handlers:
             try:
diff --git a/astrbot/core/pipeline/result_decorate/stage.py b/astrbot/core/pipeline/result_decorate/stage.py
index a0be2423b8..957e2a491e 100644
--- a/astrbot/core/pipeline/result_decorate/stage.py
+++ b/astrbot/core/pipeline/result_decorate/stage.py
@@ -96,7 +96,7 @@ async def process(
 
         # 发送消息前事件钩子
         handlers = star_handlers_registry.get_handlers_by_event_type(
-            EventType.OnDecoratingResultEvent
+            EventType.OnDecoratingResultEvent, platform_id=event.get_platform_id()
         )
         for handler in handlers:
             try:
diff --git a/astrbot/core/platform/astr_message_event.py b/astrbot/core/platform/astr_message_event.py
index 8d3bc4c59e..96a7ad6f1c 100644
--- a/astrbot/core/platform/astr_message_event.py
+++ b/astrbot/core/platform/astr_message_event.py
@@ -81,6 +81,9 @@ def __init__(
     def get_platform_name(self):
         return self.platform_meta.name
 
+    def get_platform_id(self):
+        return self.platform_meta.id
+
     def get_message_str(self) -> str:
         """
         获取消息字符串。
diff --git a/astrbot/core/platform/platform_metadata.py b/astrbot/core/platform/platform_metadata.py
index 48fe23af76..dd0e93fec1 100644
--- a/astrbot/core/platform/platform_metadata.py
+++ b/astrbot/core/platform/platform_metadata.py
@@ -7,6 +7,8 @@ class PlatformMetadata:
     """平台的名称"""
     description: str
     """平台的描述"""
+    id: str = None
+    """平台的唯一标识符，用于配置中识别特定平台"""
 
     default_config_tmpl: dict = None
     """平台的默认配置模板"""
diff --git a/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py b/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py
index e41071a564..88f2ae3fc1 100644
--- a/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py
+++ b/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py
@@ -39,8 +39,9 @@ def __init__(
         self.port = platform_config["ws_reverse_port"]
 
         self.metadata = PlatformMetadata(
-            "aiocqhttp",
-            "适用于 OneBot 标准的消息平台适配器，支持反向 WebSockets。",
+            name="aiocqhttp",
+            description="适用于 OneBot 标准的消息平台适配器，支持反向 WebSockets。",
+            id=self.config.get("id"),
         )
 
         self.bot = CQHttp(
diff --git a/astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py b/astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py
index 95347172bb..7a83a8abe7 100644
--- a/astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py
+++ b/astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py
@@ -73,8 +73,9 @@ async def send_by_session(
 
     def meta(self) -> PlatformMetadata:
         return PlatformMetadata(
-            "dingtalk",
-            "钉钉机器人官方 API 适配器",
+            name="dingtalk",
+            description="钉钉机器人官方 API 适配器",
+            id=self.config.get("id"),
         )
 
     async def convert_msg(
diff --git a/astrbot/core/platform/sources/gewechat/gewechat_platform_adapter.py b/astrbot/core/platform/sources/gewechat/gewechat_platform_adapter.py
index acf39197fb..9303598370 100644
--- a/astrbot/core/platform/sources/gewechat/gewechat_platform_adapter.py
+++ b/astrbot/core/platform/sources/gewechat/gewechat_platform_adapter.py
@@ -60,8 +60,9 @@ async def send_by_session(
     @override
     def meta(self) -> PlatformMetadata:
         return PlatformMetadata(
-            "gewechat",
-            "基于 gewechat 的 Wechat 适配器",
+            name="gewechat",
+            description="基于 gewechat 的 Wechat 适配器",
+            id=self.config.get("id"),
         )
 
     async def terminate(self):
diff --git a/astrbot/core/platform/sources/lark/lark_adapter.py b/astrbot/core/platform/sources/lark/lark_adapter.py
index cbc3a45bb9..8ea2ce36ba 100644
--- a/astrbot/core/platform/sources/lark/lark_adapter.py
+++ b/astrbot/core/platform/sources/lark/lark_adapter.py
@@ -70,8 +70,9 @@ async def send_by_session(
 
     def meta(self) -> PlatformMetadata:
         return PlatformMetadata(
-            "lark",
-            "飞书机器人官方 API 适配器",
+            name="lark",
+            description="飞书机器人官方 API 适配器",
+            id=self.config.get("id"),
         )
 
     async def convert_msg(self, event: lark.im.v1.P2ImMessageReceiveV1):
diff --git a/astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py b/astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py
index 57bc8683fc..d5285f759a 100644
--- a/astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py
+++ b/astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py
@@ -126,8 +126,9 @@ async def send_by_session(
 
     def meta(self) -> PlatformMetadata:
         return PlatformMetadata(
-            "qq_official",
-            "QQ 机器人官方 API 适配器",
+            name="qq_official",
+            description="QQ 机器人官方 API 适配器",
+            id=self.config.get("id"),
         )
 
     @staticmethod
diff --git a/astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_adapter.py b/astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_adapter.py
index ede09e7fd9..cc12e97650 100644
--- a/astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_adapter.py
+++ b/astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_adapter.py
@@ -99,8 +99,9 @@ async def send_by_session(
 
     def meta(self) -> PlatformMetadata:
         return PlatformMetadata(
-            "qq_official_webhook",
-            "QQ 机器人官方 API 适配器",
+            name="qq_official_webhook",
+            description="QQ 机器人官方 API 适配器",
+            id=self.config.get("id"),
         )
 
     async def run(self):
diff --git a/astrbot/core/platform/sources/telegram/tg_adapter.py b/astrbot/core/platform/sources/telegram/tg_adapter.py
index 12f17a8194..9ff761c06d 100644
--- a/astrbot/core/platform/sources/telegram/tg_adapter.py
+++ b/astrbot/core/platform/sources/telegram/tg_adapter.py
@@ -80,8 +80,7 @@ async def send_by_session(
     @override
     def meta(self) -> PlatformMetadata:
         return PlatformMetadata(
-            "telegram",
-            "telegram 适配器",
+            name="telegram", description="telegram 适配器", id=self.config.get("id")
         )
 
     @override
diff --git a/astrbot/core/platform/sources/webchat/webchat_adapter.py b/astrbot/core/platform/sources/webchat/webchat_adapter.py
index 6fa3d5c598..01a042fb86 100644
--- a/astrbot/core/platform/sources/webchat/webchat_adapter.py
+++ b/astrbot/core/platform/sources/webchat/webchat_adapter.py
@@ -43,8 +43,7 @@ def __init__(
         self.imgs_dir = "data/webchat/imgs"
 
         self.metadata = PlatformMetadata(
-            "webchat",
-            "webchat",
+            name="webchat", description="webchat", id=self.config.get("id")
         )
 
     async def send_by_session(
diff --git a/astrbot/core/star/filter/command.py b/astrbot/core/star/filter/command.py
old mode 100644
new mode 100755
diff --git a/astrbot/core/star/filter/command_group.py b/astrbot/core/star/filter/command_group.py
old mode 100644
new mode 100755
diff --git a/astrbot/core/star/star.py b/astrbot/core/star/star.py
index 5215134496..10cf90c8bd 100644
--- a/astrbot/core/star/star.py
+++ b/astrbot/core/star/star.py
@@ -47,5 +47,29 @@ class StarMetadata:
     star_handler_full_names: List[str] = field(default_factory=list)
     """注册的 Handler 的全名列表"""
 
+    supported_platforms: Dict[str, bool] = field(default_factory=dict)
+    """插件支持的平台ID字典，key为平台ID，value为是否支持"""
+
     def __str__(self) -> str:
         return f"StarMetadata({self.name}, {self.desc}, {self.version}, {self.repo})"
+
+    def update_platform_compatibility(self, plugin_enable_config: dict) -> None:
+        """更新插件支持的平台列表
+
+        Args:
+            plugin_enable_config: 平台插件启用配置，即platform_settings.plugin_enable配置项
+        """
+        if not plugin_enable_config:
+            return
+
+        # 清空之前的配置
+        self.supported_platforms.clear()
+
+        # 遍历所有平台配置
+        for platform_id, plugins in plugin_enable_config.items():
+            # 检查该插件在当前平台的配置
+            if self.name in plugins:
+                self.supported_platforms[platform_id] = plugins[self.name]
+            else:
+                # 如果没有明确配置，默认为启用
+                self.supported_platforms[platform_id] = True
diff --git a/astrbot/core/star/star_handler.py b/astrbot/core/star/star_handler.py
index 7be0e053cf..0764f15f6c 100644
--- a/astrbot/core/star/star_handler.py
+++ b/astrbot/core/star/star_handler.py
@@ -30,21 +30,36 @@ def _print_handlers(self):
             print(handler.handler_full_name)
 
     def get_handlers_by_event_type(
-        self, event_type: EventType, only_activated=True
+        self, event_type: EventType, only_activated=True, platform_id=None
     ) -> List[StarHandlerMetadata]:
-        """通过事件类型获取 Handler"""
-        handlers = [
-            handler
-            for _, handler in self._handlers
-            if handler.event_type == event_type
-            and (
-                not only_activated
-                or (
-                    star_map[handler.handler_module_path]
-                    and star_map[handler.handler_module_path].activated
-                )
-            )
-        ]
+        """通过事件类型获取 Handler
+
+        Args:
+            event_type: 事件类型
+            only_activated: 是否只返回已激活的插件的处理器
+            platform_id: 平台ID，如果提供此参数，将过滤掉在此平台不兼容的处理器
+
+        Returns:
+            List[StarHandlerMetadata]: 处理器列表
+        """
+        handlers = []
+        for _, handler in self._handlers:
+            if handler.event_type != event_type:
+                continue
+
+            # 只激活的插件处理器
+            if only_activated:
+                plugin = star_map.get(handler.handler_module_path)
+                if not (plugin and plugin.activated):
+                    continue
+
+            # 平台兼容性过滤
+            if platform_id and event_type != EventType.OnAstrBotLoadedEvent:
+                if not handler.is_enabled_for_platform(platform_id):
+                    continue
+
+            handlers.append(handler)
+
         return handlers
 
     def get_handler_by_full_name(self, full_name: str) -> StarHandlerMetadata:
@@ -139,3 +154,32 @@ def __lt__(self, other: StarHandlerMetadata):
         return self.extras_configs.get("priority", 0) < other.extras_configs.get(
             "priority", 0
         )
+
+    def is_enabled_for_platform(self, platform_id: str) -> bool:
+        """检查插件是否在指定平台启用
+
+        Args:
+            platform_id: 平台ID，这是从event.get_platform_id()获取的，用于唯一标识平台实例
+
+        Returns:
+            bool: 是否启用，True表示启用，False表示禁用
+        """
+        plugin = star_map.get(self.handler_module_path)
+
+        # 如果插件元数据不存在，默认允许执行
+        if not plugin or not plugin.name:
+            return True
+
+        # 先检查插件是否被激活
+        if not plugin.activated:
+            return False
+
+        # 直接使用StarMetadata中缓存的supported_platforms判断平台兼容性
+        if (
+            hasattr(plugin, "supported_platforms")
+            and platform_id in plugin.supported_platforms
+        ):
+            return plugin.supported_platforms[platform_id]
+
+        # 如果没有缓存数据，默认允许执行
+        return True
diff --git a/astrbot/core/star/star_manager.py b/astrbot/core/star/star_manager.py
index 2d610f15c7..a4ae482507 100644
--- a/astrbot/core/star/star_manager.py
+++ b/astrbot/core/star/star_manager.py
@@ -209,7 +209,31 @@ async def reload(self, specified_plugin_name=None):
 
                 await self._unbind_plugin(smd.name, specified_module_path)
 
-        return await self.load(specified_module_path)
+        result = await self.load(specified_module_path)
+
+        # 更新所有插件的平台兼容性
+        await self.update_all_platform_compatibility()
+
+        return result
+
+    async def update_all_platform_compatibility(self):
+        """更新所有插件的平台兼容性设置"""
+        # 获取最新的平台插件启用配置
+        plugin_enable_config = self.config.get("platform_settings", {}).get(
+            "plugin_enable", {}
+        )
+        logger.debug(
+            f"更新所有插件的平台兼容性设置，平台数量: {len(plugin_enable_config)}"
+        )
+
+        # 遍历所有插件，更新平台兼容性
+        for plugin in self.context.get_all_stars():
+            plugin.update_platform_compatibility(plugin_enable_config)
+            logger.debug(
+                f"插件 {plugin.name} 支持的平台: {list(plugin.supported_platforms.keys())}"
+            )
+
+        return True
 
     async def load(self, specified_module_path=None, specified_dir_name=None):
         """载入插件。
@@ -320,6 +344,12 @@ async def load(self, specified_module_path=None, specified_dir_name=None):
                     metadata.root_dir_name = root_dir_name
                     metadata.reserved = reserved
 
+                    # 更新插件的平台兼容性
+                    plugin_enable_config = self.config.get("platform_settings", {}).get(
+                        "plugin_enable", {}
+                    )
+                    metadata.update_platform_compatibility(plugin_enable_config)
+
                     # 绑定 handler
                     related_handlers = (
                         star_handlers_registry.get_handlers_by_module_name(
diff --git a/astrbot/core/utils/shared_preferences.py b/astrbot/core/utils/shared_preferences.py
index bf88ba8dba..b119873226 100644
--- a/astrbot/core/utils/shared_preferences.py
+++ b/astrbot/core/utils/shared_preferences.py
@@ -15,7 +15,7 @@ def _load_preferences(self):
 
     def _save_preferences(self):
         with open(self.path, "w") as f:
-            json.dump(self._data, f, indent=4)
+            json.dump(self._data, f, indent=4, ensure_ascii=False)
             f.flush()
 
     def get(self, key, default=None):
diff --git a/astrbot/dashboard/routes/plugin.py b/astrbot/dashboard/routes/plugin.py
index 81d9b0bfea..9fb9d231a8 100644
--- a/astrbot/dashboard/routes/plugin.py
+++ b/astrbot/dashboard/routes/plugin.py
@@ -38,6 +38,8 @@ def __init__(
             "/plugin/on": ("POST", self.on_plugin),
             "/plugin/reload": ("POST", self.reload_plugins),
             "/plugin/readme": ("GET", self.get_plugin_readme),
+            "/plugin/platform_enable/get": ("GET", self.get_plugin_platform_enable),
+            "/plugin/platform_enable/set": ("POST", self.set_plugin_platform_enable),
         }
         self.core_lifecycle = core_lifecycle
         self.plugin_manager = plugin_manager
@@ -323,38 +325,131 @@ async def on_plugin(self):
     async def get_plugin_readme(self):
         plugin_name = request.args.get("name")
         logger.debug(f"正在获取插件 {plugin_name} 的README文件内容")
-        
+
         if not plugin_name:
             logger.warning("插件名称为空")
             return Response().error("插件名称不能为空").__dict__
-        
+
         plugin_obj = None
         for plugin in self.plugin_manager.context.get_all_stars():
             if plugin.name == plugin_name:
                 plugin_obj = plugin
                 break
-                
+
         if not plugin_obj:
             logger.warning(f"插件 {plugin_name} 不存在")
             return Response().error(f"插件 {plugin_name} 不存在").__dict__
-            
-        plugin_dir = os.path.join(self.plugin_manager.plugin_store_path, plugin_obj.root_dir_name)
-            
+
+        plugin_dir = os.path.join(
+            self.plugin_manager.plugin_store_path, plugin_obj.root_dir_name
+        )
+
         if not os.path.isdir(plugin_dir):
             logger.warning(f"无法找到插件目录: {plugin_dir}")
             return Response().error(f"无法找到插件 {plugin_name} 的目录").__dict__
-        
+
         readme_path = os.path.join(plugin_dir, "README.md")
-        
+
         if not os.path.isfile(readme_path):
             logger.warning(f"插件 {plugin_name} 没有README文件")
             return Response().error(f"插件 {plugin_name} 没有README文件").__dict__
-        
+
         try:
-            with open(readme_path, 'r', encoding='utf-8') as f:
+            with open(readme_path, "r", encoding="utf-8") as f:
                 readme_content = f.read()
-            
-            return Response().ok({"content": readme_content}, "成功获取README内容").__dict__
+
+            return (
+                Response()
+                .ok({"content": readme_content}, "成功获取README内容")
+                .__dict__
+            )
         except Exception as e:
             logger.error(f"/api/plugin/readme: {traceback.format_exc()}")
             return Response().error(f"读取README文件失败: {str(e)}").__dict__
+
+    async def get_plugin_platform_enable(self):
+        """获取插件在各平台的可用性配置"""
+        try:
+            platform_enable = self.core_lifecycle.astrbot_config.get(
+                "platform_settings", {}
+            ).get("plugin_enable", {})
+
+            # 获取所有可用平台
+            platforms = []
+
+            for platform in self.core_lifecycle.astrbot_config.get("platform", []):
+                platform_type = platform.get("type", "")
+                platform_id = platform.get("id", "")
+
+                platforms.append(
+                    {
+                        "name": platform_id,  # 使用type作为name，这是系统内部使用的平台名称
+                        "id": platform_id,  # 保留id字段以便前端可以显示
+                        "type": platform_type,
+                        "display_name": f"{platform_type}({platform_id})",
+                    }
+                )
+
+            adjusted_platform_enable = {}
+            for platform_id, plugins in platform_enable.items():
+                adjusted_platform_enable[platform_id] = plugins
+
+            # 获取所有插件，包括系统内部插件
+            plugins = []
+            for plugin in self.plugin_manager.context.get_all_stars():
+                plugins.append(
+                    {
+                        "name": plugin.name,
+                        "desc": plugin.desc,
+                        "reserved": plugin.reserved,  # 添加reserved标志
+                    }
+                )
+
+            logger.debug(
+                f"获取插件平台配置: 原始配置={platform_enable}, 调整后={adjusted_platform_enable}"
+            )
+
+            return (
+                Response()
+                .ok(
+                    {
+                        "platforms": platforms,
+                        "plugins": plugins,
+                        "platform_enable": adjusted_platform_enable,
+                    }
+                )
+                .__dict__
+            )
+        except Exception as e:
+            logger.error(f"/api/plugin/platform_enable/get: {traceback.format_exc()}")
+            return Response().error(str(e)).__dict__
+
+    async def set_plugin_platform_enable(self):
+        """设置插件在各平台的可用性配置"""
+        if DEMO_MODE:
+            return (
+                Response()
+                .error("You are not permitted to do this operation in demo mode")
+                .__dict__
+            )
+
+        try:
+            data = await request.json
+            platform_enable = data.get("platform_enable", {})
+
+            # 更新配置
+            config = self.core_lifecycle.astrbot_config
+            platform_settings = config.get("platform_settings", {})
+            platform_settings["plugin_enable"] = platform_enable
+            config["platform_settings"] = platform_settings
+            config.save_config()
+
+            # 更新插件的平台兼容性缓存
+            await self.plugin_manager.update_all_platform_compatibility()
+
+            logger.info(f"插件平台可用性配置已更新: {platform_enable}")
+
+            return Response().ok(None, "插件平台可用性配置已更新").__dict__
+        except Exception as e:
+            logger.error(f"/api/plugin/platform_enable/set: {traceback.format_exc()}")
+            return Response().error(str(e)).__dict__
diff --git a/dashboard/src/views/ExtensionPage.vue b/dashboard/src/views/ExtensionPage.vue
index 33f78dd017..2f34deccff 100644
--- a/dashboard/src/views/ExtensionPage.vue
+++ b/dashboard/src/views/ExtensionPage.vue
@@ -41,6 +41,14 @@ const readmeDialog = reactive({
   pluginName: '',
   repoUrl: null
 });
+// 平台插件配置
+const platformEnableDialog = ref(false);
+const platformEnableData = reactive({
+  platforms: [],
+  plugins: [],
+  platform_enable: {}
+});
+const loadingPlatformData = ref(false);
 
 const plugin_handler_info_headers = [
   { title: '行为类型', key: 'event_type_h' },
@@ -238,6 +246,101 @@ const viewReadme = (plugin) => {
   readmeDialog.show = true;
 };
 
+// 获取插件平台可用性配置
+const getPlatformEnableConfig = async () => {
+  loadingPlatformData.value = true;
+  try {
+    const res = await axios.get('/api/plugin/platform_enable/get');
+    if (res.data.status === "error") {
+      toast(res.data.message, "error");
+      return;
+    }
+    
+    platformEnableData.platforms = res.data.data.platforms;
+    platformEnableData.plugins = res.data.data.plugins;
+    platformEnableData.platform_enable = res.data.data.platform_enable;
+    
+    // 如果没有平台，给出提示但仍显示对话框
+    if (platformEnableData.platforms.length === 0) {
+      toast("未添加任何平台适配器，请先在平台管理中添加平台", "warning");
+    } else {
+      // 确保每个平台都有一个配置对象
+      platformEnableData.platforms.forEach(platform => {
+        if (!platformEnableData.platform_enable[platform.name]) {
+          platformEnableData.platform_enable[platform.name] = {};
+        }
+        
+        // 确保每个插件在每个平台都有一个配置项
+        platformEnableData.plugins.forEach(plugin => {
+          if (platformEnableData.platform_enable[platform.name][plugin.name] === undefined) {
+            platformEnableData.platform_enable[platform.name][plugin.name] = true; // 默认启用
+          }
+        });
+      });
+    }
+    
+    platformEnableDialog.value = true;
+  } catch (err) {
+    toast("获取平台插件配置失败: " + err, "error");
+  } finally {
+    loadingPlatformData.value = false;
+  }
+};
+
+// 保存插件平台可用性配置
+const savePlatformEnableConfig = async () => {
+  loadingPlatformData.value = true;
+  try {
+    const res = await axios.post('/api/plugin/platform_enable/set', {
+      platform_enable: platformEnableData.platform_enable
+    });
+    
+    if (res.data.status === "error") {
+      toast(res.data.message, "error");
+      return;
+    }
+    
+    toast(res.data.message, "success");
+    platformEnableDialog.value = false;
+  } catch (err) {
+    toast("保存平台插件配置失败: " + err, "error");
+  } finally {
+    loadingPlatformData.value = false;
+  }
+};
+
+// 全选指定平台的所有插件
+const selectAllPluginsForPlatform = (platformName, isSelected, onlyReserved = null) => {
+  // 确保平台存在于platform_enable中
+  if (!platformEnableData.platform_enable[platformName]) {
+    platformEnableData.platform_enable[platformName] = {};
+  }
+  
+  // 为所有插件设置相同的状态
+  platformEnableData.plugins.forEach(plugin => {
+    // 如果onlyReserved为null，处理所有插件
+    // 如果onlyReserved为true，只处理系统插件
+    // 如果onlyReserved为false，只处理非系统插件
+    if (onlyReserved === null || plugin.reserved === onlyReserved) {
+      platformEnableData.platform_enable[platformName][plugin.name] = isSelected;
+    }
+  });
+};
+
+// 反选指定平台的所有插件
+const toggleAllPluginsForPlatform = (platformName) => {
+  // 确保平台存在于platform_enable中
+  if (!platformEnableData.platform_enable[platformName]) {
+    platformEnableData.platform_enable[platformName] = {};
+  }
+  
+  // 对每个插件进行反选操作
+  platformEnableData.plugins.forEach(plugin => {
+    const currentState = platformEnableData.platform_enable[platformName][plugin.name];
+    platformEnableData.platform_enable[platformName][plugin.name] = !currentState;
+  });
+};
+
 // 生命周期
 onMounted(async () => {
   await getExtensions();
@@ -261,6 +364,9 @@ onMounted(async () => {
           <v-btn class="text-none ml-2" size="small" variant="flat" border @click="toggleShowReserved">
             {{ showReserved ? '隐藏系统保留插件' : '显示系统保留插件' }}
           </v-btn>
+          <v-btn class="text-none ml-2" size="small" variant="flat" color="primary" border @click="getPlatformEnableConfig">
+            平台命令配置
+          </v-btn>
           <v-dialog max-width="500px" v-if="extension_data.message">
             <template v-slot:activator="{ props }">
               <v-btn v-bind="props" icon size="small" color="error" style="margin-left: auto;" variant="plain">
@@ -298,6 +404,105 @@ onMounted(async () => {
     </v-col>
   </v-row>
 
+  <!-- 插件平台配置对话框 -->
+  <v-dialog v-model="platformEnableDialog" max-width="800" persistent>
+    <v-card>
+      <v-card-title>
+        <span class="headline">平台命令可用性配置</span>
+      </v-card-title>
+      <v-card-subtitle>
+        设置每个插件在不同平台上的可用性，勾选表示启用
+      </v-card-subtitle>
+      <v-card-text>
+        <v-overlay
+          :model-value="loadingPlatformData"
+          class="align-center justify-center"
+          persistent
+        >
+          <v-progress-circular
+            color="primary"
+            indeterminate
+            size="64"
+          ></v-progress-circular>
+        </v-overlay>
+        
+        <div v-if="platformEnableData.platforms.length === 0" class="text-center pa-4">
+          <v-icon icon="mdi-alert" color="warning" size="64" class="mb-4"></v-icon>
+          <div class="text-h6 mb-2">未找到平台适配器</div>
+          <div class="text-body-1 mb-4">请先在 <strong>平台管理</strong> 中添加并配置平台适配器，然后再设置插件的平台可用性</div>
+          <v-btn color="primary" to="/platforms">前往平台管理</v-btn>
+        </div>
+        
+        <v-table v-else>
+          <thead>
+            <tr>
+              <th>插件名称</th>
+              <th v-for="platform in platformEnableData.platforms" :key="platform.name">
+                <div class="d-flex align-center">
+                  {{ platform.display_name }}
+                  <v-menu>
+                    <template v-slot:activator="{ props }">
+                      <v-btn
+                        icon
+                        density="compact"
+                        variant="text"
+                        size="small"
+                        v-bind="props"
+                        class="ms-1"
+                      >
+                        <v-icon>mdi-dots-vertical</v-icon>
+                      </v-btn>
+                    </template>
+                    <v-list>
+                      <v-list-item @click="selectAllPluginsForPlatform(platform.name, true)">
+                        <v-list-item-title>全选</v-list-item-title>
+                      </v-list-item>
+                      <v-list-item @click="selectAllPluginsForPlatform(platform.name, true, false)">
+                        <v-list-item-title>全选普通插件</v-list-item-title>
+                      </v-list-item>
+                      <v-list-item @click="selectAllPluginsForPlatform(platform.name, true, true)">
+                        <v-list-item-title>全选系统插件</v-list-item-title>
+                      </v-list-item>
+                      <v-list-item @click="selectAllPluginsForPlatform(platform.name, false)">
+                        <v-list-item-title>全不选</v-list-item-title>
+                      </v-list-item>
+                      <v-list-item @click="toggleAllPluginsForPlatform(platform.name)">
+                        <v-list-item-title>反选</v-list-item-title>
+                      </v-list-item>
+                    </v-list>
+                  </v-menu>
+                </div>
+              </th>
+            </tr>
+          </thead>
+          <tbody>
+            <tr v-for="plugin in platformEnableData.plugins" :key="plugin.name">
+              <td>
+                <div class="d-flex align-center">
+                  {{ plugin.name }}
+                  <v-chip v-if="plugin.reserved" color="primary" size="x-small" class="ml-2">系统</v-chip>
+                </div>
+                <div class="text-caption text-grey">{{ plugin.desc }}</div>
+              </td>
+              <td v-for="platform in platformEnableData.platforms" :key="platform.name">
+                <v-checkbox
+                  v-model="platformEnableData.platform_enable[platform.name][plugin.name]"
+                  hide-details
+                  density="compact"
+                ></v-checkbox>
+              </td>
+            </tr>
+          </tbody>
+        </v-table>
+      </v-card-text>
+      <v-card-actions>
+        <v-spacer></v-spacer>
+        <v-btn color="grey" text @click="platformEnableDialog = false">关闭</v-btn>
+        <v-btn v-if="platformEnableData.platforms.length > 0" color="primary" @click="savePlatformEnableConfig">保存</v-btn>
+      </v-card-actions>
+    </v-card>
+  </v-dialog>
+
   <!-- 配置对话框 -->
   <v-dialog v-model="configDialog" width="1000">
     <v-card>
@@ -385,4 +590,13 @@ onMounted(async () => {
     :plugin-name="readmeDialog.pluginName"
     :repo-url="readmeDialog.repoUrl"
   />
-</template>
\ No newline at end of file
+</template>
+
+<style scoped>
+.plugin-handler-item {
+  margin-bottom: 10px;
+  padding: 5px;
+  border-radius: 5px;
+  background-color: #f5f5f5;
+}
+</style>


Reviews:
user: login: sourcery-ai[bot]
body: Hey @zhx8702 - I've reviewed your changes - here's some feedback:

**Overall Comments**:
- Consider adding a more descriptive name for `platformEnableData` to improve readability.
- The frontend code could benefit from some refactoring to reduce the complexity of the `getPlatformEnableConfig` function.

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

- 🟡 **General issues**: 2 issues found
- 🟢 **Security**: all looks good
- 🟢 **Testing**: all looks good
- 🟡 **Complexity**: 1 issue 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>
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T00:51:05Z
user: login: copilot-pull-request-reviewer[bot]
body: 

Copilot reviewed 15 out of 15 changed files in this pull request and generated no comments.


<details>
<summary>Comments suppressed due to low confidence (2)</summary>

**dashboard/src/views/ExtensionPage.vue:294**
* Consider converting the error object to a more descriptive string (e.g., using err.message if available) to improve clarity in error messages.
```
toast("保存平台插件配置失败: " + err, "error");
```
**astrbot/dashboard/routes/plugin.py:399**
* Mapping platforms by type may override values if multiple platforms share the same type. Consider using a unique identifier as the key or storing values in a list to ensure no data is lost.
```
type_to_id_map[platform_type] = platform_id
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T01:58:23Z
user: login: Raven95676
body: 附：当前实现仅能保证指令针对不同消息平台开启关闭，事件钩子在各个阶段直接执行，没有经过平台启用检查。
当前实现较为繁琐，建议直接新增一个阶段集中检查
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-04-08T03:50:00Z
user: login: zhx8702
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-08T05:37:39Z
user: login: Raven95676
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-04-08T16:18:04Z
user: login: zhx8702
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-09T08:53:09Z
user: login: Soulter
body: LGTM
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-09T09:24:21Z
user: login: Raven95676
body: :thumbsup:
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-09T10:47:35Z
user: login: Soulter
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-10T02:49:51Z

Review comments:
diff_hunk: @@ -7,6 +7,8 @@ class PlatformMetadata:
     """平台的名称"""
     description: str
     """平台的描述"""
+    id: str = None
path: astrbot/core/platform/platform_metadata.py
user: login: sourcery-ai[bot]
body: **suggestion:** Clarify the usage of the 'id' field in PlatformMetadata.

Since the 'id' field is essential for mapping platform configurations in the backend and is used throughout the adapters, consider enforcing its presence (or adding validation logic) to avoid potential misconfigurations if 'self.config.get("id")' returns a null or falsy value.

Suggested implementation:

```python
    id: str
    """平台的唯一标识符，用于配置中识别特定平台"""

```

```python
    default_config_tmpl: dict = None
    """平台的默认配置模板"""

    def __post_init__(self):
        if not self.id:
            raise ValueError("平台的唯一标识符 'id' 是必填项，请在配置中提供一个有效的值。")

```

Ensure that if PlatformMetadata is used in other parts of the application, you now pass a valid "id" value when creating an instance. This adjustment both enforces proper configuration and immediately alerts developers of misconfigurations.
created_at: 2025-04-08T00:51:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -101,6 +103,31 @@ def filter(self, event: AstrMessageEvent, cfg: AstrBotConfig) -> bool:
         if not event.is_at_or_wake_command:
path: astrbot/core/star/filter/command_group.py
user: login: sourcery-ai[bot]
body: **issue (complexity):** Consider extracting the platform enable check into a dedicated helper method to improve readability and reduce code duplication by removing nested loops and duplicated config extraction..

Consider extracting the platform enable check into a dedicated helper method to eliminate nested loops and duplicate config extraction. For example, you could create a helper like:

```python
def is_handler_enabled(handler_module_path: str, cfg: AstrBotConfig, event: AstrMessageEvent) -> bool:
    # Extract plugin enable config once
    plugin_enable_config = cfg.get("platform_settings", {}).get("plugin_enable", {})
    for handler in star_handlers_registry:
        if handler.handler_module_path == handler_module_path:
            return handler.is_enabled_for_platform(plugin_enable_config, event.get_platform_id())
    return True  # if no handler is found, default to enabled
```

Then refactor your `filter` method as follows:

```python
def filter(self, event: AstrMessageEvent, cfg: AstrBotConfig) -> bool:
    if not event.is_at_or_wake_command:
        return False

    handler_module_path = getattr(self, "handler_module_path", None)
    if handler_module_path:
        plugin = star_map.get(handler_module_path)
        if plugin and not is_handler_enabled(handler_module_path, cfg, event):
            logger.info(
                f"[CommandGroupFilter] 插件 {plugin.name} 在平台 {event.get_platform_id()} 未启用，忽略指令组 {self.group_name}"
            )
            return False

    if not self.custom_filter_ok(event, cfg):
        return False

    complete_command_names = self.get_complete_command_names()
    if event.message_str.strip() in complete_command_names:
        tree = (
            self.group_name
            + "\n"
            + self.print_cmd_tree(self.sub_command_filters, event=event, cfg=cfg)
        )
    # ...rest of the code remains unchanged
```

This consolidation removes duplicated logic, simplifies the `filter` method, and keeps functionality intact.
created_at: 2025-04-08T00:51:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -317,3 +319,102 @@ async def on_plugin(self):
         except Exception as e:
             logger.error(f"/api/plugin/on: {traceback.format_exc()}")
             return Response().error(str(e)).__dict__
+
+    async def get_plugin_platform_enable(self):
+        """获取插件在各平台的可用性配置"""
+        try:
+            platform_enable = self.core_lifecycle.astrbot_config.get(
+                "platform_settings", {}
+            ).get("plugin_enable", {})
+
+            # 获取所有可用平台
+            platforms = []
+
+            for platform in self.core_lifecycle.astrbot_config.get("platform", []):
+                platform_type = platform.get("type", "")
+                platform_id = platform.get("id", "")
+
+                platforms.append(
+                    {
+                        "name": platform_id,  # 使用type作为name，这是系统内部使用的平台名称
+                        "id": platform_id,  # 保留id字段以便前端可以显示
+                        "type": platform_type,
+                        "display_name": f"{platform_type}({platform_id})",
+                    }
+                )
+
+            adjusted_platform_enable = {}
+            for platform_id, plugins in platform_enable.items():
+                adjusted_platform_enable[platform_id] = plugins
+
+            # 获取所有插件，包括系统内部插件
+            plugins = []
+            for plugin in self.plugin_manager.context.get_all_stars():
+                plugins.append(
+                    {
+                        "name": plugin.name,
+                        "desc": plugin.desc,
+                        "reserved": plugin.reserved,  # 添加reserved标志
+                    }
+                )
+
+            logger.debug(
+                f"获取插件平台配置: 原始配置={platform_enable}, 调整后={adjusted_platform_enable}"
+            )
+
+            return (
+                Response()
+                .ok(
+                    {
+                        "platforms": platforms,
+                        "plugins": plugins,
+                        "platform_enable": adjusted_platform_enable,
+                    }
+                )
+                .__dict__
+            )
+        except Exception as e:
+            logger.error(f"/api/plugin/platform_enable/get: {traceback.format_exc()}")
+            return Response().error(str(e)).__dict__
+
+    async def set_plugin_platform_enable(self):
+        """设置插件在各平台的可用性配置"""
+        if DEMO_MODE:
+            return (
+                Response()
+                .error("You are not permitted to do this operation in demo mode")
+                .__dict__
+            )
+
+        try:
+            data = await request.json
+            platform_enable = data.get("platform_enable", {})
+
+            # 转换平台名称(type)到平台ID(id)的映射
+            type_to_id_map = {}
path: astrbot/dashboard/routes/plugin.py
user: login: Raven95676
body: 如果用户配置了多个相同类型但不同ID的平台，后面的会覆盖前面的
created_at: 2025-04-08T03:32:03Z
author_association: MEMBER
diff_hunk: @@ -317,3 +319,102 @@ async def on_plugin(self):
         except Exception as e:
             logger.error(f"/api/plugin/on: {traceback.format_exc()}")
             return Response().error(str(e)).__dict__
+
+    async def get_plugin_platform_enable(self):
+        """获取插件在各平台的可用性配置"""
+        try:
+            platform_enable = self.core_lifecycle.astrbot_config.get(
+                "platform_settings", {}
+            ).get("plugin_enable", {})
+
+            # 获取所有可用平台
+            platforms = []
+
+            for platform in self.core_lifecycle.astrbot_config.get("platform", []):
+                platform_type = platform.get("type", "")
+                platform_id = platform.get("id", "")
+
+                platforms.append(
+                    {
+                        "name": platform_id,  # 使用type作为name，这是系统内部使用的平台名称
+                        "id": platform_id,  # 保留id字段以便前端可以显示
+                        "type": platform_type,
+                        "display_name": f"{platform_type}({platform_id})",
+                    }
+                )
+
+            adjusted_platform_enable = {}
+            for platform_id, plugins in platform_enable.items():
+                adjusted_platform_enable[platform_id] = plugins
+
+            # 获取所有插件，包括系统内部插件
+            plugins = []
+            for plugin in self.plugin_manager.context.get_all_stars():
+                plugins.append(
+                    {
+                        "name": plugin.name,
+                        "desc": plugin.desc,
+                        "reserved": plugin.reserved,  # 添加reserved标志
+                    }
+                )
+
+            logger.debug(
+                f"获取插件平台配置: 原始配置={platform_enable}, 调整后={adjusted_platform_enable}"
+            )
+
+            return (
+                Response()
+                .ok(
+                    {
+                        "platforms": platforms,
+                        "plugins": plugins,
+                        "platform_enable": adjusted_platform_enable,
+                    }
+                )
+                .__dict__
+            )
+        except Exception as e:
+            logger.error(f"/api/plugin/platform_enable/get: {traceback.format_exc()}")
+            return Response().error(str(e)).__dict__
+
+    async def set_plugin_platform_enable(self):
+        """设置插件在各平台的可用性配置"""
+        if DEMO_MODE:
+            return (
+                Response()
+                .error("You are not permitted to do this operation in demo mode")
+                .__dict__
+            )
+
+        try:
+            data = await request.json
+            platform_enable = data.get("platform_enable", {})
+
+            # 转换平台名称(type)到平台ID(id)的映射
+            type_to_id_map = {}
path: astrbot/dashboard/routes/plugin.py
user: login: zhx8702
body: 移除多余转换逻辑
created_at: 2025-04-08T05:37:39Z
author_association: NONE
diff_hunk: @@ -0,0 +1,62 @@
+from ..stage import Stage, register_stage
+from ..context import PipelineContext
+from typing import Union, AsyncGenerator
+from astrbot.core.platform.astr_message_event import AstrMessageEvent
+from astrbot.core.star.star import star_map
+from astrbot.core import logger
+
+
+@register_stage
+class PlatformCompatibilityStage(Stage):
+    """检查所有处理器的平台兼容性。
+
+    这个阶段会检查所有处理器是否在当前平台启用，如果未启用则从activated_handlers中移除。
+    """
+
+    async def initialize(self, ctx: PipelineContext) -> None:
+        """初始化平台兼容性检查阶段
+
+        Args:
+            ctx (PipelineContext): 消息管道上下文对象, 包括配置和插件管理器
+        """
+        self.ctx = ctx
+
+    async def process(
+        self, event: AstrMessageEvent
+    ) -> Union[None, AsyncGenerator[None, None]]:
+        # 获取当前平台ID
+        platform_id = event.get_platform_id()
+
+        # 获取平台插件启用配置
+        plugin_enable_config = self.ctx.astrbot_config.get("platform_settings", {}).get(
+            "plugin_enable", {}
+        )
+
+        # 获取已激活的处理器
+        activated_handlers = event.get_extra("activated_handlers")
+        if activated_handlers is None:
+            activated_handlers = []
+
+        # 过滤掉不兼容的处理器
+        compatible_handlers = []
+        for handler in activated_handlers:
+            # 检查处理器是否在当前平台启用
+            enabled = handler.is_enabled_for_platform(plugin_enable_config, platform_id)
+            if not enabled:
+                plugin_name = "unknown"
+                if handler.handler_module_path in star_map:
+                    plugin_name = star_map[handler.handler_module_path].name
+                logger.debug(
+                    f"[PlatformCompatibilityStage] 插件 {plugin_name} 在平台 {platform_id} 未启用，跳过执行处理器 {handler.handler_name}"
+                )
+                continue
+
+            compatible_handlers.append(handler)
+
+        # 更新已激活的处理器列表
+        event.set_extra("activated_handlers", compatible_handlers)
+
+        # 如果没有兼容的处理器，则停止事件传播
+        if not compatible_handlers and activated_handlers:
+            logger.debug("[PlatformCompatibilityStage] 没有兼容的处理器，停止事件传播")
+            event.stop_event()
path: astrbot/core/pipeline/platform_compatibility/stage.py
user: login: Raven95676
body: 若在这里停止事件传播，当所有插件均未启用时，平台将无法处理LLM请求，建议删除
created_at: 2025-04-08T14:36:19Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,62 @@
+from ..stage import Stage, register_stage
+from ..context import PipelineContext
+from typing import Union, AsyncGenerator
+from astrbot.core.platform.astr_message_event import AstrMessageEvent
+from astrbot.core.star.star import star_map
+from astrbot.core import logger
+
+
+@register_stage
+class PlatformCompatibilityStage(Stage):
+    """检查所有处理器的平台兼容性。
+
+    这个阶段会检查所有处理器是否在当前平台启用，如果未启用则从activated_handlers中移除。
+    """
+
+    async def initialize(self, ctx: PipelineContext) -> None:
+        """初始化平台兼容性检查阶段
+
+        Args:
+            ctx (PipelineContext): 消息管道上下文对象, 包括配置和插件管理器
+        """
+        self.ctx = ctx
+
+    async def process(
+        self, event: AstrMessageEvent
+    ) -> Union[None, AsyncGenerator[None, None]]:
+        # 获取当前平台ID
+        platform_id = event.get_platform_id()
+
+        # 获取平台插件启用配置
+        plugin_enable_config = self.ctx.astrbot_config.get("platform_settings", {}).get(
+            "plugin_enable", {}
+        )
+
+        # 获取已激活的处理器
+        activated_handlers = event.get_extra("activated_handlers")
+        if activated_handlers is None:
+            activated_handlers = []
+
+        # 过滤掉不兼容的处理器
+        compatible_handlers = []
+        for handler in activated_handlers:
+            # 检查处理器是否在当前平台启用
+            enabled = handler.is_enabled_for_platform(plugin_enable_config, platform_id)
+            if not enabled:
+                plugin_name = "unknown"
+                if handler.handler_module_path in star_map:
path: astrbot/core/pipeline/platform_compatibility/stage.py
user: login: Raven95676
body: 插件一定有名称，这里的逻辑是不必要的
created_at: 2025-04-08T14:37:01Z
author_association: MEMBER
path: astrbot/core/pipeline/platform_compatibility/stage.py
user: login: Raven95676
body: 个人不建议在这里用拦截的逻辑。可以考虑设置一个特殊标识，然后在后继的stage检查这个标识，如果存在就只执行LLM请求逻辑。记得处理LLM Tools。
created_at: 2025-04-08T14:38:36Z
author_association: MEMBER
diff_hunk: @@ -139,3 +140,63 @@ def __lt__(self, other: StarHandlerMetadata):
         return self.extras_configs.get("priority", 0) < other.extras_configs.get(
             "priority", 0
         )
+
+    def is_enabled_for_platform(self, plugin_enable_config, platform_id: str) -> bool:
+        """检查插件是否在指定平台启用
+
+        Args:
+            plugin_enable_config: 平台插件启用配置，即platform_settings.plugin_enable配置项
+            platform_id: 平台ID，这是从event.get_platform_id()获取的，用于唯一标识平台实例
+
+        Returns:
+            bool: 是否启用，True表示启用，False表示禁用
+        """
+        plugin = star_map.get(self.handler_module_path)
+        if not plugin or not plugin.name:
+            logger.debug(
+                f"[平台过滤] 模块 {self.handler_module_path} 未注册为插件，默认允许执行"
+            )
+            return True  # 未注册插件的默认允许执行
+
+        try:
+            # 如果plugin_enable_config不存在或为空，默认允许执行
+            if not plugin_enable_config:
+                logger.debug(
+                    "[平台过滤] plugin_enable_config 配置不存在或为空，默认允许执行"
+                )
+                return True
+
+            # 检查可用的平台配置，记录日志以便调试
+            platform_ids = list(plugin_enable_config.keys())
+            logger.debug(f"[平台过滤] 平台ID: {platform_id}, 可用平台: {platform_ids}")
+
+            # 如果没有当前平台的配置，默认为True（启用）
+            if platform_id not in plugin_enable_config:
+                logger.debug(
+                    f"[平台过滤] 平台 {platform_id} 没有配置，插件 {plugin.name} 默认允许执行"
+                )
+                return True  # 平台未配置，默认启用
+
+            platform_plugins = plugin_enable_config.get(platform_id, {})
+            logger.debug(f"[平台过滤] 平台 {platform_id} 配置: {platform_plugins}")
+
+            # 明确检查插件是否被设置为False，否则默认为True
+            if plugin.name in platform_plugins:
+                is_enabled = platform_plugins[plugin.name]
+                logger.debug(
+                    f"[平台过滤] 插件 {plugin.name} 在平台 {platform_id} 的启用状态为: {is_enabled}"
+                )
+                if is_enabled is False:
+                    return False  # 插件被明确禁用
+                return True
+            else:
+                logger.debug(
+                    f"[平台过滤] 插件 {plugin.name} 在平台 {platform_id} 没有配置，默认允许执行"
+                )
+                return True  # 默认启用
+        except Exception as e:
+            # 如果获取配置过程中出现任何错误，默认允许执行
+            logger.warning(
+                f"[平台过滤] 检查插件 {plugin.name} 在平台 {platform_id} 的启用状态时出错: {e}"
+            )
+            return True
path: astrbot/core/star/star_handler.py
user: login: Raven95676
body: 冗余逻辑过多，可适当简化。
另外，推荐的做法其实是在`StarMetadata`维护一个已启用的平台适配器列表。

```suggestion
        try:
            plugin = star_map.get(self.handler_module_path)
            if not plugin_enable_config or not plugin:
                return True

            platform_plugins = plugin_enable_config.get(platform_id)
            if platform_plugins and platform_plugins.get(plugin.name) is False:
                return False

            return True
        except Exception as e:
            logger.error(f"检查插件是否在指定平台启用失败: {e!s}")
            return True
```
created_at: 2025-04-08T14:48:12Z
author_association: MEMBER
path: astrbot/core/pipeline/platform_compatibility/stage.py
user: login: zhx8702
body: > 个人不建议在这里用拦截的逻辑。可以考虑设置一个特殊标识，然后在后继的stage检查这个标识，如果存在就只执行LLM请求逻辑。记得处理LLM Tools。
感谢提醒，这块逻辑之前不是很熟。

目前做了如下改动
1. `platform_compatibility` 处理activated_handlers 添加 platform_compatible Tag（感觉这里直接过滤掉更简单一点 后续不需要额外处理）

2. 在 `StarMetadata` 中维护了每个插件对各平台的兼容性配置，存储为 `supported_platforms` 字典
   - 格式为 `{platform_id: bool}`，表示该插件是否在特定平台启用

3. 在处理链路中的过滤逻辑：
    activated_handlers 过滤带了platform_compatible 的 tag 
  `get_handlers_by_event_type` 方法：集中过滤特定事件类型的处理器
    - 当指定 `platform_id` 时，会调用 `is_enabled_for_platform` 方法过滤不兼容的处理器
    - 若只需要激活的插件（`only_activated=True`），会检查插件的 `activated` 状态
    - 特殊情况下，`OnAstrBotLoadedEvent` 事件类型会跳过平台兼容性检查

4. `is_enabled_for_platform` 方法：处理单个处理器的平台兼容性
   - 检查插件元数据是否存在、平台是否在支持列表中，并返回相应的兼容性状态

5. 当平台配置更新时：
   - `updateAllPlatformCompatibility` 方法会遍历所有插件，更新其平台兼容性设置
   - 同时会刷新所有处理器的缓存，确保处理器使用最新的兼容性配置

created_at: 2025-04-09T08:53:09Z
author_association: NONE

PR comments:
user: login: sourcery-ai[bot]
created_at: 2025-04-08T00:50:02Z
body: <!-- Generated by sourcery-ai[bot]: start review_guide -->

```
## Sourcery 评审者指南

此 Pull Request 引入了一项新功能，允许管理员在每个平台的基础上启用或禁用插件。它包括对仪表板 UI、API 端点、命令过滤器和平台适配器的更改，以支持此功能。

#### 用于设置插件平台启用配置的序列图

```mermaid
sequenceDiagram
  participant Dashboard as UI
  participant API
  participant CoreLifecycle as Core
  participant AstrBotConfig as Config

  UI->>API: POST /api/plugin/platform_enable/set with {platform_enable}
  API->>Core: set_plugin_platform_enable(platform_enable)
  Core->>Config: astrbot_config.get("platform", [])
  Config-->>Core: platforms
  Core->>Config: config.get("platform_settings", {})
  Config-->>Core: platform_settings
  Core->>Config: platform_settings["plugin_enable"] = adjusted_platform_enable
  Core->>Config: config.save_config()
  Config-->>Core: None
  Core-->>API: OK
  API-->>UI: Response with success message
```

#### PlatformMetadata 的更新类图

```mermaid
classDiagram
  class PlatformMetadata {
    -name: str
    -description: str
    -id: str
    -default_config_tmpl: dict
    +PlatformMetadata(name: str, description: str, id: str, default_config_tmpl: dict)
  }
  note for PlatformMetadata "添加了 'id' 属性，用于在配置中唯一标识平台。"
```

### 文件级别更改

| 变更 | 详情 | 文件 |
| ------ | ------- | ----- |
| 引入了一项为不同消息传递平台启用或禁用插件的功能。 | <ul><li>添加了一个新的 API 端点，用于检索特定于平台的插件配置。</li><li>添加了一个新的 API 端点，用于设置特定于平台的插件配置。</li><li>更新了仪表板 UI，允许用户配置每个平台的插件可用性。</li><li>修改了命令过滤器，以遵守特定于平台的插件配置。</li><li>将平台 ID 添加到平台元数据。</li></ul> | `dashboard/src/views/ExtensionPage.vue`<br/>`astrbot/dashboard/routes/plugin.py`<br/>`astrbot/core/star/star_handler.py`<br/>`astrbot/core/star/filter/command.py`<br/>`astrbot/core/star/filter/command_group.py`<br/>`astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py`<br/>`astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py`<br/>`astrbot/core/platform/sources/gewechat/gewechat_platform_adapter.py`<br/>`astrbot/core/platform/sources/lark/lark_adapter.py`<br/>`astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py`<br/>`astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_adapter.py`<br/>`astrbot/core/platform/astr_message_event.py`<br/>`astrbot/core/platform/sources/telegram/tg_adapter.py`<br/>`astrbot/core/platform/sources/webchat/webchat_adapter.py`<br/>`astrbot/core/platform/platform_metadata.py` |

---

<details>
<summary>提示和命令</summary>

#### 与 Sourcery 交互

- **触发新的审查：** 在 Pull Request 上评论 `@sourcery-ai review`。
- **继续讨论：** 直接回复 Sourcery 的审查评论。
- **从审查评论生成 GitHub issue：** 通过回复审查评论，要求 Sourcery 从该评论创建一个 issue。您也可以回复审查评论并使用 `@sourcery-ai issue` 从该评论创建一个 issue。
- **生成 Pull Request 标题：** 在 Pull Request 标题中的任何位置写入 `@sourcery-ai` 以随时生成标题。您也可以在 Pull Request 上评论 `@sourcery-ai title` 以随时（重新）生成标题。
- **生成 Pull Request 摘要：** 在 Pull Request 正文中的任何位置写入 `@sourcery-ai summary` 以随时在您想要的位置生成 PR 摘要。您也可以在 Pull Request 上评论 `@sourcery-ai summary` 以随时（重新）生成摘要。
- **生成评审者指南：** 在 Pull Request 上评论 `@sourcery-ai guide` 以随时（重新）生成评审者指南。
- **解决所有 Sourcery 评论：** 在 Pull Request 上评论 `@sourcery-ai resolve` 以解决所有 Sourcery 评论。如果您已经解决了所有评论并且不想再看到它们，这将非常有用。
- **驳回所有 Sourcery 审查：** 在 Pull Request 上评论 `@sourcery-ai dismiss` 以驳回所有现有的 Sourcery 审查。如果您想从新的审查开始，这将特别有用 - 不要忘记评论 `@sourcery-ai review` 以触发新的审查！
- **为 issue 生成行动计划：** 在 issue 上评论 `@sourcery-ai plan` 以为其生成行动计划。

#### 自定义您的体验

访问您的 [仪表板](https://app.sourcery.ai) 以：
- 启用或禁用审查功能，例如 Sourcery 生成的 Pull Request 摘要、评审者指南等。
- 更改审查语言。
- 添加、删除或编辑自定义审查说明。
- 调整其他审查设置。

#### 获得帮助

- [联系我们的支持团队](mailto:support@sourcery.ai) 提出问题或反馈。
- 访问我们的 [文档](https://docs.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 by Sourcery

This pull request introduces a new feature that allows administrators to enable or disable plugins on a per-platform basis. It includes changes to the dashboard UI, API endpoints, command filters, and platform adapters to support this functionality.

#### Sequence Diagram for Setting Plugin Platform Enable Configuration

```mermaid
sequenceDiagram
  participant Dashboard as UI
  participant API
  participant CoreLifecycle as Core
  participant AstrBotConfig as Config

  UI->>API: POST /api/plugin/platform_enable/set with {platform_enable}
  API->>Core: set_plugin_platform_enable(platform_enable)
  Core->>Config: astrbot_config.get("platform", [])
  Config-->>Core: platforms
  Core->>Config: config.get("platform_settings", {})
  Config-->>Core: platform_settings
  Core->>Config: platform_settings["plugin_enable"] = adjusted_platform_enable
  Core->>Config: config.save_config()
  Config-->>Core: None
  Core-->>API: OK
  API-->>UI: Response with success message
```

#### Updated Class Diagram for PlatformMetadata

```mermaid
classDiagram
  class PlatformMetadata {
    -name: str
    -description: str
    -id: str
    -default_config_tmpl: dict
    +PlatformMetadata(name: str, description: str, id: str, default_config_tmpl: dict)
  }
  note for PlatformMetadata "Added 'id' attribute to uniquely identify platforms in configurations."
```

### File-Level Changes

| Change | Details | Files |
| ------ | ------- | ----- |
| Introduces a feature to enable or disable plugins for different messaging platforms. | <ul><li>Adds a new API endpoint for retrieving platform-specific plugin configurations.</li><li>Adds a new API endpoint for setting platform-specific plugin configurations.</li><li>Updates the dashboard UI to allow users to configure plugin availability per platform.</li><li>Modifies command filters to respect platform-specific plugin configurations.</li><li>Adds platform ID to platform metadata.</li></ul> | `dashboard/src/views/ExtensionPage.vue`<br/>`astrbot/dashboard/routes/plugin.py`<br/>`astrbot/core/star/star_handler.py`<br/>`astrbot/core/star/filter/command.py`<br/>`astrbot/core/star/filter/command_group.py`<br/>`astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py`<br/>`astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py`<br/>`astrbot/core/platform/sources/gewechat/gewechat_platform_adapter.py`<br/>`astrbot/core/platform/sources/lark/lark_adapter.py`<br/>`astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py`<br/>`astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_adapter.py`<br/>`astrbot/core/platform/astr_message_event.py`<br/>`astrbot/core/platform/sources/telegram/tg_adapter.py`<br/>`astrbot/core/platform/sources/webchat/webchat_adapter.py`<br/>`astrbot/core/platform/platform_metadata.py` |

---

<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!
- **Generate a plan of action for an issue:** Comment `@sourcery-ai plan` on
  an issue to generate a plan of action for it.

#### 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: zhx8702
created_at: 2025-04-08T08:41:55Z
body: > 附：当前实现仅能保证指令针对不同消息平台开启关闭，事件钩子在各个阶段直接执行，没有经过平台启用检查。 当前实现较为繁琐，建议直接新增一个阶段集中检查

创建一个新的Pipeline阶段：PlatformCompatibilityStage
在WakingCheckStage之后、ProcessStage之前执行，用于集中检查所有处理器的平台兼容性。
还需要帮忙看一下有什么没有考虑到的问题。
author_association: NONE