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,77 +1,137 @@
1
- import aiohttp
2
- import os
1
+ import asyncio
2
+ import logging
3
+ import random
3
4
  import ssl
5
+
6
+ import aiohttp
4
7
  import certifi
5
8
 
6
- from . import RenderStrategy
7
9
  from astrbot.core.config import VERSION
8
10
  from astrbot.core.utils.io import download_image_by_url
11
+ from astrbot.core.utils.t2i.template_manager import TemplateManager
12
+
13
+ from . import RenderStrategy
9
14
 
10
15
  ASTRBOT_T2I_DEFAULT_ENDPOINT = "https://t2i.soulter.top/text2img"
11
16
 
17
+ logger = logging.getLogger("astrbot")
18
+
12
19
 
13
20
  class NetworkRenderStrategy(RenderStrategy):
14
- def __init__(self, base_url: str = ASTRBOT_T2I_DEFAULT_ENDPOINT) -> None:
21
+ def __init__(self, base_url: str | None = None) -> None:
15
22
  super().__init__()
16
23
  if not base_url:
17
- base_url = ASTRBOT_T2I_DEFAULT_ENDPOINT
18
- self.BASE_RENDER_URL = base_url
19
- self.TEMPLATE_PATH = os.path.join(os.path.dirname(__file__), "template")
24
+ self.BASE_RENDER_URL = ASTRBOT_T2I_DEFAULT_ENDPOINT
25
+ else:
26
+ self.BASE_RENDER_URL = self._clean_url(base_url)
20
27
 
21
- if self.BASE_RENDER_URL.endswith("/"):
22
- self.BASE_RENDER_URL = self.BASE_RENDER_URL[:-1]
23
- if not self.BASE_RENDER_URL.endswith("text2img"):
24
- self.BASE_RENDER_URL += "/text2img"
28
+ self.endpoints = [self.BASE_RENDER_URL]
29
+ self.template_manager = TemplateManager()
25
30
 
26
- def set_endpoint(self, base_url: str):
27
- if not base_url:
28
- base_url = ASTRBOT_T2I_DEFAULT_ENDPOINT
29
- self.BASE_RENDER_URL = base_url
31
+ async def initialize(self):
32
+ if self.BASE_RENDER_URL == ASTRBOT_T2I_DEFAULT_ENDPOINT:
33
+ asyncio.create_task(self.get_official_endpoints())
30
34
 
31
- if self.BASE_RENDER_URL.endswith("/"):
32
- self.BASE_RENDER_URL = self.BASE_RENDER_URL[:-1]
33
- if not self.BASE_RENDER_URL.endswith("text2img"):
34
- self.BASE_RENDER_URL += "/text2img"
35
+ async def get_template(self, name: str = "base") -> str:
36
+ """通过名称获取文转图 HTML 模板"""
37
+ return self.template_manager.get_template(name)
38
+
39
+ async def get_official_endpoints(self):
40
+ """获取官方的 t2i 端点列表。"""
41
+ try:
42
+ async with aiohttp.ClientSession() as session:
43
+ async with session.get(
44
+ "https://api.soulter.top/astrbot/t2i-endpoints",
45
+ ) as resp:
46
+ if resp.status == 200:
47
+ data = await resp.json()
48
+ all_endpoints: list[dict] = data.get("data", [])
49
+ self.endpoints = [
50
+ ep.get("url")
51
+ for ep in all_endpoints
52
+ if ep.get("active") and ep.get("url")
53
+ ]
54
+ logger.info(
55
+ f"Successfully got {len(self.endpoints)} official T2I endpoints.",
56
+ )
57
+ except Exception as e:
58
+ logger.error(f"Failed to get official endpoints: {e}")
59
+
60
+ def _clean_url(self, url: str):
61
+ url = url.removesuffix("/")
62
+ if not url.endswith("text2img"):
63
+ url += "/text2img"
64
+ return url
35
65
 
36
66
  async def render_custom_template(
37
- self, tmpl_str: str, tmpl_data: dict, return_url: bool = True
67
+ self,
68
+ tmpl_str: str,
69
+ tmpl_data: dict,
70
+ return_url: bool = True,
71
+ options: dict | None = None,
38
72
  ) -> str:
39
73
  """使用自定义文转图模板"""
74
+ default_options = {"full_page": True, "type": "jpeg", "quality": 40}
75
+ if options:
76
+ default_options |= options
77
+
40
78
  post_data = {
41
79
  "tmpl": tmpl_str,
42
80
  "json": return_url,
43
81
  "tmpldata": tmpl_data,
44
- "options": {
45
- "full_page": True,
46
- "type": "jpeg",
47
- "quality": 40,
48
- },
82
+ "options": default_options,
49
83
  }
50
- if return_url:
51
- ssl_context = ssl.create_default_context(cafile=certifi.where())
52
- connector = aiohttp.TCPConnector(ssl=ssl_context)
53
- async with aiohttp.ClientSession(
54
- trust_env=True, connector=connector
55
- ) as session:
56
- async with session.post(
57
- f"{self.BASE_RENDER_URL}/generate", json=post_data
58
- ) as resp:
59
- ret = await resp.json()
60
- return f"{self.BASE_RENDER_URL}/{ret['data']['id']}"
61
- return await download_image_by_url(
62
- f"{self.BASE_RENDER_URL}/generate", post=True, post_data=post_data
63
- )
64
84
 
65
- async def render(self, text: str, return_url: bool = False) -> str:
66
- """
67
- 返回图像的文件路径
68
- """
69
- with open(
70
- os.path.join(self.TEMPLATE_PATH, "base.html"), "r", encoding="utf-8"
71
- ) as f:
72
- tmpl_str = f.read()
73
- assert tmpl_str
85
+ endpoints = self.endpoints.copy() if self.endpoints else [self.BASE_RENDER_URL]
86
+ random.shuffle(endpoints)
87
+ last_exception = None
88
+ for endpoint in endpoints:
89
+ try:
90
+ if return_url:
91
+ ssl_context = ssl.create_default_context(cafile=certifi.where())
92
+ connector = aiohttp.TCPConnector(ssl=ssl_context)
93
+ async with (
94
+ aiohttp.ClientSession(
95
+ trust_env=True,
96
+ connector=connector,
97
+ ) as session,
98
+ session.post(
99
+ f"{endpoint}/generate",
100
+ json=post_data,
101
+ ) as resp,
102
+ ):
103
+ if resp.status == 200:
104
+ ret = await resp.json()
105
+ return f"{endpoint}/{ret['data']['id']}"
106
+ raise Exception(f"HTTP {resp.status}")
107
+ else:
108
+ # download_image_by_url 失败时抛异常
109
+ return await download_image_by_url(
110
+ f"{endpoint}/generate",
111
+ post=True,
112
+ post_data=post_data,
113
+ )
114
+ except Exception as e:
115
+ last_exception = e
116
+ logger.warning(f"Endpoint {endpoint} failed: {e}, trying next...")
117
+ continue
118
+ # 全部失败
119
+ logger.error(f"All endpoints failed: {last_exception}")
120
+ raise RuntimeError(f"All endpoints failed: {last_exception}")
121
+
122
+ async def render(
123
+ self,
124
+ text: str,
125
+ return_url: bool = False,
126
+ template_name: str | None = "base",
127
+ ) -> str:
128
+ """返回图像的文件路径"""
129
+ if not template_name:
130
+ template_name = "base"
131
+ tmpl_str = await self.get_template(name=template_name)
74
132
  text = text.replace("`", "\\`")
75
133
  return await self.render_custom_template(
76
- tmpl_str, {"text": text, "version": f"v{VERSION}"}, return_url
134
+ tmpl_str,
135
+ {"text": text, "version": f"v{VERSION}"},
136
+ return_url,
77
137
  )
@@ -1,45 +1,60 @@
1
- from .network_strategy import NetworkRenderStrategy
2
- from .local_strategy import LocalRenderStrategy
3
1
  from astrbot.core.log import LogManager
4
2
 
3
+ from .local_strategy import LocalRenderStrategy
4
+ from .network_strategy import NetworkRenderStrategy
5
+
5
6
  logger = LogManager.GetLogger(log_name="astrbot")
6
7
 
7
8
 
8
9
  class HtmlRenderer:
9
- def __init__(self, endpoint_url: str = None):
10
+ def __init__(self, endpoint_url: str | None = None):
10
11
  self.network_strategy = NetworkRenderStrategy(endpoint_url)
11
12
  self.local_strategy = LocalRenderStrategy()
12
13
 
13
- def set_network_endpoint(self, endpoint_url: str):
14
- """设置 t2i 的网络端点。"""
15
- logger.info("文本转图像服务接口: " + endpoint_url)
16
- self.network_strategy.set_endpoint(endpoint_url)
14
+ async def initialize(self):
15
+ await self.network_strategy.initialize()
17
16
 
18
17
  async def render_custom_template(
19
- self, tmpl_str: str, tmpl_data: dict, return_url: bool = False
18
+ self,
19
+ tmpl_str: str,
20
+ tmpl_data: dict,
21
+ return_url: bool = False,
22
+ options: dict | None = None,
20
23
  ):
21
24
  """使用自定义文转图模板。该方法会通过网络调用 t2i 终结点图文渲染API。
22
25
  @param tmpl_str: HTML Jinja2 模板。
23
26
  @param tmpl_data: jinja2 模板数据。
27
+ @param options: 渲染选项。
24
28
 
25
29
  @return: 图片 URL 或者文件路径,取决于 return_url 参数。
26
30
 
27
31
  @example: 参见 https://astrbot.app 插件开发部分。
28
32
  """
29
- local = locals()
30
- local.pop("self")
31
- return await self.network_strategy.render_custom_template(**local)
33
+ return await self.network_strategy.render_custom_template(
34
+ tmpl_str,
35
+ tmpl_data,
36
+ return_url,
37
+ options,
38
+ )
32
39
 
33
40
  async def render_t2i(
34
- self, text: str, use_network: bool = True, return_url: bool = False
41
+ self,
42
+ text: str,
43
+ use_network: bool = True,
44
+ return_url: bool = False,
45
+ template_name: str | None = None,
35
46
  ):
36
47
  """使用默认文转图模板。"""
37
48
  if use_network:
38
49
  try:
39
- return await self.network_strategy.render(text, return_url=return_url)
50
+ return await self.network_strategy.render(
51
+ text,
52
+ return_url=return_url,
53
+ template_name=template_name,
54
+ )
40
55
  except BaseException as e:
41
56
  logger.error(
42
- f"Failed to render image via AstrBot API: {e}. Falling back to local rendering."
57
+ f"Failed to render image via AstrBot API: {e}. Falling back to local rendering.",
43
58
  )
44
59
  return await self.local_strategy.render(text)
45
60
  else:
@@ -0,0 +1,184 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8"/>
5
+ <title>Astrbot PowerShell {{ version }} </title>
6
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/katex.min.css" integrity="sha384-wcIxkf4k558AjM3Yz3BBFQUbk/zgIYC2R0QpeeYb+TwlBVMrlgLqwRjRtGZiK7ww" crossorigin="anonymous">
7
+ <script src="https://cdn.jsdelivr.net/npm/highlight.js@11.9.0/lib/common.min.js"></script>
8
+ <script>hljs.highlightAll();</script>
9
+ <script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/katex.min.js" integrity="sha384-hIoBPJpTUs74ddyc4bFZSM1TVlQDA60VBbJS0oA934VSz82sBx1X7kSx2ATBDIyd" crossorigin="anonymous"></script>
10
+ <script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/contrib/auto-render.min.js" integrity="sha384-43gviWU0YVjaDtb/GhzOouOXtZMP/7XUzwPTstBeZFe/+rCMvRwr4yROQP43s0Xk" crossorigin="anonymous"
11
+ onload="renderMathInElement(document.getElementById('content'),{delimiters: [{left: '$$', right: '$$', display: true},{left: '$', right: '$', display: false}]});"></script>
12
+ <style>
13
+ :root {
14
+ --bg-color: #010409;
15
+ --text-color: #e6edf3;
16
+ --title-bar-color: #161b22;
17
+ --title-text-color: #e6edf3;
18
+ --font-family: 'Consolas', 'Microsoft YaHei Mono', 'Dengxian Mono', 'Courier New', monospace;
19
+ --glow-color: rgba(200, 220, 255, 0.7);
20
+ }
21
+
22
+ @keyframes scanline {
23
+ 0% {
24
+ background-position: 0 0;
25
+ }
26
+ 100% {
27
+ background-position: 0 100%;
28
+ }
29
+ }
30
+
31
+ body {
32
+ background-color: var(--bg-color);
33
+ color: var(--text-color);
34
+ font-family: var(--font-family);
35
+ margin: 0;
36
+ padding: 0;
37
+ line-height: 1.6;
38
+ font-size: 18px;
39
+ /* The CRT glow effect from the image */
40
+ text-shadow: 0 0 15px var(--glow-color), 0 0 7px rgba(255, 255, 255, 1);
41
+ position: relative;
42
+ overflow: hidden;
43
+ }
44
+
45
+ body::after {
46
+ content: " ";
47
+ display: block;
48
+ position: absolute;
49
+ top: 0;
50
+ left: 0;
51
+ right: 0;
52
+ bottom: 0;
53
+ background: linear-gradient(to bottom, transparent 50%, rgba(0, 0, 0, 0.3) 50%);
54
+ background-size: 100% 4px;
55
+ z-index: 2;
56
+ pointer-events: none;
57
+ animation: scanline 8s linear infinite;
58
+ }
59
+
60
+ .header {
61
+ background-color: var(--title-bar-color);
62
+ padding: 12px 18px;
63
+ color: var(--title-text-color);
64
+ font-size: 16px;
65
+ border-bottom: 1px solid #30363d;
66
+ text-shadow: none; /* No glow for title bar */
67
+ }
68
+
69
+ .header .title {
70
+ font-weight: bold;
71
+ font-size: 28px;
72
+ }
73
+
74
+ .header .version {
75
+ opacity: 0.8;
76
+ margin-left: 1rem;
77
+ }
78
+
79
+ main {
80
+ padding: 1rem 1.5rem;
81
+ }
82
+
83
+ #content {
84
+ /* min-width and max-width removed as per request */
85
+ }
86
+
87
+ /* --- Markdown Styles adjusted for terminal look --- */
88
+ h1, h2, h3, h4, h5, h6 {
89
+ line-height: 1.4;
90
+ margin-top: 20px;
91
+ margin-bottom: 10px;
92
+ padding-bottom: 5px;
93
+ border-bottom: 1px solid #30363d;
94
+ color: var(--text-color);
95
+ }
96
+ h1 { font-size: 2rem; }
97
+ h2 { font-size: 1.7rem; }
98
+ h3 { font-size: 1.4rem; }
99
+
100
+ p {
101
+ margin-top: 1rem;
102
+ margin-bottom: 1rem;
103
+ }
104
+
105
+ strong {
106
+ color: var(--text-color);
107
+ font-weight: bold;
108
+ }
109
+
110
+ img {
111
+ max-width: 100%;
112
+ border: 1px solid #30363d;
113
+ display: block;
114
+ margin: 1rem auto;
115
+ }
116
+
117
+ hr {
118
+ border: 0;
119
+ border-top: 1px dashed #30363d;
120
+ margin: 2rem 0;
121
+ }
122
+
123
+ code {
124
+ font-family: var(--font-family);
125
+ padding: 0.2em 0.4em;
126
+ margin: 0;
127
+ font-size: 90%;
128
+ background-color: #161b22;
129
+ border-radius: 4px;
130
+ }
131
+
132
+ pre {
133
+ font-family: var(--font-family);
134
+ border-radius: 4px;
135
+ background: #0d1117;
136
+ padding: 1rem;
137
+ overflow-x: auto;
138
+ border: 1px solid #30363d;
139
+ }
140
+
141
+ pre > code {
142
+ padding: 0;
143
+ margin: 0;
144
+ font-size: 100%;
145
+ background-color: transparent;
146
+ border-radius: 0;
147
+ text-shadow: none; /* Disable glow inside code blocks for clarity */
148
+ }
149
+
150
+ a {
151
+ color: #58a6ff;
152
+ text-decoration: underline;
153
+ }
154
+ a:hover {
155
+ text-decoration: underline;
156
+ }
157
+
158
+ blockquote {
159
+ border-left: 4px solid #30363d;
160
+ padding: 0.5rem 1rem;
161
+ margin: 1.5rem 0;
162
+ color: #8b949e;
163
+ background-color: #161b22;
164
+ }
165
+ </style>
166
+ </head>
167
+ <body>
168
+
169
+ <div class="header">
170
+ <span class="title">> Astrbot PowerShell</span>
171
+ <span class="version">{{ version }}</span>
172
+ </div>
173
+
174
+ <main>
175
+ <div id="content"></div>
176
+ </main>
177
+
178
+ <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
179
+ <script>
180
+ document.getElementById('content').innerHTML = marked.parse(`{{ text | safe }}`);
181
+ </script>
182
+
183
+ </body>
184
+ </html>
@@ -0,0 +1,111 @@
1
+ # astrbot/core/utils/t2i/template_manager.py
2
+
3
+ import os
4
+ import shutil
5
+
6
+ from astrbot.core.utils.astrbot_path import get_astrbot_data_path, get_astrbot_path
7
+
8
+
9
+ class TemplateManager:
10
+ """负责管理 t2i HTML 模板的 CRUD 和重置操作。
11
+ 采用“用户覆盖内置”策略:用户模板存储在 data 目录中,并优先于内置模板加载。
12
+ 所有创建、更新、删除操作仅影响用户目录,以确保更新框架时用户数据安全。
13
+ """
14
+
15
+ CORE_TEMPLATES = ["base.html", "astrbot_powershell.html"]
16
+
17
+ def __init__(self):
18
+ self.builtin_template_dir = os.path.join(
19
+ get_astrbot_path(),
20
+ "astrbot",
21
+ "core",
22
+ "utils",
23
+ "t2i",
24
+ "template",
25
+ )
26
+ self.user_template_dir = os.path.join(get_astrbot_data_path(), "t2i_templates")
27
+
28
+ os.makedirs(self.user_template_dir, exist_ok=True)
29
+ self._initialize_user_templates()
30
+
31
+ def _copy_core_templates(self, overwrite: bool = False):
32
+ """从内置目录复制核心模板到用户目录。"""
33
+ for filename in self.CORE_TEMPLATES:
34
+ src = os.path.join(self.builtin_template_dir, filename)
35
+ dst = os.path.join(self.user_template_dir, filename)
36
+ if os.path.exists(src) and (overwrite or not os.path.exists(dst)):
37
+ shutil.copyfile(src, dst)
38
+
39
+ def _initialize_user_templates(self):
40
+ """如果用户目录下缺少核心模板,则进行复制。"""
41
+ self._copy_core_templates(overwrite=False)
42
+
43
+ def _get_user_template_path(self, name: str) -> str:
44
+ """获取用户模板的完整路径,防止路径遍历漏洞。"""
45
+ if ".." in name or "/" in name or "\\" in name:
46
+ raise ValueError("模板名称包含非法字符。")
47
+ return os.path.join(self.user_template_dir, f"{name}.html")
48
+
49
+ def _read_file(self, path: str) -> str:
50
+ """读取文件内容。"""
51
+ with open(path, encoding="utf-8") as f:
52
+ return f.read()
53
+
54
+ def list_templates(self) -> list[dict]:
55
+ """列出所有可用模板。
56
+ 该列表是内置模板和用户模板的合并视图,用户模板将覆盖同名的内置模板。
57
+ """
58
+ dirs_to_scan = [self.builtin_template_dir, self.user_template_dir]
59
+ all_names = {
60
+ os.path.splitext(f)[0]
61
+ for d in dirs_to_scan
62
+ for f in os.listdir(d)
63
+ if f.endswith(".html")
64
+ }
65
+ return [
66
+ {"name": name, "is_default": name == "base"} for name in sorted(all_names)
67
+ ]
68
+
69
+ def get_template(self, name: str) -> str:
70
+ """获取指定模板的内容。
71
+ 优先从用户目录加载,如果不存在则回退到内置目录。
72
+ """
73
+ user_path = self._get_user_template_path(name)
74
+ if os.path.exists(user_path):
75
+ return self._read_file(user_path)
76
+
77
+ builtin_path = os.path.join(self.builtin_template_dir, f"{name}.html")
78
+ if os.path.exists(builtin_path):
79
+ return self._read_file(builtin_path)
80
+
81
+ raise FileNotFoundError("模板不存在。")
82
+
83
+ def create_template(self, name: str, content: str):
84
+ """在用户目录中创建一个新的模板文件。"""
85
+ path = self._get_user_template_path(name)
86
+ if os.path.exists(path):
87
+ raise FileExistsError("同名模板已存在。")
88
+ with open(path, "w", encoding="utf-8") as f:
89
+ f.write(content)
90
+
91
+ def update_template(self, name: str, content: str):
92
+ """更新一个模板。此操作始终写入用户目录。
93
+ 如果更新的是一个内置模板,此操作实际上会在用户目录中创建一个修改后的副本,
94
+ 从而实现对内置模板的“覆盖”。
95
+ """
96
+ path = self._get_user_template_path(name)
97
+ with open(path, "w", encoding="utf-8") as f:
98
+ f.write(content)
99
+
100
+ def delete_template(self, name: str):
101
+ """仅删除用户目录中的模板文件。
102
+ 如果删除的是一个覆盖了内置模板的用户模板,这将有效地“恢复”到内置版本。
103
+ """
104
+ path = self._get_user_template_path(name)
105
+ if not os.path.exists(path):
106
+ raise FileNotFoundError("用户模板不存在,无法删除。")
107
+ os.remove(path)
108
+
109
+ def reset_default_template(self):
110
+ """将核心模板从内置目录强制重置到用户目录。"""
111
+ self._copy_core_templates(overwrite=True)
@@ -1,6 +1,14 @@
1
+ import asyncio
2
+ import base64
3
+ import os
4
+ import subprocess
5
+ import tempfile
1
6
  import wave
2
7
  from io import BytesIO
3
8
 
9
+ from astrbot.core import logger
10
+ from astrbot.core.utils.astrbot_path import get_astrbot_data_path
11
+
4
12
 
5
13
  async def tencent_silk_to_wav(silk_path: str, output_path: str) -> str:
6
14
  import pysilk
@@ -28,7 +36,7 @@ async def wav_to_tencent_silk(wav_path: str, output_path: str) -> int:
28
36
  import pilk
29
37
  except (ImportError, ModuleNotFoundError) as _:
30
38
  raise Exception(
31
- "pilk 模块未安装,请前往管理面板->控制台->安装pip库 安装 pilk 这个库"
39
+ "pilk 模块未安装,请前往管理面板->控制台->安装pip库 安装 pilk 这个库",
32
40
  )
33
41
  # with wave.open(wav_path, 'rb') as wav:
34
42
  # wav_data = wav.readframes(wav.getnframes())
@@ -50,3 +58,109 @@ async def wav_to_tencent_silk(wav_path: str, output_path: str) -> int:
50
58
  rate = wav.getframerate()
51
59
  duration = pilk.encode(wav_path, output_path, pcm_rate=rate, tencent=True)
52
60
  return duration
61
+
62
+
63
+ async def convert_to_pcm_wav(input_path: str, output_path: str) -> str:
64
+ """将 MP3 或其他音频格式转换为 PCM 16bit WAV,采样率24000Hz,单声道。
65
+ 若转换失败则抛出异常。
66
+ """
67
+ try:
68
+ from pyffmpeg import FFmpeg
69
+
70
+ ff = FFmpeg()
71
+ ff.convert(input=input_path, output=output_path)
72
+ except Exception as e:
73
+ logger.debug(f"pyffmpeg 转换失败: {e}, 尝试使用 ffmpeg 命令行进行转换")
74
+
75
+ p = await asyncio.create_subprocess_exec(
76
+ "ffmpeg",
77
+ "-y",
78
+ "-i",
79
+ input_path,
80
+ "-acodec",
81
+ "pcm_s16le",
82
+ "-ar",
83
+ "24000",
84
+ "-ac",
85
+ "1",
86
+ "-af",
87
+ "apad=pad_dur=2",
88
+ "-fflags",
89
+ "+genpts",
90
+ "-hide_banner",
91
+ output_path,
92
+ stdout=subprocess.PIPE,
93
+ stderr=subprocess.PIPE,
94
+ )
95
+ stdout, stderr = await p.communicate()
96
+ logger.info(f"[FFmpeg] stdout: {stdout.decode().strip()}")
97
+ logger.debug(f"[FFmpeg] stderr: {stderr.decode().strip()}")
98
+ logger.info(f"[FFmpeg] return code: {p.returncode}")
99
+
100
+ if os.path.exists(output_path) and os.path.getsize(output_path) > 0:
101
+ return output_path
102
+ raise RuntimeError("生成的WAV文件不存在或为空")
103
+
104
+
105
+ async def audio_to_tencent_silk_base64(audio_path: str) -> tuple[str, float]:
106
+ """将 MP3/WAV 文件转为 Tencent Silk 并返回 base64 编码与时长(秒)。
107
+
108
+ 参数:
109
+ - audio_path: 输入音频文件路径(.mp3 或 .wav)
110
+
111
+ 返回:
112
+ - silk_b64: Base64 编码的 Silk 字符串
113
+ - duration: 音频时长(秒)
114
+ """
115
+ try:
116
+ import pilk
117
+ except ImportError as e:
118
+ raise Exception("未安装 pilk: pip install pilk") from e
119
+
120
+ temp_dir = os.path.join(get_astrbot_data_path(), "temp")
121
+ os.makedirs(temp_dir, exist_ok=True)
122
+
123
+ # 是否需要转换为 WAV
124
+ ext = os.path.splitext(audio_path)[1].lower()
125
+ temp_wav = tempfile.NamedTemporaryFile(
126
+ suffix=".wav",
127
+ delete=False,
128
+ dir=temp_dir,
129
+ ).name
130
+
131
+ if ext != ".wav":
132
+ await convert_to_pcm_wav(audio_path, temp_wav)
133
+ # 删除原文件
134
+ os.remove(audio_path)
135
+ wav_path = temp_wav
136
+ else:
137
+ wav_path = audio_path
138
+
139
+ with wave.open(wav_path, "rb") as wav_file:
140
+ rate = wav_file.getframerate()
141
+
142
+ silk_path = tempfile.NamedTemporaryFile(
143
+ suffix=".silk",
144
+ delete=False,
145
+ dir=temp_dir,
146
+ ).name
147
+
148
+ try:
149
+ duration = await asyncio.to_thread(
150
+ pilk.encode,
151
+ wav_path,
152
+ silk_path,
153
+ pcm_rate=rate,
154
+ tencent=True,
155
+ )
156
+
157
+ with open(silk_path, "rb") as f:
158
+ silk_bytes = await asyncio.to_thread(f.read)
159
+ silk_b64 = base64.b64encode(silk_bytes).decode("utf-8")
160
+
161
+ return silk_b64, duration # 已是秒
162
+ finally:
163
+ if os.path.exists(wav_path) and wav_path != audio_path:
164
+ os.remove(wav_path)
165
+ if os.path.exists(silk_path):
166
+ os.remove(silk_path)