Skip to content

Commit 5ae5f3a

Browse files
committed
refactor: Use on_regex for command matching and update pyproject
1 parent e47eb71 commit 5ae5f3a

File tree

5 files changed

+1469
-182
lines changed

5 files changed

+1469
-182
lines changed

README.md

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,6 @@
1616
</a>
1717
<img src="https://img.shields.io/badge/python-3.10+-blue.svg" alt="python">
1818
<img src="https://img.shields.io/badge/adapter-OneBot_V11-blueviolet" alt="adapter">
19-
<a href="https://github.com/astral-sh/ruff">
20-
<img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json" alt="ruff">
21-
</a>
2219
<a href="https://github.com/astral-sh/uv">
2320
<img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json" alt="uv">
2421
</a>
Lines changed: 182 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -1,114 +1,57 @@
11
import httpx
2+
import re
23
import nonebot
3-
import re # 需要正则表达式
4-
from nonebot import on_command
4+
from nonebot import on_regex
55
from nonebot.log import logger
6-
from typing import Union, Optional
6+
from typing import Union, Optional, Tuple
77
from nonebot.adapters.onebot.v11 import Message, MessageSegment, Bot, Event
88
from nonebot.adapters.onebot.v11 import GroupMessageEvent, PrivateMessageEvent
9-
from nonebot.params import CommandArg
9+
from nonebot.params import RegexGroup
1010
from nonebot.plugin import PluginMetadata
1111
from .config import config, Config
1212
from 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 保持不变) ---
2845
async 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-
10650
async 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

Comments
 (0)