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,926 @@
1
+ import asyncio
2
+ import base64
3
+ import json
4
+ import os
5
+ import time
6
+ import traceback
7
+
8
+ import aiohttp
9
+ import anyio
10
+ import websockets
11
+
12
+ from astrbot import logger
13
+ from astrbot.api.message_components import At, Image, Plain, Record
14
+ from astrbot.api.platform import Platform, PlatformMetadata
15
+ from astrbot.core.message.message_event_result import MessageChain
16
+ from astrbot.core.platform.astr_message_event import MessageSesion
17
+ from astrbot.core.platform.astrbot_message import (
18
+ AstrBotMessage,
19
+ MessageMember,
20
+ MessageType,
21
+ )
22
+ from astrbot.core.utils.astrbot_path import get_astrbot_data_path
23
+
24
+ from ...register import register_platform_adapter
25
+ from .wechatpadpro_message_event import WeChatPadProMessageEvent
26
+
27
+ try:
28
+ from .xml_data_parser import GeweDataParser
29
+ except ImportError as e:
30
+ logger.warning(
31
+ f"警告: 可能未安装 defusedxml 依赖库,将导致无法解析微信的 表情包、引用 类型的消息: {e!s}",
32
+ )
33
+
34
+
35
+ @register_platform_adapter(
36
+ "wechatpadpro", "WeChatPadPro 消息平台适配器", support_streaming_message=False
37
+ )
38
+ class WeChatPadProAdapter(Platform):
39
+ def __init__(
40
+ self,
41
+ platform_config: dict,
42
+ platform_settings: dict,
43
+ event_queue: asyncio.Queue,
44
+ ) -> None:
45
+ super().__init__(event_queue)
46
+ self._shutdown_event = None
47
+ self.wxnewpass = None
48
+ self.config = platform_config
49
+ self.settings = platform_settings
50
+ self.unique_session = platform_settings.get("unique_session", False)
51
+
52
+ self.metadata = PlatformMetadata(
53
+ name="wechatpadpro",
54
+ description="WeChatPadPro 消息平台适配器",
55
+ id=self.config.get("id", "wechatpadpro"),
56
+ support_streaming_message=False,
57
+ )
58
+
59
+ # 保存配置信息
60
+ self.admin_key = self.config.get("admin_key")
61
+ self.host = self.config.get("host")
62
+ self.port = self.config.get("port")
63
+ self.active_mesasge_poll: bool = self.config.get(
64
+ "wpp_active_message_poll",
65
+ False,
66
+ )
67
+ self.active_message_poll_interval: int = self.config.get(
68
+ "wpp_active_message_poll_interval",
69
+ 5,
70
+ )
71
+ self.base_url = f"http://{self.host}:{self.port}"
72
+ self.auth_key = None # 用于保存生成的授权码
73
+ self.wxid = None # 用于保存登录成功后的 wxid
74
+ self.credentials_file = os.path.join(
75
+ get_astrbot_data_path(),
76
+ "wechatpadpro_credentials.json",
77
+ ) # 持久化文件路径
78
+ self.ws_handle_task = None
79
+
80
+ # 添加图片消息缓存,用于引用消息处理
81
+ self.cached_images = {}
82
+ """缓存图片消息。key是NewMsgId (对应引用消息的svrid),value是图片的base64数据"""
83
+ # 设置缓存大小限制,避免内存占用过大
84
+ self.max_image_cache = 50
85
+
86
+ # 添加文本消息缓存,用于引用消息处理
87
+ self.cached_texts = {}
88
+ """缓存文本消息。key是NewMsgId (对应引用消息的svrid),value是消息文本内容"""
89
+ # 设置文本缓存大小限制
90
+ self.max_text_cache = 100
91
+
92
+ async def run(self) -> None:
93
+ """启动平台适配器的运行实例。"""
94
+ logger.info("WeChatPadPro 适配器正在启动...")
95
+
96
+ if loaded_credentials := self.load_credentials():
97
+ self.auth_key = loaded_credentials.get("auth_key")
98
+ self.wxid = loaded_credentials.get("wxid")
99
+
100
+ isLoginIn = await self.check_online_status()
101
+
102
+ # 检查在线状态
103
+ if self.auth_key and isLoginIn:
104
+ logger.info("WeChatPadPro 设备已在线,凭据存在,跳过扫码登录。")
105
+ # 如果在线,连接 WebSocket 接收消息
106
+ self.ws_handle_task = asyncio.create_task(self.connect_websocket())
107
+ else:
108
+ # 1. 生成授权码
109
+ if not self.auth_key:
110
+ logger.info("WeChatPadPro 无可用凭据,将生成新的授权码。")
111
+ await self.generate_auth_key()
112
+
113
+ # 2. 获取登录二维码
114
+ if not isLoginIn:
115
+ logger.info("WeChatPadPro 设备已离线,开始扫码登录。")
116
+ qr_code_url = await self.get_login_qr_code()
117
+
118
+ if qr_code_url:
119
+ logger.info(f"请扫描以下二维码登录: {qr_code_url}")
120
+ else:
121
+ logger.error("无法获取登录二维码。")
122
+ return
123
+
124
+ # 3. 检测扫码状态
125
+ login_successful = await self.check_login_status()
126
+
127
+ if login_successful:
128
+ logger.info("登录成功,WeChatPadPro适配器已连接。")
129
+ else:
130
+ logger.warning("登录失败或超时,WeChatPadPro 适配器将关闭。")
131
+ await self.terminate()
132
+ return
133
+
134
+ # 登录成功后,连接 WebSocket 接收消息
135
+ self.ws_handle_task = asyncio.create_task(self.connect_websocket())
136
+
137
+ self._shutdown_event = asyncio.Event()
138
+ await self._shutdown_event.wait()
139
+ logger.info("WeChatPadPro 适配器已停止。")
140
+
141
+ def load_credentials(self):
142
+ """从文件中加载 auth_key 和 wxid。"""
143
+ if os.path.exists(self.credentials_file):
144
+ try:
145
+ with open(self.credentials_file) as f:
146
+ credentials = json.load(f)
147
+ logger.info("成功加载 WeChatPadPro 凭据。")
148
+ return credentials
149
+ except Exception as e:
150
+ logger.error(f"加载 WeChatPadPro 凭据失败: {e}")
151
+ return None
152
+
153
+ def save_credentials(self):
154
+ """将 auth_key 和 wxid 保存到文件。"""
155
+ credentials = {
156
+ "auth_key": self.auth_key,
157
+ "wxid": self.wxid,
158
+ }
159
+ try:
160
+ # 确保数据目录存在
161
+ data_dir = os.path.dirname(self.credentials_file)
162
+ os.makedirs(data_dir, exist_ok=True)
163
+ with open(self.credentials_file, "w") as f:
164
+ json.dump(credentials, f)
165
+ except Exception as e:
166
+ logger.error(f"保存 WeChatPadPro 凭据失败: {e}")
167
+
168
+ async def check_online_status(self):
169
+ """检查 WeChatPadPro 设备是否在线。"""
170
+ if not self.auth_key:
171
+ return False
172
+ url = f"{self.base_url}/login/GetLoginStatus"
173
+ params = {"key": self.auth_key}
174
+
175
+ async with aiohttp.ClientSession() as session:
176
+ try:
177
+ async with session.get(url, params=params) as response:
178
+ response_data = await response.json()
179
+ # 根据提供的在线接口返回示例,成功状态码是 200,loginState 为 1 表示在线
180
+ if response.status == 200 and response_data.get("Code") == 200:
181
+ login_state = response_data.get("Data", {}).get("loginState")
182
+ if login_state == 1:
183
+ logger.info("WeChatPadPro 设备当前在线。")
184
+ return True
185
+ # login_state == 3 为离线状态
186
+ if login_state == 3:
187
+ logger.info("WeChatPadPro 设备不在线。")
188
+ return False
189
+ logger.error(f"未知的在线状态: {response_data}")
190
+ return False
191
+ # Code == 300 为微信退出状态。
192
+ if response.status == 200 and response_data.get("Code") == 300:
193
+ logger.info("WeChatPadPro 设备已退出。")
194
+ return False
195
+ if response.status == 200 and response_data.get("Code") == -2:
196
+ # 该链接不存在
197
+ self.auth_key = None
198
+ return False
199
+ logger.error(
200
+ f"检查在线状态失败: {response.status}, {response_data}",
201
+ )
202
+ return False
203
+
204
+ except aiohttp.ClientConnectorError as e:
205
+ logger.error(f"连接到 WeChatPadPro 服务失败: {e}")
206
+ return False
207
+ except Exception as e:
208
+ logger.error(f"检查在线状态时发生错误: {e}")
209
+ logger.error(traceback.format_exc())
210
+ return False
211
+
212
+ def _extract_auth_key(self, data):
213
+ """Helper method to extract auth_key from response data."""
214
+ if isinstance(data, dict):
215
+ auth_keys = data.get("authKeys") # 新接口
216
+ if isinstance(auth_keys, list) and auth_keys:
217
+ return auth_keys[0]
218
+ elif isinstance(data, list) and data: # 旧接口
219
+ return data[0]
220
+ return None
221
+
222
+ async def generate_auth_key(self):
223
+ """生成授权码。"""
224
+ url = f"{self.base_url}/admin/GenAuthKey1"
225
+ params = {"key": self.admin_key}
226
+ payload = {"Count": 1, "Days": 365} # 生成一个有效期365天的授权码
227
+
228
+ self.auth_key = None # Reset auth_key before generating a new one
229
+
230
+ async with aiohttp.ClientSession() as session:
231
+ try:
232
+ async with session.post(url, params=params, json=payload) as response:
233
+ if response.status != 200:
234
+ logger.error(
235
+ f"生成授权码失败: {response.status}, {await response.text()}",
236
+ )
237
+ return
238
+
239
+ response_data = await response.json()
240
+ if response_data.get("Code") == 200:
241
+ if data := response_data.get("Data"):
242
+ self.auth_key = self._extract_auth_key(data)
243
+
244
+ if self.auth_key:
245
+ logger.info("成功获取授权码")
246
+ else:
247
+ logger.error(
248
+ f"生成授权码成功但未找到授权码: {response_data}",
249
+ )
250
+ else:
251
+ logger.error(f"生成授权码失败: {response_data}")
252
+ except aiohttp.ClientConnectorError as e:
253
+ logger.error(f"连接到 WeChatPadPro 服务失败: {e}")
254
+ except Exception as e:
255
+ logger.error(f"生成授权码时发生错误: {e}")
256
+
257
+ async def get_login_qr_code(self):
258
+ """获取登录二维码地址。"""
259
+ url = f"{self.base_url}/login/GetLoginQrCodeNew"
260
+ params = {"key": self.auth_key}
261
+ payload = {} # 根据文档,这个接口的 body 可以为空
262
+
263
+ async with aiohttp.ClientSession() as session:
264
+ try:
265
+ async with session.post(url, params=params, json=payload) as response:
266
+ response_data = await response.json()
267
+ if response.status == 200 and response_data.get("Code") == 200:
268
+ # 二维码地址在 Data.QrCodeUrl 字段中
269
+ if response_data.get("Data") and response_data["Data"].get(
270
+ "QrCodeUrl",
271
+ ):
272
+ return response_data["Data"]["QrCodeUrl"]
273
+ logger.error(
274
+ f"获取登录二维码成功但未找到二维码地址: {response_data}",
275
+ )
276
+ return None
277
+ if "该 key 无效" in response_data.get("Text"):
278
+ logger.error(
279
+ "授权码无效,已经清除。请重新启动 AstrBot 或者本消息适配器。原因也可能是 WeChatPadPro 的 MySQL 服务没有启动成功,请检查 WeChatPadPro 服务的日志。",
280
+ )
281
+ self.auth_key = None
282
+ self.save_credentials()
283
+ return None
284
+ logger.error(
285
+ f"获取登录二维码失败: {response.status}, {response_data}",
286
+ )
287
+ return None
288
+ except aiohttp.ClientConnectorError as e:
289
+ logger.error(f"连接到 WeChatPadPro 服务失败: {e}")
290
+ return None
291
+ except Exception as e:
292
+ logger.error(f"获取登录二维码时发生错误: {e}")
293
+ return None
294
+
295
+ async def check_login_status(self):
296
+ """循环检测扫码状态。
297
+ 尝试 6 次后跳出循环,添加倒计时。
298
+ 返回 True 如果登录成功,否则返回 False。
299
+ """
300
+ url = f"{self.base_url}/login/CheckLoginStatus"
301
+ params = {"key": self.auth_key}
302
+
303
+ attempts = 0 # 初始化尝试次数
304
+ max_attempts = 36 # 最大尝试次数
305
+ countdown = 180 # 倒计时时长
306
+ logger.info(f"请在 {countdown} 秒内扫码登录。")
307
+ while attempts < max_attempts:
308
+ async with aiohttp.ClientSession() as session:
309
+ try:
310
+ async with session.get(url, params=params) as response:
311
+ response_data = await response.json()
312
+ # 成功判断条件和数据提取路径
313
+ if response.status == 200 and response_data.get("Code") == 200:
314
+ if (
315
+ response_data.get("Data")
316
+ and response_data["Data"].get("state") is not None
317
+ ):
318
+ status = response_data["Data"]["state"]
319
+ logger.info(
320
+ f"第 {attempts + 1} 次尝试,当前登录状态: {status},还剩{countdown - attempts * 5}秒",
321
+ )
322
+ if status == 2: # 状态 2 表示登录成功
323
+ self.wxid = response_data["Data"].get("wxid")
324
+ self.wxnewpass = response_data["Data"].get(
325
+ "wxnewpass",
326
+ )
327
+ logger.info(
328
+ f"登录成功,wxid: {self.wxid}, wxnewpass: {self.wxnewpass}",
329
+ )
330
+ self.save_credentials() # 登录成功后保存凭据
331
+ return True
332
+ if status == -2: # 二维码过期
333
+ logger.error("二维码已过期,请重新获取。")
334
+ return False
335
+ else:
336
+ logger.error(
337
+ f"检测登录状态成功但未找到登录状态: {response_data}",
338
+ )
339
+ elif response_data.get("Code") == 300:
340
+ # "不存在状态"
341
+ pass
342
+ else:
343
+ logger.info(
344
+ f"检测登录状态失败: {response.status}, {response_data}",
345
+ )
346
+
347
+ except aiohttp.ClientConnectorError as e:
348
+ logger.error(f"连接到 WeChatPadPro 服务失败: {e}")
349
+ await asyncio.sleep(5)
350
+ attempts += 1
351
+ continue
352
+ except Exception as e:
353
+ logger.error(f"检测登录状态时发生错误: {e}")
354
+ attempts += 1
355
+ continue
356
+
357
+ attempts += 1
358
+ await asyncio.sleep(5) # 每隔5秒检测一次
359
+ logger.warning("登录检测超过最大尝试次数,退出检测。")
360
+ return False
361
+
362
+ async def connect_websocket(self):
363
+ """建立 WebSocket 连接并处理接收到的消息。"""
364
+ os.environ["no_proxy"] = f"localhost,127.0.0.1,{self.host}"
365
+ ws_url = f"ws://{self.host}:{self.port}/ws/GetSyncMsg?key={self.auth_key}"
366
+ logger.info(
367
+ f"正在连接 WebSocket: ws://{self.host}:{self.port}/ws/GetSyncMsg?key=***",
368
+ )
369
+ while True:
370
+ try:
371
+ async with websockets.connect(ws_url) as websocket:
372
+ logger.debug("WebSocket 连接成功。")
373
+ # 设置空闲超时重连
374
+ wait_time = (
375
+ self.active_message_poll_interval
376
+ if self.active_mesasge_poll
377
+ else 120
378
+ )
379
+ while True:
380
+ try:
381
+ message = await asyncio.wait_for(
382
+ websocket.recv(),
383
+ timeout=wait_time,
384
+ )
385
+ # logger.debug(message) # 不显示原始消息内容
386
+ asyncio.create_task(self.handle_websocket_message(message))
387
+ except asyncio.TimeoutError:
388
+ logger.debug(f"WebSocket 连接空闲超过 {wait_time} s")
389
+ break
390
+ except websockets.exceptions.ConnectionClosedOK:
391
+ logger.info("WebSocket 连接正常关闭。")
392
+ break
393
+ except Exception as e:
394
+ logger.error(f"处理 WebSocket 消息时发生错误: {e}")
395
+ break
396
+ except Exception as e:
397
+ logger.error(
398
+ f"WebSocket 连接失败: {e}, 请检查WeChatPadPro服务状态,或尝试重启WeChatPadPro适配器。",
399
+ )
400
+ await asyncio.sleep(5)
401
+
402
+ async def handle_websocket_message(self, message: str):
403
+ """处理从 WebSocket 接收到的消息。"""
404
+ logger.debug(f"收到 WebSocket 消息: {message}")
405
+ try:
406
+ message_data = json.loads(message)
407
+ if (
408
+ message_data.get("msg_id") is not None
409
+ and message_data.get("from_user_name") is not None
410
+ ):
411
+ abm = await self.convert_message(message_data)
412
+ if abm:
413
+ # 创建 WeChatPadProMessageEvent 实例
414
+ message_event = WeChatPadProMessageEvent(
415
+ message_str=abm.message_str,
416
+ message_obj=abm,
417
+ platform_meta=self.meta(),
418
+ session_id=abm.session_id,
419
+ # 传递适配器实例,以便在事件中调用 send 方法
420
+ adapter=self,
421
+ )
422
+ # 提交事件到事件队列
423
+ self.commit_event(message_event)
424
+ else:
425
+ logger.warning(f"收到未知结构的 WebSocket 消息: {message_data}")
426
+
427
+ except json.JSONDecodeError:
428
+ logger.error(f"无法解析 WebSocket 消息为 JSON: {message}")
429
+ except Exception as e:
430
+ logger.error(f"处理 WebSocket 消息时发生错误: {e}")
431
+
432
+ async def convert_message(self, raw_message: dict) -> AstrBotMessage | None:
433
+ """将 WeChatPadPro 原始消息转换为 AstrBotMessage。"""
434
+ abm = AstrBotMessage()
435
+ abm.raw_message = raw_message
436
+ abm.message_id = str(raw_message.get("msg_id"))
437
+ abm.timestamp = raw_message.get("create_time")
438
+ abm.self_id = self.wxid
439
+
440
+ if int(time.time()) - abm.timestamp > 180:
441
+ logger.warning(
442
+ f"忽略 3 分钟前的旧消息:消息时间戳 {abm.timestamp} 超过当前时间 {int(time.time())}。",
443
+ )
444
+ return None
445
+
446
+ from_user_name = raw_message.get("from_user_name", {}).get("str", "")
447
+ to_user_name = raw_message.get("to_user_name", {}).get("str", "")
448
+ content = raw_message.get("content", {}).get("str", "")
449
+ push_content = raw_message.get("push_content", "")
450
+ msg_type = raw_message.get("msg_type")
451
+
452
+ abm.message_str = ""
453
+ abm.message = []
454
+
455
+ # 如果是机器人自己发送的消息、回显消息或系统消息,忽略
456
+ if from_user_name == self.wxid:
457
+ logger.info("忽略来自自己的消息。")
458
+ return None
459
+
460
+ if from_user_name in ["weixin", "newsapp", "newsapp_wechat"]:
461
+ logger.info("忽略来自微信团队的消息。")
462
+ return None
463
+
464
+ # 先判断群聊/私聊并设置基本属性
465
+ if await self._process_chat_type(
466
+ abm,
467
+ raw_message,
468
+ from_user_name,
469
+ to_user_name,
470
+ content,
471
+ push_content,
472
+ ):
473
+ # 再根据消息类型处理消息内容
474
+ await self._process_message_content(abm, raw_message, msg_type, content)
475
+
476
+ return abm
477
+ return None
478
+
479
+ async def _process_chat_type(
480
+ self,
481
+ abm: AstrBotMessage,
482
+ raw_message: dict,
483
+ from_user_name: str,
484
+ to_user_name: str,
485
+ content: str,
486
+ push_content: str,
487
+ ):
488
+ """判断消息是群聊还是私聊,并设置 AstrBotMessage 的基本属性。"""
489
+ if from_user_name == "weixin":
490
+ return False
491
+ at_me = False
492
+ if "@chatroom" in from_user_name:
493
+ abm.type = MessageType.GROUP_MESSAGE
494
+ abm.group_id = from_user_name
495
+
496
+ parts = content.split(":\n", 1)
497
+ sender_wxid = parts[0] if len(parts) == 2 else ""
498
+ abm.sender = MessageMember(user_id=sender_wxid, nickname="")
499
+
500
+ # 获取群聊发送者的nickname
501
+ if sender_wxid:
502
+ accurate_nickname = await self._get_group_member_nickname(
503
+ abm.group_id,
504
+ sender_wxid,
505
+ )
506
+ if accurate_nickname:
507
+ abm.sender.nickname = accurate_nickname
508
+
509
+ # 对于群聊,session_id 可以是群聊 ID 或发送者 ID + 群聊 ID (如果 unique_session 为 True)
510
+ if self.unique_session:
511
+ abm.session_id = f"{from_user_name}#{abm.sender.user_id}"
512
+ else:
513
+ abm.session_id = from_user_name
514
+
515
+ msg_source = raw_message.get("msg_source", "")
516
+ if self.wxid in msg_source:
517
+ at_me = True
518
+ if "在群聊中@了你" in raw_message.get("push_content", ""):
519
+ at_me = True
520
+ if at_me:
521
+ abm.message.insert(0, At(qq=abm.self_id, name=""))
522
+ else:
523
+ abm.type = MessageType.FRIEND_MESSAGE
524
+ abm.group_id = ""
525
+ nick_name = ""
526
+ if push_content and " : " in push_content:
527
+ nick_name = push_content.split(" : ")[0]
528
+ abm.sender = MessageMember(user_id=from_user_name, nickname=nick_name)
529
+ abm.session_id = from_user_name
530
+ return True
531
+
532
+ async def _get_group_member_nickname(
533
+ self,
534
+ group_id: str,
535
+ member_wxid: str,
536
+ ) -> str | None:
537
+ """通过接口获取群成员的昵称。"""
538
+ url = f"{self.base_url}/group/GetChatroomMemberDetail"
539
+ params = {"key": self.auth_key}
540
+ payload = {
541
+ "ChatRoomName": group_id,
542
+ }
543
+
544
+ async with aiohttp.ClientSession() as session:
545
+ try:
546
+ async with session.post(url, params=params, json=payload) as response:
547
+ response_data = await response.json()
548
+ if response.status == 200 and response_data.get("Code") == 200:
549
+ # 从返回数据中查找对应成员的昵称
550
+ member_list = (
551
+ response_data.get("Data", {})
552
+ .get("member_data", {})
553
+ .get("chatroom_member_list", [])
554
+ )
555
+ for member in member_list:
556
+ if member.get("user_name") == member_wxid:
557
+ return member.get("nick_name")
558
+ logger.warning(
559
+ f"在群 {group_id} 中未找到成员 {member_wxid} 的昵称",
560
+ )
561
+ else:
562
+ logger.error(
563
+ f"获取群成员详情失败: {response.status}, {response_data}",
564
+ )
565
+ return None
566
+ except aiohttp.ClientConnectorError as e:
567
+ logger.error(f"连接到 WeChatPadPro 服务失败: {e}")
568
+ return None
569
+ except Exception as e:
570
+ logger.error(f"获取群成员详情时发生错误: {e}")
571
+ return None
572
+
573
+ async def _download_raw_image(
574
+ self,
575
+ from_user_name: str,
576
+ to_user_name: str,
577
+ msg_id: int,
578
+ ):
579
+ """下载原始图片。"""
580
+ url = f"{self.base_url}/message/GetMsgBigImg"
581
+ params = {"key": self.auth_key}
582
+ payload = {
583
+ "CompressType": 0,
584
+ "FromUserName": from_user_name,
585
+ "MsgId": msg_id,
586
+ "Section": {"DataLen": 61440, "StartPos": 0},
587
+ "ToUserName": to_user_name,
588
+ "TotalLen": 0,
589
+ }
590
+ async with aiohttp.ClientSession() as session:
591
+ try:
592
+ async with session.post(url, params=params, json=payload) as response:
593
+ if response.status == 200:
594
+ return await response.json()
595
+ logger.error(f"下载图片失败: {response.status}")
596
+ return None
597
+ except aiohttp.ClientConnectorError as e:
598
+ logger.error(f"连接到 WeChatPadPro 服务失败: {e}")
599
+ return None
600
+ except Exception as e:
601
+ logger.error(f"下载图片时发生错误: {e}")
602
+ return None
603
+
604
+ async def download_voice(
605
+ self,
606
+ to_user_name: str,
607
+ new_msg_id: str,
608
+ bufid: str,
609
+ length: int,
610
+ ):
611
+ """下载原始音频。"""
612
+ url = f"{self.base_url}/message/GetMsgVoice"
613
+ params = {"key": self.auth_key}
614
+ payload = {
615
+ "Bufid": bufid,
616
+ "ToUserName": to_user_name,
617
+ "NewMsgId": new_msg_id,
618
+ "Length": length,
619
+ }
620
+ async with aiohttp.ClientSession() as session:
621
+ try:
622
+ async with session.post(url, params=params, json=payload) as response:
623
+ if response.status == 200:
624
+ return await response.json()
625
+ logger.error(f"下载音频失败: {response.status}")
626
+ return None
627
+ except aiohttp.ClientConnectorError as e:
628
+ logger.error(f"连接到 WeChatPadPro 服务失败: {e}")
629
+ return None
630
+ except Exception as e:
631
+ logger.error(f"下载音频时发生错误: {e}")
632
+ return None
633
+
634
+ async def _process_message_content(
635
+ self,
636
+ abm: AstrBotMessage,
637
+ raw_message: dict,
638
+ msg_type: int,
639
+ content: str,
640
+ ):
641
+ """根据消息类型处理消息内容,填充 AstrBotMessage 的 message 列表。"""
642
+ if msg_type == 1: # 文本消息
643
+ abm.message_str = content
644
+ if abm.type == MessageType.GROUP_MESSAGE:
645
+ parts = content.split(":\n", 1)
646
+ if len(parts) == 2:
647
+ message_content = parts[1]
648
+ abm.message_str = message_content
649
+
650
+ # 检查是否@了机器人,参考 gewechat 的实现方式
651
+ # 微信大部分客户端在@用户昵称后面,紧接着是一个\u2005字符(四分之一空格)
652
+ at_me = False
653
+
654
+ # 检查 msg_source 中是否包含机器人的 wxid
655
+ # wechatpadpro 的格式: <atuserlist>wxid</atuserlist>
656
+ # gewechat 的格式: <atuserlist><![CDATA[wxid]]></atuserlist>
657
+ msg_source = raw_message.get("msg_source", "")
658
+ if (
659
+ f"<atuserlist>{abm.self_id}</atuserlist>" in msg_source
660
+ or f"<atuserlist>{abm.self_id}," in msg_source
661
+ or f",{abm.self_id}</atuserlist>" in msg_source
662
+ ):
663
+ at_me = True
664
+
665
+ # 也检查 push_content 中是否有@提示
666
+ push_content = raw_message.get("push_content", "")
667
+ if "在群聊中@了你" in push_content:
668
+ at_me = True
669
+
670
+ if at_me:
671
+ # 被@了,在消息开头插入At组件(参考gewechat的做法)
672
+ bot_nickname = await self._get_group_member_nickname(
673
+ abm.group_id,
674
+ abm.self_id,
675
+ )
676
+ abm.message.insert(
677
+ 0,
678
+ At(qq=abm.self_id, name=bot_nickname or abm.self_id),
679
+ )
680
+
681
+ # 只有当消息内容不仅仅是@时才添加Plain组件
682
+ if "\u2005" in message_content:
683
+ # 检查@之后是否还有其他内容
684
+ parts = message_content.split("\u2005")
685
+ if len(parts) > 1 and any(
686
+ part.strip() for part in parts[1:]
687
+ ):
688
+ abm.message.append(Plain(message_content))
689
+ else:
690
+ # 检查是否只包含@机器人
691
+ is_pure_at = False
692
+ if (
693
+ bot_nickname
694
+ and message_content.strip() == f"@{bot_nickname}"
695
+ ):
696
+ is_pure_at = True
697
+ if not is_pure_at:
698
+ abm.message.append(Plain(message_content))
699
+ else:
700
+ # 没有@机器人,作为普通文本处理
701
+ abm.message.append(Plain(message_content))
702
+ else:
703
+ abm.message.append(Plain(abm.message_str))
704
+ else: # 私聊消息
705
+ abm.message.append(Plain(abm.message_str))
706
+
707
+ # 缓存文本消息,以便引用消息可以查找
708
+ try:
709
+ # 获取msg_id作为缓存的key
710
+ new_msg_id = raw_message.get("new_msg_id")
711
+ if new_msg_id:
712
+ # 限制缓存大小
713
+ if (
714
+ len(self.cached_texts) >= self.max_text_cache
715
+ and self.cached_texts
716
+ ):
717
+ # 删除最早的一条缓存
718
+ oldest_key = next(iter(self.cached_texts))
719
+ self.cached_texts.pop(oldest_key)
720
+
721
+ logger.debug(f"缓存文本消息,new_msg_id={new_msg_id}")
722
+ self.cached_texts[str(new_msg_id)] = content
723
+ except Exception as e:
724
+ logger.error(f"缓存文本消息失败: {e}")
725
+ elif msg_type == 3:
726
+ # 图片消息
727
+ from_user_name = raw_message.get("from_user_name", {}).get("str", "")
728
+ to_user_name = raw_message.get("to_user_name", {}).get("str", "")
729
+ msg_id = raw_message.get("msg_id")
730
+ image_resp = await self._download_raw_image(
731
+ from_user_name,
732
+ to_user_name,
733
+ msg_id,
734
+ )
735
+ image_bs64_data = (
736
+ image_resp.get("Data", {}).get("Data", {}).get("Buffer", None)
737
+ )
738
+ if image_bs64_data:
739
+ abm.message.append(Image.fromBase64(image_bs64_data))
740
+ # 缓存图片,以便引用消息可以查找
741
+ try:
742
+ # 获取msg_id作为缓存的key
743
+ new_msg_id = raw_message.get("new_msg_id")
744
+ if new_msg_id:
745
+ # 限制缓存大小
746
+ if (
747
+ len(self.cached_images) >= self.max_image_cache
748
+ and self.cached_images
749
+ ):
750
+ # 删除最早的一条缓存
751
+ oldest_key = next(iter(self.cached_images))
752
+ self.cached_images.pop(oldest_key)
753
+
754
+ logger.debug(f"缓存图片消息,new_msg_id={new_msg_id}")
755
+ self.cached_images[str(new_msg_id)] = image_bs64_data
756
+ except Exception as e:
757
+ logger.error(f"缓存图片消息失败: {e}")
758
+ elif msg_type == 47:
759
+ # 视频消息 (注意:表情消息也是 47,需要区分)
760
+ data_parser = GeweDataParser(
761
+ content=content,
762
+ is_private_chat=(abm.type != MessageType.GROUP_MESSAGE),
763
+ raw_message=raw_message,
764
+ )
765
+ emoji_message = data_parser.parse_emoji()
766
+ if emoji_message is not None:
767
+ abm.message.append(emoji_message)
768
+ elif msg_type == 50:
769
+ logger.warning("收到语音/视频消息,待实现。")
770
+ elif msg_type == 34:
771
+ # 语音消息
772
+ bufid = 0
773
+ to_user_name = raw_message.get("to_user_name", {}).get("str", "")
774
+ new_msg_id = raw_message.get("new_msg_id")
775
+ data_parser = GeweDataParser(
776
+ content=content,
777
+ is_private_chat=(abm.type != MessageType.GROUP_MESSAGE),
778
+ raw_message=raw_message,
779
+ )
780
+
781
+ voicemsg = data_parser._format_to_xml().find("voicemsg")
782
+ bufid = voicemsg.get("bufid") or "0"
783
+ length = int(voicemsg.get("length") or 0)
784
+ voice_resp = await self.download_voice(
785
+ to_user_name=to_user_name,
786
+ new_msg_id=new_msg_id,
787
+ bufid=bufid,
788
+ length=length,
789
+ )
790
+ voice_bs64_data = voice_resp.get("Data", {}).get("Base64", None)
791
+ if voice_bs64_data:
792
+ voice_bs64_data = base64.b64decode(voice_bs64_data)
793
+ temp_dir = os.path.join(get_astrbot_data_path(), "temp")
794
+ file_path = os.path.join(
795
+ temp_dir,
796
+ f"wechatpadpro_voice_{abm.message_id}.silk",
797
+ )
798
+
799
+ async with await anyio.open_file(file_path, "wb") as f:
800
+ await f.write(voice_bs64_data)
801
+ abm.message.append(Record(file=file_path, url=file_path))
802
+ elif msg_type == 49:
803
+ try:
804
+ parser = GeweDataParser(
805
+ content=content,
806
+ is_private_chat=(abm.type != MessageType.GROUP_MESSAGE),
807
+ cached_texts=self.cached_texts,
808
+ cached_images=self.cached_images,
809
+ raw_message=raw_message,
810
+ downloader=self._download_raw_image,
811
+ )
812
+ components = await parser.parse_mutil_49()
813
+ if components:
814
+ abm.message.extend(components)
815
+ abm.message_str = "\n".join(
816
+ c.text for c in components if isinstance(c, Plain)
817
+ )
818
+ except Exception as e:
819
+ logger.warning(f"msg_type 49 处理失败: {e}")
820
+ abm.message.append(Plain("[XML 消息处理失败]"))
821
+ abm.message_str = "[XML 消息处理失败]"
822
+ else:
823
+ logger.warning(f"收到未处理的消息类型: {msg_type}。")
824
+
825
+ async def terminate(self):
826
+ """终止一个平台的运行实例。"""
827
+ logger.info("终止 WeChatPadPro 适配器。")
828
+ try:
829
+ if self.ws_handle_task:
830
+ self.ws_handle_task.cancel()
831
+ self._shutdown_event.set()
832
+ except Exception:
833
+ pass
834
+
835
+ def meta(self) -> PlatformMetadata:
836
+ """得到一个平台的元数据。"""
837
+ return self.metadata
838
+
839
+ async def send_by_session(
840
+ self,
841
+ session: MessageSesion,
842
+ message_chain: MessageChain,
843
+ ):
844
+ dummy_message_obj = AstrBotMessage()
845
+ dummy_message_obj.session_id = session.session_id
846
+ # 根据 session_id 判断消息类型
847
+ if "@chatroom" in session.session_id:
848
+ dummy_message_obj.type = MessageType.GROUP_MESSAGE
849
+ if "#" in session.session_id:
850
+ dummy_message_obj.group_id = session.session_id.split("#")[0]
851
+ else:
852
+ dummy_message_obj.group_id = session.session_id
853
+ dummy_message_obj.sender = MessageMember(user_id="", nickname="")
854
+ else:
855
+ dummy_message_obj.type = MessageType.FRIEND_MESSAGE
856
+ dummy_message_obj.group_id = ""
857
+ dummy_message_obj.sender = MessageMember(user_id="", nickname="")
858
+ sending_event = WeChatPadProMessageEvent(
859
+ message_str="",
860
+ message_obj=dummy_message_obj,
861
+ platform_meta=self.meta(),
862
+ session_id=session.session_id,
863
+ adapter=self,
864
+ )
865
+ # 调用实例方法 send
866
+ await sending_event.send(message_chain)
867
+
868
+ async def get_contact_list(self):
869
+ """获取联系人列表。"""
870
+ url = f"{self.base_url}/friend/GetContactList"
871
+ params = {"key": self.auth_key}
872
+ payload = {"CurrentChatRoomContactSeq": 0, "CurrentWxcontactSeq": 0}
873
+ async with aiohttp.ClientSession() as session:
874
+ try:
875
+ async with session.post(url, params=params, json=payload) as response:
876
+ if response.status != 200:
877
+ logger.error(f"获取联系人列表失败: {response.status}")
878
+ return None
879
+ result = await response.json()
880
+ if result.get("Code") == 200 and result.get("Data"):
881
+ contact_list = (
882
+ result.get("Data", {})
883
+ .get("ContactList", {})
884
+ .get("contactUsernameList", [])
885
+ )
886
+ return contact_list
887
+ logger.error(f"获取联系人列表失败: {result}")
888
+ return None
889
+ except aiohttp.ClientConnectorError as e:
890
+ logger.error(f"连接到 WeChatPadPro 服务失败: {e}")
891
+ return None
892
+ except Exception as e:
893
+ logger.error(f"获取联系人列表时发生错误: {e}")
894
+ return None
895
+
896
+ async def get_contact_details_list(
897
+ self,
898
+ room_wx_id_list: list[str] = None,
899
+ user_names: list[str] = None,
900
+ ) -> dict | None:
901
+ """获取联系人详情列表。"""
902
+ if room_wx_id_list is None:
903
+ room_wx_id_list = []
904
+ if user_names is None:
905
+ user_names = []
906
+ url = f"{self.base_url}/friend/GetContactDetailsList"
907
+ params = {"key": self.auth_key}
908
+ payload = {"RoomWxIDList": room_wx_id_list, "UserNames": user_names}
909
+ async with aiohttp.ClientSession() as session:
910
+ try:
911
+ async with session.post(url, params=params, json=payload) as response:
912
+ if response.status != 200:
913
+ logger.error(f"获取联系人详情列表失败: {response.status}")
914
+ return None
915
+ result = await response.json()
916
+ if result.get("Code") == 200 and result.get("Data"):
917
+ contact_list = result.get("Data", {}).get("contactList", {})
918
+ return contact_list
919
+ logger.error(f"获取联系人详情列表失败: {result}")
920
+ return None
921
+ except aiohttp.ClientConnectorError as e:
922
+ logger.error(f"连接到 WeChatPadPro 服务失败: {e}")
923
+ return None
924
+ except Exception as e:
925
+ logger.error(f"获取联系人详情列表时发生错误: {e}")
926
+ return None