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,313 @@
1
+ import asyncio
2
+ import base64
3
+ import binascii
4
+ from collections.abc import AsyncGenerator
5
+ from io import BytesIO
6
+ from pathlib import Path
7
+
8
+ import discord
9
+
10
+ from astrbot import logger
11
+ from astrbot.api.event import AstrMessageEvent, MessageChain
12
+ from astrbot.api.message_components import (
13
+ BaseMessageComponent,
14
+ File,
15
+ Image,
16
+ Plain,
17
+ Reply,
18
+ )
19
+ from astrbot.api.platform import AstrBotMessage, At, PlatformMetadata
20
+
21
+ from .client import DiscordBotClient
22
+ from .components import DiscordEmbed, DiscordView
23
+
24
+
25
+ # 自定义Discord视图组件(兼容旧版本)
26
+ class DiscordViewComponent(BaseMessageComponent):
27
+ type: str = "discord_view"
28
+
29
+ def __init__(self, view: discord.ui.View):
30
+ self.view = view
31
+
32
+
33
+ class DiscordPlatformEvent(AstrMessageEvent):
34
+ def __init__(
35
+ self,
36
+ message_str: str,
37
+ message_obj: AstrBotMessage,
38
+ platform_meta: PlatformMetadata,
39
+ session_id: str,
40
+ client: DiscordBotClient,
41
+ interaction_followup_webhook: discord.Webhook | None = None,
42
+ ):
43
+ super().__init__(message_str, message_obj, platform_meta, session_id)
44
+ self.client = client
45
+ self.interaction_followup_webhook = interaction_followup_webhook
46
+
47
+ async def send(self, message: MessageChain):
48
+ """发送消息到Discord平台"""
49
+ # 解析消息链为 Discord 所需的对象
50
+ try:
51
+ (
52
+ content,
53
+ files,
54
+ view,
55
+ embeds,
56
+ reference_message_id,
57
+ ) = await self._parse_to_discord(message)
58
+ except Exception as e:
59
+ logger.error(f"[Discord] 解析消息链时失败: {e}", exc_info=True)
60
+ return
61
+
62
+ kwargs = {}
63
+ if content:
64
+ kwargs["content"] = content
65
+ if files:
66
+ kwargs["files"] = files
67
+ if view:
68
+ kwargs["view"] = view
69
+ if embeds:
70
+ kwargs["embeds"] = embeds
71
+ if reference_message_id and not self.interaction_followup_webhook:
72
+ kwargs["reference"] = self.client.get_message(int(reference_message_id))
73
+ if not kwargs:
74
+ logger.debug("[Discord] 尝试发送空消息,已忽略。")
75
+ return
76
+
77
+ # 根据上下文执行发送/回复操作
78
+ try:
79
+ # -- 斜杠指令/交互上下文 --
80
+ if self.interaction_followup_webhook:
81
+ await self.interaction_followup_webhook.send(**kwargs)
82
+
83
+ # -- 常规消息上下文 --
84
+ else:
85
+ channel = await self._get_channel()
86
+ if not channel:
87
+ return
88
+ await channel.send(**kwargs)
89
+
90
+ except Exception as e:
91
+ logger.error(f"[Discord] 发送消息时发生未知错误: {e}", exc_info=True)
92
+
93
+ await super().send(message)
94
+
95
+ async def send_streaming(
96
+ self, generator: AsyncGenerator[MessageChain, None], use_fallback: bool = False
97
+ ):
98
+ buffer = None
99
+ async for chain in generator:
100
+ if not buffer:
101
+ buffer = chain
102
+ else:
103
+ buffer.chain.extend(chain.chain)
104
+ if not buffer:
105
+ return None
106
+ buffer.squash_plain()
107
+ await self.send(buffer)
108
+ return await super().send_streaming(generator, use_fallback)
109
+
110
+ async def _get_channel(self) -> discord.abc.Messageable | None:
111
+ """获取当前事件对应的频道对象"""
112
+ try:
113
+ channel_id = int(self.session_id)
114
+ return self.client.get_channel(
115
+ channel_id,
116
+ ) or await self.client.fetch_channel(channel_id)
117
+ except (ValueError, discord.errors.NotFound, discord.errors.Forbidden):
118
+ logger.error(f"[Discord] 无法获取频道 {self.session_id}")
119
+ return None
120
+
121
+ async def _parse_to_discord(
122
+ self,
123
+ message: MessageChain,
124
+ ) -> tuple[str, list[discord.File], discord.ui.View | None, list[discord.Embed]]:
125
+ """将 MessageChain 解析为 Discord 发送所需的内容"""
126
+ content_parts = []
127
+ files = []
128
+ view = None
129
+ embeds = []
130
+ reference_message_id = None
131
+ for i in message.chain: # 遍历消息链
132
+ if isinstance(i, Plain): # 如果是文字类型的
133
+ content_parts.append(i.text)
134
+ elif isinstance(i, Reply):
135
+ reference_message_id = i.id
136
+ elif isinstance(i, At):
137
+ content_parts.append(f"<@{i.qq}>")
138
+ elif isinstance(i, Image):
139
+ logger.debug(f"[Discord] 开始处理 Image 组件: {i}")
140
+ try:
141
+ filename = getattr(i, "filename", None)
142
+ file_content = getattr(i, "file", None)
143
+
144
+ if not file_content:
145
+ logger.warning(f"[Discord] Image 组件没有 file 属性: {i}")
146
+ continue
147
+
148
+ discord_file = None
149
+
150
+ # 1. URL
151
+ if file_content.startswith("http"):
152
+ logger.debug(f"[Discord] 处理 URL 图片: {file_content}")
153
+ embed = discord.Embed().set_image(url=file_content)
154
+ embeds.append(embed)
155
+ continue
156
+
157
+ # 2. File URI
158
+ if file_content.startswith("file:///"):
159
+ logger.debug(f"[Discord] 处理 File URI: {file_content}")
160
+ path = Path(file_content[8:])
161
+ if await asyncio.to_thread(path.exists):
162
+ file_bytes = await asyncio.to_thread(path.read_bytes)
163
+ discord_file = discord.File(
164
+ BytesIO(file_bytes),
165
+ filename=filename or path.name,
166
+ )
167
+ else:
168
+ logger.warning(f"[Discord] 图片文件不存在: {path}")
169
+
170
+ # 3. Base64 URI
171
+ elif file_content.startswith("base64://"):
172
+ logger.debug("[Discord] 处理 Base64 URI")
173
+ b64_data = file_content.split("base64://", 1)[1]
174
+ missing_padding = len(b64_data) % 4
175
+ if missing_padding:
176
+ b64_data += "=" * (4 - missing_padding)
177
+ img_bytes = base64.b64decode(b64_data)
178
+ discord_file = discord.File(
179
+ BytesIO(img_bytes),
180
+ filename=filename or "image.png",
181
+ )
182
+
183
+ # 4. 裸 Base64 或本地路径
184
+ else:
185
+ try:
186
+ logger.debug("[Discord] 尝试作为裸 Base64 处理")
187
+ b64_data = file_content
188
+ missing_padding = len(b64_data) % 4
189
+ if missing_padding:
190
+ b64_data += "=" * (4 - missing_padding)
191
+ img_bytes = base64.b64decode(b64_data)
192
+ discord_file = discord.File(
193
+ BytesIO(img_bytes),
194
+ filename=filename or "image.png",
195
+ )
196
+ except (ValueError, TypeError, binascii.Error):
197
+ logger.debug(
198
+ f"[Discord] 裸 Base64 解码失败,作为本地路径处理: {file_content}",
199
+ )
200
+ path = Path(file_content)
201
+ if await asyncio.to_thread(path.exists):
202
+ file_bytes = await asyncio.to_thread(path.read_bytes)
203
+ discord_file = discord.File(
204
+ BytesIO(file_bytes),
205
+ filename=filename or path.name,
206
+ )
207
+ else:
208
+ logger.warning(f"[Discord] 图片文件不存在: {path}")
209
+
210
+ if discord_file:
211
+ files.append(discord_file)
212
+
213
+ except Exception:
214
+ # 使用 getattr 来安全地访问 i.file,以防 i 本身就是问题
215
+ file_info = getattr(i, "file", "未知")
216
+ logger.error(
217
+ f"[Discord] 处理图片时发生未知严重错误: {file_info}",
218
+ exc_info=True,
219
+ )
220
+ elif isinstance(i, File):
221
+ try:
222
+ file_path_str = await i.get_file()
223
+ if file_path_str:
224
+ path = Path(file_path_str)
225
+ if await asyncio.to_thread(path.exists):
226
+ file_bytes = await asyncio.to_thread(path.read_bytes)
227
+ files.append(
228
+ discord.File(BytesIO(file_bytes), filename=i.name),
229
+ )
230
+ else:
231
+ logger.warning(
232
+ f"[Discord] 获取文件失败,路径不存在: {file_path_str}",
233
+ )
234
+ else:
235
+ logger.warning(f"[Discord] 获取文件失败: {i.name}")
236
+ except Exception as e:
237
+ logger.warning(f"[Discord] 处理文件失败: {i.name}, 错误: {e}")
238
+ elif isinstance(i, DiscordEmbed):
239
+ # Discord Embed消息
240
+ embeds.append(i.to_discord_embed())
241
+ elif isinstance(i, DiscordView):
242
+ # Discord视图组件(按钮、选择菜单等)
243
+ view = i.to_discord_view()
244
+ elif isinstance(i, DiscordViewComponent):
245
+ # 如果消息链中包含Discord视图组件(兼容旧版本)
246
+ if isinstance(i.view, discord.ui.View):
247
+ view = i.view
248
+ else:
249
+ logger.debug(f"[Discord] 忽略了不支持的消息组件: {i.type}")
250
+
251
+ content = "".join(content_parts)
252
+ if len(content) > 2000:
253
+ logger.warning("[Discord] 消息内容超过2000字符,将被截断。")
254
+ content = content[:2000]
255
+ return content, files, view, embeds, reference_message_id
256
+
257
+ async def react(self, emoji: str):
258
+ """对原消息添加反应"""
259
+ try:
260
+ if hasattr(self.message_obj, "raw_message") and hasattr(
261
+ self.message_obj.raw_message,
262
+ "add_reaction",
263
+ ):
264
+ await self.message_obj.raw_message.add_reaction(emoji)
265
+ except Exception as e:
266
+ logger.error(f"[Discord] 添加反应失败: {e}")
267
+
268
+ def is_slash_command(self) -> bool:
269
+ """判断是否为斜杠命令"""
270
+ return (
271
+ hasattr(self.message_obj, "raw_message")
272
+ and hasattr(self.message_obj.raw_message, "type")
273
+ and self.message_obj.raw_message.type
274
+ == discord.InteractionType.application_command
275
+ )
276
+
277
+ def is_button_interaction(self) -> bool:
278
+ """判断是否为按钮交互"""
279
+ return (
280
+ hasattr(self.message_obj, "raw_message")
281
+ and hasattr(self.message_obj.raw_message, "type")
282
+ and self.message_obj.raw_message.type == discord.InteractionType.component
283
+ )
284
+
285
+ def get_interaction_custom_id(self) -> str:
286
+ """获取交互组件的custom_id"""
287
+ if self.is_button_interaction():
288
+ try:
289
+ return self.message_obj.raw_message.data.get("custom_id", "")
290
+ except Exception:
291
+ pass
292
+ return ""
293
+
294
+ def is_mentioned(self) -> bool:
295
+ """判断机器人是否被@"""
296
+ if hasattr(self.message_obj, "raw_message") and hasattr(
297
+ self.message_obj.raw_message,
298
+ "mentions",
299
+ ):
300
+ return any(
301
+ mention.id == int(self.message_obj.self_id)
302
+ for mention in self.message_obj.raw_message.mentions
303
+ )
304
+ return False
305
+
306
+ def get_mention_clean_content(self) -> str:
307
+ """获取去除@后的清洁内容"""
308
+ if hasattr(self.message_obj, "raw_message") and hasattr(
309
+ self.message_obj.raw_message,
310
+ "clean_content",
311
+ ):
312
+ return self.message_obj.raw_message.clean_content
313
+ return self.message_str
@@ -1,30 +1,37 @@
1
- import base64
2
1
  import asyncio
2
+ import base64
3
3
  import json
4
4
  import re
5
5
  import uuid
6
- import astrbot.api.message_components as Comp
7
6
 
7
+ import lark_oapi as lark
8
+ from lark_oapi.api.im.v1 import *
9
+
10
+ import astrbot.api.message_components as Comp
11
+ from astrbot import logger
12
+ from astrbot.api.event import MessageChain
8
13
  from astrbot.api.platform import (
9
- Platform,
10
14
  AstrBotMessage,
11
15
  MessageMember,
12
16
  MessageType,
17
+ Platform,
13
18
  PlatformMetadata,
14
19
  )
15
- from astrbot.api.event import MessageChain
16
20
  from astrbot.core.platform.astr_message_event import MessageSesion
17
- from .lark_event import LarkMessageEvent
21
+
18
22
  from ...register import register_platform_adapter
19
- from astrbot import logger
20
- import lark_oapi as lark
21
- from lark_oapi.api.im.v1 import *
23
+ from .lark_event import LarkMessageEvent
22
24
 
23
25
 
24
- @register_platform_adapter("lark", "飞书机器人官方 API 适配器")
26
+ @register_platform_adapter(
27
+ "lark", "飞书机器人官方 API 适配器", support_streaming_message=False
28
+ )
25
29
  class LarkPlatformAdapter(Platform):
26
30
  def __init__(
27
- self, platform_config: dict, platform_settings: dict, event_queue: asyncio.Queue
31
+ self,
32
+ platform_config: dict,
33
+ platform_settings: dict,
34
+ event_queue: asyncio.Queue,
28
35
  ) -> None:
29
36
  super().__init__(event_queue)
30
37
 
@@ -65,14 +72,16 @@ class LarkPlatformAdapter(Platform):
65
72
  )
66
73
 
67
74
  async def send_by_session(
68
- self, session: MessageSesion, message_chain: MessageChain
75
+ self,
76
+ session: MessageSesion,
77
+ message_chain: MessageChain,
69
78
  ):
70
79
  res = await LarkMessageEvent._convert_to_lark(message_chain, self.lark_api)
71
80
  wrapped = {
72
81
  "zh_cn": {
73
82
  "title": "",
74
83
  "content": res,
75
- }
84
+ },
76
85
  }
77
86
 
78
87
  if session.message_type == MessageType.GROUP_MESSAGE:
@@ -91,7 +100,7 @@ class LarkPlatformAdapter(Platform):
91
100
  .content(json.dumps(wrapped))
92
101
  .msg_type("post")
93
102
  .uuid(str(uuid.uuid4()))
94
- .build()
103
+ .build(),
95
104
  )
96
105
  .build()
97
106
  )
@@ -108,6 +117,7 @@ class LarkPlatformAdapter(Platform):
108
117
  name="lark",
109
118
  description="飞书机器人官方 API 适配器",
110
119
  id=self.config.get("id"),
120
+ support_streaming_message=False,
111
121
  )
112
122
 
113
123
  async def convert_msg(self, event: lark.im.v1.P2ImMessageReceiveV1):
@@ -160,7 +170,7 @@ class LarkPlatformAdapter(Platform):
160
170
  content_json_b = _ls
161
171
  elif message.message_type == "image":
162
172
  content_json_b = [
163
- {"tag": "img", "image_key": content_json_b["image_key"], "style": []}
173
+ {"tag": "img", "image_key": content_json_b["image_key"], "style": []},
164
174
  ]
165
175
 
166
176
  if message.message_type in ("post", "image"):
@@ -200,11 +210,10 @@ class LarkPlatformAdapter(Platform):
200
210
  abm.session_id = abm.group_id
201
211
  else:
202
212
  abm.session_id = abm.sender.user_id
213
+ elif abm.type == MessageType.GROUP_MESSAGE:
214
+ abm.session_id = f"{abm.sender.user_id}%{abm.group_id}" # 也保留群组id
203
215
  else:
204
- if abm.type == MessageType.GROUP_MESSAGE:
205
- abm.session_id = f"{abm.sender.user_id}%{abm.group_id}" # 也保留群组id
206
- else:
207
- abm.session_id = abm.sender.user_id
216
+ abm.session_id = abm.sender.user_id
208
217
 
209
218
  logger.debug(abm)
210
219
  await self.handle_msg(abm)
@@ -1,25 +1,34 @@
1
+ import base64
1
2
  import json
3
+ import os
2
4
  import uuid
3
- import base64
4
- import lark_oapi as lark
5
5
  from io import BytesIO
6
- from typing import List
7
- from astrbot.api.event import AstrMessageEvent, MessageChain
8
- from astrbot.api.message_components import Plain, Image as AstrBotImage, At
9
- from astrbot.core.utils.io import download_image_by_url
6
+
7
+ import lark_oapi as lark
10
8
  from lark_oapi.api.im.v1 import *
9
+
11
10
  from astrbot import logger
11
+ from astrbot.api.event import AstrMessageEvent, MessageChain
12
+ from astrbot.api.message_components import At, Plain
13
+ from astrbot.api.message_components import Image as AstrBotImage
14
+ from astrbot.core.utils.astrbot_path import get_astrbot_data_path
15
+ from astrbot.core.utils.io import download_image_by_url
12
16
 
13
17
 
14
18
  class LarkMessageEvent(AstrMessageEvent):
15
19
  def __init__(
16
- self, message_str, message_obj, platform_meta, session_id, bot: lark.Client
20
+ self,
21
+ message_str,
22
+ message_obj,
23
+ platform_meta,
24
+ session_id,
25
+ bot: lark.Client,
17
26
  ):
18
27
  super().__init__(message_str, message_obj, platform_meta, session_id)
19
28
  self.bot = bot
20
29
 
21
30
  @staticmethod
22
- async def _convert_to_lark(message: MessageChain, lark_client: lark.Client) -> List:
31
+ async def _convert_to_lark(message: MessageChain, lark_client: lark.Client) -> list:
23
32
  ret = []
24
33
  _stage = []
25
34
  for comp in message.chain:
@@ -40,7 +49,8 @@ class LarkMessageEvent(AstrMessageEvent):
40
49
  base64_str = comp.file.removeprefix("base64://")
41
50
  image_data = base64.b64decode(base64_str)
42
51
  # save as temp file
43
- file_path = f"data/temp/{uuid.uuid4()}_test.jpg"
52
+ temp_dir = os.path.join(get_astrbot_data_path(), "temp")
53
+ file_path = os.path.join(temp_dir, f"{uuid.uuid4()}_test.jpg")
44
54
  with open(file_path, "wb") as f:
45
55
  f.write(BytesIO(image_data).getvalue())
46
56
  else:
@@ -55,7 +65,7 @@ class LarkMessageEvent(AstrMessageEvent):
55
65
  CreateImageRequestBody.builder()
56
66
  .image_type("message")
57
67
  .image(image_file)
58
- .build()
68
+ .build(),
59
69
  )
60
70
  .build()
61
71
  )
@@ -80,7 +90,7 @@ class LarkMessageEvent(AstrMessageEvent):
80
90
  "zh_cn": {
81
91
  "title": "",
82
92
  "content": res,
83
- }
93
+ },
84
94
  }
85
95
 
86
96
  request = (
@@ -92,7 +102,7 @@ class LarkMessageEvent(AstrMessageEvent):
92
102
  .msg_type("post")
93
103
  .uuid(str(uuid.uuid4()))
94
104
  .reply_in_thread(False)
95
- .build()
105
+ .build(),
96
106
  )
97
107
  .build()
98
108
  )
@@ -104,6 +114,22 @@ class LarkMessageEvent(AstrMessageEvent):
104
114
 
105
115
  await super().send(message)
106
116
 
117
+ async def react(self, emoji: str):
118
+ request = (
119
+ CreateMessageReactionRequest.builder()
120
+ .message_id(self.message_obj.message_id)
121
+ .request_body(
122
+ CreateMessageReactionRequestBody.builder()
123
+ .reaction_type(Emoji.builder().emoji_type(emoji).build())
124
+ .build(),
125
+ )
126
+ .build()
127
+ )
128
+ response = await self.bot.im.v1.message_reaction.acreate(request)
129
+ if not response.success():
130
+ logger.error(f"发送飞书表情回应失败({response.code}): {response.msg}")
131
+ return
132
+
107
133
  async def send_streaming(self, generator, use_fallback: bool = False):
108
134
  buffer = None
109
135
  async for chain in generator:
@@ -112,7 +138,7 @@ class LarkMessageEvent(AstrMessageEvent):
112
138
  else:
113
139
  buffer.chain.extend(chain.chain)
114
140
  if not buffer:
115
- return
141
+ return None
116
142
  buffer.squash_plain()
117
143
  await self.send(buffer)
118
144
  return await super().send_streaming(generator, use_fallback)