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,58 +1,38 @@
1
- import random
2
1
  import asyncio
3
2
  import math
4
- import traceback
3
+ import random
4
+ from collections.abc import AsyncGenerator
5
+
5
6
  import astrbot.core.message.components as Comp
6
- from typing import Union, AsyncGenerator
7
- from ..stage import register_stage, Stage
8
- from ..context import PipelineContext
9
- from astrbot.core.platform.astr_message_event import AstrMessageEvent
10
- from astrbot.core.message.message_event_result import MessageChain, ResultContentType
11
7
  from astrbot.core import logger
12
- from astrbot.core.message.message_event_result import BaseMessageComponent
13
- from astrbot.core.star.star_handler import star_handlers_registry, EventType
14
- from astrbot.core.star.star import star_map
8
+ from astrbot.core.message.components import BaseMessageComponent, ComponentType
9
+ from astrbot.core.message.message_event_result import MessageChain, ResultContentType
10
+ from astrbot.core.platform.astr_message_event import AstrMessageEvent
11
+ from astrbot.core.star.star_handler import EventType
15
12
  from astrbot.core.utils.path_util import path_Mapping
16
13
 
14
+ from ..context import PipelineContext, call_event_hook
15
+ from ..stage import Stage, register_stage
16
+
17
17
 
18
18
  @register_stage
19
19
  class RespondStage(Stage):
20
20
  # 组件类型到其非空判断函数的映射
21
21
  _component_validators = {
22
22
  Comp.Plain: lambda comp: bool(
23
- comp.text and comp.text.strip()
23
+ comp.text and comp.text.strip(),
24
24
  ), # 纯文本消息需要strip
25
25
  Comp.Face: lambda comp: comp.id is not None, # QQ表情
26
26
  Comp.Record: lambda comp: bool(comp.file), # 语音
27
27
  Comp.Video: lambda comp: bool(comp.file), # 视频
28
28
  Comp.At: lambda comp: bool(comp.qq) or bool(comp.name), # @
29
- Comp.AtAll: lambda comp: True, # @所有人
30
- Comp.RPS: lambda comp: True, # 不知道是啥(未完成)
31
- Comp.Dice: lambda comp: True, # 骰子(未完成)
32
- Comp.Shake: lambda comp: True, # 摇一摇(未完成)
33
- Comp.Anonymous: lambda comp: True, # 匿名(未完成)
34
- Comp.Share: lambda comp: bool(comp.url) and bool(comp.title), # 分享
35
- Comp.Contact: lambda comp: True, # 联系人(未完成)
36
- Comp.Location: lambda comp: bool(comp.lat and comp.lon), # 位置
37
- Comp.Music: lambda comp: bool(comp._type)
38
- and bool(comp.url)
39
- and bool(comp.audio), # 音乐
40
29
  Comp.Image: lambda comp: bool(comp.file), # 图片
41
30
  Comp.Reply: lambda comp: bool(comp.id) and comp.sender_id is not None, # 回复
42
- Comp.RedBag: lambda comp: bool(comp.title), # 红包
43
31
  Comp.Poke: lambda comp: comp.id != 0 and comp.qq != 0, # 戳一戳
44
- Comp.Forward: lambda comp: bool(comp.id and comp.id.strip()), # 转发
45
- Comp.Node: lambda comp: bool(comp.name)
46
- and comp.uin != 0
47
- and bool(comp.content), # 一个转发节点
32
+ Comp.Node: lambda comp: bool(comp.content), # 转发节点
48
33
  Comp.Nodes: lambda comp: bool(comp.nodes), # 多个转发节点
49
- Comp.Xml: lambda comp: bool(comp.data and comp.data.strip()), # XML
50
- Comp.Json: lambda comp: bool(comp.data), # JSON
51
- Comp.CardImage: lambda comp: bool(comp.file), # 卡片图片
52
- Comp.TTS: lambda comp: bool(comp.text and comp.text.strip()), # 语音合成
53
- Comp.Unknown: lambda comp: bool(comp.text and comp.text.strip()), # 未知消息
54
- Comp.File: lambda comp: bool(comp.file), # 文件
55
- Comp.WechatEmoji: lambda comp: bool(comp.md5), # 微信表情
34
+ Comp.File: lambda comp: bool(comp.file_ or comp.url),
35
+ Comp.WechatEmoji: lambda comp: comp.md5 is not None, # 微信表情
56
36
  }
57
37
 
58
38
  async def initialize(self, ctx: PipelineContext):
@@ -79,7 +59,7 @@ class RespondStage(Stage):
79
59
  "segmented_reply"
80
60
  ]["interval_method"]
81
61
  self.log_base = float(
82
- ctx.astrbot_config["platform_settings"]["segmented_reply"]["log_base"]
62
+ ctx.astrbot_config["platform_settings"]["segmented_reply"]["log_base"],
83
63
  )
84
64
  interval_str: str = ctx.astrbot_config["platform_settings"]["segmented_reply"][
85
65
  "interval"
@@ -107,17 +87,16 @@ class RespondStage(Stage):
107
87
  wc = await self._word_cnt(comp.text)
108
88
  i = math.log(wc + 1, self.log_base)
109
89
  return random.uniform(i, i + 0.5)
110
- else:
111
- return random.uniform(1, 1.75)
112
- else:
113
- # random
114
- return random.uniform(self.interval[0], self.interval[1])
90
+ return random.uniform(1, 1.75)
91
+ # random
92
+ return random.uniform(self.interval[0], self.interval[1])
115
93
 
116
94
  async def _is_empty_message_chain(self, chain: list[BaseMessageComponent]):
117
95
  """检查消息链是否为空
118
96
 
119
97
  Args:
120
98
  chain (list[BaseMessageComponent]): 包含消息对象的列表
99
+
121
100
  """
122
101
  if not chain:
123
102
  return True
@@ -129,32 +108,77 @@ class RespondStage(Stage):
129
108
  if comp_type in self._component_validators:
130
109
  if self._component_validators[comp_type](comp):
131
110
  return False
132
- else:
133
- logger.info(f"空内容检查: 无法识别的组件类型: {comp_type.__name__}")
134
111
 
135
112
  # 如果所有组件都为空
136
113
  return True
137
114
 
115
+ def is_seg_reply_required(self, event: AstrMessageEvent) -> bool:
116
+ """检查是否需要分段回复"""
117
+ if not self.enable_seg:
118
+ return False
119
+
120
+ if self.only_llm_result and not event.get_result().is_llm_result():
121
+ return False
122
+
123
+ if event.get_platform_name() in [
124
+ "qq_official",
125
+ "weixin_official_account",
126
+ "dingtalk",
127
+ ]:
128
+ return False
129
+
130
+ return True
131
+
132
+ def _extract_comp(
133
+ self,
134
+ raw_chain: list[BaseMessageComponent],
135
+ extract_types: set[ComponentType],
136
+ modify_raw_chain: bool = True,
137
+ ):
138
+ extracted = []
139
+ if modify_raw_chain:
140
+ remaining = []
141
+ for comp in raw_chain:
142
+ if comp.type in extract_types:
143
+ extracted.append(comp)
144
+ else:
145
+ remaining.append(comp)
146
+ raw_chain[:] = remaining
147
+ else:
148
+ extracted = [comp for comp in raw_chain if comp.type in extract_types]
149
+
150
+ return extracted
151
+
138
152
  async def process(
139
- self, event: AstrMessageEvent
140
- ) -> Union[None, AsyncGenerator[None, None]]:
153
+ self,
154
+ event: AstrMessageEvent,
155
+ ) -> None | AsyncGenerator[None, None]:
141
156
  result = event.get_result()
142
157
  if result is None:
143
158
  return
144
159
  if result.result_content_type == ResultContentType.STREAMING_FINISH:
145
160
  return
146
161
 
162
+ logger.info(
163
+ f"Prepare to send - {event.get_sender_name()}/{event.get_sender_id()}: {event._outline_chain(result.chain)}",
164
+ )
165
+
147
166
  if result.result_content_type == ResultContentType.STREAMING_RESULT:
167
+ if result.async_stream is None:
168
+ logger.warning("async_stream 为空,跳过发送。")
169
+ return
148
170
  # 流式结果直接交付平台适配器处理
149
- use_fallback = self.config.get("provider_settings", {}).get(
150
- "streaming_segmented", False
171
+ realtime_segmenting = (
172
+ self.config.get("provider_settings", {}).get(
173
+ "unsupported_streaming_strategy",
174
+ "realtime_segmenting",
175
+ )
176
+ == "realtime_segmenting"
151
177
  )
152
- logger.info(f"应用流式输出({event.get_platform_name()})")
153
- await event._pre_send()
154
- await event.send_streaming(result.async_stream, use_fallback)
155
- await event._post_send()
178
+ logger.info(f"应用流式输出({event.get_platform_id()})")
179
+ await event.send_streaming(result.async_stream, realtime_segmenting)
156
180
  return
157
- elif len(result.chain) > 0:
181
+ if len(result.chain) > 0:
158
182
  # 检查路径映射
159
183
  if mappings := self.platform_settings.get("path_mapping", []):
160
184
  for idx, component in enumerate(result.chain):
@@ -163,71 +187,88 @@ class RespondStage(Stage):
163
187
  component.file = path_Mapping(mappings, component.file)
164
188
  event.get_result().chain[idx] = component
165
189
 
166
- await event._pre_send()
167
-
168
190
  # 检查消息链是否为空
169
191
  try:
170
192
  if await self._is_empty_message_chain(result.chain):
171
193
  logger.info("消息为空,跳过发送阶段")
172
- event.clear_result()
173
- event.stop_event()
174
194
  return
175
195
  except Exception as e:
176
196
  logger.warning(f"空内容检查异常: {e}")
177
197
 
178
- if self.enable_seg and (
179
- (self.only_llm_result and result.is_llm_result())
180
- or not self.only_llm_result
181
- ):
182
- decorated_comps = []
183
- if self.reply_with_mention:
184
- for comp in result.chain:
185
- if isinstance(comp, Comp.At):
186
- decorated_comps.append(comp)
187
- result.chain.remove(comp)
188
- break
189
- if self.reply_with_quote:
190
- for comp in result.chain:
191
- if isinstance(comp, Comp.Reply):
192
- decorated_comps.append(comp)
193
- result.chain.remove(comp)
194
- break
195
- # 分段回复
198
+ # Plain 为空的消息段移除
199
+ result.chain = [
200
+ comp
201
+ for comp in result.chain
202
+ if not (
203
+ isinstance(comp, Comp.Plain)
204
+ and (not comp.text or not comp.text.strip())
205
+ )
206
+ ]
207
+
208
+ # 发送消息链
209
+ # Record 需要强制单独发送
210
+ need_separately = {ComponentType.Record}
211
+ if self.is_seg_reply_required(event):
212
+ header_comps = self._extract_comp(
213
+ result.chain,
214
+ {ComponentType.Reply, ComponentType.At},
215
+ modify_raw_chain=True,
216
+ )
217
+ if not result.chain or len(result.chain) == 0:
218
+ # may fix #2670
219
+ logger.warning(
220
+ f"实际消息链为空, 跳过发送阶段。header_chain: {header_comps}, actual_chain: {result.chain}",
221
+ )
222
+ return
196
223
  for comp in result.chain:
197
224
  i = await self._calc_comp_interval(comp)
198
225
  await asyncio.sleep(i)
199
226
  try:
200
- await event.send(MessageChain([*decorated_comps, comp]))
227
+ if comp.type in need_separately:
228
+ await event.send(MessageChain([comp]))
229
+ else:
230
+ await event.send(MessageChain([*header_comps, comp]))
231
+ header_comps.clear()
201
232
  except Exception as e:
202
- logger.error(f"发送消息失败: {e} chain: {result.chain}")
203
- break
233
+ logger.error(
234
+ f"发送消息链失败: chain = {MessageChain([comp])}, error = {e}",
235
+ exc_info=True,
236
+ )
204
237
  else:
205
- try:
206
- await event.send(result)
207
- except Exception as e:
208
- logger.error(traceback.format_exc())
209
- logger.error(f"发送消息失败: {e} chain: {result.chain}")
210
- await event._post_send()
211
- logger.info(
212
- f"AstrBot -> {event.get_sender_name()}/{event.get_sender_id()}: {event._outline_chain(result.chain)}"
213
- )
214
-
215
- handlers = star_handlers_registry.get_handlers_by_event_type(
216
- EventType.OnAfterMessageSentEvent, platform_id=event.get_platform_id()
217
- )
218
- for handler in handlers:
219
- try:
220
- logger.debug(
221
- f"hook(on_after_message_sent) -> {star_map[handler.handler_module_path].name} - {handler.handler_name}"
238
+ if all(
239
+ comp.type in {ComponentType.Reply, ComponentType.At}
240
+ for comp in result.chain
241
+ ):
242
+ # may fix #2670
243
+ logger.warning(
244
+ f"消息链全为 Reply 和 At 消息段, 跳过发送阶段。chain: {result.chain}",
245
+ )
246
+ return
247
+ sep_comps = self._extract_comp(
248
+ result.chain,
249
+ need_separately,
250
+ modify_raw_chain=True,
222
251
  )
223
- await handler.handler(event)
224
- except BaseException:
225
- logger.error(traceback.format_exc())
252
+ for comp in sep_comps:
253
+ chain = MessageChain([comp])
254
+ try:
255
+ await event.send(chain)
256
+ except Exception as e:
257
+ logger.error(
258
+ f"发送消息链失败: chain = {chain}, error = {e}",
259
+ exc_info=True,
260
+ )
261
+ chain = MessageChain(result.chain)
262
+ if result.chain and len(result.chain) > 0:
263
+ try:
264
+ await event.send(chain)
265
+ except Exception as e:
266
+ logger.error(
267
+ f"发送消息链失败: chain = {chain}, error = {e}",
268
+ exc_info=True,
269
+ )
226
270
 
227
- if event.is_stopped():
228
- logger.info(
229
- f"{star_map[handler.handler_module_path].name} - {handler.handler_name} 终止了事件传播。"
230
- )
231
- return
271
+ if await call_event_hook(event, EventType.OnAfterMessageSentEvent):
272
+ return
232
273
 
233
274
  event.clear_result()
@@ -1,17 +1,19 @@
1
- import time
2
1
  import re
2
+ import time
3
3
  import traceback
4
- from typing import Union, AsyncGenerator
5
- from ..stage import Stage, register_stage, registered_stages
6
- from ..context import PipelineContext
7
- from astrbot.core.platform.astr_message_event import AstrMessageEvent
4
+ from collections.abc import AsyncGenerator
5
+
6
+ from astrbot.core import file_token_service, html_renderer, logger
7
+ from astrbot.core.message.components import At, File, Image, Node, Plain, Record, Reply
8
8
  from astrbot.core.message.message_event_result import ResultContentType
9
+ from astrbot.core.platform.astr_message_event import AstrMessageEvent
9
10
  from astrbot.core.platform.message_type import MessageType
10
- from astrbot.core import logger
11
- from astrbot.core.message.components import Plain, Image, At, Reply, Record, File, Node
12
- from astrbot.core import html_renderer
13
- from astrbot.core.star.star_handler import star_handlers_registry, EventType
11
+ from astrbot.core.star.session_llm_manager import SessionServiceManager
14
12
  from astrbot.core.star.star import star_map
13
+ from astrbot.core.star.star_handler import EventType, star_handlers_registry
14
+
15
+ from ..context import PipelineContext
16
+ from ..stage import Stage, register_stage, registered_stages
15
17
 
16
18
 
17
19
  @register_stage
@@ -28,12 +30,12 @@ class ResultDecorateStage(Stage):
28
30
  self.t2i_word_threshold = ctx.astrbot_config["t2i_word_threshold"]
29
31
  try:
30
32
  self.t2i_word_threshold = int(self.t2i_word_threshold)
31
- if self.t2i_word_threshold < 50:
32
- self.t2i_word_threshold = 50
33
+ self.t2i_word_threshold = max(self.t2i_word_threshold, 50)
33
34
  except BaseException:
34
35
  self.t2i_word_threshold = 150
35
36
  self.t2i_strategy = ctx.astrbot_config["t2i_strategy"]
36
37
  self.t2i_use_network = self.t2i_strategy == "remote"
38
+ self.t2i_active_template = ctx.astrbot_config["t2i_active_template"]
37
39
 
38
40
  self.forward_threshold = ctx.astrbot_config["platform_settings"][
39
41
  "forward_threshold"
@@ -43,7 +45,7 @@ class ResultDecorateStage(Stage):
43
45
  self.words_count_threshold = int(
44
46
  ctx.astrbot_config["platform_settings"]["segmented_reply"][
45
47
  "words_count_threshold"
46
- ]
48
+ ],
47
49
  )
48
50
  self.enable_segmented_reply = ctx.astrbot_config["platform_settings"][
49
51
  "segmented_reply"
@@ -62,13 +64,15 @@ class ResultDecorateStage(Stage):
62
64
  ]
63
65
  self.content_safe_check_stage = None
64
66
  if self.content_safe_check_reply:
65
- for stage in registered_stages:
66
- if stage.__class__.__name__ == "ContentSafetyCheckStage":
67
- self.content_safe_check_stage = stage
67
+ for stage_cls in registered_stages:
68
+ if stage_cls.__name__ == "ContentSafetyCheckStage":
69
+ self.content_safe_check_stage = stage_cls()
70
+ await self.content_safe_check_stage.initialize(ctx)
68
71
 
69
72
  async def process(
70
- self, event: AstrMessageEvent
71
- ) -> Union[None, AsyncGenerator[None, None]]:
73
+ self,
74
+ event: AstrMessageEvent,
75
+ ) -> None | AsyncGenerator[None, None]:
72
76
  result = event.get_result()
73
77
  if result is None or not result.chain:
74
78
  return
@@ -90,34 +94,36 @@ class ResultDecorateStage(Stage):
90
94
  if isinstance(comp, Plain):
91
95
  text += comp.text
92
96
  async for _ in self.content_safe_check_stage.process(
93
- event, check_text=text
97
+ event,
98
+ check_text=text,
94
99
  ):
95
100
  yield
96
101
 
97
102
  # 发送消息前事件钩子
98
103
  handlers = star_handlers_registry.get_handlers_by_event_type(
99
- EventType.OnDecoratingResultEvent, platform_id=event.get_platform_id()
104
+ EventType.OnDecoratingResultEvent,
105
+ plugins_name=event.plugins_name,
100
106
  )
101
107
  for handler in handlers:
102
108
  try:
103
109
  logger.debug(
104
- f"hook(on_decorating_result) -> {star_map[handler.handler_module_path].name} - {handler.handler_name}"
110
+ f"hook(on_decorating_result) -> {star_map[handler.handler_module_path].name} - {handler.handler_name}",
105
111
  )
106
112
  if is_stream:
107
113
  logger.warning(
108
- "启用流式输出时,依赖发送消息前事件钩子的插件可能无法正常工作"
114
+ "启用流式输出时,依赖发送消息前事件钩子的插件可能无法正常工作",
109
115
  )
110
116
  await handler.handler(event)
111
117
  if event.get_result() is None or not event.get_result().chain:
112
118
  logger.debug(
113
- f"hook(on_decorating_result) -> {star_map[handler.handler_module_path].name} - {handler.handler_name} 将消息结果清空。"
119
+ f"hook(on_decorating_result) -> {star_map[handler.handler_module_path].name} - {handler.handler_name} 将消息结果清空。",
114
120
  )
115
121
  except BaseException:
116
122
  logger.error(traceback.format_exc())
117
123
 
118
124
  if event.is_stopped():
119
125
  logger.info(
120
- f"{star_map[handler.handler_module_path].name} - {handler.handler_name} 终止了事件传播。"
126
+ f"{star_map[handler.handler_module_path].name} - {handler.handler_name} 终止了事件传播。",
121
127
  )
122
128
  return
123
129
 
@@ -140,7 +146,11 @@ class ResultDecorateStage(Stage):
140
146
  break
141
147
 
142
148
  # 分段回复
143
- if self.enable_segmented_reply:
149
+ if self.enable_segmented_reply and event.get_platform_name() not in [
150
+ "qq_official",
151
+ "weixin_official_account",
152
+ "dingtalk",
153
+ ]:
144
154
  if (
145
155
  self.only_llm_result and result.is_llm_result()
146
156
  ) or not self.only_llm_result:
@@ -151,9 +161,21 @@ class ResultDecorateStage(Stage):
151
161
  # 不分段回复
152
162
  new_chain.append(comp)
153
163
  continue
154
- split_response = re.findall(
155
- self.regex, comp.text, re.DOTALL | re.MULTILINE
156
- )
164
+ try:
165
+ split_response = re.findall(
166
+ self.regex,
167
+ comp.text,
168
+ re.DOTALL | re.MULTILINE,
169
+ )
170
+ except re.error:
171
+ logger.error(
172
+ f"分段回复正则表达式错误,使用默认分段方式: {traceback.format_exc()}",
173
+ )
174
+ split_response = re.findall(
175
+ r".*?[。?!~…]+|.+$",
176
+ comp.text,
177
+ re.DOTALL | re.MULTILINE,
178
+ )
157
179
  if not split_response:
158
180
  new_chain.append(comp)
159
181
  continue
@@ -168,68 +190,110 @@ class ResultDecorateStage(Stage):
168
190
  result.chain = new_chain
169
191
 
170
192
  # TTS
193
+ tts_provider = self.ctx.plugin_manager.context.get_using_tts_provider(
194
+ event.unified_msg_origin,
195
+ )
196
+
171
197
  if (
172
198
  self.ctx.astrbot_config["provider_tts_settings"]["enable"]
173
199
  and result.is_llm_result()
200
+ and SessionServiceManager.should_process_tts_request(event)
174
201
  ):
175
- tts_provider = self.ctx.plugin_manager.context.provider_manager.curr_tts_provider_inst
176
- new_chain = []
177
- for comp in result.chain:
178
- if isinstance(comp, Plain) and len(comp.text) > 1:
179
- try:
180
- logger.info("TTS 请求: " + comp.text)
181
- audio_path = await tts_provider.get_audio(comp.text)
182
- logger.info("TTS 结果: " + audio_path)
183
- if audio_path:
202
+ if not tts_provider:
203
+ logger.warning(
204
+ f"会话 {event.unified_msg_origin} 未配置文本转语音模型。",
205
+ )
206
+ else:
207
+ new_chain = []
208
+ for comp in result.chain:
209
+ if isinstance(comp, Plain) and len(comp.text) > 1:
210
+ try:
211
+ logger.info(f"TTS 请求: {comp.text}")
212
+ audio_path = await tts_provider.get_audio(comp.text)
213
+ logger.info(f"TTS 结果: {audio_path}")
214
+ if not audio_path:
215
+ logger.error(
216
+ f"由于 TTS 音频文件未找到,消息段转语音失败: {comp.text}",
217
+ )
218
+ new_chain.append(comp)
219
+ continue
220
+
221
+ use_file_service = self.ctx.astrbot_config[
222
+ "provider_tts_settings"
223
+ ]["use_file_service"]
224
+ callback_api_base = self.ctx.astrbot_config[
225
+ "callback_api_base"
226
+ ]
227
+ dual_output = self.ctx.astrbot_config[
228
+ "provider_tts_settings"
229
+ ]["dual_output"]
230
+
231
+ url = None
232
+ if use_file_service and callback_api_base:
233
+ token = await file_token_service.register_file(
234
+ audio_path,
235
+ )
236
+ url = f"{callback_api_base}/api/file/{token}"
237
+ logger.debug(f"已注册:{url}")
238
+
184
239
  new_chain.append(
185
- Record(file=audio_path, url=audio_path)
240
+ Record(
241
+ file=url or audio_path,
242
+ url=url or audio_path,
243
+ ),
186
244
  )
187
- if(self.ctx.astrbot_config["provider_tts_settings"]["dual_output"]):
245
+ if dual_output:
188
246
  new_chain.append(comp)
189
- else:
190
- logger.error(
191
- f"由于 TTS 音频文件没找到,消息段转语音失败: {comp.text}"
192
- )
247
+ except Exception:
248
+ logger.error(traceback.format_exc())
249
+ logger.error("TTS 失败,使用文本发送。")
193
250
  new_chain.append(comp)
194
- except BaseException:
195
- logger.error(traceback.format_exc())
196
- logger.error("TTS 失败,使用文本发送。")
251
+ else:
197
252
  new_chain.append(comp)
198
- else:
199
- new_chain.append(comp)
200
- result.chain = new_chain
253
+ result.chain = new_chain
201
254
 
202
255
  # 文本转图片
203
256
  elif (
204
257
  result.use_t2i_ is None and self.ctx.astrbot_config["t2i"]
205
258
  ) or result.use_t2i_:
206
- plain_str = ""
259
+ parts = []
207
260
  for comp in result.chain:
208
261
  if isinstance(comp, Plain):
209
- plain_str += "\n\n" + comp.text
262
+ parts.append("\n\n" + comp.text)
210
263
  else:
211
264
  break
265
+ plain_str = "".join(parts)
212
266
  if plain_str and len(plain_str) > self.t2i_word_threshold:
213
267
  render_start = time.time()
214
268
  try:
215
269
  url = await html_renderer.render_t2i(
216
- plain_str, return_url=True, use_network=self.t2i_use_network
270
+ plain_str,
271
+ return_url=True,
272
+ use_network=self.t2i_use_network,
273
+ template_name=self.t2i_active_template,
217
274
  )
218
275
  except BaseException:
219
276
  logger.error("文本转图片失败,使用文本发送。")
220
277
  return
221
278
  if time.time() - render_start > 3:
222
279
  logger.warning(
223
- "文本转图片耗时超过了 3 秒,如果觉得很慢可以使用 /t2i 关闭文本转图片模式。"
280
+ "文本转图片耗时超过了 3 秒,如果觉得很慢可以使用 /t2i 关闭文本转图片模式。",
224
281
  )
225
282
  if url:
226
283
  if url.startswith("http"):
227
284
  result.chain = [Image.fromURL(url)]
285
+ elif (
286
+ self.ctx.astrbot_config["t2i_use_file_service"]
287
+ and self.ctx.astrbot_config["callback_api_base"]
288
+ ):
289
+ token = await file_token_service.register_file(url)
290
+ url = f"{self.ctx.astrbot_config['callback_api_base']}/api/file/{token}"
291
+ logger.debug(f"已注册:{url}")
292
+ result.chain = [Image.fromURL(url)]
228
293
  else:
229
294
  result.chain = [Image.fromFileSystem(url)]
230
295
 
231
296
  # 触发转发消息
232
- has_forwarded = False
233
297
  if event.get_platform_name() == "aiocqhttp":
234
298
  word_cnt = 0
235
299
  for comp in result.chain:
@@ -237,19 +301,22 @@ class ResultDecorateStage(Stage):
237
301
  word_cnt += len(comp.text)
238
302
  if word_cnt > self.forward_threshold:
239
303
  node = Node(
240
- uin=event.get_self_id(), name="AstrBot", content=[*result.chain]
304
+ uin=event.get_self_id(),
305
+ name="AstrBot",
306
+ content=[*result.chain],
241
307
  )
242
308
  result.chain = [node]
243
- has_forwarded = True
244
309
 
245
- if not has_forwarded:
310
+ has_plain = any(isinstance(item, Plain) for item in result.chain)
311
+ if has_plain:
246
312
  # at 回复
247
313
  if (
248
314
  self.reply_with_mention
249
315
  and event.get_message_type() != MessageType.FRIEND_MESSAGE
250
316
  ):
251
317
  result.chain.insert(
252
- 0, At(qq=event.get_sender_id(), name=event.get_sender_name())
318
+ 0,
319
+ At(qq=event.get_sender_id(), name=event.get_sender_name()),
253
320
  )
254
321
  if len(result.chain) > 1 and isinstance(result.chain[1], Plain):
255
322
  result.chain[1].text = "\n" + result.chain[1].text