22import asyncio
33import logging
44import statistics
5+ import random
6+ from datetime import datetime
57from collections import deque
68from typing import Any , TypedDict
79
1113 FriendRequestEvent ,
1214 GroupMsgEmojiLikeEvent ,
1315 FriendPokeEvent ,
14- GroupMessageEvent ,
15- GroupPokeEvent ,
16+ GroupMessageEvent ,
17+ GroupPokeEvent ,
1618 Message ,
1719 NodeInline ,
1820 NodeReference ,
1921 Reply ,
2022 Text ,
23+ At ,
2124)
2225
2326# ================= 日志配置 =================
4043# ================= 程序启动时间 =================
4144STARTED_AT = time .time ()
4245
43- # 防抖间隔(秒),可按需调整或分别为每种指令设置不同值
46+ # 防抖间隔(秒)
4447DEBOUNCE_SECONDS = {
4548 "say" : 5 ,
4649 "more" : 5 ,
4952# 好友申请去重缓存(flag -> 处理时间戳)
5053PROCESSED_FRIEND_REQUESTS_EXPIRE = 60 # 缓存保留 60 秒
5154
55+ # ================= 成员可用时间段配置 =================
56+ # 格式:{ QQ号: { 星期几英文: [(开始时间, 结束时间), ...] } }
57+ # 星期几可用 "monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"
58+ AVAILABILITY : dict [int , dict [str , list [tuple [str , str ]]]] = {
59+ 234666220 : { # yjx
60+ "monday" : [("00:00" , "11:59" ), ("12:00" , "23:59" )],
61+ "tuesday" : [],
62+ "wednesday" : [],
63+ "thursday" : [],
64+ "friday" : [],
65+ "saturday" : [],
66+ "sunday" : [],
67+ },
68+ 1284656930 : { # cz
69+ "monday" : [],
70+ "tuesday" : [("00:00" , "11:59" ), ("12:00" , "23:59" )],
71+ "wednesday" : [],
72+ "thursday" : [],
73+ "friday" : [],
74+ "saturday" : [],
75+ "sunday" : [],
76+ },
77+ 3499737435 : { # hyn
78+ "monday" : [],
79+ "tuesday" : [],
80+ "wednesday" : [("00:00" , "11:59" ), ("12:00" , "23:59" )],
81+ "thursday" : [],
82+ "friday" : [],
83+ "saturday" : [],
84+ "sunday" : [],
85+ },
86+ 3525362286 : { # djh
87+ "monday" : [],
88+ "tuesday" : [],
89+ "wednesday" : [],
90+ "thursday" : [("00:00" , "11:59" ), ("12:00" , "23:59" )],
91+ "friday" : [],
92+ "saturday" : [],
93+ "sunday" : [],
94+ },
95+ 1372438873 : { # ljh
96+ "monday" : [],
97+ "tuesday" : [],
98+ "wednesday" : [],
99+ "thursday" : [],
100+ "friday" : [("00:00" , "11:59" ), ("12:00" , "23:59" )],
101+ "saturday" : [],
102+ "sunday" : [],
103+ },
104+ 3026425376 : { # pwy
105+ "monday" : [],
106+ "tuesday" : [],
107+ "wednesday" : [],
108+ "thursday" : [],
109+ "friday" : [],
110+ "saturday" : [("00:00" , "11:59" ), ("12:00" , "23:59" )],
111+ "sunday" : [],
112+ },
113+ 2018160654 : { # yyx
114+ "monday" : [],
115+ "tuesday" : [],
116+ "wednesday" : [],
117+ "thursday" : [],
118+ "friday" : [],
119+ "saturday" : [],
120+ "sunday" : [("00:00" , "11:59" ), ("12:00" , "23:59" )],
121+ }
122+ }
123+
124+ # 星期几数字到英文的映射(datetime.weekday() 返回 0=周一, 6=周日)
125+ WEEKDAY_MAP = {
126+ 0 : "monday" ,
127+ 1 : "tuesday" ,
128+ 2 : "wednesday" ,
129+ 3 : "thursday" ,
130+ 4 : "friday" ,
131+ 5 : "saturday" ,
132+ 6 : "sunday" ,
133+ }
134+ # =================================================
135+
52136# ================= 回复耗时记录(秒) =================
53- REPLY_DURATION_MAXLEN = 1000 # 保留最近 N 次回复耗时,防止列表无限增长
137+ REPLY_DURATION_MAXLEN = 1000 # 保留最近 N 次回复耗时
54138reply_durations : deque [float ] = deque (maxlen = REPLY_DURATION_MAXLEN )
55139
56140class CustomerData (TypedDict ):
57141 last_active : float
58142 msg_ids : list [int ]
59143 is_newly_reported : bool
60144 reported_milestones : set [int ]
61- pending_since : float # 本轮开始等待回复的时间戳
145+ pending_since : float # 本轮开始等待回复的时间戳
62146
63147
64148class ForwardMonitorData (TypedDict ):
@@ -70,8 +154,8 @@ class ForwardMonitorData(TypedDict):
70154unreplied_customers : dict [int , CustomerData ] = {}
71155monitored_forward_order : deque [int ] = deque ()
72156monitored_forwards : dict [int , ForwardMonitorData ] = {}
73- last_command_time : dict [tuple [int , str ], float ] = {} # 记录每条消息上各指令的最后执行时间,键为 (message_id, command_type)
74- processed_friend_requests : dict [str , float ] = {} # 好友申请去重缓存(flag -> 处理时间戳)
157+ last_command_time : dict [tuple [int , str ], float ] = {} # 记录每条消息上各指令的最后执行时间
158+ processed_friend_requests : dict [str , float ] = {} # 好友申请去重缓存
75159
76160# 客户端对象
77161client : NapCatClient = NapCatClient (WS_URL , WS_TOKEN )
@@ -121,9 +205,10 @@ def pop_tracked_forward(message_id: int) -> ForwardMonitorData | None:
121205
122206 return data
123207
208+
124209# ================= 格式化时长 =================
125210def format_duration (seconds : float ) -> str :
126- """将秒数转换为易读的文本(如 3分12秒、1小时05分) """
211+ """将秒数转换为易读的文本"""
127212 if seconds < 60 :
128213 return f"{ int (seconds )} 秒"
129214 elif seconds < 3600 :
@@ -228,11 +313,6 @@ async def send_nested_forward(group_id: int, customer_list: list[tuple[int, Cust
228313
229314 log .info ("开始构造合并转发 -> 群 %d, 共 %d 名客户" , group_id , len (customer_list ))
230315 outer_nodes : list [Message ] = [
231- NodeInline (
232- nickname = "客服系统提示" ,
233- user_id = str (client .self_id ),
234- content = serialize_message_segments ([Text (text = summary_text )]),
235- ),
236316 NodeInline (
237317 nickname = "快捷传送门" ,
238318 user_id = str (client .self_id ),
@@ -270,6 +350,62 @@ async def send_nested_forward(group_id: int, customer_list: list[tuple[int, Cust
270350 log .error ("发送合并转发失败: %s" , e , exc_info = True )
271351 return None
272352
353+
354+ # ================= 新增:获取当前可用成员 =================
355+ def get_current_available_members () -> list [int ]:
356+ """根据当前系统时间,返回所有可用的成员QQ列表"""
357+ now = datetime .now ()
358+ weekday_name = WEEKDAY_MAP [now .weekday ()]
359+ current_time = now .time ()
360+
361+ available : list [int ] = [] # 明确类型为整数列表
362+ for qq , schedule in AVAILABILITY .items ():
363+ time_slots = schedule .get (weekday_name , [])
364+ for start_str , end_str in time_slots :
365+ try :
366+ start = datetime .strptime (start_str , "%H:%M" ).time ()
367+ end = datetime .strptime (end_str , "%H:%M" ).time ()
368+ if start <= current_time <= end :
369+ available .append (qq ) # 现在类型检查器知道 qq 是 int
370+ break
371+ except Exception as e :
372+ log .error ("解析时间段失败: qq=%s, slot=%s-%s, err=%s" , qq , start_str , end_str , e )
373+ continue
374+ return available
375+
376+
377+ # ================= 新增:发送带 @ 的提醒 =================
378+ async def send_reminder_with_at (group_id : int , summary : str , customer_list : list [tuple [int , CustomerData ]]) -> int | None :
379+ """
380+ 发送提醒:先尝试 @ 一位当前可用成员,再发送合并转发。
381+ 若无可用成员,则直接发送合并转发(无 @)。
382+ """
383+ available = get_current_available_members ()
384+ if available :
385+ chosen = random .choice (available )
386+ # 构造 @ 消息,使用库提供的消息段类
387+ at_msg : list [Message ] = [
388+ At (qq = str (chosen )), # 假设 At 接受 qq 参数
389+ Text (text = f" { summary } " ),
390+ ]
391+ try :
392+ await client .send_group_msg (group_id = str (group_id ), message = at_msg )
393+ log .info ("已发送 @%d 提醒: %s" , chosen , summary )
394+ except Exception as e :
395+ log .error ("发送 @ 提醒失败: %s" , e , exc_info = True )
396+ else :
397+ log .debug ("当前无可用成员,直接发送合并转发" )
398+
399+ # 发送合并转发
400+ message_id = await send_nested_forward (group_id , customer_list , summary )
401+ if message_id is not None :
402+ track_forward_message (
403+ message_id = message_id ,
404+ customer_ids = [qq for qq , _ in customer_list ],
405+ group_id = group_id ,
406+ )
407+ return message_id
408+
273409# ================= 核心逻辑:定时巡检任务 =================
274410async def monitor_loop ():
275411 """每分钟执行一次的巡检死循环"""
@@ -326,13 +462,8 @@ async def monitor_loop():
326462
327463 # 只通报新触发的客户,避免每次都全量刷屏
328464 summary = f"📢 刚刚有 { len (new_customers_to_report )} 名客户发来消息,请及时回复!"
329- message_id = await send_nested_forward (INTERNAL_GROUP_ID , new_customers_to_report , summary )
330- if message_id is not None :
331- track_forward_message (
332- message_id = message_id ,
333- customer_ids = [qq for qq , _ in new_customers_to_report ],
334- group_id = INTERNAL_GROUP_ID ,
335- )
465+ # 替换为带 @ 的提醒
466+ await send_reminder_with_at (INTERNAL_GROUP_ID , summary , new_customers_to_report )
336467 else :
337468 log .debug ("阶段1: 无新客户需要通报" )
338469
@@ -364,13 +495,8 @@ async def monitor_loop():
364495 log .warning ("阶段2: 里程碑 %s 触发, 涉及 %d 名客户: %s" ,
365496 unit , len (delay_list ), [qq for qq , _ in delay_list ])
366497 summary = f"⚠️ 以下 { len (delay_list )} 名客户已等待长达 { unit } !"
367- message_id = await send_nested_forward (INTERNAL_GROUP_ID , delay_list , summary )
368- if message_id is not None :
369- track_forward_message (
370- message_id = message_id ,
371- customer_ids = [qq for qq , _ in delay_list ],
372- group_id = INTERNAL_GROUP_ID ,
373- )
498+ # 替换为带 @ 的提醒
499+ await send_reminder_with_at (INTERNAL_GROUP_ID , summary , delay_list )
374500
375501 log .info ("===== 巡检结束 =====" )
376502
@@ -452,9 +578,8 @@ async def main():
452578 log .debug ("收到事件: type=%s, post_type=%s" , type (event ).__name__ , getattr (event , 'post_type' , '?' ))
453579 match event :
454580 # 0. 自动通过好友申请
455- case FriendRequestEvent (user_id = uid , comment = comment ,flag = flag ):
456- log .info ("收到好友申请:" + str (uid )+ " " + comment )
457- # 去重检查:如果同一个 flag 在短时间内已处理过,则跳过
581+ case FriendRequestEvent (user_id = uid , comment = comment , flag = flag ):
582+ log .info ("收到好友申请:" + str (uid ) + " " + comment )
458583 now = time .time ()
459584 last_time = processed_friend_requests .get (flag )
460585 if last_time and (now - last_time ) < PROCESSED_FRIEND_REQUESTS_EXPIRE :
@@ -559,7 +684,6 @@ async def main():
559684 if closed :
560685 log .info ("私聊戳一戳结束会话: user_id=%s" , sid )
561686 else :
562- # 客户不在队列中,但已发送结束语
563687 log .info ("私聊戳一戳自动发送结束语(客户不在队列): user_id=%s" , sid )
564688
565689 # 5. 内部群戳一戳 -> 发送状态面板
0 commit comments