From ac74baf3fbbb8d2ffcfed6e393ebf9e9853f1e0b Mon Sep 17 00:00:00 2001 From: ninemine <1371605831@qq.com> Date: Thu, 20 Nov 2025 11:36:29 +0800 Subject: [PATCH] =?UTF-8?q?=E5=B0=9D=E8=AF=95=E6=96=B0=E5=A2=9Eai=E4=BC=9A?= =?UTF-8?q?=E8=AF=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .tasks/2025-11-20_1_ai_chat_plugin.md | 217 +++++++++++++ Plugins/Others/ChatAI.py | 423 ++++++++++++++++++++++++++ Plugins/Others/NewsReport.py | 4 +- 3 files changed, 641 insertions(+), 3 deletions(-) create mode 100644 .tasks/2025-11-20_1_ai_chat_plugin.md create mode 100644 Plugins/Others/ChatAI.py diff --git a/.tasks/2025-11-20_1_ai_chat_plugin.md b/.tasks/2025-11-20_1_ai_chat_plugin.md new file mode 100644 index 0000000..145f949 --- /dev/null +++ b/.tasks/2025-11-20_1_ai_chat_plugin.md @@ -0,0 +1,217 @@ +# 背景 +文件名:2025-11-20_1_ai_chat_plugin.md +创建于:2025-11-20_11:06:10 +创建者:admin +主分支:main +任务分支:无(不创建分支) +Yolo模式:Off + +# 任务描述 +创建一个 AI 对话插件,支持多用户、多群聊的会话隔离,具备短期记忆功能(内存存储,无需持久化)。 + +## 核心需求 +1. 基于 LlamaIndex + Ollama 的 AI 对话功能 +2. **维护一个全局的会话历史**(不按用户/群聊分隔) +3. AI 能同时在不同群聊和不同用户对话 +4. 历史消息包含时间戳、群聊ID、用户ID信息,AI 能分辨消息来源 +5. 最大历史消息数量可配置(默认20条) +6. 不需要持久化,重启后会话清空 +7. 提供清空全局历史会话的指令 +8. **AI 能够 @ 用户**:使用 `` 格式,禁止直接输出 user_id + +## 指令入口 +- `ai_chat`: 对话指令 +- `ai_chat_clear`: 清空当前用户的会话历史 + +## 技术要点 +- 参考 NewsReport.py 的架构模式 +- 使用内存字典存储会话历史 +- 模型使用 qwen3:0.6b +- 消息格式包含上下文信息(时间、群聊、用户) + +# 项目概览 +这是一个基于 PWF 框架的 WPS 机器人项目,使用插件化架构。 +- 插件基类:WPSAPI → BasicWPSInterface → PluginInterface +- 每个插件通过 callback(message, chat_id, user_id) 处理消息 +- 支持数据库、定时任务、路由等功能 +- 已有多个游戏系统插件(菜园、战斗、炼金等) + +# 分析 +## 现有架构分析 +1. **插件系统**: + - 插件通过继承 WPSAPI 实现 + - callback 方法接收 message(已去除 command)、chat_id、user_id + - 通过 register_plugin(command) 注册指令入口 + - 通过 dependencies() 声明依赖关系 + +2. **NewsReport.py 架构**: + - NewsAIAgent 类:封装 AI 逻辑(LlamaIndex + Ollama) + - NewsAIPlugin 类:继承 WPSAPI,作为插件入口 + - 使用 ProjectConfig().GetFile() 进行文件缓存 + +3. **会话管理需求**: + - **全局单一会话历史**,不按用户/群聊分隔 + - 每条消息标记 chat_id(群聊ID)和 user_id(用户ID) + - AI 能看到所有群聊、所有用户的对话历史 + - 使用 List[Dict] 存储全局消息列表 + - 内存存储,无需持久化 + +4. **配置系统**: + - 使用 ProjectConfig.FindItem(key, default) 读取配置 + - 使用 ProjectConfig.SaveProperties() 保存配置 + - 配置存储在 Assets/config.json + +## 技术选型 +- **AI 框架**:LlamaIndex + Ollama +- **模型**:qwen3:0.6b +- **存储方式**:内存列表(List[Dict])- 全局单一历史 +- **消息格式**:包含 timestamp, chat_id, user_id, role, content + +# 提议的解决方案 +## 整体架构 +``` +ChatAI.py +├── ChatAIAgent (AI 智能体) +│ ├── 会话字典管理 +│ ├── 消息历史维护 +│ └── LLM 对话调用 +├── ChatAIPlugin (对话插件) +│ ├── 注册 ai_chat 指令 +│ └── 处理用户对话 +└── ChatAIClearPlugin (清空历史插件) + ├── 注册 ai_chat_clear 指令 + └── 清空会话历史 +``` + +## 核心类设计 +### 1. ChatAIAgent +- 单例模式,通过 Architecture 注册 +- 维护 **全局消息历史**: List[Dict] +- 每条消息格式: + ```python + { + "timestamp": "2025-11-20 11:06:10", + "chat_id": 12345, + "user_id": 67890, + "role": "user" | "assistant", + "content": "消息内容" + } + ``` +- AI 能够看到所有群聊、所有用户的完整对话历史 + +### 2. ChatAIPlugin +- 继承 WPSAPI +- 依赖 WPSAPI +- 初始化时创建/获取 ChatAIAgent +- callback 处理用户消息,调用 Agent.chat() + +### 3. ChatAIClearPlugin +- 继承 WPSAPI +- 依赖 ChatAIPlugin(确保 Agent 已初始化) +- callback 调用 Agent.clear_history() 清空全局历史 + +## 系统提示词设计 +AI 需要理解的关键信息: +1. **多群聊、多用户环境**:能看到所有群聊和用户的对话历史 +2. **消息格式**:每条消息包含 [时间] [群聊ID] [用户ID] 标记 +3. **@ 用户格式**:AI 回复时,当需要特指或回复某个用户时,使用 `` +4. **禁止行为**:禁止直接输出裸露的 user_id 数字 + +示例提示词: +``` +你是一个友好的AI助手,能够同时在多个群聊中与不同用户对话。 + +重要规则: +- 你能看到所有群聊的对话历史,每条消息都标注了时间、群聊ID和用户ID +- 当你需要特指某个用户或专门回复某个用户时,必须在回复中使用以下格式: +- 禁止直接输出用户ID数字,始终使用 标签包裹 +- 注意区分不同群聊和不同用户的对话上下文 + +示例对话: +[2025-11-20 10:00:00] [群聊#12345] [用户#67890]: 你好 +AI回复: 你好!有什么可以帮助你的吗? + +[2025-11-20 10:01:00] [群聊#12345] [用户#11111]: 今天天气怎么样? +AI回复: 抱歉,我无法获取实时天气信息。 + +[2025-11-20 10:02:00] [群聊#67890] [用户#22222]: 刚才12345群的用户说了什么? +AI回复: 刚才群聊12345中,用户67890问候了我,用户11111询问了天气。 +``` + +## 消息流程 +``` +用户消息 (chat_id, user_id) → ChatAIPlugin.callback() + ↓ +获取当前时间 + ↓ +添加用户消息到全局历史(带时间戳、chat_id、user_id) + ↓ +格式化全局历史消息为 LLM 输入 + ├─ 包含所有群聊、所有用户的历史 + ├─ 每条消息标注来源(群聊、用户、时间) + └─ AI 能理解跨群聊、跨用户的上下文 + ↓ +调用 Ollama LLM(使用包含 @ 格式说明的系统提示词) + ↓ +添加 AI 回复到全局历史(标注当前 chat_id, user_id) + ↓ +返回格式化的回复 +``` + +# 当前执行步骤:"1. 创建任务文件" + +# 任务进度 +[2025-11-20_11:06:10] +- 已创建:.tasks/2025-11-20_1_ai_chat_plugin.md +- 更改:创建任务文件,记录任务背景和设计方案 +- 原因:按照 RIPER-5 协议要求,在进入 EXECUTE 模式前创建任务文件 +- 阻碍因素:无 +- 状态:成功 + +[2025-11-20_11:13:25] +- 已修改:.tasks/2025-11-20_1_ai_chat_plugin.md +- 更改: + 1. 更新核心需求,明确全局单一会话历史设计 + 2. 添加系统提示词设计章节 + 3. 说明 @ 用户格式: + 4. 禁止直接输出 user_id +- 原因:根据用户反馈,澄清架构设计(全局历史 vs 分用户历史) +- 阻碍因素:无 +- 状态:成功 + +[2025-11-20_11:16:49] +- 已修改:.tasks/2025-11-20_1_ai_chat_plugin.md +- 更改:修正系统提示词设计,明确用户消息是普通文本,只有 AI 回复时才使用 格式 +- 原因:用户澄清 @ 用户格式只用于 AI 回复,不用于用户消息 +- 阻碍因素:无 +- 状态:成功 + +[2025-11-20_11:22:38] +- 已创建:Plugins/Others/ChatAI.py +- 更改:完整实现 AI 对话插件系统 + 1. ChatAIAgent 类:全局会话管理、消息历史维护、LLM 对话 + 2. ChatAIPlugin 类:注册 ai_chat 指令,处理用户对话 + 3. ChatAIClearPlugin 类:注册 ai_chat_clear 指令,清空历史 + 4. 使用 qwen3:0.6b 模型 + 5. 最大历史消息数量可配置(默认20) + 6. 系统提示词包含 @ 用户格式说明 + 7. 全局单一会话历史设计 +- 原因:实施 EXECUTE 模式清单项 1-28 +- 阻碍因素:无 +- 状态:成功 + +[2025-11-20_11:29:24] +- 已修改:Plugins/Others/ChatAI.py +- 更改:修复 AI 重复用户消息的问题 + 1. 从 acomplete(文本补全)改为 achat(对话)方法 + 2. 使用 ChatMessage 结构化消息格式 + 3. 分离系统提示词和对话历史 + 4. 优化提示词结构,更清晰地指示 AI 回复 + 5. 添加 llama_index.core.llms.ChatMessage 导入 +- 原因:用户反馈 AI 一直重复用户说的话 +- 阻碍因素:无 +- 状态:未确认 + +# 最终审查 +待完成 + diff --git a/Plugins/Others/ChatAI.py b/Plugins/Others/ChatAI.py new file mode 100644 index 0000000..733b3a0 --- /dev/null +++ b/Plugins/Others/ChatAI.py @@ -0,0 +1,423 @@ +from Plugins.WPSAPI import * +from datetime import datetime +from llama_index.llms.ollama import Ollama +from llama_index.core.llms import ChatMessage +from typing import List, Dict, Optional + +logger: ProjectConfig = Architecture.Get(ProjectConfig) +OLLAMA_URL = logger.FindItem("ollama_url", "http://ollama.liubai.site") +OLLAMA_MODEL = logger.FindItem("ollama_model", "qwen3:0.6b") +MAX_HISTORY = logger.FindItem("chat_ai_max_history", 20) +logger.SaveProperties() + + +class ChatAIAgent: + """AI 对话智能体 - 维护全局会话历史""" + + _instance: Optional['ChatAIAgent'] = None + + def __init__(self, ollama_url: str = OLLAMA_URL, max_history: int = MAX_HISTORY): + """初始化 AI 智能体 + + Args: + ollama_url: Ollama 服务地址 + max_history: 最大历史消息数量 + """ + self.ollama_url = ollama_url + self.max_history = max_history + self.llm = Ollama(model=OLLAMA_MODEL, base_url=ollama_url, request_timeout=600.0) + + # 全局消息历史列表 + self.global_history: List[Dict[str, any]] = [] + + # 系统提示词 + self.system_prompt = """你是一个友好的AI助手,能够同时在多个群聊中与不同用户对话。 + +重要规则: +- 你能看到所有群聊的对话历史,每条消息都标注了时间、群聊ID和用户ID +- 当你需要特指某个用户或专门回复某个用户时,必须在回复中使用以下格式: +- 禁止直接输出用户ID数字,始终使用 标签包裹 +- 注意区分不同群聊和不同用户的对话上下文 + +示例对话: +[2025-11-20 10:00:00] [群聊#12345] [用户#67890]: 你好 +你的回复: 你好!有什么可以帮助你的吗? + +[2025-11-20 10:01:00] [群聊#12345] [用户#11111]: 今天天气怎么样? +你的回复: 抱歉,我无法获取实时天气信息。""" + + logger.Log("Info", f"{ConsoleFrontColor.GREEN}ChatAIAgent 初始化完成{ConsoleFrontColor.RESET}") + logger.Log("Info", f"模型: {OLLAMA_MODEL}, 最大历史: {max_history}") + + @classmethod + def get_instance(cls) -> 'ChatAIAgent': + """获取单例实例""" + if cls._instance is None: + cls._instance = cls() + return cls._instance + + def _add_message(self, chat_id: int, user_id: int, role: str, content: str) -> None: + """添加消息到全局历史 + + Args: + chat_id: 群聊ID + user_id: 用户ID + role: 角色 (user/assistant) + content: 消息内容 + """ + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + message = { + "timestamp": timestamp, + "chat_id": chat_id, + "user_id": user_id, + "role": role, + "content": content + } + self.global_history.append(message) + self._trim_history() + + def _trim_history(self) -> None: + """裁剪历史消息到最大长度""" + if len(self.global_history) > self.max_history: + # 保留最新的 max_history 条消息 + self.global_history = self.global_history[-self.max_history:] + logger.Log("Info", f"历史消息已裁剪到 {self.max_history} 条") + + def clear_history(self) -> bool: + """清空全局历史 + + Returns: + 是否成功 + """ + try: + old_count = len(self.global_history) + self.global_history.clear() + logger.Log("Info", f"已清空全局历史,共删除 {old_count} 条消息") + return True + except Exception as e: + logger.Log("Error", f"清空历史失败: {e}") + return False + + def _format_message_with_context(self, msg: Dict[str, any]) -> str: + """格式化单条消息,包含上下文信息 + + Args: + msg: 消息字典 + + Returns: + 格式化后的消息字符串 + """ + timestamp = msg.get("timestamp", "") + chat_id = msg.get("chat_id", "") + user_id = msg.get("user_id", "") + role = msg.get("role", "") + content = msg.get("content", "") + + if role == "user": + return f"[{timestamp}] [群聊#{chat_id}] [用户#{user_id}]: {content}" + else: # assistant + return f"[{timestamp}] [AI助手]: {content}" + + def _format_history_for_llm(self) -> str: + """格式化全局历史为 LLM 输入 + + Returns: + 格式化后的历史字符串 + """ + if not self.global_history: + return "" + + formatted_messages = [ + self._format_message_with_context(msg) + for msg in self.global_history + ] + return "\n".join(formatted_messages) + + async def chat(self, message: str, chat_id: int, user_id: int) -> str: + """处理对话 + + Args: + message: 用户消息 + chat_id: 群聊ID + user_id: 用户ID + + Returns: + AI 回复 + """ + try: + # 添加用户消息到历史 + self._add_message(chat_id, user_id, "user", message) + + # 格式化历史消息(不包括刚添加的用户消息) + history_without_last = self.global_history[:-1] if len(self.global_history) > 1 else [] + history_text = "\n".join([ + self._format_message_with_context(msg) + for msg in history_without_last + ]) if history_without_last else "(这是第一条对话)" + + # 构建当前用户消息的格式化版本 + current_msg = f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] [群聊#{chat_id}] [用户#{user_id}]: {message}" + + # 构建对话提示 + conversation_prompt = f"""以下是历史对话记录: +{history_text} + +当前消息: +{current_msg} + +请作为AI助手回复当前用户。记住: +1. 使用 格式来称呼该用户 +2. 根据对话历史和当前消息,给出有意义的回复 +3. 如果历史中有其他群聊或用户的信息,你可以引用它们""" + + logger.Log("Info", f"处理对话 - 群聊#{chat_id} 用户#{user_id}") + logger.Log("Info", f"当前历史消息数: {len(self.global_history)}") + + # 使用 achat 方法进行对话 + messages = [ + ChatMessage(role="system", content=self.system_prompt), + ChatMessage(role="user", content=conversation_prompt) + ] + + response = await self.llm.achat(messages) + answer = str(response.message.content) + + # 添加 AI 回复到历史 + self._add_message(chat_id, user_id, "assistant", answer) + + logger.Log("Info", f"AI 回复长度: {len(answer)} 字符") + + return answer + + except Exception as e: + logger.Log("Error", f"对话处理失败: {e}") + import traceback + error_trace = traceback.format_exc() + logger.Log("Error", f"详细错误:\n{error_trace}") + return f"处理对话时出错: {str(e)}" + + +class ChatAIPlugin(WPSAPI): + """AI 对话插件""" + + def __init__(self): + super().__init__() + self.ai_agent = ChatAIAgent.get_instance() + + @override + def dependencies(self) -> List[Type]: + return [WPSAPI] + + @override + def is_enable_plugin(self) -> bool: + return True + + def get_guide_title(self) -> str: + return "AI 智能对话" + + def get_guide_subtitle(self) -> str: + return "基于 LlamaIndex + Ollama 的多群聊 AI 对话系统" + + def get_guide_metadata(self) -> Dict[str, str]: + return { + "AI模型": OLLAMA_MODEL, + "最大历史": str(MAX_HISTORY), + "功能": "跨群聊智能对话", + } + + def collect_command_entries(self) -> Sequence[GuideEntry]: + return ( + { + "title": "AI对话", + "identifier": "ai_chat", + "description": "与AI助手对话,AI能看到所有群聊的历史消息并智能回复。", + "metadata": {"模型": OLLAMA_MODEL}, + "icon": "🤖", + "badge": "AI", + "details": [ + { + "type": "list", + "items": [ + "AI 维护全局会话历史,能看到所有群聊的对话", + "每条消息包含时间戳、群聊ID、用户ID", + f"最多保留最近 {MAX_HISTORY} 条消息", + "AI 会使用 @用户 格式回复", + "示例:ai_chat 你好", + "示例:ai_chat 刚才其他群聊说了什么?", + ] + } + ] + }, + ) + + def collect_guide_entries(self) -> Sequence[GuideEntry]: + return ( + { + "title": "全局会话", + "description": ( + "AI 维护一个全局的对话历史,能够跨群聊、跨用户理解上下文。" + "所有群聊的对话都在同一个历史中,AI 能够关联不同群聊的信息。" + ), + "icon": "🌐", + }, + { + "title": "智能回复", + "description": ( + "AI 能够识别消息来源(群聊、用户、时间),并使用 @ 格式回复特定用户。" + ), + "icon": "💬", + }, + { + "title": "历史管理", + "description": ( + f"自动保留最近 {MAX_HISTORY} 条消息,超出部分自动清理。" + "可使用 ai_chat_clear 指令手动清空所有历史。" + ), + "icon": "📝", + }, + ) + + @override + def wake_up(self) -> None: + logger.Log("Info", f"{ConsoleFrontColor.GREEN}ChatAIPlugin AI对话插件已加载{ConsoleFrontColor.RESET}") + self.register_plugin("ai_chat") + + @override + async def callback(self, message: str, chat_id: int, user_id: int) -> str|None: + """处理用户对话""" + try: + if not message or message.strip() == "": + help_text = f"""# 🤖 AI 智能对话使用帮助 + +**直接发送消息即可与 AI 对话** + +**特性:** +- ✨ AI 能看到所有群聊的对话历史 +- 🌐 跨群聊智能理解上下文 +- 💬 自动使用 @ 格式回复用户 +- 📝 保留最近 {MAX_HISTORY} 条消息 + +**示例:** +- `ai_chat 你好` +- `ai_chat 今天天气怎么样?` +- `ai_chat 刚才其他群聊说了什么?` +- `ai_chat 请总结一下最近的对话` + +**清空历史:** +使用 `ai_chat_clear` 指令清空所有历史消息 + +**技术:** +基于 LlamaIndex + Ollama ({OLLAMA_MODEL})""" + return await self.send_markdown_message(help_text, chat_id, user_id) + + # 使用智能体处理对话 + answer = await self.ai_agent.chat(message, chat_id, user_id) + + # 格式化返回结果 + formatted_answer = f"""🤖 **AI 智能对话** + +{answer} + +--- +*由 LlamaIndex + Ollama 驱动*""" + + return await self.send_markdown_message(formatted_answer, chat_id, user_id) + + except Exception as e: + logger.Log("Error", f"AI对话异常: {e}") + import traceback + error_detail = traceback.format_exc() + logger.Log("Error", f"详细错误: {error_detail}") + error_msg = f"""❌ **处理对话时出错** + +错误信息:{str(e)} + +请稍后重试或联系管理员。""" + return await self.send_markdown_message(error_msg, chat_id, user_id) + + +class ChatAIClearPlugin(WPSAPI): + """清空 AI 对话历史插件""" + + def __init__(self): + super().__init__() + + @override + def dependencies(self) -> List[Type]: + return [ChatAIPlugin] + + @override + def is_enable_plugin(self) -> bool: + return True + + def get_guide_title(self) -> str: + return "清空 AI 对话历史" + + def get_guide_subtitle(self) -> str: + return "清空全局 AI 对话历史记录" + + def get_guide_metadata(self) -> Dict[str, str]: + return { + "功能": "历史管理", + "作用范围": "全局", + } + + def collect_command_entries(self) -> Sequence[GuideEntry]: + return ( + { + "title": "清空历史", + "identifier": "ai_chat_clear", + "description": "清空 AI 的全局对话历史记录。", + "icon": "🗑️", + "badge": "管理", + "details": [ + { + "type": "list", + "items": [ + "清空所有群聊的对话历史", + "AI 将不再记得之前的对话", + "不影响 AI 的基础功能", + "示例:ai_chat_clear", + ] + } + ] + }, + ) + + @override + def wake_up(self) -> None: + logger.Log("Info", f"{ConsoleFrontColor.GREEN}ChatAIClearPlugin 清空历史插件已加载{ConsoleFrontColor.RESET}") + self.register_plugin("ai_chat_clear") + + @override + async def callback(self, message: str, chat_id: int, user_id: int) -> str|None: + """清空对话历史""" + try: + # 获取 AI Agent 实例 + ai_agent = ChatAIAgent.get_instance() + + # 清空历史 + success = ai_agent.clear_history() + + if success: + result_msg = """✅ **历史已清空** + +AI 的全局对话历史已成功清空。 + +AI 将不再记得之前的对话内容。""" + else: + result_msg = """❌ **清空失败** + +清空历史时出现错误,请稍后重试。""" + + return await self.send_markdown_message(result_msg, chat_id, user_id) + + except Exception as e: + logger.Log("Error", f"清空历史异常: {e}") + error_msg = f"""❌ **清空历史时出错** + +错误信息:{str(e)} + +请稍后重试或联系管理员。""" + return await self.send_markdown_message(error_msg, chat_id, user_id) + diff --git a/Plugins/Others/NewsReport.py b/Plugins/Others/NewsReport.py index 38aed63..51fca51 100644 --- a/Plugins/Others/NewsReport.py +++ b/Plugins/Others/NewsReport.py @@ -59,9 +59,7 @@ class NewsAIAgent: 【重要】你必须按照以下步骤使用工具: 步骤1: 确定日期 -- 如果用户问"今天"或"今日",调用 get_current_date -- 如果用户问"昨天"或"昨日",调用 get_yesterday_date -- 如果用户提到具体日期(如"2025年11月17日"),调用 parse_date_from_text +- 如果用户问"今天"或"今日"或者是与今日日期相关的问题,调用 get_current_date 获取今日日期 步骤2: 获取新闻内容 - 拿到日期后,必须调用 get_news_content(date="YYYY-MM-DD") 获取新闻