AstrBot 4.5.0__py3-none-any.whl → 4.5.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- astrbot/api/__init__.py +10 -11
- astrbot/api/event/__init__.py +5 -6
- astrbot/api/event/filter/__init__.py +37 -36
- astrbot/api/platform/__init__.py +7 -8
- astrbot/api/provider/__init__.py +7 -7
- astrbot/api/star/__init__.py +3 -4
- astrbot/api/util/__init__.py +2 -2
- astrbot/cli/__main__.py +5 -5
- astrbot/cli/commands/__init__.py +3 -3
- astrbot/cli/commands/cmd_conf.py +19 -16
- astrbot/cli/commands/cmd_init.py +3 -2
- astrbot/cli/commands/cmd_plug.py +8 -10
- astrbot/cli/commands/cmd_run.py +5 -6
- astrbot/cli/utils/__init__.py +6 -6
- astrbot/cli/utils/basic.py +14 -14
- astrbot/cli/utils/plugin.py +24 -15
- astrbot/cli/utils/version_comparator.py +10 -12
- astrbot/core/__init__.py +8 -6
- astrbot/core/agent/agent.py +3 -2
- astrbot/core/agent/handoff.py +6 -2
- astrbot/core/agent/hooks.py +9 -6
- astrbot/core/agent/mcp_client.py +50 -15
- astrbot/core/agent/message.py +168 -0
- astrbot/core/agent/response.py +2 -1
- astrbot/core/agent/run_context.py +2 -3
- astrbot/core/agent/runners/base.py +10 -13
- astrbot/core/agent/runners/tool_loop_agent_runner.py +52 -51
- astrbot/core/agent/tool.py +60 -41
- astrbot/core/agent/tool_executor.py +9 -3
- astrbot/core/astr_agent_context.py +3 -1
- astrbot/core/astrbot_config_mgr.py +29 -9
- astrbot/core/config/__init__.py +2 -2
- astrbot/core/config/astrbot_config.py +28 -26
- astrbot/core/config/default.py +44 -6
- astrbot/core/conversation_mgr.py +105 -36
- astrbot/core/core_lifecycle.py +68 -54
- astrbot/core/db/__init__.py +33 -18
- astrbot/core/db/migration/helper.py +18 -13
- astrbot/core/db/migration/migra_3_to_4.py +53 -34
- astrbot/core/db/migration/migra_45_to_46.py +1 -1
- astrbot/core/db/migration/shared_preferences_v3.py +2 -1
- astrbot/core/db/migration/sqlite_v3.py +26 -23
- astrbot/core/db/po.py +27 -18
- astrbot/core/db/sqlite.py +74 -45
- astrbot/core/db/vec_db/base.py +10 -14
- astrbot/core/db/vec_db/faiss_impl/document_storage.py +90 -77
- astrbot/core/db/vec_db/faiss_impl/embedding_storage.py +9 -3
- astrbot/core/db/vec_db/faiss_impl/vec_db.py +36 -31
- astrbot/core/event_bus.py +8 -6
- astrbot/core/file_token_service.py +6 -5
- astrbot/core/initial_loader.py +7 -5
- astrbot/core/knowledge_base/chunking/__init__.py +1 -3
- astrbot/core/knowledge_base/chunking/base.py +1 -0
- astrbot/core/knowledge_base/chunking/fixed_size.py +2 -0
- astrbot/core/knowledge_base/chunking/recursive.py +16 -10
- astrbot/core/knowledge_base/kb_db_sqlite.py +50 -48
- astrbot/core/knowledge_base/kb_helper.py +30 -17
- astrbot/core/knowledge_base/kb_mgr.py +6 -7
- astrbot/core/knowledge_base/models.py +10 -4
- astrbot/core/knowledge_base/parsers/__init__.py +3 -5
- astrbot/core/knowledge_base/parsers/base.py +1 -0
- astrbot/core/knowledge_base/parsers/markitdown_parser.py +2 -1
- astrbot/core/knowledge_base/parsers/pdf_parser.py +2 -1
- astrbot/core/knowledge_base/parsers/text_parser.py +1 -0
- astrbot/core/knowledge_base/parsers/util.py +1 -1
- astrbot/core/knowledge_base/retrieval/__init__.py +6 -8
- astrbot/core/knowledge_base/retrieval/manager.py +17 -14
- astrbot/core/knowledge_base/retrieval/rank_fusion.py +7 -3
- astrbot/core/knowledge_base/retrieval/sparse_retriever.py +11 -5
- astrbot/core/log.py +21 -13
- astrbot/core/message/components.py +123 -217
- astrbot/core/message/message_event_result.py +24 -24
- astrbot/core/persona_mgr.py +20 -11
- astrbot/core/pipeline/__init__.py +7 -7
- astrbot/core/pipeline/content_safety_check/stage.py +13 -9
- astrbot/core/pipeline/content_safety_check/strategies/__init__.py +1 -2
- astrbot/core/pipeline/content_safety_check/strategies/baidu_aip.py +12 -13
- astrbot/core/pipeline/content_safety_check/strategies/keywords.py +1 -0
- astrbot/core/pipeline/content_safety_check/strategies/strategy.py +6 -6
- astrbot/core/pipeline/context.py +4 -1
- astrbot/core/pipeline/context_utils.py +77 -7
- astrbot/core/pipeline/preprocess_stage/stage.py +12 -9
- astrbot/core/pipeline/process_stage/method/llm_request.py +125 -72
- astrbot/core/pipeline/process_stage/method/star_request.py +19 -17
- astrbot/core/pipeline/process_stage/stage.py +13 -10
- astrbot/core/pipeline/process_stage/utils.py +6 -5
- astrbot/core/pipeline/rate_limit_check/stage.py +37 -36
- astrbot/core/pipeline/respond/stage.py +23 -20
- astrbot/core/pipeline/result_decorate/stage.py +31 -23
- astrbot/core/pipeline/scheduler.py +12 -8
- astrbot/core/pipeline/session_status_check/stage.py +12 -8
- astrbot/core/pipeline/stage.py +10 -4
- astrbot/core/pipeline/waking_check/stage.py +24 -18
- astrbot/core/pipeline/whitelist_check/stage.py +10 -7
- astrbot/core/platform/__init__.py +6 -6
- astrbot/core/platform/astr_message_event.py +76 -110
- astrbot/core/platform/astrbot_message.py +11 -13
- astrbot/core/platform/manager.py +16 -15
- astrbot/core/platform/message_session.py +5 -3
- astrbot/core/platform/platform.py +16 -24
- astrbot/core/platform/platform_metadata.py +4 -4
- astrbot/core/platform/register.py +8 -8
- astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py +23 -15
- astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py +51 -33
- astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py +47 -29
- astrbot/core/platform/sources/dingtalk/dingtalk_event.py +7 -3
- astrbot/core/platform/sources/discord/client.py +9 -6
- astrbot/core/platform/sources/discord/components.py +18 -14
- astrbot/core/platform/sources/discord/discord_platform_adapter.py +45 -30
- astrbot/core/platform/sources/discord/discord_platform_event.py +38 -30
- astrbot/core/platform/sources/lark/lark_adapter.py +23 -17
- astrbot/core/platform/sources/lark/lark_event.py +21 -14
- astrbot/core/platform/sources/misskey/misskey_adapter.py +107 -67
- astrbot/core/platform/sources/misskey/misskey_api.py +153 -129
- astrbot/core/platform/sources/misskey/misskey_event.py +20 -15
- astrbot/core/platform/sources/misskey/misskey_utils.py +74 -62
- astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py +63 -44
- astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py +41 -26
- astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_adapter.py +36 -17
- astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_event.py +3 -1
- astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_server.py +12 -7
- astrbot/core/platform/sources/satori/satori_adapter.py +56 -38
- astrbot/core/platform/sources/satori/satori_event.py +34 -25
- astrbot/core/platform/sources/slack/client.py +11 -9
- astrbot/core/platform/sources/slack/slack_adapter.py +52 -36
- astrbot/core/platform/sources/slack/slack_event.py +34 -24
- astrbot/core/platform/sources/telegram/tg_adapter.py +38 -18
- astrbot/core/platform/sources/telegram/tg_event.py +32 -18
- astrbot/core/platform/sources/webchat/webchat_adapter.py +27 -17
- astrbot/core/platform/sources/webchat/webchat_event.py +14 -10
- astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py +115 -120
- astrbot/core/platform/sources/wechatpadpro/wechatpadpro_message_event.py +9 -8
- astrbot/core/platform/sources/wechatpadpro/xml_data_parser.py +15 -16
- astrbot/core/platform/sources/wecom/wecom_adapter.py +35 -18
- astrbot/core/platform/sources/wecom/wecom_event.py +55 -48
- astrbot/core/platform/sources/wecom/wecom_kf.py +34 -44
- astrbot/core/platform/sources/wecom/wecom_kf_message.py +26 -10
- astrbot/core/platform/sources/wecom_ai_bot/WXBizJsonMsgCrypt.py +18 -10
- astrbot/core/platform/sources/wecom_ai_bot/__init__.py +3 -5
- astrbot/core/platform/sources/wecom_ai_bot/ierror.py +0 -1
- astrbot/core/platform/sources/wecom_ai_bot/wecomai_adapter.py +61 -37
- astrbot/core/platform/sources/wecom_ai_bot/wecomai_api.py +67 -28
- astrbot/core/platform/sources/wecom_ai_bot/wecomai_event.py +8 -9
- astrbot/core/platform/sources/wecom_ai_bot/wecomai_queue_mgr.py +18 -9
- astrbot/core/platform/sources/wecom_ai_bot/wecomai_server.py +14 -12
- astrbot/core/platform/sources/wecom_ai_bot/wecomai_utils.py +22 -12
- astrbot/core/platform/sources/weixin_official_account/weixin_offacc_adapter.py +40 -26
- astrbot/core/platform/sources/weixin_official_account/weixin_offacc_event.py +47 -45
- astrbot/core/platform_message_history_mgr.py +5 -3
- astrbot/core/provider/__init__.py +2 -3
- astrbot/core/provider/entites.py +8 -8
- astrbot/core/provider/entities.py +61 -75
- astrbot/core/provider/func_tool_manager.py +59 -55
- astrbot/core/provider/manager.py +40 -22
- astrbot/core/provider/provider.py +72 -46
- astrbot/core/provider/register.py +7 -7
- astrbot/core/provider/sources/anthropic_source.py +48 -30
- astrbot/core/provider/sources/azure_tts_source.py +17 -13
- astrbot/core/provider/sources/coze_api_client.py +27 -17
- astrbot/core/provider/sources/coze_source.py +104 -87
- astrbot/core/provider/sources/dashscope_source.py +18 -11
- astrbot/core/provider/sources/dashscope_tts.py +36 -23
- astrbot/core/provider/sources/dify_source.py +25 -20
- astrbot/core/provider/sources/edge_tts_source.py +21 -17
- astrbot/core/provider/sources/fishaudio_tts_api_source.py +22 -14
- astrbot/core/provider/sources/gemini_embedding_source.py +12 -13
- astrbot/core/provider/sources/gemini_source.py +72 -58
- astrbot/core/provider/sources/gemini_tts_source.py +8 -6
- astrbot/core/provider/sources/gsv_selfhosted_source.py +17 -14
- astrbot/core/provider/sources/gsvi_tts_source.py +11 -7
- astrbot/core/provider/sources/minimax_tts_api_source.py +50 -40
- astrbot/core/provider/sources/openai_embedding_source.py +6 -8
- astrbot/core/provider/sources/openai_source.py +102 -69
- astrbot/core/provider/sources/openai_tts_api_source.py +14 -6
- astrbot/core/provider/sources/sensevoice_selfhosted_source.py +13 -11
- astrbot/core/provider/sources/vllm_rerank_source.py +10 -4
- astrbot/core/provider/sources/volcengine_tts.py +38 -31
- astrbot/core/provider/sources/whisper_api_source.py +14 -12
- astrbot/core/provider/sources/whisper_selfhosted_source.py +15 -11
- astrbot/core/provider/sources/xinference_rerank_source.py +116 -0
- astrbot/core/provider/sources/xinference_stt_provider.py +197 -0
- astrbot/core/star/__init__.py +16 -11
- astrbot/core/star/config.py +10 -15
- astrbot/core/star/context.py +109 -84
- astrbot/core/star/filter/__init__.py +4 -3
- astrbot/core/star/filter/command.py +30 -28
- astrbot/core/star/filter/command_group.py +27 -24
- astrbot/core/star/filter/custom_filter.py +6 -5
- astrbot/core/star/filter/event_message_type.py +4 -2
- astrbot/core/star/filter/permission.py +4 -2
- astrbot/core/star/filter/platform_adapter_type.py +4 -2
- astrbot/core/star/filter/regex.py +4 -2
- astrbot/core/star/register/__init__.py +19 -19
- astrbot/core/star/register/star.py +6 -2
- astrbot/core/star/register/star_handler.py +96 -73
- astrbot/core/star/session_llm_manager.py +48 -14
- astrbot/core/star/session_plugin_manager.py +29 -15
- astrbot/core/star/star.py +1 -2
- astrbot/core/star/star_handler.py +13 -8
- astrbot/core/star/star_manager.py +151 -59
- astrbot/core/star/star_tools.py +44 -37
- astrbot/core/star/updator.py +10 -10
- astrbot/core/umop_config_router.py +10 -4
- astrbot/core/updator.py +13 -5
- astrbot/core/utils/astrbot_path.py +3 -5
- astrbot/core/utils/dify_api_client.py +33 -15
- astrbot/core/utils/io.py +66 -42
- astrbot/core/utils/log_pipe.py +1 -1
- astrbot/core/utils/metrics.py +7 -7
- astrbot/core/utils/path_util.py +15 -16
- astrbot/core/utils/pip_installer.py +5 -5
- astrbot/core/utils/session_waiter.py +19 -20
- astrbot/core/utils/shared_preferences.py +45 -20
- astrbot/core/utils/t2i/__init__.py +4 -1
- astrbot/core/utils/t2i/network_strategy.py +35 -26
- astrbot/core/utils/t2i/renderer.py +11 -5
- astrbot/core/utils/t2i/template_manager.py +14 -15
- astrbot/core/utils/tencent_record_helper.py +19 -13
- astrbot/core/utils/version_comparator.py +10 -13
- astrbot/core/zip_updator.py +43 -40
- astrbot/dashboard/routes/__init__.py +18 -18
- astrbot/dashboard/routes/auth.py +10 -8
- astrbot/dashboard/routes/chat.py +30 -21
- astrbot/dashboard/routes/config.py +92 -75
- astrbot/dashboard/routes/conversation.py +46 -39
- astrbot/dashboard/routes/file.py +4 -2
- astrbot/dashboard/routes/knowledge_base.py +47 -40
- astrbot/dashboard/routes/log.py +9 -4
- astrbot/dashboard/routes/persona.py +19 -16
- astrbot/dashboard/routes/plugin.py +69 -55
- astrbot/dashboard/routes/route.py +3 -1
- astrbot/dashboard/routes/session_management.py +130 -116
- astrbot/dashboard/routes/stat.py +34 -34
- astrbot/dashboard/routes/t2i.py +15 -12
- astrbot/dashboard/routes/tools.py +47 -52
- astrbot/dashboard/routes/update.py +32 -28
- astrbot/dashboard/server.py +30 -26
- astrbot/dashboard/utils.py +8 -4
- {astrbot-4.5.0.dist-info → astrbot-4.5.2.dist-info}/METADATA +4 -2
- astrbot-4.5.2.dist-info/RECORD +261 -0
- astrbot-4.5.0.dist-info/RECORD +0 -258
- {astrbot-4.5.0.dist-info → astrbot-4.5.2.dist-info}/WHEEL +0 -0
- {astrbot-4.5.0.dist-info → astrbot-4.5.2.dist-info}/entry_points.txt +0 -0
- {astrbot-4.5.0.dist-info → astrbot-4.5.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
"""
|
|
2
|
-
插件的重载、启停、安装、卸载等操作。
|
|
3
|
-
"""
|
|
1
|
+
"""插件的重载、启停、安装、卸载等操作。"""
|
|
4
2
|
|
|
5
3
|
import asyncio
|
|
6
4
|
import functools
|
|
@@ -15,6 +13,7 @@ from types import ModuleType
|
|
|
15
13
|
import yaml
|
|
16
14
|
|
|
17
15
|
from astrbot.core import logger, pip_installer, sp
|
|
16
|
+
from astrbot.core.agent.handoff import FunctionTool, HandoffTool
|
|
18
17
|
from astrbot.core.config.astrbot_config import AstrBotConfig
|
|
19
18
|
from astrbot.core.provider.register import llm_tools
|
|
20
19
|
from astrbot.core.utils.astrbot_path import (
|
|
@@ -22,7 +21,6 @@ from astrbot.core.utils.astrbot_path import (
|
|
|
22
21
|
get_astrbot_plugin_path,
|
|
23
22
|
)
|
|
24
23
|
from astrbot.core.utils.io import remove_dir
|
|
25
|
-
from astrbot.core.agent.handoff import HandoffTool, FunctionTool
|
|
26
24
|
|
|
27
25
|
from . import StarMetadata
|
|
28
26
|
from .context import Context
|
|
@@ -52,8 +50,9 @@ class PluginManager:
|
|
|
52
50
|
"""存储插件配置的路径。data/config"""
|
|
53
51
|
self.reserved_plugin_path = os.path.abspath(
|
|
54
52
|
os.path.join(
|
|
55
|
-
os.path.dirname(os.path.abspath(__file__)),
|
|
56
|
-
|
|
53
|
+
os.path.dirname(os.path.abspath(__file__)),
|
|
54
|
+
"../../../packages",
|
|
55
|
+
),
|
|
57
56
|
)
|
|
58
57
|
"""保留插件的路径。在 packages 目录下"""
|
|
59
58
|
self.conf_schema_fname = "_conf_schema.json"
|
|
@@ -80,7 +79,7 @@ class PluginManager:
|
|
|
80
79
|
except asyncio.CancelledError:
|
|
81
80
|
pass
|
|
82
81
|
except Exception as e:
|
|
83
|
-
logger.error(f"插件热重载监视任务异常: {
|
|
82
|
+
logger.error(f"插件热重载监视任务异常: {e!s}")
|
|
84
83
|
logger.error(traceback.format_exc())
|
|
85
84
|
|
|
86
85
|
async def _handle_file_changes(self, changes):
|
|
@@ -95,11 +94,13 @@ class PluginManager:
|
|
|
95
94
|
continue
|
|
96
95
|
if star.reserved:
|
|
97
96
|
plugin_dir_path = os.path.join(
|
|
98
|
-
self.reserved_plugin_path,
|
|
97
|
+
self.reserved_plugin_path,
|
|
98
|
+
star.root_dir_name,
|
|
99
99
|
)
|
|
100
100
|
else:
|
|
101
101
|
plugin_dir_path = os.path.join(
|
|
102
|
-
self.plugin_store_path,
|
|
102
|
+
self.plugin_store_path,
|
|
103
|
+
star.root_dir_name,
|
|
103
104
|
)
|
|
104
105
|
plugins_to_check.append((plugin_dir_path, star.name))
|
|
105
106
|
reloaded_plugins = set()
|
|
@@ -143,14 +144,14 @@ class PluginManager:
|
|
|
143
144
|
logger.info(f"插件 {d} 未找到 main.py 或者 {d}.py,跳过。")
|
|
144
145
|
continue
|
|
145
146
|
if os.path.exists(os.path.join(path, d, "main.py")) or os.path.exists(
|
|
146
|
-
os.path.join(path, d, d + ".py")
|
|
147
|
+
os.path.join(path, d, d + ".py"),
|
|
147
148
|
):
|
|
148
149
|
modules.append(
|
|
149
150
|
{
|
|
150
151
|
"pname": d,
|
|
151
152
|
"module": module_str,
|
|
152
153
|
"module_path": os.path.join(path, d, module_str),
|
|
153
|
-
}
|
|
154
|
+
},
|
|
154
155
|
)
|
|
155
156
|
return modules
|
|
156
157
|
|
|
@@ -186,7 +187,7 @@ class PluginManager:
|
|
|
186
187
|
try:
|
|
187
188
|
await pip_installer.install(requirements_path=pth)
|
|
188
189
|
except Exception as e:
|
|
189
|
-
logger.error(f"更新插件 {p} 的依赖失败。Code: {
|
|
190
|
+
logger.error(f"更新插件 {p} 的依赖失败。Code: {e!s}")
|
|
190
191
|
|
|
191
192
|
@staticmethod
|
|
192
193
|
def _load_plugin_metadata(plugin_path: str, plugin_obj=None) -> StarMetadata | None:
|
|
@@ -201,7 +202,8 @@ class PluginManager:
|
|
|
201
202
|
|
|
202
203
|
if os.path.exists(os.path.join(plugin_path, "metadata.yaml")):
|
|
203
204
|
with open(
|
|
204
|
-
os.path.join(plugin_path, "metadata.yaml"),
|
|
205
|
+
os.path.join(plugin_path, "metadata.yaml"),
|
|
206
|
+
encoding="utf-8",
|
|
205
207
|
) as f:
|
|
206
208
|
metadata = yaml.safe_load(f)
|
|
207
209
|
elif plugin_obj and hasattr(plugin_obj, "info"):
|
|
@@ -219,7 +221,7 @@ class PluginManager:
|
|
|
219
221
|
or "author" not in metadata
|
|
220
222
|
):
|
|
221
223
|
raise Exception(
|
|
222
|
-
"插件元数据信息不完整。name, desc, version, author 是必须的字段。"
|
|
224
|
+
"插件元数据信息不完整。name, desc, version, author 是必须的字段。",
|
|
223
225
|
)
|
|
224
226
|
metadata = StarMetadata(
|
|
225
227
|
name=metadata["name"],
|
|
@@ -234,7 +236,8 @@ class PluginManager:
|
|
|
234
236
|
|
|
235
237
|
@staticmethod
|
|
236
238
|
def _get_plugin_related_modules(
|
|
237
|
-
plugin_root_dir: str,
|
|
239
|
+
plugin_root_dir: str,
|
|
240
|
+
is_reserved: bool = False,
|
|
238
241
|
) -> list[str]:
|
|
239
242
|
"""获取与指定插件相关的所有已加载模块名
|
|
240
243
|
|
|
@@ -246,6 +249,7 @@ class PluginManager:
|
|
|
246
249
|
|
|
247
250
|
Returns:
|
|
248
251
|
list[str]: 与该插件相关的模块名列表
|
|
252
|
+
|
|
249
253
|
"""
|
|
250
254
|
prefix = "packages." if is_reserved else "data.plugins."
|
|
251
255
|
return [
|
|
@@ -268,6 +272,7 @@ class PluginManager:
|
|
|
268
272
|
module_patterns: 要移除的模块名模式列表(例如 ["data.plugins", "packages"])
|
|
269
273
|
root_dir_name: 插件根目录名,用于移除与该插件相关的所有模块
|
|
270
274
|
is_reserved: 插件是否为保留插件(影响模块路径前缀)
|
|
275
|
+
|
|
271
276
|
"""
|
|
272
277
|
if module_patterns:
|
|
273
278
|
for pattern in module_patterns:
|
|
@@ -278,7 +283,8 @@ class PluginManager:
|
|
|
278
283
|
|
|
279
284
|
if root_dir_name:
|
|
280
285
|
for module_name in self._get_plugin_related_modules(
|
|
281
|
-
root_dir_name,
|
|
286
|
+
root_dir_name,
|
|
287
|
+
is_reserved,
|
|
282
288
|
):
|
|
283
289
|
try:
|
|
284
290
|
del sys.modules[module_name]
|
|
@@ -297,6 +303,7 @@ class PluginManager:
|
|
|
297
303
|
tuple: 返回 load() 方法的结果,包含 (success, error_message)
|
|
298
304
|
- success (bool): 重载是否成功
|
|
299
305
|
- error_message (str|None): 错误信息,成功时为 None
|
|
306
|
+
|
|
300
307
|
"""
|
|
301
308
|
async with self._pm_lock:
|
|
302
309
|
specified_module_path = None
|
|
@@ -315,7 +322,7 @@ class PluginManager:
|
|
|
315
322
|
except Exception as e:
|
|
316
323
|
logger.warning(traceback.format_exc())
|
|
317
324
|
logger.warning(
|
|
318
|
-
f"插件 {smd.name} 未被正常终止: {
|
|
325
|
+
f"插件 {smd.name} 未被正常终止: {e!s}, 可能会导致该插件运行不正常。",
|
|
319
326
|
)
|
|
320
327
|
if smd.name and smd.module_path:
|
|
321
328
|
await self._unbind_plugin(smd.name, smd.module_path)
|
|
@@ -332,7 +339,7 @@ class PluginManager:
|
|
|
332
339
|
except Exception as e:
|
|
333
340
|
logger.warning(traceback.format_exc())
|
|
334
341
|
logger.warning(
|
|
335
|
-
f"插件 {smd.name} 未被正常终止: {
|
|
342
|
+
f"插件 {smd.name} 未被正常终止: {e!s}, 可能会导致该插件运行不正常。",
|
|
336
343
|
)
|
|
337
344
|
if smd.name:
|
|
338
345
|
await self._unbind_plugin(smd.name, specified_module_path)
|
|
@@ -353,6 +360,7 @@ class PluginManager:
|
|
|
353
360
|
tuple: (success, error_message)
|
|
354
361
|
- success (bool): 是否全部加载成功
|
|
355
362
|
- error_message (str|None): 错误信息,成功时为 None
|
|
363
|
+
|
|
356
364
|
"""
|
|
357
365
|
inactivated_plugins = await sp.global_get("inactivated_plugins", [])
|
|
358
366
|
inactivated_llm_tools = await sp.global_get("inactivated_llm_tools", [])
|
|
@@ -371,7 +379,8 @@ class PluginManager:
|
|
|
371
379
|
# module_path = plugin_module['module_path']
|
|
372
380
|
root_dir_name = plugin_module["pname"] # 插件的目录名
|
|
373
381
|
reserved = plugin_module.get(
|
|
374
|
-
"reserved",
|
|
382
|
+
"reserved",
|
|
383
|
+
False,
|
|
375
384
|
) # 是否是保留插件。目前在 packages/ 目录下的都是保留插件。保留插件不可以卸载。
|
|
376
385
|
|
|
377
386
|
path = "data.plugins." if not reserved else "packages."
|
|
@@ -394,7 +403,7 @@ class PluginManager:
|
|
|
394
403
|
module = __import__(path, fromlist=[module_str])
|
|
395
404
|
except Exception as e:
|
|
396
405
|
logger.error(traceback.format_exc())
|
|
397
|
-
logger.error(f"插件 {root_dir_name} 导入失败。原因:{
|
|
406
|
+
logger.error(f"插件 {root_dir_name} 导入失败。原因:{e!s}")
|
|
398
407
|
continue
|
|
399
408
|
|
|
400
409
|
# 检查 _conf_schema.json
|
|
@@ -405,14 +414,16 @@ class PluginManager:
|
|
|
405
414
|
else os.path.join(self.reserved_plugin_path, root_dir_name)
|
|
406
415
|
)
|
|
407
416
|
plugin_schema_path = os.path.join(
|
|
408
|
-
plugin_dir_path,
|
|
417
|
+
plugin_dir_path,
|
|
418
|
+
self.conf_schema_fname,
|
|
409
419
|
)
|
|
410
420
|
if os.path.exists(plugin_schema_path):
|
|
411
421
|
# 加载插件配置
|
|
412
422
|
with open(plugin_schema_path, encoding="utf-8") as f:
|
|
413
423
|
plugin_config = AstrBotConfig(
|
|
414
424
|
config_path=os.path.join(
|
|
415
|
-
self.plugin_config_path,
|
|
425
|
+
self.plugin_config_path,
|
|
426
|
+
f"{root_dir_name}_config.json",
|
|
416
427
|
),
|
|
417
428
|
schema=json.loads(f.read()),
|
|
418
429
|
)
|
|
@@ -425,7 +436,7 @@ class PluginManager:
|
|
|
425
436
|
try:
|
|
426
437
|
# yaml 文件的元数据优先
|
|
427
438
|
metadata_yaml = self._load_plugin_metadata(
|
|
428
|
-
plugin_path=plugin_dir_path
|
|
439
|
+
plugin_path=plugin_dir_path,
|
|
429
440
|
)
|
|
430
441
|
if metadata_yaml:
|
|
431
442
|
metadata.name = metadata_yaml.name
|
|
@@ -436,7 +447,7 @@ class PluginManager:
|
|
|
436
447
|
metadata.display_name = metadata_yaml.display_name
|
|
437
448
|
except Exception as e:
|
|
438
449
|
logger.warning(
|
|
439
|
-
f"插件 {root_dir_name} 元数据载入失败: {
|
|
450
|
+
f"插件 {root_dir_name} 元数据载入失败: {e!s}。使用默认元数据。",
|
|
440
451
|
)
|
|
441
452
|
logger.info(metadata)
|
|
442
453
|
metadata.config = plugin_config
|
|
@@ -445,15 +456,16 @@ class PluginManager:
|
|
|
445
456
|
if plugin_config and metadata.star_cls_type:
|
|
446
457
|
try:
|
|
447
458
|
metadata.star_cls = metadata.star_cls_type(
|
|
448
|
-
context=self.context,
|
|
459
|
+
context=self.context,
|
|
460
|
+
config=plugin_config,
|
|
449
461
|
)
|
|
450
462
|
except TypeError as _:
|
|
451
463
|
metadata.star_cls = metadata.star_cls_type(
|
|
452
|
-
context=self.context
|
|
464
|
+
context=self.context,
|
|
453
465
|
)
|
|
454
466
|
elif metadata.star_cls_type:
|
|
455
467
|
metadata.star_cls = metadata.star_cls_type(
|
|
456
|
-
context=self.context
|
|
468
|
+
context=self.context,
|
|
457
469
|
)
|
|
458
470
|
else:
|
|
459
471
|
logger.info(f"插件 {metadata.name} 已被禁用。")
|
|
@@ -469,7 +481,7 @@ class PluginManager:
|
|
|
469
481
|
# 绑定 handler
|
|
470
482
|
related_handlers = (
|
|
471
483
|
star_handlers_registry.get_handlers_by_module_name(
|
|
472
|
-
metadata.module_path
|
|
484
|
+
metadata.module_path,
|
|
473
485
|
)
|
|
474
486
|
)
|
|
475
487
|
for handler in related_handlers:
|
|
@@ -505,7 +517,7 @@ class PluginManager:
|
|
|
505
517
|
else:
|
|
506
518
|
# v3.4.0 以前的方式注册插件
|
|
507
519
|
logger.debug(
|
|
508
|
-
f"插件 {path} 未通过装饰器注册。尝试通过旧版本方式载入。"
|
|
520
|
+
f"插件 {path} 未通过装饰器注册。尝试通过旧版本方式载入。",
|
|
509
521
|
)
|
|
510
522
|
classes = self._get_classes(module)
|
|
511
523
|
|
|
@@ -514,19 +526,21 @@ class PluginManager:
|
|
|
514
526
|
if plugin_config:
|
|
515
527
|
try:
|
|
516
528
|
obj = getattr(module, classes[0])(
|
|
517
|
-
context=self.context,
|
|
529
|
+
context=self.context,
|
|
530
|
+
config=plugin_config,
|
|
518
531
|
) # 实例化插件类
|
|
519
532
|
except TypeError as _:
|
|
520
533
|
obj = getattr(module, classes[0])(
|
|
521
|
-
context=self.context
|
|
534
|
+
context=self.context,
|
|
522
535
|
) # 实例化插件类
|
|
523
536
|
else:
|
|
524
537
|
obj = getattr(module, classes[0])(
|
|
525
|
-
context=self.context
|
|
538
|
+
context=self.context,
|
|
526
539
|
) # 实例化插件类
|
|
527
540
|
|
|
528
541
|
metadata = self._load_plugin_metadata(
|
|
529
|
-
plugin_path=plugin_dir_path,
|
|
542
|
+
plugin_path=plugin_dir_path,
|
|
543
|
+
plugin_obj=obj,
|
|
530
544
|
)
|
|
531
545
|
if not metadata:
|
|
532
546
|
raise Exception(f"无法找到插件 {plugin_dir_path} 的元数据。")
|
|
@@ -552,7 +566,7 @@ class PluginManager:
|
|
|
552
566
|
|
|
553
567
|
full_names = []
|
|
554
568
|
for handler in star_handlers_registry.get_handlers_by_module_name(
|
|
555
|
-
metadata.module_path
|
|
569
|
+
metadata.module_path,
|
|
556
570
|
):
|
|
557
571
|
full_names.append(handler.handler_full_name)
|
|
558
572
|
|
|
@@ -562,7 +576,8 @@ class PluginManager:
|
|
|
562
576
|
and handler.handler_name in alter_cmd[metadata.name]
|
|
563
577
|
):
|
|
564
578
|
cmd_type = alter_cmd[metadata.name][handler.handler_name].get(
|
|
565
|
-
"permission",
|
|
579
|
+
"permission",
|
|
580
|
+
"member",
|
|
566
581
|
)
|
|
567
582
|
found_permission_filter = False
|
|
568
583
|
for filter_ in handler.event_filters:
|
|
@@ -578,12 +593,12 @@ class PluginManager:
|
|
|
578
593
|
PermissionTypeFilter(
|
|
579
594
|
PermissionType.ADMIN
|
|
580
595
|
if cmd_type == "admin"
|
|
581
|
-
else PermissionType.MEMBER
|
|
582
|
-
)
|
|
596
|
+
else PermissionType.MEMBER,
|
|
597
|
+
),
|
|
583
598
|
)
|
|
584
599
|
|
|
585
600
|
logger.debug(
|
|
586
|
-
f"插入权限过滤器 {cmd_type} 到 {metadata.name} 的 {handler.handler_name} 方法。"
|
|
601
|
+
f"插入权限过滤器 {cmd_type} 到 {metadata.name} 的 {handler.handler_name} 方法。",
|
|
587
602
|
)
|
|
588
603
|
|
|
589
604
|
metadata.star_handler_full_names = full_names
|
|
@@ -598,7 +613,7 @@ class PluginManager:
|
|
|
598
613
|
for line in errors.split("\n"):
|
|
599
614
|
logger.error(f"| {line}")
|
|
600
615
|
logger.error("----------------------------------")
|
|
601
|
-
fail_rec += f"加载 {root_dir_name} 插件时出现问题,原因 {
|
|
616
|
+
fail_rec += f"加载 {root_dir_name} 插件时出现问题,原因 {e!s}。\n"
|
|
602
617
|
|
|
603
618
|
# 清除 pip.main 导致的多余的 logging handlers
|
|
604
619
|
for handler in logging.root.handlers[:]:
|
|
@@ -606,9 +621,8 @@ class PluginManager:
|
|
|
606
621
|
|
|
607
622
|
if not fail_rec:
|
|
608
623
|
return True, None
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
return False, fail_rec
|
|
624
|
+
self.failed_plugin_info = fail_rec
|
|
625
|
+
return False, fail_rec
|
|
612
626
|
|
|
613
627
|
async def install_plugin(self, repo_url: str, proxy=""):
|
|
614
628
|
"""从仓库 URL 安装插件
|
|
@@ -624,6 +638,7 @@ class PluginManager:
|
|
|
624
638
|
- repo: 插件的仓库 URL
|
|
625
639
|
- readme: README.md 文件的内容(如果存在)
|
|
626
640
|
如果找不到插件元数据则返回 None。
|
|
641
|
+
|
|
627
642
|
"""
|
|
628
643
|
async with self._pm_lock:
|
|
629
644
|
plugin_path = await self.updator.install(repo_url, proxy)
|
|
@@ -652,7 +667,7 @@ class PluginManager:
|
|
|
652
667
|
readme_content = f.read()
|
|
653
668
|
except Exception as e:
|
|
654
669
|
logger.warning(
|
|
655
|
-
f"读取插件 {dir_name} 的 README.md 文件失败: {
|
|
670
|
+
f"读取插件 {dir_name} 的 README.md 文件失败: {e!s}",
|
|
656
671
|
)
|
|
657
672
|
|
|
658
673
|
plugin_info = None
|
|
@@ -665,14 +680,22 @@ class PluginManager:
|
|
|
665
680
|
|
|
666
681
|
return plugin_info
|
|
667
682
|
|
|
668
|
-
async def uninstall_plugin(
|
|
683
|
+
async def uninstall_plugin(
|
|
684
|
+
self,
|
|
685
|
+
plugin_name: str,
|
|
686
|
+
delete_config: bool = False,
|
|
687
|
+
delete_data: bool = False,
|
|
688
|
+
):
|
|
669
689
|
"""卸载指定的插件。
|
|
670
690
|
|
|
671
691
|
Args:
|
|
672
692
|
plugin_name (str): 要卸载的插件名称
|
|
693
|
+
delete_config (bool): 是否删除插件配置文件,默认为 False
|
|
694
|
+
delete_data (bool): 是否删除插件数据,默认为 False
|
|
673
695
|
|
|
674
696
|
Raises:
|
|
675
697
|
Exception: 当插件不存在、是保留插件时,或删除插件文件夹失败时抛出异常
|
|
698
|
+
|
|
676
699
|
"""
|
|
677
700
|
async with self._pm_lock:
|
|
678
701
|
plugin = self.context.get_registered_star(plugin_name)
|
|
@@ -689,7 +712,7 @@ class PluginManager:
|
|
|
689
712
|
except Exception as e:
|
|
690
713
|
logger.warning(traceback.format_exc())
|
|
691
714
|
logger.warning(
|
|
692
|
-
f"插件 {plugin_name} 未被正常终止 {
|
|
715
|
+
f"插件 {plugin_name} 未被正常终止 {e!s}, 可能会导致资源泄露等问题。",
|
|
693
716
|
)
|
|
694
717
|
|
|
695
718
|
# 从 star_registry 和 star_map 中删除
|
|
@@ -698,12 +721,58 @@ class PluginManager:
|
|
|
698
721
|
|
|
699
722
|
await self._unbind_plugin(plugin_name, plugin.module_path)
|
|
700
723
|
|
|
724
|
+
# 删除插件文件夹
|
|
701
725
|
try:
|
|
702
726
|
remove_dir(os.path.join(ppath, root_dir_name))
|
|
703
727
|
except Exception as e:
|
|
704
728
|
raise Exception(
|
|
705
|
-
f"移除插件成功,但是删除插件文件夹失败: {
|
|
729
|
+
f"移除插件成功,但是删除插件文件夹失败: {e!s}。您可以手动删除该文件夹,位于 addons/plugins/ 下。",
|
|
730
|
+
)
|
|
731
|
+
|
|
732
|
+
# 删除插件配置文件
|
|
733
|
+
if delete_config and root_dir_name:
|
|
734
|
+
config_file = os.path.join(
|
|
735
|
+
self.plugin_config_path,
|
|
736
|
+
f"{root_dir_name}_config.json",
|
|
737
|
+
)
|
|
738
|
+
if os.path.exists(config_file):
|
|
739
|
+
try:
|
|
740
|
+
os.remove(config_file)
|
|
741
|
+
logger.info(f"已删除插件 {plugin_name} 的配置文件")
|
|
742
|
+
except Exception as e:
|
|
743
|
+
logger.warning(f"删除插件配置文件失败: {e!s}")
|
|
744
|
+
|
|
745
|
+
# 删除插件持久化数据
|
|
746
|
+
# 注意:需要检查两个可能的目录名(plugin_data 和 plugins_data)
|
|
747
|
+
# data/temp 目录可能被多个插件共享,不自动删除以防误删
|
|
748
|
+
if delete_data and root_dir_name:
|
|
749
|
+
data_base_dir = os.path.dirname(ppath) # data/
|
|
750
|
+
|
|
751
|
+
# 删除 data/plugin_data 下的插件持久化数据(单数形式,新版本)
|
|
752
|
+
plugin_data_dir = os.path.join(
|
|
753
|
+
data_base_dir, "plugin_data", root_dir_name
|
|
706
754
|
)
|
|
755
|
+
if os.path.exists(plugin_data_dir):
|
|
756
|
+
try:
|
|
757
|
+
remove_dir(plugin_data_dir)
|
|
758
|
+
logger.info(
|
|
759
|
+
f"已删除插件 {plugin_name} 的持久化数据 (plugin_data)"
|
|
760
|
+
)
|
|
761
|
+
except Exception as e:
|
|
762
|
+
logger.warning(f"删除插件持久化数据失败 (plugin_data): {e!s}")
|
|
763
|
+
|
|
764
|
+
# 删除 data/plugins_data 下的插件持久化数据(复数形式,旧版本兼容)
|
|
765
|
+
plugins_data_dir = os.path.join(
|
|
766
|
+
data_base_dir, "plugins_data", root_dir_name
|
|
767
|
+
)
|
|
768
|
+
if os.path.exists(plugins_data_dir):
|
|
769
|
+
try:
|
|
770
|
+
remove_dir(plugins_data_dir)
|
|
771
|
+
logger.info(
|
|
772
|
+
f"已删除插件 {plugin_name} 的持久化数据 (plugins_data)"
|
|
773
|
+
)
|
|
774
|
+
except Exception as e:
|
|
775
|
+
logger.warning(f"删除插件持久化数据失败 (plugins_data): {e!s}")
|
|
707
776
|
|
|
708
777
|
async def _unbind_plugin(self, plugin_name: str, plugin_module_path: str):
|
|
709
778
|
"""解绑并移除一个插件。
|
|
@@ -711,6 +780,7 @@ class PluginManager:
|
|
|
711
780
|
Args:
|
|
712
781
|
plugin_name: 要解绑的插件名称
|
|
713
782
|
plugin_module_path: 插件的完整模块路径
|
|
783
|
+
|
|
714
784
|
"""
|
|
715
785
|
plugin = None
|
|
716
786
|
del star_map[plugin_module_path]
|
|
@@ -720,10 +790,10 @@ class PluginManager:
|
|
|
720
790
|
del star_registry[i]
|
|
721
791
|
break
|
|
722
792
|
for handler in star_handlers_registry.get_handlers_by_module_name(
|
|
723
|
-
plugin_module_path
|
|
793
|
+
plugin_module_path,
|
|
724
794
|
):
|
|
725
795
|
logger.info(
|
|
726
|
-
f"移除了插件 {plugin_name} 的处理函数 {handler.handler_name} ({len(star_handlers_registry)})"
|
|
796
|
+
f"移除了插件 {plugin_name} 的处理函数 {handler.handler_name} ({len(star_handlers_registry)})",
|
|
727
797
|
)
|
|
728
798
|
star_handlers_registry.remove(handler)
|
|
729
799
|
|
|
@@ -734,11 +804,25 @@ class PluginManager:
|
|
|
734
804
|
]:
|
|
735
805
|
del star_handlers_registry.star_handlers_map[k]
|
|
736
806
|
|
|
807
|
+
# llm_tools 中移除该插件的工具函数绑定
|
|
808
|
+
to_remove = []
|
|
809
|
+
for func_tool in llm_tools.func_list:
|
|
810
|
+
mp = func_tool.handler_module_path
|
|
811
|
+
if (
|
|
812
|
+
mp
|
|
813
|
+
and mp.startswith(plugin_module_path)
|
|
814
|
+
and not mp.endswith(("packages", "data.plugins"))
|
|
815
|
+
):
|
|
816
|
+
to_remove.append(func_tool)
|
|
817
|
+
for func_tool in to_remove:
|
|
818
|
+
llm_tools.func_list.remove(func_tool)
|
|
819
|
+
|
|
737
820
|
if plugin is None:
|
|
738
821
|
return
|
|
739
822
|
|
|
740
823
|
self._purge_modules(
|
|
741
|
-
root_dir_name=plugin.root_dir_name,
|
|
824
|
+
root_dir_name=plugin.root_dir_name,
|
|
825
|
+
is_reserved=plugin.reserved,
|
|
742
826
|
)
|
|
743
827
|
|
|
744
828
|
async def update_plugin(self, plugin_name: str, proxy=""):
|
|
@@ -753,8 +837,7 @@ class PluginManager:
|
|
|
753
837
|
await self.reload(plugin_name)
|
|
754
838
|
|
|
755
839
|
async def turn_off_plugin(self, plugin_name: str):
|
|
756
|
-
"""
|
|
757
|
-
禁用一个插件。
|
|
840
|
+
"""禁用一个插件。
|
|
758
841
|
调用插件的 terminate() 方法,
|
|
759
842
|
将插件的 module_path 加入到 data/shared_preferences.json 的 inactivated_plugins 列表中。
|
|
760
843
|
并且同时将插件启用的 llm_tool 禁用。
|
|
@@ -773,12 +856,18 @@ class PluginManager:
|
|
|
773
856
|
inactivated_plugins.append(plugin.module_path)
|
|
774
857
|
|
|
775
858
|
inactivated_llm_tools: list = list(
|
|
776
|
-
set(await sp.global_get("inactivated_llm_tools", []))
|
|
859
|
+
set(await sp.global_get("inactivated_llm_tools", [])),
|
|
777
860
|
) # 后向兼容
|
|
778
861
|
|
|
779
862
|
# 禁用插件启用的 llm_tool
|
|
780
863
|
for func_tool in llm_tools.func_list:
|
|
781
|
-
|
|
864
|
+
mp = func_tool.handler_module_path
|
|
865
|
+
if (
|
|
866
|
+
plugin.module_path
|
|
867
|
+
and mp
|
|
868
|
+
and plugin.module_path.startswith(mp)
|
|
869
|
+
and not mp.endswith(("packages", "data.plugins"))
|
|
870
|
+
):
|
|
782
871
|
func_tool.active = False
|
|
783
872
|
if func_tool.name not in inactivated_llm_tools:
|
|
784
873
|
inactivated_llm_tools.append(func_tool.name)
|
|
@@ -803,7 +892,8 @@ class PluginManager:
|
|
|
803
892
|
|
|
804
893
|
if "__del__" in star_metadata.star_cls_type.__dict__:
|
|
805
894
|
asyncio.get_event_loop().run_in_executor(
|
|
806
|
-
None,
|
|
895
|
+
None,
|
|
896
|
+
star_metadata.star_cls.__del__,
|
|
807
897
|
)
|
|
808
898
|
elif "terminate" in star_metadata.star_cls_type.__dict__:
|
|
809
899
|
await star_metadata.star_cls.terminate()
|
|
@@ -820,8 +910,12 @@ class PluginManager:
|
|
|
820
910
|
|
|
821
911
|
# 启用插件启用的 llm_tool
|
|
822
912
|
for func_tool in llm_tools.func_list:
|
|
913
|
+
mp = func_tool.handler_module_path
|
|
823
914
|
if (
|
|
824
|
-
|
|
915
|
+
plugin.module_path
|
|
916
|
+
and mp
|
|
917
|
+
and plugin.module_path.startswith(mp)
|
|
918
|
+
and not mp.endswith(("packages", "data.plugins"))
|
|
825
919
|
and func_tool.name in inactivated_llm_tools
|
|
826
920
|
):
|
|
827
921
|
inactivated_llm_tools.remove(func_tool.name)
|
|
@@ -830,8 +924,6 @@ class PluginManager:
|
|
|
830
924
|
|
|
831
925
|
await self.reload(plugin_name)
|
|
832
926
|
|
|
833
|
-
# plugin.activated = True
|
|
834
|
-
|
|
835
927
|
async def install_plugin_from_file(self, zip_file_path: str):
|
|
836
928
|
dir_name = os.path.basename(zip_file_path).replace(".zip", "")
|
|
837
929
|
dir_name = dir_name.removesuffix("-master").removesuffix("-main").lower()
|
|
@@ -842,7 +934,7 @@ class PluginManager:
|
|
|
842
934
|
try:
|
|
843
935
|
os.remove(zip_file_path)
|
|
844
936
|
except BaseException as e:
|
|
845
|
-
logger.warning(f"删除插件压缩包失败: {
|
|
937
|
+
logger.warning(f"删除插件压缩包失败: {e!s}")
|
|
846
938
|
# await self.reload()
|
|
847
939
|
await self.load(specified_dir_name=dir_name)
|
|
848
940
|
|
|
@@ -866,7 +958,7 @@ class PluginManager:
|
|
|
866
958
|
with open(readme_path, encoding="utf-8") as f:
|
|
867
959
|
readme_content = f.read()
|
|
868
960
|
except Exception as e:
|
|
869
|
-
logger.warning(f"读取插件 {dir_name} 的 README.md 文件失败: {
|
|
961
|
+
logger.warning(f"读取插件 {dir_name} 的 README.md 文件失败: {e!s}")
|
|
870
962
|
|
|
871
963
|
plugin_info = None
|
|
872
964
|
if plugin:
|