Undefined-bot 2.1.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 (211) hide show
  1. Undefined/__init__.py +3 -0
  2. Undefined/__main__.py +6 -0
  3. Undefined/ai.py +1215 -0
  4. Undefined/config.py +371 -0
  5. Undefined/end_summary_storage.py +48 -0
  6. Undefined/faq.py +244 -0
  7. Undefined/handlers.py +1247 -0
  8. Undefined/injection_response_agent.py +131 -0
  9. Undefined/main.py +126 -0
  10. Undefined/memory.py +120 -0
  11. Undefined/onebot.py +512 -0
  12. Undefined/rate_limit.py +130 -0
  13. Undefined/render.py +123 -0
  14. Undefined/scheduled_task_storage.py +88 -0
  15. Undefined/services/__init__.py +1 -0
  16. Undefined/services/queue_manager.py +206 -0
  17. Undefined/skills/README.md +53 -0
  18. Undefined/skills/__init__.py +10 -0
  19. Undefined/skills/agents/README.md +144 -0
  20. Undefined/skills/agents/__init__.py +116 -0
  21. Undefined/skills/agents/entertainment_agent/config.json +17 -0
  22. Undefined/skills/agents/entertainment_agent/handler.py +220 -0
  23. Undefined/skills/agents/entertainment_agent/intro.md +25 -0
  24. Undefined/skills/agents/entertainment_agent/prompt.md +20 -0
  25. Undefined/skills/agents/entertainment_agent/tools/__init__.py +1 -0
  26. Undefined/skills/agents/entertainment_agent/tools/ai_draw_one/config.json +34 -0
  27. Undefined/skills/agents/entertainment_agent/tools/ai_draw_one/handler.py +62 -0
  28. Undefined/skills/agents/entertainment_agent/tools/ai_study_helper/config.json +22 -0
  29. Undefined/skills/agents/entertainment_agent/tools/ai_study_helper/handler.py +35 -0
  30. Undefined/skills/agents/entertainment_agent/tools/get_current_time/config.json +12 -0
  31. Undefined/skills/agents/entertainment_agent/tools/get_current_time/handler.py +5 -0
  32. Undefined/skills/agents/entertainment_agent/tools/horoscope/config.json +24 -0
  33. Undefined/skills/agents/entertainment_agent/tools/horoscope/handler.py +141 -0
  34. Undefined/skills/agents/entertainment_agent/tools/minecraft_skin/config.json +43 -0
  35. Undefined/skills/agents/entertainment_agent/tools/minecraft_skin/handler.py +55 -0
  36. Undefined/skills/agents/entertainment_agent/tools/novel_search/config.json +25 -0
  37. Undefined/skills/agents/entertainment_agent/tools/novel_search/handler.py +31 -0
  38. Undefined/skills/agents/entertainment_agent/tools/renjian/config.json +12 -0
  39. Undefined/skills/agents/entertainment_agent/tools/renjian/handler.py +30 -0
  40. Undefined/skills/agents/entertainment_agent/tools/wenchang_dijun/config.json +12 -0
  41. Undefined/skills/agents/entertainment_agent/tools/wenchang_dijun/handler.py +44 -0
  42. Undefined/skills/agents/file_analysis_agent/__init__.py +1 -0
  43. Undefined/skills/agents/file_analysis_agent/config.json +21 -0
  44. Undefined/skills/agents/file_analysis_agent/handler.py +248 -0
  45. Undefined/skills/agents/file_analysis_agent/intro.md +22 -0
  46. Undefined/skills/agents/file_analysis_agent/prompt.md +36 -0
  47. Undefined/skills/agents/file_analysis_agent/tools/__init__.py +1 -0
  48. Undefined/skills/agents/file_analysis_agent/tools/analyze_code/config.json +17 -0
  49. Undefined/skills/agents/file_analysis_agent/tools/analyze_code/handler.py +427 -0
  50. Undefined/skills/agents/file_analysis_agent/tools/analyze_multimodal/config.json +25 -0
  51. Undefined/skills/agents/file_analysis_agent/tools/analyze_multimodal/handler.py +178 -0
  52. Undefined/skills/agents/file_analysis_agent/tools/cleanup_temp/config.json +16 -0
  53. Undefined/skills/agents/file_analysis_agent/tools/cleanup_temp/handler.py +35 -0
  54. Undefined/skills/agents/file_analysis_agent/tools/detect_file_type/config.json +17 -0
  55. Undefined/skills/agents/file_analysis_agent/tools/detect_file_type/handler.py +221 -0
  56. Undefined/skills/agents/file_analysis_agent/tools/download_file/config.json +21 -0
  57. Undefined/skills/agents/file_analysis_agent/tools/download_file/handler.py +124 -0
  58. Undefined/skills/agents/file_analysis_agent/tools/extract_archive/config.json +25 -0
  59. Undefined/skills/agents/file_analysis_agent/tools/extract_archive/handler.py +190 -0
  60. Undefined/skills/agents/file_analysis_agent/tools/extract_docx/config.json +17 -0
  61. Undefined/skills/agents/file_analysis_agent/tools/extract_docx/handler.py +78 -0
  62. Undefined/skills/agents/file_analysis_agent/tools/extract_pdf/config.json +21 -0
  63. Undefined/skills/agents/file_analysis_agent/tools/extract_pdf/handler.py +67 -0
  64. Undefined/skills/agents/file_analysis_agent/tools/extract_pptx/config.json +17 -0
  65. Undefined/skills/agents/file_analysis_agent/tools/extract_pptx/handler.py +73 -0
  66. Undefined/skills/agents/file_analysis_agent/tools/extract_xlsx/config.json +17 -0
  67. Undefined/skills/agents/file_analysis_agent/tools/extract_xlsx/handler.py +101 -0
  68. Undefined/skills/agents/file_analysis_agent/tools/get_current_time/config.json +12 -0
  69. Undefined/skills/agents/file_analysis_agent/tools/get_current_time/handler.py +5 -0
  70. Undefined/skills/agents/file_analysis_agent/tools/read_text_file/config.json +21 -0
  71. Undefined/skills/agents/file_analysis_agent/tools/read_text_file/handler.py +90 -0
  72. Undefined/skills/agents/info_agent/config.json +17 -0
  73. Undefined/skills/agents/info_agent/handler.py +220 -0
  74. Undefined/skills/agents/info_agent/intro.md +22 -0
  75. Undefined/skills/agents/info_agent/prompt.md +27 -0
  76. Undefined/skills/agents/info_agent/tools/__init__.py +1 -0
  77. Undefined/skills/agents/info_agent/tools/baiduhot/config.json +18 -0
  78. Undefined/skills/agents/info_agent/tools/baiduhot/handler.py +49 -0
  79. Undefined/skills/agents/info_agent/tools/base64/config.json +22 -0
  80. Undefined/skills/agents/info_agent/tools/base64/handler.py +44 -0
  81. Undefined/skills/agents/info_agent/tools/douyinhot/config.json +18 -0
  82. Undefined/skills/agents/info_agent/tools/douyinhot/handler.py +53 -0
  83. Undefined/skills/agents/info_agent/tools/get_current_time/config.json +12 -0
  84. Undefined/skills/agents/info_agent/tools/get_current_time/handler.py +5 -0
  85. Undefined/skills/agents/info_agent/tools/gold_price/config.json +12 -0
  86. Undefined/skills/agents/info_agent/tools/gold_price/handler.py +58 -0
  87. Undefined/skills/agents/info_agent/tools/hash/config.json +22 -0
  88. Undefined/skills/agents/info_agent/tools/hash/handler.py +43 -0
  89. Undefined/skills/agents/info_agent/tools/history/config.json +12 -0
  90. Undefined/skills/agents/info_agent/tools/history/handler.py +37 -0
  91. Undefined/skills/agents/info_agent/tools/net_check/config.json +17 -0
  92. Undefined/skills/agents/info_agent/tools/net_check/handler.py +117 -0
  93. Undefined/skills/agents/info_agent/tools/news_tencent/config.json +17 -0
  94. Undefined/skills/agents/info_agent/tools/news_tencent/handler.py +38 -0
  95. Undefined/skills/agents/info_agent/tools/qq_level_query/config.json +29 -0
  96. Undefined/skills/agents/info_agent/tools/qq_level_query/handler.py +48 -0
  97. Undefined/skills/agents/info_agent/tools/speed/config.json +17 -0
  98. Undefined/skills/agents/info_agent/tools/speed/handler.py +37 -0
  99. Undefined/skills/agents/info_agent/tools/tcping/config.json +21 -0
  100. Undefined/skills/agents/info_agent/tools/tcping/handler.py +53 -0
  101. Undefined/skills/agents/info_agent/tools/weather_query/config.json +22 -0
  102. Undefined/skills/agents/info_agent/tools/weather_query/handler.py +207 -0
  103. Undefined/skills/agents/info_agent/tools/weibohot/config.json +18 -0
  104. Undefined/skills/agents/info_agent/tools/weibohot/handler.py +49 -0
  105. Undefined/skills/agents/info_agent/tools/whois/config.json +17 -0
  106. Undefined/skills/agents/info_agent/tools/whois/handler.py +63 -0
  107. Undefined/skills/agents/naga_code_analysis_agent/config.json +17 -0
  108. Undefined/skills/agents/naga_code_analysis_agent/handler.py +222 -0
  109. Undefined/skills/agents/naga_code_analysis_agent/intro.md +17 -0
  110. Undefined/skills/agents/naga_code_analysis_agent/prompt.md +19 -0
  111. Undefined/skills/agents/naga_code_analysis_agent/tools/__init__.py +1 -0
  112. Undefined/skills/agents/naga_code_analysis_agent/tools/get_current_time/config.json +12 -0
  113. Undefined/skills/agents/naga_code_analysis_agent/tools/get_current_time/handler.py +5 -0
  114. Undefined/skills/agents/naga_code_analysis_agent/tools/glob/config.json +17 -0
  115. Undefined/skills/agents/naga_code_analysis_agent/tools/glob/handler.py +37 -0
  116. Undefined/skills/agents/naga_code_analysis_agent/tools/list_directory/config.json +17 -0
  117. Undefined/skills/agents/naga_code_analysis_agent/tools/list_directory/handler.py +31 -0
  118. Undefined/skills/agents/naga_code_analysis_agent/tools/read_file/config.json +17 -0
  119. Undefined/skills/agents/naga_code_analysis_agent/tools/read_file/handler.py +66 -0
  120. Undefined/skills/agents/naga_code_analysis_agent/tools/read_naga_intro/config.json +12 -0
  121. Undefined/skills/agents/naga_code_analysis_agent/tools/read_naga_intro/handler.py +327 -0
  122. Undefined/skills/agents/naga_code_analysis_agent/tools/search_file_content/config.json +25 -0
  123. Undefined/skills/agents/naga_code_analysis_agent/tools/search_file_content/handler.py +46 -0
  124. Undefined/skills/agents/scheduler_agent/__init__.py +1 -0
  125. Undefined/skills/agents/scheduler_agent/config.json +17 -0
  126. Undefined/skills/agents/scheduler_agent/handler.py +218 -0
  127. Undefined/skills/agents/scheduler_agent/intro.md +17 -0
  128. Undefined/skills/agents/scheduler_agent/prompt.md +67 -0
  129. Undefined/skills/agents/scheduler_agent/tools/__init__.py +1 -0
  130. Undefined/skills/agents/scheduler_agent/tools/create_schedule_task/config.json +37 -0
  131. Undefined/skills/agents/scheduler_agent/tools/create_schedule_task/handler.py +68 -0
  132. Undefined/skills/agents/scheduler_agent/tools/delete_schedule_task/config.json +19 -0
  133. Undefined/skills/agents/scheduler_agent/tools/delete_schedule_task/handler.py +26 -0
  134. Undefined/skills/agents/scheduler_agent/tools/get_current_time/config.json +12 -0
  135. Undefined/skills/agents/scheduler_agent/tools/get_current_time/handler.py +5 -0
  136. Undefined/skills/agents/scheduler_agent/tools/list_schedule_tasks/config.json +11 -0
  137. Undefined/skills/agents/scheduler_agent/tools/list_schedule_tasks/handler.py +47 -0
  138. Undefined/skills/agents/scheduler_agent/tools/update_schedule_task/config.json +39 -0
  139. Undefined/skills/agents/scheduler_agent/tools/update_schedule_task/handler.py +46 -0
  140. Undefined/skills/agents/social_agent/config.json +17 -0
  141. Undefined/skills/agents/social_agent/handler.py +220 -0
  142. Undefined/skills/agents/social_agent/intro.md +17 -0
  143. Undefined/skills/agents/social_agent/prompt.md +19 -0
  144. Undefined/skills/agents/social_agent/tools/__init__.py +1 -0
  145. Undefined/skills/agents/social_agent/tools/bilibili_search/config.json +21 -0
  146. Undefined/skills/agents/social_agent/tools/bilibili_search/handler.py +68 -0
  147. Undefined/skills/agents/social_agent/tools/bilibili_user_info/config.json +17 -0
  148. Undefined/skills/agents/social_agent/tools/bilibili_user_info/handler.py +68 -0
  149. Undefined/skills/agents/social_agent/tools/get_current_time/config.json +12 -0
  150. Undefined/skills/agents/social_agent/tools/get_current_time/handler.py +5 -0
  151. Undefined/skills/agents/social_agent/tools/music_global_search/config.json +21 -0
  152. Undefined/skills/agents/social_agent/tools/music_global_search/handler.py +47 -0
  153. Undefined/skills/agents/social_agent/tools/music_info_get/config.json +22 -0
  154. Undefined/skills/agents/social_agent/tools/music_info_get/handler.py +35 -0
  155. Undefined/skills/agents/social_agent/tools/music_lyrics/config.json +22 -0
  156. Undefined/skills/agents/social_agent/tools/music_lyrics/handler.py +26 -0
  157. Undefined/skills/agents/social_agent/tools/video_random_recommend/config.json +21 -0
  158. Undefined/skills/agents/social_agent/tools/video_random_recommend/handler.py +21 -0
  159. Undefined/skills/agents/web_agent/config.json +17 -0
  160. Undefined/skills/agents/web_agent/handler.py +221 -0
  161. Undefined/skills/agents/web_agent/intro.md +14 -0
  162. Undefined/skills/agents/web_agent/prompt.md +16 -0
  163. Undefined/skills/agents/web_agent/tools/__init__.py +1 -0
  164. Undefined/skills/agents/web_agent/tools/crawl_webpage/config.json +21 -0
  165. Undefined/skills/agents/web_agent/tools/crawl_webpage/handler.py +102 -0
  166. Undefined/skills/agents/web_agent/tools/get_current_time/config.json +12 -0
  167. Undefined/skills/agents/web_agent/tools/get_current_time/handler.py +5 -0
  168. Undefined/skills/agents/web_agent/tools/web_search/config.json +21 -0
  169. Undefined/skills/agents/web_agent/tools/web_search/handler.py +29 -0
  170. Undefined/skills/tools/README.md +85 -0
  171. Undefined/skills/tools/__init__.py +120 -0
  172. Undefined/skills/tools/debug/config.json +17 -0
  173. Undefined/skills/tools/debug/handler.py +35 -0
  174. Undefined/skills/tools/end/config.json +17 -0
  175. Undefined/skills/tools/end/handler.py +24 -0
  176. Undefined/skills/tools/get_current_time/config.json +12 -0
  177. Undefined/skills/tools/get_current_time/handler.py +5 -0
  178. Undefined/skills/tools/get_forward_msg/config.json +17 -0
  179. Undefined/skills/tools/get_forward_msg/handler.py +131 -0
  180. Undefined/skills/tools/get_group_member_info/config.json +38 -0
  181. Undefined/skills/tools/get_group_member_info/handler.py +142 -0
  182. Undefined/skills/tools/get_messages_by_time/config.json +30 -0
  183. Undefined/skills/tools/get_messages_by_time/handler.py +128 -0
  184. Undefined/skills/tools/get_picture/config.json +45 -0
  185. Undefined/skills/tools/get_picture/handler.py +191 -0
  186. Undefined/skills/tools/get_recent_messages/config.json +30 -0
  187. Undefined/skills/tools/get_recent_messages/handler.py +88 -0
  188. Undefined/skills/tools/qq_like/config.json +22 -0
  189. Undefined/skills/tools/qq_like/handler.py +58 -0
  190. Undefined/skills/tools/render_html/config.json +26 -0
  191. Undefined/skills/tools/render_html/handler.py +39 -0
  192. Undefined/skills/tools/render_latex/config.json +26 -0
  193. Undefined/skills/tools/render_latex/handler.py +78 -0
  194. Undefined/skills/tools/render_markdown/config.json +26 -0
  195. Undefined/skills/tools/render_markdown/handler.py +63 -0
  196. Undefined/skills/tools/save_memory/config.json +17 -0
  197. Undefined/skills/tools/save_memory/handler.py +17 -0
  198. Undefined/skills/tools/send_message/config.json +21 -0
  199. Undefined/skills/tools/send_message/handler.py +60 -0
  200. Undefined/skills/tools/send_private_message/config.json +21 -0
  201. Undefined/skills/tools/send_private_message/handler.py +35 -0
  202. Undefined/utils/__init__.py +0 -0
  203. Undefined/utils/common.py +186 -0
  204. Undefined/utils/history.py +284 -0
  205. Undefined/utils/scheduler.py +286 -0
  206. Undefined/utils/sender.py +140 -0
  207. undefined_bot-2.1.0.dist-info/METADATA +259 -0
  208. undefined_bot-2.1.0.dist-info/RECORD +211 -0
  209. undefined_bot-2.1.0.dist-info/WHEEL +4 -0
  210. undefined_bot-2.1.0.dist-info/entry_points.txt +2 -0
  211. undefined_bot-2.1.0.dist-info/licenses/LICENSE +7 -0
Undefined/onebot.py ADDED
@@ -0,0 +1,512 @@
1
+ """OneBot WebSocket 客户端"""
2
+
3
+ import asyncio
4
+ import json
5
+ import logging
6
+ import time
7
+ from typing import Any, Callable, Coroutine
8
+ from datetime import datetime
9
+
10
+ import websockets
11
+ from websockets.asyncio.client import ClientConnection
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class OneBotClient:
17
+ """OneBot v11 WebSocket 客户端"""
18
+
19
+ def __init__(self, ws_url: str, token: str = ""):
20
+ self.ws_url = ws_url
21
+ self.token = token
22
+ self.ws: ClientConnection | None = None
23
+ self._message_id = 0
24
+ self._pending_responses: dict[str, asyncio.Future[dict[str, Any]]] = {}
25
+ self._message_handler: (
26
+ Callable[[dict[str, Any]], Coroutine[Any, Any, None]] | None
27
+ ) = None
28
+ self._running = False
29
+
30
+ def set_message_handler(
31
+ self, handler: Callable[[dict[str, Any]], Coroutine[Any, Any, None]]
32
+ ) -> None:
33
+ """设置消息处理器"""
34
+ self._message_handler = handler
35
+
36
+ async def connect(self) -> None:
37
+ """连接到 OneBot WebSocket"""
38
+ # 构建带 token 的 URL
39
+ url = self.ws_url
40
+ if self.token:
41
+ separator = "&" if "?" in url else "?"
42
+ url = f"{url}{separator}access_token={self.token}"
43
+
44
+ logger.info(f"[WebSocket] 正在连接到 {self.ws_url}...")
45
+
46
+ # 同时在请求头中传递 token(兼容不同实现)
47
+ extra_headers = {}
48
+ if self.token:
49
+ extra_headers["Authorization"] = f"Bearer {self.token}"
50
+
51
+ try:
52
+ self.ws = await websockets.connect(
53
+ url,
54
+ ping_interval=20,
55
+ ping_timeout=20,
56
+ additional_headers=extra_headers if extra_headers else None,
57
+ )
58
+ logger.info("[WebSocket] 连接成功")
59
+ except Exception as e:
60
+ logger.error(f"[WebSocket] 连接失败: {e}")
61
+ raise
62
+
63
+ async def disconnect(self) -> None:
64
+ """断开连接"""
65
+ self._running = False
66
+ if self.ws:
67
+ logger.info("[WebSocket] 正在主动断开连接...")
68
+ await self.ws.close()
69
+ self.ws = None
70
+ logger.info("[WebSocket] 连接已断开")
71
+
72
+ async def _call_api(
73
+ self, action: str, params: dict[str, Any] | None = None
74
+ ) -> dict[str, Any]:
75
+ """调用 OneBot API"""
76
+ if not self.ws:
77
+ raise RuntimeError("WebSocket 未连接")
78
+
79
+ self._message_id += 1
80
+ echo = str(self._message_id) # 使用字符串类型
81
+
82
+ request = {
83
+ "action": action,
84
+ "params": params or {},
85
+ "echo": echo,
86
+ }
87
+
88
+ logger.debug(f"[API请求] {action} (ID={echo}) | 参数: {params}")
89
+
90
+ # 创建 Future 等待响应
91
+ future: asyncio.Future[dict[str, Any]] = asyncio.Future()
92
+ self._pending_responses[echo] = future
93
+
94
+ start_time = time.perf_counter()
95
+
96
+ try:
97
+ await self.ws.send(json.dumps(request))
98
+ # 等待响应,超时 30 秒
99
+ response = await asyncio.wait_for(future, timeout=30.0)
100
+ duration = time.perf_counter() - start_time
101
+
102
+ # 检查响应状态
103
+ status = response.get("status")
104
+ if status == "failed":
105
+ retcode = response.get("retcode", -1)
106
+ msg = response.get("message", "未知错误")
107
+ logger.error(
108
+ f"[API失败] {action} (ID={echo}) | 耗时={duration:.2f}s | retcode={retcode} | message={msg}"
109
+ )
110
+ raise RuntimeError(f"API 调用失败: {msg} (retcode={retcode})")
111
+
112
+ logger.info(f"[API成功] {action} (ID={echo}) | 耗时={duration:.2f}s")
113
+ return response
114
+ except asyncio.TimeoutError:
115
+ duration = time.perf_counter() - start_time
116
+ logger.error(f"[API超时] {action} (ID={echo}) | 耗时={duration:.2f}s")
117
+ raise
118
+ finally:
119
+ self._pending_responses.pop(echo, None)
120
+
121
+ async def send_group_message(
122
+ self, group_id: int, message: str | list[dict[str, Any]]
123
+ ) -> dict[str, Any]:
124
+ """发送群消息"""
125
+ return await self._call_api(
126
+ "send_group_msg",
127
+ {
128
+ "group_id": group_id,
129
+ "message": message,
130
+ },
131
+ )
132
+
133
+ async def send_private_message(
134
+ self, user_id: int, message: str | list[dict[str, Any]]
135
+ ) -> dict[str, Any]:
136
+ """发送私聊消息"""
137
+ return await self._call_api(
138
+ "send_private_msg",
139
+ {
140
+ "user_id": user_id,
141
+ "message": message,
142
+ },
143
+ )
144
+
145
+ async def get_group_msg_history(
146
+ self,
147
+ group_id: int,
148
+ message_seq: int | None = None,
149
+ count: int = 500,
150
+ ) -> list[dict[str, Any]]:
151
+ """获取群消息历史
152
+
153
+ 参数:
154
+ group_id: 群号
155
+ message_seq: 起始消息序号,None 表示从最新消息开始
156
+ count: 获取的消息数量
157
+
158
+ 返回:
159
+ 消息列表
160
+ """
161
+ params: dict[str, Any] = {
162
+ "group_id": group_id,
163
+ "count": count,
164
+ }
165
+ if message_seq is not None:
166
+ params["message_seq"] = message_seq
167
+
168
+ result = await self._call_api("get_group_msg_history", params)
169
+
170
+ # 安全获取消息列表
171
+ if result is None:
172
+ logger.warning("get_group_msg_history 返回 None")
173
+ return []
174
+
175
+ data = result.get("data")
176
+ if data is None:
177
+ logger.warning(f"get_group_msg_history 响应无 data 字段: {result}")
178
+ return []
179
+
180
+ messages: list[dict[str, Any]] = data.get("messages", [])
181
+ logger.debug(f"获取到 {len(messages)} 条历史消息")
182
+ return messages
183
+
184
+ async def get_image(self, file: str) -> str:
185
+ """获取图片信息
186
+
187
+ 参数:
188
+ file: 图片文件名或 URL
189
+
190
+ 返回:
191
+ 图片的本地路径或 URL
192
+ """
193
+ result = await self._call_api("get_image", {"file": file})
194
+ data: dict[str, str] = result.get("data", {})
195
+ url: str = data.get("url", "") or data.get("file", "")
196
+ return url
197
+
198
+ async def get_group_info(self, group_id: int) -> dict[str, Any] | None:
199
+ """获取群信息
200
+
201
+ 参数:
202
+ group_id: 群号
203
+
204
+ 返回:
205
+ 群信息字典,包含 group_name 等字段
206
+ """
207
+ try:
208
+ result = await self._call_api("get_group_info", {"group_id": group_id})
209
+ data: dict[str, Any] = result.get("data", {})
210
+ return data
211
+ except Exception as e:
212
+ logger.error(f"获取群信息失败: {e}")
213
+ return None
214
+
215
+ async def get_stranger_info(self, user_id: int) -> dict[str, Any] | None:
216
+ """获取陌生人信息
217
+
218
+ 参数:
219
+ user_id: 用户QQ号
220
+
221
+ 返回:
222
+ 用户信息字典,包含 nickname 等字段
223
+ """
224
+ try:
225
+ result = await self._call_api("get_stranger_info", {"user_id": user_id})
226
+ data: dict[str, Any] = result.get("data", {})
227
+ return data
228
+ except Exception as e:
229
+ logger.error(f"获取陌生人信息失败: {e}")
230
+ return None
231
+
232
+ async def get_group_member_info(
233
+ self, group_id: int, user_id: int, no_cache: bool = False
234
+ ) -> dict[str, Any] | None:
235
+ """获取群成员信息
236
+
237
+ 参数:
238
+ group_id: 群号
239
+ user_id: 群成员QQ号
240
+ no_cache: 是否不使用缓存(默认 false)
241
+
242
+ 返回:
243
+ 群成员信息字典,包含群昵称、QQ昵称、加群时间、等级、最后发言时间等字段
244
+ """
245
+ try:
246
+ result = await self._call_api(
247
+ "get_group_member_info",
248
+ {"group_id": group_id, "user_id": user_id, "no_cache": no_cache},
249
+ )
250
+ data: dict[str, Any] = result.get("data", {})
251
+ return data
252
+ except Exception as e:
253
+ logger.error(f"获取群成员信息失败: {e}")
254
+ return None
255
+
256
+ async def get_group_member_list(self, group_id: int) -> list[dict[str, Any]]:
257
+ """获取群成员列表
258
+
259
+ 参数:
260
+ group_id: 群号
261
+
262
+ 返回:
263
+ 群成员信息列表
264
+ """
265
+ try:
266
+ result = await self._call_api(
267
+ "get_group_member_list", {"group_id": group_id}
268
+ )
269
+ data: list[dict[str, Any]] = result.get("data", [])
270
+ return data
271
+ except Exception as e:
272
+ logger.error(f"获取群成员列表失败: {e}")
273
+ return []
274
+
275
+ async def get_forward_msg(self, id: str) -> list[dict[str, Any]]:
276
+ """获取合并转发消息详情
277
+
278
+ 参数:
279
+ id: 合并转发 ID
280
+
281
+ 返回:
282
+ 消息节点列表
283
+ """
284
+ try:
285
+ result = await self._call_api("get_forward_msg", {"message_id": id})
286
+ data = result.get("data", {})
287
+ # data 可能是字典(包含 messages)或列表(直接是 nodes)
288
+ if isinstance(data, dict):
289
+ messages: list[dict[str, Any]] = data.get("messages", [])
290
+ return messages
291
+ elif isinstance(data, list):
292
+ nodes: list[dict[str, Any]] = data
293
+ return nodes
294
+ return []
295
+ except Exception as e:
296
+ logger.error(f"获取合并转发消息失败: {e}")
297
+ return []
298
+
299
+ async def get_msg(self, message_id: int) -> dict[str, Any] | None:
300
+ """获取单条消息详情
301
+
302
+ 参数:
303
+ message_id: 消息 ID
304
+
305
+ 返回:
306
+ 消息详情字典
307
+ """
308
+ try:
309
+ result = await self._call_api("get_msg", {"message_id": message_id})
310
+ return result.get("data")
311
+ except Exception as e:
312
+ logger.error(f"获取消息详情失败: {e}")
313
+ return None
314
+
315
+ async def send_forward_msg(
316
+ self, group_id: int, messages: list[dict[str, Any]]
317
+ ) -> dict[str, Any]:
318
+ """发送合并转发消息到群聊
319
+
320
+ 参数:
321
+ group_id: 群号
322
+ messages: 消息节点列表,每个节点格式为:
323
+ {
324
+ "type": "node",
325
+ "data": {
326
+ "name": "发送者昵称",
327
+ "uin": "发送者QQ号",
328
+ "content": "消息内容(字符串或消息段数组)",
329
+ "time": "时间戳(可选)"
330
+ }
331
+ }
332
+
333
+ 返回:
334
+ API 响应
335
+ """
336
+ return await self._call_api(
337
+ "send_forward_msg", {"group_id": group_id, "messages": messages}
338
+ )
339
+
340
+ async def send_like(self, user_id: int, times: int = 1) -> dict[str, Any]:
341
+ """给用户点赞
342
+
343
+ 参数:
344
+ user_id: 对方 QQ 号
345
+ times: 赞的次数(默认1次)
346
+
347
+ 返回:
348
+ API 响应
349
+ """
350
+ return await self._call_api("send_like", {"user_id": user_id, "times": times})
351
+
352
+ async def run(self) -> None:
353
+ """运行消息接收循环"""
354
+ if not self.ws:
355
+ raise RuntimeError("WebSocket 未连接")
356
+
357
+ self._running = True
358
+ self._tasks: set[asyncio.Task[None]] = set()
359
+ logger.info("[WebSocket] 消息接收循环已启动")
360
+
361
+ try:
362
+ while self._running:
363
+ raw_message = ""
364
+ try:
365
+ message_data = await self.ws.recv()
366
+ raw_message = (
367
+ message_data.decode("utf-8")
368
+ if isinstance(message_data, bytes)
369
+ else message_data
370
+ )
371
+ data = json.loads(raw_message)
372
+ # 处理消息(不阻塞接收循环)
373
+ await self._dispatch_message(data)
374
+ except json.JSONDecodeError as e:
375
+ logger.error(
376
+ f"[WebSocket] 无法解析 JSON 消息: {raw_message!r}, 错误: {e}"
377
+ )
378
+ except websockets.ConnectionClosed:
379
+ logger.warning("[WebSocket] 连接已关闭,接收循环结束")
380
+ break
381
+ except Exception as e:
382
+ logger.exception(f"[WebSocket] 接收消息时发生异常: {e}")
383
+ finally:
384
+ self._running = False
385
+ # 等待所有后台任务完成
386
+ if self._tasks:
387
+ logger.debug(
388
+ f"[WebSocket] 正在等待 {len(self._tasks)} 个异步任务完成..."
389
+ )
390
+ await asyncio.gather(*self._tasks, return_exceptions=True)
391
+ logger.info("[WebSocket] 接收循环已停止")
392
+
393
+ async def _dispatch_message(self, data: dict[str, Any]) -> None:
394
+ """分发消息(API响应同步处理,事件异步处理)"""
395
+ # 检查是否是 API 响应(需要立即处理)
396
+ echo = data.get("echo")
397
+ if echo is not None:
398
+ echo_str = str(echo)
399
+ if echo_str in self._pending_responses:
400
+ logger.debug(f"收到 API 响应: echo={echo_str}")
401
+ self._pending_responses[echo_str].set_result(data)
402
+ return
403
+ else:
404
+ logger.debug(
405
+ f"收到未知 echo 响应: {echo_str}, 待处理: {list(self._pending_responses.keys())}"
406
+ )
407
+ return
408
+
409
+ # 事件类型的消息异步处理,不阻塞接收循环
410
+ post_type = data.get("post_type")
411
+ if post_type == "message":
412
+ msg_type = data.get("message_type", "unknown")
413
+ sender = data.get("sender", {}).get("user_id", "unknown")
414
+ logger.info(f"收到消息: type={msg_type}, sender={sender}")
415
+ if self._message_handler:
416
+ # 创建后台任务处理消息
417
+ task = asyncio.create_task(self._safe_handle_message(data))
418
+ self._tasks.add(task)
419
+ task.add_done_callback(self._tasks.discard)
420
+ elif post_type == "notice":
421
+ notice_type = data.get("notice_type", "")
422
+ sub_type = data.get("sub_type", "")
423
+ # 处理拍一拍事件
424
+ if notice_type == "notify" and sub_type == "poke":
425
+ target_id = data.get("target_id", 0)
426
+ sender_id = data.get("user_id", 0)
427
+ group_id = data.get("group_id", 0)
428
+ logger.info(
429
+ f"收到拍一拍: sender={sender_id}, target={target_id}, group={group_id}"
430
+ )
431
+ if self._message_handler:
432
+ # 将 poke 事件转换为类似消息的格式,方便 handler 处理
433
+ poke_event = {
434
+ "post_type": "notice",
435
+ "notice_type": "poke",
436
+ "group_id": group_id,
437
+ "user_id": sender_id,
438
+ "sender": {"user_id": sender_id},
439
+ "target_id": target_id,
440
+ "message": [], # 空消息
441
+ }
442
+ task = asyncio.create_task(self._safe_handle_message(poke_event))
443
+ self._tasks.add(task)
444
+ task.add_done_callback(self._tasks.discard)
445
+ else:
446
+ logger.debug(
447
+ f"收到通知事件: notice_type={notice_type}, sub_type={sub_type}"
448
+ )
449
+ elif post_type:
450
+ logger.debug(
451
+ f"收到事件: post_type={post_type}, meta={data.get('meta_event_type', '')}"
452
+ )
453
+
454
+ async def _safe_handle_message(self, data: dict[str, Any]) -> None:
455
+ """安全地处理消息(捕获异常)"""
456
+ try:
457
+ if self._message_handler:
458
+ await self._message_handler(data)
459
+ except Exception as e:
460
+ logger.exception(f"处理消息时出错: {e}")
461
+
462
+ async def run_with_reconnect(self, reconnect_interval: float = 5.0) -> None:
463
+ """带自动重连的运行"""
464
+ self._should_stop = False
465
+ reconnect_count = 0
466
+
467
+ while not self._should_stop:
468
+ try:
469
+ if reconnect_count > 0:
470
+ logger.info(f"[WebSocket] 正在尝试第 {reconnect_count} 次重连...")
471
+ await self.connect()
472
+ reconnect_count = 0 # 连接成功重置计数
473
+ await self.run()
474
+ except websockets.ConnectionClosed as e:
475
+ logger.warning(f"[WebSocket] 连接已断开: {e}")
476
+ except Exception as e:
477
+ logger.error(f"[WebSocket] 发生错误: {e}")
478
+
479
+ if self._should_stop:
480
+ break
481
+
482
+ reconnect_count += 1
483
+ logger.info(f"{reconnect_interval} 秒后尝试重连...")
484
+ await asyncio.sleep(reconnect_interval)
485
+
486
+ def stop(self) -> None:
487
+ """停止运行"""
488
+ self._should_stop = True
489
+ self._running = False
490
+
491
+
492
+ def parse_message_time(message: dict[str, Any]) -> datetime:
493
+ """解析消息时间"""
494
+ timestamp = message.get("time", 0)
495
+ return datetime.fromtimestamp(timestamp)
496
+
497
+
498
+ def get_message_sender_id(message: dict[str, Any]) -> int:
499
+ """获取消息发送者 QQ 号"""
500
+ sender: dict[str, Any] = message.get("sender", {})
501
+ user_id: int = sender.get("user_id", 0)
502
+ return user_id
503
+
504
+
505
+ def get_message_content(message: dict[str, Any]) -> list[dict[str, Any]]:
506
+ """获取消息内容(CQ 码数组格式)"""
507
+ msg = message.get("message", [])
508
+ if isinstance(msg, str):
509
+ # 如果是字符串格式,转换为数组格式
510
+ return [{"type": "text", "data": {"text": msg}}]
511
+ content: list[dict[str, Any]] = msg
512
+ return content
@@ -0,0 +1,130 @@
1
+ """速率限制模块"""
2
+
3
+ import time
4
+ from typing import TYPE_CHECKING
5
+
6
+ if TYPE_CHECKING:
7
+ from .config import Config
8
+
9
+
10
+ class RateLimiter:
11
+ """速率限制器
12
+
13
+ 规则:
14
+ - 超级管理员:无限制
15
+ - 管理员:5秒/次
16
+ - 普通用户:10秒/次
17
+ - /ask 命令:1分钟/次(所有人)
18
+ """
19
+
20
+ # 冷却时间(秒)
21
+ ADMIN_COOLDOWN = 5
22
+ USER_COOLDOWN = 10
23
+ ASK_COOLDOWN = 60 # /ask 命令:1分钟
24
+
25
+ def __init__(self, config: "Config") -> None:
26
+ self.config = config
27
+ # 记录每个用户最后一次调用时间
28
+ # 格式: {user_id: last_call_time}
29
+ self._last_calls: dict[int, float] = {}
30
+ # /ask 命令单独记录
31
+ self._last_ask_calls: dict[int, float] = {}
32
+
33
+ def check(self, user_id: int) -> tuple[bool, int]:
34
+ """检查用户是否可以执行命令
35
+
36
+ 参数:
37
+ user_id: 用户 QQ 号
38
+
39
+ 返回:
40
+ (是否允许, 剩余等待秒数)
41
+ """
42
+ # 超级管理员无限制
43
+ if self.config.is_superadmin(user_id):
44
+ return (True, 0)
45
+
46
+ now = time.time()
47
+ last_call = self._last_calls.get(user_id, 0)
48
+
49
+ # 确定冷却时间
50
+ if self.config.is_admin(user_id):
51
+ cooldown = self.ADMIN_COOLDOWN
52
+ else:
53
+ cooldown = self.USER_COOLDOWN
54
+
55
+ elapsed = now - last_call
56
+
57
+ if elapsed >= cooldown:
58
+ return (True, 0)
59
+ else:
60
+ remaining = int(cooldown - elapsed) + 1
61
+ return (False, remaining)
62
+
63
+ def check_ask(self, user_id: int) -> tuple[bool, int]:
64
+ """检查用户是否可以使用 /ask 命令
65
+
66
+ 参数:
67
+ user_id: 用户 QQ 号
68
+
69
+ 返回:
70
+ (是否允许, 剩余等待秒数)
71
+ """
72
+ # 超级管理员无限制
73
+ if self.config.is_superadmin(user_id):
74
+ return (True, 0)
75
+
76
+ now = time.time()
77
+ last_call = self._last_ask_calls.get(user_id, 0)
78
+
79
+ elapsed = now - last_call
80
+
81
+ if elapsed >= self.ASK_COOLDOWN:
82
+ return (True, 0)
83
+ else:
84
+ remaining = int(self.ASK_COOLDOWN - elapsed) + 1
85
+ return (False, remaining)
86
+
87
+ def record(self, user_id: int) -> None:
88
+ """记录用户调用时间
89
+
90
+ 参数:
91
+ user_id: 用户 QQ 号
92
+ """
93
+ # 超级管理员不记录
94
+ if self.config.is_superadmin(user_id):
95
+ return
96
+
97
+ self._last_calls[user_id] = time.time()
98
+
99
+ def record_ask(self, user_id: int) -> None:
100
+ """记录用户 /ask 命令调用时间
101
+
102
+ 参数:
103
+ user_id: 用户 QQ 号
104
+ """
105
+ # 超级管理员不记录
106
+ if self.config.is_superadmin(user_id):
107
+ return
108
+
109
+ self._last_ask_calls[user_id] = time.time()
110
+
111
+ def clear(self, user_id: int) -> None:
112
+ """清除用户的限制记录
113
+
114
+ 参数:
115
+ user_id: 用户 QQ 号
116
+ """
117
+ self._last_calls.pop(user_id, None)
118
+
119
+ def clear_ask(self, user_id: int) -> None:
120
+ """清除用户的 /ask 命令限制记录
121
+
122
+ 参数:
123
+ user_id: 用户 QQ 号
124
+ """
125
+ self._last_ask_calls.pop(user_id, None)
126
+
127
+ def clear_all(self) -> None:
128
+ """清除所有限制记录"""
129
+ self._last_calls.clear()
130
+ self._last_ask_calls.clear()