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,432 @@
1
+ from typing import TYPE_CHECKING
2
+
3
+ from astrbot.api import logger
4
+ from astrbot.api.event import AstrMessageEvent, MessageChain
5
+ from astrbot.api.message_components import (
6
+ At,
7
+ File,
8
+ Forward,
9
+ Image,
10
+ Node,
11
+ Nodes,
12
+ Plain,
13
+ Record,
14
+ Reply,
15
+ Video,
16
+ )
17
+ from astrbot.api.platform import AstrBotMessage, PlatformMetadata
18
+
19
+ if TYPE_CHECKING:
20
+ from .satori_adapter import SatoriPlatformAdapter
21
+
22
+
23
+ class SatoriPlatformEvent(AstrMessageEvent):
24
+ def __init__(
25
+ self,
26
+ message_str: str,
27
+ message_obj: AstrBotMessage,
28
+ platform_meta: PlatformMetadata,
29
+ session_id: str,
30
+ adapter: "SatoriPlatformAdapter",
31
+ ):
32
+ # 更新平台元数据
33
+ if adapter and hasattr(adapter, "logins") and adapter.logins:
34
+ current_login = adapter.logins[0]
35
+ platform_name = current_login.get("platform", "satori")
36
+ user = current_login.get("user", {})
37
+ user_id = user.get("id", "") if user else ""
38
+ if not platform_meta.id and user_id:
39
+ platform_meta.id = f"{platform_name}({user_id})"
40
+
41
+ super().__init__(message_str, message_obj, platform_meta, session_id)
42
+ self.adapter = adapter
43
+ self.platform = None
44
+ self.user_id = None
45
+ if (
46
+ hasattr(message_obj, "raw_message")
47
+ and message_obj.raw_message
48
+ and isinstance(message_obj.raw_message, dict)
49
+ ):
50
+ login = message_obj.raw_message.get("login", {})
51
+ self.platform = login.get("platform")
52
+ user = login.get("user", {})
53
+ self.user_id = user.get("id") if user else None
54
+
55
+ @classmethod
56
+ async def send_with_adapter(
57
+ cls,
58
+ adapter: "SatoriPlatformAdapter",
59
+ message: MessageChain,
60
+ session_id: str,
61
+ ):
62
+ try:
63
+ content_parts = []
64
+
65
+ for component in message.chain:
66
+ component_content = await cls._convert_component_to_satori_static(
67
+ component,
68
+ )
69
+ if component_content:
70
+ content_parts.append(component_content)
71
+
72
+ # 特殊处理 Node 和 Nodes 组件
73
+ if isinstance(component, Node):
74
+ # 单个转发节点
75
+ node_content = await cls._convert_node_to_satori_static(component)
76
+ if node_content:
77
+ content_parts.append(node_content)
78
+
79
+ elif isinstance(component, Nodes):
80
+ # 合并转发消息
81
+ node_content = await cls._convert_nodes_to_satori_static(component)
82
+ if node_content:
83
+ content_parts.append(node_content)
84
+
85
+ content = "".join(content_parts)
86
+ channel_id = session_id
87
+ data = {"channel_id": channel_id, "content": content}
88
+
89
+ platform = None
90
+ user_id = None
91
+
92
+ if hasattr(adapter, "logins") and adapter.logins:
93
+ current_login = adapter.logins[0]
94
+ platform = current_login.get("platform", "")
95
+ user = current_login.get("user", {})
96
+ user_id = user.get("id", "") if user else ""
97
+
98
+ result = await adapter.send_http_request(
99
+ "POST",
100
+ "/message.create",
101
+ data,
102
+ platform,
103
+ user_id,
104
+ )
105
+ if result:
106
+ return result
107
+ return None
108
+
109
+ except Exception as e:
110
+ logger.error(f"Satori 消息发送异常: {e}")
111
+ return None
112
+
113
+ async def send(self, message: MessageChain):
114
+ platform = getattr(self, "platform", None)
115
+ user_id = getattr(self, "user_id", None)
116
+
117
+ if not platform or not user_id:
118
+ if hasattr(self.adapter, "logins") and self.adapter.logins:
119
+ current_login = self.adapter.logins[0]
120
+ platform = current_login.get("platform", "")
121
+ user = current_login.get("user", {})
122
+ user_id = user.get("id", "") if user else ""
123
+
124
+ try:
125
+ content_parts = []
126
+
127
+ for component in message.chain:
128
+ component_content = await self._convert_component_to_satori(component)
129
+ if component_content:
130
+ content_parts.append(component_content)
131
+
132
+ # 特殊处理 Node 和 Nodes 组件
133
+ if isinstance(component, Node):
134
+ # 单个转发节点
135
+ node_content = await self._convert_node_to_satori(component)
136
+ if node_content:
137
+ content_parts.append(node_content)
138
+
139
+ elif isinstance(component, Nodes):
140
+ # 合并转发消息
141
+ node_content = await self._convert_nodes_to_satori(component)
142
+ if node_content:
143
+ content_parts.append(node_content)
144
+
145
+ content = "".join(content_parts)
146
+ channel_id = self.session_id
147
+ data = {"channel_id": channel_id, "content": content}
148
+
149
+ result = await self.adapter.send_http_request(
150
+ "POST",
151
+ "/message.create",
152
+ data,
153
+ platform,
154
+ user_id,
155
+ )
156
+ if not result:
157
+ logger.error("Satori 消息发送失败")
158
+ except Exception as e:
159
+ logger.error(f"Satori 消息发送异常: {e}")
160
+
161
+ await super().send(message)
162
+
163
+ async def send_streaming(self, generator, use_fallback: bool = False):
164
+ try:
165
+ content_parts = []
166
+
167
+ async for chain in generator:
168
+ if isinstance(chain, MessageChain):
169
+ if chain.type == "break":
170
+ if content_parts:
171
+ content = "".join(content_parts)
172
+ temp_chain = MessageChain([Plain(text=content)])
173
+ await self.send(temp_chain)
174
+ content_parts = []
175
+ continue
176
+
177
+ for component in chain.chain:
178
+ if isinstance(component, Plain):
179
+ content_parts.append(component.text)
180
+ elif isinstance(component, Image):
181
+ if content_parts:
182
+ content = "".join(content_parts)
183
+ temp_chain = MessageChain([Plain(text=content)])
184
+ await self.send(temp_chain)
185
+ content_parts = []
186
+ try:
187
+ image_base64 = await component.convert_to_base64()
188
+ if image_base64:
189
+ img_chain = MessageChain(
190
+ [
191
+ Plain(
192
+ text=f'<img src="data:image/jpeg;base64,{image_base64}"/>',
193
+ ),
194
+ ],
195
+ )
196
+ await self.send(img_chain)
197
+ except Exception as e:
198
+ logger.error(f"图片转换为base64失败: {e}")
199
+ else:
200
+ content_parts.append(str(component))
201
+
202
+ if content_parts:
203
+ content = "".join(content_parts)
204
+ temp_chain = MessageChain([Plain(text=content)])
205
+ await self.send(temp_chain)
206
+
207
+ except Exception as e:
208
+ logger.error(f"Satori 流式消息发送异常: {e}")
209
+
210
+ return await super().send_streaming(generator, use_fallback)
211
+
212
+ async def _convert_component_to_satori(self, component) -> str:
213
+ """将单个消息组件转换为 Satori 格式"""
214
+ try:
215
+ if isinstance(component, Plain):
216
+ text = (
217
+ component.text.replace("&", "&amp;")
218
+ .replace("<", "&lt;")
219
+ .replace(">", "&gt;")
220
+ )
221
+ return text
222
+
223
+ if isinstance(component, At):
224
+ if component.qq:
225
+ return f'<at id="{component.qq}"/>'
226
+ if component.name:
227
+ return f'<at name="{component.name}"/>'
228
+
229
+ elif isinstance(component, Image):
230
+ try:
231
+ image_base64 = await component.convert_to_base64()
232
+ if image_base64:
233
+ return f'<img src="data:image/jpeg;base64,{image_base64}"/>'
234
+ except Exception as e:
235
+ logger.error(f"图片转换为base64失败: {e}")
236
+
237
+ elif isinstance(component, File):
238
+ return (
239
+ f'<file src="{component.file}" name="{component.name or "文件"}"/>'
240
+ )
241
+
242
+ elif isinstance(component, Record):
243
+ try:
244
+ record_base64 = await component.convert_to_base64()
245
+ if record_base64:
246
+ return f'<audio src="data:audio/wav;base64,{record_base64}"/>'
247
+ except Exception as e:
248
+ logger.error(f"语音转换为base64失败: {e}")
249
+
250
+ elif isinstance(component, Reply):
251
+ return f'<reply id="{component.id}"/>'
252
+
253
+ elif isinstance(component, Video):
254
+ try:
255
+ video_path_url = await component.convert_to_file_path()
256
+ if video_path_url:
257
+ return f'<video src="{video_path_url}"/>'
258
+ except Exception as e:
259
+ logger.error(f"视频文件转换失败: {e}")
260
+
261
+ elif isinstance(component, Forward):
262
+ return f'<message id="{component.id}" forward/>'
263
+
264
+ # 对于其他未处理的组件类型,返回空字符串
265
+ return ""
266
+
267
+ except Exception as e:
268
+ logger.error(f"转换消息组件失败: {e}")
269
+ return ""
270
+
271
+ async def _convert_node_to_satori(self, node: Node) -> str:
272
+ """将单个转发节点转换为 Satori 格式"""
273
+ try:
274
+ content_parts = []
275
+ if node.content:
276
+ for content_component in node.content:
277
+ component_content = await self._convert_component_to_satori(
278
+ content_component,
279
+ )
280
+ if component_content:
281
+ content_parts.append(component_content)
282
+
283
+ content = "".join(content_parts)
284
+
285
+ # 如果内容为空,添加默认内容
286
+ if not content.strip():
287
+ content = "[转发消息]"
288
+
289
+ # 构建 Satori 格式的转发节点
290
+ author_attrs = []
291
+ if node.uin:
292
+ author_attrs.append(f'id="{node.uin}"')
293
+ if node.name:
294
+ author_attrs.append(f'name="{node.name}"')
295
+
296
+ author_attr_str = " ".join(author_attrs)
297
+
298
+ return f"<message><author {author_attr_str}/>{content}</message>"
299
+
300
+ except Exception as e:
301
+ logger.error(f"转换转发节点失败: {e}")
302
+ return ""
303
+
304
+ @classmethod
305
+ async def _convert_component_to_satori_static(cls, component) -> str:
306
+ """将单个消息组件转换为 Satori 格式"""
307
+ try:
308
+ if isinstance(component, Plain):
309
+ text = (
310
+ component.text.replace("&", "&amp;")
311
+ .replace("<", "&lt;")
312
+ .replace(">", "&gt;")
313
+ )
314
+ return text
315
+
316
+ if isinstance(component, At):
317
+ if component.qq:
318
+ return f'<at id="{component.qq}"/>'
319
+ if component.name:
320
+ return f'<at name="{component.name}"/>'
321
+
322
+ elif isinstance(component, Image):
323
+ try:
324
+ image_base64 = await component.convert_to_base64()
325
+ if image_base64:
326
+ return f'<img src="data:image/jpeg;base64,{image_base64}"/>'
327
+ except Exception as e:
328
+ logger.error(f"图片转换为base64失败: {e}")
329
+
330
+ elif isinstance(component, File):
331
+ return (
332
+ f'<file src="{component.file}" name="{component.name or "文件"}"/>'
333
+ )
334
+
335
+ elif isinstance(component, Record):
336
+ try:
337
+ record_base64 = await component.convert_to_base64()
338
+ if record_base64:
339
+ return f'<audio src="data:audio/wav;base64,{record_base64}"/>'
340
+ except Exception as e:
341
+ logger.error(f"语音转换为base64失败: {e}")
342
+
343
+ elif isinstance(component, Reply):
344
+ return f'<reply id="{component.id}"/>'
345
+
346
+ elif isinstance(component, Video):
347
+ try:
348
+ video_path_url = await component.convert_to_file_path()
349
+ if video_path_url:
350
+ return f'<video src="{video_path_url}"/>'
351
+ except Exception as e:
352
+ logger.error(f"视频文件转换失败: {e}")
353
+
354
+ elif isinstance(component, Forward):
355
+ return f'<message id="{component.id}" forward/>'
356
+
357
+ # 对于其他未处理的组件类型,返回空字符串
358
+ return ""
359
+
360
+ except Exception as e:
361
+ logger.error(f"转换消息组件失败: {e}")
362
+ return ""
363
+
364
+ @classmethod
365
+ async def _convert_node_to_satori_static(cls, node: Node) -> str:
366
+ """将单个转发节点转换为 Satori 格式"""
367
+ try:
368
+ content_parts = []
369
+ if node.content:
370
+ for content_component in node.content:
371
+ component_content = await cls._convert_component_to_satori_static(
372
+ content_component,
373
+ )
374
+ if component_content:
375
+ content_parts.append(component_content)
376
+
377
+ content = "".join(content_parts)
378
+
379
+ # 如果内容为空,添加默认内容
380
+ if not content.strip():
381
+ content = "[转发消息]"
382
+
383
+ author_attrs = []
384
+ if node.uin:
385
+ author_attrs.append(f'id="{node.uin}"')
386
+ if node.name:
387
+ author_attrs.append(f'name="{node.name}"')
388
+
389
+ author_attr_str = " ".join(author_attrs)
390
+
391
+ return f"<message><author {author_attr_str}/>{content}</message>"
392
+
393
+ except Exception as e:
394
+ logger.error(f"转换转发节点失败: {e}")
395
+ return ""
396
+
397
+ async def _convert_nodes_to_satori(self, nodes: Nodes) -> str:
398
+ """将多个转发节点转换为 Satori 格式的合并转发"""
399
+ try:
400
+ node_parts = []
401
+
402
+ for node in nodes.nodes:
403
+ node_content = await self._convert_node_to_satori(node)
404
+ if node_content:
405
+ node_parts.append(node_content)
406
+
407
+ if node_parts:
408
+ return f"<message forward>{''.join(node_parts)}</message>"
409
+ return ""
410
+
411
+ except Exception as e:
412
+ logger.error(f"转换合并转发消息失败: {e}")
413
+ return ""
414
+
415
+ @classmethod
416
+ async def _convert_nodes_to_satori_static(cls, nodes: Nodes) -> str:
417
+ """将多个转发节点转换为 Satori 格式的合并转发"""
418
+ try:
419
+ node_parts = []
420
+
421
+ for node in nodes.nodes:
422
+ node_content = await cls._convert_node_to_satori_static(node)
423
+ if node_content:
424
+ node_parts.append(node_content)
425
+
426
+ if node_parts:
427
+ return f"<message forward>{''.join(node_parts)}</message>"
428
+ return ""
429
+
430
+ except Exception as e:
431
+ logger.error(f"转换合并转发消息失败: {e}")
432
+ return ""
@@ -0,0 +1,164 @@
1
+ import asyncio
2
+ import hashlib
3
+ import hmac
4
+ import json
5
+ import logging
6
+ from collections.abc import Callable
7
+
8
+ from quart import Quart, Response, request
9
+ from slack_sdk.socket_mode.aiohttp import SocketModeClient
10
+ from slack_sdk.socket_mode.request import SocketModeRequest
11
+ from slack_sdk.socket_mode.response import SocketModeResponse
12
+ from slack_sdk.web.async_client import AsyncWebClient
13
+
14
+ from astrbot.api import logger
15
+
16
+
17
+ class SlackWebhookClient:
18
+ """Slack Webhook 模式客户端,使用 Quart 作为 Web 服务器"""
19
+
20
+ def __init__(
21
+ self,
22
+ web_client: AsyncWebClient,
23
+ signing_secret: str,
24
+ host: str = "0.0.0.0",
25
+ port: int = 3000,
26
+ path: str = "/slack/events",
27
+ event_handler: Callable | None = None,
28
+ ):
29
+ self.web_client = web_client
30
+ self.signing_secret = signing_secret
31
+ self.host = host
32
+ self.port = port
33
+ self.path = path
34
+ self.event_handler = event_handler
35
+
36
+ self.app = Quart(__name__)
37
+ self._setup_routes()
38
+
39
+ # 禁用 Quart 的默认日志输出
40
+ logging.getLogger("quart.app").setLevel(logging.WARNING)
41
+ logging.getLogger("quart.serving").setLevel(logging.WARNING)
42
+
43
+ self.shutdown_event = asyncio.Event()
44
+
45
+ def _setup_routes(self):
46
+ """设置路由"""
47
+
48
+ @self.app.route(self.path, methods=["POST"])
49
+ async def slack_events():
50
+ """处理 Slack 事件"""
51
+ try:
52
+ # 获取请求体和头部
53
+ body = await request.get_data()
54
+ event_data = json.loads(body.decode("utf-8"))
55
+
56
+ # Verify Slack request signature
57
+ timestamp = request.headers.get("X-Slack-Request-Timestamp")
58
+ signature = request.headers.get("X-Slack-Signature")
59
+ if not timestamp or not signature:
60
+ return Response("Missing headers", status=400)
61
+ # Calculate the HMAC signature
62
+ sig_basestring = f"v0:{timestamp}:{body.decode('utf-8')}"
63
+ my_signature = (
64
+ "v0="
65
+ + hmac.new(
66
+ self.signing_secret.encode("utf-8"),
67
+ sig_basestring.encode("utf-8"),
68
+ hashlib.sha256,
69
+ ).hexdigest()
70
+ )
71
+ # Verify the signature
72
+ if not hmac.compare_digest(my_signature, signature):
73
+ logger.warning("Slack request signature verification failed")
74
+ return Response("Invalid signature", status=400)
75
+ logger.info(f"Received Slack event: {event_data}")
76
+
77
+ # 处理 URL 验证事件
78
+ if event_data.get("type") == "url_verification":
79
+ return {"challenge": event_data.get("challenge")}
80
+ # 处理事件
81
+ if self.event_handler and event_data.get("type") == "event_callback":
82
+ await self.event_handler(event_data)
83
+
84
+ return Response("", status=200)
85
+
86
+ except Exception as e:
87
+ logger.error(f"处理 Slack 事件时出错: {e}")
88
+ return Response("Internal Server Error", status=500)
89
+
90
+ @self.app.route("/health", methods=["GET"])
91
+ async def health_check():
92
+ """健康检查端点"""
93
+ return {"status": "ok", "service": "slack-webhook"}
94
+
95
+ async def start(self):
96
+ """启动 Webhook 服务器"""
97
+ logger.info(
98
+ f"Slack Webhook 服务器启动中,监听 {self.host}:{self.port}{self.path}...",
99
+ )
100
+
101
+ await self.app.run_task(
102
+ host=self.host,
103
+ port=self.port,
104
+ debug=False,
105
+ shutdown_trigger=self.shutdown_trigger,
106
+ )
107
+
108
+ async def shutdown_trigger(self):
109
+ await self.shutdown_event.wait()
110
+
111
+ async def stop(self):
112
+ """停止 Webhook 服务器"""
113
+ self.shutdown_event.set()
114
+ logger.info("Slack Webhook 服务器已停止")
115
+
116
+
117
+ class SlackSocketClient:
118
+ """Slack Socket 模式客户端"""
119
+
120
+ def __init__(
121
+ self,
122
+ web_client: AsyncWebClient,
123
+ app_token: str,
124
+ event_handler: Callable | None = None,
125
+ ):
126
+ self.web_client = web_client
127
+ self.app_token = app_token
128
+ self.event_handler = event_handler
129
+ self.socket_client = None
130
+
131
+ async def _handle_events(self, _: SocketModeClient, req: SocketModeRequest):
132
+ """处理 Socket Mode 事件"""
133
+ try:
134
+ # 确认收到事件
135
+ response = SocketModeResponse(envelope_id=req.envelope_id)
136
+ await self.socket_client.send_socket_mode_response(response)
137
+
138
+ # 处理事件
139
+ if self.event_handler:
140
+ await self.event_handler(req)
141
+
142
+ except Exception as e:
143
+ logger.error(f"处理 Socket Mode 事件时出错: {e}")
144
+
145
+ async def start(self):
146
+ """启动 Socket Mode 连接"""
147
+ self.socket_client = SocketModeClient(
148
+ app_token=self.app_token,
149
+ logger=logger,
150
+ web_client=self.web_client,
151
+ )
152
+
153
+ # 注册事件处理器
154
+ self.socket_client.socket_mode_request_listeners.append(self._handle_events)
155
+
156
+ logger.info("Slack Socket Mode 客户端启动中...")
157
+ await self.socket_client.connect()
158
+
159
+ async def stop(self):
160
+ """停止 Socket Mode 连接"""
161
+ if self.socket_client:
162
+ await self.socket_client.disconnect()
163
+ await self.socket_client.close()
164
+ logger.info("Slack Socket Mode 客户端已停止")