mirror of
https://github.com/Zippland/Bubbles.git
synced 2026-02-04 17:47:33 +08:00
优化提醒命令处理,支持批量添加多个提醒并更新解析逻辑,改进用户反馈信息,确保返回结果为 JSON 数组格式。
This commit is contained in:
@@ -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:
|
||||
"""处理查看提醒命令(支持群聊和私聊)"""
|
||||
|
||||
Reference in New Issue
Block a user