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/handlers.py
ADDED
|
@@ -0,0 +1,1247 @@
|
|
|
1
|
+
"""消息处理和命令分发"""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import random
|
|
6
|
+
import re
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from .ai import AIClient
|
|
11
|
+
from .config import Config
|
|
12
|
+
from .faq import FAQStorage, extract_faq_title
|
|
13
|
+
from .injection_response_agent import InjectionResponseAgent
|
|
14
|
+
from .services.queue_manager import QueueManager
|
|
15
|
+
from .onebot import (
|
|
16
|
+
OneBotClient,
|
|
17
|
+
get_message_content,
|
|
18
|
+
get_message_sender_id,
|
|
19
|
+
parse_message_time,
|
|
20
|
+
)
|
|
21
|
+
from .rate_limit import RateLimiter
|
|
22
|
+
from .utils.common import (
|
|
23
|
+
extract_text,
|
|
24
|
+
parse_message_content_for_history,
|
|
25
|
+
matches_xinliweiyuan,
|
|
26
|
+
)
|
|
27
|
+
from .utils.history import MessageHistoryManager
|
|
28
|
+
from .utils.scheduler import TaskScheduler
|
|
29
|
+
from .utils.sender import MessageSender
|
|
30
|
+
from .scheduled_task_storage import ScheduledTaskStorage
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
with open("res/prepared_messages/help_message.txt", "r", encoding="utf-8") as f:
|
|
35
|
+
HELP_MESSAGE = f.read()
|
|
36
|
+
|
|
37
|
+
class MessageHandler:
|
|
38
|
+
"""消息处理器"""
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
config: Config,
|
|
43
|
+
onebot: OneBotClient,
|
|
44
|
+
ai: AIClient,
|
|
45
|
+
faq_storage: FAQStorage,
|
|
46
|
+
task_storage: ScheduledTaskStorage,
|
|
47
|
+
) -> None:
|
|
48
|
+
self.config = config
|
|
49
|
+
self.onebot = onebot
|
|
50
|
+
self.ai = ai
|
|
51
|
+
self.faq_storage = faq_storage
|
|
52
|
+
self.rate_limiter = RateLimiter(config)
|
|
53
|
+
# 注入攻击回复生成器
|
|
54
|
+
self.injection_response_agent = InjectionResponseAgent(config.security_model)
|
|
55
|
+
|
|
56
|
+
# 初始化 Utils
|
|
57
|
+
self.history_manager = MessageHistoryManager()
|
|
58
|
+
self.sender = MessageSender(onebot, self.history_manager, config.bot_qq)
|
|
59
|
+
|
|
60
|
+
# 初始化定时任务调度器
|
|
61
|
+
self.scheduler = TaskScheduler(
|
|
62
|
+
ai, self.sender, onebot, self.history_manager, task_storage
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# 初始化队列管理器
|
|
66
|
+
self.queue_manager = QueueManager()
|
|
67
|
+
self.queue_manager.start(self._handle_queue_request)
|
|
68
|
+
|
|
69
|
+
async def handle_message(self, event: dict[str, Any]) -> None:
|
|
70
|
+
"""处理收到的消息事件"""
|
|
71
|
+
post_type = event.get("post_type", "message")
|
|
72
|
+
|
|
73
|
+
# 处理拍一拍事件(效果同被 @)
|
|
74
|
+
if post_type == "notice" and event.get("notice_type") == "poke":
|
|
75
|
+
target_id = event.get("target_id", 0)
|
|
76
|
+
# 只有拍机器人才响应
|
|
77
|
+
if target_id != self.config.bot_qq:
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
poke_group_id: int = event.get("group_id", 0)
|
|
81
|
+
poke_sender_id: int = event.get("user_id", 0)
|
|
82
|
+
|
|
83
|
+
logger.info(
|
|
84
|
+
f"[通知事件] 收到拍一拍: group={poke_group_id}, sender={poke_sender_id}"
|
|
85
|
+
)
|
|
86
|
+
logger.debug(f"[通知详情] 拍一拍完整数据: {event}")
|
|
87
|
+
|
|
88
|
+
# 如果 group_id 为 0,说明是私聊拍一拍
|
|
89
|
+
if poke_group_id == 0:
|
|
90
|
+
logger.info("私聊拍一拍,触发私聊回复")
|
|
91
|
+
await self._handle_private_reply(
|
|
92
|
+
poke_sender_id,
|
|
93
|
+
"(拍了拍你)", # 空消息文本
|
|
94
|
+
[], # 空消息内容
|
|
95
|
+
is_poke=True,
|
|
96
|
+
sender_name=str(poke_sender_id),
|
|
97
|
+
)
|
|
98
|
+
else:
|
|
99
|
+
# 群聊拍一拍,触发群聊自动回复
|
|
100
|
+
await self._handle_auto_reply(
|
|
101
|
+
poke_group_id,
|
|
102
|
+
poke_sender_id,
|
|
103
|
+
"(拍了拍你)", # 空消息文本
|
|
104
|
+
[], # 空消息内容
|
|
105
|
+
is_poke=True,
|
|
106
|
+
sender_name=str(poke_sender_id),
|
|
107
|
+
group_name=str(poke_group_id),
|
|
108
|
+
)
|
|
109
|
+
return
|
|
110
|
+
|
|
111
|
+
# 处理私聊消息
|
|
112
|
+
if event.get("message_type") == "private":
|
|
113
|
+
private_sender_id: int = get_message_sender_id(event)
|
|
114
|
+
private_message_content: list[dict[str, Any]] = get_message_content(event)
|
|
115
|
+
|
|
116
|
+
# 获取发送者昵称
|
|
117
|
+
private_sender: dict[str, Any] = event.get("sender", {})
|
|
118
|
+
private_sender_nickname: str = private_sender.get("nickname", "")
|
|
119
|
+
|
|
120
|
+
# 获取私聊用户昵称
|
|
121
|
+
user_name = private_sender_nickname
|
|
122
|
+
if not user_name:
|
|
123
|
+
try:
|
|
124
|
+
user_info = await self.onebot.get_stranger_info(private_sender_id)
|
|
125
|
+
if user_info:
|
|
126
|
+
user_name = user_info.get("nickname", "")
|
|
127
|
+
except Exception as e:
|
|
128
|
+
logger.warning(f"获取用户昵称失败: {e}")
|
|
129
|
+
|
|
130
|
+
# 处理图片:在历史记录中仅保留占位符,由 AI 决定是否分析
|
|
131
|
+
processed_message_content = []
|
|
132
|
+
for segment in private_message_content:
|
|
133
|
+
if segment.get("type") == "image":
|
|
134
|
+
file = segment.get("data", {}).get("file", "") or segment.get(
|
|
135
|
+
"data", {}
|
|
136
|
+
).get("url", "")
|
|
137
|
+
text_repr = f"[图片: {file}]"
|
|
138
|
+
processed_message_content.append(
|
|
139
|
+
{"type": "text", "data": {"text": text_repr}}
|
|
140
|
+
)
|
|
141
|
+
else:
|
|
142
|
+
processed_message_content.append(segment)
|
|
143
|
+
|
|
144
|
+
# 从处理后的内容中提取文本
|
|
145
|
+
text = extract_text(processed_message_content, self.config.bot_qq)
|
|
146
|
+
logger.info(
|
|
147
|
+
f"[私聊消息] 发送者={private_sender_id} ({user_name}) | 内容: {text[:100]}"
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# 处理图片:在历史记录中仅保留占位符,由 AI 决定是否分析
|
|
151
|
+
processed_message_content = []
|
|
152
|
+
for segment in private_message_content:
|
|
153
|
+
if segment.get("type") == "image":
|
|
154
|
+
file = segment.get("data", {}).get("file", "") or segment.get(
|
|
155
|
+
"data", {}
|
|
156
|
+
).get("url", "")
|
|
157
|
+
text_repr = f"[图片: {file}]"
|
|
158
|
+
processed_message_content.append(
|
|
159
|
+
{"type": "text", "data": {"text": text_repr}}
|
|
160
|
+
)
|
|
161
|
+
else:
|
|
162
|
+
processed_message_content.append(segment)
|
|
163
|
+
|
|
164
|
+
# 从处理后的内容中提取文本
|
|
165
|
+
text = extract_text(processed_message_content, self.config.bot_qq)
|
|
166
|
+
logger.info(
|
|
167
|
+
f"[私聊消息] 发送者={private_sender_id} ({user_name}) | 内容: {text[:100]}"
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
# 保存私聊消息到历史记录(保存处理后的内容)
|
|
171
|
+
# 使用新的 utils
|
|
172
|
+
parsed_content = await parse_message_content_for_history(
|
|
173
|
+
processed_message_content, self.config.bot_qq, self.onebot.get_msg
|
|
174
|
+
)
|
|
175
|
+
logger.debug(
|
|
176
|
+
f"[历史记录] 保存私聊记录: user={private_sender_id}, content={parsed_content[:50]}..."
|
|
177
|
+
)
|
|
178
|
+
self.history_manager.add_private_message(
|
|
179
|
+
user_id=private_sender_id,
|
|
180
|
+
text_content=parsed_content,
|
|
181
|
+
display_name=private_sender_nickname,
|
|
182
|
+
user_name=user_name,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
# 如果是 bot 自己的消息,只保存不触发回复,避免无限循环
|
|
186
|
+
if private_sender_id == self.config.bot_qq:
|
|
187
|
+
return
|
|
188
|
+
|
|
189
|
+
# 私聊消息直接触发回复(相当于被 @),使用处理后的内容
|
|
190
|
+
await self._handle_private_reply(
|
|
191
|
+
private_sender_id,
|
|
192
|
+
text,
|
|
193
|
+
processed_message_content,
|
|
194
|
+
sender_name=user_name,
|
|
195
|
+
)
|
|
196
|
+
return
|
|
197
|
+
|
|
198
|
+
# 只处理群消息
|
|
199
|
+
if event.get("message_type") != "group":
|
|
200
|
+
return
|
|
201
|
+
|
|
202
|
+
group_id: int = event.get("group_id", 0)
|
|
203
|
+
sender_id: int = get_message_sender_id(event)
|
|
204
|
+
message_content: list[dict[str, Any]] = get_message_content(event)
|
|
205
|
+
|
|
206
|
+
# 获取发送者昵称信息
|
|
207
|
+
group_sender: dict[str, Any] = event.get("sender", {})
|
|
208
|
+
sender_card: str = group_sender.get("card", "")
|
|
209
|
+
sender_nickname: str = group_sender.get("nickname", "")
|
|
210
|
+
|
|
211
|
+
# 提取文本内容
|
|
212
|
+
text = extract_text(message_content, self.config.bot_qq)
|
|
213
|
+
logger.info(
|
|
214
|
+
f"[群消息] 群:{group_id} | 发送者:{sender_id} ({sender_card or sender_nickname}) | 内容: {text[:100]}"
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
# 提取文本内容
|
|
218
|
+
text = extract_text(message_content, self.config.bot_qq)
|
|
219
|
+
logger.info(
|
|
220
|
+
f"[群消息] 群:{group_id} | 发送者:{sender_id} ({sender_card or sender_nickname}) | 内容: {text[:100]}"
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
# 处理图片:在历史记录中仅保留占位符
|
|
224
|
+
processed_message_content = []
|
|
225
|
+
for segment in message_content:
|
|
226
|
+
if segment.get("type") == "image":
|
|
227
|
+
file = segment.get("data", {}).get("file", "") or segment.get(
|
|
228
|
+
"data", {}
|
|
229
|
+
).get("url", "")
|
|
230
|
+
text_repr = f"[图片: {file}]"
|
|
231
|
+
processed_message_content.append(
|
|
232
|
+
{"type": "text", "data": {"text": text_repr}}
|
|
233
|
+
)
|
|
234
|
+
else:
|
|
235
|
+
processed_message_content.append(segment)
|
|
236
|
+
|
|
237
|
+
# 保存消息到历史记录 (使用处理后的内容)
|
|
238
|
+
# 获取群聊名
|
|
239
|
+
group_name = ""
|
|
240
|
+
try:
|
|
241
|
+
group_info = await self.onebot.get_group_info(group_id)
|
|
242
|
+
if group_info:
|
|
243
|
+
group_name = group_info.get("group_name", "")
|
|
244
|
+
except Exception as e:
|
|
245
|
+
logger.warning(f"获取群聊名失败: {e}")
|
|
246
|
+
|
|
247
|
+
# 使用新的 utils
|
|
248
|
+
parsed_content = await parse_message_content_for_history(
|
|
249
|
+
processed_message_content, self.config.bot_qq, self.onebot.get_msg
|
|
250
|
+
)
|
|
251
|
+
logger.debug(
|
|
252
|
+
f"[历史记录] 保存群聊记录: group={group_id}, sender={sender_id}, content={parsed_content[:50]}..."
|
|
253
|
+
)
|
|
254
|
+
self.history_manager.add_group_message(
|
|
255
|
+
group_id=group_id,
|
|
256
|
+
sender_id=sender_id,
|
|
257
|
+
text_content=parsed_content,
|
|
258
|
+
sender_card=sender_card,
|
|
259
|
+
sender_nickname=sender_nickname,
|
|
260
|
+
group_name=group_name,
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
# 如果是 bot 自己的消息,只保存不触发回复,避免无限循环
|
|
264
|
+
if sender_id == self.config.bot_qq:
|
|
265
|
+
return
|
|
266
|
+
|
|
267
|
+
# 关键词自动回复:心理委员 (使用原始消息内容提取文本,保证关键词触发不受影响)
|
|
268
|
+
if matches_xinliweiyuan(text):
|
|
269
|
+
rand_val = random.random()
|
|
270
|
+
if rand_val < 0.1: # 10% 发送图片
|
|
271
|
+
image_path = os.path.abspath("data/img/xlwy.jpg")
|
|
272
|
+
message = f"[CQ:image,file={image_path}]"
|
|
273
|
+
# 50% 概率 @ 发送者
|
|
274
|
+
if random.random() < 0.5:
|
|
275
|
+
message = f"[CQ:at,qq={sender_id}] {message}"
|
|
276
|
+
logger.info("关键词回复: 发送图片 xlwy.jpg")
|
|
277
|
+
else: # 90% 原有逻辑
|
|
278
|
+
if random.random() < 0.7:
|
|
279
|
+
reply = "受着"
|
|
280
|
+
else:
|
|
281
|
+
reply = "那咋了"
|
|
282
|
+
# 50% 概率 @ 发送者
|
|
283
|
+
if random.random() < 0.5:
|
|
284
|
+
message = f"[CQ:at,qq={sender_id}] {reply}"
|
|
285
|
+
else:
|
|
286
|
+
message = reply
|
|
287
|
+
logger.info(f"关键词回复: {reply}")
|
|
288
|
+
# 使用 sender 发送
|
|
289
|
+
await self.sender.send_group_message(group_id, message)
|
|
290
|
+
return
|
|
291
|
+
|
|
292
|
+
# 提取文本内容
|
|
293
|
+
# (已在上方提取用于日志记录)
|
|
294
|
+
|
|
295
|
+
# 检查是否 @ 了机器人
|
|
296
|
+
is_at_bot = self._is_at_bot(message_content)
|
|
297
|
+
|
|
298
|
+
# 只有被@时才处理斜杠命令
|
|
299
|
+
if is_at_bot:
|
|
300
|
+
command = self._parse_command(text)
|
|
301
|
+
|
|
302
|
+
if command:
|
|
303
|
+
# 分发命令
|
|
304
|
+
cmd_name: str = command["name"]
|
|
305
|
+
cmd_args: list[str] = command["args"]
|
|
306
|
+
|
|
307
|
+
# 有命令,执行命令
|
|
308
|
+
logger.info(f"[命令解析] 解析到命令: /{cmd_name} | 参数: {cmd_args}")
|
|
309
|
+
|
|
310
|
+
# 有命令,执行命令
|
|
311
|
+
logger.info(f"[命令解析] 解析到命令: /{cmd_name} | 参数: {cmd_args}")
|
|
312
|
+
|
|
313
|
+
try:
|
|
314
|
+
# 公开命令 - 无权限限制但有速率限制
|
|
315
|
+
if cmd_name == "help":
|
|
316
|
+
await self._handle_help(group_id)
|
|
317
|
+
elif cmd_name == "lsfaq":
|
|
318
|
+
await self._check_rate_limit_and_handle(
|
|
319
|
+
group_id, sender_id, self._handle_lsfaq, group_id
|
|
320
|
+
)
|
|
321
|
+
elif cmd_name == "viewfaq":
|
|
322
|
+
await self._check_rate_limit_and_handle(
|
|
323
|
+
group_id,
|
|
324
|
+
sender_id,
|
|
325
|
+
self._handle_viewfaq,
|
|
326
|
+
group_id,
|
|
327
|
+
cmd_args,
|
|
328
|
+
)
|
|
329
|
+
elif cmd_name == "searchfaq":
|
|
330
|
+
await self._check_rate_limit_and_handle(
|
|
331
|
+
group_id,
|
|
332
|
+
sender_id,
|
|
333
|
+
self._handle_searchfaq,
|
|
334
|
+
group_id,
|
|
335
|
+
cmd_args,
|
|
336
|
+
)
|
|
337
|
+
elif cmd_name == "lsadmin":
|
|
338
|
+
await self._handle_lsadmin(group_id)
|
|
339
|
+
|
|
340
|
+
# 管理员命令
|
|
341
|
+
elif cmd_name == "delfaq":
|
|
342
|
+
if not self.config.is_admin(sender_id):
|
|
343
|
+
logger.warning(
|
|
344
|
+
f"[权限控制] 非管理员 {sender_id} 尝试执行 /{cmd_name}"
|
|
345
|
+
)
|
|
346
|
+
await self.sender.send_group_message(
|
|
347
|
+
group_id, "⚠️ 权限不足:只有管理员可以使用此命令"
|
|
348
|
+
)
|
|
349
|
+
return
|
|
350
|
+
await self._check_rate_limit_and_handle(
|
|
351
|
+
group_id, sender_id, self._handle_delfaq, group_id, cmd_args
|
|
352
|
+
)
|
|
353
|
+
elif cmd_name == "bugfix":
|
|
354
|
+
if not self.config.is_admin(sender_id):
|
|
355
|
+
logger.warning(
|
|
356
|
+
f"[权限控制] 非管理员 {sender_id} 尝试执行 /{cmd_name}"
|
|
357
|
+
)
|
|
358
|
+
await self.sender.send_group_message(
|
|
359
|
+
group_id, "⚠️ 权限不足:只有管理员可以使用此命令"
|
|
360
|
+
)
|
|
361
|
+
return
|
|
362
|
+
await self._check_rate_limit_and_handle(
|
|
363
|
+
group_id,
|
|
364
|
+
sender_id,
|
|
365
|
+
self._handle_bugfix,
|
|
366
|
+
group_id,
|
|
367
|
+
sender_id,
|
|
368
|
+
cmd_args,
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
# 超级管理员命令
|
|
372
|
+
elif cmd_name == "addadmin":
|
|
373
|
+
if not self.config.is_superadmin(sender_id):
|
|
374
|
+
logger.warning(
|
|
375
|
+
f"[权限控制] 非超级管理员 {sender_id} 尝试执行 /{cmd_name}"
|
|
376
|
+
)
|
|
377
|
+
await self.sender.send_group_message(
|
|
378
|
+
group_id, "⚠️ 权限不足:只有超级管理员可以使用此命令"
|
|
379
|
+
)
|
|
380
|
+
return
|
|
381
|
+
await self._handle_addadmin(group_id, cmd_args)
|
|
382
|
+
elif cmd_name == "rmadmin":
|
|
383
|
+
if not self.config.is_superadmin(sender_id):
|
|
384
|
+
logger.warning(
|
|
385
|
+
f"[权限控制] 非超级管理员 {sender_id} 尝试执行 /{cmd_name}"
|
|
386
|
+
)
|
|
387
|
+
await self.sender.send_group_message(
|
|
388
|
+
group_id, "⚠️ 权限不足:只有超级管理员可以使用此命令"
|
|
389
|
+
)
|
|
390
|
+
return
|
|
391
|
+
await self._handle_rmadmin(group_id, cmd_args)
|
|
392
|
+
|
|
393
|
+
else:
|
|
394
|
+
logger.info(f"[命令执行] 未知命令: /{cmd_name}")
|
|
395
|
+
await self.sender.send_group_message(
|
|
396
|
+
group_id,
|
|
397
|
+
f"❌ 未知命令: {cmd_name}\n使用 /help 查看可用命令",
|
|
398
|
+
)
|
|
399
|
+
logger.info(f"[命令执行] /{cmd_name} 执行完成")
|
|
400
|
+
except Exception as e:
|
|
401
|
+
logger.exception(f"[命令错误] 执行 /{cmd_name} 失败: {e}")
|
|
402
|
+
await self.sender.send_group_message(
|
|
403
|
+
group_id, f"❌ 命令执行失败: {e}"
|
|
404
|
+
)
|
|
405
|
+
return
|
|
406
|
+
|
|
407
|
+
# 自动回复处理(没被@或被@但没有命令)
|
|
408
|
+
# 注意:未被@的消息中的斜杠命令不会被处理,只作为普通文本
|
|
409
|
+
display_name = sender_card or sender_nickname or str(sender_id)
|
|
410
|
+
await self._handle_auto_reply(
|
|
411
|
+
group_id,
|
|
412
|
+
sender_id,
|
|
413
|
+
text,
|
|
414
|
+
message_content,
|
|
415
|
+
sender_name=display_name,
|
|
416
|
+
group_name=group_name,
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
async def _handle_queue_request(self, request: dict[str, Any]) -> None:
|
|
420
|
+
"""处理来自 QueueManager 的请求"""
|
|
421
|
+
request_type = request.get("type", "unknown")
|
|
422
|
+
|
|
423
|
+
if request_type == "auto_reply":
|
|
424
|
+
await self._execute_auto_reply(request)
|
|
425
|
+
elif request_type == "private_reply":
|
|
426
|
+
await self._execute_private_reply(request)
|
|
427
|
+
else:
|
|
428
|
+
logger.warning(f"未知的请求类型: {request_type}")
|
|
429
|
+
|
|
430
|
+
async def _execute_auto_reply(self, request: dict[str, Any]) -> None:
|
|
431
|
+
"""执行自动回复请求"""
|
|
432
|
+
group_id = request["group_id"]
|
|
433
|
+
sender_id = request["sender_id"]
|
|
434
|
+
full_question = request["full_question"]
|
|
435
|
+
|
|
436
|
+
# 定义回调 - 使用 sender
|
|
437
|
+
async def send_message_callback(
|
|
438
|
+
message: str, at_user: int | None = None
|
|
439
|
+
) -> None:
|
|
440
|
+
if at_user:
|
|
441
|
+
message = f"[CQ:at,qq={at_user}] {message}"
|
|
442
|
+
logger.debug(
|
|
443
|
+
f"send_message_callback: group_id={group_id}, message={message[:50]}..."
|
|
444
|
+
)
|
|
445
|
+
await self.sender.send_group_message(group_id, message)
|
|
446
|
+
|
|
447
|
+
# 使用 history_manager 获取历史
|
|
448
|
+
async def get_recent_messages_callback(
|
|
449
|
+
chat_id: str, msg_type: str, start: int, end: int
|
|
450
|
+
) -> list[dict[str, Any]]:
|
|
451
|
+
return self.history_manager.get_recent(chat_id, msg_type, start, end)
|
|
452
|
+
|
|
453
|
+
# 定义私聊发送回调
|
|
454
|
+
async def send_private_message_callback(user_id: int, message: str) -> None:
|
|
455
|
+
logger.debug(
|
|
456
|
+
f"send_private_message_callback: user_id={user_id}, message={message[:50]}..."
|
|
457
|
+
)
|
|
458
|
+
await self.sender.send_private_message(user_id, message)
|
|
459
|
+
|
|
460
|
+
# 定义发送图片回调
|
|
461
|
+
async def send_image_callback(
|
|
462
|
+
target_id: int, msg_type: str, image_path: str
|
|
463
|
+
) -> None:
|
|
464
|
+
logger.debug(
|
|
465
|
+
f"send_image_callback: target_id={target_id}, msg_type={msg_type}, image={image_path}"
|
|
466
|
+
)
|
|
467
|
+
await self._send_image(target_id, msg_type, image_path)
|
|
468
|
+
|
|
469
|
+
# 定义点赞回调
|
|
470
|
+
async def send_like_callback(target_user_id: int, times: int = 1) -> None:
|
|
471
|
+
logger.debug(
|
|
472
|
+
f"send_like_callback: target_user_id={target_user_id}, times={times}"
|
|
473
|
+
)
|
|
474
|
+
await self.onebot.send_like(target_user_id, times)
|
|
475
|
+
|
|
476
|
+
try:
|
|
477
|
+
self.ai.current_group_id = group_id
|
|
478
|
+
self.ai.current_user_id = sender_id
|
|
479
|
+
self.ai._send_private_message_callback = send_private_message_callback
|
|
480
|
+
self.ai._send_image_callback = send_image_callback
|
|
481
|
+
|
|
482
|
+
await self.ai.ask(
|
|
483
|
+
full_question,
|
|
484
|
+
send_message_callback=send_message_callback,
|
|
485
|
+
get_recent_messages_callback=get_recent_messages_callback,
|
|
486
|
+
get_image_url_callback=self.onebot.get_image,
|
|
487
|
+
get_forward_msg_callback=self.onebot.get_forward_msg,
|
|
488
|
+
send_like_callback=send_like_callback,
|
|
489
|
+
sender=self.sender,
|
|
490
|
+
history_manager=self.history_manager,
|
|
491
|
+
onebot_client=self.onebot,
|
|
492
|
+
scheduler=self.scheduler,
|
|
493
|
+
)
|
|
494
|
+
except Exception as e:
|
|
495
|
+
logger.error(f"自动回复处理出错: {e}")
|
|
496
|
+
|
|
497
|
+
async def _execute_private_reply(self, request: dict[str, Any]) -> None:
|
|
498
|
+
"""执行私聊回复请求"""
|
|
499
|
+
user_id = request["user_id"]
|
|
500
|
+
full_question = request["full_question"]
|
|
501
|
+
|
|
502
|
+
# 定义回调 - 使用 sender (private)
|
|
503
|
+
async def send_message_callback(
|
|
504
|
+
message: str, at_user: int | None = None
|
|
505
|
+
) -> None:
|
|
506
|
+
await self.sender.send_private_message(user_id, message)
|
|
507
|
+
# sender 内部已经自动保存历史,不需要手动调用
|
|
508
|
+
|
|
509
|
+
# 获取私聊历史消息
|
|
510
|
+
async def get_recent_messages_callback(
|
|
511
|
+
chat_id: str, msg_type: str, start: int, end: int
|
|
512
|
+
) -> list[dict[str, Any]]:
|
|
513
|
+
return self.history_manager.get_recent(chat_id, msg_type, start, end)
|
|
514
|
+
|
|
515
|
+
# 定义发送图片回调
|
|
516
|
+
async def send_image_callback(
|
|
517
|
+
target_id: int, msg_type: str, image_path: str
|
|
518
|
+
) -> None:
|
|
519
|
+
logger.debug(
|
|
520
|
+
f"send_image_callback: target_id={target_id}, msg_type={msg_type}, image={image_path}"
|
|
521
|
+
)
|
|
522
|
+
await self._send_image(target_id, msg_type, image_path)
|
|
523
|
+
|
|
524
|
+
# 定义点赞回调
|
|
525
|
+
async def send_like_callback(target_user_id: int, times: int = 1) -> None:
|
|
526
|
+
logger.debug(
|
|
527
|
+
f"send_like_callback: target_user_id={target_user_id}, times={times}"
|
|
528
|
+
)
|
|
529
|
+
await self.onebot.send_like(target_user_id, times)
|
|
530
|
+
|
|
531
|
+
try:
|
|
532
|
+
self.ai.current_group_id = None
|
|
533
|
+
self.ai.current_user_id = user_id
|
|
534
|
+
self.ai._send_image_callback = send_image_callback
|
|
535
|
+
result = await self.ai.ask(
|
|
536
|
+
full_question,
|
|
537
|
+
send_message_callback=send_message_callback,
|
|
538
|
+
get_recent_messages_callback=get_recent_messages_callback,
|
|
539
|
+
get_image_url_callback=self.onebot.get_image,
|
|
540
|
+
get_forward_msg_callback=self.onebot.get_forward_msg,
|
|
541
|
+
send_like_callback=send_like_callback,
|
|
542
|
+
sender=self.sender,
|
|
543
|
+
history_manager=self.history_manager,
|
|
544
|
+
onebot_client=self.onebot,
|
|
545
|
+
scheduler=self.scheduler,
|
|
546
|
+
)
|
|
547
|
+
# 如果 AI 直接返回了文本(没有调用工具),自动发送
|
|
548
|
+
if result:
|
|
549
|
+
logger.info(f"AI 直接返回文本,自动发送私聊消息: {result[:50]}...")
|
|
550
|
+
await self.sender.send_private_message(user_id, result)
|
|
551
|
+
# sender 内部已自动保存历史
|
|
552
|
+
except Exception as e:
|
|
553
|
+
logger.error(f"私聊回复处理出错: {e}")
|
|
554
|
+
|
|
555
|
+
async def _handle_auto_reply(
|
|
556
|
+
self,
|
|
557
|
+
group_id: int,
|
|
558
|
+
sender_id: int,
|
|
559
|
+
text: str,
|
|
560
|
+
message_content: list[dict[str, Any]],
|
|
561
|
+
is_poke: bool = False,
|
|
562
|
+
sender_name: str = "未知用户",
|
|
563
|
+
group_name: str = "未知群聊",
|
|
564
|
+
) -> None:
|
|
565
|
+
"""自动回复处理:根据上下文决定是否回复"""
|
|
566
|
+
is_at_bot = is_poke or self._is_at_bot(message_content)
|
|
567
|
+
|
|
568
|
+
if sender_id != self.config.superadmin_qq:
|
|
569
|
+
logger.debug(
|
|
570
|
+
f"[安全检测] 正在进行注入检测: group={group_id}, user={sender_id}, text={text[:50]}..."
|
|
571
|
+
)
|
|
572
|
+
is_injection = await self.ai.detect_injection(text, message_content)
|
|
573
|
+
if is_injection:
|
|
574
|
+
logger.warning(
|
|
575
|
+
f"[安全警告] 检测到提示词注入攻击: group={group_id}, user={sender_id}, text={text[:200]}"
|
|
576
|
+
)
|
|
577
|
+
self.history_manager.modify_last_group_message(
|
|
578
|
+
group_id, sender_id, "<这句话检测到用户进行注入,已删除>"
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
if is_at_bot:
|
|
582
|
+
await self._handle_injection_response(
|
|
583
|
+
group_id, text, sender_id=sender_id
|
|
584
|
+
)
|
|
585
|
+
return
|
|
586
|
+
else:
|
|
587
|
+
logger.debug(
|
|
588
|
+
f"[安全检测] 注入检测通过: group={group_id}, user={sender_id}"
|
|
589
|
+
)
|
|
590
|
+
|
|
591
|
+
prompt_prefix = ""
|
|
592
|
+
if is_poke:
|
|
593
|
+
prompt_prefix = "(用户拍了拍你) "
|
|
594
|
+
elif is_at_bot:
|
|
595
|
+
prompt_prefix = "(用户 @ 了你) "
|
|
596
|
+
|
|
597
|
+
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
598
|
+
location = group_name if group_name.endswith("群") else f"{group_name}群"
|
|
599
|
+
|
|
600
|
+
full_question = f"""{prompt_prefix}<message sender="{sender_name}" sender_id="{sender_id}" location="{location}" time="{current_time}">
|
|
601
|
+
<content>{text}</content>
|
|
602
|
+
</message>
|
|
603
|
+
|
|
604
|
+
【回复策略 - 极低频参与】
|
|
605
|
+
1. 如果用户 @ 了你或拍了拍你 → 【必须回复】
|
|
606
|
+
2. 如果消息中明确提到了你(根据上下文判断用户是在叫你,如提到'bugfix'、'机器人'、'bot'等) → 【必须回复】
|
|
607
|
+
3. 如果问题明确涉及 NagaAgent 技术或代码 → 【尽量回复,先读代码再回答】
|
|
608
|
+
4. 其他技术问题(与 NagaAgent 无关)→ 【酌情回复,可结合自己知识或搜索】
|
|
609
|
+
5. 普通闲聊、水群、吐槽:
|
|
610
|
+
- 【几乎不回复】(99.9% 以上情况直接调用 end 不回复)
|
|
611
|
+
- 不要发送任何敷衍消息(如'懒得掺和'、'哦'等),不想回复就直接调用 end
|
|
612
|
+
- 只有内容极其有趣、特别相关、能提供独特价值时才考虑回复
|
|
613
|
+
- 不要为了"参与"而参与,保持安静
|
|
614
|
+
- 绝不要刷屏、绝不要每条都回
|
|
615
|
+
|
|
616
|
+
简单说:像个极度安静的群友。被@或明确提到才回应,NagaAgent技术问题尽量回复,其他几乎不理。"""
|
|
617
|
+
|
|
618
|
+
if is_at_bot:
|
|
619
|
+
logger.info(f"[自动回复] 触发原因: {'拍一拍' if is_poke else '@机器人'}")
|
|
620
|
+
await self.queue_manager.add_group_mention_request(
|
|
621
|
+
{
|
|
622
|
+
"type": "auto_reply",
|
|
623
|
+
"group_id": group_id,
|
|
624
|
+
"sender_id": sender_id,
|
|
625
|
+
"text": text,
|
|
626
|
+
"full_question": full_question,
|
|
627
|
+
"is_at_bot": is_at_bot,
|
|
628
|
+
}
|
|
629
|
+
)
|
|
630
|
+
else:
|
|
631
|
+
logger.info("[自动回复] 投递至普通请求队列 (非 @ 消息)")
|
|
632
|
+
await self.queue_manager.add_group_normal_request(
|
|
633
|
+
{
|
|
634
|
+
"type": "auto_reply",
|
|
635
|
+
"group_id": group_id,
|
|
636
|
+
"sender_id": sender_id,
|
|
637
|
+
"text": text,
|
|
638
|
+
"full_question": full_question,
|
|
639
|
+
"is_at_bot": is_at_bot,
|
|
640
|
+
}
|
|
641
|
+
)
|
|
642
|
+
|
|
643
|
+
async def _handle_private_reply(
|
|
644
|
+
self,
|
|
645
|
+
user_id: int,
|
|
646
|
+
text: str,
|
|
647
|
+
message_content: list[dict[str, Any]],
|
|
648
|
+
is_poke: bool = False,
|
|
649
|
+
sender_name: str = "未知用户",
|
|
650
|
+
) -> None:
|
|
651
|
+
"""私聊回复处理"""
|
|
652
|
+
is_superadmin = user_id == self.config.superadmin_qq
|
|
653
|
+
|
|
654
|
+
if not is_superadmin:
|
|
655
|
+
logger.info(
|
|
656
|
+
f"对私聊消息进行注入检测: user_id={user_id}, text={text[:50]}..."
|
|
657
|
+
)
|
|
658
|
+
is_injection = await self.ai.detect_injection(text, message_content)
|
|
659
|
+
if is_injection:
|
|
660
|
+
logger.warning(
|
|
661
|
+
f"检测到提示词注入攻击: user_id={user_id}, text={text[:100]}..."
|
|
662
|
+
)
|
|
663
|
+
self.history_manager.modify_last_private_message(
|
|
664
|
+
user_id, "<这句话检测到用户进行注入,已删除>"
|
|
665
|
+
)
|
|
666
|
+
await self._handle_injection_response(user_id, text, is_private=True)
|
|
667
|
+
return
|
|
668
|
+
|
|
669
|
+
prompt_prefix = "(用户拍了拍你) " if is_poke else ""
|
|
670
|
+
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
671
|
+
full_question = f"""{prompt_prefix}<message sender="{sender_name}" sender_id="{user_id}" location="私聊" time="{current_time}">
|
|
672
|
+
<content>{text}</content>
|
|
673
|
+
</message>
|
|
674
|
+
|
|
675
|
+
【私聊消息】
|
|
676
|
+
|
|
677
|
+
这是私聊消息,用户专门来找你说话。你可以自由选择是否回复:
|
|
678
|
+
- 如果想回复,先调用 send_message 工具发送回复内容,然后调用 end 结束对话
|
|
679
|
+
- 如果不想回复,直接调用 end 结束对话即可"""
|
|
680
|
+
|
|
681
|
+
is_superadmin = user_id == self.config.superadmin_qq
|
|
682
|
+
|
|
683
|
+
if is_superadmin:
|
|
684
|
+
await self.queue_manager.add_superadmin_request(
|
|
685
|
+
{
|
|
686
|
+
"type": "private_reply",
|
|
687
|
+
"user_id": user_id,
|
|
688
|
+
"text": text,
|
|
689
|
+
"full_question": full_question,
|
|
690
|
+
}
|
|
691
|
+
)
|
|
692
|
+
else:
|
|
693
|
+
await self.queue_manager.add_private_request(
|
|
694
|
+
{
|
|
695
|
+
"type": "private_reply",
|
|
696
|
+
"user_id": user_id,
|
|
697
|
+
"text": text,
|
|
698
|
+
"full_question": full_question,
|
|
699
|
+
}
|
|
700
|
+
)
|
|
701
|
+
|
|
702
|
+
async def _send_image(
|
|
703
|
+
self, target_id: int, message_type: str, image_path: str
|
|
704
|
+
) -> None:
|
|
705
|
+
"""发送图片或音频到指定目标(群聊或私聊)
|
|
706
|
+
|
|
707
|
+
参数:
|
|
708
|
+
target_id: 目标 ID(群号或用户 QQ 号)
|
|
709
|
+
message_type: 消息类型(group 或 private)
|
|
710
|
+
image_path: 媒体文件路径
|
|
711
|
+
"""
|
|
712
|
+
# 检查文件是否存在
|
|
713
|
+
if not os.path.exists(image_path):
|
|
714
|
+
logger.error(f"文件不存在: {image_path}")
|
|
715
|
+
return
|
|
716
|
+
|
|
717
|
+
# 使用绝对路径
|
|
718
|
+
abs_path = os.path.abspath(image_path)
|
|
719
|
+
# 根据文件扩展名确定消息类型
|
|
720
|
+
ext = os.path.splitext(image_path)[1].lower()
|
|
721
|
+
|
|
722
|
+
# 检查文件大小(限制在100MB以内)
|
|
723
|
+
file_size = os.path.getsize(abs_path)
|
|
724
|
+
MAX_FILE_SIZE = 100 * 1024 * 1024 # 100MB
|
|
725
|
+
|
|
726
|
+
if file_size > MAX_FILE_SIZE:
|
|
727
|
+
logger.error(f"文件过大: {file_size}字节 > {MAX_FILE_SIZE}字节限制")
|
|
728
|
+
return
|
|
729
|
+
|
|
730
|
+
if ext in [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp"]:
|
|
731
|
+
# 图片文件
|
|
732
|
+
message = f"[CQ:image,file={abs_path}]"
|
|
733
|
+
media_type = "图片"
|
|
734
|
+
elif ext in [".mp3", ".wav", ".ogg", ".flac", ".m4a", ".aac"]:
|
|
735
|
+
# 音频文件,统一使用record格式尝试发送
|
|
736
|
+
message = f"[CQ:record,file={abs_path}]"
|
|
737
|
+
media_type = "音频"
|
|
738
|
+
else:
|
|
739
|
+
logger.error(f"不支持的媒体文件格式: {ext}")
|
|
740
|
+
return
|
|
741
|
+
|
|
742
|
+
try:
|
|
743
|
+
if message_type == "group":
|
|
744
|
+
await self.onebot.send_group_message(target_id, message)
|
|
745
|
+
logger.info(
|
|
746
|
+
f"已发送{media_type}到群聊 {target_id}: {image_path} (大小: {file_size}字节)"
|
|
747
|
+
)
|
|
748
|
+
elif message_type == "private":
|
|
749
|
+
await self.onebot.send_private_message(target_id, message)
|
|
750
|
+
logger.info(
|
|
751
|
+
f"已发送{media_type}到私聊 {target_id}: {image_path} (大小: {file_size}字节)"
|
|
752
|
+
)
|
|
753
|
+
else:
|
|
754
|
+
logger.error(f"未知的消息类型: {message_type}")
|
|
755
|
+
except Exception as e:
|
|
756
|
+
logger.exception(f"发送{media_type}失败: {e}")
|
|
757
|
+
# 重新抛出异常,让上层处理
|
|
758
|
+
raise
|
|
759
|
+
|
|
760
|
+
async def _handle_injection_response(
|
|
761
|
+
self,
|
|
762
|
+
target_id: int,
|
|
763
|
+
original_message: str,
|
|
764
|
+
is_private: bool = False,
|
|
765
|
+
sender_id: int | None = None,
|
|
766
|
+
) -> None:
|
|
767
|
+
"""处理注入攻击的回复(使用 undefined 人设)"""
|
|
768
|
+
reply = await self.injection_response_agent.generate_response(original_message)
|
|
769
|
+
|
|
770
|
+
if is_private:
|
|
771
|
+
await self.sender.send_private_message(target_id, reply, auto_history=False)
|
|
772
|
+
# 历史记录中仅保留占位符
|
|
773
|
+
self.history_manager.add_private_message(
|
|
774
|
+
user_id=target_id,
|
|
775
|
+
text_content="<对注入消息的回复>",
|
|
776
|
+
display_name="Bot",
|
|
777
|
+
user_name="Bot",
|
|
778
|
+
)
|
|
779
|
+
logger.info(f"已发送注入攻击警告(私聊): user_id={target_id}")
|
|
780
|
+
else:
|
|
781
|
+
if sender_id:
|
|
782
|
+
reply_with_at = f"[CQ:at,qq={sender_id}] {reply}"
|
|
783
|
+
await self.sender.send_group_message(
|
|
784
|
+
target_id, reply_with_at, auto_history=False
|
|
785
|
+
)
|
|
786
|
+
else:
|
|
787
|
+
await self.sender.send_group_message(
|
|
788
|
+
target_id, reply, auto_history=False
|
|
789
|
+
)
|
|
790
|
+
|
|
791
|
+
# 历史记录中仅保留占位符
|
|
792
|
+
self.history_manager.add_group_message(
|
|
793
|
+
group_id=target_id,
|
|
794
|
+
sender_id=self.config.bot_qq,
|
|
795
|
+
text_content="<对注入消息的回复>",
|
|
796
|
+
sender_nickname="Bot",
|
|
797
|
+
group_name="",
|
|
798
|
+
)
|
|
799
|
+
logger.info(
|
|
800
|
+
f"已发送注入攻击警告(群聊): group_id={target_id}, sender_id={sender_id}"
|
|
801
|
+
)
|
|
802
|
+
|
|
803
|
+
async def _check_rate_limit_and_handle(
|
|
804
|
+
self, group_id: int, user_id: int, handler: Any, *args: Any
|
|
805
|
+
) -> None:
|
|
806
|
+
"""检查速率限制并执行处理器"""
|
|
807
|
+
allowed, remaining = self.rate_limiter.check(user_id)
|
|
808
|
+
|
|
809
|
+
if not allowed:
|
|
810
|
+
await self.sender.send_group_message(
|
|
811
|
+
group_id, f"⏳ 操作太频繁,请 {remaining} 秒后再试"
|
|
812
|
+
)
|
|
813
|
+
return
|
|
814
|
+
|
|
815
|
+
self.rate_limiter.record(user_id)
|
|
816
|
+
await handler(*args)
|
|
817
|
+
|
|
818
|
+
def _is_at_bot(self, message_content: list[dict[str, Any]]) -> bool:
|
|
819
|
+
"""检查消息是否 @ 了机器人"""
|
|
820
|
+
for segment in message_content:
|
|
821
|
+
if segment.get("type") == "at":
|
|
822
|
+
qq = segment.get("data", {}).get("qq", "")
|
|
823
|
+
if str(qq) == str(self.config.bot_qq):
|
|
824
|
+
return True
|
|
825
|
+
return False
|
|
826
|
+
|
|
827
|
+
def _parse_command(self, text: str) -> dict[str, Any] | None:
|
|
828
|
+
"""解析命令"""
|
|
829
|
+
clean_text = re.sub(r"\[@\s*\d+\]", "", text).strip()
|
|
830
|
+
match = re.match(r"/(\w+)\s*(.*)", clean_text)
|
|
831
|
+
if not match:
|
|
832
|
+
return None
|
|
833
|
+
|
|
834
|
+
cmd_name = match.group(1).lower()
|
|
835
|
+
args_str = match.group(2).strip()
|
|
836
|
+
|
|
837
|
+
return {
|
|
838
|
+
"name": cmd_name,
|
|
839
|
+
"args": args_str.split() if args_str else [],
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
async def _handle_help(self, group_id: int) -> None:
|
|
843
|
+
"""处理 /help 命令"""
|
|
844
|
+
await self.sender.send_group_message(group_id, HELP_MESSAGE)
|
|
845
|
+
|
|
846
|
+
async def _handle_lsfaq(self, group_id: int) -> None:
|
|
847
|
+
"""处理 /lsfaq 命令"""
|
|
848
|
+
faqs = self.faq_storage.list_all(group_id)
|
|
849
|
+
|
|
850
|
+
if not faqs:
|
|
851
|
+
await self.sender.send_group_message(group_id, "📭 当前群组没有保存的 FAQ")
|
|
852
|
+
return
|
|
853
|
+
|
|
854
|
+
lines = ["📋 FAQ 列表:", ""]
|
|
855
|
+
for faq in faqs[:20]:
|
|
856
|
+
lines.append(f"📌 [{faq.id}] {faq.title}")
|
|
857
|
+
lines.append(f" 创建时间: {faq.created_at[:10]}")
|
|
858
|
+
lines.append("")
|
|
859
|
+
|
|
860
|
+
if len(faqs) > 20:
|
|
861
|
+
lines.append(f"... 还有 {len(faqs) - 20} 条")
|
|
862
|
+
|
|
863
|
+
await self.sender.send_group_message(group_id, "\n".join(lines))
|
|
864
|
+
|
|
865
|
+
async def _handle_viewfaq(self, group_id: int, args: list[str]) -> None:
|
|
866
|
+
"""处理 /viewfaq 命令"""
|
|
867
|
+
if not args:
|
|
868
|
+
await self.sender.send_group_message(
|
|
869
|
+
group_id, "❌ 用法: /viewfaq <ID>\n示例: /viewfaq 20241205-001"
|
|
870
|
+
)
|
|
871
|
+
return
|
|
872
|
+
|
|
873
|
+
faq_id = args[0]
|
|
874
|
+
faq = self.faq_storage.get(group_id, faq_id)
|
|
875
|
+
|
|
876
|
+
if not faq:
|
|
877
|
+
await self.sender.send_group_message(group_id, f"❌ FAQ 不存在: {faq_id}")
|
|
878
|
+
return
|
|
879
|
+
|
|
880
|
+
message = f"""📖 FAQ: {faq.title}
|
|
881
|
+
|
|
882
|
+
🆔 ID: {faq.id}
|
|
883
|
+
👤 分析对象: {faq.target_qq}
|
|
884
|
+
📅 时间范围: {faq.start_time} ~ {faq.end_time}
|
|
885
|
+
🕐 创建时间: {faq.created_at}
|
|
886
|
+
|
|
887
|
+
{faq.content}"""
|
|
888
|
+
|
|
889
|
+
await self.sender.send_group_message(group_id, message)
|
|
890
|
+
|
|
891
|
+
async def _handle_searchfaq(self, group_id: int, args: list[str]) -> None:
|
|
892
|
+
"""处理 /searchfaq 命令"""
|
|
893
|
+
if not args:
|
|
894
|
+
await self.sender.send_group_message(
|
|
895
|
+
group_id, "❌ 用法: /searchfaq <关键词>\n示例: /searchfaq 登录"
|
|
896
|
+
)
|
|
897
|
+
return
|
|
898
|
+
|
|
899
|
+
keyword = " ".join(args)
|
|
900
|
+
results = self.faq_storage.search(group_id, keyword)
|
|
901
|
+
|
|
902
|
+
if not results:
|
|
903
|
+
await self.sender.send_group_message(
|
|
904
|
+
group_id, f'🔍 未找到包含 "{keyword}" 的 FAQ'
|
|
905
|
+
)
|
|
906
|
+
return
|
|
907
|
+
|
|
908
|
+
lines = [f'🔍 搜索 "{keyword}" 找到 {len(results)} 条结果:', ""]
|
|
909
|
+
for faq in results[:10]:
|
|
910
|
+
lines.append(f"📌 [{faq.id}] {faq.title}")
|
|
911
|
+
lines.append("")
|
|
912
|
+
|
|
913
|
+
if len(results) > 10:
|
|
914
|
+
lines.append(f"... 还有 {len(results) - 10} 条")
|
|
915
|
+
|
|
916
|
+
lines.append("\n使用 /viewfaq <ID> 查看详情")
|
|
917
|
+
|
|
918
|
+
await self.sender.send_group_message(group_id, "\n".join(lines))
|
|
919
|
+
|
|
920
|
+
async def _handle_delfaq(self, group_id: int, args: list[str]) -> None:
|
|
921
|
+
"""处理 /delfaq 命令"""
|
|
922
|
+
if not args:
|
|
923
|
+
await self.sender.send_group_message(
|
|
924
|
+
group_id, "❌ 用法: /delfaq <ID>\n示例: /delfaq 20241205-001"
|
|
925
|
+
)
|
|
926
|
+
return
|
|
927
|
+
|
|
928
|
+
faq_id = args[0]
|
|
929
|
+
|
|
930
|
+
faq = self.faq_storage.get(group_id, faq_id)
|
|
931
|
+
if not faq:
|
|
932
|
+
await self.sender.send_group_message(group_id, f"❌ FAQ 不存在: {faq_id}")
|
|
933
|
+
return
|
|
934
|
+
|
|
935
|
+
success = self.faq_storage.delete(group_id, faq_id)
|
|
936
|
+
if success:
|
|
937
|
+
await self.sender.send_group_message(
|
|
938
|
+
group_id, f"✅ 已删除 FAQ: [{faq_id}] {faq.title}"
|
|
939
|
+
)
|
|
940
|
+
else:
|
|
941
|
+
await self.sender.send_group_message(group_id, f"❌ 删除失败: {faq_id}")
|
|
942
|
+
|
|
943
|
+
async def _handle_lsadmin(self, group_id: int) -> None:
|
|
944
|
+
"""处理 /lsadmin 命令"""
|
|
945
|
+
lines: list[str] = []
|
|
946
|
+
lines.append(f"👑 超级管理员: {self.config.superadmin_qq}")
|
|
947
|
+
|
|
948
|
+
admins = [qq for qq in self.config.admin_qqs if qq != self.config.superadmin_qq]
|
|
949
|
+
if admins:
|
|
950
|
+
admin_list = "\n".join([f"- {qq}" for qq in admins])
|
|
951
|
+
lines.append(f"\n📋 管理员列表:\n{admin_list}")
|
|
952
|
+
else:
|
|
953
|
+
lines.append("\n📋 暂无其他管理员")
|
|
954
|
+
|
|
955
|
+
await self.sender.send_group_message(group_id, "\n".join(lines))
|
|
956
|
+
|
|
957
|
+
async def _handle_addadmin(self, group_id: int, args: list[str]) -> None:
|
|
958
|
+
"""处理 /addadmin 命令"""
|
|
959
|
+
if not args:
|
|
960
|
+
await self.sender.send_group_message(
|
|
961
|
+
group_id, "❌ 用法: /addadmin <QQ号>\n示例: /addadmin 123456789"
|
|
962
|
+
)
|
|
963
|
+
return
|
|
964
|
+
|
|
965
|
+
try:
|
|
966
|
+
new_admin_qq = int(args[0])
|
|
967
|
+
except ValueError:
|
|
968
|
+
await self.sender.send_group_message(
|
|
969
|
+
group_id, "❌ QQ 号格式错误,必须为数字"
|
|
970
|
+
)
|
|
971
|
+
return
|
|
972
|
+
|
|
973
|
+
if self.config.is_admin(new_admin_qq):
|
|
974
|
+
await self.sender.send_group_message(
|
|
975
|
+
group_id, f"⚠️ {new_admin_qq} 已经是管理员了"
|
|
976
|
+
)
|
|
977
|
+
return
|
|
978
|
+
|
|
979
|
+
try:
|
|
980
|
+
self.config.add_admin(new_admin_qq)
|
|
981
|
+
await self.sender.send_group_message(
|
|
982
|
+
group_id, f"✅ 已添加管理员: {new_admin_qq}"
|
|
983
|
+
)
|
|
984
|
+
except Exception as e:
|
|
985
|
+
logger.exception(f"添加管理员失败: {e}")
|
|
986
|
+
await self.sender.send_group_message(group_id, f"❌ 添加管理员失败: {e}")
|
|
987
|
+
|
|
988
|
+
async def _handle_rmadmin(self, group_id: int, args: list[str]) -> None:
|
|
989
|
+
"""处理 /rmadmin 命令"""
|
|
990
|
+
if not args:
|
|
991
|
+
await self.sender.send_group_message(
|
|
992
|
+
group_id, "❌ 用法: /rmadmin <QQ号>\n示例: /rmadmin 123456789"
|
|
993
|
+
)
|
|
994
|
+
return
|
|
995
|
+
|
|
996
|
+
try:
|
|
997
|
+
target_qq = int(args[0])
|
|
998
|
+
except ValueError:
|
|
999
|
+
await self.sender.send_group_message(
|
|
1000
|
+
group_id, "❌ QQ 号格式错误,必须为数字"
|
|
1001
|
+
)
|
|
1002
|
+
return
|
|
1003
|
+
|
|
1004
|
+
if self.config.is_superadmin(target_qq):
|
|
1005
|
+
await self.sender.send_group_message(group_id, "❌ 无法移除超级管理员")
|
|
1006
|
+
return
|
|
1007
|
+
|
|
1008
|
+
if not self.config.is_admin(target_qq):
|
|
1009
|
+
await self.sender.send_group_message(group_id, f"⚠️ {target_qq} 不是管理员")
|
|
1010
|
+
return
|
|
1011
|
+
|
|
1012
|
+
try:
|
|
1013
|
+
self.config.remove_admin(target_qq)
|
|
1014
|
+
await self.sender.send_group_message(
|
|
1015
|
+
group_id, f"✅ 已移除管理员: {target_qq}"
|
|
1016
|
+
)
|
|
1017
|
+
except Exception as e:
|
|
1018
|
+
logger.exception(f"移除管理员失败: {e}")
|
|
1019
|
+
await self.sender.send_group_message(group_id, f"❌ 移除管理员失败: {e}")
|
|
1020
|
+
|
|
1021
|
+
async def _handle_bugfix(
|
|
1022
|
+
self, group_id: int, admin_id: int, args: list[str]
|
|
1023
|
+
) -> None:
|
|
1024
|
+
"""处理 /bugfix 命令"""
|
|
1025
|
+
if len(args) < 3:
|
|
1026
|
+
await self.sender.send_group_message(
|
|
1027
|
+
group_id,
|
|
1028
|
+
"❌ 用法: /bugfix <QQ号1> [QQ号2] ... <开始时间> <结束时间>\n"
|
|
1029
|
+
"时间格式: YYYY/MM/DD/HH:MM,结束时间可用 now\n"
|
|
1030
|
+
"示例: /bugfix 123456 2024/12/01/09:00 now",
|
|
1031
|
+
)
|
|
1032
|
+
return
|
|
1033
|
+
|
|
1034
|
+
target_qqs: list[int] = []
|
|
1035
|
+
time_args = args[-2:]
|
|
1036
|
+
qq_args = args[:-2]
|
|
1037
|
+
|
|
1038
|
+
try:
|
|
1039
|
+
for arg in qq_args:
|
|
1040
|
+
target_qqs.append(int(arg))
|
|
1041
|
+
except ValueError:
|
|
1042
|
+
await self.sender.send_group_message(
|
|
1043
|
+
group_id, "❌ QQ 号格式错误,必须为数字"
|
|
1044
|
+
)
|
|
1045
|
+
return
|
|
1046
|
+
|
|
1047
|
+
if not target_qqs:
|
|
1048
|
+
await self.sender.send_group_message(group_id, "❌ 请至少指定一个目标 QQ")
|
|
1049
|
+
return
|
|
1050
|
+
|
|
1051
|
+
try:
|
|
1052
|
+
start_date = datetime.strptime(time_args[0], "%Y/%m/%d/%H:%M")
|
|
1053
|
+
if time_args[1].lower() == "now":
|
|
1054
|
+
end_date = datetime.now()
|
|
1055
|
+
end_date_str = "now"
|
|
1056
|
+
else:
|
|
1057
|
+
end_date = datetime.strptime(time_args[1], "%Y/%m/%d/%H:%M")
|
|
1058
|
+
end_date_str = time_args[1]
|
|
1059
|
+
except ValueError:
|
|
1060
|
+
await self.sender.send_group_message(
|
|
1061
|
+
group_id,
|
|
1062
|
+
"❌ 时间格式错误,请使用 YYYY/MM/DD/HH:MM 格式\n示例: 2024/12/01/09:00",
|
|
1063
|
+
)
|
|
1064
|
+
return
|
|
1065
|
+
|
|
1066
|
+
targets_str = ", ".join(map(str, target_qqs))
|
|
1067
|
+
await self.sender.send_group_message(
|
|
1068
|
+
group_id,
|
|
1069
|
+
f"🔍 正在获取与 {targets_str} 在 {time_args[0]} ~ {end_date_str} 的对话记录...",
|
|
1070
|
+
)
|
|
1071
|
+
|
|
1072
|
+
try:
|
|
1073
|
+
messages = await self._fetch_messages(
|
|
1074
|
+
group_id, target_qqs, start_date, end_date
|
|
1075
|
+
)
|
|
1076
|
+
except Exception as e:
|
|
1077
|
+
logger.exception(f"获取消息历史失败: {e}")
|
|
1078
|
+
await self.sender.send_group_message(group_id, f"❌ 获取消息历史失败: {e}")
|
|
1079
|
+
return
|
|
1080
|
+
|
|
1081
|
+
if not messages:
|
|
1082
|
+
await self.sender.send_group_message(
|
|
1083
|
+
group_id, "❌ 未找到符合条件的对话记录"
|
|
1084
|
+
)
|
|
1085
|
+
return
|
|
1086
|
+
|
|
1087
|
+
logger.info(f"找到 {len(messages)} 条消息,正在处理...")
|
|
1088
|
+
|
|
1089
|
+
processed_text = await self._process_messages(messages)
|
|
1090
|
+
|
|
1091
|
+
total_tokens = self.ai.count_tokens(processed_text)
|
|
1092
|
+
max_tokens = self.config.chat_model.max_tokens
|
|
1093
|
+
|
|
1094
|
+
if total_tokens <= max_tokens:
|
|
1095
|
+
summary = await self.ai.summarize_chat(processed_text)
|
|
1096
|
+
else:
|
|
1097
|
+
await self.sender.send_group_message(
|
|
1098
|
+
group_id, f"📊 消息较长({total_tokens} tokens),正在分段处理..."
|
|
1099
|
+
)
|
|
1100
|
+
|
|
1101
|
+
chunks = self.ai.split_messages_by_tokens(processed_text, max_tokens)
|
|
1102
|
+
summaries: list[str] = []
|
|
1103
|
+
|
|
1104
|
+
for i, chunk in enumerate(chunks):
|
|
1105
|
+
logger.info(f"处理分段 {i + 1}/{len(chunks)}...")
|
|
1106
|
+
chunk_summary = await self.ai.summarize_chat(chunk)
|
|
1107
|
+
summaries.append(chunk_summary)
|
|
1108
|
+
|
|
1109
|
+
summary = await self.ai.merge_summaries(summaries)
|
|
1110
|
+
|
|
1111
|
+
title = extract_faq_title(summary)
|
|
1112
|
+
if not title or title == "未命名问题":
|
|
1113
|
+
logger.info("无法提取标题,尝试使用 AI 生成...")
|
|
1114
|
+
title = await self.ai.generate_title(summary)
|
|
1115
|
+
|
|
1116
|
+
faq = self.faq_storage.create(
|
|
1117
|
+
group_id=group_id,
|
|
1118
|
+
target_qq=target_qqs[0],
|
|
1119
|
+
start_time=time_args[0],
|
|
1120
|
+
end_time=end_date_str,
|
|
1121
|
+
title=title,
|
|
1122
|
+
content=summary,
|
|
1123
|
+
)
|
|
1124
|
+
|
|
1125
|
+
result_message = f"""✅ Bug 修复分析完成!
|
|
1126
|
+
|
|
1127
|
+
📌 FAQ ID: {faq.id}
|
|
1128
|
+
📋 标题: {title}
|
|
1129
|
+
|
|
1130
|
+
{summary}
|
|
1131
|
+
|
|
1132
|
+
💡 使用 /viewfaq {faq.id} 可以再次查看此 FAQ"""
|
|
1133
|
+
|
|
1134
|
+
await self.sender.send_group_message(group_id, result_message)
|
|
1135
|
+
|
|
1136
|
+
async def _fetch_messages(
|
|
1137
|
+
self,
|
|
1138
|
+
group_id: int,
|
|
1139
|
+
target_qqs: list[int],
|
|
1140
|
+
start_date: datetime,
|
|
1141
|
+
end_date: datetime,
|
|
1142
|
+
) -> list[dict[str, Any]]:
|
|
1143
|
+
"""获取指定时间段内与目标用户的对话"""
|
|
1144
|
+
all_messages: list[dict[str, Any]] = []
|
|
1145
|
+
|
|
1146
|
+
logger.info(
|
|
1147
|
+
f"开始获取消息历史: group={group_id}, targets={target_qqs}, "
|
|
1148
|
+
f"start={start_date}, end={end_date}"
|
|
1149
|
+
)
|
|
1150
|
+
|
|
1151
|
+
try:
|
|
1152
|
+
batch = await self.onebot.get_group_msg_history(
|
|
1153
|
+
group_id,
|
|
1154
|
+
count=2500,
|
|
1155
|
+
)
|
|
1156
|
+
except RuntimeError as e:
|
|
1157
|
+
logger.error(f"获取历史消息失败: {e}")
|
|
1158
|
+
raise
|
|
1159
|
+
|
|
1160
|
+
if not batch:
|
|
1161
|
+
logger.info("没有获取到任何消息")
|
|
1162
|
+
return []
|
|
1163
|
+
|
|
1164
|
+
first_time = parse_message_time(batch[0])
|
|
1165
|
+
last_time = parse_message_time(batch[-1])
|
|
1166
|
+
logger.info(f"获取到 {len(batch)} 条消息, 时间范围: {last_time} ~ {first_time}")
|
|
1167
|
+
|
|
1168
|
+
for msg in batch:
|
|
1169
|
+
msg_time = parse_message_time(msg)
|
|
1170
|
+
sender_id = get_message_sender_id(msg)
|
|
1171
|
+
|
|
1172
|
+
if msg_time < start_date:
|
|
1173
|
+
continue
|
|
1174
|
+
|
|
1175
|
+
if msg_time > end_date:
|
|
1176
|
+
continue
|
|
1177
|
+
|
|
1178
|
+
if sender_id in target_qqs:
|
|
1179
|
+
all_messages.append(msg)
|
|
1180
|
+
|
|
1181
|
+
logger.info(f"共获取到 {len(all_messages)} 条符合条件的消息")
|
|
1182
|
+
|
|
1183
|
+
all_messages.sort(key=lambda m: m.get("time", 0))
|
|
1184
|
+
return all_messages
|
|
1185
|
+
|
|
1186
|
+
async def _process_messages(self, messages: list[dict[str, Any]]) -> str:
|
|
1187
|
+
"""处理消息列表,将图片转换为文字描述"""
|
|
1188
|
+
lines: list[str] = []
|
|
1189
|
+
|
|
1190
|
+
for msg in messages:
|
|
1191
|
+
sender_id = get_message_sender_id(msg)
|
|
1192
|
+
msg_time = parse_message_time(msg)
|
|
1193
|
+
content = get_message_content(msg)
|
|
1194
|
+
|
|
1195
|
+
time_str = msg_time.strftime("%Y-%m-%d %H:%M:%S")
|
|
1196
|
+
text_parts: list[str] = []
|
|
1197
|
+
|
|
1198
|
+
for segment in content:
|
|
1199
|
+
seg_type = segment.get("type", "")
|
|
1200
|
+
seg_data = segment.get("data", {})
|
|
1201
|
+
|
|
1202
|
+
if seg_type == "text":
|
|
1203
|
+
text_parts.append(seg_data.get("text", ""))
|
|
1204
|
+
|
|
1205
|
+
elif seg_type == "image":
|
|
1206
|
+
file = seg_data.get("file", "") or seg_data.get("url", "")
|
|
1207
|
+
if file:
|
|
1208
|
+
try:
|
|
1209
|
+
image_url = await self.onebot.get_image(file)
|
|
1210
|
+
if image_url:
|
|
1211
|
+
result = await self.ai.analyze_multimodal(
|
|
1212
|
+
image_url, "image"
|
|
1213
|
+
)
|
|
1214
|
+
desc = result.get("description", "")
|
|
1215
|
+
ocr = result.get("ocr_text", "")
|
|
1216
|
+
text_parts.append(
|
|
1217
|
+
f"[pic]<desc>{desc}</desc><text>{ocr}</text>[/pic]"
|
|
1218
|
+
)
|
|
1219
|
+
else:
|
|
1220
|
+
text_parts.append(
|
|
1221
|
+
"[pic]<desc>图片加载失败</desc>[/pic]"
|
|
1222
|
+
)
|
|
1223
|
+
except Exception as e:
|
|
1224
|
+
logger.error(f"处理图片失败: {e}")
|
|
1225
|
+
text_parts.append("[pic]<desc>图片处理失败</desc>[/pic]")
|
|
1226
|
+
|
|
1227
|
+
elif seg_type == "at":
|
|
1228
|
+
qq = seg_data.get("qq", "")
|
|
1229
|
+
text_parts.append(f"@{qq}")
|
|
1230
|
+
|
|
1231
|
+
elif seg_type == "face":
|
|
1232
|
+
text_parts.append("[表情]")
|
|
1233
|
+
|
|
1234
|
+
elif seg_type == "reply":
|
|
1235
|
+
text_parts.append("[回复]")
|
|
1236
|
+
|
|
1237
|
+
if text_parts:
|
|
1238
|
+
message_text = "".join(text_parts)
|
|
1239
|
+
lines.append(f"[{time_str}] {sender_id}: {message_text}")
|
|
1240
|
+
|
|
1241
|
+
return "\n".join(lines)
|
|
1242
|
+
|
|
1243
|
+
async def close(self) -> None:
|
|
1244
|
+
"""关闭消息处理器,取消队列处理任务"""
|
|
1245
|
+
logger.info("正在关闭消息处理器...")
|
|
1246
|
+
await self.queue_manager.stop()
|
|
1247
|
+
logger.info("消息处理器已关闭")
|