diff --git a/commands/handlers.py b/commands/handlers.py index 7da19b6..e7af275 100644 --- a/commands/handlers.py +++ b/commands/handlers.py @@ -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: """处理查看提醒命令(支持群聊和私聊)"""