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
@@ -37,7 +37,10 @@ else:
37
37
  @register_platform_adapter("telegram", "telegram 适配器")
38
38
  class TelegramPlatformAdapter(Platform):
39
39
  def __init__(
40
- self, platform_config: dict, platform_settings: dict, event_queue: asyncio.Queue
40
+ self,
41
+ platform_config: dict,
42
+ platform_settings: dict,
43
+ event_queue: asyncio.Queue,
41
44
  ) -> None:
42
45
  super().__init__(event_queue)
43
46
  self.config = platform_config
@@ -45,13 +48,15 @@ class TelegramPlatformAdapter(Platform):
45
48
  self.client_self_id = uuid.uuid4().hex[:8]
46
49
 
47
50
  base_url = self.config.get(
48
- "telegram_api_base_url", "https://api.telegram.org/bot"
51
+ "telegram_api_base_url",
52
+ "https://api.telegram.org/bot",
49
53
  )
50
54
  if not base_url:
51
55
  base_url = "https://api.telegram.org/bot"
52
56
 
53
57
  file_base_url = self.config.get(
54
- "telegram_file_base_url", "https://api.telegram.org/file/bot"
58
+ "telegram_file_base_url",
59
+ "https://api.telegram.org/file/bot",
55
60
  )
56
61
  if not file_base_url:
57
62
  file_base_url = "https://api.telegram.org/file/bot"
@@ -59,10 +64,12 @@ class TelegramPlatformAdapter(Platform):
59
64
  self.base_url = base_url
60
65
 
61
66
  self.enable_command_register = self.config.get(
62
- "telegram_command_register", True
67
+ "telegram_command_register",
68
+ True,
63
69
  )
64
70
  self.enable_command_refresh = self.config.get(
65
- "telegram_command_auto_refresh", True
71
+ "telegram_command_auto_refresh",
72
+ True,
66
73
  )
67
74
  self.last_command_hash = None
68
75
 
@@ -85,19 +92,22 @@ class TelegramPlatformAdapter(Platform):
85
92
 
86
93
  @override
87
94
  async def send_by_session(
88
- self, session: MessageSesion, message_chain: MessageChain
95
+ self,
96
+ session: MessageSesion,
97
+ message_chain: MessageChain,
89
98
  ):
90
99
  from_username = session.session_id
91
100
  await TelegramPlatformEvent.send_with_client(
92
- self.client, message_chain, from_username
101
+ self.client,
102
+ message_chain,
103
+ from_username,
93
104
  )
94
105
  await super().send_by_session(session, message_chain)
95
106
 
96
107
  @override
97
108
  def meta(self) -> PlatformMetadata:
98
- return PlatformMetadata(
99
- name="telegram", description="telegram 适配器", id=self.config.get("id")
100
- )
109
+ id_ = self.config.get("id") or "telegram"
110
+ return PlatformMetadata(name="telegram", description="telegram 适配器", id=id_)
101
111
 
102
112
  @override
103
113
  async def run(self):
@@ -117,6 +127,10 @@ class TelegramPlatformAdapter(Platform):
117
127
  )
118
128
  self.scheduler.start()
119
129
 
130
+ if not self.application.updater:
131
+ logger.error("Telegram Updater is not initialized. Cannot start polling.")
132
+ return
133
+
120
134
  queue = self.application.updater.start_polling()
121
135
  logger.info("Telegram Platform Adapter is running.")
122
136
  await queue
@@ -128,7 +142,7 @@ class TelegramPlatformAdapter(Platform):
128
142
 
129
143
  if commands:
130
144
  current_hash = hash(
131
- tuple((cmd.command, cmd.description) for cmd in commands)
145
+ tuple((cmd.command, cmd.description) for cmd in commands),
132
146
  )
133
147
  if current_hash == self.last_command_hash:
134
148
  return
@@ -144,13 +158,15 @@ class TelegramPlatformAdapter(Platform):
144
158
  command_dict = {}
145
159
  skip_commands = {"start"}
146
160
 
147
- for handler_md in star_handlers_registry._handlers:
148
- handler_metadata = handler_md[1]
161
+ for handler_md in star_handlers_registry:
162
+ handler_metadata = handler_md
149
163
  if not star_map[handler_metadata.handler_module_path].activated:
150
164
  continue
151
165
  for event_filter in handler_metadata.event_filters:
152
166
  cmd_info = self._extract_command_info(
153
- event_filter, handler_metadata, skip_commands
167
+ event_filter,
168
+ handler_metadata,
169
+ skip_commands,
154
170
  )
155
171
  if cmd_info:
156
172
  cmd_name, description = cmd_info
@@ -161,7 +177,9 @@ class TelegramPlatformAdapter(Platform):
161
177
 
162
178
  @staticmethod
163
179
  def _extract_command_info(
164
- event_filter, handler_metadata, skip_commands: set
180
+ event_filter,
181
+ handler_metadata,
182
+ skip_commands: set,
165
183
  ) -> tuple[str, str] | None:
166
184
  """从事件过滤器中提取指令信息"""
167
185
  cmd_name = None
@@ -183,7 +201,6 @@ class TelegramPlatformAdapter(Platform):
183
201
  return None
184
202
 
185
203
  if not re.match(r"^[a-z0-9_]+$", cmd_name) or len(cmd_name) > 32:
186
- logger.debug(f"跳过无法注册的命令: {cmd_name}")
187
204
  return None
188
205
 
189
206
  # Build description.
@@ -195,8 +212,14 @@ class TelegramPlatformAdapter(Platform):
195
212
  return cmd_name, description
196
213
 
197
214
  async def start(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
215
+ if not update.effective_chat:
216
+ logger.warning(
217
+ "Received a start command without an effective chat, skipping /start reply.",
218
+ )
219
+ return
198
220
  await context.bot.send_message(
199
- chat_id=update.effective_chat.id, text=self.config["start_message"]
221
+ chat_id=update.effective_chat.id,
222
+ text=self.config["start_message"],
200
223
  )
201
224
 
202
225
  async def message_handler(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
@@ -206,16 +229,24 @@ class TelegramPlatformAdapter(Platform):
206
229
  await self.handle_msg(abm)
207
230
 
208
231
  async def convert_message(
209
- self, update: Update, context: ContextTypes.DEFAULT_TYPE, get_reply=True
210
- ) -> AstrBotMessage:
232
+ self,
233
+ update: Update,
234
+ context: ContextTypes.DEFAULT_TYPE,
235
+ get_reply=True,
236
+ ) -> AstrBotMessage | None:
211
237
  """转换 Telegram 的消息对象为 AstrBotMessage 对象。
212
238
 
213
239
  @param update: Telegram 的 Update 对象。
214
240
  @param context: Telegram 的 Context 对象。
215
241
  @param get_reply: 是否获取回复消息。这个参数是为了防止多个回复嵌套。
216
242
  """
243
+ if not update.message:
244
+ logger.warning("Received an update without a message.")
245
+ return None
246
+
217
247
  message = AstrBotMessage()
218
248
  message.session_id = str(update.message.chat.id)
249
+
219
250
  # 获得是群聊还是私聊
220
251
  if update.message.chat.type == ChatType.PRIVATE:
221
252
  message.type = MessageType.FRIEND_MESSAGE
@@ -226,10 +257,14 @@ class TelegramPlatformAdapter(Platform):
226
257
  # Topic Group
227
258
  message.group_id += "#" + str(update.message.message_thread_id)
228
259
  message.session_id = message.group_id
229
-
230
260
  message.message_id = str(update.message.message_id)
261
+ _from_user = update.message.from_user
262
+ if not _from_user:
263
+ logger.warning("[Telegram] Received a message without a from_user.")
264
+ return None
231
265
  message.sender = MessageMember(
232
- str(update.message.from_user.id), update.message.from_user.username
266
+ str(_from_user.id),
267
+ _from_user.username or "Unknown",
233
268
  )
234
269
  message.self_id = str(context.bot.username)
235
270
  message.raw_message = update
@@ -248,22 +283,32 @@ class TelegramPlatformAdapter(Platform):
248
283
  )
249
284
  reply_abm = await self.convert_message(reply_update, context, False)
250
285
 
251
- message.message.append(
252
- Comp.Reply(
253
- id=reply_abm.message_id,
254
- chain=reply_abm.message,
255
- sender_id=reply_abm.sender.user_id,
256
- sender_nickname=reply_abm.sender.nickname,
257
- time=reply_abm.timestamp,
258
- message_str=reply_abm.message_str,
259
- text=reply_abm.message_str,
260
- qq=reply_abm.sender.user_id,
286
+ if reply_abm:
287
+ message.message.append(
288
+ Comp.Reply(
289
+ id=reply_abm.message_id,
290
+ chain=reply_abm.message,
291
+ sender_id=reply_abm.sender.user_id,
292
+ sender_nickname=reply_abm.sender.nickname,
293
+ time=reply_abm.timestamp,
294
+ message_str=reply_abm.message_str,
295
+ text=reply_abm.message_str,
296
+ qq=reply_abm.sender.user_id,
297
+ ),
261
298
  )
262
- )
263
299
 
264
300
  if update.message.text:
265
301
  # 处理文本消息
266
302
  plain_text = update.message.text
303
+ if (
304
+ message.type == MessageType.GROUP_MESSAGE
305
+ and update.message
306
+ and update.message.reply_to_message
307
+ and update.message.reply_to_message.from_user
308
+ and update.message.reply_to_message.from_user.id == context.bot.id
309
+ ):
310
+ plain_text2 = f"/@{context.bot.username} " + plain_text
311
+ plain_text = plain_text2
267
312
 
268
313
  # 群聊场景命令特殊处理
269
314
  if plain_text.startswith("/"):
@@ -282,10 +327,12 @@ class TelegramPlatformAdapter(Platform):
282
327
  entity.offset + 1 : entity.offset + entity.length
283
328
  ]
284
329
  message.message.append(Comp.At(qq=name, name=name))
285
- plain_text = (
286
- plain_text[: entity.offset]
287
- + plain_text[entity.offset + entity.length :]
288
- )
330
+ # 如果mention是当前bot则移除;否则保留
331
+ if name.lower() == context.bot.username.lower():
332
+ plain_text = (
333
+ plain_text[: entity.offset]
334
+ + plain_text[entity.offset + entity.length :]
335
+ )
289
336
 
290
337
  if plain_text:
291
338
  message.message.append(Comp.Plain(plain_text))
@@ -293,7 +340,7 @@ class TelegramPlatformAdapter(Platform):
293
340
 
294
341
  if message.message_str.strip() == "/start":
295
342
  await self.start(update, context)
296
- return
343
+ return None
297
344
 
298
345
  elif update.message.voice:
299
346
  file = await update.message.voice.get_file()
@@ -327,15 +374,25 @@ class TelegramPlatformAdapter(Platform):
327
374
 
328
375
  elif update.message.document:
329
376
  file = await update.message.document.get_file()
330
- message.message = [
331
- Comp.File(file=file.file_path, name=update.message.document.file_name),
332
- ]
377
+ file_name = update.message.document.file_name or uuid.uuid4().hex
378
+ file_path = file.file_path
379
+ if file_path is None:
380
+ logger.warning(
381
+ f"Telegram document file_path is None, cannot save the file {file_name}.",
382
+ )
383
+ else:
384
+ message.message.append(Comp.File(file=file_path, name=file_name))
333
385
 
334
386
  elif update.message.video:
335
387
  file = await update.message.video.get_file()
336
- message.message = [
337
- Comp.Video(file=file.file_path, path=file.file_path),
338
- ]
388
+ file_name = update.message.video.file_name or uuid.uuid4().hex
389
+ file_path = file.file_path
390
+ if file_path is None:
391
+ logger.warning(
392
+ f"Telegram video file_path is None, cannot save the file {file_name}.",
393
+ )
394
+ else:
395
+ message.message.append(Comp.Video(file=file_path, path=file.file_path))
339
396
 
340
397
  return message
341
398
 
@@ -1,21 +1,37 @@
1
1
  import asyncio
2
+ import os
3
+ import re
4
+
2
5
  import telegramify_markdown
6
+ from telegram import ReactionTypeCustomEmoji, ReactionTypeEmoji
7
+ from telegram.ext import ExtBot
8
+
9
+ from astrbot import logger
3
10
  from astrbot.api.event import AstrMessageEvent, MessageChain
4
- from astrbot.api.platform import AstrBotMessage, PlatformMetadata, MessageType
5
11
  from astrbot.api.message_components import (
6
- Plain,
7
- Image,
8
- Reply,
9
12
  At,
10
13
  File,
14
+ Image,
15
+ Plain,
11
16
  Record,
17
+ Reply,
12
18
  )
13
- from telegram.ext import ExtBot
19
+ from astrbot.api.platform import AstrBotMessage, MessageType, PlatformMetadata
20
+ from astrbot.core.utils.astrbot_path import get_astrbot_data_path
14
21
  from astrbot.core.utils.io import download_file
15
- from astrbot import logger
16
22
 
17
23
 
18
24
  class TelegramPlatformEvent(AstrMessageEvent):
25
+ # Telegram 的最大消息长度限制
26
+ MAX_MESSAGE_LENGTH = 4096
27
+
28
+ SPLIT_PATTERNS = {
29
+ "paragraph": re.compile(r"\n\n"),
30
+ "line": re.compile(r"\n"),
31
+ "sentence": re.compile(r"[.!?。!?]"),
32
+ "word": re.compile(r"\s"),
33
+ }
34
+
19
35
  def __init__(
20
36
  self,
21
37
  message_str: str,
@@ -27,8 +43,38 @@ class TelegramPlatformEvent(AstrMessageEvent):
27
43
  super().__init__(message_str, message_obj, platform_meta, session_id)
28
44
  self.client = client
29
45
 
30
- @staticmethod
31
- async def send_with_client(client: ExtBot, message: MessageChain, user_name: str):
46
+ @classmethod
47
+ def _split_message(cls, text: str) -> list[str]:
48
+ if len(text) <= cls.MAX_MESSAGE_LENGTH:
49
+ return [text]
50
+
51
+ chunks = []
52
+ while text:
53
+ if len(text) <= cls.MAX_MESSAGE_LENGTH:
54
+ chunks.append(text)
55
+ break
56
+
57
+ split_point = cls.MAX_MESSAGE_LENGTH
58
+ segment = text[: cls.MAX_MESSAGE_LENGTH]
59
+
60
+ for _, pattern in cls.SPLIT_PATTERNS.items():
61
+ if matches := list(pattern.finditer(segment)):
62
+ last_match = matches[-1]
63
+ split_point = last_match.end()
64
+ break
65
+
66
+ chunks.append(text[:split_point])
67
+ text = text[split_point:].lstrip()
68
+
69
+ return chunks
70
+
71
+ @classmethod
72
+ async def send_with_client(
73
+ cls,
74
+ client: ExtBot,
75
+ message: MessageChain,
76
+ user_name: str,
77
+ ):
32
78
  image_path = None
33
79
 
34
80
  has_reply = False
@@ -57,25 +103,33 @@ class TelegramPlatformEvent(AstrMessageEvent):
57
103
 
58
104
  if isinstance(i, Plain):
59
105
  if at_user_id and not at_flag:
60
- i.text = f"@{at_user_id} " + i.text
106
+ i.text = f"@{at_user_id} {i.text}"
61
107
  at_flag = True
62
- text = i.text
63
- try:
64
- text = telegramify_markdown.markdownify(
65
- i.text, max_line_length=None, normalize_whitespace=False
66
- )
67
- except Exception as e:
68
- logger.warning(
69
- f"MarkdownV2 conversion failed: {e}. Using plain text instead."
70
- )
71
- return
72
- await client.send_message(text=text, parse_mode="MarkdownV2", **payload)
108
+ chunks = cls._split_message(i.text)
109
+ for chunk in chunks:
110
+ try:
111
+ md_text = telegramify_markdown.markdownify(
112
+ chunk,
113
+ max_line_length=None,
114
+ normalize_whitespace=False,
115
+ )
116
+ await client.send_message(
117
+ text=md_text,
118
+ parse_mode="MarkdownV2",
119
+ **payload,
120
+ )
121
+ except Exception as e:
122
+ logger.warning(
123
+ f"MarkdownV2 send failed: {e}. Using plain text instead.",
124
+ )
125
+ await client.send_message(text=chunk, **payload)
73
126
  elif isinstance(i, Image):
74
127
  image_path = await i.convert_to_file_path()
75
128
  await client.send_photo(photo=image_path, **payload)
76
129
  elif isinstance(i, File):
77
130
  if i.file.startswith("https://"):
78
- path = "data/temp/" + i.name
131
+ temp_dir = os.path.join(get_astrbot_data_path(), "temp")
132
+ path = os.path.join(temp_dir, i.name)
79
133
  await download_file(i.file, path)
80
134
  i.file = path
81
135
 
@@ -91,6 +145,38 @@ class TelegramPlatformEvent(AstrMessageEvent):
91
145
  await self.send_with_client(self.client, message, self.get_sender_id())
92
146
  await super().send(message)
93
147
 
148
+ async def react(self, emoji: str | None, big: bool = False):
149
+ """给原消息添加 Telegram 反应:
150
+ - 普通 emoji:传入 '👍'、'😂' 等
151
+ - 自定义表情:传入其 custom_emoji_id(纯数字字符串)
152
+ - 取消本机器人的反应:传入 None 或空字符串
153
+ """
154
+ try:
155
+ # 解析 chat_id(去掉超级群的 "#<thread_id>" 片段)
156
+ if self.get_message_type() == MessageType.GROUP_MESSAGE:
157
+ chat_id = (self.message_obj.group_id or "").split("#")[0]
158
+ else:
159
+ chat_id = self.get_sender_id()
160
+
161
+ message_id = int(self.message_obj.message_id)
162
+
163
+ # 组装 reaction 参数(必须是 ReactionType 的列表)
164
+ if not emoji: # 清空本 bot 的反应
165
+ reaction_param = [] # 空列表表示移除本 bot 的反应
166
+ elif emoji.isdigit(): # 自定义表情:传 custom_emoji_id
167
+ reaction_param = [ReactionTypeCustomEmoji(emoji)]
168
+ else: # 普通 emoji
169
+ reaction_param = [ReactionTypeEmoji(emoji)]
170
+
171
+ await self.client.set_message_reaction(
172
+ chat_id=chat_id,
173
+ message_id=message_id,
174
+ reaction=reaction_param, # 注意是列表
175
+ is_big=big, # 可选:大动画
176
+ )
177
+ except Exception as e:
178
+ logger.error(f"[Telegram] 添加反应失败: {e}")
179
+
94
180
  async def send_streaming(self, generator, use_fallback: bool = False):
95
181
  message_thread_id = None
96
182
 
@@ -116,6 +202,12 @@ class TelegramPlatformEvent(AstrMessageEvent):
116
202
 
117
203
  async for chain in generator:
118
204
  if isinstance(chain, MessageChain):
205
+ if chain.type == "break":
206
+ # 分割符
207
+ message_id = None # 重置消息 ID
208
+ delta = "" # 重置 delta
209
+ continue
210
+
119
211
  # 处理消息链中的每个组件
120
212
  for i in chain.chain:
121
213
  if isinstance(i, Plain):
@@ -126,12 +218,15 @@ class TelegramPlatformEvent(AstrMessageEvent):
126
218
  continue
127
219
  elif isinstance(i, File):
128
220
  if i.file.startswith("https://"):
129
- path = "data/temp/" + i.name
221
+ temp_dir = os.path.join(get_astrbot_data_path(), "temp")
222
+ path = os.path.join(temp_dir, i.name)
130
223
  await download_file(i.file, path)
131
224
  i.file = path
132
225
 
133
226
  await self.client.send_document(
134
- document=i.file, filename=i.name, **payload
227
+ document=i.file,
228
+ filename=i.name,
229
+ **payload,
135
230
  )
136
231
  continue
137
232
  elif isinstance(i, Record):
@@ -143,17 +238,7 @@ class TelegramPlatformEvent(AstrMessageEvent):
143
238
  continue
144
239
 
145
240
  # Plain
146
- if not message_id:
147
- try:
148
- msg = await self.client.send_message(text=delta, **payload)
149
- current_content = delta
150
- except Exception as e:
151
- logger.warning(f"发送消息失败(streaming): {e!s}")
152
- message_id = msg.message_id
153
- last_edit_time = (
154
- asyncio.get_event_loop().time()
155
- ) # 记录初始消息发送时间
156
- else:
241
+ if message_id and len(delta) <= self.MAX_MESSAGE_LENGTH:
157
242
  current_time = asyncio.get_event_loop().time()
158
243
  time_since_last_edit = current_time - last_edit_time
159
244
 
@@ -172,12 +257,25 @@ class TelegramPlatformEvent(AstrMessageEvent):
172
257
  last_edit_time = (
173
258
  asyncio.get_event_loop().time()
174
259
  ) # 更新上次编辑的时间
260
+ else:
261
+ # delta 长度一般不会大于 4096,因此这里直接发送
262
+ try:
263
+ msg = await self.client.send_message(text=delta, **payload)
264
+ current_content = delta
265
+ except Exception as e:
266
+ logger.warning(f"发送消息失败(streaming): {e!s}")
267
+ message_id = msg.message_id
268
+ last_edit_time = (
269
+ asyncio.get_event_loop().time()
270
+ ) # 记录初始消息发送时间
175
271
 
176
272
  try:
177
273
  if delta and current_content != delta:
178
274
  try:
179
275
  markdown_text = telegramify_markdown.markdownify(
180
- delta, max_line_length=None, normalize_whitespace=False
276
+ delta,
277
+ max_line_length=None,
278
+ normalize_whitespace=False,
181
279
  )
182
280
  await self.client.edit_message_text(
183
281
  text=markdown_text,
@@ -188,7 +286,9 @@ class TelegramPlatformEvent(AstrMessageEvent):
188
286
  except Exception as e:
189
287
  logger.warning(f"Markdown转换失败,使用普通文本: {e!s}")
190
288
  await self.client.edit_message_text(
191
- text=delta, chat_id=payload["chat_id"], message_id=message_id
289
+ text=delta,
290
+ chat_id=payload["chat_id"],
291
+ message_id=message_id,
192
292
  )
193
293
  except Exception as e:
194
294
  logger.warning(f"编辑消息失败(streaming): {e!s}")