优化提醒命令处理,支持批量添加多个提醒并更新解析逻辑,改进用户反馈信息,确保返回结果为 JSON 数组格式。

This commit is contained in:
Zylan
2025-04-23 15:06:52 +08:00
parent 6e0a143ddf
commit a1a5074f34

View File

@@ -824,31 +824,37 @@ def handle_perplexity_ask(ctx: 'MessageContext', match: Optional[Match]) -> bool
return was_handled
def handle_reminder(ctx: 'MessageContext', match: Optional[Match]) -> bool:
"""处理来自私聊或群聊的 '提醒' 命令"""
"""处理来自私聊或群聊的 '提醒' 命令,支持批量添加多个提醒"""
# 2. 获取用户输入的提醒内容 (现在从完整消息获取)
raw_text = ctx.msg.content.strip() # 修改:从 ctx.msg.content 获取
if not raw_text: # 修改:仅检查是否为空
# 在群聊中@用户回复
at_list = ctx.msg.sender if ctx.is_group else ""
ctx.send_text("请告诉我需要提醒什么内容和时间呀~ (例如提醒我明天下午3点开会)", at_list) # 修改示例
ctx.send_text("请告诉我需要提醒什么内容和时间呀~ (例如提醒我明天下午3点开会)", at_list)
return True
# 3. 构造给 AI 的 Prompt
# 3. 构造给 AI 的 Prompt,更新为支持批量提醒
sys_prompt = """
你是提醒解析助手。请分析用户输入的提醒信息,并严格按照以下 JSON 格式输出结果:
{{
"type": "once" | "daily" | "weekly", // 提醒类型: "once" (一次性) 或 "daily" (每日重复) 或 "weekly" (每周重复)
"time": "YYYY-MM-DD HH:MM" | "HH:MM", // "once"类型必须是 'YYYY-MM-DD HH:MM' 格式, "daily""weekly"类型必须是 'HH:MM' 格式。时间必须是未来的。
"content": "提醒的具体内容文本",
"weekday": 0-6, // 仅当 type="weekly" 时需要,周一=0, 周二=1, ..., 周日=6
"extra": {{}} // 保留字段,目前为空对象即可
}}
- 分析用户意图判断是 `once`, `daily` 还是 `weekly`。
- 如果是相对时间(如"明天""后天""下周一"),请计算出精确的 `YYYY-MM-DD HH:MM` 格式。
- 如果只说了时间(如"每天早上9点"),类型设为 `daily`,时间格式为 `HH:MM`。
- 如果是每周特定时间(如"每周一下午3点"),类型设为 `weekly`,提供正确的 weekday 值和 HH:MM 时间。
- 如果无法确定时间或内容,不要猜测,返回错误提示,这样我可以提醒用户提供更明确的信息
- 输出结果必须是纯 JSON不包含任何其他说明文字
你是提醒解析助手。请仔细分析用户输入的提醒信息,**识别其中可能包含的所有独立提醒请求**。将所有成功解析的提醒严格按照以下 JSON **数组** 格式输出结果,数组中的每个元素代表一个独立的提醒:
[
{{
"type": "once" | "daily" | "weekly", // 提醒类型: "once" (一次性) 或 "daily" (每日重复) 或 "weekly" (每周重复)
"time": "YYYY-MM-DD HH:MM" | "HH:MM", // "once"类型必须是 'YYYY-MM-DD HH:MM' 格式, "daily""weekly"类型必须是 'HH:MM' 格式。时间必须是未来的。
"content": "提醒的具体内容文本",
"weekday": 0-6, // 仅当 type="weekly" 时需要,周一=0, 周二=1, ..., 周日=6
"extra": {{}} // 保留字段,目前为空对象即可
}},
// ... 可能有更多提醒对象 ...
]
- **仔细分析用户输入,识别所有独立的提醒请求。**
- 对每一个识别出的提醒,判断其类型 (`once`, `daily`, `weekly`) 并计算准确时间
- "once"类型时间必须是 'YYYY-MM-DD HH:MM' 格式, "daily"/"weekly"类型必须是 'HH:MM' 格式。时间必须是未来的
- "weekly"类型必须提供 weekday (周一=0...周日=6)。
- **将所有解析成功的提醒对象放入一个 JSON 数组中返回。**
- 如果只识别出一个提醒,返回包含单个元素的数组。
- **如果无法识别出任何有效提醒,返回空数组 `[]`。**
- 如果用户输入的某个提醒部分信息不完整或格式错误,请尝试解析其他部分,并在最终数组中仅包含解析成功的提醒。
- 输出结果必须是纯 JSON 数组,不包含任何其他说明文字。
当前准确时间是:{current_datetime}
"""
@@ -856,8 +862,7 @@ def handle_reminder(ctx: 'MessageContext', match: Optional[Match]) -> bool:
formatted_prompt = sys_prompt.format(current_datetime=current_dt_str)
# 4. 调用AI模型并解析
q_for_ai = f"请解析以下用户提醒:\n{raw_text}"
data = None
q_for_ai = f"请解析以下用户提醒,识别所有独立的提醒请求:\n{raw_text}"
try:
# 检查AI模型
if not hasattr(ctx, 'chat') or not ctx.chat:
@@ -867,103 +872,170 @@ def handle_reminder(ctx: 'MessageContext', match: Optional[Match]) -> bool:
at_list = ctx.msg.sender if ctx.is_group else ""
ai_response = ctx.chat.get_answer(q_for_ai, ctx.get_receiver(), system_prompt_override=formatted_prompt)
# 尝试提取和解析JSON
# 尝试提取和解析 JSON 数组
parsed_reminders = [] # 初始化为空列表
json_str = None
json_match = re.search(r'\{.*\}', ai_response, re.DOTALL)
if json_match:
json_str = json_match.group(0)
# 尝试匹配 [...] 或 {...} (兼容单个提醒的情况,但优先列表)
json_match_list = re.search(r'\[.*\]', ai_response, re.DOTALL)
json_match_obj = re.search(r'\{.*\}', ai_response, re.DOTALL)
if json_match_list:
json_str = json_match_list.group(0)
elif json_match_obj: # 如果没找到列表,尝试找单个对象 (增加兼容性)
json_str = json_match_obj.group(0)
else:
json_str = ai_response
json_str = ai_response # 如果都找不到,直接尝试解析原始回复
try:
data = json.loads(json_str)
except:
ctx.send_text("❌ 无法解析AI的回复为有效的JSON格式", at_list)
parsed_data = json.loads(json_str)
# 确保解析结果是一个列表,如果不是(比如解析了单个对象),包装成列表
if isinstance(parsed_data, dict):
parsed_reminders = [parsed_data] # 包装成单元素列表
elif isinstance(parsed_data, list):
parsed_reminders = parsed_data # 本身就是列表
else:
# 解析结果不是列表也不是字典,无法处理
raise ValueError("AI 返回的不是有效的 JSON 列表或对象")
except json.JSONDecodeError:
ctx.send_text(f"❌ 无法解析AI的回复为有效的JSON格式", at_list)
if ctx.logger: ctx.logger.warning(f"AI 返回 JSON 解析失败: {ai_response}")
return True
# 验证数据
if not data.get("type") or not data.get("time") or not data.get("content"):
ctx.send_text("❌ AI返回的数据缺少必要字段(类型/时间/内容)", at_list)
except ValueError as e:
ctx.send_text(f"❌ 处理AI返回的数据时出错: {e}", at_list)
if ctx.logger: ctx.logger.warning(f"AI 返回数据格式错误: {ai_response}")
return True
# 检查 ReminderManager 是否存在
if not hasattr(ctx.robot, 'reminder_manager'):
ctx.send_text("❌ 内部错误:提醒管理器未初始化。", at_list)
if ctx.logger: ctx.logger.error("handle_reminder 无法访问 ctx.robot.reminder_manager")
return True
# 验证内容
if len(data.get("content", "").strip()) < 2:
ctx.send_text("❌ 提醒内容太短,请提供更具体的提醒内容", at_list)
# 如果AI返回空列表告知用户
if not parsed_reminders:
ctx.send_text("🤔 嗯... 我好像没太明白您想设置什么提醒,可以换种方式再说一次吗?", at_list)
return True
# 验证时间格式
if data["type"] == "once":
try:
dt = datetime.strptime(data["time"], "%Y-%m-%d %H:%M")
if dt < datetime.now():
ctx.send_text("❌ 提醒时间必须是未来的时间", at_list)
return True
except ValueError:
ctx.send_text("❌ 一次性提醒的时间格式不正确", at_list)
return True
# ---- 批量处理提醒 ----
results = [] # 用于存储每个提醒的处理结果
roomid = ctx.msg.roomid if ctx.is_group else None
for index, data in enumerate(parsed_reminders):
reminder_label = f"提醒{index+1}" # 给每个提醒一个标签,方便反馈
validation_error = None # 存储验证错误信息
# **验证单个提醒数据**
if not isinstance(data, dict):
validation_error = "格式错误 (不是有效的提醒对象)"
elif not data.get("type") or not data.get("time") or not data.get("content"):
validation_error = "缺少必要字段(类型/时间/内容)"
elif len(data.get("content", "").strip()) < 2:
validation_error = "提醒内容太短"
else:
# 验证时间格式
try:
if data["type"] == "once":
dt = datetime.strptime(data["time"], "%Y-%m-%d %H:%M")
if dt < datetime.now():
validation_error = f"时间 ({data['time']}) 必须是未来的时间"
elif data["type"] in ["daily", "weekly"]:
datetime.strptime(data["time"], "%H:%M") # 仅校验格式
else:
validation_error = f"不支持的提醒类型: {data.get('type')}"
except ValueError:
validation_error = f"时间格式错误 ({data.get('time', '')})"
# 验证周提醒 (如果类型是 weekly 且无验证错误)
if not validation_error and data["type"] == "weekly":
if not (isinstance(data.get("weekday"), int) and 0 <= data.get("weekday") <= 6):
validation_error = "每周提醒需要指定周几(0-6)"
# 如果验证通过,尝试添加到数据库
if not validation_error:
try:
success, result_or_id = ctx.robot.reminder_manager.add_reminder(ctx.msg.sender, data, roomid=roomid)
if success:
results.append({"label": reminder_label, "success": True, "id": result_or_id, "data": data})
if ctx.logger: ctx.logger.info(f"成功添加提醒 {result_or_id} for {ctx.msg.sender} (来自批量处理)")
else:
# add_reminder 返回错误信息
results.append({"label": reminder_label, "success": False, "error": result_or_id, "data": data})
if ctx.logger: ctx.logger.warning(f"添加提醒失败 (来自批量处理): {result_or_id}")
except Exception as db_e:
# 捕获 add_reminder 可能抛出的其他异常
error_msg = f"数据库错误: {db_e}"
results.append({"label": reminder_label, "success": False, "error": error_msg, "data": data})
if ctx.logger: ctx.logger.error(f"添加提醒时数据库出错 (来自批量处理): {db_e}", exc_info=True)
else:
# 验证失败
results.append({"label": reminder_label, "success": False, "error": validation_error, "data": data})
if ctx.logger: ctx.logger.warning(f"提醒数据验证失败 ({reminder_label}): {validation_error} - Data: {data}")
# ---- 构建汇总反馈消息 ----
reply_parts = []
successful_count = sum(1 for res in results if res["success"])
failed_count = len(results) - successful_count
# 添加总览信息
if len(results) > 1: # 只有多个提醒时才需要总览
scope_info = "在本群" if ctx.is_group else "在私聊中"
if successful_count > 0 and failed_count > 0:
reply_parts.append(f"✅ 已{scope_info}成功设置 {successful_count} 个提醒,{failed_count} 个设置失败:\n")
elif successful_count > 0:
reply_parts.append(f"✅ 已{scope_info}成功设置全部 {successful_count} 个提醒:\n")
else:
reply_parts.append(f"❌ 抱歉,所有 {len(results)} 个提醒设置均失败:\n")
# 验证周提醒
if data["type"] == "weekly" and not (isinstance(data.get("weekday"), int) and 0 <= data.get("weekday") <= 6):
ctx.send_text("❌ 每周提醒需要指定是周几(0-6)", at_list)
return True
# 记录日志
if ctx.logger:
ctx.logger.info(f"成功解析提醒: {data}")
except Exception as e:
at_list = ctx.msg.sender if ctx.is_group else ""
ctx.send_text(f"❌ 处理提醒时出错: {str(e)}", at_list)
if ctx.logger:
ctx.logger.error(f"处理提醒出错: {e}", exc_info=True)
return True
# 添加每个提醒的详细信息
for res in results:
content_preview = res['data'].get('content', '未知内容')
# 如果内容太长截取前20个字符加省略号
if len(content_preview) > 20:
content_preview = content_preview[:20] + "..."
if res["success"]:
reminder_id = res['id']
type_str = {"once": "一次性", "daily": "每日", "weekly": "每周"}.get(res['data'].get('type'), "未知")
time_display = res['data'].get("time", "?")
# 为周提醒格式化显示
if res['data'].get("type") == "weekly" and "weekday" in res['data']:
weekdays = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"]
if 0 <= res['data']["weekday"] <= 6:
time_display = f"{weekdays[res['data']['weekday']]} {time_display}"
# 单个提醒或多个提醒的第一个,不需要标签
if len(results) == 1:
scope_info = "在本群" if ctx.is_group else "私聊"
reply_parts.append(f"✅ 好的,已为您{scope_info}设置{type_str}提醒 (ID: {reminder_id[:6]}):\n"
f"时间: {time_display}\n"
f"内容: {res['data'].get('content', '')}")
else:
reply_parts.append(f"{res['label']} (ID: {reminder_id[:6]}): {type_str} {time_display} - \"{content_preview}\"")
else:
# 失败的提醒
if len(results) == 1:
reply_parts.append(f"❌ 设置提醒失败: {res['error']}")
else:
reply_parts.append(f"{res['label']}: \"{content_preview}\" - {res['error']}")
# 6. 将解析结果交给 ReminderManager 处理
if not hasattr(ctx.robot, 'reminder_manager'):
at_list = ctx.msg.sender if ctx.is_group else ""
ctx.send_text("❌ 内部错误:提醒管理器未初始化。", at_list)
if ctx.logger:
ctx.logger.error("handle_reminder 无法访问 ctx.robot.reminder_manager")
return True
# 发送汇总消息
ctx.send_text("\n".join(reply_parts), at_list)
# 根据当前环境群聊或私聊设置roomid参数
roomid = ctx.msg.roomid if ctx.is_group else None
success, result_or_id = ctx.robot.reminder_manager.add_reminder(ctx.msg.sender, data, roomid=roomid)
# 7. 向用户反馈结果
# 在群聊中@用户
at_list = ctx.msg.sender if ctx.is_group else ""
if success:
reminder_id = result_or_id
# 构建更友好的回复,根据提醒类型进行定制
type_str = {
"once": "一次性",
"daily": "每日",
"weekly": "每周"
}.get(data.get("type"), "未知类型")
# 格式化时间显示,使其更友好
time_display = data.get("time", "未知时间")
if data.get("type") == "weekly" and "weekday" in data:
weekdays = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"]
if 0 <= data["weekday"] <= 6:
time_display = f"{weekdays[data['weekday']]} {time_display}"
# 添加设置环境提示(群聊/私聊)
scope_info = f"在本群" if ctx.is_group else "私聊"
reply_msg = f"✅ 好的,已为您{scope_info}设置{type_str}提醒 (ID: {reminder_id[:6]}):\n" \
f"时间: {time_display}\n" \
f"内容: {data.get('content', '')}"
ctx.send_text(reply_msg, at_list)
# 尝试触发馈赠(如果在群聊中)
if ctx.is_group and hasattr(ctx.robot, "goblin_gift_manager"):
# 如果有成功设置的提醒,并且在群聊中,尝试触发馈赠
if successful_count > 0 and ctx.is_group and hasattr(ctx.robot, "goblin_gift_manager"):
ctx.robot.goblin_gift_manager.try_trigger(ctx.msg)
else:
error_message = result_or_id # 此时 result_or_id 是错误信息
ctx.send_text(f"❌ 设置提醒失败: {error_message}", at_list)
return True # 命令处理流程结束
return True # 命令处理流程结束
except Exception as e: # 捕获代码块顶层的其他潜在错误
at_list = ctx.msg.sender if ctx.is_group else ""
error_message = f"处理提醒时发生意外错误: {str(e)}"
ctx.send_text(f"{error_message}", at_list)
if ctx.logger:
ctx.logger.error(f"handle_reminder 顶层错误: {e}", exc_info=True)
return True
def handle_list_reminders(ctx: 'MessageContext', match: Optional[Match]) -> bool:
"""处理查看提醒命令(支持群聊和私聊)"""