From 908b5c8c07cc7ff8ea3c68502225301a4ecb2fa5 Mon Sep 17 00:00:00 2001 From: shihao <3127647737@qq.com> Date: Thu, 11 Dec 2025 14:30:55 +0800 Subject: [PATCH] =?UTF-8?q?feta:api=E7=BB=9F=E4=B8=80=E4=B8=BAGemini?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Memory Bank/activeContext.md | 239 ----------- Memory Bank/decisionLog.md | 254 ------------ Memory Bank/progress.md | 315 --------------- Memory Bank/projectBrief.md | 77 ---- Memory Bank/systemPatterns.md | 252 ------------ plugins/AIChat.zip | Bin 0 -> 84937 bytes plugins/AIChat/main.py | 719 ++++++++++++++++++++++++++++++---- test_gemini_video.py | 83 ---- 8 files changed, 653 insertions(+), 1286 deletions(-) delete mode 100644 Memory Bank/activeContext.md delete mode 100644 Memory Bank/decisionLog.md delete mode 100644 Memory Bank/progress.md delete mode 100644 Memory Bank/projectBrief.md delete mode 100644 Memory Bank/systemPatterns.md create mode 100644 plugins/AIChat.zip delete mode 100644 test_gemini_video.py diff --git a/Memory Bank/activeContext.md b/Memory Bank/activeContext.md deleted file mode 100644 index e22de85..0000000 --- a/Memory Bank/activeContext.md +++ /dev/null @@ -1,239 +0,0 @@ -# 当前开发上下文 - -**更新时间:** 2025-01-12 14:30 -**当前阶段:** 开发完成,等待测试 -**当前任务:** 提交代码给用户进行远程测试 - -## ✅ 已完成的所有工作 - -### 第一阶段:WechatHook 层 ✅ - -**已实现的模块:** -1. ✅ `WechatHook/loader.py` - NoveLoader 类 (~280行) -2. ✅ `WechatHook/client.py` - WechatHookClient 类 (~450行) -3. ✅ `WechatHook/message_types.py` - 消息类型映射 (~180行) -4. ✅ `WechatHook/callbacks.py` - 回调处理器 (~180行) -5. ✅ `WechatHook/__init__.py` - 模块导出 - -### 第二阶段:Bot 核心层 ✅ - -**已实现的模块:** -1. ✅ 从 XYBotV2 复制 utils/ 目录(5个文件) -2. ✅ 从 XYBotV2 复制 database/ 目录(3个文件) -3. ✅ `utils/hookbot.py` - HookBot 核心类 (~120行) -4. ✅ `bot.py` - 主入口 (~200行) - -### 第三阶段:插件系统 ✅ - -**已实现的模块:** -1. ✅ `plugins/ExamplePlugin/main.py` - 示例插件 (~50行) -2. ✅ 插件系统完全集成 - -## 📊 代码统计 - -**总代码行数:** ~2000+ 行 - -**模块分布:** -- WechatHook 层: ~1090 行 -- Bot 核心层: ~320 行 -- 示例插件: ~50 行 -- 复用代码: ~600 行(utils + database) - -## 🎯 项目完成度 - -**总体进度:** 90% - -``` -[██████████████████░░] 90% -``` - -**剩余工作:** -- 远程设备测试 -- 根据测试结果调整 -- 修复发现的问题 - -## 📝 需要用户测试的内容 - -### 1. 基础功能测试 - -**测试步骤:** -```bash -# 1. 确保微信已登录 -# 2. 运行程序 -python bot.py - -# 3. 观察日志输出 -# 应该看到: -# - Loader.dll 加载成功 -# - 注入微信成功 -# - 插件加载成功 -# - 机器人启动成功 -``` - -### 2. 消息接收测试 - -**测试方法:** -- 给机器人发送文本消息 "ping" -- 应该收到回复 "pong" -- 查看日志中的消息 type 和 data - -**需要反馈的信息:** -``` -收到的消息格式: -{ - "type": ???, # 实际的 type 值 - "data": { - "from_wxid": "...", - "content": "...", - # 其他字段 - } -} -``` - -### 3. 消息类型测试 - -**请依次发送以下类型的消息并反馈日志:** -- 文本消息 -- 图片消息 -- 文件消息 -- 语音消息 -- 视频消息 -- @ 消息(在群里) -- 名片消息 - -**需要记录:** -- 每种消息的 type 值 -- data 中的字段名称 -- 是否正常触发插件 - -### 4. API 测试 - -**测试发送功能:** -- 修改 ExamplePlugin,尝试发送图片/文件 -- 查看是否发送成功 -- 记录错误信息(如果有) - -### 5. 错误日志 - -**如果出现错误,请提供:** -- 完整的错误堆栈 -- logs/hookbot.log 文件内容 -- 出错时的操作步骤 - -## ⚠️ 已知需要确认的问题 - -### 1. 消息类型 type 值 - -**当前使用推测值:** -```python -MT_TEXT = 10001 -MT_IMAGE = 10002 -MT_VOICE = 10003 -# ... -``` - -**需要确认:** 实际的 type 值是否正确 - -### 2. API type 值 - -**当前使用推测值:** -```python -MT_SEND_TEXT = 11036 -send_image = 11037 -send_file = 11038 -# ... -``` - -**需要确认:** 发送 API 的实际 type 值 - -### 3. 消息数据字段 - -**需要确认的字段名:** -- from_wxid / FromWxid -- to_wxid / ToWxid -- content / Content -- sender_wxid / SenderWxid -- at_list / Ats - -### 4. 登录信息获取 - -**当前问题:** -- get_login_info() 调用后需要从回调中获取返回数据 -- 暂时使用占位符 "unknown" - -**需要确认:** -- 登录信息的返回方式 -- 如何从回调中提取 wxid 和 nickname - -## 🔧 可能需要的调整 - -### 根据测试结果可能需要修改: - -1. **message_types.py** - - 调整 MessageType 常量的值 - - 修改 normalize_message() 的字段映射 - -2. **client.py** - - 调整各个 API 的 type 值 - - 修改 data 字段名称 - -3. **hookbot.py** - - 根据实际消息格式调整处理逻辑 - -4. **bot.py** - - 实现登录信息的正确获取 - -## 📦 项目文件清单 - -``` -WechatHookBot/ -├── Memory Bank/ # ✅ 项目管理文档 -├── docs/ # ✅ 技术文档 -├── WechatHook/ # ✅ Hook 层(4个文件) -├── utils/ # ✅ 工具类(6个文件) -├── database/ # ✅ 数据库(4个文件) -├── plugins/ # ✅ 插件(ExamplePlugin) -├── libs/ # ✅ DLL 文件 -├── bot.py # ✅ 主入口 -├── main_config.toml # ✅ 配置文件 -└── requirements.txt # ✅ 依赖列表 -``` - -## 🚀 下一步行动 - -1. **用户测试** - - 在远程设备运行 bot.py - - 测试各项功能 - - 记录日志和错误 - -2. **反馈收集** - - 消息 type 值 - - API 返回格式 - - 错误信息 - -3. **代码调整** - - 根据反馈修改代码 - - 修复发现的问题 - -4. **迭代测试** - - 重复测试直到稳定 - -## 💡 使用建议 - -1. **首次运行** - - 先确保微信已登录 - - 使用 32位 Python - - 关闭杀毒软件或添加信任 - -2. **查看日志** - - 控制台会显示彩色日志 - - logs/hookbot.log 包含详细日志 - -3. **测试插件** - - 发送 "ping" 测试 ExamplePlugin - - 观察是否收到 "pong" 回复 - -4. **遇到问题** - - 查看完整的错误堆栈 - - 提供 logs/hookbot.log 文件 - - 描述具体的操作步骤 diff --git a/Memory Bank/decisionLog.md b/Memory Bank/decisionLog.md deleted file mode 100644 index cc2626c..0000000 --- a/Memory Bank/decisionLog.md +++ /dev/null @@ -1,254 +0,0 @@ -# 技术决策日志 - -记录项目中的重要技术决策、原因和影响。 - ---- - -## 2025-01-12 - -### 决策 #001: 项目架构选择 - -**决策:** 采用四层架构设计(DLL Hook → WechatHook → Bot Core → Plugin) - -**原因:** -- 清晰的职责分离 -- 便于维护和扩展 -- 最大化复用 XYBotV2 代码 - -**影响:** -- 代码结构清晰 -- 80% 代码可复用 -- 学习成本低 - -**状态:** ✅ 已实施 - ---- - -### 决策 #002: DLL 文件存放位置 - -**决策:** DLL 文件放在项目的 `libs/` 目录,不放到微信安装目录 - -**原因:** -- Loader.dll 由 Python 程序直接加载 -- Helper.dll 由 Loader.dll 动态注入到微信进程 -- 不需要修改微信安装目录 - -**影响:** -- 部署更简单 -- 不污染微信目录 -- 便于版本管理 - -**状态:** ✅ 已实施 - ---- - -### 决策 #003: 不进行本地测试 - -**决策:** 所有测试在远程设备进行,开发过程中不在本地运行 - -**原因:** -- 用户要求 -- 避免本地环境污染 -- 专注于代码实现 - -**影响:** -- 需要更仔细的代码审查 -- 依赖用户反馈进行调试 -- 开发周期可能稍长 - -**状态:** ✅ 已实施 - ---- - -### 决策 #004: 复用 XYBotV2 插件系统 - -**决策:** 完全复用 XYBotV2 的插件系统(PluginBase、EventManager、装饰器) - -**原因:** -- 插件系统设计优秀 -- 已经过验证 -- 减少开发工作量 -- 保持插件兼容性 - -**影响:** -- 开发速度快 -- XYBot 插件可直接使用 -- 代码质量有保证 - -**状态:** ✅ 已确定,待实施 - ---- - -### 决策 #005: 使用 Memory Bank 系统 - -**决策:** 创建 Memory Bank 文件夹,实时跟踪项目进度和决策 - -**原因:** -- 用户要求 -- 便于项目管理 -- 保持开发上下文 -- 方便后续维护 - -**影响:** -- 项目管理更规范 -- 决策过程可追溯 -- 便于团队协作 - -**文件结构:** -- projectBrief.md - 项目简介 -- activeContext.md - 当前上下文 -- progress.md - 进度跟踪 -- decisionLog.md - 决策日志 -- systemPatterns.md - 系统模式 - -**状态:** ✅ 已实施 - ---- - -### 决策 #006: 消息类型映射策略 - -**决策:** 创建独立的 message_types.py 文件,定义 API type 到内部 event 的映射 - -**原因:** -- 解耦消息类型定义 -- 便于维护和扩展 -- 统一消息格式转换 - -**实现方式:** -```python -MESSAGE_TYPE_MAP = { - 10001: "text_message", - 10002: "image_message", - # ... -} -``` - -**影响:** -- 代码更清晰 -- 易于添加新消息类型 -- 便于调试 - -**状态:** ✅ 已确定,待实施 - ---- - -### 决策 #007: 回调处理机制 - -**决策:** 使用装饰器模式实现回调处理(@CONNECT_CALLBACK, @RECV_CALLBACK, @CLOSE_CALLBACK) - -**原因:** -- 参考 python_demo.py 的实现 -- 代码简洁优雅 -- 易于扩展 - -**影响:** -- 回调注册简单 -- 支持多个回调处理器 -- 代码可读性好 - -**状态:** ✅ 已确定,待实施 - ---- - -### 决策 #008: 异步编程模型 - -**决策:** 使用 asyncio 作为异步编程框架,所有 API 都是异步函数 - -**原因:** -- 与 XYBotV2 保持一致 -- 提高并发性能 -- 插件系统要求 - -**影响:** -- 所有函数必须使用 async/await -- 需要处理同步/异步转换 -- 性能更好 - -**状态:** ✅ 已确定,待实施 - ---- - -### 决策 #009: 配置文件格式 - -**决策:** 使用 TOML 格式作为配置文件格式 - -**原因:** -- 与 XYBotV2 保持一致 -- 可读性好 -- Python 3.11+ 原生支持 - -**影响:** -- 配置文件易于编辑 -- 支持注释 -- 类型安全 - -**状态:** ✅ 已实施 - ---- - -### 决策 #010: 数据库选择 - -**决策:** 使用 SQLite + aiosqlite 作为数据库 - -**原因:** -- 与 XYBotV2 保持一致 -- 无需额外服务 -- 支持异步操作 - -**影响:** -- 部署简单 -- 性能足够 -- 便于备份 - -**状态:** ✅ 已确定,待实施 - ---- - -## 待决策事项 - -### 待决策 #001: 消息类型 type 值 - -**问题:** 个微 API 各类消息的具体 type 值未知 - -**选项:** -1. 参考 API 文档推测 -2. 实际测试获取 - -**倾向:** 选项 2 - 实际测试获取 - -**需要:** 用户提供实际消息的 type 值 - ---- - -### 待决策 #002: WebUI 是否实现 - -**问题:** 是否需要实现 Web 管理界面 - -**选项:** -1. 立即实现 -2. 基础功能完成后再实现 -3. 不实现 - -**倾向:** 选项 2 - 基础功能完成后再实现 - -**原因:** 先保证核心功能稳定 - ---- - -## 决策模板 - -```markdown -### 决策 #XXX: 决策标题 - -**决策:** 简要描述决策内容 - -**原因:** -- 原因1 -- 原因2 - -**影响:** -- 影响1 -- 影响2 - -**状态:** ✅ 已实施 / 🚧 进行中 / ⏳ 待实施 / ❌ 已废弃 -``` diff --git a/Memory Bank/progress.md b/Memory Bank/progress.md deleted file mode 100644 index 5f5f331..0000000 --- a/Memory Bank/progress.md +++ /dev/null @@ -1,315 +0,0 @@ -# 开发进度跟踪 - -**项目开始:** 2025-01-12 -**最后更新:** 2025-01-12 14:35 -**当前状态:** ✅ 开发完成,等待测试 - -## 总体进度 - -**当前阶段:** 开发完成 -**完成度:** 90% - -``` -[██████████████████░░] 90% -``` - -## 阶段进度 - -### ✅ 第零阶段:项目初始化 (100%) - -- [x] 创建项目目录结构 -- [x] 编写完整文档系统 (6个文档) -- [x] 创建配置文件模板 -- [x] 复制 DLL 文件到 libs/ -- [x] 创建 Memory Bank 系统 - -**完成时间:** 2025-01-12 13:50 - ---- - -### ✅ 第一阶段:WechatHook 层 (100%) - -**目标:** 实现 DLL 调用和 API 封装 - -#### 1.1 NoveLoader 实现 (100%) -- [x] 创建 WechatHook/__init__.py -- [x] 实现 loader.py 基础结构 -- [x] 实现 DLL 函数偏移调用 -- [x] 实现所有 DLL 函数封装 -- [x] 添加详细日志 - -#### 1.2 WechatHookClient 实现 (100%) -- [x] 创建 client.py 基础结构 -- [x] 实现消息发送 API (8个方法) -- [x] 实现好友管理 API (6个方法) -- [x] 实现群聊管理 API (9个方法) -- [x] 实现登录信息 API - -#### 1.3 消息类型映射 (100%) -- [x] 创建 message_types.py -- [x] 定义消息类型常量 (MessageType 类) -- [x] 创建 type 到 event 的映射表 -- [x] 实现消息格式转换函数 (normalize_message) - -#### 1.4 回调处理器 (100%) -- [x] 创建 callbacks.py -- [x] 实现连接回调处理 (CONNECT_CALLBACK) -- [x] 实现接收回调处理 (RECV_CALLBACK) -- [x] 实现断开回调处理 (CLOSE_CALLBACK) -- [x] 实现回调注册机制 - -**完成时间:** 2025-01-12 14:10 - -**代码统计:** -- loader.py: ~280 行 -- client.py: ~450 行 -- message_types.py: ~180 行 -- callbacks.py: ~180 行 -- 总计:~1090 行 - ---- - -### ✅ 第二阶段:Bot 核心层 (100%) - -**目标:** 实现消息路由和事件分发 - -#### 2.1 复用 XYBot 代码 (100%) -- [x] 复制 utils/ 目录 (5个文件) -- [x] 复制 database/ 目录 (3个文件) -- [x] 创建模块 __init__.py - -#### 2.2 HookBot 实现 (100%) -- [x] 创建 utils/hookbot.py -- [x] 实现消息预处理 -- [x] 实现消息类型映射 -- [x] 实现白名单/黑名单过滤 -- [x] 实现事件分发 - -#### 2.3 主入口实现 (100%) -- [x] 创建 bot.py -- [x] 实现初始化流程 -- [x] 实现插件加载 -- [x] 实现回调注册 -- [x] 实现主循环 - -**完成时间:** 2025-01-12 14:30 - -**代码统计:** -- hookbot.py: ~120 行 -- bot.py: ~200 行 -- 总计:~320 行 - ---- - -### ✅ 第三阶段:插件系统 (100%) - -**目标:** 集成插件系统并创建示例 - -- [x] 创建 ExamplePlugin 示例 -- [x] 实现文本消息处理 -- [x] 实现定时任务 -- [x] 测试装饰器系统 -- [x] 插件系统完全集成 - -**完成时间:** 2025-01-12 14:35 - -**代码统计:** -- ExamplePlugin/main.py: ~50 行 - ---- - -### ⏳ 第四阶段:功能测试 (0%) - -**目标:** 远程设备测试所有功能 - -- [ ] 测试 DLL 注入 -- [ ] 测试消息接收 -- [ ] 测试消息发送 -- [ ] 测试插件加载 -- [ ] 测试定时任务 -- [ ] 修复发现的问题 - -**预计完成:** 2025-01-13 - ---- - -## 每日进度 - -### 2025-01-12 - -**完成:** -- ✅ 创建完整文档系统 (6个文档) -- ✅ 创建配置文件和依赖列表 -- ✅ 复制 DLL 文件到项目目录 -- ✅ 创建 Memory Bank 管理系统 (5个文档) -- ✅ 实现 WechatHook 层完整代码 (4个模块,~1090行) -- ✅ 复用 XYBot 代码 (utils + database) -- ✅ 实现 Bot 核心层 (hookbot + bot.py,~320行) -- ✅ 创建 ExamplePlugin 示例插件 (~50行) - -**总耗时:** 约 2 小时 - -**代码质量:** -- 详细的日志记录 -- 完整的错误处理 -- 清晰的代码结构 -- 丰富的注释文档 - ---- - -## 里程碑 - -- [x] **M0:** 项目初始化完成 (2025-01-12 13:50) -- [x] **M1:** WechatHook 层完成 (2025-01-12 14:10) -- [x] **M2:** Bot 核心层完成 (2025-01-12 14:30) -- [x] **M3:** 插件系统集成完成 (2025-01-12 14:35) -- [ ] **M4:** 基础功能测试通过 -- [ ] **M5:** 项目可用版本发布 - ---- - -## 最终统计数据 - -**总任务数:** 45 -**已完成:** 40 -**进行中:** 0 -**待开始:** 5 - -**代码行数:** -- WechatHook 层: ~1090 行 -- Bot 核心层: ~320 行 -- 示例插件: ~50 行 -- 复用代码: ~600 行 -- **总计:~2060 行** - -**文档页数:** 11 -- 技术文档: 6 个 -- Memory Bank: 5 个 - -**测试用例:** 0 (待用户测试) - ---- - -## 项目文件结构 - -``` -WechatHookBot/ -├── Memory Bank/ # 项目管理文档 -│ ├── projectBrief.md -│ ├── activeContext.md -│ ├── progress.md -│ ├── decisionLog.md -│ └── systemPatterns.md -├── docs/ # 技术文档 -│ ├── 架构设计.md -│ ├── 插件开发.md -│ ├── API文档.md -│ ├── 快速开始.md -│ └── 项目概览.md -├── WechatHook/ # Hook 层 -│ ├── __init__.py -│ ├── loader.py -│ ├── client.py -│ ├── message_types.py -│ └── callbacks.py -├── utils/ # 工具类 -│ ├── __init__.py -│ ├── plugin_base.py -│ ├── plugin_manager.py -│ ├── event_manager.py -│ ├── decorators.py -│ ├── singleton.py -│ └── hookbot.py -├── database/ # 数据库 -│ ├── __init__.py -│ ├── XYBotDB.py -│ ├── keyvalDB.py -│ └── messsagDB.py -├── plugins/ # 插件 -│ └── ExamplePlugin/ -│ ├── __init__.py -│ └── main.py -├── libs/ # DLL 文件 -│ ├── Loader.dll -│ └── Helper.dll -├── bot.py # 主入口 -├── main_config.toml # 配置文件 -├── requirements.txt # 依赖列表 -├── .gitignore # Git 忽略 -└── README.md # 项目说明 -``` - ---- - -## 待测试确认事项 - -### 高优先级 - -1. **消息类型 type 值** - 需要实际测试获取 -2. **API type 值** - 需要实际测试获取 -3. **消息数据字段名** - 需要实际测试确认 -4. **登录信息获取** - 需要实现正确的获取方式 - -### 中优先级 - -5. **API 返回格式** - 需要用户反馈 -6. **错误处理** - 需要测试各种异常情况 -7. **性能优化** - 需要测试消息处理速度 - -### 低优先级 - -8. **WebUI 实现** - 可选功能 -9. **更多插件** - 根据需求添加 -10. **文档完善** - 根据测试结果补充 - ---- - -## 项目亮点 - -1. **完整的架构设计** - 四层架构,职责清晰 -2. **高度代码复用** - 80% 复用 XYBot 代码 -3. **详细的文档** - 11 个文档文件 -4. **Memory Bank 系统** - 完整的项目管理 -5. **插件兼容性** - 完全兼容 XYBot 插件 -6. **异步编程** - 全异步设计,性能优秀 -7. **详细日志** - 便于调试和问题定位 -8. **错误处理** - 完善的异常处理机制 - ---- - -## 下一步行动 - -### 用户需要做的: - -1. **安装依赖** - ```bash - pip install -r requirements.txt - ``` - -2. **运行程序** - ```bash - python bot.py - ``` - -3. **测试功能** - - 发送 "ping" 测试 - - 发送各类消息 - - 查看日志输出 - -4. **反馈信息** - - 消息 type 值 - - 数据字段名 - - 错误日志 - - 功能是否正常 - -### 开发者需要做的: - -1. **等待测试反馈** -2. **根据反馈调整代码** -3. **修复发现的问题** -4. **迭代优化** - ---- - -**项目状态:** ✅ 开发完成,等待测试 -**预计可用时间:** 测试通过后即可使用 diff --git a/Memory Bank/projectBrief.md b/Memory Bank/projectBrief.md deleted file mode 100644 index 4975861..0000000 --- a/Memory Bank/projectBrief.md +++ /dev/null @@ -1,77 +0,0 @@ -# WechatHookBot 项目简介 - -## 项目概述 - -**项目名称:** WechatHookBot -**创建时间:** 2025-01-12 -**当前版本:** v0.1.0-dev -**项目状态:** 🚧 开发中 - -## 项目目标 - -基于个微大客户版 Hook API 构建一个类似 XYBotV2 的微信机器人框架,实现插件化、事件驱动的架构。 - -## 核心特性 - -- ✅ **无需登录系统**:Hook 已登录的微信客户端 -- ✅ **插件化架构**:完全兼容 XYBotV2 插件系统 -- ✅ **实时消息回调**:Socket 回调机制 -- ✅ **轻量级设计**:无需 Redis 依赖 - -## 技术栈 - -- **语言:** Python 3.x (32位) -- **异步框架:** asyncio -- **DLL 调用:** ctypes -- **数据库:** SQLite + aiosqlite -- **定时任务:** APScheduler -- **Web 框架:** Flask + SocketIO (可选) - -## 架构设计 - -``` -DLL Hook 层 → WechatHook 层 → Bot 核心层 → 插件层 → WebUI 层 -``` - -## 参考项目 - -- **XYBotV2** - 插件系统、事件管理、数据库架构 -- **个微大客户版** - DLL Hook API 和调用示例 - -## 开发原则 - -1. **代码复用优先**:最大化复用 XYBotV2 代码 -2. **最小化实现**:只写必要的代码 -3. **不本地测试**:所有测试在远程设备进行 -4. **文档先行**:保持 Memory Bank 实时更新 - -## 项目结构 - -``` -WechatHookBot/ -├── Memory Bank/ # 项目管理和进度跟踪 -├── docs/ # 技术文档 -├── WechatHook/ # Hook 层实现 -├── utils/ # 工具类(复用 XYBot) -├── database/ # 数据库(复用 XYBot) -├── plugins/ # 插件目录 -├── WebUI/ # Web 界面(可选) -├── libs/ # DLL 文件 -├── bot.py # 主入口 -└── main_config.toml # 配置文件 -``` - -## 关键里程碑 - -- [x] 文档系统完成 -- [x] DLL 文件准备 -- [ ] WechatHook 层实现 -- [ ] Bot 核心层实现 -- [ ] 插件系统集成 -- [ ] 基础功能测试 -- [ ] WebUI 实现(可选) - -## 联系方式 - -**开发者:** Claude -**项目路径:** D:\project\shrobot\WechatHookBot\ diff --git a/Memory Bank/systemPatterns.md b/Memory Bank/systemPatterns.md deleted file mode 100644 index 4d6b51b..0000000 --- a/Memory Bank/systemPatterns.md +++ /dev/null @@ -1,252 +0,0 @@ -# 系统模式和最佳实践 - -## 代码模式 - -### 1. DLL 函数调用模式 - -```python -# 通过内存偏移调用未导出函数 -def __get_non_exported_func(self, offset: int, arg_types, return_type): - func_addr = self.loader_module_base + offset - func_type = ctypes.WINFUNCTYPE(return_type, *arg_types) - return func_type(func_addr) -``` - -### 2. 回调装饰器模式 - -```python -@CONNECT_CALLBACK(in_class=True) -def on_connect(self, client_id): - # 处理连接 - pass - -@RECV_CALLBACK(in_class=True) -def on_receive(self, client_id, message_type, data): - # 处理消息 - pass -``` - -### 3. 异步 API 封装模式 - -```python -async def send_text(self, to_wxid: str, content: str) -> bool: - payload = {"type": 11036, "data": {"to_wxid": to_wxid, "content": content}} - return await asyncio.to_thread( - self.loader.SendWeChatData, - self.client_id, - json.dumps(payload, ensure_ascii=False) - ) -``` - -### 4. 消息类型映射模式 - -```python -MESSAGE_TYPE_MAP = { - 10001: "text_message", - 10002: "image_message", -} - -event_type = MESSAGE_TYPE_MAP.get(msg_type) -if event_type: - await EventManager.emit(event_type, client, message) -``` - -### 5. 插件事件处理模式 - -```python -@on_text_message(priority=50) -async def handle_text(self, client, message): - # 处理逻辑 - return True # 继续执行后续处理器 -``` - -## 架构模式 - -### 分层架构 - -``` -应用层 (Plugins) - ↓ -业务层 (HookBot) - ↓ -服务层 (WechatHookClient) - ↓ -基础层 (NoveLoader) - ↓ -系统层 (DLL) -``` - -### 事件驱动模式 - -``` -消息接收 → 回调触发 → 类型映射 → 事件分发 → 插件处理 -``` - -## 命名规范 - -### 文件命名 -- 模块文件:小写下划线 `loader.py`, `message_types.py` -- 类文件:大驼峰 `WechatHookClient`, `HookBot` - -### 变量命名 -- 常量:大写下划线 `MESSAGE_TYPE_MAP` -- 变量:小写下划线 `client_id`, `msg_type` -- 类名:大驼峰 `NoveLoader` -- 函数:小写下划线 `send_text()` - -### API 命名 -- 发送类:`send_xxx()` - send_text, send_image -- 获取类:`get_xxx()` - get_friend_list, get_chatroom_info -- 设置类:`set_xxx()` - set_friend_remark - -## 错误处理模式 - -### API 调用错误处理 - -```python -try: - result = await client.send_text(wxid, content) - if result: - logger.info("发送成功") - else: - logger.error("发送失败") -except Exception as e: - logger.error(f"发送异常: {e}") -``` - -### DLL 调用错误处理 - -```python -if not os.path.exists(dll_path): - logger.error(f"DLL 文件不存在: {dll_path}") - return False -``` - -## 日志模式 - -### 日志级别使用 - -```python -logger.debug("调试信息") # 详细的调试信息 -logger.info("普通信息") # 一般信息 -logger.success("成功信息") # 操作成功 -logger.warning("警告信息") # 警告但不影响运行 -logger.error("错误信息") # 错误需要关注 -``` - -### 日志格式 - -```python -logger.info(f"收到消息: FromWxid={from_wxid}, Content={content}") -``` - -## 配置模式 - -### TOML 配置读取 - -```python -import tomllib - -with open("main_config.toml", "rb") as f: - config = tomllib.load(f) - -value = config.get("section", {}).get("key", default_value) -``` - -## 数据库模式 - -### 异步数据库操作 - -```python -from database.keyvalDB import KeyvalDB - -keyval_db = KeyvalDB() -await keyval_db.set("key", "value") -value = await keyval_db.get("key") -``` - -## 测试模式 - -### 远程测试流程 - -1. 实现功能代码 -2. 添加详细日志 -3. 提交给用户测试 -4. 根据反馈修复 -5. 重复直到通过 - -### 需要用户提供的测试信息 - -- API 返回的实际数据格式 -- 消息的实际 type 值 -- 错误信息和日志 -- 功能是否正常工作 - -## 代码复用模式 - -### 从 XYBot 复用 - -```python -# 完全复用(不修改) -- utils/plugin_base.py -- utils/plugin_manager.py -- utils/event_manager.py -- utils/decorators.py -- utils/singleton.py -- database/ - -# 参考实现(需修改) -- utils/xybot.py → utils/hookbot.py -``` - -## 性能优化模式 - -### 异步并发 - -```python -# 并发执行多个任务 -tasks = [ - client.send_text(wxid1, msg1), - client.send_text(wxid2, msg2), -] -results = await asyncio.gather(*tasks) -``` - -### 消息队列 - -```python -# 避免消息发送过快 -await asyncio.sleep(0.5) # 每条消息间隔 -``` - -## 安全模式 - -### 路径处理 - -```python -# 使用绝对路径 -path = os.path.realpath(relative_path) -``` - -### 参数验证 - -```python -if not wxid or not content: - logger.error("参数不能为空") - return False -``` - -## 扩展模式 - -### 添加新消息类型 - -1. 在 `message_types.py` 添加映射 -2. 在 `decorators.py` 添加装饰器(如需要) -3. 在 `client.py` 添加发送方法(如需要) - -### 添加新 API - -1. 在 `client.py` 添加方法 -2. 构造正确的 payload -3. 调用 `SendWeChatData` -4. 处理返回结果 diff --git a/plugins/AIChat.zip b/plugins/AIChat.zip new file mode 100644 index 0000000000000000000000000000000000000000..be810733f2839ff37f277d2f3486c906fce8784d GIT binary patch literal 84937 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
3?Tp^+p~?W4(yMWvGW@4M3Y=zioj5Tp~Q|!qBP= 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 literal 0 HcmV?d00001 diff --git a/plugins/AIChat/main.py b/plugins/AIChat/main.py index 9c47f75..313daea 100644 --- a/plugins/AIChat/main.py +++ b/plugins/AIChat/main.py @@ -620,6 +620,615 @@ class AIChat(PluginBase): return tools + # ==================== Gemini API 格式转换方法 ==================== + + def _convert_tools_to_gemini(self, openai_tools: list) -> list: + """ + 将 OpenAI 格式的工具定义转换为 Gemini 格式 + + OpenAI: [{"type": "function", "function": {"name": ..., "parameters": ...}}] + Gemini: [{"function_declarations": [{"name": ..., "parameters": ...}]}] + """ + if not openai_tools: + return [] + + function_declarations = [] + for tool in openai_tools: + if tool.get("type") == "function": + func = tool.get("function", {}) + function_declarations.append({ + "name": func.get("name", ""), + "description": func.get("description", ""), + "parameters": func.get("parameters", {}) + }) + + if function_declarations: + return [{"function_declarations": function_declarations}] + return [] + + def _build_gemini_contents(self, system_content: str, history_messages: list, + current_message: dict, is_group: bool = False) -> list: + """ + 构建 Gemini API 的 contents 格式 + + Args: + system_content: 系统提示词(包含人设、时间、持久记忆等) + history_messages: 历史消息列表 + current_message: 当前用户消息 {"text": str, "media": optional} + is_group: 是否群聊 + + Returns: + Gemini contents 格式的列表 + """ + contents = [] + + # Gemini 没有 system role,将系统提示放在第一条 user 消息中 + # 然后用一条简短的 model 回复来"确认" + system_parts = [{"text": f"[系统指令]\n{system_content}\n\n请按照以上指令进行对话。"}] + contents.append({"role": "user", "parts": system_parts}) + contents.append({"role": "model", "parts": [{"text": "好的,我会按照指令进行对话。"}]}) + + # 添加历史消息 + for msg in history_messages: + gemini_msg = self._convert_message_to_gemini(msg, is_group) + if gemini_msg: + contents.append(gemini_msg) + + # 添加当前用户消息 + current_parts = [] + if current_message.get("text"): + current_parts.append({"text": current_message["text"]}) + + # 添加媒体内容(图片/视频) + if current_message.get("image_base64"): + image_data = current_message["image_base64"] + # 去除 data:image/xxx;base64, 前缀 + if image_data.startswith("data:"): + mime_type = image_data.split(";")[0].split(":")[1] + image_data = image_data.split(",", 1)[1] + else: + mime_type = "image/jpeg" + current_parts.append({ + "inline_data": { + "mime_type": mime_type, + "data": image_data + } + }) + + if current_message.get("video_base64"): + video_data = current_message["video_base64"] + # 去除 data:video/xxx;base64, 前缀 + if video_data.startswith("data:"): + video_data = video_data.split(",", 1)[1] + current_parts.append({ + "inline_data": { + "mime_type": "video/mp4", + "data": video_data + } + }) + + if current_parts: + contents.append({"role": "user", "parts": current_parts}) + + return contents + + def _convert_message_to_gemini(self, msg: dict, is_group: bool = False) -> dict: + """ + 将单条历史消息转换为 Gemini 格式 + + 支持的输入格式: + 1. 群聊历史: {"nickname": str, "content": str|list} + 2. 私聊记忆: {"role": "user"|"assistant", "content": str|list} + """ + parts = [] + + # 群聊历史格式 + if "nickname" in msg: + nickname = msg.get("nickname", "") + content = msg.get("content", "") + + if isinstance(content, list): + # 多模态内容 + for item in content: + if item.get("type") == "text": + text = item.get("text", "") + parts.append({"text": f"[{nickname}] {text}" if nickname else text}) + elif item.get("type") == "image_url": + image_url = item.get("image_url", {}).get("url", "") + if image_url.startswith("data:"): + mime_type = image_url.split(";")[0].split(":")[1] + image_data = image_url.split(",", 1)[1] + parts.append({ + "inline_data": { + "mime_type": mime_type, + "data": image_data + } + }) + else: + # 纯文本 + parts.append({"text": f"[{nickname}] {content}" if nickname else content}) + + # 群聊历史都作为 user 消息(因为是多人对话记录) + return {"role": "user", "parts": parts} if parts else None + + # 私聊记忆格式 + elif "role" in msg: + role = msg.get("role", "user") + content = msg.get("content", "") + + # 转换角色名 + gemini_role = "model" if role == "assistant" else "user" + + if isinstance(content, list): + for item in content: + if item.get("type") == "text": + parts.append({"text": item.get("text", "")}) + elif item.get("type") == "image_url": + image_url = item.get("image_url", {}).get("url", "") + if image_url.startswith("data:"): + mime_type = image_url.split(";")[0].split(":")[1] + image_data = image_url.split(",", 1)[1] + parts.append({ + "inline_data": { + "mime_type": mime_type, + "data": image_data + } + }) + else: + parts.append({"text": content}) + + return {"role": gemini_role, "parts": parts} if parts else None + + return None + + def _parse_gemini_tool_calls(self, response_parts: list) -> list: + """ + 从 Gemini 响应中解析工具调用 + + Gemini 格式: {"functionCall": {"name": "...", "args": {...}}} + 转换为内部格式: {"id": "...", "function": {"name": "...", "arguments": "..."}} + """ + tool_calls = [] + for i, part in enumerate(response_parts): + if "functionCall" in part: + func_call = part["functionCall"] + tool_calls.append({ + "id": f"call_{uuid.uuid4().hex[:8]}", + "type": "function", + "function": { + "name": func_call.get("name", ""), + "arguments": json.dumps(func_call.get("args", {}), ensure_ascii=False) + } + }) + return tool_calls + + def _build_tool_response_contents(self, contents: list, tool_calls: list, + tool_results: list) -> list: + """ + 构建包含工具调用结果的 contents,用于继续对话 + + Args: + contents: 原始 contents + tool_calls: 工具调用列表 + tool_results: 工具执行结果列表 + """ + new_contents = contents.copy() + + # 添加 model 的工具调用响应 + function_call_parts = [] + for tc in tool_calls: + function_call_parts.append({ + "functionCall": { + "name": tc["function"]["name"], + "args": json.loads(tc["function"]["arguments"]) + } + }) + if function_call_parts: + new_contents.append({"role": "model", "parts": function_call_parts}) + + # 添加工具执行结果 + function_response_parts = [] + for i, result in enumerate(tool_results): + tool_name = tool_calls[i]["function"]["name"] if i < len(tool_calls) else "unknown" + function_response_parts.append({ + "functionResponse": { + "name": tool_name, + "response": {"result": result.get("message", str(result))} + } + }) + if function_response_parts: + new_contents.append({"role": "user", "parts": function_response_parts}) + + return new_contents + + # ==================== 统一的 Gemini API 调用 ==================== + + async def _call_gemini_api(self, contents: list, tools: list = None, + bot=None, from_wxid: str = None, + chat_id: str = None, nickname: str = "", + user_wxid: str = None, is_group: bool = False) -> tuple: + """ + 统一的 Gemini API 调用方法 + + Args: + contents: Gemini 格式的对话内容 + tools: Gemini 格式的工具定义(可选) + bot: WechatHookClient 实例 + from_wxid: 消息来源 + chat_id: 会话ID + nickname: 用户昵称 + user_wxid: 用户wxid + is_group: 是否群聊 + + Returns: + (response_text, tool_calls) - 响应文本和工具调用列表 + """ + import json + + api_config = self.config["api"] + model = api_config["model"] + api_url = api_config.get("gemini_url", api_config.get("url", "").replace("/v1/chat/completions", "/v1beta/models")) + api_key = api_config["api_key"] + + # 构建完整 URL + full_url = f"{api_url}/{model}:streamGenerateContent?alt=sse" + + # 构建请求体 + payload = { + "contents": contents, + "generationConfig": { + "maxOutputTokens": api_config.get("max_tokens", 8192) + } + } + + if tools: + payload["tools"] = tools + + headers = { + "Content-Type": "application/json", + "Authorization": f"Bearer {api_key}" + } + + timeout = aiohttp.ClientTimeout(total=api_config.get("timeout", 120)) + + # 配置代理 + connector = None + proxy_config = self.config.get("proxy", {}) + if proxy_config.get("enabled", False) and PROXY_SUPPORT: + proxy_type = proxy_config.get("type", "socks5").upper() + proxy_host = proxy_config.get("host", "127.0.0.1") + proxy_port = proxy_config.get("port", 7890) + proxy_username = proxy_config.get("username") + proxy_password = proxy_config.get("password") + + if proxy_username and proxy_password: + proxy_url = f"{proxy_type}://{proxy_username}:{proxy_password}@{proxy_host}:{proxy_port}" + else: + proxy_url = f"{proxy_type}://{proxy_host}:{proxy_port}" + + try: + connector = ProxyConnector.from_url(proxy_url) + logger.debug(f"[Gemini] 使用代理: {proxy_type}://{proxy_host}:{proxy_port}") + except Exception as e: + logger.warning(f"[Gemini] 代理配置失败: {e}") + + # 保存用户信息供工具调用使用 + self._current_user_wxid = user_wxid + self._current_is_group = is_group + + try: + async with aiohttp.ClientSession(timeout=timeout, connector=connector) as session: + logger.debug(f"[Gemini] 发送流式请求: {full_url}") + async with session.post(full_url, json=payload, headers=headers) as resp: + if resp.status != 200: + error_text = await resp.text() + logger.error(f"[Gemini] API 错误: {resp.status}, {error_text[:500]}") + raise Exception(f"Gemini API 错误 {resp.status}: {error_text[:200]}") + + # 流式接收响应 + full_text = "" + all_parts = [] + tool_call_hint_sent = False + + async for line in resp.content: + line = line.decode('utf-8').strip() + if not line or not line.startswith("data: "): + continue + + try: + data = json.loads(line[6:]) + candidates = data.get("candidates", []) + if not candidates: + continue + + content = candidates[0].get("content", ) + parts = content.get("parts", []) + + for part in parts: + all_parts.append(part) + + # 收集文本 + if "text" in part: + full_text += part["text"] + + # 检测到工具调用时,先发送已有文本 + if "functionCall" in part: + if not tool_call_hint_sent and bot and from_wxid: + tool_call_hint_sent = True + if full_text.strip(): + logger.info(f"[Gemini] 检测到工具调用,先发送文本: {full_text[:30]}...") + await bot.send_text(from_wxid, full_text.strip()) + + except json.JSONDecodeError: + continue + + # 解析工具调用 + tool_calls = self._parse_gemini_tool_calls(all_parts) + + logger.info(f"[Gemini] 响应完成, 文本长度: {len(full_text)}, 工具调用: {len(tool_calls)}") + + return full_text.strip(), tool_calls + + except aiohttp.ClientError as e: + logger.error(f"[Gemini] 网络请求失败: {e}") + raise + except asyncio.TimeoutError: + logger.error(f"[Gemini] 请求超时") + raise + + async def _handle_gemini_response(self, response_text: str, tool_calls: list, + contents: list, tools: list, + bot, from_wxid: str, chat_id: str, + nickname: str, user_wxid: str, is_group: bool): + """ + 处理 Gemini API 响应,包括工具调用 + + Args: + response_text: AI 响应文本 + tool_calls: 工具调用列表 + contents: 原始 contents(用于工具调用后继续对话) + tools: 工具定义 + bot, from_wxid, chat_id, nickname, user_wxid, is_group: 上下文信息 + """ + if tool_calls: + # 有工具调用,异步执行 + logger.info(f"[Gemini] 启动异步工具执行,共 {len(tool_calls)} 个工具") + asyncio.create_task( + self._execute_gemini_tools_async( + tool_calls, contents, tools, + bot, from_wxid, chat_id, nickname, user_wxid, is_group + ) + ) + return None # 工具调用异步处理 + + return response_text + + async def _execute_gemini_tools_async(self, tool_calls: list, contents: list, tools: list, + bot, from_wxid: str, chat_id: str, + nickname: str, user_wxid: str, is_group: bool): + """ + 异步执行 Gemini 工具调用 + """ + import json + + try: + logger.info(f"[Gemini] 开始执行 {len(tool_calls)} 个工具") + + # 收集需要 AI 回复的工具结果 + need_ai_reply_results = [] + tool_results = [] + + for tool_call in tool_calls: + function_name = tool_call["function"]["name"] + try: + arguments = json.loads(tool_call["function"]["arguments"]) + except: + arguments = {} + + logger.info(f"[Gemini] 执行工具: {function_name}, 参数: {arguments}") + + result = await self._execute_tool_and_get_result(function_name, arguments, bot, from_wxid) + tool_results.append(result) + + if result and result.get("success"): + logger.success(f"[Gemini] 工具 {function_name} 执行成功") + + # 检查是否需要 AI 继续回复 + if result.get("need_ai_reply"): + need_ai_reply_results.append({ + "tool_call": tool_call, + "result": result + }) + elif not result.get("already_sent") and result.get("message"): + if result.get("send_result_text"): + await bot.send_text(from_wxid, result["message"]) + else: + logger.warning(f"[Gemini] 工具 {function_name} 执行失败: {result}") + if result and result.get("message"): + await bot.send_text(from_wxid, f"❌ {result['message']}") + + # 如果有需要 AI 回复的工具结果,继续对话 + if need_ai_reply_results: + await self._continue_gemini_with_tool_results( + contents, tools, tool_calls, tool_results, + bot, from_wxid, chat_id, nickname, user_wxid, is_group + ) + + logger.info("[Gemini] 所有工具执行完成") + + except Exception as e: + logger.error(f"[Gemini] 工具执行异常: {e}") + import traceback + logger.error(traceback.format_exc()) + try: + await bot.send_text(from_wxid, "❌ 工具执行出错") + except: + pass + + async def _continue_gemini_with_tool_results(self, contents: list, tools: list, + tool_calls: list, tool_results: list, + bot, from_wxid: str, chat_id: str, + nickname: str, user_wxid: str, is_group: bool): + """ + 基于工具结果继续 Gemini 对话 + """ + try: + # 构建包含工具结果的新 contents + new_contents = self._build_tool_response_contents(contents, tool_calls, tool_results) + + # 继续调用 API(不带工具,避免循环调用) + response_text, new_tool_calls = await self._call_gemini_api( + new_contents, tools=None, + bot=bot, from_wxid=from_wxid, chat_id=chat_id, + nickname=nickname, user_wxid=user_wxid, is_group=is_group + ) + + if response_text: + await bot.send_text(from_wxid, response_text) + logger.success(f"[Gemini] 工具回传后 AI 回复: {response_text[:50]}...") + + # 保存到记忆 + if chat_id: + self._add_to_memory(chat_id, "assistant", response_text) + if is_group: + import tomllib + with open("main_config.toml", "rb") as f: + main_config = tomllib.load(f) + bot_nickname = main_config.get("Bot", {}).get("nickname", "机器人") + await self._add_to_history(from_wxid, bot_nickname, response_text) + + except Exception as e: + logger.error(f"[Gemini] 工具回传后继续对话失败: {e}") + import traceback + logger.error(traceback.format_exc()) + + async def _process_with_gemini(self, text: str = "", image_base64: str = None, + video_base64: str = None, bot=None, + from_wxid: str = None, chat_id: str = None, + nickname: str = "", user_wxid: str = None, + is_group: bool = False) -> str: + """ + 统一的 Gemini 消息处理入口 + + 支持:纯文本、图片+文本、视频+文本 + + Args: + text: 用户消息文本 + image_base64: 图片 base64(可选) + video_base64: 视频 base64(可选) + bot, from_wxid, chat_id, nickname, user_wxid, is_group: 上下文信息 + + Returns: + AI 响应文本,如果是工具调用则返回 None + """ + import json + + # 1. 构建系统提示词 + system_content = self._build_system_content(nickname, from_wxid, user_wxid, is_group) + + # 2. 加载历史消息 + history_messages = [] + if is_group and from_wxid: + history = await self._load_history(from_wxid) + max_context = self.config.get("history", {}).get("max_context", 50) + history_messages = history[-max_context:] if len(history) > max_context else history + elif chat_id: + memory_messages = self._get_memory_messages(chat_id) + if memory_messages and len(memory_messages) > 1: + history_messages = memory_messages[:-1] # 排除刚添加的当前消息 + + # 3. 构建当前消息 + current_message = {"text": f"[{nickname}] {text}" if is_group and nickname else text} + if image_base64: + current_message["image_base64"] = image_base64 + if video_base64: + current_message["video_base64"] = video_base64 + + # 4. 构建 Gemini contents + contents = self._build_gemini_contents(system_content, history_messages, current_message, is_group) + + # 5. 收集并转换工具 + openai_tools = self._collect_tools() + gemini_tools = self._convert_tools_to_gemini(openai_tools) + + if gemini_tools: + logger.info(f"[Gemini] 已加载 {len(openai_tools)} 个工具") + + # 6. 调用 Gemini API(带重试) + max_retries = self.config.get("api", {}).get("max_retries", 2) + last_error = None + + for attempt in range(max_retries + 1): + try: + response_text, tool_calls = await self._call_gemini_api( + contents=contents, + tools=gemini_tools if gemini_tools else None, + bot=bot, + from_wxid=from_wxid, + chat_id=chat_id, + nickname=nickname, + user_wxid=user_wxid, + is_group=is_group + ) + + # 处理工具调用 + if tool_calls: + result = await self._handle_gemini_response( + response_text, tool_calls, contents, gemini_tools, + bot, from_wxid, chat_id, nickname, user_wxid, is_group + ) + return result # None 表示工具调用已异步处理 + + # 检查空响应 + if not response_text and attempt < max_retries: + logger.warning(f"[Gemini] 返回空内容,重试 {attempt + 1}/{max_retries}") + await asyncio.sleep(1) + continue + + return response_text + + except Exception as e: + last_error = e + if attempt < max_retries: + logger.warning(f"[Gemini] API 调用失败,重试 {attempt + 1}/{max_retries}: {e}") + await asyncio.sleep(1) + else: + raise + + return "" + + def _build_system_content(self, nickname: str, from_wxid: str, + user_wxid: str, is_group: bool) -> str: + """构建系统提示词(包含人设、时间、持久记忆等)""" + system_content = self.system_prompt + + # 添加当前时间 + current_time = datetime.now() + weekday_map = { + 0: "星期一", 1: "星期二", 2: "星期三", 3: "星期四", + 4: "星期五", 5: "星期六", 6: "星期日" + } + weekday = weekday_map[current_time.weekday()] + time_str = current_time.strftime(f"%Y年%m月%d日 %H:%M:%S {weekday}") + system_content += f"\n\n当前时间:{time_str}" + + if nickname: + system_content += f"\n当前对话用户的昵称是:{nickname}" + + # 加载持久记忆 + memory_chat_id = from_wxid if is_group else user_wxid + if memory_chat_id: + persistent_memories = self._get_persistent_memories(memory_chat_id) + if persistent_memories: + system_content += "\n\n【持久记忆】以下是用户要求你记住的重要信息:\n" + for m in persistent_memories: + mem_time = m['time'][:10] if m['time'] else "" + system_content += f"- [{mem_time}] {m['nickname']}: {m['content']}\n" + + return system_content + + # ==================== 结束 Gemini API 方法 ==================== + async def _handle_list_prompts(self, bot, from_wxid: str): """处理人设列表指令""" try: @@ -982,41 +1591,19 @@ class AIChat(PluginBase): chat_id = self._get_chat_id(from_wxid, user_wxid, is_group) self._add_to_memory(chat_id, "user", actual_content) - # 调用 AI API(带重试机制) - max_retries = self.config.get("api", {}).get("max_retries", 2) - response = None - last_error = None - - for attempt in range(max_retries + 1): - try: - response = await self._call_ai_api(actual_content, bot, from_wxid, chat_id, nickname, user_wxid, is_group) - - # 检查返回值: - # - None: 工具调用已异步处理,不需要重试 - # - "": 真正的空响应,需要重试 - # - 有内容: 正常响应 - if response is None: - # 工具调用,不重试 - logger.info("AI 触发工具调用,已异步处理") - break - - if response == "" and attempt < max_retries: - logger.warning(f"AI 返回空内容,重试 {attempt + 1}/{max_retries}") - await asyncio.sleep(1) # 等待1秒后重试 - continue - - break # 成功或已达到最大重试次数 - - except Exception as e: - last_error = e - if attempt < max_retries: - logger.warning(f"AI API 调用失败,重试 {attempt + 1}/{max_retries}: {e}") - await asyncio.sleep(1) - else: - raise + # 使用统一的 Gemini API 处理消息 + response = await self._process_with_gemini( + text=actual_content, + bot=bot, + from_wxid=from_wxid, + chat_id=chat_id, + nickname=nickname, + user_wxid=user_wxid, + is_group=is_group + ) # 发送回复并添加到记忆 - # 注意:如果返回 None 或空字符串,说明已经以其他形式处理了,不需要再发送文本 + # 注意:如果返回 None 或空字符串,说明已经以其他形式处理了(如工具调用) if response: await bot.send_text(from_wxid, response) self._add_to_memory(chat_id, "assistant", response) @@ -1028,7 +1615,7 @@ class AIChat(PluginBase): await self._add_to_history(from_wxid, bot_nickname, response) logger.success(f"AI 回复成功: {response[:50]}...") else: - logger.info("AI 回复为空或已通过其他方式发送(如聊天记录)") + logger.info("AI 回复为空或已通过其他方式发送(如工具调用)") except Exception as e: import traceback @@ -2060,8 +2647,17 @@ class AIChat(PluginBase): if is_group: await self._add_to_history(from_wxid, nickname, title_text, image_base64=image_base64) - # 调用AI API(带图片) - response = await self._call_ai_api_with_image(title_text, image_base64, bot, from_wxid, chat_id, nickname, user_wxid, is_group) + # 使用统一的 Gemini API 处理图片消息 + response = await self._process_with_gemini( + text=title_text, + image_base64=image_base64, + bot=bot, + from_wxid=from_wxid, + chat_id=chat_id, + nickname=nickname, + user_wxid=user_wxid, + is_group=is_group + ) if response: await bot.send_text(from_wxid, response) @@ -2074,7 +2670,7 @@ class AIChat(PluginBase): bot_nickname = main_config.get("Bot", {}).get("nickname", "机器人") await self._add_to_history(from_wxid, bot_nickname, response) logger.success(f"AI回复成功: {response[:50]}...") - + return False except Exception as e: @@ -2083,15 +2679,8 @@ class AIChat(PluginBase): async def _handle_quote_video(self, bot, video_elem, title_text: str, from_wxid: str, user_wxid: str, is_group: bool, nickname: str, chat_id: str): - """处理引用的视频消息 - 双AI架构""" + """处理引用的视频消息 - 统一 Gemini API(直接处理视频)""" try: - # 检查视频识别功能是否启用 - video_config = self.config.get("video_recognition", {}) - if not video_config.get("enabled", True): - logger.info("[视频识别] 功能未启用") - await bot.send_text(from_wxid, "❌ 视频识别功能未启用") - return False - # 提取视频 CDN 信息 cdnvideourl = video_elem.get("cdnvideourl", "") aeskey = video_elem.get("aeskey", "") @@ -2102,11 +2691,11 @@ class AIChat(PluginBase): aeskey = video_elem.get("cdnrawvideoaeskey", "") if not cdnvideourl or not aeskey: - logger.warning(f"[视频识别] 视频信息不完整: cdnurl={bool(cdnvideourl)}, aeskey={bool(aeskey)}") + logger.warning(f"[视频] 视频信息不完整: cdnurl={bool(cdnvideourl)}, aeskey={bool(aeskey)}") await bot.send_text(from_wxid, "❌ 无法获取视频信息") return False - logger.info(f"[视频识别] 处理引用视频: {title_text[:50]}...") + logger.info(f"[视频] 处理引用视频: {title_text[:50]}...") # 提示用户正在处理 await bot.send_text(from_wxid, "🎬 正在分析视频,请稍候...") @@ -2114,35 +2703,33 @@ class AIChat(PluginBase): # 下载并编码视频 video_base64 = await self._download_and_encode_video(bot, cdnvideourl, aeskey) if not video_base64: - logger.error("[视频识别] 视频下载失败") + logger.error("[视频] 视频下载失败") await bot.send_text(from_wxid, "❌ 视频下载失败") return False - logger.info("[视频识别] 视频下载和编码成功") + logger.info("[视频] 视频下载和编码成功") - # ========== 第一步:视频AI 分析视频内容 ========== - video_description = await self._analyze_video_content(video_base64, video_config) - if not video_description: - logger.error("[视频识别] 视频AI分析失败") - await bot.send_text(from_wxid, "❌ 视频分析失败") - return False - - logger.info(f"[视频识别] 视频AI分析完成: {video_description[:100]}...") - - # ========== 第二步:主AI 基于视频描述生成回复 ========== - # 构造包含视频描述的用户消息 + # 用户问题 user_question = title_text.strip() if title_text.strip() else "这个视频讲了什么?" - combined_message = f"[用户发送了一个视频,以下是视频内容描述]\n{video_description}\n\n[用户的问题]\n{user_question}" - # 添加到记忆(让主AI知道用户发了视频) - self._add_to_memory(chat_id, "user", combined_message) + # 添加到记忆 + self._add_to_memory(chat_id, "user", f"[发送了一个视频] {user_question}") # 如果是群聊,添加到历史记录 if is_group: await self._add_to_history(from_wxid, nickname, f"[发送了一个视频] {user_question}") - # 调用主AI生成回复(使用现有的 _call_ai_api 方法,继承完整上下文) - response = await self._call_ai_api(combined_message, chat_id, from_wxid, is_group, nickname) + # 使用统一的 Gemini API 直接处理视频(不再需要两步架构) + response = await self._process_with_gemini( + text=user_question, + video_base64=video_base64, + bot=bot, + from_wxid=from_wxid, + chat_id=chat_id, + nickname=nickname, + user_wxid=user_wxid, + is_group=is_group + ) if response: await bot.send_text(from_wxid, response) @@ -2154,7 +2741,7 @@ class AIChat(PluginBase): main_config = tomllib.load(f) bot_nickname = main_config.get("Bot", {}).get("nickname", "机器人") await self._add_to_history(from_wxid, bot_nickname, response) - logger.success(f"[视频识别] 主AI回复成功: {response[:50]}...") + logger.success(f"[视频] AI回复成功: {response[:50]}...") else: await bot.send_text(from_wxid, "❌ AI 回复生成失败") diff --git a/test_gemini_video.py b/test_gemini_video.py deleted file mode 100644 index a87b0bd..0000000 --- a/test_gemini_video.py +++ /dev/null @@ -1,83 +0,0 @@ -""" -测试 Gemini API 视频识别 -""" -import base64 -import requests -import json - -# 配置 -API_URL = "https://api.functen.cn/v1beta/models/gemini-3-pro-preview:generateContent" -API_KEY = "sk-NeOtq0kOU39x3LMqY09aKYLoOBJIgFkgGuDwVGgGEstXPn3M" -VIDEO_PATH = r"D:\project\shrobot\WechatHookBot\plugins\AIChat\265d0df9ea89578bcb86f824c5255a42.mp4" - -def test_video(): - # 读取视频并编码为 base64 - print(f"读取视频: {VIDEO_PATH}") - with open(VIDEO_PATH, "rb") as f: - video_data = f.read() - - video_base64 = base64.b64encode(video_data).decode() - print(f"视频大小: {len(video_data) / 1024 / 1024:.2f} MB") - print(f"Base64 长度: {len(video_base64)}") - - # 构建 Gemini 原生格式请求 - payload = { - "contents": [ - { - "parts": [ - {"text": "请描述这个视频的内容"}, - { - "inline_data": { - "mime_type": "video/mp4", - "data": video_base64 - } - } - ] - } - ], - "generationConfig": { - "maxOutputTokens": 4096 - } - } - - headers = { - "Content-Type": "application/json", - "Authorization": f"Bearer {API_KEY}" - } - - print(f"\n发送请求到: {API_URL}") - print("请求中...") - - try: - response = requests.post( - API_URL, - headers=headers, - json=payload, - timeout=180 - ) - - print(f"\n状态码: {response.status_code}") - - if response.status_code == 200: - result = response.json() - print("\n=== 响应内容 ===") - print(json.dumps(result, ensure_ascii=False, indent=2)) - - # 提取文本 - if "candidates" in result: - for candidate in result["candidates"]: - content = candidate.get("content", {}) - for part in content.get("parts", []): - if "text" in part: - print("\n=== AI 回复 ===") - print(part["text"]) - else: - print(f"\n错误响应: {response.text}") - - except Exception as e: - print(f"\n请求失败: {e}") - import traceback - traceback.print_exc() - -if __name__ == "__main__": - test_video()