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,792 @@
1
+ import asyncio
2
+ import json
3
+ import time
4
+ from xml.etree import ElementTree as ET
5
+
6
+ import websockets
7
+ from aiohttp import ClientSession, ClientTimeout
8
+ from websockets.asyncio.client import ClientConnection, connect
9
+
10
+ from astrbot.api import logger
11
+ from astrbot.api.event import MessageChain
12
+ from astrbot.api.message_components import (
13
+ At,
14
+ File,
15
+ Image,
16
+ Plain,
17
+ Record,
18
+ Reply,
19
+ )
20
+ from astrbot.api.platform import (
21
+ AstrBotMessage,
22
+ MessageMember,
23
+ MessageType,
24
+ Platform,
25
+ PlatformMetadata,
26
+ register_platform_adapter,
27
+ )
28
+ from astrbot.core.platform.astr_message_event import MessageSession
29
+
30
+
31
+ @register_platform_adapter(
32
+ "satori", "Satori 协议适配器", support_streaming_message=False
33
+ )
34
+ class SatoriPlatformAdapter(Platform):
35
+ def __init__(
36
+ self,
37
+ platform_config: dict,
38
+ platform_settings: dict,
39
+ event_queue: asyncio.Queue,
40
+ ) -> None:
41
+ super().__init__(event_queue)
42
+ self.config = platform_config
43
+ self.settings = platform_settings
44
+
45
+ self.api_base_url = self.config.get(
46
+ "satori_api_base_url",
47
+ "http://localhost:5140/satori/v1",
48
+ )
49
+ self.token = self.config.get("satori_token", "")
50
+ self.endpoint = self.config.get(
51
+ "satori_endpoint",
52
+ "ws://localhost:5140/satori/v1/events",
53
+ )
54
+ self.auto_reconnect = self.config.get("satori_auto_reconnect", True)
55
+ self.heartbeat_interval = self.config.get("satori_heartbeat_interval", 10)
56
+ self.reconnect_delay = self.config.get("satori_reconnect_delay", 5)
57
+
58
+ self.metadata = PlatformMetadata(
59
+ name="satori",
60
+ description="Satori 通用协议适配器",
61
+ id=self.config["id"],
62
+ support_streaming_message=False,
63
+ )
64
+
65
+ self.ws: ClientConnection | None = None
66
+ self.session: ClientSession | None = None
67
+ self.sequence = 0
68
+ self.logins = []
69
+ self.running = False
70
+ self.heartbeat_task: asyncio.Task | None = None
71
+ self.ready_received = False
72
+
73
+ async def send_by_session(
74
+ self,
75
+ session: MessageSession,
76
+ message_chain: MessageChain,
77
+ ):
78
+ from .satori_event import SatoriPlatformEvent
79
+
80
+ await SatoriPlatformEvent.send_with_adapter(
81
+ self,
82
+ message_chain,
83
+ session.session_id,
84
+ )
85
+ await super().send_by_session(session, message_chain)
86
+
87
+ def meta(self) -> PlatformMetadata:
88
+ return self.metadata
89
+
90
+ def _is_websocket_closed(self, ws) -> bool:
91
+ """检查WebSocket连接是否已关闭"""
92
+ if not ws:
93
+ return True
94
+ try:
95
+ if hasattr(ws, "closed"):
96
+ return ws.closed
97
+ if hasattr(ws, "close_code"):
98
+ return ws.close_code is not None
99
+ return False
100
+ except AttributeError:
101
+ return False
102
+
103
+ async def run(self):
104
+ self.running = True
105
+ self.session = ClientSession(timeout=ClientTimeout(total=30))
106
+
107
+ retry_count = 0
108
+ max_retries = 10
109
+
110
+ while self.running:
111
+ try:
112
+ await self.connect_websocket()
113
+ retry_count = 0
114
+ except websockets.exceptions.ConnectionClosed as e:
115
+ logger.warning(f"Satori WebSocket 连接关闭: {e}")
116
+ retry_count += 1
117
+ except Exception as e:
118
+ logger.error(f"Satori WebSocket 连接失败: {e}")
119
+ retry_count += 1
120
+
121
+ if not self.running:
122
+ break
123
+
124
+ if retry_count >= max_retries:
125
+ logger.error(f"达到最大重试次数 ({max_retries}),停止重试")
126
+ break
127
+
128
+ if not self.auto_reconnect:
129
+ break
130
+
131
+ delay = min(self.reconnect_delay * (2 ** (retry_count - 1)), 60)
132
+ await asyncio.sleep(delay)
133
+
134
+ if self.session:
135
+ await self.session.close()
136
+
137
+ async def connect_websocket(self):
138
+ logger.info(f"Satori 适配器正在连接到 WebSocket: {self.endpoint}")
139
+ logger.info(f"Satori 适配器 HTTP API 地址: {self.api_base_url}")
140
+
141
+ if not self.endpoint.startswith(("ws://", "wss://")):
142
+ logger.error(f"无效的WebSocket URL: {self.endpoint}")
143
+ raise ValueError(f"WebSocket URL必须以ws://或wss://开头: {self.endpoint}")
144
+
145
+ try:
146
+ websocket = await connect(self.endpoint, additional_headers={})
147
+ self.ws = websocket
148
+
149
+ await asyncio.sleep(0.1)
150
+
151
+ await self.send_identify()
152
+
153
+ self.heartbeat_task = asyncio.create_task(self.heartbeat_loop())
154
+
155
+ async for message in websocket:
156
+ try:
157
+ await self.handle_message(message) # type: ignore
158
+ except Exception as e:
159
+ logger.error(f"Satori 处理消息异常: {e}")
160
+
161
+ except websockets.exceptions.ConnectionClosed as e:
162
+ logger.warning(f"Satori WebSocket 连接关闭: {e}")
163
+ raise
164
+ except Exception as e:
165
+ logger.error(f"Satori WebSocket 连接异常: {e}")
166
+ raise
167
+ finally:
168
+ if self.heartbeat_task:
169
+ self.heartbeat_task.cancel()
170
+ try:
171
+ await self.heartbeat_task
172
+ except asyncio.CancelledError:
173
+ pass
174
+ if self.ws:
175
+ try:
176
+ await self.ws.close()
177
+ except Exception as e:
178
+ logger.error(f"Satori WebSocket 关闭异常: {e}")
179
+
180
+ async def send_identify(self):
181
+ if not self.ws:
182
+ raise Exception("WebSocket连接未建立")
183
+
184
+ if self._is_websocket_closed(self.ws):
185
+ raise Exception("WebSocket连接已关闭")
186
+
187
+ identify_payload = {
188
+ "op": 3, # IDENTIFY
189
+ "body": {
190
+ "token": str(self.token) if self.token else "", # 字符串
191
+ },
192
+ }
193
+
194
+ # 只有在有序列号时才添加sn字段
195
+ if self.sequence > 0:
196
+ identify_payload["body"]["sn"] = self.sequence
197
+
198
+ try:
199
+ message_str = json.dumps(identify_payload, ensure_ascii=False)
200
+ await self.ws.send(message_str)
201
+ except websockets.exceptions.ConnectionClosed as e:
202
+ logger.error(f"发送 IDENTIFY 信令时连接关闭: {e}")
203
+ raise
204
+ except Exception as e:
205
+ logger.error(f"发送 IDENTIFY 信令失败: {e}")
206
+ raise
207
+
208
+ async def heartbeat_loop(self):
209
+ try:
210
+ while self.running and self.ws:
211
+ await asyncio.sleep(self.heartbeat_interval)
212
+
213
+ if self.ws and not self._is_websocket_closed(self.ws):
214
+ try:
215
+ ping_payload = {
216
+ "op": 1, # PING
217
+ "body": {},
218
+ }
219
+ await self.ws.send(json.dumps(ping_payload, ensure_ascii=False))
220
+ except websockets.exceptions.ConnectionClosed as e:
221
+ logger.error(f"Satori WebSocket 连接关闭: {e}")
222
+ break
223
+ except Exception as e:
224
+ logger.error(f"Satori WebSocket 发送心跳失败: {e}")
225
+ break
226
+ else:
227
+ break
228
+ except asyncio.CancelledError:
229
+ pass
230
+ except Exception as e:
231
+ logger.error(f"心跳任务异常: {e}")
232
+
233
+ async def handle_message(self, message: str):
234
+ try:
235
+ data = json.loads(message)
236
+ op = data.get("op")
237
+ body = data.get("body", {})
238
+
239
+ if op == 4: # READY
240
+ self.logins = body.get("logins", [])
241
+ self.ready_received = True
242
+
243
+ # 输出连接成功的bot信息
244
+ if self.logins:
245
+ for i, login in enumerate(self.logins):
246
+ platform = login.get("platform", "")
247
+ user = login.get("user", {})
248
+ user_id = user.get("id", "")
249
+ user_name = user.get("name", "")
250
+ logger.info(
251
+ f"Satori 连接成功 - Bot {i + 1}: platform={platform}, user_id={user_id}, user_name={user_name}",
252
+ )
253
+
254
+ if "sn" in body:
255
+ self.sequence = body["sn"]
256
+
257
+ elif op == 2: # PONG
258
+ pass
259
+
260
+ elif op == 0: # EVENT
261
+ await self.handle_event(body)
262
+ if "sn" in body:
263
+ self.sequence = body["sn"]
264
+
265
+ elif op == 5: # META
266
+ if "sn" in body:
267
+ self.sequence = body["sn"]
268
+
269
+ except json.JSONDecodeError as e:
270
+ logger.error(f"解析 WebSocket 消息失败: {e}, 消息内容: {message}")
271
+ except Exception as e:
272
+ logger.error(f"处理 WebSocket 消息异常: {e}")
273
+
274
+ async def handle_event(self, event_data: dict):
275
+ try:
276
+ event_type = event_data.get("type")
277
+ sn = event_data.get("sn")
278
+ if sn:
279
+ self.sequence = sn
280
+
281
+ if event_type == "message-created":
282
+ message = event_data.get("message", {})
283
+ user = event_data.get("user", {})
284
+ channel = event_data.get("channel", {})
285
+ guild = event_data.get("guild")
286
+ login = event_data.get("login", {})
287
+ timestamp = event_data.get("timestamp")
288
+
289
+ if user.get("id") == login.get("user", {}).get("id"):
290
+ return
291
+
292
+ abm = await self.convert_satori_message(
293
+ message,
294
+ user,
295
+ channel,
296
+ guild,
297
+ login,
298
+ timestamp,
299
+ )
300
+ if abm:
301
+ await self.handle_msg(abm)
302
+
303
+ except Exception as e:
304
+ logger.error(f"处理事件失败: {e}")
305
+
306
+ async def convert_satori_message(
307
+ self,
308
+ message: dict,
309
+ user: dict,
310
+ channel: dict,
311
+ guild: dict | None,
312
+ login: dict,
313
+ timestamp: int | None = None,
314
+ ) -> AstrBotMessage | None:
315
+ try:
316
+ abm = AstrBotMessage()
317
+ abm.message_id = message.get("id", "")
318
+ abm.raw_message = {
319
+ "message": message,
320
+ "user": user,
321
+ "channel": channel,
322
+ "guild": guild,
323
+ "login": login,
324
+ }
325
+
326
+ if guild and guild.get("id"):
327
+ abm.type = MessageType.GROUP_MESSAGE
328
+ abm.group_id = guild.get("id", "")
329
+ abm.session_id = channel.get("id", "")
330
+ else:
331
+ abm.type = MessageType.FRIEND_MESSAGE
332
+ abm.session_id = channel.get("id", "")
333
+
334
+ abm.sender = MessageMember(
335
+ user_id=user.get("id", ""),
336
+ nickname=user.get("nick", user.get("name", "")),
337
+ )
338
+
339
+ abm.self_id = login.get("user", {}).get("id", "")
340
+
341
+ # 消息链
342
+ abm.message = []
343
+
344
+ content = message.get("content", "")
345
+
346
+ quote = message.get("quote")
347
+ content_for_parsing = content # 副本
348
+
349
+ # 提取<quote>标签
350
+ if "<quote" in content:
351
+ try:
352
+ quote_info = await self._extract_quote_element(content)
353
+ if quote_info:
354
+ quote = quote_info["quote"]
355
+ content_for_parsing = quote_info["content_without_quote"]
356
+ except Exception as e:
357
+ logger.error(f"解析<quote>标签时发生错误: {e}, 错误内容: {content}")
358
+
359
+ if quote:
360
+ # 引用消息
361
+ quote_abm = await self._convert_quote_message(quote)
362
+ if quote_abm:
363
+ sender_id = quote_abm.sender.user_id
364
+ if isinstance(sender_id, str) and sender_id.isdigit():
365
+ sender_id = int(sender_id)
366
+ elif not isinstance(sender_id, int):
367
+ sender_id = 0 # 默认值
368
+
369
+ reply_component = Reply(
370
+ id=quote_abm.message_id,
371
+ chain=quote_abm.message,
372
+ sender_id=quote_abm.sender.user_id,
373
+ sender_nickname=quote_abm.sender.nickname,
374
+ time=quote_abm.timestamp,
375
+ message_str=quote_abm.message_str,
376
+ text=quote_abm.message_str,
377
+ qq=sender_id,
378
+ )
379
+ abm.message.append(reply_component)
380
+
381
+ # 解析消息内容
382
+ content_elements = await self.parse_satori_elements(content_for_parsing)
383
+ abm.message.extend(content_elements)
384
+
385
+ abm.message_str = ""
386
+ for comp in content_elements:
387
+ if isinstance(comp, Plain):
388
+ abm.message_str += comp.text
389
+
390
+ # 优先使用Satori事件中的时间戳
391
+ if timestamp is not None:
392
+ abm.timestamp = timestamp
393
+ else:
394
+ abm.timestamp = int(time.time())
395
+
396
+ return abm
397
+
398
+ except Exception as e:
399
+ logger.error(f"转换 Satori 消息失败: {e}")
400
+ return None
401
+
402
+ def _extract_namespace_prefixes(self, content: str) -> set:
403
+ """提取XML内容中的命名空间前缀"""
404
+ prefixes = set()
405
+
406
+ # 查找所有标签
407
+ i = 0
408
+ while i < len(content):
409
+ # 查找开始标签
410
+ if content[i] == "<" and i + 1 < len(content) and content[i + 1] != "/":
411
+ # 找到标签结束位置
412
+ tag_end = content.find(">", i)
413
+ if tag_end != -1:
414
+ # 提取标签内容
415
+ tag_content = content[i + 1 : tag_end]
416
+ # 检查是否有命名空间前缀
417
+ if ":" in tag_content and "xmlns:" not in tag_content:
418
+ # 分割标签名
419
+ parts = tag_content.split()
420
+ if parts:
421
+ tag_name = parts[0]
422
+ if ":" in tag_name:
423
+ prefix = tag_name.split(":")[0]
424
+ # 确保是有效的命名空间前缀
425
+ if (
426
+ prefix.isalnum()
427
+ or prefix.replace("_", "").isalnum()
428
+ ):
429
+ prefixes.add(prefix)
430
+ i = tag_end + 1
431
+ else:
432
+ i += 1
433
+ # 查找结束标签
434
+ elif content[i] == "<" and i + 1 < len(content) and content[i + 1] == "/":
435
+ # 找到标签结束位置
436
+ tag_end = content.find(">", i)
437
+ if tag_end != -1:
438
+ # 提取标签内容
439
+ tag_content = content[i + 2 : tag_end]
440
+ # 检查是否有命名空间前缀
441
+ if ":" in tag_content:
442
+ prefix = tag_content.split(":")[0]
443
+ # 确保是有效的命名空间前缀
444
+ if prefix.isalnum() or prefix.replace("_", "").isalnum():
445
+ prefixes.add(prefix)
446
+ i = tag_end + 1
447
+ else:
448
+ i += 1
449
+ else:
450
+ i += 1
451
+
452
+ return prefixes
453
+
454
+ async def _extract_quote_element(self, content: str) -> dict | None:
455
+ """提取<quote>标签信息"""
456
+ try:
457
+ # 处理命名空间前缀问题
458
+ processed_content = content
459
+ if ":" in content and not content.startswith("<root"):
460
+ prefixes = self._extract_namespace_prefixes(content)
461
+
462
+ # 构建命名空间声明
463
+ ns_declarations = " ".join(
464
+ [
465
+ f'xmlns:{prefix}="http://temp.uri/{prefix}"'
466
+ for prefix in prefixes
467
+ ],
468
+ )
469
+
470
+ # 包装内容
471
+ processed_content = f"<root {ns_declarations}>{content}</root>"
472
+ elif not content.startswith("<root"):
473
+ processed_content = f"<root>{content}</root>"
474
+ else:
475
+ processed_content = content
476
+
477
+ root = ET.fromstring(processed_content)
478
+
479
+ # 查找<quote>标签
480
+ quote_element = None
481
+ for elem in root.iter():
482
+ tag_name = elem.tag
483
+ if "}" in tag_name:
484
+ tag_name = tag_name.split("}")[1]
485
+ if tag_name.lower() == "quote":
486
+ quote_element = elem
487
+ break
488
+
489
+ if quote_element is not None:
490
+ # 提取quote标签的属性
491
+ quote_id = quote_element.get("id", "")
492
+
493
+ # 提取<quote>标签内部的内容
494
+ inner_content = ""
495
+ if quote_element.text:
496
+ inner_content += quote_element.text
497
+ for child in quote_element:
498
+ inner_content += ET.tostring(
499
+ child,
500
+ encoding="unicode",
501
+ method="xml",
502
+ )
503
+ if child.tail:
504
+ inner_content += child.tail
505
+
506
+ # 构造移除了<quote>标签的内容
507
+ content_without_quote = content.replace(
508
+ ET.tostring(quote_element, encoding="unicode", method="xml"),
509
+ "",
510
+ )
511
+
512
+ return {
513
+ "quote": {"id": quote_id, "content": inner_content},
514
+ "content_without_quote": content_without_quote,
515
+ }
516
+
517
+ return None
518
+ except ET.ParseError as e:
519
+ logger.warning(f"XML解析失败,使用正则提取: {e}")
520
+ return await self._extract_quote_with_regex(content)
521
+ except Exception as e:
522
+ logger.error(f"提取<quote>标签时发生错误: {e}")
523
+ return None
524
+
525
+ async def _extract_quote_with_regex(self, content: str) -> dict | None:
526
+ """使用正则表达式提取quote标签信息"""
527
+ import re
528
+
529
+ quote_pattern = r"<quote\s+([^>]*)>(.*?)</quote>"
530
+ match = re.search(quote_pattern, content, re.DOTALL)
531
+
532
+ if not match:
533
+ return None
534
+
535
+ attrs_str = match.group(1)
536
+ inner_content = match.group(2)
537
+
538
+ id_match = re.search(r'id\s*=\s*["\']([^"\']*)["\']', attrs_str)
539
+ quote_id = id_match.group(1) if id_match else ""
540
+ content_without_quote = content.replace(match.group(0), "")
541
+ content_without_quote = content_without_quote.strip()
542
+
543
+ return {
544
+ "quote": {"id": quote_id, "content": inner_content},
545
+ "content_without_quote": content_without_quote,
546
+ }
547
+
548
+ async def _convert_quote_message(self, quote: dict) -> AstrBotMessage | None:
549
+ """转换引用消息"""
550
+ try:
551
+ quote_abm = AstrBotMessage()
552
+ quote_abm.message_id = quote.get("id", "")
553
+
554
+ # 解析引用消息的发送者
555
+ quote_author = quote.get("author", {})
556
+ if quote_author:
557
+ quote_abm.sender = MessageMember(
558
+ user_id=quote_author.get("id", ""),
559
+ nickname=quote_author.get("nick", quote_author.get("name", "")),
560
+ )
561
+ else:
562
+ # 如果没有作者信息,使用默认值
563
+ quote_abm.sender = MessageMember(
564
+ user_id=quote.get("user_id", ""),
565
+ nickname="内容",
566
+ )
567
+
568
+ # 解析引用消息内容
569
+ quote_content = quote.get("content", "")
570
+ quote_abm.message = await self.parse_satori_elements(quote_content)
571
+
572
+ quote_abm.message_str = ""
573
+ for comp in quote_abm.message:
574
+ if isinstance(comp, Plain):
575
+ quote_abm.message_str += comp.text
576
+
577
+ quote_abm.timestamp = int(quote.get("timestamp", time.time()))
578
+
579
+ # 如果没有任何内容,使用默认文本
580
+ if not quote_abm.message_str.strip():
581
+ quote_abm.message_str = "[引用消息]"
582
+
583
+ return quote_abm
584
+ except Exception as e:
585
+ logger.error(f"转换引用消息失败: {e}")
586
+ return None
587
+
588
+ async def parse_satori_elements(self, content: str) -> list:
589
+ """解析 Satori 消息元素"""
590
+ elements = []
591
+
592
+ if not content:
593
+ return elements
594
+
595
+ try:
596
+ # 处理命名空间前缀问题
597
+ processed_content = content
598
+ if ":" in content and not content.startswith("<root"):
599
+ prefixes = self._extract_namespace_prefixes(content)
600
+
601
+ # 构建命名空间声明
602
+ ns_declarations = " ".join(
603
+ [
604
+ f'xmlns:{prefix}="http://temp.uri/{prefix}"'
605
+ for prefix in prefixes
606
+ ],
607
+ )
608
+
609
+ # 包装内容
610
+ processed_content = f"<root {ns_declarations}>{content}</root>"
611
+ elif not content.startswith("<root"):
612
+ processed_content = f"<root>{content}</root>"
613
+ else:
614
+ processed_content = content
615
+
616
+ root = ET.fromstring(processed_content)
617
+ await self._parse_xml_node(root, elements)
618
+ except ET.ParseError as e:
619
+ logger.warning(f"解析 Satori 元素时发生解析错误: {e}, 错误内容: {content}")
620
+ # 如果解析失败,将整个内容当作纯文本
621
+ if content.strip():
622
+ elements.append(Plain(text=content))
623
+ except Exception as e:
624
+ logger.error(f"解析 Satori 元素时发生未知错误: {e}")
625
+ raise e
626
+
627
+ # 如果没有解析到任何元素,将整个内容当作纯文本
628
+ if not elements and content.strip():
629
+ elements.append(Plain(text=content))
630
+
631
+ return elements
632
+
633
+ async def _parse_xml_node(self, node: ET.Element, elements: list) -> None:
634
+ """递归解析 XML 节点"""
635
+ if node.text and node.text.strip():
636
+ elements.append(Plain(text=node.text))
637
+
638
+ for child in node:
639
+ # 获取标签名,去除命名空间前缀
640
+ tag_name = child.tag
641
+ if "}" in tag_name:
642
+ tag_name = tag_name.split("}")[1]
643
+ tag_name = tag_name.lower()
644
+
645
+ attrs = child.attrib
646
+
647
+ if tag_name == "at":
648
+ user_id = attrs.get("id") or attrs.get("name", "")
649
+ elements.append(At(qq=user_id, name=user_id))
650
+
651
+ elif tag_name in ("img", "image"):
652
+ src = attrs.get("src", "")
653
+ if not src:
654
+ continue
655
+ elements.append(Image(file=src))
656
+
657
+ elif tag_name == "file":
658
+ src = attrs.get("src", "")
659
+ name = attrs.get("name", "文件")
660
+ if src:
661
+ elements.append(File(name=name, file=src))
662
+
663
+ elif tag_name in ("audio", "record"):
664
+ src = attrs.get("src", "")
665
+ if not src:
666
+ continue
667
+ elements.append(Record(file=src))
668
+
669
+ elif tag_name == "quote":
670
+ # quote标签已经被特殊处理
671
+ pass
672
+
673
+ elif tag_name == "face":
674
+ face_id = attrs.get("id", "")
675
+ face_name = attrs.get("name", "")
676
+ face_type = attrs.get("type", "")
677
+
678
+ if face_name:
679
+ elements.append(Plain(text=f"[表情:{face_name}]"))
680
+ elif face_id and face_type:
681
+ elements.append(Plain(text=f"[表情ID:{face_id},类型:{face_type}]"))
682
+ elif face_id:
683
+ elements.append(Plain(text=f"[表情ID:{face_id}]"))
684
+ else:
685
+ elements.append(Plain(text="[表情]"))
686
+
687
+ elif tag_name == "ark":
688
+ # 作为纯文本添加到消息链中
689
+ data = attrs.get("data", "")
690
+ if data:
691
+ import html
692
+
693
+ decoded_data = html.unescape(data)
694
+ elements.append(Plain(text=f"[ARK卡片数据: {decoded_data}]"))
695
+ else:
696
+ elements.append(Plain(text="[ARK卡片]"))
697
+
698
+ elif tag_name == "json":
699
+ # JSON标签 视为ARK卡片消息
700
+ data = attrs.get("data", "")
701
+ if data:
702
+ import html
703
+
704
+ decoded_data = html.unescape(data)
705
+ elements.append(Plain(text=f"[ARK卡片数据: {decoded_data}]"))
706
+ else:
707
+ elements.append(Plain(text="[JSON卡片]"))
708
+
709
+ else:
710
+ # 未知标签,递归处理其内容
711
+ if child.text and child.text.strip():
712
+ elements.append(Plain(text=child.text))
713
+ await self._parse_xml_node(child, elements)
714
+
715
+ # 处理标签后的文本
716
+ if child.tail and child.tail.strip():
717
+ elements.append(Plain(text=child.tail))
718
+
719
+ async def handle_msg(self, message: AstrBotMessage):
720
+ from .satori_event import SatoriPlatformEvent
721
+
722
+ message_event = SatoriPlatformEvent(
723
+ message_str=message.message_str,
724
+ message_obj=message,
725
+ platform_meta=self.meta(),
726
+ session_id=message.session_id,
727
+ adapter=self,
728
+ )
729
+ self.commit_event(message_event)
730
+
731
+ async def send_http_request(
732
+ self,
733
+ method: str,
734
+ path: str,
735
+ data: dict | None = None,
736
+ platform: str | None = None,
737
+ user_id: str | None = None,
738
+ ) -> dict:
739
+ if not self.session:
740
+ raise Exception("HTTP session 未初始化")
741
+
742
+ headers = {
743
+ "Content-Type": "application/json",
744
+ }
745
+
746
+ if self.token:
747
+ headers["Authorization"] = f"Bearer {self.token}"
748
+
749
+ if platform and user_id:
750
+ headers["satori-platform"] = platform
751
+ headers["satori-user-id"] = user_id
752
+ elif self.logins:
753
+ current_login = self.logins[0]
754
+ headers["satori-platform"] = current_login.get("platform", "")
755
+ user = current_login.get("user", {})
756
+ headers["satori-user-id"] = user.get("id", "") if user else ""
757
+
758
+ if not path.startswith("/"):
759
+ path = "/" + path
760
+
761
+ # 使用新的API地址配置
762
+ url = f"{self.api_base_url.rstrip('/')}{path}"
763
+
764
+ try:
765
+ async with self.session.request(
766
+ method,
767
+ url,
768
+ json=data,
769
+ headers=headers,
770
+ ) as response:
771
+ if response.status == 200:
772
+ result = await response.json()
773
+ return result
774
+ return {}
775
+ except Exception as e:
776
+ logger.error(f"Satori HTTP 请求异常: {e}")
777
+ return {}
778
+
779
+ async def terminate(self):
780
+ self.running = False
781
+
782
+ if self.heartbeat_task:
783
+ self.heartbeat_task.cancel()
784
+
785
+ if self.ws:
786
+ try:
787
+ await self.ws.close()
788
+ except Exception as e:
789
+ logger.error(f"Satori WebSocket 关闭异常: {e}")
790
+
791
+ if self.session:
792
+ await self.session.close()