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
@@ -0,0 +1,209 @@
1
+ import hashlib
2
+ import json
3
+ import zoneinfo
4
+ from collections.abc import Callable
5
+ from typing import Any
6
+
7
+ import click
8
+
9
+ from ..utils import check_astrbot_root, get_astrbot_root
10
+
11
+
12
+ def _validate_log_level(value: str) -> str:
13
+ """验证日志级别"""
14
+ value = value.upper()
15
+ if value not in ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]:
16
+ raise click.ClickException(
17
+ "日志级别必须是 DEBUG/INFO/WARNING/ERROR/CRITICAL 之一",
18
+ )
19
+ return value
20
+
21
+
22
+ def _validate_dashboard_port(value: str) -> int:
23
+ """验证 Dashboard 端口"""
24
+ try:
25
+ port = int(value)
26
+ if port < 1 or port > 65535:
27
+ raise click.ClickException("端口必须在 1-65535 范围内")
28
+ return port
29
+ except ValueError:
30
+ raise click.ClickException("端口必须是数字")
31
+
32
+
33
+ def _validate_dashboard_username(value: str) -> str:
34
+ """验证 Dashboard 用户名"""
35
+ if not value:
36
+ raise click.ClickException("用户名不能为空")
37
+ return value
38
+
39
+
40
+ def _validate_dashboard_password(value: str) -> str:
41
+ """验证 Dashboard 密码"""
42
+ if not value:
43
+ raise click.ClickException("密码不能为空")
44
+ return hashlib.md5(value.encode()).hexdigest()
45
+
46
+
47
+ def _validate_timezone(value: str) -> str:
48
+ """验证时区"""
49
+ try:
50
+ zoneinfo.ZoneInfo(value)
51
+ except Exception:
52
+ raise click.ClickException(f"无效的时区: {value},请使用有效的IANA时区名称")
53
+ return value
54
+
55
+
56
+ def _validate_callback_api_base(value: str) -> str:
57
+ """验证回调接口基址"""
58
+ if not value.startswith("http://") and not value.startswith("https://"):
59
+ raise click.ClickException("回调接口基址必须以 http:// 或 https:// 开头")
60
+ return value
61
+
62
+
63
+ # 可通过CLI设置的配置项,配置键到验证器函数的映射
64
+ CONFIG_VALIDATORS: dict[str, Callable[[str], Any]] = {
65
+ "timezone": _validate_timezone,
66
+ "log_level": _validate_log_level,
67
+ "dashboard.port": _validate_dashboard_port,
68
+ "dashboard.username": _validate_dashboard_username,
69
+ "dashboard.password": _validate_dashboard_password,
70
+ "callback_api_base": _validate_callback_api_base,
71
+ }
72
+
73
+
74
+ def _load_config() -> dict[str, Any]:
75
+ """加载或初始化配置文件"""
76
+ root = get_astrbot_root()
77
+ if not check_astrbot_root(root):
78
+ raise click.ClickException(
79
+ f"{root}不是有效的 AstrBot 根目录,如需初始化请使用 astrbot init",
80
+ )
81
+
82
+ config_path = root / "data" / "cmd_config.json"
83
+ if not config_path.exists():
84
+ from astrbot.core.config.default import DEFAULT_CONFIG
85
+
86
+ config_path.write_text(
87
+ json.dumps(DEFAULT_CONFIG, ensure_ascii=False, indent=2),
88
+ encoding="utf-8-sig",
89
+ )
90
+
91
+ try:
92
+ return json.loads(config_path.read_text(encoding="utf-8-sig"))
93
+ except json.JSONDecodeError as e:
94
+ raise click.ClickException(f"配置文件解析失败: {e!s}")
95
+
96
+
97
+ def _save_config(config: dict[str, Any]) -> None:
98
+ """保存配置文件"""
99
+ config_path = get_astrbot_root() / "data" / "cmd_config.json"
100
+
101
+ config_path.write_text(
102
+ json.dumps(config, ensure_ascii=False, indent=2),
103
+ encoding="utf-8-sig",
104
+ )
105
+
106
+
107
+ def _set_nested_item(obj: dict[str, Any], path: str, value: Any) -> None:
108
+ """设置嵌套字典中的值"""
109
+ parts = path.split(".")
110
+ for part in parts[:-1]:
111
+ if part not in obj:
112
+ obj[part] = {}
113
+ elif not isinstance(obj[part], dict):
114
+ raise click.ClickException(
115
+ f"配置路径冲突: {'.'.join(parts[: parts.index(part) + 1])} 不是字典",
116
+ )
117
+ obj = obj[part]
118
+ obj[parts[-1]] = value
119
+
120
+
121
+ def _get_nested_item(obj: dict[str, Any], path: str) -> Any:
122
+ """获取嵌套字典中的值"""
123
+ parts = path.split(".")
124
+ for part in parts:
125
+ obj = obj[part]
126
+ return obj
127
+
128
+
129
+ @click.group(name="conf")
130
+ def conf():
131
+ """配置管理命令
132
+
133
+ 支持的配置项:
134
+
135
+ - timezone: 时区设置 (例如: Asia/Shanghai)
136
+
137
+ - log_level: 日志级别 (DEBUG/INFO/WARNING/ERROR/CRITICAL)
138
+
139
+ - dashboard.port: Dashboard 端口
140
+
141
+ - dashboard.username: Dashboard 用户名
142
+
143
+ - dashboard.password: Dashboard 密码
144
+
145
+ - callback_api_base: 回调接口基址
146
+ """
147
+
148
+
149
+ @conf.command(name="set")
150
+ @click.argument("key")
151
+ @click.argument("value")
152
+ def set_config(key: str, value: str):
153
+ """设置配置项的值"""
154
+ if key not in CONFIG_VALIDATORS:
155
+ raise click.ClickException(f"不支持的配置项: {key}")
156
+
157
+ config = _load_config()
158
+
159
+ try:
160
+ old_value = _get_nested_item(config, key)
161
+ validated_value = CONFIG_VALIDATORS[key](value)
162
+ _set_nested_item(config, key, validated_value)
163
+ _save_config(config)
164
+
165
+ click.echo(f"配置已更新: {key}")
166
+ if key == "dashboard.password":
167
+ click.echo(" 原值: ********")
168
+ click.echo(" 新值: ********")
169
+ else:
170
+ click.echo(f" 原值: {old_value}")
171
+ click.echo(f" 新值: {validated_value}")
172
+
173
+ except KeyError:
174
+ raise click.ClickException(f"未知的配置项: {key}")
175
+ except Exception as e:
176
+ raise click.UsageError(f"设置配置失败: {e!s}")
177
+
178
+
179
+ @conf.command(name="get")
180
+ @click.argument("key", required=False)
181
+ def get_config(key: str | None = None):
182
+ """获取配置项的值,不提供key则显示所有可配置项"""
183
+ config = _load_config()
184
+
185
+ if key:
186
+ if key not in CONFIG_VALIDATORS:
187
+ raise click.ClickException(f"不支持的配置项: {key}")
188
+
189
+ try:
190
+ value = _get_nested_item(config, key)
191
+ if key == "dashboard.password":
192
+ value = "********"
193
+ click.echo(f"{key}: {value}")
194
+ except KeyError:
195
+ raise click.ClickException(f"未知的配置项: {key}")
196
+ except Exception as e:
197
+ raise click.UsageError(f"获取配置失败: {e!s}")
198
+ else:
199
+ click.echo("当前配置:")
200
+ for key in CONFIG_VALIDATORS:
201
+ try:
202
+ value = (
203
+ "********"
204
+ if key == "dashboard.password"
205
+ else _get_nested_item(config, key)
206
+ )
207
+ click.echo(f" {key}: {value}")
208
+ except (KeyError, TypeError):
209
+ pass
@@ -0,0 +1,56 @@
1
+ import asyncio
2
+ from pathlib import Path
3
+
4
+ import click
5
+ from filelock import FileLock, Timeout
6
+
7
+ from ..utils import check_dashboard, get_astrbot_root
8
+
9
+
10
+ async def initialize_astrbot(astrbot_root: Path) -> None:
11
+ """执行 AstrBot 初始化逻辑"""
12
+ dot_astrbot = astrbot_root / ".astrbot"
13
+
14
+ if not dot_astrbot.exists():
15
+ click.echo(f"Current Directory: {astrbot_root}")
16
+ click.echo(
17
+ "如果你确认这是 Astrbot root directory, 你需要在当前目录下创建一个 .astrbot 文件标记该目录为 AstrBot 的数据目录。",
18
+ )
19
+ if click.confirm(
20
+ f"请检查当前目录是否正确,确认正确请回车: {astrbot_root}",
21
+ default=True,
22
+ abort=True,
23
+ ):
24
+ dot_astrbot.touch()
25
+ click.echo(f"Created {dot_astrbot}")
26
+
27
+ paths = {
28
+ "data": astrbot_root / "data",
29
+ "config": astrbot_root / "data" / "config",
30
+ "plugins": astrbot_root / "data" / "plugins",
31
+ "temp": astrbot_root / "data" / "temp",
32
+ }
33
+
34
+ for name, path in paths.items():
35
+ path.mkdir(parents=True, exist_ok=True)
36
+ click.echo(f"{'Created' if not path.exists() else 'Directory exists'}: {path}")
37
+
38
+ await check_dashboard(astrbot_root / "data")
39
+
40
+
41
+ @click.command()
42
+ def init() -> None:
43
+ """初始化 AstrBot"""
44
+ click.echo("Initializing AstrBot...")
45
+ astrbot_root = get_astrbot_root()
46
+ lock_file = astrbot_root / "astrbot.lock"
47
+ lock = FileLock(lock_file, timeout=5)
48
+
49
+ try:
50
+ with lock.acquire():
51
+ asyncio.run(initialize_astrbot(astrbot_root))
52
+ except Timeout:
53
+ raise click.ClickException("无法获取锁文件,请检查是否有其他实例正在运行")
54
+
55
+ except Exception as e:
56
+ raise click.ClickException(f"初始化失败: {e!s}")
@@ -0,0 +1,245 @@
1
+ import re
2
+ import shutil
3
+ from pathlib import Path
4
+
5
+ import click
6
+
7
+ from ..utils import (
8
+ PluginStatus,
9
+ build_plug_list,
10
+ check_astrbot_root,
11
+ get_astrbot_root,
12
+ get_git_repo,
13
+ manage_plugin,
14
+ )
15
+
16
+
17
+ @click.group()
18
+ def plug():
19
+ """插件管理"""
20
+
21
+
22
+ def _get_data_path() -> Path:
23
+ base = get_astrbot_root()
24
+ if not check_astrbot_root(base):
25
+ raise click.ClickException(
26
+ f"{base}不是有效的 AstrBot 根目录,如需初始化请使用 astrbot init",
27
+ )
28
+ return (base / "data").resolve()
29
+
30
+
31
+ def display_plugins(plugins, title=None, color=None):
32
+ if title:
33
+ click.echo(click.style(title, fg=color, bold=True))
34
+
35
+ click.echo(f"{'名称':<20} {'版本':<10} {'状态':<10} {'作者':<15} {'描述':<30}")
36
+ click.echo("-" * 85)
37
+
38
+ for p in plugins:
39
+ desc = p["desc"][:30] + ("..." if len(p["desc"]) > 30 else "")
40
+ click.echo(
41
+ f"{p['name']:<20} {p['version']:<10} {p['status']:<10} "
42
+ f"{p['author']:<15} {desc:<30}",
43
+ )
44
+
45
+
46
+ @plug.command()
47
+ @click.argument("name")
48
+ def new(name: str):
49
+ """创建新插件"""
50
+ base_path = _get_data_path()
51
+ plug_path = base_path / "plugins" / name
52
+
53
+ if plug_path.exists():
54
+ raise click.ClickException(f"插件 {name} 已存在")
55
+
56
+ author = click.prompt("请输入插件作者", type=str)
57
+ desc = click.prompt("请输入插件描述", type=str)
58
+ version = click.prompt("请输入插件版本", type=str)
59
+ if not re.match(r"^\d+\.\d+(\.\d+)?$", version.lower().lstrip("v")):
60
+ raise click.ClickException("版本号必须为 x.y 或 x.y.z 格式")
61
+ repo = click.prompt("请输入插件仓库:", type=str)
62
+ if not repo.startswith("http"):
63
+ raise click.ClickException("仓库地址必须以 http 开头")
64
+
65
+ click.echo("下载插件模板...")
66
+ get_git_repo(
67
+ "https://github.com/Soulter/helloworld",
68
+ plug_path,
69
+ )
70
+
71
+ click.echo("重写插件信息...")
72
+ # 重写 metadata.yaml
73
+ with open(plug_path / "metadata.yaml", "w", encoding="utf-8") as f:
74
+ f.write(
75
+ f"name: {name}\n"
76
+ f"desc: {desc}\n"
77
+ f"version: {version}\n"
78
+ f"author: {author}\n"
79
+ f"repo: {repo}\n",
80
+ )
81
+
82
+ # 重写 README.md
83
+ with open(plug_path / "README.md", "w", encoding="utf-8") as f:
84
+ f.write(f"# {name}\n\n{desc}\n\n# 支持\n\n[帮助文档](https://astrbot.app)\n")
85
+
86
+ # 重写 main.py
87
+ with open(plug_path / "main.py", encoding="utf-8") as f:
88
+ content = f.read()
89
+
90
+ new_content = content.replace(
91
+ '@register("helloworld", "YourName", "一个简单的 Hello World 插件", "1.0.0")',
92
+ f'@register("{name}", "{author}", "{desc}", "{version}")',
93
+ )
94
+
95
+ with open(plug_path / "main.py", "w", encoding="utf-8") as f:
96
+ f.write(new_content)
97
+
98
+ click.echo(f"插件 {name} 创建成功")
99
+
100
+
101
+ @plug.command()
102
+ @click.option("--all", "-a", is_flag=True, help="列出未安装的插件")
103
+ def list(all: bool):
104
+ """列出插件"""
105
+ base_path = _get_data_path()
106
+ plugins = build_plug_list(base_path / "plugins")
107
+
108
+ # 未发布的插件
109
+ not_published_plugins = [
110
+ p for p in plugins if p["status"] == PluginStatus.NOT_PUBLISHED
111
+ ]
112
+ if not_published_plugins:
113
+ display_plugins(not_published_plugins, "未发布的插件", "red")
114
+
115
+ # 需要更新的插件
116
+ need_update_plugins = [
117
+ p for p in plugins if p["status"] == PluginStatus.NEED_UPDATE
118
+ ]
119
+ if need_update_plugins:
120
+ display_plugins(need_update_plugins, "需要更新的插件", "yellow")
121
+
122
+ # 已安装的插件
123
+ installed_plugins = [p for p in plugins if p["status"] == PluginStatus.INSTALLED]
124
+ if installed_plugins:
125
+ display_plugins(installed_plugins, "已安装的插件", "green")
126
+
127
+ # 未安装的插件
128
+ not_installed_plugins = [
129
+ p for p in plugins if p["status"] == PluginStatus.NOT_INSTALLED
130
+ ]
131
+ if not_installed_plugins and all:
132
+ display_plugins(not_installed_plugins, "未安装的插件", "blue")
133
+
134
+ if (
135
+ not any([not_published_plugins, need_update_plugins, installed_plugins])
136
+ and not all
137
+ ):
138
+ click.echo("未安装任何插件")
139
+
140
+
141
+ @plug.command()
142
+ @click.argument("name")
143
+ @click.option("--proxy", help="代理服务器地址")
144
+ def install(name: str, proxy: str | None):
145
+ """安装插件"""
146
+ base_path = _get_data_path()
147
+ plug_path = base_path / "plugins"
148
+ plugins = build_plug_list(base_path / "plugins")
149
+
150
+ plugin = next(
151
+ (
152
+ p
153
+ for p in plugins
154
+ if p["name"] == name and p["status"] == PluginStatus.NOT_INSTALLED
155
+ ),
156
+ None,
157
+ )
158
+
159
+ if not plugin:
160
+ raise click.ClickException(f"未找到可安装的插件 {name},可能是不存在或已安装")
161
+
162
+ manage_plugin(plugin, plug_path, is_update=False, proxy=proxy)
163
+
164
+
165
+ @plug.command()
166
+ @click.argument("name")
167
+ def remove(name: str):
168
+ """卸载插件"""
169
+ base_path = _get_data_path()
170
+ plugins = build_plug_list(base_path / "plugins")
171
+ plugin = next((p for p in plugins if p["name"] == name), None)
172
+
173
+ if not plugin or not plugin.get("local_path"):
174
+ raise click.ClickException(f"插件 {name} 不存在或未安装")
175
+
176
+ plugin_path = plugin["local_path"]
177
+
178
+ click.confirm(f"确定要卸载插件 {name} 吗?", default=False, abort=True)
179
+
180
+ try:
181
+ shutil.rmtree(plugin_path)
182
+ click.echo(f"插件 {name} 已卸载")
183
+ except Exception as e:
184
+ raise click.ClickException(f"卸载插件 {name} 失败: {e}")
185
+
186
+
187
+ @plug.command()
188
+ @click.argument("name", required=False)
189
+ @click.option("--proxy", help="Github代理地址")
190
+ def update(name: str, proxy: str | None):
191
+ """更新插件"""
192
+ base_path = _get_data_path()
193
+ plug_path = base_path / "plugins"
194
+ plugins = build_plug_list(base_path / "plugins")
195
+
196
+ if name:
197
+ plugin = next(
198
+ (
199
+ p
200
+ for p in plugins
201
+ if p["name"] == name and p["status"] == PluginStatus.NEED_UPDATE
202
+ ),
203
+ None,
204
+ )
205
+
206
+ if not plugin:
207
+ raise click.ClickException(f"插件 {name} 不需要更新或无法更新")
208
+
209
+ manage_plugin(plugin, plug_path, is_update=True, proxy=proxy)
210
+ else:
211
+ need_update_plugins = [
212
+ p for p in plugins if p["status"] == PluginStatus.NEED_UPDATE
213
+ ]
214
+
215
+ if not need_update_plugins:
216
+ click.echo("没有需要更新的插件")
217
+ return
218
+
219
+ click.echo(f"发现 {len(need_update_plugins)} 个插件需要更新")
220
+ for plugin in need_update_plugins:
221
+ plugin_name = plugin["name"]
222
+ click.echo(f"正在更新插件 {plugin_name}...")
223
+ manage_plugin(plugin, plug_path, is_update=True, proxy=proxy)
224
+
225
+
226
+ @plug.command()
227
+ @click.argument("query")
228
+ def search(query: str):
229
+ """搜索插件"""
230
+ base_path = _get_data_path()
231
+ plugins = build_plug_list(base_path / "plugins")
232
+
233
+ matched_plugins = [
234
+ p
235
+ for p in plugins
236
+ if query.lower() in p["name"].lower()
237
+ or query.lower() in p["desc"].lower()
238
+ or query.lower() in p["author"].lower()
239
+ ]
240
+
241
+ if not matched_plugins:
242
+ click.echo(f"未找到匹配 '{query}' 的插件")
243
+ return
244
+
245
+ display_plugins(matched_plugins, f"搜索结果: '{query}'", "cyan")
@@ -0,0 +1,62 @@
1
+ import asyncio
2
+ import os
3
+ import sys
4
+ import traceback
5
+ from pathlib import Path
6
+
7
+ import click
8
+ from filelock import FileLock, Timeout
9
+
10
+ from ..utils import check_astrbot_root, check_dashboard, get_astrbot_root
11
+
12
+
13
+ async def run_astrbot(astrbot_root: Path):
14
+ """运行 AstrBot"""
15
+ from astrbot.core import LogBroker, LogManager, db_helper, logger
16
+ from astrbot.core.initial_loader import InitialLoader
17
+
18
+ await check_dashboard(astrbot_root / "data")
19
+
20
+ log_broker = LogBroker()
21
+ LogManager.set_queue_handler(logger, log_broker)
22
+ db = db_helper
23
+
24
+ core_lifecycle = InitialLoader(db, log_broker)
25
+
26
+ await core_lifecycle.start()
27
+
28
+
29
+ @click.option("--reload", "-r", is_flag=True, help="插件自动重载")
30
+ @click.option("--port", "-p", help="Astrbot Dashboard端口", required=False, type=str)
31
+ @click.command()
32
+ def run(reload: bool, port: str) -> None:
33
+ """运行 AstrBot"""
34
+ try:
35
+ os.environ["ASTRBOT_CLI"] = "1"
36
+ astrbot_root = get_astrbot_root()
37
+
38
+ if not check_astrbot_root(astrbot_root):
39
+ raise click.ClickException(
40
+ f"{astrbot_root}不是有效的 AstrBot 根目录,如需初始化请使用 astrbot init",
41
+ )
42
+
43
+ os.environ["ASTRBOT_ROOT"] = str(astrbot_root)
44
+ sys.path.insert(0, str(astrbot_root))
45
+
46
+ if port:
47
+ os.environ["DASHBOARD_PORT"] = port
48
+
49
+ if reload:
50
+ click.echo("启用插件自动重载")
51
+ os.environ["ASTRBOT_RELOAD"] = "1"
52
+
53
+ lock_file = astrbot_root / "astrbot.lock"
54
+ lock = FileLock(lock_file, timeout=5)
55
+ with lock.acquire():
56
+ asyncio.run(run_astrbot(astrbot_root))
57
+ except KeyboardInterrupt:
58
+ click.echo("AstrBot 已关闭...")
59
+ except Timeout:
60
+ raise click.ClickException("无法获取锁文件,请检查是否有其他实例正在运行")
61
+ except Exception as e:
62
+ raise click.ClickException(f"运行时出现错误: {e}\n{traceback.format_exc()}")
@@ -0,0 +1,18 @@
1
+ from .basic import (
2
+ check_astrbot_root,
3
+ check_dashboard,
4
+ get_astrbot_root,
5
+ )
6
+ from .plugin import PluginStatus, build_plug_list, get_git_repo, manage_plugin
7
+ from .version_comparator import VersionComparator
8
+
9
+ __all__ = [
10
+ "PluginStatus",
11
+ "VersionComparator",
12
+ "build_plug_list",
13
+ "check_astrbot_root",
14
+ "check_dashboard",
15
+ "get_astrbot_root",
16
+ "get_git_repo",
17
+ "manage_plugin",
18
+ ]
@@ -0,0 +1,76 @@
1
+ from pathlib import Path
2
+
3
+ import click
4
+
5
+
6
+ def check_astrbot_root(path: str | Path) -> bool:
7
+ """检查路径是否为 AstrBot 根目录"""
8
+ if not isinstance(path, Path):
9
+ path = Path(path)
10
+ if not path.exists() or not path.is_dir():
11
+ return False
12
+ if not (path / ".astrbot").exists():
13
+ return False
14
+ return True
15
+
16
+
17
+ def get_astrbot_root() -> Path:
18
+ """获取Astrbot根目录路径"""
19
+ return Path.cwd()
20
+
21
+
22
+ async def check_dashboard(astrbot_root: Path) -> None:
23
+ """检查是否安装了dashboard"""
24
+ from astrbot.core.config.default import VERSION
25
+ from astrbot.core.utils.io import download_dashboard, get_dashboard_version
26
+
27
+ from .version_comparator import VersionComparator
28
+
29
+ try:
30
+ dashboard_version = await get_dashboard_version()
31
+ match dashboard_version:
32
+ case None:
33
+ click.echo("未安装管理面板")
34
+ if click.confirm(
35
+ "是否安装管理面板?",
36
+ default=True,
37
+ abort=True,
38
+ ):
39
+ click.echo("正在安装管理面板...")
40
+ await download_dashboard(
41
+ path="data/dashboard.zip",
42
+ extract_path=str(astrbot_root),
43
+ version=f"v{VERSION}",
44
+ latest=False,
45
+ )
46
+ click.echo("管理面板安装完成")
47
+
48
+ case str():
49
+ if VersionComparator.compare_version(VERSION, dashboard_version) <= 0:
50
+ click.echo("管理面板已是最新版本")
51
+ return
52
+ try:
53
+ version = dashboard_version.split("v")[1]
54
+ click.echo(f"管理面板版本: {version}")
55
+ await download_dashboard(
56
+ path="data/dashboard.zip",
57
+ extract_path=str(astrbot_root),
58
+ version=f"v{VERSION}",
59
+ latest=False,
60
+ )
61
+ except Exception as e:
62
+ click.echo(f"下载管理面板失败: {e}")
63
+ return
64
+ except FileNotFoundError:
65
+ click.echo("初始化管理面板目录...")
66
+ try:
67
+ await download_dashboard(
68
+ path=str(astrbot_root / "dashboard.zip"),
69
+ extract_path=str(astrbot_root),
70
+ version=f"v{VERSION}",
71
+ latest=False,
72
+ )
73
+ click.echo("管理面板初始化完成")
74
+ except Exception as e:
75
+ click.echo(f"下载管理面板失败: {e}")
76
+ return