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,550 @@
1
+ """Misskey 平台适配器通用工具函数"""
2
+
3
+ from typing import Any
4
+
5
+ import astrbot.api.message_components as Comp
6
+ from astrbot.api.platform import AstrBotMessage, MessageMember, MessageType
7
+
8
+
9
+ class FileIDExtractor:
10
+ """从 API 响应中提取文件 ID 的帮助类(无状态)。"""
11
+
12
+ @staticmethod
13
+ def extract_file_id(result: Any) -> str | None:
14
+ if not isinstance(result, dict):
15
+ return None
16
+
17
+ id_paths = [
18
+ lambda r: r.get("createdFile", {}).get("id"),
19
+ lambda r: r.get("file", {}).get("id"),
20
+ lambda r: r.get("id"),
21
+ ]
22
+
23
+ for p in id_paths:
24
+ try:
25
+ if fid := p(result):
26
+ return fid
27
+ except Exception:
28
+ continue
29
+
30
+ return None
31
+
32
+
33
+ class MessagePayloadBuilder:
34
+ """构建不同类型消息负载的帮助类(无状态)。"""
35
+
36
+ @staticmethod
37
+ def build_chat_payload(
38
+ user_id: str,
39
+ text: str | None,
40
+ file_id: str | None = None,
41
+ ) -> dict[str, Any]:
42
+ payload = {"toUserId": user_id}
43
+ if text:
44
+ payload["text"] = text
45
+ if file_id:
46
+ payload["fileId"] = file_id
47
+ return payload
48
+
49
+ @staticmethod
50
+ def build_room_payload(
51
+ room_id: str,
52
+ text: str | None,
53
+ file_id: str | None = None,
54
+ ) -> dict[str, Any]:
55
+ payload = {"toRoomId": room_id}
56
+ if text:
57
+ payload["text"] = text
58
+ if file_id:
59
+ payload["fileId"] = file_id
60
+ return payload
61
+
62
+ @staticmethod
63
+ def build_note_payload(
64
+ text: str | None,
65
+ file_ids: list[str] | None = None,
66
+ **kwargs,
67
+ ) -> dict[str, Any]:
68
+ payload: dict[str, Any] = {}
69
+ if text:
70
+ payload["text"] = text
71
+ if file_ids:
72
+ payload["fileIds"] = file_ids
73
+ payload |= kwargs
74
+ return payload
75
+
76
+
77
+ def serialize_message_chain(chain: list[Any]) -> tuple[str, bool]:
78
+ """将消息链序列化为文本字符串"""
79
+ text_parts = []
80
+ has_at = False
81
+
82
+ def process_component(component):
83
+ nonlocal has_at
84
+ if isinstance(component, Comp.Plain):
85
+ return component.text
86
+ if isinstance(component, Comp.File):
87
+ # 为文件组件返回占位符,但适配器仍会处理原组件
88
+ return "[文件]"
89
+ if isinstance(component, Comp.Image):
90
+ # 为图片组件返回占位符,但适配器仍会处理原组件
91
+ return "[图片]"
92
+ if isinstance(component, Comp.At):
93
+ has_at = True
94
+ # 优先使用name字段(用户名),如果没有则使用qq字段
95
+ # 这样可以避免在Misskey中生成 @<user_id> 这样的无效提及
96
+ if hasattr(component, "name") and component.name:
97
+ return f"@{component.name}"
98
+ return f"@{component.qq}"
99
+ if hasattr(component, "text"):
100
+ text = getattr(component, "text", "")
101
+ if "@" in text:
102
+ has_at = True
103
+ return text
104
+ return str(component)
105
+
106
+ for component in chain:
107
+ if isinstance(component, Comp.Node) and component.content:
108
+ for node_comp in component.content:
109
+ result = process_component(node_comp)
110
+ if result:
111
+ text_parts.append(result)
112
+ else:
113
+ result = process_component(component)
114
+ if result:
115
+ text_parts.append(result)
116
+
117
+ return "".join(text_parts), has_at
118
+
119
+
120
+ def resolve_message_visibility(
121
+ user_id: str | None = None,
122
+ user_cache: dict[str, Any] | None = None,
123
+ self_id: str | None = None,
124
+ raw_message: dict[str, Any] | None = None,
125
+ default_visibility: str = "public",
126
+ ) -> tuple[str, list[str] | None]:
127
+ """解析 Misskey 消息的可见性设置
128
+
129
+ 可以从 user_cache 或 raw_message 中解析,支持两种调用方式:
130
+ 1. 基于 user_cache: resolve_message_visibility(user_id, user_cache, self_id)
131
+ 2. 基于 raw_message: resolve_message_visibility(raw_message=raw_message, self_id=self_id)
132
+ """
133
+ visibility = default_visibility
134
+ visible_user_ids = None
135
+
136
+ # 优先从 user_cache 解析
137
+ if user_id and user_cache:
138
+ user_info = user_cache.get(user_id)
139
+ if user_info:
140
+ original_visibility = user_info.get("visibility", default_visibility)
141
+ if original_visibility == "specified":
142
+ visibility = "specified"
143
+ original_visible_users = user_info.get("visible_user_ids", [])
144
+ users_to_include = [user_id]
145
+ if self_id:
146
+ users_to_include.append(self_id)
147
+ visible_user_ids = list(set(original_visible_users + users_to_include))
148
+ visible_user_ids = [uid for uid in visible_user_ids if uid]
149
+ else:
150
+ visibility = original_visibility
151
+ return visibility, visible_user_ids
152
+
153
+ # 回退到从 raw_message 解析
154
+ if raw_message:
155
+ original_visibility = raw_message.get("visibility", default_visibility)
156
+ if original_visibility == "specified":
157
+ visibility = "specified"
158
+ original_visible_users = raw_message.get("visibleUserIds", [])
159
+ sender_id = raw_message.get("userId", "")
160
+
161
+ users_to_include = []
162
+ if sender_id:
163
+ users_to_include.append(sender_id)
164
+ if self_id:
165
+ users_to_include.append(self_id)
166
+
167
+ visible_user_ids = list(set(original_visible_users + users_to_include))
168
+ visible_user_ids = [uid for uid in visible_user_ids if uid]
169
+ else:
170
+ visibility = original_visibility
171
+
172
+ return visibility, visible_user_ids
173
+
174
+
175
+ # 保留旧函数名作为向后兼容的别名
176
+ def resolve_visibility_from_raw_message(
177
+ raw_message: dict[str, Any],
178
+ self_id: str | None = None,
179
+ ) -> tuple[str, list[str] | None]:
180
+ """从原始消息数据中解析可见性设置(已弃用,使用 resolve_message_visibility 替代)"""
181
+ return resolve_message_visibility(raw_message=raw_message, self_id=self_id)
182
+
183
+
184
+ def is_valid_user_session_id(session_id: str | Any) -> bool:
185
+ """检查 session_id 是否是有效的聊天用户 session_id (仅限chat%前缀)"""
186
+ if not isinstance(session_id, str) or "%" not in session_id:
187
+ return False
188
+
189
+ parts = session_id.split("%")
190
+ return (
191
+ len(parts) == 2
192
+ and parts[0] == "chat"
193
+ and bool(parts[1])
194
+ and parts[1] != "unknown"
195
+ )
196
+
197
+
198
+ def is_valid_room_session_id(session_id: str | Any) -> bool:
199
+ """检查 session_id 是否是有效的房间 session_id (仅限room%前缀)"""
200
+ if not isinstance(session_id, str) or "%" not in session_id:
201
+ return False
202
+
203
+ parts = session_id.split("%")
204
+ return (
205
+ len(parts) == 2
206
+ and parts[0] == "room"
207
+ and bool(parts[1])
208
+ and parts[1] != "unknown"
209
+ )
210
+
211
+
212
+ def is_valid_chat_session_id(session_id: str | Any) -> bool:
213
+ """检查 session_id 是否是有效的聊天 session_id (仅限chat%前缀)"""
214
+ if not isinstance(session_id, str) or "%" not in session_id:
215
+ return False
216
+
217
+ parts = session_id.split("%")
218
+ return (
219
+ len(parts) == 2
220
+ and parts[0] == "chat"
221
+ and bool(parts[1])
222
+ and parts[1] != "unknown"
223
+ )
224
+
225
+
226
+ def extract_user_id_from_session_id(session_id: str) -> str:
227
+ """从 session_id 中提取用户 ID"""
228
+ if "%" in session_id:
229
+ parts = session_id.split("%")
230
+ if len(parts) >= 2:
231
+ return parts[1]
232
+ return session_id
233
+
234
+
235
+ def extract_room_id_from_session_id(session_id: str) -> str:
236
+ """从 session_id 中提取房间 ID"""
237
+ if "%" in session_id:
238
+ parts = session_id.split("%")
239
+ if len(parts) >= 2 and parts[0] == "room":
240
+ return parts[1]
241
+ return session_id
242
+
243
+
244
+ def add_at_mention_if_needed(
245
+ text: str,
246
+ user_info: dict[str, Any] | None,
247
+ has_at: bool = False,
248
+ ) -> str:
249
+ """如果需要且没有@用户,则添加@用户
250
+
251
+ 注意:仅在有有效的username时才添加@提及,避免使用用户ID
252
+ """
253
+ if has_at or not user_info:
254
+ return text
255
+
256
+ username = user_info.get("username")
257
+ # 如果没有username,则不添加@提及,返回原文本
258
+ # 这样可以避免生成 @<user_id> 这样的无效提及
259
+ if not username:
260
+ return text
261
+
262
+ mention = f"@{username}"
263
+ if not text.startswith(mention):
264
+ text = f"{mention}\n{text}".strip()
265
+
266
+ return text
267
+
268
+
269
+ def create_file_component(file_info: dict[str, Any]) -> tuple[Any, str]:
270
+ """创建文件组件和描述文本"""
271
+ file_url = file_info.get("url", "")
272
+ file_name = file_info.get("name", "未知文件")
273
+ file_type = file_info.get("type", "")
274
+
275
+ if file_type.startswith("image/"):
276
+ return Comp.Image(url=file_url, file=file_name), f"图片[{file_name}]"
277
+ if file_type.startswith("audio/"):
278
+ return Comp.Record(url=file_url, file=file_name), f"音频[{file_name}]"
279
+ if file_type.startswith("video/"):
280
+ return Comp.Video(url=file_url, file=file_name), f"视频[{file_name}]"
281
+ return Comp.File(name=file_name, url=file_url), f"文件[{file_name}]"
282
+
283
+
284
+ def process_files(
285
+ message: AstrBotMessage,
286
+ files: list,
287
+ include_text_parts: bool = True,
288
+ ) -> list:
289
+ """处理文件列表,添加到消息组件中并返回文本描述"""
290
+ file_parts = []
291
+ for file_info in files:
292
+ component, part_text = create_file_component(file_info)
293
+ message.message.append(component)
294
+ if include_text_parts:
295
+ file_parts.append(part_text)
296
+ return file_parts
297
+
298
+
299
+ def format_poll(poll: dict[str, Any]) -> str:
300
+ """将 Misskey 的 poll 对象格式化为可读字符串。"""
301
+ if not poll or not isinstance(poll, dict):
302
+ return ""
303
+ multiple = poll.get("multiple", False)
304
+ choices = poll.get("choices", [])
305
+ text_choices = [
306
+ f"({idx}) {c.get('text', '')} [{c.get('votes', 0)}票]"
307
+ for idx, c in enumerate(choices, start=1)
308
+ ]
309
+ parts = ["[投票]", ("允许多选" if multiple else "单选")] + (
310
+ ["选项: " + ", ".join(text_choices)] if text_choices else []
311
+ )
312
+ return " ".join(parts)
313
+
314
+
315
+ def extract_sender_info(
316
+ raw_data: dict[str, Any],
317
+ is_chat: bool = False,
318
+ ) -> dict[str, Any]:
319
+ """提取发送者信息"""
320
+ if is_chat:
321
+ sender = raw_data.get("fromUser", {})
322
+ sender_id = str(sender.get("id", "") or raw_data.get("fromUserId", ""))
323
+ else:
324
+ sender = raw_data.get("user", {})
325
+ sender_id = str(sender.get("id", ""))
326
+
327
+ return {
328
+ "sender": sender,
329
+ "sender_id": sender_id,
330
+ "nickname": sender.get("name", sender.get("username", "")),
331
+ "username": sender.get("username", ""),
332
+ }
333
+
334
+
335
+ def create_base_message(
336
+ raw_data: dict[str, Any],
337
+ sender_info: dict[str, Any],
338
+ client_self_id: str,
339
+ is_chat: bool = False,
340
+ room_id: str | None = None,
341
+ unique_session: bool = False,
342
+ ) -> AstrBotMessage:
343
+ """创建基础消息对象"""
344
+ message = AstrBotMessage()
345
+ message.raw_message = raw_data
346
+ message.message = []
347
+
348
+ message.sender = MessageMember(
349
+ user_id=sender_info["sender_id"],
350
+ nickname=sender_info["nickname"],
351
+ )
352
+
353
+ if room_id:
354
+ session_prefix = "room"
355
+ session_id = f"{session_prefix}%{room_id}"
356
+ if unique_session:
357
+ session_id += f"_{sender_info['sender_id']}"
358
+ message.type = MessageType.GROUP_MESSAGE
359
+ message.group_id = room_id
360
+ elif is_chat:
361
+ session_prefix = "chat"
362
+ session_id = f"{session_prefix}%{sender_info['sender_id']}"
363
+ message.type = MessageType.FRIEND_MESSAGE
364
+ else:
365
+ session_prefix = "note"
366
+ session_id = f"{session_prefix}%{sender_info['sender_id']}"
367
+ message.type = MessageType.OTHER_MESSAGE
368
+
369
+ message.session_id = (
370
+ session_id if sender_info["sender_id"] else f"{session_prefix}%unknown"
371
+ )
372
+ message.message_id = str(raw_data.get("id", ""))
373
+ message.self_id = client_self_id
374
+
375
+ return message
376
+
377
+
378
+ def process_at_mention(
379
+ message: AstrBotMessage,
380
+ raw_text: str,
381
+ bot_username: str,
382
+ client_self_id: str,
383
+ ) -> tuple[list[str], str]:
384
+ """处理@提及逻辑,返回消息部分列表和处理后的文本"""
385
+ message_parts = []
386
+
387
+ if not raw_text:
388
+ return message_parts, ""
389
+
390
+ if bot_username and raw_text.startswith(f"@{bot_username}"):
391
+ at_mention = f"@{bot_username}"
392
+ message.message.append(Comp.At(qq=client_self_id))
393
+ remaining_text = raw_text[len(at_mention) :].strip()
394
+ if remaining_text:
395
+ message.message.append(Comp.Plain(remaining_text))
396
+ message_parts.append(remaining_text)
397
+ return message_parts, remaining_text
398
+ message.message.append(Comp.Plain(raw_text))
399
+ message_parts.append(raw_text)
400
+ return message_parts, raw_text
401
+
402
+
403
+ def cache_user_info(
404
+ user_cache: dict[str, Any],
405
+ sender_info: dict[str, Any],
406
+ raw_data: dict[str, Any],
407
+ client_self_id: str,
408
+ is_chat: bool = False,
409
+ ):
410
+ """缓存用户信息"""
411
+ if is_chat:
412
+ user_cache_data = {
413
+ "username": sender_info["username"],
414
+ "nickname": sender_info["nickname"],
415
+ "visibility": "specified",
416
+ "visible_user_ids": [client_self_id, sender_info["sender_id"]],
417
+ }
418
+ else:
419
+ user_cache_data = {
420
+ "username": sender_info["username"],
421
+ "nickname": sender_info["nickname"],
422
+ "visibility": raw_data.get("visibility", "public"),
423
+ "visible_user_ids": raw_data.get("visibleUserIds", []),
424
+ # 保存原消息ID,用于回复时作为reply_id
425
+ "reply_to_note_id": raw_data.get("id"),
426
+ }
427
+
428
+ user_cache[sender_info["sender_id"]] = user_cache_data
429
+
430
+
431
+ def cache_room_info(
432
+ user_cache: dict[str, Any],
433
+ raw_data: dict[str, Any],
434
+ client_self_id: str,
435
+ ):
436
+ """缓存房间信息"""
437
+ room_data = raw_data.get("toRoom")
438
+ room_id = raw_data.get("toRoomId")
439
+
440
+ if room_data and room_id:
441
+ room_cache_key = f"room:{room_id}"
442
+ user_cache[room_cache_key] = {
443
+ "room_id": room_id,
444
+ "room_name": room_data.get("name", ""),
445
+ "room_description": room_data.get("description", ""),
446
+ "owner_id": room_data.get("ownerId", ""),
447
+ "visibility": "specified",
448
+ "visible_user_ids": [client_self_id],
449
+ }
450
+
451
+
452
+ async def resolve_component_url_or_path(
453
+ comp: Any,
454
+ ) -> tuple[str | None, str | None]:
455
+ """尝试从组件解析可上传的远程 URL 或本地路径。
456
+
457
+ 返回 (url_candidate, local_path)。两者可能都为 None。
458
+ 这个函数尽量不抛异常,调用方可按需处理 None。
459
+ """
460
+ url_candidate = None
461
+ local_path = None
462
+
463
+ async def _get_str_value(coro_or_val):
464
+ """辅助函数:统一处理协程或普通值"""
465
+ try:
466
+ if hasattr(coro_or_val, "__await__"):
467
+ result = await coro_or_val
468
+ else:
469
+ result = coro_or_val
470
+ return result if isinstance(result, str) else None
471
+ except Exception:
472
+ return None
473
+
474
+ try:
475
+ # 1. 尝试异步方法
476
+ for method in ["convert_to_file_path", "get_file", "register_to_file_service"]:
477
+ if not hasattr(comp, method):
478
+ continue
479
+ try:
480
+ value = await _get_str_value(getattr(comp, method)())
481
+ if value:
482
+ if value.startswith("http"):
483
+ url_candidate = value
484
+ break
485
+ local_path = value
486
+ except Exception:
487
+ continue
488
+
489
+ # 2. 尝试 get_file(True) 获取可直接访问的 URL
490
+ if not url_candidate and hasattr(comp, "get_file"):
491
+ try:
492
+ value = await _get_str_value(comp.get_file(True))
493
+ if value and value.startswith("http"):
494
+ url_candidate = value
495
+ except Exception:
496
+ pass
497
+
498
+ # 3. 回退到同步属性
499
+ if not url_candidate and not local_path:
500
+ for attr in ("file", "url", "path", "src", "source"):
501
+ try:
502
+ value = getattr(comp, attr, None)
503
+ if value and isinstance(value, str):
504
+ if value.startswith("http"):
505
+ url_candidate = value
506
+ break
507
+ local_path = value
508
+ break
509
+ except Exception:
510
+ continue
511
+
512
+ except Exception:
513
+ pass
514
+
515
+ return url_candidate, local_path
516
+
517
+
518
+ def summarize_component_for_log(comp: Any) -> dict[str, Any]:
519
+ """生成适合日志的组件属性字典(尽量不抛异常)。"""
520
+ attrs = {}
521
+ for a in ("file", "url", "path", "src", "source", "name"):
522
+ try:
523
+ v = getattr(comp, a, None)
524
+ if v is not None:
525
+ attrs[a] = v
526
+ except Exception:
527
+ continue
528
+ return attrs
529
+
530
+
531
+ async def upload_local_with_retries(
532
+ api: Any,
533
+ local_path: str,
534
+ preferred_name: str | None,
535
+ folder_id: str | None,
536
+ ) -> str | None:
537
+ """尝试本地上传,返回 file id 或 None。如果文件类型不允许则直接失败。"""
538
+ try:
539
+ res = await api.upload_file(local_path, preferred_name, folder_id)
540
+ if isinstance(res, dict):
541
+ fid = res.get("id") or (res.get("raw") or {}).get("createdFile", {}).get(
542
+ "id",
543
+ )
544
+ if fid:
545
+ return str(fid)
546
+ except Exception:
547
+ # 上传失败,直接返回 None,让上层处理错误
548
+ return None
549
+
550
+ return None