AstrBot 3.5.6__py3-none-any.whl → 4.7.0__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.
Files changed (288) hide show
  1. astrbot/api/__init__.py +16 -4
  2. astrbot/api/all.py +2 -1
  3. astrbot/api/event/__init__.py +5 -6
  4. astrbot/api/event/filter/__init__.py +37 -34
  5. astrbot/api/platform/__init__.py +7 -8
  6. astrbot/api/provider/__init__.py +8 -7
  7. astrbot/api/star/__init__.py +3 -4
  8. astrbot/api/util/__init__.py +2 -2
  9. astrbot/cli/__init__.py +1 -0
  10. astrbot/cli/__main__.py +18 -197
  11. astrbot/cli/commands/__init__.py +6 -0
  12. astrbot/cli/commands/cmd_conf.py +209 -0
  13. astrbot/cli/commands/cmd_init.py +56 -0
  14. astrbot/cli/commands/cmd_plug.py +245 -0
  15. astrbot/cli/commands/cmd_run.py +62 -0
  16. astrbot/cli/utils/__init__.py +18 -0
  17. astrbot/cli/utils/basic.py +76 -0
  18. astrbot/cli/utils/plugin.py +246 -0
  19. astrbot/cli/utils/version_comparator.py +90 -0
  20. astrbot/core/__init__.py +17 -19
  21. astrbot/core/agent/agent.py +14 -0
  22. astrbot/core/agent/handoff.py +38 -0
  23. astrbot/core/agent/hooks.py +30 -0
  24. astrbot/core/agent/mcp_client.py +385 -0
  25. astrbot/core/agent/message.py +175 -0
  26. astrbot/core/agent/response.py +14 -0
  27. astrbot/core/agent/run_context.py +22 -0
  28. astrbot/core/agent/runners/__init__.py +3 -0
  29. astrbot/core/agent/runners/base.py +65 -0
  30. astrbot/core/agent/runners/coze/coze_agent_runner.py +367 -0
  31. astrbot/core/agent/runners/coze/coze_api_client.py +324 -0
  32. astrbot/core/agent/runners/dashscope/dashscope_agent_runner.py +403 -0
  33. astrbot/core/agent/runners/dify/dify_agent_runner.py +336 -0
  34. astrbot/core/agent/runners/dify/dify_api_client.py +195 -0
  35. astrbot/core/agent/runners/tool_loop_agent_runner.py +400 -0
  36. astrbot/core/agent/tool.py +285 -0
  37. astrbot/core/agent/tool_executor.py +17 -0
  38. astrbot/core/astr_agent_context.py +19 -0
  39. astrbot/core/astr_agent_hooks.py +36 -0
  40. astrbot/core/astr_agent_run_util.py +80 -0
  41. astrbot/core/astr_agent_tool_exec.py +246 -0
  42. astrbot/core/astrbot_config_mgr.py +275 -0
  43. astrbot/core/config/__init__.py +2 -2
  44. astrbot/core/config/astrbot_config.py +60 -20
  45. astrbot/core/config/default.py +1972 -453
  46. astrbot/core/config/i18n_utils.py +110 -0
  47. astrbot/core/conversation_mgr.py +285 -75
  48. astrbot/core/core_lifecycle.py +167 -62
  49. astrbot/core/db/__init__.py +305 -102
  50. astrbot/core/db/migration/helper.py +69 -0
  51. astrbot/core/db/migration/migra_3_to_4.py +357 -0
  52. astrbot/core/db/migration/migra_45_to_46.py +44 -0
  53. astrbot/core/db/migration/migra_webchat_session.py +131 -0
  54. astrbot/core/db/migration/shared_preferences_v3.py +48 -0
  55. astrbot/core/db/migration/sqlite_v3.py +497 -0
  56. astrbot/core/db/po.py +259 -55
  57. astrbot/core/db/sqlite.py +773 -528
  58. astrbot/core/db/vec_db/base.py +73 -0
  59. astrbot/core/db/vec_db/faiss_impl/__init__.py +3 -0
  60. astrbot/core/db/vec_db/faiss_impl/document_storage.py +392 -0
  61. astrbot/core/db/vec_db/faiss_impl/embedding_storage.py +93 -0
  62. astrbot/core/db/vec_db/faiss_impl/sqlite_init.sql +17 -0
  63. astrbot/core/db/vec_db/faiss_impl/vec_db.py +204 -0
  64. astrbot/core/event_bus.py +26 -22
  65. astrbot/core/exceptions.py +9 -0
  66. astrbot/core/file_token_service.py +98 -0
  67. astrbot/core/initial_loader.py +19 -10
  68. astrbot/core/knowledge_base/chunking/__init__.py +9 -0
  69. astrbot/core/knowledge_base/chunking/base.py +25 -0
  70. astrbot/core/knowledge_base/chunking/fixed_size.py +59 -0
  71. astrbot/core/knowledge_base/chunking/recursive.py +161 -0
  72. astrbot/core/knowledge_base/kb_db_sqlite.py +301 -0
  73. astrbot/core/knowledge_base/kb_helper.py +642 -0
  74. astrbot/core/knowledge_base/kb_mgr.py +330 -0
  75. astrbot/core/knowledge_base/models.py +120 -0
  76. astrbot/core/knowledge_base/parsers/__init__.py +13 -0
  77. astrbot/core/knowledge_base/parsers/base.py +51 -0
  78. astrbot/core/knowledge_base/parsers/markitdown_parser.py +26 -0
  79. astrbot/core/knowledge_base/parsers/pdf_parser.py +101 -0
  80. astrbot/core/knowledge_base/parsers/text_parser.py +42 -0
  81. astrbot/core/knowledge_base/parsers/url_parser.py +103 -0
  82. astrbot/core/knowledge_base/parsers/util.py +13 -0
  83. astrbot/core/knowledge_base/prompts.py +65 -0
  84. astrbot/core/knowledge_base/retrieval/__init__.py +14 -0
  85. astrbot/core/knowledge_base/retrieval/hit_stopwords.txt +767 -0
  86. astrbot/core/knowledge_base/retrieval/manager.py +276 -0
  87. astrbot/core/knowledge_base/retrieval/rank_fusion.py +142 -0
  88. astrbot/core/knowledge_base/retrieval/sparse_retriever.py +136 -0
  89. astrbot/core/log.py +21 -15
  90. astrbot/core/message/components.py +413 -287
  91. astrbot/core/message/message_event_result.py +35 -24
  92. astrbot/core/persona_mgr.py +192 -0
  93. astrbot/core/pipeline/__init__.py +14 -14
  94. astrbot/core/pipeline/content_safety_check/stage.py +13 -9
  95. astrbot/core/pipeline/content_safety_check/strategies/__init__.py +1 -2
  96. astrbot/core/pipeline/content_safety_check/strategies/baidu_aip.py +13 -14
  97. astrbot/core/pipeline/content_safety_check/strategies/keywords.py +2 -1
  98. astrbot/core/pipeline/content_safety_check/strategies/strategy.py +6 -6
  99. astrbot/core/pipeline/context.py +7 -1
  100. astrbot/core/pipeline/context_utils.py +107 -0
  101. astrbot/core/pipeline/preprocess_stage/stage.py +63 -36
  102. astrbot/core/pipeline/process_stage/method/agent_request.py +48 -0
  103. astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py +464 -0
  104. astrbot/core/pipeline/process_stage/method/agent_sub_stages/third_party.py +202 -0
  105. astrbot/core/pipeline/process_stage/method/star_request.py +26 -32
  106. astrbot/core/pipeline/process_stage/stage.py +21 -15
  107. astrbot/core/pipeline/process_stage/utils.py +125 -0
  108. astrbot/core/pipeline/rate_limit_check/stage.py +34 -36
  109. astrbot/core/pipeline/respond/stage.py +142 -101
  110. astrbot/core/pipeline/result_decorate/stage.py +124 -57
  111. astrbot/core/pipeline/scheduler.py +21 -16
  112. astrbot/core/pipeline/session_status_check/stage.py +37 -0
  113. astrbot/core/pipeline/stage.py +11 -76
  114. astrbot/core/pipeline/waking_check/stage.py +69 -33
  115. astrbot/core/pipeline/whitelist_check/stage.py +10 -7
  116. astrbot/core/platform/__init__.py +6 -6
  117. astrbot/core/platform/astr_message_event.py +107 -129
  118. astrbot/core/platform/astrbot_message.py +32 -12
  119. astrbot/core/platform/manager.py +62 -18
  120. astrbot/core/platform/message_session.py +30 -0
  121. astrbot/core/platform/platform.py +16 -24
  122. astrbot/core/platform/platform_metadata.py +9 -4
  123. astrbot/core/platform/register.py +12 -7
  124. astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py +136 -60
  125. astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py +126 -46
  126. astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py +63 -31
  127. astrbot/core/platform/sources/dingtalk/dingtalk_event.py +30 -26
  128. astrbot/core/platform/sources/discord/client.py +129 -0
  129. astrbot/core/platform/sources/discord/components.py +139 -0
  130. astrbot/core/platform/sources/discord/discord_platform_adapter.py +473 -0
  131. astrbot/core/platform/sources/discord/discord_platform_event.py +313 -0
  132. astrbot/core/platform/sources/lark/lark_adapter.py +27 -18
  133. astrbot/core/platform/sources/lark/lark_event.py +39 -13
  134. astrbot/core/platform/sources/misskey/misskey_adapter.py +770 -0
  135. astrbot/core/platform/sources/misskey/misskey_api.py +964 -0
  136. astrbot/core/platform/sources/misskey/misskey_event.py +163 -0
  137. astrbot/core/platform/sources/misskey/misskey_utils.py +550 -0
  138. astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py +149 -33
  139. astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py +41 -26
  140. astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_adapter.py +36 -17
  141. astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_event.py +3 -1
  142. astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_server.py +14 -8
  143. astrbot/core/platform/sources/satori/satori_adapter.py +792 -0
  144. astrbot/core/platform/sources/satori/satori_event.py +432 -0
  145. astrbot/core/platform/sources/slack/client.py +164 -0
  146. astrbot/core/platform/sources/slack/slack_adapter.py +416 -0
  147. astrbot/core/platform/sources/slack/slack_event.py +253 -0
  148. astrbot/core/platform/sources/telegram/tg_adapter.py +100 -43
  149. astrbot/core/platform/sources/telegram/tg_event.py +136 -36
  150. astrbot/core/platform/sources/webchat/webchat_adapter.py +72 -22
  151. astrbot/core/platform/sources/webchat/webchat_event.py +46 -22
  152. astrbot/core/platform/sources/webchat/webchat_queue_mgr.py +35 -0
  153. astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py +926 -0
  154. astrbot/core/platform/sources/wechatpadpro/wechatpadpro_message_event.py +178 -0
  155. astrbot/core/platform/sources/wechatpadpro/xml_data_parser.py +159 -0
  156. astrbot/core/platform/sources/wecom/wecom_adapter.py +169 -27
  157. astrbot/core/platform/sources/wecom/wecom_event.py +162 -77
  158. astrbot/core/platform/sources/wecom/wecom_kf.py +279 -0
  159. astrbot/core/platform/sources/wecom/wecom_kf_message.py +196 -0
  160. astrbot/core/platform/sources/wecom_ai_bot/WXBizJsonMsgCrypt.py +297 -0
  161. astrbot/core/platform/sources/wecom_ai_bot/__init__.py +15 -0
  162. astrbot/core/platform/sources/wecom_ai_bot/ierror.py +19 -0
  163. astrbot/core/platform/sources/wecom_ai_bot/wecomai_adapter.py +472 -0
  164. astrbot/core/platform/sources/wecom_ai_bot/wecomai_api.py +417 -0
  165. astrbot/core/platform/sources/wecom_ai_bot/wecomai_event.py +152 -0
  166. astrbot/core/platform/sources/wecom_ai_bot/wecomai_queue_mgr.py +153 -0
  167. astrbot/core/platform/sources/wecom_ai_bot/wecomai_server.py +168 -0
  168. astrbot/core/platform/sources/wecom_ai_bot/wecomai_utils.py +209 -0
  169. astrbot/core/platform/sources/weixin_official_account/weixin_offacc_adapter.py +306 -0
  170. astrbot/core/platform/sources/weixin_official_account/weixin_offacc_event.py +186 -0
  171. astrbot/core/platform_message_history_mgr.py +49 -0
  172. astrbot/core/provider/__init__.py +2 -3
  173. astrbot/core/provider/entites.py +8 -8
  174. astrbot/core/provider/entities.py +154 -98
  175. astrbot/core/provider/func_tool_manager.py +446 -458
  176. astrbot/core/provider/manager.py +345 -207
  177. astrbot/core/provider/provider.py +188 -73
  178. astrbot/core/provider/register.py +9 -7
  179. astrbot/core/provider/sources/anthropic_source.py +295 -115
  180. astrbot/core/provider/sources/azure_tts_source.py +224 -0
  181. astrbot/core/provider/sources/bailian_rerank_source.py +236 -0
  182. astrbot/core/provider/sources/dashscope_tts.py +138 -14
  183. astrbot/core/provider/sources/edge_tts_source.py +24 -19
  184. astrbot/core/provider/sources/fishaudio_tts_api_source.py +58 -13
  185. astrbot/core/provider/sources/gemini_embedding_source.py +61 -0
  186. astrbot/core/provider/sources/gemini_source.py +310 -132
  187. astrbot/core/provider/sources/gemini_tts_source.py +81 -0
  188. astrbot/core/provider/sources/groq_source.py +15 -0
  189. astrbot/core/provider/sources/gsv_selfhosted_source.py +151 -0
  190. astrbot/core/provider/sources/gsvi_tts_source.py +14 -7
  191. astrbot/core/provider/sources/minimax_tts_api_source.py +159 -0
  192. astrbot/core/provider/sources/openai_embedding_source.py +40 -0
  193. astrbot/core/provider/sources/openai_source.py +241 -145
  194. astrbot/core/provider/sources/openai_tts_api_source.py +18 -7
  195. astrbot/core/provider/sources/sensevoice_selfhosted_source.py +13 -11
  196. astrbot/core/provider/sources/vllm_rerank_source.py +71 -0
  197. astrbot/core/provider/sources/volcengine_tts.py +115 -0
  198. astrbot/core/provider/sources/whisper_api_source.py +18 -13
  199. astrbot/core/provider/sources/whisper_selfhosted_source.py +19 -12
  200. astrbot/core/provider/sources/xinference_rerank_source.py +116 -0
  201. astrbot/core/provider/sources/xinference_stt_provider.py +197 -0
  202. astrbot/core/provider/sources/zhipu_source.py +6 -73
  203. astrbot/core/star/__init__.py +43 -11
  204. astrbot/core/star/config.py +17 -18
  205. astrbot/core/star/context.py +362 -138
  206. astrbot/core/star/filter/__init__.py +4 -3
  207. astrbot/core/star/filter/command.py +111 -35
  208. astrbot/core/star/filter/command_group.py +46 -34
  209. astrbot/core/star/filter/custom_filter.py +6 -5
  210. astrbot/core/star/filter/event_message_type.py +4 -2
  211. astrbot/core/star/filter/permission.py +4 -2
  212. astrbot/core/star/filter/platform_adapter_type.py +45 -12
  213. astrbot/core/star/filter/regex.py +4 -2
  214. astrbot/core/star/register/__init__.py +19 -15
  215. astrbot/core/star/register/star.py +41 -13
  216. astrbot/core/star/register/star_handler.py +236 -86
  217. astrbot/core/star/session_llm_manager.py +280 -0
  218. astrbot/core/star/session_plugin_manager.py +170 -0
  219. astrbot/core/star/star.py +36 -43
  220. astrbot/core/star/star_handler.py +47 -85
  221. astrbot/core/star/star_manager.py +442 -260
  222. astrbot/core/star/star_tools.py +167 -45
  223. astrbot/core/star/updator.py +17 -20
  224. astrbot/core/umop_config_router.py +106 -0
  225. astrbot/core/updator.py +38 -13
  226. astrbot/core/utils/astrbot_path.py +39 -0
  227. astrbot/core/utils/command_parser.py +1 -1
  228. astrbot/core/utils/io.py +119 -60
  229. astrbot/core/utils/log_pipe.py +1 -1
  230. astrbot/core/utils/metrics.py +11 -10
  231. astrbot/core/utils/migra_helper.py +73 -0
  232. astrbot/core/utils/path_util.py +63 -62
  233. astrbot/core/utils/pip_installer.py +37 -15
  234. astrbot/core/utils/session_lock.py +29 -0
  235. astrbot/core/utils/session_waiter.py +19 -20
  236. astrbot/core/utils/shared_preferences.py +174 -34
  237. astrbot/core/utils/t2i/__init__.py +4 -1
  238. astrbot/core/utils/t2i/local_strategy.py +386 -238
  239. astrbot/core/utils/t2i/network_strategy.py +109 -49
  240. astrbot/core/utils/t2i/renderer.py +29 -14
  241. astrbot/core/utils/t2i/template/astrbot_powershell.html +184 -0
  242. astrbot/core/utils/t2i/template_manager.py +111 -0
  243. astrbot/core/utils/tencent_record_helper.py +115 -1
  244. astrbot/core/utils/version_comparator.py +10 -13
  245. astrbot/core/zip_updator.py +112 -65
  246. astrbot/dashboard/routes/__init__.py +20 -13
  247. astrbot/dashboard/routes/auth.py +20 -9
  248. astrbot/dashboard/routes/chat.py +297 -141
  249. astrbot/dashboard/routes/config.py +652 -55
  250. astrbot/dashboard/routes/conversation.py +107 -37
  251. astrbot/dashboard/routes/file.py +26 -0
  252. astrbot/dashboard/routes/knowledge_base.py +1244 -0
  253. astrbot/dashboard/routes/log.py +27 -2
  254. astrbot/dashboard/routes/persona.py +202 -0
  255. astrbot/dashboard/routes/plugin.py +197 -139
  256. astrbot/dashboard/routes/route.py +27 -7
  257. astrbot/dashboard/routes/session_management.py +354 -0
  258. astrbot/dashboard/routes/stat.py +85 -18
  259. astrbot/dashboard/routes/static_file.py +5 -2
  260. astrbot/dashboard/routes/t2i.py +233 -0
  261. astrbot/dashboard/routes/tools.py +184 -120
  262. astrbot/dashboard/routes/update.py +59 -36
  263. astrbot/dashboard/server.py +96 -36
  264. astrbot/dashboard/utils.py +165 -0
  265. astrbot-4.7.0.dist-info/METADATA +294 -0
  266. astrbot-4.7.0.dist-info/RECORD +274 -0
  267. {astrbot-3.5.6.dist-info → astrbot-4.7.0.dist-info}/WHEEL +1 -1
  268. astrbot/core/db/plugin/sqlite_impl.py +0 -112
  269. astrbot/core/db/sqlite_init.sql +0 -50
  270. astrbot/core/pipeline/platform_compatibility/stage.py +0 -56
  271. astrbot/core/pipeline/process_stage/method/llm_request.py +0 -606
  272. astrbot/core/platform/sources/gewechat/client.py +0 -806
  273. astrbot/core/platform/sources/gewechat/downloader.py +0 -55
  274. astrbot/core/platform/sources/gewechat/gewechat_event.py +0 -255
  275. astrbot/core/platform/sources/gewechat/gewechat_platform_adapter.py +0 -103
  276. astrbot/core/platform/sources/gewechat/xml_data_parser.py +0 -110
  277. astrbot/core/provider/sources/dashscope_source.py +0 -203
  278. astrbot/core/provider/sources/dify_source.py +0 -281
  279. astrbot/core/provider/sources/llmtuner_source.py +0 -132
  280. astrbot/core/rag/embedding/openai_source.py +0 -20
  281. astrbot/core/rag/knowledge_db_mgr.py +0 -94
  282. astrbot/core/rag/store/__init__.py +0 -9
  283. astrbot/core/rag/store/chroma_db.py +0 -42
  284. astrbot/core/utils/dify_api_client.py +0 -152
  285. astrbot-3.5.6.dist-info/METADATA +0 -249
  286. astrbot-3.5.6.dist-info/RECORD +0 -158
  287. {astrbot-3.5.6.dist-info → astrbot-4.7.0.dist-info}/entry_points.txt +0 -0
  288. {astrbot-3.5.6.dist-info → astrbot-4.7.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,29 +1,39 @@
1
- """
2
- 插件的重载、启停、安装、卸载等操作。
3
- """
1
+ """插件的重载、启停、安装、卸载等操作。"""
4
2
 
5
- import inspect
3
+ import asyncio
6
4
  import functools
5
+ import inspect
6
+ import json
7
+ import logging
7
8
  import os
8
9
  import sys
9
- import json
10
10
  import traceback
11
- import yaml
12
- import logging
13
- import asyncio
14
11
  from types import ModuleType
15
- from typing import List
12
+
13
+ import yaml
14
+
15
+ from astrbot.core import logger, pip_installer, sp
16
+ from astrbot.core.agent.handoff import FunctionTool, HandoffTool
16
17
  from astrbot.core.config.astrbot_config import AstrBotConfig
17
- from astrbot.core import logger, sp, pip_installer
18
- from .context import Context
19
- from . import StarMetadata
20
- from .updator import PluginUpdator
18
+ from astrbot.core.provider.register import llm_tools
19
+ from astrbot.core.utils.astrbot_path import (
20
+ get_astrbot_config_path,
21
+ get_astrbot_plugin_path,
22
+ )
21
23
  from astrbot.core.utils.io import remove_dir
22
- from .star import star_registry, star_map
24
+
25
+ from . import StarMetadata
26
+ from .context import Context
27
+ from .filter.permission import PermissionType, PermissionTypeFilter
28
+ from .star import star_map, star_registry
23
29
  from .star_handler import star_handlers_registry
24
- from astrbot.core.provider.register import llm_tools
30
+ from .updator import PluginUpdator
25
31
 
26
- from .filter.permission import PermissionTypeFilter, PermissionType
32
+ try:
33
+ from watchfiles import PythonFilter, awatch
34
+ except ImportError:
35
+ if os.getenv("ASTRBOT_RELOAD", "0") == "1":
36
+ logger.warning("未安装 watchfiles,无法实现插件的热重载。")
27
37
 
28
38
 
29
39
  class PluginManager:
@@ -31,33 +41,84 @@ class PluginManager:
31
41
  self.updator = PluginUpdator()
32
42
 
33
43
  self.context = context
34
- self.context._star_manager = self
44
+ self.context._star_manager = self # type: ignore
35
45
 
36
46
  self.config = config
37
- self.plugin_store_path = os.path.abspath(
38
- os.path.join(
39
- os.path.dirname(os.path.abspath(__file__)), "../../../data/plugins"
40
- )
41
- )
47
+ self.plugin_store_path = get_astrbot_plugin_path()
42
48
  """存储插件的路径。即 data/plugins"""
43
- self.plugin_config_path = os.path.abspath(
44
- os.path.join(
45
- os.path.dirname(os.path.abspath(__file__)), "../../../data/config"
46
- )
47
- )
49
+ self.plugin_config_path = get_astrbot_config_path()
48
50
  """存储插件配置的路径。data/config"""
49
51
  self.reserved_plugin_path = os.path.abspath(
50
52
  os.path.join(
51
- os.path.dirname(os.path.abspath(__file__)), "../../../packages"
52
- )
53
+ os.path.dirname(os.path.abspath(__file__)),
54
+ "../../../packages",
55
+ ),
53
56
  )
54
57
  """保留插件的路径。在 packages 目录下"""
55
58
  self.conf_schema_fname = "_conf_schema.json"
59
+ self.logo_fname = "logo.png"
56
60
  """插件配置 Schema 文件名"""
61
+ self._pm_lock = asyncio.Lock()
62
+ """StarManager操作互斥锁"""
57
63
 
58
64
  self.failed_plugin_info = ""
65
+ if os.getenv("ASTRBOT_RELOAD", "0") == "1":
66
+ asyncio.create_task(self._watch_plugins_changes())
67
+
68
+ async def _watch_plugins_changes(self):
69
+ """监视插件文件变化"""
70
+ try:
71
+ async for changes in awatch(
72
+ self.plugin_store_path,
73
+ self.reserved_plugin_path,
74
+ watch_filter=PythonFilter(),
75
+ recursive=True,
76
+ ):
77
+ # 处理文件变化
78
+ await self._handle_file_changes(changes)
79
+ except asyncio.CancelledError:
80
+ pass
81
+ except Exception as e:
82
+ logger.error(f"插件热重载监视任务异常: {e!s}")
83
+ logger.error(traceback.format_exc())
84
+
85
+ async def _handle_file_changes(self, changes):
86
+ """处理文件变化"""
87
+ logger.info(f"检测到文件变化: {changes}")
88
+ plugins_to_check = []
89
+
90
+ for star in star_registry:
91
+ if not star.activated:
92
+ continue
93
+ if star.root_dir_name is None:
94
+ continue
95
+ if star.reserved:
96
+ plugin_dir_path = os.path.join(
97
+ self.reserved_plugin_path,
98
+ star.root_dir_name,
99
+ )
100
+ else:
101
+ plugin_dir_path = os.path.join(
102
+ self.plugin_store_path,
103
+ star.root_dir_name,
104
+ )
105
+ plugins_to_check.append((plugin_dir_path, star.name))
106
+ reloaded_plugins = set()
107
+ for change in changes:
108
+ _, file_path = change
109
+ for plugin_dir_path, plugin_name in plugins_to_check:
110
+ if (
111
+ os.path.commonpath([plugin_dir_path])
112
+ == os.path.commonpath([plugin_dir_path, file_path])
113
+ and plugin_name not in reloaded_plugins
114
+ ):
115
+ logger.info(f"检测到插件 {plugin_name} 文件变化,正在重载...")
116
+ await self.reload(plugin_name)
117
+ reloaded_plugins.add(plugin_name)
118
+ break
59
119
 
60
- def _get_classes(self, arg: ModuleType):
120
+ @staticmethod
121
+ def _get_classes(arg: ModuleType):
61
122
  """获取指定模块(可以理解为一个 python 文件)下所有的类"""
62
123
  classes = []
63
124
  clsmembers = inspect.getmembers(arg, inspect.isclass)
@@ -67,7 +128,8 @@ class PluginManager:
67
128
  break
68
129
  return classes
69
130
 
70
- def _get_modules(self, path):
131
+ @staticmethod
132
+ def _get_modules(path):
71
133
  modules = []
72
134
 
73
135
  dirs = os.listdir(path)
@@ -82,18 +144,18 @@ class PluginManager:
82
144
  logger.info(f"插件 {d} 未找到 main.py 或者 {d}.py,跳过。")
83
145
  continue
84
146
  if os.path.exists(os.path.join(path, d, "main.py")) or os.path.exists(
85
- os.path.join(path, d, d + ".py")
147
+ os.path.join(path, d, d + ".py"),
86
148
  ):
87
149
  modules.append(
88
150
  {
89
151
  "pname": d,
90
152
  "module": module_str,
91
153
  "module_path": os.path.join(path, d, module_str),
92
- }
154
+ },
93
155
  )
94
156
  return modules
95
157
 
96
- def _get_plugin_modules(self) -> List[dict]:
158
+ def _get_plugin_modules(self) -> list[dict]:
97
159
  plugins = []
98
160
  if os.path.exists(self.plugin_store_path):
99
161
  plugins.extend(self._get_modules(self.plugin_store_path))
@@ -104,7 +166,7 @@ class PluginManager:
104
166
  plugins.extend(_p)
105
167
  return plugins
106
168
 
107
- def _check_plugin_dept_update(self, target_plugin: str = None):
169
+ async def _check_plugin_dept_update(self, target_plugin: str | None = None):
108
170
  """检查插件的依赖
109
171
  如果 target_plugin 为 None,则检查所有插件的依赖
110
172
  """
@@ -123,14 +185,15 @@ class PluginManager:
123
185
  pth = os.path.join(plugin_path, "requirements.txt")
124
186
  logger.info(f"正在安装插件 {p} 所需的依赖库: {pth}")
125
187
  try:
126
- pip_installer.install(requirements_path=pth)
188
+ await pip_installer.install(requirements_path=pth)
127
189
  except Exception as e:
128
- logger.error(f"更新插件 {p} 的依赖失败。Code: {str(e)}")
190
+ logger.error(f"更新插件 {p} 的依赖失败。Code: {e!s}")
129
191
 
130
- def _load_plugin_metadata(self, plugin_path: str, plugin_obj=None) -> StarMetadata:
131
- """v3.4.0 以前的方式载入插件元数据
192
+ @staticmethod
193
+ def _load_plugin_metadata(plugin_path: str, plugin_obj=None) -> StarMetadata | None:
194
+ """先寻找 metadata.yaml 文件,如果不存在,则使用插件对象的 info() 函数获取元数据。
132
195
 
133
- 先寻找 metadata.yaml 文件,如果不存在,则使用插件对象的 info() 函数获取元数据。
196
+ Notes: 旧版本 AstrBot 插件可能使用的是 info() 函数来获取元数据。
134
197
  """
135
198
  metadata = None
136
199
 
@@ -139,14 +202,18 @@ class PluginManager:
139
202
 
140
203
  if os.path.exists(os.path.join(plugin_path, "metadata.yaml")):
141
204
  with open(
142
- os.path.join(plugin_path, "metadata.yaml"), "r", encoding="utf-8"
205
+ os.path.join(plugin_path, "metadata.yaml"),
206
+ encoding="utf-8",
143
207
  ) as f:
144
208
  metadata = yaml.safe_load(f)
145
- elif plugin_obj:
209
+ elif plugin_obj and hasattr(plugin_obj, "info"):
146
210
  # 使用 info() 函数
147
211
  metadata = plugin_obj.info()
148
212
 
149
213
  if isinstance(metadata, dict):
214
+ if "desc" not in metadata and "description" in metadata:
215
+ metadata["desc"] = metadata["description"]
216
+
150
217
  if (
151
218
  "name" not in metadata
152
219
  or "desc" not in metadata
@@ -154,7 +221,7 @@ class PluginManager:
154
221
  or "author" not in metadata
155
222
  ):
156
223
  raise Exception(
157
- "插件元数据信息不完整。name, desc, version, author 是必须的字段。"
224
+ "插件元数据信息不完整。name, desc, version, author 是必须的字段。",
158
225
  )
159
226
  metadata = StarMetadata(
160
227
  name=metadata["name"],
@@ -162,12 +229,15 @@ class PluginManager:
162
229
  desc=metadata["desc"],
163
230
  version=metadata["version"],
164
231
  repo=metadata["repo"] if "repo" in metadata else None,
232
+ display_name=metadata.get("display_name", None),
165
233
  )
166
234
 
167
235
  return metadata
168
236
 
237
+ @staticmethod
169
238
  def _get_plugin_related_modules(
170
- self, plugin_root_dir: str, is_reserved: bool = False
239
+ plugin_root_dir: str,
240
+ is_reserved: bool = False,
171
241
  ) -> list[str]:
172
242
  """获取与指定插件相关的所有已加载模块名
173
243
 
@@ -179,6 +249,7 @@ class PluginManager:
179
249
 
180
250
  Returns:
181
251
  list[str]: 与该插件相关的模块名列表
252
+
182
253
  """
183
254
  prefix = "packages." if is_reserved else "data.plugins."
184
255
  return [
@@ -189,8 +260,8 @@ class PluginManager:
189
260
 
190
261
  def _purge_modules(
191
262
  self,
192
- module_patterns: list[str] = None,
193
- root_dir_name: str = None,
263
+ module_patterns: list[str] | None = None,
264
+ root_dir_name: str | None = None,
194
265
  is_reserved: bool = False,
195
266
  ):
196
267
  """从 sys.modules 中移除指定的模块
@@ -201,6 +272,7 @@ class PluginManager:
201
272
  module_patterns: 要移除的模块名模式列表(例如 ["data.plugins", "packages"])
202
273
  root_dir_name: 插件根目录名,用于移除与该插件相关的所有模块
203
274
  is_reserved: 插件是否为保留插件(影响模块路径前缀)
275
+
204
276
  """
205
277
  if module_patterns:
206
278
  for pattern in module_patterns:
@@ -211,7 +283,8 @@ class PluginManager:
211
283
 
212
284
  if root_dir_name:
213
285
  for module_name in self._get_plugin_related_modules(
214
- root_dir_name, is_reserved
286
+ root_dir_name,
287
+ is_reserved,
215
288
  ):
216
289
  try:
217
290
  del sys.modules[module_name]
@@ -230,70 +303,50 @@ class PluginManager:
230
303
  tuple: 返回 load() 方法的结果,包含 (success, error_message)
231
304
  - success (bool): 重载是否成功
232
305
  - error_message (str|None): 错误信息,成功时为 None
233
- """
234
- specified_module_path = None
235
- if specified_plugin_name:
236
- for smd in star_registry:
237
- if smd.name == specified_plugin_name:
238
- specified_module_path = smd.module_path
239
- break
240
-
241
- # 终止插件
242
- if not specified_module_path:
243
- # 重载所有插件
244
- for smd in star_registry:
245
- try:
246
- await self._terminate_plugin(smd)
247
- except Exception as e:
248
- logger.warning(traceback.format_exc())
249
- logger.warning(
250
- f"插件 {smd.name} 未被正常终止: {str(e)}, 可能会导致该插件运行不正常。"
251
- )
252
-
253
- await self._unbind_plugin(smd.name, smd.module_path)
254
-
255
- star_handlers_registry.clear()
256
- star_map.clear()
257
- star_registry.clear()
258
- else:
259
- # 只重载指定插件
260
- smd = star_map.get(specified_module_path)
261
- if smd:
262
- try:
263
- await self._terminate_plugin(smd)
264
- except Exception as e:
265
- logger.warning(traceback.format_exc())
266
- logger.warning(
267
- f"插件 {smd.name} 未被正常终止: {str(e)}, 可能会导致该插件运行不正常。"
268
- )
269
-
270
- await self._unbind_plugin(smd.name, specified_module_path)
271
306
 
272
- result = await self.load(specified_module_path)
273
-
274
- # 更新所有插件的平台兼容性
275
- await self.update_all_platform_compatibility()
276
-
277
- return result
278
-
279
- async def update_all_platform_compatibility(self):
280
- """更新所有插件的平台兼容性设置"""
281
- # 获取最新的平台插件启用配置
282
- plugin_enable_config = self.config.get("platform_settings", {}).get(
283
- "plugin_enable", {}
284
- )
285
- logger.debug(
286
- f"更新所有插件的平台兼容性设置,平台数量: {len(plugin_enable_config)}"
287
- )
307
+ """
308
+ async with self._pm_lock:
309
+ specified_module_path = None
310
+ if specified_plugin_name:
311
+ for smd in star_registry:
312
+ if smd.name == specified_plugin_name:
313
+ specified_module_path = smd.module_path
314
+ break
315
+
316
+ # 终止插件
317
+ if not specified_module_path:
318
+ # 重载所有插件
319
+ for smd in star_registry:
320
+ try:
321
+ await self._terminate_plugin(smd)
322
+ except Exception as e:
323
+ logger.warning(traceback.format_exc())
324
+ logger.warning(
325
+ f"插件 {smd.name} 未被正常终止: {e!s}, 可能会导致该插件运行不正常。",
326
+ )
327
+ if smd.name and smd.module_path:
328
+ await self._unbind_plugin(smd.name, smd.module_path)
329
+
330
+ star_handlers_registry.clear()
331
+ star_map.clear()
332
+ star_registry.clear()
333
+ else:
334
+ # 只重载指定插件
335
+ smd = star_map.get(specified_module_path)
336
+ if smd:
337
+ try:
338
+ await self._terminate_plugin(smd)
339
+ except Exception as e:
340
+ logger.warning(traceback.format_exc())
341
+ logger.warning(
342
+ f"插件 {smd.name} 未被正常终止: {e!s}, 可能会导致该插件运行不正常。",
343
+ )
344
+ if smd.name:
345
+ await self._unbind_plugin(smd.name, specified_module_path)
288
346
 
289
- # 遍历所有插件,更新平台兼容性
290
- for plugin in self.context.get_all_stars():
291
- plugin.update_platform_compatibility(plugin_enable_config)
292
- logger.debug(
293
- f"插件 {plugin.name} 支持的平台: {list(plugin.supported_platforms.keys())}"
294
- )
347
+ result = await self.load(specified_module_path)
295
348
 
296
- return True
349
+ return result
297
350
 
298
351
  async def load(self, specified_module_path=None, specified_dir_name=None):
299
352
  """载入插件。
@@ -307,11 +360,11 @@ class PluginManager:
307
360
  tuple: (success, error_message)
308
361
  - success (bool): 是否全部加载成功
309
362
  - error_message (str|None): 错误信息,成功时为 None
310
- """
311
- inactivated_plugins: list = sp.get("inactivated_plugins", [])
312
- inactivated_llm_tools: list = sp.get("inactivated_llm_tools", [])
313
363
 
314
- alter_cmd = sp.get("alter_cmd", {})
364
+ """
365
+ inactivated_plugins = await sp.global_get("inactivated_plugins", [])
366
+ inactivated_llm_tools = await sp.global_get("inactivated_llm_tools", [])
367
+ alter_cmd = await sp.global_get("alter_cmd", {})
315
368
 
316
369
  plugin_modules = self._get_plugin_modules()
317
370
  if plugin_modules is None:
@@ -326,7 +379,8 @@ class PluginManager:
326
379
  # module_path = plugin_module['module_path']
327
380
  root_dir_name = plugin_module["pname"] # 插件的目录名
328
381
  reserved = plugin_module.get(
329
- "reserved", False
382
+ "reserved",
383
+ False,
330
384
  ) # 是否是保留插件。目前在 packages/ 目录下的都是保留插件。保留插件不可以卸载。
331
385
 
332
386
  path = "data.plugins." if not reserved else "packages."
@@ -345,11 +399,11 @@ class PluginManager:
345
399
  module = __import__(path, fromlist=[module_str])
346
400
  except (ModuleNotFoundError, ImportError):
347
401
  # 尝试安装依赖
348
- self._check_plugin_dept_update(target_plugin=root_dir_name)
402
+ await self._check_plugin_dept_update(target_plugin=root_dir_name)
349
403
  module = __import__(path, fromlist=[module_str])
350
404
  except Exception as e:
351
405
  logger.error(traceback.format_exc())
352
- logger.error(f"插件 {root_dir_name} 导入失败。原因:{str(e)}")
406
+ logger.error(f"插件 {root_dir_name} 导入失败。原因:{e!s}")
353
407
  continue
354
408
 
355
409
  # 检查 _conf_schema.json
@@ -360,26 +414,29 @@ class PluginManager:
360
414
  else os.path.join(self.reserved_plugin_path, root_dir_name)
361
415
  )
362
416
  plugin_schema_path = os.path.join(
363
- plugin_dir_path, self.conf_schema_fname
417
+ plugin_dir_path,
418
+ self.conf_schema_fname,
364
419
  )
365
420
  if os.path.exists(plugin_schema_path):
366
421
  # 加载插件配置
367
- with open(plugin_schema_path, "r", encoding="utf-8") as f:
422
+ with open(plugin_schema_path, encoding="utf-8") as f:
368
423
  plugin_config = AstrBotConfig(
369
424
  config_path=os.path.join(
370
- self.plugin_config_path, f"{root_dir_name}_config.json"
425
+ self.plugin_config_path,
426
+ f"{root_dir_name}_config.json",
371
427
  ),
372
428
  schema=json.loads(f.read()),
373
429
  )
430
+ logo_path = os.path.join(plugin_dir_path, self.logo_fname)
374
431
 
375
432
  if path in star_map:
376
- # 通过装饰器的方式注册插件
433
+ # 通过 __init__subclass__ 注册插件
377
434
  metadata = star_map[path]
378
435
 
379
436
  try:
380
437
  # yaml 文件的元数据优先
381
438
  metadata_yaml = self._load_plugin_metadata(
382
- plugin_path=plugin_dir_path
439
+ plugin_path=plugin_dir_path,
383
440
  )
384
441
  if metadata_yaml:
385
442
  metadata.name = metadata_yaml.name
@@ -387,24 +444,28 @@ class PluginManager:
387
444
  metadata.desc = metadata_yaml.desc
388
445
  metadata.version = metadata_yaml.version
389
446
  metadata.repo = metadata_yaml.repo
390
- except Exception:
391
- pass
392
-
447
+ metadata.display_name = metadata_yaml.display_name
448
+ except Exception as e:
449
+ logger.warning(
450
+ f"插件 {root_dir_name} 元数据载入失败: {e!s}。使用默认元数据。",
451
+ )
452
+ logger.info(metadata)
453
+ metadata.config = plugin_config
393
454
  if path not in inactivated_plugins:
394
455
  # 只有没有禁用插件时才实例化插件类
395
- if plugin_config:
396
- metadata.config = plugin_config
456
+ if plugin_config and metadata.star_cls_type:
397
457
  try:
398
458
  metadata.star_cls = metadata.star_cls_type(
399
- context=self.context, config=plugin_config
459
+ context=self.context,
460
+ config=plugin_config,
400
461
  )
401
462
  except TypeError as _:
402
463
  metadata.star_cls = metadata.star_cls_type(
403
- context=self.context
464
+ context=self.context,
404
465
  )
405
- else:
466
+ elif metadata.star_cls_type:
406
467
  metadata.star_cls = metadata.star_cls_type(
407
- context=self.context
468
+ context=self.context,
408
469
  )
409
470
  else:
410
471
  logger.info(f"插件 {metadata.name} 已被禁用。")
@@ -413,39 +474,50 @@ class PluginManager:
413
474
  metadata.root_dir_name = root_dir_name
414
475
  metadata.reserved = reserved
415
476
 
416
- # 更新插件的平台兼容性
417
- plugin_enable_config = self.config.get("platform_settings", {}).get(
418
- "plugin_enable", {}
477
+ assert metadata.module_path is not None, (
478
+ f"插件 {metadata.name} 的模块路径为空。"
419
479
  )
420
- metadata.update_platform_compatibility(plugin_enable_config)
421
480
 
422
481
  # 绑定 handler
423
482
  related_handlers = (
424
483
  star_handlers_registry.get_handlers_by_module_name(
425
- metadata.module_path
484
+ metadata.module_path,
426
485
  )
427
486
  )
428
487
  for handler in related_handlers:
429
488
  handler.handler = functools.partial(
430
- handler.handler, metadata.star_cls
489
+ handler.handler,
490
+ metadata.star_cls, # type: ignore
431
491
  )
432
492
  # 绑定 llm_tool handler
433
493
  for func_tool in llm_tools.func_list:
434
- if (
435
- func_tool.handler
436
- and func_tool.handler.__module__ == metadata.module_path
437
- ):
438
- func_tool.handler_module_path = metadata.module_path
439
- func_tool.handler = functools.partial(
440
- func_tool.handler, metadata.star_cls
441
- )
442
- if func_tool.name in inactivated_llm_tools:
443
- func_tool.active = False
494
+ if isinstance(func_tool, HandoffTool):
495
+ need_apply = []
496
+ sub_tools = func_tool.agent.tools
497
+ if sub_tools:
498
+ for sub_tool in sub_tools:
499
+ if isinstance(sub_tool, FunctionTool):
500
+ need_apply.append(sub_tool)
501
+ else:
502
+ need_apply = [func_tool]
503
+
504
+ for ft in need_apply:
505
+ if (
506
+ ft.handler
507
+ and ft.handler.__module__ == metadata.module_path
508
+ ):
509
+ ft.handler_module_path = metadata.module_path
510
+ ft.handler = functools.partial(
511
+ ft.handler,
512
+ metadata.star_cls, # type: ignore
513
+ )
514
+ if ft.name in inactivated_llm_tools:
515
+ ft.active = False
444
516
 
445
517
  else:
446
518
  # v3.4.0 以前的方式注册插件
447
519
  logger.debug(
448
- f"插件 {path} 未通过装饰器注册。尝试通过旧版本方式载入。"
520
+ f"插件 {path} 未通过装饰器注册。尝试通过旧版本方式载入。",
449
521
  )
450
522
  classes = self._get_classes(module)
451
523
 
@@ -454,23 +526,24 @@ class PluginManager:
454
526
  if plugin_config:
455
527
  try:
456
528
  obj = getattr(module, classes[0])(
457
- context=self.context, config=plugin_config
529
+ context=self.context,
530
+ config=plugin_config,
458
531
  ) # 实例化插件类
459
532
  except TypeError as _:
460
533
  obj = getattr(module, classes[0])(
461
- context=self.context
534
+ context=self.context,
462
535
  ) # 实例化插件类
463
536
  else:
464
537
  obj = getattr(module, classes[0])(
465
- context=self.context
538
+ context=self.context,
466
539
  ) # 实例化插件类
467
- else:
468
- logger.info(f"插件 {metadata.name} 已被禁用。")
469
540
 
470
- metadata = None
471
541
  metadata = self._load_plugin_metadata(
472
- plugin_path=plugin_dir_path, plugin_obj=obj
542
+ plugin_path=plugin_dir_path,
543
+ plugin_obj=obj,
473
544
  )
545
+ if not metadata:
546
+ raise Exception(f"无法找到插件 {plugin_dir_path} 的元数据。")
474
547
  metadata.star_cls = obj
475
548
  metadata.config = plugin_config
476
549
  metadata.module = module
@@ -485,9 +558,15 @@ class PluginManager:
485
558
  if metadata.module_path in inactivated_plugins:
486
559
  metadata.activated = False
487
560
 
561
+ # Plugin logo path
562
+ if os.path.exists(logo_path):
563
+ metadata.logo_path = logo_path
564
+
565
+ assert metadata.module_path, f"插件 {metadata.name} 模块路径为空"
566
+
488
567
  full_names = []
489
568
  for handler in star_handlers_registry.get_handlers_by_module_name(
490
- metadata.module_path
569
+ metadata.module_path,
491
570
  ):
492
571
  full_names.append(handler.handler_full_name)
493
572
 
@@ -497,7 +576,8 @@ class PluginManager:
497
576
  and handler.handler_name in alter_cmd[metadata.name]
498
577
  ):
499
578
  cmd_type = alter_cmd[metadata.name][handler.handler_name].get(
500
- "permission", "member"
579
+ "permission",
580
+ "member",
501
581
  )
502
582
  found_permission_filter = False
503
583
  for filter_ in handler.event_filters:
@@ -513,18 +593,18 @@ class PluginManager:
513
593
  PermissionTypeFilter(
514
594
  PermissionType.ADMIN
515
595
  if cmd_type == "admin"
516
- else PermissionType.MEMBER
517
- )
596
+ else PermissionType.MEMBER,
597
+ ),
518
598
  )
519
599
 
520
600
  logger.debug(
521
- f"插入权限过滤器 {cmd_type} 到 {metadata.name} 的 {handler.handler_name} 方法。"
601
+ f"插入权限过滤器 {cmd_type} 到 {metadata.name} 的 {handler.handler_name} 方法。",
522
602
  )
523
603
 
524
604
  metadata.star_handler_full_names = full_names
525
605
 
526
606
  # 执行 initialize() 方法
527
- if hasattr(metadata.star_cls, "initialize"):
607
+ if hasattr(metadata.star_cls, "initialize") and metadata.star_cls:
528
608
  await metadata.star_cls.initialize()
529
609
 
530
610
  except BaseException as e:
@@ -533,7 +613,7 @@ class PluginManager:
533
613
  for line in errors.split("\n"):
534
614
  logger.error(f"| {line}")
535
615
  logger.error("----------------------------------")
536
- fail_rec += f"加载 {root_dir_name} 插件时出现问题,原因 {str(e)}。\n"
616
+ fail_rec += f"加载 {root_dir_name} 插件时出现问题,原因 {e!s}。\n"
537
617
 
538
618
  # 清除 pip.main 导致的多余的 logging handlers
539
619
  for handler in logging.root.handlers[:]:
@@ -541,9 +621,8 @@ class PluginManager:
541
621
 
542
622
  if not fail_rec:
543
623
  return True, None
544
- else:
545
- self.failed_plugin_info = fail_rec
546
- return False, fail_rec
624
+ self.failed_plugin_info = fail_rec
625
+ return False, fail_rec
547
626
 
548
627
  async def install_plugin(self, repo_url: str, proxy=""):
549
628
  """从仓库 URL 安装插件
@@ -559,75 +638,141 @@ class PluginManager:
559
638
  - repo: 插件的仓库 URL
560
639
  - readme: README.md 文件的内容(如果存在)
561
640
  如果找不到插件元数据则返回 None。
562
- """
563
- plugin_path = await self.updator.install(repo_url, proxy)
564
- # reload the plugin
565
- dir_name = os.path.basename(plugin_path)
566
- await self.load(specified_dir_name=dir_name)
567
-
568
- # Get the plugin metadata to return repo info
569
- plugin = self.context.get_registered_star(dir_name)
570
- if not plugin:
571
- # Try to find by other name if directory name doesn't match plugin name
572
- for star in self.context.get_all_stars():
573
- if star.root_dir_name == dir_name:
574
- plugin = star
575
- break
576
-
577
- # Extract README.md content if exists
578
- readme_content = None
579
- readme_path = os.path.join(plugin_path, "README.md")
580
- if not os.path.exists(readme_path):
581
- readme_path = os.path.join(plugin_path, "readme.md")
582
641
 
583
- if os.path.exists(readme_path):
584
- try:
585
- with open(readme_path, "r", encoding="utf-8") as f:
586
- readme_content = f.read()
587
- except Exception as e:
588
- logger.warning(f"读取插件 {dir_name} 的 README.md 文件失败: {str(e)}")
642
+ """
643
+ async with self._pm_lock:
644
+ plugin_path = await self.updator.install(repo_url, proxy)
645
+ # reload the plugin
646
+ dir_name = os.path.basename(plugin_path)
647
+ await self.load(specified_dir_name=dir_name)
648
+
649
+ # Get the plugin metadata to return repo info
650
+ plugin = self.context.get_registered_star(dir_name)
651
+ if not plugin:
652
+ # Try to find by other name if directory name doesn't match plugin name
653
+ for star in self.context.get_all_stars():
654
+ if star.root_dir_name == dir_name:
655
+ plugin = star
656
+ break
657
+
658
+ # Extract README.md content if exists
659
+ readme_content = None
660
+ readme_path = os.path.join(plugin_path, "README.md")
661
+ if not os.path.exists(readme_path):
662
+ readme_path = os.path.join(plugin_path, "readme.md")
663
+
664
+ if os.path.exists(readme_path):
665
+ try:
666
+ with open(readme_path, encoding="utf-8") as f:
667
+ readme_content = f.read()
668
+ except Exception as e:
669
+ logger.warning(
670
+ f"读取插件 {dir_name} 的 README.md 文件失败: {e!s}",
671
+ )
589
672
 
590
- plugin_info = None
591
- if plugin:
592
- plugin_info = {"repo": plugin.repo, "readme": readme_content}
673
+ plugin_info = None
674
+ if plugin:
675
+ plugin_info = {
676
+ "repo": plugin.repo,
677
+ "readme": readme_content,
678
+ "name": plugin.name,
679
+ }
593
680
 
594
- return plugin_info
681
+ return plugin_info
595
682
 
596
- async def uninstall_plugin(self, plugin_name: str):
683
+ async def uninstall_plugin(
684
+ self,
685
+ plugin_name: str,
686
+ delete_config: bool = False,
687
+ delete_data: bool = False,
688
+ ):
597
689
  """卸载指定的插件。
598
690
 
599
691
  Args:
600
692
  plugin_name (str): 要卸载的插件名称
693
+ delete_config (bool): 是否删除插件配置文件,默认为 False
694
+ delete_data (bool): 是否删除插件数据,默认为 False
601
695
 
602
696
  Raises:
603
697
  Exception: 当插件不存在、是保留插件时,或删除插件文件夹失败时抛出异常
698
+
604
699
  """
605
- plugin = self.context.get_registered_star(plugin_name)
606
- if not plugin:
607
- raise Exception("插件不存在。")
608
- if plugin.reserved:
609
- raise Exception("该插件是 AstrBot 保留插件,无法卸载。")
610
- root_dir_name = plugin.root_dir_name
611
- ppath = self.plugin_store_path
700
+ async with self._pm_lock:
701
+ plugin = self.context.get_registered_star(plugin_name)
702
+ if not plugin:
703
+ raise Exception("插件不存在。")
704
+ if plugin.reserved:
705
+ raise Exception("该插件是 AstrBot 保留插件,无法卸载。")
706
+ root_dir_name = plugin.root_dir_name
707
+ ppath = self.plugin_store_path
708
+
709
+ # 终止插件
710
+ try:
711
+ await self._terminate_plugin(plugin)
712
+ except Exception as e:
713
+ logger.warning(traceback.format_exc())
714
+ logger.warning(
715
+ f"插件 {plugin_name} 未被正常终止 {e!s}, 可能会导致资源泄露等问题。",
716
+ )
612
717
 
613
- # 终止插件
614
- try:
615
- await self._terminate_plugin(plugin)
616
- except Exception as e:
617
- logger.warning(traceback.format_exc())
618
- logger.warning(
619
- f"插件 {plugin_name} 未被正常终止 {str(e)}, 可能会导致资源泄露等问题。"
620
- )
718
+ # 从 star_registry 和 star_map 中删除
719
+ if plugin.module_path is None or root_dir_name is None:
720
+ raise Exception(f"插件 {plugin_name} 数据不完整,无法卸载。")
621
721
 
622
- # star_registry 和 star_map 中删除
623
- await self._unbind_plugin(plugin_name, plugin.module_path)
722
+ await self._unbind_plugin(plugin_name, plugin.module_path)
624
723
 
625
- try:
626
- remove_dir(os.path.join(ppath, root_dir_name))
627
- except Exception as e:
628
- raise Exception(
629
- f"移除插件成功,但是删除插件文件夹失败: {str(e)}。您可以手动删除该文件夹,位于 addons/plugins/ 下。"
630
- )
724
+ # 删除插件文件夹
725
+ try:
726
+ remove_dir(os.path.join(ppath, root_dir_name))
727
+ except Exception as e:
728
+ raise Exception(
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
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}")
631
776
 
632
777
  async def _unbind_plugin(self, plugin_name: str, plugin_module_path: str):
633
778
  """解绑并移除一个插件。
@@ -635,6 +780,7 @@ class PluginManager:
635
780
  Args:
636
781
  plugin_name: 要解绑的插件名称
637
782
  plugin_module_path: 插件的完整模块路径
783
+
638
784
  """
639
785
  plugin = None
640
786
  del star_map[plugin_module_path]
@@ -644,10 +790,10 @@ class PluginManager:
644
790
  del star_registry[i]
645
791
  break
646
792
  for handler in star_handlers_registry.get_handlers_by_module_name(
647
- plugin_module_path
793
+ plugin_module_path,
648
794
  ):
649
795
  logger.info(
650
- f"移除了插件 {plugin_name} 的处理函数 {handler.handler_name} ({len(star_handlers_registry)})"
796
+ f"移除了插件 {plugin_name} 的处理函数 {handler.handler_name} ({len(star_handlers_registry)})",
651
797
  )
652
798
  star_handlers_registry.remove(handler)
653
799
 
@@ -658,8 +804,25 @@ class PluginManager:
658
804
  ]:
659
805
  del star_handlers_registry.star_handlers_map[k]
660
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
+
820
+ if plugin is None:
821
+ return
822
+
661
823
  self._purge_modules(
662
- root_dir_name=plugin.root_dir_name, is_reserved=plugin.reserved
824
+ root_dir_name=plugin.root_dir_name,
825
+ is_reserved=plugin.reserved,
663
826
  )
664
827
 
665
828
  async def update_plugin(self, plugin_name: str, proxy=""):
@@ -674,41 +837,48 @@ class PluginManager:
674
837
  await self.reload(plugin_name)
675
838
 
676
839
  async def turn_off_plugin(self, plugin_name: str):
677
- """
678
- 禁用一个插件。
840
+ """禁用一个插件。
679
841
  调用插件的 terminate() 方法,
680
842
  将插件的 module_path 加入到 data/shared_preferences.json 的 inactivated_plugins 列表中。
681
843
  并且同时将插件启用的 llm_tool 禁用。
682
844
  """
683
- plugin = self.context.get_registered_star(plugin_name)
684
- if not plugin:
685
- raise Exception("插件不存在。")
686
-
687
- # 调用插件的终止方法
688
- await self._terminate_plugin(plugin)
689
-
690
- # 加入到 shared_preferences 中
691
- inactivated_plugins: list = sp.get("inactivated_plugins", [])
692
- if plugin.module_path not in inactivated_plugins:
693
- inactivated_plugins.append(plugin.module_path)
845
+ async with self._pm_lock:
846
+ plugin = self.context.get_registered_star(plugin_name)
847
+ if not plugin:
848
+ raise Exception("插件不存在。")
694
849
 
695
- inactivated_llm_tools: list = list(
696
- set(sp.get("inactivated_llm_tools", []))
697
- ) # 后向兼容
850
+ # 调用插件的终止方法
851
+ await self._terminate_plugin(plugin)
698
852
 
699
- # 禁用插件启用的 llm_tool
700
- for func_tool in llm_tools.func_list:
701
- if func_tool.handler_module_path == plugin.module_path:
702
- func_tool.active = False
703
- if func_tool.name not in inactivated_llm_tools:
704
- inactivated_llm_tools.append(func_tool.name)
853
+ # 加入到 shared_preferences 中
854
+ inactivated_plugins: list = await sp.global_get("inactivated_plugins", [])
855
+ if plugin.module_path not in inactivated_plugins:
856
+ inactivated_plugins.append(plugin.module_path)
857
+
858
+ inactivated_llm_tools: list = list(
859
+ set(await sp.global_get("inactivated_llm_tools", [])),
860
+ ) # 后向兼容
861
+
862
+ # 禁用插件启用的 llm_tool
863
+ for func_tool in llm_tools.func_list:
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
+ ):
871
+ func_tool.active = False
872
+ if func_tool.name not in inactivated_llm_tools:
873
+ inactivated_llm_tools.append(func_tool.name)
705
874
 
706
- sp.put("inactivated_plugins", inactivated_plugins)
707
- sp.put("inactivated_llm_tools", inactivated_llm_tools)
875
+ await sp.global_put("inactivated_plugins", inactivated_plugins)
876
+ await sp.global_put("inactivated_llm_tools", inactivated_llm_tools)
708
877
 
709
- plugin.activated = False
878
+ plugin.activated = False
710
879
 
711
- async def _terminate_plugin(self, star_metadata: StarMetadata):
880
+ @staticmethod
881
+ async def _terminate_plugin(star_metadata: StarMetadata):
712
882
  """终止插件,调用插件的 terminate() 和 __del__() 方法"""
713
883
  logger.info(f"正在终止插件 {star_metadata.name} ...")
714
884
 
@@ -717,35 +887,43 @@ class PluginManager:
717
887
  logger.debug(f"插件 {star_metadata.name} 未被激活,不需要终止,跳过。")
718
888
  return
719
889
 
720
- if hasattr(star_metadata.star_cls, "__del__"):
890
+ if star_metadata.star_cls is None:
891
+ return
892
+
893
+ if "__del__" in star_metadata.star_cls_type.__dict__:
721
894
  asyncio.get_event_loop().run_in_executor(
722
- None, star_metadata.star_cls.__del__
895
+ None,
896
+ star_metadata.star_cls.__del__,
723
897
  )
724
- elif hasattr(star_metadata.star_cls, "terminate"):
898
+ elif "terminate" in star_metadata.star_cls_type.__dict__:
725
899
  await star_metadata.star_cls.terminate()
726
900
 
727
901
  async def turn_on_plugin(self, plugin_name: str):
728
902
  plugin = self.context.get_registered_star(plugin_name)
729
- inactivated_plugins: list = sp.get("inactivated_plugins", [])
730
- inactivated_llm_tools: list = sp.get("inactivated_llm_tools", [])
903
+ if plugin is None:
904
+ raise Exception(f"插件 {plugin_name} 不存在。")
905
+ inactivated_plugins: list = await sp.global_get("inactivated_plugins", [])
906
+ inactivated_llm_tools: list = await sp.global_get("inactivated_llm_tools", [])
731
907
  if plugin.module_path in inactivated_plugins:
732
908
  inactivated_plugins.remove(plugin.module_path)
733
- sp.put("inactivated_plugins", inactivated_plugins)
909
+ await sp.global_put("inactivated_plugins", inactivated_plugins)
734
910
 
735
911
  # 启用插件启用的 llm_tool
736
912
  for func_tool in llm_tools.func_list:
913
+ mp = func_tool.handler_module_path
737
914
  if (
738
- func_tool.handler_module_path == plugin.module_path
915
+ plugin.module_path
916
+ and mp
917
+ and plugin.module_path.startswith(mp)
918
+ and not mp.endswith(("packages", "data.plugins"))
739
919
  and func_tool.name in inactivated_llm_tools
740
920
  ):
741
921
  inactivated_llm_tools.remove(func_tool.name)
742
922
  func_tool.active = True
743
- sp.put("inactivated_llm_tools", inactivated_llm_tools)
923
+ await sp.global_put("inactivated_llm_tools", inactivated_llm_tools)
744
924
 
745
925
  await self.reload(plugin_name)
746
926
 
747
- # plugin.activated = True
748
-
749
927
  async def install_plugin_from_file(self, zip_file_path: str):
750
928
  dir_name = os.path.basename(zip_file_path).replace(".zip", "")
751
929
  dir_name = dir_name.removesuffix("-master").removesuffix("-main").lower()
@@ -756,7 +934,7 @@ class PluginManager:
756
934
  try:
757
935
  os.remove(zip_file_path)
758
936
  except BaseException as e:
759
- logger.warning(f"删除插件压缩包失败: {str(e)}")
937
+ logger.warning(f"删除插件压缩包失败: {e!s}")
760
938
  # await self.reload()
761
939
  await self.load(specified_dir_name=dir_name)
762
940
 
@@ -777,13 +955,17 @@ class PluginManager:
777
955
 
778
956
  if os.path.exists(readme_path):
779
957
  try:
780
- with open(readme_path, "r", encoding="utf-8") as f:
958
+ with open(readme_path, encoding="utf-8") as f:
781
959
  readme_content = f.read()
782
960
  except Exception as e:
783
- logger.warning(f"读取插件 {dir_name} 的 README.md 文件失败: {str(e)}")
961
+ logger.warning(f"读取插件 {dir_name} 的 README.md 文件失败: {e!s}")
784
962
 
785
963
  plugin_info = None
786
964
  if plugin:
787
- plugin_info = {"repo": plugin.repo, "readme": readme_content}
965
+ plugin_info = {
966
+ "repo": plugin.repo,
967
+ "readme": readme_content,
968
+ "name": plugin.name,
969
+ }
788
970
 
789
971
  return plugin_info