11import httpx
2+ import re
23import nonebot
3- import re # 需要正则表达式
4- from nonebot import on_command
4+ from nonebot import on_regex
55from nonebot .log import logger
6- from typing import Union , Optional
6+ from typing import Union , Optional , Tuple
77from nonebot .adapters .onebot .v11 import Message , MessageSegment , Bot , Event
88from nonebot .adapters .onebot .v11 import GroupMessageEvent , PrivateMessageEvent
9- from nonebot .params import CommandArg
9+ from nonebot .params import RegexGroup
1010from nonebot .plugin import PluginMetadata
1111from .config import config , Config
1212from nonebot .matcher import Matcher
1313
1414__plugin_meta__ = PluginMetadata (
15- name = "QQ详细信息查询" ,
16- description = "让机器人查询QQ详细信息" ,
17- usage = "/detail <QQ号(5-11位)> 或 /detail @用户\n /info <QQ号(5-11位)> 或 /info @用户" ,
15+ name = "QQ详细信息查询 (Regex v2) " ,
16+ description = "让机器人查询QQ详细信息 (使用正则严格匹配) " ,
17+ usage = "/detail[空格] <QQ号| @用户> \n /info[空格] <QQ号| @用户> \n (无参数查询自己) " ,
1818 type = "application" ,
1919 homepage = "https://github.com/006lp/nonebot-plugin-qqdetail" ,
2020 supported_adapters = {"~onebot.v11" }
2121)
2222
23- # 创建命令处理器
24- qq_detail = on_command ("detail" , aliases = {"info" }, priority = 5 , block = True )
23+ # --- 获取命令前缀 ---
24+ command_start = ""
25+ try :
26+ command_start = next (iter (nonebot .get_driver ().config .command_start ))
27+ except Exception :
28+ logger .warning ("未配置 COMMAND_START,将假定命令前缀为空或'/'" )
29+ if "/" in nonebot .get_driver ().config .command_start :
30+ command_start = "/"
2531
26- # --- 辅助函数 ---
32+ escaped_prefix = re . escape ( command_start )
2733
34+ # --- 正则表达式保持不变 ---
35+ # 它用于:
36+ # 1. 匹配命令格式(命令 或 命令+空格+任意内容)
37+ # 2. 捕获命令名 (group 1)
38+ # 3. 区分是否有参数部分 (通过 group 2 是否为 None 判断)
39+ # !! 我们不再依赖 group 2 的 *内容* 来获取参数 !!
40+ pattern_str = rf"^{ escaped_prefix } (detail|info)(?:\s+(.*))?\s*$"
41+
42+ qq_detail_regex = on_regex (pattern_str , priority = 5 , block = True )
43+
44+ # --- 辅助函数 (is_whitelisted, fetch_qq_detail 保持不变) ---
2845async def is_whitelisted (uid : str ) -> bool :
2946 """检查 UID 是否在白名单内"""
3047 whitelist = getattr (config , 'qqdetail_whitelist' , []) or []
3148 return uid in whitelist
3249
33- # 再次修改 get_uid,优先使用 event.raw_message
34- async def get_uid (event : Event , arg : Message ) -> Optional [str ]:
35- """
36- 获取目标用户的 UID,优先从原始消息字符串解析 @。
37- 优先级: 原始消息(@) > CommandArg(纯数字QQ) > 仅命令(自己) > 错误
38- 返回: UID 字符串 或 None (表示格式错误或无效)
39- """
40- logger .debug (f"开始解析 UID: CommandArg={ arg !r} , Event Type={ type (event )} " )
41-
42- # --- 优先级 1: 尝试从 event.raw_message 解析 @mention ---
43- try :
44- # 检查 event 是否有 raw_message 属性且为字符串
45- raw_message_str = getattr (event , 'raw_message' , None )
46- if isinstance (raw_message_str , str ):
47- logger .debug (f"获取到 event.raw_message: '{ raw_message_str } '" )
48-
49- # 正则表达式匹配命令后紧跟一个 CQ:at 并允许末尾有空格
50- # \s+ 匹配命令和 @ 之间的空格
51- # (\d{5,11}) 捕获 5-11 位 QQ 号
52- # \s*$ 匹配结尾的任意空格
53- pattern = r"/(?:detail|info)\s+\[CQ:at,qq=(\d{5,11})\]\s*$" # $表示字符串结尾
54- match = re .match (pattern , raw_message_str .strip ()) # 使用 match 匹配开头, strip()去除首尾空格
55-
56- if match :
57- uid = match .group (1 )
58- logger .info (f"从 raw_message 严格匹配到 @mention UID: { uid } " )
59- return uid
60- else :
61- logger .debug ("raw_message 未能严格匹配 '命令 + @用户' 格式。" )
62- # 如果 raw_message 包含 @ 但格式不对,也应视为错误,这里不返回,让后续逻辑处理
63-
64- else :
65- logger .debug ("event.raw_message 不可用或类型不是字符串。" )
66-
67- except Exception as e :
68- logger .error (f"解析 event.raw_message 时发生异常: { e } " , exc_info = True )
69- # 发生异常时,不应继续,可能意味着解析逻辑有误或事件结构异常
70-
71- # --- 优先级 2: 如果 raw_message 未成功解析 @, 则检查 CommandArg 是否为纯数字 QQ ---
72- logger .debug ("未从 raw_message 成功解析 @mention,继续检查 CommandArg。" )
73- plain_text_arg = arg .extract_plain_text ().strip ()
74-
75- # 检查是否为 5-11 位纯数字
76- if re .fullmatch (r"\d{5,11}" , plain_text_arg ):
77- # 确保 arg 中只包含纯文本段,并且组合起来就是这个数字
78- is_purely_text = True
79- combined_text = ""
80- for seg in arg :
81- if seg .is_text ():
82- combined_text += seg .data .get ("text" , "" )
83- else :
84- is_purely_text = False
85- break
86-
87- if is_purely_text and combined_text .strip () == plain_text_arg :
88- logger .info (f"从 CommandArg 纯文本提取到有效 QQ UID: { plain_text_arg } " )
89- return plain_text_arg
90- else :
91- logger .warning (f"CommandArg 文本为有效 QQ,但混杂非文本段或多余文本: { arg !r} " )
92- return None # 格式错误
93-
94- # --- 优先级 3: 检查是否仅发送了命令 (arg 为空) ---
95- # 仅当 arg 为空,并且上面 raw_message 解析也没成功时执行
96- if not arg :
97- sender_id = str (event .get_user_id ())
98- logger .info (f"未提供有效参数 (@ 或纯数字 QQ),默认查询发送者 UID: { sender_id } " )
99- return sender_id
100-
101- # --- 优先级 4: 其他所有情况视为格式错误 ---
102- logger .warning (f"无法识别的命令参数格式: raw_message 解析失败且 CommandArg 内容无效: { arg !r} " )
103- return None
104-
105-
10650async def fetch_qq_detail (uid : str ) -> dict :
107- """调用 API 获取 QQ 详细信息 (保持不变) """
51+ """调用 API 获取 QQ 详细信息"""
10852 url = f"https://api.yyy001.com/api/qqdetail?qq={ uid } "
109- headers = {'User-Agent' : 'NoneBot-Plugin-QQDetail/1.2' }
11053 try :
111- async with httpx .AsyncClient (timeout = 10.0 , headers = headers ) as client :
54+ async with httpx .AsyncClient (timeout = 10.0 ) as client :
11255 response = await client .get (url )
11356 response .raise_for_status ()
11457 data = response .json ()
@@ -119,41 +62,133 @@ async def fetch_qq_detail(uid: str) -> dict:
11962 return {"response" : {"code" : 408 , "msg" : "请求API超时" }}
12063 except httpx .HTTPStatusError as e :
12164 logger .error (f"HTTP error for { uid } : Status { e .response .status_code } " )
122- return {"response" : {"code" : e .response .status_code , "msg" : f"API请求失败: { e .response .status_code } " }}
65+ error_msg = f"API请求失败: { e .response .status_code } "
66+ try :
67+ err_data = e .response .json ()
68+ if isinstance (err_data , dict ) and 'msg' in err_data :
69+ error_msg += f" - { err_data ['msg' ]} "
70+ except Exception : pass
71+ return {"response" : {"code" : e .response .status_code , "msg" : error_msg }}
12372 except Exception as e :
12473 logger .exception (f"Unexpected error fetching detail for { uid } : { e } " )
12574 return {"response" : {"code" : 500 , "msg" : f"处理API请求时发生内部错误" }}
12675
127- # --- 主处理器 (保持不变) ---
128- @qq_detail .handle ()
129- async def handle_info (bot : Bot , event : Union [PrivateMessageEvent , GroupMessageEvent ], matcher : Matcher , arg : Message = CommandArg ()):
130- logger .debug (f"Received event (ID: { event .message_id } ) type: { event .get_event_name ()} " )
131- # 仍然记录这些,即使它们可能不准确,以供对比
132- logger .debug (f"Handler看到 Raw Message Obj: { event .get_message ()!r} " )
133- logger .debug (f"Handler看到 Command Argument (arg): { arg !r} " )
134- try :
135- logger .debug (f"Handler看到 Event Raw Message Attr: { getattr (event , 'raw_message' , 'N/A' )} " )
136- except : pass
137-
138- # 获取目标 UID,使用最新的 get_uid 逻辑
139- target_uid = await get_uid (event , arg )
14076
141- # 检查格式错误
77+ # --- Regex 主处理器 (修改版) ---
78+ @qq_detail_regex .handle ()
79+ async def handle_regex_detail (bot : Bot , event : Union [PrivateMessageEvent , GroupMessageEvent ], matcher : Matcher , match : Tuple [Optional [str ], ...] = RegexGroup ()):
80+ """
81+ 使用 on_regex 处理查询请求。
82+ match[0]: 命令名 (detail, info)
83+ match[1]: 用于判断 *是否* 有参数部分 (None 表示无, 非 None 表示有), 但不依赖其内容。
84+ """
85+ command_name = match [0 ]
86+ has_argument_part = match [1 ] is not None # 通过第二捕获组是否为None判断有无参数段
87+
88+ raw_message = getattr (event , 'raw_message' , '' ).strip ()
89+ logger .debug (f"Regex handler triggered: command='{ command_name } ', has_argument_part={ has_argument_part } " )
90+ logger .debug (f"Raw message: { raw_message } " )
91+
92+ target_uid : Optional [str ] = None
93+ parameter_error = False
94+ actual_arg_str = "" # 存储实际解析出的参数字符串
95+
96+ if not has_argument_part :
97+ # --- 情况 1: 无参数 ---
98+ # 确保 raw_message 确实只是命令本身
99+ expected_command_only = f"{ command_start } { command_name } "
100+ if raw_message == expected_command_only :
101+ target_uid = str (event .get_user_id ())
102+ logger .info (f"命令 '{ command_name } ' 无参数,查询发送者: { target_uid } " )
103+ else :
104+ # 理论上正则不应该匹配到这里,但作为保险
105+ logger .warning (f"Regex matched no arg part, but raw_message '{ raw_message } ' != '{ expected_command_only } '. Ignoring." )
106+ await matcher .finish () # 或者提示错误
107+ return
108+ else :
109+ # --- 情况 2: 有参数部分 ---
110+ # 从 raw_message 中手动提取参数
111+ # 找到命令 (包括前缀) 之后第一个空格的位置
112+ prefix_and_command = f"{ command_start } { command_name } "
113+ try :
114+ # 寻找第一个空格,且必须紧跟在命令名之后
115+ first_space_index = - 1
116+ # 确保命令确实以我们期望的前缀和名称开头
117+ if raw_message .startswith (prefix_and_command ):
118+ # 从命令名之后开始查找空格
119+ potential_space_index = len (prefix_and_command )
120+ # 检查命令名后紧跟着的是否是空格
121+ if potential_space_index < len (raw_message ) and raw_message [potential_space_index ].isspace ():
122+ # 跳过所有连续的空格
123+ first_non_space_after_command = potential_space_index
124+ while first_non_space_after_command < len (raw_message ) and raw_message [first_non_space_after_command ].isspace ():
125+ first_non_space_after_command += 1
126+ # 如果空格后还有内容,提取它
127+ if first_non_space_after_command < len (raw_message ):
128+ actual_arg_str = raw_message [first_non_space_after_command :].strip ()
129+ else :
130+ # 只有命令+空格的情况
131+ actual_arg_str = "" # 明确设置为空字符串
132+ else :
133+ # 命令后没有空格直接跟了其他字符,理论上正则不匹配,但也处理下
134+ parameter_error = True
135+ logger .warning (f"命令 '{ command_name } ' 后缺少空格。" )
136+
137+ else : # raw_message 开头不是预期的命令,理论上不该发生
138+ logger .error (f"Internal error: Raw message '{ raw_message } ' doesn't start with expected command '{ prefix_and_command } '." )
139+ await matcher .finish ("处理请求时发生内部错误。" )
140+ return
141+
142+ logger .debug (f"Extracted actual argument string: '{ actual_arg_str } '" )
143+
144+ if not parameter_error : # 仅在没有发现明显格式错误时校验参数内容
145+ if not actual_arg_str :
146+ # 命令后只有空格的情况
147+ parameter_error = True
148+ logger .warning (f"命令 '{ command_name } ' 后只有空格,格式错误。" )
149+ else :
150+ # 校验 actual_arg_str 的内容
151+ # 2.1 尝试匹配 @用户
152+ cq_at_match = re .fullmatch (r"\[CQ:at,qq=(\d{5,11})\]" , actual_arg_str )
153+ if cq_at_match :
154+ target_uid = cq_at_match .group (1 )
155+ logger .info (f"从参数 '{ actual_arg_str } ' 解析到 @用户 UID: { target_uid } " )
156+ else :
157+ # 2.2 尝试匹配纯数字 QQ 号
158+ if re .fullmatch (r"\d{5,11}" , actual_arg_str ):
159+ target_uid = actual_arg_str
160+ logger .info (f"从参数 '{ actual_arg_str } ' 解析到纯数字 QQ UID: { target_uid } " )
161+ else :
162+ # 2.3 参数既不是有效 @ 也不是有效 QQ 号
163+ parameter_error = True
164+ logger .warning (f"命令 '{ command_name } ' 的参数 '{ actual_arg_str } ' 格式无效。" )
165+
166+ except Exception as e :
167+ logger .exception (f"Error parsing arguments from raw_message: { e } " )
168+ parameter_error = True
169+
170+ # 如果在参数解析或校验过程中发现错误
171+ if parameter_error :
172+ usage_msg = __plugin_meta__ .usage or "请检查命令用法。"
173+ await matcher .finish (f"命令参数格式错误。\n 请提供有效的 QQ号(5-11位) 或 @用户。\n \n 用法:\n { usage_msg } " )
174+ return
175+
176+ # --- 后续通用逻辑 ---
142177 if target_uid is None :
143- usage_message = __plugin_meta__ . usage or "/detail <QQ号(5-11位)> 或 /detail @用户"
144- await qq_detail .finish (f"命令格式错误、QQ号无效或包含多余参数。 \n 请使用: \n { usage_message } \n /detail (查询自己) " )
145- return
178+ logger . error ( "Logic error: target_uid is None after argument processing." )
179+ await matcher .finish ("处理请求时发生内部错误。 " )
180+ return
146181
147- # --- 后续逻辑 (白名单、API调用、发送结果) 保持不变 ---
148182 sender_id = str (event .get_user_id ())
149183 superusers = getattr (bot .config , "superusers" , set ())
150184 is_sender_superuser = sender_id in superusers
151185
152- logger .debug (f"Query Target UID: { target_uid } , Sender UID: { sender_id } , Is Superuser: { is_sender_superuser } " )
186+ logger .debug (f"最终 Query Target UID: { target_uid } , Sender UID: { sender_id } , Is Superuser: { is_sender_superuser } " )
153187
154- if await is_whitelisted (target_uid ) and not is_sender_superuser :
155- if sender_id != target_uid :
156- await qq_detail .finish (f"抱歉,您没有权限查询受保护用户 (UID: { target_uid } ) 的信息。" )
188+ # ... (白名单检查, API 调用, 发送结果逻辑保持不变)
189+ if await is_whitelisted (target_uid ) and not is_sender_superuser and sender_id != target_uid :
190+ await matcher .finish (f"抱歉,您没有权限查询该用户 (UID: { target_uid } ) 的信息。" )
191+ return
157192
158193 data = await fetch_qq_detail (target_uid )
159194
@@ -162,22 +197,52 @@ async def handle_info(bot: Bot, event: Union[PrivateMessageEvent, GroupMessageEv
162197 nickname = response_data .get ('name' , '未知' )
163198 headimg_url = response_data .get ('headimg' )
164199 details = [
165- f"查询对象:{ response_data .get ('qq' , 'N/A' )} " , f"昵称:{ nickname } " ,
166- f"QID:{ response_data .get ('qid' , '未设置' )} " , f"性别:{ response_data .get ('sex' , '未知' )} " ,
167- f"年龄:{ response_data .get ('age' , '未知' )} " , f"等级:Lv.{ response_data .get ('level' , 'N/A' )} " ,
168- f"VIP等级:{ response_data .get ('iVipLevel' , 'N/A' )} " , f"注册时间:{ response_data .get ('RegistrationTime' , '未知' )} " ,
169- f"签名:{ response_data .get ('sign' , '无' )} " , f"IP城市:{ response_data .get ('ip_city' , '未知' )} "
200+ f"查询对象:{ response_data .get ('qq' )} " ,
201+ f"昵称:{ nickname } " ,
202+ f"QID:{ response_data .get ('qid' )} " ,
203+ f"性别:{ response_data .get ('sex' )} " ,
204+ f"年龄:{ response_data .get ('age' )} " ,
205+ f"IP属地:{ response_data .get ('ip_city' )} " ,
206+ f"等级:Lv.{ response_data .get ('level' )} " ,
207+ f"等级图标:{ response_data .get ('icon' )} " ,
208+ f"能量值:{ response_data .get ('energy_value' )} " ,
209+ f"注册时间:{ response_data .get ('RegistrationTime' )} " ,
210+ f"注册时长:{ response_data .get ('RegTimeLength' )} " ,
211+ f"连续在线天数:{ response_data .get ('iLoginDays' )} " ,
212+ f"总活跃天数:{ response_data .get ('iTotalActiveDay' )} " ,
213+ f"加速状态:{ response_data .get ('Accelerate' )} " ,
214+ f"升到下一级预计天数:{ response_data .get ('iNextLevelDay' )} " ,
215+ f"成长值:{ response_data .get ('iGrowthValue' )} " ,
216+ f"VIP标识:{ response_data .get ('iVip' )} " ,
217+ f"SVIP标识:{ response_data .get ('iSVip' )} " ,
218+ f"年费会员:{ response_data .get ('NVip' )} " ,
219+ f"VIP等级:{ response_data .get ('iVipLevel' )} " ,
220+ f"VIP到期时间:{ response_data .get ('sVipExpireTime' )} " ,
221+ f"SVIP到期时间:{ response_data .get ('sSVipExpireTime' )} " ,
222+ f"年费到期时间:{ response_data .get ('sYearExpireTime' )} " ,
223+ f"大会员标识:{ response_data .get ('XVip' )} " ,
224+ f"年费大会员标识:{ response_data .get ('NXVip' )} " ,
225+ f"大会员等级:{ response_data .get ('XVipLevel' )} " ,
226+ f"大会员成长值:{ response_data .get ('XVipGrowth' )} " ,
227+ f"大会员每日成长速度:{ response_data .get ('XVipSpeed' )} " ,
228+ f"昨日在线:{ response_data .get ('iYesterdayLogin' )} " ,
229+ f"今日在线:{ response_data .get ('iTodayLogin' )} " ,
230+ f"今日安卓在线时长:{ response_data .get ('iMobileQQOnlineTime' )} " ,
231+ f"今日电脑在线时长:{ response_data .get ('iPCQQOnlineTime' )} " ,
232+ f"今日已加速天数:{ response_data .get ('iRealDays' )} " ,
233+ f"今日最大可加速天数:{ response_data .get ('iMaxLvlRealDays' )} " ,
234+ f"签名:{ response_data .get ('sign' )} "
170235 ]
171- qq_detail_message_text = "\n " .join (details )
236+ qq_detail_message_text = "\n " .join (filter ( None , details ) )
172237 message_to_send = Message ()
173238 if headimg_url :
174239 try : message_to_send .append (MessageSegment .image (headimg_url ))
175240 except Exception as e : logger .warning (f"无法创建头像图片 for { headimg_url } : { e } " )
176241 message_to_send .append (MessageSegment .text (qq_detail_message_text ))
177- await qq_detail .finish (message_to_send )
242+ await matcher .finish (message_to_send )
178243 else :
179244 error_msg = "未知错误"
180245 if isinstance (response_data , dict ): error_msg = response_data .get ('msg' , error_msg )
181246 elif isinstance (data , dict ) and "msg" in data : error_msg = data .get ('msg' , error_msg )
182247 logger .warning (f"获取 QQ 详细信息失败 UID { target_uid } . API Msg: { error_msg } " )
183- await qq_detail .finish (f"获取QQ信息失败 (UID: { target_uid } )。\n 原因:{ error_msg } " )
248+ await matcher .finish (f"获取QQ信息失败 (UID: { target_uid } )。\n 原因:{ error_msg } " )
0 commit comments