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,178 @@
1
+ import asyncio
2
+ import base64
3
+ import io
4
+ from collections.abc import AsyncGenerator
5
+ from typing import TYPE_CHECKING
6
+
7
+ import aiohttp
8
+ from PIL import Image as PILImage # 使用别名避免冲突
9
+
10
+ from astrbot import logger
11
+ from astrbot.core.message.components import (
12
+ Image,
13
+ Plain,
14
+ Record,
15
+ WechatEmoji,
16
+ ) # Import Image
17
+ from astrbot.core.message.message_event_result import MessageChain
18
+ from astrbot.core.platform.astr_message_event import AstrMessageEvent
19
+ from astrbot.core.platform.astrbot_message import AstrBotMessage, MessageType
20
+ from astrbot.core.platform.platform_metadata import PlatformMetadata
21
+ from astrbot.core.utils.tencent_record_helper import audio_to_tencent_silk_base64
22
+
23
+ if TYPE_CHECKING:
24
+ from .wechatpadpro_adapter import WeChatPadProAdapter
25
+
26
+
27
+ class WeChatPadProMessageEvent(AstrMessageEvent):
28
+ def __init__(
29
+ self,
30
+ message_str: str,
31
+ message_obj: AstrBotMessage,
32
+ platform_meta: PlatformMetadata,
33
+ session_id: str,
34
+ adapter: "WeChatPadProAdapter", # 传递适配器实例
35
+ ):
36
+ super().__init__(message_str, message_obj, platform_meta, session_id)
37
+ self.message_obj = message_obj # Save the full message object
38
+ self.adapter = adapter # Save the adapter instance
39
+
40
+ async def send(self, message: MessageChain):
41
+ async with aiohttp.ClientSession() as session:
42
+ for comp in message.chain:
43
+ await asyncio.sleep(1)
44
+ if isinstance(comp, Plain):
45
+ await self._send_text(session, comp.text)
46
+ elif isinstance(comp, Image):
47
+ await self._send_image(session, comp)
48
+ elif isinstance(comp, WechatEmoji):
49
+ await self._send_emoji(session, comp)
50
+ elif isinstance(comp, Record):
51
+ await self._send_voice(session, comp)
52
+ await super().send(message)
53
+
54
+ async def send_streaming(
55
+ self, generator: AsyncGenerator[MessageChain, None], use_fallback: bool = False
56
+ ):
57
+ buffer = None
58
+ async for chain in generator:
59
+ if not buffer:
60
+ buffer = chain
61
+ else:
62
+ buffer.chain.extend(chain.chain)
63
+ if not buffer:
64
+ return None
65
+ buffer.squash_plain()
66
+ await self.send(buffer)
67
+ return await super().send_streaming(generator, use_fallback)
68
+
69
+ async def _send_image(self, session: aiohttp.ClientSession, comp: Image):
70
+ b64 = await comp.convert_to_base64()
71
+ raw = self._validate_base64(b64)
72
+ b64c = self._compress_image(raw)
73
+ payload = {
74
+ "MsgItem": [
75
+ {"ImageContent": b64c, "MsgType": 3, "ToUserName": self.session_id},
76
+ ],
77
+ }
78
+ url = f"{self.adapter.base_url}/message/SendImageNewMessage"
79
+ await self._post(session, url, payload)
80
+
81
+ async def _send_text(self, session: aiohttp.ClientSession, text: str):
82
+ if (
83
+ self.message_obj.type == MessageType.GROUP_MESSAGE # 确保是群聊消息
84
+ and self.adapter.settings.get(
85
+ "reply_with_mention",
86
+ False,
87
+ ) # 检查适配器设置是否启用 reply_with_mention
88
+ and self.message_obj.sender # 确保有发送者信息
89
+ and (
90
+ self.message_obj.sender.user_id or self.message_obj.sender.nickname
91
+ ) # 确保发送者有 ID 或昵称
92
+ ):
93
+ # 优先使用 nickname,如果没有则使用 user_id
94
+ mention_text = (
95
+ self.message_obj.sender.nickname or self.message_obj.sender.user_id
96
+ )
97
+ message_text = f"@{mention_text} {text}"
98
+ # logger.info(f"已添加 @ 信息: {message_text}")
99
+ else:
100
+ message_text = text
101
+ if self.get_group_id() and "#" in self.session_id:
102
+ session_id = self.session_id.split("#")[0]
103
+ else:
104
+ session_id = self.session_id
105
+ payload = {
106
+ "MsgItem": [
107
+ {
108
+ "MsgType": 1,
109
+ "TextContent": message_text,
110
+ "ToUserName": session_id,
111
+ },
112
+ ],
113
+ }
114
+ url = f"{self.adapter.base_url}/message/SendTextMessage"
115
+ await self._post(session, url, payload)
116
+
117
+ async def _send_emoji(self, session: aiohttp.ClientSession, comp: WechatEmoji):
118
+ payload = {
119
+ "EmojiList": [
120
+ {
121
+ "EmojiMd5": comp.md5,
122
+ "EmojiSize": comp.md5_len,
123
+ "ToUserName": self.session_id,
124
+ },
125
+ ],
126
+ }
127
+ url = f"{self.adapter.base_url}/message/SendEmojiMessage"
128
+ await self._post(session, url, payload)
129
+
130
+ async def _send_voice(self, session: aiohttp.ClientSession, comp: Record):
131
+ record_path = await comp.convert_to_file_path()
132
+ # 默认已经存在 data/temp 中
133
+ b64, duration = await audio_to_tencent_silk_base64(record_path)
134
+ payload = {
135
+ "ToUserName": self.session_id,
136
+ "VoiceData": b64,
137
+ "VoiceFormat": 4,
138
+ "VoiceSecond": duration,
139
+ }
140
+ url = f"{self.adapter.base_url}/message/SendVoice"
141
+ await self._post(session, url, payload)
142
+
143
+ @staticmethod
144
+ def _validate_base64(b64: str) -> bytes:
145
+ return base64.b64decode(b64, validate=True)
146
+
147
+ @staticmethod
148
+ def _compress_image(data: bytes) -> str:
149
+ img = PILImage.open(io.BytesIO(data))
150
+ buf = io.BytesIO()
151
+ if img.format == "JPEG":
152
+ img.save(buf, "JPEG", quality=80)
153
+ else:
154
+ if img.mode in ("RGBA", "P"):
155
+ img = img.convert("RGB")
156
+ img.save(buf, "JPEG", quality=80)
157
+ # logger.info("图片处理完成!!!")
158
+ return base64.b64encode(buf.getvalue()).decode()
159
+
160
+ async def _post(self, session, url, payload):
161
+ params = {"key": self.adapter.auth_key}
162
+ try:
163
+ async with session.post(url, params=params, json=payload) as resp:
164
+ data = await resp.json()
165
+ if resp.status != 200 or data.get("Code") != 200:
166
+ logger.error(f"{url} failed: {resp.status} {data}")
167
+ except Exception as e:
168
+ logger.error(f"{url} error: {e}")
169
+
170
+
171
+ # TODO: 添加对其他消息组件类型的处理 (Record, Video, At等)
172
+ # elif isinstance(component, Record):
173
+ # pass
174
+ # elif isinstance(component, Video):
175
+ # pass
176
+ # elif isinstance(component, At):
177
+ # pass
178
+ # ...
@@ -0,0 +1,159 @@
1
+ from defusedxml import ElementTree as eT
2
+
3
+ from astrbot.api import logger
4
+ from astrbot.api.message_components import (
5
+ BaseMessageComponent,
6
+ Image,
7
+ Plain,
8
+ )
9
+ from astrbot.api.message_components import (
10
+ WechatEmoji as Emoji,
11
+ )
12
+
13
+
14
+ class GeweDataParser:
15
+ def __init__(
16
+ self,
17
+ content: str,
18
+ is_private_chat: bool = False,
19
+ cached_texts=None,
20
+ cached_images=None,
21
+ raw_message: dict | None = None,
22
+ downloader=None,
23
+ ):
24
+ self._xml = None
25
+ self.content = content
26
+ self.is_private_chat = is_private_chat
27
+ self.cached_texts = cached_texts or {}
28
+ self.cached_images = cached_images or {}
29
+ self.downloader = downloader
30
+
31
+ raw_message = raw_message or {}
32
+ self.from_user_name = raw_message.get("from_user_name", {}).get("str", "")
33
+ self.to_user_name = raw_message.get("to_user_name", {}).get("str", "")
34
+ self.msg_id = raw_message.get("msg_id", "")
35
+
36
+ def _format_to_xml(self):
37
+ if self._xml:
38
+ return self._xml
39
+
40
+ try:
41
+ msg_str = self.content
42
+ if not self.is_private_chat:
43
+ parts = self.content.split(":\n", 1)
44
+ msg_str = parts[1] if len(parts) == 2 else self.content
45
+
46
+ self._xml = eT.fromstring(msg_str)
47
+ return self._xml
48
+ except Exception as e:
49
+ logger.error(f"[XML解析失败] {e}")
50
+ raise
51
+
52
+ async def parse_mutil_49(self) -> list[BaseMessageComponent] | None:
53
+ """处理 msg_type == 49 的多种 appmsg 类型(目前支持 type==57)"""
54
+ try:
55
+ appmsg_type = self._format_to_xml().findtext(".//appmsg/type")
56
+ if appmsg_type == "57":
57
+ return await self.parse_reply()
58
+ except Exception as e:
59
+ logger.warning(f"[parse_mutil_49] 解析失败: {e}")
60
+ return None
61
+
62
+ async def parse_reply(self) -> list[BaseMessageComponent]:
63
+ """处理 type == 57 的引用消息:支持文本(1)、图片(3)、嵌套49(49)"""
64
+ components = []
65
+
66
+ try:
67
+ appmsg = self._format_to_xml().find("appmsg")
68
+ if appmsg is None:
69
+ return [Plain("[引用消息解析失败]")]
70
+
71
+ refermsg = appmsg.find("refermsg")
72
+ if refermsg is None:
73
+ return [Plain("[引用消息解析失败]")]
74
+
75
+ quote_type = int(refermsg.findtext("type", "0"))
76
+ nickname = refermsg.findtext("displayname", "未知发送者")
77
+ quote_content = refermsg.findtext("content", "")
78
+ svrid = refermsg.findtext("svrid")
79
+
80
+ match quote_type:
81
+ case 1: # 文本引用
82
+ quoted_text = self.cached_texts.get(str(svrid), quote_content)
83
+ components.append(Plain(f"[引用] {nickname}: {quoted_text}"))
84
+
85
+ case 3: # 图片引用
86
+ quoted_image_b64 = self.cached_images.get(str(svrid))
87
+ if not quoted_image_b64:
88
+ try:
89
+ quote_xml = eT.fromstring(quote_content)
90
+ img = quote_xml.find("img")
91
+ cdn_url = (
92
+ img.get("cdnbigimgurl") or img.get("cdnmidimgurl")
93
+ if img is not None
94
+ else None
95
+ )
96
+ if cdn_url and self.downloader:
97
+ image_resp = await self.downloader(
98
+ self.from_user_name,
99
+ self.to_user_name,
100
+ self.msg_id,
101
+ )
102
+ quoted_image_b64 = (
103
+ image_resp.get("Data", {})
104
+ .get("Data", {})
105
+ .get("Buffer")
106
+ )
107
+ except Exception as e:
108
+ logger.warning(f"[引用图片解析失败] svrid={svrid} err={e}")
109
+
110
+ if quoted_image_b64:
111
+ components.extend(
112
+ [
113
+ Image.fromBase64(quoted_image_b64),
114
+ Plain(f"[引用] {nickname}: [引用的图片]"),
115
+ ],
116
+ )
117
+ else:
118
+ components.append(
119
+ Plain(f"[引用] {nickname}: [引用的图片 - 未能获取]"),
120
+ )
121
+
122
+ case 49: # 嵌套引用
123
+ try:
124
+ nested_root = eT.fromstring(quote_content)
125
+ nested_title = nested_root.findtext(".//appmsg/title", "")
126
+ components.append(Plain(f"[引用] {nickname}: {nested_title}"))
127
+ except Exception as e:
128
+ logger.warning(f"[嵌套引用解析失败] err={e}")
129
+ components.append(Plain(f"[引用] {nickname}: [嵌套引用消息]"))
130
+
131
+ case _: # 其他未识别类型
132
+ logger.info(f"[未知引用类型] quote_type={quote_type}")
133
+ components.append(Plain(f"[引用] {nickname}: [不支持的引用类型]"))
134
+
135
+ # 主消息标题
136
+ title = appmsg.findtext("title", "")
137
+ if title:
138
+ components.append(Plain(title))
139
+
140
+ except Exception as e:
141
+ logger.error(f"[parse_reply] 总体解析失败: {e}")
142
+ return [Plain("[引用消息解析失败]")]
143
+
144
+ return components
145
+
146
+ def parse_emoji(self) -> Emoji | None:
147
+ """处理 msg_type == 47 的表情消息(emoji)"""
148
+ try:
149
+ emoji_element = self._format_to_xml().find(".//emoji")
150
+ if emoji_element is not None:
151
+ return Emoji(
152
+ md5=emoji_element.get("md5"),
153
+ md5_len=emoji_element.get("len"),
154
+ cdnurl=emoji_element.get("cdnurl"),
155
+ )
156
+ except Exception as e:
157
+ logger.error(f"[parse_emoji] 解析失败: {e}")
158
+
159
+ return None
@@ -1,28 +1,33 @@
1
+ import asyncio
2
+ import os
1
3
  import sys
2
4
  import uuid
3
- import asyncio
5
+
4
6
  import quart
7
+ from requests import Response
8
+ from wechatpy.enterprise import WeChatClient, parse_message
9
+ from wechatpy.enterprise.crypto import WeChatCrypto
10
+ from wechatpy.enterprise.messages import ImageMessage, TextMessage, VoiceMessage
11
+ from wechatpy.exceptions import InvalidSignatureException
12
+ from wechatpy.messages import BaseMessage
5
13
 
14
+ from astrbot.api.event import MessageChain
15
+ from astrbot.api.message_components import Image, Plain, Record
6
16
  from astrbot.api.platform import (
7
- Platform,
8
17
  AstrBotMessage,
9
18
  MessageMember,
10
- PlatformMetadata,
11
19
  MessageType,
20
+ Platform,
21
+ PlatformMetadata,
22
+ register_platform_adapter,
12
23
  )
13
- from astrbot.api.event import MessageChain
14
- from astrbot.api.message_components import Plain, Image, Record
15
- from astrbot.core.platform.astr_message_event import MessageSesion
16
- from astrbot.api.platform import register_platform_adapter
17
24
  from astrbot.core import logger
18
- from requests import Response
25
+ from astrbot.core.platform.astr_message_event import MessageSesion
26
+ from astrbot.core.utils.astrbot_path import get_astrbot_data_path
19
27
 
20
- from wechatpy.enterprise.crypto import WeChatCrypto
21
- from wechatpy.enterprise import WeChatClient
22
- from wechatpy.enterprise.messages import TextMessage, ImageMessage, VoiceMessage
23
- from wechatpy.exceptions import InvalidSignatureException
24
- from wechatpy.enterprise import parse_message
25
28
  from .wecom_event import WecomPlatformEvent
29
+ from .wecom_kf import WeChatKF
30
+ from .wecom_kf_message import WeChatKFMessage
26
31
 
27
32
  if sys.version_info >= (3, 12):
28
33
  from typing import override
@@ -36,10 +41,14 @@ class WecomServer:
36
41
  self.port = int(config.get("port"))
37
42
  self.callback_server_host = config.get("callback_server_host", "0.0.0.0")
38
43
  self.server.add_url_rule(
39
- "/callback/command", view_func=self.verify, methods=["GET"]
44
+ "/callback/command",
45
+ view_func=self.verify,
46
+ methods=["GET"],
40
47
  )
41
48
  self.server.add_url_rule(
42
- "/callback/command", view_func=self.callback_command, methods=["POST"]
49
+ "/callback/command",
50
+ view_func=self.callback_command,
51
+ methods=["POST"],
43
52
  )
44
53
  self.event_queue = event_queue
45
54
 
@@ -89,7 +98,7 @@ class WecomServer:
89
98
 
90
99
  async def start_polling(self):
91
100
  logger.info(
92
- f"将在 {self.callback_server_host}:{self.port} 端口启动 企业微信 适配器。"
101
+ f"将在 {self.callback_server_host}:{self.port} 端口启动 企业微信 适配器。",
93
102
  )
94
103
  await self.server.run_task(
95
104
  host=self.callback_server_host,
@@ -101,24 +110,27 @@ class WecomServer:
101
110
  await self.shutdown_event.wait()
102
111
 
103
112
 
104
- @register_platform_adapter("wecom", "wecom 适配器")
113
+ @register_platform_adapter("wecom", "wecom 适配器", support_streaming_message=False)
105
114
  class WecomPlatformAdapter(Platform):
106
115
  def __init__(
107
- self, platform_config: dict, platform_settings: dict, event_queue: asyncio.Queue
116
+ self,
117
+ platform_config: dict,
118
+ platform_settings: dict,
119
+ event_queue: asyncio.Queue,
108
120
  ) -> None:
109
121
  super().__init__(event_queue)
110
122
  self.config = platform_config
111
123
  self.settingss = platform_settings
112
124
  self.client_self_id = uuid.uuid4().hex[:8]
113
125
  self.api_base_url = platform_config.get(
114
- "api_base_url", "https://qyapi.weixin.qq.com/cgi-bin/"
126
+ "api_base_url",
127
+ "https://qyapi.weixin.qq.com/cgi-bin/",
115
128
  )
116
129
 
117
130
  if not self.api_base_url:
118
131
  self.api_base_url = "https://qyapi.weixin.qq.com/cgi-bin/"
119
132
 
120
- if self.api_base_url.endswith("/"):
121
- self.api_base_url = self.api_base_url[:-1]
133
+ self.api_base_url = self.api_base_url.removesuffix("/")
122
134
  if not self.api_base_url.endswith("/cgi-bin"):
123
135
  self.api_base_url += "/cgi-bin"
124
136
 
@@ -131,16 +143,50 @@ class WecomPlatformAdapter(Platform):
131
143
  self.config["corpid"].strip(),
132
144
  self.config["secret"].strip(),
133
145
  )
146
+
147
+ # 微信客服
148
+ self.kf_name = self.config.get("kf_name", None)
149
+ if self.kf_name:
150
+ # inject
151
+ self.wechat_kf_api = WeChatKF(client=self.client)
152
+ self.wechat_kf_message_api = WeChatKFMessage(self.client)
153
+ self.client.kf = self.wechat_kf_api
154
+ self.client.kf_message = self.wechat_kf_message_api
155
+
134
156
  self.client.API_BASE_URL = self.api_base_url
135
157
 
136
- async def callback(msg):
158
+ async def callback(msg: BaseMessage):
159
+ if msg.type == "unknown" and msg._data["Event"] == "kf_msg_or_event":
160
+
161
+ def get_latest_msg_item() -> dict | None:
162
+ token = msg._data["Token"]
163
+ kfid = msg._data["OpenKfId"]
164
+ has_more = 1
165
+ ret = {}
166
+ while has_more:
167
+ ret = self.wechat_kf_api.sync_msg(token, kfid)
168
+ has_more = ret["has_more"]
169
+ msg_list = ret.get("msg_list", [])
170
+ if msg_list:
171
+ return msg_list[-1]
172
+ return None
173
+
174
+ msg_new = await asyncio.get_event_loop().run_in_executor(
175
+ None,
176
+ get_latest_msg_item,
177
+ )
178
+ if msg_new:
179
+ await self.convert_wechat_kf_message(msg_new)
180
+ return
137
181
  await self.convert_message(msg)
138
182
 
139
183
  self.server.callback = callback
140
184
 
141
185
  @override
142
186
  async def send_by_session(
143
- self, session: MessageSesion, message_chain: MessageChain
187
+ self,
188
+ session: MessageSesion,
189
+ message_chain: MessageChain,
144
190
  ):
145
191
  await super().send_by_session(session, message_chain)
146
192
 
@@ -149,13 +195,46 @@ class WecomPlatformAdapter(Platform):
149
195
  return PlatformMetadata(
150
196
  "wecom",
151
197
  "wecom 适配器",
198
+ id=self.config.get("id", "wecom"),
199
+ support_streaming_message=False,
152
200
  )
153
201
 
154
202
  @override
155
203
  async def run(self):
204
+ loop = asyncio.get_event_loop()
205
+ if self.kf_name:
206
+ try:
207
+ acc_list = (
208
+ await loop.run_in_executor(
209
+ None,
210
+ self.wechat_kf_api.get_account_list,
211
+ )
212
+ ).get("account_list", [])
213
+ logger.debug(f"获取到微信客服列表: {acc_list!s}")
214
+ for acc in acc_list:
215
+ name = acc.get("name", None)
216
+ if name != self.kf_name:
217
+ continue
218
+ open_kfid = acc.get("open_kfid", None)
219
+ if not open_kfid:
220
+ logger.error("获取微信客服失败,open_kfid 为空。")
221
+ logger.debug(f"Found open_kfid: {open_kfid!s}")
222
+ kf_url = (
223
+ await loop.run_in_executor(
224
+ None,
225
+ self.wechat_kf_api.add_contact_way,
226
+ open_kfid,
227
+ "astrbot_placeholder",
228
+ )
229
+ ).get("url", "")
230
+ logger.info(
231
+ f"请打开以下链接,在微信扫码以获取客服微信: https://api.cl2wm.cn/api/qrcode/code?text={kf_url}",
232
+ )
233
+ except Exception as e:
234
+ logger.error(e)
156
235
  await self.server.start_polling()
157
236
 
158
- async def convert_message(self, msg):
237
+ async def convert_message(self, msg: BaseMessage) -> AstrBotMessage | None:
159
238
  abm = AstrBotMessage()
160
239
  if msg.type == "text":
161
240
  assert isinstance(msg, TextMessage)
@@ -189,16 +268,19 @@ class WecomPlatformAdapter(Platform):
189
268
  assert isinstance(msg, VoiceMessage)
190
269
 
191
270
  resp: Response = await asyncio.get_event_loop().run_in_executor(
192
- None, self.client.media.download, msg.media_id
271
+ None,
272
+ self.client.media.download,
273
+ msg.media_id,
193
274
  )
194
- path = f"data/temp/wecom_{msg.media_id}.amr"
275
+ temp_dir = os.path.join(get_astrbot_data_path(), "temp")
276
+ path = os.path.join(temp_dir, f"wecom_{msg.media_id}.amr")
195
277
  with open(path, "wb") as f:
196
278
  f.write(resp.content)
197
279
 
198
280
  try:
199
281
  from pydub import AudioSegment
200
282
 
201
- path_wav = f"data/temp/wecom_{msg.media_id}.wav"
283
+ path_wav = os.path.join(temp_dir, f"wecom_{msg.media_id}.wav")
202
284
  audio = AudioSegment.from_file(path)
203
285
  audio.export(path_wav, format="wav")
204
286
  except Exception as e:
@@ -218,10 +300,70 @@ class WecomPlatformAdapter(Platform):
218
300
  abm.timestamp = msg.time
219
301
  abm.session_id = abm.sender.user_id
220
302
  abm.raw_message = msg
303
+ else:
304
+ logger.warning(f"暂未实现的事件: {msg.type}")
305
+ return
221
306
 
222
307
  logger.info(f"abm: {abm}")
223
308
  await self.handle_msg(abm)
224
309
 
310
+ async def convert_wechat_kf_message(self, msg: dict) -> AstrBotMessage | None:
311
+ msgtype = msg.get("msgtype")
312
+ external_userid = msg.get("external_userid")
313
+ abm = AstrBotMessage()
314
+ abm.raw_message = msg
315
+ abm.raw_message["_wechat_kf_flag"] = None # 方便处理
316
+ abm.self_id = msg["open_kfid"]
317
+ abm.sender = MessageMember(external_userid, external_userid)
318
+ abm.session_id = external_userid
319
+ abm.type = MessageType.FRIEND_MESSAGE
320
+ abm.message_id = msg.get("msgid", uuid.uuid4().hex[:8])
321
+ abm.message_str = ""
322
+ if msgtype == "text":
323
+ text = msg.get("text", {}).get("content", "").strip()
324
+ abm.message = [Plain(text=text)]
325
+ abm.message_str = text
326
+ elif msgtype == "image":
327
+ media_id = msg.get("image", {}).get("media_id", "")
328
+ resp: Response = await asyncio.get_event_loop().run_in_executor(
329
+ None,
330
+ self.client.media.download,
331
+ media_id,
332
+ )
333
+ path = f"data/temp/wechat_kf_{media_id}.jpg"
334
+ with open(path, "wb") as f:
335
+ f.write(resp.content)
336
+ abm.message = [Image(file=path, url=path)]
337
+ elif msgtype == "voice":
338
+ media_id = msg.get("voice", {}).get("media_id", "")
339
+ resp: Response = await asyncio.get_event_loop().run_in_executor(
340
+ None,
341
+ self.client.media.download,
342
+ media_id,
343
+ )
344
+
345
+ temp_dir = os.path.join(get_astrbot_data_path(), "temp")
346
+ path = os.path.join(temp_dir, f"weixinkefu_{media_id}.amr")
347
+ with open(path, "wb") as f:
348
+ f.write(resp.content)
349
+
350
+ try:
351
+ from pydub import AudioSegment
352
+
353
+ path_wav = os.path.join(temp_dir, f"weixinkefu_{media_id}.wav")
354
+ audio = AudioSegment.from_file(path)
355
+ audio.export(path_wav, format="wav")
356
+ except Exception as e:
357
+ logger.error(f"转换音频失败: {e}。如果没有安装 ffmpeg 请先安装。")
358
+ path_wav = path
359
+ return
360
+
361
+ abm.message = [Record(file=path_wav, url=path_wav)]
362
+ else:
363
+ logger.warning(f"未实现的微信客服消息事件: {msg}")
364
+ return
365
+ await self.handle_msg(abm)
366
+
225
367
  async def handle_msg(self, message: AstrBotMessage):
226
368
  message_event = WecomPlatformEvent(
227
369
  message_str=message.message_str,