Feature: 支持在配置文件配置可用的插件组 (#2505)

* feat: 增加可用插件集合配置项

* remove: 旧版平台可用性配置

已经基于多配置文件实现。

* feat: 应用配置文件插件可用性配置

* perf: hoist if from if
This commit is contained in:
Soulter
2025-08-20 15:25:41 +08:00
committed by GitHub
parent 6ab90fc123
commit d2df4d0cce
21 changed files with 351 additions and 485 deletions
+4 -1
View File
@@ -7,12 +7,15 @@ from .run_context import TContext
class HandoffTool(FunctionTool, Generic[TContext]):
"""Handoff tool for delegating tasks to another agent."""
def __init__(self, agent: Agent[TContext], parameters: dict | None = None):
def __init__(
self, agent: Agent[TContext], parameters: dict | None = None, **kwargs
):
self.agent = agent
super().__init__(
name=f"transfer_to_{agent.name}",
parameters=parameters or self.default_parameters(),
description=agent.instructions or self.default_description(agent.name),
**kwargs,
)
def default_parameters(self) -> dict:
+21 -7
View File
@@ -13,7 +13,6 @@ DB_PATH = os.path.join(get_astrbot_data_path(), "data_v4.db")
DEFAULT_CONFIG = {
"config_version": 2,
"platform_settings": {
"plugin_enable": {},
"unique_session": False,
"rate_limit": {
"time": 60,
@@ -53,7 +52,7 @@ DEFAULT_CONFIG = {
"default_provider_id": "",
"default_image_caption_provider_id": "",
"image_caption_prompt": "Please describe the image using Chinese.",
"provider_pool": ["all"], # "all" 表示使用所有可用的提供者
"provider_pool": ["*"], # "*" 表示使用所有可用的提供者
"wake_prefix": "",
"web_search": False,
"websearch_provider": "default",
@@ -63,7 +62,7 @@ DEFAULT_CONFIG = {
"identifier": False,
"datetime_system_prompt": True,
"default_personality": "default",
"persona_pool": ["all"],
"persona_pool": ["*"],
"prompt_prefix": "",
"max_context_length": -1,
"dequeue_context_length": 1,
@@ -122,6 +121,7 @@ DEFAULT_CONFIG = {
"timezone": "Asia/Shanghai",
"callback_api_base": "",
"default_kb_collection": "", # 默认知识库名称
"plugin_set": ["*"], # "*" 表示使用所有可用的插件, 空列表表示不使用任何插件
}
@@ -387,9 +387,6 @@ CONFIG_METADATA_2 = {
"platform_settings": {
"type": "object",
"items": {
"plugin_enable": {
"invisible": True, # 隐藏插件启用配置
},
"unique_session": {
"type": "bool",
},
@@ -2129,8 +2126,25 @@ CONFIG_METADATA_3 = {
},
},
},
"plugin_group": {
"name": "插件配置",
"metadata": {
"plugin": {
"description": "插件",
"type": "object",
"items": {
"plugin_set": {
"description": "可用插件",
"type": "bool",
"hint": "默认启用全部未被禁用的插件。若插件在插件页面被禁用,则此处的选择不会生效。",
"_special": "select_plugin_set",
},
},
},
},
},
"ext_group": {
"name": "扩展配置",
"name": "扩展功能",
"metadata": {
"segmented_reply": {
"description": "分段回复",
-3
View File
@@ -4,7 +4,6 @@ from astrbot.core.message.message_event_result import (
)
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 .rate_limit_check.stage import RateLimitStage
@@ -21,7 +20,6 @@ STAGES_ORDER = [
"SessionStatusCheckStage", # 检查会话是否整体启用
"RateLimitStage", # 检查会话是否超过频率限制
"ContentSafetyCheckStage", # 检查内容安全
"PlatformCompatibilityStage", # 检查所有处理器的平台兼容性
"PreProcessStage", # 预处理
"ProcessStage", # 交由 Stars 处理(a.k.a 插件),或者 LLM 调用
"ResultDecorateStage", # 处理结果,比如添加回复前缀、t2i、转换为语音 等
@@ -34,7 +32,6 @@ __all__ = [
"SessionStatusCheckStage",
"RateLimitStage",
"ContentSafetyCheckStage",
"PlatformCompatibilityStage",
"PreProcessStage",
"ProcessStage",
"ResultDecorateStage",
+2 -3
View File
@@ -77,10 +77,9 @@ async def call_event_hook(
Returns:
bool: 如果事件被终止,返回 True
"""
platform_id = event.get_platform_id()
# """
handlers = star_handlers_registry.get_handlers_by_event_type(
hook_type, platform_id=platform_id
hook_type, plugins_name=event.plugins_name
)
for handler in handlers:
try:
@@ -1,56 +0,0 @@
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)
@@ -32,6 +32,7 @@ from astrbot.core.utils.metrics import Metric
from ...context import PipelineContext, call_event_hook, call_handler
from ..stage import Stage
from astrbot.core.provider.register import llm_tools
from astrbot.core.star.star_handler import star_map
from astrbot.core.astr_agent_context import AstrAgentContext
try:
@@ -422,9 +423,20 @@ class LLMRequestSubStage(Stage):
req.image_urls = []
if req.func_tool:
provider_cfg = provider.provider_config.get("modalities", ["tool_use"])
# 如果模型不支持工具使用,但请求中包含工具列表,则清空。
if "tool_use" not in provider_cfg:
logger.debug(f"用户设置提供商 {provider} 不支持工具使用,清空工具列表。")
req.func_tool = None
# 插件可用性设置
if event.plugins_name is not None and req.func_tool:
new_tool_set = ToolSet()
for tool in req.func_tool.tools:
plugin = star_map.get(tool.handler_module_path)
if not plugin:
continue
if plugin.name in event.plugins_name or plugin.reserved:
new_tool_set.add_tool(tool)
req.func_tool = new_tool_set
# run agent
agent_runner = AgentRunner()
@@ -33,16 +33,6 @@ class StarRequestSubStage(Stage):
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:
+1 -1
View File
@@ -214,7 +214,7 @@ class RespondStage(Stage):
)
handlers = star_handlers_registry.get_handlers_by_event_type(
EventType.OnAfterMessageSentEvent, platform_id=event.get_platform_id()
EventType.OnAfterMessageSentEvent, plugins_name=event.plugins_name
)
for handler in handlers:
try:
@@ -99,7 +99,7 @@ class ResultDecorateStage(Stage):
# 发送消息前事件钩子
handlers = star_handlers_registry.get_handlers_by_event_type(
EventType.OnDecoratingResultEvent, platform_id=event.get_platform_id()
EventType.OnDecoratingResultEvent, plugins_name=event.plugins_name
)
for handler in handlers:
try:
+10 -1
View File
@@ -112,8 +112,17 @@ class WakingCheckStage(Stage):
activated_handlers = []
handlers_parsed_params = {} # 注册了指令的 handler
# 将 plugins_name 设置到 event 中
enabled_plugins_name = self.ctx.astrbot_config.get("plugin_set", ["*"])
if enabled_plugins_name == ["*"]:
# 如果是 *,则表示所有插件都启用
event.plugins_name = None
else:
event.plugins_name = enabled_plugins_name
logger.debug(f"enabled_plugins_name: {enabled_plugins_name}")
for handler in star_handlers_registry.get_handlers_by_event_type(
EventType.AdapterMessageEvent
EventType.AdapterMessageEvent, plugins_name=event.plugins_name
):
# filter 需满足 AND 逻辑关系
passed = True
@@ -6,6 +6,7 @@ import uuid
from typing import List, Union, Optional, AsyncGenerator
from astrbot import logger
from astrbot.core.db.po import Conversation
from astrbot.core.message.components import (
Plain,
@@ -64,6 +65,9 @@ class AstrMessageEvent(abc.ABC):
self.call_llm = False
"""是否在此消息事件中禁止默认的 LLM 请求"""
self.plugins_name: list[str] | None = None
"""该事件启用的插件名称列表。None 表示所有插件都启用。空列表表示没有启用任何插件。"""
# back_compability
self.platform = platform_meta
@@ -181,6 +185,7 @@ class AstrMessageEvent(abc.ABC):
"""
清除额外的信息
"""
logger.info(f"清除 {self.get_platform_name()} 的额外信息: {self._extras}")
self._extras.clear()
def is_private_chat(self) -> bool:
+4 -2
View File
@@ -412,7 +412,7 @@ def register_agent(
"""
tools_ = tools or []
def decorator(_):
def decorator(awaitable: Awaitable):
AstrAgent = Agent[AstrAgentContext]
agent = AstrAgent(
name=name,
@@ -420,7 +420,9 @@ def register_agent(
tools=tools_,
run_hooks=run_hooks or BaseAgentRunHooks[AstrAgentContext](),
)
llm_tools.func_list.append(HandoffTool(agent=agent))
handoff_tool = HandoffTool(agent=agent)
handoff_tool.handler=awaitable
llm_tools.func_list.append(handoff_tool)
return RegisteringAgent(agent)
return decorator
-24
View File
@@ -56,32 +56,8 @@ 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"Plugin {self.name} ({self.version}) by {self.author}: {self.desc}"
def __repr__(self) -> str:
return f"Plugin {self.name} ({self.version}) by {self.author}: {self.desc}"
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
+17 -33
View File
@@ -7,6 +7,7 @@ from .star import star_map
T = TypeVar("T", bound="StarHandlerMetadata")
class StarHandlerRegistry(Generic[T]):
def __init__(self):
self.star_handlers_map: Dict[str, StarHandlerMetadata] = {}
@@ -26,7 +27,10 @@ class StarHandlerRegistry(Generic[T]):
print(handler.handler_full_name)
def get_handlers_by_event_type(
self, event_type: EventType, only_activated=True, platform_id=None
self,
event_type: EventType,
only_activated=True,
plugins_name: list[str] | None = None,
) -> List[StarHandlerMetadata]:
handlers = []
for handler in self._handlers:
@@ -36,8 +40,15 @@ class StarHandlerRegistry(Generic[T]):
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):
if plugins_name is not None and plugins_name != ["*"]:
plugin = star_map.get(handler.handler_module_path)
if not plugin:
continue
if (
plugin.name not in plugins_name
and event_type != EventType.OnAstrBotLoadedEvent
and not plugin.reserved
):
continue
handlers.append(handler)
return handlers
@@ -49,7 +60,8 @@ class StarHandlerRegistry(Generic[T]):
self, module_name: str
) -> List[StarHandlerMetadata]:
return [
handler for handler in self._handlers
handler
for handler in self._handlers
if handler.handler_module_path == module_name
]
@@ -67,6 +79,7 @@ class StarHandlerRegistry(Generic[T]):
def __len__(self):
return len(self._handlers)
star_handlers_registry = StarHandlerRegistry()
@@ -119,32 +132,3 @@ class 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
-28
View File
@@ -337,30 +337,8 @@ class PluginManager:
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):
"""载入插件。
specified_module_path 或者 specified_dir_name 不为 None 只载入指定的插件
@@ -480,12 +458,6 @@ class PluginManager:
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)
assert metadata.module_path is not None, (
f"插件 {metadata.name} 的模块路径为空。"
)
-89
View File
@@ -40,8 +40,6 @@ class PluginRoute(Route):
"/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
@@ -482,90 +480,3 @@ class PluginRoute(Route):
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__
@@ -5,6 +5,7 @@ import ListConfigItem from './ListConfigItem.vue'
import ProviderSelector from './ProviderSelector.vue'
import PersonaSelector from './PersonaSelector.vue'
import KnowledgeBaseSelector from './KnowledgeBaseSelector.vue'
import PluginSetSelector from './PluginSetSelector.vue'
import { useI18n } from '@/i18n/composables'
@@ -240,7 +241,34 @@ function hasVisibleItemsAfter(items, currentIndex) {
v-model="createSelectorModel(itemKey).value"
/>
</div>
<div v-else-if="itemMeta?._special === 'select_plugin_set'">
<PluginSetSelector
v-model="createSelectorModel(itemKey).value"
/>
</div>
</v-col>
</v-row>
<!-- Plugin Set Selector 全宽显示区域 -->
<v-row v-if="!itemMeta?.invisible && itemMeta?._special === 'select_plugin_set'" class="plugin-set-display-row">
<v-col cols="12" class="plugin-set-display">
<div v-if="createSelectorModel(itemKey).value && createSelectorModel(itemKey).value.length > 0" class="selected-plugins-full-width">
<div class="plugins-header">
<small class="text-grey">已选择的插件</small>
</div>
<div class="d-flex flex-wrap ga-2 mt-2">
<v-chip
v-for="plugin in (createSelectorModel(itemKey).value || [])"
:key="plugin"
size="small"
label
color="primary"
variant="outlined"
>
{{ plugin === '*' ? '所有插件' : plugin }}
</v-chip>
</div>
</div>
</v-col>
</v-row>
</template>
@@ -386,6 +414,26 @@ function hasVisibleItemsAfter(items, currentIndex) {
background-color: rgba(0, 0, 0, 0.5);
}
.plugin-set-display-row {
margin: 16px;
margin-top: 0;
}
.plugin-set-display {
padding: 0 8px;
}
.selected-plugins-full-width {
background-color: rgba(var(--v-theme-primary), 0.05);
border: 1px solid rgba(var(--v-theme-primary), 0.1);
border-radius: 8px;
padding: 12px;
}
.plugins-header {
margin-bottom: 4px;
}
@media (max-width: 600px) {
.nested-object {
padding-left: 8px;
@@ -0,0 +1,226 @@
<template>
<div>
<!-- 顶部操作区域 -->
<div class="d-flex align-center justify-space-between mb-2">
<div class="flex-grow-1">
<span v-if="!modelValue || modelValue.length === 0" style="color: rgb(var(--v-theme-primaryText));">
未启用任何插件
</span>
<span v-else-if="isAllPlugins" style="color: rgb(var(--v-theme-primaryText));">
启用所有插件 (*)
</span>
<span v-else style="color: rgb(var(--v-theme-primaryText));">
已选择 {{ modelValue.length }} 个插件
</span>
</div>
<v-btn size="small" color="primary" variant="tonal" @click="openDialog">
{{ buttonText }}
</v-btn>
</div>
</div>
<!-- Plugin Set Selection Dialog -->
<v-dialog v-model="dialog" max-width="700px">
<v-card>
<v-card-title class="text-h3 py-4" style="font-weight: normal;">
选择插件集合
</v-card-title>
<v-card-text class="pa-4">
<v-progress-linear v-if="loading" indeterminate color="primary"></v-progress-linear>
<div v-if="!loading">
<!-- 预设选项 -->
<v-radio-group v-model="selectionMode" class="mb-4" hide-details>
<v-radio
value="all"
label="启用所有插件"
color="primary"
></v-radio>
<v-radio
value="none"
label="不启用任何插件"
color="primary"
></v-radio>
<v-radio
value="custom"
label="自定义选择"
color="primary"
></v-radio>
</v-radio-group>
<!-- 自定义选择时显示插件列表 -->
<div v-if="selectionMode === 'custom'" style="max-height: 300px; overflow-y: auto;">
<v-list v-if="pluginList.length > 0" density="compact">
<v-list-item
v-for="plugin in pluginList"
:key="plugin.name"
rounded="md"
class="ma-1">
<template v-slot:prepend>
<v-checkbox
v-model="selectedPlugins"
:value="plugin.name"
color="primary"
hide-details
></v-checkbox>
</template>
<v-list-item-title>{{ plugin.name }}</v-list-item-title>
<v-list-item-subtitle>
{{ plugin.desc || '无描述' }}
<v-chip v-if="!plugin.activated" size="x-small" color="grey" class="ml-1">
未激活
</v-chip>
</v-list-item-subtitle>
</v-list-item>
<div class="pl-8 pt-2">
<small>*不显示系统插件和已经在插件页禁用的插件</small>
</div>
</v-list>
<div v-else class="text-center py-8">
<v-icon size="64" color="grey-lighten-1">mdi-puzzle-outline</v-icon>
<p class="text-grey mt-4">暂无可用的插件</p>
</div>
</div>
</div>
</v-card-text>
<v-card-actions class="pa-4">
<v-spacer></v-spacer>
<v-btn variant="text" @click="cancelSelection">取消</v-btn>
<v-btn
color="primary"
@click="confirmSelection">
确认选择
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
import axios from 'axios'
const props = defineProps({
modelValue: {
type: Array,
default: () => []
},
buttonText: {
type: String,
default: '选择插件集合...'
},
maxDisplayItems: {
type: Number,
default: 3
}
})
const emit = defineEmits(['update:modelValue'])
const dialog = ref(false)
const pluginList = ref([])
const loading = ref(false)
const selectionMode = ref('custom') // 'all', 'none', 'custom'
const selectedPlugins = ref([])
// ""
const isAllPlugins = computed(() => {
return props.modelValue && props.modelValue.length === 1 && props.modelValue[0] === '*'
})
//
function removePlugin(pluginName) {
if (props.modelValue && props.modelValue.length > 0) {
const newValue = props.modelValue.filter(name => name !== pluginName)
emit('update:modelValue', newValue)
}
}
// modelValue
watch(() => props.modelValue, (newValue) => {
if (!newValue || newValue.length === 0) {
selectionMode.value = 'none'
selectedPlugins.value = []
} else if (newValue.length === 1 && newValue[0] === '*') {
selectionMode.value = 'all'
selectedPlugins.value = []
} else {
selectionMode.value = 'custom'
selectedPlugins.value = [...newValue]
}
}, { immediate: true })
async function openDialog() {
dialog.value = true
await loadPlugins()
}
async function loadPlugins() {
loading.value = true
try {
const response = await axios.get('/api/plugin/get')
if (response.data.status === 'ok') {
//
pluginList.value = (response.data.data || [])
.filter(plugin => plugin.activated && !plugin.reserved)
.sort((a, b) => a.name.localeCompare(b.name))
}
} catch (error) {
console.error('加载插件列表失败:', error)
pluginList.value = []
} finally {
loading.value = false
}
}
function confirmSelection() {
let newValue = []
switch (selectionMode.value) {
case 'all':
newValue = ['*']
break
case 'none':
newValue = []
break
case 'custom':
newValue = [...selectedPlugins.value]
break
}
emit('update:modelValue', newValue)
dialog.value = false
}
function cancelSelection() {
//
const currentValue = props.modelValue || []
if (currentValue.length === 0) {
selectionMode.value = 'none'
selectedPlugins.value = []
} else if (currentValue.length === 1 && currentValue[0] === '*') {
selectionMode.value = 'all'
selectedPlugins.value = []
} else {
selectionMode.value = 'custom'
selectedPlugins.value = [...currentValue]
}
dialog.value = false
}
</script>
<style scoped>
.v-list-item {
transition: all 0.2s ease;
}
.v-list-item:hover {
background-color: rgba(var(--v-theme-primary), 0.04);
}
</style>
@@ -16,7 +16,6 @@
"buttons": {
"showSystemPlugins": "Show System Extensions",
"hideSystemPlugins": "Hide System Extensions",
"platformConfig": "Platform Command Config",
"install": "Install",
"uninstall": "Uninstall",
"update": "Update",
@@ -88,18 +87,6 @@
"title": "Error Information",
"checkConsole": "Please check console for details"
},
"platformConfig": {
"title": "Platform Command Availability Configuration",
"description": "Set the availability of each extension on different platforms, check to enable",
"noAdapters": "No Platform Adapters Found",
"noAdaptersDesc": "Please add and configure platform adapters in Platform Management first, then set extension platform availability",
"goPlatforms": "Go to Platform Management",
"selectAll": "Select All",
"selectAllNormal": "Select All Normal Extensions",
"selectAllSystem": "Select All System Extensions",
"selectNone": "Select None",
"toggleAll": "Toggle All"
},
"config": {
"title": "Extension Configuration",
"noConfig": "This extension has no configuration"
@@ -137,8 +124,6 @@
"installing": "Installing extension from file",
"installingFromUrl": "Installing extension from URL...",
"installFailed": "Extension installation failed:",
"getPlatformConfigFailed": "Failed to get platform extension config:",
"savePlatformConfigFailed": "Failed to save platform extension config:",
"getMarketDataFailed": "Failed to get extension market data:",
"hasUpdate": "New version available:",
"confirmDelete": "Are you sure you want to delete this extension?",
@@ -16,7 +16,6 @@
"buttons": {
"showSystemPlugins": "显示系统插件",
"hideSystemPlugins": "隐藏系统插件",
"platformConfig": "平台命令配置",
"install": "安装",
"uninstall": "卸载",
"update": "更新",
@@ -88,18 +87,6 @@
"title": "错误信息",
"checkConsole": "详情请检查控制台"
},
"platformConfig": {
"title": "平台命令可用性配置",
"description": "设置每个插件在不同平台上的可用性,勾选表示启用",
"noAdapters": "未找到平台适配器",
"noAdaptersDesc": "请先在 平台管理 中添加并配置平台适配器,然后再设置插件的平台可用性",
"goPlatforms": "前往平台管理",
"selectAll": "全选",
"selectAllNormal": "全选普通插件",
"selectAllSystem": "全选系统插件",
"selectNone": "全不选",
"toggleAll": "反选"
},
"config": {
"title": "插件配置",
"noConfig": "这个插件没有配置"
@@ -137,8 +124,6 @@
"installing": "正在从文件安装插件",
"installingFromUrl": "正在从链接安装插件...",
"installFailed": "安装插件失败:",
"getPlatformConfigFailed": "获取平台插件配置失败:",
"savePlatformConfigFailed": "保存平台插件配置失败:",
"getMarketDataFailed": "获取插件市场数据失败:",
"hasUpdate": "有新版本:",
"confirmDelete": "确定要删除插件吗?",
-196
View File
@@ -45,14 +45,6 @@ const readmeDialog = reactive({
pluginName: '',
repoUrl: null
});
//
const platformEnableDialog = ref(false);
const platformEnableData = reactive({
platforms: [],
plugins: [],
platform_enable: {}
});
const loadingPlatformData = ref(false);
//
const isListView = ref(false);
@@ -326,100 +318,7 @@ 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(tm('dialogs.platformConfig.noAdaptersDesc'), "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(tm('messages.getPlatformConfigFailed') + " " + 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(tm('messages.savePlatformConfigFailed') + " " + 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 => {
// onlyReservednull
// onlyReservedtrue
// onlyReservedfalse
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;
});
};
const open = (link) => {
if (link) {
@@ -685,11 +584,6 @@ onMounted(async () => {
{{ showReserved ? tm('buttons.hideSystemPlugins') : tm('buttons.showSystemPlugins') }}
</v-btn>
<v-btn class="ml-2" variant="tonal" @click="getPlatformEnableConfig">
<v-icon>mdi-cog</v-icon>
{{ tm('buttons.platformConfig') }}
</v-btn>
<v-btn class="ml-2" color="primary" variant="tonal" @click="dialog = true">
<v-icon>mdi-plus</v-icon>
{{ tm('buttons.install') }}
@@ -965,96 +859,6 @@ onMounted(async () => {
</v-col>
</v-row>
<!-- 插件平台配置对话框 -->
<v-dialog v-model="platformEnableDialog" max-width="900" persistent>
<v-card class="rounded-lg">
<v-toolbar color="primary" density="comfortable" flat>
<v-toolbar-title class="text-white">{{ tm('dialogs.platformConfig.title') }}</v-toolbar-title>
<v-spacer></v-spacer>
<v-btn icon @click="platformEnableDialog = false" variant="text" color="white">
<v-icon>mdi-close</v-icon>
</v-btn>
</v-toolbar>
<v-card-text class="pt-4">
<p class="text-body-2 mb-4">{{ tm('dialogs.platformConfig.description') }}</p>
<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-8">
<v-icon icon="mdi-alert" color="warning" size="64" class="mb-4"></v-icon>
<div class="text-h5 mb-2">{{ tm('dialogs.platformConfig.noAdapters') }}</div>
<div class="text-body-1 mb-4">{{ tm('dialogs.platformConfig.noAdaptersDesc') }}</div>
<v-btn color="primary" to="/platforms" variant="elevated">{{ tm('dialogs.platformConfig.goPlatforms')
}}</v-btn>
</div>
<v-sheet v-else class="rounded-lg overflow-hidden">
<v-table hover class="elevation-1">
<thead>
<tr>
<th class="text-left">{{ tm('table.headers.name') }}</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>{{ tm('dialogs.platformConfig.selectAll') }}</v-list-item-title>
</v-list-item>
<v-list-item @click="selectAllPluginsForPlatform(platform.name, true, false)">
<v-list-item-title>{{ tm('dialogs.platformConfig.selectAllNormal') }}</v-list-item-title>
</v-list-item>
<v-list-item @click="selectAllPluginsForPlatform(platform.name, true, true)">
<v-list-item-title>{{ tm('dialogs.platformConfig.selectAllSystem') }}</v-list-item-title>
</v-list-item>
<v-list-item @click="selectAllPluginsForPlatform(platform.name, false)">
<v-list-item-title>{{ tm('dialogs.platformConfig.selectNone') }}</v-list-item-title>
</v-list-item>
<v-list-item @click="toggleAllPluginsForPlatform(platform.name)">
<v-list-item-title>{{ tm('dialogs.platformConfig.toggleAll') }}</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">{{ tm('status.system')
}}</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-sheet>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="grey" text @click="platformEnableDialog = false">{{ tm('buttons.close') }}</v-btn>
<v-btn v-if="platformEnableData.platforms.length > 0" color="primary" @click="savePlatformEnableConfig">{{
tm('buttons.save') }}</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- 配置对话框 -->
<v-dialog v-model="configDialog" width="1000">
<v-card>