mirror of
https://github.com/Zippland/Bubbles.git
synced 2026-01-19 01:21:15 +08:00
更新 README 文件,增加项目背景介绍和功能描述,调整标题格式;优化删除提醒命令的处理逻辑,支持自然语言描述,改进用户反馈信息,确保返回结果为有效的 JSON 格式。
This commit is contained in:
131
README.MD
131
README.MD
@@ -1,22 +1,46 @@
|
||||
# Bubbles-WechatAI
|
||||
> 我叫 泡泡(Bubbles) - 一个个人微信助手
|
||||
<div align="center">
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
# 🫧 Bubbles - WechatAI 🫧
|
||||
|
||||
## 🔗 写在前面
|
||||
我一直在尝试做一个全面的的生态助手(个人助理),能够连接我使用的任何工具、日程表、数据库、资料库。并基于数据资料,实时地和我交流,以及帮我安排日程、提醒、规划时间和出行,安排我的任务计划。
|
||||
*我叫 泡泡(Bubbles) - 一个致力于链接万物的个人微信助手*
|
||||
|
||||
之前做了 [LifeSync-AI](https://github.com/Zippland/LifeSync-AI),基于Github Action、Notion、Zapier进行app互联,帮我进行每天的任务规划。但定时任务是被动的,而且缺少一个统一的数据处理中心,能够通过与用户交流进行实时的任务调度。
|
||||
</div>
|
||||
|
||||
这就是这个项目的初衷。
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/badge/版本-1.0.0-red" alt="版本"/>
|
||||
<img src="https://img.shields.io/badge/wcferry-39.5.1-blue" alt="wcferry"/>
|
||||
<img src="https://img.shields.io/badge/Python-3.8+-green" alt="Python"/>
|
||||
<img src="https://img.shields.io/badge/License-Apache2.0-yellow" alt="License"/>
|
||||
</p>
|
||||
|
||||
> ✉️ 致来访者
|
||||
>
|
||||
> 我一直在尝试做一个全面的的生态助手(个人助理),能够连接我使用的任何工具、日程表、数据库、资料库。并基于数据资料,实时地和我交流,以及帮我安排日程、提醒、规划时间和出行,安排我的任务计划。
|
||||
>
|
||||
> 之前做了 [LifeSync-AI](https://github.com/Zippland/LifeSync-AI) 这个项目,核心思想是基于Github Action、Notion、Zapier进行app互联,帮我进行每天的任务规划。但定时任务是被动的,而且缺少一个统一的数据处理中心,能够通过与用户交流进行实时的任务调度。
|
||||
>
|
||||
>于是我在好奇,是否能使用聊天工具,通过和AI直接对话的形式,再通过 function call 或者 MCP Server 链接到工具,进而链接我的所有需求。
|
||||
>
|
||||
> **这就是这个项目的初衷。**
|
||||
>
|
||||
> 玩得开心,
|
||||
>
|
||||
> Zylan
|
||||
>
|
||||
|
||||
|
||||
## 📝 项目简介
|
||||
|
||||
Bubbles 是一个功能丰富的微信机器人框架,基于 [wcferry](https://github.com/lich0821/wcferry) 和 [WechatRobot](https://github.com/lich0821/wechatrobot) 开发,支持接入多种大型语言模型(LLM),提供丰富的交互功能和定时任务。该项目旨在将微信客户端转变为一个智能的个人助手,可以执行多种实用功能,带来便捷的用户体验。
|
||||
Bubbles 是一个功能丰富的微信机器人框架,基于 [wcferry](https://github.com/lich0821/wcferry) 和 [WechatRobot](https://github.com/lich0821/wechatrobot) 开发,支持接入多种LLM,提供丰富的交互功能和定时任务。该项目旨在将微信客户端转变为一个智能的个人助手,可以执行多种实用功能,带来便捷的用户体验。
|
||||
|
||||
和一般机器人框架不同的是,Bubbles 实现了一个 [**命令路由系统**](https://github.com/Zippland/Bubbles/blob/main/commands/registry.py) ,相当于一个主线 Hub,只用通过简单正则逻辑,即可将外部服务进行注册,将海量的、不同种类的工具集成到 AI 里。
|
||||
|
||||
|
||||
#### 案例演示:使用自然语言设置提醒
|
||||
|
||||
<img src="img_1.jpg" width="400"/>
|
||||
|
||||
<img src="img_2.jpg" width="400"/>
|
||||
|
||||
## ✨ 核心特性
|
||||
|
||||
@@ -37,9 +61,6 @@ Bubbles 是一个功能丰富的微信机器人框架,基于 [wcferry](https:/
|
||||
- 支持自定义命令及参数
|
||||
- 预设 [多种实用和娱乐命令](#可用命令)
|
||||
|
||||
#### 🎨 AI 图像生成
|
||||
- 支持调用多种 AI 绘图模型生成图片
|
||||
|
||||
#### ⏰ 定时任务与提醒功能
|
||||
- 每日天气预报推送
|
||||
- 每日新闻资讯推送
|
||||
@@ -187,52 +208,62 @@ python main.py
|
||||
- `我的装备`、`查看装备` - 查看自己的装备(仅群聊)
|
||||
- `改名 旧名称 新名称` - 更改昵称(仅群聊)
|
||||
|
||||
## 🎮 游戏功能详解
|
||||
|
||||
#### 决斗系统
|
||||
|
||||
Bubbles 内置了一个有趣的决斗游戏系统,用户可以在群聊中挑战其他成员:
|
||||
|
||||
- **开始决斗**:使用 `决斗 @用户` 开始一场决斗
|
||||
- **偷袭玩家**:使用 `偷袭 @用户` 偷袭其他玩家
|
||||
- **查看排名**:使用 `决斗排行` 查看全服决斗排行榜
|
||||
- **个人统计**:使用 `决斗战绩` 查看个人决斗数据
|
||||
- **查看装备**:使用 `我的装备` 查看自己当前的装备
|
||||
- **更改名称**:使用 `改名 旧名称 新名称` 更改自己在决斗系统中的显示名称
|
||||
|
||||
#### 古灵阁妖精馈赠
|
||||
|
||||
这是一个随机事件系统,机器人会在聊天中随机触发"古灵阁妖精馈赠"事件,为用户提供惊喜奖励。
|
||||
|
||||
## 📋 项目结构
|
||||
|
||||
```
|
||||
Bubbles-WechatAI/
|
||||
├── ai_providers/ # AI 模型接口实现
|
||||
├── commands/ # 命令系统实现
|
||||
├── data/ # 数据文件
|
||||
├── function/ # 功能模块
|
||||
│ ├── func_duel.py # 决斗功能
|
||||
│ ├── func_news.py # 新闻功能
|
||||
│ ├── func_weather.py # 天气功能
|
||||
├── ai_providers/ # AI 模块
|
||||
│ ├── ai_name.py # AI 模型接口实现
|
||||
│ └── ...
|
||||
├── commands/ # 命令系统
|
||||
│ ├── registry.py # 命令注册
|
||||
│ ├── handlers.py # 实现功能调用的函数
|
||||
│ └── ...
|
||||
├── data/ # 数据文件
|
||||
│
|
||||
├── function/ # 功能模块
|
||||
│ ├── func_feature.py # 各种功能的具体实现
|
||||
│ └── ...
|
||||
├── image/ # 图像生成相关
|
||||
├── logs/ # 日志目录
|
||||
├── config.yaml # 配置文件
|
||||
├── config.yaml.template # 配置模板
|
||||
├── constants.py # 常量定义
|
||||
├── main.py # 入口文件
|
||||
├── robot.py # 机器人核心实现
|
||||
└── requirements.txt # 项目依赖
|
||||
└── ...
|
||||
```
|
||||
|
||||
## 🤝 贡献指南
|
||||
### ✨ 如何添加新功能(命令路由系统)
|
||||
|
||||
欢迎对本项目做出贡献!您可以通过以下方式参与:
|
||||
本项目设计了一套简单的命令路由系统,让添加新功能变得容易。主要流程如下:
|
||||
|
||||
1. **报告问题**:提交 issue 报告 bug 或提出功能建议
|
||||
2. **提交代码**:通过 Pull Request 提交您的改进
|
||||
3. **完善文档**:帮助改进项目文档
|
||||
1. **定义功能逻辑 (可选但推荐)**:
|
||||
* 如果你的功能逻辑比较复杂,建议在 `function/` 目录下创建一个新的 Python 文件 (例如 `func_your_feature.py`)。
|
||||
* 在这个文件中实现你的核心功能代码,例如定义类或函数。这有助于保持代码结构清晰。
|
||||
|
||||
2. **创建命令处理器**:
|
||||
* 打开 `commands/handlers.py` 文件。
|
||||
* 添加一个新的处理函数,例如 `handle_your_feature(ctx: 'MessageContext', match: Optional[Match]) -> bool:`。
|
||||
* 这个函数接收 `MessageContext` (包含消息上下文信息) 和 `match` (正则表达式匹配结果) 作为参数。
|
||||
* 在函数内部,你可以:
|
||||
* 调用你在 `function/` 目录下创建的功能模块。
|
||||
* 使用 `ctx.send_text()` 发送回复消息。
|
||||
* 根据需要处理 `match` 对象提取用户输入的参数。
|
||||
* 函数应返回 `True` 表示命令已被处理,`False` 则表示未处理 (会继续尝试匹配后续命令或进行闲聊)。
|
||||
* 确保从 `function` 目录导入必要的模块。
|
||||
|
||||
3. **注册命令**:
|
||||
* 打开 `commands/registry.py` 文件。
|
||||
* 在 `COMMANDS` 列表中,按照优先级顺序添加一个新的 `Command` 对象。
|
||||
* 配置 `Command` 参数:
|
||||
* `name`: 命令的唯一标识名 (小写下划线)。
|
||||
* `pattern`: 用于匹配用户输入的正则表达式 (`re.compile`)。注意捕获用户参数。
|
||||
* `scope`: 命令适用范围 (`"group"`, `"private"`, `"both"`)。
|
||||
* `need_at`: 在群聊中是否需要 `@` 机器人才能触发 (`True`/`False`)。
|
||||
* `priority`: 命令的优先级 (数字越小越优先匹配)。
|
||||
* `handler`: 指向你在 `handlers.py` 中创建的处理函数 (例如 `handle_your_feature`)。
|
||||
* `description`: 命令的简短描述,用于帮助信息。
|
||||
* 确保从 `handlers.py` 导入你的新处理函数。
|
||||
|
||||
4. **更新帮助信息 (可选)**:
|
||||
* 如果希望用户能在 `帮助` 命令中看到你的新功能,可以更新 `commands/handlers.py` 中的 `handle_help` 函数,将新命令的用法添加到帮助文本中。
|
||||
|
||||
完成以上步骤后,重启机器人即可测试你的新功能!
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
|
||||
@@ -845,6 +845,9 @@ def handle_reminder(ctx: 'MessageContext', match: Optional[Match]) -> bool:
|
||||
}},
|
||||
// ... 可能有更多提醒对象 ...
|
||||
]
|
||||
|
||||
**重要:** 你的回复必须仅包含有效的JSON数组,不要包含任何其他说明文字。所有JSON中的布尔值、数字应该没有引号,字符串需要有引号。
|
||||
|
||||
- **仔细分析用户输入,识别所有独立的提醒请求。**
|
||||
- 对每一个识别出的提醒,判断其类型 (`once`, `daily`, `weekly`) 并计算准确时间。
|
||||
- "once"类型时间必须是 'YYYY-MM-DD HH:MM' 格式, "daily"/"weekly"类型必须是 'HH:MM' 格式。时间必须是未来的。
|
||||
@@ -869,42 +872,64 @@ def handle_reminder(ctx: 'MessageContext', match: Optional[Match]) -> bool:
|
||||
|
||||
# 获取AI回答
|
||||
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 数组
|
||||
# 实现最多尝试3次解析AI回复的逻辑
|
||||
max_retries = 3
|
||||
retry_count = 0
|
||||
parsed_reminders = [] # 初始化为空列表
|
||||
json_str = None
|
||||
# 尝试匹配 [...] 或 {...} (兼容单个提醒的情况,但优先列表)
|
||||
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 # 如果都找不到,直接尝试解析原始回复
|
||||
|
||||
try:
|
||||
parsed_data = json.loads(json_str)
|
||||
# 确保解析结果是一个列表,如果不是(比如解析了单个对象),包装成列表
|
||||
if isinstance(parsed_data, dict):
|
||||
parsed_reminders = [parsed_data] # 包装成单元素列表
|
||||
elif isinstance(parsed_data, list):
|
||||
parsed_reminders = parsed_data # 本身就是列表
|
||||
ai_parsing_success = False
|
||||
|
||||
while retry_count < max_retries and not ai_parsing_success:
|
||||
# 如果是重试,更新提示信息
|
||||
if retry_count > 0:
|
||||
enhanced_prompt = sys_prompt + f"\n\n**重要提示:** 这是第{retry_count+1}次尝试。你之前的回复格式有误,无法被解析为有效的JSON。请确保你的回复仅包含有效的JSON数组,没有其他任何文字。"
|
||||
formatted_prompt = enhanced_prompt.format(current_datetime=current_dt_str)
|
||||
# 在重试时提供更明确的信息
|
||||
retry_q = f"请再次解析以下提醒,并返回严格的JSON数组格式(第{retry_count+1}次尝试):\n{raw_text}"
|
||||
q_for_ai = retry_q
|
||||
|
||||
ai_response = ctx.chat.get_answer(q_for_ai, ctx.get_receiver(), system_prompt_override=formatted_prompt)
|
||||
|
||||
# 尝试匹配 [...] 或 {...} (兼容单个提醒的情况,但优先列表)
|
||||
json_match_list = re.search(r'\[.*\]', ai_response, re.DOTALL)
|
||||
json_match_obj = re.search(r'\{.*\}', ai_response, re.DOTALL)
|
||||
|
||||
json_str = None
|
||||
if json_match_list:
|
||||
json_str = json_match_list.group(0)
|
||||
elif json_match_obj: # 如果没找到列表,尝试找单个对象 (增加兼容性)
|
||||
json_str = f"[{json_match_obj.group(0)}]" # 将单个对象包装成数组
|
||||
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
|
||||
except ValueError as e:
|
||||
ctx.send_text(f"❌ 处理AI返回的数据时出错: {e}", at_list)
|
||||
if ctx.logger: ctx.logger.warning(f"AI 返回数据格式错误: {ai_response}")
|
||||
return True
|
||||
|
||||
json_str = ai_response # 如果都找不到,直接尝试解析原始回复
|
||||
|
||||
try:
|
||||
# 尝试解析JSON
|
||||
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 列表或对象")
|
||||
|
||||
# 如果能到这里,说明解析成功
|
||||
ai_parsing_success = True
|
||||
|
||||
except (json.JSONDecodeError, ValueError) as e:
|
||||
# JSON解析失败
|
||||
retry_count += 1
|
||||
if ctx.logger:
|
||||
ctx.logger.warning(f"AI 返回 JSON 解析失败(第{retry_count}次尝试): {ai_response}, 错误: {str(e)}")
|
||||
|
||||
if retry_count >= max_retries:
|
||||
# 达到最大重试次数,返回错误
|
||||
ctx.send_text(f"❌ 抱歉,无法理解您的提醒请求。请尝试换一种方式表达,或分开设置多个提醒。", at_list)
|
||||
if ctx.logger: ctx.logger.error(f"解析AI回复失败,已达到最大重试次数({max_retries}): {ai_response}")
|
||||
return True
|
||||
# 否则继续下一次循环重试
|
||||
|
||||
# 检查 ReminderManager 是否存在
|
||||
if not hasattr(ctx.robot, 'reminder_manager'):
|
||||
ctx.send_text("❌ 内部错误:提醒管理器未初始化。", at_list)
|
||||
@@ -978,11 +1003,10 @@ def handle_reminder(ctx: 'MessageContext', match: Optional[Match]) -> bool:
|
||||
|
||||
# 添加总览信息
|
||||
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")
|
||||
reply_parts.append(f"✅ 已设置 {successful_count} 个提醒,{failed_count} 个设置失败:\n")
|
||||
elif successful_count > 0:
|
||||
reply_parts.append(f"✅ 已{scope_info}成功设置全部 {successful_count} 个提醒:\n")
|
||||
reply_parts.append(f"✅ 已设置 {successful_count} 个提醒:\n")
|
||||
else:
|
||||
reply_parts.append(f"❌ 抱歉,所有 {len(results)} 个提醒设置均失败:\n")
|
||||
|
||||
@@ -1006,12 +1030,11 @@ def handle_reminder(ctx: 'MessageContext', match: Optional[Match]) -> bool:
|
||||
|
||||
# 单个提醒或多个提醒的第一个,不需要标签
|
||||
if len(results) == 1:
|
||||
scope_info = "在本群" if ctx.is_group else "私聊"
|
||||
reply_parts.append(f"✅ 好的,已为您{scope_info}设置{type_str}提醒 (ID: {reminder_id[:6]}):\n"
|
||||
reply_parts.append(f"✅ 已为您设置{type_str}提醒:\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}\"")
|
||||
reply_parts.append(f"✅ {res['label']}: {type_str}\n {time_display} - \"{content_preview}\"")
|
||||
else:
|
||||
# 失败的提醒
|
||||
if len(results) == 1:
|
||||
@@ -1094,69 +1117,32 @@ def handle_list_reminders(ctx: 'MessageContext', match: Optional[Match]) -> bool
|
||||
def handle_delete_reminder(ctx: 'MessageContext', match: Optional[Match]) -> bool:
|
||||
"""
|
||||
处理删除提醒命令(支持群聊和私聊)。
|
||||
优先尝试匹配 ID 或 "all",否则使用 AI 理解自然语言描述。
|
||||
检查消息是否包含"提醒"和"删"相关字眼,然后使用 AI 理解具体意图。
|
||||
"""
|
||||
if not hasattr(ctx.robot, 'reminder_manager'):
|
||||
ctx.send_text("❌ 内部错误:提醒管理器未初始化。", ctx.msg.sender if ctx.is_group else "")
|
||||
return True
|
||||
# 1. 获取用户输入的完整内容
|
||||
raw_text = ctx.msg.content.strip()
|
||||
|
||||
user_input_description = match.group(2).strip() # 用户描述要删除哪个提醒
|
||||
if not user_input_description:
|
||||
# 如果用户只说了"删除提醒"而没有说删哪个
|
||||
ctx.send_text("请告诉我您想删除哪个提醒(例如:删除提醒 开会的那个 / 删除提醒 ID: xxxxxx / 删除提醒 all)", ctx.msg.sender if ctx.is_group else "")
|
||||
return True
|
||||
# 2. 检查是否包含删除提醒的两个核心要素:"提醒"和"删/删除/取消"
|
||||
# Regex 已经保证了后者,这里只需检查前者
|
||||
if "提醒" not in raw_text:
|
||||
# 如果消息匹配了 "删" 但没有 "提醒",说明不是删除提醒的意图,不处理
|
||||
return False # 返回 False,让命令路由器可以尝试匹配其他命令
|
||||
|
||||
# 3. 检查 ReminderManager 是否存在
|
||||
if not hasattr(ctx.robot, 'reminder_manager'):
|
||||
# 这个检查需要保留,是内部依赖
|
||||
ctx.send_text("❌ 内部错误:提醒管理器未初始化。", ctx.msg.sender if ctx.is_group else "")
|
||||
return True # 确实是想处理,但内部错误,返回 True
|
||||
|
||||
# 在群聊中@用户
|
||||
at_list = ctx.msg.sender if ctx.is_group else ""
|
||||
|
||||
# --- 步骤 1: 检查是否删除所有 ---
|
||||
if user_input_description.lower() in ["all", "所有", "全部"]:
|
||||
success, message, count = ctx.robot.reminder_manager.delete_all_reminders(ctx.msg.sender)
|
||||
ctx.send_text(message, at_list)
|
||||
# 尝试触发馈赠
|
||||
if success and count > 0 and ctx.is_group and hasattr(ctx.robot, "goblin_gift_manager"):
|
||||
ctx.robot.goblin_gift_manager.try_trigger(ctx.msg)
|
||||
return True
|
||||
# --- 核心流程:直接使用 AI 分析 ---
|
||||
|
||||
# --- 步骤 2: 尝试直接匹配 ID ---
|
||||
potential_id_match = re.match(r"^(?:id[::\s]*)?([a-f0-9]{6,})$", user_input_description, re.IGNORECASE)
|
||||
if potential_id_match:
|
||||
partial_id = potential_id_match.group(1)
|
||||
reminders = ctx.robot.reminder_manager.list_reminders(ctx.msg.sender) # 获取列表用于查找完整ID
|
||||
found_id = None
|
||||
possible_matches = 0
|
||||
matched_reminder_content = "" # 记录匹配到的提醒内容
|
||||
|
||||
for r in reminders:
|
||||
if r['id'].startswith(partial_id):
|
||||
found_id = r['id']
|
||||
matched_reminder_content = r['content'][:30] # 获取部分内容用于反馈
|
||||
possible_matches += 1
|
||||
|
||||
if possible_matches == 1:
|
||||
# 精确匹配到一个ID,直接删除
|
||||
success, message = ctx.robot.reminder_manager.delete_reminder(ctx.msg.sender, found_id)
|
||||
# 在成功消息中包含部分内容,让用户更确定删对了
|
||||
if success:
|
||||
final_message = f"✅ 已成功删除提醒 (ID: {found_id[:6]}... 内容: \"{matched_reminder_content}...\")"
|
||||
else:
|
||||
final_message = message # 如果删除失败,显示原始错误信息
|
||||
ctx.send_text(final_message, at_list)
|
||||
# 尝试触发馈赠
|
||||
if success and ctx.is_group and hasattr(ctx.robot, "goblin_gift_manager"):
|
||||
ctx.robot.goblin_gift_manager.try_trigger(ctx.msg)
|
||||
return True # ID 匹配成功,流程结束
|
||||
elif possible_matches > 1:
|
||||
ctx.send_text(f"❌ 找到多个以 '{partial_id}' 开头的提醒ID,请提供更完整的ID。", at_list)
|
||||
return True # 找到多个,流程结束
|
||||
else:
|
||||
# 看起来像ID,但没找到,此时可以继续尝试 AI(或者提示未找到)
|
||||
# 决定:继续尝试 AI,也许用户输入了错误的 ID 但描述是对的
|
||||
pass # 让流程继续到 AI 部分
|
||||
|
||||
# --- 步骤 3: 如果不是 "all" 且 ID 匹配不成功(或压根不像ID),使用 AI ---
|
||||
# 4. 获取用户的所有提醒作为 AI 的上下文
|
||||
reminders = ctx.robot.reminder_manager.list_reminders(ctx.msg.sender)
|
||||
if not reminders:
|
||||
# 如果用户没有任何提醒,直接告知
|
||||
ctx.send_text("您当前没有任何提醒可供删除。", at_list)
|
||||
return True
|
||||
|
||||
@@ -1168,12 +1154,12 @@ def handle_delete_reminder(ctx: 'MessageContext', match: Optional[Match]) -> boo
|
||||
if ctx.logger: ctx.logger.error(f"序列化提醒列表失败: {e}", exc_info=True)
|
||||
return True
|
||||
|
||||
# 构造 AI Prompt
|
||||
# 注意:在 prompt 中所有字面量的 { 和 } 都需要转义为 {{ 和 }}
|
||||
# 5. 构造 AI Prompt (与之前相同,AI 需要能处理所有情况)
|
||||
# 注意:确保 prompt 中的 {{ 和 }} 转义正确
|
||||
sys_prompt = """
|
||||
你是提醒删除助手。用户会提出删除提醒的请求,可能是描述内容/时间,也可能是要求删除全部(虽然 'all' 的情况我们已经处理了,但你也要能理解)。我会提供用户的请求原文,以及一个包含该用户所有当前提醒的 JSON 列表。
|
||||
你是提醒删除助手。用户会提出删除提醒的请求。我会提供用户的**完整请求原文**,以及一个包含该用户所有当前提醒的 JSON 列表。
|
||||
|
||||
你的任务是:根据用户请求和提醒列表,判断用户的意图,并确定要删除哪些提醒。
|
||||
你的任务是:根据用户请求和提醒列表,判断用户的意图,并确定要删除哪些提醒。用户可能要求删除特定提醒(通过描述内容、时间、ID等),也可能要求删除所有提醒。
|
||||
|
||||
**必须严格**按照以下几种 JSON 格式之一返回结果:
|
||||
|
||||
@@ -1220,7 +1206,8 @@ def handle_delete_reminder(ctx: 'MessageContext', match: Optional[Match]) -> boo
|
||||
```
|
||||
|
||||
**重要:**
|
||||
- 仔细分析用户请求和提供的提醒列表 JSON 进行匹配。
|
||||
- 仔细分析用户的**完整请求原文**和提供的提醒列表 JSON 进行匹配。
|
||||
- 用户请求中可能直接包含 ID,也需要你能识别并匹配。
|
||||
- 匹配时要综合考虑内容、时间、类型(一次性/每日/每周)等信息。
|
||||
- 如果返回 `delete_specific`,必须提供 **完整** 的 reminder ID。
|
||||
- **只输出 JSON 结构,不要包含任何额外的解释性文字。**
|
||||
@@ -1243,38 +1230,69 @@ def handle_delete_reminder(ctx: 'MessageContext', match: Optional[Match]) -> boo
|
||||
return True
|
||||
|
||||
|
||||
# 调用 AI
|
||||
q_for_ai = f"请根据以下用户请求,分析需要删除哪个提醒:\n{user_input_description}"
|
||||
# 6. 调用 AI (使用完整的用户原始输入)
|
||||
q_for_ai = f"请根据以下用户完整请求,分析需要删除哪个提醒:\n{raw_text}" # 使用 raw_text
|
||||
try:
|
||||
if not hasattr(ctx, 'chat') or not ctx.chat:
|
||||
raise ValueError("当前上下文中没有可用的AI模型")
|
||||
|
||||
ai_response = ctx.chat.get_answer(q_for_ai, ctx.get_receiver(), system_prompt_override=formatted_prompt)
|
||||
|
||||
# 解析 AI 的 JSON 回复
|
||||
# 实现最多尝试3次解析AI回复的逻辑
|
||||
max_retries = 3
|
||||
retry_count = 0
|
||||
parsed_ai_response = None
|
||||
json_str = None
|
||||
# 优先匹配 {...} 因为我们期望的是一个对象
|
||||
json_match_obj = re.search(r'\{.*\}', ai_response, re.DOTALL)
|
||||
if json_match_obj:
|
||||
json_str = json_match_obj.group(0)
|
||||
else:
|
||||
json_str = ai_response # 没有找到对象,尝试整个解析
|
||||
ai_parsing_success = False
|
||||
|
||||
while retry_count < max_retries and not ai_parsing_success:
|
||||
# 如果是重试,更新提示信息
|
||||
if retry_count > 0:
|
||||
enhanced_prompt = sys_prompt + f"\n\n**重要提示:** 这是第{retry_count+1}次尝试。你之前的回复格式有误,无法被解析为有效的JSON。请确保你的回复仅包含有效的JSON对象,没有其他任何文字。"
|
||||
try:
|
||||
formatted_prompt = enhanced_prompt.format(
|
||||
reminders_list_json=reminders_json_str,
|
||||
current_datetime=current_dt_str
|
||||
)
|
||||
except Exception as e:
|
||||
ctx.send_text("❌ 内部错误:构建重试请求时出错。", at_list)
|
||||
if ctx.logger: ctx.logger.error(f"格式化重试 prompt 失败: {e}", exc_info=True)
|
||||
return True
|
||||
|
||||
# 在重试时提供更明确的信息
|
||||
retry_q = f"请再次分析以下删除提醒请求,并返回严格的JSON格式(第{retry_count+1}次尝试):\n{raw_text}"
|
||||
q_for_ai = retry_q
|
||||
|
||||
try:
|
||||
parsed_ai_response = json.loads(json_str)
|
||||
if not isinstance(parsed_ai_response, dict) or "action" not in parsed_ai_response:
|
||||
raise ValueError("AI 返回的 JSON 格式不符合预期(缺少 action 字段)")
|
||||
except json.JSONDecodeError:
|
||||
ctx.send_text(f"❌ 无法解析 AI 的删除指令。", at_list)
|
||||
if ctx.logger: ctx.logger.warning(f"AI 删除提醒 JSON 解析失败: {ai_response}")
|
||||
return True
|
||||
except ValueError as e:
|
||||
ctx.send_text(f"❌ AI 返回的删除指令格式错误。", at_list)
|
||||
if ctx.logger: ctx.logger.warning(f"AI 删除提醒 JSON 格式错误: {e} - Response: {ai_response}")
|
||||
return True
|
||||
# 获取AI回答
|
||||
ai_response = ctx.chat.get_answer(q_for_ai, ctx.get_receiver(), system_prompt_override=formatted_prompt)
|
||||
|
||||
# --- 步骤 4: 根据 AI 指令执行操作 ---
|
||||
# 7. 解析 AI 的 JSON 回复
|
||||
json_str = None
|
||||
json_match_obj = re.search(r'\{.*\}', ai_response, re.DOTALL)
|
||||
if json_match_obj:
|
||||
json_str = json_match_obj.group(0)
|
||||
else:
|
||||
json_str = ai_response
|
||||
|
||||
try:
|
||||
parsed_ai_response = json.loads(json_str)
|
||||
if not isinstance(parsed_ai_response, dict) or "action" not in parsed_ai_response:
|
||||
raise ValueError("AI 返回的 JSON 格式不符合预期(缺少 action 字段)")
|
||||
|
||||
# 如果能到这里,说明解析成功
|
||||
ai_parsing_success = True
|
||||
|
||||
except (json.JSONDecodeError, ValueError) as e:
|
||||
# JSON解析失败
|
||||
retry_count += 1
|
||||
if ctx.logger:
|
||||
ctx.logger.warning(f"AI 删除提醒 JSON 解析失败(第{retry_count}次尝试): {ai_response}, 错误: {str(e)}")
|
||||
|
||||
if retry_count >= max_retries:
|
||||
# 达到最大重试次数,返回错误
|
||||
ctx.send_text(f"❌ 抱歉,无法理解您的删除提醒请求。请尝试换一种方式表达,或使用提醒ID进行精确删除。", at_list)
|
||||
if ctx.logger: ctx.logger.error(f"解析AI删除提醒回复失败,已达到最大重试次数({max_retries}): {ai_response}")
|
||||
return True
|
||||
# 否则继续下一次循环重试
|
||||
|
||||
# 8. 根据 AI 指令执行操作 (与之前相同)
|
||||
action = parsed_ai_response.get("action")
|
||||
|
||||
if action == "delete_specific":
|
||||
@@ -1285,11 +1303,9 @@ def handle_delete_reminder(ctx: 'MessageContext', match: Optional[Match]) -> boo
|
||||
|
||||
delete_results = []
|
||||
successful_deletes = 0
|
||||
# 记录删除的提醒描述,用于反馈
|
||||
deleted_descriptions = []
|
||||
|
||||
for r_id in reminder_ids_to_delete:
|
||||
# 从原始列表中查找提醒内容,用于反馈
|
||||
original_reminder = next((r for r in reminders if r['id'] == r_id), None)
|
||||
desc = f"ID:{r_id[:6]}..."
|
||||
if original_reminder:
|
||||
@@ -1301,9 +1317,8 @@ def handle_delete_reminder(ctx: 'MessageContext', match: Optional[Match]) -> boo
|
||||
successful_deletes += 1
|
||||
deleted_descriptions.append(desc)
|
||||
|
||||
# 构建反馈消息
|
||||
if successful_deletes == len(reminder_ids_to_delete):
|
||||
reply_msg = f"✅ 已成功删除 {successful_deletes} 个提醒:\n" + "\n".join([f"- {d}" for d in deleted_descriptions])
|
||||
reply_msg = f"✅ 已删除 {successful_deletes} 个提醒:\n" + "\n".join([f"- {d}" for d in deleted_descriptions])
|
||||
elif successful_deletes > 0:
|
||||
reply_msg = f"⚠️ 部分提醒删除完成 ({successful_deletes}/{len(reminder_ids_to_delete)}):\n"
|
||||
for res in delete_results:
|
||||
@@ -1315,28 +1330,23 @@ def handle_delete_reminder(ctx: 'MessageContext', match: Optional[Match]) -> boo
|
||||
reply_msg += f"- {res['description']}: 失败原因: {res['message']}\n"
|
||||
|
||||
ctx.send_text(reply_msg.strip(), at_list)
|
||||
# 尝试触发馈赠
|
||||
if successful_deletes > 0 and ctx.is_group and hasattr(ctx.robot, "goblin_gift_manager"):
|
||||
ctx.robot.goblin_gift_manager.try_trigger(ctx.msg)
|
||||
|
||||
elif action == "delete_all":
|
||||
success, message, count = ctx.robot.reminder_manager.delete_all_reminders(ctx.msg.sender)
|
||||
ctx.send_text(message, at_list)
|
||||
# 尝试触发馈赠
|
||||
if success and count > 0 and ctx.is_group and hasattr(ctx.robot, "goblin_gift_manager"):
|
||||
ctx.robot.goblin_gift_manager.try_trigger(ctx.msg)
|
||||
|
||||
elif action in ["clarify", "not_found", "error"]:
|
||||
# 直接转发 AI 给用户的消息
|
||||
message_to_user = parsed_ai_response.get("message", "抱歉,我没能处理您的请求。")
|
||||
# 可以选择性地格式化 options
|
||||
if action == "clarify" and "options" in parsed_ai_response:
|
||||
options_text = "\n可能的选项:\n" + "\n".join([f"- ID: {opt.get('id', 'N/A')} ({opt.get('description', '无描述')})" for opt in parsed_ai_response["options"]])
|
||||
message_to_user += options_text
|
||||
ctx.send_text(message_to_user, at_list)
|
||||
|
||||
else:
|
||||
# AI 返回了未知的 action
|
||||
ctx.send_text("❌ AI 返回了无法理解的指令。", at_list)
|
||||
if ctx.logger: ctx.logger.error(f"AI 删除提醒返回未知 action: {action} - Response: {ai_response}")
|
||||
|
||||
|
||||
@@ -191,13 +191,13 @@ COMMANDS = [
|
||||
|
||||
Command(
|
||||
name="delete_reminder",
|
||||
# 匹配 "删除提醒 " 后跟任意内容,用于删除特定提醒
|
||||
pattern=re.compile(r"^(删除提醒|取消提醒)\s+(.+)$", re.IGNORECASE | re.DOTALL),
|
||||
# 修改为只匹配包含"删"、"删除"或"取消"的消息,不再要求特定格式
|
||||
pattern=re.compile(r"(?:删|删除|取消)", re.IGNORECASE),
|
||||
scope="both", # 支持群聊和私聊
|
||||
need_at=True, # 在群聊中需要@机器人
|
||||
priority=37,
|
||||
handler=handle_delete_reminder,
|
||||
description="删除指定提醒 (可用自然语言描述,如'删掉明天开会的那个',或用'ID:xxxxxx',或用'all'删除全部)"
|
||||
description="删除提醒 (包含'删'和'提醒'关键字即可,如: 把开会的提醒删了)"
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user