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.
- Undefined/__init__.py +3 -0
- Undefined/__main__.py +6 -0
- Undefined/ai.py +1215 -0
- Undefined/config.py +371 -0
- Undefined/end_summary_storage.py +48 -0
- Undefined/faq.py +244 -0
- Undefined/handlers.py +1247 -0
- Undefined/injection_response_agent.py +131 -0
- Undefined/main.py +126 -0
- Undefined/memory.py +120 -0
- Undefined/onebot.py +512 -0
- Undefined/rate_limit.py +130 -0
- Undefined/render.py +123 -0
- Undefined/scheduled_task_storage.py +88 -0
- Undefined/services/__init__.py +1 -0
- Undefined/services/queue_manager.py +206 -0
- Undefined/skills/README.md +53 -0
- Undefined/skills/__init__.py +10 -0
- Undefined/skills/agents/README.md +144 -0
- Undefined/skills/agents/__init__.py +116 -0
- Undefined/skills/agents/entertainment_agent/config.json +17 -0
- Undefined/skills/agents/entertainment_agent/handler.py +220 -0
- Undefined/skills/agents/entertainment_agent/intro.md +25 -0
- Undefined/skills/agents/entertainment_agent/prompt.md +20 -0
- Undefined/skills/agents/entertainment_agent/tools/__init__.py +1 -0
- Undefined/skills/agents/entertainment_agent/tools/ai_draw_one/config.json +34 -0
- Undefined/skills/agents/entertainment_agent/tools/ai_draw_one/handler.py +62 -0
- Undefined/skills/agents/entertainment_agent/tools/ai_study_helper/config.json +22 -0
- Undefined/skills/agents/entertainment_agent/tools/ai_study_helper/handler.py +35 -0
- Undefined/skills/agents/entertainment_agent/tools/get_current_time/config.json +12 -0
- Undefined/skills/agents/entertainment_agent/tools/get_current_time/handler.py +5 -0
- Undefined/skills/agents/entertainment_agent/tools/horoscope/config.json +24 -0
- Undefined/skills/agents/entertainment_agent/tools/horoscope/handler.py +141 -0
- Undefined/skills/agents/entertainment_agent/tools/minecraft_skin/config.json +43 -0
- Undefined/skills/agents/entertainment_agent/tools/minecraft_skin/handler.py +55 -0
- Undefined/skills/agents/entertainment_agent/tools/novel_search/config.json +25 -0
- Undefined/skills/agents/entertainment_agent/tools/novel_search/handler.py +31 -0
- Undefined/skills/agents/entertainment_agent/tools/renjian/config.json +12 -0
- Undefined/skills/agents/entertainment_agent/tools/renjian/handler.py +30 -0
- Undefined/skills/agents/entertainment_agent/tools/wenchang_dijun/config.json +12 -0
- Undefined/skills/agents/entertainment_agent/tools/wenchang_dijun/handler.py +44 -0
- Undefined/skills/agents/file_analysis_agent/__init__.py +1 -0
- Undefined/skills/agents/file_analysis_agent/config.json +21 -0
- Undefined/skills/agents/file_analysis_agent/handler.py +248 -0
- Undefined/skills/agents/file_analysis_agent/intro.md +22 -0
- Undefined/skills/agents/file_analysis_agent/prompt.md +36 -0
- Undefined/skills/agents/file_analysis_agent/tools/__init__.py +1 -0
- Undefined/skills/agents/file_analysis_agent/tools/analyze_code/config.json +17 -0
- Undefined/skills/agents/file_analysis_agent/tools/analyze_code/handler.py +427 -0
- Undefined/skills/agents/file_analysis_agent/tools/analyze_multimodal/config.json +25 -0
- Undefined/skills/agents/file_analysis_agent/tools/analyze_multimodal/handler.py +178 -0
- Undefined/skills/agents/file_analysis_agent/tools/cleanup_temp/config.json +16 -0
- Undefined/skills/agents/file_analysis_agent/tools/cleanup_temp/handler.py +35 -0
- Undefined/skills/agents/file_analysis_agent/tools/detect_file_type/config.json +17 -0
- Undefined/skills/agents/file_analysis_agent/tools/detect_file_type/handler.py +221 -0
- Undefined/skills/agents/file_analysis_agent/tools/download_file/config.json +21 -0
- Undefined/skills/agents/file_analysis_agent/tools/download_file/handler.py +124 -0
- Undefined/skills/agents/file_analysis_agent/tools/extract_archive/config.json +25 -0
- Undefined/skills/agents/file_analysis_agent/tools/extract_archive/handler.py +190 -0
- Undefined/skills/agents/file_analysis_agent/tools/extract_docx/config.json +17 -0
- Undefined/skills/agents/file_analysis_agent/tools/extract_docx/handler.py +78 -0
- Undefined/skills/agents/file_analysis_agent/tools/extract_pdf/config.json +21 -0
- Undefined/skills/agents/file_analysis_agent/tools/extract_pdf/handler.py +67 -0
- Undefined/skills/agents/file_analysis_agent/tools/extract_pptx/config.json +17 -0
- Undefined/skills/agents/file_analysis_agent/tools/extract_pptx/handler.py +73 -0
- Undefined/skills/agents/file_analysis_agent/tools/extract_xlsx/config.json +17 -0
- Undefined/skills/agents/file_analysis_agent/tools/extract_xlsx/handler.py +101 -0
- Undefined/skills/agents/file_analysis_agent/tools/get_current_time/config.json +12 -0
- Undefined/skills/agents/file_analysis_agent/tools/get_current_time/handler.py +5 -0
- Undefined/skills/agents/file_analysis_agent/tools/read_text_file/config.json +21 -0
- Undefined/skills/agents/file_analysis_agent/tools/read_text_file/handler.py +90 -0
- Undefined/skills/agents/info_agent/config.json +17 -0
- Undefined/skills/agents/info_agent/handler.py +220 -0
- Undefined/skills/agents/info_agent/intro.md +22 -0
- Undefined/skills/agents/info_agent/prompt.md +27 -0
- Undefined/skills/agents/info_agent/tools/__init__.py +1 -0
- Undefined/skills/agents/info_agent/tools/baiduhot/config.json +18 -0
- Undefined/skills/agents/info_agent/tools/baiduhot/handler.py +49 -0
- Undefined/skills/agents/info_agent/tools/base64/config.json +22 -0
- Undefined/skills/agents/info_agent/tools/base64/handler.py +44 -0
- Undefined/skills/agents/info_agent/tools/douyinhot/config.json +18 -0
- Undefined/skills/agents/info_agent/tools/douyinhot/handler.py +53 -0
- Undefined/skills/agents/info_agent/tools/get_current_time/config.json +12 -0
- Undefined/skills/agents/info_agent/tools/get_current_time/handler.py +5 -0
- Undefined/skills/agents/info_agent/tools/gold_price/config.json +12 -0
- Undefined/skills/agents/info_agent/tools/gold_price/handler.py +58 -0
- Undefined/skills/agents/info_agent/tools/hash/config.json +22 -0
- Undefined/skills/agents/info_agent/tools/hash/handler.py +43 -0
- Undefined/skills/agents/info_agent/tools/history/config.json +12 -0
- Undefined/skills/agents/info_agent/tools/history/handler.py +37 -0
- Undefined/skills/agents/info_agent/tools/net_check/config.json +17 -0
- Undefined/skills/agents/info_agent/tools/net_check/handler.py +117 -0
- Undefined/skills/agents/info_agent/tools/news_tencent/config.json +17 -0
- Undefined/skills/agents/info_agent/tools/news_tencent/handler.py +38 -0
- Undefined/skills/agents/info_agent/tools/qq_level_query/config.json +29 -0
- Undefined/skills/agents/info_agent/tools/qq_level_query/handler.py +48 -0
- Undefined/skills/agents/info_agent/tools/speed/config.json +17 -0
- Undefined/skills/agents/info_agent/tools/speed/handler.py +37 -0
- Undefined/skills/agents/info_agent/tools/tcping/config.json +21 -0
- Undefined/skills/agents/info_agent/tools/tcping/handler.py +53 -0
- Undefined/skills/agents/info_agent/tools/weather_query/config.json +22 -0
- Undefined/skills/agents/info_agent/tools/weather_query/handler.py +207 -0
- Undefined/skills/agents/info_agent/tools/weibohot/config.json +18 -0
- Undefined/skills/agents/info_agent/tools/weibohot/handler.py +49 -0
- Undefined/skills/agents/info_agent/tools/whois/config.json +17 -0
- Undefined/skills/agents/info_agent/tools/whois/handler.py +63 -0
- Undefined/skills/agents/naga_code_analysis_agent/config.json +17 -0
- Undefined/skills/agents/naga_code_analysis_agent/handler.py +222 -0
- Undefined/skills/agents/naga_code_analysis_agent/intro.md +17 -0
- Undefined/skills/agents/naga_code_analysis_agent/prompt.md +19 -0
- Undefined/skills/agents/naga_code_analysis_agent/tools/__init__.py +1 -0
- Undefined/skills/agents/naga_code_analysis_agent/tools/get_current_time/config.json +12 -0
- Undefined/skills/agents/naga_code_analysis_agent/tools/get_current_time/handler.py +5 -0
- Undefined/skills/agents/naga_code_analysis_agent/tools/glob/config.json +17 -0
- Undefined/skills/agents/naga_code_analysis_agent/tools/glob/handler.py +37 -0
- Undefined/skills/agents/naga_code_analysis_agent/tools/list_directory/config.json +17 -0
- Undefined/skills/agents/naga_code_analysis_agent/tools/list_directory/handler.py +31 -0
- Undefined/skills/agents/naga_code_analysis_agent/tools/read_file/config.json +17 -0
- Undefined/skills/agents/naga_code_analysis_agent/tools/read_file/handler.py +66 -0
- Undefined/skills/agents/naga_code_analysis_agent/tools/read_naga_intro/config.json +12 -0
- Undefined/skills/agents/naga_code_analysis_agent/tools/read_naga_intro/handler.py +327 -0
- Undefined/skills/agents/naga_code_analysis_agent/tools/search_file_content/config.json +25 -0
- Undefined/skills/agents/naga_code_analysis_agent/tools/search_file_content/handler.py +46 -0
- Undefined/skills/agents/scheduler_agent/__init__.py +1 -0
- Undefined/skills/agents/scheduler_agent/config.json +17 -0
- Undefined/skills/agents/scheduler_agent/handler.py +218 -0
- Undefined/skills/agents/scheduler_agent/intro.md +17 -0
- Undefined/skills/agents/scheduler_agent/prompt.md +67 -0
- Undefined/skills/agents/scheduler_agent/tools/__init__.py +1 -0
- Undefined/skills/agents/scheduler_agent/tools/create_schedule_task/config.json +37 -0
- Undefined/skills/agents/scheduler_agent/tools/create_schedule_task/handler.py +68 -0
- Undefined/skills/agents/scheduler_agent/tools/delete_schedule_task/config.json +19 -0
- Undefined/skills/agents/scheduler_agent/tools/delete_schedule_task/handler.py +26 -0
- Undefined/skills/agents/scheduler_agent/tools/get_current_time/config.json +12 -0
- Undefined/skills/agents/scheduler_agent/tools/get_current_time/handler.py +5 -0
- Undefined/skills/agents/scheduler_agent/tools/list_schedule_tasks/config.json +11 -0
- Undefined/skills/agents/scheduler_agent/tools/list_schedule_tasks/handler.py +47 -0
- Undefined/skills/agents/scheduler_agent/tools/update_schedule_task/config.json +39 -0
- Undefined/skills/agents/scheduler_agent/tools/update_schedule_task/handler.py +46 -0
- Undefined/skills/agents/social_agent/config.json +17 -0
- Undefined/skills/agents/social_agent/handler.py +220 -0
- Undefined/skills/agents/social_agent/intro.md +17 -0
- Undefined/skills/agents/social_agent/prompt.md +19 -0
- Undefined/skills/agents/social_agent/tools/__init__.py +1 -0
- Undefined/skills/agents/social_agent/tools/bilibili_search/config.json +21 -0
- Undefined/skills/agents/social_agent/tools/bilibili_search/handler.py +68 -0
- Undefined/skills/agents/social_agent/tools/bilibili_user_info/config.json +17 -0
- Undefined/skills/agents/social_agent/tools/bilibili_user_info/handler.py +68 -0
- Undefined/skills/agents/social_agent/tools/get_current_time/config.json +12 -0
- Undefined/skills/agents/social_agent/tools/get_current_time/handler.py +5 -0
- Undefined/skills/agents/social_agent/tools/music_global_search/config.json +21 -0
- Undefined/skills/agents/social_agent/tools/music_global_search/handler.py +47 -0
- Undefined/skills/agents/social_agent/tools/music_info_get/config.json +22 -0
- Undefined/skills/agents/social_agent/tools/music_info_get/handler.py +35 -0
- Undefined/skills/agents/social_agent/tools/music_lyrics/config.json +22 -0
- Undefined/skills/agents/social_agent/tools/music_lyrics/handler.py +26 -0
- Undefined/skills/agents/social_agent/tools/video_random_recommend/config.json +21 -0
- Undefined/skills/agents/social_agent/tools/video_random_recommend/handler.py +21 -0
- Undefined/skills/agents/web_agent/config.json +17 -0
- Undefined/skills/agents/web_agent/handler.py +221 -0
- Undefined/skills/agents/web_agent/intro.md +14 -0
- Undefined/skills/agents/web_agent/prompt.md +16 -0
- Undefined/skills/agents/web_agent/tools/__init__.py +1 -0
- Undefined/skills/agents/web_agent/tools/crawl_webpage/config.json +21 -0
- Undefined/skills/agents/web_agent/tools/crawl_webpage/handler.py +102 -0
- Undefined/skills/agents/web_agent/tools/get_current_time/config.json +12 -0
- Undefined/skills/agents/web_agent/tools/get_current_time/handler.py +5 -0
- Undefined/skills/agents/web_agent/tools/web_search/config.json +21 -0
- Undefined/skills/agents/web_agent/tools/web_search/handler.py +29 -0
- Undefined/skills/tools/README.md +85 -0
- Undefined/skills/tools/__init__.py +120 -0
- Undefined/skills/tools/debug/config.json +17 -0
- Undefined/skills/tools/debug/handler.py +35 -0
- Undefined/skills/tools/end/config.json +17 -0
- Undefined/skills/tools/end/handler.py +24 -0
- Undefined/skills/tools/get_current_time/config.json +12 -0
- Undefined/skills/tools/get_current_time/handler.py +5 -0
- Undefined/skills/tools/get_forward_msg/config.json +17 -0
- Undefined/skills/tools/get_forward_msg/handler.py +131 -0
- Undefined/skills/tools/get_group_member_info/config.json +38 -0
- Undefined/skills/tools/get_group_member_info/handler.py +142 -0
- Undefined/skills/tools/get_messages_by_time/config.json +30 -0
- Undefined/skills/tools/get_messages_by_time/handler.py +128 -0
- Undefined/skills/tools/get_picture/config.json +45 -0
- Undefined/skills/tools/get_picture/handler.py +191 -0
- Undefined/skills/tools/get_recent_messages/config.json +30 -0
- Undefined/skills/tools/get_recent_messages/handler.py +88 -0
- Undefined/skills/tools/qq_like/config.json +22 -0
- Undefined/skills/tools/qq_like/handler.py +58 -0
- Undefined/skills/tools/render_html/config.json +26 -0
- Undefined/skills/tools/render_html/handler.py +39 -0
- Undefined/skills/tools/render_latex/config.json +26 -0
- Undefined/skills/tools/render_latex/handler.py +78 -0
- Undefined/skills/tools/render_markdown/config.json +26 -0
- Undefined/skills/tools/render_markdown/handler.py +63 -0
- Undefined/skills/tools/save_memory/config.json +17 -0
- Undefined/skills/tools/save_memory/handler.py +17 -0
- Undefined/skills/tools/send_message/config.json +21 -0
- Undefined/skills/tools/send_message/handler.py +60 -0
- Undefined/skills/tools/send_private_message/config.json +21 -0
- Undefined/skills/tools/send_private_message/handler.py +35 -0
- Undefined/utils/__init__.py +0 -0
- Undefined/utils/common.py +186 -0
- Undefined/utils/history.py +284 -0
- Undefined/utils/scheduler.py +286 -0
- Undefined/utils/sender.py +140 -0
- undefined_bot-2.1.0.dist-info/METADATA +259 -0
- undefined_bot-2.1.0.dist-info/RECORD +211 -0
- undefined_bot-2.1.0.dist-info/WHEEL +4 -0
- undefined_bot-2.1.0.dist-info/entry_points.txt +2 -0
- 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
|
Undefined/rate_limit.py
ADDED
|
@@ -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()
|