AstrBot 3.5.6__py3-none-any.whl → 4.7.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (288) hide show
  1. astrbot/api/__init__.py +16 -4
  2. astrbot/api/all.py +2 -1
  3. astrbot/api/event/__init__.py +5 -6
  4. astrbot/api/event/filter/__init__.py +37 -34
  5. astrbot/api/platform/__init__.py +7 -8
  6. astrbot/api/provider/__init__.py +8 -7
  7. astrbot/api/star/__init__.py +3 -4
  8. astrbot/api/util/__init__.py +2 -2
  9. astrbot/cli/__init__.py +1 -0
  10. astrbot/cli/__main__.py +18 -197
  11. astrbot/cli/commands/__init__.py +6 -0
  12. astrbot/cli/commands/cmd_conf.py +209 -0
  13. astrbot/cli/commands/cmd_init.py +56 -0
  14. astrbot/cli/commands/cmd_plug.py +245 -0
  15. astrbot/cli/commands/cmd_run.py +62 -0
  16. astrbot/cli/utils/__init__.py +18 -0
  17. astrbot/cli/utils/basic.py +76 -0
  18. astrbot/cli/utils/plugin.py +246 -0
  19. astrbot/cli/utils/version_comparator.py +90 -0
  20. astrbot/core/__init__.py +17 -19
  21. astrbot/core/agent/agent.py +14 -0
  22. astrbot/core/agent/handoff.py +38 -0
  23. astrbot/core/agent/hooks.py +30 -0
  24. astrbot/core/agent/mcp_client.py +385 -0
  25. astrbot/core/agent/message.py +175 -0
  26. astrbot/core/agent/response.py +14 -0
  27. astrbot/core/agent/run_context.py +22 -0
  28. astrbot/core/agent/runners/__init__.py +3 -0
  29. astrbot/core/agent/runners/base.py +65 -0
  30. astrbot/core/agent/runners/coze/coze_agent_runner.py +367 -0
  31. astrbot/core/agent/runners/coze/coze_api_client.py +324 -0
  32. astrbot/core/agent/runners/dashscope/dashscope_agent_runner.py +403 -0
  33. astrbot/core/agent/runners/dify/dify_agent_runner.py +336 -0
  34. astrbot/core/agent/runners/dify/dify_api_client.py +195 -0
  35. astrbot/core/agent/runners/tool_loop_agent_runner.py +400 -0
  36. astrbot/core/agent/tool.py +285 -0
  37. astrbot/core/agent/tool_executor.py +17 -0
  38. astrbot/core/astr_agent_context.py +19 -0
  39. astrbot/core/astr_agent_hooks.py +36 -0
  40. astrbot/core/astr_agent_run_util.py +80 -0
  41. astrbot/core/astr_agent_tool_exec.py +246 -0
  42. astrbot/core/astrbot_config_mgr.py +275 -0
  43. astrbot/core/config/__init__.py +2 -2
  44. astrbot/core/config/astrbot_config.py +60 -20
  45. astrbot/core/config/default.py +1972 -453
  46. astrbot/core/config/i18n_utils.py +110 -0
  47. astrbot/core/conversation_mgr.py +285 -75
  48. astrbot/core/core_lifecycle.py +167 -62
  49. astrbot/core/db/__init__.py +305 -102
  50. astrbot/core/db/migration/helper.py +69 -0
  51. astrbot/core/db/migration/migra_3_to_4.py +357 -0
  52. astrbot/core/db/migration/migra_45_to_46.py +44 -0
  53. astrbot/core/db/migration/migra_webchat_session.py +131 -0
  54. astrbot/core/db/migration/shared_preferences_v3.py +48 -0
  55. astrbot/core/db/migration/sqlite_v3.py +497 -0
  56. astrbot/core/db/po.py +259 -55
  57. astrbot/core/db/sqlite.py +773 -528
  58. astrbot/core/db/vec_db/base.py +73 -0
  59. astrbot/core/db/vec_db/faiss_impl/__init__.py +3 -0
  60. astrbot/core/db/vec_db/faiss_impl/document_storage.py +392 -0
  61. astrbot/core/db/vec_db/faiss_impl/embedding_storage.py +93 -0
  62. astrbot/core/db/vec_db/faiss_impl/sqlite_init.sql +17 -0
  63. astrbot/core/db/vec_db/faiss_impl/vec_db.py +204 -0
  64. astrbot/core/event_bus.py +26 -22
  65. astrbot/core/exceptions.py +9 -0
  66. astrbot/core/file_token_service.py +98 -0
  67. astrbot/core/initial_loader.py +19 -10
  68. astrbot/core/knowledge_base/chunking/__init__.py +9 -0
  69. astrbot/core/knowledge_base/chunking/base.py +25 -0
  70. astrbot/core/knowledge_base/chunking/fixed_size.py +59 -0
  71. astrbot/core/knowledge_base/chunking/recursive.py +161 -0
  72. astrbot/core/knowledge_base/kb_db_sqlite.py +301 -0
  73. astrbot/core/knowledge_base/kb_helper.py +642 -0
  74. astrbot/core/knowledge_base/kb_mgr.py +330 -0
  75. astrbot/core/knowledge_base/models.py +120 -0
  76. astrbot/core/knowledge_base/parsers/__init__.py +13 -0
  77. astrbot/core/knowledge_base/parsers/base.py +51 -0
  78. astrbot/core/knowledge_base/parsers/markitdown_parser.py +26 -0
  79. astrbot/core/knowledge_base/parsers/pdf_parser.py +101 -0
  80. astrbot/core/knowledge_base/parsers/text_parser.py +42 -0
  81. astrbot/core/knowledge_base/parsers/url_parser.py +103 -0
  82. astrbot/core/knowledge_base/parsers/util.py +13 -0
  83. astrbot/core/knowledge_base/prompts.py +65 -0
  84. astrbot/core/knowledge_base/retrieval/__init__.py +14 -0
  85. astrbot/core/knowledge_base/retrieval/hit_stopwords.txt +767 -0
  86. astrbot/core/knowledge_base/retrieval/manager.py +276 -0
  87. astrbot/core/knowledge_base/retrieval/rank_fusion.py +142 -0
  88. astrbot/core/knowledge_base/retrieval/sparse_retriever.py +136 -0
  89. astrbot/core/log.py +21 -15
  90. astrbot/core/message/components.py +413 -287
  91. astrbot/core/message/message_event_result.py +35 -24
  92. astrbot/core/persona_mgr.py +192 -0
  93. astrbot/core/pipeline/__init__.py +14 -14
  94. astrbot/core/pipeline/content_safety_check/stage.py +13 -9
  95. astrbot/core/pipeline/content_safety_check/strategies/__init__.py +1 -2
  96. astrbot/core/pipeline/content_safety_check/strategies/baidu_aip.py +13 -14
  97. astrbot/core/pipeline/content_safety_check/strategies/keywords.py +2 -1
  98. astrbot/core/pipeline/content_safety_check/strategies/strategy.py +6 -6
  99. astrbot/core/pipeline/context.py +7 -1
  100. astrbot/core/pipeline/context_utils.py +107 -0
  101. astrbot/core/pipeline/preprocess_stage/stage.py +63 -36
  102. astrbot/core/pipeline/process_stage/method/agent_request.py +48 -0
  103. astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py +464 -0
  104. astrbot/core/pipeline/process_stage/method/agent_sub_stages/third_party.py +202 -0
  105. astrbot/core/pipeline/process_stage/method/star_request.py +26 -32
  106. astrbot/core/pipeline/process_stage/stage.py +21 -15
  107. astrbot/core/pipeline/process_stage/utils.py +125 -0
  108. astrbot/core/pipeline/rate_limit_check/stage.py +34 -36
  109. astrbot/core/pipeline/respond/stage.py +142 -101
  110. astrbot/core/pipeline/result_decorate/stage.py +124 -57
  111. astrbot/core/pipeline/scheduler.py +21 -16
  112. astrbot/core/pipeline/session_status_check/stage.py +37 -0
  113. astrbot/core/pipeline/stage.py +11 -76
  114. astrbot/core/pipeline/waking_check/stage.py +69 -33
  115. astrbot/core/pipeline/whitelist_check/stage.py +10 -7
  116. astrbot/core/platform/__init__.py +6 -6
  117. astrbot/core/platform/astr_message_event.py +107 -129
  118. astrbot/core/platform/astrbot_message.py +32 -12
  119. astrbot/core/platform/manager.py +62 -18
  120. astrbot/core/platform/message_session.py +30 -0
  121. astrbot/core/platform/platform.py +16 -24
  122. astrbot/core/platform/platform_metadata.py +9 -4
  123. astrbot/core/platform/register.py +12 -7
  124. astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py +136 -60
  125. astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py +126 -46
  126. astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py +63 -31
  127. astrbot/core/platform/sources/dingtalk/dingtalk_event.py +30 -26
  128. astrbot/core/platform/sources/discord/client.py +129 -0
  129. astrbot/core/platform/sources/discord/components.py +139 -0
  130. astrbot/core/platform/sources/discord/discord_platform_adapter.py +473 -0
  131. astrbot/core/platform/sources/discord/discord_platform_event.py +313 -0
  132. astrbot/core/platform/sources/lark/lark_adapter.py +27 -18
  133. astrbot/core/platform/sources/lark/lark_event.py +39 -13
  134. astrbot/core/platform/sources/misskey/misskey_adapter.py +770 -0
  135. astrbot/core/platform/sources/misskey/misskey_api.py +964 -0
  136. astrbot/core/platform/sources/misskey/misskey_event.py +163 -0
  137. astrbot/core/platform/sources/misskey/misskey_utils.py +550 -0
  138. astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py +149 -33
  139. astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py +41 -26
  140. astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_adapter.py +36 -17
  141. astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_event.py +3 -1
  142. astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_server.py +14 -8
  143. astrbot/core/platform/sources/satori/satori_adapter.py +792 -0
  144. astrbot/core/platform/sources/satori/satori_event.py +432 -0
  145. astrbot/core/platform/sources/slack/client.py +164 -0
  146. astrbot/core/platform/sources/slack/slack_adapter.py +416 -0
  147. astrbot/core/platform/sources/slack/slack_event.py +253 -0
  148. astrbot/core/platform/sources/telegram/tg_adapter.py +100 -43
  149. astrbot/core/platform/sources/telegram/tg_event.py +136 -36
  150. astrbot/core/platform/sources/webchat/webchat_adapter.py +72 -22
  151. astrbot/core/platform/sources/webchat/webchat_event.py +46 -22
  152. astrbot/core/platform/sources/webchat/webchat_queue_mgr.py +35 -0
  153. astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py +926 -0
  154. astrbot/core/platform/sources/wechatpadpro/wechatpadpro_message_event.py +178 -0
  155. astrbot/core/platform/sources/wechatpadpro/xml_data_parser.py +159 -0
  156. astrbot/core/platform/sources/wecom/wecom_adapter.py +169 -27
  157. astrbot/core/platform/sources/wecom/wecom_event.py +162 -77
  158. astrbot/core/platform/sources/wecom/wecom_kf.py +279 -0
  159. astrbot/core/platform/sources/wecom/wecom_kf_message.py +196 -0
  160. astrbot/core/platform/sources/wecom_ai_bot/WXBizJsonMsgCrypt.py +297 -0
  161. astrbot/core/platform/sources/wecom_ai_bot/__init__.py +15 -0
  162. astrbot/core/platform/sources/wecom_ai_bot/ierror.py +19 -0
  163. astrbot/core/platform/sources/wecom_ai_bot/wecomai_adapter.py +472 -0
  164. astrbot/core/platform/sources/wecom_ai_bot/wecomai_api.py +417 -0
  165. astrbot/core/platform/sources/wecom_ai_bot/wecomai_event.py +152 -0
  166. astrbot/core/platform/sources/wecom_ai_bot/wecomai_queue_mgr.py +153 -0
  167. astrbot/core/platform/sources/wecom_ai_bot/wecomai_server.py +168 -0
  168. astrbot/core/platform/sources/wecom_ai_bot/wecomai_utils.py +209 -0
  169. astrbot/core/platform/sources/weixin_official_account/weixin_offacc_adapter.py +306 -0
  170. astrbot/core/platform/sources/weixin_official_account/weixin_offacc_event.py +186 -0
  171. astrbot/core/platform_message_history_mgr.py +49 -0
  172. astrbot/core/provider/__init__.py +2 -3
  173. astrbot/core/provider/entites.py +8 -8
  174. astrbot/core/provider/entities.py +154 -98
  175. astrbot/core/provider/func_tool_manager.py +446 -458
  176. astrbot/core/provider/manager.py +345 -207
  177. astrbot/core/provider/provider.py +188 -73
  178. astrbot/core/provider/register.py +9 -7
  179. astrbot/core/provider/sources/anthropic_source.py +295 -115
  180. astrbot/core/provider/sources/azure_tts_source.py +224 -0
  181. astrbot/core/provider/sources/bailian_rerank_source.py +236 -0
  182. astrbot/core/provider/sources/dashscope_tts.py +138 -14
  183. astrbot/core/provider/sources/edge_tts_source.py +24 -19
  184. astrbot/core/provider/sources/fishaudio_tts_api_source.py +58 -13
  185. astrbot/core/provider/sources/gemini_embedding_source.py +61 -0
  186. astrbot/core/provider/sources/gemini_source.py +310 -132
  187. astrbot/core/provider/sources/gemini_tts_source.py +81 -0
  188. astrbot/core/provider/sources/groq_source.py +15 -0
  189. astrbot/core/provider/sources/gsv_selfhosted_source.py +151 -0
  190. astrbot/core/provider/sources/gsvi_tts_source.py +14 -7
  191. astrbot/core/provider/sources/minimax_tts_api_source.py +159 -0
  192. astrbot/core/provider/sources/openai_embedding_source.py +40 -0
  193. astrbot/core/provider/sources/openai_source.py +241 -145
  194. astrbot/core/provider/sources/openai_tts_api_source.py +18 -7
  195. astrbot/core/provider/sources/sensevoice_selfhosted_source.py +13 -11
  196. astrbot/core/provider/sources/vllm_rerank_source.py +71 -0
  197. astrbot/core/provider/sources/volcengine_tts.py +115 -0
  198. astrbot/core/provider/sources/whisper_api_source.py +18 -13
  199. astrbot/core/provider/sources/whisper_selfhosted_source.py +19 -12
  200. astrbot/core/provider/sources/xinference_rerank_source.py +116 -0
  201. astrbot/core/provider/sources/xinference_stt_provider.py +197 -0
  202. astrbot/core/provider/sources/zhipu_source.py +6 -73
  203. astrbot/core/star/__init__.py +43 -11
  204. astrbot/core/star/config.py +17 -18
  205. astrbot/core/star/context.py +362 -138
  206. astrbot/core/star/filter/__init__.py +4 -3
  207. astrbot/core/star/filter/command.py +111 -35
  208. astrbot/core/star/filter/command_group.py +46 -34
  209. astrbot/core/star/filter/custom_filter.py +6 -5
  210. astrbot/core/star/filter/event_message_type.py +4 -2
  211. astrbot/core/star/filter/permission.py +4 -2
  212. astrbot/core/star/filter/platform_adapter_type.py +45 -12
  213. astrbot/core/star/filter/regex.py +4 -2
  214. astrbot/core/star/register/__init__.py +19 -15
  215. astrbot/core/star/register/star.py +41 -13
  216. astrbot/core/star/register/star_handler.py +236 -86
  217. astrbot/core/star/session_llm_manager.py +280 -0
  218. astrbot/core/star/session_plugin_manager.py +170 -0
  219. astrbot/core/star/star.py +36 -43
  220. astrbot/core/star/star_handler.py +47 -85
  221. astrbot/core/star/star_manager.py +442 -260
  222. astrbot/core/star/star_tools.py +167 -45
  223. astrbot/core/star/updator.py +17 -20
  224. astrbot/core/umop_config_router.py +106 -0
  225. astrbot/core/updator.py +38 -13
  226. astrbot/core/utils/astrbot_path.py +39 -0
  227. astrbot/core/utils/command_parser.py +1 -1
  228. astrbot/core/utils/io.py +119 -60
  229. astrbot/core/utils/log_pipe.py +1 -1
  230. astrbot/core/utils/metrics.py +11 -10
  231. astrbot/core/utils/migra_helper.py +73 -0
  232. astrbot/core/utils/path_util.py +63 -62
  233. astrbot/core/utils/pip_installer.py +37 -15
  234. astrbot/core/utils/session_lock.py +29 -0
  235. astrbot/core/utils/session_waiter.py +19 -20
  236. astrbot/core/utils/shared_preferences.py +174 -34
  237. astrbot/core/utils/t2i/__init__.py +4 -1
  238. astrbot/core/utils/t2i/local_strategy.py +386 -238
  239. astrbot/core/utils/t2i/network_strategy.py +109 -49
  240. astrbot/core/utils/t2i/renderer.py +29 -14
  241. astrbot/core/utils/t2i/template/astrbot_powershell.html +184 -0
  242. astrbot/core/utils/t2i/template_manager.py +111 -0
  243. astrbot/core/utils/tencent_record_helper.py +115 -1
  244. astrbot/core/utils/version_comparator.py +10 -13
  245. astrbot/core/zip_updator.py +112 -65
  246. astrbot/dashboard/routes/__init__.py +20 -13
  247. astrbot/dashboard/routes/auth.py +20 -9
  248. astrbot/dashboard/routes/chat.py +297 -141
  249. astrbot/dashboard/routes/config.py +652 -55
  250. astrbot/dashboard/routes/conversation.py +107 -37
  251. astrbot/dashboard/routes/file.py +26 -0
  252. astrbot/dashboard/routes/knowledge_base.py +1244 -0
  253. astrbot/dashboard/routes/log.py +27 -2
  254. astrbot/dashboard/routes/persona.py +202 -0
  255. astrbot/dashboard/routes/plugin.py +197 -139
  256. astrbot/dashboard/routes/route.py +27 -7
  257. astrbot/dashboard/routes/session_management.py +354 -0
  258. astrbot/dashboard/routes/stat.py +85 -18
  259. astrbot/dashboard/routes/static_file.py +5 -2
  260. astrbot/dashboard/routes/t2i.py +233 -0
  261. astrbot/dashboard/routes/tools.py +184 -120
  262. astrbot/dashboard/routes/update.py +59 -36
  263. astrbot/dashboard/server.py +96 -36
  264. astrbot/dashboard/utils.py +165 -0
  265. astrbot-4.7.0.dist-info/METADATA +294 -0
  266. astrbot-4.7.0.dist-info/RECORD +274 -0
  267. {astrbot-3.5.6.dist-info → astrbot-4.7.0.dist-info}/WHEEL +1 -1
  268. astrbot/core/db/plugin/sqlite_impl.py +0 -112
  269. astrbot/core/db/sqlite_init.sql +0 -50
  270. astrbot/core/pipeline/platform_compatibility/stage.py +0 -56
  271. astrbot/core/pipeline/process_stage/method/llm_request.py +0 -606
  272. astrbot/core/platform/sources/gewechat/client.py +0 -806
  273. astrbot/core/platform/sources/gewechat/downloader.py +0 -55
  274. astrbot/core/platform/sources/gewechat/gewechat_event.py +0 -255
  275. astrbot/core/platform/sources/gewechat/gewechat_platform_adapter.py +0 -103
  276. astrbot/core/platform/sources/gewechat/xml_data_parser.py +0 -110
  277. astrbot/core/provider/sources/dashscope_source.py +0 -203
  278. astrbot/core/provider/sources/dify_source.py +0 -281
  279. astrbot/core/provider/sources/llmtuner_source.py +0 -132
  280. astrbot/core/rag/embedding/openai_source.py +0 -20
  281. astrbot/core/rag/knowledge_db_mgr.py +0 -94
  282. astrbot/core/rag/store/__init__.py +0 -9
  283. astrbot/core/rag/store/chroma_db.py +0 -42
  284. astrbot/core/utils/dify_api_client.py +0 -152
  285. astrbot-3.5.6.dist-info/METADATA +0 -249
  286. astrbot-3.5.6.dist-info/RECORD +0 -158
  287. {astrbot-3.5.6.dist-info → astrbot-4.7.0.dist-info}/entry_points.txt +0 -0
  288. {astrbot-3.5.6.dist-info → astrbot-4.7.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,416 @@
1
+ import asyncio
2
+ import base64
3
+ import re
4
+ import time
5
+ import uuid
6
+ from collections.abc import Awaitable
7
+ from typing import Any
8
+
9
+ import aiohttp
10
+ from slack_sdk.socket_mode.request import SocketModeRequest
11
+ from slack_sdk.web.async_client import AsyncWebClient
12
+
13
+ from astrbot.api import logger
14
+ from astrbot.api.event import MessageChain
15
+ from astrbot.api.message_components import *
16
+ from astrbot.api.platform import (
17
+ AstrBotMessage,
18
+ MessageMember,
19
+ MessageType,
20
+ Platform,
21
+ PlatformMetadata,
22
+ )
23
+ from astrbot.core.platform.astr_message_event import MessageSesion
24
+
25
+ from ...register import register_platform_adapter
26
+ from .client import SlackSocketClient, SlackWebhookClient
27
+ from .slack_event import SlackMessageEvent
28
+
29
+
30
+ @register_platform_adapter(
31
+ "slack",
32
+ "适用于 Slack 的消息平台适配器,支持 Socket Mode 和 Webhook Mode。",
33
+ support_streaming_message=False,
34
+ )
35
+ class SlackAdapter(Platform):
36
+ def __init__(
37
+ self,
38
+ platform_config: dict,
39
+ platform_settings: dict,
40
+ event_queue: asyncio.Queue,
41
+ ) -> None:
42
+ super().__init__(event_queue)
43
+
44
+ self.config = platform_config
45
+ self.settings = platform_settings
46
+ self.unique_session = platform_settings.get("unique_session", False)
47
+
48
+ self.bot_token = platform_config.get("bot_token")
49
+ self.app_token = platform_config.get("app_token")
50
+ self.signing_secret = platform_config.get("signing_secret")
51
+ self.connection_mode = platform_config.get("slack_connection_mode", "socket")
52
+ self.webhook_host = platform_config.get("slack_webhook_host", "0.0.0.0")
53
+ self.webhook_port = platform_config.get("slack_webhook_port", 3000)
54
+ self.webhook_path = platform_config.get(
55
+ "slack_webhook_path",
56
+ "/astrbot-slack-webhook/callback",
57
+ )
58
+
59
+ if not self.bot_token:
60
+ raise ValueError("Slack bot_token 是必需的")
61
+
62
+ if self.connection_mode == "socket" and not self.app_token:
63
+ raise ValueError("Socket Mode 需要 app_token")
64
+
65
+ if self.connection_mode == "webhook" and not self.signing_secret:
66
+ raise ValueError("Webhook Mode 需要 signing_secret")
67
+
68
+ self.metadata = PlatformMetadata(
69
+ name="slack",
70
+ description="适用于 Slack 的消息平台适配器,支持 Socket Mode 和 Webhook Mode。",
71
+ id=self.config.get("id"),
72
+ support_streaming_message=False,
73
+ )
74
+
75
+ # 初始化 Slack Web Client
76
+ self.web_client = AsyncWebClient(token=self.bot_token, logger=logger)
77
+ self.socket_client = None
78
+ self.webhook_client = None
79
+
80
+ self.bot_self_id = None
81
+
82
+ async def send_by_session(
83
+ self,
84
+ session: MessageSesion,
85
+ message_chain: MessageChain,
86
+ ):
87
+ blocks, text = await SlackMessageEvent._parse_slack_blocks(
88
+ message_chain=message_chain,
89
+ web_client=self.web_client,
90
+ )
91
+
92
+ try:
93
+ if session.message_type == MessageType.GROUP_MESSAGE:
94
+ # 发送到频道
95
+ channel_id = (
96
+ session.session_id.split("_")[-1]
97
+ if "_" in session.session_id
98
+ else session.session_id
99
+ )
100
+ await self.web_client.chat_postMessage(
101
+ channel=channel_id,
102
+ text=text,
103
+ blocks=blocks if blocks else None,
104
+ )
105
+ else:
106
+ # 发送私信
107
+ await self.web_client.chat_postMessage(
108
+ channel=session.session_id,
109
+ text=text,
110
+ blocks=blocks if blocks else None,
111
+ )
112
+ except Exception as e:
113
+ logger.error(f"Slack 发送消息失败: {e}")
114
+
115
+ await super().send_by_session(session, message_chain)
116
+
117
+ async def convert_message(self, event: dict) -> AstrBotMessage:
118
+ logger.debug(f"[slack] RawMessage {event}")
119
+
120
+ abm = AstrBotMessage()
121
+ abm.self_id = self.bot_self_id
122
+
123
+ # 获取用户信息
124
+ user_id = event.get("user", "")
125
+ try:
126
+ user_info = await self.web_client.users_info(user=user_id)
127
+ user_data = user_info["user"]
128
+ user_name = user_data.get("real_name") or user_data.get("name", user_id)
129
+ except Exception:
130
+ user_name = user_id
131
+
132
+ abm.sender = MessageMember(user_id=user_id, nickname=user_name)
133
+
134
+ # 判断消息类型
135
+ channel_id = event.get("channel", "")
136
+ try:
137
+ channel_info = await self.web_client.conversations_info(channel=channel_id)
138
+ is_im = channel_info["channel"]["is_im"]
139
+
140
+ if is_im:
141
+ abm.type = MessageType.FRIEND_MESSAGE
142
+ else:
143
+ abm.type = MessageType.GROUP_MESSAGE
144
+ abm.group_id = channel_id
145
+ except Exception:
146
+ # 默认作为群组消息处理
147
+ abm.type = MessageType.GROUP_MESSAGE
148
+ abm.group_id = channel_id
149
+
150
+ # 设置会话ID
151
+ if self.unique_session and abm.type == MessageType.GROUP_MESSAGE:
152
+ abm.session_id = f"{user_id}_{channel_id}"
153
+ else:
154
+ abm.session_id = (
155
+ channel_id if abm.type == MessageType.GROUP_MESSAGE else user_id
156
+ )
157
+
158
+ abm.message_id = event.get("client_msg_id", uuid.uuid4().hex)
159
+ abm.timestamp = int(float(event.get("ts", time.time())))
160
+
161
+ # 处理消息内容
162
+ message_text = event.get("text", "")
163
+ abm.message_str = message_text
164
+ abm.message = []
165
+
166
+ # 优先使用 blocks 字段解析消息
167
+ if event.get("blocks"):
168
+ abm.message = self._parse_blocks(event["blocks"])
169
+ # 更新 message_str
170
+ abm.message_str = ""
171
+ for component in abm.message:
172
+ if isinstance(component, Plain):
173
+ abm.message_str += component.text
174
+ elif message_text:
175
+ # 处理传统的文本消息
176
+ if "<@" in message_text:
177
+ mentions = re.findall(r"<@([^>]+)>", message_text)
178
+ for mention in mentions:
179
+ try:
180
+ mentioned_user = await self.web_client.users_info(user=mention)
181
+ user_data = mentioned_user["user"]
182
+ user_name = user_data.get("real_name") or user_data.get(
183
+ "name",
184
+ mention,
185
+ )
186
+ abm.message.append(At(qq=mention, name=user_name))
187
+ except Exception:
188
+ abm.message.append(At(qq=mention, name=""))
189
+
190
+ # 清理消息文本中的@标记
191
+ if clean_text := re.sub(r"<@[^>]+>", "", message_text).strip():
192
+ abm.message.append(Plain(text=clean_text))
193
+ else:
194
+ abm.message.append(Plain(text=message_text))
195
+
196
+ # 处理文件附件
197
+ if "files" in event:
198
+ for file_info in event["files"]:
199
+ file_name = file_info.get("name", "unknown")
200
+ file_url = file_info.get("url_private", "")
201
+ if file_info.get("mimetype", "").startswith("image/"):
202
+ file_url = await self.get_file_base64(file_url)
203
+ abm.message.append(Image.fromBase64(base64=file_url))
204
+ else:
205
+ # TODO: 下载鉴权
206
+ abm.message.append(
207
+ File(name=file_name, file=file_url, url=file_url),
208
+ )
209
+
210
+ abm.raw_message = event
211
+ return abm
212
+
213
+ def _parse_blocks(self, blocks: list) -> list:
214
+ """解析 Slack blocks 格式的消息内容"""
215
+ message_components = []
216
+
217
+ for block in blocks:
218
+ block_type = block.get("type", "")
219
+
220
+ if block_type == "rich_text":
221
+ # 处理富文本块
222
+ elements = block.get("elements", [])
223
+ for element in elements:
224
+ if element.get("type") == "rich_text_section":
225
+ # 处理富文本段落
226
+ section_elements = element.get("elements", [])
227
+ text_parts = []
228
+ for section_element in section_elements:
229
+ element_type = section_element.get("type", "")
230
+
231
+ if element_type == "text":
232
+ # 普通文本
233
+ text_parts.append(section_element.get("text", ""))
234
+ elif element_type == "user":
235
+ # @用户提及
236
+ user_id = section_element.get("user_id", "")
237
+ if user_id:
238
+ # 将之前的文本内容先添加到组件中
239
+ text_content = "".join(text_parts)
240
+ if text_content.strip():
241
+ message_components.append(
242
+ Plain(text=text_content),
243
+ )
244
+ text_parts = []
245
+ # 添加@提及组件
246
+ message_components.append(At(qq=user_id, name=""))
247
+ elif element_type == "channel":
248
+ # #频道提及
249
+ channel_id = section_element.get("channel_id", "")
250
+ text_parts.append(f"#{channel_id}")
251
+ elif element_type == "link":
252
+ # 链接
253
+ url = section_element.get("url", "")
254
+ link_text = section_element.get("text", url)
255
+ text_parts.append(f"[{link_text}]({url})")
256
+ elif element_type == "emoji":
257
+ # 表情符号
258
+ emoji_name = section_element.get("name", "")
259
+ text_parts.append(f":{emoji_name}:")
260
+
261
+ text_content = "".join(text_parts)
262
+
263
+ if text_content.strip():
264
+ message_components.append(Plain(text=text_content))
265
+
266
+ elif element.get("type") == "rich_text_list":
267
+ # 处理列表
268
+ list_items = element.get("elements", [])
269
+ list_text = ""
270
+ for item in list_items:
271
+ if item.get("type") == "rich_text_section":
272
+ item_elements = item.get("elements", [])
273
+ item_text = ""
274
+ for item_element in item_elements:
275
+ if item_element.get("type") == "text":
276
+ item_text += item_element.get("text", "")
277
+ list_text += f"• {item_text}\n"
278
+
279
+ if list_text.strip():
280
+ message_components.append(Plain(text=list_text.strip()))
281
+
282
+ elif block_type == "section":
283
+ # 处理段落块
284
+ if "text" in block:
285
+ text_obj = block["text"]
286
+ if text_obj.get("type") == "mrkdwn":
287
+ text_content = text_obj.get("text", "")
288
+ message_components.append(Plain(text=text_content))
289
+
290
+ return message_components
291
+
292
+ async def _handle_socket_event(self, req: SocketModeRequest):
293
+ """处理 Socket Mode 事件"""
294
+ if req.type == "events_api":
295
+ # 事件 API
296
+ event = req.payload.get("event", {})
297
+
298
+ # 忽略机器人自己的消息和消息编辑
299
+ if event.get("subtype") in [
300
+ "bot_message",
301
+ "message_changed",
302
+ "message_deleted",
303
+ ]:
304
+ return
305
+
306
+ if event.get("bot_id"):
307
+ return
308
+
309
+ if event.get("type") in ["message", "app_mention"]:
310
+ abm = await self.convert_message(event)
311
+ if abm:
312
+ await self.handle_msg(abm)
313
+
314
+ async def get_bot_user_id(self):
315
+ auth_info = await self.web_client.auth_test()
316
+ return auth_info.get("user_id")
317
+
318
+ async def get_file_base64(self, url: str) -> str:
319
+ """下载 Slack 文件并返回 Base64 编码的内容"""
320
+ headers = {"Authorization": f"Bearer {self.bot_token}"}
321
+ async with aiohttp.ClientSession() as session:
322
+ async with session.get(url, headers=headers) as resp:
323
+ if resp.status == 200:
324
+ content = await resp.read()
325
+ base64_content = base64.b64encode(content).decode("utf-8")
326
+ return base64_content
327
+ logger.error(
328
+ f"Failed to download slack file: {resp.status} {await resp.text()}",
329
+ )
330
+ raise Exception(f"下载文件失败: {resp.status}")
331
+
332
+ async def run(self) -> Awaitable[Any]:
333
+ self.bot_self_id = await self.get_bot_user_id()
334
+ logger.info(f"Slack auth test OK. Bot ID: {self.bot_self_id}")
335
+
336
+ if self.connection_mode == "socket":
337
+ if not self.app_token:
338
+ raise ValueError("Socket Mode 需要 app_token")
339
+
340
+ # 创建 Socket 客户端
341
+ self.socket_client = SlackSocketClient(
342
+ self.web_client,
343
+ self.app_token,
344
+ self._handle_socket_event,
345
+ )
346
+
347
+ logger.info("Slack 适配器 (Socket Mode) 启动中...")
348
+ await self.socket_client.start()
349
+
350
+ elif self.connection_mode == "webhook":
351
+ if not self.signing_secret:
352
+ raise ValueError("Webhook Mode 需要 signing_secret")
353
+
354
+ # 创建 Webhook 客户端
355
+ self.webhook_client = SlackWebhookClient(
356
+ self.web_client,
357
+ self.signing_secret,
358
+ self.webhook_host,
359
+ self.webhook_port,
360
+ self.webhook_path,
361
+ self._handle_webhook_event,
362
+ )
363
+
364
+ logger.info(
365
+ f"Slack 适配器 (Webhook Mode) 启动中,监听 {self.webhook_host}:{self.webhook_port}{self.webhook_path}...",
366
+ )
367
+ await self.webhook_client.start()
368
+
369
+ else:
370
+ raise ValueError(
371
+ f"不支持的连接模式: {self.connection_mode},请使用 'socket' 或 'webhook'",
372
+ )
373
+
374
+ async def _handle_webhook_event(self, event_data: dict):
375
+ """处理 Webhook 事件"""
376
+ event = event_data.get("event", {})
377
+
378
+ # 忽略机器人自己的消息和消息编辑
379
+ if event.get("subtype") in [
380
+ "bot_message",
381
+ "message_changed",
382
+ "message_deleted",
383
+ ]:
384
+ return
385
+
386
+ if event.get("bot_id"):
387
+ return
388
+
389
+ if event.get("type") in ["message", "app_mention"]:
390
+ abm = await self.convert_message(event)
391
+ if abm:
392
+ await self.handle_msg(abm)
393
+
394
+ async def terminate(self):
395
+ if self.socket_client:
396
+ await self.socket_client.stop()
397
+ if self.webhook_client:
398
+ await self.webhook_client.stop()
399
+ logger.info("Slack 适配器已被优雅地关闭")
400
+
401
+ def meta(self) -> PlatformMetadata:
402
+ return self.metadata
403
+
404
+ async def handle_msg(self, message: AstrBotMessage):
405
+ message_event = SlackMessageEvent(
406
+ message_str=message.message_str,
407
+ message_obj=message,
408
+ platform_meta=self.meta(),
409
+ session_id=message.session_id,
410
+ web_client=self.web_client,
411
+ )
412
+
413
+ self.commit_event(message_event)
414
+
415
+ def get_client(self):
416
+ return self.web_client
@@ -0,0 +1,253 @@
1
+ import asyncio
2
+ import re
3
+ from collections.abc import AsyncGenerator
4
+
5
+ from slack_sdk.web.async_client import AsyncWebClient
6
+
7
+ from astrbot.api import logger
8
+ from astrbot.api.event import AstrMessageEvent, MessageChain
9
+ from astrbot.api.message_components import (
10
+ BaseMessageComponent,
11
+ File,
12
+ Image,
13
+ Plain,
14
+ )
15
+ from astrbot.api.platform import Group, MessageMember
16
+
17
+
18
+ class SlackMessageEvent(AstrMessageEvent):
19
+ def __init__(
20
+ self,
21
+ message_str,
22
+ message_obj,
23
+ platform_meta,
24
+ session_id,
25
+ web_client: AsyncWebClient,
26
+ ):
27
+ super().__init__(message_str, message_obj, platform_meta, session_id)
28
+ self.web_client = web_client
29
+
30
+ @staticmethod
31
+ async def _from_segment_to_slack_block(
32
+ segment: BaseMessageComponent,
33
+ web_client: AsyncWebClient,
34
+ ) -> dict:
35
+ """将消息段转换为 Slack 块格式"""
36
+ if isinstance(segment, Plain):
37
+ return {"type": "section", "text": {"type": "mrkdwn", "text": segment.text}}
38
+ if isinstance(segment, Image):
39
+ # upload file
40
+ url = segment.url or segment.file
41
+ if url.startswith("http"):
42
+ return {
43
+ "type": "image",
44
+ "image_url": url,
45
+ "alt_text": "图片",
46
+ }
47
+ path = await segment.convert_to_file_path()
48
+ response = await web_client.files_upload_v2(
49
+ file=path,
50
+ filename="image.jpg",
51
+ )
52
+ if not response["ok"]:
53
+ logger.error(f"Slack file upload failed: {response['error']}")
54
+ return {
55
+ "type": "section",
56
+ "text": {"type": "mrkdwn", "text": "图片上传失败"},
57
+ }
58
+ image_url = response["files"][0]["url_private"]
59
+ logger.debug(f"Slack file upload response: {response}")
60
+ return {
61
+ "type": "image",
62
+ "slack_file": {
63
+ "url": image_url,
64
+ },
65
+ "alt_text": "图片",
66
+ }
67
+ if isinstance(segment, File):
68
+ # upload file
69
+ url = segment.url or segment.file
70
+ response = await web_client.files_upload_v2(
71
+ file=url,
72
+ filename=segment.name or "file",
73
+ )
74
+ if not response["ok"]:
75
+ logger.error(f"Slack file upload failed: {response['error']}")
76
+ return {
77
+ "type": "section",
78
+ "text": {"type": "mrkdwn", "text": "文件上传失败"},
79
+ }
80
+ file_url = response["files"][0]["permalink"]
81
+ return {
82
+ "type": "section",
83
+ "text": {
84
+ "type": "mrkdwn",
85
+ "text": f"文件: <{file_url}|{segment.name or '文件'}>",
86
+ },
87
+ }
88
+ return {"type": "section", "text": {"type": "mrkdwn", "text": str(segment)}}
89
+
90
+ @staticmethod
91
+ async def _parse_slack_blocks(
92
+ message_chain: MessageChain,
93
+ web_client: AsyncWebClient,
94
+ ):
95
+ """解析成 Slack 块格式"""
96
+ blocks = []
97
+ text_content = ""
98
+
99
+ for segment in message_chain.chain:
100
+ if isinstance(segment, Plain):
101
+ text_content += segment.text
102
+ else:
103
+ # 如果有文本内容,先添加文本块
104
+ if text_content.strip():
105
+ blocks.append(
106
+ {
107
+ "type": "section",
108
+ "text": {"type": "mrkdwn", "text": text_content},
109
+ },
110
+ )
111
+ text_content = ""
112
+
113
+ # 添加其他类型的块
114
+ block = await SlackMessageEvent._from_segment_to_slack_block(
115
+ segment,
116
+ web_client,
117
+ )
118
+ blocks.append(block)
119
+
120
+ # 如果最后还有文本内容
121
+ if text_content.strip():
122
+ blocks.append(
123
+ {"type": "section", "text": {"type": "mrkdwn", "text": text_content}},
124
+ )
125
+
126
+ return blocks, "" if blocks else text_content
127
+
128
+ async def send(self, message: MessageChain):
129
+ blocks, text = await SlackMessageEvent._parse_slack_blocks(
130
+ message,
131
+ self.web_client,
132
+ )
133
+
134
+ try:
135
+ if self.get_group_id():
136
+ # 发送到频道
137
+ await self.web_client.chat_postMessage(
138
+ channel=self.get_group_id(),
139
+ text=text,
140
+ blocks=blocks or None,
141
+ )
142
+ else:
143
+ # 发送私信
144
+ await self.web_client.chat_postMessage(
145
+ channel=self.get_sender_id(),
146
+ text=text,
147
+ blocks=blocks or None,
148
+ )
149
+ except Exception:
150
+ # 如果块发送失败,尝试只发送文本
151
+ parts = []
152
+ for segment in message.chain:
153
+ if isinstance(segment, Plain):
154
+ parts.append(segment.text)
155
+ elif isinstance(segment, File):
156
+ parts.append(f" [文件: {segment.name}] ")
157
+ elif isinstance(segment, Image):
158
+ parts.append(" [图片] ")
159
+ fallback_text = "".join(parts)
160
+
161
+ if self.get_group_id():
162
+ await self.web_client.chat_postMessage(
163
+ channel=self.get_group_id(),
164
+ text=fallback_text,
165
+ )
166
+ else:
167
+ await self.web_client.chat_postMessage(
168
+ channel=self.get_sender_id(),
169
+ text=fallback_text,
170
+ )
171
+
172
+ await super().send(message)
173
+
174
+ async def send_streaming(
175
+ self,
176
+ generator: AsyncGenerator,
177
+ use_fallback: bool = False,
178
+ ):
179
+ if not use_fallback:
180
+ buffer = None
181
+ async for chain in generator:
182
+ if not buffer:
183
+ buffer = chain
184
+ else:
185
+ buffer.chain.extend(chain.chain)
186
+ if not buffer:
187
+ return None
188
+ buffer.squash_plain()
189
+ await self.send(buffer)
190
+ return await super().send_streaming(generator, use_fallback)
191
+
192
+ buffer = ""
193
+ pattern = re.compile(r"[^。?!~…]+[。?!~…]+")
194
+
195
+ async for chain in generator:
196
+ if isinstance(chain, MessageChain):
197
+ for comp in chain.chain:
198
+ if isinstance(comp, Plain):
199
+ buffer += comp.text
200
+ if any(p in buffer for p in "。?!~…"):
201
+ buffer = await self.process_buffer(buffer, pattern)
202
+ else:
203
+ await self.send(MessageChain(chain=[comp]))
204
+ await asyncio.sleep(1.5) # 限速
205
+
206
+ if buffer.strip():
207
+ await self.send(MessageChain([Plain(buffer)]))
208
+ return await super().send_streaming(generator, use_fallback)
209
+
210
+ async def get_group(self, group_id=None, **kwargs):
211
+ if group_id:
212
+ channel_id = group_id
213
+ elif self.get_group_id():
214
+ channel_id = self.get_group_id()
215
+ else:
216
+ return None
217
+
218
+ try:
219
+ # 获取频道信息
220
+ channel_info = await self.web_client.conversations_info(channel=channel_id)
221
+
222
+ # 获取频道成员
223
+ members_response = await self.web_client.conversations_members(
224
+ channel=channel_id,
225
+ )
226
+
227
+ members = []
228
+ for member_id in members_response["members"]:
229
+ try:
230
+ user_info = await self.web_client.users_info(user=member_id)
231
+ user_data = user_info["user"]
232
+ members.append(
233
+ MessageMember(
234
+ user_id=member_id,
235
+ nickname=user_data.get("real_name")
236
+ or user_data.get("name", member_id),
237
+ ),
238
+ )
239
+ except Exception:
240
+ # 如果获取用户信息失败,使用默认信息
241
+ members.append(MessageMember(user_id=member_id, nickname=member_id))
242
+
243
+ channel_data = channel_info["channel"]
244
+ return Group(
245
+ group_id=channel_id,
246
+ group_name=channel_data.get("name", ""),
247
+ group_avatar="",
248
+ group_admins=[], # Slack 的管理员信息需要特殊权限获取
249
+ group_owner=channel_data.get("creator", ""),
250
+ members=members,
251
+ )
252
+ except Exception:
253
+ return None