commit 0406b5664fc73851cd1807e6b65b6ae21338b435 Author: 你的用户名 <你的邮箱> Date: Sat Nov 1 21:58:31 2025 +0800 chore: initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4022bea --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +.DS_Store +__pycache__/ +*.pyc +.idea/ +.vscode/ +.env +logs/ +*.log +*.log.* +*.gz +cache.db +data/*.db +*.session +telegram_sessions/ +node_modules/ +venv/ +*.session* +._* diff --git a/ARCHITECTURE_V3.md b/ARCHITECTURE_V3.md new file mode 100644 index 0000000..dd0d710 --- /dev/null +++ b/ARCHITECTURE_V3.md @@ -0,0 +1,144 @@ +# Bot V3 架构设计 + +## 核心原则 +1. 模块化设计 - 每个功能独立模块 +2. 清晰的数据流 - 用户输入 → AI分析 → 按钮选择 → 执行 → 结果展示 → 可返回 +3. 完整的错误处理 - 每一步都有降级方案 +4. 所有bytes正确处理 - 统一转换为hex字符串存储 + +## 模块划分 + +### 1. SessionManager (会话管理) +```python +class SessionManager: + - 管理用户会话状态 + - 存储AI分析结果 + - 存储用户选择历史 + - 支持返回上一步 +``` + +### 2. AIAnalyzer (AI意图分析) +```python +class AIAnalyzer: + - 调用Claude API分析用户输入 + - 生成3-5个搜索建议 + - 提取关键词和命令 + - 返回结构化数据 +``` + +### 3. ButtonGenerator (按钮生成器) +```python +class ButtonGenerator: + - 根据AI建议生成按钮 + - 为搜索结果添加控制按钮 + - 统一管理callback_data +``` + +### 4. SearchExecutor (搜索执行器) +```python +class SearchExecutor: + - 转发搜索到目标bot + - 接收并处理结果 + - 正确处理bytes类型 + - 触发后台翻页 +``` + +### 5. CacheManager (缓存管理器) +```python +class CacheManager: + - 所有bytes转hex存储 + - 读取时hex转bytes + - 统一的存取接口 +``` + +## 用户交互流程 + +``` +用户发送: "我想找德州扑克群" + ↓ +[AI分析器] 分析意图 + ↓ +生成建议: + 🔍 按名称搜索"德州扑克群" + 💬 搜索讨论"德州扑克"的群 + 🎯 搜索"扑克"相关内容 + ✍️ 手动输入命令 + ↓ +用户点击: "搜索讨论德州扑克的群" + ↓ +[搜索执行器] 执行 /text 德州扑克 + ↓ +展示结果 + 底部控制按钮: + [...搜索结果...] + [下一页] [上一页] + ───────────── + [🔙 返回重选] [🔄 优化搜索] + ↓ +用户点击: "返回重选" + ↓ +返回建议列表(从会话恢复) +``` + +## 数据结构 + +### 用户会话 +```python +{ + "user_id": 123, + "stage": "suggestions" | "searching" | "browsing", + "history": [ + { + "step": "input", + "content": "我想找德州扑克群", + "timestamp": "..." + }, + { + "step": "analysis", + "suggestions": [...], + "timestamp": "..." + }, + { + "step": "selected", + "command": "/text", + "keyword": "德州扑克", + "timestamp": "..." + } + ], + "can_go_back": True +} +``` + +### AI建议 +```python +{ + "explanation": "根据您的需求,我推荐以下搜索方式", + "suggestions": [ + { + "command": "/text", + "keyword": "德州扑克", + "description": "搜索讨论德州扑克的群组", + "icon": "💬", + "priority": 1 + } + ] +} +``` + +## 文件结构 + +``` +telegram-bot/ +├── bot_v3.py # 主程序 +├── modules/ +│ ├── __init__.py +│ ├── session_manager.py # 会话管理 +│ ├── ai_analyzer.py # AI分析 +│ ├── button_generator.py # 按钮生成 +│ ├── search_executor.py # 搜索执行 +│ └── cache_manager.py # 缓存管理 +├── utils/ +│ ├── __init__.py +│ ├── bytes_helper.py # bytes工具函数 +│ └── logger.py # 日志封装 +└── config.py # 配置文件 +``` diff --git a/BOT_README.md b/BOT_README.md new file mode 100644 index 0000000..f5c1d92 --- /dev/null +++ b/BOT_README.md @@ -0,0 +1,213 @@ +# Telegram Bot 管理指南 + +## 📌 概述 + +**唯一运行的脚本**: `integrated_bot_ai.py` + +这是一个**统一的、完整集成的** Telegram bot,包含所有功能: +- ✅ AI 对话引导(使用 claude-agent-sdk) +- ✅ Pyrogram 搜索(镜像 @openaiw_bot) +- ✅ 自动翻页缓存(SQLite 30天) +- ✅ 智能按钮生成 + +## 🚀 快速使用 + +### 使用管理脚本(推荐) + +```bash +# SSH 到虚拟机 +ssh atai@192.168.9.159 + +# 查看所有命令 +/home/atai/telegram-bot/manage_bot.sh + +# 常用命令 +/home/atai/telegram-bot/manage_bot.sh status # 查看状态 +/home/atai/telegram-bot/manage_bot.sh start # 启动 bot +/home/atai/telegram-bot/manage_bot.sh stop # 停止 bot +/home/atai/telegram-bot/manage_bot.sh restart # 重启 bot +/home/atai/telegram-bot/manage_bot.sh logs # 查看日志 +/home/atai/telegram-bot/manage_bot.sh info # 显示信息 +``` + +### 手动操作 + +```bash +# 启动 +cd /home/atai/telegram-bot +export ANTHROPIC_BASE_URL="http://202.79.167.23:3000/api" +export ANTHROPIC_AUTH_TOKEN="cr_9792f20a98f055e204248a41f280780ca2fb8f08f35e60c785e5245653937e06" +export ALL_PROXY="socks5://127.0.0.1:1080" +screen -dmS agent_bot bash -c 'python3 -u integrated_bot_ai.py 2>&1 | tee bot_agent_sdk.log' + +# 查看运行状态 +screen -ls + +# 查看日志 +tail -f bot_agent_sdk.log + +# 进入 screen 会话 +screen -r agent_bot +# 退出 screen: Ctrl+A, D + +# 停止 +screen -S agent_bot -X quit +``` + +## 📁 文件说明 + +### 🟢 当前使用的文件 + +| 文件 | 说明 | +|------|------| +| `integrated_bot_ai.py` | **主bot脚本**(唯一运行) | +| `claude_agent_wrapper.py` | Claude Agent SDK 包装器 | +| `manage_bot.sh` | Bot 管理脚本 | +| `bot_agent_sdk.log` | 运行日志 | +| `cache.db` | SQLite 缓存数据库 | +| `user_session.session` | Pyrogram 会话文件 | + +### 🟡 备份文件(不使用) + +| 文件 | 说明 | +|------|------| +| `integrated_bot_ai_backup_*.py` | 自动备份 | +| `integrated_bot_ai.backup.py` | 手动备份 | + +### 🔴 旧版文件(可删除) + +| 文件 | 说明 | +|------|------| +| `agent_bot.py` | 旧版 Agent Bot | +| `unified_telegram_bot.py` | 旧版统一 Bot | +| `integrated_bot.py` | 旧版集成 Bot | +| `bot_without_mirror.py` | 旧版无镜像 Bot | + +## 🔧 配置信息 + +### 环境变量 + +已配置在 `~/.bashrc`: +```bash +export ANTHROPIC_BASE_URL="http://202.79.167.23:3000/api" +export ANTHROPIC_AUTH_TOKEN="cr_9792f20a98f055e204248a41f280780ca2fb8f08f35e60c785e5245653937e06" +``` + +### Bot 信息 + +- **Bot名称**: @ktfund_bot +- **使用SDK**: claude-agent-sdk (Python) +- **AI模型**: claude-sonnet-4-5-20250929 +- **镜像Bot**: @openaiw_bot +- **代理**: socks5://127.0.0.1:1080 + +## 🔍 监控与调试 + +### 实时监控日志 + +```bash +# 监控所有日志 +tail -f /home/atai/telegram-bot/bot_agent_sdk.log + +# 监控 AI 调用 +tail -f /home/atai/telegram-bot/bot_agent_sdk.log | grep -E 'Claude|Agent|AI' + +# 监控用户消息 +tail -f /home/atai/telegram-bot/bot_agent_sdk.log | grep -E '用户|消息|搜索' + +# 监控错误 +tail -f /home/atai/telegram-bot/bot_agent_sdk.log | grep -E 'ERROR|❌|失败' +``` + +### 检查运行状态 + +```bash +# 检查 screen 会话 +screen -ls + +# 检查进程 +ps aux | grep integrated_bot_ai.py + +# 检查日志最新内容 +tail -50 /home/atai/telegram-bot/bot_agent_sdk.log +``` + +## 🐛 常见问题 + +### Bot 无响应 + +1. 检查是否运行:`/home/atai/telegram-bot/manage_bot.sh status` +2. 查看日志错误:`tail -100 /home/atai/telegram-bot/bot_agent_sdk.log | grep ERROR` +3. 重启 bot:`/home/atai/telegram-bot/manage_bot.sh restart` + +### AI 调用失败 + +检查环境变量: +```bash +echo $ANTHROPIC_BASE_URL +echo $ANTHROPIC_AUTH_TOKEN +``` + +如果为空,运行: +```bash +source ~/.bashrc +``` + +### Pyrogram 搜索失败 + +1. 检查代理:`curl --socks5 127.0.0.1:1080 https://api.telegram.org` +2. 检查会话文件:`ls -l user_session.session` + +## 📊 系统架构 + +``` +Telegram 用户 + ↓ +@ktfund_bot (虚拟机) + ↓ +integrated_bot_ai.py + ├─ Claude Agent SDK → AI 对话 + ├─ Pyrogram → 搜索 @openaiw_bot + ├─ SQLite → 缓存管理 + └─ Auto Pagination → 后台翻页 +``` + +## ⚙️ 维护建议 + +### 定期检查 + +- 每天检查 bot 状态:`/home/atai/telegram-bot/manage_bot.sh status` +- 每周清理旧日志:保留最近30天 +- 每月备份数据库:`cache.db` + +### 日志管理 + +```bash +# 查看日志大小 +du -h /home/atai/telegram-bot/bot_agent_sdk.log + +# 如果日志太大,可以轮转 +cd /home/atai/telegram-bot +mv bot_agent_sdk.log bot_agent_sdk.log.old +/home/atai/telegram-bot/manage_bot.sh restart +``` + +## 🎯 性能优化 + +当前配置已优化: +- ✅ 使用 SQLite 缓存(30天) +- ✅ 自动翻页(后台异步) +- ✅ 对话历史管理(最近5轮) +- ✅ 智能按钮去重 + +## 📝 更新日志 + +### 2025-10-07 +- ✅ 完成 claude-agent-sdk 集成 +- ✅ 创建统一管理脚本 +- ✅ 虚拟机完全独立运行 +- ✅ 不再依赖 Mac 服务 + +--- + +**注意**:其他所有旧 bot 脚本都已弃用,只需运行 `integrated_bot_ai.py`! diff --git a/CACHE_ANALYSIS.md b/CACHE_ANALYSIS.md new file mode 100644 index 0000000..a5b4e0f --- /dev/null +++ b/CACHE_ANALYSIS.md @@ -0,0 +1,228 @@ +# 缓存与自动翻页功能分析报告 + +生成时间: 2025-10-08 15:10 + +## ✅ 功能实现情况 + +### 1. 数据库缓存系统 ✅ + +**状态:已实现并正常工作** + +- **数据库位置:** `/home/atai/bot_data/cache.db` +- **数据库大小:** 652KB +- **缓存记录总数:** 245条 +- **按钮数据完整性:** 100% (245/245条都包含按钮数据) + +**缓存内容统计:** +``` +最多缓存的搜索: +- /text "德州扑克" - 73页缓存(最大页码51) +- /text "科技" - 52页缓存(最大页码51) +- /text "德州" - 50页缓存(最大页码49) +- /text "翻译" - 25页缓存(最大页码23) +- /text "十三" - 20页缓存(最大页码18) +``` + +**数据库功能测试:** +- ✅ 保存缓存 - 正常 +- ✅ 读取缓存 - 正常 +- ✅ 按钮数据保存 - 正常 +- ✅ 访问计数更新 - 正常 +- ✅ 过期清理机制 - 正常 + +### 2. 自动翻页功能 ✅ + +**状态:已实现,后台异步执行** + +**实现细节:** +```python +class AutoPaginationManager: + - 后台异步任务(asyncio.create_task) + - 用户首次搜索后自动触发 + - 最多翻10页 + - 每页间隔2秒 + - 自动检测"下一页"按钮 + - 全程后台运行,不影响用户界面 +``` + +**工作流程:** +1. 用户发起搜索(如 `/text 德州扑克`) +2. Bot立即返回第1页给用户 +3. 后台任务自动启动,开始翻页 +4. 第2-10页在后台自动保存到数据库 +5. 用户无感知,可以继续其他操作 + +### 3. 缓存复用机制 ✅ + +**状态:已实现,支持两个场景** + +#### 场景1:用户重复搜索相同关键词 +``` +流程: +1. 用户输入 /text 德州扑克 +2. Bot检查数据库缓存 +3. 如果命中,直接从缓存返回(含按钮) +4. 如果未命中,转发到搜索bot +5. 触发后台自动翻页并保存 +``` + +**代码位置:** `handle_search_command()` 函数,第553行 + +#### 场景2:用户点击翻页按钮 +``` +流程: +1. 用户点击"下一页"按钮 +2. Bot先检查缓存中是否有该页 +3. 如果有,直接从缓存加载(秒开) +4. 如果没有,转发callback到搜索bot +``` + +**代码位置:** `handle_callback()` 函数,第755行 + +## 🔍 当前发现的问题 + +### 问题1:缓存访问次数为0 + +**现象:** +- 数据库有245条缓存 +- 但所有缓存的`access_count`都是0 +- 说明这些缓存可能是旧版本bot生成的,还未被新版本使用 + +**原因分析:** +1. 缓存是10月7日之前生成的 +2. 10月8日bot重启后还没有用户进行搜索 +3. 缓存功能正常,只是还没有实际使用场景 + +**验证方法:** +```bash +# 测试缓存读取(已验证通过) +cd ~/telegram-bot +python3 -c " +from database import CacheDatabase +db = CacheDatabase(/home/atai/bot_data/cache.db) +cached = db.get_cache(/text, 德州扑克, 1) +print(缓存读取:, 成功 if cached else 失败) +" +``` + +### 问题2:后台翻页与用户前台翻页的隔离 + +**当前实现:** ✅ 已隔离 +- 后台翻页使用 `asyncio.create_task()` 异步执行 +- 用户收到第1页后立即可以操作 +- 后台任务独立运行,互不干扰 + +**用户体验:** +- ✅ 用户看到第1页响应速度快(秒开) +- ✅ 用户可以立即点击翻页或返回 +- ✅ 如果用户手动翻页到后面几页,缓存可能还没完成,会实时加载 +- ✅ 下次同样搜索,所有页都从缓存秒开 + +## 📊 性能优化建议 + +### 1. 预加载策略 ⭐ + +**建议:** 热门关键词预加载 +```python +# 在bot启动时预加载热门搜索 +async def preload_popular_searches(): + popular = [ + (/text, 德州扑克), + (/text, 科技), + (/search, 交易), + # ... + ] + for cmd, keyword in popular: + # 检查缓存是否过期,过期则重新加载 +``` + +### 2. 缓存刷新策略 + +**当前:** 30天过期 +**建议:** +- 热门搜索:7天刷新一次(保持内容新鲜) +- 冷门搜索:30天过期 +- 手动刷新:管理员命令 `/refresh ` + +### 3. 智能翻页深度 + +**当前:** 固定翻10页 +**建议:** 根据内容动态调整 +```python +# 如果前3页都没有结果,不继续翻页 +# 如果结果很多,翻到20页 +``` + +## 🎯 测试建议 + +### 完整测试流程 + +1. **首次搜索(缓存未命中)** + ``` + 用户: /text AI工具 + 预期: + - 立即返回第1页 + - 后台自动翻页2-10页 + - 数据库新增10条记录 + ``` + +2. **重复搜索(缓存命中)** + ``` + 用户: /text AI工具 + 预期: + - 秒开(从缓存) + - 按钮完整显示 + - access_count +1 + ``` + +3. **翻页测试(缓存命中)** + ``` + 用户: 点击"下一页" + 预期: + - 从缓存加载(秒开) + - 不再请求搜索bot + ``` + +### 日志验证 + +**关键日志标识:** +``` +[缓存命中] - 成功从缓存读取 +[翻页] 后台任务启动 - 开始自动翻页 +[翻页] 第X页已保存 - 每页保存成功 +[翻页] 完成,共X页 - 翻页完成 +``` + +**查看日志:** +```bash +# 实时查看详细日志 +tail -f ~/telegram-bot/logs/integrated_bot_detailed.log | grep "缓存\|翻页" + +# 查看审计日志 +tail -f ~/telegram-bot/logs/audit_202510.log | grep "缓存\|翻页" +``` + +## ✅ 结论 + +**所有核心功能已正确实现:** + +1. ✅ **自动翻页** - 后台异步,不干扰用户 +2. ✅ **数据持久化** - 完整保存到SQLite数据库 +3. ✅ **缓存复用** - 重复查询直接从缓存读取 +4. ✅ **按钮完整性** - 100%保存并恢复按钮数据 +5. ✅ **用户体验** - 首页秒开,后台翻页不打扰 + +**待用户实际使用验证:** +- 真实场景下的缓存命中率 +- 后台翻页的实际表现 +- 大量并发搜索的性能 + +**日志系统已完善:** +- 所有操作都有详细日志 +- 按文件名和行号追踪 +- 永不删档,按日期归档 + +--- + +生成者:Claude AI Assistant +版本:v2.1 diff --git a/CREATE_V3.md b/CREATE_V3.md new file mode 100644 index 0000000..49f2013 --- /dev/null +++ b/CREATE_V3.md @@ -0,0 +1,33 @@ +# Bot V3 重构计划 + +## 当前问题 +1. bytes处理不一致导致翻页失败 +2. 缺少智能引导流程 +3. 代码复杂度高,难以维护 + +## 解决方案 +创建bot_v3_simple.py - 简洁版本,包含: + +### 核心功能 +1. ✅ 基础搜索(/search, /text等) +2. ✅ 缓存和自动翻页 +3. ✅ bytes统一处理 +4. ✅ 简化的AI引导: + - 用户输入 → AI生成3个建议按钮 + - 用户点击 → 执行搜索 + - 结果底部有"返回重选"按钮 + +### 代码结构 +- 单文件,清晰分段 +- 工具函数集中在顶部 +- 所有bytes用hex存储 +- 完整错误处理 + +## 实施步骤 +1. 基于当前版本创建v3 +2. 只修复必要的bug +3. 添加简单的AI按钮引导 +4. 充分测试后替换 + +## 下一步 +正在创建 bot_v3_simple.py ... diff --git a/DEPLOY_TO_GITHUB.md b/DEPLOY_TO_GITHUB.md new file mode 100644 index 0000000..86e64da --- /dev/null +++ b/DEPLOY_TO_GITHUB.md @@ -0,0 +1,78 @@ +# 上传到 GitHub 的步骤 + +## 1. 创建 GitHub 仓库 + +1. 登录 [GitHub](https://github.com) +2. 点击右上角的 "+" → "New repository" +3. 填写仓库信息: + - Repository name: `telegram-customer-bot` + - Description: `Telegram 客服机器人 - 自动转发客户消息给管理员` + - 选择 Public 或 Private + - 不要初始化 README(我们已经有了) + +## 2. 连接本地仓库到 GitHub + +```bash +# 添加远程仓库(替换 YOUR_USERNAME) +git remote add origin https://github.com/YOUR_USERNAME/telegram-customer-bot.git + +# 或使用 SSH(如果配置了 SSH key) +git remote add origin git@github.com:YOUR_USERNAME/telegram-customer-bot.git + +# 推送代码 +git branch -M main +git push -u origin main +``` + +## 3. 项目功能说明 + +### 核心功能 +- ✅ **消息转发**:客户消息自动转发给管理员 +- ✅ **快速回复**:管理员直接输入文字即可回复最近客户 +- ✅ **会话管理**:追踪所有活跃会话 +- ✅ **数据持久化**:SQLite 数据库存储历史记录 +- ✅ **模块化架构**:清晰的代码结构,易于维护 + +### 技术特点 +- 🔧 生产级代码质量 +- 📝 完整的错误处理 +- 🎯 装饰器模式应用 +- 🗂️ 分层架构设计 +- ⚙️ 环境变量配置 + +### 目录结构 +``` +src/ +├── core/ # 核心业务逻辑 +│ ├── bot.py # 主机器人类 +│ ├── router.py # 消息路由 +│ └── handlers.py # 处理器基类 +├── config/ # 配置管理 +├── utils/ # 工具函数 +└── modules/ # 扩展模块 + └── storage/ # 数据存储 +``` + +## 4. 部署说明 + +1. 克隆仓库 +2. 复制 `.env.example` 为 `.env` +3. 填写你的 Bot Token 和管理员 ID +4. 安装依赖:`pip install -r requirements.txt` +5. 运行:`python main.py` + +## 5. 重要文件说明 + +- `.env.example` - 配置模板(不包含敏感信息) +- `.gitignore` - 忽略敏感文件(.env、数据库、日志) +- `requirements.txt` - Python 依赖 +- `LICENSE` - MIT 开源协议 + +## 注意事项 + +⚠️ **安全提醒**: +- 永远不要提交 `.env` 文件 +- Bot Token 必须保密 +- 定期备份数据库文件 + +🎉 项目已准备就绪,可以上传到 GitHub! diff --git a/FINAL_FIX_REPORT.md b/FINAL_FIX_REPORT.md new file mode 100644 index 0000000..21a5e7d --- /dev/null +++ b/FINAL_FIX_REPORT.md @@ -0,0 +1,134 @@ +# 镜像功能修复完成报告 + +生成时间: 2025-10-30 05:50 + +## 问题诊断 + +### 原始问题 (2025-10-26) +- ❌ AUTH_KEY_UNREGISTERED +- 原因: Session 文件过期 + +### 新问题 (2025-10-30) +- ❌ Connection lost +- 原因: Pyrogram 长时间运行后连接断开(运行3天+) + +## 解决方案 + +### 1. Session 修复 ✅ +- 在本地创建新的 session 文件 +- 上传到服务器 +- 设置正确权限 (600) +- 创建备份系统 + +### 2. 连接问题修复 ✅ +- 重启机器人重建 Pyrogram 连接 +- Pyrogram 客户端已重新连接: 05:48:26 +- 已连接到搜索机器人: openaiw_bot + +## 当前状态 + +**机器人进程** +- ✅ 运行中 (PID: 875994) +- ✅ 启动时间: 2025-10-30 05:48:24 + +**Pyrogram 客户端** +- ✅ 已启动: 05:48:25 +- ✅ 已连接: openaiw_bot +- ✅ 状态: 正常 + +**Session 文件** +- ✅ user_session.session (28K, 权限: 600) +- ✅ 备份: session_backups/ + +**错误状态** +- ✅ 重启后无新错误 +- ✅ 连接正常 + +## 保护机制 + +### 1. Session 保护 +- **自动备份**: 每 6 小时 +- **备份保留**: 7 天 +- **自动恢复**: Session 丢失时从备份恢复 + +### 2. 健康监控 +- **Session 监控**: 每 30 分钟 +- **检查项目**: + - Session 文件存在性 + - 文件大小 + - AUTH_KEY 错误 + - Pyrogram 状态 + +### 3. 自动重连 🆕 +- **检测频率**: 每 15 分钟 +- **触发条件**: 检测到 3+ 个 "Connection lost" 错误 +- **动作**: 自动重启机器人 +- **验证**: 检查 Pyrogram 是否重连成功 + +## 定时任务 + +``` +每 6 小时 - 备份 session +每 30 分钟 - 监控 session 健康 +每 15 分钟 - 检测连接错误并自动重启 🆕 +``` + +## 维护命令 + +**查看当前状态** +```bash +ssh atai@172.16.74.159 'cd telegram-bot && ./monitor_session.sh' +``` + +**手动重启** +```bash +ssh atai@172.16.74.159 'cd telegram-bot && ./manage_bot.sh restart' +``` + +**查看自动重启日志** +```bash +ssh atai@172.16.74.159 'tail -f telegram-bot/logs/auto_restart.log' +``` + +**查看错误日志** +```bash +ssh atai@172.16.74.159 'tail -f telegram-bot/logs/integrated_bot_errors.log' +``` + +## 脚本文件 + +- `protect_session.sh` - Session 备份和恢复 +- `monitor_session.sh` - Session 健康监控 +- `auto_restart_on_error.sh` - 自动重启机制 🆕 +- `manage_bot.sh` - 机器人管理 + +## 预防措施 + +### 问题 1: Session 过期 +- ✅ 已解决 +- ✅ 自动备份 +- ✅ 自动恢复 + +### 问题 2: 连接断开 +- ✅ 已解决 +- ✅ 自动检测 +- ✅ 自动重启 + +## 未来改进 + +1. **代码层面改进** (可选) + - 在代码中添加 Pyrogram 连接监控 + - 自动重连机制(无需重启整个进程) + +2. **通知机制** (可选) + - 发送 Telegram 通知给管理员 + - 当发生自动重启时通知 + +--- + +**结论**: +- ✅ 镜像功能已完全修复 +- ✅ 已部署完整的保护和自动恢复机制 +- ✅ 系统可以自动应对两类主要问题 + +最后更新: 2025-10-30 05:50 diff --git a/FINAL_STATUS.md b/FINAL_STATUS.md new file mode 100644 index 0000000..966d578 --- /dev/null +++ b/FINAL_STATUS.md @@ -0,0 +1,125 @@ +# 🎉 完整部署验证报告 + +**时间**: 2025-10-07 16:24:06 +**状态**: ✅ 所有服务完全在虚拟机上运行 + +--- + +## 📍 部署架构 + +``` +Telegram用户 + ↓ +@ktfund_bot + ↓ +虚拟机 (192.168.9.159) + ├─ integrated_bot_ai.py (唯一脚本) + ├─ claude-agent-sdk (Python) + ├─ Pyrogram (镜像@openaiw_bot) + ├─ SQLite缓存 (30天) + └─ V2Ray代理 + +❌ Mac (无依赖) - 所有服务已断开 +``` + +--- + +## ✅ 运行状态 + +### 1. Bot进程 +``` +Screen: agent_bot +PID: +脚本: /home/atai/telegram-bot/integrated_bot_ai.py +日志: /home/atai/telegram-bot/bot_agent_sdk.log +``` + +### 2. 网络服务 +- ✅ V2Ray: 运行中 +- ✅ SOCKS5代理: 127.0.0.1:1080 +- ✅ Telegram API: 正常连接 +- ✅ Claude API: 正常连接 + +### 3. Bot组件 +- ✅ Claude Agent SDK: 已初始化 +- ✅ Pyrogram: 会话已建立 +- ✅ 自动翻页: 已启用 +- ✅ SQLite缓存: 已启用 + +--- + +## 📊 实时指标 + +**最近10次Telegram轮询**: 全部成功 (200 OK) +**轮询间隔**: 每10秒 +**响应时间**: 正常 + +最新日志: +``` + +``` + +--- + +## 🔧 管理命令 + +### 查看状态 +```bash +/home/atai/telegram-bot/manage_bot.sh status +``` + +### 重启Bot +```bash +/home/atai/telegram-bot/manage_bot.sh restart +``` + +### 查看日志 +```bash +/home/atai/telegram-bot/manage_bot.sh logs +``` + +### 完整帮助 +```bash +/home/atai/telegram-bot/manage_bot.sh +``` + +--- + +## 📁 关键文件 + +| 文件 | 用途 | +|------|------| +| `integrated_bot_ai.py` | 主bot脚本(唯一) | +| `claude_agent_wrapper.py` | Agent SDK包装器 | +| `manage_bot.sh` | 管理脚本 | +| `bot_agent_sdk.log` | 运行日志 | +| `cache.db` | SQLite缓存 | +| `BOT_README.md` | 完整文档 | +| `FINAL_STATUS.md` | 此报告 | + +--- + +## 🎯 验证清单 + +- [x] Mac上所有服务已停止 +- [x] 虚拟机bot正常运行 +- [x] Claude Agent SDK正常工作 +- [x] Pyrogram连接正常 +- [x] Telegram API轮询成功 +- [x] 网络代理正常 +- [x] 日志记录正常 +- [x] 管理脚本可用 + +--- + +## 🚀 下一步 + +Bot已完全准备就绪!你可以: + +1. 向 @ktfund_bot 发送消息测试 +2. 使用 `manage_bot.sh` 管理bot +3. 查看 `BOT_README.md` 了解详细文档 + +--- + +**✅ 部署完成!所有服务100%在虚拟机上运行,不依赖Mac!** diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d12aa82 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Lucas (@xiaobai_80) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/QUICK_FIX.sh b/QUICK_FIX.sh new file mode 100755 index 0000000..0b932b5 --- /dev/null +++ b/QUICK_FIX.sh @@ -0,0 +1,91 @@ +#\!/bin/bash +# 镜像功能快速修复脚本 + +clear +echo "============================================" +echo " Telegram Bot 镜像功能修复工具" +echo "============================================" +echo "" +echo "问题:AUTH_KEY_UNREGISTERED" +echo "原因:Pyrogram Session 已过期" +echo "" +echo "============================================" +echo "" + +# 检查是否已有 session +if [ -f "user_session.session" ]; then + echo "⚠️ 检测到现有 session 文件" + echo " 文件:$(ls -lh user_session.session | awk '{print $9, $5}')" + echo "" + read -p "是否删除并重新创建?(y/N): " confirm + if [ "$confirm" \!= "y" ] && [ "$confirm" \!= "Y" ]; then + echo "操作已取消" + exit 0 + fi + echo "" + echo "备份现有文件..." + cp user_session.session user_session.session.old_$(date +%Y%m%d_%H%M%S) + rm -f user_session.session user_session.session-journal +fi + +echo "============================================" +echo " 准备创建新的 Session" +echo "============================================" +echo "" +echo "配置信息:" +echo " - API ID: 24660516" +echo " - 电话号码: +66621394851" +echo " - 代理: SOCKS5://127.0.0.1:1080" +echo "" +echo "请确保:" +echo " ✓ Telegram 应用已打开" +echo " ✓ 手机在身边(接收验证码)" +echo "" +read -p "按 Enter 开始创建 session..." dummy + +echo "" +echo "正在启动 Pyrogram 客户端..." +echo "============================================" +echo "" + +# 运行创建脚本 +python3 create_session_correct.py + +# 检查结果 +echo "" +echo "============================================" +if [ -f "user_session.session" ]; then + echo "✅ Session 创建成功!" + echo "" + echo "文件信息:" + ls -lh user_session.session* + echo "" + echo "下一步:重启机器人" + echo "" + read -p "是否现在重启机器人?(y/N): " restart + if [ "$restart" = "y" ] || [ "$restart" = "Y" ]; then + echo "" + echo "正在重启机器人..." + ./manage_bot.sh restart + echo "" + echo "请使用以下命令查看日志:" + echo " tail -f logs/integrated_bot_errors.log" + else + echo "" + echo "手动重启命令:" + echo " screen -r agent_bot # 然后按 Ctrl+C" + echo " # 或" + echo " ./manage_bot.sh restart" + fi +else + echo "❌ Session 创建失败" + echo "" + echo "请检查:" + echo " 1. 代理是否正常" + echo " 2. 验证码是否正确" + echo " 3. 网络连接" + echo "" + echo "查看详细说明:" + echo " cat README_SESSION.md" +fi +echo "============================================" diff --git a/README.md b/README.md new file mode 100644 index 0000000..206fa10 --- /dev/null +++ b/README.md @@ -0,0 +1,234 @@ +# Telegram 整合机器人 - NewBot925 🤖 + +[![Python Version](https://img.shields.io/badge/python-3.9%2B-blue)](https://www.python.org/downloads/) +[![License](https://img.shields.io/badge/license-MIT-green)](LICENSE) +[![Telegram Bot API](https://img.shields.io/badge/Telegram%20Bot%20API-Latest-blue)](https://core.telegram.org/bots/api) + +一个功能强大的Telegram机器人,集成了客服系统和搜索镜像功能。 + +## 项目说明 + +该项目构建了一个多管理员协作的 Telegram 客服机器人,支持消息自动转发、镜像搜索与统计看板等高级功能。通过模块化架构与丰富的脚本工具,可以快速部署、生成会话凭证并实现自动化运维。项目已预置示例配置与日志目录,适合作为客服团队的统一入口或集成至更大的业务系统。 + +## ✨ 核心功能 + +### 客服中转系统 +- 🔄 **消息自动转发**:客户消息自动转发给管理员 +- 💬 **便捷回复**:管理员直接回复转发消息即可回复客户 +- 👥 **会话管理**:追踪和管理多个客户会话 +- 📊 **实时统计**:消息数量、会话状态等统计信息 + +### 智能功能 +- ⏰ **营业时间管理**:自动识别工作时间 +- 🤖 **自动回复**:非工作时间自动回复 +- 📝 **消息历史**:完整的对话记录存储 +- 🏷️ **标签系统**:客户和会话标签管理 + +### 管理功能 +- 📈 **统计仪表板**:查看详细统计信息 +- 🔍 **会话监控**:实时查看活跃会话 +- 📢 **广播消息**:向所有客户发送通知 +- ⚙️ **动态配置**:运行时调整设置 + +## 🏗️ 系统架构 + +``` +telegram-customer-bot/ +├── src/ +│ ├── core/ # 核心模块 +│ │ ├── bot.py # 主机器人类 +│ │ ├── router.py # 消息路由系统 +│ │ └── handlers.py # 处理器基类 +│ ├── modules/ # 功能模块 +│ │ └── storage/ # 数据存储 +│ ├── utils/ # 工具函数 +│ │ ├── logger.py # 日志系统 +│ │ ├── exceptions.py # 异常处理 +│ │ └── decorators.py # 装饰器 +│ └── config/ # 配置管理 +├── tests/ # 测试文件 +├── logs/ # 日志文件 +├── data/ # 数据存储 +└── main.py # 程序入口 +``` + +## 🚀 快速开始 + +### 1. 环境要求 +- Python 3.9+ +- pip + +### 2. 安装 + +```bash +# 克隆或下载项目 +cd /Users/lucas/telegram-customer-bot + +# 安装依赖 +pip install -r requirements.txt +``` + +### 3. 配置 + +复制 `.env.example` 为 `.env` 并填写配置: + +```bash +cp .env.example .env +``` + +编辑 `.env` 文件(已配置你的信息): +- `BOT_TOKEN`: 你的机器人 Token +- `ADMIN_ID`: 你的 Telegram ID (7363537082) +- 其他配置根据需要调整 + +### 4. 运行 + +```bash +python main.py +``` + +## 📝 使用指南 + +### 客户端命令 +- `/start` - 开始使用机器人 +- `/help` - 获取帮助信息 +- `/status` - 查看服务状态 +- `/contact` - 联系人工客服 + +### 管理员命令 +- `/stats` - 查看统计信息 +- `/sessions` - 查看活跃会话 +- `/reply <用户ID> <消息>` - 回复指定用户 +- `/broadcast <消息>` - 广播消息 +- `/settings` - 机器人设置 + +### 回复客户消息 +1. **直接回复**:回复机器人转发的消息 +2. **命令回复**:使用 `/reply` 命令 +3. **快捷按钮**:使用消息下方的快捷操作按钮 + +## 🔧 高级配置 + +### 环境变量说明 + +| 变量名 | 说明 | 默认值 | +|--------|------|--------| +| `BOT_TOKEN` | Telegram Bot Token | 必填 | +| `ADMIN_ID` | 管理员 Telegram ID | 必填 | +| `LOG_LEVEL` | 日志级别 | INFO | +| `DATABASE_TYPE` | 数据库类型 | sqlite | +| `BUSINESS_HOURS_START` | 营业开始时间 | 09:00 | +| `BUSINESS_HOURS_END` | 营业结束时间 | 18:00 | +| `TIMEZONE` | 时区 | Asia/Shanghai | + +### 功能开关 + +在 `.env` 文件中可以控制功能开关: + +- `ENABLE_AUTO_REPLY` - 自动回复 +- `ENABLE_STATISTICS` - 统计功能 +- `ENABLE_CUSTOMER_HISTORY` - 客户历史记录 + +## 🛡️ 安全特性 + +- ✅ **权限控制**:严格的管理员权限验证 +- ✅ **速率限制**:防止消息轰炸 +- ✅ **错误处理**:完善的异常捕获和处理 +- ✅ **日志记录**:详细的操作日志 +- ✅ **数据加密**:敏感数据加密存储(可选) + +## 📊 监控和维护 + +### 日志文件 +- 位置:`logs/bot.log` +- JSON 格式:`logs/bot.json` +- 自动轮转:达到 10MB 自动轮转 + +### 数据库维护 +- 自动清理:30天以上的已关闭会话 +- 备份建议:定期备份 `data/bot.db` + +### 性能优化 +- 异步处理:所有 I/O 操作异步执行 +- 连接池:数据库连接池管理 +- 缓存:频繁访问数据缓存 + +## 🔄 更新和升级 + +```bash +# 备份数据 +cp -r data data_backup + +# 更新代码 +git pull # 如果使用git + +# 更新依赖 +pip install -r requirements.txt --upgrade + +# 重启机器人 +python main.py +``` + +## 🐛 故障排除 + +### 常见问题 + +1. **机器人无响应** + - 检查 Token 是否正确 + - 检查网络连接 + - 查看日志文件 + +2. **消息未转发** + - 确认管理员 ID 正确 + - 检查机器人权限 + +3. **数据库错误** + - 检查 data 目录权限 + - 尝试删除并重建数据库 + +### 调试模式 + +在 `.env` 中设置 `DEBUG=true` 启用调试模式。 + +## 📈 扩展开发 + +### 添加新功能模块 + +1. 在 `src/modules/` 创建新模块 +2. 继承 `BaseHandler` 类 +3. 在 `bot.py` 中注册处理器 + +### 自定义中间件 + +```python +from src.core.router import MessageRouter + +router = MessageRouter(config) + +@router.middleware() +async def custom_middleware(context, telegram_context): + # 处理逻辑 + return True # 继续处理 +``` + +## 🤝 技术支持 + +- 查看日志:`tail -f logs/bot.log` +- 数据库查询:使用 SQLite 工具打开 `data/bot.db` +- 性能监控:查看 `/stats` 命令输出 + +## 📄 许可证 + +MIT License + +## 🙏 致谢 + +- python-telegram-bot - Telegram Bot API 库 +- SQLite - 轻量级数据库 +- 所有开源贡献者 + +--- + +**当前版本**: 1.0.0 +**最后更新**: 2025-09-24 +**作者**: 阿泰 (@xiaobai_80) diff --git a/README_SESSION.md b/README_SESSION.md new file mode 100644 index 0000000..dff77ab --- /dev/null +++ b/README_SESSION.md @@ -0,0 +1,100 @@ +# Pyrogram Session 修复指南 + +## 问题描述 +镜像搜索功能报错:`AUTH_KEY_UNREGISTERED` - Session 文件已过期 + +## 解决方案 + +### 方法 1:使用自动化脚本(推荐) + +运行以下命令: + +```bash +cd ~/telegram-bot +./auto_create_session.exp +``` + +脚本会自动: +1. 输入电话号码 (+66621394851) +2. 等待您输入 Telegram 验证码 +3. 创建新的 session 文件 + +### 方法 2:手动创建 + +```bash +cd ~/telegram-bot +python3 create_session_correct.py +``` + +按提示操作: +1. 输入电话号码: `+66621394851` +2. 确认: `y` +3. 输入收到的验证码 +4. 确认: `y` + +### 方法 3:一键修复命令 + +```bash +sshpass -p 'wengewudi666808' ssh -tt atai@172.16.74.159 'cd telegram-bot && ./auto_create_session.exp' +``` + +## 完成后 + +检查 session 文件是否创建成功: + +```bash +ls -lh ~/telegram-bot/user_session.session* +``` + +重启机器人: + +```bash +screen -r agent_bot +# 按 Ctrl+C 停止 +# 等待几秒 +# 机器人会自动重启(run_bot_loop.sh) +``` + +或使用管理脚本: + +```bash +~/telegram-bot/manage_bot.sh restart +``` + +## 验证 + +重启后检查日志: + +```bash +tail -f ~/telegram-bot/logs/integrated_bot_errors.log +``` + +确认没有 `AUTH_KEY_UNREGISTERED` 错误。 + +## 文件说明 + +- `create_session_correct.py` - Session 创建脚本(Python) +- `auto_create_session.exp` - 自动化脚本(Expect) +- `user_session.session` - Session 文件(创建后) + +## 故障排除 + +### 代理问题 +如果连接失败,检查代理: + +```bash +curl --socks5 127.0.0.1:1080 https://api.telegram.org +``` + +### 验证码问题 +- 确保 Telegram 应用已打开 +- 验证码可能在"Telegram" 或"Saved Messages" +- 如果收不到验证码,等待几分钟后重试 + +### 文件权限 +```bash +chmod 600 ~/telegram-bot/user_session.session* +``` + +--- +创建时间: 2025-10-26 diff --git a/SESSION_STATUS.md b/SESSION_STATUS.md new file mode 100644 index 0000000..7f9b589 --- /dev/null +++ b/SESSION_STATUS.md @@ -0,0 +1,106 @@ +# Session 状态报告 + +生成时间: 2025-10-26 13:17 + +## ✅ Session 修复完成 + +### 当前状态 + +**Session 文件** +- ✅ user_session.session (28K) - 权限: 600 +- ✅ user_session.session-journal (17K) - 权限: 600 +- ✅ 备份文件已创建 + +**机器人状态** +- ✅ 进程运行中 (PID: 726279) +- ✅ Pyrogram 客户端已启动 (13:14:22) +- ✅ 已连接到搜索机器人: openaiw_bot (13:14:22) +- ✅ 没有新的 AUTH_KEY 错误 (重启后) + +### 错误历史 + +- 最后的 AUTH_KEY 错误: 2025-10-26 12:05:23 +- 机器人重启时间: 2025-10-26 13:14:21 +- **重启后无新错误** ✅ + +### 保护措施 + +**1. 文件权限** +- Session 文件权限设置为 600(仅所有者可读写) + +**2. 自动备份** +- 定时任务: 每 6 小时自动备份 +- 备份保留: 7 天 +- 备份位置: ~/telegram-bot/session_backups/ + +**3. 健康监控** +- 定时任务: 每 30 分钟检查 +- 监控项目: + - Session 文件存在性 + - 文件大小 + - AUTH_KEY 错误 + - Pyrogram 客户端状态 + +**4. 自动恢复** +- 如果 session 丢失,自动从备份恢复 + +### 定时任务 + +``` +0 */6 * * * 备份 session 文件 +*/30 * * * * 监控 session 健康状态 +``` + +### 维护命令 + +**手动备份** +```bash +cd ~/telegram-bot +./protect_session.sh +``` + +**健康检查** +```bash +cd ~/telegram-bot +./monitor_session.sh +``` + +**查看备份** +```bash +ls -lh ~/telegram-bot/session_backups/ +``` + +**查看监控日志** +```bash +tail -f ~/telegram-bot/logs/session_monitor.log +``` + +**查看备份日志** +```bash +tail -f ~/telegram-bot/logs/session_backup.log +``` + +## 技术细节 + +### Session 创建信息 +- API ID: 24660516 +- 电话: +66621394851 +- 账户: 阿泰 +- 创建时间: 2025-10-26 13:14 +- 创建方式: 本地创建,上传到服务器 + +### 文件位置 +- Session: ~/telegram-bot/user_session.session +- 备份: ~/telegram-bot/session_backups/ +- 日志: ~/telegram-bot/logs/ + +### 安全性 +- ✅ 文件权限限制(600) +- ✅ 定期自动备份 +- ✅ 健康监控 +- ✅ 自动恢复机制 + +--- + +**结论**: Session 已成功修复,镜像功能正常工作,已设置完整的保护和监控机制。 + diff --git a/SMART_MONITORING.md b/SMART_MONITORING.md new file mode 100644 index 0000000..74a3ba7 --- /dev/null +++ b/SMART_MONITORING.md @@ -0,0 +1,131 @@ +# 智能监控系统配置 + +更新时间: 2025-10-30 06:01 + +## 设计原则 + +**只在有问题时干预 - 正常运行时不碰它** + +## 定时任务配置 + +### 1. Session 自动备份 +``` +时间: 每 6 小时(0, 6, 12, 18点) +命令: ./protect_session.sh +功能: + - 备份 session 文件 + - 自动恢复(如果丢失) + - 保留 7 天历史 +``` + +### 2. 智能健康检查 +``` +时间: 每 12 小时(0点和12点) +命令: ./smart_health_check.sh +功能: + - 检查机器人进程 + - 检查 Pyrogram 连接 + - 检查最近错误 + - 只在有问题时重启 +``` + +## 智能检查逻辑 + +### 检查项目 +1. ✅ 机器人进程是否运行 +2. ✅ Pyrogram 客户端状态 +3. ✅ 最近1小时 Connection lost 错误 +4. ✅ 最近1小时 AUTH_KEY 错误 + +### 触发重启条件(满足任一条件) +- ❌ 机器人进程未运行 +- ❌ Connection lost 错误 > 5个(1小时内) +- ❌ AUTH_KEY 错误 > 0个(1小时内) +- ❌ Pyrogram 状态异常 + Connection 错误 > 2个 + +### 正常运行时 +- ✅ 检查所有状态 +- ✅ 记录日志 +- ✅ **不采取任何行动** + +## 对比之前的配置 + +### 之前(过于激进) +- 每 15 分钟检测并可能重启 +- 每 30 分钟监控 +- 频繁干预 + +### 现在(温和智能) +- 每 12 小时检查一次 +- 只在真正有问题时干预 +- 让系统自然运行 + +## 运行记录 + +### 首次测试 (2025-10-30 05:59:46) +``` +✅ Pyrogram 客户端状态: 正常 +✅ 最近1小时 Connection lost 错误: 0 个 +✅ 最近1小时 AUTH_KEY 错误: 0 个 +✅ 一切正常,无需干预 +操作: 无操作 +``` + +## 日志文件 + +- `logs/smart_health_check.log` - 检查记录 +- `logs/session_backup.log` - 备份记录 +- `logs/integrated_bot_errors.log` - 机器人错误 + +## 查看命令 + +**查看定时任务** +```bash +crontab -l +``` + +**查看检查日志** +```bash +tail -50 ~/telegram-bot/logs/smart_health_check.log +``` + +**手动运行检查** +```bash +cd ~/telegram-bot && ./smart_health_check.sh +``` + +**查看机器人状态** +```bash +ps aux | grep integrated_bot +``` + +## 时间表 + +``` +00:00 - 智能健康检查 + Session备份 +06:00 - Session备份 +08:00 - 每日自检(系统原有) +12:00 - 智能健康检查 + Session备份 +18:00 - Session备份 +``` + +## 预期效果 + +1. ✅ 系统正常运行时:完全不干预 +2. ✅ 出现小问题时:等待自然恢复 +3. ✅ 出现严重问题时:自动重启恢复 +4. ✅ Session 定期备份:防止数据丢失 + +## 维护建议 + +- 定期查看日志(每周一次) +- 如果频繁重启,检查根本原因 +- 备份文件每月清理一次(自动) + +--- + +**总结**: +- 每12小时检查一次 +- 只在真正有问题时采取行动 +- 让系统保持自然稳定运行 + diff --git a/UPDATE_LOG_20251008.md b/UPDATE_LOG_20251008.md new file mode 100644 index 0000000..0e09b82 --- /dev/null +++ b/UPDATE_LOG_20251008.md @@ -0,0 +1,142 @@ +# Bot更新日志 - 2025年10月8日 + +## ✅ 已完成的更新 + +### 1. 修复Claude API认证问题 +**问题:** Bot无法调用Claude API,报错"Could not resolve authentication method" +**解决方案:** +- 在 `.env` 文件中添加了 `ANTHROPIC_AUTH_TOKEN` 和 `ANTHROPIC_BASE_URL` +- 创建了新的启动脚本 `start_bot_fixed.sh`,自动加载环境变量 +- 验证API调用成功(模型:claude-sonnet-4-20250514) + +### 2. 添加快捷按钮功能 +**新增功能:** +用户点击 `/start` 后会看到三个快捷按钮: +- 🔍 搜索群组 (`quick_search`) - 引导用户选择搜索类型 +- 📚 使用指南 (`quick_help`) - 显示详细的使用说明 +- 🔥 热门分类 (`quick_topchat`) - 直接触发 `/topchat` 命令 + +**实现细节:** +- 在 `handle_callback` 函数中添加了三个按钮的处理逻辑 +- `quick_search`: 显示搜索类型选择菜单(search/text/human) +- `quick_help`: 显示详细使用指南和示例 +- `quick_topchat`: 自动执行 `/topchat` 命令,展示热门群组分类 + +### 3. 增强型日志系统 +**核心特性:** +- ✅ **不删档** - 所有日志永久保留 +- ✅ **自动轮转** - 按日期和大小自动轮转 +- ✅ **多级存储** - 详细日志、错误日志、审计日志分别存储 +- ✅ **完整追踪** - 包含文件名、行号、时间戳 + +**日志文件说明:** +``` +logs/ +├── integrated_bot_detailed.log # 详细日志(DEBUG级别,按天轮转,保留90天) +├── integrated_bot_detailed.log.20251007 # 昨天的归档 +├── integrated_bot_errors.log # 错误日志(ERROR级别,50MB轮转,保留10个文件) +├── audit_202510.log # 审计日志(按月,永久保存) +└── archive/ # 归档目录 +``` + +**日志级别:** +- 控制台输出:INFO及以上 +- 详细日志:DEBUG及以上(包含文件名和行号) +- 错误日志:ERROR及以上(详细堆栈信息) +- 审计日志:INFO及以上(永久记录) + +### 4. 文件备份 +创建了代码备份: +- `integrated_bot_ai.backup.20251008_HHMMSS.py` +- 所有修改前都有自动备份 + +## 📁 新增文件 + +1. **enhanced_logger.py** - 增强型日志模块 +2. **start_bot_fixed.sh** - 修复后的启动脚本 +3. **logs/** - 日志目录(自动创建) + +## 🔧 修改的文件 + +1. **integrated_bot_ai.py** + - 集成 `EnhancedLogger` + - 添加快捷按钮处理逻辑(`quick_search`, `quick_help`, `quick_topchat`) + +2. **.env** + - 添加 `ANTHROPIC_AUTH_TOKEN` + - 添加 `ANTHROPIC_BASE_URL` + +## 🚀 启动命令 + +```bash +cd ~/telegram-bot +./start_bot_fixed.sh +``` + +## 📊 查看日志 + +```bash +# 查看实时日志 +tail -f ~/telegram-bot/bot_agent_sdk.log + +# 查看详细日志 +tail -f ~/telegram-bot/logs/integrated_bot_detailed.log + +# 查看错误日志 +cat ~/telegram-bot/logs/integrated_bot_errors.log + +# 查看审计日志 +cat ~/telegram-bot/logs/audit_202510.log + +# 查看screen会话 +screen -r agent_bot +``` + +## ✅ 验证测试 + +### Bot状态检查 +```bash +# 检查进程 +ps aux | grep integrated_bot_ai.py + +# 检查日志目录 +ls -lh ~/telegram-bot/logs/ + +# 测试Claude API +cd ~/telegram-bot && python3 test_claude_api3.py +``` + +### 功能测试清单 +- [x] Bot启动成功 +- [x] Claude API认证成功 +- [x] 快捷按钮显示正常 +- [x] 点击"搜索群组"按钮有响应 +- [x] 点击"使用指南"按钮显示帮助 +- [x] 点击"热门分类"按钮触发topchat +- [x] 日志文件正常创建 +- [x] 日志包含详细信息(文件名、行号) +- [x] 错误日志独立存储 + +## 📝 注意事项 + +1. **日志不会自动删除** - 详细日志保留90天,审计日志永久保存 +2. **日志会自动归档** - 每天午夜自动轮转 +3. **环境变量必须正确** - 使用 `start_bot_fixed.sh` 启动以确保环境变量加载 +4. **backup目录** - 所有旧版本代码都保存在backup文件中 + +## 🎯 用户体验改进 + +用户现在可以: +1. 点击按钮直接操作,无需输入命令 +2. 获得更清晰的引导和帮助信息 +3. 快速访问热门分类 + +开发者现在可以: +1. 查看完整的操作日志(不会丢失) +2. 快速定位错误(包含文件名和行号) +3. 审计所有用户操作(永久记录) + +--- +生成时间:2025-10-08 14:58 +Bot版本:AI增强版 v2.1 +更新者:Claude AI Assistant diff --git a/WORKFLOW_VISUALIZATION.txt b/WORKFLOW_VISUALIZATION.txt new file mode 100644 index 0000000..68ac513 --- /dev/null +++ b/WORKFLOW_VISUALIZATION.txt @@ -0,0 +1,205 @@ +╔══════════════════════════════════════════════════════════════════════════╗ +║ 自动翻页与缓存系统工作流程图 ║ +╚══════════════════════════════════════════════════════════════════════════╝ + +场景1: 首次搜索(缓存未命中) +═══════════════════════════════════════════════════════════════════ + +┌─────────────┐ +│ 用户发送命令 │ /text 德州扑克 +└──────┬──────┘ + │ + ↓ +┌──────────────────┐ +│ Bot检查数据库缓存 │ +└──────┬───────────┘ + │ + ↓ + ┌─────────┐ + │ 未命中? │ + └────┬────┘ + │ Yes + ↓ +┌────────────────────────┐ ┌─────────────────────────┐ +│ 转发到搜索Bot @openaiw │──────→│ 搜索Bot返回第1页结果 │ +└────────────────────────┘ └───────────┬─────────────┘ + │ + ↓ + ┌────────────────────────────────────┐ + │ 【前台】立即显示给用户(秒开) │ + │ - 显示第1页内容 │ + │ - 显示翻页按钮 │ + │ - 用户可以立即操作 │ + └────────────────────────────────────┘ + │ + ↓ + ┌────────────────────────────────────┐ + │ 【后台】启动自动翻页任务(异步) │ + │ asyncio.create_task() │ + └───────────┬────────────────────────┘ + │ + ↓ + ┌───────────────────────────────────────┐ + │ 后台循环翻页(用户无感知) │ + │ │ + │ for page in range(2, 11): │ + │ ├─ 等待2秒 │ + │ ├─ 点击"下一页"按钮 │ + │ ├─ 获取新页面内容 │ + │ ├─ 保存到数据库 │ + │ │ • 文本内容 │ + │ │ • 按钮数据(JSON) │ + │ │ • 创建时间 │ + │ └─ 检查是否还有下一页 │ + │ │ + │ 【日志】第2页已保存 │ + │ 【日志】第3页已保存 │ + │ ... │ + │ 【日志】完成,共10页 │ + └───────────────────────────────────────┘ + │ + ↓ + ┌─────────────────────────┐ + │ 全部保存到SQLite数据库 │ + │ /home/atai/bot_data/ │ + │ cache.db │ + └─────────────────────────┘ + + +场景2: 重复搜索(缓存命中) +═══════════════════════════════════════════════════════════════════ + +┌─────────────┐ +│ 用户发送命令 │ /text 德州扑克(再次搜索) +└──────┬──────┘ + │ + ↓ +┌──────────────────┐ +│ Bot检查数据库缓存 │ +└──────┬───────────┘ + │ + ↓ + ┌─────────┐ + │ 命中? │ + └────┬────┘ + │ Yes! + ↓ +┌────────────────────────────────────┐ +│ 从数据库读取缓存数据(秒开!) │ +│ • 读取文本内容 │ +│ • 读取按钮JSON │ +│ • 重建InlineKeyboardMarkup │ +│ • 更新access_count +1 │ +└────────┬───────────────────────────┘ + │ + ↓ +┌─────────────────────────┐ +│ 立即显示给用户(秒开!) │ +│ - 不需要请求搜索Bot │ +│ - 不需要网络请求 │ +│ - 按钮完整恢复 │ +│ - 响应时间 < 100ms │ +└─────────────────────────┘ + + +场景3: 用户翻页(缓存命中) +═══════════════════════════════════════════════════════════════════ + +┌──────────────┐ +│ 用户点击按钮 │ [下一页] +└──────┬───────┘ + │ + ↓ +┌───────────────────────┐ +│ Bot解析callback_data │ 例如:page_5 +└──────┬────────────────┘ + │ + ↓ +┌─────────────────────────────┐ +│ 从session获取搜索上下文 │ +│ • command: /text │ +│ • keyword: 德州扑克 │ +│ • page: 5 │ +└──────┬──────────────────────┘ + │ + ↓ +┌──────────────────────┐ +│ 检查数据库缓存 │ get_cache(/text, 德州扑克, 5) +└──────┬───────────────┘ + │ + ↓ + ┌─────────┐ + │ 命中? │ + └────┬────┘ + │ Yes! + ↓ +┌─────────────────────────────┐ +│ 从缓存加载第5页(秒开!) │ +│ - 完整内容 │ +│ - 所有按钮 │ +│ - edit_message直接替换 │ +└─────────────────────────────┘ + + +═══════════════════════════════════════════════════════════════════ + 关键性能指标 +═══════════════════════════════════════════════════════════════════ + +首次搜索: + ├─ 用户看到第1页:~1-2秒(取决于搜索Bot) + ├─ 后台翻页启动:立即(异步) + └─ 完成10页缓存:~20秒(用户无感知) + +重复搜索: + ├─ 缓存命中:<100ms(秒开!) + ├─ 按钮完整性:100% + └─ 无需网络请求 + +缓存翻页: + ├─ 页面切换:<100ms(秒开!) + ├─ 从缓存加载:是 + └─ 请求搜索Bot:否 + +═══════════════════════════════════════════════════════════════════ + 数据库结构 +═══════════════════════════════════════════════════════════════════ + +search_cache 表: +┌─────────────────┬──────────────────────────────────────┐ +│ 字段 │ 说明 │ +├─────────────────┼──────────────────────────────────────┤ +│ id │ 主键,自增 │ +│ command │ 命令(如 /text, /search) │ +│ keyword │ 关键词 │ +│ page │ 页码 │ +│ result_text │ 结果文本 │ +│ result_html │ HTML格式(备用) │ +│ buttons_json │ 按钮数据(JSON格式) │ +│ result_hash │ 内容哈希(用于去重) │ +│ created_at │ 创建时间 │ +│ expires_at │ 过期时间(30天) │ +│ access_count │ 访问次数 │ +│ last_accessed │ 最后访问时间 │ +└─────────────────┴──────────────────────────────────────┘ + +索引: + • idx_unique_cache (command, keyword, page, result_hash) - 防重复 + • idx_search (command, keyword, page) - 快速查询 + • idx_expires (expires_at) - 过期清理 + +═══════════════════════════════════════════════════════════════════ + 当前数据统计 +═══════════════════════════════════════════════════════════════════ + +总缓存记录:245条 +按钮完整性:100% (245/245) +数据库大小:652KB +有效缓存:245条 +过期缓存:0条 + +热门缓存: + 1. /text "德州扑克" - 73页 + 2. /text "科技" - 52页 + 3. /text "德州" - 50页 + +═══════════════════════════════════════════════════════════════════ diff --git a/admin_commands.py b/admin_commands.py new file mode 100644 index 0000000..8f0c011 --- /dev/null +++ b/admin_commands.py @@ -0,0 +1,347 @@ +#!/usr/bin/env python3 +""" +管理员命令模块 +为integrated_bot_ai.py添加管理员后台功能 +""" +import sqlite3 +from datetime import datetime, timedelta +from typing import Dict, List +import os +import psutil + +ADMIN_ID = 7363537082 + +class AdminCommands: + """管理员命令处理类""" + + def __init__(self, bot_instance, logger): + self.bot = bot_instance + self.logger = logger + self.stats = { + "start_time": datetime.now(), + "messages_received": 0, + "searches_performed": 0, + "ai_queries": 0, + "cache_hits": 0 + } + + async def handle_stats(self, update, context): + """处理 /stats 命令 - 查看统计信息""" + user = update.effective_user + if user.id != ADMIN_ID: + return + + uptime = datetime.now() - self.stats["start_time"] + days = uptime.days + hours = uptime.seconds // 3600 + minutes = (uptime.seconds % 3600) // 60 + + # 获取进程信息 + process = psutil.Process(os.getpid()) + memory_mb = process.memory_info().rss / 1024 / 1024 + + # 查询日志获取用户数 + user_count = self._get_unique_users_from_logs() + + # 查询缓存数据库 + cache_stats = self._get_cache_stats() + + text = ( + "📊 **机器人统计信息**\\n\\n" + f"⏱ **运行时间:** {days}天 {hours}小时 {minutes}分钟\\n" + f"📅 **启动时间:** {self.stats['start_time'].strftime('%Y-%m-%d %H:%M:%S')}\\n\\n" + + "👥 **用户统计:**\\n" + f"• 总访问用户:{user_count}人\\n" + f"• AI对话次数:{self.stats['ai_queries']}\\n" + f"• 搜索执行次数:{self.stats['searches_performed']}\\n\\n" + + "💾 **缓存统计:**\\n" + f"• 缓存记录数:{cache_stats['total']}\\n" + f"• 缓存命中率:{cache_stats['hit_rate']:.1f}%\\n\\n" + + "💻 **系统资源:**\\n" + f"• 内存使用:{memory_mb:.1f} MB\\n" + f"• CPU核心数:{psutil.cpu_count()}\\n" + ) + + await update.message.reply_text(text, parse_mode='Markdown') + self.logger.info(f"[管理员] 查看统计信息") + + async def handle_users(self, update, context): + """处理 /users 命令 - 查看用户列表""" + user = update.effective_user + if user.id != ADMIN_ID: + return + + users = self._get_users_from_logs() + + if not users: + await update.message.reply_text("暂无用户访问记录") + return + + text = f"👥 **访问用户列表** ({len(users)}人)\\n\\n" + + for i, (user_id, count) in enumerate(users[:20], 1): + text += f"{i}. 用户 `{user_id}` - {count}次交互\\n" + + if len(users) > 20: + text += f"\\n... 还有 {len(users)-20} 个用户" + + text += f"\\n\\n💡 使用 /userinfo <用户ID> 查看详情" + + await update.message.reply_text(text, parse_mode='Markdown') + self.logger.info(f"[管理员] 查看用户列表") + + async def handle_userinfo(self, update, context): + """处理 /userinfo 命令 - 查看指定用户信息""" + user = update.effective_user + if user.id != ADMIN_ID: + return + + if not context.args: + await update.message.reply_text( + "用法:/userinfo <用户ID>\\n" + "示例:/userinfo 123456789" + ) + return + + try: + target_user_id = int(context.args[0]) + info = self._get_user_info(target_user_id) + + if not info: + await update.message.reply_text(f"未找到用户 {target_user_id} 的记录") + return + + text = ( + f"👤 **用户信息** (ID: `{target_user_id}`)\\n\\n" + f"📊 交互次数:{info['interactions']}\\n" + f"🔍 搜索次数:{info['searches']}\\n" + f"💬 AI对话次数:{info['ai_chats']}\\n" + f"⏰ 首次访问:{info['first_seen']}\\n" + f"🕐 最后活跃:{info['last_seen']}\\n" + ) + + await update.message.reply_text(text, parse_mode='Markdown') + + except ValueError: + await update.message.reply_text("❌ 用户ID格式错误,请输入数字") + except Exception as e: + await update.message.reply_text(f"❌ 查询失败:{str(e)}") + self.logger.error(f"[管理员] 查询用户信息失败: {e}") + + async def handle_logs(self, update, context): + """处理 /logs 命令 - 查看最近日志""" + user = update.effective_user + if user.id != ADMIN_ID: + return + + lines = context.args[0] if context.args else "50" + try: + num_lines = int(lines) + log_file = "./logs/integrated_bot_detailed.log" + + if os.path.exists(log_file): + with open(log_file, 'r') as f: + all_lines = f.readlines() + recent_lines = all_lines[-num_lines:] + + log_text = ''.join(recent_lines) + + # Telegram消息限制4096字符 + if len(log_text) > 4000: + log_text = log_text[-4000:] + + await update.message.reply_text( + f"📋 **最近 {num_lines} 行日志:**\\n\\n```\\n{log_text}\\n```", + parse_mode='Markdown' + ) + else: + await update.message.reply_text("❌ 日志文件不存在") + + except Exception as e: + await update.message.reply_text(f"❌ 读取日志失败:{str(e)}") + + async def handle_cache(self, update, context): + """处理 /cache 命令 - 查看缓存详情""" + user = update.effective_user + if user.id != ADMIN_ID: + return + + stats = self._get_cache_details() + + text = ( + "💾 **缓存详细信息**\\n\\n" + f"📦 总记录数:{stats['total']}\\n" + f"✅ 有效记录:{stats['valid']}\\n" + f"❌ 过期记录:{stats['expired']}\\n" + f"📈 命中率:{stats['hit_rate']:.1f}%\\n" + f"💿 数据库大小:{stats['db_size_mb']:.2f} MB\\n\\n" + "🔝 **热门搜索:**\\n" + ) + + for item in stats['top_searches'][:10]: + text += f"• {item['command']} {item['keyword']} ({item['count']}次)\\n" + + await update.message.reply_text(text, parse_mode='Markdown') + + def _get_unique_users_from_logs(self): + """从日志中提取唯一用户数""" + try: + log_file = "./logs/integrated_bot_detailed.log" + if not os.path.exists(log_file): + return 0 + + user_ids = set() + with open(log_file, 'r') as f: + for line in f: + if '用户' in line: + import re + matches = re.findall(r'用户 (\\d+)', line) + user_ids.update(matches) + + return len(user_ids) + except: + return 0 + + def _get_users_from_logs(self): + """从日志中获取用户列表及交互次数""" + try: + log_file = "./logs/integrated_bot_detailed.log" + if not os.path.exists(log_file): + return [] + + user_counts = {} + with open(log_file, 'r') as f: + for line in f: + if '用户' in line: + import re + matches = re.findall(r'用户 (\\d+)', line) + for user_id in matches: + user_counts[user_id] = user_counts.get(user_id, 0) + 1 + + return sorted(user_counts.items(), key=lambda x: x[1], reverse=True) + except: + return [] + + def _get_user_info(self, user_id): + """获取指定用户的详细信息""" + try: + log_file = "./logs/integrated_bot_detailed.log" + if not os.path.exists(log_file): + return None + + info = { + 'interactions': 0, + 'searches': 0, + 'ai_chats': 0, + 'first_seen': None, + 'last_seen': None + } + + with open(log_file, 'r') as f: + for line in f: + if f'用户 {user_id}' in line: + info['interactions'] += 1 + + # 提取时间戳 + import re + time_match = re.search(r'(\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2})', line) + if time_match: + timestamp = time_match.group(1) + if not info['first_seen']: + info['first_seen'] = timestamp + info['last_seen'] = timestamp + + if '镜像' in line or '搜索' in line: + info['searches'] += 1 + if 'AI对话' in line or 'Claude API' in line: + info['ai_chats'] += 1 + + return info if info['interactions'] > 0 else None + except: + return None + + def _get_cache_stats(self): + """获取缓存统计""" + try: + db_path = "/home/atai/bot_data/cache.db" + if not os.path.exists(db_path): + return {'total': 0, 'hit_rate': 0} + + conn = sqlite3.connect(db_path) + cursor = conn.cursor() + + # 总记录数 + cursor.execute("SELECT COUNT(*) FROM search_cache") + total = cursor.fetchone()[0] + + # 访问统计 + cursor.execute("SELECT SUM(access_count) FROM search_cache") + total_accesses = cursor.fetchone()[0] or 0 + + conn.close() + + hit_rate = (total_accesses / (total_accesses + total) * 100) if total > 0 else 0 + + return { + 'total': total, + 'hit_rate': hit_rate + } + except: + return {'total': 0, 'hit_rate': 0} + + def _get_cache_details(self): + """获取缓存详细统计""" + try: + db_path = "/home/atai/bot_data/cache.db" + if not os.path.exists(db_path): + return {'total': 0, 'valid': 0, 'expired': 0, 'hit_rate': 0, 'db_size_mb': 0, 'top_searches': []} + + conn = sqlite3.connect(db_path) + cursor = conn.cursor() + + # 总记录数 + cursor.execute("SELECT COUNT(*) FROM search_cache") + total = cursor.fetchone()[0] + + # 有效记录数 + cursor.execute("SELECT COUNT(*) FROM search_cache WHERE expires_at IS NULL OR expires_at > ?", + (datetime.now().isoformat(),)) + valid = cursor.fetchone()[0] + + # 访问统计 + cursor.execute("SELECT SUM(access_count) FROM search_cache") + total_accesses = cursor.fetchone()[0] or 0 + + # 热门搜索 + cursor.execute(""" + SELECT command, keyword, access_count + FROM search_cache + ORDER BY access_count DESC + LIMIT 10 + """) + top_searches = [ + {'command': row[0], 'keyword': row[1], 'count': row[2]} + for row in cursor.fetchall() + ] + + conn.close() + + # 数据库大小 + db_size_mb = os.path.getsize(db_path) / 1024 / 1024 + + hit_rate = (total_accesses / (total_accesses + total) * 100) if total > 0 else 0 + + return { + 'total': total, + 'valid': valid, + 'expired': total - valid, + 'hit_rate': hit_rate, + 'db_size_mb': db_size_mb, + 'top_searches': top_searches + } + except Exception as e: + print(f"Error: {e}") + return {'total': 0, 'valid': 0, 'expired': 0, 'hit_rate': 0, 'db_size_mb': 0, 'top_searches': []} diff --git a/admin_panel.sh b/admin_panel.sh new file mode 100755 index 0000000..3061f90 --- /dev/null +++ b/admin_panel.sh @@ -0,0 +1,97 @@ +#!/bin/bash +# 管理员后台面板脚本 + +BOLD="\\033[1m" +RESET="\\033[0m" +GREEN="\\033[32m" +BLUE="\\033[34m" +YELLOW="\\033[33m" + +echo "==========================================" +echo "${BOLD}📊 @ktfund_bot 管理员后台${RESET}" +echo "==========================================" +echo "" + +# 1. 统计信息 +echo "${BOLD}${BLUE}📈 统计信息${RESET}" +echo "----------------------------------------" + +# 用户统计 +user_count=$(grep -oE "用户 [0-9]+" logs/integrated_bot_detailed.log 2>/dev/null | grep -oE "[0-9]+" | sort -u | wc -l) +echo "👥 总访问用户数: ${GREEN}${user_count}${RESET}" + +# 搜索次数 +search_count=$(grep -c "镜像.*已转发" logs/*.log 2>/dev/null) +echo "🔍 搜索执行次数: ${search_count}" + +# AI对话次数 +ai_count=$(grep -c "AI对话" logs/*.log 2>/dev/null) +echo "💬 AI对话次数: ${ai_count}" + +# 运行时长 +bot_pid=$(pgrep -f "integrated_bot_ai.py" | head -1) +if [ -n "$bot_pid" ]; then + uptime=$(ps -p $bot_pid -o etime= | tr -d ) + echo "⏱ 运行时长: ${GREEN}${uptime}${RESET}" +else + echo "❌ 机器人未运行" +fi + +echo "" + +# 2. 用户列表 +echo "${BOLD}${BLUE}👥 访问用户列表 (Top 10)${RESET}" +echo "----------------------------------------" +grep -oE "用户 [0-9]+" logs/integrated_bot_detailed.log 2>/dev/null | \ + grep -oE "[0-9]+" | sort | uniq -c | sort -rn | head -10 | \ + awk {printf %2d. 用户 %s - %d次交互\n, NR, , } +echo "" + +# 3. 最近活动 +echo "${BOLD}${BLUE}📋 最近用户活动 (最后10条)${RESET}" +echo "----------------------------------------" +grep "用户 [0-9]" logs/integrated_bot_detailed.log 2>/dev/null | tail -10 | \ + sed "s/^/ /" +echo "" + +# 4. 缓存统计 +echo "${BOLD}${BLUE}💾 缓存统计${RESET}" +echo "----------------------------------------" +if [ -f "/home/atai/bot_data/cache.db" ]; then + cache_count=$(sqlite3 /home/atai/bot_data/cache.db "SELECT COUNT(*) FROM search_cache;" 2>/dev/null) + cache_size=$(du -h /home/atai/bot_data/cache.db | cut -f1) + echo "📦 缓存记录数: ${cache_count}" + echo "💿 数据库大小: ${cache_size}" + + echo "" + echo "🔝 热门搜索 (Top 5):" + sqlite3 /home/atai/bot_data/cache.db " + SELECT command, keyword, access_count + FROM search_cache + ORDER BY access_count DESC + LIMIT 5; + " 2>/dev/null | awk -F"|" {printf • %s %s (%d次)\n, , , } +else + echo "❌ 缓存数据库不存在" +fi + +echo "" +echo "==========================================" +echo "💡 命令说明:" +echo " ./admin_panel.sh - 查看完整面板" +echo " ./admin_panel.sh users - 查看所有用户" +echo " ./admin_panel.sh logs - 查看实时日志" +echo "==========================================" + +# 交互命令 +if [ "$1" == "users" ]; then + echo "" + echo "${BOLD}完整用户列表:${RESET}" + grep -oE "用户 [0-9]+" logs/integrated_bot_detailed.log 2>/dev/null | \ + grep -oE "[0-9]+" | sort | uniq -c | sort -rn | \ + awk {printf %3d. 用户 %-12s - %d次交互\n, NR, , } +elif [ "$1" == "logs" ]; then + echo "" + echo "${BOLD}实时日志监控 (Ctrl+C 退出):${RESET}" + tail -f logs/integrated_bot_detailed.log 2>/dev/null | grep --line-buffered "用户" +fi diff --git a/admin_查看后台.sh b/admin_查看后台.sh new file mode 100755 index 0000000..b405c61 --- /dev/null +++ b/admin_查看后台.sh @@ -0,0 +1,45 @@ +#!/bin/bash +echo "==========================================" +echo " 📊 @ktfund_bot 管理员后台" +echo "==========================================" +echo "" + +echo "1️⃣ 统计信息" +echo "----------------------------------------" +user_count=$(grep -oE "用户 [0-9]+" logs/integrated_bot_detailed.log 2>/dev/null | grep -oE "[0-9]+" | sort -u | wc -l) +search_count=$(grep -c "镜像.*已转发" logs/integrated_bot_detailed.log 2>/dev/null) +ai_count=$(grep -c "AI对话" logs/integrated_bot_detailed.log 2>/dev/null) +bot_pid=$(pgrep -f "integrated_bot_ai.py") + +echo "👥 总访问用户数: $user_count" +echo "🔍 搜索执行次数: $search_count" +echo "💬 AI对话次数: $ai_count" + +if [ -n "$bot_pid" ]; then + uptime=$(ps -p $bot_pid -o etime= | tr -d " ") + echo "⏱ 运行时长: $uptime" +else + echo "❌ 机器人未运行" +fi +echo "" + +echo "2️⃣ 访问用户列表 (Top 10)" +echo "----------------------------------------" +grep -oE "用户 [0-9]+" logs/integrated_bot_detailed.log 2>/dev/null | grep -oE "[0-9]+" | sort | uniq -c | sort -rn | head -10 | awk "{printf \"%2d. 用户 %s - %d次交互\n\", NR, \$2, \$1}" +echo "" + +echo "3️⃣ 最近用户活动 (最后5条)" +echo "----------------------------------------" +grep "用户 [0-9]" logs/integrated_bot_detailed.log 2>/dev/null | tail -5 | sed "s/^/ /" +echo "" + +echo "4️⃣ 缓存统计" +echo "----------------------------------------" +if [ -f "/home/atai/bot_data/cache.db" ]; then + cache_count=$(sqlite3 /home/atai/bot_data/cache.db "SELECT COUNT(*) FROM search_cache;" 2>/dev/null || echo "0") + cache_size=$(du -h /home/atai/bot_data/cache.db 2>/dev/null | cut -f1 || echo "0") + echo "📦 缓存记录数: $cache_count" + echo "💿 数据库大小: $cache_size" +fi +echo "" +echo "==========================================" diff --git a/agent_bot.py b/agent_bot.py new file mode 100644 index 0000000..df76e1f --- /dev/null +++ b/agent_bot.py @@ -0,0 +1,403 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Agent模式Telegram Bot - 使用Anthropic SDK实现工具调用和决策循环 +100% 虚拟机运行,使用Sonnet 4.5 +""" + +import os +import json +import logging +import asyncio +from datetime import datetime +from typing import Dict, List, Any, Optional +from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup +from telegram.ext import Application, CommandHandler, MessageHandler, CallbackQueryHandler, filters, ContextTypes +import anthropic +from pyrogram import Client + +# 日志配置 +logging.basicConfig( + format='%(asctime)s - %(levelname)s - %(message)s', + level=logging.INFO, + handlers=[ + logging.FileHandler('agent_bot.log'), + logging.StreamHandler() + ] +) +logger = logging.getLogger(__name__) + +# ===== 配置 ===== +TELEGRAM_TOKEN = "8426529617:AAHAxzohSMFBAxInzbAVJsZfkB5bHnOyFC4" +SEARCH_BOT_USERNAME = "openaiw_bot" + +# Claude API配置 +try: + CLAUDE_CLIENT = anthropic.Anthropic( + auth_token=os.environ.get('ANTHROPIC_AUTH_TOKEN'), + base_url=os.environ.get('ANTHROPIC_BASE_URL', 'https://api.anthropic.com') + ) + logger.info("✅ Claude Agent客户端初始化成功") +except Exception as e: + logger.error(f"❌ Claude客户端初始化失败: {e}") + CLAUDE_CLIENT = None + +# ===== 工具定义 ===== +TOOLS = [ + { + "name": "search_telegram_groups", + "description": "在Telegram中搜索群组。当用户想要查找群组、频道或者需要搜索特定关键词时使用此工具。", + "input_schema": { + "type": "object", + "properties": { + "keyword": { + "type": "string", + "description": "搜索关键词,例如 'AI'、'翻译'、'编程' 等" + }, + "search_type": { + "type": "string", + "enum": ["groups", "text", "human", "topchat"], + "description": "搜索类型:groups=群组名称,text=讨论内容,human=用户,topchat=热门分类", + "default": "groups" + } + }, + "required": ["keyword"] + } + }, + { + "name": "get_cached_results", + "description": "从数据库获取已缓存的搜索结果。用于快速返回之前搜索过的内容。", + "input_schema": { + "type": "object", + "properties": { + "command": { + "type": "string", + "description": "搜索命令,如 'search'、'text' 等" + }, + "keyword": { + "type": "string", + "description": "搜索关键词" + } + }, + "required": ["command", "keyword"] + } + } +] + +# ===== Agent决策引擎 ===== +class ClaudeAgent: + """Claude Agent - 带工具调用和决策循环""" + + def __init__(self): + self.client = CLAUDE_CLIENT + self.model = "claude-sonnet-4-5-20250929" + self.max_tokens = 2048 + self.conversations: Dict[int, List[Dict]] = {} # 用户对话历史 + self.max_history = 10 + logger.info("✅ Claude Agent引擎初始化完成") + + def get_history(self, user_id: int) -> List[Dict]: + """获取用户对话历史""" + if user_id not in self.conversations: + self.conversations[user_id] = [] + return self.conversations[user_id][-self.max_history:] + + def add_to_history(self, user_id: int, role: str, content: Any): + """添加到对话历史""" + if user_id not in self.conversations: + self.conversations[user_id] = [] + self.conversations[user_id].append({"role": role, "content": content}) + + async def think_and_act(self, user_id: int, user_message: str) -> Dict[str, Any]: + """ + 决策循环:思考 -> 选择工具 -> 执行 -> 返回结果 + + 返回: + { + "response": "AI回复文本", + "tools_used": [{"name": "tool_name", "input": {...}, "result": ...}], + "buttons": [{"text": "...", "callback_data": "..."}] + } + """ + logger.info(f"[Agent] 用户 {user_id} 发起对话: {user_message}") + + # 构建消息历史 + history = self.get_history(user_id) + messages = history + [{"role": "user", "content": user_message}] + + try: + # 第一轮:调用Claude获取决策 + logger.info(f"[Agent] 调用Claude API(带工具)") + response = self.client.messages.create( + model=self.model, + max_tokens=self.max_tokens, + tools=TOOLS, + messages=messages + ) + + logger.info(f"[Agent] Claude响应类型: {response.stop_reason}") + + # 处理工具调用 + tools_used = [] + final_text = "" + + if response.stop_reason == "tool_use": + # Claude决定使用工具 + logger.info(f"[Agent] Claude决定使用工具") + + # 提取工具调用和文本 + tool_results = [] + for block in response.content: + if block.type == "text": + final_text += block.text + elif block.type == "tool_use": + logger.info(f"[Agent] 工具调用: {block.name} - {block.input}") + + # 执行工具 + tool_result = await self._execute_tool(block.name, block.input) + tools_used.append({ + "name": block.name, + "input": block.input, + "result": tool_result + }) + + # 准备工具结果给Claude + tool_results.append({ + "type": "tool_result", + "tool_use_id": block.id, + "content": json.dumps(tool_result, ensure_ascii=False) + }) + + # 第二轮:将工具结果返回给Claude + if tool_results: + logger.info(f"[Agent] 将工具结果返回给Claude") + messages.append({"role": "assistant", "content": response.content}) + messages.append({"role": "user", "content": tool_results}) + + # 再次调用Claude获取最终回复 + final_response = self.client.messages.create( + model=self.model, + max_tokens=self.max_tokens, + tools=TOOLS, + messages=messages + ) + + # 提取最终文本 + for block in final_response.content: + if block.type == "text": + final_text += block.text + + else: + # 直接回复,无需工具 + for block in response.content: + if block.type == "text": + final_text += block.text + + # 保存对话历史 + self.add_to_history(user_id, "user", user_message) + self.add_to_history(user_id, "assistant", final_text) + + # 提取按钮 + buttons = self._extract_buttons(final_text) + + logger.info(f"[Agent] ✅ 完成决策循环,使用了 {len(tools_used)} 个工具") + + return { + "response": final_text, + "tools_used": tools_used, + "buttons": buttons + } + + except Exception as e: + logger.error(f"[Agent] ❌ 决策失败: {e}") + return { + "response": f"抱歉,我遇到了一些问题:{str(e)}", + "tools_used": [], + "buttons": [] + } + + async def _execute_tool(self, tool_name: str, tool_input: Dict) -> Any: + """执行工具调用""" + logger.info(f"[工具执行] {tool_name}({tool_input})") + + if tool_name == "search_telegram_groups": + keyword = tool_input.get("keyword", "") + search_type = tool_input.get("search_type", "groups") + + # 调用实际搜索(通过Pyrogram镜像) + result = await self._perform_telegram_search(keyword, search_type) + return result + + elif tool_name == "get_cached_results": + command = tool_input.get("command", "") + keyword = tool_input.get("keyword", "") + + # 从数据库获取缓存 + # TODO: 实际连接数据库 + return { + "status": "success", + "cached": True, + "results": [] + } + + return {"status": "unknown_tool"} + + async def _perform_telegram_search(self, keyword: str, search_type: str) -> Dict: + """执行Telegram搜索(镜像openaiw_bot)""" + # TODO: 实际通过Pyrogram发送搜索命令 + logger.info(f"[搜索] 类型={search_type}, 关键词={keyword}") + + # 模拟返回结果 + return { + "status": "success", + "keyword": keyword, + "search_type": search_type, + "results_count": 5, + "message": f"搜索 '{keyword}' 完成" + } + + def _extract_buttons(self, text: str) -> List[Dict[str, str]]: + """从AI回复中提取可点击按钮""" + buttons = [] + + # 提取命令格式:/search xxx, /text xxx + import re + patterns = [ + r'/search\s+(\S+)', + r'/text\s+(\S+)', + r'/human\s+(\S+)', + r'/topchat' + ] + + for pattern in patterns: + matches = re.findall(pattern, text) + for match in matches: + if pattern == r'/topchat': + buttons.append({ + "text": "🔥 热门分类", + "callback_data": "cmd_topchat" + }) + else: + cmd = pattern.split('\\s')[0].replace('/', '') + buttons.append({ + "text": f"🔍 {cmd} {match}", + "callback_data": f"cmd_{cmd}_{match}"[:64] + }) + + return buttons + +# ===== Bot处理器 ===== +class AgentBot: + """Agent模式Telegram Bot""" + + def __init__(self): + self.agent = ClaudeAgent() + self.app = None + logger.info("✅ Agent Bot初始化完成") + + async def start_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """处理 /start 命令""" + user_id = update.effective_user.id + logger.info(f"[命令] 用户 {user_id} 启动Bot") + + welcome = ( + "👋 你好!我是AI Agent Bot\n\n" + "💡 我可以帮你:\n" + "- 🔍 智能搜索Telegram群组\n" + "- 💬 自然语言对话\n" + "- 🤖 自动选择合适的工具\n\n" + "直接告诉我你想做什么吧!" + ) + await update.message.reply_text(welcome) + + async def handle_message(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """处理用户消息 - Agent决策入口""" + user_id = update.effective_user.id + user_message = update.message.text + + logger.info(f"[消息] 用户 {user_id}: {user_message}") + + # 调用Agent决策循环 + result = await self.agent.think_and_act(user_id, user_message) + + # 构建回复 + response_text = result["response"] + buttons = result["buttons"] + tools_used = result["tools_used"] + + # 添加工具使用信息 + if tools_used: + tool_info = "\n\n🔧 使用的工具:\n" + for tool in tools_used: + tool_info += f"- {tool['name']}\n" + response_text += tool_info + + # 发送回复(带按钮) + if buttons: + keyboard = [[InlineKeyboardButton(btn["text"], callback_data=btn["callback_data"])] + for btn in buttons] + reply_markup = InlineKeyboardMarkup(keyboard) + await update.message.reply_text( + response_text, + reply_markup=reply_markup + ) + logger.info(f"[回复] 已发送(带 {len(buttons)} 个按钮)") + else: + await update.message.reply_text(response_text) + logger.info(f"[回复] 已发送") + + async def handle_button(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """处理按钮点击""" + query = update.callback_query + await query.answer() + + callback_data = query.data + logger.info(f"[按钮] 用户点击: {callback_data}") + + # 解析按钮命令 + if callback_data.startswith("cmd_"): + parts = callback_data[4:].split("_") + command = parts[0] + keyword = "_".join(parts[1:]) if len(parts) > 1 else "" + + # 将按钮点击转换为消息,重新进入Agent决策 + user_message = f"/{command} {keyword}".strip() + user_id = query.from_user.id + + logger.info(f"[按钮->命令] 转换为消息: {user_message}") + + result = await self.agent.think_and_act(user_id, user_message) + + await query.message.reply_text(result["response"]) + + def run(self): + """启动Bot""" + logger.info("🚀 启动Agent Bot...") + + # 创建Application + self.app = Application.builder().token(TELEGRAM_TOKEN).build() + + # 注册处理器 + self.app.add_handler(CommandHandler("start", self.start_command)) + self.app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, self.handle_message)) + self.app.add_handler(CallbackQueryHandler(self.handle_button)) + + # 启动轮询 + logger.info("✅ Agent Bot已启动,等待用户消息...") + self.app.run_polling(allowed_updates=Update.ALL_TYPES) + +# ===== 主入口 ===== +if __name__ == "__main__": + logger.info("=" * 60) + logger.info("🤖 Claude Agent Bot - 启动中") + logger.info(f"📅 时间: {datetime.now()}") + logger.info(f"🔑 Auth Token: {os.environ.get('ANTHROPIC_AUTH_TOKEN', 'NOT SET')[:20]}...") + logger.info(f"🌐 Base URL: {os.environ.get('ANTHROPIC_BASE_URL', 'NOT SET')}") + logger.info("=" * 60) + + if not CLAUDE_CLIENT: + logger.error("❌ Claude客户端未初始化,无法启动") + exit(1) + + bot = AgentBot() + bot.run() diff --git a/auto_create_session.exp b/auto_create_session.exp new file mode 100755 index 0000000..2604be0 --- /dev/null +++ b/auto_create_session.exp @@ -0,0 +1,51 @@ +#\!/usr/bin/expect -f + +set timeout 60 +set phone "+66621394851" + +log_user 1 + +puts "\n==========================================" +puts "Pyrogram Session 自动创建" +puts "==========================================\n" + +spawn python3 create_session_correct.py + +# 匹配电话号码输入 +expect { + -re "(Enter phone|phone number)" { + puts "\n>>> 自动输入电话号码: $phone" + send "$phone\r" + exp_continue + } + -re "(Is .* correct|确认)" { + puts "\n>>> 确认电话号码" + send "y\r" + exp_continue + } + -re "(code|验证码)" { + puts "\n==========================================" + puts "Telegram 验证码已发送到您的手机!" + puts "==========================================" + + # 交互模式 - 让用户输入验证码 + interact { + -re "\r" { + send "\r" + } + } + } + "Session*成功" { + puts "\n✅ Session 创建成功!" + } + timeout { + puts "\n❌ 超时" + exit 1 + } + eof { + puts "\n完成" + } +} + +# 等待结束 +expect eof diff --git a/auto_restart_on_error.sh b/auto_restart_on_error.sh new file mode 100755 index 0000000..85610b5 --- /dev/null +++ b/auto_restart_on_error.sh @@ -0,0 +1,44 @@ +#\!/bin/bash +# 检测连接错误并自动重启机器人 + +LOG_FILE="logs/auto_restart.log" +ERROR_LOG="logs/integrated_bot_errors.log" +CHECK_MINUTES=10 # 检查最近10分钟的错误 + +log() { + echo "[$(date "+%Y-%m-%d %H:%M:%S")] $1" | tee -a "$LOG_FILE" +} + +# 检查最近的 "Connection lost" 错误 +RECENT_ERRORS=$(tail -200 "$ERROR_LOG" 2>/dev/null | grep -c "Connection lost") + +if [ "$RECENT_ERRORS" -gt 3 ]; then + log "⚠️ 检测到 $RECENT_ERRORS 个连接丢失错误" + + # 检查是否是最近10分钟的错误 + TIMESTAMP=$(date -d "$CHECK_MINUTES minutes ago" "+%Y-%m-%d %H:%M" 2>/dev/null || date -v-${CHECK_MINUTES}M "+%Y-%m-%d %H:%M") + VERY_RECENT=$(tail -100 "$ERROR_LOG" 2>/dev/null | grep "Connection lost" | grep "$TIMESTAMP" | wc -l) + + if [ "$VERY_RECENT" -gt 2 ]; then + log "❌ 检测到最近的连接错误,准备重启..." + + # 重启机器人 + ./manage_bot.sh restart >> "$LOG_FILE" 2>&1 + + log "✅ 机器人已重启" + + # 等待几秒让它初始化 + sleep 10 + + # 验证是否启动成功 + if grep -q "✅ Pyrogram客户端已启动" logs/integrated_bot_detailed.log 2>/dev/null; then + log "✅ Pyrogram 客户端重连成功" + else + log "⚠️ 警告: Pyrogram 客户端状态未知" + fi + else + log "✅ 错误较旧,不需要重启" + fi +else + log "✅ 连接正常,无需操作" +fi diff --git a/auto_session.exp b/auto_session.exp new file mode 100755 index 0000000..86e9cc9 --- /dev/null +++ b/auto_session.exp @@ -0,0 +1,30 @@ +#\!/usr/bin/expect +set timeout 30 + +spawn python3 -c " +from pyrogram import Client +proxy = { + 'scheme': 'socks5', + 'hostname': '127.0.0.1', + 'port': 1080 +} +app = Client('user_session', api_id=24660516, api_hash='eae564578880a59c9963916ff1bbbd3a', proxy=proxy) +app.start() +me = app.get_me() +print(f'Session created for: {me.first_name}') +app.stop() +" + +expect "Enter phone number or bot token:" +send "+66621394851\r" + +expect "Is \"+66621394851\" correct? (y/N):" +send "y\r" + +expect "Enter confirmation code:" +send "77194\r" + +expect "Is \"77194\" correct? (y/N):" +send "y\r" + +expect eof diff --git a/bot_v3.py b/bot_v3.py new file mode 100644 index 0000000..03ab96b --- /dev/null +++ b/bot_v3.py @@ -0,0 +1,875 @@ +#!/usr/bin/env python3 +""" +Telegram Bot V3 - 完整重构版 +特性: +1. 智能AI引导 - 用户说需求,AI分析给出按钮选项 +2. 完整的bytes处理 - 所有callback_data统一用hex存储 +3. 返回重选功能 - 搜索结果可返回重新选择 +4. 缓存与按需翻页 - 兼顾用户体验 +5. 增强日志系统 - 不删档完整记录 +""" + +import asyncio +import logging +import time +import os +import httpx +import anthropic +import json +import re +from typing import Dict, Optional, List +from datetime import datetime, timedelta + +# Pyrogram +from pyrogram import Client as PyrogramClient, filters +from pyrogram.types import Message as PyrogramMessage +from pyrogram.raw.functions.messages import GetBotCallbackAnswer + +# Telegram Bot +from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup +from telegram.ext import Application, CommandHandler, MessageHandler, CallbackQueryHandler, filters as tg_filters, ContextTypes +from telegram.request import HTTPXRequest + +# 数据库 +import sys +sys.path.insert(0, "/home/atai/bot_data") +from database import CacheDatabase + +# 增强日志 +from enhanced_logger import EnhancedLogger + +# ==================== 配置 ==================== +API_ID = 24660516 +API_HASH = "eae564578880a59c9963916ff1bbbd3a" +SESSION_NAME = "user_session" +BOT_TOKEN = os.environ.get("BOT_TOKEN", "8426529617:AAHAxzohSMFBAxInzbAVJsZfkB5bHnOyFC4") +TARGET_BOT = "@openaiw_bot" +ADMIN_ID = 7363537082 + +# 初始化日志 +enhanced_log = EnhancedLogger("bot_v3", log_dir="./logs") +logger = enhanced_log.get_logger() +logger.info("🚀 Bot V3 启动中...") + +# 初始化Claude +try: + claude_client = anthropic.Anthropic( + api_key=os.environ.get('ANTHROPIC_AUTH_TOKEN'), + base_url=os.environ.get('ANTHROPIC_BASE_URL', 'https://api.anthropic.com') + ) + logger.info("✅ Claude API已初始化") +except Exception as e: + logger.error(f"❌ Claude API初始化失败: {e}") + claude_client = None + + +# ==================== 工具函数 ==================== + +def bytes_to_hex(data) -> Optional[str]: + """bytes转hex字符串 - 用于JSON存储""" + if data is None: + return None + if isinstance(data, bytes): + return data.hex() + return str(data) + +def hex_to_bytes(hex_str): + """hex字符串转bytes - 用于恢复callback""" + if hex_str is None: + return None + if isinstance(hex_str, bytes): + return hex_str + try: + return bytes.fromhex(hex_str) + except (ValueError, AttributeError): + return hex_str.encode('utf-8') if isinstance(hex_str, str) else hex_str + + +# ==================== 会话管理器 ==================== + +class SessionManager: + """用户会话管理""" + def __init__(self): + self.sessions: Dict[int, dict] = {} + self.timeout = timedelta(minutes=30) + + def create(self, user_id: int, query: str) -> dict: + """创建会话""" + session = { + "user_id": user_id, + "stage": "initial", + "query": query, + "analysis": None, + "selected": None, + "can_back": False, + "created_at": datetime.now() + } + self.sessions[user_id] = session + logger.info(f"[会话] 创建: user={user_id}") + return session + + def get(self, user_id: int) -> Optional[dict]: + """获取会话""" + session = self.sessions.get(user_id) + if session and datetime.now() - session['created_at'] > self.timeout: + del self.sessions[user_id] + return None + return session + + def update(self, user_id: int, **kwargs): + """更新会话""" + session = self.get(user_id) + if session: + session.update(kwargs) + + def clear(self, user_id: int): + """清除会话""" + if user_id in self.sessions: + del self.sessions[user_id] + + +# ==================== AI分析器 ==================== + +class AIAnalyzer: + """AI意图分析""" + def __init__(self, client): + self.client = client + self.model = "claude-sonnet-4-20250514" + + async def analyze(self, user_input: str) -> dict: + """分析用户意图 - 生成30个相关关键词""" + if not self.client: + return self._fallback(user_input) + + prompt = f"""分析Telegram搜索需求,生成30个相关的关键词。 + +用户输入: "{user_input}" + +要求: +1. 生成30个与用户输入相关的关键词 +2. 关键词要具体、可搜索 +3. 涵盖不同角度和相关话题 +4. 按相关性排序(最相关的在前) + +返回JSON格式: +{{ + "explanation": "1句话说明用户想要什么", + "keywords": [ + "关键词1", + "关键词2", + ...共30个 + ] +}} + +示例: +用户: "德州" +返回: {{"explanation": "德州扑克相关", "keywords": ["德州扑克", "德州扑克俱乐部", "德州扑克教学", ...]}}""" + + try: + response = self.client.messages.create( + model=self.model, + max_tokens=1200, + messages=[{"role": "user", "content": prompt}] + ) + + text = response.content[0].text.strip() + + # 提取JSON + match = re.search(r'```json\s*(.*?)\s*```', text, re.DOTALL) + if match: + text = match.group(1) + + # 尝试找到{} + match = re.search(r'\{.*\}', text, re.DOTALL) + if match: + text = match.group(0) + + result = json.loads(text) + + # 验证 + if 'keywords' in result and isinstance(result['keywords'], list): + logger.info(f"[AI] 分析成功: {len(result['keywords'])}个关键词") + return result + else: + raise ValueError("格式错误") + + except Exception as e: + logger.error(f"[AI] 分析失败: {e}") + return self._fallback(user_input) + + +def _fallback(self, user_input: str) -> dict: + """Fallback - AI失败时生成基础关键词""" + suffixes = [ + "", + "群", + "群聊", + "交流群", + "交流群组", + "俱乐部", + "社群", + "社区", + "论坛", + "讨论组", + "频道", + "频道推荐", + "资源", + "资源分享", + "教程", + "教程分享", + "学习", + "学习群", + "干货", + "工具", + "工具包", + "软件", + "APP", + "推荐", + "最新", + "官方", + "中文", + "免费", + "精品", + "入门" + ] + keywords = [] + seen = set() + for suffix in suffixes: + keyword = f"{user_input}{suffix}".strip() + lower = keyword.lower() + if keyword and lower not in seen: + keywords.append(keyword) + seen.add(lower) + if len(keywords) >= 30: + break + return { + "explanation": f"为「{user_input}」生成的关键词", + "keywords": keywords[:30] + } + + + +class TelegramBotV3: + """主Bot类""" + + def __init__(self): + self.sessions = SessionManager() + self.ai = AIAnalyzer(claude_client) + self.cache_db = None + self.pyrogram_client = None + self.app = None + self.target_bot_id = None + + # Callback映射 + self.callback_map = {} + + # Pyrogram消息映射 + self.pyro_to_tg = {} + self.tg_to_pyro = {} + + # 搜索会话 + self.search_sessions = {} + + async def setup_pyrogram(self) -> bool: + """设置Pyrogram客户端""" + try: + proxy = {"scheme": "socks5", "hostname": "127.0.0.1", "port": 1080} + + self.pyrogram_client = PyrogramClient( + SESSION_NAME, + API_ID, + API_HASH, + workdir="/home/atai/telegram-bot", + proxy=proxy + ) + + await self.pyrogram_client.start() + + # 获取目标bot + target = await self.pyrogram_client.get_users(TARGET_BOT) + self.target_bot_id = target.id + + # 设置消息处理 + @self.pyrogram_client.on_message(filters.user(self.target_bot_id)) + async def handle_bot_message(client, message): + await self.handle_search_response(message) + + logger.info(f"✅ Pyrogram已启动: {TARGET_BOT}") + return True + + except Exception as e: + logger.error(f"❌ Pyrogram失败: {e}") + return False + + async def initialize(self) -> bool: + """初始化""" + try: + logger.info("正在初始化...") + + # 初始化Pyrogram + if not await self.setup_pyrogram(): + return False + + # 初始化缓存 + try: + self.cache_db = CacheDatabase("/home/atai/bot_data/cache.db") + logger.info("✅ 缓存已加载") + except Exception as e: + logger.warning(f"缓存加载失败: {e}") + + + # 初始化Telegram Bot + builder = Application.builder().token(BOT_TOKEN) + + if os.environ.get('ALL_PROXY'): + proxy_url = os.environ.get('ALL_PROXY') + request = HTTPXRequest( + proxy=proxy_url, + connect_timeout=30.0, + read_timeout=30.0 + ) + builder = builder.request(request) + + self.app = builder.build() + + # 注册处理器 + self.app.add_handler(CommandHandler("start", self.handle_start)) + self.app.add_handler(MessageHandler(tg_filters.TEXT & ~tg_filters.COMMAND, self.handle_message)) + self.app.add_handler(CallbackQueryHandler(self.handle_callback)) + + logger.info("✅ 初始化完成") + return True + + except Exception as e: + logger.error(f"❌ 初始化失败: {e}") + return False + + async def handle_start(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """处理/start""" + user = update.effective_user + + welcome = ( + f"👋 您好 {user.first_name}!\n\n" + "我是智能搜索助手,可以帮您找到Telegram群组和频道。\n\n" + "💬 直接告诉我您想找什么,我会为您准备搜索方案!\n\n" + "例如:\n" + "• 我想找德州扑克群\n" + "• 寻找AI工具讨论\n" + "• 科技资讯频道" + ) + + keyboard = [ + [InlineKeyboardButton("🔥 浏览热门分类", callback_data="cmd_topchat")], + [InlineKeyboardButton("📖 使用帮助", callback_data="show_help")] + ] + + await update.message.reply_text(welcome, reply_markup=InlineKeyboardMarkup(keyboard)) + + # 通知管理员 + if user.id != ADMIN_ID: + try: + await context.bot.send_message( + chat_id=ADMIN_ID, + text=f"🆕 新用户: {user.first_name} (@{user.username or '无'}) - {user.id}" + ) + except: + pass + async def handle_message(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """处理用户消息 - 不再提供关键词推荐""" + user = update.effective_user + raw_text = update.message.text or "" + text = raw_text.strip() + + if not text: + await update.message.reply_text("请发送要搜索的内容,例如“德州扑克群”。") + return + + logger.info(f"[用户 {user.id}] 输入: {text}") + + self.sessions.create(user.id, text) + self.sessions.update( + user.id, + selected_keyword=text, + stage="commands", + can_back=False, + analysis=None + ) + + buttons = [ + [InlineKeyboardButton("🔍 按名称搜索 (/search)", callback_data=f"cmd_{user.id}_search")], + [InlineKeyboardButton("💬 按内容搜索 (/text)", callback_data=f"cmd_{user.id}_text")], + [InlineKeyboardButton("👤 按用户搜索 (/human)", callback_data=f"cmd_{user.id}_human")], + [InlineKeyboardButton("📊 查看信息 (/info)", callback_data=f"cmd_{user.id}_info")], + ] + + reply_text = f"收到「{text}」\n\n请选择需要使用的搜索方式,或直接输入具体命令。" + + await update.message.reply_text( + reply_text, + reply_markup=InlineKeyboardMarkup(buttons) + ) + + async def handle_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """处理按钮点击""" + query = update.callback_query + data = query.data + user = query.from_user + + logger.info(f"[回调] user={user.id}, data={data}") + + await query.answer() + + # 【第二级】处理指令选择 - 执行搜索 + if data.startswith("cmd_"): + await self.handle_command_click(query) + return + + # 返回搜索方式 + if data == "back_to_keywords": + await self.handle_back_to_keywords(query) + return + + # 手动输入 + if data == "manual_input": + await query.message.edit_text( + "✍️ 请直接发送命令:\n\n" + "• /search 关键词\n" + "• /text 关键词\n" + "• /human 关键词\n" + "• /topchat" + ) + return + + # 快捷搜索 + if data.startswith("quick_"): + parts = data.split("_", 2) + if len(parts) == 3: + cmd_type = parts[1] + keyword = parts[2] + + await query.message.edit_text(f"🔍 搜索中: {keyword}\n请稍候...") + + try: + await self.pyrogram_client.send_message( + self.target_bot_id, + f"/{cmd_type} {keyword}" + ) + + self.search_sessions[user.id] = { + 'chat_id': query.message.chat_id, + 'wait_msg_id': query.message.message_id, + 'command': f"/{cmd_type}", + 'keyword': keyword, + 'can_back': False + } + except Exception as e: + logger.error(f"[搜索] 失败: {e}") + await query.message.edit_text("❌ 搜索失败") + return + + # 翻页callback + if data.startswith("cb_"): + await self.handle_pagination(query, data) + return + + logger.warning(f"[回调] 未知: {data}") + + async def handle_back_to_keywords(self, query): + """返回搜索选项""" + user = query.from_user + session = self.sessions.get(user.id) + + if not session: + await query.message.edit_text("❌ 会话已过期,请重新输入") + return + + keyword = session.get('selected_keyword') or session.get('query') or "" + + buttons = [ + [InlineKeyboardButton("🔍 按名称搜索 (/search)", callback_data=f"cmd_{user.id}_search")], + [InlineKeyboardButton("💬 按内容搜索 (/text)", callback_data=f"cmd_{user.id}_text")], + [InlineKeyboardButton("👤 按用户搜索 (/human)", callback_data=f"cmd_{user.id}_human")], + [InlineKeyboardButton("📊 查看信息 (/info)", callback_data=f"cmd_{user.id}_info")], + ] + + reply_text = f"当前搜索词:{keyword or '(未指定)'}\n\n请选择需要使用的搜索方式,或直接输入具体命令。" + + await query.message.edit_text( + reply_text, + reply_markup=InlineKeyboardMarkup(buttons) + ) + + session['stage'] = 'commands' + session['can_back'] = False + self.sessions.update(user.id, stage='commands', can_back=False) + + logger.info(f"[用户 {user.id}] 返回搜索方式") + + async def handle_command_click(self, query): + """【第二级】指令点击 - 执行搜索""" + user = query.from_user + data = query.data + + # 解析: cmd_userid_command + parts = data.split("_") + if len(parts) < 3: + return + + command = parts[2] # search/text/human/info + + # 获取会话 + session = self.sessions.get(user.id) + if not session or not session.get('selected_keyword'): + await query.message.edit_text("❌ 会话已过期,请重新输入") + return + + keyword = session['selected_keyword'] + + # 构建完整命令 + full_cmd = f"/{command} {keyword}" + + logger.info(f"[用户 {user.id}] 执行: {full_cmd}") + + # 先检查缓存 + + # 缓存未命中,显示搜索中 + await query.message.edit_text( + f"✅ 执行指令: {full_cmd}\n\n🔍 正在搜索,请稍候...", + parse_mode='HTML' + ) + + # 执行搜索 + try: + await self.pyrogram_client.send_message(self.target_bot_id, full_cmd) + + self.search_sessions[user.id] = { + 'chat_id': query.message.chat_id, + 'wait_msg_id': query.message.message_id, + 'command': f"/{command}", + 'keyword': keyword, + 'can_back': True, + 'last_page': 1, + 'source_msg_id': None, + 'timestamp': datetime.now() + } + + logger.info(f"[搜索] 已转发: {full_cmd}") + + except Exception as e: + logger.error(f"[搜索] 失败: {e}") + await query.message.edit_text("❌ 搜索失败,请重试") + + + + async def handle_search_response(self, message: PyrogramMessage): + """处理服务商返回的搜索结果""" + try: + for user_id, session in list(self.search_sessions.items()): + if datetime.now() - session.get('timestamp', datetime.now()) > timedelta(seconds=10): + continue + + try: + text = message.text.html + except Exception: + text = message.text or message.caption or "" + + keyboard = self.convert_keyboard(message) + + if session.get('can_back') and keyboard: + buttons = list(keyboard.inline_keyboard) + buttons.append([InlineKeyboardButton("🔙 返回搜索方式", callback_data="back_to_keywords")]) + keyboard = InlineKeyboardMarkup(buttons) + + updated_message = None + try: + updated_message = await self.app.bot.edit_message_text( + chat_id=session['chat_id'], + message_id=session['wait_msg_id'], + text=text[:4000], + reply_markup=keyboard, + parse_mode='HTML' + ) + except Exception as edit_error: + logger.warning(f"[搜索响应] 编辑消息失败: {edit_error}") + try: + updated_message = await self.app.bot.send_message( + chat_id=session['chat_id'], + text=text[:4000], + reply_markup=keyboard, + parse_mode='HTML' + ) + except Exception as send_error: + logger.error(f"[搜索响应] 发送消息失败: {send_error}") + continue + + session['message_id'] = updated_message.message_id + session['chat_id'] = updated_message.chat_id + session['wait_msg_id'] = updated_message.message_id + session['source_msg_id'] = message.id + session['last_page'] = 1 + session['can_back'] = True + + self.pyro_to_tg[message.id] = updated_message.message_id + self.tg_to_pyro[updated_message.message_id] = message.id + + if self.cache_db and session.get('keyword'): + buttons_data = self.extract_buttons(message) + self.cache_db.save_cache( + session['command'], + session['keyword'], + 1, + text, + buttons_data + ) + + session['timestamp'] = datetime.now() + self.search_sessions[user_id] = session + break + + except Exception as e: + logger.error(f"[搜索响应] 失败: {e}") + + + async def fetch_updated_message(self, message_id: int, attempts: int = 6, delay: float = 0.7): + for _ in range(attempts): + try: + msg = await self.pyrogram_client.get_messages(self.target_bot_id, message_id) + except Exception as exc: + logger.error(f"[翻页] 获取消息失败: {exc}") + msg = None + if msg and (msg.reply_markup or msg.text or msg.caption): + return msg + await asyncio.sleep(delay) + return None + + + async def handle_pagination(self, query, data): + """处理翻页按钮""" + user = query.from_user + + if data not in self.callback_map: + await query.answer('按钮已过期', show_alert=False) + return + + orig_msg_id, orig_callback = self.callback_map[data] + session = self.search_sessions.get(user.id) + if not session: + await query.answer('会话已过期', show_alert=True) + return + + if isinstance(orig_callback, bytes): + callback_bytes = orig_callback + callback_str = orig_callback.decode('utf-8', 'ignore') + else: + callback_str = str(orig_callback) + callback_bytes = hex_to_bytes(callback_str) + + if orig_msg_id == 0 and session.get('source_msg_id'): + orig_msg_id = session['source_msg_id'] + + page = None + match = re.search(r"page_(\d+)", callback_str or "") + if match: + page = int(match.group(1)) + elif session.get('last_page'): + page = session['last_page'] + 1 + + cached = None + if self.cache_db and session.get('keyword') and page: + cached = self.cache_db.get_cache(session['command'], session['keyword'], page) + + await query.answer('正在加载...', show_alert=False) + + try: + await self.pyrogram_client.invoke( + GetBotCallbackAnswer( + peer=await self.pyrogram_client.resolve_peer(self.target_bot_id), + msg_id=orig_msg_id, + data=callback_bytes + ) + ) + except Exception as e: + logger.error(f"[翻页] 回调失败: {e}") + if cached: + await self._apply_cached_page(query, session, cached, page) + else: + await query.message.edit_text("❌ 翻页失败") + return + + updated_msg = await self.fetch_updated_message(orig_msg_id) + if not updated_msg: + if cached: + await self._apply_cached_page(query, session, cached, page) + return + await query.message.edit_text("❌ 未获取到新内容,请稍后重试") + return + + try: + text = updated_msg.text.html + except Exception: + text = updated_msg.text or updated_msg.caption or "" + + keyboard = self.convert_keyboard(updated_msg) + if session.get('can_back') and keyboard: + buttons = list(keyboard.inline_keyboard) + buttons.append([InlineKeyboardButton("🔙 返回搜索方式", callback_data="back_to_keywords")]) + keyboard = InlineKeyboardMarkup(buttons) + + await query.message.edit_text( + text[:4000], + reply_markup=keyboard, + parse_mode='HTML' + ) + + if self.cache_db and session.get('keyword') and page: + buttons_data = self.extract_buttons(updated_msg) + self.cache_db.save_cache( + session['command'], + session['keyword'], + page, + text, + buttons_data + ) + + if page: + session['last_page'] = page + session['source_msg_id'] = updated_msg.id + session['timestamp'] = datetime.now() + self.search_sessions[user.id] = session + + + async def _apply_cached_page(self, query, session, cached, page): + keyboard = self.rebuild_keyboard(cached.get('buttons', []), session.get('can_back', False)) + await query.message.edit_text( + cached['text'][:4000], + reply_markup=keyboard, + parse_mode='HTML' + ) + if page: + session['last_page'] = page + session['timestamp'] = datetime.now() + self.search_sessions[query.from_user.id] = session + + + def convert_keyboard(self, message: PyrogramMessage): + if not message.reply_markup or not message.reply_markup.inline_keyboard: + return None + try: + buttons = [] + for row in message.reply_markup.inline_keyboard: + button_row = [] + for btn in row: + if btn.url: + button_row.append(InlineKeyboardButton(text=btn.text, url=btn.url)) + elif btn.callback_data: + callback_id = f"cb_{time.time():.0f}_{len(self.callback_map)}" + self.callback_map[callback_id] = (message.id, btn.callback_data) + button_row.append(InlineKeyboardButton(text=btn.text, callback_data=callback_id[:64])) + else: + button_row.append(InlineKeyboardButton(text=btn.text, callback_data="unknown")) + if button_row: + buttons.append(button_row) + return InlineKeyboardMarkup(buttons) if buttons else None + except Exception as e: + logger.error(f"[键盘转换] 失败: {e}") + return None + + + def extract_buttons(self, message: PyrogramMessage) -> list: + if not message.reply_markup or not message.reply_markup.inline_keyboard: + return [] + buttons = [] + for row in message.reply_markup.inline_keyboard: + for btn in row: + btn_data = {"text": btn.text, "msg_id": message.id} + if btn.url: + btn_data["url"] = btn.url + if btn.callback_data: + btn_data["callback_data"] = bytes_to_hex(btn.callback_data) + buttons.append(btn_data) + return buttons + + + def rebuild_keyboard(self, buttons_data: list, can_back: bool = False): + if not buttons_data: + if can_back: + return InlineKeyboardMarkup([[InlineKeyboardButton("🔙 返回搜索方式", callback_data="back_to_keywords")]]) + return None + + session_msg_id = 0 + for btn_data in buttons_data: + if btn_data.get('msg_id'): + session_msg_id = btn_data['msg_id'] + break + + buttons = [] + current_row = [] + for btn_data in buttons_data: + btn = None + if btn_data.get('url'): + btn = InlineKeyboardButton(text=btn_data['text'], url=btn_data['url']) + elif btn_data.get('callback_data'): + callback_id = f"cb_{time.time():.0f}_{len(self.callback_map)}" + callback_bytes = hex_to_bytes(btn_data['callback_data']) + source_msg_id = btn_data.get('msg_id') or session_msg_id + self.callback_map[callback_id] = (source_msg_id, callback_bytes) + btn = InlineKeyboardButton(text=btn_data['text'], callback_data=callback_id[:64]) + if not btn: + continue + current_row.append(btn) + if len(current_row) >= 4: + buttons.append(current_row) + current_row = [] + if current_row: + buttons.append(current_row) + if can_back: + buttons.append([InlineKeyboardButton("🔙 返回搜索方式", callback_data="back_to_keywords")]) + return InlineKeyboardMarkup(buttons) if buttons else None + + + async def run(self): + """运行""" + try: + await self.app.initialize() + await self.app.start() + await self.app.updater.start_polling(drop_pending_updates=True) + + logger.info("=" * 60) + logger.info("✅ Bot V3 已启动") + logger.info("=" * 60) + + await asyncio.Event().wait() + + except KeyboardInterrupt: + logger.info("收到停止信号") + finally: + await self.cleanup() + + + async def cleanup(self): + """清理""" + logger.info("正在清理...") + + if self.app: + await self.app.updater.stop() + await self.app.stop() + await self.app.shutdown() + + if self.pyrogram_client: + await self.pyrogram_client.stop() + + logger.info("✅ 清理完成") + + +async def main(): + """主函数""" + bot = TelegramBotV3() + + if await bot.initialize(): + await bot.run() + else: + logger.error("初始化失败,退出") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/bot_without_mirror.py b/bot_without_mirror.py new file mode 100755 index 0000000..2cdeff6 --- /dev/null +++ b/bot_without_mirror.py @@ -0,0 +1,171 @@ +#!/usr/bin/env python3 +""" +客服机器人 - 简化版(无镜像功能) +只包含消息转发和管理员回复功能 +""" +import asyncio +import logging +from datetime import datetime +from telegram import Update +from telegram.ext import Application, CommandHandler, MessageHandler, filters, ContextTypes + +# 配置 +BOT_TOKEN = "8426529617:AAHAxzohSMFBAxInzbAVJsZfkB5bHnOyFC4" +ADMIN_ID = 7363537082 +ADMIN_USERNAME = "xiaobai_80" + +# 日志配置 +logging.basicConfig( + format="%(asctime)s | %(levelname)-8s | %(message)s", + level=logging.INFO, + datefmt="%Y-%m-%d %H:%M:%S" +) +logger = logging.getLogger(__name__) + +class CustomerServiceBot: + def __init__(self): + self.app = None + self.user_sessions = {} # 存储用户会话 + + async def handle_start(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """处理/start命令""" + user = update.effective_user + welcome_text = ( + f"👋 您好 {user.first_name}!\n\n" + "我是您的智能客服助手\n\n" + "直接发送消息即可联系人工客服\n" + f"技术支持:@{ADMIN_USERNAME}\n\n" + "⚠️ 搜索功能暂时维护中..." + ) + await update.message.reply_text(welcome_text) + + # 通知管理员 + if user.id != ADMIN_ID: + admin_msg = ( + f"🆕 新用户访问:\n" + f"用户: {user.first_name} (@{user.username or '无'})\n" + f"ID: {user.id}" + ) + try: + await context.bot.send_message(ADMIN_ID, admin_msg) + except: + pass + + async def handle_message(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """处理用户消息""" + user = update.effective_user + + # 如果是管理员回复用户 + if user.id == ADMIN_ID: + # 检查是否是回复命令 + text = update.message.text + if text.startswith("/reply "): + await self.handle_reply_command(update, context) + return + elif text == "/list": + await self.handle_list_command(update, context) + return + + # 普通用户消息,转发给管理员 + if user.id != ADMIN_ID: + await self.forward_to_admin(update, context) + + async def forward_to_admin(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """转发消息给管理员""" + user = update.effective_user + msg = update.message.text + + # 保存用户信息 + self.user_sessions[user.id] = { + "username": user.username, + "first_name": user.first_name, + "last_name": user.last_name, + "last_message": datetime.now() + } + + forward_msg = ( + f"📨 用户消息\n" + f"━━━━━━━━━━━━━━━━\n" + f"👤 {user.first_name} {user.last_name or ''}\n" + f"🆔 ID: {user.id}\n" + f"👤 用户名: @{user.username or '无'}\n" + f"💬 内容:\n{msg}\n" + f"━━━━━━━━━━━━━━━━\n" + f"回复: /reply {user.id} 您的消息" + ) + + try: + await context.bot.send_message(ADMIN_ID, forward_msg) + await update.message.reply_text("✅ 消息已发送给客服,请稍候...") + logger.info(f"转发消息 - 用户 {user.id}: {msg[:50]}") + except Exception as e: + logger.error(f"转发失败: {e}") + await update.message.reply_text("❌ 发送失败,请稍后重试") + + async def handle_reply_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """处理管理员回复""" + parts = update.message.text.split(maxsplit=2) + if len(parts) < 3: + await update.message.reply_text("格式: /reply 用户ID 消息") + return + + try: + user_id = int(parts[1]) + reply_msg = parts[2] + + await context.bot.send_message( + user_id, + f"💬 客服回复:\n{reply_msg}" + ) + await update.message.reply_text(f"✅ 已回复用户 {user_id}") + logger.info(f"回复用户 {user_id}: {reply_msg[:50]}") + except Exception as e: + await update.message.reply_text(f"❌ 发送失败: {e}") + + async def handle_list_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """列出最近的用户""" + if not self.user_sessions: + await update.message.reply_text("暂无用户会话") + return + + msg = "📋 最近的用户:\n\n" + for user_id, info in self.user_sessions.items(): + msg += f"ID: {user_id}\n" + msg += f"姓名: {info['first_name']} {info.get('last_name', '')}\\n" + msg += f"用户名: @{info.get('username', '无')}\\n" + msg += f"最后消息: {info['last_message'].strftime('%Y-%m-%d %H:%M:%S')}\\n" + msg += "━━━━━━━━━━\n" + + await update.message.reply_text(msg) + + async def run(self): + """运行机器人""" + try: + logger.info("启动客服机器人...") + + self.app = Application.builder().token(BOT_TOKEN).build() + + # 注册处理器 + self.app.add_handler(CommandHandler("start", self.handle_start)) + self.app.add_handler(CommandHandler("help", self.handle_start)) + self.app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, self.handle_message)) + + logger.info("✅ 机器人已启动!") + logger.info(f"管理员: @{ADMIN_USERNAME} (ID: {ADMIN_ID})") + + await self.app.initialize() + await self.app.start() + await self.app.updater.start_polling() + + while True: + await asyncio.sleep(1) + + except Exception as e: + logger.error(f"运行错误: {e}") + finally: + if self.app: + await self.app.stop() + +if __name__ == "__main__": + bot = CustomerServiceBot() + asyncio.run(bot.run()) diff --git a/check_all.sh b/check_all.sh new file mode 100755 index 0000000..f049530 --- /dev/null +++ b/check_all.sh @@ -0,0 +1,40 @@ +#!/bin/bash +echo "======================================" +echo "虚拟机 Bot 完整状态检查" +echo "======================================" +echo + +echo "1. Bot进程状态:" +/home/atai/telegram-bot/manage_bot.sh status | grep -E "运行中|Detached|未找到" +echo + +echo "2. Screen会话:" +screen -ls | grep agent_bot +echo + +echo "3. 最新活动 (最近5条):" +tail -5 /home/atai/telegram-bot/bot_agent_sdk.log | grep -E "INFO|ERROR" | tail -3 +echo + +echo "4. 数据库记录数:" +python3 << 'PYEOF' +import sqlite3 +conn = sqlite3.connect('/home/atai/bot_data/cache.db') +cursor = conn.cursor() +cursor.execute('SELECT COUNT(*) FROM search_cache') +total = cursor.fetchone()[0] +cursor.execute('SELECT COUNT(DISTINCT command || keyword) FROM search_cache') +unique = cursor.fetchone()[0] +print(f"总记录: {total}, 唯一搜索: {unique}") +conn.close() +PYEOF +echo + +echo "5. 环境变量配置:" +echo "ANTHROPIC_AUTH_TOKEN: ${ANTHROPIC_AUTH_TOKEN:0:20}..." +echo "ANTHROPIC_BASE_URL: $ANTHROPIC_BASE_URL" +echo + +echo "======================================" +echo "✅ 检查完成" +echo "======================================" diff --git a/check_pagination.sh b/check_pagination.sh new file mode 100755 index 0000000..9faf40a --- /dev/null +++ b/check_pagination.sh @@ -0,0 +1,84 @@ +#!/bin/bash +echo "===========================================" +echo "🔍 自动翻页功能检查" +echo "===========================================" +echo "" + +echo "📂 1. 数据库状态" +echo "-------------------------------------------" +if [ -f cache.db ]; then + DB_SIZE=$(du -h cache.db | cut -f1) + echo "✅ 数据库存在: cache.db ($DB_SIZE)" + + # 检查表结构 + echo "" + echo "表结构:" + sqlite3 cache.db '.schema search_cache' 2>/dev/null || echo "⚠️ 无法读取表结构" + + # 统计记录 + echo "" + TOTAL_RECORDS=$(sqlite3 cache.db 'SELECT COUNT(*) FROM search_cache;' 2>/dev/null) + echo "总记录数: $TOTAL_RECORDS" + + if [ "$TOTAL_RECORDS" -gt 0 ]; then + echo "" + echo "📊 缓存统计 (按搜索分组):" + sqlite3 cache.db 'SELECT command, keyword, COUNT(*) as pages, MAX(page) as max_page FROM search_cache GROUP BY command, keyword;' 2>/dev/null + else + echo "⚠️ 数据库为空,还没有搜索记录" + fi +else + echo "❌ 数据库不存在" +fi + +echo "" +echo "📝 2. 日志中的翻页记录" +echo "-------------------------------------------" +PAGINATION_LOGS=$(grep -c '\[翻页\]' bot_agent_sdk.log 2>/dev/null) +if [ "$PAGINATION_LOGS" -gt 0 ]; then + echo "✅ 找到 $PAGINATION_LOGS 条翻页日志" + echo "" + echo "最近的翻页活动:" + grep '\[翻页\]' bot_agent_sdk.log 2>/dev/null | tail -10 +else + echo "⚠️ 还没有翻页活动记录" +fi + +echo "" +echo "🔧 3. 代码检查" +echo "-------------------------------------------" +if grep -q 'class AutoPaginationManager' integrated_bot_ai.py; then + echo "✅ AutoPaginationManager 类存在" +fi +if grep -q 'async def _paginate' integrated_bot_ai.py; then + echo "✅ _paginate 方法存在" +fi +if grep -q 'start_pagination' integrated_bot_ai.py; then + echo "✅ start_pagination 方法存在" +fi +if grep -q '_has_next' integrated_bot_ai.py; then + echo "✅ _has_next 按钮检测方法存在" +fi +if grep -q '_click_next' integrated_bot_ai.py; then + echo "✅ _click_next 点击方法存在" +fi + +echo "" +echo "===========================================" +echo "📝 总结" +echo "===========================================" + +if [ "$TOTAL_RECORDS" -gt 0 ]; then + echo "✅ 翻页功能正常,已保存 $TOTAL_RECORDS 条记录" +elif [ "$PAGINATION_LOGS" -gt 0 ]; then + echo "⚠️ 翻页功能运行过,但数据库可能已清空" +else + echo "ℹ️ 翻页功能已配置,等待用户触发搜索" +fi + +echo "" +echo "💡 触发方法:" +echo " 1. 向 @ktfund_bot 发送消息" +echo " 2. 点击AI回复的搜索按钮" +echo " 3. 或直接发送 /search 关键词" +echo "===========================================" diff --git a/claude_agent_wrapper.backup.20251007_171621.py b/claude_agent_wrapper.backup.20251007_171621.py new file mode 100644 index 0000000..21618ca --- /dev/null +++ b/claude_agent_wrapper.backup.20251007_171621.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 +""" +Claude Agent SDK 包装器 V2 +简化版本,避免断开连接时的异步问题 +""" + +import os +import asyncio +import re +from claude_agent_sdk import ClaudeSDKClient +import logging + +logger = logging.getLogger(__name__) + +class ClaudeAgentWrapper: + """Claude Agent SDK 的同步包装器""" + + def __init__(self): + self._env_set = False + + def _ensure_env(self): + """确保环境变量已设置""" + if not self._env_set: + if not os.environ.get('ANTHROPIC_AUTH_TOKEN'): + raise Exception("ANTHROPIC_AUTH_TOKEN not set") + if not os.environ.get('ANTHROPIC_BASE_URL'): + logger.warning("ANTHROPIC_BASE_URL not set, using default") + self._env_set = True + + async def _async_chat(self, messages: list) -> str: + """异步聊天(每次创建新连接)""" + self._ensure_env() + + client = None + try: + # 创建新客户端 + client = ClaudeSDKClient() + await client.connect() + + # 构建提示词 + prompt_parts = [] + for msg in messages: + role = msg.get('role', 'user') + content = msg.get('content', '') + if role == 'user': + prompt_parts.append(f"User: {content}") + else: + prompt_parts.append(f"Assistant: {content}") + + full_prompt = "\n\n".join(prompt_parts) + + # 发送查询 + await client.query(full_prompt) + + # 接收响应 + response_text = '' + async for chunk in client.receive_response(): + chunk_str = str(chunk) + if 'AssistantMessage' in chunk_str: + # 提取文本内容 + match = re.search(r"text='([^']*)'", chunk_str) + if match: + response_text += match.group(1) + if not match: + match = re.search(r'text="([^"]*)"', chunk_str) + if match: + response_text += match.group(1) + + return response_text.strip() + + except Exception as e: + logger.error(f"Chat error: {e}") + raise + finally: + # 不主动断开,让客户端自然关闭 + if client: + try: + await client.disconnect() + except: + pass # 忽略断开连接时的错误 + + def chat(self, messages: list, model: str = "claude-sonnet-4-20250514", + max_tokens: int = 512, temperature: float = 0.7) -> str: + """同步聊天接口(兼容原 Anthropic SDK 格式)""" + try: + # 每次都创建新的事件循环 + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + try: + response_text = loop.run_until_complete(self._async_chat(messages)) + return response_text + finally: + loop.close() + except Exception as e: + logger.error(f"Chat failed: {e}") + raise + +# 创建全局客户端实例 +claude_agent_client = None + +def init_claude_agent(): + """初始化 Claude Agent SDK 客户端""" + global claude_agent_client + + # 确保环境变量已设置 + if not os.environ.get('ANTHROPIC_AUTH_TOKEN'): + logger.error("ANTHROPIC_AUTH_TOKEN not set") + return None + + try: + claude_agent_client = ClaudeAgentWrapper() + logger.info("✅ Claude Agent SDK wrapper initialized") + return claude_agent_client + except Exception as e: + logger.error(f"❌ Init failed: {e}") + return None diff --git a/claude_agent_wrapper.py b/claude_agent_wrapper.py new file mode 100644 index 0000000..bca075b --- /dev/null +++ b/claude_agent_wrapper.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python3 +""" +Claude Agent SDK 包装器 V3 +修复事件循环冲突问题 - 在async环境中直接await +""" + +import os +import asyncio +import re +from claude_agent_sdk import ClaudeSDKClient +import logging + +logger = logging.getLogger(__name__) + +class ClaudeAgentWrapper: + """Claude Agent SDK 的包装器 - 支持async环境""" + + def __init__(self): + self._env_set = False + + def _ensure_env(self): + """确保环境变量已设置""" + if not self._env_set: + if not os.environ.get('ANTHROPIC_AUTH_TOKEN'): + raise Exception("ANTHROPIC_AUTH_TOKEN not set") + if not os.environ.get('ANTHROPIC_BASE_URL'): + logger.warning("ANTHROPIC_BASE_URL not set, using default") + self._env_set = True + + async def _async_chat(self, messages: list) -> str: + """异步聊天(每次创建新连接)""" + self._ensure_env() + + client = None + try: + # 创建新客户端 + client = ClaudeSDKClient() + await client.connect() + + # 构建提示词 + prompt_parts = [] + for msg in messages: + role = msg.get('role', 'user') + content = msg.get('content', '') + if role == 'user': + prompt_parts.append(f"User: {content}") + else: + prompt_parts.append(f"Assistant: {content}") + + full_prompt = "\n\n".join(prompt_parts) + + # 发送查询 + await client.query(full_prompt) + + # 接收响应 + response_text = '' + async for chunk in client.receive_response(): + chunk_str = str(chunk) + if 'AssistantMessage' in chunk_str: + # 提取文本内容 + match = re.search(r"text='([^']*)'", chunk_str) + if match: + response_text += match.group(1) + if not match: + match = re.search(r'text="([^"]*)"', chunk_str) + if match: + response_text += match.group(1) + + return response_text.strip() + + except Exception as e: + logger.error(f"Chat error: {e}") + raise + finally: + # 不主动断开,让客户端自然关闭 + if client: + try: + await client.disconnect() + except: + pass # 忽略断开连接时的错误 + + async def chat_async(self, messages: list, model: str = "claude-sonnet-4-20250514", + max_tokens: int = 512, temperature: float = 0.7) -> str: + """异步聊天接口(在async环境中使用)""" + return await self._async_chat(messages) + + def chat(self, messages: list, model: str = "claude-sonnet-4-20250514", + max_tokens: int = 512, temperature: float = 0.7) -> str: + """同步聊天接口(兼容原 Anthropic SDK 格式)""" + try: + # 尝试获取当前事件循环 + try: + loop = asyncio.get_running_loop() + # 如果已经在事件循环中,不能使用 run_until_complete + # 需要使用 asyncio.create_task 或直接 await + logger.error("Cannot use sync chat() in async context, use chat_async() instead") + raise RuntimeError("Use chat_async() in async context") + except RuntimeError: + # 没有运行中的事件循环,创建新的 + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + try: + response_text = loop.run_until_complete(self._async_chat(messages)) + return response_text + finally: + loop.close() + except Exception as e: + logger.error(f"Chat failed: {e}") + raise + +# 创建全局客户端实例 +claude_agent_client = None + +def init_claude_agent(): + """初始化 Claude Agent SDK 客户端""" + global claude_agent_client + + # 确保环境变量已设置 + if not os.environ.get('ANTHROPIC_AUTH_TOKEN'): + logger.error("ANTHROPIC_AUTH_TOKEN not set") + return None + + try: + claude_agent_client = ClaudeAgentWrapper() + logger.info("✅ Claude Agent SDK wrapper initialized") + return claude_agent_client + except Exception as e: + logger.error(f"❌ Init failed: {e}") + return None diff --git a/create_final_session.py b/create_final_session.py new file mode 100755 index 0000000..044c8e8 --- /dev/null +++ b/create_final_session.py @@ -0,0 +1,25 @@ +#\!/usr/bin/env python3 +from pyrogram import Client + +API_ID = 24660516 +API_HASH = "eae564578880a59c9963916ff1bbbd3a" + +proxy = { + "scheme": "socks5", + "hostname": "127.0.0.1", + "port": 1080 +} + +app = Client( + "user_session", + api_id=API_ID, + api_hash=API_HASH, + proxy=proxy +) + +app.start() +me = app.get_me() +print(f"✅ Session创建成功!") +print(f"账号: {me.first_name}") +print(f"ID: {me.id}") +app.stop() diff --git a/create_session.exp b/create_session.exp new file mode 100755 index 0000000..0af29aa --- /dev/null +++ b/create_session.exp @@ -0,0 +1,74 @@ +#!/usr/bin/expect -f +# Expect script to automate Pyrogram session creation + +set timeout 120 +set phone_number "+66621394851" + +log_user 1 + +spawn python3 create_session_correct.py + +# 等待电话号码提示 +expect { + "Enter phone number*" { + send "$phone_number\r" + } + timeout { + puts "\n超时:未收到电话号码提示" + exit 1 + } +} + +# 等待确认提示 +expect { + "*correct*" { + send "y\r" + } + timeout { + puts "\n超时:未收到确认提示" + exit 1 + } +} + +# 等待验证码提示 +puts "\n\n\033\[1;33m================================================\033\[0m" +puts "\033\[1;33m请查看您的 Telegram 应用,输入收到的验证码:\033\[0m" +puts "\033\[1;33m================================================\033\[0m" + +expect "Enter*code*" { + # 从标准输入读取验证码 + exec stty -echo + expect_user -re "(.*)\n" + set code $expect_out(1,string) + exec stty echo + + send "$code\r" +} + +# 等待验证码确认 +expect { + "*correct*" { + send "y\r" + } + "*Invalid*" { + puts "\n验证码错误,请重试" + exit 1 + } +} + +# 等待完成 +expect { + "Session*成功*" { + puts "\n\n✅ Session 创建成功!" + } + "错误*" { + puts "\n❌ 创建失败" + exit 1 + } + timeout { + puts "\n超时" + exit 1 + } +} + +expect eof diff --git a/create_session.py b/create_session.py new file mode 100644 index 0000000..d9dc75f --- /dev/null +++ b/create_session.py @@ -0,0 +1,16 @@ +from pyrogram import Client + +API_ID = 24660516 +API_HASH = "eae564578880a59c9963916ff1bbbd3a" + +app = Client( + "user_session", + api_id=API_ID, + api_hash=API_HASH +) + +print("开始创建session...") +app.start() +me = app.get_me() +print(f"✅ Session创建成功!用户: {me.first_name}") +app.stop() diff --git a/create_session_correct.py b/create_session_correct.py new file mode 100755 index 0000000..062a5d0 --- /dev/null +++ b/create_session_correct.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +""" +创建 Pyrogram Session - 使用代理 +执行此脚本后,按照提示输入手机号码和验证码 +""" +import sys +from pyrogram import Client + +API_ID = 24660516 +API_HASH = "eae564578880a59c9963916ff1bbbd3a" + +# 使用代理配置 +proxy = { + "scheme": "socks5", + "hostname": "127.0.0.1", + "port": 1080 +} + +print("=" * 60) +print("Pyrogram Session 创建工具") +print("=" * 60) +print(f"API ID: {API_ID}") +print(f"代理: SOCKS5://127.0.0.1:1080") +print("Session 文件: user_session.session") +print("=" * 60) +print("\n准备连接到 Telegram...") +print("请准备好您的:") +print(" 1. 手机号码(国际格式,例如:+66621394851)") +print(" 2. Telegram 验证码(将发送到您的手机)") +print("=" * 60) +print() + +try: + app = Client( + "user_session", + api_id=API_ID, + api_hash=API_HASH, + proxy=proxy + ) + + print("正在启动客户端...") + app.start() + + print("\n正在获取账户信息...") + me = app.get_me() + + print("\n" + "=" * 60) + print("✅ Session 创建成功!") + print("=" * 60) + print(f"账户名称: {me.first_name} {me.last_name or ''}") + print(f"用户名: @{me.username or 'N/A'}") + print(f"用户 ID: {me.id}") + print(f"手机号: {me.phone_number or 'N/A'}") + print("=" * 60) + print(f"\nSession 文件已创建: user_session.session") + print("可以开始使用镜像功能了!") + print("=" * 60) + + app.stop() + print("\n客户端已关闭") + +except KeyboardInterrupt: + print("\n\n操作已取消") + sys.exit(1) +except Exception as e: + print(f"\n❌ 错误: {e}") + print("\n可能的原因:") + print(" 1. 代理不可用 - 请检查 SOCKS5 代理是否运行") + print(" 2. 网络连接问题") + print(" 3. API 凭证无效") + print(" 4. 手机号或验证码错误") + sys.exit(1) diff --git a/create_session_manual.py b/create_session_manual.py new file mode 100755 index 0000000..d2e0a37 --- /dev/null +++ b/create_session_manual.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +""" +手动创建Session - 避开验证码限制 +""" +from pyrogram import Client +import asyncio + +API_ID = 24660516 +API_HASH = "eae564578880a59c9963916ff1bbbd3a" + +# 不使用代理,避免IP被限制 +app = Client( + "user_session_new", # 使用新的session名称 + api_id=API_ID, + api_hash=API_HASH, + # 不使用代理,直接连接 +) + +print("=" * 50) +print("创建新的Session文件") +print("=" * 50) +print("\n重要提示:") +print("1. 使用新的session名称: user_session_new") +print("2. 直接连接Telegram(不通过代理)") +print("3. 如果还是被限制,请等待几小时后再试") +print("=" * 50) + +try: + app.start() + me = app.get_me() + print(f"\n✅ Session创建成功!") + print(f"账号: {me.first_name}") + print(f"ID: {me.id}") + print(f"Session文件: user_session_new.session") + app.stop() +except Exception as e: + print(f"\n❌ 错误: {e}") + if "FLOOD_WAIT" in str(e): + print("还在限制期内,请稍后再试") diff --git a/create_session_now.py b/create_session_now.py new file mode 100644 index 0000000..dd1d775 --- /dev/null +++ b/create_session_now.py @@ -0,0 +1,30 @@ +from pyrogram import Client + +API_ID = 24660516 +API_HASH = "eae564578880a59c9963916ff1bbbd3a" +CODE = "77194" + +proxy = { + "scheme": "socks5", + "hostname": "127.0.0.1", + "port": 1080 +} + +print("创建session中...") + +app = Client( + "user_session", + api_id=API_ID, + api_hash=API_HASH, + proxy=proxy +) + +app.start() +me = app.get_me() +print(f"✅ Session创建成功!") +print(f"账号: {me.first_name} {me.last_name or ''}") +print(f"用户名: @{me.username if me.username else '无'}") +print(f"ID: {me.id}") +print(f"是否为Bot: {me.is_bot}") +print(f"Session已保存: user_session.session") +app.stop() diff --git a/create_session_proxy.py b/create_session_proxy.py new file mode 100644 index 0000000..dbf56e4 --- /dev/null +++ b/create_session_proxy.py @@ -0,0 +1,25 @@ +from pyrogram import Client + +API_ID = 24660516 +API_HASH = "eae564578880a59c9963916ff1bbbd3a" + +# 添加代理配置 +proxy = { + "scheme": "socks5", + "hostname": "127.0.0.1", + "port": 1080 +} + +app = Client( + "user_session", + api_id=API_ID, + api_hash=API_HASH, + proxy=proxy # 使用代理 +) + +print("使用代理连接: SOCKS5 127.0.0.1:1080") +print("开始创建session...") +app.start() +me = app.get_me() +print(f"✅ Session创建成功!用户: {me.first_name}") +app.stop() diff --git a/create_user_session_interactive.py b/create_user_session_interactive.py new file mode 100755 index 0000000..abcfa3f --- /dev/null +++ b/create_user_session_interactive.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +""" +创建用户Session用于镜像功能 +需要交互式输入手机号码和验证码 +""" +from pyrogram import Client + +API_ID = 24660516 +API_HASH = "eae564578880a59c9963916ff1bbbd3a" +SESSION_NAME = "user_session" + +# 设置代理 +proxy = { + "scheme": "socks5", + "hostname": "127.0.0.1", + "port": 1080 +} + +print("=" * 50) +print("创建用户Session - 用于镜像搜索功能") +print("=" * 50) +print("\n重要提示:") +print("1. 需要使用真实的Telegram账号(非Bot)") +print("2. 输入手机号码格式:+国家代码手机号 (如 +86138xxxxxxxx)") +print("3. 您将收到验证码,请准备好输入") +print("=" * 50) + +try: + app = Client( + SESSION_NAME, + api_id=API_ID, + api_hash=API_HASH, + proxy=proxy + ) + + with app: + me = app.get_me() + print(f"\n✅ Session创建成功!") + print(f"账号信息:") + print(f" 姓名:{me.first_name} {me.last_name or ''}") + print(f" 用户名:@{me.username if me.username else '未设置'}") + print(f" ID:{me.id}") + print(f" 是否Bot:{'是' if me.is_bot else '否'}") + print(f"\nSession已保存为:{SESSION_NAME}.session") + print("\n现在可以使用此账号进行镜像搜索了!") + +except Exception as e: + print(f"\n❌ 创建失败:{e}") + print("\n可能的原因:") + print("1. 手机号码格式错误") + print("2. 验证码输入错误") + print("3. 网络连接问题") + print("4. 代理设置问题") diff --git a/database.py b/database.py new file mode 100755 index 0000000..a5a1d8e --- /dev/null +++ b/database.py @@ -0,0 +1,193 @@ +#!/usr/bin/env python3 +""" +数据库管理模块 - SQLite缓存系统 +""" + +import sqlite3 +import json +from datetime import datetime, timedelta +from typing import Optional, List, Dict +import logging + +logger = logging.getLogger(__name__) + + +class CacheDatabase: + """缓存数据库管理 - 使用(command,keyword,page)作为唯一键,新数据覆盖旧数据""" + + def __init__(self, db_path="/home/atai/bot_data/cache.db"): + self.db_path = db_path + self.init_database() + + def init_database(self): + """初始化数据库表""" + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + # 创建缓存表 + cursor.execute(""" + CREATE TABLE IF NOT EXISTS search_cache ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + command TEXT NOT NULL, + keyword TEXT NOT NULL, + page INTEGER NOT NULL, + result_text TEXT, + buttons_json TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + expires_at TIMESTAMP, + access_count INTEGER DEFAULT 0, + last_accessed TIMESTAMP, + UNIQUE(command, keyword, page) + ) + """) + + # 创建查询索引 + cursor.execute(""" + CREATE INDEX IF NOT EXISTS idx_search + ON search_cache(command, keyword, page) + """) + + # 创建过期时间索引 + cursor.execute(""" + CREATE INDEX IF NOT EXISTS idx_expires + ON search_cache(expires_at) + """) + + conn.commit() + conn.close() + logger.info("数据库初始化完成") + + def get_cache(self, command: str, keyword: str, page: int) -> Optional[Dict]: + """ + 获取缓存 - 返回与服务商bot完全相同的格式 + 返回: {"text": str, "buttons": list} 或 None + """ + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + cursor.execute(""" + SELECT result_text, buttons_json, id + FROM search_cache + WHERE command = ? AND keyword = ? AND page = ? + AND (expires_at IS NULL OR expires_at > ?) + ORDER BY created_at DESC + LIMIT 1 + """, (command, keyword, page, datetime.now())) + + row = cursor.fetchone() + + if row: + # 更新访问统计 + cursor.execute(""" + UPDATE search_cache + SET access_count = access_count + 1, + last_accessed = ? + WHERE id = ? + """, (datetime.now(), row[2])) + conn.commit() + + result = { + "text": row[0], + "buttons": json.loads(row[1]) if row[1] else [] + } + conn.close() + logger.info(f"缓存命中: {command} {keyword} page{page}") + return result + + conn.close() + return None + + def save_cache(self, command: str, keyword: str, page: int, + result_text: str, buttons: list = None, expiry_days: int = 30): + """ + 保存缓存 - 新数据自动覆盖旧数据 + 使用(command, keyword, page)作为唯一键 + """ + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + # 计算过期时间 + expires_at = datetime.now() + timedelta(days=expiry_days) + + # 按钮JSON化 + buttons_json = json.dumps(buttons, ensure_ascii=False) if buttons else None + + try: + # INSERT OR REPLACE: 如果(command,keyword,page)已存在,则更新 + cursor.execute(""" + INSERT OR REPLACE INTO search_cache + (command, keyword, page, result_text, buttons_json, + expires_at, last_accessed, access_count, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?, + COALESCE((SELECT access_count FROM search_cache WHERE command=? AND keyword=? AND page=?), 0), + CURRENT_TIMESTAMP) + """, (command, keyword, page, result_text, buttons_json, + expires_at, datetime.now(), command, keyword, page)) + + conn.commit() + logger.info(f"缓存已保存/更新: {command} {keyword} page{page}") + return True + + except Exception as e: + logger.error(f"保存缓存失败: {e}") + return False + finally: + conn.close() + + def clean_expired(self): + """清理过期缓存""" + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + cursor.execute(""" + DELETE FROM search_cache + WHERE expires_at IS NOT NULL AND expires_at < ? + """, (datetime.now(),)) + + deleted = cursor.rowcount + conn.commit() + conn.close() + + if deleted > 0: + logger.info(f"清理过期缓存: {deleted}条") + return deleted + + def get_stats(self) -> Dict: + """获取统计信息""" + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + # 总缓存数 + cursor.execute("SELECT COUNT(*) FROM search_cache") + total = cursor.fetchone()[0] + + # 有效缓存数 + cursor.execute(""" + SELECT COUNT(*) FROM search_cache + WHERE expires_at IS NULL OR expires_at > ? + """, (datetime.now(),)) + valid = cursor.fetchone()[0] + + # 过期缓存数 + expired = total - valid + + # 最常访问 + cursor.execute(""" + SELECT command, keyword, access_count + FROM search_cache + ORDER BY access_count DESC + LIMIT 10 + """) + top_accessed = cursor.fetchall() + + conn.close() + + return { + "total": total, + "valid": valid, + "expired": expired, + "top_accessed": [ + {"command": row[0], "keyword": row[1], "count": row[2]} + for row in top_accessed + ] + } diff --git a/database.py.bak b/database.py.bak new file mode 100755 index 0000000..2319a07 --- /dev/null +++ b/database.py.bak @@ -0,0 +1,237 @@ +#!/usr/bin/env python3 +""" +数据库管理模块 - SQLite缓存系统 +""" + +import sqlite3 +import json +import hashlib +from datetime import datetime, timedelta +from typing import Optional, List, Dict +import logging + +logger = logging.getLogger(__name__) + + +class CacheDatabase: + """缓存数据库管理""" + + def __init__(self, db_path="/home/atai/bot_data/cache.db"): + self.db_path = db_path + self.init_database() + + def init_database(self): + """初始化数据库表""" + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + # 创建缓存表 + cursor.execute(""" + CREATE TABLE IF NOT EXISTS search_cache ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + command TEXT NOT NULL, + keyword TEXT NOT NULL, + page INTEGER NOT NULL, + result_text TEXT, + result_html TEXT, + buttons_json TEXT, + result_hash TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + expires_at TIMESTAMP, + access_count INTEGER DEFAULT 0, + last_accessed TIMESTAMP + ) + """) + + # 创建唯一索引(防止重复) + cursor.execute(""" + CREATE UNIQUE INDEX IF NOT EXISTS idx_unique_cache + ON search_cache(command, keyword, page, result_hash) + """) + + # 创建查询索引 + cursor.execute(""" + CREATE INDEX IF NOT EXISTS idx_search + ON search_cache(command, keyword, page) + """) + + # 创建过期时间索引 + cursor.execute(""" + CREATE INDEX IF NOT EXISTS idx_expires + ON search_cache(expires_at) + """) + + conn.commit() + conn.close() + logger.info("数据库初始化完成") + + def get_cache(self, command: str, keyword: str, page: int) -> Optional[Dict]: + """ + 获取缓存 + 返回: {"text": str, "html": str, "buttons": list} 或 None + """ + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + cursor.execute(""" + SELECT result_text, result_html, buttons_json, id + FROM search_cache + WHERE command = ? AND keyword = ? AND page = ? + AND (expires_at IS NULL OR expires_at > ?) + ORDER BY created_at DESC + LIMIT 1 + """, (command, keyword, page, datetime.now())) + + row = cursor.fetchone() + + if row: + # 更新访问统计 + cursor.execute(""" + UPDATE search_cache + SET access_count = access_count + 1, + last_accessed = ? + WHERE id = ? + """, (datetime.now(), row[3])) + conn.commit() + + result = { + "text": row[0], + "html": row[1], + "buttons": json.loads(row[2]) if row[2] else None + } + conn.close() + logger.info(f"缓存命中: {command} {keyword} page{page}") + return result + + conn.close() + return None + + def save_cache(self, command: str, keyword: str, page: int, + result_text: str, buttons_or_html = None, + buttons: list = None, expiry_days: int = 30): + """ + 保存缓存 - 兼容两种调用方式 + """ + # 兼容老代码: save_cache(cmd, keyword, page, text, buttons) + if isinstance(buttons_or_html, list): + buttons = buttons_or_html + result_html = None + else: + result_html = buttons_or_html + + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + # 计算结果哈希(用于去重) + result_hash = hashlib.md5(result_text.encode()).hexdigest() + + # 计算过期时间 + expires_at = datetime.now() + timedelta(days=expiry_days) + + # 按钮JSON化 + buttons_json = json.dumps(buttons, ensure_ascii=False) if buttons else None + + try: + cursor.execute(""" + INSERT OR REPLACE INTO search_cache + (command, keyword, page, result_text, result_html, buttons_json, + result_hash, expires_at, last_accessed) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + """, (command, keyword, page, result_text, result_html, buttons_json, + result_hash, expires_at, datetime.now())) + + conn.commit() + logger.info(f"缓存已保存: {command} {keyword} page{page}") + return True + + except sqlite3.IntegrityError as e: + logger.debug(f"缓存已存在(去重): {e}") + return False + finally: + conn.close() + + def clean_expired(self): + """清理过期缓存""" + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + cursor.execute(""" + DELETE FROM search_cache + WHERE expires_at IS NOT NULL AND expires_at < ? + """, (datetime.now(),)) + + deleted = cursor.rowcount + conn.commit() + conn.close() + + if deleted > 0: + logger.info(f"清理过期缓存: {deleted}条") + return deleted + + def get_stats(self) -> Dict: + """获取统计信息""" + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + # 总缓存数 + cursor.execute("SELECT COUNT(*) FROM search_cache") + total = cursor.fetchone()[0] + + # 有效缓存数 + cursor.execute(""" + SELECT COUNT(*) FROM search_cache + WHERE expires_at IS NULL OR expires_at > ? + """, (datetime.now(),)) + valid = cursor.fetchone()[0] + + # 过期缓存数 + expired = total - valid + + # 最常访问 + cursor.execute(""" + SELECT command, keyword, access_count + FROM search_cache + ORDER BY access_count DESC + LIMIT 10 + """) + top_accessed = cursor.fetchall() + + conn.close() + + return { + "total": total, + "valid": valid, + "expired": expired, + "top_accessed": [ + {"command": row[0], "keyword": row[1], "count": row[2]} + for row in top_accessed + ] + } + + def search_history(self, keyword: str, limit: int = 20) -> List[Dict]: + """搜索历史记录""" + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + cursor.execute(""" + SELECT DISTINCT command, keyword, MAX(page) as max_page, + COUNT(*) as page_count, MAX(created_at) as latest + FROM search_cache + WHERE keyword LIKE ? + GROUP BY command, keyword + ORDER BY latest DESC + LIMIT ? + """, (f"%{keyword}%", limit)) + + results = [] + for row in cursor.fetchall(): + results.append({ + "command": row[0], + "keyword": row[1], + "total_pages": row[2], + "cached_pages": row[3], + "latest_update": row[4] + }) + + conn.close() + return results diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..b283010 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,177 @@ +#!/bin/bash + +# ======================================== +# Telegram Bot 部署脚本 +# 用于在虚拟机上自动部署和运行机器人 +# ======================================== + +echo "=========================================" +echo "开始部署 Telegram 整合机器人" +echo "=========================================" + +# 1. 更新系统包 +echo "📦 更新系统包..." +sudo apt-get update -y +sudo apt-get upgrade -y + +# 2. 安装必要的软件 +echo "🔧 安装必要软件..." +sudo apt-get install -y python3 python3-pip git screen + +# 3. 克隆项目 +echo "📥 克隆项目..." +if [ -d "newbot925" ]; then + echo "项目已存在,更新代码..." + cd newbot925 + git pull +else + git clone https://github.com/woshiqp465/newbot925.git + cd newbot925 +fi + +# 4. 安装Python依赖 +echo "📚 安装Python依赖..." +pip3 install -r requirements.txt + +# 5. 创建.env文件(如果不存在) +if [ ! -f .env ]; then + echo "⚙️ 创建配置文件..." + cp .env.example .env + echo "" + echo "⚠️ 请编辑 .env 文件并填写你的配置:" + echo " nano .env" + echo "" + echo "需要配置:" + echo "- BOT_TOKEN=你的机器人token" + echo "- ADMIN_ID=你的Telegram ID" + echo "" +fi + +# 6. 创建启动脚本 +echo "📝 创建启动脚本..." +cat > start_bot.sh << 'EOF' +#!/bin/bash +# 检查是否已有session在运行 +if screen -list | grep -q "telegram_bot"; then + echo "❌ 机器人已在运行!" + echo "使用 'screen -r telegram_bot' 查看" + echo "使用 './stop_bot.sh' 停止" + exit 1 +fi + +# 在screen会话中启动机器人 +echo "🚀 启动机器人..." +screen -dmS telegram_bot python3 integrated_bot.py +echo "✅ 机器人已在后台启动!" +echo "" +echo "使用以下命令管理:" +echo "- 查看日志: screen -r telegram_bot" +echo "- 退出查看: Ctrl+A 然后按 D" +echo "- 停止机器人: ./stop_bot.sh" +EOF + +# 7. 创建停止脚本 +cat > stop_bot.sh << 'EOF' +#!/bin/bash +# 停止机器人 +if screen -list | grep -q "telegram_bot"; then + screen -X -S telegram_bot quit + echo "✅ 机器人已停止" +else + echo "❌ 机器人未在运行" +fi +EOF + +# 8. 创建查看日志脚本 +cat > logs.sh << 'EOF' +#!/bin/bash +# 查看机器人日志 +if screen -list | grep -q "telegram_bot"; then + screen -r telegram_bot +else + echo "❌ 机器人未在运行" + echo "使用 './start_bot.sh' 启动" +fi +EOF + +# 9. 创建自动重启脚本(防止意外停止) +cat > monitor_bot.sh << 'EOF' +#!/bin/bash +# 监控并自动重启机器人 +while true; do + if ! screen -list | grep -q "telegram_bot"; then + echo "[$(date)] 机器人已停止,正在重启..." + screen -dmS telegram_bot python3 integrated_bot.py + echo "[$(date)] 机器人已重启" + fi + sleep 60 # 每60秒检查一次 +done +EOF + +# 10. 设置脚本权限 +chmod +x start_bot.sh stop_bot.sh logs.sh monitor_bot.sh + +# 11. 创建系统服务(可选,开机自启) +echo "📝 创建系统服务..." +sudo cat > /tmp/telegram-bot.service << EOF +[Unit] +Description=Telegram Integration Bot +After=network.target + +[Service] +Type=simple +User=$USER +WorkingDirectory=$PWD +ExecStart=/usr/bin/python3 $PWD/integrated_bot.py +Restart=always +RestartSec=10 + +[Install] +WantedBy=multi-user.target +EOF + +# 询问是否安装为系统服务 +echo "" +echo "是否将机器人安装为系统服务?(开机自动启动) [y/N]" +read -r response +if [[ "$response" =~ ^[Yy]$ ]]; then + sudo mv /tmp/telegram-bot.service /etc/systemd/system/ + sudo systemctl daemon-reload + sudo systemctl enable telegram-bot.service + echo "✅ 系统服务已安装" + echo "" + echo "使用以下命令管理服务:" + echo "- 启动: sudo systemctl start telegram-bot" + echo "- 停止: sudo systemctl stop telegram-bot" + echo "- 状态: sudo systemctl status telegram-bot" + echo "- 日志: sudo journalctl -u telegram-bot -f" +else + rm /tmp/telegram-bot.service +fi + +echo "" +echo "=========================================" +echo "✅ 部署完成!" +echo "=========================================" +echo "" +echo "📋 使用说明:" +echo "" +echo "1. 首先配置环境变量:" +echo " nano .env" +echo "" +echo "2. 创建Pyrogram session:" +echo " python3 create_session.py" +echo "" +echo "3. 启动机器人:" +echo " ./start_bot.sh" +echo "" +echo "4. 查看运行状态:" +echo " ./logs.sh" +echo "" +echo "5. 停止机器人:" +echo " ./stop_bot.sh" +echo "" +echo "6. 自动监控(推荐):" +echo " screen -dmS monitor ./monitor_bot.sh" +echo "" +echo "=========================================" \ No newline at end of file diff --git a/enhanced_logger.py b/enhanced_logger.py new file mode 100644 index 0000000..3f39488 --- /dev/null +++ b/enhanced_logger.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +""" +增强型日志系统 - 不删档、自动轮转、详细记录 +""" +import logging +import os +from logging.handlers import RotatingFileHandler, TimedRotatingFileHandler +from datetime import datetime +import json + +class EnhancedLogger: + def __init__(self, name, log_dir="./logs"): + self.name = name + self.log_dir = log_dir + self.logger = logging.getLogger(name) + os.makedirs(log_dir, exist_ok=True) + os.makedirs(f"{log_dir}/archive", exist_ok=True) + self.logger.setLevel(logging.DEBUG) + self.logger.handlers = [] + self._setup_handlers() + + def _setup_handlers(self): + # 1. 控制台输出 + console_handler = logging.StreamHandler() + console_handler.setLevel(logging.INFO) + console_formatter = logging.Formatter( + '%(asctime)s - %(name)s - %(levelname)s - %(message)s', + datefmt='%Y-%m-%d %H:%M:%S' + ) + console_handler.setFormatter(console_formatter) + self.logger.addHandler(console_handler) + + # 2. 详细日志(按日期轮转,保留90天) + detailed_log = f"{self.log_dir}/{self.name}_detailed.log" + detailed_handler = TimedRotatingFileHandler( + detailed_log, when='midnight', interval=1, + backupCount=90, encoding='utf-8' + ) + detailed_handler.setLevel(logging.DEBUG) + detailed_handler.suffix = "%Y%m%d" + detailed_formatter = logging.Formatter( + '%(asctime)s - %(name)s - %(levelname)s - [%(filename)s:%(lineno)d] - %(message)s', + datefmt='%Y-%m-%d %H:%M:%S' + ) + detailed_handler.setFormatter(detailed_formatter) + self.logger.addHandler(detailed_handler) + + # 3. 错误日志(50MB轮转) + error_log = f"{self.log_dir}/{self.name}_errors.log" + error_handler = RotatingFileHandler( + error_log, maxBytes=50*1024*1024, backupCount=10, encoding='utf-8' + ) + error_handler.setLevel(logging.ERROR) + error_formatter = logging.Formatter( + '%(asctime)s - %(levelname)s - [%(filename)s:%(lineno)d]\n%(message)s\n' + '='*80, + datefmt='%Y-%m-%d %H:%M:%S' + ) + error_handler.setFormatter(error_formatter) + self.logger.addHandler(error_handler) + + # 4. 审计日志(永久保存) + audit_log = f"{self.log_dir}/audit_{datetime.now().strftime('%Y%m')}.log" + audit_handler = logging.FileHandler(audit_log, encoding='utf-8') + audit_handler.setLevel(logging.INFO) + audit_formatter = logging.Formatter( + '%(asctime)s - %(levelname)s - %(message)s', + datefmt='%Y-%m-%d %H:%M:%S' + ) + audit_handler.setFormatter(audit_formatter) + self.logger.addHandler(audit_handler) + + def get_logger(self): + return self.logger + + def log_user_action(self, user_id, action, details=None): + msg = f"[用户操作] user_id={user_id}, action={action}" + if details: + msg += f", details={details}" + self.logger.info(msg) + + def log_api_call(self, api_name, params=None, response=None, error=None): + msg = f"[API调用] {api_name}" + if params: + msg += f", params={params}" + if error: + self.logger.error(f"{msg}, error={error}") + else: + self.logger.info(msg) + +def get_enhanced_logger(name="bot", log_dir="./logs"): + return EnhancedLogger(name, log_dir).get_logger() diff --git a/fix_claude_auth.py b/fix_claude_auth.py new file mode 100644 index 0000000..7c2995a --- /dev/null +++ b/fix_claude_auth.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 +import sys + +with open('integrated_bot_ai.py', 'r') as f: + content = f.read() + +# 替换初始化代码 +old_init = ''' claude_client = anthropic.Anthropic( + api_key=os.environ.get('ANTHROPIC_AUTH_TOKEN'), + base_url=os.environ.get('ANTHROPIC_BASE_URL', 'https://api.anthropic.com') + )''' + +new_init = ''' claude_client = anthropic.Anthropic( + auth_token=os.environ.get('ANTHROPIC_AUTH_TOKEN'), + base_url=os.environ.get('ANTHROPIC_BASE_URL', 'https://api.anthropic.com') + )''' + +content = content.replace(old_init, new_init) + +with open('integrated_bot_ai.py', 'w') as f: + f.write(content) + +print('✅ 修改完成') diff --git a/integrated_bot.py b/integrated_bot.py new file mode 100644 index 0000000..6b99030 --- /dev/null +++ b/integrated_bot.py @@ -0,0 +1,508 @@ +#!/usr/bin/env python3 +""" +整合版客服机器人 - 包含镜像搜索功能 +修复了事件循环冲突问题 +""" + +import asyncio +import logging +import time +import os +import httpx +from typing import Dict, Optional +from datetime import datetime + +# Pyrogram imports +from pyrogram import Client as PyrogramClient, filters +from pyrogram.types import Message as PyrogramMessage +from pyrogram.raw.functions.messages import GetBotCallbackAnswer + +# Telegram Bot imports +from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup +from telegram.ext import Application, CommandHandler, MessageHandler, CallbackQueryHandler, filters as tg_filters +from telegram.ext import ContextTypes + +# 移除src依赖,使用环境变量 + +# ================== 配置 ================== +API_ID = 24660516 +API_HASH = "eae564578880a59c9963916ff1bbbd3a" +SESSION_NAME = "user_session" +BOT_TOKEN = "8426529617:AAHAxzohSMFBAxInzbAVJsZfkB5bHnOyFC4" +TARGET_BOT = "@openaiw_bot" +ADMIN_ID = 7363537082 + +# 搜索命令列表 +SEARCH_COMMANDS = ['/topchat', '/search', '/text', '/human'] + +# 日志配置 +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + + +class IntegratedBot: + """整合的客服机器人 - 包含镜像搜索功能""" + + def __init__(self): + # 直接使用常量配置,不依赖Settings类 + + # Bot应用 + self.app = None + + # Pyrogram客户端(用于镜像) + self.pyrogram_client: Optional[PyrogramClient] = None + self.target_bot_id: Optional[int] = None + + # 消息映射 + self.pyrogram_to_telegram = {} # pyrogram_msg_id -> telegram_msg_id + self.telegram_to_pyrogram = {} # telegram_msg_id -> pyrogram_msg_id + self.callback_data_map = {} # telegram_callback_id -> (pyrogram_msg_id, original_callback_data) + self.user_search_sessions = {} # user_id -> search_session_info + + async def setup_pyrogram(self): + """设置Pyrogram客户端用于镜像""" + try: + # 检查是否需要使用代理 + proxy_config = None + if os.environ.get('ALL_PROXY'): + proxy_url = os.environ.get('ALL_PROXY', '').replace('socks5://', '') + if proxy_url: + host, port = proxy_url.split(':') + proxy_config = { + "scheme": "socks5", + "hostname": host, + "port": int(port) + } + logger.info(f"使用代理: {host}:{port}") + + self.pyrogram_client = PyrogramClient( + SESSION_NAME, + api_id=API_ID, + api_hash=API_HASH, + proxy=proxy_config if proxy_config else None + ) + + await self.pyrogram_client.start() + logger.info("✅ Pyrogram客户端已启动") + + # 获取目标机器人信息 + target = await self.pyrogram_client.get_users(TARGET_BOT) + self.target_bot_id = target.id + logger.info(f"✅ 已连接到搜索机器人: {target.username} (ID: {target.id})") + + # 设置消息监听器 + @self.pyrogram_client.on_message(filters.user(self.target_bot_id)) + async def on_bot_response(_, message: PyrogramMessage): + await self.handle_search_response(message) + + @self.pyrogram_client.on_edited_message(filters.user(self.target_bot_id)) + async def on_message_edited(_, message: PyrogramMessage): + await self.handle_search_response(message, is_edit=True) + + logger.info("✅ 搜索监听器已设置") + return True + + except Exception as e: + logger.error(f"Pyrogram设置失败: {e}") + return False + + async def handle_start(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """处理/start命令""" + user = update.effective_user + welcome_text = ( + f"👋 您好 {user.first_name}!\n\n" + "暂时支持的搜索指令:\n\n" + "- 群组目录 /topchat\n" + "- 群组搜索 /search\n" + "- 按消息文本搜索 /text\n" + "- 按名称搜索 /human\n\n" + "您可以使用以上指令进行搜索,或直接发送消息联系客服。" + ) + await update.message.reply_text(welcome_text) + + # 通知管理员有新用户访问 + admin_notification = ( + f"🆕 新用户访问:\n" + f"👤 姓名: {user.first_name} {user.last_name or ''}\n" + f"🆔 ID: {user.id}\n" + f"👤 用户名: @{user.username or '无'}\n" + f"📱 命令: /start\n" + f"⏰ 时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" + ) + + await context.bot.send_message( + chat_id=ADMIN_ID, + text=admin_notification + ) + + logger.info(f"新用户访问 /start: {user.id} ({user.first_name})") + + async def handle_message(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """处理所有消息""" + if not update.message or not update.message.text: + return + + user = update.effective_user + text = update.message.text + is_admin = user.id == ADMIN_ID + + # 管理员回复逻辑 + if is_admin and update.message.reply_to_message: + await self.handle_admin_reply(update, context) + return + + # 搜索命令处理 + if self.is_search_command(text): + await self.handle_search_command(update, context) + return + + # 普通客服消息转发 + await self.forward_to_admin(update, context) + + def is_search_command(self, text: str) -> bool: + """检查是否是搜索命令""" + if not text: + return False + command = text.split()[0] + return command in SEARCH_COMMANDS + + async def handle_search_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """处理搜索命令 - 通过Pyrogram转发""" + user = update.effective_user + user_id = user.id + command = update.message.text + + try: + # 通知管理员有用户执行搜索 + admin_notification = ( + f"🔍 用户执行搜索:\n" + f"👤 姓名: {user.first_name} {user.last_name or ''}\n" + f"🆔 ID: {user_id}\n" + f"👤 用户名: @{user.username or '无'}\n" + f"📝 搜索内容: {command}\n" + f"⏰ 时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" + ) + + await context.bot.send_message( + chat_id=ADMIN_ID, + text=admin_notification + ) + + # 发送等待消息 + wait_msg = await update.message.reply_text("🔍 正在搜索,请稍候...") + + # 记录搜索会话 + self.user_search_sessions[user_id] = { + 'chat_id': update.effective_chat.id, + 'wait_msg_id': wait_msg.message_id, + 'command': command, + 'timestamp': datetime.now() + } + + # 通过Pyrogram发送到搜索机器人 + await self.pyrogram_client.send_message(self.target_bot_id, command) + logger.info(f"用户 {user.first_name}({user_id}) 执行搜索: {command}") + + except Exception as e: + logger.error(f"搜索命令处理失败: {e}") + await update.message.reply_text("❌ 搜索失败,请稍后重试") + + async def handle_search_response(self, message: PyrogramMessage, is_edit: bool = False): + """处理搜索机器人的响应""" + try: + # 查找最近的搜索请求 + if not self.user_search_sessions: + return + + # 获取最近的请求用户 + user_id = max( + self.user_search_sessions.keys(), + key=lambda k: self.user_search_sessions[k]['timestamp'] + ) + + session = self.user_search_sessions[user_id] + + # 提取消息内容 + text = message.text or message.caption or "无结果" + + # 处理HTML格式 + try: + if message.text and hasattr(message.text, 'html'): + text = message.text.html + except: + pass + + # 转换键盘 + keyboard = self.convert_keyboard(message) + + # 更新或发送消息 + if is_edit and message.id in self.pyrogram_to_telegram: + # 编辑现有消息 + telegram_msg_id = self.pyrogram_to_telegram[message.id] + await self.app.bot.edit_message_text( + chat_id=session['chat_id'], + message_id=telegram_msg_id, + text=text[:4000], + reply_markup=keyboard, + parse_mode='HTML' + ) + else: + # 删除等待消息,发送新消息 + try: + await self.app.bot.delete_message( + chat_id=session['chat_id'], + message_id=session['wait_msg_id'] + ) + except: + pass + + sent = await self.app.bot.send_message( + chat_id=session['chat_id'], + text=text[:4000], + reply_markup=keyboard, + parse_mode='HTML' + ) + + # 记录映射 + self.pyrogram_to_telegram[message.id] = sent.message_id + self.telegram_to_pyrogram[sent.message_id] = message.id + + except Exception as e: + logger.error(f"处理搜索响应失败: {e}") + + def convert_keyboard(self, message: PyrogramMessage) -> Optional[InlineKeyboardMarkup]: + """转换Pyrogram键盘为Telegram键盘""" + if not message.reply_markup or not message.reply_markup.inline_keyboard: + return None + + try: + buttons = [] + for row in message.reply_markup.inline_keyboard: + button_row = [] + for btn in row: + if btn.url: + button_row.append(InlineKeyboardButton( + text=btn.text, + url=btn.url + )) + elif btn.callback_data: + # 创建callback ID + callback_id = f"cb_{time.time():.0f}_{len(self.callback_data_map)}" + self.callback_data_map[callback_id] = ( + message.id, + btn.callback_data + ) + + button_row.append(InlineKeyboardButton( + text=btn.text, + callback_data=callback_id[:64] + )) + + if button_row: + buttons.append(button_row) + + return InlineKeyboardMarkup(buttons) if buttons else None + + except Exception as e: + logger.error(f"键盘转换失败: {e}") + return None + + async def handle_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """处理回调查询(翻页等)""" + query = update.callback_query + callback_id = query.data + + await query.answer("正在加载...") + + if callback_id not in self.callback_data_map: + await query.answer("按钮已过期", show_alert=True) + return + + pyrogram_msg_id, original_callback = self.callback_data_map[callback_id] + + try: + # 准备callback数据 + if not isinstance(original_callback, bytes): + original_callback = original_callback.encode() if original_callback else b'' + + # 调用原始callback + result = await self.pyrogram_client.invoke( + GetBotCallbackAnswer( + peer=await self.pyrogram_client.resolve_peer(self.target_bot_id), + msg_id=pyrogram_msg_id, + data=original_callback + ) + ) + + # 等待Bot编辑消息 + await asyncio.sleep(1) + + logger.info("✅ Callback已处理") + + except Exception as e: + logger.error(f"Callback处理失败: {e}") + await query.answer("操作失败", show_alert=True) + + async def forward_to_admin(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """转发客户消息给管理员""" + user = update.effective_user + message = update.effective_message + + # 构建转发消息 + forward_text = ( + f"📬 新消息来自客户:\n" + f"👤 {user.first_name} {user.last_name or ''}\n" + f"🆔 ID: {user.id}\n" + f"👤 用户名: @{user.username or '无'}\n" + f"💬 消息: {message.text}\n" + f"⏰ 时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" + ) + + # 发送给管理员 + sent = await context.bot.send_message( + chat_id=ADMIN_ID, + text=forward_text + ) + + logger.info(f"已转发消息给管理员: 来自 {user.id}") + + async def handle_admin_reply(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """处理管理员回复""" + reply_to = update.message.reply_to_message + + if not reply_to or not reply_to.text: + return + + # 从回复的消息中提取用户ID + lines = reply_to.text.split('\n') + user_id = None + for line in lines: + if 'ID:' in line or '🆔' in line: + try: + # 尝试多种格式提取ID + if '🆔 ID:' in line: + user_id = int(line.split('🆔 ID:')[1].strip()) + elif 'ID:' in line: + id_part = line.split('ID:')[1].strip() + # 提取数字部分 + import re + numbers = re.findall(r'\d+', id_part) + if numbers: + user_id = int(numbers[0]) + break + except Exception as e: + logger.debug(f"提取ID失败: {e}, line: {line}") + + if not user_id: + logger.warning(f"无法识别用户ID,消息内容:{reply_to.text}") + await update.message.reply_text("❌ 无法识别用户ID") + return + + # 发送回复给用户 + try: + await context.bot.send_message( + chat_id=user_id, + text=update.message.text + ) + + # 给管理员确认 + await update.message.reply_text(f"✅ 已回复给用户 {user_id}") + logger.info(f"管理员回复了用户 {user_id}: {update.message.text}") + + except Exception as e: + logger.error(f"回复失败: {e}") + await update.message.reply_text(f"❌ 回复失败: {str(e)}") + + async def initialize(self): + """初始化机器人""" + try: + logger.info("正在初始化整合机器人...") + + # 初始化Pyrogram客户端 + if not await self.setup_pyrogram(): + logger.error("Pyrogram初始化失败") + return False + + # 创建Bot应用,配置代理 + builder = Application.builder().token(BOT_TOKEN) + + # 如果设置了代理环境变量,配置httpx客户端 + if os.environ.get('HTTP_PROXY'): + proxy_url = os.environ.get('HTTP_PROXY') + logger.info(f"配置Telegram Bot代理: {proxy_url}") + # 创建自定义httpx客户端 + request = httpx.AsyncClient( + proxies={ + "http://": proxy_url, + "https://": proxy_url, + }, + timeout=30.0 + ) + builder = builder.request(request) + + self.app = builder.build() + + # 注册处理器 + self.app.add_handler(CommandHandler("start", self.handle_start)) + self.app.add_handler(CallbackQueryHandler(self.handle_callback)) + self.app.add_handler(MessageHandler(tg_filters.ALL, self.handle_message)) + + logger.info("✅ 整合机器人初始化完成") + return True + + except Exception as e: + logger.error(f"初始化失败: {e}") + return False + + async def run(self): + """运行机器人""" + try: + # 启动Bot + await self.app.initialize() + await self.app.start() + await self.app.updater.start_polling(drop_pending_updates=True) + + logger.info("="*50) + logger.info("✅ 整合机器人已启动") + logger.info(f"客服功能: 消息转发给管理员 {ADMIN_ID}") + logger.info(f"搜索功能: 镜像 {TARGET_BOT}") + logger.info("="*50) + + # 保持运行 + await asyncio.Event().wait() + + except KeyboardInterrupt: + logger.info("收到停止信号") + finally: + await self.cleanup() + + async def cleanup(self): + """清理资源""" + logger.info("正在清理...") + + if self.app: + await self.app.updater.stop() + await self.app.stop() + await self.app.shutdown() + + if self.pyrogram_client: + await self.pyrogram_client.stop() + + logger.info("✅ 清理完成") + + +async def main(): + """主函数""" + bot = IntegratedBot() + + if await bot.initialize(): + await bot.run() + else: + logger.error("初始化失败,退出") + + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/integrated_bot_ai.backup.20251007_164306.py b/integrated_bot_ai.backup.20251007_164306.py new file mode 100755 index 0000000..a293c20 --- /dev/null +++ b/integrated_bot_ai.backup.20251007_164306.py @@ -0,0 +1,865 @@ +#!/usr/bin/env python3 +""" +整合版客服机器人 - AI增强版 +包含: +1. AI对话引导 +2. 镜像搜索功能 +3. 自动翻页缓存 +4. 智能去重 +""" + +import asyncio +import logging +import time +import os +import httpx +# import anthropic # 已替换为 claude-agent-sdk +from claude_agent_wrapper import init_claude_agent +import json +import sys +from typing import Dict, Optional +from datetime import datetime + +# 添加路径 +sys.path.insert(0, "/home/atai/bot_data") + +# Pyrogram imports +from pyrogram import Client as PyrogramClient, filters +from pyrogram.types import Message as PyrogramMessage +from pyrogram.raw.functions.messages import GetBotCallbackAnswer + +# Telegram Bot imports +from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup +from telegram.ext import Application, CommandHandler, MessageHandler, CallbackQueryHandler, filters as tg_filters +from telegram.ext import ContextTypes + +# 导入数据库 +try: + from database import CacheDatabase +except ImportError: + CacheDatabase = None + logging.warning("database.py未找到,缓存功能将禁用") + +# ================== 配置 ================== +API_ID = 24660516 +API_HASH = "eae564578880a59c9963916ff1bbbd3a" +SESSION_NAME = "user_session" +BOT_TOKEN = "8426529617:AAHAxzohSMFBAxInzbAVJsZfkB5bHnOyFC4" +TARGET_BOT = "@openaiw_bot" +ADMIN_ID = 7363537082 + +# AI服务配置 +MAC_API_URL = "http://192.168.9.10:8000" + +# 搜索命令列表 +SEARCH_COMMANDS = ['/topchat', '/search', '/text', '/human'] + +# 日志配置 +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +# 初始化Claude Agent SDK客户端 +try: + claude_client = init_claude_agent() + if claude_client: + logger.info("✅ Claude Agent SDK客户端已初始化") + else: + logger.error("❌ Claude Agent SDK初始化失败") + claude_client = None +except Exception as e: + logger.error(f"❌ Claude Agent SDK初始化失败: {e}") + claude_client = None + + + + +# ================== 对话管理 ================== +class ConversationManager: + """管理用户对话上下文""" + + def __init__(self, max_history=5): + self.conversations = {} + self.max_history = max_history + + def add_message(self, user_id: int, role: str, content: str): + """添加消息到历史""" + if user_id not in self.conversations: + self.conversations[user_id] = [] + + self.conversations[user_id].append({ + "role": role, + "content": content, + "timestamp": datetime.now().isoformat() + }) + + # 保持最近的N条消息 + if len(self.conversations[user_id]) > self.max_history * 2: + self.conversations[user_id] = self.conversations[user_id][-self.max_history * 2:] + + def get_history(self, user_id: int, limit: int = 2) -> list: + """获取用户对话历史""" + if user_id not in self.conversations: + return [] + + history = self.conversations[user_id][-limit * 2:] + return [{"role": msg["role"], "content": msg["content"]} for msg in history] + + def clear_history(self, user_id: int): + """清空用户历史""" + if user_id in self.conversations: + del self.conversations[user_id] + + +# ================== 自动翻页管理器 ================== +class AutoPaginationManager: + """后台自动翻页 - 用户无感知""" + + def __init__(self, pyrogram_client, cache_db, target_bot_id, logger): + self.pyrogram_client = pyrogram_client + self.cache_db = cache_db + self.target_bot_id = target_bot_id + self.logger = logger + self.active_tasks = {} + + async def start_pagination(self, user_id, command, keyword, first_message): + """启动后台翻页任务""" + if user_id in self.active_tasks: + return + + task = asyncio.create_task(self._paginate(user_id, command, keyword, first_message)) + self.active_tasks[user_id] = task + self.logger.info(f"[翻页] 后台任务启动: {command} {keyword}") + + async def _paginate(self, user_id, command, keyword, message): + """执行翻页""" + try: + page = 1 + self._save_to_cache(command, keyword, page, message) + + if not self._has_next(message): + self.logger.info(f"[翻页] 只有1页") + return + + current = message + page = 2 + max_pages = 100 # 最多100页防止死循环 + + while page <= max_pages: + await asyncio.sleep(2) + + next_msg = await self._click_next(current) + if not next_msg: + self.logger.info(f"[翻页] 点击失败,停止翻页") + break + + self._save_to_cache(command, keyword, page, next_msg) + self.logger.info(f"[翻页] 第{page}页已保存") + + if not self._has_next(next_msg): + self.logger.info(f"[翻页] ✅ 完成,共{page}页") + break + + current = next_msg + page += 1 + + except Exception as e: + self.logger.error(f"[翻页] 错误: {e}") + finally: + if user_id in self.active_tasks: + del self.active_tasks[user_id] + + def _has_next(self, msg): + """检查是否有下一页""" + if not msg.reply_markup: + return False + for row in msg.reply_markup.inline_keyboard: + for btn in row: + if btn.text: + text = btn.text.strip() + # 检查右箭头(排除左箭头) + if any(arrow in text for arrow in ["➡️", "▶", "→", "»"]): + if not any(prev in text for prev in ["⬅️", "◀", "←", "«"]): + return True + # 检查文字 + if any(x in text for x in ["下一页", "Next"]): + return True + return False + async def _click_next(self, msg): + """点击下一页""" + try: + from pyrogram.raw.functions.messages import GetBotCallbackAnswer + + for row in msg.reply_markup.inline_keyboard: + for btn in row: + if btn.text and btn.callback_data: + text = btn.text.strip() + # 检查是否是下一页按钮 + is_next = False + if any(arrow in text for arrow in ['➡️', '▶', '→', '»']): + if not any(prev in text for prev in ['⬅️', '◀', '←', '«']): + is_next = True + if any(x in text for x in ['下一页', 'Next']): + is_next = True + if is_next: + await self.pyrogram_client.invoke( + GetBotCallbackAnswer( + peer=await self.pyrogram_client.resolve_peer(self.target_bot_id), + msg_id=msg.id, + data=btn.callback_data + ) + ) + await asyncio.sleep(1.5) + return await self.pyrogram_client.get_messages(self.target_bot_id, msg.id) + except Exception as e: + self.logger.error(f"[翻页] 点击失败: {e}") + return None + + def _save_to_cache(self, cmd, keyword, page, msg): + """保存到缓存""" + if not self.cache_db: + return + try: + text = msg.text or msg.caption or "" + buttons = [] + if msg.reply_markup: + for row in msg.reply_markup.inline_keyboard: + for btn in row: + buttons.append({"text": btn.text, "url": btn.url if btn.url else None}) + self.cache_db.save_cache(cmd, keyword, page, text, buttons) + except Exception as e: + self.logger.error(f"[翻页] 保存失败: {e}") + + +class IntegratedBotAI: + """整合的客服机器人 - AI增强版""" + + def __init__(self): + # Bot应用 + self.app = None + + # Pyrogram客户端(用于镜像) + self.pyrogram_client: Optional[PyrogramClient] = None + self.target_bot_id: Optional[int] = None + + # 消息映射 + self.pyrogram_to_telegram = {} + self.telegram_to_pyrogram = {} + self.callback_data_map = {} + self.user_search_sessions = {} + + # AI会话状态 + self.user_ai_sessions = {} + + # 缓存数据库 + self.cache_db = CacheDatabase() if CacheDatabase else None + + # 对话管理器 + self.conversation_manager = ConversationManager() + self.pagination_manager = None + + async def setup_pyrogram(self): + """设置Pyrogram客户端""" + try: + proxy_config = None + if os.environ.get('ALL_PROXY'): + proxy_url = os.environ.get('ALL_PROXY', '').replace('socks5://', '') + if proxy_url: + host, port = proxy_url.split(':') + proxy_config = {"scheme": "socks5", "hostname": host, "port": int(port)} + + self.pyrogram_client = PyrogramClient( + SESSION_NAME, api_id=API_ID, api_hash=API_HASH, + proxy=proxy_config if proxy_config else None + ) + + await self.pyrogram_client.start() + logger.info("✅ Pyrogram客户端已启动") + + # 初始化自动翻页管理器 + self.pagination_manager = AutoPaginationManager( + self.pyrogram_client, self.cache_db, self.target_bot_id, logger + ) + logger.info("✅ 自动翻页管理器已初始化") + + target = await self.pyrogram_client.get_users(TARGET_BOT) + self.target_bot_id = target.id + logger.info(f"✅ 已连接到搜索机器人: {target.username}") + + @self.pyrogram_client.on_message(filters.user(self.target_bot_id)) + async def on_bot_response(_, message: PyrogramMessage): + await self.handle_search_response(message) + + @self.pyrogram_client.on_edited_message(filters.user(self.target_bot_id)) + async def on_message_edited(_, message: PyrogramMessage): + await self.handle_search_response(message, is_edit=True) + + return True + except Exception as e: + logger.error(f"Pyrogram设置失败: {e}") + return False + + async def call_ai_service(self, user_id: int, message: str, context: dict = None) -> dict: + """优化的Claude API调用 - 带上下文记忆和改进提示词""" + + if not claude_client: + logger.error("Claude客户端未初始化") + return { + "type": "auto", + "response": "👋 我来帮你搜索!\n\n直接发关键词,或试试:\n• /search 群组名\n• /text 讨论内容\n• /topchat 热门分类", + "confidence": 0.3 + } + + try: + logger.info(f"[用户 {user_id}] 调用Claude API: {message}") + + username = context.get('username', f'user_{user_id}') if context else f'user_{user_id}' + first_name = context.get('first_name', '') if context else '' + + # 构建对话历史 + messages = [] + + # 添加历史对话(最近2轮) + history = self.conversation_manager.get_history(user_id, limit=2) + messages.extend(history) + + # 添加当前消息(优化的提示词) + current_prompt = f"""你是@ktfund_bot的AI助手,专业的Telegram群组搜索助手。 + +【重要】你的回复中可以包含可执行的命令,我会为它们生成按钮。 +命令格式:/search 关键词 或 /text 关键词 + +用户信息:@{username} ({first_name}) +用户说:"{message}" + +【可用工具】 +• /search [关键词] - 搜索群组名称 +• /text [关键词] - 搜索讨论内容 +• /human [关键词] - 搜索用户 +• /topchat - 热门分类 + +【回复要求】 +1. 简短友好(2-4行) +2. 给1-2个具体命令建议 +3. 口语化,像朋友聊天 +4. 命令要在独立的一行 + +【示例】 +用户:"找AI群" +回复: +找AI群的话,试试: +/search AI +/text ChatGPT + +直接回复:""" + + messages.append({ + "role": "user", + "content": current_prompt + }) + + # 调用Claude Agent SDK + claude_response = claude_client.chat( + messages=messages, + model="claude-sonnet-4-5-20250929", + max_tokens=512, + temperature=0.7 + ) + + # 保存对话历史 + self.conversation_manager.add_message(user_id, "user", message) + self.conversation_manager.add_message(user_id, "assistant", claude_response) + + logger.info(f"[用户 {user_id}] ✅ Claude回复成功 ({len(claude_response)}字)") + + # 智能提取命令建议 + suggested_commands = self._extract_commands(claude_response) + + return { + "type": "ai", + "response": claude_response, + "confidence": 1.0, + "suggested_commands": suggested_commands + } + + except Exception as e: + logger.error(f"[用户 {user_id}] ❌ Claude API失败: {e}") + return { + "type": "auto", + "response": "👋 我来帮你搜索!\n\n直接发关键词,或试试:\n• /search 群组名\n• /text 讨论内容\n• /topchat 热门分类", + "confidence": 0.3 + } + + def _extract_commands(self, response_text: str) -> list: + """从回复中提取建议的命令""" + import re + commands = [] + + # 匹配 /command pattern + patterns = [ + r'/search\s+[\w\s]+', + r'/text\s+[\w\s]+', + r'/human\s+[\w\s]+', + r'/topchat' + ] + + for pattern in patterns: + matches = re.findall(pattern, response_text) + commands.extend([m.strip() for m in matches[:1]]) + + return commands[:2] + + + + def _extract_command_buttons(self, text: str) -> list: + """从AI回复中提取命令按钮""" + import re + buttons = [] + + # 匹配:/command keyword + pattern = r'/(search|text|human|topchat)\s*([^\n]*)' + matches = re.findall(pattern, text, re.IGNORECASE) + + for cmd, keywords in matches[:3]: + cmd = cmd.lower() + keywords = keywords.strip()[:30] # 限制长度 + + if keywords: + display = f"/{cmd} {keywords}" + callback = f"cmd_{cmd}_{keywords.replace(' ', '_')}"[:64] + else: + display = f"/{cmd}" + callback = f"cmd_{cmd}" + + buttons.append((display, callback)) + + return buttons + + async def handle_start(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """处理/start命令 - AI引导模式""" + user = update.effective_user + user_id = user.id + + self.user_ai_sessions[user_id] = {"started_at": datetime.now(), "conversation": []} + + welcome_text = ( + f"👋 您好 {user.first_name}!\n\n" + "我是智能搜索助手,可以帮您找到Telegram上的群组和频道。\n\n" + "🔍 我能做什么:\n" + "• 搜索群组/频道\n" + "• 搜索特定话题的讨论\n" + "• 查找用户\n" + "• 浏览热门分类\n\n" + "💬 直接告诉我您想找什么,我会帮您选择最合适的搜索方式!" + ) + + keyboard = [ + [InlineKeyboardButton("🔍 搜索群组", callback_data="quick_search"), + InlineKeyboardButton("📚 使用指南", callback_data="quick_help")], + [InlineKeyboardButton("🔥 热门分类", callback_data="quick_topchat")] + ] + + await update.message.reply_text(welcome_text, reply_markup=InlineKeyboardMarkup(keyboard)) + + # 通知管理员 + admin_notification = ( + f"🆕 新用户访问 (AI模式):\n" + f"👤 {user.first_name} {user.last_name or ''}\n" + f"🆔 {user.id}\n" + f"👤 @{user.username or '无'}\n" + f"⏰ {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" + ) + await context.bot.send_message(chat_id=ADMIN_ID, text=admin_notification) + + async def handle_message(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """处理所有消息 - AI智能路由""" + if not update.message or not update.message.text: + return + + user = update.effective_user + user_id = user.id + text = update.message.text + is_admin = user_id == ADMIN_ID + + if is_admin and update.message.reply_to_message: + await self.handle_admin_reply(update, context) + return + + if self.is_search_command(text): + await self.handle_search_command(update, context) + return + + await self.handle_ai_conversation(update, context) + + def is_search_command(self, text: str) -> bool: + """检查是否是搜索命令""" + return text and text.split()[0] in SEARCH_COMMANDS + + async def handle_ai_conversation(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """AI对话处理 - 带智能按钮""" + user = update.effective_user + user_id = user.id + message = update.message.text + + # 显示"正在输入" + await update.message.chat.send_action("typing") + + # 构建上下文 + user_context = { + "username": user.username or f"user{user_id}", + "first_name": user.first_name or "朋友", + "last_name": user.last_name + } + + # 调用AI + ai_response = await self.call_ai_service(user_id, message, user_context) + response_text = ai_response.get("response", "") + + # 提取命令按钮 + buttons = self._extract_command_buttons(response_text) + + try: + if buttons: + # 构建按钮键盘 + keyboard = [] + for display, callback in buttons: + keyboard.append([InlineKeyboardButton( + f"🔍 {display}", + callback_data=callback + )]) + + # 添加常用按钮 + keyboard.append([ + InlineKeyboardButton("🔥 热门目录", callback_data="cmd_topchat"), + InlineKeyboardButton("📖 帮助", callback_data="cmd_help") + ]) + + await update.message.reply_text( + response_text, + reply_markup=InlineKeyboardMarkup(keyboard) + ) + logger.info(f"[AI对话] 已回复用户 {user_id} (带{len(buttons)}个按钮)") + else: + # 无按钮版本 + await update.message.reply_text(response_text) + logger.info(f"[AI对话] 已回复用户 {user_id}") + + except Exception as e: + logger.error(f"[AI对话] 发送失败: {e}, 降级为纯文本") + try: + await update.message.reply_text(response_text) + except: + await update.message.reply_text("抱歉,回复失败。请直接发送命令,如:/search AI") + + + + async def handle_search_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """处理搜索命令 - 带缓存""" + user = update.effective_user + user_id = user.id + command = update.message.text + + # 提取命令和关键词 + parts = command.split(maxsplit=1) + cmd = parts[0] + keyword = parts[1] if len(parts) > 1 else "" + + # 检查缓存 + if self.cache_db and keyword: + cached = self.cache_db.get_cache(cmd, keyword, 1) + if cached: + logger.info(f"返回缓存结果: {cmd} {keyword}") + await update.message.reply_text( + f"📦 从缓存返回结果:\n\n{cached['text'][:4000]}", + parse_mode='HTML' + ) + return + + # 通知管理员 + admin_notification = ( + f"🔍 用户执行搜索:\n" + f"👤 {user.first_name} {user.last_name or ''}\n" + f"🆔 {user_id}\n" + f"📝 {command}\n" + f"⏰ {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" + ) + await context.bot.send_message(chat_id=ADMIN_ID, text=admin_notification) + + wait_msg = await update.message.reply_text("🔍 正在搜索,请稍候...") + + self.user_search_sessions[user_id] = { + 'chat_id': update.effective_chat.id, + 'wait_msg_id': wait_msg.message_id, + 'command': cmd, + 'keyword': keyword, + 'timestamp': datetime.now() + } + + await self.pyrogram_client.send_message(self.target_bot_id, command) + logger.info(f"搜索: {command}") + + async def handle_search_response(self, message: PyrogramMessage, is_edit: bool = False): + """处理搜索机器人的响应 - 保存到缓存""" + try: + if not self.user_search_sessions: + return + + user_id = max(self.user_search_sessions.keys(), key=lambda k: self.user_search_sessions[k]['timestamp']) + session = self.user_search_sessions[user_id] + + text = message.text or message.caption or "无结果" + + try: + if message.text and hasattr(message.text, 'html'): + text = message.text.html + except: + pass + + keyboard = self.convert_keyboard(message) + + if is_edit and message.id in self.pyrogram_to_telegram: + telegram_msg_id = self.pyrogram_to_telegram[message.id] + await self.app.bot.edit_message_text( + chat_id=session['chat_id'], + message_id=telegram_msg_id, + text=text[:4000], + reply_markup=keyboard, + parse_mode='HTML' + ) + else: + try: + await self.app.bot.delete_message( + chat_id=session['chat_id'], + message_id=session['wait_msg_id'] + ) + except: + pass + + sent = await self.app.bot.send_message( + chat_id=session['chat_id'], + text=text[:4000], + reply_markup=keyboard, + parse_mode='HTML' + ) + + self.pyrogram_to_telegram[message.id] = sent.message_id + self.telegram_to_pyrogram[sent.message_id] = message.id + + # 保存到缓存 + if self.cache_db and session.get('keyword'): + buttons = self.extract_buttons(message) + self.cache_db.save_cache( + session['command'], + session['keyword'], + 1, # 第一页 + text, + text, + buttons + ) + + # 后台自动翻页(用户无感知) + if self.pagination_manager: + asyncio.create_task( + self.pagination_manager.start_pagination( + user_id, session['command'], session['keyword'], message + ) + ) + + except Exception as e: + logger.error(f"处理搜索响应失败: {e}") + + def convert_keyboard(self, message: PyrogramMessage) -> Optional[InlineKeyboardMarkup]: + """转换键盘""" + if not message.reply_markup or not message.reply_markup.inline_keyboard: + return None + + try: + buttons = [] + for row in message.reply_markup.inline_keyboard: + button_row = [] + for btn in row: + if btn.url: + button_row.append(InlineKeyboardButton(text=btn.text, url=btn.url)) + elif btn.callback_data: + callback_id = f"cb_{time.time():.0f}_{len(self.callback_data_map)}" + self.callback_data_map[callback_id] = (message.id, btn.callback_data) + button_row.append(InlineKeyboardButton(text=btn.text, callback_data=callback_id[:64])) + + if button_row: + buttons.append(button_row) + + return InlineKeyboardMarkup(buttons) if buttons else None + except Exception as e: + logger.error(f"键盘转换失败: {e}") + return None + + def extract_buttons(self, message: PyrogramMessage) -> list: + """提取按钮数据""" + if not message.reply_markup or not message.reply_markup.inline_keyboard: + return [] + + buttons = [] + for row in message.reply_markup.inline_keyboard: + for btn in row: + buttons.append({"text": btn.text, "url": btn.url if btn.url else None}) + return buttons + + async def handle_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """处理按钮点击 - 执行搜索命令""" + query = update.callback_query + data = query.data + user = query.from_user + + await query.answer() + + if data.startswith("cmd_"): + # 解析命令 + parts = data.replace("cmd_", "").split("_", 1) + cmd = parts[0] + keywords = parts[1].replace("_", " ") if len(parts) > 1 else "" + + # 构造完整命令 + command = f"/{cmd} {keywords}" if keywords else f"/{cmd}" + + logger.info(f"[用户 {user.id}] 点击按钮: {command}") + + # 显示执行提示 + await query.message.reply_text(f"🔍 正在执行:{command}\n请稍候...") + + # 转发到搜索bot + try: + await self.pyrogram_client.send_message(self.target_bot_id, command) + + # 记录搜索会话 + self.user_search_sessions[user.id] = { + 'chat_id': query.message.chat_id, + 'wait_msg_id': query.message.message_id + 1, + 'command': f"/{cmd}", + 'keyword': keywords, + 'timestamp': datetime.now() + } + + logger.info(f"[镜像] 已转发: {command}") + + except Exception as e: + logger.error(f"[镜像] 转发失败: {e}") + await query.message.reply_text("❌ 搜索失败,请稍后重试或直接发送命令") + + elif data == "cmd_help": + await query.message.reply_text( + "📖 使用指南:\n\n" + "• /search [关键词] - 按群组名称搜索\n" + "• /text [关键词] - 按消息内容搜索\n" + "• /human [关键词] - 按用户名搜索\n" + "• /topchat - 热门群组目录\n\n" + "💡 或者直接告诉我你想找什么!" + ) + + else: + logger.warning(f"未知callback: {data}") + + + async def handle_admin_reply(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """处理管理员回复""" + reply_to = update.message.reply_to_message + if not reply_to or not reply_to.text: + return + + import re + user_id = None + for line in reply_to.text.split('\n'): + if '🆔' in line or 'ID:' in line: + numbers = re.findall(r'\d+', line) + if numbers: + user_id = int(numbers[0]) + break + + if not user_id: + await update.message.reply_text("❌ 无法识别用户ID") + return + + try: + await context.bot.send_message(chat_id=user_id, text=update.message.text) + await update.message.reply_text(f"✅ 已回复给用户 {user_id}") + except Exception as e: + await update.message.reply_text(f"❌ 回复失败: {str(e)}") + + async def initialize(self): + """初始化机器人""" + try: + logger.info("正在初始化整合机器人...") + + if not await self.setup_pyrogram(): + logger.error("Pyrogram初始化失败") + return False + + builder = Application.builder().token(BOT_TOKEN) + + if os.environ.get('HTTP_PROXY'): + proxy_url = os.environ.get('HTTP_PROXY') + logger.info(f"配置Telegram Bot代理: {proxy_url}") + request = httpx.AsyncClient(proxies={"http://": proxy_url, "https://": proxy_url}, timeout=30.0) + builder = builder.request(request) + + self.app = builder.build() + + self.app.add_handler(CommandHandler("start", self.handle_start)) + self.app.add_handler(CallbackQueryHandler(self.handle_callback)) + self.app.add_handler(MessageHandler(tg_filters.ALL, self.handle_message)) + + logger.info("✅ 整合机器人初始化完成") + return True + + except Exception as e: + logger.error(f"初始化失败: {e}") + return False + + async def run(self): + """运行机器人""" + try: + await self.app.initialize() + await self.app.start() + await self.app.updater.start_polling(drop_pending_updates=True) + + logger.info("="*50) + logger.info("✅ AI增强版Bot已启动") + logger.info(f"AI服务: {MAC_API_URL}") + logger.info(f"缓存功能: {'启用' if self.cache_db else '禁用'}") + logger.info("="*50) + + await asyncio.Event().wait() + + except KeyboardInterrupt: + logger.info("收到停止信号") + finally: + await self.cleanup() + + async def cleanup(self): + """清理资源""" + logger.info("正在清理...") + + if self.app: + await self.app.updater.stop() + await self.app.stop() + await self.app.shutdown() + + if self.pyrogram_client: + await self.pyrogram_client.stop() + + logger.info("✅ 清理完成") + + +async def main(): + """主函数""" + bot = IntegratedBotAI() + + if await bot.initialize(): + await bot.run() + else: + logger.error("初始化失败,退出") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/integrated_bot_ai.backup.20251007_172359.py b/integrated_bot_ai.backup.20251007_172359.py new file mode 100755 index 0000000..ae047f2 --- /dev/null +++ b/integrated_bot_ai.backup.20251007_172359.py @@ -0,0 +1,865 @@ +#!/usr/bin/env python3 +""" +整合版客服机器人 - AI增强版 +包含: +1. AI对话引导 +2. 镜像搜索功能 +3. 自动翻页缓存 +4. 智能去重 +""" + +import asyncio +import logging +import time +import os +import httpx +# import anthropic # 已替换为 claude-agent-sdk +from claude_agent_wrapper import init_claude_agent +import json +import sys +from typing import Dict, Optional +from datetime import datetime + +# 添加路径 +sys.path.insert(0, "/home/atai/bot_data") + +# Pyrogram imports +from pyrogram import Client as PyrogramClient, filters +from pyrogram.types import Message as PyrogramMessage +from pyrogram.raw.functions.messages import GetBotCallbackAnswer + +# Telegram Bot imports +from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup +from telegram.ext import Application, CommandHandler, MessageHandler, CallbackQueryHandler, filters as tg_filters +from telegram.ext import ContextTypes + +# 导入数据库 +try: + from database import CacheDatabase +except ImportError: + CacheDatabase = None + logging.warning("database.py未找到,缓存功能将禁用") + +# ================== 配置 ================== +API_ID = 24660516 +API_HASH = "eae564578880a59c9963916ff1bbbd3a" +SESSION_NAME = "user_session" +BOT_TOKEN = "8426529617:AAHAxzohSMFBAxInzbAVJsZfkB5bHnOyFC4" +TARGET_BOT = "@openaiw_bot" +ADMIN_ID = 7363537082 + +# AI服务配置 +MAC_API_URL = "http://192.168.9.10:8000" + +# 搜索命令列表 +SEARCH_COMMANDS = ['/topchat', '/search', '/text', '/human'] + +# 日志配置 +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +# 初始化Claude Agent SDK客户端 +try: + claude_client = init_claude_agent() + if claude_client: + logger.info("✅ Claude Agent SDK客户端已初始化") + else: + logger.error("❌ Claude Agent SDK初始化失败") + claude_client = None +except Exception as e: + logger.error(f"❌ Claude Agent SDK初始化失败: {e}") + claude_client = None + + + + +# ================== 对话管理 ================== +class ConversationManager: + """管理用户对话上下文""" + + def __init__(self, max_history=5): + self.conversations = {} + self.max_history = max_history + + def add_message(self, user_id: int, role: str, content: str): + """添加消息到历史""" + if user_id not in self.conversations: + self.conversations[user_id] = [] + + self.conversations[user_id].append({ + "role": role, + "content": content, + "timestamp": datetime.now().isoformat() + }) + + # 保持最近的N条消息 + if len(self.conversations[user_id]) > self.max_history * 2: + self.conversations[user_id] = self.conversations[user_id][-self.max_history * 2:] + + def get_history(self, user_id: int, limit: int = 2) -> list: + """获取用户对话历史""" + if user_id not in self.conversations: + return [] + + history = self.conversations[user_id][-limit * 2:] + return [{"role": msg["role"], "content": msg["content"]} for msg in history] + + def clear_history(self, user_id: int): + """清空用户历史""" + if user_id in self.conversations: + del self.conversations[user_id] + + +# ================== 自动翻页管理器 ================== +class AutoPaginationManager: + """后台自动翻页 - 用户无感知""" + + def __init__(self, pyrogram_client, cache_db, target_bot_id, logger): + self.pyrogram_client = pyrogram_client + self.cache_db = cache_db + self.target_bot_id = target_bot_id + self.logger = logger + self.active_tasks = {} + + async def start_pagination(self, user_id, command, keyword, first_message): + """启动后台翻页任务""" + if user_id in self.active_tasks: + return + + task = asyncio.create_task(self._paginate(user_id, command, keyword, first_message)) + self.active_tasks[user_id] = task + self.logger.info(f"[翻页] 后台任务启动: {command} {keyword}") + + async def _paginate(self, user_id, command, keyword, message): + """执行翻页""" + try: + page = 1 + self._save_to_cache(command, keyword, page, message) + + if not self._has_next(message): + self.logger.info(f"[翻页] 只有1页") + return + + current = message + page = 2 + max_pages = 100 # 最多100页防止死循环 + + while page <= max_pages: + await asyncio.sleep(2) + + next_msg = await self._click_next(current) + if not next_msg: + self.logger.info(f"[翻页] 点击失败,停止翻页") + break + + self._save_to_cache(command, keyword, page, next_msg) + self.logger.info(f"[翻页] 第{page}页已保存") + + if not self._has_next(next_msg): + self.logger.info(f"[翻页] ✅ 完成,共{page}页") + break + + current = next_msg + page += 1 + + except Exception as e: + self.logger.error(f"[翻页] 错误: {e}") + finally: + if user_id in self.active_tasks: + del self.active_tasks[user_id] + + def _has_next(self, msg): + """检查是否有下一页""" + if not msg.reply_markup: + return False + for row in msg.reply_markup.inline_keyboard: + for btn in row: + if btn.text: + text = btn.text.strip() + # 检查右箭头(排除左箭头) + if any(arrow in text for arrow in ["➡️", "▶", "→", "»"]): + if not any(prev in text for prev in ["⬅️", "◀", "←", "«"]): + return True + # 检查文字 + if any(x in text for x in ["下一页", "Next"]): + return True + return False + async def _click_next(self, msg): + """点击下一页""" + try: + from pyrogram.raw.functions.messages import GetBotCallbackAnswer + + for row in msg.reply_markup.inline_keyboard: + for btn in row: + if btn.text and btn.callback_data: + text = btn.text.strip() + # 检查是否是下一页按钮 + is_next = False + if any(arrow in text for arrow in ['➡️', '▶', '→', '»']): + if not any(prev in text for prev in ['⬅️', '◀', '←', '«']): + is_next = True + if any(x in text for x in ['下一页', 'Next']): + is_next = True + if is_next: + await self.pyrogram_client.invoke( + GetBotCallbackAnswer( + peer=await self.pyrogram_client.resolve_peer(self.target_bot_id), + msg_id=msg.id, + data=btn.callback_data + ) + ) + await asyncio.sleep(1.5) + return await self.pyrogram_client.get_messages(self.target_bot_id, msg.id) + except Exception as e: + self.logger.error(f"[翻页] 点击失败: {e}") + return None + + def _save_to_cache(self, cmd, keyword, page, msg): + """保存到缓存""" + if not self.cache_db: + return + try: + text = msg.text or msg.caption or "" + buttons = [] + if msg.reply_markup: + for row in msg.reply_markup.inline_keyboard: + for btn in row: + buttons.append({"text": btn.text, "url": btn.url if btn.url else None}) + self.cache_db.save_cache(cmd, keyword, page, text, result_html=None, buttons=buttons) + except Exception as e: + self.logger.error(f"[翻页] 保存失败: {e}") + + +class IntegratedBotAI: + """整合的客服机器人 - AI增强版""" + + def __init__(self): + # Bot应用 + self.app = None + + # Pyrogram客户端(用于镜像) + self.pyrogram_client: Optional[PyrogramClient] = None + self.target_bot_id: Optional[int] = None + + # 消息映射 + self.pyrogram_to_telegram = {} + self.telegram_to_pyrogram = {} + self.callback_data_map = {} + self.user_search_sessions = {} + + # AI会话状态 + self.user_ai_sessions = {} + + # 缓存数据库 + self.cache_db = CacheDatabase() if CacheDatabase else None + + # 对话管理器 + self.conversation_manager = ConversationManager() + self.pagination_manager = None + + async def setup_pyrogram(self): + """设置Pyrogram客户端""" + try: + proxy_config = None + if os.environ.get('ALL_PROXY'): + proxy_url = os.environ.get('ALL_PROXY', '').replace('socks5://', '') + if proxy_url: + host, port = proxy_url.split(':') + proxy_config = {"scheme": "socks5", "hostname": host, "port": int(port)} + + self.pyrogram_client = PyrogramClient( + SESSION_NAME, api_id=API_ID, api_hash=API_HASH, + proxy=proxy_config if proxy_config else None + ) + + await self.pyrogram_client.start() + logger.info("✅ Pyrogram客户端已启动") + + # 初始化自动翻页管理器 + self.pagination_manager = AutoPaginationManager( + self.pyrogram_client, self.cache_db, self.target_bot_id, logger + ) + logger.info("✅ 自动翻页管理器已初始化") + + target = await self.pyrogram_client.get_users(TARGET_BOT) + self.target_bot_id = target.id + logger.info(f"✅ 已连接到搜索机器人: {target.username}") + + @self.pyrogram_client.on_message(filters.user(self.target_bot_id)) + async def on_bot_response(_, message: PyrogramMessage): + await self.handle_search_response(message) + + @self.pyrogram_client.on_edited_message(filters.user(self.target_bot_id)) + async def on_message_edited(_, message: PyrogramMessage): + await self.handle_search_response(message, is_edit=True) + + return True + except Exception as e: + logger.error(f"Pyrogram设置失败: {e}") + return False + + async def call_ai_service(self, user_id: int, message: str, context: dict = None) -> dict: + """优化的Claude API调用 - 带上下文记忆和改进提示词""" + + if not claude_client: + logger.error("Claude客户端未初始化") + return { + "type": "auto", + "response": "👋 我来帮你搜索!\n\n直接发关键词,或试试:\n• /search 群组名\n• /text 讨论内容\n• /topchat 热门分类", + "confidence": 0.3 + } + + try: + logger.info(f"[用户 {user_id}] 调用Claude API: {message}") + + username = context.get('username', f'user_{user_id}') if context else f'user_{user_id}' + first_name = context.get('first_name', '') if context else '' + + # 构建对话历史 + messages = [] + + # 添加历史对话(最近2轮) + history = self.conversation_manager.get_history(user_id, limit=2) + messages.extend(history) + + # 添加当前消息(优化的提示词) + current_prompt = f"""你是@ktfund_bot的AI助手,专业的Telegram群组搜索助手。 + +【重要】你的回复中可以包含可执行的命令,我会为它们生成按钮。 +命令格式:/search 关键词 或 /text 关键词 + +用户信息:@{username} ({first_name}) +用户说:"{message}" + +【可用工具】 +• /search [关键词] - 搜索群组名称 +• /text [关键词] - 搜索讨论内容 +• /human [关键词] - 搜索用户 +• /topchat - 热门分类 + +【回复要求】 +1. 简短友好(2-4行) +2. 给1-2个具体命令建议 +3. 口语化,像朋友聊天 +4. 命令要在独立的一行 + +【示例】 +用户:"找AI群" +回复: +找AI群的话,试试: +/search AI +/text ChatGPT + +直接回复:""" + + messages.append({ + "role": "user", + "content": current_prompt + }) + + # 调用Claude Agent SDK + claude_response = await claude_client.chat_async( + messages=messages, + model="claude-sonnet-4-5-20250929", + max_tokens=512, + temperature=0.7 + ) + + # 保存对话历史 + self.conversation_manager.add_message(user_id, "user", message) + self.conversation_manager.add_message(user_id, "assistant", claude_response) + + logger.info(f"[用户 {user_id}] ✅ Claude回复成功 ({len(claude_response)}字)") + + # 智能提取命令建议 + suggested_commands = self._extract_commands(claude_response) + + return { + "type": "ai", + "response": claude_response, + "confidence": 1.0, + "suggested_commands": suggested_commands + } + + except Exception as e: + logger.error(f"[用户 {user_id}] ❌ Claude API失败: {e}") + return { + "type": "auto", + "response": "👋 我来帮你搜索!\n\n直接发关键词,或试试:\n• /search 群组名\n• /text 讨论内容\n• /topchat 热门分类", + "confidence": 0.3 + } + + def _extract_commands(self, response_text: str) -> list: + """从回复中提取建议的命令""" + import re + commands = [] + + # 匹配 /command pattern + patterns = [ + r'/search\s+[\w\s]+', + r'/text\s+[\w\s]+', + r'/human\s+[\w\s]+', + r'/topchat' + ] + + for pattern in patterns: + matches = re.findall(pattern, response_text) + commands.extend([m.strip() for m in matches[:1]]) + + return commands[:2] + + + + def _extract_command_buttons(self, text: str) -> list: + """从AI回复中提取命令按钮""" + import re + buttons = [] + + # 匹配:/command keyword + pattern = r'/(search|text|human|topchat)\s*([^\n]*)' + matches = re.findall(pattern, text, re.IGNORECASE) + + for cmd, keywords in matches[:3]: + cmd = cmd.lower() + keywords = keywords.strip()[:30] # 限制长度 + + if keywords: + display = f"/{cmd} {keywords}" + callback = f"cmd_{cmd}_{keywords.replace(' ', '_')}"[:64] + else: + display = f"/{cmd}" + callback = f"cmd_{cmd}" + + buttons.append((display, callback)) + + return buttons + + async def handle_start(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """处理/start命令 - AI引导模式""" + user = update.effective_user + user_id = user.id + + self.user_ai_sessions[user_id] = {"started_at": datetime.now(), "conversation": []} + + welcome_text = ( + f"👋 您好 {user.first_name}!\n\n" + "我是智能搜索助手,可以帮您找到Telegram上的群组和频道。\n\n" + "🔍 我能做什么:\n" + "• 搜索群组/频道\n" + "• 搜索特定话题的讨论\n" + "• 查找用户\n" + "• 浏览热门分类\n\n" + "💬 直接告诉我您想找什么,我会帮您选择最合适的搜索方式!" + ) + + keyboard = [ + [InlineKeyboardButton("🔍 搜索群组", callback_data="quick_search"), + InlineKeyboardButton("📚 使用指南", callback_data="quick_help")], + [InlineKeyboardButton("🔥 热门分类", callback_data="quick_topchat")] + ] + + await update.message.reply_text(welcome_text, reply_markup=InlineKeyboardMarkup(keyboard)) + + # 通知管理员 + admin_notification = ( + f"🆕 新用户访问 (AI模式):\n" + f"👤 {user.first_name} {user.last_name or ''}\n" + f"🆔 {user.id}\n" + f"👤 @{user.username or '无'}\n" + f"⏰ {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" + ) + await context.bot.send_message(chat_id=ADMIN_ID, text=admin_notification) + + async def handle_message(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """处理所有消息 - AI智能路由""" + if not update.message or not update.message.text: + return + + user = update.effective_user + user_id = user.id + text = update.message.text + is_admin = user_id == ADMIN_ID + + if is_admin and update.message.reply_to_message: + await self.handle_admin_reply(update, context) + return + + if self.is_search_command(text): + await self.handle_search_command(update, context) + return + + await self.handle_ai_conversation(update, context) + + def is_search_command(self, text: str) -> bool: + """检查是否是搜索命令""" + return text and text.split()[0] in SEARCH_COMMANDS + + async def handle_ai_conversation(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """AI对话处理 - 带智能按钮""" + user = update.effective_user + user_id = user.id + message = update.message.text + + # 显示"正在输入" + await update.message.chat.send_action("typing") + + # 构建上下文 + user_context = { + "username": user.username or f"user{user_id}", + "first_name": user.first_name or "朋友", + "last_name": user.last_name + } + + # 调用AI + ai_response = await self.call_ai_service(user_id, message, user_context) + response_text = ai_response.get("response", "") + + # 提取命令按钮 + buttons = self._extract_command_buttons(response_text) + + try: + if buttons: + # 构建按钮键盘 + keyboard = [] + for display, callback in buttons: + keyboard.append([InlineKeyboardButton( + f"🔍 {display}", + callback_data=callback + )]) + + # 添加常用按钮 + keyboard.append([ + InlineKeyboardButton("🔥 热门目录", callback_data="cmd_topchat"), + InlineKeyboardButton("📖 帮助", callback_data="cmd_help") + ]) + + await update.message.reply_text( + response_text, + reply_markup=InlineKeyboardMarkup(keyboard) + ) + logger.info(f"[AI对话] 已回复用户 {user_id} (带{len(buttons)}个按钮)") + else: + # 无按钮版本 + await update.message.reply_text(response_text) + logger.info(f"[AI对话] 已回复用户 {user_id}") + + except Exception as e: + logger.error(f"[AI对话] 发送失败: {e}, 降级为纯文本") + try: + await update.message.reply_text(response_text) + except: + await update.message.reply_text("抱歉,回复失败。请直接发送命令,如:/search AI") + + + + async def handle_search_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """处理搜索命令 - 带缓存""" + user = update.effective_user + user_id = user.id + command = update.message.text + + # 提取命令和关键词 + parts = command.split(maxsplit=1) + cmd = parts[0] + keyword = parts[1] if len(parts) > 1 else "" + + # 检查缓存 + if self.cache_db and keyword: + cached = self.cache_db.get_cache(cmd, keyword, 1) + if cached: + logger.info(f"返回缓存结果: {cmd} {keyword}") + await update.message.reply_text( + f"📦 从缓存返回结果:\n\n{cached['text'][:4000]}", + parse_mode='HTML' + ) + return + + # 通知管理员 + admin_notification = ( + f"🔍 用户执行搜索:\n" + f"👤 {user.first_name} {user.last_name or ''}\n" + f"🆔 {user_id}\n" + f"📝 {command}\n" + f"⏰ {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" + ) + await context.bot.send_message(chat_id=ADMIN_ID, text=admin_notification) + + wait_msg = await update.message.reply_text("🔍 正在搜索,请稍候...") + + self.user_search_sessions[user_id] = { + 'chat_id': update.effective_chat.id, + 'wait_msg_id': wait_msg.message_id, + 'command': cmd, + 'keyword': keyword, + 'timestamp': datetime.now() + } + + await self.pyrogram_client.send_message(self.target_bot_id, command) + logger.info(f"搜索: {command}") + + async def handle_search_response(self, message: PyrogramMessage, is_edit: bool = False): + """处理搜索机器人的响应 - 保存到缓存""" + try: + if not self.user_search_sessions: + return + + user_id = max(self.user_search_sessions.keys(), key=lambda k: self.user_search_sessions[k]['timestamp']) + session = self.user_search_sessions[user_id] + + text = message.text or message.caption or "无结果" + + try: + if message.text and hasattr(message.text, 'html'): + text = message.text.html + except: + pass + + keyboard = self.convert_keyboard(message) + + if is_edit and message.id in self.pyrogram_to_telegram: + telegram_msg_id = self.pyrogram_to_telegram[message.id] + await self.app.bot.edit_message_text( + chat_id=session['chat_id'], + message_id=telegram_msg_id, + text=text[:4000], + reply_markup=keyboard, + parse_mode='HTML' + ) + else: + try: + await self.app.bot.delete_message( + chat_id=session['chat_id'], + message_id=session['wait_msg_id'] + ) + except: + pass + + sent = await self.app.bot.send_message( + chat_id=session['chat_id'], + text=text[:4000], + reply_markup=keyboard, + parse_mode='HTML' + ) + + self.pyrogram_to_telegram[message.id] = sent.message_id + self.telegram_to_pyrogram[sent.message_id] = message.id + + # 保存到缓存 + if self.cache_db and session.get('keyword'): + buttons = self.extract_buttons(message) + self.cache_db.save_cache( + session['command'], + session['keyword'], + 1, # 第一页 + text, + text, + buttons + ) + + # 后台自动翻页(用户无感知) + if self.pagination_manager: + asyncio.create_task( + self.pagination_manager.start_pagination( + user_id, session['command'], session['keyword'], message + ) + ) + + except Exception as e: + logger.error(f"处理搜索响应失败: {e}") + + def convert_keyboard(self, message: PyrogramMessage) -> Optional[InlineKeyboardMarkup]: + """转换键盘""" + if not message.reply_markup or not message.reply_markup.inline_keyboard: + return None + + try: + buttons = [] + for row in message.reply_markup.inline_keyboard: + button_row = [] + for btn in row: + if btn.url: + button_row.append(InlineKeyboardButton(text=btn.text, url=btn.url)) + elif btn.callback_data: + callback_id = f"cb_{time.time():.0f}_{len(self.callback_data_map)}" + self.callback_data_map[callback_id] = (message.id, btn.callback_data) + button_row.append(InlineKeyboardButton(text=btn.text, callback_data=callback_id[:64])) + + if button_row: + buttons.append(button_row) + + return InlineKeyboardMarkup(buttons) if buttons else None + except Exception as e: + logger.error(f"键盘转换失败: {e}") + return None + + def extract_buttons(self, message: PyrogramMessage) -> list: + """提取按钮数据""" + if not message.reply_markup or not message.reply_markup.inline_keyboard: + return [] + + buttons = [] + for row in message.reply_markup.inline_keyboard: + for btn in row: + buttons.append({"text": btn.text, "url": btn.url if btn.url else None}) + return buttons + + async def handle_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """处理按钮点击 - 执行搜索命令""" + query = update.callback_query + data = query.data + user = query.from_user + + await query.answer() + + if data.startswith("cmd_"): + # 解析命令 + parts = data.replace("cmd_", "").split("_", 1) + cmd = parts[0] + keywords = parts[1].replace("_", " ") if len(parts) > 1 else "" + + # 构造完整命令 + command = f"/{cmd} {keywords}" if keywords else f"/{cmd}" + + logger.info(f"[用户 {user.id}] 点击按钮: {command}") + + # 显示执行提示 + await query.message.reply_text(f"🔍 正在执行:{command}\n请稍候...") + + # 转发到搜索bot + try: + await self.pyrogram_client.send_message(self.target_bot_id, command) + + # 记录搜索会话 + self.user_search_sessions[user.id] = { + 'chat_id': query.message.chat_id, + 'wait_msg_id': query.message.message_id + 1, + 'command': f"/{cmd}", + 'keyword': keywords, + 'timestamp': datetime.now() + } + + logger.info(f"[镜像] 已转发: {command}") + + except Exception as e: + logger.error(f"[镜像] 转发失败: {e}") + await query.message.reply_text("❌ 搜索失败,请稍后重试或直接发送命令") + + elif data == "cmd_help": + await query.message.reply_text( + "📖 使用指南:\n\n" + "• /search [关键词] - 按群组名称搜索\n" + "• /text [关键词] - 按消息内容搜索\n" + "• /human [关键词] - 按用户名搜索\n" + "• /topchat - 热门群组目录\n\n" + "💡 或者直接告诉我你想找什么!" + ) + + else: + logger.warning(f"未知callback: {data}") + + + async def handle_admin_reply(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """处理管理员回复""" + reply_to = update.message.reply_to_message + if not reply_to or not reply_to.text: + return + + import re + user_id = None + for line in reply_to.text.split('\n'): + if '🆔' in line or 'ID:' in line: + numbers = re.findall(r'\d+', line) + if numbers: + user_id = int(numbers[0]) + break + + if not user_id: + await update.message.reply_text("❌ 无法识别用户ID") + return + + try: + await context.bot.send_message(chat_id=user_id, text=update.message.text) + await update.message.reply_text(f"✅ 已回复给用户 {user_id}") + except Exception as e: + await update.message.reply_text(f"❌ 回复失败: {str(e)}") + + async def initialize(self): + """初始化机器人""" + try: + logger.info("正在初始化整合机器人...") + + if not await self.setup_pyrogram(): + logger.error("Pyrogram初始化失败") + return False + + builder = Application.builder().token(BOT_TOKEN) + + if os.environ.get('HTTP_PROXY'): + proxy_url = os.environ.get('HTTP_PROXY') + logger.info(f"配置Telegram Bot代理: {proxy_url}") + request = httpx.AsyncClient(proxies={"http://": proxy_url, "https://": proxy_url}, timeout=30.0) + builder = builder.request(request) + + self.app = builder.build() + + self.app.add_handler(CommandHandler("start", self.handle_start)) + self.app.add_handler(CallbackQueryHandler(self.handle_callback)) + self.app.add_handler(MessageHandler(tg_filters.ALL, self.handle_message)) + + logger.info("✅ 整合机器人初始化完成") + return True + + except Exception as e: + logger.error(f"初始化失败: {e}") + return False + + async def run(self): + """运行机器人""" + try: + await self.app.initialize() + await self.app.start() + await self.app.updater.start_polling(drop_pending_updates=True) + + logger.info("="*50) + logger.info("✅ AI增强版Bot已启动") + logger.info(f"AI服务: {MAC_API_URL}") + logger.info(f"缓存功能: {'启用' if self.cache_db else '禁用'}") + logger.info("="*50) + + await asyncio.Event().wait() + + except KeyboardInterrupt: + logger.info("收到停止信号") + finally: + await self.cleanup() + + async def cleanup(self): + """清理资源""" + logger.info("正在清理...") + + if self.app: + await self.app.updater.stop() + await self.app.stop() + await self.app.shutdown() + + if self.pyrogram_client: + await self.pyrogram_client.stop() + + logger.info("✅ 清理完成") + + +async def main(): + """主函数""" + bot = IntegratedBotAI() + + if await bot.initialize(): + await bot.run() + else: + logger.error("初始化失败,退出") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/integrated_bot_ai.backup.20251008_065416.py b/integrated_bot_ai.backup.20251008_065416.py new file mode 100755 index 0000000..f0d3aa0 --- /dev/null +++ b/integrated_bot_ai.backup.20251008_065416.py @@ -0,0 +1,956 @@ +#!/usr/bin/env python3 +""" +整合版客服机器人 - AI增强版 +包含: +1. AI对话引导 +2. 镜像搜索功能 +3. 自动翻页缓存 +4. 智能去重 +""" + +import asyncio +import logging +import time +import os +import httpx +import anthropic +import json +import sys +from typing import Dict, Optional +from datetime import datetime + +# 添加路径 +sys.path.insert(0, "/home/atai/bot_data") + +# Pyrogram imports +from pyrogram import Client as PyrogramClient, filters +from pyrogram.types import Message as PyrogramMessage +from pyrogram.raw.functions.messages import GetBotCallbackAnswer + +# Telegram Bot imports +from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup +from telegram.ext import Application, CommandHandler, MessageHandler, CallbackQueryHandler, filters as tg_filters +from telegram.ext import ContextTypes + +# 导入数据库 +try: + from database import CacheDatabase +except ImportError: + CacheDatabase = None + logging.warning("database.py未找到,缓存功能将禁用") + +# ================== 配置 ================== +API_ID = 24660516 +API_HASH = "eae564578880a59c9963916ff1bbbd3a" +SESSION_NAME = "user_session" +BOT_TOKEN = "8426529617:AAHAxzohSMFBAxInzbAVJsZfkB5bHnOyFC4" +TARGET_BOT = "@openaiw_bot" +ADMIN_ID = 7363537082 + +# AI服务配置 +MAC_API_URL = "http://192.168.9.10:8000" + +# 搜索命令列表 +SEARCH_COMMANDS = ['/topchat', '/search', '/text', '/human'] + +# 日志配置 +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +# 初始化Claude客户端 +try: + claude_client = anthropic.Anthropic( + api_key=os.environ.get('ANTHROPIC_AUTH_TOKEN'), + base_url=os.environ.get('ANTHROPIC_BASE_URL', 'https://api.anthropic.com') + ) + logger.info("✅ Claude API客户端已初始化") +except Exception as e: + logger.error(f"❌ Claude API初始化失败: {e}") + claude_client = None + + + + +# ================== 对话管理 ================== +class ConversationManager: + """管理用户对话上下文""" + + def __init__(self, max_history=5): + self.conversations = {} + self.max_history = max_history + + def add_message(self, user_id: int, role: str, content: str): + """添加消息到历史""" + if user_id not in self.conversations: + self.conversations[user_id] = [] + + self.conversations[user_id].append({ + "role": role, + "content": content, + "timestamp": datetime.now().isoformat() + }) + + # 保持最近的N条消息 + if len(self.conversations[user_id]) > self.max_history * 2: + self.conversations[user_id] = self.conversations[user_id][-self.max_history * 2:] + + def get_history(self, user_id: int, limit: int = 2) -> list: + """获取用户对话历史""" + if user_id not in self.conversations: + return [] + + history = self.conversations[user_id][-limit * 2:] + return [{"role": msg["role"], "content": msg["content"]} for msg in history] + + def clear_history(self, user_id: int): + """清空用户历史""" + if user_id in self.conversations: + del self.conversations[user_id] + + +# ================== 自动翻页管理器 ================== +class AutoPaginationManager: + """后台自动翻页 - 用户无感知""" + + def __init__(self, pyrogram_client, cache_db, target_bot_id, logger): + self.pyrogram_client = pyrogram_client + self.cache_db = cache_db + self.target_bot_id = target_bot_id + self.logger = logger + self.active_tasks = {} + + async def start_pagination(self, user_id, command, keyword, first_message): + """启动后台翻页任务""" + if user_id in self.active_tasks: + return + + task = asyncio.create_task(self._paginate(user_id, command, keyword, first_message)) + self.active_tasks[user_id] = task + self.logger.info(f"[翻页] 后台任务启动: {command} {keyword}") + + async def _paginate(self, user_id, command, keyword, message): + """执行翻页""" + try: + page = 1 + self._save_to_cache(command, keyword, page, message) + + if not self._has_next(message): + self.logger.info(f"[翻页] 只有1页") + return + + current = message + for page in range(2, 11): # 最多10页 + await asyncio.sleep(2) + + next_msg = await self._click_next(current) + if not next_msg: + break + + self._save_to_cache(command, keyword, page, next_msg) + self.logger.info(f"[翻页] 第{page}页已保存") + + if not self._has_next(next_msg): + self.logger.info(f"[翻页] 完成,共{page}页") + break + + current = next_msg + + except Exception as e: + self.logger.error(f"[翻页] 错误: {e}") + finally: + if user_id in self.active_tasks: + del self.active_tasks[user_id] + + def _has_next(self, msg): + """检查是否有下一页""" + if not msg.reply_markup: + return False + for row in msg.reply_markup.inline_keyboard: + for btn in row: + if btn.text and any(x in btn.text for x in ['下一页', 'Next', '▶']): + return True + return False + + async def _click_next(self, msg): + """点击下一页""" + try: + from pyrogram.raw.functions.messages import GetBotCallbackAnswer + + for row in msg.reply_markup.inline_keyboard: + for btn in row: + if btn.text and any(x in btn.text for x in ['下一页', 'Next', '▶']): + await self.pyrogram_client.invoke( + GetBotCallbackAnswer( + peer=await self.pyrogram_client.resolve_peer(self.target_bot_id), + msg_id=msg.id, + data=btn.callback_data + ) + ) + await asyncio.sleep(1.5) + return await self.pyrogram_client.get_messages(self.target_bot_id, msg.id) + except Exception as e: + self.logger.error(f"[翻页] 点击失败: {e}") + return None + + def _save_to_cache(self, cmd, keyword, page, msg): + """保存到缓存""" + if not self.cache_db: + return + try: + text = msg.text or msg.caption or "" + buttons = [] + if msg.reply_markup: + for row in msg.reply_markup.inline_keyboard: + for btn in row: + buttons.append({"text": btn.text, "url": btn.url if btn.url else None}) + self.cache_db.save_cache(cmd, keyword, page, text, buttons) + except Exception as e: + self.logger.error(f"[翻页] 保存失败: {e}") + + +class IntegratedBotAI: + """整合的客服机器人 - AI增强版""" + + def __init__(self): + # Bot应用 + self.app = None + + # Pyrogram客户端(用于镜像) + self.pyrogram_client: Optional[PyrogramClient] = None + self.target_bot_id: Optional[int] = None + + # 消息映射 + self.pyrogram_to_telegram = {} + self.telegram_to_pyrogram = {} + self.callback_data_map = {} + self.user_search_sessions = {} + + # AI会话状态 + self.user_ai_sessions = {} + + # 缓存数据库 + self.cache_db = CacheDatabase() if CacheDatabase else None + + # 对话管理器 + self.conversation_manager = ConversationManager() + self.pagination_manager = None + + async def setup_pyrogram(self): + """设置Pyrogram客户端""" + try: + proxy_config = None + if os.environ.get('ALL_PROXY'): + proxy_url = os.environ.get('ALL_PROXY', '').replace('socks5://', '') + if proxy_url: + host, port = proxy_url.split(':') + proxy_config = {"scheme": "socks5", "hostname": host, "port": int(port)} + + self.pyrogram_client = PyrogramClient( + SESSION_NAME, api_id=API_ID, api_hash=API_HASH, + proxy=proxy_config if proxy_config else None + ) + + await self.pyrogram_client.start() + logger.info("✅ Pyrogram客户端已启动") + + # 初始化自动翻页管理器 + self.pagination_manager = AutoPaginationManager( + self.pyrogram_client, self.cache_db, self.target_bot_id, logger + ) + logger.info("✅ 自动翻页管理器已初始化") + + target = await self.pyrogram_client.get_users(TARGET_BOT) + self.target_bot_id = target.id + logger.info(f"✅ 已连接到搜索机器人: {target.username}") + + @self.pyrogram_client.on_message(filters.user(self.target_bot_id)) + async def on_bot_response(_, message: PyrogramMessage): + await self.handle_search_response(message) + + @self.pyrogram_client.on_edited_message(filters.user(self.target_bot_id)) + async def on_message_edited(_, message: PyrogramMessage): + await self.handle_search_response(message, is_edit=True) + + return True + except Exception as e: + logger.error(f"Pyrogram设置失败: {e}") + return False + + async def call_ai_service(self, user_id: int, message: str, context: dict = None) -> dict: + """优化的Claude API调用 - 带上下文记忆和改进提示词""" + + if not claude_client: + logger.error("Claude客户端未初始化") + return { + "type": "auto", + "response": "👋 我来帮你搜索!\n\n直接发关键词,或试试:\n• /search 群组名\n• /text 讨论内容\n• /topchat 热门分类", + "confidence": 0.3 + } + + try: + logger.info(f"[用户 {user_id}] 调用Claude API: {message}") + + username = context.get('username', f'user_{user_id}') if context else f'user_{user_id}' + first_name = context.get('first_name', '') if context else '' + + # 构建对话历史 + messages = [] + + # 添加历史对话(最近2轮) + history = self.conversation_manager.get_history(user_id, limit=2) + messages.extend(history) + + # 添加当前消息(优化的提示词) + current_prompt = f"""你是@ktfund_bot的AI助手,专业的Telegram群组搜索助手。 + +【重要】你的回复中可以包含可执行的命令,我会为它们生成按钮。 +命令格式:/search 关键词 或 /text 关键词 + +用户信息:@{username} ({first_name}) +用户说:"{message}" + +【可用工具】 +• /search [关键词] - 搜索群组名称 +• /text [关键词] - 搜索讨论内容 +• /human [关键词] - 搜索用户 +• /topchat - 热门分类 + +【回复要求】 +1. 简短友好(2-4行) +2. 给1-2个具体命令建议 +3. 口语化,像朋友聊天 +4. 命令要在独立的一行 + +【示例】 +用户:"找AI群" +回复: +找AI群的话,试试: +/search AI +/text ChatGPT + +直接回复:""" + + messages.append({ + "role": "user", + "content": current_prompt + }) + + # 调用Claude API + response = claude_client.messages.create( + model="claude-sonnet-4-20250514", + max_tokens=512, + temperature=0.7, + messages=messages + ) + + claude_response = response.content[0].text.strip() + + # 保存对话历史 + self.conversation_manager.add_message(user_id, "user", message) + self.conversation_manager.add_message(user_id, "assistant", claude_response) + + logger.info(f"[用户 {user_id}] ✅ Claude回复成功 ({len(claude_response)}字)") + + # 智能提取命令建议 + suggested_commands = self._extract_commands(claude_response) + + return { + "type": "ai", + "response": claude_response, + "confidence": 1.0, + "suggested_commands": suggested_commands + } + + except Exception as e: + logger.error(f"[用户 {user_id}] ❌ Claude API失败: {e}") + return { + "type": "auto", + "response": "👋 我来帮你搜索!\n\n直接发关键词,或试试:\n• /search 群组名\n• /text 讨论内容\n• /topchat 热门分类", + "confidence": 0.3 + } + + def _extract_commands(self, response_text: str) -> list: + """从回复中提取建议的命令""" + import re + commands = [] + + # 匹配 /command pattern + patterns = [ + r'/search\s+[\w\s]+', + r'/text\s+[\w\s]+', + r'/human\s+[\w\s]+', + r'/topchat' + ] + + for pattern in patterns: + matches = re.findall(pattern, response_text) + commands.extend([m.strip() for m in matches[:1]]) + + return commands[:2] + + + + def _extract_command_buttons(self, text: str) -> list: + """从AI回复中提取命令按钮""" + import re + buttons = [] + + # 匹配:/command keyword + pattern = r'/(search|text|human|topchat)\s*([^\n]*)' + matches = re.findall(pattern, text, re.IGNORECASE) + + for cmd, keywords in matches[:3]: + cmd = cmd.lower() + keywords = keywords.strip()[:30] # 限制长度 + + if keywords: + display = f"/{cmd} {keywords}" + callback = f"cmd_{cmd}_{keywords.replace(' ', '_')}"[:64] + else: + display = f"/{cmd}" + callback = f"cmd_{cmd}" + + buttons.append((display, callback)) + + return buttons + + async def handle_start(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """处理/start命令 - AI引导模式""" + user = update.effective_user + user_id = user.id + + self.user_ai_sessions[user_id] = {"started_at": datetime.now(), "conversation": []} + + welcome_text = ( + f"👋 您好 {user.first_name}!\n\n" + "我是智能搜索助手,可以帮您找到Telegram上的群组和频道。\n\n" + "🔍 我能做什么:\n" + "• 搜索群组/频道\n" + "• 搜索特定话题的讨论\n" + "• 查找用户\n" + "• 浏览热门分类\n\n" + "💬 直接告诉我您想找什么,我会帮您选择最合适的搜索方式!" + ) + + keyboard = [ + [InlineKeyboardButton("🔍 搜索群组", callback_data="quick_search"), + InlineKeyboardButton("📚 使用指南", callback_data="quick_help")], + [InlineKeyboardButton("🔥 热门分类", callback_data="quick_topchat")] + ] + + await update.message.reply_text(welcome_text, reply_markup=InlineKeyboardMarkup(keyboard)) + + # 通知管理员 + admin_notification = ( + f"🆕 新用户访问 (AI模式):\n" + f"👤 {user.first_name} {user.last_name or ''}\n" + f"🆔 {user.id}\n" + f"👤 @{user.username or '无'}\n" + f"⏰ {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" + ) + await context.bot.send_message(chat_id=ADMIN_ID, text=admin_notification) + + async def handle_message(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """处理所有消息 - AI智能路由""" + if not update.message or not update.message.text: + return + + user = update.effective_user + user_id = user.id + text = update.message.text + is_admin = user_id == ADMIN_ID + + if is_admin and update.message.reply_to_message: + await self.handle_admin_reply(update, context) + return + + if self.is_search_command(text): + await self.handle_search_command(update, context) + return + + await self.handle_ai_conversation(update, context) + + def is_search_command(self, text: str) -> bool: + """检查是否是搜索命令""" + return text and text.split()[0] in SEARCH_COMMANDS + + async def handle_ai_conversation(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """AI对话处理 - 带智能按钮""" + user = update.effective_user + user_id = user.id + message = update.message.text + + # 显示"正在输入" + await update.message.chat.send_action("typing") + + # 构建上下文 + user_context = { + "username": user.username or f"user{user_id}", + "first_name": user.first_name or "朋友", + "last_name": user.last_name + } + + # 调用AI + ai_response = await self.call_ai_service(user_id, message, user_context) + response_text = ai_response.get("response", "") + + # 提取命令按钮 + buttons = self._extract_command_buttons(response_text) + + try: + if buttons: + # 构建按钮键盘 + keyboard = [] + for display, callback in buttons: + keyboard.append([InlineKeyboardButton( + f"🔍 {display}", + callback_data=callback + )]) + + # 添加常用按钮 + keyboard.append([ + InlineKeyboardButton("🔥 热门目录", callback_data="cmd_topchat"), + InlineKeyboardButton("📖 帮助", callback_data="cmd_help") + ]) + + await update.message.reply_text( + response_text, + reply_markup=InlineKeyboardMarkup(keyboard) + ) + logger.info(f"[AI对话] 已回复用户 {user_id} (带{len(buttons)}个按钮)") + else: + # 无按钮版本 + await update.message.reply_text(response_text) + logger.info(f"[AI对话] 已回复用户 {user_id}") + + except Exception as e: + logger.error(f"[AI对话] 发送失败: {e}, 降级为纯文本") + try: + await update.message.reply_text(response_text) + except: + await update.message.reply_text("抱歉,回复失败。请直接发送命令,如:/search AI") + + + + async def handle_search_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """处理搜索命令 - 带缓存""" + user = update.effective_user + user_id = user.id + command = update.message.text + + # 提取命令和关键词 + parts = command.split(maxsplit=1) + cmd = parts[0] + keyword = parts[1] if len(parts) > 1 else "" + + # 检查缓存 + if self.cache_db and keyword: + cached = self.cache_db.get_cache(cmd, keyword, 1) + if cached: + logger.info(f"[缓存命中] {cmd} {keyword} page1") + + # 恢复按钮 + keyboard = None + if cached.get('buttons'): + buttons = [] + for btn_data in cached['buttons']: + if btn_data.get('url'): + buttons.append([InlineKeyboardButton(text=btn_data['text'], url=btn_data['url'])]) + elif btn_data.get('callback_data'): + # 生成callback_id + callback_id = f"cb_{time.time():.0f}_{len(self.callback_data_map)}" + # 需要存储原始message_id,这里用0作为占位符,实际翻页时从缓存获取 + self.callback_data_map[callback_id] = (0, btn_data['callback_data']) + buttons.append([InlineKeyboardButton(text=btn_data['text'], callback_data=callback_id[:64])]) + + if buttons: + keyboard = InlineKeyboardMarkup(buttons) + + # 发送缓存结果(带按钮) + sent = await update.message.reply_text( + cached['text'][:4000], + reply_markup=keyboard, + parse_mode='HTML' + ) + + # 记录会话,以便翻页时使用 + self.user_search_sessions[user_id] = { + 'chat_id': update.effective_chat.id, + 'wait_msg_id': sent.message_id, + 'command': cmd, + 'keyword': keyword, + 'timestamp': datetime.now() + } + + return + + # 通知管理员 + admin_notification = ( + f"🔍 用户执行搜索:\n" + f"👤 {user.first_name} {user.last_name or ''}\n" + f"🆔 {user_id}\n" + f"📝 {command}\n" + f"⏰ {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" + ) + await context.bot.send_message(chat_id=ADMIN_ID, text=admin_notification) + + wait_msg = await update.message.reply_text("🔍 正在搜索,请稍候...") + + self.user_search_sessions[user_id] = { + 'chat_id': update.effective_chat.id, + 'wait_msg_id': wait_msg.message_id, + 'command': cmd, + 'keyword': keyword, + 'timestamp': datetime.now() + } + + await self.pyrogram_client.send_message(self.target_bot_id, command) + logger.info(f"搜索: {command}") + + async def handle_search_response(self, message: PyrogramMessage, is_edit: bool = False): + """处理搜索机器人的响应 - 保存到缓存""" + try: + if not self.user_search_sessions: + return + + user_id = max(self.user_search_sessions.keys(), key=lambda k: self.user_search_sessions[k]['timestamp']) + session = self.user_search_sessions[user_id] + + text = message.text or message.caption or "无结果" + + try: + if message.text and hasattr(message.text, 'html'): + text = message.text.html + except: + pass + + keyboard = self.convert_keyboard(message) + + if is_edit and message.id in self.pyrogram_to_telegram: + telegram_msg_id = self.pyrogram_to_telegram[message.id] + await self.app.bot.edit_message_text( + chat_id=session['chat_id'], + message_id=telegram_msg_id, + text=text[:4000], + reply_markup=keyboard, + parse_mode='HTML' + ) + else: + try: + await self.app.bot.delete_message( + chat_id=session['chat_id'], + message_id=session['wait_msg_id'] + ) + except: + pass + + sent = await self.app.bot.send_message( + chat_id=session['chat_id'], + text=text[:4000], + reply_markup=keyboard, + parse_mode='HTML' + ) + + self.pyrogram_to_telegram[message.id] = sent.message_id + self.telegram_to_pyrogram[sent.message_id] = message.id + + # 保存到缓存 + if self.cache_db and session.get('keyword'): + buttons = self.extract_buttons(message) + self.cache_db.save_cache( + session['command'], + session['keyword'], + 1, # 第一页 + text, + text, + buttons + ) + + # 后台自动翻页(用户无感知) + if self.pagination_manager: + asyncio.create_task( + self.pagination_manager.start_pagination( + user_id, session['command'], session['keyword'], message + ) + ) + + except Exception as e: + logger.error(f"处理搜索响应失败: {e}") + + def convert_keyboard(self, message: PyrogramMessage) -> Optional[InlineKeyboardMarkup]: + """转换键盘""" + if not message.reply_markup or not message.reply_markup.inline_keyboard: + return None + + try: + buttons = [] + for row in message.reply_markup.inline_keyboard: + button_row = [] + for btn in row: + if btn.url: + button_row.append(InlineKeyboardButton(text=btn.text, url=btn.url)) + elif btn.callback_data: + callback_id = f"cb_{time.time():.0f}_{len(self.callback_data_map)}" + self.callback_data_map[callback_id] = (message.id, btn.callback_data) + button_row.append(InlineKeyboardButton(text=btn.text, callback_data=callback_id[:64])) + + if button_row: + buttons.append(button_row) + + return InlineKeyboardMarkup(buttons) if buttons else None + except Exception as e: + logger.error(f"键盘转换失败: {e}") + return None + + def extract_buttons(self, message: PyrogramMessage) -> list: + """提取按钮数据(包含callback_data用于缓存)""" + if not message.reply_markup or not message.reply_markup.inline_keyboard: + return [] + + buttons = [] + for row in message.reply_markup.inline_keyboard: + for btn in row: + btn_data = {"text": btn.text} + if btn.url: + btn_data["url"] = btn.url + if btn.callback_data: + btn_data["callback_data"] = btn.callback_data + buttons.append(btn_data) + return buttons + + async def handle_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """处理按钮点击 - 执行搜索命令或翻页""" + query = update.callback_query + data = query.data + user = query.from_user + + logger.info(f"[回调] 收到callback: user={user.id}, data={data}") + + await query.answer() + + if data.startswith("cb_"): + # 处理翻页按钮 + if data in self.callback_data_map: + orig_msg_id, orig_callback = self.callback_data_map[data] + logger.info(f"[翻页] 用户 {user.id} 点击: {orig_callback}") + + # 解析callback_data获取页码(格式如:page_2) + try: + if orig_callback.startswith("page_"): + page = int(orig_callback.split("_")[1]) + + # 从会话获取搜索信息 + session = self.user_search_sessions.get(user.id) + if session and 'command' in session and 'keyword' in session: + cmd = session['command'] + keyword = session['keyword'] + + # 先检查缓存 + cached = self.cache_db.get_cache(cmd, keyword, page) + if cached: + logger.info(f"[翻页缓存] 命中: {cmd} {keyword} page{page}") + + # 从缓存恢复按钮 + keyboard = None + if cached.get('buttons'): + buttons = [] + for btn_data in cached['buttons']: + if btn_data.get('url'): + buttons.append([InlineKeyboardButton(text=btn_data['text'], url=btn_data['url'])]) + elif btn_data.get('callback_data'): + # 重新生成callback_id + callback_id = f"cb_{time.time():.0f}_{len(self.callback_data_map)}" + self.callback_data_map[callback_id] = (orig_msg_id, btn_data['callback_data']) + buttons.append([InlineKeyboardButton(text=btn_data['text'], callback_data=callback_id[:64])]) + + if buttons: + keyboard = InlineKeyboardMarkup(buttons) + + # 发送缓存结果 + await query.message.edit_text( + text=cached['text'], + reply_markup=keyboard, + parse_mode='HTML' + ) + return + + else: + logger.info(f"[翻页] 缓存未命中,转发到搜索bot") + + # 如果缓存未命中或不是page_格式,转发到搜索bot + await self.pyrogram_client.request_callback_answer( + chat_id=self.target_bot_id, + message_id=orig_msg_id, + callback_data=orig_callback.encode() + ) + + # 记录等待响应 + self.user_search_sessions[user.id] = { + 'chat_id': query.message.chat_id, + 'wait_msg_id': query.message.message_id, + 'command': session.get('command') if session else None, + 'keyword': session.get('keyword') if session else None, + 'timestamp': datetime.now() + } + + logger.info(f"[翻页] 已转发callback到搜索bot") + + except Exception as e: + logger.error(f"[翻页] 处理失败: {e}") + await query.message.reply_text("❌ 翻页失败,请稍后重试") + else: + logger.warning(f"[翻页] callback_id不存在: {data}") + await query.message.reply_text("❌ 按钮已过期,请重新搜索") + + elif data.startswith("cmd_"): + # 解析命令 + parts = data.replace("cmd_", "").split("_", 1) + cmd = parts[0] + keywords = parts[1].replace("_", " ") if len(parts) > 1 else "" + + # 构造完整命令 + command = f"/{cmd} {keywords}" if keywords else f"/{cmd}" + + logger.info(f"[用户 {user.id}] 点击按钮: {command}") + + # 显示执行提示 + await query.message.reply_text(f"🔍 正在执行:{command}\n请稍候...") + + # 转发到搜索bot + try: + await self.pyrogram_client.send_message(self.target_bot_id, command) + + # 记录搜索会话 + self.user_search_sessions[user.id] = { + 'chat_id': query.message.chat_id, + 'wait_msg_id': query.message.message_id + 1, + 'command': f"/{cmd}", + 'keyword': keywords, + 'timestamp': datetime.now() + } + + logger.info(f"[镜像] 已转发: {command}") + + except Exception as e: + logger.error(f"[镜像] 转发失败: {e}") + await query.message.reply_text("❌ 搜索失败,请稍后重试或直接发送命令") + + elif data == "cmd_help": + await query.message.reply_text( + "📖 使用指南:\n\n" + "• /search [关键词] - 按群组名称搜索\n" + "• /text [关键词] - 按消息内容搜索\n" + "• /human [关键词] - 按用户名搜索\n" + "• /topchat - 热门群组目录\n\n" + "💡 或者直接告诉我你想找什么!" + ) + + else: + logger.warning(f"未知callback: {data}") + + + async def handle_admin_reply(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """处理管理员回复""" + reply_to = update.message.reply_to_message + if not reply_to or not reply_to.text: + return + + import re + user_id = None + for line in reply_to.text.split('\n'): + if '🆔' in line or 'ID:' in line: + numbers = re.findall(r'\d+', line) + if numbers: + user_id = int(numbers[0]) + break + + if not user_id: + await update.message.reply_text("❌ 无法识别用户ID") + return + + try: + await context.bot.send_message(chat_id=user_id, text=update.message.text) + await update.message.reply_text(f"✅ 已回复给用户 {user_id}") + except Exception as e: + await update.message.reply_text(f"❌ 回复失败: {str(e)}") + + async def initialize(self): + """初始化机器人""" + try: + logger.info("正在初始化整合机器人...") + + if not await self.setup_pyrogram(): + logger.error("Pyrogram初始化失败") + return False + + builder = Application.builder().token(BOT_TOKEN) + + if os.environ.get('HTTP_PROXY'): + proxy_url = os.environ.get('HTTP_PROXY') + logger.info(f"配置Telegram Bot代理: {proxy_url}") + request = httpx.AsyncClient(proxies={"http://": proxy_url, "https://": proxy_url}, timeout=30.0) + builder = builder.request(request) + + self.app = builder.build() + + self.app.add_handler(CommandHandler("start", self.handle_start)) + self.app.add_handler(CallbackQueryHandler(self.handle_callback)) + self.app.add_handler(MessageHandler(tg_filters.ALL, self.handle_message)) + + logger.info("✅ 整合机器人初始化完成") + return True + + except Exception as e: + logger.error(f"初始化失败: {e}") + return False + + async def run(self): + """运行机器人""" + try: + await self.app.initialize() + await self.app.start() + await self.app.updater.start_polling(drop_pending_updates=True) + + logger.info("="*50) + logger.info("✅ AI增强版Bot已启动") + logger.info(f"AI服务: {MAC_API_URL}") + logger.info(f"缓存功能: {'启用' if self.cache_db else '禁用'}") + logger.info("="*50) + + await asyncio.Event().wait() + + except KeyboardInterrupt: + logger.info("收到停止信号") + finally: + await self.cleanup() + + async def cleanup(self): + """清理资源""" + logger.info("正在清理...") + + if self.app: + await self.app.updater.stop() + await self.app.stop() + await self.app.shutdown() + + if self.pyrogram_client: + await self.pyrogram_client.stop() + + logger.info("✅ 清理完成") + + +async def main(): + """主函数""" + bot = IntegratedBotAI() + + if await bot.initialize(): + await bot.run() + else: + logger.error("初始化失败,退出") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/integrated_bot_ai.backup.before_fix.py b/integrated_bot_ai.backup.before_fix.py new file mode 100755 index 0000000..fc057b4 --- /dev/null +++ b/integrated_bot_ai.backup.before_fix.py @@ -0,0 +1,1007 @@ +#!/usr/bin/env python3 +""" +整合版客服机器人 - AI增强版 +包含: +1. AI对话引导 +2. 镜像搜索功能 +3. 自动翻页缓存 +4. 智能去重 +""" + +import asyncio +import logging +from enhanced_logger import EnhancedLogger +import time +import os +import httpx +import anthropic +import json +import sys +from typing import Dict, Optional +from datetime import datetime + +# 添加路径 +sys.path.insert(0, "/home/atai/bot_data") + +# Pyrogram imports +from pyrogram import Client as PyrogramClient, filters +from pyrogram.types import Message as PyrogramMessage +from pyrogram.raw.functions.messages import GetBotCallbackAnswer + +# Telegram Bot imports +from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup +from telegram.ext import Application, CommandHandler, MessageHandler, CallbackQueryHandler, filters as tg_filters +from telegram.ext import ContextTypes + +# 导入数据库 +try: + from database import CacheDatabase +except ImportError: + CacheDatabase = None + logging.warning("database.py未找到,缓存功能将禁用") + +# ================== 配置 ================== +API_ID = 24660516 +API_HASH = "eae564578880a59c9963916ff1bbbd3a" +SESSION_NAME = "user_session" +BOT_TOKEN = "8426529617:AAHAxzohSMFBAxInzbAVJsZfkB5bHnOyFC4" +TARGET_BOT = "@openaiw_bot" +ADMIN_ID = 7363537082 + +# AI服务配置 +MAC_API_URL = "http://192.168.9.10:8000" + +# 搜索命令列表 +SEARCH_COMMANDS = ['/topchat', '/search', '/text', '/human'] + +# 日志配置 +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +# 使用增强型日志系统 +enhanced_log = EnhancedLogger("integrated_bot", log_dir="./logs") +logger = enhanced_log.get_logger() +logger.info("🚀 增强型日志系统已启动 - 所有日志将被完整保留") + +# 初始化Claude客户端 +try: + claude_client = anthropic.Anthropic( + api_key=os.environ.get('ANTHROPIC_AUTH_TOKEN'), + base_url=os.environ.get('ANTHROPIC_BASE_URL', 'https://api.anthropic.com') + ) + logger.info("✅ Claude API客户端已初始化") +except Exception as e: + logger.error(f"❌ Claude API初始化失败: {e}") + claude_client = None + + + + +# ================== 对话管理 ================== +class ConversationManager: + """管理用户对话上下文""" + + def __init__(self, max_history=5): + self.conversations = {} + self.max_history = max_history + + def add_message(self, user_id: int, role: str, content: str): + """添加消息到历史""" + if user_id not in self.conversations: + self.conversations[user_id] = [] + + self.conversations[user_id].append({ + "role": role, + "content": content, + "timestamp": datetime.now().isoformat() + }) + + # 保持最近的N条消息 + if len(self.conversations[user_id]) > self.max_history * 2: + self.conversations[user_id] = self.conversations[user_id][-self.max_history * 2:] + + def get_history(self, user_id: int, limit: int = 2) -> list: + """获取用户对话历史""" + if user_id not in self.conversations: + return [] + + history = self.conversations[user_id][-limit * 2:] + return [{"role": msg["role"], "content": msg["content"]} for msg in history] + + def clear_history(self, user_id: int): + """清空用户历史""" + if user_id in self.conversations: + del self.conversations[user_id] + + +# ================== 自动翻页管理器 ================== +class AutoPaginationManager: + """后台自动翻页 - 用户无感知""" + + def __init__(self, pyrogram_client, cache_db, target_bot_id, logger): + self.pyrogram_client = pyrogram_client + self.cache_db = cache_db + self.target_bot_id = target_bot_id + self.logger = logger + self.active_tasks = {} + + async def start_pagination(self, user_id, command, keyword, first_message): + """启动后台翻页任务""" + if user_id in self.active_tasks: + return + + task = asyncio.create_task(self._paginate(user_id, command, keyword, first_message)) + self.active_tasks[user_id] = task + self.logger.info(f"[翻页] 后台任务启动: {command} {keyword}") + + async def _paginate(self, user_id, command, keyword, message): + """执行翻页""" + try: + page = 1 + self._save_to_cache(command, keyword, page, message) + + if not self._has_next(message): + self.logger.info(f"[翻页] 只有1页") + return + + current = message + for page in range(2, 11): # 最多10页 + await asyncio.sleep(2) + + next_msg = await self._click_next(current) + if not next_msg: + break + + self._save_to_cache(command, keyword, page, next_msg) + self.logger.info(f"[翻页] 第{page}页已保存") + + if not self._has_next(next_msg): + self.logger.info(f"[翻页] 完成,共{page}页") + break + + current = next_msg + + except Exception as e: + self.logger.error(f"[翻页] 错误: {e}") + finally: + if user_id in self.active_tasks: + del self.active_tasks[user_id] + + def _has_next(self, msg): + """检查是否有下一页""" + if not msg.reply_markup: + return False + for row in msg.reply_markup.inline_keyboard: + for btn in row: + if btn.text and any(x in btn.text for x in ['下一页', 'Next', '▶']): + return True + return False + + async def _click_next(self, msg): + """点击下一页""" + try: + from pyrogram.raw.functions.messages import GetBotCallbackAnswer + + for row in msg.reply_markup.inline_keyboard: + for btn in row: + if btn.text and any(x in btn.text for x in ['下一页', 'Next', '▶']): + await self.pyrogram_client.invoke( + GetBotCallbackAnswer( + peer=await self.pyrogram_client.resolve_peer(self.target_bot_id), + msg_id=msg.id, + data=btn.callback_data + ) + ) + await asyncio.sleep(1.5) + return await self.pyrogram_client.get_messages(self.target_bot_id, msg.id) + except Exception as e: + self.logger.error(f"[翻页] 点击失败: {e}") + return None + + def _save_to_cache(self, cmd, keyword, page, msg): + """保存到缓存""" + if not self.cache_db: + return + try: + text = msg.text or msg.caption or "" + buttons = [] + if msg.reply_markup: + for row in msg.reply_markup.inline_keyboard: + for btn in row: + buttons.append({"text": btn.text, "url": btn.url if btn.url else None}) + self.cache_db.save_cache(cmd, keyword, page, text, buttons) + except Exception as e: + self.logger.error(f"[翻页] 保存失败: {e}") + + +class IntegratedBotAI: + """整合的客服机器人 - AI增强版""" + + def __init__(self): + # Bot应用 + self.app = None + + # Pyrogram客户端(用于镜像) + self.pyrogram_client: Optional[PyrogramClient] = None + self.target_bot_id: Optional[int] = None + + # 消息映射 + self.pyrogram_to_telegram = {} + self.telegram_to_pyrogram = {} + self.callback_data_map = {} + self.user_search_sessions = {} + + # AI会话状态 + self.user_ai_sessions = {} + + # 缓存数据库 + self.cache_db = CacheDatabase() if CacheDatabase else None + + # 对话管理器 + self.conversation_manager = ConversationManager() + self.pagination_manager = None + + async def setup_pyrogram(self): + """设置Pyrogram客户端""" + try: + proxy_config = None + if os.environ.get('ALL_PROXY'): + proxy_url = os.environ.get('ALL_PROXY', '').replace('socks5://', '') + if proxy_url: + host, port = proxy_url.split(':') + proxy_config = {"scheme": "socks5", "hostname": host, "port": int(port)} + + self.pyrogram_client = PyrogramClient( + SESSION_NAME, api_id=API_ID, api_hash=API_HASH, + proxy=proxy_config if proxy_config else None + ) + + await self.pyrogram_client.start() + logger.info("✅ Pyrogram客户端已启动") + + # 初始化自动翻页管理器 + self.pagination_manager = AutoPaginationManager( + self.pyrogram_client, self.cache_db, self.target_bot_id, logger + ) + logger.info("✅ 自动翻页管理器已初始化") + + target = await self.pyrogram_client.get_users(TARGET_BOT) + self.target_bot_id = target.id + logger.info(f"✅ 已连接到搜索机器人: {target.username}") + + @self.pyrogram_client.on_message(filters.user(self.target_bot_id)) + async def on_bot_response(_, message: PyrogramMessage): + await self.handle_search_response(message) + + @self.pyrogram_client.on_edited_message(filters.user(self.target_bot_id)) + async def on_message_edited(_, message: PyrogramMessage): + await self.handle_search_response(message, is_edit=True) + + return True + except Exception as e: + logger.error(f"Pyrogram设置失败: {e}") + return False + + async def call_ai_service(self, user_id: int, message: str, context: dict = None) -> dict: + """优化的Claude API调用 - 带上下文记忆和改进提示词""" + + if not claude_client: + logger.error("Claude客户端未初始化") + return { + "type": "auto", + "response": "👋 我来帮你搜索!\n\n直接发关键词,或试试:\n• /search 群组名\n• /text 讨论内容\n• /topchat 热门分类", + "confidence": 0.3 + } + + try: + logger.info(f"[用户 {user_id}] 调用Claude API: {message}") + + username = context.get('username', f'user_{user_id}') if context else f'user_{user_id}' + first_name = context.get('first_name', '') if context else '' + + # 构建对话历史 + messages = [] + + # 添加历史对话(最近2轮) + history = self.conversation_manager.get_history(user_id, limit=2) + messages.extend(history) + + # 添加当前消息(优化的提示词) + current_prompt = f"""你是@ktfund_bot的AI助手,专业的Telegram群组搜索助手。 + +【重要】你的回复中可以包含可执行的命令,我会为它们生成按钮。 +命令格式:/search 关键词 或 /text 关键词 + +用户信息:@{username} ({first_name}) +用户说:"{message}" + +【可用工具】 +• /search [关键词] - 搜索群组名称 +• /text [关键词] - 搜索讨论内容 +• /human [关键词] - 搜索用户 +• /topchat - 热门分类 + +【回复要求】 +1. 简短友好(2-4行) +2. 给1-2个具体命令建议 +3. 口语化,像朋友聊天 +4. 命令要在独立的一行 + +【示例】 +用户:"找AI群" +回复: +找AI群的话,试试: +/search AI +/text ChatGPT + +直接回复:""" + + messages.append({ + "role": "user", + "content": current_prompt + }) + + # 调用Claude API + response = claude_client.messages.create( + model="claude-sonnet-4-20250514", + max_tokens=512, + temperature=0.7, + messages=messages + ) + + claude_response = response.content[0].text.strip() + + # 保存对话历史 + self.conversation_manager.add_message(user_id, "user", message) + self.conversation_manager.add_message(user_id, "assistant", claude_response) + + logger.info(f"[用户 {user_id}] ✅ Claude回复成功 ({len(claude_response)}字)") + + # 智能提取命令建议 + suggested_commands = self._extract_commands(claude_response) + + return { + "type": "ai", + "response": claude_response, + "confidence": 1.0, + "suggested_commands": suggested_commands + } + + except Exception as e: + logger.error(f"[用户 {user_id}] ❌ Claude API失败: {e}") + return { + "type": "auto", + "response": "👋 我来帮你搜索!\n\n直接发关键词,或试试:\n• /search 群组名\n• /text 讨论内容\n• /topchat 热门分类", + "confidence": 0.3 + } + + def _extract_commands(self, response_text: str) -> list: + """从回复中提取建议的命令""" + import re + commands = [] + + # 匹配 /command pattern + patterns = [ + r'/search\s+[\w\s]+', + r'/text\s+[\w\s]+', + r'/human\s+[\w\s]+', + r'/topchat' + ] + + for pattern in patterns: + matches = re.findall(pattern, response_text) + commands.extend([m.strip() for m in matches[:1]]) + + return commands[:2] + + + + def _extract_command_buttons(self, text: str) -> list: + """从AI回复中提取命令按钮""" + import re + buttons = [] + + # 匹配:/command keyword + pattern = r'/(search|text|human|topchat)\s*([^\n]*)' + matches = re.findall(pattern, text, re.IGNORECASE) + + for cmd, keywords in matches[:3]: + cmd = cmd.lower() + keywords = keywords.strip()[:30] # 限制长度 + + if keywords: + display = f"/{cmd} {keywords}" + callback = f"cmd_{cmd}_{keywords.replace(' ', '_')}"[:64] + else: + display = f"/{cmd}" + callback = f"cmd_{cmd}" + + buttons.append((display, callback)) + + return buttons + + async def handle_start(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """处理/start命令 - AI引导模式""" + user = update.effective_user + user_id = user.id + + self.user_ai_sessions[user_id] = {"started_at": datetime.now(), "conversation": []} + + welcome_text = ( + f"👋 您好 {user.first_name}!\n\n" + "我是智能搜索助手,可以帮您找到Telegram上的群组和频道。\n\n" + "🔍 我能做什么:\n" + "• 搜索群组/频道\n" + "• 搜索特定话题的讨论\n" + "• 查找用户\n" + "• 浏览热门分类\n\n" + "💬 直接告诉我您想找什么,我会帮您选择最合适的搜索方式!" + ) + + keyboard = [ + [InlineKeyboardButton("🔍 搜索群组", callback_data="quick_search"), + InlineKeyboardButton("📚 使用指南", callback_data="quick_help")], + [InlineKeyboardButton("🔥 热门分类", callback_data="quick_topchat")] + ] + + await update.message.reply_text(welcome_text, reply_markup=InlineKeyboardMarkup(keyboard)) + + # 通知管理员 + admin_notification = ( + f"🆕 新用户访问 (AI模式):\n" + f"👤 {user.first_name} {user.last_name or ''}\n" + f"🆔 {user.id}\n" + f"👤 @{user.username or '无'}\n" + f"⏰ {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" + ) + await context.bot.send_message(chat_id=ADMIN_ID, text=admin_notification) + + async def handle_message(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """处理所有消息 - AI智能路由""" + if not update.message or not update.message.text: + return + + user = update.effective_user + user_id = user.id + text = update.message.text + is_admin = user_id == ADMIN_ID + + if is_admin and update.message.reply_to_message: + await self.handle_admin_reply(update, context) + return + + if self.is_search_command(text): + await self.handle_search_command(update, context) + return + + await self.handle_ai_conversation(update, context) + + def is_search_command(self, text: str) -> bool: + """检查是否是搜索命令""" + return text and text.split()[0] in SEARCH_COMMANDS + + async def handle_ai_conversation(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """AI对话处理 - 带智能按钮""" + user = update.effective_user + user_id = user.id + message = update.message.text + + # 显示"正在输入" + await update.message.chat.send_action("typing") + + # 构建上下文 + user_context = { + "username": user.username or f"user{user_id}", + "first_name": user.first_name or "朋友", + "last_name": user.last_name + } + + # 调用AI + ai_response = await self.call_ai_service(user_id, message, user_context) + response_text = ai_response.get("response", "") + + # 提取命令按钮 + buttons = self._extract_command_buttons(response_text) + + try: + if buttons: + # 构建按钮键盘 + keyboard = [] + for display, callback in buttons: + keyboard.append([InlineKeyboardButton( + f"🔍 {display}", + callback_data=callback + )]) + + # 添加常用按钮 + keyboard.append([ + InlineKeyboardButton("🔥 热门目录", callback_data="cmd_topchat"), + InlineKeyboardButton("📖 帮助", callback_data="cmd_help") + ]) + + await update.message.reply_text( + response_text, + reply_markup=InlineKeyboardMarkup(keyboard) + ) + logger.info(f"[AI对话] 已回复用户 {user_id} (带{len(buttons)}个按钮)") + else: + # 无按钮版本 + await update.message.reply_text(response_text) + logger.info(f"[AI对话] 已回复用户 {user_id}") + + except Exception as e: + logger.error(f"[AI对话] 发送失败: {e}, 降级为纯文本") + try: + await update.message.reply_text(response_text) + except: + await update.message.reply_text("抱歉,回复失败。请直接发送命令,如:/search AI") + + + + async def handle_search_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """处理搜索命令 - 带缓存""" + user = update.effective_user + user_id = user.id + command = update.message.text + + # 提取命令和关键词 + parts = command.split(maxsplit=1) + cmd = parts[0] + keyword = parts[1] if len(parts) > 1 else "" + + # 检查缓存 + if self.cache_db and keyword: + cached = self.cache_db.get_cache(cmd, keyword, 1) + if cached: + logger.info(f"[缓存命中] {cmd} {keyword} page1") + + # 恢复按钮 + keyboard = None + if cached.get('buttons'): + buttons = [] + for btn_data in cached['buttons']: + if btn_data.get('url'): + buttons.append([InlineKeyboardButton(text=btn_data['text'], url=btn_data['url'])]) + elif btn_data.get('callback_data'): + # 生成callback_id + callback_id = f"cb_{time.time():.0f}_{len(self.callback_data_map)}" + # 需要存储原始message_id,这里用0作为占位符,实际翻页时从缓存获取 + self.callback_data_map[callback_id] = (0, btn_data['callback_data']) + buttons.append([InlineKeyboardButton(text=btn_data['text'], callback_data=callback_id[:64])]) + + if buttons: + keyboard = InlineKeyboardMarkup(buttons) + + # 发送缓存结果(带按钮) + sent = await update.message.reply_text( + cached['text'][:4000], + reply_markup=keyboard, + parse_mode='HTML' + ) + + # 记录会话,以便翻页时使用 + self.user_search_sessions[user_id] = { + 'chat_id': update.effective_chat.id, + 'wait_msg_id': sent.message_id, + 'command': cmd, + 'keyword': keyword, + 'timestamp': datetime.now() + } + + return + + # 通知管理员 + admin_notification = ( + f"🔍 用户执行搜索:\n" + f"👤 {user.first_name} {user.last_name or ''}\n" + f"🆔 {user_id}\n" + f"📝 {command}\n" + f"⏰ {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" + ) + await context.bot.send_message(chat_id=ADMIN_ID, text=admin_notification) + + wait_msg = await update.message.reply_text("🔍 正在搜索,请稍候...") + + self.user_search_sessions[user_id] = { + 'chat_id': update.effective_chat.id, + 'wait_msg_id': wait_msg.message_id, + 'command': cmd, + 'keyword': keyword, + 'timestamp': datetime.now() + } + + await self.pyrogram_client.send_message(self.target_bot_id, command) + logger.info(f"搜索: {command}") + + async def handle_search_response(self, message: PyrogramMessage, is_edit: bool = False): + """处理搜索机器人的响应 - 保存到缓存""" + try: + if not self.user_search_sessions: + return + + user_id = max(self.user_search_sessions.keys(), key=lambda k: self.user_search_sessions[k]['timestamp']) + session = self.user_search_sessions[user_id] + + text = message.text or message.caption or "无结果" + + try: + if message.text and hasattr(message.text, 'html'): + text = message.text.html + except: + pass + + keyboard = self.convert_keyboard(message) + + if is_edit and message.id in self.pyrogram_to_telegram: + telegram_msg_id = self.pyrogram_to_telegram[message.id] + await self.app.bot.edit_message_text( + chat_id=session['chat_id'], + message_id=telegram_msg_id, + text=text[:4000], + reply_markup=keyboard, + parse_mode='HTML' + ) + else: + try: + await self.app.bot.delete_message( + chat_id=session['chat_id'], + message_id=session['wait_msg_id'] + ) + except: + pass + + sent = await self.app.bot.send_message( + chat_id=session['chat_id'], + text=text[:4000], + reply_markup=keyboard, + parse_mode='HTML' + ) + + self.pyrogram_to_telegram[message.id] = sent.message_id + self.telegram_to_pyrogram[sent.message_id] = message.id + + # 保存到缓存 + if self.cache_db and session.get('keyword'): + buttons = self.extract_buttons(message) + self.cache_db.save_cache( + session['command'], + session['keyword'], + 1, # 第一页 + text, + text, + buttons + ) + + # 后台自动翻页(用户无感知) + if self.pagination_manager: + asyncio.create_task( + self.pagination_manager.start_pagination( + user_id, session['command'], session['keyword'], message + ) + ) + + except Exception as e: + logger.error(f"处理搜索响应失败: {e}") + + def convert_keyboard(self, message: PyrogramMessage) -> Optional[InlineKeyboardMarkup]: + """转换键盘""" + if not message.reply_markup or not message.reply_markup.inline_keyboard: + return None + + try: + buttons = [] + for row in message.reply_markup.inline_keyboard: + button_row = [] + for btn in row: + if btn.url: + button_row.append(InlineKeyboardButton(text=btn.text, url=btn.url)) + elif btn.callback_data: + callback_id = f"cb_{time.time():.0f}_{len(self.callback_data_map)}" + self.callback_data_map[callback_id] = (message.id, btn.callback_data) + button_row.append(InlineKeyboardButton(text=btn.text, callback_data=callback_id[:64])) + + if button_row: + buttons.append(button_row) + + return InlineKeyboardMarkup(buttons) if buttons else None + except Exception as e: + logger.error(f"键盘转换失败: {e}") + return None + + def extract_buttons(self, message: PyrogramMessage) -> list: + """提取按钮数据(包含callback_data用于缓存)""" + if not message.reply_markup or not message.reply_markup.inline_keyboard: + return [] + + buttons = [] + for row in message.reply_markup.inline_keyboard: + for btn in row: + btn_data = {"text": btn.text} + if btn.url: + btn_data["url"] = btn.url + if btn.callback_data: + btn_data["callback_data"] = btn.callback_data + buttons.append(btn_data) + return buttons + + async def handle_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """处理按钮点击 - 执行搜索命令或翻页""" + query = update.callback_query + data = query.data + user = query.from_user + + logger.info(f"[回调] 收到callback: user={user.id}, data={data}") + + await query.answer() + + if data.startswith("cb_"): + # 处理翻页按钮 + if data in self.callback_data_map: + orig_msg_id, orig_callback = self.callback_data_map[data] + logger.info(f"[翻页] 用户 {user.id} 点击: {orig_callback}") + + # 解析callback_data获取页码(格式如:page_2) + try: + if orig_callback.startswith("page_"): + page = int(orig_callback.split("_")[1]) + + # 从会话获取搜索信息 + session = self.user_search_sessions.get(user.id) + if session and 'command' in session and 'keyword' in session: + cmd = session['command'] + keyword = session['keyword'] + + # 先检查缓存 + cached = self.cache_db.get_cache(cmd, keyword, page) + if cached: + logger.info(f"[翻页缓存] 命中: {cmd} {keyword} page{page}") + + # 从缓存恢复按钮 + keyboard = None + if cached.get('buttons'): + buttons = [] + for btn_data in cached['buttons']: + if btn_data.get('url'): + buttons.append([InlineKeyboardButton(text=btn_data['text'], url=btn_data['url'])]) + elif btn_data.get('callback_data'): + # 重新生成callback_id + callback_id = f"cb_{time.time():.0f}_{len(self.callback_data_map)}" + self.callback_data_map[callback_id] = (orig_msg_id, btn_data['callback_data']) + buttons.append([InlineKeyboardButton(text=btn_data['text'], callback_data=callback_id[:64])]) + + if buttons: + keyboard = InlineKeyboardMarkup(buttons) + + # 发送缓存结果 + await query.message.edit_text( + text=cached['text'], + reply_markup=keyboard, + parse_mode='HTML' + ) + return + + else: + logger.info(f"[翻页] 缓存未命中,转发到搜索bot") + + # 如果缓存未命中或不是page_格式,转发到搜索bot + await self.pyrogram_client.request_callback_answer( + chat_id=self.target_bot_id, + message_id=orig_msg_id, + callback_data=orig_callback.encode() + ) + + # 记录等待响应 + self.user_search_sessions[user.id] = { + 'chat_id': query.message.chat_id, + 'wait_msg_id': query.message.message_id, + 'command': session.get('command') if session else None, + 'keyword': session.get('keyword') if session else None, + 'timestamp': datetime.now() + } + + logger.info(f"[翻页] 已转发callback到搜索bot") + + except Exception as e: + logger.error(f"[翻页] 处理失败: {e}") + await query.message.reply_text("❌ 翻页失败,请稍后重试") + else: + logger.warning(f"[翻页] callback_id不存在: {data}") + await query.message.reply_text("❌ 按钮已过期,请重新搜索") + + elif data.startswith("cmd_"): + # 解析命令 + parts = data.replace("cmd_", "").split("_", 1) + cmd = parts[0] + keywords = parts[1].replace("_", " ") if len(parts) > 1 else "" + + # 构造完整命令 + command = f"/{cmd} {keywords}" if keywords else f"/{cmd}" + + logger.info(f"[用户 {user.id}] 点击按钮: {command}") + + # 显示执行提示 + await query.message.reply_text(f"🔍 正在执行:{command}\n请稍候...") + + # 转发到搜索bot + try: + await self.pyrogram_client.send_message(self.target_bot_id, command) + + # 记录搜索会话 + self.user_search_sessions[user.id] = { + 'chat_id': query.message.chat_id, + 'wait_msg_id': query.message.message_id + 1, + 'command': f"/{cmd}", + 'keyword': keywords, + 'timestamp': datetime.now() + } + + logger.info(f"[镜像] 已转发: {command}") + + except Exception as e: + logger.error(f"[镜像] 转发失败: {e}") + await query.message.reply_text("❌ 搜索失败,请稍后重试或直接发送命令") + + elif data == "quick_search": + # 搜索群组引导 + keyboard = [ + [InlineKeyboardButton("🔍 搜索群组", callback_data="cmd_search")], + [InlineKeyboardButton("💬 搜索消息内容", callback_data="cmd_text")], + [InlineKeyboardButton("👤 搜索用户", callback_data="cmd_human")] + ] + await query.message.edit_text( + "请选择搜索类型,或直接发送关键词:", + reply_markup=InlineKeyboardMarkup(keyboard) + ) + + elif data == "quick_help": + await query.message.edit_text( + "📖 使用指南:\n\n" + "🔍 搜索方式:\n" + "• /search [关键词] - 按群组名称搜索\n" + "• /text [关键词] - 按消息内容搜索\n" + "• /human [关键词] - 按用户名搜索\n" + "• /topchat - 浏览热门群组目录\n\n" + "💡 快捷使用:\n" + "直接发送关键词,我会智能分析并选择最合适的搜索方式!\n\n" + "📋 示例:\n" + "• 发送 '区块链' → 自动搜索相关群组\n" + "• 发送 'NFT交易' → 智能搜索讨论内容\n\n" + "❓ 有任何问题都可以直接问我!" + ) + + elif data == "quick_topchat": + # 直接触发topchat命令 + logger.info(f"[用户 {user.id}] 点击热门分类按钮") + await query.message.edit_text("🔥 正在加载热门分类...\n请稍候...") + + try: + await self.pyrogram_client.send_message(self.target_bot_id, "/topchat") + self.user_search_sessions[user.id] = { + 'chat_id': query.message.chat_id, + 'wait_msg_id': query.message.message_id, + 'command': '/topchat', + 'keyword': '', + 'timestamp': datetime.now() + } + logger.info(f"[镜像] 已转发: /topchat") + except Exception as e: + logger.error(f"[镜像] 转发失败: {e}") + await query.message.edit_text("❌ 加载失败,请稍后重试") + + elif data == "cmd_help": + await query.message.reply_text( + "📖 使用指南:\n\n" + "• /search [关键词] - 按群组名称搜索\n" + "• /text [关键词] - 按消息内容搜索\n" + "• /human [关键词] - 按用户名搜索\n" + "• /topchat - 热门群组目录\n\n" + "💡 或者直接告诉我你想找什么!" + ) + + else: + logger.warning(f"未知callback: {data}") + + + async def handle_admin_reply(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """处理管理员回复""" + reply_to = update.message.reply_to_message + if not reply_to or not reply_to.text: + return + + import re + user_id = None + for line in reply_to.text.split('\n'): + if '🆔' in line or 'ID:' in line: + numbers = re.findall(r'\d+', line) + if numbers: + user_id = int(numbers[0]) + break + + if not user_id: + await update.message.reply_text("❌ 无法识别用户ID") + return + + try: + await context.bot.send_message(chat_id=user_id, text=update.message.text) + await update.message.reply_text(f"✅ 已回复给用户 {user_id}") + except Exception as e: + await update.message.reply_text(f"❌ 回复失败: {str(e)}") + + async def initialize(self): + """初始化机器人""" + try: + logger.info("正在初始化整合机器人...") + + if not await self.setup_pyrogram(): + logger.error("Pyrogram初始化失败") + return False + + builder = Application.builder().token(BOT_TOKEN) + + if os.environ.get('HTTP_PROXY'): + proxy_url = os.environ.get('HTTP_PROXY') + logger.info(f"配置Telegram Bot代理: {proxy_url}") + request = httpx.AsyncClient(proxies={"http://": proxy_url, "https://": proxy_url}, timeout=30.0) + builder = builder.request(request) + + self.app = builder.build() + + self.app.add_handler(CommandHandler("start", self.handle_start)) + self.app.add_handler(CallbackQueryHandler(self.handle_callback)) + self.app.add_handler(MessageHandler(tg_filters.ALL, self.handle_message)) + + logger.info("✅ 整合机器人初始化完成") + return True + + except Exception as e: + logger.error(f"初始化失败: {e}") + return False + + async def run(self): + """运行机器人""" + try: + await self.app.initialize() + await self.app.start() + await self.app.updater.start_polling(drop_pending_updates=True) + + logger.info("="*50) + logger.info("✅ AI增强版Bot已启动") + logger.info(f"AI服务: {MAC_API_URL}") + logger.info(f"缓存功能: {'启用' if self.cache_db else '禁用'}") + logger.info("="*50) + + await asyncio.Event().wait() + + except KeyboardInterrupt: + logger.info("收到停止信号") + finally: + await self.cleanup() + + async def cleanup(self): + """清理资源""" + logger.info("正在清理...") + + if self.app: + await self.app.updater.stop() + await self.app.stop() + await self.app.shutdown() + + if self.pyrogram_client: + await self.pyrogram_client.stop() + + logger.info("✅ 清理完成") + + +async def main(): + """主函数""" + bot = IntegratedBotAI() + + if await bot.initialize(): + await bot.run() + else: + logger.error("初始化失败,退出") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/integrated_bot_ai.backup.py b/integrated_bot_ai.backup.py new file mode 100755 index 0000000..efdd977 --- /dev/null +++ b/integrated_bot_ai.backup.py @@ -0,0 +1,845 @@ +#!/usr/bin/env python3 +""" +整合版客服机器人 - AI增强版 +包含: +1. AI对话引导 +2. 镜像搜索功能 +3. 自动翻页缓存 +4. 智能去重 +""" + +import asyncio +import logging +import time +import os +import httpx +import anthropic +import json +import sys +from typing import Dict, Optional +from datetime import datetime + +# 添加路径 +sys.path.insert(0, "/home/atai/bot_data") + +# Pyrogram imports +from pyrogram import Client as PyrogramClient, filters +from pyrogram.types import Message as PyrogramMessage +from pyrogram.raw.functions.messages import GetBotCallbackAnswer + +# Telegram Bot imports +from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup +from telegram.ext import Application, CommandHandler, MessageHandler, CallbackQueryHandler, filters as tg_filters +from telegram.ext import ContextTypes + +# 导入数据库 +try: + from database import CacheDatabase +except ImportError: + CacheDatabase = None + logging.warning("database.py未找到,缓存功能将禁用") + +# ================== 配置 ================== +API_ID = 24660516 +API_HASH = "eae564578880a59c9963916ff1bbbd3a" +SESSION_NAME = "user_session" +BOT_TOKEN = "8426529617:AAHAxzohSMFBAxInzbAVJsZfkB5bHnOyFC4" +TARGET_BOT = "@openaiw_bot" +ADMIN_ID = 7363537082 + +# AI服务配置 +MAC_API_URL = "http://192.168.9.10:8000" + +# 搜索命令列表 +SEARCH_COMMANDS = ['/topchat', '/search', '/text', '/human'] + +# 日志配置 +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +# 初始化Claude客户端 +try: + claude_client = anthropic.Anthropic( + api_key=os.environ.get('ANTHROPIC_AUTH_TOKEN'), + base_url=os.environ.get('ANTHROPIC_BASE_URL', 'https://api.anthropic.com') + ) + logger.info("✅ Claude API客户端已初始化") +except Exception as e: + logger.error(f"❌ Claude API初始化失败: {e}") + claude_client = None + + + + +# ================== 对话管理 ================== +class ConversationManager: + """管理用户对话上下文""" + + def __init__(self, max_history=5): + self.conversations = {} + self.max_history = max_history + + def add_message(self, user_id: int, role: str, content: str): + """添加消息到历史""" + if user_id not in self.conversations: + self.conversations[user_id] = [] + + self.conversations[user_id].append({ + "role": role, + "content": content, + "timestamp": datetime.now().isoformat() + }) + + # 保持最近的N条消息 + if len(self.conversations[user_id]) > self.max_history * 2: + self.conversations[user_id] = self.conversations[user_id][-self.max_history * 2:] + + def get_history(self, user_id: int, limit: int = 2) -> list: + """获取用户对话历史""" + if user_id not in self.conversations: + return [] + + history = self.conversations[user_id][-limit * 2:] + return [{"role": msg["role"], "content": msg["content"]} for msg in history] + + def clear_history(self, user_id: int): + """清空用户历史""" + if user_id in self.conversations: + del self.conversations[user_id] + + +# ================== 自动翻页管理器 ================== +class AutoPaginationManager: + """后台自动翻页 - 用户无感知""" + + def __init__(self, pyrogram_client, cache_db, target_bot_id, logger): + self.pyrogram_client = pyrogram_client + self.cache_db = cache_db + self.target_bot_id = target_bot_id + self.logger = logger + self.active_tasks = {} + + async def start_pagination(self, user_id, command, keyword, first_message): + """启动后台翻页任务""" + if user_id in self.active_tasks: + return + + task = asyncio.create_task(self._paginate(user_id, command, keyword, first_message)) + self.active_tasks[user_id] = task + self.logger.info(f"[翻页] 后台任务启动: {command} {keyword}") + + async def _paginate(self, user_id, command, keyword, message): + """执行翻页""" + try: + page = 1 + self._save_to_cache(command, keyword, page, message) + + if not self._has_next(message): + self.logger.info(f"[翻页] 只有1页") + return + + current = message + for page in range(2, 11): # 最多10页 + await asyncio.sleep(2) + + next_msg = await self._click_next(current) + if not next_msg: + break + + self._save_to_cache(command, keyword, page, next_msg) + self.logger.info(f"[翻页] 第{page}页已保存") + + if not self._has_next(next_msg): + self.logger.info(f"[翻页] 完成,共{page}页") + break + + current = next_msg + + except Exception as e: + self.logger.error(f"[翻页] 错误: {e}") + finally: + if user_id in self.active_tasks: + del self.active_tasks[user_id] + + def _has_next(self, msg): + """检查是否有下一页""" + if not msg.reply_markup: + return False + for row in msg.reply_markup.inline_keyboard: + for btn in row: + if btn.text and any(x in btn.text for x in ['下一页', 'Next', '▶']): + return True + return False + + async def _click_next(self, msg): + """点击下一页""" + try: + from pyrogram.raw.functions.messages import GetBotCallbackAnswer + + for row in msg.reply_markup.inline_keyboard: + for btn in row: + if btn.text and any(x in btn.text for x in ['下一页', 'Next', '▶']): + await self.pyrogram_client.invoke( + GetBotCallbackAnswer( + peer=await self.pyrogram_client.resolve_peer(self.target_bot_id), + msg_id=msg.id, + data=btn.callback_data + ) + ) + await asyncio.sleep(1.5) + return await self.pyrogram_client.get_messages(self.target_bot_id, msg.id) + except Exception as e: + self.logger.error(f"[翻页] 点击失败: {e}") + return None + + def _save_to_cache(self, cmd, keyword, page, msg): + """保存到缓存""" + if not self.cache_db: + return + try: + text = msg.text or msg.caption or "" + buttons = [] + if msg.reply_markup: + for row in msg.reply_markup.inline_keyboard: + for btn in row: + buttons.append({"text": btn.text, "url": btn.url if btn.url else None}) + self.cache_db.save_cache(cmd, keyword, page, text, buttons) + except Exception as e: + self.logger.error(f"[翻页] 保存失败: {e}") + + +class IntegratedBotAI: + """整合的客服机器人 - AI增强版""" + + def __init__(self): + # Bot应用 + self.app = None + + # Pyrogram客户端(用于镜像) + self.pyrogram_client: Optional[PyrogramClient] = None + self.target_bot_id: Optional[int] = None + + # 消息映射 + self.pyrogram_to_telegram = {} + self.telegram_to_pyrogram = {} + self.callback_data_map = {} + self.user_search_sessions = {} + + # AI会话状态 + self.user_ai_sessions = {} + + # 缓存数据库 + self.cache_db = CacheDatabase() if CacheDatabase else None + + # 对话管理器 + self.conversation_manager = ConversationManager() + self.pagination_manager = None + + async def setup_pyrogram(self): + """设置Pyrogram客户端""" + try: + proxy_config = None + if os.environ.get('ALL_PROXY'): + proxy_url = os.environ.get('ALL_PROXY', '').replace('socks5://', '') + if proxy_url: + host, port = proxy_url.split(':') + proxy_config = {"scheme": "socks5", "hostname": host, "port": int(port)} + + self.pyrogram_client = PyrogramClient( + SESSION_NAME, api_id=API_ID, api_hash=API_HASH, + proxy=proxy_config if proxy_config else None + ) + + await self.pyrogram_client.start() + logger.info("✅ Pyrogram客户端已启动") + + # 初始化自动翻页管理器 + self.pagination_manager = AutoPaginationManager( + self.pyrogram_client, self.cache_db, self.target_bot_id, logger + ) + logger.info("✅ 自动翻页管理器已初始化") + + target = await self.pyrogram_client.get_users(TARGET_BOT) + self.target_bot_id = target.id + logger.info(f"✅ 已连接到搜索机器人: {target.username}") + + @self.pyrogram_client.on_message(filters.user(self.target_bot_id)) + async def on_bot_response(_, message: PyrogramMessage): + await self.handle_search_response(message) + + @self.pyrogram_client.on_edited_message(filters.user(self.target_bot_id)) + async def on_message_edited(_, message: PyrogramMessage): + await self.handle_search_response(message, is_edit=True) + + return True + except Exception as e: + logger.error(f"Pyrogram设置失败: {e}") + return False + + async def call_ai_service(self, user_id: int, message: str, context: dict = None) -> dict: + """优化的Claude API调用 - 带上下文记忆和改进提示词""" + + if not claude_client: + logger.error("Claude客户端未初始化") + return { + "type": "auto", + "response": "👋 我来帮你搜索!\n\n直接发关键词,或试试:\n• /search 群组名\n• /text 讨论内容\n• /topchat 热门分类", + "confidence": 0.3 + } + + try: + logger.info(f"[用户 {user_id}] 调用Claude API: {message}") + + username = context.get('username', f'user_{user_id}') if context else f'user_{user_id}' + first_name = context.get('first_name', '') if context else '' + + # 构建对话历史 + messages = [] + + # 添加历史对话(最近2轮) + history = self.conversation_manager.get_history(user_id, limit=2) + messages.extend(history) + + # 添加当前消息(优化的提示词) + current_prompt = f"""你是@ktfund_bot的AI助手,专业的Telegram群组搜索助手。 + +【重要】你的回复中可以包含可执行的命令,我会为它们生成按钮。 +命令格式:/search 关键词 或 /text 关键词 + +用户信息:@{username} ({first_name}) +用户说:"{message}" + +【可用工具】 +• /search [关键词] - 搜索群组名称 +• /text [关键词] - 搜索讨论内容 +• /human [关键词] - 搜索用户 +• /topchat - 热门分类 + +【回复要求】 +1. 简短友好(2-4行) +2. 给1-2个具体命令建议 +3. 口语化,像朋友聊天 +4. 命令要在独立的一行 + +【示例】 +用户:"找AI群" +回复: +找AI群的话,试试: +/search AI +/text ChatGPT + +直接回复:""" + + messages.append({ + "role": "user", + "content": current_prompt + }) + + # 调用Claude API + response = claude_client.messages.create( + model="claude-sonnet-4-20250514", + max_tokens=512, + temperature=0.7, + messages=messages + ) + + claude_response = response.content[0].text.strip() + + # 保存对话历史 + self.conversation_manager.add_message(user_id, "user", message) + self.conversation_manager.add_message(user_id, "assistant", claude_response) + + logger.info(f"[用户 {user_id}] ✅ Claude回复成功 ({len(claude_response)}字)") + + # 智能提取命令建议 + suggested_commands = self._extract_commands(claude_response) + + return { + "type": "ai", + "response": claude_response, + "confidence": 1.0, + "suggested_commands": suggested_commands + } + + except Exception as e: + logger.error(f"[用户 {user_id}] ❌ Claude API失败: {e}") + return { + "type": "auto", + "response": "👋 我来帮你搜索!\n\n直接发关键词,或试试:\n• /search 群组名\n• /text 讨论内容\n• /topchat 热门分类", + "confidence": 0.3 + } + + def _extract_commands(self, response_text: str) -> list: + """从回复中提取建议的命令""" + import re + commands = [] + + # 匹配 /command pattern + patterns = [ + r'/search\s+[\w\s]+', + r'/text\s+[\w\s]+', + r'/human\s+[\w\s]+', + r'/topchat' + ] + + for pattern in patterns: + matches = re.findall(pattern, response_text) + commands.extend([m.strip() for m in matches[:1]]) + + return commands[:2] + + + + def _extract_command_buttons(self, text: str) -> list: + """从AI回复中提取命令按钮""" + import re + buttons = [] + + # 匹配:/command keyword + pattern = r'/(search|text|human|topchat)\s*([^\n]*)' + matches = re.findall(pattern, text, re.IGNORECASE) + + for cmd, keywords in matches[:3]: + cmd = cmd.lower() + keywords = keywords.strip()[:30] # 限制长度 + + if keywords: + display = f"/{cmd} {keywords}" + callback = f"cmd_{cmd}_{keywords.replace(' ', '_')}"[:64] + else: + display = f"/{cmd}" + callback = f"cmd_{cmd}" + + buttons.append((display, callback)) + + return buttons + + async def handle_start(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """处理/start命令 - AI引导模式""" + user = update.effective_user + user_id = user.id + + self.user_ai_sessions[user_id] = {"started_at": datetime.now(), "conversation": []} + + welcome_text = ( + f"👋 您好 {user.first_name}!\n\n" + "我是智能搜索助手,可以帮您找到Telegram上的群组和频道。\n\n" + "🔍 我能做什么:\n" + "• 搜索群组/频道\n" + "• 搜索特定话题的讨论\n" + "• 查找用户\n" + "• 浏览热门分类\n\n" + "💬 直接告诉我您想找什么,我会帮您选择最合适的搜索方式!" + ) + + keyboard = [ + [InlineKeyboardButton("🔍 搜索群组", callback_data="quick_search"), + InlineKeyboardButton("📚 使用指南", callback_data="quick_help")], + [InlineKeyboardButton("🔥 热门分类", callback_data="quick_topchat")] + ] + + await update.message.reply_text(welcome_text, reply_markup=InlineKeyboardMarkup(keyboard)) + + # 通知管理员 + admin_notification = ( + f"🆕 新用户访问 (AI模式):\n" + f"👤 {user.first_name} {user.last_name or ''}\n" + f"🆔 {user.id}\n" + f"👤 @{user.username or '无'}\n" + f"⏰ {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" + ) + await context.bot.send_message(chat_id=ADMIN_ID, text=admin_notification) + + async def handle_message(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """处理所有消息 - AI智能路由""" + if not update.message or not update.message.text: + return + + user = update.effective_user + user_id = user.id + text = update.message.text + is_admin = user_id == ADMIN_ID + + if is_admin and update.message.reply_to_message: + await self.handle_admin_reply(update, context) + return + + if self.is_search_command(text): + await self.handle_search_command(update, context) + return + + await self.handle_ai_conversation(update, context) + + def is_search_command(self, text: str) -> bool: + """检查是否是搜索命令""" + return text and text.split()[0] in SEARCH_COMMANDS + + async def handle_ai_conversation(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """AI对话处理 - 带智能按钮""" + user = update.effective_user + user_id = user.id + message = update.message.text + + # 显示"正在输入" + await update.message.chat.send_action("typing") + + # 构建上下文 + user_context = { + "username": user.username or f"user{user_id}", + "first_name": user.first_name or "朋友", + "last_name": user.last_name + } + + # 调用AI + ai_response = await self.call_ai_service(user_id, message, user_context) + response_text = ai_response.get("response", "") + + # 提取命令按钮 + buttons = self._extract_command_buttons(response_text) + + try: + if buttons: + # 构建按钮键盘 + keyboard = [] + for display, callback in buttons: + keyboard.append([InlineKeyboardButton( + f"🔍 {display}", + callback_data=callback + )]) + + # 添加常用按钮 + keyboard.append([ + InlineKeyboardButton("🔥 热门目录", callback_data="cmd_topchat"), + InlineKeyboardButton("📖 帮助", callback_data="cmd_help") + ]) + + await update.message.reply_text( + response_text, + reply_markup=InlineKeyboardMarkup(keyboard) + ) + logger.info(f"[AI对话] 已回复用户 {user_id} (带{len(buttons)}个按钮)") + else: + # 无按钮版本 + await update.message.reply_text(response_text) + logger.info(f"[AI对话] 已回复用户 {user_id}") + + except Exception as e: + logger.error(f"[AI对话] 发送失败: {e}, 降级为纯文本") + try: + await update.message.reply_text(response_text) + except: + await update.message.reply_text("抱歉,回复失败。请直接发送命令,如:/search AI") + + + + async def handle_search_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """处理搜索命令 - 带缓存""" + user = update.effective_user + user_id = user.id + command = update.message.text + + # 提取命令和关键词 + parts = command.split(maxsplit=1) + cmd = parts[0] + keyword = parts[1] if len(parts) > 1 else "" + + # 检查缓存 + if self.cache_db and keyword: + cached = self.cache_db.get_cache(cmd, keyword, 1) + if cached: + logger.info(f"返回缓存结果: {cmd} {keyword}") + await update.message.reply_text( + f"📦 从缓存返回结果:\n\n{cached['text'][:4000]}", + parse_mode='HTML' + ) + return + + # 通知管理员 + admin_notification = ( + f"🔍 用户执行搜索:\n" + f"👤 {user.first_name} {user.last_name or ''}\n" + f"🆔 {user_id}\n" + f"📝 {command}\n" + f"⏰ {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" + ) + await context.bot.send_message(chat_id=ADMIN_ID, text=admin_notification) + + wait_msg = await update.message.reply_text("🔍 正在搜索,请稍候...") + + self.user_search_sessions[user_id] = { + 'chat_id': update.effective_chat.id, + 'wait_msg_id': wait_msg.message_id, + 'command': cmd, + 'keyword': keyword, + 'timestamp': datetime.now() + } + + await self.pyrogram_client.send_message(self.target_bot_id, command) + logger.info(f"搜索: {command}") + + async def handle_search_response(self, message: PyrogramMessage, is_edit: bool = False): + """处理搜索机器人的响应 - 保存到缓存""" + try: + if not self.user_search_sessions: + return + + user_id = max(self.user_search_sessions.keys(), key=lambda k: self.user_search_sessions[k]['timestamp']) + session = self.user_search_sessions[user_id] + + text = message.text or message.caption or "无结果" + + try: + if message.text and hasattr(message.text, 'html'): + text = message.text.html + except: + pass + + keyboard = self.convert_keyboard(message) + + if is_edit and message.id in self.pyrogram_to_telegram: + telegram_msg_id = self.pyrogram_to_telegram[message.id] + await self.app.bot.edit_message_text( + chat_id=session['chat_id'], + message_id=telegram_msg_id, + text=text[:4000], + reply_markup=keyboard, + parse_mode='HTML' + ) + else: + try: + await self.app.bot.delete_message( + chat_id=session['chat_id'], + message_id=session['wait_msg_id'] + ) + except: + pass + + sent = await self.app.bot.send_message( + chat_id=session['chat_id'], + text=text[:4000], + reply_markup=keyboard, + parse_mode='HTML' + ) + + self.pyrogram_to_telegram[message.id] = sent.message_id + self.telegram_to_pyrogram[sent.message_id] = message.id + + # 保存到缓存 + if self.cache_db and session.get('keyword'): + buttons = self.extract_buttons(message) + self.cache_db.save_cache( + session['command'], + session['keyword'], + 1, # 第一页 + text, + text, + buttons + ) + + # 后台自动翻页(用户无感知) + if self.pagination_manager: + asyncio.create_task( + self.pagination_manager.start_pagination( + user_id, session['command'], session['keyword'], message + ) + ) + + except Exception as e: + logger.error(f"处理搜索响应失败: {e}") + + def convert_keyboard(self, message: PyrogramMessage) -> Optional[InlineKeyboardMarkup]: + """转换键盘""" + if not message.reply_markup or not message.reply_markup.inline_keyboard: + return None + + try: + buttons = [] + for row in message.reply_markup.inline_keyboard: + button_row = [] + for btn in row: + if btn.url: + button_row.append(InlineKeyboardButton(text=btn.text, url=btn.url)) + elif btn.callback_data: + callback_id = f"cb_{time.time():.0f}_{len(self.callback_data_map)}" + self.callback_data_map[callback_id] = (message.id, btn.callback_data) + button_row.append(InlineKeyboardButton(text=btn.text, callback_data=callback_id[:64])) + + if button_row: + buttons.append(button_row) + + return InlineKeyboardMarkup(buttons) if buttons else None + except Exception as e: + logger.error(f"键盘转换失败: {e}") + return None + + def extract_buttons(self, message: PyrogramMessage) -> list: + """提取按钮数据""" + if not message.reply_markup or not message.reply_markup.inline_keyboard: + return [] + + buttons = [] + for row in message.reply_markup.inline_keyboard: + for btn in row: + buttons.append({"text": btn.text, "url": btn.url if btn.url else None}) + return buttons + + async def handle_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """处理按钮点击 - 执行搜索命令""" + query = update.callback_query + data = query.data + user = query.from_user + + await query.answer() + + if data.startswith("cmd_"): + # 解析命令 + parts = data.replace("cmd_", "").split("_", 1) + cmd = parts[0] + keywords = parts[1].replace("_", " ") if len(parts) > 1 else "" + + # 构造完整命令 + command = f"/{cmd} {keywords}" if keywords else f"/{cmd}" + + logger.info(f"[用户 {user.id}] 点击按钮: {command}") + + # 显示执行提示 + await query.message.reply_text(f"🔍 正在执行:{command}\n请稍候...") + + # 转发到搜索bot + try: + await self.pyrogram_client.send_message(self.target_bot_id, command) + + # 记录搜索会话 + self.user_search_sessions[user.id] = { + 'chat_id': query.message.chat_id, + 'wait_msg_id': query.message.message_id + 1, + 'command': f"/{cmd}", + 'keyword': keywords, + 'timestamp': datetime.now() + } + + logger.info(f"[镜像] 已转发: {command}") + + except Exception as e: + logger.error(f"[镜像] 转发失败: {e}") + await query.message.reply_text("❌ 搜索失败,请稍后重试或直接发送命令") + + elif data == "cmd_help": + await query.message.reply_text( + "📖 使用指南:\n\n" + "• /search [关键词] - 按群组名称搜索\n" + "• /text [关键词] - 按消息内容搜索\n" + "• /human [关键词] - 按用户名搜索\n" + "• /topchat - 热门群组目录\n\n" + "💡 或者直接告诉我你想找什么!" + ) + + else: + logger.warning(f"未知callback: {data}") + + + async def handle_admin_reply(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """处理管理员回复""" + reply_to = update.message.reply_to_message + if not reply_to or not reply_to.text: + return + + import re + user_id = None + for line in reply_to.text.split('\n'): + if '🆔' in line or 'ID:' in line: + numbers = re.findall(r'\d+', line) + if numbers: + user_id = int(numbers[0]) + break + + if not user_id: + await update.message.reply_text("❌ 无法识别用户ID") + return + + try: + await context.bot.send_message(chat_id=user_id, text=update.message.text) + await update.message.reply_text(f"✅ 已回复给用户 {user_id}") + except Exception as e: + await update.message.reply_text(f"❌ 回复失败: {str(e)}") + + async def initialize(self): + """初始化机器人""" + try: + logger.info("正在初始化整合机器人...") + + if not await self.setup_pyrogram(): + logger.error("Pyrogram初始化失败") + return False + + builder = Application.builder().token(BOT_TOKEN) + + if os.environ.get('HTTP_PROXY'): + proxy_url = os.environ.get('HTTP_PROXY') + logger.info(f"配置Telegram Bot代理: {proxy_url}") + request = httpx.AsyncClient(proxies={"http://": proxy_url, "https://": proxy_url}, timeout=30.0) + builder = builder.request(request) + + self.app = builder.build() + + self.app.add_handler(CommandHandler("start", self.handle_start)) + self.app.add_handler(CallbackQueryHandler(self.handle_callback)) + self.app.add_handler(MessageHandler(tg_filters.ALL, self.handle_message)) + + logger.info("✅ 整合机器人初始化完成") + return True + + except Exception as e: + logger.error(f"初始化失败: {e}") + return False + + async def run(self): + """运行机器人""" + try: + await self.app.initialize() + await self.app.start() + await self.app.updater.start_polling(drop_pending_updates=True) + + logger.info("="*50) + logger.info("✅ AI增强版Bot已启动") + logger.info(f"AI服务: {MAC_API_URL}") + logger.info(f"缓存功能: {'启用' if self.cache_db else '禁用'}") + logger.info("="*50) + + await asyncio.Event().wait() + + except KeyboardInterrupt: + logger.info("收到停止信号") + finally: + await self.cleanup() + + async def cleanup(self): + """清理资源""" + logger.info("正在清理...") + + if self.app: + await self.app.updater.stop() + await self.app.stop() + await self.app.shutdown() + + if self.pyrogram_client: + await self.pyrogram_client.stop() + + logger.info("✅ 清理完成") + + +async def main(): + """主函数""" + bot = IntegratedBotAI() + + if await bot.initialize(): + await bot.run() + else: + logger.error("初始化失败,退出") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/integrated_bot_ai.py b/integrated_bot_ai.py new file mode 100755 index 0000000..784f846 --- /dev/null +++ b/integrated_bot_ai.py @@ -0,0 +1,1190 @@ +#!/usr/bin/env python3 +""" +整合版客服机器人 - AI增强版 +包含: +1. AI对话引导 +2. 镜像搜索功能 +3. 自动翻页缓存 +4. 智能去重 +""" + +import asyncio +import logging +from enhanced_logger import EnhancedLogger +import time +import os +import httpx +import re +import anthropic +import json +import sys +from typing import Dict, Optional +from datetime import datetime, timedelta + +# 添加路径 +sys.path.insert(0, "/home/atai/bot_data") + +# Pyrogram imports +from pyrogram import Client as PyrogramClient, filters +from pyrogram.types import Message as PyrogramMessage +from pyrogram.raw.functions.messages import GetBotCallbackAnswer + +# Telegram Bot imports +from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup +from telegram.ext import Application, CommandHandler, MessageHandler, CallbackQueryHandler, filters as tg_filters +from telegram.ext import ContextTypes + +# 导入数据库 +try: + from database import CacheDatabase +except ImportError: + CacheDatabase = None + logging.warning("database.py未找到,缓存功能将禁用") + +# ================== 配置 ================== +API_ID = 24660516 +API_HASH = "eae564578880a59c9963916ff1bbbd3a" +SESSION_NAME = "user_session" +BOT_TOKEN = "8426529617:AAHAxzohSMFBAxInzbAVJsZfkB5bHnOyFC4" +TARGET_BOT = "@openaiw_bot" +ADMIN_ID = 7363537082 + +# AI服务配置 +MAC_API_URL = "http://192.168.9.10:8000" + +# 搜索命令列表 +SEARCH_COMMANDS = ['/topchat', '/search', '/text', '/human'] + +# 日志配置 +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +# 使用增强型日志系统 +enhanced_log = EnhancedLogger("integrated_bot", log_dir="./logs") +logger = enhanced_log.get_logger() +logger.info("🚀 增强型日志系统已启动 - 所有日志将被完整保留") + +# 初始化Claude客户端 +try: + claude_client = anthropic.Anthropic( + api_key=os.environ.get('ANTHROPIC_AUTH_TOKEN'), + base_url=os.environ.get('ANTHROPIC_BASE_URL', 'https://api.anthropic.com') + ) + logger.info("✅ Claude API客户端已初始化") +except Exception as e: + logger.error(f"❌ Claude API初始化失败: {e}") + claude_client = None + + +def serialize_callback_data(value): + """将按钮callback_data序列化为可JSON存储的结构""" + if value is None: + return None + if isinstance(value, bytes): + return {"type": "bytes", "value": value.hex()} + if isinstance(value, str): + return {"type": "str", "value": value} + return None + + +def deserialize_callback_data(data): + """从缓存中恢复原始callback_data""" + if not data: + return None + if isinstance(data, dict): + data_type = data.get("type") + value = data.get("value") + if data_type == "bytes" and isinstance(value, str): + try: + return bytes.fromhex(value) + except ValueError: + return None + if data_type == "str": + return value + if isinstance(data, str): + if data.startswith('hex:'): + try: + return bytes.fromhex(data[4:]) + except ValueError: + return None + return data + return None + + + + + +# ================== 对话管理 ================== +class ConversationManager: + """管理用户对话上下文""" + + def __init__(self, max_history=5): + self.conversations = {} + self.max_history = max_history + + def add_message(self, user_id: int, role: str, content: str): + """添加消息到历史""" + if user_id not in self.conversations: + self.conversations[user_id] = [] + + self.conversations[user_id].append({ + "role": role, + "content": content, + "timestamp": datetime.now().isoformat() + }) + + # 保持最近的N条消息 + if len(self.conversations[user_id]) > self.max_history * 2: + self.conversations[user_id] = self.conversations[user_id][-self.max_history * 2:] + + def get_history(self, user_id: int, limit: int = 2) -> list: + """获取用户对话历史""" + if user_id not in self.conversations: + return [] + + history = self.conversations[user_id][-limit * 2:] + return [{"role": msg["role"], "content": msg["content"]} for msg in history] + + def clear_history(self, user_id: int): + """清空用户历史""" + if user_id in self.conversations: + del self.conversations[user_id] + + +# ================== 自动翻页管理器 ================== +class AutoPaginationManager: + """后台自动翻页 - 用户无感知""" + + def __init__(self, pyrogram_client, cache_db, target_bot_id, logger): + self.pyrogram_client = pyrogram_client + self.cache_db = cache_db + self.target_bot_id = target_bot_id + self.logger = logger + self.active_tasks = {} + + async def start_pagination(self, user_id, command, keyword, first_message): + """启动后台翻页任务""" + if user_id in self.active_tasks: + return + + task = asyncio.create_task(self._paginate(user_id, command, keyword, first_message)) + self.active_tasks[user_id] = task + self.logger.info(f"[翻页] 后台任务启动: {command} {keyword}") + + async def _paginate(self, user_id, command, keyword, message): + """执行翻页""" + try: + page = 1 + self._save_to_cache(command, keyword, page, message) + + if not self._has_next(message): + self.logger.info(f"[翻页] 只有1页") + return + + current = message + for page in range(2, 11): # 最多10页 + await asyncio.sleep(2) + + next_msg = await self._click_next(current) + if not next_msg: + break + + self._save_to_cache(command, keyword, page, next_msg) + self.logger.info(f"[翻页] 第{page}页已保存") + + if not self._has_next(next_msg): + self.logger.info(f"[翻页] 完成,共{page}页") + break + + current = next_msg + + except Exception as e: + self.logger.error(f"[翻页] 错误: {e}") + finally: + if user_id in self.active_tasks: + del self.active_tasks[user_id] + + def _has_next(self, msg): + """检查是否有下一页""" + if not msg.reply_markup: + return False + for row in msg.reply_markup.inline_keyboard: + for btn in row: + if btn.text and any(x in btn.text for x in ['下一页', 'Next', '▶']): + return True + return False + + async def _click_next(self, msg): + """点击下一页""" + try: + from pyrogram.raw.functions.messages import GetBotCallbackAnswer + + for row in msg.reply_markup.inline_keyboard: + for btn in row: + if btn.text and any(x in btn.text for x in ['下一页', 'Next', '▶']): + await self.pyrogram_client.invoke( + GetBotCallbackAnswer( + peer=await self.pyrogram_client.resolve_peer(self.target_bot_id), + msg_id=msg.id, + data=btn.callback_data + ) + ) + await asyncio.sleep(1.5) + return await self.pyrogram_client.get_messages(self.target_bot_id, msg.id) + except Exception as e: + self.logger.error(f"[翻页] 点击失败: {e}") + return None + + def _save_to_cache(self, cmd, keyword, page, msg): + """保存到缓存""" + if not self.cache_db: + return + try: + text = msg.text or msg.caption or "" + buttons = [] + if getattr(msg, 'reply_markup', None) and getattr(msg.reply_markup, 'inline_keyboard', None): + for row in msg.reply_markup.inline_keyboard: + for btn in row: + btn_data = {'text': btn.text} + if btn.url: + btn_data['url'] = btn.url + if btn.callback_data is not None: + serialized = serialize_callback_data(btn.callback_data) + if serialized: + btn_data['callback_data'] = serialized + buttons.append(btn_data) + self.cache_db.save_cache(cmd, keyword, page, text, buttons) + except Exception as e: + self.logger.error(f"[翻页] 保存失败: {e}") + +class IntegratedBotAI: + """整合的客服机器人 - AI增强版""" + + def __init__(self): + # Bot应用 + self.app = None + + # Pyrogram客户端(用于镜像) + self.pyrogram_client: Optional[PyrogramClient] = None + self.target_bot_id: Optional[int] = None + + # 消息映射 + self.pyrogram_to_telegram = {} + self.telegram_to_pyrogram = {} + self.callback_data_map = {} + self.user_search_sessions = {} + + # AI会话状态 + self.user_ai_sessions = {} + + # 缓存数据库 + self.cache_db = CacheDatabase() if CacheDatabase else None + + # 对话管理器 + self.conversation_manager = ConversationManager() + self.pagination_manager = None + + # 健康监控与告警 + self.health_monitor_task = None + self.remote_error_tracker = {"count": 0, "first_seen": datetime.now()} + self.last_admin_alert = datetime.now() - timedelta(minutes=30) + + async def setup_pyrogram(self): + """设置Pyrogram客户端""" + try: + proxy_config = None + if os.environ.get('ALL_PROXY'): + proxy_url = os.environ.get('ALL_PROXY', '').replace('socks5://', '') + if proxy_url: + host, port = proxy_url.split(':') + proxy_config = {"scheme": "socks5", "hostname": host, "port": int(port)} + + self.pyrogram_client = PyrogramClient( + SESSION_NAME, api_id=API_ID, api_hash=API_HASH, + proxy=proxy_config if proxy_config else None + ) + + await self.pyrogram_client.start() + logger.info("✅ Pyrogram客户端已启动") + + # 初始化自动翻页管理器 + self.pagination_manager = AutoPaginationManager( + self.pyrogram_client, self.cache_db, self.target_bot_id, logger + ) + logger.info("✅ 自动翻页管理器已初始化") + + target = await self.pyrogram_client.get_users(TARGET_BOT) + self.target_bot_id = target.id + logger.info(f"✅ 已连接到搜索机器人: {target.username}") + + @self.pyrogram_client.on_message(filters.user(self.target_bot_id)) + async def on_bot_response(_, message: PyrogramMessage): + await self.handle_search_response(message) + + @self.pyrogram_client.on_edited_message(filters.user(self.target_bot_id)) + async def on_message_edited(_, message: PyrogramMessage): + await self.handle_search_response(message, is_edit=True) + + return True + except Exception as e: + logger.error(f"Pyrogram设置失败: {e}") + return False + + async def call_ai_service(self, user_id: int, message: str, context: dict = None) -> dict: + """优化的Claude API调用 - 带上下文记忆和改进提示词""" + + if not claude_client: + logger.error("Claude客户端未初始化") + return { + "type": "auto", + "response": "👋 我来帮你搜索!\n\n直接发关键词,或试试:\n• /search 群组名\n• /text 讨论内容\n• /topchat 热门分类", + "confidence": 0.3 + } + + try: + logger.info(f"[用户 {user_id}] 调用Claude API: {message}") + + username = context.get('username', f'user_{user_id}') if context else f'user_{user_id}' + first_name = context.get('first_name', '') if context else '' + + # 构建对话历史 + messages = [] + + # 添加历史对话(最近2轮) + history = self.conversation_manager.get_history(user_id, limit=2) + messages.extend(history) + + # 添加当前消息(优化的提示词) + current_prompt = f"""你是@ktfund_bot的AI助手,专业的Telegram群组搜索助手。 + +【重要】你的回复中可以包含可执行的命令,我会为它们生成按钮。 +命令格式:/search 关键词 或 /text 关键词 + +用户信息:@{username} ({first_name}) +用户说:"{message}" + +【可用工具】 +• /search [关键词] - 搜索群组名称 +• /text [关键词] - 搜索讨论内容 +• /human [关键词] - 搜索用户 +• /topchat - 热门分类 + +【回复要求】 +1. 简短友好(2-4行) +2. 给1-2个具体命令建议 +3. 口语化,像朋友聊天 +4. 命令要在独立的一行 + +【示例】 +用户:"找AI群" +回复: +找AI群的话,试试: +/search AI +/text ChatGPT + +直接回复:""" + + messages.append({ + "role": "user", + "content": current_prompt + }) + + # 调用Claude API + response = claude_client.messages.create( + model="claude-sonnet-4-20250514", + max_tokens=512, + temperature=0.7, + messages=messages + ) + + claude_response = response.content[0].text.strip() + + # 保存对话历史 + self.conversation_manager.add_message(user_id, "user", message) + self.conversation_manager.add_message(user_id, "assistant", claude_response) + + logger.info(f"[用户 {user_id}] ✅ Claude回复成功 ({len(claude_response)}字)") + + # 智能提取命令建议 + suggested_commands = self._extract_commands(claude_response) + + return { + "type": "ai", + "response": claude_response, + "confidence": 1.0, + "suggested_commands": suggested_commands + } + + except Exception as e: + logger.error(f"[用户 {user_id}] ❌ Claude API失败: {e}") + return { + "type": "auto", + "response": "👋 我来帮你搜索!\n\n直接发关键词,或试试:\n• /search 群组名\n• /text 讨论内容\n• /topchat 热门分类", + "confidence": 0.3 + } + + def _extract_commands(self, response_text: str) -> list: + """从回复中提取建议的命令""" + import re + commands = [] + + # 匹配 /command pattern + patterns = [ + r'/search\s+[\w\s]+', + r'/text\s+[\w\s]+', + r'/human\s+[\w\s]+', + r'/topchat' + ] + + for pattern in patterns: + matches = re.findall(pattern, response_text) + commands.extend([m.strip() for m in matches[:1]]) + + return commands[:2] + + + + def _extract_command_buttons(self, text: str) -> list: + """从AI回复中提取命令按钮""" + import re + buttons = [] + + # 匹配:/command keyword + pattern = r'/(search|text|human|topchat)\s*([^\n]*)' + matches = re.findall(pattern, text, re.IGNORECASE) + + for cmd, keywords in matches[:3]: + cmd = cmd.lower() + keywords = keywords.strip()[:30] # 限制长度 + + if keywords: + display = f"/{cmd} {keywords}" + callback = f"cmd_{cmd}_{keywords.replace(' ', '_')}"[:64] + else: + display = f"/{cmd}" + callback = f"cmd_{cmd}" + + buttons.append((display, callback)) + + return buttons + + async def handle_start(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """处理/start命令 - AI引导模式""" + user = update.effective_user + user_id = user.id + + self.user_ai_sessions[user_id] = {"started_at": datetime.now(), "conversation": []} + + welcome_text = ( + f"👋 您好 {user.first_name}!\n\n" + "我是智能搜索助手,可以帮您找到Telegram上的群组和频道。\n\n" + "🔍 我能做什么:\n" + "• 搜索群组/频道\n" + "• 搜索特定话题的讨论\n" + "• 查找用户\n" + "• 浏览热门分类\n\n" + "💬 直接告诉我您想找什么,我会帮您选择最合适的搜索方式!" + ) + + keyboard = [ + [InlineKeyboardButton("🔍 搜索群组", callback_data="quick_search"), + InlineKeyboardButton("📚 使用指南", callback_data="quick_help")], + [InlineKeyboardButton("🔥 热门分类", callback_data="quick_topchat")] + ] + + await update.message.reply_text(welcome_text, reply_markup=InlineKeyboardMarkup(keyboard)) + + # 通知管理员 + admin_notification = ( + f"🆕 新用户访问 (AI模式):\n" + f"👤 {user.first_name} {user.last_name or ''}\n" + f"🆔 {user.id}\n" + f"👤 @{user.username or '无'}\n" + f"⏰ {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" + ) + await context.bot.send_message(chat_id=ADMIN_ID, text=admin_notification) + + async def handle_message(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """处理所有消息 - AI智能路由""" + if not update.message or not update.message.text: + return + + user = update.effective_user + user_id = user.id + text = update.message.text + is_admin = user_id == ADMIN_ID + + if is_admin and update.message.reply_to_message: + await self.handle_admin_reply(update, context) + return + + if self.is_search_command(text): + await self.handle_search_command(update, context) + return + + await self.handle_ai_conversation(update, context) + + def _prepare_keyword_for_buttons(self, keyword: str) -> Optional[tuple[str, str]]: + """根据用户输入生成展示关键词和callback参数""" + if not keyword: + return None + cleaned = re.sub(r'\s+', ' ', keyword.strip()) + if not cleaned: + return None + display = cleaned[:30] + callback_arg = display.replace(' ', '_') + return display, callback_arg + + + def is_search_command(self, text: str) -> bool: + """检查是否是搜索命令""" + return text and text.split()[0] in SEARCH_COMMANDS + + async def handle_ai_conversation(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """AI对话处理 - 带智能按钮""" + user = update.effective_user + user_id = user.id + message = update.message.text + + # 显示"正在输入" + await update.message.chat.send_action("typing") + + # 构建上下文 + user_context = { + "username": user.username or f"user{user_id}", + "first_name": user.first_name or "朋友", + "last_name": user.last_name + } + + # 调用AI + ai_response = await self.call_ai_service(user_id, message, user_context) + response_text = ai_response.get("response", "") + + # 提取命令按钮 + buttons = self._extract_command_buttons(response_text) + button_callbacks = {cb for _, cb in buttons} + + # 默认提供基于原始输入的命令按钮,确保用户可一键选择 + prepared = self._prepare_keyword_for_buttons(message) + if prepared: + display_kw, callback_kw = prepared + base_commands = [ + (f"/search {display_kw}", f"cmd_search_{callback_kw}"[:64]), + (f"/text {display_kw}", f"cmd_text_{callback_kw}"[:64]), + (f"/human {display_kw}", f"cmd_human_{callback_kw}"[:64]) + ] + default_buttons = [] + for display, callback in base_commands: + if callback not in button_callbacks: + default_buttons.append((display, callback)) + button_callbacks.add(callback) + if default_buttons: + buttons = default_buttons + buttons + + try: + if buttons: + # 构建按钮键盘 + keyboard = [] + for display, callback in buttons: + keyboard.append([InlineKeyboardButton( + f"🔍 {display}", + callback_data=callback + )]) + + # 添加常用按钮 + keyboard.append([ + InlineKeyboardButton("🔥 热门目录", callback_data="cmd_topchat"), + InlineKeyboardButton("📖 帮助", callback_data="cmd_help") + ]) + + await update.message.reply_text( + response_text, + reply_markup=InlineKeyboardMarkup(keyboard) + ) + logger.info(f"[AI对话] 已回复用户 {user_id} (带{len(buttons)}个按钮)") + else: + # 无按钮版本 + await update.message.reply_text(response_text) + logger.info(f"[AI对话] 已回复用户 {user_id}") + + except Exception as e: + logger.error(f"[AI对话] 发送失败: {e}, 降级为纯文本") + try: + await update.message.reply_text(response_text) + except: + await update.message.reply_text("抱歉,回复失败。请直接发送命令,如:/search AI") + + + + async def handle_search_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """处理搜索命令 - 带缓存""" + user = update.effective_user + user_id = user.id + command = update.message.text + + # 提取命令和关键词 + parts = command.split(maxsplit=1) + cmd = parts[0] + keyword = parts[1] if len(parts) > 1 else "" + + # 检查缓存 + if self.cache_db and keyword: + cached = self.cache_db.get_cache(cmd, keyword, 1) + if cached: + logger.info(f"[缓存命中] {cmd} {keyword} page1") + + # 恢复按钮 + keyboard = None + if cached.get('buttons'): + buttons = [] + for btn_data in cached['buttons']: + if btn_data.get('url'): + buttons.append([InlineKeyboardButton(text=btn_data['text'], url=btn_data['url'])]) + elif btn_data.get('callback_data'): + original_callback = deserialize_callback_data(btn_data.get('callback_data')) + if original_callback is not None: + callback_id = f"cb_{time.time():.0f}_{len(self.callback_data_map)}" + # 需要存储原始message_id,这里用0作为占位符,实际翻页时从缓存获取 + self.callback_data_map[callback_id] = (0, original_callback) + buttons.append([InlineKeyboardButton(text=btn_data['text'], callback_data=callback_id[:64])]) + + if buttons: + keyboard = InlineKeyboardMarkup(buttons) + + # 发送缓存结果(带按钮) + sent = await update.message.reply_text( + cached['text'][:4000], + reply_markup=keyboard, + parse_mode='HTML' + ) + + # 记录会话,以便翻页时使用 + self.user_search_sessions[user_id] = { + 'chat_id': update.effective_chat.id, + 'wait_msg_id': sent.message_id, + 'command': cmd, + 'keyword': keyword, + 'timestamp': datetime.now() + } + + return + + # 通知管理员 + admin_notification = ( + f"🔍 用户执行搜索:\n" + f"👤 {user.first_name} {user.last_name or ''}\n" + f"🆔 {user_id}\n" + f"📝 {command}\n" + f"⏰ {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" + ) + await context.bot.send_message(chat_id=ADMIN_ID, text=admin_notification) + + wait_msg = await update.message.reply_text("🔍 正在搜索,请稍候...") + + self.user_search_sessions[user_id] = { + 'chat_id': update.effective_chat.id, + 'wait_msg_id': wait_msg.message_id, + 'command': cmd, + 'keyword': keyword, + 'timestamp': datetime.now() + } + + await self.pyrogram_client.send_message(self.target_bot_id, command) + logger.info(f"搜索: {command}") + + async def handle_search_response(self, message: PyrogramMessage, is_edit: bool = False): + """处理搜索机器人的响应 - 保存到缓存""" + try: + if not self.user_search_sessions: + return + + user_id = max(self.user_search_sessions.keys(), key=lambda k: self.user_search_sessions[k]['timestamp']) + session = self.user_search_sessions[user_id] + + text = message.text or message.caption or "无结果" + + try: + if message.text and hasattr(message.text, 'html'): + text = message.text.html + except: + pass + + keyboard = self.convert_keyboard(message) + + if is_edit and message.id in self.pyrogram_to_telegram: + telegram_msg_id = self.pyrogram_to_telegram[message.id] + await self.app.bot.edit_message_text( + chat_id=session['chat_id'], + message_id=telegram_msg_id, + text=text[:4000], + reply_markup=keyboard, + parse_mode='HTML' + ) + else: + try: + await self.app.bot.delete_message( + chat_id=session['chat_id'], + message_id=session['wait_msg_id'] + ) + except: + pass + + sent = await self.app.bot.send_message( + chat_id=session['chat_id'], + text=text[:4000], + reply_markup=keyboard, + parse_mode='HTML' + ) + + self.pyrogram_to_telegram[message.id] = sent.message_id + self.telegram_to_pyrogram[sent.message_id] = message.id + + # 保存到缓存 + if self.cache_db and session.get('keyword'): + buttons = self.extract_buttons(message) + self.cache_db.save_cache( + session['command'], + session['keyword'], + 1, # 第一页 + text, + buttons + ) + + # 后台自动翻页(用户无感知) + if self.pagination_manager: + asyncio.create_task( + self.pagination_manager.start_pagination( + user_id, session['command'], session['keyword'], message + ) + ) + + except Exception as e: + logger.error(f"处理搜索响应失败: {e}") + + def convert_keyboard(self, message: PyrogramMessage) -> Optional[InlineKeyboardMarkup]: + """转换键盘""" + if not message.reply_markup or not message.reply_markup.inline_keyboard: + return None + + try: + buttons = [] + for row in message.reply_markup.inline_keyboard: + button_row = [] + for btn in row: + if btn.url: + button_row.append(InlineKeyboardButton(text=btn.text, url=btn.url)) + elif btn.callback_data: + callback_id = f"cb_{time.time():.0f}_{len(self.callback_data_map)}" + self.callback_data_map[callback_id] = (message.id, btn.callback_data) + button_row.append(InlineKeyboardButton(text=btn.text, callback_data=callback_id[:64])) + + if button_row: + buttons.append(button_row) + + return InlineKeyboardMarkup(buttons) if buttons else None + except Exception as e: + logger.error(f"键盘转换失败: {e}") + return None + + def extract_buttons(self, message: PyrogramMessage) -> list: + """提取按钮数据(包含callback_data用于缓存)""" + if not message.reply_markup or not message.reply_markup.inline_keyboard: + return [] + + buttons = [] + for row in message.reply_markup.inline_keyboard: + for btn in row: + btn_data = {"text": btn.text} + if btn.url: + btn_data["url"] = btn.url + if btn.callback_data is not None: + serialized = serialize_callback_data(btn.callback_data) + if serialized: + btn_data["callback_data"] = serialized + buttons.append(btn_data) + return buttons + + async def handle_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """处理按钮点击 - 执行搜索命令或翻页""" + query = update.callback_query + data = query.data + user = query.from_user + + logger.info(f"[回调] 收到callback: user={user.id}, data={data}") + + await query.answer() + + if data.startswith("cb_"): + # 处理翻页按钮 + if data in self.callback_data_map: + orig_msg_id, orig_callback = self.callback_data_map[data] + logger.info(f"[翻页] 用户 {user.id} 点击: {orig_callback}") + + if isinstance(orig_callback, bytes): + orig_callback_bytes = orig_callback + try: + orig_callback_text = orig_callback.decode() + except UnicodeDecodeError: + orig_callback_text = None + else: + orig_callback_text = str(orig_callback) if orig_callback is not None else None + orig_callback_bytes = orig_callback_text.encode() if orig_callback_text is not None else None + + session = self.user_search_sessions.get(user.id) + + # 解析callback_data获取页码(格式如:page_2) + try: + if orig_callback_text and orig_callback_text.startswith("page_"): + page = int(orig_callback_text.split("_")[1]) + + # 从会话获取搜索信息 + if session and 'command' in session and 'keyword' in session: + cmd = session['command'] + keyword = session['keyword'] + + # 先检查缓存 + cached = self.cache_db.get_cache(cmd, keyword, page) if self.cache_db else None + if cached: + logger.info(f"[翻页缓存] 命中: {cmd} {keyword} page{page}") + + # 从缓存恢复按钮 + keyboard = None + if cached.get('buttons'): + buttons = [] + for btn_data in cached['buttons']: + if btn_data.get('url'): + buttons.append([InlineKeyboardButton(text=btn_data['text'], url=btn_data['url'])]) + elif btn_data.get('callback_data'): + restored = deserialize_callback_data(btn_data.get('callback_data')) + if restored is not None: + # 重新生成callback_id + callback_id = f"cb_{time.time():.0f}_{len(self.callback_data_map)}" + self.callback_data_map[callback_id] = (orig_msg_id, restored) + buttons.append([InlineKeyboardButton(text=btn_data['text'], callback_data=callback_id[:64])]) + + if buttons: + keyboard = InlineKeyboardMarkup(buttons) + + # 发送缓存结果 + await query.message.edit_text( + text=cached['text'], + reply_markup=keyboard, + parse_mode='HTML' + ) + return + + else: + logger.info(f"[翻页] 缓存未命中,转发到搜索bot") + + # 如果缓存未命中或不是page_格式,转发到搜索bot + if orig_callback_bytes is None: + raise ValueError("callback_data 无法编码") + + await self.pyrogram_client.request_callback_answer( + chat_id=self.target_bot_id, + message_id=orig_msg_id, + callback_data=orig_callback_bytes + ) + + # 记录等待响应 + self.user_search_sessions[user.id] = { + 'chat_id': query.message.chat_id, + 'wait_msg_id': query.message.message_id, + 'command': session.get('command') if session else None, + 'keyword': session.get('keyword') if session else None, + 'timestamp': datetime.now() + } + + logger.info(f"[翻页] 已转发callback到搜索bot") + + except Exception as e: + logger.error(f"[翻页] 处理失败: {e}") + await query.message.reply_text("❌ 翻页失败,请稍后重试") + else: + logger.warning(f"[翻页] callback_id不存在: {data}") + await query.message.reply_text("❌ 按钮已过期,请重新搜索") + + elif data.startswith("cmd_"): + # 解析命令 + parts = data.replace("cmd_", "").split("_", 1) + cmd = parts[0] + keywords = parts[1].replace("_", " ") if len(parts) > 1 else "" + + # 构造完整命令 + command = f"/{cmd} {keywords}" if keywords else f"/{cmd}" + + logger.info(f"[用户 {user.id}] 点击按钮: {command}") + + # 显示执行提示 + await query.message.reply_text(f"🔍 正在执行:{command}\n请稍候...") + + # 转发到搜索bot + try: + await self.pyrogram_client.send_message(self.target_bot_id, command) + + # 记录搜索会话 + self.user_search_sessions[user.id] = { + 'chat_id': query.message.chat_id, + 'wait_msg_id': query.message.message_id + 1, + 'command': f"/{cmd}", + 'keyword': keywords, + 'timestamp': datetime.now() + } + + logger.info(f"[镜像] 已转发: {command}") + + except Exception as e: + logger.error(f"[镜像] 转发失败: {e}") + await query.message.reply_text("❌ 搜索失败,请稍后重试或直接发送命令") + + elif data == "quick_search": + # 搜索群组引导 + keyboard = [ + [InlineKeyboardButton("🔍 搜索群组", callback_data="cmd_search")], + [InlineKeyboardButton("💬 搜索消息内容", callback_data="cmd_text")], + [InlineKeyboardButton("👤 搜索用户", callback_data="cmd_human")] + ] + await query.message.edit_text( + "请选择搜索类型,或直接发送关键词:", + reply_markup=InlineKeyboardMarkup(keyboard) + ) + + elif data == "quick_help": + await query.message.edit_text( + "📖 使用指南:\n\n" + "🔍 搜索方式:\n" + "• /search [关键词] - 按群组名称搜索\n" + "• /text [关键词] - 按消息内容搜索\n" + "• /human [关键词] - 按用户名搜索\n" + "• /topchat - 浏览热门群组目录\n\n" + "💡 快捷使用:\n" + "直接发送关键词,我会智能分析并选择最合适的搜索方式!\n\n" + "📋 示例:\n" + "• 发送 '区块链' → 自动搜索相关群组\n" + "• 发送 'NFT交易' → 智能搜索讨论内容\n\n" + "❓ 有任何问题都可以直接问我!" + ) + + elif data == "quick_topchat": + # 直接触发topchat命令 + logger.info(f"[用户 {user.id}] 点击热门分类按钮") + await query.message.edit_text("🔥 正在加载热门分类...\n请稍候...") + + try: + await self.pyrogram_client.send_message(self.target_bot_id, "/topchat") + self.user_search_sessions[user.id] = { + 'chat_id': query.message.chat_id, + 'wait_msg_id': query.message.message_id, + 'command': '/topchat', + 'keyword': '', + 'timestamp': datetime.now() + } + logger.info(f"[镜像] 已转发: /topchat") + except Exception as e: + logger.error(f"[镜像] 转发失败: {e}") + await query.message.edit_text("❌ 加载失败,请稍后重试") + + elif data == "cmd_help": + await query.message.reply_text( + "📖 使用指南:\n\n" + "• /search [关键词] - 按群组名称搜索\n" + "• /text [关键词] - 按消息内容搜索\n" + "• /human [关键词] - 按用户名搜索\n" + "• /topchat - 热门群组目录\n\n" + "💡 或者直接告诉我你想找什么!" + ) + + else: + logger.warning(f"未知callback: {data}") + + + async def handle_admin_reply(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """处理管理员回复""" + reply_to = update.message.reply_to_message + if not reply_to or not reply_to.text: + return + + import re + user_id = None + for line in reply_to.text.split('\n'): + if '🆔' in line or 'ID:' in line: + numbers = re.findall(r'\d+', line) + if numbers: + user_id = int(numbers[0]) + break + + if not user_id: + await update.message.reply_text("❌ 无法识别用户ID") + return + + try: + await context.bot.send_message(chat_id=user_id, text=update.message.text) + await update.message.reply_text(f"✅ 已回复给用户 {user_id}") + except Exception as e: + await update.message.reply_text(f"❌ 回复失败: {str(e)}") + + def _reset_remote_error_counter(self): + """重置网络异常计数""" + self.remote_error_tracker["count"] = 0 + self.remote_error_tracker["first_seen"] = datetime.now() + + async def _record_remote_error(self, source: str, exception: Exception): + """记录网络异常并必要时提醒管理员""" + now = datetime.now() + tracker = self.remote_error_tracker + if now - tracker["first_seen"] > timedelta(minutes=5): + tracker["first_seen"] = now + tracker["count"] = 0 + tracker["count"] += 1 + logger.warning(f"[网络波动] {source} 异常: {exception}") + + if tracker["count"] >= 3 and (now - self.last_admin_alert) > timedelta(minutes=10): + self.last_admin_alert = now + if self.app: + try: + await self.app.bot.send_message( + chat_id=ADMIN_ID, + text=( + "⚠️ 网络波动提醒\n" + f"5分钟内出现 {tracker['count']} 次连接异常。\n" + "机器人已尝试自动恢复,请关注代理或网络状况。" + ) + ) + except Exception as notify_error: + logger.error(f"管理员通知失败: {notify_error}") + + async def handle_error(self, update: object, context: ContextTypes.DEFAULT_TYPE): + """统一错误处理,捕获网络异常并记录""" + error = getattr(context, 'error', None) + if not error: + return + logger.error(f"处理更新时发生异常: {error!r}") + + if isinstance(error, httpx.RemoteProtocolError): + await self._record_remote_error("Telegram轮询", error) + + async def monitor_health(self): + """周期性健康检查,确保Telegram与Pyrogram连接可用""" + try: + while True: + await asyncio.sleep(120) + if not self.app: + continue + + # 检查 Telegram Bot 接口 + try: + await self.app.bot.get_me() + self._reset_remote_error_counter() + except httpx.HTTPError as e: + await self._record_remote_error("Telegram接口", e) + except Exception as e: + logger.warning(f"Telegram 健康检查异常: {e}") + + # 检查 Pyrogram 连接 + if self.pyrogram_client and not getattr(self.pyrogram_client, "is_connected", False): + logger.warning("Pyrogram 客户端掉线,尝试重连...") + try: + await self.pyrogram_client.start() + logger.info("Pyrogram 客户端重连成功") + except Exception as reconnect_error: + await self._record_remote_error("Pyrogram重连", reconnect_error) + except asyncio.CancelledError: + logger.info("健康监控任务已停止") + raise + async def initialize(self): + """初始化机器人""" + try: + logger.info("正在初始化整合机器人...") + + if not await self.setup_pyrogram(): + logger.error("Pyrogram初始化失败") + return False + + builder = ( + Application.builder() + .token(BOT_TOKEN) + .connect_timeout(10) + .read_timeout(60) + .write_timeout(60) + .pool_timeout(10) + ) + + if os.environ.get('HTTP_PROXY'): + proxy_url = os.environ.get('HTTP_PROXY') + logger.info(f"配置Telegram Bot代理: {proxy_url}") + request = httpx.AsyncClient(proxies={"http://": proxy_url, "https://": proxy_url}, timeout=30.0) + builder = builder.request(request) + + self.app = builder.build() + self.app.add_error_handler(self.handle_error) + + self.app.add_handler(CommandHandler("start", self.handle_start)) + self.app.add_handler(CallbackQueryHandler(self.handle_callback)) + self.app.add_handler(MessageHandler(tg_filters.ALL, self.handle_message)) + + logger.info("✅ 整合机器人初始化完成") + return True + + except Exception as e: + logger.error(f"初始化失败: {e}") + return False + + async def run(self): + """运行机器人""" + try: + await self.app.initialize() + await self.app.start() + await self.app.updater.start_polling(drop_pending_updates=True) + + if not self.health_monitor_task or self.health_monitor_task.done(): + self.health_monitor_task = asyncio.create_task(self.monitor_health()) + + logger.info("="*50) + logger.info("✅ AI增强版Bot已启动") + logger.info(f"AI服务: {MAC_API_URL}") + logger.info(f"缓存功能: {'启用' if self.cache_db else '禁用'}") + logger.info("="*50) + + await asyncio.Event().wait() + + except KeyboardInterrupt: + logger.info("收到停止信号") + finally: + await self.cleanup() + + async def cleanup(self): + """清理资源""" + logger.info("正在清理...") + + if self.health_monitor_task: + self.health_monitor_task.cancel() + try: + await self.health_monitor_task + except asyncio.CancelledError: + pass + self.health_monitor_task = None + + if self.app: + await self.app.updater.stop() + await self.app.stop() + await self.app.shutdown() + + if self.pyrogram_client: + await self.pyrogram_client.stop() + + logger.info("✅ 清理完成") + + +async def main(): + """主函数""" + bot = IntegratedBotAI() + + if await bot.initialize(): + await bot.run() + else: + logger.error("初始化失败,退出") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/integrated_bot_ai.py.backup_20251006_165614 b/integrated_bot_ai.py.backup_20251006_165614 new file mode 100755 index 0000000..1c93e5f --- /dev/null +++ b/integrated_bot_ai.py.backup_20251006_165614 @@ -0,0 +1,561 @@ +#!/usr/bin/env python3 +""" +整合版客服机器人 - AI增强版 +包含: +1. AI对话引导 +2. 镜像搜索功能 +3. 自动翻页缓存 +4. 智能去重 +""" + +import asyncio +import logging +import time +import os +import httpx +import json +import sys +from typing import Dict, Optional +from datetime import datetime + +# 添加路径 +sys.path.insert(0, "/home/atai/bot_data") + +# Pyrogram imports +from pyrogram import Client as PyrogramClient, filters +from pyrogram.types import Message as PyrogramMessage +from pyrogram.raw.functions.messages import GetBotCallbackAnswer + +# Telegram Bot imports +from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup +from telegram.ext import Application, CommandHandler, MessageHandler, CallbackQueryHandler, filters as tg_filters +from telegram.ext import ContextTypes + +# 导入数据库 +try: + from database import CacheDatabase +except ImportError: + CacheDatabase = None + logging.warning("database.py未找到,缓存功能将禁用") + +# ================== 配置 ================== +API_ID = 24660516 +API_HASH = "eae564578880a59c9963916ff1bbbd3a" +SESSION_NAME = "user_session" +BOT_TOKEN = "8426529617:AAHAxzohSMFBAxInzbAVJsZfkB5bHnOyFC4" +TARGET_BOT = "@openaiw_bot" +ADMIN_ID = 7363537082 + +# AI服务配置 +MAC_API_URL = "http://192.168.9.10:8000" + +# 搜索命令列表 +SEARCH_COMMANDS = ['/topchat', '/search', '/text', '/human'] + +# 日志配置 +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + + +class IntegratedBotAI: + """整合的客服机器人 - AI增强版""" + + def __init__(self): + # Bot应用 + self.app = None + + # Pyrogram客户端(用于镜像) + self.pyrogram_client: Optional[PyrogramClient] = None + self.target_bot_id: Optional[int] = None + + # 消息映射 + self.pyrogram_to_telegram = {} + self.telegram_to_pyrogram = {} + self.callback_data_map = {} + self.user_search_sessions = {} + + # AI会话状态 + self.user_ai_sessions = {} + + # 缓存数据库 + self.cache_db = CacheDatabase() if CacheDatabase else None + + async def setup_pyrogram(self): + """设置Pyrogram客户端""" + try: + proxy_config = None + if os.environ.get('ALL_PROXY'): + proxy_url = os.environ.get('ALL_PROXY', '').replace('socks5://', '') + if proxy_url: + host, port = proxy_url.split(':') + proxy_config = {"scheme": "socks5", "hostname": host, "port": int(port)} + + self.pyrogram_client = PyrogramClient( + SESSION_NAME, api_id=API_ID, api_hash=API_HASH, + proxy=proxy_config if proxy_config else None + ) + + await self.pyrogram_client.start() + logger.info("✅ Pyrogram客户端已启动") + + target = await self.pyrogram_client.get_users(TARGET_BOT) + self.target_bot_id = target.id + logger.info(f"✅ 已连接到搜索机器人: {target.username}") + + @self.pyrogram_client.on_message(filters.user(self.target_bot_id)) + async def on_bot_response(_, message: PyrogramMessage): + await self.handle_search_response(message) + + @self.pyrogram_client.on_edited_message(filters.user(self.target_bot_id)) + async def on_message_edited(_, message: PyrogramMessage): + await self.handle_search_response(message, is_edit=True) + + return True + except Exception as e: + logger.error(f"Pyrogram设置失败: {e}") + return False + + async def call_ai_service(self, user_id: int, message: str, context: dict = None) -> dict: + """调用Mac AI服务 - 带重试和降级""" + # 尝试3次 + for attempt in range(3): + try: + async with httpx.AsyncClient(timeout=5.0) as client: + response = await client.post( + f"{MAC_API_URL}/ai/chat", + json={"user_id": user_id, "message": message, "context": context or {}} + ) + if response.status_code == 200: + logger.info(f"✅ AI服务响应成功 (尝试{attempt+1}/3)") + return response.json() + else: + logger.warning(f"AI服务返回错误: {response.status_code} (尝试{attempt+1}/3)") + except asyncio.TimeoutError: + logger.warning(f"AI服务超时 (尝试{attempt+1}/3)") + if attempt < 2: + await asyncio.sleep(0.5) + continue + except Exception as e: + logger.warning(f"AI服务连接失败: {e} (尝试{attempt+1}/3)") + if attempt < 2: + await asyncio.sleep(0.5) + continue + + # 降级:返回简单的自动回复 + logger.error("AI服务不可用,使用降级模式") + return { + "type": "auto", + "response": "👋 请直接发送搜索关键词,或使用以下命令:\n\n• /search [关键词] - 搜索群组名称\n• /text [关键词] - 搜索消息内容\n• /topchat - 热门分类", + "confidence": 0.5 + } + + async def handle_start(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """处理/start命令 - AI引导模式""" + user = update.effective_user + user_id = user.id + + self.user_ai_sessions[user_id] = {"started_at": datetime.now(), "conversation": []} + + welcome_text = ( + f"👋 您好 {user.first_name}!\n\n" + "我是智能搜索助手,可以帮您找到Telegram上的群组和频道。\n\n" + "🔍 我能做什么:\n" + "• 搜索群组/频道\n" + "• 搜索特定话题的讨论\n" + "• 查找用户\n" + "• 浏览热门分类\n\n" + "💬 直接告诉我您想找什么,我会帮您选择最合适的搜索方式!" + ) + + keyboard = [ + [InlineKeyboardButton("🔍 搜索群组", callback_data="quick_search"), + InlineKeyboardButton("📚 使用指南", callback_data="quick_help")], + [InlineKeyboardButton("🔥 热门分类", callback_data="quick_topchat")] + ] + + await update.message.reply_text(welcome_text, reply_markup=InlineKeyboardMarkup(keyboard)) + + # 通知管理员 + admin_notification = ( + f"🆕 新用户访问 (AI模式):\n" + f"👤 {user.first_name} {user.last_name or ''}\n" + f"🆔 {user.id}\n" + f"👤 @{user.username or '无'}\n" + f"⏰ {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" + ) + await context.bot.send_message(chat_id=ADMIN_ID, text=admin_notification) + + async def handle_message(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """处理所有消息 - AI智能路由""" + if not update.message or not update.message.text: + return + + user = update.effective_user + user_id = user.id + text = update.message.text + is_admin = user_id == ADMIN_ID + + if is_admin and update.message.reply_to_message: + await self.handle_admin_reply(update, context) + return + + if self.is_search_command(text): + await self.handle_search_command(update, context) + return + + await self.handle_ai_conversation(update, context) + + def is_search_command(self, text: str) -> bool: + """检查是否是搜索命令""" + return text and text.split()[0] in SEARCH_COMMANDS + + async def handle_ai_conversation(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """AI对话处理""" + user = update.effective_user + user_id = user.id + message = update.message.text + + # 构建上下文信息 + user_context = { + "username": user.username or f"{user.first_name}_{user.id}", + "first_name": user.first_name, + "last_name": user.last_name + } + + ai_response = await self.call_ai_service(user_id, message, user_context) + + if ai_response.get("type") == "auto": + response_text = ai_response.get("response", "") + suggested_cmd = ai_response.get("suggested_command") + keywords = ai_response.get("keywords") + + if suggested_cmd and keywords: + keyboard = [ + [InlineKeyboardButton( + f"✅ 开始搜索: {suggested_cmd} {keywords}", + callback_data=f"exec_{suggested_cmd.replace('/', '')}_{keywords}"[:64] + )], + [InlineKeyboardButton("✏️ 修改关键词", callback_data="modify_keywords")] + ] + await update.message.reply_text(response_text, reply_markup=InlineKeyboardMarkup(keyboard)) + + self.user_ai_sessions[user_id] = { + "suggested_command": suggested_cmd, + "keywords": keywords, + "original_message": message + } + else: + await update.message.reply_text(response_text) + else: + response_text = ai_response.get("response", "抱歉,我没有理解您的需求。") + await update.message.reply_text(response_text) + + async def handle_search_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """处理搜索命令 - 带缓存""" + user = update.effective_user + user_id = user.id + command = update.message.text + + # 提取命令和关键词 + parts = command.split(maxsplit=1) + cmd = parts[0] + keyword = parts[1] if len(parts) > 1 else "" + + # 检查缓存 + if self.cache_db and keyword: + cached = self.cache_db.get_cache(cmd, keyword, 1) + if cached: + logger.info(f"返回缓存结果: {cmd} {keyword}") + await update.message.reply_text( + f"📦 从缓存返回结果:\n\n{cached['text'][:4000]}", + parse_mode='HTML' + ) + return + + # 通知管理员 + admin_notification = ( + f"🔍 用户执行搜索:\n" + f"👤 {user.first_name} {user.last_name or ''}\n" + f"🆔 {user_id}\n" + f"📝 {command}\n" + f"⏰ {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" + ) + await context.bot.send_message(chat_id=ADMIN_ID, text=admin_notification) + + wait_msg = await update.message.reply_text("🔍 正在搜索,请稍候...") + + self.user_search_sessions[user_id] = { + 'chat_id': update.effective_chat.id, + 'wait_msg_id': wait_msg.message_id, + 'command': cmd, + 'keyword': keyword, + 'timestamp': datetime.now() + } + + await self.pyrogram_client.send_message(self.target_bot_id, command) + logger.info(f"搜索: {command}") + + async def handle_search_response(self, message: PyrogramMessage, is_edit: bool = False): + """处理搜索机器人的响应 - 保存到缓存""" + try: + if not self.user_search_sessions: + return + + user_id = max(self.user_search_sessions.keys(), key=lambda k: self.user_search_sessions[k]['timestamp']) + session = self.user_search_sessions[user_id] + + text = message.text or message.caption or "无结果" + + try: + if message.text and hasattr(message.text, 'html'): + text = message.text.html + except: + pass + + keyboard = self.convert_keyboard(message) + + if is_edit and message.id in self.pyrogram_to_telegram: + telegram_msg_id = self.pyrogram_to_telegram[message.id] + await self.app.bot.edit_message_text( + chat_id=session['chat_id'], + message_id=telegram_msg_id, + text=text[:4000], + reply_markup=keyboard, + parse_mode='HTML' + ) + else: + try: + await self.app.bot.delete_message( + chat_id=session['chat_id'], + message_id=session['wait_msg_id'] + ) + except: + pass + + sent = await self.app.bot.send_message( + chat_id=session['chat_id'], + text=text[:4000], + reply_markup=keyboard, + parse_mode='HTML' + ) + + self.pyrogram_to_telegram[message.id] = sent.message_id + self.telegram_to_pyrogram[sent.message_id] = message.id + + # 保存到缓存 + if self.cache_db and session.get('keyword'): + buttons = self.extract_buttons(message) + self.cache_db.save_cache( + session['command'], + session['keyword'], + 1, # 第一页 + text, + text, + buttons + ) + + except Exception as e: + logger.error(f"处理搜索响应失败: {e}") + + def convert_keyboard(self, message: PyrogramMessage) -> Optional[InlineKeyboardMarkup]: + """转换键盘""" + if not message.reply_markup or not message.reply_markup.inline_keyboard: + return None + + try: + buttons = [] + for row in message.reply_markup.inline_keyboard: + button_row = [] + for btn in row: + if btn.url: + button_row.append(InlineKeyboardButton(text=btn.text, url=btn.url)) + elif btn.callback_data: + callback_id = f"cb_{time.time():.0f}_{len(self.callback_data_map)}" + self.callback_data_map[callback_id] = (message.id, btn.callback_data) + button_row.append(InlineKeyboardButton(text=btn.text, callback_data=callback_id[:64])) + + if button_row: + buttons.append(button_row) + + return InlineKeyboardMarkup(buttons) if buttons else None + except Exception as e: + logger.error(f"键盘转换失败: {e}") + return None + + def extract_buttons(self, message: PyrogramMessage) -> list: + """提取按钮数据""" + if not message.reply_markup or not message.reply_markup.inline_keyboard: + return [] + + buttons = [] + for row in message.reply_markup.inline_keyboard: + for btn in row: + buttons.append({"text": btn.text, "url": btn.url if btn.url else None}) + return buttons + + async def handle_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """处理回调查询""" + query = update.callback_query + data = query.data + + await query.answer() + + if data == "quick_search": + await query.message.reply_text("请告诉我您想搜索什么内容") + return + elif data == "quick_help": + await query.message.reply_text( + "📖 使用指南:\n\n" + "• /search [关键词] - 按群组名称搜索\n" + "• /text [关键词] - 按消息内容搜索\n" + "• /human [关键词] - 按用户名搜索\n" + "• /topchat - 热门群组目录" + ) + return + elif data == "quick_topchat": + # 创建假update来执行搜索 + from types import SimpleNamespace + fake_update = SimpleNamespace( + effective_user=query.from_user, + effective_chat=query.message.chat, + message=SimpleNamespace(text='/topchat') + ) + await self.handle_search_command(fake_update, context) + return + elif data.startswith("exec_"): + parts = data.replace("exec_", "").split("_", 1) + if len(parts) == 2: + command, keywords = parts + search_text = f"/{command} {keywords}" + from types import SimpleNamespace + fake_update = SimpleNamespace( + effective_user=query.from_user, + effective_chat=query.message.chat, + message=SimpleNamespace(text=search_text) + ) + await self.handle_search_command(fake_update, context) + return + + # 翻页callback + if data in self.callback_data_map: + pyrogram_msg_id, original_callback = self.callback_data_map[data] + try: + if not isinstance(original_callback, bytes): + original_callback = original_callback.encode() if original_callback else b'' + + await self.pyrogram_client.invoke( + GetBotCallbackAnswer( + peer=await self.pyrogram_client.resolve_peer(self.target_bot_id), + msg_id=pyrogram_msg_id, + data=original_callback + ) + ) + await asyncio.sleep(1) + except Exception as e: + logger.error(f"Callback处理失败: {e}") + + async def handle_admin_reply(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """处理管理员回复""" + reply_to = update.message.reply_to_message + if not reply_to or not reply_to.text: + return + + import re + user_id = None + for line in reply_to.text.split('\n'): + if '🆔' in line or 'ID:' in line: + numbers = re.findall(r'\d+', line) + if numbers: + user_id = int(numbers[0]) + break + + if not user_id: + await update.message.reply_text("❌ 无法识别用户ID") + return + + try: + await context.bot.send_message(chat_id=user_id, text=update.message.text) + await update.message.reply_text(f"✅ 已回复给用户 {user_id}") + except Exception as e: + await update.message.reply_text(f"❌ 回复失败: {str(e)}") + + async def initialize(self): + """初始化机器人""" + try: + logger.info("正在初始化整合机器人...") + + if not await self.setup_pyrogram(): + logger.error("Pyrogram初始化失败") + return False + + builder = Application.builder().token(BOT_TOKEN) + + if os.environ.get('HTTP_PROXY'): + proxy_url = os.environ.get('HTTP_PROXY') + logger.info(f"配置Telegram Bot代理: {proxy_url}") + request = httpx.AsyncClient(proxies={"http://": proxy_url, "https://": proxy_url}, timeout=30.0) + builder = builder.request(request) + + self.app = builder.build() + + self.app.add_handler(CommandHandler("start", self.handle_start)) + self.app.add_handler(CallbackQueryHandler(self.handle_callback)) + self.app.add_handler(MessageHandler(tg_filters.ALL, self.handle_message)) + + logger.info("✅ 整合机器人初始化完成") + return True + + except Exception as e: + logger.error(f"初始化失败: {e}") + return False + + async def run(self): + """运行机器人""" + try: + await self.app.initialize() + await self.app.start() + await self.app.updater.start_polling(drop_pending_updates=True) + + logger.info("="*50) + logger.info("✅ AI增强版Bot已启动") + logger.info(f"AI服务: {MAC_API_URL}") + logger.info(f"缓存功能: {'启用' if self.cache_db else '禁用'}") + logger.info("="*50) + + await asyncio.Event().wait() + + except KeyboardInterrupt: + logger.info("收到停止信号") + finally: + await self.cleanup() + + async def cleanup(self): + """清理资源""" + logger.info("正在清理...") + + if self.app: + await self.app.updater.stop() + await self.app.stop() + await self.app.shutdown() + + if self.pyrogram_client: + await self.pyrogram_client.stop() + + logger.info("✅ 清理完成") + + +async def main(): + """主函数""" + bot = IntegratedBotAI() + + if await bot.initialize(): + await bot.run() + else: + logger.error("初始化失败,退出") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/integrated_bot_ai.py.backup_before_admin b/integrated_bot_ai.py.backup_before_admin new file mode 100755 index 0000000..784f846 --- /dev/null +++ b/integrated_bot_ai.py.backup_before_admin @@ -0,0 +1,1190 @@ +#!/usr/bin/env python3 +""" +整合版客服机器人 - AI增强版 +包含: +1. AI对话引导 +2. 镜像搜索功能 +3. 自动翻页缓存 +4. 智能去重 +""" + +import asyncio +import logging +from enhanced_logger import EnhancedLogger +import time +import os +import httpx +import re +import anthropic +import json +import sys +from typing import Dict, Optional +from datetime import datetime, timedelta + +# 添加路径 +sys.path.insert(0, "/home/atai/bot_data") + +# Pyrogram imports +from pyrogram import Client as PyrogramClient, filters +from pyrogram.types import Message as PyrogramMessage +from pyrogram.raw.functions.messages import GetBotCallbackAnswer + +# Telegram Bot imports +from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup +from telegram.ext import Application, CommandHandler, MessageHandler, CallbackQueryHandler, filters as tg_filters +from telegram.ext import ContextTypes + +# 导入数据库 +try: + from database import CacheDatabase +except ImportError: + CacheDatabase = None + logging.warning("database.py未找到,缓存功能将禁用") + +# ================== 配置 ================== +API_ID = 24660516 +API_HASH = "eae564578880a59c9963916ff1bbbd3a" +SESSION_NAME = "user_session" +BOT_TOKEN = "8426529617:AAHAxzohSMFBAxInzbAVJsZfkB5bHnOyFC4" +TARGET_BOT = "@openaiw_bot" +ADMIN_ID = 7363537082 + +# AI服务配置 +MAC_API_URL = "http://192.168.9.10:8000" + +# 搜索命令列表 +SEARCH_COMMANDS = ['/topchat', '/search', '/text', '/human'] + +# 日志配置 +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +# 使用增强型日志系统 +enhanced_log = EnhancedLogger("integrated_bot", log_dir="./logs") +logger = enhanced_log.get_logger() +logger.info("🚀 增强型日志系统已启动 - 所有日志将被完整保留") + +# 初始化Claude客户端 +try: + claude_client = anthropic.Anthropic( + api_key=os.environ.get('ANTHROPIC_AUTH_TOKEN'), + base_url=os.environ.get('ANTHROPIC_BASE_URL', 'https://api.anthropic.com') + ) + logger.info("✅ Claude API客户端已初始化") +except Exception as e: + logger.error(f"❌ Claude API初始化失败: {e}") + claude_client = None + + +def serialize_callback_data(value): + """将按钮callback_data序列化为可JSON存储的结构""" + if value is None: + return None + if isinstance(value, bytes): + return {"type": "bytes", "value": value.hex()} + if isinstance(value, str): + return {"type": "str", "value": value} + return None + + +def deserialize_callback_data(data): + """从缓存中恢复原始callback_data""" + if not data: + return None + if isinstance(data, dict): + data_type = data.get("type") + value = data.get("value") + if data_type == "bytes" and isinstance(value, str): + try: + return bytes.fromhex(value) + except ValueError: + return None + if data_type == "str": + return value + if isinstance(data, str): + if data.startswith('hex:'): + try: + return bytes.fromhex(data[4:]) + except ValueError: + return None + return data + return None + + + + + +# ================== 对话管理 ================== +class ConversationManager: + """管理用户对话上下文""" + + def __init__(self, max_history=5): + self.conversations = {} + self.max_history = max_history + + def add_message(self, user_id: int, role: str, content: str): + """添加消息到历史""" + if user_id not in self.conversations: + self.conversations[user_id] = [] + + self.conversations[user_id].append({ + "role": role, + "content": content, + "timestamp": datetime.now().isoformat() + }) + + # 保持最近的N条消息 + if len(self.conversations[user_id]) > self.max_history * 2: + self.conversations[user_id] = self.conversations[user_id][-self.max_history * 2:] + + def get_history(self, user_id: int, limit: int = 2) -> list: + """获取用户对话历史""" + if user_id not in self.conversations: + return [] + + history = self.conversations[user_id][-limit * 2:] + return [{"role": msg["role"], "content": msg["content"]} for msg in history] + + def clear_history(self, user_id: int): + """清空用户历史""" + if user_id in self.conversations: + del self.conversations[user_id] + + +# ================== 自动翻页管理器 ================== +class AutoPaginationManager: + """后台自动翻页 - 用户无感知""" + + def __init__(self, pyrogram_client, cache_db, target_bot_id, logger): + self.pyrogram_client = pyrogram_client + self.cache_db = cache_db + self.target_bot_id = target_bot_id + self.logger = logger + self.active_tasks = {} + + async def start_pagination(self, user_id, command, keyword, first_message): + """启动后台翻页任务""" + if user_id in self.active_tasks: + return + + task = asyncio.create_task(self._paginate(user_id, command, keyword, first_message)) + self.active_tasks[user_id] = task + self.logger.info(f"[翻页] 后台任务启动: {command} {keyword}") + + async def _paginate(self, user_id, command, keyword, message): + """执行翻页""" + try: + page = 1 + self._save_to_cache(command, keyword, page, message) + + if not self._has_next(message): + self.logger.info(f"[翻页] 只有1页") + return + + current = message + for page in range(2, 11): # 最多10页 + await asyncio.sleep(2) + + next_msg = await self._click_next(current) + if not next_msg: + break + + self._save_to_cache(command, keyword, page, next_msg) + self.logger.info(f"[翻页] 第{page}页已保存") + + if not self._has_next(next_msg): + self.logger.info(f"[翻页] 完成,共{page}页") + break + + current = next_msg + + except Exception as e: + self.logger.error(f"[翻页] 错误: {e}") + finally: + if user_id in self.active_tasks: + del self.active_tasks[user_id] + + def _has_next(self, msg): + """检查是否有下一页""" + if not msg.reply_markup: + return False + for row in msg.reply_markup.inline_keyboard: + for btn in row: + if btn.text and any(x in btn.text for x in ['下一页', 'Next', '▶']): + return True + return False + + async def _click_next(self, msg): + """点击下一页""" + try: + from pyrogram.raw.functions.messages import GetBotCallbackAnswer + + for row in msg.reply_markup.inline_keyboard: + for btn in row: + if btn.text and any(x in btn.text for x in ['下一页', 'Next', '▶']): + await self.pyrogram_client.invoke( + GetBotCallbackAnswer( + peer=await self.pyrogram_client.resolve_peer(self.target_bot_id), + msg_id=msg.id, + data=btn.callback_data + ) + ) + await asyncio.sleep(1.5) + return await self.pyrogram_client.get_messages(self.target_bot_id, msg.id) + except Exception as e: + self.logger.error(f"[翻页] 点击失败: {e}") + return None + + def _save_to_cache(self, cmd, keyword, page, msg): + """保存到缓存""" + if not self.cache_db: + return + try: + text = msg.text or msg.caption or "" + buttons = [] + if getattr(msg, 'reply_markup', None) and getattr(msg.reply_markup, 'inline_keyboard', None): + for row in msg.reply_markup.inline_keyboard: + for btn in row: + btn_data = {'text': btn.text} + if btn.url: + btn_data['url'] = btn.url + if btn.callback_data is not None: + serialized = serialize_callback_data(btn.callback_data) + if serialized: + btn_data['callback_data'] = serialized + buttons.append(btn_data) + self.cache_db.save_cache(cmd, keyword, page, text, buttons) + except Exception as e: + self.logger.error(f"[翻页] 保存失败: {e}") + +class IntegratedBotAI: + """整合的客服机器人 - AI增强版""" + + def __init__(self): + # Bot应用 + self.app = None + + # Pyrogram客户端(用于镜像) + self.pyrogram_client: Optional[PyrogramClient] = None + self.target_bot_id: Optional[int] = None + + # 消息映射 + self.pyrogram_to_telegram = {} + self.telegram_to_pyrogram = {} + self.callback_data_map = {} + self.user_search_sessions = {} + + # AI会话状态 + self.user_ai_sessions = {} + + # 缓存数据库 + self.cache_db = CacheDatabase() if CacheDatabase else None + + # 对话管理器 + self.conversation_manager = ConversationManager() + self.pagination_manager = None + + # 健康监控与告警 + self.health_monitor_task = None + self.remote_error_tracker = {"count": 0, "first_seen": datetime.now()} + self.last_admin_alert = datetime.now() - timedelta(minutes=30) + + async def setup_pyrogram(self): + """设置Pyrogram客户端""" + try: + proxy_config = None + if os.environ.get('ALL_PROXY'): + proxy_url = os.environ.get('ALL_PROXY', '').replace('socks5://', '') + if proxy_url: + host, port = proxy_url.split(':') + proxy_config = {"scheme": "socks5", "hostname": host, "port": int(port)} + + self.pyrogram_client = PyrogramClient( + SESSION_NAME, api_id=API_ID, api_hash=API_HASH, + proxy=proxy_config if proxy_config else None + ) + + await self.pyrogram_client.start() + logger.info("✅ Pyrogram客户端已启动") + + # 初始化自动翻页管理器 + self.pagination_manager = AutoPaginationManager( + self.pyrogram_client, self.cache_db, self.target_bot_id, logger + ) + logger.info("✅ 自动翻页管理器已初始化") + + target = await self.pyrogram_client.get_users(TARGET_BOT) + self.target_bot_id = target.id + logger.info(f"✅ 已连接到搜索机器人: {target.username}") + + @self.pyrogram_client.on_message(filters.user(self.target_bot_id)) + async def on_bot_response(_, message: PyrogramMessage): + await self.handle_search_response(message) + + @self.pyrogram_client.on_edited_message(filters.user(self.target_bot_id)) + async def on_message_edited(_, message: PyrogramMessage): + await self.handle_search_response(message, is_edit=True) + + return True + except Exception as e: + logger.error(f"Pyrogram设置失败: {e}") + return False + + async def call_ai_service(self, user_id: int, message: str, context: dict = None) -> dict: + """优化的Claude API调用 - 带上下文记忆和改进提示词""" + + if not claude_client: + logger.error("Claude客户端未初始化") + return { + "type": "auto", + "response": "👋 我来帮你搜索!\n\n直接发关键词,或试试:\n• /search 群组名\n• /text 讨论内容\n• /topchat 热门分类", + "confidence": 0.3 + } + + try: + logger.info(f"[用户 {user_id}] 调用Claude API: {message}") + + username = context.get('username', f'user_{user_id}') if context else f'user_{user_id}' + first_name = context.get('first_name', '') if context else '' + + # 构建对话历史 + messages = [] + + # 添加历史对话(最近2轮) + history = self.conversation_manager.get_history(user_id, limit=2) + messages.extend(history) + + # 添加当前消息(优化的提示词) + current_prompt = f"""你是@ktfund_bot的AI助手,专业的Telegram群组搜索助手。 + +【重要】你的回复中可以包含可执行的命令,我会为它们生成按钮。 +命令格式:/search 关键词 或 /text 关键词 + +用户信息:@{username} ({first_name}) +用户说:"{message}" + +【可用工具】 +• /search [关键词] - 搜索群组名称 +• /text [关键词] - 搜索讨论内容 +• /human [关键词] - 搜索用户 +• /topchat - 热门分类 + +【回复要求】 +1. 简短友好(2-4行) +2. 给1-2个具体命令建议 +3. 口语化,像朋友聊天 +4. 命令要在独立的一行 + +【示例】 +用户:"找AI群" +回复: +找AI群的话,试试: +/search AI +/text ChatGPT + +直接回复:""" + + messages.append({ + "role": "user", + "content": current_prompt + }) + + # 调用Claude API + response = claude_client.messages.create( + model="claude-sonnet-4-20250514", + max_tokens=512, + temperature=0.7, + messages=messages + ) + + claude_response = response.content[0].text.strip() + + # 保存对话历史 + self.conversation_manager.add_message(user_id, "user", message) + self.conversation_manager.add_message(user_id, "assistant", claude_response) + + logger.info(f"[用户 {user_id}] ✅ Claude回复成功 ({len(claude_response)}字)") + + # 智能提取命令建议 + suggested_commands = self._extract_commands(claude_response) + + return { + "type": "ai", + "response": claude_response, + "confidence": 1.0, + "suggested_commands": suggested_commands + } + + except Exception as e: + logger.error(f"[用户 {user_id}] ❌ Claude API失败: {e}") + return { + "type": "auto", + "response": "👋 我来帮你搜索!\n\n直接发关键词,或试试:\n• /search 群组名\n• /text 讨论内容\n• /topchat 热门分类", + "confidence": 0.3 + } + + def _extract_commands(self, response_text: str) -> list: + """从回复中提取建议的命令""" + import re + commands = [] + + # 匹配 /command pattern + patterns = [ + r'/search\s+[\w\s]+', + r'/text\s+[\w\s]+', + r'/human\s+[\w\s]+', + r'/topchat' + ] + + for pattern in patterns: + matches = re.findall(pattern, response_text) + commands.extend([m.strip() for m in matches[:1]]) + + return commands[:2] + + + + def _extract_command_buttons(self, text: str) -> list: + """从AI回复中提取命令按钮""" + import re + buttons = [] + + # 匹配:/command keyword + pattern = r'/(search|text|human|topchat)\s*([^\n]*)' + matches = re.findall(pattern, text, re.IGNORECASE) + + for cmd, keywords in matches[:3]: + cmd = cmd.lower() + keywords = keywords.strip()[:30] # 限制长度 + + if keywords: + display = f"/{cmd} {keywords}" + callback = f"cmd_{cmd}_{keywords.replace(' ', '_')}"[:64] + else: + display = f"/{cmd}" + callback = f"cmd_{cmd}" + + buttons.append((display, callback)) + + return buttons + + async def handle_start(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """处理/start命令 - AI引导模式""" + user = update.effective_user + user_id = user.id + + self.user_ai_sessions[user_id] = {"started_at": datetime.now(), "conversation": []} + + welcome_text = ( + f"👋 您好 {user.first_name}!\n\n" + "我是智能搜索助手,可以帮您找到Telegram上的群组和频道。\n\n" + "🔍 我能做什么:\n" + "• 搜索群组/频道\n" + "• 搜索特定话题的讨论\n" + "• 查找用户\n" + "• 浏览热门分类\n\n" + "💬 直接告诉我您想找什么,我会帮您选择最合适的搜索方式!" + ) + + keyboard = [ + [InlineKeyboardButton("🔍 搜索群组", callback_data="quick_search"), + InlineKeyboardButton("📚 使用指南", callback_data="quick_help")], + [InlineKeyboardButton("🔥 热门分类", callback_data="quick_topchat")] + ] + + await update.message.reply_text(welcome_text, reply_markup=InlineKeyboardMarkup(keyboard)) + + # 通知管理员 + admin_notification = ( + f"🆕 新用户访问 (AI模式):\n" + f"👤 {user.first_name} {user.last_name or ''}\n" + f"🆔 {user.id}\n" + f"👤 @{user.username or '无'}\n" + f"⏰ {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" + ) + await context.bot.send_message(chat_id=ADMIN_ID, text=admin_notification) + + async def handle_message(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """处理所有消息 - AI智能路由""" + if not update.message or not update.message.text: + return + + user = update.effective_user + user_id = user.id + text = update.message.text + is_admin = user_id == ADMIN_ID + + if is_admin and update.message.reply_to_message: + await self.handle_admin_reply(update, context) + return + + if self.is_search_command(text): + await self.handle_search_command(update, context) + return + + await self.handle_ai_conversation(update, context) + + def _prepare_keyword_for_buttons(self, keyword: str) -> Optional[tuple[str, str]]: + """根据用户输入生成展示关键词和callback参数""" + if not keyword: + return None + cleaned = re.sub(r'\s+', ' ', keyword.strip()) + if not cleaned: + return None + display = cleaned[:30] + callback_arg = display.replace(' ', '_') + return display, callback_arg + + + def is_search_command(self, text: str) -> bool: + """检查是否是搜索命令""" + return text and text.split()[0] in SEARCH_COMMANDS + + async def handle_ai_conversation(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """AI对话处理 - 带智能按钮""" + user = update.effective_user + user_id = user.id + message = update.message.text + + # 显示"正在输入" + await update.message.chat.send_action("typing") + + # 构建上下文 + user_context = { + "username": user.username or f"user{user_id}", + "first_name": user.first_name or "朋友", + "last_name": user.last_name + } + + # 调用AI + ai_response = await self.call_ai_service(user_id, message, user_context) + response_text = ai_response.get("response", "") + + # 提取命令按钮 + buttons = self._extract_command_buttons(response_text) + button_callbacks = {cb for _, cb in buttons} + + # 默认提供基于原始输入的命令按钮,确保用户可一键选择 + prepared = self._prepare_keyword_for_buttons(message) + if prepared: + display_kw, callback_kw = prepared + base_commands = [ + (f"/search {display_kw}", f"cmd_search_{callback_kw}"[:64]), + (f"/text {display_kw}", f"cmd_text_{callback_kw}"[:64]), + (f"/human {display_kw}", f"cmd_human_{callback_kw}"[:64]) + ] + default_buttons = [] + for display, callback in base_commands: + if callback not in button_callbacks: + default_buttons.append((display, callback)) + button_callbacks.add(callback) + if default_buttons: + buttons = default_buttons + buttons + + try: + if buttons: + # 构建按钮键盘 + keyboard = [] + for display, callback in buttons: + keyboard.append([InlineKeyboardButton( + f"🔍 {display}", + callback_data=callback + )]) + + # 添加常用按钮 + keyboard.append([ + InlineKeyboardButton("🔥 热门目录", callback_data="cmd_topchat"), + InlineKeyboardButton("📖 帮助", callback_data="cmd_help") + ]) + + await update.message.reply_text( + response_text, + reply_markup=InlineKeyboardMarkup(keyboard) + ) + logger.info(f"[AI对话] 已回复用户 {user_id} (带{len(buttons)}个按钮)") + else: + # 无按钮版本 + await update.message.reply_text(response_text) + logger.info(f"[AI对话] 已回复用户 {user_id}") + + except Exception as e: + logger.error(f"[AI对话] 发送失败: {e}, 降级为纯文本") + try: + await update.message.reply_text(response_text) + except: + await update.message.reply_text("抱歉,回复失败。请直接发送命令,如:/search AI") + + + + async def handle_search_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """处理搜索命令 - 带缓存""" + user = update.effective_user + user_id = user.id + command = update.message.text + + # 提取命令和关键词 + parts = command.split(maxsplit=1) + cmd = parts[0] + keyword = parts[1] if len(parts) > 1 else "" + + # 检查缓存 + if self.cache_db and keyword: + cached = self.cache_db.get_cache(cmd, keyword, 1) + if cached: + logger.info(f"[缓存命中] {cmd} {keyword} page1") + + # 恢复按钮 + keyboard = None + if cached.get('buttons'): + buttons = [] + for btn_data in cached['buttons']: + if btn_data.get('url'): + buttons.append([InlineKeyboardButton(text=btn_data['text'], url=btn_data['url'])]) + elif btn_data.get('callback_data'): + original_callback = deserialize_callback_data(btn_data.get('callback_data')) + if original_callback is not None: + callback_id = f"cb_{time.time():.0f}_{len(self.callback_data_map)}" + # 需要存储原始message_id,这里用0作为占位符,实际翻页时从缓存获取 + self.callback_data_map[callback_id] = (0, original_callback) + buttons.append([InlineKeyboardButton(text=btn_data['text'], callback_data=callback_id[:64])]) + + if buttons: + keyboard = InlineKeyboardMarkup(buttons) + + # 发送缓存结果(带按钮) + sent = await update.message.reply_text( + cached['text'][:4000], + reply_markup=keyboard, + parse_mode='HTML' + ) + + # 记录会话,以便翻页时使用 + self.user_search_sessions[user_id] = { + 'chat_id': update.effective_chat.id, + 'wait_msg_id': sent.message_id, + 'command': cmd, + 'keyword': keyword, + 'timestamp': datetime.now() + } + + return + + # 通知管理员 + admin_notification = ( + f"🔍 用户执行搜索:\n" + f"👤 {user.first_name} {user.last_name or ''}\n" + f"🆔 {user_id}\n" + f"📝 {command}\n" + f"⏰ {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" + ) + await context.bot.send_message(chat_id=ADMIN_ID, text=admin_notification) + + wait_msg = await update.message.reply_text("🔍 正在搜索,请稍候...") + + self.user_search_sessions[user_id] = { + 'chat_id': update.effective_chat.id, + 'wait_msg_id': wait_msg.message_id, + 'command': cmd, + 'keyword': keyword, + 'timestamp': datetime.now() + } + + await self.pyrogram_client.send_message(self.target_bot_id, command) + logger.info(f"搜索: {command}") + + async def handle_search_response(self, message: PyrogramMessage, is_edit: bool = False): + """处理搜索机器人的响应 - 保存到缓存""" + try: + if not self.user_search_sessions: + return + + user_id = max(self.user_search_sessions.keys(), key=lambda k: self.user_search_sessions[k]['timestamp']) + session = self.user_search_sessions[user_id] + + text = message.text or message.caption or "无结果" + + try: + if message.text and hasattr(message.text, 'html'): + text = message.text.html + except: + pass + + keyboard = self.convert_keyboard(message) + + if is_edit and message.id in self.pyrogram_to_telegram: + telegram_msg_id = self.pyrogram_to_telegram[message.id] + await self.app.bot.edit_message_text( + chat_id=session['chat_id'], + message_id=telegram_msg_id, + text=text[:4000], + reply_markup=keyboard, + parse_mode='HTML' + ) + else: + try: + await self.app.bot.delete_message( + chat_id=session['chat_id'], + message_id=session['wait_msg_id'] + ) + except: + pass + + sent = await self.app.bot.send_message( + chat_id=session['chat_id'], + text=text[:4000], + reply_markup=keyboard, + parse_mode='HTML' + ) + + self.pyrogram_to_telegram[message.id] = sent.message_id + self.telegram_to_pyrogram[sent.message_id] = message.id + + # 保存到缓存 + if self.cache_db and session.get('keyword'): + buttons = self.extract_buttons(message) + self.cache_db.save_cache( + session['command'], + session['keyword'], + 1, # 第一页 + text, + buttons + ) + + # 后台自动翻页(用户无感知) + if self.pagination_manager: + asyncio.create_task( + self.pagination_manager.start_pagination( + user_id, session['command'], session['keyword'], message + ) + ) + + except Exception as e: + logger.error(f"处理搜索响应失败: {e}") + + def convert_keyboard(self, message: PyrogramMessage) -> Optional[InlineKeyboardMarkup]: + """转换键盘""" + if not message.reply_markup or not message.reply_markup.inline_keyboard: + return None + + try: + buttons = [] + for row in message.reply_markup.inline_keyboard: + button_row = [] + for btn in row: + if btn.url: + button_row.append(InlineKeyboardButton(text=btn.text, url=btn.url)) + elif btn.callback_data: + callback_id = f"cb_{time.time():.0f}_{len(self.callback_data_map)}" + self.callback_data_map[callback_id] = (message.id, btn.callback_data) + button_row.append(InlineKeyboardButton(text=btn.text, callback_data=callback_id[:64])) + + if button_row: + buttons.append(button_row) + + return InlineKeyboardMarkup(buttons) if buttons else None + except Exception as e: + logger.error(f"键盘转换失败: {e}") + return None + + def extract_buttons(self, message: PyrogramMessage) -> list: + """提取按钮数据(包含callback_data用于缓存)""" + if not message.reply_markup or not message.reply_markup.inline_keyboard: + return [] + + buttons = [] + for row in message.reply_markup.inline_keyboard: + for btn in row: + btn_data = {"text": btn.text} + if btn.url: + btn_data["url"] = btn.url + if btn.callback_data is not None: + serialized = serialize_callback_data(btn.callback_data) + if serialized: + btn_data["callback_data"] = serialized + buttons.append(btn_data) + return buttons + + async def handle_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """处理按钮点击 - 执行搜索命令或翻页""" + query = update.callback_query + data = query.data + user = query.from_user + + logger.info(f"[回调] 收到callback: user={user.id}, data={data}") + + await query.answer() + + if data.startswith("cb_"): + # 处理翻页按钮 + if data in self.callback_data_map: + orig_msg_id, orig_callback = self.callback_data_map[data] + logger.info(f"[翻页] 用户 {user.id} 点击: {orig_callback}") + + if isinstance(orig_callback, bytes): + orig_callback_bytes = orig_callback + try: + orig_callback_text = orig_callback.decode() + except UnicodeDecodeError: + orig_callback_text = None + else: + orig_callback_text = str(orig_callback) if orig_callback is not None else None + orig_callback_bytes = orig_callback_text.encode() if orig_callback_text is not None else None + + session = self.user_search_sessions.get(user.id) + + # 解析callback_data获取页码(格式如:page_2) + try: + if orig_callback_text and orig_callback_text.startswith("page_"): + page = int(orig_callback_text.split("_")[1]) + + # 从会话获取搜索信息 + if session and 'command' in session and 'keyword' in session: + cmd = session['command'] + keyword = session['keyword'] + + # 先检查缓存 + cached = self.cache_db.get_cache(cmd, keyword, page) if self.cache_db else None + if cached: + logger.info(f"[翻页缓存] 命中: {cmd} {keyword} page{page}") + + # 从缓存恢复按钮 + keyboard = None + if cached.get('buttons'): + buttons = [] + for btn_data in cached['buttons']: + if btn_data.get('url'): + buttons.append([InlineKeyboardButton(text=btn_data['text'], url=btn_data['url'])]) + elif btn_data.get('callback_data'): + restored = deserialize_callback_data(btn_data.get('callback_data')) + if restored is not None: + # 重新生成callback_id + callback_id = f"cb_{time.time():.0f}_{len(self.callback_data_map)}" + self.callback_data_map[callback_id] = (orig_msg_id, restored) + buttons.append([InlineKeyboardButton(text=btn_data['text'], callback_data=callback_id[:64])]) + + if buttons: + keyboard = InlineKeyboardMarkup(buttons) + + # 发送缓存结果 + await query.message.edit_text( + text=cached['text'], + reply_markup=keyboard, + parse_mode='HTML' + ) + return + + else: + logger.info(f"[翻页] 缓存未命中,转发到搜索bot") + + # 如果缓存未命中或不是page_格式,转发到搜索bot + if orig_callback_bytes is None: + raise ValueError("callback_data 无法编码") + + await self.pyrogram_client.request_callback_answer( + chat_id=self.target_bot_id, + message_id=orig_msg_id, + callback_data=orig_callback_bytes + ) + + # 记录等待响应 + self.user_search_sessions[user.id] = { + 'chat_id': query.message.chat_id, + 'wait_msg_id': query.message.message_id, + 'command': session.get('command') if session else None, + 'keyword': session.get('keyword') if session else None, + 'timestamp': datetime.now() + } + + logger.info(f"[翻页] 已转发callback到搜索bot") + + except Exception as e: + logger.error(f"[翻页] 处理失败: {e}") + await query.message.reply_text("❌ 翻页失败,请稍后重试") + else: + logger.warning(f"[翻页] callback_id不存在: {data}") + await query.message.reply_text("❌ 按钮已过期,请重新搜索") + + elif data.startswith("cmd_"): + # 解析命令 + parts = data.replace("cmd_", "").split("_", 1) + cmd = parts[0] + keywords = parts[1].replace("_", " ") if len(parts) > 1 else "" + + # 构造完整命令 + command = f"/{cmd} {keywords}" if keywords else f"/{cmd}" + + logger.info(f"[用户 {user.id}] 点击按钮: {command}") + + # 显示执行提示 + await query.message.reply_text(f"🔍 正在执行:{command}\n请稍候...") + + # 转发到搜索bot + try: + await self.pyrogram_client.send_message(self.target_bot_id, command) + + # 记录搜索会话 + self.user_search_sessions[user.id] = { + 'chat_id': query.message.chat_id, + 'wait_msg_id': query.message.message_id + 1, + 'command': f"/{cmd}", + 'keyword': keywords, + 'timestamp': datetime.now() + } + + logger.info(f"[镜像] 已转发: {command}") + + except Exception as e: + logger.error(f"[镜像] 转发失败: {e}") + await query.message.reply_text("❌ 搜索失败,请稍后重试或直接发送命令") + + elif data == "quick_search": + # 搜索群组引导 + keyboard = [ + [InlineKeyboardButton("🔍 搜索群组", callback_data="cmd_search")], + [InlineKeyboardButton("💬 搜索消息内容", callback_data="cmd_text")], + [InlineKeyboardButton("👤 搜索用户", callback_data="cmd_human")] + ] + await query.message.edit_text( + "请选择搜索类型,或直接发送关键词:", + reply_markup=InlineKeyboardMarkup(keyboard) + ) + + elif data == "quick_help": + await query.message.edit_text( + "📖 使用指南:\n\n" + "🔍 搜索方式:\n" + "• /search [关键词] - 按群组名称搜索\n" + "• /text [关键词] - 按消息内容搜索\n" + "• /human [关键词] - 按用户名搜索\n" + "• /topchat - 浏览热门群组目录\n\n" + "💡 快捷使用:\n" + "直接发送关键词,我会智能分析并选择最合适的搜索方式!\n\n" + "📋 示例:\n" + "• 发送 '区块链' → 自动搜索相关群组\n" + "• 发送 'NFT交易' → 智能搜索讨论内容\n\n" + "❓ 有任何问题都可以直接问我!" + ) + + elif data == "quick_topchat": + # 直接触发topchat命令 + logger.info(f"[用户 {user.id}] 点击热门分类按钮") + await query.message.edit_text("🔥 正在加载热门分类...\n请稍候...") + + try: + await self.pyrogram_client.send_message(self.target_bot_id, "/topchat") + self.user_search_sessions[user.id] = { + 'chat_id': query.message.chat_id, + 'wait_msg_id': query.message.message_id, + 'command': '/topchat', + 'keyword': '', + 'timestamp': datetime.now() + } + logger.info(f"[镜像] 已转发: /topchat") + except Exception as e: + logger.error(f"[镜像] 转发失败: {e}") + await query.message.edit_text("❌ 加载失败,请稍后重试") + + elif data == "cmd_help": + await query.message.reply_text( + "📖 使用指南:\n\n" + "• /search [关键词] - 按群组名称搜索\n" + "• /text [关键词] - 按消息内容搜索\n" + "• /human [关键词] - 按用户名搜索\n" + "• /topchat - 热门群组目录\n\n" + "💡 或者直接告诉我你想找什么!" + ) + + else: + logger.warning(f"未知callback: {data}") + + + async def handle_admin_reply(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """处理管理员回复""" + reply_to = update.message.reply_to_message + if not reply_to or not reply_to.text: + return + + import re + user_id = None + for line in reply_to.text.split('\n'): + if '🆔' in line or 'ID:' in line: + numbers = re.findall(r'\d+', line) + if numbers: + user_id = int(numbers[0]) + break + + if not user_id: + await update.message.reply_text("❌ 无法识别用户ID") + return + + try: + await context.bot.send_message(chat_id=user_id, text=update.message.text) + await update.message.reply_text(f"✅ 已回复给用户 {user_id}") + except Exception as e: + await update.message.reply_text(f"❌ 回复失败: {str(e)}") + + def _reset_remote_error_counter(self): + """重置网络异常计数""" + self.remote_error_tracker["count"] = 0 + self.remote_error_tracker["first_seen"] = datetime.now() + + async def _record_remote_error(self, source: str, exception: Exception): + """记录网络异常并必要时提醒管理员""" + now = datetime.now() + tracker = self.remote_error_tracker + if now - tracker["first_seen"] > timedelta(minutes=5): + tracker["first_seen"] = now + tracker["count"] = 0 + tracker["count"] += 1 + logger.warning(f"[网络波动] {source} 异常: {exception}") + + if tracker["count"] >= 3 and (now - self.last_admin_alert) > timedelta(minutes=10): + self.last_admin_alert = now + if self.app: + try: + await self.app.bot.send_message( + chat_id=ADMIN_ID, + text=( + "⚠️ 网络波动提醒\n" + f"5分钟内出现 {tracker['count']} 次连接异常。\n" + "机器人已尝试自动恢复,请关注代理或网络状况。" + ) + ) + except Exception as notify_error: + logger.error(f"管理员通知失败: {notify_error}") + + async def handle_error(self, update: object, context: ContextTypes.DEFAULT_TYPE): + """统一错误处理,捕获网络异常并记录""" + error = getattr(context, 'error', None) + if not error: + return + logger.error(f"处理更新时发生异常: {error!r}") + + if isinstance(error, httpx.RemoteProtocolError): + await self._record_remote_error("Telegram轮询", error) + + async def monitor_health(self): + """周期性健康检查,确保Telegram与Pyrogram连接可用""" + try: + while True: + await asyncio.sleep(120) + if not self.app: + continue + + # 检查 Telegram Bot 接口 + try: + await self.app.bot.get_me() + self._reset_remote_error_counter() + except httpx.HTTPError as e: + await self._record_remote_error("Telegram接口", e) + except Exception as e: + logger.warning(f"Telegram 健康检查异常: {e}") + + # 检查 Pyrogram 连接 + if self.pyrogram_client and not getattr(self.pyrogram_client, "is_connected", False): + logger.warning("Pyrogram 客户端掉线,尝试重连...") + try: + await self.pyrogram_client.start() + logger.info("Pyrogram 客户端重连成功") + except Exception as reconnect_error: + await self._record_remote_error("Pyrogram重连", reconnect_error) + except asyncio.CancelledError: + logger.info("健康监控任务已停止") + raise + async def initialize(self): + """初始化机器人""" + try: + logger.info("正在初始化整合机器人...") + + if not await self.setup_pyrogram(): + logger.error("Pyrogram初始化失败") + return False + + builder = ( + Application.builder() + .token(BOT_TOKEN) + .connect_timeout(10) + .read_timeout(60) + .write_timeout(60) + .pool_timeout(10) + ) + + if os.environ.get('HTTP_PROXY'): + proxy_url = os.environ.get('HTTP_PROXY') + logger.info(f"配置Telegram Bot代理: {proxy_url}") + request = httpx.AsyncClient(proxies={"http://": proxy_url, "https://": proxy_url}, timeout=30.0) + builder = builder.request(request) + + self.app = builder.build() + self.app.add_error_handler(self.handle_error) + + self.app.add_handler(CommandHandler("start", self.handle_start)) + self.app.add_handler(CallbackQueryHandler(self.handle_callback)) + self.app.add_handler(MessageHandler(tg_filters.ALL, self.handle_message)) + + logger.info("✅ 整合机器人初始化完成") + return True + + except Exception as e: + logger.error(f"初始化失败: {e}") + return False + + async def run(self): + """运行机器人""" + try: + await self.app.initialize() + await self.app.start() + await self.app.updater.start_polling(drop_pending_updates=True) + + if not self.health_monitor_task or self.health_monitor_task.done(): + self.health_monitor_task = asyncio.create_task(self.monitor_health()) + + logger.info("="*50) + logger.info("✅ AI增强版Bot已启动") + logger.info(f"AI服务: {MAC_API_URL}") + logger.info(f"缓存功能: {'启用' if self.cache_db else '禁用'}") + logger.info("="*50) + + await asyncio.Event().wait() + + except KeyboardInterrupt: + logger.info("收到停止信号") + finally: + await self.cleanup() + + async def cleanup(self): + """清理资源""" + logger.info("正在清理...") + + if self.health_monitor_task: + self.health_monitor_task.cancel() + try: + await self.health_monitor_task + except asyncio.CancelledError: + pass + self.health_monitor_task = None + + if self.app: + await self.app.updater.stop() + await self.app.stop() + await self.app.shutdown() + + if self.pyrogram_client: + await self.pyrogram_client.stop() + + logger.info("✅ 清理完成") + + +async def main(): + """主函数""" + bot = IntegratedBotAI() + + if await bot.initialize(): + await bot.run() + else: + logger.error("初始化失败,退出") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/integrated_bot_ai.py.bak b/integrated_bot_ai.py.bak new file mode 100755 index 0000000..efdd977 --- /dev/null +++ b/integrated_bot_ai.py.bak @@ -0,0 +1,845 @@ +#!/usr/bin/env python3 +""" +整合版客服机器人 - AI增强版 +包含: +1. AI对话引导 +2. 镜像搜索功能 +3. 自动翻页缓存 +4. 智能去重 +""" + +import asyncio +import logging +import time +import os +import httpx +import anthropic +import json +import sys +from typing import Dict, Optional +from datetime import datetime + +# 添加路径 +sys.path.insert(0, "/home/atai/bot_data") + +# Pyrogram imports +from pyrogram import Client as PyrogramClient, filters +from pyrogram.types import Message as PyrogramMessage +from pyrogram.raw.functions.messages import GetBotCallbackAnswer + +# Telegram Bot imports +from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup +from telegram.ext import Application, CommandHandler, MessageHandler, CallbackQueryHandler, filters as tg_filters +from telegram.ext import ContextTypes + +# 导入数据库 +try: + from database import CacheDatabase +except ImportError: + CacheDatabase = None + logging.warning("database.py未找到,缓存功能将禁用") + +# ================== 配置 ================== +API_ID = 24660516 +API_HASH = "eae564578880a59c9963916ff1bbbd3a" +SESSION_NAME = "user_session" +BOT_TOKEN = "8426529617:AAHAxzohSMFBAxInzbAVJsZfkB5bHnOyFC4" +TARGET_BOT = "@openaiw_bot" +ADMIN_ID = 7363537082 + +# AI服务配置 +MAC_API_URL = "http://192.168.9.10:8000" + +# 搜索命令列表 +SEARCH_COMMANDS = ['/topchat', '/search', '/text', '/human'] + +# 日志配置 +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +# 初始化Claude客户端 +try: + claude_client = anthropic.Anthropic( + api_key=os.environ.get('ANTHROPIC_AUTH_TOKEN'), + base_url=os.environ.get('ANTHROPIC_BASE_URL', 'https://api.anthropic.com') + ) + logger.info("✅ Claude API客户端已初始化") +except Exception as e: + logger.error(f"❌ Claude API初始化失败: {e}") + claude_client = None + + + + +# ================== 对话管理 ================== +class ConversationManager: + """管理用户对话上下文""" + + def __init__(self, max_history=5): + self.conversations = {} + self.max_history = max_history + + def add_message(self, user_id: int, role: str, content: str): + """添加消息到历史""" + if user_id not in self.conversations: + self.conversations[user_id] = [] + + self.conversations[user_id].append({ + "role": role, + "content": content, + "timestamp": datetime.now().isoformat() + }) + + # 保持最近的N条消息 + if len(self.conversations[user_id]) > self.max_history * 2: + self.conversations[user_id] = self.conversations[user_id][-self.max_history * 2:] + + def get_history(self, user_id: int, limit: int = 2) -> list: + """获取用户对话历史""" + if user_id not in self.conversations: + return [] + + history = self.conversations[user_id][-limit * 2:] + return [{"role": msg["role"], "content": msg["content"]} for msg in history] + + def clear_history(self, user_id: int): + """清空用户历史""" + if user_id in self.conversations: + del self.conversations[user_id] + + +# ================== 自动翻页管理器 ================== +class AutoPaginationManager: + """后台自动翻页 - 用户无感知""" + + def __init__(self, pyrogram_client, cache_db, target_bot_id, logger): + self.pyrogram_client = pyrogram_client + self.cache_db = cache_db + self.target_bot_id = target_bot_id + self.logger = logger + self.active_tasks = {} + + async def start_pagination(self, user_id, command, keyword, first_message): + """启动后台翻页任务""" + if user_id in self.active_tasks: + return + + task = asyncio.create_task(self._paginate(user_id, command, keyword, first_message)) + self.active_tasks[user_id] = task + self.logger.info(f"[翻页] 后台任务启动: {command} {keyword}") + + async def _paginate(self, user_id, command, keyword, message): + """执行翻页""" + try: + page = 1 + self._save_to_cache(command, keyword, page, message) + + if not self._has_next(message): + self.logger.info(f"[翻页] 只有1页") + return + + current = message + for page in range(2, 11): # 最多10页 + await asyncio.sleep(2) + + next_msg = await self._click_next(current) + if not next_msg: + break + + self._save_to_cache(command, keyword, page, next_msg) + self.logger.info(f"[翻页] 第{page}页已保存") + + if not self._has_next(next_msg): + self.logger.info(f"[翻页] 完成,共{page}页") + break + + current = next_msg + + except Exception as e: + self.logger.error(f"[翻页] 错误: {e}") + finally: + if user_id in self.active_tasks: + del self.active_tasks[user_id] + + def _has_next(self, msg): + """检查是否有下一页""" + if not msg.reply_markup: + return False + for row in msg.reply_markup.inline_keyboard: + for btn in row: + if btn.text and any(x in btn.text for x in ['下一页', 'Next', '▶']): + return True + return False + + async def _click_next(self, msg): + """点击下一页""" + try: + from pyrogram.raw.functions.messages import GetBotCallbackAnswer + + for row in msg.reply_markup.inline_keyboard: + for btn in row: + if btn.text and any(x in btn.text for x in ['下一页', 'Next', '▶']): + await self.pyrogram_client.invoke( + GetBotCallbackAnswer( + peer=await self.pyrogram_client.resolve_peer(self.target_bot_id), + msg_id=msg.id, + data=btn.callback_data + ) + ) + await asyncio.sleep(1.5) + return await self.pyrogram_client.get_messages(self.target_bot_id, msg.id) + except Exception as e: + self.logger.error(f"[翻页] 点击失败: {e}") + return None + + def _save_to_cache(self, cmd, keyword, page, msg): + """保存到缓存""" + if not self.cache_db: + return + try: + text = msg.text or msg.caption or "" + buttons = [] + if msg.reply_markup: + for row in msg.reply_markup.inline_keyboard: + for btn in row: + buttons.append({"text": btn.text, "url": btn.url if btn.url else None}) + self.cache_db.save_cache(cmd, keyword, page, text, buttons) + except Exception as e: + self.logger.error(f"[翻页] 保存失败: {e}") + + +class IntegratedBotAI: + """整合的客服机器人 - AI增强版""" + + def __init__(self): + # Bot应用 + self.app = None + + # Pyrogram客户端(用于镜像) + self.pyrogram_client: Optional[PyrogramClient] = None + self.target_bot_id: Optional[int] = None + + # 消息映射 + self.pyrogram_to_telegram = {} + self.telegram_to_pyrogram = {} + self.callback_data_map = {} + self.user_search_sessions = {} + + # AI会话状态 + self.user_ai_sessions = {} + + # 缓存数据库 + self.cache_db = CacheDatabase() if CacheDatabase else None + + # 对话管理器 + self.conversation_manager = ConversationManager() + self.pagination_manager = None + + async def setup_pyrogram(self): + """设置Pyrogram客户端""" + try: + proxy_config = None + if os.environ.get('ALL_PROXY'): + proxy_url = os.environ.get('ALL_PROXY', '').replace('socks5://', '') + if proxy_url: + host, port = proxy_url.split(':') + proxy_config = {"scheme": "socks5", "hostname": host, "port": int(port)} + + self.pyrogram_client = PyrogramClient( + SESSION_NAME, api_id=API_ID, api_hash=API_HASH, + proxy=proxy_config if proxy_config else None + ) + + await self.pyrogram_client.start() + logger.info("✅ Pyrogram客户端已启动") + + # 初始化自动翻页管理器 + self.pagination_manager = AutoPaginationManager( + self.pyrogram_client, self.cache_db, self.target_bot_id, logger + ) + logger.info("✅ 自动翻页管理器已初始化") + + target = await self.pyrogram_client.get_users(TARGET_BOT) + self.target_bot_id = target.id + logger.info(f"✅ 已连接到搜索机器人: {target.username}") + + @self.pyrogram_client.on_message(filters.user(self.target_bot_id)) + async def on_bot_response(_, message: PyrogramMessage): + await self.handle_search_response(message) + + @self.pyrogram_client.on_edited_message(filters.user(self.target_bot_id)) + async def on_message_edited(_, message: PyrogramMessage): + await self.handle_search_response(message, is_edit=True) + + return True + except Exception as e: + logger.error(f"Pyrogram设置失败: {e}") + return False + + async def call_ai_service(self, user_id: int, message: str, context: dict = None) -> dict: + """优化的Claude API调用 - 带上下文记忆和改进提示词""" + + if not claude_client: + logger.error("Claude客户端未初始化") + return { + "type": "auto", + "response": "👋 我来帮你搜索!\n\n直接发关键词,或试试:\n• /search 群组名\n• /text 讨论内容\n• /topchat 热门分类", + "confidence": 0.3 + } + + try: + logger.info(f"[用户 {user_id}] 调用Claude API: {message}") + + username = context.get('username', f'user_{user_id}') if context else f'user_{user_id}' + first_name = context.get('first_name', '') if context else '' + + # 构建对话历史 + messages = [] + + # 添加历史对话(最近2轮) + history = self.conversation_manager.get_history(user_id, limit=2) + messages.extend(history) + + # 添加当前消息(优化的提示词) + current_prompt = f"""你是@ktfund_bot的AI助手,专业的Telegram群组搜索助手。 + +【重要】你的回复中可以包含可执行的命令,我会为它们生成按钮。 +命令格式:/search 关键词 或 /text 关键词 + +用户信息:@{username} ({first_name}) +用户说:"{message}" + +【可用工具】 +• /search [关键词] - 搜索群组名称 +• /text [关键词] - 搜索讨论内容 +• /human [关键词] - 搜索用户 +• /topchat - 热门分类 + +【回复要求】 +1. 简短友好(2-4行) +2. 给1-2个具体命令建议 +3. 口语化,像朋友聊天 +4. 命令要在独立的一行 + +【示例】 +用户:"找AI群" +回复: +找AI群的话,试试: +/search AI +/text ChatGPT + +直接回复:""" + + messages.append({ + "role": "user", + "content": current_prompt + }) + + # 调用Claude API + response = claude_client.messages.create( + model="claude-sonnet-4-20250514", + max_tokens=512, + temperature=0.7, + messages=messages + ) + + claude_response = response.content[0].text.strip() + + # 保存对话历史 + self.conversation_manager.add_message(user_id, "user", message) + self.conversation_manager.add_message(user_id, "assistant", claude_response) + + logger.info(f"[用户 {user_id}] ✅ Claude回复成功 ({len(claude_response)}字)") + + # 智能提取命令建议 + suggested_commands = self._extract_commands(claude_response) + + return { + "type": "ai", + "response": claude_response, + "confidence": 1.0, + "suggested_commands": suggested_commands + } + + except Exception as e: + logger.error(f"[用户 {user_id}] ❌ Claude API失败: {e}") + return { + "type": "auto", + "response": "👋 我来帮你搜索!\n\n直接发关键词,或试试:\n• /search 群组名\n• /text 讨论内容\n• /topchat 热门分类", + "confidence": 0.3 + } + + def _extract_commands(self, response_text: str) -> list: + """从回复中提取建议的命令""" + import re + commands = [] + + # 匹配 /command pattern + patterns = [ + r'/search\s+[\w\s]+', + r'/text\s+[\w\s]+', + r'/human\s+[\w\s]+', + r'/topchat' + ] + + for pattern in patterns: + matches = re.findall(pattern, response_text) + commands.extend([m.strip() for m in matches[:1]]) + + return commands[:2] + + + + def _extract_command_buttons(self, text: str) -> list: + """从AI回复中提取命令按钮""" + import re + buttons = [] + + # 匹配:/command keyword + pattern = r'/(search|text|human|topchat)\s*([^\n]*)' + matches = re.findall(pattern, text, re.IGNORECASE) + + for cmd, keywords in matches[:3]: + cmd = cmd.lower() + keywords = keywords.strip()[:30] # 限制长度 + + if keywords: + display = f"/{cmd} {keywords}" + callback = f"cmd_{cmd}_{keywords.replace(' ', '_')}"[:64] + else: + display = f"/{cmd}" + callback = f"cmd_{cmd}" + + buttons.append((display, callback)) + + return buttons + + async def handle_start(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """处理/start命令 - AI引导模式""" + user = update.effective_user + user_id = user.id + + self.user_ai_sessions[user_id] = {"started_at": datetime.now(), "conversation": []} + + welcome_text = ( + f"👋 您好 {user.first_name}!\n\n" + "我是智能搜索助手,可以帮您找到Telegram上的群组和频道。\n\n" + "🔍 我能做什么:\n" + "• 搜索群组/频道\n" + "• 搜索特定话题的讨论\n" + "• 查找用户\n" + "• 浏览热门分类\n\n" + "💬 直接告诉我您想找什么,我会帮您选择最合适的搜索方式!" + ) + + keyboard = [ + [InlineKeyboardButton("🔍 搜索群组", callback_data="quick_search"), + InlineKeyboardButton("📚 使用指南", callback_data="quick_help")], + [InlineKeyboardButton("🔥 热门分类", callback_data="quick_topchat")] + ] + + await update.message.reply_text(welcome_text, reply_markup=InlineKeyboardMarkup(keyboard)) + + # 通知管理员 + admin_notification = ( + f"🆕 新用户访问 (AI模式):\n" + f"👤 {user.first_name} {user.last_name or ''}\n" + f"🆔 {user.id}\n" + f"👤 @{user.username or '无'}\n" + f"⏰ {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" + ) + await context.bot.send_message(chat_id=ADMIN_ID, text=admin_notification) + + async def handle_message(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """处理所有消息 - AI智能路由""" + if not update.message or not update.message.text: + return + + user = update.effective_user + user_id = user.id + text = update.message.text + is_admin = user_id == ADMIN_ID + + if is_admin and update.message.reply_to_message: + await self.handle_admin_reply(update, context) + return + + if self.is_search_command(text): + await self.handle_search_command(update, context) + return + + await self.handle_ai_conversation(update, context) + + def is_search_command(self, text: str) -> bool: + """检查是否是搜索命令""" + return text and text.split()[0] in SEARCH_COMMANDS + + async def handle_ai_conversation(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """AI对话处理 - 带智能按钮""" + user = update.effective_user + user_id = user.id + message = update.message.text + + # 显示"正在输入" + await update.message.chat.send_action("typing") + + # 构建上下文 + user_context = { + "username": user.username or f"user{user_id}", + "first_name": user.first_name or "朋友", + "last_name": user.last_name + } + + # 调用AI + ai_response = await self.call_ai_service(user_id, message, user_context) + response_text = ai_response.get("response", "") + + # 提取命令按钮 + buttons = self._extract_command_buttons(response_text) + + try: + if buttons: + # 构建按钮键盘 + keyboard = [] + for display, callback in buttons: + keyboard.append([InlineKeyboardButton( + f"🔍 {display}", + callback_data=callback + )]) + + # 添加常用按钮 + keyboard.append([ + InlineKeyboardButton("🔥 热门目录", callback_data="cmd_topchat"), + InlineKeyboardButton("📖 帮助", callback_data="cmd_help") + ]) + + await update.message.reply_text( + response_text, + reply_markup=InlineKeyboardMarkup(keyboard) + ) + logger.info(f"[AI对话] 已回复用户 {user_id} (带{len(buttons)}个按钮)") + else: + # 无按钮版本 + await update.message.reply_text(response_text) + logger.info(f"[AI对话] 已回复用户 {user_id}") + + except Exception as e: + logger.error(f"[AI对话] 发送失败: {e}, 降级为纯文本") + try: + await update.message.reply_text(response_text) + except: + await update.message.reply_text("抱歉,回复失败。请直接发送命令,如:/search AI") + + + + async def handle_search_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """处理搜索命令 - 带缓存""" + user = update.effective_user + user_id = user.id + command = update.message.text + + # 提取命令和关键词 + parts = command.split(maxsplit=1) + cmd = parts[0] + keyword = parts[1] if len(parts) > 1 else "" + + # 检查缓存 + if self.cache_db and keyword: + cached = self.cache_db.get_cache(cmd, keyword, 1) + if cached: + logger.info(f"返回缓存结果: {cmd} {keyword}") + await update.message.reply_text( + f"📦 从缓存返回结果:\n\n{cached['text'][:4000]}", + parse_mode='HTML' + ) + return + + # 通知管理员 + admin_notification = ( + f"🔍 用户执行搜索:\n" + f"👤 {user.first_name} {user.last_name or ''}\n" + f"🆔 {user_id}\n" + f"📝 {command}\n" + f"⏰ {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" + ) + await context.bot.send_message(chat_id=ADMIN_ID, text=admin_notification) + + wait_msg = await update.message.reply_text("🔍 正在搜索,请稍候...") + + self.user_search_sessions[user_id] = { + 'chat_id': update.effective_chat.id, + 'wait_msg_id': wait_msg.message_id, + 'command': cmd, + 'keyword': keyword, + 'timestamp': datetime.now() + } + + await self.pyrogram_client.send_message(self.target_bot_id, command) + logger.info(f"搜索: {command}") + + async def handle_search_response(self, message: PyrogramMessage, is_edit: bool = False): + """处理搜索机器人的响应 - 保存到缓存""" + try: + if not self.user_search_sessions: + return + + user_id = max(self.user_search_sessions.keys(), key=lambda k: self.user_search_sessions[k]['timestamp']) + session = self.user_search_sessions[user_id] + + text = message.text or message.caption or "无结果" + + try: + if message.text and hasattr(message.text, 'html'): + text = message.text.html + except: + pass + + keyboard = self.convert_keyboard(message) + + if is_edit and message.id in self.pyrogram_to_telegram: + telegram_msg_id = self.pyrogram_to_telegram[message.id] + await self.app.bot.edit_message_text( + chat_id=session['chat_id'], + message_id=telegram_msg_id, + text=text[:4000], + reply_markup=keyboard, + parse_mode='HTML' + ) + else: + try: + await self.app.bot.delete_message( + chat_id=session['chat_id'], + message_id=session['wait_msg_id'] + ) + except: + pass + + sent = await self.app.bot.send_message( + chat_id=session['chat_id'], + text=text[:4000], + reply_markup=keyboard, + parse_mode='HTML' + ) + + self.pyrogram_to_telegram[message.id] = sent.message_id + self.telegram_to_pyrogram[sent.message_id] = message.id + + # 保存到缓存 + if self.cache_db and session.get('keyword'): + buttons = self.extract_buttons(message) + self.cache_db.save_cache( + session['command'], + session['keyword'], + 1, # 第一页 + text, + text, + buttons + ) + + # 后台自动翻页(用户无感知) + if self.pagination_manager: + asyncio.create_task( + self.pagination_manager.start_pagination( + user_id, session['command'], session['keyword'], message + ) + ) + + except Exception as e: + logger.error(f"处理搜索响应失败: {e}") + + def convert_keyboard(self, message: PyrogramMessage) -> Optional[InlineKeyboardMarkup]: + """转换键盘""" + if not message.reply_markup or not message.reply_markup.inline_keyboard: + return None + + try: + buttons = [] + for row in message.reply_markup.inline_keyboard: + button_row = [] + for btn in row: + if btn.url: + button_row.append(InlineKeyboardButton(text=btn.text, url=btn.url)) + elif btn.callback_data: + callback_id = f"cb_{time.time():.0f}_{len(self.callback_data_map)}" + self.callback_data_map[callback_id] = (message.id, btn.callback_data) + button_row.append(InlineKeyboardButton(text=btn.text, callback_data=callback_id[:64])) + + if button_row: + buttons.append(button_row) + + return InlineKeyboardMarkup(buttons) if buttons else None + except Exception as e: + logger.error(f"键盘转换失败: {e}") + return None + + def extract_buttons(self, message: PyrogramMessage) -> list: + """提取按钮数据""" + if not message.reply_markup or not message.reply_markup.inline_keyboard: + return [] + + buttons = [] + for row in message.reply_markup.inline_keyboard: + for btn in row: + buttons.append({"text": btn.text, "url": btn.url if btn.url else None}) + return buttons + + async def handle_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """处理按钮点击 - 执行搜索命令""" + query = update.callback_query + data = query.data + user = query.from_user + + await query.answer() + + if data.startswith("cmd_"): + # 解析命令 + parts = data.replace("cmd_", "").split("_", 1) + cmd = parts[0] + keywords = parts[1].replace("_", " ") if len(parts) > 1 else "" + + # 构造完整命令 + command = f"/{cmd} {keywords}" if keywords else f"/{cmd}" + + logger.info(f"[用户 {user.id}] 点击按钮: {command}") + + # 显示执行提示 + await query.message.reply_text(f"🔍 正在执行:{command}\n请稍候...") + + # 转发到搜索bot + try: + await self.pyrogram_client.send_message(self.target_bot_id, command) + + # 记录搜索会话 + self.user_search_sessions[user.id] = { + 'chat_id': query.message.chat_id, + 'wait_msg_id': query.message.message_id + 1, + 'command': f"/{cmd}", + 'keyword': keywords, + 'timestamp': datetime.now() + } + + logger.info(f"[镜像] 已转发: {command}") + + except Exception as e: + logger.error(f"[镜像] 转发失败: {e}") + await query.message.reply_text("❌ 搜索失败,请稍后重试或直接发送命令") + + elif data == "cmd_help": + await query.message.reply_text( + "📖 使用指南:\n\n" + "• /search [关键词] - 按群组名称搜索\n" + "• /text [关键词] - 按消息内容搜索\n" + "• /human [关键词] - 按用户名搜索\n" + "• /topchat - 热门群组目录\n\n" + "💡 或者直接告诉我你想找什么!" + ) + + else: + logger.warning(f"未知callback: {data}") + + + async def handle_admin_reply(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """处理管理员回复""" + reply_to = update.message.reply_to_message + if not reply_to or not reply_to.text: + return + + import re + user_id = None + for line in reply_to.text.split('\n'): + if '🆔' in line or 'ID:' in line: + numbers = re.findall(r'\d+', line) + if numbers: + user_id = int(numbers[0]) + break + + if not user_id: + await update.message.reply_text("❌ 无法识别用户ID") + return + + try: + await context.bot.send_message(chat_id=user_id, text=update.message.text) + await update.message.reply_text(f"✅ 已回复给用户 {user_id}") + except Exception as e: + await update.message.reply_text(f"❌ 回复失败: {str(e)}") + + async def initialize(self): + """初始化机器人""" + try: + logger.info("正在初始化整合机器人...") + + if not await self.setup_pyrogram(): + logger.error("Pyrogram初始化失败") + return False + + builder = Application.builder().token(BOT_TOKEN) + + if os.environ.get('HTTP_PROXY'): + proxy_url = os.environ.get('HTTP_PROXY') + logger.info(f"配置Telegram Bot代理: {proxy_url}") + request = httpx.AsyncClient(proxies={"http://": proxy_url, "https://": proxy_url}, timeout=30.0) + builder = builder.request(request) + + self.app = builder.build() + + self.app.add_handler(CommandHandler("start", self.handle_start)) + self.app.add_handler(CallbackQueryHandler(self.handle_callback)) + self.app.add_handler(MessageHandler(tg_filters.ALL, self.handle_message)) + + logger.info("✅ 整合机器人初始化完成") + return True + + except Exception as e: + logger.error(f"初始化失败: {e}") + return False + + async def run(self): + """运行机器人""" + try: + await self.app.initialize() + await self.app.start() + await self.app.updater.start_polling(drop_pending_updates=True) + + logger.info("="*50) + logger.info("✅ AI增强版Bot已启动") + logger.info(f"AI服务: {MAC_API_URL}") + logger.info(f"缓存功能: {'启用' if self.cache_db else '禁用'}") + logger.info("="*50) + + await asyncio.Event().wait() + + except KeyboardInterrupt: + logger.info("收到停止信号") + finally: + await self.cleanup() + + async def cleanup(self): + """清理资源""" + logger.info("正在清理...") + + if self.app: + await self.app.updater.stop() + await self.app.stop() + await self.app.shutdown() + + if self.pyrogram_client: + await self.pyrogram_client.stop() + + logger.info("✅ 清理完成") + + +async def main(): + """主函数""" + bot = IntegratedBotAI() + + if await bot.initialize(): + await bot.run() + else: + logger.error("初始化失败,退出") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/integrated_bot_ai.py.before_optimization b/integrated_bot_ai.py.before_optimization new file mode 100755 index 0000000..c4b6904 --- /dev/null +++ b/integrated_bot_ai.py.before_optimization @@ -0,0 +1,604 @@ +#!/usr/bin/env python3 +""" +整合版客服机器人 - AI增强版 +包含: +1. AI对话引导 +2. 镜像搜索功能 +3. 自动翻页缓存 +4. 智能去重 +""" + +import asyncio +import logging +import time +import os +import httpx +import anthropic +import json +import sys +from typing import Dict, Optional +from datetime import datetime + +# 添加路径 +sys.path.insert(0, "/home/atai/bot_data") + +# Pyrogram imports +from pyrogram import Client as PyrogramClient, filters +from pyrogram.types import Message as PyrogramMessage +from pyrogram.raw.functions.messages import GetBotCallbackAnswer + +# Telegram Bot imports +from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup +from telegram.ext import Application, CommandHandler, MessageHandler, CallbackQueryHandler, filters as tg_filters +from telegram.ext import ContextTypes + +# 导入数据库 +try: + from database import CacheDatabase +except ImportError: + CacheDatabase = None + logging.warning("database.py未找到,缓存功能将禁用") + +# ================== 配置 ================== +API_ID = 24660516 +API_HASH = "eae564578880a59c9963916ff1bbbd3a" +SESSION_NAME = "user_session" +BOT_TOKEN = "8426529617:AAHAxzohSMFBAxInzbAVJsZfkB5bHnOyFC4" +TARGET_BOT = "@openaiw_bot" +ADMIN_ID = 7363537082 + +# AI服务配置 +MAC_API_URL = "http://192.168.9.10:8000" + +# 搜索命令列表 +SEARCH_COMMANDS = ['/topchat', '/search', '/text', '/human'] + +# 日志配置 +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +# 初始化Claude客户端 +try: + claude_client = anthropic.Anthropic( + api_key=os.environ.get('ANTHROPIC_AUTH_TOKEN'), + base_url=os.environ.get('ANTHROPIC_BASE_URL', 'https://api.anthropic.com') + ) + logger.info("✅ Claude API客户端已初始化") +except Exception as e: + logger.error(f"❌ Claude API初始化失败: {e}") + claude_client = None + + + +class IntegratedBotAI: + """整合的客服机器人 - AI增强版""" + + def __init__(self): + # Bot应用 + self.app = None + + # Pyrogram客户端(用于镜像) + self.pyrogram_client: Optional[PyrogramClient] = None + self.target_bot_id: Optional[int] = None + + # 消息映射 + self.pyrogram_to_telegram = {} + self.telegram_to_pyrogram = {} + self.callback_data_map = {} + self.user_search_sessions = {} + + # AI会话状态 + self.user_ai_sessions = {} + + # 缓存数据库 + self.cache_db = CacheDatabase() if CacheDatabase else None + + async def setup_pyrogram(self): + """设置Pyrogram客户端""" + try: + proxy_config = None + if os.environ.get('ALL_PROXY'): + proxy_url = os.environ.get('ALL_PROXY', '').replace('socks5://', '') + if proxy_url: + host, port = proxy_url.split(':') + proxy_config = {"scheme": "socks5", "hostname": host, "port": int(port)} + + self.pyrogram_client = PyrogramClient( + SESSION_NAME, api_id=API_ID, api_hash=API_HASH, + proxy=proxy_config if proxy_config else None + ) + + await self.pyrogram_client.start() + logger.info("✅ Pyrogram客户端已启动") + + target = await self.pyrogram_client.get_users(TARGET_BOT) + self.target_bot_id = target.id + logger.info(f"✅ 已连接到搜索机器人: {target.username}") + + @self.pyrogram_client.on_message(filters.user(self.target_bot_id)) + async def on_bot_response(_, message: PyrogramMessage): + await self.handle_search_response(message) + + @self.pyrogram_client.on_edited_message(filters.user(self.target_bot_id)) + async def on_message_edited(_, message: PyrogramMessage): + await self.handle_search_response(message, is_edit=True) + + return True + except Exception as e: + logger.error(f"Pyrogram设置失败: {e}") + return False + + async def call_ai_service(self, user_id: int, message: str, context: dict = None) -> dict: + """直接调用Claude API""" + if not claude_client: + logger.error("Claude客户端未初始化") + return { + "type": "auto", + "response": "👋 请直接发送搜索关键词,或使用以下命令:\n\n• /search [关键词] - 搜索群组名称\n• /text [关键词] - 搜索消息内容\n• /topchat - 热门分类", + "confidence": 0.5 + } + + try: + logger.info(f"[用户 {user_id}] 调用Claude API处理消息: {message}") + + username = context.get('username', f'user_{user_id}') if context else f'user_{user_id}' + first_name = context.get('first_name', '') if context else '' + + response = claude_client.messages.create( + model="claude-sonnet-4-20250514", + max_tokens=1024, + messages=[{ + "role": "user", + "content": f"""你是Telegram搜索助手Bot (@ktfund_bot)。 + +用户信息: +- 用户名: @{username} +- 姓名: {first_name} +- ID: {user_id} + +用户消息: "{message}" + +请分析用户意图并提供友好的回复。可用的搜索命令: +- /search [关键词] - 按群组/频道名称搜索 +- /text [关键词] - 按消息内容搜索 +- /human [关键词] - 按用户名搜索 +- /topchat - 查看热门群组目录 + +要求: +1. 用中文回复(除非用户用英文) +2. 友好、简洁、有帮助 +3. 如果是搜索需求,建议合适的命令 +4. 如果是投诉/问题,表示理解并提供帮助 +5. 直接给出回复内容,不要解释你的思考过程 + +直接回复用户:""" + }] + ) + + claude_response = response.content[0].text.strip() + logger.info(f"[用户 {user_id}] ✅ Claude回复成功") + + return { + "type": "ai", + "response": claude_response, + "confidence": 1.0 + } + + except Exception as e: + logger.error(f"[用户 {user_id}] ❌ Claude API调用失败: {e}") + return { + "type": "auto", + "response": "👋 请直接发送搜索关键词,或使用以下命令:\n\n• /search [关键词] - 搜索群组名称\n• /text [关键词] - 搜索消息内容\n• /topchat - 热门分类", + "confidence": 0.5 + } + + async def handle_start(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """处理/start命令 - AI引导模式""" + user = update.effective_user + user_id = user.id + + self.user_ai_sessions[user_id] = {"started_at": datetime.now(), "conversation": []} + + welcome_text = ( + f"👋 您好 {user.first_name}!\n\n" + "我是智能搜索助手,可以帮您找到Telegram上的群组和频道。\n\n" + "🔍 我能做什么:\n" + "• 搜索群组/频道\n" + "• 搜索特定话题的讨论\n" + "• 查找用户\n" + "• 浏览热门分类\n\n" + "💬 直接告诉我您想找什么,我会帮您选择最合适的搜索方式!" + ) + + keyboard = [ + [InlineKeyboardButton("🔍 搜索群组", callback_data="quick_search"), + InlineKeyboardButton("📚 使用指南", callback_data="quick_help")], + [InlineKeyboardButton("🔥 热门分类", callback_data="quick_topchat")] + ] + + await update.message.reply_text(welcome_text, reply_markup=InlineKeyboardMarkup(keyboard)) + + # 通知管理员 + admin_notification = ( + f"🆕 新用户访问 (AI模式):\n" + f"👤 {user.first_name} {user.last_name or ''}\n" + f"🆔 {user.id}\n" + f"👤 @{user.username or '无'}\n" + f"⏰ {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" + ) + await context.bot.send_message(chat_id=ADMIN_ID, text=admin_notification) + + async def handle_message(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """处理所有消息 - AI智能路由""" + if not update.message or not update.message.text: + return + + user = update.effective_user + user_id = user.id + text = update.message.text + is_admin = user_id == ADMIN_ID + + if is_admin and update.message.reply_to_message: + await self.handle_admin_reply(update, context) + return + + if self.is_search_command(text): + await self.handle_search_command(update, context) + return + + await self.handle_ai_conversation(update, context) + + def is_search_command(self, text: str) -> bool: + """检查是否是搜索命令""" + return text and text.split()[0] in SEARCH_COMMANDS + + async def handle_ai_conversation(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """AI对话处理""" + user = update.effective_user + user_id = user.id + message = update.message.text + + # 构建上下文信息 + user_context = { + "username": user.username or f"{user.first_name}_{user.id}", + "first_name": user.first_name, + "last_name": user.last_name + } + + ai_response = await self.call_ai_service(user_id, message, user_context) + + if ai_response.get("type") == "auto": + response_text = ai_response.get("response", "") + suggested_cmd = ai_response.get("suggested_command") + keywords = ai_response.get("keywords") + + if suggested_cmd and keywords: + keyboard = [ + [InlineKeyboardButton( + f"✅ 开始搜索: {suggested_cmd} {keywords}", + callback_data=f"exec_{suggested_cmd.replace('/', '')}_{keywords}"[:64] + )], + [InlineKeyboardButton("✏️ 修改关键词", callback_data="modify_keywords")] + ] + await update.message.reply_text(response_text, reply_markup=InlineKeyboardMarkup(keyboard)) + + self.user_ai_sessions[user_id] = { + "suggested_command": suggested_cmd, + "keywords": keywords, + "original_message": message + } + else: + await update.message.reply_text(response_text) + else: + response_text = ai_response.get("response", "抱歉,我没有理解您的需求。") + await update.message.reply_text(response_text) + + async def handle_search_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """处理搜索命令 - 带缓存""" + user = update.effective_user + user_id = user.id + command = update.message.text + + # 提取命令和关键词 + parts = command.split(maxsplit=1) + cmd = parts[0] + keyword = parts[1] if len(parts) > 1 else "" + + # 检查缓存 + if self.cache_db and keyword: + cached = self.cache_db.get_cache(cmd, keyword, 1) + if cached: + logger.info(f"返回缓存结果: {cmd} {keyword}") + await update.message.reply_text( + f"📦 从缓存返回结果:\n\n{cached['text'][:4000]}", + parse_mode='HTML' + ) + return + + # 通知管理员 + admin_notification = ( + f"🔍 用户执行搜索:\n" + f"👤 {user.first_name} {user.last_name or ''}\n" + f"🆔 {user_id}\n" + f"📝 {command}\n" + f"⏰ {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" + ) + await context.bot.send_message(chat_id=ADMIN_ID, text=admin_notification) + + wait_msg = await update.message.reply_text("🔍 正在搜索,请稍候...") + + self.user_search_sessions[user_id] = { + 'chat_id': update.effective_chat.id, + 'wait_msg_id': wait_msg.message_id, + 'command': cmd, + 'keyword': keyword, + 'timestamp': datetime.now() + } + + await self.pyrogram_client.send_message(self.target_bot_id, command) + logger.info(f"搜索: {command}") + + async def handle_search_response(self, message: PyrogramMessage, is_edit: bool = False): + """处理搜索机器人的响应 - 保存到缓存""" + try: + if not self.user_search_sessions: + return + + user_id = max(self.user_search_sessions.keys(), key=lambda k: self.user_search_sessions[k]['timestamp']) + session = self.user_search_sessions[user_id] + + text = message.text or message.caption or "无结果" + + try: + if message.text and hasattr(message.text, 'html'): + text = message.text.html + except: + pass + + keyboard = self.convert_keyboard(message) + + if is_edit and message.id in self.pyrogram_to_telegram: + telegram_msg_id = self.pyrogram_to_telegram[message.id] + await self.app.bot.edit_message_text( + chat_id=session['chat_id'], + message_id=telegram_msg_id, + text=text[:4000], + reply_markup=keyboard, + parse_mode='HTML' + ) + else: + try: + await self.app.bot.delete_message( + chat_id=session['chat_id'], + message_id=session['wait_msg_id'] + ) + except: + pass + + sent = await self.app.bot.send_message( + chat_id=session['chat_id'], + text=text[:4000], + reply_markup=keyboard, + parse_mode='HTML' + ) + + self.pyrogram_to_telegram[message.id] = sent.message_id + self.telegram_to_pyrogram[sent.message_id] = message.id + + # 保存到缓存 + if self.cache_db and session.get('keyword'): + buttons = self.extract_buttons(message) + self.cache_db.save_cache( + session['command'], + session['keyword'], + 1, # 第一页 + text, + text, + buttons + ) + + except Exception as e: + logger.error(f"处理搜索响应失败: {e}") + + def convert_keyboard(self, message: PyrogramMessage) -> Optional[InlineKeyboardMarkup]: + """转换键盘""" + if not message.reply_markup or not message.reply_markup.inline_keyboard: + return None + + try: + buttons = [] + for row in message.reply_markup.inline_keyboard: + button_row = [] + for btn in row: + if btn.url: + button_row.append(InlineKeyboardButton(text=btn.text, url=btn.url)) + elif btn.callback_data: + callback_id = f"cb_{time.time():.0f}_{len(self.callback_data_map)}" + self.callback_data_map[callback_id] = (message.id, btn.callback_data) + button_row.append(InlineKeyboardButton(text=btn.text, callback_data=callback_id[:64])) + + if button_row: + buttons.append(button_row) + + return InlineKeyboardMarkup(buttons) if buttons else None + except Exception as e: + logger.error(f"键盘转换失败: {e}") + return None + + def extract_buttons(self, message: PyrogramMessage) -> list: + """提取按钮数据""" + if not message.reply_markup or not message.reply_markup.inline_keyboard: + return [] + + buttons = [] + for row in message.reply_markup.inline_keyboard: + for btn in row: + buttons.append({"text": btn.text, "url": btn.url if btn.url else None}) + return buttons + + async def handle_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """处理回调查询""" + query = update.callback_query + data = query.data + + await query.answer() + + if data == "quick_search": + await query.message.reply_text("请告诉我您想搜索什么内容") + return + elif data == "quick_help": + await query.message.reply_text( + "📖 使用指南:\n\n" + "• /search [关键词] - 按群组名称搜索\n" + "• /text [关键词] - 按消息内容搜索\n" + "• /human [关键词] - 按用户名搜索\n" + "• /topchat - 热门群组目录" + ) + return + elif data == "quick_topchat": + # 创建假update来执行搜索 + from types import SimpleNamespace + fake_update = SimpleNamespace( + effective_user=query.from_user, + effective_chat=query.message.chat, + message=SimpleNamespace(text='/topchat') + ) + await self.handle_search_command(fake_update, context) + return + elif data.startswith("exec_"): + parts = data.replace("exec_", "").split("_", 1) + if len(parts) == 2: + command, keywords = parts + search_text = f"/{command} {keywords}" + from types import SimpleNamespace + fake_update = SimpleNamespace( + effective_user=query.from_user, + effective_chat=query.message.chat, + message=SimpleNamespace(text=search_text) + ) + await self.handle_search_command(fake_update, context) + return + + # 翻页callback + if data in self.callback_data_map: + pyrogram_msg_id, original_callback = self.callback_data_map[data] + try: + if not isinstance(original_callback, bytes): + original_callback = original_callback.encode() if original_callback else b'' + + await self.pyrogram_client.invoke( + GetBotCallbackAnswer( + peer=await self.pyrogram_client.resolve_peer(self.target_bot_id), + msg_id=pyrogram_msg_id, + data=original_callback + ) + ) + await asyncio.sleep(1) + except Exception as e: + logger.error(f"Callback处理失败: {e}") + + async def handle_admin_reply(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """处理管理员回复""" + reply_to = update.message.reply_to_message + if not reply_to or not reply_to.text: + return + + import re + user_id = None + for line in reply_to.text.split('\n'): + if '🆔' in line or 'ID:' in line: + numbers = re.findall(r'\d+', line) + if numbers: + user_id = int(numbers[0]) + break + + if not user_id: + await update.message.reply_text("❌ 无法识别用户ID") + return + + try: + await context.bot.send_message(chat_id=user_id, text=update.message.text) + await update.message.reply_text(f"✅ 已回复给用户 {user_id}") + except Exception as e: + await update.message.reply_text(f"❌ 回复失败: {str(e)}") + + async def initialize(self): + """初始化机器人""" + try: + logger.info("正在初始化整合机器人...") + + if not await self.setup_pyrogram(): + logger.error("Pyrogram初始化失败") + return False + + builder = Application.builder().token(BOT_TOKEN) + + if os.environ.get('HTTP_PROXY'): + proxy_url = os.environ.get('HTTP_PROXY') + logger.info(f"配置Telegram Bot代理: {proxy_url}") + request = httpx.AsyncClient(proxies={"http://": proxy_url, "https://": proxy_url}, timeout=30.0) + builder = builder.request(request) + + self.app = builder.build() + + self.app.add_handler(CommandHandler("start", self.handle_start)) + self.app.add_handler(CallbackQueryHandler(self.handle_callback)) + self.app.add_handler(MessageHandler(tg_filters.ALL, self.handle_message)) + + logger.info("✅ 整合机器人初始化完成") + return True + + except Exception as e: + logger.error(f"初始化失败: {e}") + return False + + async def run(self): + """运行机器人""" + try: + await self.app.initialize() + await self.app.start() + await self.app.updater.start_polling(drop_pending_updates=True) + + logger.info("="*50) + logger.info("✅ AI增强版Bot已启动") + logger.info(f"AI服务: {MAC_API_URL}") + logger.info(f"缓存功能: {'启用' if self.cache_db else '禁用'}") + logger.info("="*50) + + await asyncio.Event().wait() + + except KeyboardInterrupt: + logger.info("收到停止信号") + finally: + await self.cleanup() + + async def cleanup(self): + """清理资源""" + logger.info("正在清理...") + + if self.app: + await self.app.updater.stop() + await self.app.stop() + await self.app.shutdown() + + if self.pyrogram_client: + await self.pyrogram_client.stop() + + logger.info("✅ 清理完成") + + +async def main(): + """主函数""" + bot = IntegratedBotAI() + + if await bot.initialize(): + await bot.run() + else: + logger.error("初始化失败,退出") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/integrated_bot_ai_backup_20251007_155823.py b/integrated_bot_ai_backup_20251007_155823.py new file mode 100755 index 0000000..3364cee --- /dev/null +++ b/integrated_bot_ai_backup_20251007_155823.py @@ -0,0 +1,865 @@ +#!/usr/bin/env python3 +""" +整合版客服机器人 - AI增强版 +包含: +1. AI对话引导 +2. 镜像搜索功能 +3. 自动翻页缓存 +4. 智能去重 +""" + +import asyncio +import logging +import time +import os +import httpx +import anthropic +import json +import sys +from typing import Dict, Optional +from datetime import datetime + +# 添加路径 +sys.path.insert(0, "/home/atai/bot_data") + +# Pyrogram imports +from pyrogram import Client as PyrogramClient, filters +from pyrogram.types import Message as PyrogramMessage +from pyrogram.raw.functions.messages import GetBotCallbackAnswer + +# Telegram Bot imports +from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup +from telegram.ext import Application, CommandHandler, MessageHandler, CallbackQueryHandler, filters as tg_filters +from telegram.ext import ContextTypes + +# 导入数据库 +try: + from database import CacheDatabase +except ImportError: + CacheDatabase = None + logging.warning("database.py未找到,缓存功能将禁用") + +# ================== 配置 ================== +API_ID = 24660516 +API_HASH = "eae564578880a59c9963916ff1bbbd3a" +SESSION_NAME = "user_session" +BOT_TOKEN = "8426529617:AAHAxzohSMFBAxInzbAVJsZfkB5bHnOyFC4" +TARGET_BOT = "@openaiw_bot" +ADMIN_ID = 7363537082 + +# AI服务配置 +MAC_API_URL = "http://192.168.9.10:8000" + +# 搜索命令列表 +SEARCH_COMMANDS = ['/topchat', '/search', '/text', '/human'] + +# 日志配置 +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +# 初始化Claude客户端 +try: + claude_client = anthropic.Anthropic( + auth_token=os.environ.get('ANTHROPIC_AUTH_TOKEN'), + base_url=os.environ.get('ANTHROPIC_BASE_URL', 'https://api.anthropic.com') + ) + logger.info("✅ Claude API客户端已初始化") +except Exception as e: + logger.error(f"❌ Claude API初始化失败: {e}") + claude_client = None + + + + +# ================== 对话管理 ================== +class ConversationManager: + """管理用户对话上下文""" + + def __init__(self, max_history=5): + self.conversations = {} + self.max_history = max_history + + def add_message(self, user_id: int, role: str, content: str): + """添加消息到历史""" + if user_id not in self.conversations: + self.conversations[user_id] = [] + + self.conversations[user_id].append({ + "role": role, + "content": content, + "timestamp": datetime.now().isoformat() + }) + + # 保持最近的N条消息 + if len(self.conversations[user_id]) > self.max_history * 2: + self.conversations[user_id] = self.conversations[user_id][-self.max_history * 2:] + + def get_history(self, user_id: int, limit: int = 2) -> list: + """获取用户对话历史""" + if user_id not in self.conversations: + return [] + + history = self.conversations[user_id][-limit * 2:] + return [{"role": msg["role"], "content": msg["content"]} for msg in history] + + def clear_history(self, user_id: int): + """清空用户历史""" + if user_id in self.conversations: + del self.conversations[user_id] + + +# ================== 自动翻页管理器 ================== +class AutoPaginationManager: + """后台自动翻页 - 用户无感知""" + + def __init__(self, pyrogram_client, cache_db, target_bot_id, logger): + self.pyrogram_client = pyrogram_client + self.cache_db = cache_db + self.target_bot_id = target_bot_id + self.logger = logger + self.active_tasks = {} + + async def start_pagination(self, user_id, command, keyword, first_message): + """启动后台翻页任务""" + if user_id in self.active_tasks: + return + + task = asyncio.create_task(self._paginate(user_id, command, keyword, first_message)) + self.active_tasks[user_id] = task + self.logger.info(f"[翻页] 后台任务启动: {command} {keyword}") + + async def _paginate(self, user_id, command, keyword, message): + """执行翻页""" + try: + page = 1 + self._save_to_cache(command, keyword, page, message) + + if not self._has_next(message): + self.logger.info(f"[翻页] 只有1页") + return + + current = message + page = 2 + max_pages = 100 # 最多100页防止死循环 + + while page <= max_pages: + await asyncio.sleep(2) + + next_msg = await self._click_next(current) + if not next_msg: + self.logger.info(f"[翻页] 点击失败,停止翻页") + break + + self._save_to_cache(command, keyword, page, next_msg) + self.logger.info(f"[翻页] 第{page}页已保存") + + if not self._has_next(next_msg): + self.logger.info(f"[翻页] ✅ 完成,共{page}页") + break + + current = next_msg + page += 1 + + except Exception as e: + self.logger.error(f"[翻页] 错误: {e}") + finally: + if user_id in self.active_tasks: + del self.active_tasks[user_id] + + def _has_next(self, msg): + """检查是否有下一页""" + if not msg.reply_markup: + return False + for row in msg.reply_markup.inline_keyboard: + for btn in row: + if btn.text: + text = btn.text.strip() + # 检查右箭头(排除左箭头) + if any(arrow in text for arrow in ["➡️", "▶", "→", "»"]): + if not any(prev in text for prev in ["⬅️", "◀", "←", "«"]): + return True + # 检查文字 + if any(x in text for x in ["下一页", "Next"]): + return True + return False + async def _click_next(self, msg): + """点击下一页""" + try: + from pyrogram.raw.functions.messages import GetBotCallbackAnswer + + for row in msg.reply_markup.inline_keyboard: + for btn in row: + if btn.text and btn.callback_data: + text = btn.text.strip() + # 检查是否是下一页按钮 + is_next = False + if any(arrow in text for arrow in ['➡️', '▶', '→', '»']): + if not any(prev in text for prev in ['⬅️', '◀', '←', '«']): + is_next = True + if any(x in text for x in ['下一页', 'Next']): + is_next = True + if is_next: + await self.pyrogram_client.invoke( + GetBotCallbackAnswer( + peer=await self.pyrogram_client.resolve_peer(self.target_bot_id), + msg_id=msg.id, + data=btn.callback_data + ) + ) + await asyncio.sleep(1.5) + return await self.pyrogram_client.get_messages(self.target_bot_id, msg.id) + except Exception as e: + self.logger.error(f"[翻页] 点击失败: {e}") + return None + + def _save_to_cache(self, cmd, keyword, page, msg): + """保存到缓存""" + if not self.cache_db: + return + try: + text = msg.text or msg.caption or "" + buttons = [] + if msg.reply_markup: + for row in msg.reply_markup.inline_keyboard: + for btn in row: + buttons.append({"text": btn.text, "url": btn.url if btn.url else None}) + self.cache_db.save_cache(cmd, keyword, page, text, buttons) + except Exception as e: + self.logger.error(f"[翻页] 保存失败: {e}") + + +class IntegratedBotAI: + """整合的客服机器人 - AI增强版""" + + def __init__(self): + # Bot应用 + self.app = None + + # Pyrogram客户端(用于镜像) + self.pyrogram_client: Optional[PyrogramClient] = None + self.target_bot_id: Optional[int] = None + + # 消息映射 + self.pyrogram_to_telegram = {} + self.telegram_to_pyrogram = {} + self.callback_data_map = {} + self.user_search_sessions = {} + + # AI会话状态 + self.user_ai_sessions = {} + + # 缓存数据库 + self.cache_db = CacheDatabase() if CacheDatabase else None + + # 对话管理器 + self.conversation_manager = ConversationManager() + self.pagination_manager = None + + async def setup_pyrogram(self): + """设置Pyrogram客户端""" + try: + proxy_config = None + if os.environ.get('ALL_PROXY'): + proxy_url = os.environ.get('ALL_PROXY', '').replace('socks5://', '') + if proxy_url: + host, port = proxy_url.split(':') + proxy_config = {"scheme": "socks5", "hostname": host, "port": int(port)} + + self.pyrogram_client = PyrogramClient( + SESSION_NAME, api_id=API_ID, api_hash=API_HASH, + proxy=proxy_config if proxy_config else None + ) + + await self.pyrogram_client.start() + logger.info("✅ Pyrogram客户端已启动") + + # 初始化自动翻页管理器 + self.pagination_manager = AutoPaginationManager( + self.pyrogram_client, self.cache_db, self.target_bot_id, logger + ) + logger.info("✅ 自动翻页管理器已初始化") + + target = await self.pyrogram_client.get_users(TARGET_BOT) + self.target_bot_id = target.id + logger.info(f"✅ 已连接到搜索机器人: {target.username}") + + @self.pyrogram_client.on_message(filters.user(self.target_bot_id)) + async def on_bot_response(_, message: PyrogramMessage): + await self.handle_search_response(message) + + @self.pyrogram_client.on_edited_message(filters.user(self.target_bot_id)) + async def on_message_edited(_, message: PyrogramMessage): + await self.handle_search_response(message, is_edit=True) + + return True + except Exception as e: + logger.error(f"Pyrogram设置失败: {e}") + return False + + async def call_ai_service(self, user_id: int, message: str, context: dict = None) -> dict: + """优化的Claude API调用 - 带上下文记忆和改进提示词""" + + if not claude_client: + logger.error("Claude客户端未初始化") + return { + "type": "auto", + "response": "👋 我来帮你搜索!\n\n直接发关键词,或试试:\n• /search 群组名\n• /text 讨论内容\n• /topchat 热门分类", + "confidence": 0.3 + } + + try: + logger.info(f"[用户 {user_id}] 调用Claude API: {message}") + + username = context.get('username', f'user_{user_id}') if context else f'user_{user_id}' + first_name = context.get('first_name', '') if context else '' + + # 构建对话历史 + messages = [] + + # 添加历史对话(最近2轮) + history = self.conversation_manager.get_history(user_id, limit=2) + messages.extend(history) + + # 添加当前消息(优化的提示词) + current_prompt = f"""你是@ktfund_bot的AI助手,专业的Telegram群组搜索助手。 + +【重要】你的回复中可以包含可执行的命令,我会为它们生成按钮。 +命令格式:/search 关键词 或 /text 关键词 + +用户信息:@{username} ({first_name}) +用户说:"{message}" + +【可用工具】 +• /search [关键词] - 搜索群组名称 +• /text [关键词] - 搜索讨论内容 +• /human [关键词] - 搜索用户 +• /topchat - 热门分类 + +【回复要求】 +1. 简短友好(2-4行) +2. 给1-2个具体命令建议 +3. 口语化,像朋友聊天 +4. 命令要在独立的一行 + +【示例】 +用户:"找AI群" +回复: +找AI群的话,试试: +/search AI +/text ChatGPT + +直接回复:""" + + messages.append({ + "role": "user", + "content": current_prompt + }) + + # 调用Claude API + response = claude_client.messages.create( + model="claude-sonnet-4-20250514", + max_tokens=512, + temperature=0.7, + messages=messages + ) + + claude_response = response.content[0].text.strip() + + # 保存对话历史 + self.conversation_manager.add_message(user_id, "user", message) + self.conversation_manager.add_message(user_id, "assistant", claude_response) + + logger.info(f"[用户 {user_id}] ✅ Claude回复成功 ({len(claude_response)}字)") + + # 智能提取命令建议 + suggested_commands = self._extract_commands(claude_response) + + return { + "type": "ai", + "response": claude_response, + "confidence": 1.0, + "suggested_commands": suggested_commands + } + + except Exception as e: + logger.error(f"[用户 {user_id}] ❌ Claude API失败: {e}") + return { + "type": "auto", + "response": "👋 我来帮你搜索!\n\n直接发关键词,或试试:\n• /search 群组名\n• /text 讨论内容\n• /topchat 热门分类", + "confidence": 0.3 + } + + def _extract_commands(self, response_text: str) -> list: + """从回复中提取建议的命令""" + import re + commands = [] + + # 匹配 /command pattern + patterns = [ + r'/search\s+[\w\s]+', + r'/text\s+[\w\s]+', + r'/human\s+[\w\s]+', + r'/topchat' + ] + + for pattern in patterns: + matches = re.findall(pattern, response_text) + commands.extend([m.strip() for m in matches[:1]]) + + return commands[:2] + + + + def _extract_command_buttons(self, text: str) -> list: + """从AI回复中提取命令按钮""" + import re + buttons = [] + + # 匹配:/command keyword + pattern = r'/(search|text|human|topchat)\s*([^\n]*)' + matches = re.findall(pattern, text, re.IGNORECASE) + + for cmd, keywords in matches[:3]: + cmd = cmd.lower() + keywords = keywords.strip()[:30] # 限制长度 + + if keywords: + display = f"/{cmd} {keywords}" + callback = f"cmd_{cmd}_{keywords.replace(' ', '_')}"[:64] + else: + display = f"/{cmd}" + callback = f"cmd_{cmd}" + + buttons.append((display, callback)) + + return buttons + + async def handle_start(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """处理/start命令 - AI引导模式""" + user = update.effective_user + user_id = user.id + + self.user_ai_sessions[user_id] = {"started_at": datetime.now(), "conversation": []} + + welcome_text = ( + f"👋 您好 {user.first_name}!\n\n" + "我是智能搜索助手,可以帮您找到Telegram上的群组和频道。\n\n" + "🔍 我能做什么:\n" + "• 搜索群组/频道\n" + "• 搜索特定话题的讨论\n" + "• 查找用户\n" + "• 浏览热门分类\n\n" + "💬 直接告诉我您想找什么,我会帮您选择最合适的搜索方式!" + ) + + keyboard = [ + [InlineKeyboardButton("🔍 搜索群组", callback_data="quick_search"), + InlineKeyboardButton("📚 使用指南", callback_data="quick_help")], + [InlineKeyboardButton("🔥 热门分类", callback_data="quick_topchat")] + ] + + await update.message.reply_text(welcome_text, reply_markup=InlineKeyboardMarkup(keyboard)) + + # 通知管理员 + admin_notification = ( + f"🆕 新用户访问 (AI模式):\n" + f"👤 {user.first_name} {user.last_name or ''}\n" + f"🆔 {user.id}\n" + f"👤 @{user.username or '无'}\n" + f"⏰ {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" + ) + await context.bot.send_message(chat_id=ADMIN_ID, text=admin_notification) + + async def handle_message(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """处理所有消息 - AI智能路由""" + if not update.message or not update.message.text: + return + + user = update.effective_user + user_id = user.id + text = update.message.text + is_admin = user_id == ADMIN_ID + + if is_admin and update.message.reply_to_message: + await self.handle_admin_reply(update, context) + return + + if self.is_search_command(text): + await self.handle_search_command(update, context) + return + + await self.handle_ai_conversation(update, context) + + def is_search_command(self, text: str) -> bool: + """检查是否是搜索命令""" + return text and text.split()[0] in SEARCH_COMMANDS + + async def handle_ai_conversation(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """AI对话处理 - 带智能按钮""" + user = update.effective_user + user_id = user.id + message = update.message.text + + # 显示"正在输入" + await update.message.chat.send_action("typing") + + # 构建上下文 + user_context = { + "username": user.username or f"user{user_id}", + "first_name": user.first_name or "朋友", + "last_name": user.last_name + } + + # 调用AI + ai_response = await self.call_ai_service(user_id, message, user_context) + response_text = ai_response.get("response", "") + + # 提取命令按钮 + buttons = self._extract_command_buttons(response_text) + + try: + if buttons: + # 构建按钮键盘 + keyboard = [] + for display, callback in buttons: + keyboard.append([InlineKeyboardButton( + f"🔍 {display}", + callback_data=callback + )]) + + # 添加常用按钮 + keyboard.append([ + InlineKeyboardButton("🔥 热门目录", callback_data="cmd_topchat"), + InlineKeyboardButton("📖 帮助", callback_data="cmd_help") + ]) + + await update.message.reply_text( + response_text, + reply_markup=InlineKeyboardMarkup(keyboard) + ) + logger.info(f"[AI对话] 已回复用户 {user_id} (带{len(buttons)}个按钮)") + else: + # 无按钮版本 + await update.message.reply_text(response_text) + logger.info(f"[AI对话] 已回复用户 {user_id}") + + except Exception as e: + logger.error(f"[AI对话] 发送失败: {e}, 降级为纯文本") + try: + await update.message.reply_text(response_text) + except: + await update.message.reply_text("抱歉,回复失败。请直接发送命令,如:/search AI") + + + + async def handle_search_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """处理搜索命令 - 带缓存""" + user = update.effective_user + user_id = user.id + command = update.message.text + + # 提取命令和关键词 + parts = command.split(maxsplit=1) + cmd = parts[0] + keyword = parts[1] if len(parts) > 1 else "" + + # 检查缓存 + if self.cache_db and keyword: + cached = self.cache_db.get_cache(cmd, keyword, 1) + if cached: + logger.info(f"返回缓存结果: {cmd} {keyword}") + await update.message.reply_text( + f"📦 从缓存返回结果:\n\n{cached['text'][:4000]}", + parse_mode='HTML' + ) + return + + # 通知管理员 + admin_notification = ( + f"🔍 用户执行搜索:\n" + f"👤 {user.first_name} {user.last_name or ''}\n" + f"🆔 {user_id}\n" + f"📝 {command}\n" + f"⏰ {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" + ) + await context.bot.send_message(chat_id=ADMIN_ID, text=admin_notification) + + wait_msg = await update.message.reply_text("🔍 正在搜索,请稍候...") + + self.user_search_sessions[user_id] = { + 'chat_id': update.effective_chat.id, + 'wait_msg_id': wait_msg.message_id, + 'command': cmd, + 'keyword': keyword, + 'timestamp': datetime.now() + } + + await self.pyrogram_client.send_message(self.target_bot_id, command) + logger.info(f"搜索: {command}") + + async def handle_search_response(self, message: PyrogramMessage, is_edit: bool = False): + """处理搜索机器人的响应 - 保存到缓存""" + try: + if not self.user_search_sessions: + return + + user_id = max(self.user_search_sessions.keys(), key=lambda k: self.user_search_sessions[k]['timestamp']) + session = self.user_search_sessions[user_id] + + text = message.text or message.caption or "无结果" + + try: + if message.text and hasattr(message.text, 'html'): + text = message.text.html + except: + pass + + keyboard = self.convert_keyboard(message) + + if is_edit and message.id in self.pyrogram_to_telegram: + telegram_msg_id = self.pyrogram_to_telegram[message.id] + await self.app.bot.edit_message_text( + chat_id=session['chat_id'], + message_id=telegram_msg_id, + text=text[:4000], + reply_markup=keyboard, + parse_mode='HTML' + ) + else: + try: + await self.app.bot.delete_message( + chat_id=session['chat_id'], + message_id=session['wait_msg_id'] + ) + except: + pass + + sent = await self.app.bot.send_message( + chat_id=session['chat_id'], + text=text[:4000], + reply_markup=keyboard, + parse_mode='HTML' + ) + + self.pyrogram_to_telegram[message.id] = sent.message_id + self.telegram_to_pyrogram[sent.message_id] = message.id + + # 保存到缓存 + if self.cache_db and session.get('keyword'): + buttons = self.extract_buttons(message) + self.cache_db.save_cache( + session['command'], + session['keyword'], + 1, # 第一页 + text, + text, + buttons + ) + + # 后台自动翻页(用户无感知) + if self.pagination_manager: + asyncio.create_task( + self.pagination_manager.start_pagination( + user_id, session['command'], session['keyword'], message + ) + ) + + except Exception as e: + logger.error(f"处理搜索响应失败: {e}") + + def convert_keyboard(self, message: PyrogramMessage) -> Optional[InlineKeyboardMarkup]: + """转换键盘""" + if not message.reply_markup or not message.reply_markup.inline_keyboard: + return None + + try: + buttons = [] + for row in message.reply_markup.inline_keyboard: + button_row = [] + for btn in row: + if btn.url: + button_row.append(InlineKeyboardButton(text=btn.text, url=btn.url)) + elif btn.callback_data: + callback_id = f"cb_{time.time():.0f}_{len(self.callback_data_map)}" + self.callback_data_map[callback_id] = (message.id, btn.callback_data) + button_row.append(InlineKeyboardButton(text=btn.text, callback_data=callback_id[:64])) + + if button_row: + buttons.append(button_row) + + return InlineKeyboardMarkup(buttons) if buttons else None + except Exception as e: + logger.error(f"键盘转换失败: {e}") + return None + + def extract_buttons(self, message: PyrogramMessage) -> list: + """提取按钮数据""" + if not message.reply_markup or not message.reply_markup.inline_keyboard: + return [] + + buttons = [] + for row in message.reply_markup.inline_keyboard: + for btn in row: + buttons.append({"text": btn.text, "url": btn.url if btn.url else None}) + return buttons + + async def handle_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """处理按钮点击 - 执行搜索命令""" + query = update.callback_query + data = query.data + user = query.from_user + + await query.answer() + + if data.startswith("cmd_"): + # 解析命令 + parts = data.replace("cmd_", "").split("_", 1) + cmd = parts[0] + keywords = parts[1].replace("_", " ") if len(parts) > 1 else "" + + # 构造完整命令 + command = f"/{cmd} {keywords}" if keywords else f"/{cmd}" + + logger.info(f"[用户 {user.id}] 点击按钮: {command}") + + # 显示执行提示 + await query.message.reply_text(f"🔍 正在执行:{command}\n请稍候...") + + # 转发到搜索bot + try: + await self.pyrogram_client.send_message(self.target_bot_id, command) + + # 记录搜索会话 + self.user_search_sessions[user.id] = { + 'chat_id': query.message.chat_id, + 'wait_msg_id': query.message.message_id + 1, + 'command': f"/{cmd}", + 'keyword': keywords, + 'timestamp': datetime.now() + } + + logger.info(f"[镜像] 已转发: {command}") + + except Exception as e: + logger.error(f"[镜像] 转发失败: {e}") + await query.message.reply_text("❌ 搜索失败,请稍后重试或直接发送命令") + + elif data == "cmd_help": + await query.message.reply_text( + "📖 使用指南:\n\n" + "• /search [关键词] - 按群组名称搜索\n" + "• /text [关键词] - 按消息内容搜索\n" + "• /human [关键词] - 按用户名搜索\n" + "• /topchat - 热门群组目录\n\n" + "💡 或者直接告诉我你想找什么!" + ) + + else: + logger.warning(f"未知callback: {data}") + + + async def handle_admin_reply(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """处理管理员回复""" + reply_to = update.message.reply_to_message + if not reply_to or not reply_to.text: + return + + import re + user_id = None + for line in reply_to.text.split('\n'): + if '🆔' in line or 'ID:' in line: + numbers = re.findall(r'\d+', line) + if numbers: + user_id = int(numbers[0]) + break + + if not user_id: + await update.message.reply_text("❌ 无法识别用户ID") + return + + try: + await context.bot.send_message(chat_id=user_id, text=update.message.text) + await update.message.reply_text(f"✅ 已回复给用户 {user_id}") + except Exception as e: + await update.message.reply_text(f"❌ 回复失败: {str(e)}") + + async def initialize(self): + """初始化机器人""" + try: + logger.info("正在初始化整合机器人...") + + if not await self.setup_pyrogram(): + logger.error("Pyrogram初始化失败") + return False + + builder = Application.builder().token(BOT_TOKEN) + + if os.environ.get('HTTP_PROXY'): + proxy_url = os.environ.get('HTTP_PROXY') + logger.info(f"配置Telegram Bot代理: {proxy_url}") + request = httpx.AsyncClient(proxies={"http://": proxy_url, "https://": proxy_url}, timeout=30.0) + builder = builder.request(request) + + self.app = builder.build() + + self.app.add_handler(CommandHandler("start", self.handle_start)) + self.app.add_handler(CallbackQueryHandler(self.handle_callback)) + self.app.add_handler(MessageHandler(tg_filters.ALL, self.handle_message)) + + logger.info("✅ 整合机器人初始化完成") + return True + + except Exception as e: + logger.error(f"初始化失败: {e}") + return False + + async def run(self): + """运行机器人""" + try: + await self.app.initialize() + await self.app.start() + await self.app.updater.start_polling(drop_pending_updates=True) + + logger.info("="*50) + logger.info("✅ AI增强版Bot已启动") + logger.info(f"AI服务: {MAC_API_URL}") + logger.info(f"缓存功能: {'启用' if self.cache_db else '禁用'}") + logger.info("="*50) + + await asyncio.Event().wait() + + except KeyboardInterrupt: + logger.info("收到停止信号") + finally: + await self.cleanup() + + async def cleanup(self): + """清理资源""" + logger.info("正在清理...") + + if self.app: + await self.app.updater.stop() + await self.app.stop() + await self.app.shutdown() + + if self.pyrogram_client: + await self.pyrogram_client.stop() + + logger.info("✅ 清理完成") + + +async def main(): + """主函数""" + bot = IntegratedBotAI() + + if await bot.initialize(): + await bot.run() + else: + logger.error("初始化失败,退出") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/login_agent.py b/login_agent.py new file mode 100644 index 0000000..20b0e61 --- /dev/null +++ b/login_agent.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import asyncio +from claude_agent_sdk import ClaudeSDKClient + +async def login(): + print('🔐 使用提供的 API key 登录...') + + try: + client = ClaudeSDKClient() + await client.connect() + + # 尝试使用 /login 命令 + # 根据错误信息,需要运行 /login + await client.query('/login cr_9792f20a98f055e204248a41f280780ca2fb8f08f35e60c785e5245653937e06') + + print('✅ 登录命令已发送') + + # 接收响应 + async for chunk in client.receive_response(): + print(f'📝 响应: {chunk}') + + await client.disconnect() + + except Exception as e: + print(f'❌ 登录失败: {e}') + import traceback + traceback.print_exc() + +if __name__ == '__main__': + asyncio.run(login()) diff --git a/login_claude.sh b/login_claude.sh new file mode 100755 index 0000000..6101c2a --- /dev/null +++ b/login_claude.sh @@ -0,0 +1,4 @@ +#!/bin/bash +export ANTHROPIC_API_KEY=$(cat ~/.bashrc | grep ANTHROPIC_AUTH_TOKEN | cut -d"'" -f2) +echo "API Key: ${ANTHROPIC_API_KEY:0:20}..." +claude login --api-key "$ANTHROPIC_API_KEY" diff --git a/main.py b/main.py new file mode 100644 index 0000000..4a01d08 --- /dev/null +++ b/main.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +"""客服机器人主程序""" +import sys +import os +from pathlib import Path + +# 添加项目路径 +sys.path.insert(0, str(Path(__file__).parent)) + +from src.core.bot import CustomerServiceBot +from src.config.settings import Settings +from src.utils.logger import get_logger + + +logger = get_logger(__name__) + + +def main(): + """主函数""" + try: + # 加载配置 + config = Settings.from_env() + + # 创建并运行机器人 + bot = CustomerServiceBot(config) + logger.info(f"Starting Customer Service Bot v{config.version}") + bot.run() + + except KeyboardInterrupt: + logger.info("Bot stopped by user") + sys.exit(0) + except Exception as e: + logger.error(f"Bot failed to start: {e}") + sys.exit(1) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/manage_bot.sh b/manage_bot.sh new file mode 100755 index 0000000..eab7af7 --- /dev/null +++ b/manage_bot.sh @@ -0,0 +1,163 @@ +#!/bin/bash +# Telegram Bot 管理脚本 +# 统一管理 integrated_bot_ai.py (使用 claude-agent-sdk) + +set -euo pipefail + +BOT_DIR="/home/atai/telegram-bot" +BOT_SCRIPT="integrated_bot_ai.py" +SCREEN_NAME="agent_bot" +LOG_FILE="bot_agent_sdk.log" +SYSTEMD_UNIT="telegram-bot.service" +SYSTEMD_UNIT_PATH="/etc/systemd/system/${SYSTEMD_UNIT}" + +# 环境变量(非 systemd 模式时使用) +export ANTHROPIC_BASE_URL="http://202.79.167.23:3000/api" +export ANTHROPIC_AUTH_TOKEN="cr_9792f20a98f055e204248a41f280780ca2fb8f08f35e60c785e5245653937e06" +export ALL_PROXY="socks5://127.0.0.1:1080" + +cd "$BOT_DIR" || exit 1 + +if [[ -f "$SYSTEMD_UNIT_PATH" ]]; then + USE_SYSTEMD=1 +else + USE_SYSTEMD=0 +fi + +systemd_active() { + sudo systemctl is-active --quiet "$SYSTEMD_UNIT" +} + +start_bot_legacy() { + if screen -list | grep -q "$SCREEN_NAME"; then + echo "⚠️ Bot 已经在运行中" + screen -ls | grep "$SCREEN_NAME" + else + screen -dmS "$SCREEN_NAME" bash -lc "cd $BOT_DIR && exec ./run_bot_loop.sh" + sleep 2 + if screen -list | grep -q "$SCREEN_NAME"; then + echo "✅ Bot 已启动" + screen -ls | grep "$SCREEN_NAME" + echo "" + echo "📝 查看日志: tail -f $BOT_DIR/$LOG_FILE" + else + echo "❌ 启动失败,请检查日志" + fi + fi +} + +stop_bot_legacy() { + if screen -list | grep -q "$SCREEN_NAME"; then + screen -S "$SCREEN_NAME" -X quit + sleep 1 + echo "✅ Bot 已停止" + else + echo "⚠️ Bot 没有运行" + fi +} + +case "${1:-status}" in + start) + echo "🚀 启动 Telegram Bot..." + if [[ $USE_SYSTEMD -eq 1 ]]; then + if systemd_active; then + echo "⚠️ systemd 服务已在运行" + else + sudo systemctl start "$SYSTEMD_UNIT" + sleep 2 + if systemd_active; then + echo "✅ systemd 已启动机器人" + else + echo "❌ 启动失败,请查看: sudo journalctl -u $SYSTEMD_UNIT" + fi + fi + else + start_bot_legacy + fi + ;; + stop) + echo "🛑 停止 Telegram Bot..." + if [[ $USE_SYSTEMD -eq 1 ]]; then + if systemd_active; then + sudo systemctl stop "$SYSTEMD_UNIT" + echo "✅ systemd 已停止机器人" + else + echo "⚠️ systemd 服务未运行" + fi + else + stop_bot_legacy + fi + ;; + restart) + echo "🔄 重启 Telegram Bot..." + if [[ $USE_SYSTEMD -eq 1 ]]; then + sudo systemctl restart "$SYSTEMD_UNIT" + sleep 2 + if systemd_active; then + echo "✅ systemd 重启完成" + else + echo "❌ 重启失败,请检查 systemd 日志" + fi + else + stop_bot_legacy + sleep 2 + start_bot_legacy + fi + ;; + status) + echo "📊 Bot 状态检查..." + echo "" + if [[ $USE_SYSTEMD -eq 1 ]]; then + sudo systemctl status "$SYSTEMD_UNIT" --no-pager + echo "" + fi + if screen -list | grep -q "$SCREEN_NAME"; then + echo "✅ Screen 会话运行中" + screen -ls | grep "$SCREEN_NAME" + else + echo "❌ 未检测到 Screen 会话" + fi + echo "" + echo "最近日志:" + tail -20 "$BOT_DIR/$LOG_FILE" || echo "暂无日志" + ;; + logs) + echo "📝 实时日志 (Ctrl+C 退出)..." + tail -f "$BOT_DIR/$LOG_FILE" + ;; + attach) + echo "🔗 进入 Bot Screen 会话 (Ctrl+A, D 退出)..." + screen -r "$SCREEN_NAME" + ;; + info) + echo "ℹ️ Bot 信息" + echo "============================================" + echo "脚本: $BOT_SCRIPT" + echo "位置: $BOT_DIR" + echo "日志: $LOG_FILE" + echo "Screen: $SCREEN_NAME" + if [[ $USE_SYSTEMD -eq 1 ]]; then + echo "服务: $SYSTEMD_UNIT (systemd)" + else + echo "服务: Screen 模式" + fi + echo "使用: claude-agent-sdk (Python)" + echo "模型: claude-sonnet-4-5-20250929" + echo "============================================" + ;; + *) + echo "Telegram Bot 管理脚本" + echo "" + echo "用法: $0 {start|stop|restart|status|logs|attach|info}" + echo "" + echo "命令说明:" + echo " start - 启动 Bot" + echo " stop - 停止 Bot" + echo " restart - 重启 Bot" + echo " status - 查看运行状态" + echo " logs - 实时查看日志" + echo " attach - 进入 Screen 会话" + echo " info - 显示 Bot 信息" + exit 1 + ;; +esac diff --git a/modules/ai_analyzer.py b/modules/ai_analyzer.py new file mode 100644 index 0000000..6bac270 --- /dev/null +++ b/modules/ai_analyzer.py @@ -0,0 +1,51 @@ +"""AI意图分析模块""" +import json +import re +import logging +from typing import Dict + +logger = logging.getLogger(__name__) + +class AIAnalyzer: + def __init__(self, claude_client): + self.claude_client = claude_client + self.model = "claude-sonnet-4-20250514" + + async def analyze_intent(self, user_input: str) -> Dict: + prompt = f"""分析Telegram群组搜索需求,生成3-5个搜索建议。 +用户输入:"{user_input}" +可用命令:/search /text /human /topchat +返回JSON:{{"explanation":"说明","suggestions":[{{"command":"/text","keyword":"关键词","description":"描述","icon":"💬"}}]}}""" + + try: + response = self.claude_client.messages.create( + model=self.model, + max_tokens=1000, + messages=[{"role": "user", "content": prompt}] + ) + + ai_response = response.content[0].text.strip() + json_match = re.search(r'```json\s*(.*?)\s*```', ai_response, re.DOTALL) + if json_match: + ai_response = json_match.group(1) + + analysis = json.loads(ai_response) + return self._validate(analysis, user_input) + except Exception as e: + logger.error(f"AI分析失败: {e}") + return self._fallback(user_input) + + def _validate(self, analysis, user_input): + if 'suggestions' not in analysis: + raise ValueError("缺少suggestions") + return analysis + + def _fallback(self, user_input): + return { + "explanation": f"为您搜索「{user_input}」", + "suggestions": [ + {"command": "/search", "keyword": user_input, "description": f"按名称:{user_input}", "icon": "🔍"}, + {"command": "/text", "keyword": user_input, "description": f"按内容:{user_input}", "icon": "💬"}, + {"command": "/topchat", "keyword": "", "description": "浏览热门", "icon": "🔥"} + ] + } diff --git a/modules/session_manager.py b/modules/session_manager.py new file mode 100644 index 0000000..6127d49 --- /dev/null +++ b/modules/session_manager.py @@ -0,0 +1,160 @@ +"""会话管理模块 - 管理用户交互状态和历史""" +from __future__ import annotations + +from datetime import datetime, timedelta +from typing import Dict, Optional, Any +import logging + +logger = logging.getLogger(__name__) + + +class SessionManager: + """用户会话管理器""" + + def __init__(self, timeout_minutes: int = 30) -> None: + self.sessions: Dict[int, Dict[str, Any]] = {} + self.session_timeout = timedelta(minutes=timeout_minutes) + + def _now(self) -> datetime: + return datetime.now() + + def create_session(self, user_id: int, initial_query: str) -> Dict[str, Any]: + """创建新会话""" + session = { + "user_id": user_id, + "stage": "initial", + "initial_query": initial_query, + "history": [ + { + "step": "input", + "content": initial_query, + "timestamp": self._now(), + } + ], + "analysis": None, + "selected_suggestion": None, + "search_results": None, + "can_go_back": False, + "created_at": self._now(), + "last_activity": self._now(), + } + self.sessions[user_id] = session + logger.info("[会话] 创建新会话: user=%s, query=%s", user_id, initial_query) + return session + + def get_session(self, user_id: int) -> Optional[Dict[str, Any]]: + """获取会话,包含过期检查""" + session = self.sessions.get(user_id) + if not session: + return None + + if self._now() - session.get("last_activity", self._now()) > self.session_timeout: + logger.info("[会话] 会话已过期: user=%s", user_id) + self.sessions.pop(user_id, None) + return None + + session["last_activity"] = self._now() + return session + + def update_stage(self, user_id: int, stage: str, **kwargs: Any) -> Optional[Dict[str, Any]]: + """更新会话阶段并记录历史""" + session = self.get_session(user_id) + if not session: + return None + + session["stage"] = stage + session["last_activity"] = self._now() + + history_entry = { + "step": stage, + "timestamp": self._now(), + } + history_entry.update(kwargs) + session.setdefault("history", []).append(history_entry) + + for key, value in kwargs.items(): + session[key] = value + + logger.info("[会话] 更新阶段: user=%s, stage=%s", user_id, stage) + return session + + def save_analysis(self, user_id: int, analysis: Dict[str, Any]) -> None: + """保存AI分析结果""" + session = self.get_session(user_id) + if not session: + return + + session["analysis"] = analysis + session["stage"] = "suggestions" + session["can_go_back"] = True + + suggestions = analysis.get("suggestions", []) + logger.info("[会话] 保存分析: user=%s, suggestions=%s", user_id, len(suggestions)) + + def save_selection(self, user_id: int, suggestion_index: int) -> Optional[Dict[str, Any]]: + """保存用户选择的建议""" + session = self.get_session(user_id) + if not session: + return None + + analysis = session.get("analysis") or {} + suggestions = analysis.get("suggestions", []) + if 0 <= suggestion_index < len(suggestions): + selection = suggestions[suggestion_index] + session["selected_suggestion"] = selection + session["stage"] = "searching" + session.setdefault("history", []).append( + { + "step": "selection", + "timestamp": self._now(), + "selection": selection, + } + ) + logger.info( + "[会话] 保存选择: user=%s, index=%s", user_id, suggestion_index + ) + return selection + logger.warning( + "[会话] 选择索引无效: user=%s, index=%s, total=%s", + user_id, + suggestion_index, + len(suggestions), + ) + return None + + def can_go_back(self, user_id: int) -> bool: + session = self.get_session(user_id) + return bool(session and session.get("can_go_back", False)) + + def go_back_to_suggestions(self, user_id: int) -> Optional[Dict[str, Any]]: + """返回到建议阶段""" + session = self.get_session(user_id) + if not session: + return None + + analysis = session.get("analysis") + if not analysis: + return None + + session["stage"] = "suggestions" + session["selected_suggestion"] = None + logger.info("[会话] 返回建议列表: user=%s", user_id) + return analysis + + def clear_session(self, user_id: int) -> None: + """清除会话""" + if user_id in self.sessions: + self.sessions.pop(user_id, None) + logger.info("[会话] 清除会话: user=%s", user_id) + + def get_stats(self) -> Dict[str, Any]: + """获取会话统计信息""" + stage_counter: Dict[str, int] = {} + for session in self.sessions.values(): + stage_name = session.get("stage", "unknown") + stage_counter[stage_name] = stage_counter.get(stage_name, 0) + 1 + + return { + "active_sessions": len(self.sessions), + "stages": stage_counter, + } diff --git a/monitor.sh b/monitor.sh new file mode 100755 index 0000000..365e685 --- /dev/null +++ b/monitor.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +echo "📡 监控 Telegram 机器人..." +echo "================================" +echo "按 Ctrl+C 退出" +echo "" + +tail -f bot.out | grep -E "(from user|forward|reply|消息|客户|管理员|ERROR|WARNING)" \ No newline at end of file diff --git a/monitor_session.sh b/monitor_session.sh new file mode 100755 index 0000000..a55f010 --- /dev/null +++ b/monitor_session.sh @@ -0,0 +1,45 @@ +#\!/bin/bash +# Session 监控脚本 - 检查 session 健康状态 + +LOG_FILE="logs/session_monitor.log" +SESSION_FILE="user_session.session" + +log() { + echo "[$(date "+%Y-%m-%d %H:%M:%S")] $1" | tee -a "$LOG_FILE" +} + +# 检查 session 文件 +if [ \! -f "$SESSION_FILE" ]; then + log "❌ 错误: $SESSION_FILE 不存在!" + log "尝试运行恢复..." + ./protect_session.sh + exit 1 +fi + +# 检查文件大小 +SIZE=$(stat -f%z "$SESSION_FILE" 2>/dev/null || stat -c%s "$SESSION_FILE" 2>/dev/null) +if [ "$SIZE" -lt 1000 ]; then + log "⚠️ 警告: Session 文件太小 (${SIZE} bytes)" +fi + +# 检查最近的错误日志 +AUTH_ERRORS=$(tail -100 logs/integrated_bot_errors.log 2>/dev/null | grep -c "AUTH_KEY_UNREGISTERED") +if [ "$AUTH_ERRORS" -gt 0 ]; then + log "⚠️ 警告: 发现 $AUTH_ERRORS 个 AUTH_KEY 错误(可能是旧错误)" + + # 检查是否是最近5分钟的错误 + RECENT_ERRORS=$(tail -50 logs/integrated_bot_errors.log 2>/dev/null | grep "AUTH_KEY_UNREGISTERED" | wc -l) + if [ "$RECENT_ERRORS" -gt 0 ]; then + log "❌ 严重: 检测到最近的 AUTH_KEY 错误!" + log "建议立即检查 session 文件" + fi +fi + +# 检查 Pyrogram 是否运行 +if grep -q "✅ Pyrogram客户端已启动" logs/integrated_bot_detailed.log 2>/dev/null; then + log "✅ Pyrogram 客户端正常运行" +else + log "⚠️ 警告: Pyrogram 客户端状态未知" +fi + +log "Session 监控完成" diff --git a/protect_session.sh b/protect_session.sh new file mode 100755 index 0000000..b4ba8fd --- /dev/null +++ b/protect_session.sh @@ -0,0 +1,43 @@ +#\!/bin/bash +# Session 文件保护脚本 + +SESSION_FILE="user_session.session" +BACKUP_DIR="session_backups" + +# 创建备份目录 +mkdir -p "$BACKUP_DIR" + +# 检查 session 文件 +if [ \! -f "$SESSION_FILE" ]; then + echo "❌ 警告: $SESSION_FILE 不存在!" + + # 尝试从备份恢复 + LATEST_BACKUP=$(ls -t "$BACKUP_DIR"/*.session 2>/dev/null | head -1) + if [ -n "$LATEST_BACKUP" ]; then + echo "尝试从备份恢复: $LATEST_BACKUP" + cp "$LATEST_BACKUP" "$SESSION_FILE" + cp "${LATEST_BACKUP}-journal" "${SESSION_FILE}-journal" 2>/dev/null + echo "✅ 已从备份恢复" + else + echo "❌ 没有可用的备份" + exit 1 + fi +else + # 文件存在,创建备份 + TIMESTAMP=$(date +%Y%m%d_%H%M%S) + cp "$SESSION_FILE" "$BACKUP_DIR/user_session_${TIMESTAMP}.session" + if [ -f "${SESSION_FILE}-journal" ]; then + cp "${SESSION_FILE}-journal" "$BACKUP_DIR/user_session_${TIMESTAMP}.session-journal" + fi + + # 只保留最近7天的备份 + find "$BACKUP_DIR" -name "*.session*" -mtime +7 -delete + + echo "✅ Session 备份成功: $TIMESTAMP" +fi + +# 确保权限正确 +chmod 600 "$SESSION_FILE"* 2>/dev/null + +# 设置为不可修改(可选,需要 root) +# chattr +i "$SESSION_FILE" 2>/dev/null diff --git a/qr_login.py b/qr_login.py new file mode 100644 index 0000000..130bb79 --- /dev/null +++ b/qr_login.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +from pyrogram import Client +import asyncio +import qrcode +import io + +API_ID = 24660516 +API_HASH = "eae564578880a59c9963916ff1bbbd3a" + +proxy = { + "scheme": "socks5", + "hostname": "127.0.0.1", + "port": 1080 +} + +async def qr_login(): + app = Client( + "user_session", + api_id=API_ID, + api_hash=API_HASH, + proxy=proxy + ) + + @app.on_login_token() + async def on_token(client, token): + # 生成二维码URL + url = f"tg://login?token={token}" + print(f"\n扫描二维码登录:") + print(f"URL: {url}") + + # 生成二维码 + qr = qrcode.QRCode() + qr.add_data(url) + qr.make() + qr.print_ascii() + + print("\n请使用Telegram APP扫描上方二维码") + return True + + await app.start() + me = await app.get_me() + print(f"\n✅ 登录成功!") + print(f"账号:{me.first_name}") + print(f"ID:{me.id}") + await app.stop() + +print("正在生成二维码...") +asyncio.run(qr_login()) diff --git a/quick_deploy.txt b/quick_deploy.txt new file mode 100644 index 0000000..573492d --- /dev/null +++ b/quick_deploy.txt @@ -0,0 +1,69 @@ +======================================== +快速部署指南 - 在虚拟机上运行机器人 +======================================== + +第一步:连接到虚拟机 +---------------------- +ssh atai@192.168.9.159 +密码: wengewudi666808 + +第二步:一键部署(复制粘贴以下命令) +------------------------------------ +wget -O deploy.sh https://raw.githubusercontent.com/woshiqp465/newbot925/main/deploy.sh && chmod +x deploy.sh && ./deploy.sh + +或者手动执行: +------------------------------------ +# 1. 克隆项目 +git clone https://github.com/woshiqp465/newbot925.git +cd newbot925 + +# 2. 安装依赖 +pip3 install -r requirements.txt + +# 3. 配置环境变量 +cp .env.example .env +nano .env +# 编辑以下内容: +# BOT_TOKEN=8426529617:AAHAxzohSMFBAxInzbAVJsZfkB5bHnOyFC4 +# ADMIN_ID=7363537082 + +# 4. 复制session文件(从本地电脑) +# 在本地电脑执行: +scp mirror_session.session* atai@192.168.9.159:~/newbot925/ + +# 5. 使用screen运行(保持后台运行) +screen -S telegram_bot +python3 integrated_bot.py + +# 按 Ctrl+A 然后按 D 退出screen(程序继续运行) + +第三步:管理机器人 +------------------ +# 查看运行状态 +screen -r telegram_bot + +# 查看所有screen会话 +screen -ls + +# 停止机器人 +screen -X -S telegram_bot quit + +# 重新启动 +screen -dmS telegram_bot python3 integrated_bot.py + +======================================== +优点说明: +======================================== +✅ 24/7运行:虚拟机一直开着,机器人不会停 +✅ 独立运行:不占用你的电脑资源 +✅ 远程管理:随时SSH连接查看状态 +✅ 自动重连:screen会保持进程运行 +✅ 断线续传:即使SSH断开,程序继续运行 + +======================================== +注意事项: +======================================== +1. 确保虚拟机有Python 3.9+ +2. 确保虚拟机能访问Telegram API +3. session文件需要从本地复制过去 +4. 定期检查运行状态 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..0fe720a --- /dev/null +++ b/requirements.txt @@ -0,0 +1,23 @@ +# Core dependencies +python-telegram-bot==20.7 +python-dotenv==1.0.0 +pytz==2024.1 +aiohttp==3.9.1 + +# Database +aiosqlite==0.19.0 + +# Utilities +aiofiles==23.2.1 +pydantic==2.5.3 + +# Development +pytest==7.4.4 +pytest-asyncio==0.23.3 +black==24.1.0 +flake8==7.0.0 +mypy==1.8.0 + +# Optional (for production) +redis==5.0.1 +uvloop==0.19.0 \ No newline at end of file diff --git a/run.sh b/run.sh new file mode 100755 index 0000000..2174d3d --- /dev/null +++ b/run.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +# 客服机器人启动脚本 + +echo "🤖 Starting Telegram Customer Service Bot..." +echo "================================" + +# 进入项目目录 +cd /Users/lucas/telegram-customer-bot + +# 检查Python +if ! command -v python3 &> /dev/null; then + echo "❌ Python3 is not installed" + exit 1 +fi + +# 检查环境文件 +if [ ! -f .env ]; then + echo "❌ .env file not found" + echo "Please copy .env.example to .env and configure it" + exit 1 +fi + +# 创建必要的目录 +mkdir -p logs data + +# 启动机器人 +echo "📡 Connecting to Telegram..." +python3 main.py + +echo "✅ Bot stopped" \ No newline at end of file diff --git a/run_bot_loop.sh b/run_bot_loop.sh new file mode 100755 index 0000000..7da519e --- /dev/null +++ b/run_bot_loop.sh @@ -0,0 +1,15 @@ +#!/bin/bash +set -euo pipefail + +cd "$(dirname "$0")" || exit 1 +LOG_FILE="bot_agent_sdk.log" +mkdir -p logs +LOG_PATH="$(pwd)/$LOG_FILE" + +while true; do + echo "[$(date '+%Y-%m-%d %H:%M:%S')] ▶️ 启动客服机器人" | tee -a "$LOG_PATH" + python3 -u integrated_bot_ai.py 2>&1 | tee -a "$LOG_PATH" + exit_code=${PIPESTATUS[0]} + echo "[$(date '+%Y-%m-%d %H:%M:%S')] ⚠️ 机器人退出,状态码 $exit_code,5 秒后重启" | tee -a "$LOG_PATH" + sleep 5 +done diff --git a/run_session_creator.sh b/run_session_creator.sh new file mode 100755 index 0000000..b51340f --- /dev/null +++ b/run_session_creator.sh @@ -0,0 +1,45 @@ +#\!/bin/bash +# Session 创建运行脚本 + +echo "========================================" +echo "Pyrogram Session 创建工具" +echo "========================================" +echo "" +echo "此脚本将帮助您创建新的 session 文件" +echo "请确保:" +echo " 1. Telegram 应用已打开" +echo " 2. 准备接收验证码" +echo "" +echo "电话号码: +66621394851" +echo "" +read -p "按 Enter 继续..." dummy + +echo "" +echo "正在启动创建过程..." +echo "" + +# 使用 printf 提供电话号码,但验证码需要手动输入 +printf "+66621394851\ny\n" | timeout 60 python3 -u create_session_correct.py || { + echo "" + echo "首次尝试可能因为需要验证码而超时..." + echo "让我们再试一次,这次请准备好验证码" + echo "" + + # 第二次尝试,允许手动输入验证码 + python3 create_session_correct.py +} + +if [ -f "user_session.session" ]; then + echo "" + echo "========================================" + echo "✅ Session 文件创建成功!" + echo "========================================" + echo "文件: user_session.session" + ls -lh user_session.session* + echo "" + echo "现在可以重启机器人了" +else + echo "" + echo "❌ Session 文件未创建" + echo "请检查错误信息" +fi diff --git a/smart_health_check.sh b/smart_health_check.sh new file mode 100755 index 0000000..d09395a --- /dev/null +++ b/smart_health_check.sh @@ -0,0 +1,95 @@ +#\!/bin/bash +# 智能健康检查 - 只在有问题时干预 +# 每12小时运行一次 + +LOG_FILE="logs/smart_health_check.log" +ERROR_LOG="logs/integrated_bot_errors.log" +DETAIL_LOG="logs/integrated_bot_detailed.log" + +log() { + echo "[$(date "+%Y-%m-%d %H:%M:%S")] $1" | tee -a "$LOG_FILE" +} + +log "==========================================" +log "开始健康检查..." + +# 1. 检查机器人进程是否运行 +if \! pgrep -f "python.*integrated_bot_ai.py" >/dev/null; then + log "❌ 机器人进程未运行,需要启动" + ./manage_bot.sh restart >> "$LOG_FILE" 2>&1 + log "✅ 已重启机器人" + exit 0 +fi + +# 2. 检查 Pyrogram 客户端状态 +PYROGRAM_RUNNING=$(tail -100 "$DETAIL_LOG" 2>/dev/null | grep -c "✅ Pyrogram客户端已启动") +if [ "$PYROGRAM_RUNNING" -gt 0 ]; then + log "✅ Pyrogram 客户端状态: 正常" + PYROGRAM_OK=true +else + log "⚠️ Pyrogram 客户端状态: 未知" + PYROGRAM_OK=false +fi + +# 3. 检查最近1小时是否有 Connection lost 错误 +HOUR_AGO=$(date -d "1 hour ago" "+%Y-%m-%d %H" 2>/dev/null || date -v-1H "+%Y-%m-%d %H") +RECENT_CONNECTION_ERRORS=$(tail -200 "$ERROR_LOG" 2>/dev/null | grep "Connection lost" | grep "$HOUR_AGO" | wc -l) + +log "最近1小时 Connection lost 错误: $RECENT_CONNECTION_ERRORS 个" + +# 4. 检查最近1小时是否有 AUTH_KEY 错误 +RECENT_AUTH_ERRORS=$(tail -200 "$ERROR_LOG" 2>/dev/null | grep "AUTH_KEY_UNREGISTERED" | grep "$HOUR_AGO" | wc -l) + +log "最近1小时 AUTH_KEY 错误: $RECENT_AUTH_ERRORS 个" + +# 5. 决策逻辑 +NEED_RESTART=false + +if [ "$RECENT_CONNECTION_ERRORS" -gt 5 ]; then + log "❌ 检测到过多连接错误 ($RECENT_CONNECTION_ERRORS 个)" + NEED_RESTART=true +fi + +if [ "$RECENT_AUTH_ERRORS" -gt 0 ]; then + log "❌ 检测到 AUTH_KEY 错误" + NEED_RESTART=true +fi + +if [ "$PYROGRAM_OK" = false ] && [ "$RECENT_CONNECTION_ERRORS" -gt 2 ]; then + log "❌ Pyrogram 状态异常且有连接错误" + NEED_RESTART=true +fi + +# 6. 执行操作 +if [ "$NEED_RESTART" = true ]; then + log "==========================================", + log "🔄 检测到问题,准备重启机器人..." + log "==========================================", + + # 重启 + ./manage_bot.sh restart >> "$LOG_FILE" 2>&1 + + # 等待启动 + sleep 10 + + # 验证 + if grep -q "✅ Pyrogram客户端已启动" "$DETAIL_LOG" 2>/dev/null; then + log "✅ 重启成功 - Pyrogram 已重新连接" + else + log "⚠️ 重启完成 - 状态待确认" + fi + + log "==========================================" +else + log "✅ 一切正常,无需干预" + log "==========================================" +fi + +# 7. 状态摘要 +log "状态摘要:" +log " - 进程: 运行中" +log " - Pyrogram: $([ "$PYROGRAM_OK" = true ] && echo "正常" || echo "未知")" +log " - 连接错误: $RECENT_CONNECTION_ERRORS 个" +log " - AUTH错误: $RECENT_AUTH_ERRORS 个" +log " - 操作: $([ "$NEED_RESTART" = true ] && echo "已重启" || echo "无操作")" +log "" diff --git a/src/config/__init__.py b/src/config/__init__.py new file mode 100644 index 0000000..4c0c31d --- /dev/null +++ b/src/config/__init__.py @@ -0,0 +1,5 @@ +"""配置管理模块""" +from .settings import Settings +from .loader import ConfigLoader + +__all__ = ['Settings', 'ConfigLoader'] \ No newline at end of file diff --git a/src/config/loader.py b/src/config/loader.py new file mode 100644 index 0000000..24049d8 --- /dev/null +++ b/src/config/loader.py @@ -0,0 +1,155 @@ +"""配置加载器""" +import os +from typing import Any, Dict +from pathlib import Path +from dotenv import load_dotenv +from .settings import ( + Settings, TelegramConfig, DatabaseConfig, + LoggingConfig, BusinessConfig, SecurityConfig, FeatureFlags +) + + +class ConfigLoader: + """配置加载器""" + + @staticmethod + def load_env_file(env_path: str = None) -> None: + """加载环境变量文件""" + if env_path: + load_dotenv(env_path) + else: + # 查找 .env 文件 + current_dir = Path.cwd() + env_file = current_dir / ".env" + if env_file.exists(): + load_dotenv(env_file) + else: + # 向上查找 + for parent in current_dir.parents: + env_file = parent / ".env" + if env_file.exists(): + load_dotenv(env_file) + break + + @staticmethod + def get_env(key: str, default: Any = None, cast_type: type = str) -> Any: + """获取环境变量并转换类型""" + value = os.getenv(key, default) + if value is None: + return None + + if cast_type == bool: + if isinstance(value, bool): + return value + if isinstance(value, str): + return value.lower() in ('true', '1', 'yes', 'on') + return bool(value) + elif cast_type == int: + return int(value) + elif cast_type == float: + return float(value) + else: + return value + + @classmethod + def load_from_env(cls) -> Settings: + """从环境变量加载配置""" + cls.load_env_file() + + # Telegram 配置 + telegram_config = TelegramConfig( + bot_token=cls.get_env('BOT_TOKEN', ''), + admin_id=cls.get_env('ADMIN_ID', 0, int), + admin_username=cls.get_env('ADMIN_USERNAME', ''), + bot_name=cls.get_env('BOT_NAME', 'Customer Service Bot') + ) + + # 数据库配置 + database_config = DatabaseConfig( + type=cls.get_env('DATABASE_TYPE', 'sqlite'), + path=cls.get_env('DATABASE_PATH', './data/bot.db'), + host=cls.get_env('DATABASE_HOST'), + port=cls.get_env('DATABASE_PORT', cast_type=int), + user=cls.get_env('DATABASE_USER'), + password=cls.get_env('DATABASE_PASSWORD'), + database=cls.get_env('DATABASE_NAME') + ) + + # 日志配置 + logging_config = LoggingConfig( + level=cls.get_env('LOG_LEVEL', 'INFO'), + file=cls.get_env('LOG_FILE', './logs/bot.log'), + max_size=cls.get_env('LOG_MAX_SIZE', 10485760, int), + backup_count=cls.get_env('LOG_BACKUP_COUNT', 5, int) + ) + + # 业务配置 + business_config = BusinessConfig( + business_hours_start=cls.get_env('BUSINESS_HOURS_START', '09:00'), + business_hours_end=cls.get_env('BUSINESS_HOURS_END', '18:00'), + timezone=cls.get_env('TIMEZONE', 'Asia/Shanghai'), + auto_reply_delay=cls.get_env('AUTO_REPLY_DELAY', 1, int), + welcome_message=cls.get_env('WELCOME_MESSAGE', + '您好!我是客服助手,正在为您转接人工客服,请稍候...'), + offline_message=cls.get_env('OFFLINE_MESSAGE', + '非常抱歉,现在是非工作时间。我们的工作时间是 {start} - {end}。您的消息已记录,我们会在工作时间尽快回复您。') + ) + + # 安全配置 + security_config = SecurityConfig( + max_messages_per_minute=cls.get_env('MAX_MESSAGES_PER_MINUTE', 30, int), + session_timeout=cls.get_env('SESSION_TIMEOUT', 3600, int), + enable_encryption=cls.get_env('ENABLE_ENCRYPTION', False, bool), + blocked_words=cls.get_env('BLOCKED_WORDS', '').split(',') if cls.get_env('BLOCKED_WORDS') else [] + ) + + # 功能开关 + feature_flags = FeatureFlags( + enable_auto_reply=cls.get_env('ENABLE_AUTO_REPLY', True, bool), + enable_statistics=cls.get_env('ENABLE_STATISTICS', True, bool), + enable_customer_history=cls.get_env('ENABLE_CUSTOMER_HISTORY', True, bool), + enable_multi_admin=cls.get_env('ENABLE_MULTI_ADMIN', False, bool), + enable_file_transfer=cls.get_env('ENABLE_FILE_TRANSFER', True, bool), + enable_voice_message=cls.get_env('ENABLE_VOICE_MESSAGE', True, bool), + enable_location_sharing=cls.get_env('ENABLE_LOCATION_SHARING', False, bool) + ) + + # 创建设置对象 + settings = Settings( + telegram=telegram_config, + database=database_config, + logging=logging_config, + business=business_config, + security=security_config, + features=feature_flags, + debug=cls.get_env('DEBUG', False, bool), + testing=cls.get_env('TESTING', False, bool), + version=cls.get_env('VERSION', '1.0.0') + ) + + # 验证配置 + settings.validate() + return settings + + @classmethod + def load_from_dict(cls, config_dict: Dict[str, Any]) -> Settings: + """从字典加载配置""" + telegram_config = TelegramConfig(**config_dict.get('telegram', {})) + database_config = DatabaseConfig(**config_dict.get('database', {})) + logging_config = LoggingConfig(**config_dict.get('logging', {})) + business_config = BusinessConfig(**config_dict.get('business', {})) + security_config = SecurityConfig(**config_dict.get('security', {})) + feature_flags = FeatureFlags(**config_dict.get('features', {})) + + settings = Settings( + telegram=telegram_config, + database=database_config, + logging=logging_config, + business=business_config, + security=security_config, + features=feature_flags, + **config_dict.get('runtime', {}) + ) + + settings.validate() + return settings \ No newline at end of file diff --git a/src/config/settings.py b/src/config/settings.py new file mode 100644 index 0000000..52c1917 --- /dev/null +++ b/src/config/settings.py @@ -0,0 +1,140 @@ +"""系统配置定义""" +from dataclasses import dataclass, field +from typing import Optional, List +from datetime import time +import os + + +@dataclass +class TelegramConfig: + """Telegram 相关配置""" + bot_token: str + admin_id: int + admin_username: str + bot_name: str = "Customer Service Bot" + + def __post_init__(self): + if not self.bot_token: + raise ValueError("Bot token is required") + if not self.admin_id: + raise ValueError("Admin ID is required") + + +@dataclass +class DatabaseConfig: + """数据库配置""" + type: str = "sqlite" + path: str = "./data/bot.db" + host: Optional[str] = None + port: Optional[int] = None + user: Optional[str] = None + password: Optional[str] = None + database: Optional[str] = None + + def get_connection_string(self) -> str: + """获取数据库连接字符串""" + if self.type == "sqlite": + return f"sqlite:///{self.path}" + elif self.type == "postgresql": + return f"postgresql://{self.user}:{self.password}@{self.host}:{self.port}/{self.database}" + elif self.type == "mysql": + return f"mysql://{self.user}:{self.password}@{self.host}:{self.port}/{self.database}" + else: + raise ValueError(f"Unsupported database type: {self.type}") + + +@dataclass +class LoggingConfig: + """日志配置""" + level: str = "INFO" + file: str = "./logs/bot.log" + max_size: int = 10485760 # 10MB + backup_count: int = 5 + format: str = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" + + def __post_init__(self): + # 确保日志目录存在 + log_dir = os.path.dirname(self.file) + if log_dir and not os.path.exists(log_dir): + os.makedirs(log_dir, exist_ok=True) + + +@dataclass +class BusinessConfig: + """业务配置""" + business_hours_start: str = "09:00" + business_hours_end: str = "18:00" + timezone: str = "Asia/Shanghai" + auto_reply_delay: int = 1 # 秒 + welcome_message: str = "您好!我是客服助手,正在为您转接人工客服,请稍候..." + offline_message: str = "非常抱歉,现在是非工作时间。我们的工作时间是 {start} - {end}。您的消息已记录,我们会在工作时间尽快回复您。" + + def get_business_hours(self) -> tuple[time, time]: + """获取营业时间""" + start = time.fromisoformat(self.business_hours_start) + end = time.fromisoformat(self.business_hours_end) + return start, end + + +@dataclass +class SecurityConfig: + """安全配置""" + max_messages_per_minute: int = 30 + session_timeout: int = 3600 # 秒 + enable_encryption: bool = False + blocked_words: List[str] = field(default_factory=list) + allowed_file_types: List[str] = field(default_factory=lambda: [ + '.jpg', '.jpeg', '.png', '.gif', '.pdf', '.doc', '.docx' + ]) + max_file_size: int = 10485760 # 10MB + + +@dataclass +class FeatureFlags: + """功能开关""" + enable_auto_reply: bool = True + enable_statistics: bool = True + enable_customer_history: bool = True + enable_multi_admin: bool = False + enable_file_transfer: bool = True + enable_voice_message: bool = True + enable_location_sharing: bool = False + + +@dataclass +class Settings: + """主配置类""" + telegram: TelegramConfig + database: DatabaseConfig + logging: LoggingConfig + business: BusinessConfig + security: SecurityConfig + features: FeatureFlags + + # 运行时配置 + debug: bool = False + testing: bool = False + version: str = "1.0.0" + + @classmethod + def from_env(cls) -> 'Settings': + """从环境变量创建配置""" + from .loader import ConfigLoader + return ConfigLoader.load_from_env() + + def validate(self) -> bool: + """验证配置完整性""" + try: + # 验证必要配置 + assert self.telegram.bot_token, "Bot token is required" + assert self.telegram.admin_id, "Admin ID is required" + + # 验证路径 + if self.database.type == "sqlite": + db_dir = os.path.dirname(self.database.path) + if db_dir and not os.path.exists(db_dir): + os.makedirs(db_dir, exist_ok=True) + + return True + except Exception as e: + raise ValueError(f"Configuration validation failed: {e}") \ No newline at end of file diff --git a/src/core/__init__.py b/src/core/__init__.py new file mode 100644 index 0000000..b86c1fb --- /dev/null +++ b/src/core/__init__.py @@ -0,0 +1,6 @@ +"""核心模块""" +from .bot import CustomerServiceBot +from .router import MessageRouter +from .handlers import BaseHandler, HandlerContext + +__all__ = ['CustomerServiceBot', 'MessageRouter', 'BaseHandler', 'HandlerContext'] \ No newline at end of file diff --git a/src/core/bot.py b/src/core/bot.py new file mode 100644 index 0000000..06a87cd --- /dev/null +++ b/src/core/bot.py @@ -0,0 +1,693 @@ +"""客服机器人主类""" +import asyncio +from typing import Optional, Dict, Any, List +from datetime import datetime +from telegram import Update, Bot, BotCommand, InlineKeyboardButton, InlineKeyboardMarkup +from telegram.ext import Application, CommandHandler, MessageHandler, CallbackQueryHandler, filters + +from ..config.settings import Settings +from ..utils.logger import get_logger, Logger +from ..utils.exceptions import BotException, ErrorHandler +from ..utils.decorators import log_action, measure_performance +from .router import MessageRouter, RouteBuilder, MessageContext +from .handlers import BaseHandler, HandlerContext + + +logger = get_logger(__name__) + + +class CustomerServiceBot: + """客服机器人""" + + def __init__(self, config: Settings = None): + """初始化机器人""" + # 加载配置 + self.config = config or Settings.from_env() + self.config.validate() + + # 初始化日志系统 + Logger(self.config) + self.logger = get_logger(self.__class__.__name__, self.config) + + # 初始化组件 + self.application: Optional[Application] = None + self.router = MessageRouter(self.config) + self.route_builder = RouteBuilder(self.router) + self.handlers: Dict[str, BaseHandler] = {} + self.active_sessions: Dict[str, Dict[str, Any]] = {} + + # 当前会话管理 + self.current_customer = None # 当前正在对话的客户 + + # 统计信息 + self.stats = { + 'messages_received': 0, + 'messages_forwarded': 0, + 'replies_sent': 0, + 'errors': 0, + 'start_time': datetime.now() + } + + self.logger.info(f"Bot initialized with version {self.config.version}") + + async def initialize(self): + """异步初始化""" + try: + # 创建应用 + self.application = Application.builder().token( + self.config.telegram.bot_token + ).build() + + # 设置命令 + await self.setup_commands() + + # 注册处理器 + self.register_handlers() + + # 初始化数据库(如果需要) + if self.config.features.enable_customer_history: + from ..modules.storage import DatabaseManager + self.db_manager = DatabaseManager(self.config) + await self.db_manager.initialize() + + + self.logger.info("Bot initialization completed") + + except Exception as e: + self.logger.error(f"Failed to initialize bot: {e}") + raise + + async def setup_commands(self): + """设置机器人命令""" + commands = [ + BotCommand("start", "开始使用机器人"), + BotCommand("help", "获取帮助信息"), + BotCommand("status", "查看机器人状态"), + BotCommand("contact", "联系人工客服"), + ] + + # 管理员命令 + admin_commands = commands + [ + BotCommand("stats", "查看统计信息"), + BotCommand("sessions", "查看活跃会话"), + BotCommand("reply", "回复客户消息"), + BotCommand("broadcast", "广播消息"), + BotCommand("settings", "机器人设置"), + ] + + # 设置命令 + await self.application.bot.set_my_commands(commands) + + # 为管理员设置特殊命令 + await self.application.bot.set_my_commands( + admin_commands, + scope={"type": "chat", "chat_id": self.config.telegram.admin_id} + ) + + def register_handlers(self): + """注册消息处理器""" + # 命令处理器 + self.application.add_handler(CommandHandler("start", self.handle_start)) + self.application.add_handler(CommandHandler("help", self.handle_help)) + self.application.add_handler(CommandHandler("status", self.handle_status)) + self.application.add_handler(CommandHandler("contact", self.handle_contact)) + + # 管理员命令 + self.application.add_handler(CommandHandler("stats", self.handle_stats)) + self.application.add_handler(CommandHandler("sessions", self.handle_sessions)) + self.application.add_handler(CommandHandler("reply", self.handle_reply)) + self.application.add_handler(CommandHandler("broadcast", self.handle_broadcast)) + self.application.add_handler(CommandHandler("settings", self.handle_settings)) + + # 消息处理器 - 处理所有消息(包括搜索指令) + # 只排除机器人自己处理的命令,其他命令(如搜索指令)也会转发 + self.application.add_handler(MessageHandler( + filters.ALL, + self.handle_message + )) + + # 回调查询处理器 + self.application.add_handler(CallbackQueryHandler(self.handle_callback)) + + # 错误处理器 + self.application.add_error_handler(self.handle_error) + + @log_action("start_command") + async def handle_start(self, update: Update, context): + """处理 /start 命令""" + user = update.effective_user + is_admin = user.id == self.config.telegram.admin_id + + if is_admin: + text = ( + f"👋 欢迎,管理员 {user.first_name}!\n\n" + "🤖 客服机器人已就绪\n" + "📊 使用 /stats 查看统计\n" + "💬 使用 /sessions 查看会话\n" + "⚙️ 使用 /settings 进行设置" + ) + else: + text = ( + f"👋 您好 {user.first_name}!\n\n" + "暂时支持的搜索指令:\n\n" + "- 群组目录 /topchat\n" + "- 群组搜索 /search\n" + "- 按消息文本搜索 /text\n" + "- 按名称搜索 /human\n\n" + "您可以使用以上指令进行搜索,或直接发送消息联系客服。" + ) + + # 通知管理员 + await self.notify_admin_new_customer(user) + + await update.message.reply_text(text) + self.stats['messages_received'] += 1 + + async def handle_help(self, update: Update, context): + """处理 /help 命令""" + user = update.effective_user + is_admin = user.id == self.config.telegram.admin_id + + if is_admin: + text = self._get_admin_help() + else: + text = self._get_user_help() + + await update.message.reply_text(text, parse_mode='Markdown') + + async def handle_status(self, update: Update, context): + """处理 /status 命令""" + uptime = datetime.now() - self.stats['start_time'] + hours = uptime.total_seconds() / 3600 + + text = ( + "✅ 机器人运行正常\n\n" + f"⏱ 运行时间:{hours:.1f} 小时\n" + f"📊 处理消息:{self.stats['messages_received']} 条\n" + f"👥 活跃会话:{len(self.active_sessions)} 个" + ) + + await update.message.reply_text(text) + + async def handle_contact(self, update: Update, context): + """处理 /contact 命令""" + await update.message.reply_text( + "正在为您转接人工客服,请稍候...\n" + "您可以直接发送消息,客服会尽快回复您。" + ) + # 修复:传递正确的 context 参数 + await self.forward_customer_message(update, context) + + @measure_performance + async def handle_message(self, update: Update, context): + """处理普通消息""" + try: + user = update.effective_user + message = update.effective_message + is_admin = user.id == self.config.telegram.admin_id + + self.stats['messages_received'] += 1 + + if is_admin: + # 管理员消息 - 检查是否是回复 + if message.reply_to_message: + await self.handle_admin_reply(update, context) + elif self.current_customer: + # 如果有当前客户,直接发送给当前客户 + await self.reply_to_current_customer(update, context) + else: + # 没有当前客户时,提示管理员 + await message.reply_text( + "💡 提示:暂无活跃客户\n\n" + "等待客户发送消息,或使用:\n" + "• 直接回复转发的客户消息\n" + "• /sessions 查看所有会话\n" + "• /reply <用户ID> <消息> 回复指定用户" + ) + else: + # 客户消息 - 转发给管理员(包括搜索指令) + # 处理所有客户消息,包括 /topchat, /search, /text, /human 等指令 + await self.forward_customer_message(update, context) + + except Exception as e: + self.logger.error(f"Error handling message: {e}") + await self.send_error_message(update, e) + + async def forward_customer_message(self, update: Update, context): + """转发客户消息给管理员""" + user = update.effective_user + message = update.effective_message + chat = update.effective_chat + + # 创建或更新会话 + session_id = f"{chat.id}_{user.id}" + if session_id not in self.active_sessions: + self.active_sessions[session_id] = { + 'user_id': user.id, + 'username': user.username, + 'first_name': user.first_name, + 'chat_id': chat.id, + 'messages': [], + 'started_at': datetime.now() + } + + # 记录消息 + self.active_sessions[session_id]['messages'].append({ + 'message_id': message.message_id, + 'text': message.text or "[非文本消息]", + 'timestamp': datetime.now() + }) + + # 设置为当前客户 + self.current_customer = { + 'user_id': user.id, + 'chat_id': chat.id, + 'username': user.username, + 'first_name': user.first_name, + 'session_id': session_id + } + + # 构建用户信息 - 转义特殊字符 + def escape_markdown(text): + """转义 Markdown 特殊字符""" + if text is None: + return '' + # 转义特殊字符 + special_chars = ['_', '*', '[', ']', '(', ')', '~', '`', '>', '#', '+', '-', '=', '|', '{', '}', '.', '!'] + for char in special_chars: + text = str(text).replace(char, f'\\{char}') + return text + + first_name = escape_markdown(user.first_name) + last_name = escape_markdown(user.last_name) if user.last_name else '' + username = escape_markdown(user.username) if user.username else 'N/A' + + # 构建用户信息 + user_info = ( + f"📨 来自客户的消息\n" + f"👤 姓名:{first_name} {last_name}\n" + f"🆔 ID:`{user.id}`\n" + f"📱 用户名:@{username}\n" + f"💬 会话:`{session_id}`\n" + f"━━━━━━━━━━━━━━━━" + ) + + # 发送用户信息 + await context.bot.send_message( + chat_id=self.config.telegram.admin_id, + text=user_info, + parse_mode='MarkdownV2' + ) + + # 转发原始消息 + forwarded = await context.bot.forward_message( + chat_id=self.config.telegram.admin_id, + from_chat_id=chat.id, + message_id=message.message_id + ) + + # 保存转发消息ID映射 + context.bot_data.setdefault('message_map', {})[forwarded.message_id] = { + 'original_chat': chat.id, + 'original_user': user.id, + 'session_id': session_id + } + + # 提示管理员可以直接输入文字回复 + await context.bot.send_message( + chat_id=self.config.telegram.admin_id, + text="💬 现在可以直接输入文字回复此客户,或回复上方转发的消息" + ) + + # 自动回复(如果启用) + if self.config.features.enable_auto_reply and not is_business_hours(self.config): + await self.send_auto_reply(update, context) + + self.stats['messages_forwarded'] += 1 + + async def handle_admin_reply(self, update: Update, context): + """处理管理员回复""" + replied_to = update.message.reply_to_message + + # 查找原始消息信息 + message_map = context.bot_data.get('message_map', {}) + if replied_to.message_id not in message_map: + await update.message.reply_text("⚠️ 无法找到原始消息信息") + return + + original_info = message_map[replied_to.message_id] + original_chat = original_info['original_chat'] + session_id = original_info['session_id'] + + # 发送回复给客户 + try: + if update.message.text: + await context.bot.send_message( + chat_id=original_chat, + text=update.message.text + ) + elif update.message.photo: + await context.bot.send_photo( + chat_id=original_chat, + photo=update.message.photo[-1].file_id, + caption=update.message.caption + ) + elif update.message.document: + await context.bot.send_document( + chat_id=original_chat, + document=update.message.document.file_id, + caption=update.message.caption + ) + + # 确认发送 + await update.message.reply_text("✅ 消息已发送给客户") + + # 更新会话 + if session_id in self.active_sessions: + self.active_sessions[session_id]['last_reply'] = datetime.now() + + self.stats['replies_sent'] += 1 + + except Exception as e: + await update.message.reply_text(f"❌ 发送失败:{e}") + self.logger.error(f"Failed to send reply: {e}") + + async def handle_stats(self, update: Update, context): + """处理 /stats 命令(管理员)""" + if update.effective_user.id != self.config.telegram.admin_id: + return + + uptime = datetime.now() - self.stats['start_time'] + days = uptime.days + hours = uptime.seconds // 3600 + + text = ( + "📊 **统计信息**\n\n" + f"⏱ 运行时间:{days} 天 {hours} 小时\n" + f"📨 接收消息:{self.stats['messages_received']} 条\n" + f"📤 转发消息:{self.stats['messages_forwarded']} 条\n" + f"💬 回复消息:{self.stats['replies_sent']} 条\n" + f"❌ 错误次数:{self.stats['errors']} 次\n" + f"👥 活跃会话:{len(self.active_sessions)} 个\n" + f"📅 启动时间:{self.stats['start_time'].strftime('%Y-%m-%d %H:%M:%S')}" + ) + + await update.message.reply_text(text, parse_mode='Markdown') + + async def handle_sessions(self, update: Update, context): + """处理 /sessions 命令(管理员)""" + if update.effective_user.id != self.config.telegram.admin_id: + return + + if not self.active_sessions: + await update.message.reply_text("当前没有活跃会话") + return + + text = "👥 **活跃会话**\n\n" + for session_id, session in self.active_sessions.items(): + duration = datetime.now() - session['started_at'] + text += ( + f"会话 `{session_id}`\n" + f"👤 {session['first_name']} (@{session['username'] or 'N/A'})\n" + f"💬 消息数:{len(session['messages'])}\n" + f"⏱ 时长:{duration.seconds // 60} 分钟\n" + f"━━━━━━━━━━━━━━━━\n" + ) + + await update.message.reply_text(text, parse_mode='Markdown') + + async def handle_reply(self, update: Update, context): + """处理 /reply 命令(管理员)""" + if update.effective_user.id != self.config.telegram.admin_id: + return + + if len(context.args) < 2: + await update.message.reply_text( + "用法:/reply <用户ID> <消息>\n" + "示例:/reply 123456789 您好,有什么可以帮助您?" + ) + return + + try: + user_id = int(context.args[0]) + message = ' '.join(context.args[1:]) + + await context.bot.send_message(chat_id=user_id, text=message) + await update.message.reply_text(f"✅ 消息已发送给用户 {user_id}") + self.stats['replies_sent'] += 1 + + except Exception as e: + await update.message.reply_text(f"❌ 发送失败:{e}") + + + async def reply_to_current_customer(self, update: Update, context): + """回复当前客户""" + if not self.current_customer: + await update.message.reply_text("❌ 没有选中的客户") + return + + try: + message = update.effective_message + + # 发送消息给当前客户 + if message.text: + await context.bot.send_message( + chat_id=self.current_customer['chat_id'], + text=message.text + ) + elif message.photo: + await context.bot.send_photo( + chat_id=self.current_customer['chat_id'], + photo=message.photo[-1].file_id, + caption=message.caption + ) + elif message.document: + await context.bot.send_document( + chat_id=self.current_customer['chat_id'], + document=message.document.file_id, + caption=message.caption + ) + + # 简洁确认消息 + await update.message.reply_text(f"✅ → {self.current_customer['first_name']}") + self.stats['replies_sent'] += 1 + + except Exception as e: + await update.message.reply_text(f"❌ 发送失败:{e}") + self.logger.error(f"Failed to send reply: {e}") + + async def handle_broadcast(self, update: Update, context): + """处理 /broadcast 命令(管理员)""" + if update.effective_user.id != self.config.telegram.admin_id: + return + + if not context.args: + await update.message.reply_text( + "用法:/broadcast <消息>\n" + "示例:/broadcast 系统维护通知:今晚10点进行系统维护" + ) + return + + message = ' '.join(context.args) + sent = 0 + failed = 0 + + for session_id, session in self.active_sessions.items(): + try: + await context.bot.send_message( + chat_id=session['chat_id'], + text=message + ) + sent += 1 + except Exception as e: + failed += 1 + self.logger.error(f"Failed to broadcast to {session['chat_id']}: {e}") + + await update.message.reply_text( + f"✅ 广播完成\n" + f"成功:{sent} 个\n" + f"失败:{failed} 个" + ) + + async def handle_settings(self, update: Update, context): + """处理 /settings 命令(管理员)""" + if update.effective_user.id != self.config.telegram.admin_id: + return + + keyboard = [ + [ + InlineKeyboardButton( + f"{'✅' if self.config.features.enable_auto_reply else '❌'} 自动回复", + callback_data="toggle_auto_reply" + ) + ], + [ + InlineKeyboardButton( + f"{'✅' if self.config.features.enable_statistics else '❌'} 统计功能", + callback_data="toggle_statistics" + ) + ], + [ + InlineKeyboardButton("📊 查看所有设置", callback_data="view_settings") + ] + ] + reply_markup = InlineKeyboardMarkup(keyboard) + + await update.message.reply_text( + "⚙️ **机器人设置**\n\n点击按钮切换功能:", + reply_markup=reply_markup, + parse_mode='Markdown' + ) + + async def handle_callback(self, update: Update, context): + """处理回调查询""" + query = update.callback_query + await query.answer() + + data = query.data + if data.startswith("done_"): + session_id = data.replace("done_", "") + await query.edit_message_text(f"✅ 会话 {session_id} 已标记为完成") + elif data.startswith("later_"): + session_id = data.replace("later_", "") + await query.edit_message_text(f"⏸ 会话 {session_id} 已标记为稍后处理") + elif data == "toggle_auto_reply": + self.config.features.enable_auto_reply = not self.config.features.enable_auto_reply + await query.edit_message_text( + f"自动回复已{'启用' if self.config.features.enable_auto_reply else '禁用'}" + ) + + async def handle_error(self, update: Update, context): + """处理错误""" + self.stats['errors'] += 1 + error_info = await ErrorHandler.handle_error(context.error) + self.logger.error(f"Update {update} caused error {context.error}") + + if update and update.effective_message: + user_message = ErrorHandler.create_user_message(context.error) + await update.effective_message.reply_text(user_message) + + async def notify_admin_new_customer(self, user): + """通知管理员有新客户""" + def escape_markdown(text): + """转义 Markdown 特殊字符""" + if text is None: + return '' + # 转义特殊字符 + special_chars = ['_', '*', '[', ']', '(', ')', '~', '`', '>', '#', '+', '-', '=', '|', '{', '}', '.', '!'] + for char in special_chars: + text = str(text).replace(char, f'\\{char}') + return text + + first_name = escape_markdown(user.first_name) + last_name = escape_markdown(user.last_name) if user.last_name else '' + username = escape_markdown(user.username) if user.username else 'N/A' + + text = ( + f"🆕 新客户加入\n" + f"👤 姓名:{first_name} {last_name}\n" + f"🆔 ID:`{user.id}`\n" + f"📱 用户名:@{username}" + ) + + try: + await self.application.bot.send_message( + chat_id=self.config.telegram.admin_id, + text=text, + parse_mode='MarkdownV2' + ) + except Exception as e: + self.logger.error(f"Failed to notify admin: {e}") + + async def send_auto_reply(self, update: Update, context): + """发送自动回复""" + import pytz + from datetime import time + + # 检查营业时间 + tz = pytz.timezone(self.config.business.timezone) + now = datetime.now(tz).time() + start_time = time.fromisoformat(self.config.business.business_hours_start) + end_time = time.fromisoformat(self.config.business.business_hours_end) + + if not (start_time <= now <= end_time): + message = self.config.business.offline_message.format( + start=self.config.business.business_hours_start, + end=self.config.business.business_hours_end + ) + await update.message.reply_text(message) + + async def send_error_message(self, update: Update, error: Exception): + """发送错误消息给用户""" + user_message = ErrorHandler.create_user_message(error) + await update.message.reply_text(user_message) + + def _get_admin_help(self) -> str: + """获取管理员帮助信息""" + return ( + "📚 **管理员帮助**\n\n" + "**基础命令**\n" + "/start - 启动机器人\n" + "/help - 显示帮助\n" + "/status - 查看状态\n\n" + "**管理命令**\n" + "/stats - 查看统计信息\n" + "/sessions - 查看活跃会话\n" + "/reply <用户ID> <消息> - 回复指定用户\n" + "/broadcast <消息> - 广播消息给所有用户\n" + "/settings - 机器人设置\n\n" + "**快速回复客户**\n" + "• 直接输入文字 - 自动发送给最近的客户\n" + "• 回复转发消息 - 回复特定客户" + ) + + def _get_user_help(self) -> str: + """获取用户帮助信息""" + return ( + "📚 **帮助信息**\n\n" + "**使用方法**\n" + "• 直接发送消息,客服会尽快回复您\n" + "• 支持发送文字、图片、文件等\n" + "• /contact - 联系人工客服\n" + "• /status - 查看服务状态\n\n" + f"**工作时间**\n" + f"{self.config.business.business_hours_start} - {self.config.business.business_hours_end}\n\n" + "如有紧急情况,请留言,我们会尽快处理" + ) + + + def run(self): + """运行机器人""" + try: + # 同步初始化 + asyncio.get_event_loop().run_until_complete(self.initialize()) + + self.logger.info("Starting bot...") + self.application.run_polling(allowed_updates=Update.ALL_TYPES) + + except KeyboardInterrupt: + self.logger.info("Bot stopped by user") + except Exception as e: + self.logger.error(f"Bot crashed: {e}") + raise + finally: + self.cleanup() + + def cleanup(self): + """清理资源""" + self.logger.info("Cleaning up resources...") + # 保存统计信息、关闭数据库等 + pass + + +def is_business_hours(config: Settings) -> bool: + """检查是否在营业时间""" + import pytz + from datetime import time + + tz = pytz.timezone(config.business.timezone) + now = datetime.now(tz).time() + start_time = time.fromisoformat(config.business.business_hours_start) + end_time = time.fromisoformat(config.business.business_hours_end) + + return start_time <= now <= end_time \ No newline at end of file diff --git a/src/core/handlers.py b/src/core/handlers.py new file mode 100644 index 0000000..9aec6ec --- /dev/null +++ b/src/core/handlers.py @@ -0,0 +1,157 @@ +"""处理器基类和上下文""" +from abc import ABC, abstractmethod +from typing import Any, Optional, Dict, List +from dataclasses import dataclass, field +from telegram import Update, Message +from telegram.ext import ContextTypes + +from ..utils.logger import get_logger +from ..config.settings import Settings + + +logger = get_logger(__name__) + + +@dataclass +class HandlerContext: + """处理器上下文""" + update: Update + context: ContextTypes.DEFAULT_TYPE + config: Settings + user_data: Dict[str, Any] = field(default_factory=dict) + chat_data: Dict[str, Any] = field(default_factory=dict) + session_data: Dict[str, Any] = field(default_factory=dict) + + @property + def message(self) -> Message: + """获取消息""" + return self.update.effective_message + + @property + def user(self): + """获取用户""" + return self.update.effective_user + + @property + def chat(self): + """获取聊天""" + return self.update.effective_chat + + def get_session_id(self) -> str: + """获取会话ID""" + return f"{self.chat.id}_{self.user.id}" + + +class BaseHandler(ABC): + """处理器基类""" + + def __init__(self, config: Settings): + self.config = config + self.logger = get_logger(self.__class__.__name__) + + @abstractmethod + async def handle(self, handler_context: HandlerContext) -> Any: + """处理消息""" + pass + + async def __call__(self, update: Update, context: ContextTypes.DEFAULT_TYPE, + message_context: Any = None) -> Any: + """调用处理器""" + handler_context = HandlerContext( + update=update, + context=context, + config=self.config, + user_data=context.user_data, + chat_data=context.chat_data + ) + + try: + self.logger.debug(f"Handling message from user {handler_context.user.id}") + result = await self.handle(handler_context) + return result + except Exception as e: + self.logger.error(f"Error in handler: {e}") + raise + + async def reply_text(self, context: HandlerContext, text: str, **kwargs) -> Message: + """回复文本消息""" + return await context.message.reply_text(text, **kwargs) + + async def reply_photo(self, context: HandlerContext, photo, caption: str = None, **kwargs) -> Message: + """回复图片""" + return await context.message.reply_photo(photo, caption=caption, **kwargs) + + async def reply_document(self, context: HandlerContext, document, caption: str = None, **kwargs) -> Message: + """回复文档""" + return await context.message.reply_document(document, caption=caption, **kwargs) + + async def forward_to_admin(self, context: HandlerContext) -> Message: + """转发消息给管理员""" + return await context.context.bot.forward_message( + chat_id=self.config.telegram.admin_id, + from_chat_id=context.chat.id, + message_id=context.message.message_id + ) + + async def send_to_admin(self, context: HandlerContext, text: str, **kwargs) -> Message: + """发送消息给管理员""" + return await context.context.bot.send_message( + chat_id=self.config.telegram.admin_id, + text=text, + **kwargs + ) + + +class CompositeHandler(BaseHandler): + """组合处理器""" + + def __init__(self, config: Settings): + super().__init__(config) + self.handlers: List[BaseHandler] = [] + + def add_handler(self, handler: BaseHandler): + """添加处理器""" + self.handlers.append(handler) + + async def handle(self, handler_context: HandlerContext) -> Any: + """依次执行所有处理器""" + results = [] + for handler in self.handlers: + try: + result = await handler.handle(handler_context) + results.append(result) + except Exception as e: + self.logger.error(f"Error in composite handler {handler.__class__.__name__}: {e}") + # 可以选择继续或中断 + raise + + return results + + +class ConditionalHandler(BaseHandler): + """条件处理器""" + + def __init__(self, config: Settings, condition_func): + super().__init__(config) + self.condition_func = condition_func + self.true_handler: Optional[BaseHandler] = None + self.false_handler: Optional[BaseHandler] = None + + def set_true_handler(self, handler: BaseHandler): + """设置条件为真时的处理器""" + self.true_handler = handler + + def set_false_handler(self, handler: BaseHandler): + """设置条件为假时的处理器""" + self.false_handler = handler + + async def handle(self, handler_context: HandlerContext) -> Any: + """根据条件执行处理器""" + if await self.condition_func(handler_context): + if self.true_handler: + return await self.true_handler.handle(handler_context) + else: + if self.false_handler: + return await self.false_handler.handle(handler_context) + + return None \ No newline at end of file diff --git a/src/core/router.py b/src/core/router.py new file mode 100644 index 0000000..86c904b --- /dev/null +++ b/src/core/router.py @@ -0,0 +1,321 @@ +"""消息路由系统""" +import asyncio +from typing import Dict, List, Optional, Callable, Any, Type +from dataclasses import dataclass, field +from datetime import datetime +from enum import Enum +from telegram import Update, Message, User, Chat +from telegram.ext import ContextTypes + +from ..utils.logger import get_logger +from ..utils.exceptions import MessageRoutingError +from ..utils.decorators import log_action, measure_performance + + +logger = get_logger(__name__) + + +class MessageType(Enum): + """消息类型枚举""" + TEXT = "text" + PHOTO = "photo" + VIDEO = "video" + AUDIO = "audio" + VOICE = "voice" + DOCUMENT = "document" + STICKER = "sticker" + LOCATION = "location" + CONTACT = "contact" + POLL = "poll" + COMMAND = "command" + CALLBACK = "callback" + INLINE = "inline" + + +class RoutePriority(Enum): + """路由优先级""" + CRITICAL = 0 + HIGH = 1 + NORMAL = 2 + LOW = 3 + + +@dataclass +class RoutePattern: + """路由模式""" + pattern: str + type: MessageType + priority: RoutePriority = RoutePriority.NORMAL + conditions: List[Callable] = field(default_factory=list) + metadata: Dict[str, Any] = field(default_factory=dict) + + def matches(self, message: Message) -> bool: + """检查消息是否匹配模式""" + # 检查消息类型 + if not self._check_message_type(message): + return False + + # 检查模式匹配 + if self.type == MessageType.TEXT and message.text: + if not self._match_text_pattern(message.text): + return False + + # 检查条件 + for condition in self.conditions: + if not condition(message): + return False + + return True + + def _check_message_type(self, message: Message) -> bool: + """检查消息类型是否匹配""" + type_map = { + MessageType.TEXT: lambda m: m.text is not None, + MessageType.PHOTO: lambda m: m.photo is not None, + MessageType.VIDEO: lambda m: m.video is not None, + MessageType.AUDIO: lambda m: m.audio is not None, + MessageType.VOICE: lambda m: m.voice is not None, + MessageType.DOCUMENT: lambda m: m.document is not None, + MessageType.STICKER: lambda m: m.sticker is not None, + MessageType.LOCATION: lambda m: m.location is not None, + MessageType.CONTACT: lambda m: m.contact is not None, + MessageType.POLL: lambda m: m.poll is not None, + } + + check_func = type_map.get(self.type) + return check_func(message) if check_func else False + + def _match_text_pattern(self, text: str) -> bool: + """匹配文本模式""" + import re + if self.pattern.startswith("^") or self.pattern.endswith("$"): + # 正则表达式 + return bool(re.match(self.pattern, text)) + else: + # 简单包含检查 + return self.pattern in text + + +@dataclass +class MessageContext: + """消息上下文""" + message_id: str + user_id: int + chat_id: int + username: Optional[str] + first_name: Optional[str] + last_name: Optional[str] + message_type: MessageType + content: Any + timestamp: datetime + is_admin: bool = False + session_id: Optional[str] = None + metadata: Dict[str, Any] = field(default_factory=dict) + + @classmethod + def from_update(cls, update: Update, admin_id: int) -> 'MessageContext': + """从更新创建上下文""" + message = update.effective_message + user = update.effective_user + chat = update.effective_chat + + # 确定消息类型 + if message.text and message.text.startswith('/'): + msg_type = MessageType.COMMAND + elif message.text: + msg_type = MessageType.TEXT + elif message.photo: + msg_type = MessageType.PHOTO + elif message.video: + msg_type = MessageType.VIDEO + elif message.voice: + msg_type = MessageType.VOICE + elif message.document: + msg_type = MessageType.DOCUMENT + elif message.location: + msg_type = MessageType.LOCATION + else: + msg_type = MessageType.TEXT + + # 提取内容 + content = message.text or message.caption or "" + if message.photo: + content = message.photo[-1].file_id + elif message.document: + content = message.document.file_id + elif message.voice: + content = message.voice.file_id + elif message.video: + content = message.video.file_id + + return cls( + message_id=str(message.message_id), + user_id=user.id, + chat_id=chat.id, + username=user.username, + first_name=user.first_name, + last_name=user.last_name, + message_type=msg_type, + content=content, + timestamp=datetime.now(), + is_admin=(user.id == admin_id), + session_id=f"{chat.id}_{user.id}" + ) + + +class MessageRouter: + """消息路由器""" + + def __init__(self, config): + self.config = config + self.routes: Dict[RoutePriority, List[tuple[RoutePattern, Callable]]] = { + priority: [] for priority in RoutePriority + } + self.middleware: List[Callable] = [] + self.default_handler: Optional[Callable] = None + self.error_handler: Optional[Callable] = None + + def add_route(self, pattern: RoutePattern, handler: Callable): + """添加路由""" + self.routes[pattern.priority].append((pattern, handler)) + logger.debug(f"Added route: {pattern.pattern} with priority {pattern.priority}") + + def add_middleware(self, middleware: Callable): + """添加中间件""" + self.middleware.append(middleware) + logger.debug(f"Added middleware: {middleware.__name__}") + + def set_default_handler(self, handler: Callable): + """设置默认处理器""" + self.default_handler = handler + + def set_error_handler(self, handler: Callable): + """设置错误处理器""" + self.error_handler = handler + + @measure_performance + @log_action("route_message") + async def route(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> Any: + """路由消息""" + try: + # 创建消息上下文 + msg_context = MessageContext.from_update( + update, + self.config.telegram.admin_id + ) + + # 应用中间件 + for middleware in self.middleware: + result = await middleware(msg_context, context) + if result is False: + logger.debug(f"Middleware {middleware.__name__} blocked message") + return None + + # 查找匹配的路由 + handler = await self._find_handler(update.effective_message, msg_context) + + if handler: + logger.info( + f"Routing message to {handler.__name__}", + extra={'user_id': msg_context.user_id, 'handler': handler.__name__} + ) + return await handler(update, context, msg_context) + elif self.default_handler: + logger.info( + f"Using default handler", + extra={'user_id': msg_context.user_id} + ) + return await self.default_handler(update, context, msg_context) + else: + logger.warning(f"No handler found for message from user {msg_context.user_id}") + raise MessageRoutingError("No handler found for this message type") + + except Exception as e: + if self.error_handler: + return await self.error_handler(update, context, e) + else: + logger.error(f"Error in message routing: {e}") + raise + + async def _find_handler(self, message: Message, context: MessageContext) -> Optional[Callable]: + """查找合适的处理器""" + # 按优先级顺序检查路由 + for priority in RoutePriority: + for pattern, handler in self.routes[priority]: + if pattern.matches(message): + return handler + return None + + +class RouteBuilder: + """路由构建器""" + + def __init__(self, router: MessageRouter): + self.router = router + + def text(self, pattern: str = None, priority: RoutePriority = RoutePriority.NORMAL): + """文本消息路由装饰器""" + def decorator(handler: Callable): + route_pattern = RoutePattern( + pattern=pattern or ".*", + type=MessageType.TEXT, + priority=priority + ) + self.router.add_route(route_pattern, handler) + return handler + return decorator + + def command(self, command: str, priority: RoutePriority = RoutePriority.HIGH): + """命令路由装饰器""" + def decorator(handler: Callable): + route_pattern = RoutePattern( + pattern=f"^/{command}", + type=MessageType.TEXT, + priority=priority + ) + self.router.add_route(route_pattern, handler) + return handler + return decorator + + def photo(self, priority: RoutePriority = RoutePriority.NORMAL): + """图片消息路由装饰器""" + def decorator(handler: Callable): + route_pattern = RoutePattern( + pattern="", + type=MessageType.PHOTO, + priority=priority + ) + self.router.add_route(route_pattern, handler) + return handler + return decorator + + def document(self, priority: RoutePriority = RoutePriority.NORMAL): + """文档消息路由装饰器""" + def decorator(handler: Callable): + route_pattern = RoutePattern( + pattern="", + type=MessageType.DOCUMENT, + priority=priority + ) + self.router.add_route(route_pattern, handler) + return handler + return decorator + + def voice(self, priority: RoutePriority = RoutePriority.NORMAL): + """语音消息路由装饰器""" + def decorator(handler: Callable): + route_pattern = RoutePattern( + pattern="", + type=MessageType.VOICE, + priority=priority + ) + self.router.add_route(route_pattern, handler) + return handler + return decorator + + def middleware(self): + """中间件装饰器""" + def decorator(handler: Callable): + self.router.add_middleware(handler) + return handler + return decorator \ No newline at end of file diff --git a/src/modules/mirror_search.py b/src/modules/mirror_search.py new file mode 100644 index 0000000..1c5e377 --- /dev/null +++ b/src/modules/mirror_search.py @@ -0,0 +1,279 @@ +""" +搜索镜像模块 - 自动转发搜索指令到目标机器人并返回结果 +基于 jingxiang 项目的镜像机制 +""" + +import asyncio +import logging +from typing import Dict, Optional, Any +from pyrogram import Client, filters +from pyrogram.types import Message as PyrogramMessage +from pyrogram.raw.functions.messages import GetBotCallbackAnswer +from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup +from telegram.ext import ContextTypes + +logger = logging.getLogger(__name__) + + +class MirrorSearchHandler: + """处理搜索指令的镜像转发""" + + def __init__(self, config): + self.config = config + self.enabled = False + + # Pyrogram配置(需要在.env中配置) + self.api_id = None + self.api_hash = None + self.session_name = "search_mirror_session" + self.target_bot = "@openaiw_bot" # 目标搜索机器人 + + # Pyrogram客户端 + self.pyrogram_client: Optional[Client] = None + self.target_bot_id: Optional[int] = None + + # 消息映射 + self.user_search_requests: Dict[int, Dict[str, Any]] = {} # user_id -> search_info + self.pyrogram_to_user: Dict[int, int] = {} # pyrogram_msg_id -> user_id + self.user_to_telegram: Dict[int, int] = {} # user_id -> telegram_msg_id + + # 支持的搜索命令 + self.search_commands = ['/topchat', '/search', '/text', '/human'] + + async def initialize(self, api_id: int, api_hash: str): + """初始化Pyrogram客户端""" + try: + self.api_id = api_id + self.api_hash = api_hash + + self.pyrogram_client = Client( + self.session_name, + api_id=self.api_id, + api_hash=self.api_hash + ) + + await self.pyrogram_client.start() + logger.info("✅ 搜索镜像客户端已启动") + + # 获取目标机器人信息 + target = await self.pyrogram_client.get_users(self.target_bot) + self.target_bot_id = target.id + logger.info(f"✅ 连接到搜索机器人: {target.username} (ID: {target.id})") + + # 设置消息监听器 + await self._setup_listeners() + + self.enabled = True + return True + + except Exception as e: + logger.error(f"镜像搜索初始化失败: {e}") + self.enabled = False + return False + + async def _setup_listeners(self): + """设置Pyrogram消息监听器""" + if not self.pyrogram_client: + return + + @self.pyrogram_client.on_message(filters.user(self.target_bot_id)) + async def on_bot_response(_, message: PyrogramMessage): + """当收到搜索机器人的响应时""" + await self._handle_bot_response(message) + + @self.pyrogram_client.on_edited_message(filters.user(self.target_bot_id)) + async def on_message_edited(_, message: PyrogramMessage): + """当搜索机器人编辑消息时(翻页)""" + await self._handle_bot_response(message, is_edit=True) + + logger.info("✅ 消息监听器已设置") + + def is_search_command(self, text: str) -> bool: + """检查是否是搜索命令""" + if not text: + return False + command = text.split()[0] + return command in self.search_commands + + async def process_search_command( + self, + update: Update, + context: ContextTypes.DEFAULT_TYPE, + user_id: int, + command: str + ) -> bool: + """处理用户的搜索命令""" + + if not self.enabled or not self.pyrogram_client: + logger.warning("搜索镜像未启用") + return False + + try: + # 记录用户搜索请求 + self.user_search_requests[user_id] = { + 'command': command, + 'chat_id': update.effective_chat.id, + 'update': update, + 'context': context, + 'timestamp': asyncio.get_event_loop().time() + } + + # 通过Pyrogram发送命令给目标机器人 + sent_message = await self.pyrogram_client.send_message( + self.target_bot, + command + ) + + # 记录映射关系 + if sent_message: + logger.info(f"已发送搜索命令给 {self.target_bot}: {command}") + # 等待响应会通过监听器处理 + + # 发送等待提示给用户 + waiting_msg = await update.message.reply_text( + "🔍 正在搜索,请稍候..." + ) + self.user_to_telegram[user_id] = waiting_msg.message_id + + return True + + except Exception as e: + logger.error(f"发送搜索命令失败: {e}") + await update.message.reply_text( + "❌ 搜索请求失败,请稍后重试或联系管理员" + ) + return False + + async def _handle_bot_response(self, message: PyrogramMessage, is_edit: bool = False): + """处理搜索机器人的响应""" + try: + # 查找对应的用户 + # 这里需要根据时间戳或其他方式匹配用户请求 + user_id = self._find_user_for_response(message) + + if not user_id or user_id not in self.user_search_requests: + logger.debug(f"未找到对应的用户请求") + return + + user_request = self.user_search_requests[user_id] + + # 转换消息格式并发送给用户 + await self._forward_to_user(message, user_request, is_edit) + + except Exception as e: + logger.error(f"处理机器人响应失败: {e}") + + def _find_user_for_response(self, message: PyrogramMessage) -> Optional[int]: + """查找响应对应的用户""" + # 简单的实现:返回最近的请求用户 + # 实际应用中可能需要更复杂的匹配逻辑 + if self.user_search_requests: + # 获取最近的请求 + recent_user = max( + self.user_search_requests.keys(), + key=lambda k: self.user_search_requests[k].get('timestamp', 0) + ) + return recent_user + return None + + async def _forward_to_user( + self, + pyrogram_msg: PyrogramMessage, + user_request: Dict[str, Any], + is_edit: bool = False + ): + """转发搜索结果给用户""" + try: + update = user_request['update'] + context = user_request['context'] + + # 提取消息内容 + text = self._extract_text(pyrogram_msg) + keyboard = self._convert_keyboard(pyrogram_msg) + + if is_edit and user_request['user_id'] in self.user_to_telegram: + # 编辑现有消息 + telegram_msg_id = self.user_to_telegram[user_request['user_id']] + await context.bot.edit_message_text( + chat_id=user_request['chat_id'], + message_id=telegram_msg_id, + text=text, + reply_markup=keyboard, + parse_mode='HTML' + ) + else: + # 发送新消息 + sent = await context.bot.send_message( + chat_id=user_request['chat_id'], + text=text, + reply_markup=keyboard, + parse_mode='HTML' + ) + self.user_to_telegram[user_request['user_id']] = sent.message_id + + except Exception as e: + logger.error(f"转发消息给用户失败: {e}") + + def _extract_text(self, message: PyrogramMessage) -> str: + """提取消息文本""" + if message.text: + return message.text + elif message.caption: + return message.caption + return "(无文本内容)" + + def _convert_keyboard(self, message: PyrogramMessage) -> Optional[InlineKeyboardMarkup]: + """转换Pyrogram键盘为Telegram键盘""" + if not message.reply_markup: + return None + + try: + buttons = [] + for row in message.reply_markup.inline_keyboard: + button_row = [] + for button in row: + if button.text: + # 创建回调按钮 + callback_data = button.callback_data or f"mirror_{button.text}" + if len(callback_data.encode()) > 64: + # Telegram限制callback_data最大64字节 + callback_data = callback_data[:60] + "..." + + button_row.append( + InlineKeyboardButton( + text=button.text, + callback_data=callback_data + ) + ) + if button_row: + buttons.append(button_row) + + return InlineKeyboardMarkup(buttons) if buttons else None + + except Exception as e: + logger.error(f"转换键盘失败: {e}") + return None + + async def handle_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """处理回调查询(翻页等)""" + query = update.callback_query + + if not query.data.startswith("mirror_"): + return False + + try: + # 这里需要实现回调处理逻辑 + # 将回调转发给Pyrogram客户端 + await query.answer("处理中...") + return True + + except Exception as e: + logger.error(f"处理回调失败: {e}") + await query.answer("操作失败", show_alert=True) + return False + + async def cleanup(self): + """清理资源""" + if self.pyrogram_client: + await self.pyrogram_client.stop() + logger.info("搜索镜像客户端已停止") \ No newline at end of file diff --git a/src/modules/storage/__init__.py b/src/modules/storage/__init__.py new file mode 100644 index 0000000..ce5d7a2 --- /dev/null +++ b/src/modules/storage/__init__.py @@ -0,0 +1,5 @@ +"""存储模块""" +from .database import DatabaseManager +from .models import Customer, Message, Session + +__all__ = ['DatabaseManager', 'Customer', 'Message', 'Session'] \ No newline at end of file diff --git a/src/modules/storage/database.py b/src/modules/storage/database.py new file mode 100644 index 0000000..39fcdd7 --- /dev/null +++ b/src/modules/storage/database.py @@ -0,0 +1,428 @@ +"""数据库管理器""" +import sqlite3 +import json +from typing import Optional, List, Dict, Any +from datetime import datetime +from pathlib import Path +import asyncio +from contextlib import asynccontextmanager + +from .models import Customer, Message, Session, CustomerStatus, SessionStatus, MessageDirection +from ...utils.logger import get_logger +from ...utils.exceptions import DatabaseError +from ...config.settings import Settings + + +logger = get_logger(__name__) + + +class DatabaseManager: + """数据库管理器""" + + def __init__(self, config: Settings): + self.config = config + self.db_path = Path(self.config.database.path) + self.connection: Optional[sqlite3.Connection] = None + self._lock = asyncio.Lock() + + # 确保数据库目录存在 + self.db_path.parent.mkdir(parents=True, exist_ok=True) + + async def initialize(self): + """初始化数据库""" + async with self._lock: + try: + self.connection = sqlite3.connect( + str(self.db_path), + check_same_thread=False + ) + self.connection.row_factory = sqlite3.Row + await self._create_tables() + logger.info(f"Database initialized at {self.db_path}") + except Exception as e: + logger.error(f"Failed to initialize database: {e}") + raise DatabaseError(f"Database initialization failed: {e}") + + async def _create_tables(self): + """创建数据表""" + cursor = self.connection.cursor() + + # 客户表 + cursor.execute(""" + CREATE TABLE IF NOT EXISTS customers ( + user_id INTEGER PRIMARY KEY, + username TEXT, + first_name TEXT NOT NULL, + last_name TEXT, + language_code TEXT, + status TEXT NOT NULL, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + metadata TEXT, + tags TEXT, + notes TEXT + ) + """) + + # 消息表 + cursor.execute(""" + CREATE TABLE IF NOT EXISTS messages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + message_id TEXT NOT NULL, + session_id TEXT NOT NULL, + user_id INTEGER NOT NULL, + chat_id INTEGER NOT NULL, + direction TEXT NOT NULL, + content TEXT, + content_type TEXT NOT NULL, + timestamp TEXT NOT NULL, + is_read INTEGER DEFAULT 0, + is_replied INTEGER DEFAULT 0, + reply_to_message_id TEXT, + metadata TEXT, + FOREIGN KEY (user_id) REFERENCES customers (user_id) + ) + """) + + # 会话表 + cursor.execute(""" + CREATE TABLE IF NOT EXISTS sessions ( + session_id TEXT PRIMARY KEY, + customer_id INTEGER NOT NULL, + chat_id INTEGER NOT NULL, + status TEXT NOT NULL, + started_at TEXT NOT NULL, + ended_at TEXT, + last_message_at TEXT, + message_count INTEGER DEFAULT 0, + assigned_to INTEGER, + tags TEXT, + notes TEXT, + metadata TEXT, + FOREIGN KEY (customer_id) REFERENCES customers (user_id) + ) + """) + + # 创建索引 + cursor.execute("CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id)") + cursor.execute("CREATE INDEX IF NOT EXISTS idx_messages_user ON messages(user_id)") + cursor.execute("CREATE INDEX IF NOT EXISTS idx_sessions_customer ON sessions(customer_id)") + cursor.execute("CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status)") + + self.connection.commit() + + @asynccontextmanager + async def transaction(self): + """事务上下文管理器""" + async with self._lock: + cursor = self.connection.cursor() + try: + yield cursor + self.connection.commit() + except Exception as e: + self.connection.rollback() + logger.error(f"Transaction failed: {e}") + raise DatabaseError(f"Transaction failed: {e}") + + async def save_customer(self, customer: Customer) -> bool: + """保存客户""" + async with self.transaction() as cursor: + cursor.execute(""" + INSERT OR REPLACE INTO customers ( + user_id, username, first_name, last_name, language_code, + status, created_at, updated_at, metadata, tags, notes + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, ( + customer.user_id, + customer.username, + customer.first_name, + customer.last_name, + customer.language_code, + customer.status.value, + customer.created_at.isoformat(), + customer.updated_at.isoformat(), + json.dumps(customer.metadata), + json.dumps(customer.tags), + customer.notes + )) + return True + + async def get_customer(self, user_id: int) -> Optional[Customer]: + """获取客户""" + async with self._lock: + cursor = self.connection.cursor() + cursor.execute("SELECT * FROM customers WHERE user_id = ?", (user_id,)) + row = cursor.fetchone() + + if row: + return Customer( + user_id=row['user_id'], + username=row['username'], + first_name=row['first_name'], + last_name=row['last_name'], + language_code=row['language_code'], + status=CustomerStatus(row['status']), + created_at=datetime.fromisoformat(row['created_at']), + updated_at=datetime.fromisoformat(row['updated_at']), + metadata=json.loads(row['metadata'] or '{}'), + tags=json.loads(row['tags'] or '[]'), + notes=row['notes'] + ) + return None + + async def get_all_customers(self, status: Optional[CustomerStatus] = None) -> List[Customer]: + """获取所有客户""" + async with self._lock: + cursor = self.connection.cursor() + + if status: + cursor.execute("SELECT * FROM customers WHERE status = ?", (status.value,)) + else: + cursor.execute("SELECT * FROM customers") + + customers = [] + for row in cursor.fetchall(): + customers.append(Customer( + user_id=row['user_id'], + username=row['username'], + first_name=row['first_name'], + last_name=row['last_name'], + language_code=row['language_code'], + status=CustomerStatus(row['status']), + created_at=datetime.fromisoformat(row['created_at']), + updated_at=datetime.fromisoformat(row['updated_at']), + metadata=json.loads(row['metadata'] or '{}'), + tags=json.loads(row['tags'] or '[]'), + notes=row['notes'] + )) + + return customers + + async def save_message(self, message: Message) -> bool: + """保存消息""" + async with self.transaction() as cursor: + cursor.execute(""" + INSERT INTO messages ( + message_id, session_id, user_id, chat_id, direction, + content, content_type, timestamp, is_read, is_replied, + reply_to_message_id, metadata + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, ( + message.message_id, + message.session_id, + message.user_id, + message.chat_id, + message.direction.value, + message.content, + message.content_type, + message.timestamp.isoformat(), + message.is_read, + message.is_replied, + message.reply_to_message_id, + json.dumps(message.metadata) + )) + + # 更新会话的最后消息时间和消息计数 + cursor.execute(""" + UPDATE sessions + SET last_message_at = ?, message_count = message_count + 1 + WHERE session_id = ? + """, (message.timestamp.isoformat(), message.session_id)) + + return True + + async def get_messages(self, session_id: str, limit: int = 100) -> List[Message]: + """获取会话消息""" + async with self._lock: + cursor = self.connection.cursor() + cursor.execute(""" + SELECT * FROM messages + WHERE session_id = ? + ORDER BY timestamp DESC + LIMIT ? + """, (session_id, limit)) + + messages = [] + for row in cursor.fetchall(): + messages.append(Message( + message_id=row['message_id'], + session_id=row['session_id'], + user_id=row['user_id'], + chat_id=row['chat_id'], + direction=MessageDirection(row['direction']), + content=row['content'], + content_type=row['content_type'], + timestamp=datetime.fromisoformat(row['timestamp']), + is_read=bool(row['is_read']), + is_replied=bool(row['is_replied']), + reply_to_message_id=row['reply_to_message_id'], + metadata=json.loads(row['metadata'] or '{}') + )) + + return list(reversed(messages)) # 返回时间顺序 + + async def save_session(self, session: Session) -> bool: + """保存会话""" + async with self.transaction() as cursor: + cursor.execute(""" + INSERT OR REPLACE INTO sessions ( + session_id, customer_id, chat_id, status, started_at, + ended_at, last_message_at, message_count, assigned_to, + tags, notes, metadata + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, ( + session.session_id, + session.customer_id, + session.chat_id, + session.status.value, + session.started_at.isoformat(), + session.ended_at.isoformat() if session.ended_at else None, + session.last_message_at.isoformat() if session.last_message_at else None, + session.message_count, + session.assigned_to, + json.dumps(session.tags), + session.notes, + json.dumps(session.metadata) + )) + return True + + async def get_session(self, session_id: str) -> Optional[Session]: + """获取会话""" + async with self._lock: + cursor = self.connection.cursor() + cursor.execute("SELECT * FROM sessions WHERE session_id = ?", (session_id,)) + row = cursor.fetchone() + + if row: + return Session( + session_id=row['session_id'], + customer_id=row['customer_id'], + chat_id=row['chat_id'], + status=SessionStatus(row['status']), + started_at=datetime.fromisoformat(row['started_at']), + ended_at=datetime.fromisoformat(row['ended_at']) if row['ended_at'] else None, + last_message_at=datetime.fromisoformat(row['last_message_at']) if row['last_message_at'] else None, + message_count=row['message_count'], + assigned_to=row['assigned_to'], + tags=json.loads(row['tags'] or '[]'), + notes=row['notes'], + metadata=json.loads(row['metadata'] or '{}') + ) + return None + + async def get_active_sessions(self) -> List[Session]: + """获取活跃会话""" + async with self._lock: + cursor = self.connection.cursor() + cursor.execute(""" + SELECT * FROM sessions + WHERE status = ? + ORDER BY last_message_at DESC + """, (SessionStatus.ACTIVE.value,)) + + sessions = [] + for row in cursor.fetchall(): + sessions.append(Session( + session_id=row['session_id'], + customer_id=row['customer_id'], + chat_id=row['chat_id'], + status=SessionStatus(row['status']), + started_at=datetime.fromisoformat(row['started_at']), + ended_at=datetime.fromisoformat(row['ended_at']) if row['ended_at'] else None, + last_message_at=datetime.fromisoformat(row['last_message_at']) if row['last_message_at'] else None, + message_count=row['message_count'], + assigned_to=row['assigned_to'], + tags=json.loads(row['tags'] or '[]'), + notes=row['notes'], + metadata=json.loads(row['metadata'] or '{}') + )) + + return sessions + + async def update_session_status(self, session_id: str, status: SessionStatus) -> bool: + """更新会话状态""" + async with self.transaction() as cursor: + ended_at = None + if status in [SessionStatus.RESOLVED, SessionStatus.CLOSED]: + ended_at = datetime.now().isoformat() + + cursor.execute(""" + UPDATE sessions + SET status = ?, ended_at = ? + WHERE session_id = ? + """, (status.value, ended_at, session_id)) + + return cursor.rowcount > 0 + + async def get_statistics(self) -> Dict[str, Any]: + """获取统计信息""" + async with self._lock: + cursor = self.connection.cursor() + + # 客户统计 + cursor.execute("SELECT COUNT(*) as count FROM customers") + total_customers = cursor.fetchone()['count'] + + cursor.execute("SELECT COUNT(*) as count FROM customers WHERE status = ?", + (CustomerStatus.ACTIVE.value,)) + active_customers = cursor.fetchone()['count'] + + # 会话统计 + cursor.execute("SELECT COUNT(*) as count FROM sessions") + total_sessions = cursor.fetchone()['count'] + + cursor.execute("SELECT COUNT(*) as count FROM sessions WHERE status = ?", + (SessionStatus.ACTIVE.value,)) + active_sessions = cursor.fetchone()['count'] + + # 消息统计 + cursor.execute("SELECT COUNT(*) as count FROM messages") + total_messages = cursor.fetchone()['count'] + + cursor.execute(""" + SELECT COUNT(*) as count FROM messages + WHERE direction = ? AND is_replied = 0 + """, (MessageDirection.INBOUND.value,)) + unreplied_messages = cursor.fetchone()['count'] + + return { + 'customers': { + 'total': total_customers, + 'active': active_customers + }, + 'sessions': { + 'total': total_sessions, + 'active': active_sessions + }, + 'messages': { + 'total': total_messages, + 'unreplied': unreplied_messages + } + } + + async def cleanup_old_sessions(self, days: int = 30): + """清理旧会话""" + async with self.transaction() as cursor: + cutoff_date = datetime.now().timestamp() - (days * 24 * 60 * 60) + cutoff_date_str = datetime.fromtimestamp(cutoff_date).isoformat() + + cursor.execute(""" + DELETE FROM messages + WHERE session_id IN ( + SELECT session_id FROM sessions + WHERE ended_at < ? AND status IN (?, ?) + ) + """, (cutoff_date_str, SessionStatus.RESOLVED.value, SessionStatus.CLOSED.value)) + + cursor.execute(""" + DELETE FROM sessions + WHERE ended_at < ? AND status IN (?, ?) + """, (cutoff_date_str, SessionStatus.RESOLVED.value, SessionStatus.CLOSED.value)) + + logger.info(f"Cleaned up sessions older than {days} days") + + def close(self): + """关闭数据库连接""" + if self.connection: + self.connection.close() + logger.info("Database connection closed") \ No newline at end of file diff --git a/src/modules/storage/models.py b/src/modules/storage/models.py new file mode 100644 index 0000000..dcde5bf --- /dev/null +++ b/src/modules/storage/models.py @@ -0,0 +1,154 @@ +"""数据模型""" +from dataclasses import dataclass, field +from typing import Optional, List, Dict, Any +from datetime import datetime +from enum import Enum + + +class CustomerStatus(Enum): + """客户状态""" + ACTIVE = "active" + INACTIVE = "inactive" + BLOCKED = "blocked" + + +class SessionStatus(Enum): + """会话状态""" + ACTIVE = "active" + PENDING = "pending" + RESOLVED = "resolved" + CLOSED = "closed" + + +class MessageDirection(Enum): + """消息方向""" + INBOUND = "inbound" # 客户发送 + OUTBOUND = "outbound" # 管理员发送 + + +@dataclass +class Customer: + """客户模型""" + user_id: int + username: Optional[str] + first_name: str + last_name: Optional[str] + language_code: Optional[str] + status: CustomerStatus = CustomerStatus.ACTIVE + created_at: datetime = field(default_factory=datetime.now) + updated_at: datetime = field(default_factory=datetime.now) + metadata: Dict[str, Any] = field(default_factory=dict) + tags: List[str] = field(default_factory=list) + notes: Optional[str] = None + + @property + def full_name(self) -> str: + """获取全名""" + parts = [self.first_name] + if self.last_name: + parts.append(self.last_name) + return " ".join(parts) + + @property + def display_name(self) -> str: + """获取显示名称""" + if self.username: + return f"@{self.username}" + return self.full_name + + def to_dict(self) -> Dict[str, Any]: + """转换为字典""" + return { + 'user_id': self.user_id, + 'username': self.username, + 'first_name': self.first_name, + 'last_name': self.last_name, + 'language_code': self.language_code, + 'status': self.status.value, + 'created_at': self.created_at.isoformat(), + 'updated_at': self.updated_at.isoformat(), + 'metadata': self.metadata, + 'tags': self.tags, + 'notes': self.notes + } + + +@dataclass +class Message: + """消息模型""" + message_id: str + session_id: str + user_id: int + chat_id: int + direction: MessageDirection + content: str + content_type: str # text, photo, document, voice, etc. + timestamp: datetime = field(default_factory=datetime.now) + is_read: bool = False + is_replied: bool = False + reply_to_message_id: Optional[str] = None + metadata: Dict[str, Any] = field(default_factory=dict) + + def to_dict(self) -> Dict[str, Any]: + """转换为字典""" + return { + 'message_id': self.message_id, + 'session_id': self.session_id, + 'user_id': self.user_id, + 'chat_id': self.chat_id, + 'direction': self.direction.value, + 'content': self.content, + 'content_type': self.content_type, + 'timestamp': self.timestamp.isoformat(), + 'is_read': self.is_read, + 'is_replied': self.is_replied, + 'reply_to_message_id': self.reply_to_message_id, + 'metadata': self.metadata + } + + +@dataclass +class Session: + """会话模型""" + session_id: str + customer_id: int + chat_id: int + status: SessionStatus = SessionStatus.ACTIVE + started_at: datetime = field(default_factory=datetime.now) + ended_at: Optional[datetime] = None + last_message_at: Optional[datetime] = None + message_count: int = 0 + assigned_to: Optional[int] = None # 分配给哪个管理员 + tags: List[str] = field(default_factory=list) + notes: Optional[str] = None + metadata: Dict[str, Any] = field(default_factory=dict) + + @property + def duration(self) -> Optional[float]: + """获取会话时长(秒)""" + if self.ended_at: + return (self.ended_at - self.started_at).total_seconds() + return (datetime.now() - self.started_at).total_seconds() + + @property + def is_active(self) -> bool: + """是否活跃""" + return self.status == SessionStatus.ACTIVE + + def to_dict(self) -> Dict[str, Any]: + """转换为字典""" + return { + 'session_id': self.session_id, + 'customer_id': self.customer_id, + 'chat_id': self.chat_id, + 'status': self.status.value, + 'started_at': self.started_at.isoformat(), + 'ended_at': self.ended_at.isoformat() if self.ended_at else None, + 'last_message_at': self.last_message_at.isoformat() if self.last_message_at else None, + 'message_count': self.message_count, + 'assigned_to': self.assigned_to, + 'tags': self.tags, + 'notes': self.notes, + 'metadata': self.metadata, + 'duration': self.duration + } \ No newline at end of file diff --git a/src/utils/__init__.py b/src/utils/__init__.py new file mode 100644 index 0000000..f63fdcc --- /dev/null +++ b/src/utils/__init__.py @@ -0,0 +1,6 @@ +"""工具模块""" +from .logger import Logger, get_logger +from .exceptions import * +from .decorators import * + +__all__ = ['Logger', 'get_logger'] \ No newline at end of file diff --git a/src/utils/decorators.py b/src/utils/decorators.py new file mode 100644 index 0000000..a3cc881 --- /dev/null +++ b/src/utils/decorators.py @@ -0,0 +1,233 @@ +"""装饰器工具""" +import functools +import time +import asyncio +from typing import Callable, Any, Optional, Dict +from datetime import datetime, timedelta +from collections import defaultdict +from .logger import get_logger +from .exceptions import RateLimitError, AuthorizationError, ValidationError + + +logger = get_logger(__name__) + + +def async_retry(max_attempts: int = 3, delay: float = 1.0, backoff: float = 2.0, + exceptions: tuple = (Exception,)): + """异步重试装饰器""" + def decorator(func: Callable) -> Callable: + @functools.wraps(func) + async def wrapper(*args, **kwargs): + attempt = 1 + current_delay = delay + + while attempt <= max_attempts: + try: + return await func(*args, **kwargs) + except exceptions as e: + if attempt == max_attempts: + logger.error(f"Max retries ({max_attempts}) reached for {func.__name__}") + raise + + logger.warning( + f"Attempt {attempt}/{max_attempts} failed for {func.__name__}: {e}. " + f"Retrying in {current_delay:.2f}s..." + ) + await asyncio.sleep(current_delay) + current_delay *= backoff + attempt += 1 + + return wrapper + return decorator + + +def rate_limit(max_calls: int, period: float): + """速率限制装饰器""" + def decorator(func: Callable) -> Callable: + calls = defaultdict(list) + + @functools.wraps(func) + async def wrapper(*args, **kwargs): + # 获取调用者标识(假设第一个参数是 self,第二个是 update) + caller_id = None + if len(args) >= 2 and hasattr(args[1], 'effective_user'): + caller_id = args[1].effective_user.id + else: + caller_id = 'global' + + now = time.time() + calls[caller_id] = [t for t in calls[caller_id] if now - t < period] + + if len(calls[caller_id]) >= max_calls: + raise RateLimitError( + f"Rate limit exceeded: {max_calls} calls per {period} seconds", + details={'caller_id': caller_id, 'limit': max_calls, 'period': period} + ) + + calls[caller_id].append(now) + return await func(*args, **kwargs) + + return wrapper + return decorator + + +def require_admin(func: Callable) -> Callable: + """需要管理员权限装饰器""" + @functools.wraps(func) + async def wrapper(self, update, context, *args, **kwargs): + user_id = update.effective_user.id + if user_id != self.config.telegram.admin_id: + if not (hasattr(self, 'is_admin') and await self.is_admin(user_id)): + raise AuthorizationError( + "Admin privileges required", + details={'user_id': user_id} + ) + return await func(self, update, context, *args, **kwargs) + return wrapper + + +def log_action(action_type: str = None): + """记录操作日志装饰器""" + def decorator(func: Callable) -> Callable: + @functools.wraps(func) + async def wrapper(*args, **kwargs): + start_time = time.time() + action = action_type or func.__name__ + + # 提取用户信息 + user_info = {} + if len(args) >= 2 and hasattr(args[1], 'effective_user'): + user = args[1].effective_user + user_info = { + 'user_id': user.id, + 'username': user.username, + 'name': user.first_name + } + + try: + result = await func(*args, **kwargs) + duration = time.time() - start_time + + # 创建额外信息,避免覆盖保留字段 + extra_info = { + 'action': action, + 'duration': duration, + 'status': 'success' + } + # 添加用户信息,使用前缀避免冲突 + for k, v in user_info.items(): + extra_info[f'user_{k}' if k in ['name'] else k] = v + + logger.info( + f"Action completed: {action}", + extra=extra_info + ) + return result + + except Exception as e: + duration = time.time() - start_time + + # 创建额外信息,避免覆盖保留字段 + extra_info = { + 'action': action, + 'duration': duration, + 'status': 'failed', + 'error': str(e) + } + # 添加用户信息,使用前缀避免冲突 + for k, v in user_info.items(): + extra_info[f'user_{k}' if k in ['name'] else k] = v + + logger.error( + f"Action failed: {action}", + extra=extra_info + ) + raise + + return wrapper + return decorator + + +def validate_input(**validators): + """输入验证装饰器""" + def decorator(func: Callable) -> Callable: + @functools.wraps(func) + async def wrapper(*args, **kwargs): + # 合并位置参数和关键字参数 + bound_args = func.__code__.co_varnames[:func.__code__.co_argcount] + all_args = dict(zip(bound_args, args)) + all_args.update(kwargs) + + # 执行验证 + for param_name, validator in validators.items(): + if param_name in all_args: + value = all_args[param_name] + if not validator(value): + raise ValidationError( + f"Validation failed for parameter: {param_name}", + details={'parameter': param_name, 'value': value} + ) + + return await func(*args, **kwargs) + return wrapper + return decorator + + +def cache_result(ttl: int = 300): + """结果缓存装饰器""" + def decorator(func: Callable) -> Callable: + cache: Dict[str, tuple[Any, datetime]] = {} + + @functools.wraps(func) + async def wrapper(*args, **kwargs): + # 创建缓存键 + cache_key = f"{func.__name__}:{str(args)}:{str(kwargs)}" + + # 检查缓存 + if cache_key in cache: + result, timestamp = cache[cache_key] + if datetime.now() - timestamp < timedelta(seconds=ttl): + logger.debug(f"Cache hit for {func.__name__}") + return result + + # 执行函数 + result = await func(*args, **kwargs) + + # 存储结果 + cache[cache_key] = (result, datetime.now()) + logger.debug(f"Cache miss for {func.__name__}, cached for {ttl}s") + + return result + + # 添加清除缓存方法 + wrapper.clear_cache = lambda: cache.clear() + return wrapper + return decorator + + +def measure_performance(func: Callable) -> Callable: + """性能测量装饰器""" + @functools.wraps(func) + async def wrapper(*args, **kwargs): + start_time = time.perf_counter() + start_memory = 0 # 可以添加内存测量 + + try: + result = await func(*args, **kwargs) + return result + finally: + end_time = time.perf_counter() + duration = end_time - start_time + + if duration > 1.0: # 超过1秒的操作记录警告 + logger.warning( + f"Slow operation detected: {func.__name__} took {duration:.2f}s", + extra={ + 'function': func.__name__, + 'duration': duration + } + ) + else: + logger.debug(f"{func.__name__} completed in {duration:.4f}s") + + return wrapper \ No newline at end of file diff --git a/src/utils/exceptions.py b/src/utils/exceptions.py new file mode 100644 index 0000000..4001f61 --- /dev/null +++ b/src/utils/exceptions.py @@ -0,0 +1,122 @@ +"""自定义异常类""" +from typing import Optional, Any, Dict + + +class BotException(Exception): + """机器人基础异常""" + + def __init__(self, message: str, code: str = None, details: Dict[str, Any] = None): + super().__init__(message) + self.message = message + self.code = code or self.__class__.__name__ + self.details = details or {} + + def to_dict(self) -> Dict[str, Any]: + """转换为字典""" + return { + 'error': self.code, + 'message': self.message, + 'details': self.details + } + + +class ConfigurationError(BotException): + """配置错误""" + pass + + +class DatabaseError(BotException): + """数据库错误""" + pass + + +class TelegramError(BotException): + """Telegram API 错误""" + pass + + +class AuthenticationError(BotException): + """认证错误""" + pass + + +class AuthorizationError(BotException): + """授权错误""" + pass + + +class ValidationError(BotException): + """验证错误""" + pass + + +class RateLimitError(BotException): + """速率限制错误""" + pass + + +class SessionError(BotException): + """会话错误""" + pass + + +class MessageRoutingError(BotException): + """消息路由错误""" + pass + + +class BusinessLogicError(BotException): + """业务逻辑错误""" + pass + + +class ExternalServiceError(BotException): + """外部服务错误""" + pass + + +class ErrorHandler: + """错误处理器""" + + @staticmethod + async def handle_error(error: Exception, context: Dict[str, Any] = None) -> Dict[str, Any]: + """处理错误""" + from ..utils.logger import get_logger + logger = get_logger(__name__) + + error_info = { + 'type': type(error).__name__, + 'message': str(error), + 'context': context or {} + } + + if isinstance(error, BotException): + # 自定义异常 + error_info.update(error.to_dict()) + logger.error(f"Bot error: {error.message}", extra={'error_details': error_info}) + else: + # 未知异常 + logger.exception(f"Unexpected error: {error}", extra={'error_details': error_info}) + error_info['message'] = "An unexpected error occurred" + + return error_info + + @staticmethod + def create_user_message(error: Exception) -> str: + """创建用户友好的错误消息""" + if isinstance(error, AuthenticationError): + return "❌ 认证失败,请重新登录" + elif isinstance(error, AuthorizationError): + return "❌ 您没有权限执行此操作" + elif isinstance(error, ValidationError): + return f"❌ 输入无效:{error.message}" + elif isinstance(error, RateLimitError): + return "⚠️ 操作太频繁,请稍后再试" + elif isinstance(error, SessionError): + return "❌ 会话已过期,请重新开始" + elif isinstance(error, BusinessLogicError): + return f"❌ 操作失败:{error.message}" + elif isinstance(error, ExternalServiceError): + return "❌ 外部服务暂时不可用,请稍后再试" + else: + return "❌ 系统错误,请稍后再试或联系管理员" \ No newline at end of file diff --git a/src/utils/logger.py b/src/utils/logger.py new file mode 100644 index 0000000..b1fd943 --- /dev/null +++ b/src/utils/logger.py @@ -0,0 +1,167 @@ +"""日志系统""" +import logging +import sys +from logging.handlers import RotatingFileHandler +from typing import Optional +from pathlib import Path +import json +from datetime import datetime + + +class CustomFormatter(logging.Formatter): + """自定义日志格式化器""" + + # 颜色代码 + COLORS = { + 'DEBUG': '\033[36m', # 青色 + 'INFO': '\033[32m', # 绿色 + 'WARNING': '\033[33m', # 黄色 + 'ERROR': '\033[31m', # 红色 + 'CRITICAL': '\033[35m', # 紫色 + } + RESET = '\033[0m' + + def __init__(self, use_color: bool = True): + super().__init__() + self.use_color = use_color and sys.stderr.isatty() + + def format(self, record): + # 基础格式 + log_format = "%(asctime)s | %(levelname)-8s | %(name)s | %(message)s" + + # 添加额外信息 + if hasattr(record, 'user_id'): + log_format = f"%(asctime)s | %(levelname)-8s | %(name)s | User:{record.user_id} | %(message)s" + + if hasattr(record, 'chat_id'): + log_format = f"%(asctime)s | %(levelname)-8s | %(name)s | Chat:{record.chat_id} | %(message)s" + + # 应用颜色 + if self.use_color: + levelname = record.levelname + if levelname in self.COLORS: + log_format = log_format.replace( + '%(levelname)-8s', + f"{self.COLORS[levelname]}%(levelname)-8s{self.RESET}" + ) + + formatter = logging.Formatter(log_format, datefmt='%Y-%m-%d %H:%M:%S') + return formatter.format(record) + + +class JsonFormatter(logging.Formatter): + """JSON 格式化器用于结构化日志""" + + def format(self, record): + log_data = { + 'timestamp': datetime.utcnow().isoformat(), + 'level': record.levelname, + 'logger': record.name, + 'message': record.getMessage(), + 'module': record.module, + 'function': record.funcName, + 'line': record.lineno + } + + # 添加额外字段 + for key, value in record.__dict__.items(): + if key not in ['name', 'msg', 'args', 'created', 'filename', + 'funcName', 'levelname', 'levelno', 'lineno', + 'module', 'msecs', 'message', 'pathname', 'process', + 'processName', 'relativeCreated', 'thread', 'threadName']: + log_data[key] = value + + # 添加异常信息 + if record.exc_info: + log_data['exception'] = self.formatException(record.exc_info) + + return json.dumps(log_data, ensure_ascii=False) + + +class Logger: + """日志管理器""" + + _instance = None + _loggers = {} + + def __new__(cls, *args, **kwargs): + if not cls._instance: + cls._instance = super().__new__(cls) + return cls._instance + + def __init__(self, config=None): + if not hasattr(self, '_initialized'): + self._initialized = True + self.config = config + self.setup_logging() + + def setup_logging(self): + """设置日志系统""" + # 根日志配置 + root_logger = logging.getLogger() + root_logger.setLevel(logging.DEBUG) + + # 移除默认处理器 + root_logger.handlers = [] + + # 控制台处理器 + console_handler = logging.StreamHandler(sys.stdout) + console_handler.setLevel( + getattr(logging, self.config.logging.level if self.config else 'INFO') + ) + console_handler.setFormatter(CustomFormatter(use_color=True)) + root_logger.addHandler(console_handler) + + # 文件处理器 + if self.config and self.config.logging.file: + file_path = Path(self.config.logging.file) + file_path.parent.mkdir(parents=True, exist_ok=True) + + file_handler = RotatingFileHandler( + filename=str(file_path), + maxBytes=self.config.logging.max_size, + backupCount=self.config.logging.backup_count, + encoding='utf-8' + ) + file_handler.setLevel(logging.DEBUG) + file_handler.setFormatter(CustomFormatter(use_color=False)) + root_logger.addHandler(file_handler) + + # JSON 日志文件(用于分析) + json_file_path = file_path.with_suffix('.json') + json_handler = RotatingFileHandler( + filename=str(json_file_path), + maxBytes=self.config.logging.max_size, + backupCount=self.config.logging.backup_count, + encoding='utf-8' + ) + json_handler.setLevel(logging.INFO) + json_handler.setFormatter(JsonFormatter()) + root_logger.addHandler(json_handler) + + @classmethod + def get_logger(cls, name: str, config=None) -> logging.Logger: + """获取日志器""" + if name not in cls._loggers: + if not cls._instance: + cls(config) + cls._loggers[name] = logging.getLogger(name) + return cls._loggers[name] + + +def get_logger(name: str, config=None) -> logging.Logger: + """获取日志器的便捷方法""" + return Logger.get_logger(name, config) + + +class LoggerContextFilter(logging.Filter): + """日志上下文过滤器""" + + def __init__(self, **context): + super().__init__() + self.context = context + + def filter(self, record): + for key, value in self.context.items(): + setattr(record, key, value) + return True \ No newline at end of file diff --git a/start_bot.sh b/start_bot.sh new file mode 100755 index 0000000..c097cf8 --- /dev/null +++ b/start_bot.sh @@ -0,0 +1,13 @@ +#!/bin/bash +source ~/.bashrc +export ALL_PROXY=socks5://127.0.0.1:1080 +export ANTHROPIC_API_KEY="$ANTHROPIC_AUTH_TOKEN" + +echo "环境变量检查:" +echo "ANTHROPIC_AUTH_TOKEN: ${ANTHROPIC_AUTH_TOKEN:0:20}..." +echo "ANTHROPIC_BASE_URL: $ANTHROPIC_BASE_URL" +echo "ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:0:20}..." +echo "ALL_PROXY: $ALL_PROXY" +echo "" + +python3 integrated_bot_ai.py 2>&1 | tee bot_running.log diff --git a/start_bot_final.sh b/start_bot_final.sh new file mode 100755 index 0000000..efc1383 --- /dev/null +++ b/start_bot_final.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +# 直接设置环境变量(不依赖bashrc) +export ANTHROPIC_AUTH_TOKEN='cr_6054f2a49ea9e2e848b955cc65be8648df80c6476c9f3cf1164628ac5fb4f896' +export ANTHROPIC_BASE_URL='http://202.79.167.23:3000/api/' +export ALL_PROXY='socks5://127.0.0.1:1080' + +echo "=== 环境变量已设置 ===" +echo "ANTHROPIC_AUTH_TOKEN: ${ANTHROPIC_AUTH_TOKEN:0:30}..." +echo "ANTHROPIC_BASE_URL: $ANTHROPIC_BASE_URL" +echo "ALL_PROXY: $ALL_PROXY" +echo "" + +# 清理缓存 +find . -type d -name '__pycache__' -exec rm -rf {} + 2>/dev/null +find . -name '*.pyc' -delete 2>/dev/null + +# 启动Bot +python3 -u integrated_bot_ai.py 2>&1 | tee bot_running.log diff --git a/start_bot_fixed.sh b/start_bot_fixed.sh new file mode 100755 index 0000000..aaf6ed0 --- /dev/null +++ b/start_bot_fixed.sh @@ -0,0 +1,21 @@ +#\!/bin/bash + +cd /home/atai/telegram-bot + +# 加载.env文件中的环境变量 +export $(grep -v "^#" .env | grep -v "^$" | xargs) + +# 设置代理 +export ALL_PROXY=socks5://127.0.0.1:1080 + +echo "=== 环境变量检查 ===" +echo "ANTHROPIC_AUTH_TOKEN: ${ANTHROPIC_AUTH_TOKEN:0:30}..." +echo "ANTHROPIC_BASE_URL: $ANTHROPIC_BASE_URL" +echo "BOT_TOKEN: ${BOT_TOKEN:0:30}..." +echo "========================" + +# 启动bot +screen -dmS agent_bot bash -c "python3 -u integrated_bot_ai.py 2>&1 | tee bot_agent_sdk.log" + +echo "Bot已在screen会话中启动" +echo "使用 screen -r agent_bot 查看日志" diff --git a/start_bot_v2.sh b/start_bot_v2.sh new file mode 100755 index 0000000..08e9554 --- /dev/null +++ b/start_bot_v2.sh @@ -0,0 +1,18 @@ +#!/bin/bash +source ~/.bashrc + +# 显示环境变量 +echo "=== 环境变量检查 ===" +echo "ANTHROPIC_AUTH_TOKEN: ${ANTHROPIC_AUTH_TOKEN:0:30}..." +echo "ANTHROPIC_BASE_URL: $ANTHROPIC_BASE_URL" +echo "" + +# 清理Python缓存 +find . -type d -name '__pycache__' -exec rm -rf {} + 2>/dev/null +find . -name '*.pyc' -delete 2>/dev/null + +# 设置代理 +export ALL_PROXY=socks5://127.0.0.1:1080 + +# 启动Bot +python3 -u integrated_bot_ai.py 2>&1 | tee bot_running.log diff --git a/start_integrated.sh b/start_integrated.sh new file mode 100644 index 0000000..e0c3245 --- /dev/null +++ b/start_integrated.sh @@ -0,0 +1,23 @@ +#!/bin/bash +# 启动整合版机器人脚本 + +# 设置代理 +export ALL_PROXY=socks5://127.0.0.1:1080 +export export HTTP_PROXY=socks5://127.0.0.1:1080 + +echo "🔄 停止现有机器人进程..." +pkill -f "python3.*bot" 2>/dev/null +sleep 2 + +echo "✅ 代理已配置: 127.0.0.1:8118" +echo "🚀 启动整合版机器人..." + +# 使用screen启动 +screen -dmS telegram_bot python3 integrated_bot.py + +echo "✅ 机器人已在后台启动!" +echo "" +echo "使用以下命令管理:" +echo "- 查看日志: screen -r telegram_bot" +echo "- 退出查看: Ctrl+A 然后按 D" +echo "- 停止机器人: screen -X -S telegram_bot quit" \ No newline at end of file diff --git a/start_v3.sh b/start_v3.sh new file mode 100755 index 0000000..f6e7793 --- /dev/null +++ b/start_v3.sh @@ -0,0 +1,18 @@ +#\!/bin/bash +cd /home/atai/telegram-bot + +# 加载环境变量 +export $(grep -v "^#" .env | grep -v "^$" | xargs) +export ALL_PROXY=socks5://127.0.0.1:1080 + +echo "=== Bot V3 启动 ===" +echo "ANTHROPIC_AUTH_TOKEN: ${ANTHROPIC_AUTH_TOKEN:0:30}..." +echo "BOT_TOKEN: ${BOT_TOKEN:0:30}..." +echo "===================" + +# 启动V3 +screen -dmS bot_v3 bash -c "python3 -u bot_v3.py 2>&1 | tee bot_v3.log" + +echo "✅ Bot V3 已启动" +echo "查看日志: tail -f ~/telegram-bot/bot_v3.log" +echo "进入screen: screen -r bot_v3" diff --git a/test_agent_response.py b/test_agent_response.py new file mode 100644 index 0000000..a95c8a2 --- /dev/null +++ b/test_agent_response.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 +import asyncio +import os +from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions + +async def test_agent(): + # 设置环境 + os.environ['ANTHROPIC_API_KEY'] = os.environ.get('ANTHROPIC_AUTH_TOKEN', '') + + # 配置选项 + options = ClaudeAgentOptions( + system_prompt="你是一个友好的助手,请用中文回复。" + ) + + # 测试对话 + async with ClaudeSDKClient(options=options) as client: + print('发送查询...') + await client.query('你好,1+1等于几?请简短回复。') + + print('接收响应...') + full_response = [] + async for message in client.receive_response(): + print(f'收到: {message}') + full_response.append(str(message)) + + print(f'\n完整响应: {"".join(full_response)}') + +if __name__ == '__main__': + asyncio.run(test_agent()) diff --git a/test_agent_sdk.py b/test_agent_sdk.py new file mode 100644 index 0000000..115b06b --- /dev/null +++ b/test_agent_sdk.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import os +import asyncio +from claude_agent_sdk import ClaudeSDKClient + +# 设置 API key +os.environ['ANTHROPIC_API_KEY'] = 'cr_9792f20a98f055e204248a41f280780ca2fb8f08f35e60c785e5245653937e06' + +async def test_agent_sdk(): + print('🔧 测试 claude-agent-sdk 配置...') + + try: + # 初始化客户端 + client = ClaudeSDKClient() + print('✅ Claude SDK 客户端初始化成功') + + # 连接 + await client.connect() + print('✅ 连接成功') + + # 获取服务器信息 + info = await client.get_server_info() + print(f'✅ 服务器信息: {info}') + + # 测试对话 + await client.query('你好,请简短回复测试成功') + print(f'✅ 查询已发送') + + # 接收响应 + full_response = '' + async for chunk in client.receive_response(): + if hasattr(chunk, 'text'): + full_response += chunk.text + elif isinstance(chunk, dict) and 'text' in chunk: + full_response += chunk['text'] + else: + full_response += str(chunk) + + print(f'✅ 对话测试成功') + print(f'📝 AI回复: {full_response}') + + await client.disconnect() + print('✅ 断开连接成功') + + return True + + except Exception as e: + print(f'❌ 测试失败: {e}') + import traceback + traceback.print_exc() + return False + +if __name__ == '__main__': + result = asyncio.run(test_agent_sdk()) + if result: + print('\n🎉 claude-agent-sdk 配置成功!') + else: + print('\n❌ 配置失败') diff --git a/test_claude_api.py b/test_claude_api.py new file mode 100644 index 0000000..8c27f31 --- /dev/null +++ b/test_claude_api.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +import os +import anthropic + +# 加载环境变量 +with open("/home/atai/telegram-bot/.env") as f: + for line in f: + line = line.strip() + if line and not line.startswith("#") and "=" in line: + key, value = line.split("=", 1) + os.environ[key] = value + +print("环境变量检查:") +print(f"ANTHROPIC_AUTH_TOKEN: {os.environ.get('ANTHROPIC_AUTH_TOKEN', '未设置')[:30]}...") +print(f"ANTHROPIC_BASE_URL: {os.environ.get('ANTHROPIC_BASE_URL', '未设置')}") + +# 初始化客户端 +try: + client = anthropic.Anthropic( + api_key=os.environ.get('ANTHROPIC_AUTH_TOKEN'), + base_url=os.environ.get('ANTHROPIC_BASE_URL', 'https://api.anthropic.com') + ) + print("\n✅ Claude客户端初始化成功") + + # 测试API调用 + print("\n测试API调用...") + response = client.messages.create( + model="claude-3-5-sonnet-20241022", + max_tokens=100, + messages=[ + {"role": "user", "content": "你好,请用一句话介绍自己"} + ] + ) + + print(f"\n✅ API调用成功!") + print(f"响应: {response.content[0].text}") + +except Exception as e: + print(f"\n❌ 错误: {e}") diff --git a/test_claude_api2.py b/test_claude_api2.py new file mode 100644 index 0000000..7fabfee --- /dev/null +++ b/test_claude_api2.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 +import os +import anthropic + +# 加载环境变量 +with open("/home/atai/telegram-bot/.env") as f: + for line in f: + line = line.strip() + if line and not line.startswith("#") and "=" in line: + key, value = line.split("=", 1) + os.environ[key] = value + +# 初始化客户端 +try: + client = anthropic.Anthropic( + api_key=os.environ.get('ANTHROPIC_AUTH_TOKEN'), + base_url=os.environ.get('ANTHROPIC_BASE_URL', 'https://api.anthropic.com') + ) + + # 尝试不同的模型名称 + models = [ + "claude-3-5-sonnet-latest", + "claude-3-5-sonnet-20240620", + "claude-3-sonnet-20240229", + "claude-3-opus-20240229" + ] + + for model in models: + try: + print(f"\n测试模型: {model}") + response = client.messages.create( + model=model, + max_tokens=50, + messages=[{"role": "user", "content": "Hi"}] + ) + print(f"✅ {model} 成功!") + print(f"响应: {response.content[0].text[:100]}") + break + except Exception as e: + print(f"❌ {model} 失败: {str(e)[:100]}") + +except Exception as e: + print(f"\n❌ 错误: {e}") diff --git a/test_claude_api3.py b/test_claude_api3.py new file mode 100644 index 0000000..aa2ce34 --- /dev/null +++ b/test_claude_api3.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 +import os +import anthropic + +# 加载环境变量 +with open("/home/atai/telegram-bot/.env") as f: + for line in f: + line = line.strip() + if line and not line.startswith("#") and "=" in line: + key, value = line.split("=", 1) + os.environ[key] = value + +try: + client = anthropic.Anthropic( + api_key=os.environ.get('ANTHROPIC_AUTH_TOKEN'), + base_url=os.environ.get('ANTHROPIC_BASE_URL', 'https://api.anthropic.com') + ) + + print("测试模型: claude-sonnet-4-20250514") + response = client.messages.create( + model="claude-sonnet-4-20250514", + max_tokens=100, + messages=[{"role": "user", "content": "你好,请用一句话介绍自己"}] + ) + print(f"✅ API调用成功!") + print(f"响应: {response.content[0].text}") + +except Exception as e: + print(f"❌ 错误: {e}") diff --git a/test_with_proxy.py b/test_with_proxy.py new file mode 100644 index 0000000..900fe5c --- /dev/null +++ b/test_with_proxy.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +import os +import asyncio +from claude_agent_sdk import ClaudeSDKClient + +# 设置环境变量 - 包括 BASE_URL +os.environ['ANTHROPIC_AUTH_TOKEN'] = 'cr_9792f20a98f055e204248a41f280780ca2fb8f08f35e60c785e5245653937e06' +os.environ['ANTHROPIC_BASE_URL'] = 'http://202.79.167.23:3000/api/' + +async def test(): + print('Testing with BASE_URL proxy...') + try: + client = ClaudeSDKClient() + await client.connect() + info = await client.get_server_info() + print('Account info:', info.get('account')) + + await client.query('你好,请简单回复') + + response_text = '' + async for chunk in client.receive_response(): + chunk_str = str(chunk) + if 'AssistantMessage' in chunk_str and 'text=' in chunk_str: + import re + match = re.search(r"text='([^']*)'", chunk_str) + if match: + response_text += match.group(1) + + print('AI Reply:', response_text if response_text else 'No text extracted') + await client.disconnect() + return 'Error' not in response_text and 'forbidden' not in response_text + + except Exception as e: + print('Failed:', e) + import traceback + traceback.print_exc() + return False + +if __name__ == '__main__': + result = asyncio.run(test()) + print('\nResult:', 'SUCCESS!' if result else 'FAILED') diff --git a/test_with_token.py b/test_with_token.py new file mode 100644 index 0000000..77b5097 --- /dev/null +++ b/test_with_token.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +import os +import asyncio +from claude_agent_sdk import ClaudeSDKClient + +os.environ['ANTHROPIC_AUTH_TOKEN'] = 'cr_9792f20a98f055e204248a41f280780ca2fb8f08f35e60c785e5245653937e06' + +async def test(): + print('Testing claude-agent-sdk...') + try: + client = ClaudeSDKClient() + await client.connect() + info = await client.get_server_info() + print('Account info:', info.get('account')) + await client.query('Hello, reply with OK') + response_text = '' + async for chunk in client.receive_response(): + chunk_str = str(chunk) + if 'AssistantMessage' in chunk_str: + import re + match = re.search(r"text='([^']*)'", chunk_str) + if match: + response_text += match.group(1) + if response_text: + print('SUCCESS! AI replied:', response_text) + await client.disconnect() + return True + except Exception as e: + print('FAILED:', e) + import traceback + traceback.print_exc() + return False + +if __name__ == '__main__': + result = asyncio.run(test()) + print('\nResult:', 'SUCCESS' if result else 'FAILED') diff --git a/unified_telegram_bot.py b/unified_telegram_bot.py new file mode 100644 index 0000000..cfba411 --- /dev/null +++ b/unified_telegram_bot.py @@ -0,0 +1,470 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +统一Telegram Bot - 整合所有功能 +- Anthropic SDK直接调用Claude +- Pyrogram镜像搜索@openaiw_bot +- 自动翻页抓取2-10页 +- SQLite缓存管理 +- 智能按钮生成 +""" + +import os +import asyncio +import logging +import re +from datetime import datetime, timedelta +from typing import Dict, List, Optional +from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup +from telegram.ext import Application, CommandHandler, MessageHandler, CallbackQueryHandler, filters, ContextTypes +from pyrogram import Client +from pyrogram.errors import FloodWait +import sqlite3 +import anthropic + +# ===== 配置 ===== +TELEGRAM_TOKEN = "8426529617:AAHAxzohSMFBAxInzbAVJsZfkB5bHnOyFC4" +SEARCH_BOT_USERNAME = "openaiw_bot" + +# Pyrogram配置 +API_ID = 29648923 +API_HASH = "8fd250a5459ebb547c4c3985ad15bd32" +PROXY = {"scheme": "socks5", "hostname": "127.0.0.1", "port": 1080} + +# 日志配置 +logging.basicConfig( + format='%(asctime)s - %(levelname)s - %(message)s', + level=logging.INFO, + handlers=[ + logging.FileHandler('unified_bot.log'), + logging.StreamHandler() + ] +) +logger = logging.getLogger(__name__) + +# ===== 数据库管理 ===== +class Database: + """SQLite缓存数据库""" + + def __init__(self, db_path='cache.db'): + self.db_path = db_path + self.init_db() + + def init_db(self): + """初始化数据库表""" + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + cursor.execute(''' + CREATE TABLE IF NOT EXISTS cache ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + command TEXT, + keyword TEXT, + page INTEGER, + content TEXT, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP + ) + ''') + cursor.execute('CREATE INDEX IF NOT EXISTS idx_search ON cache(command, keyword, page)') + conn.commit() + conn.close() + logger.info("✅ 数据库初始化完成") + + def get_cache(self, command: str, keyword: str, page: int = 1) -> Optional[str]: + """获取缓存结果""" + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + # 检查是否有30天内的缓存 + cursor.execute(''' + SELECT content FROM cache + WHERE command = ? AND keyword = ? AND page = ? + AND timestamp > datetime('now', '-30 days') + ORDER BY timestamp DESC LIMIT 1 + ''', (command, keyword, page)) + + result = cursor.fetchone() + conn.close() + + if result: + logger.info(f"[缓存] 命中: {command} {keyword} 第{page}页") + return result[0] + return None + + def save_cache(self, command: str, keyword: str, page: int, content: str): + """保存缓存""" + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + cursor.execute(''' + INSERT INTO cache (command, keyword, page, content) + VALUES (?, ?, ?, ?) + ''', (command, keyword, page, content)) + + conn.commit() + conn.close() + logger.info(f"[缓存] 已保存: {command} {keyword} 第{page}页") + + def clean_expired(self): + """清理过期缓存(超过30天)""" + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + cursor.execute("DELETE FROM cache WHERE timestamp < datetime('now', '-30 days')") + deleted = cursor.rowcount + + conn.commit() + conn.close() + + if deleted > 0: + logger.info(f"[缓存] 清理了 {deleted} 条过期记录") + +# ===== Pyrogram镜像客户端 ===== +class PyrogramMirror: + """Pyrogram客户端 - 镜像@openaiw_bot""" + + def __init__(self): + self.client = Client( + "user_session", + api_id=API_ID, + api_hash=API_HASH, + proxy=PROXY + ) + self.search_bot = SEARCH_BOT_USERNAME + logger.info("✅ Pyrogram镜像客户端初始化") + + async def start(self): + """启动Pyrogram客户端""" + await self.client.start() + logger.info("✅ Pyrogram客户端已启动") + + async def stop(self): + """停止Pyrogram客户端""" + await self.client.stop() + + async def send_command(self, command: str, keyword: str = "", page: int = 1) -> str: + """ + 发送搜索命令到@openaiw_bot并获取结果 + + Args: + command: 命令类型 (search/text/human/topchat) + keyword: 搜索关键词 + page: 页码 + + Returns: + 搜索结果文本 + """ + try: + # 构建命令 + if command == "topchat": + cmd_text = f"/{command}" + else: + cmd_text = f"/{command} {keyword}" if page == 1 else f"next" + + logger.info(f"[Pyrogram] 发送命令: {cmd_text}") + + # 发送消息 + message = await self.client.send_message(self.search_bot, cmd_text) + + # 等待回复 + await asyncio.sleep(3) + + # 获取最新消息 + async for msg in self.client.get_chat_history(self.search_bot, limit=1): + if msg.text: + logger.info(f"[Pyrogram] 收到回复 ({len(msg.text)} 字)") + return msg.text + + return "未收到回复" + + except FloodWait as e: + logger.warning(f"[Pyrogram] 触发限流,等待 {e.value} 秒") + await asyncio.sleep(e.value) + return await self.send_command(command, keyword, page) + + except Exception as e: + logger.error(f"[Pyrogram] 错误: {e}") + return f"搜索失败: {str(e)}" + +# ===== 自动翻页管理器 ===== +class AutoPaginationManager: + """后台自动翻页 - 用户无感知抓取2-10页""" + + def __init__(self, pyrogram_client: PyrogramMirror, database: Database): + self.pyrogram = pyrogram_client + self.db = database + self.active_tasks: Dict[int, asyncio.Task] = {} + logger.info("✅ 自动翻页管理器已初始化") + + async def start_pagination(self, user_id: int, command: str, keyword: str, first_result: str): + """启动后台翻页任务""" + if user_id in self.active_tasks: + logger.info(f"[翻页] 用户 {user_id} 已有翻页任务运行中") + return + + task = asyncio.create_task( + self._paginate(user_id, command, keyword, first_result) + ) + self.active_tasks[user_id] = task + logger.info(f"[翻页] 用户 {user_id} 后台任务已启动") + + async def _paginate(self, user_id: int, command: str, keyword: str, first_result: str): + """后台翻页逻辑""" + try: + # 保存第1页 + self.db.save_cache(command, keyword, 1, first_result) + + # 抓取2-10页 + for page in range(2, 11): + # 检查缓存 + cached = self.db.get_cache(command, keyword, page) + if cached: + logger.info(f"[翻页] 第{page}页已缓存,跳过") + continue + + # 发送 next 命令 + logger.info(f"[翻页] 抓取第{page}页...") + result = await self.pyrogram.send_command("next", "", page) + + # 保存结果 + self.db.save_cache(command, keyword, page, result) + + # 等待避免限流 + await asyncio.sleep(2) + + logger.info(f"[翻页] 用户 {user_id} 完成抓取 (1-10页)") + + except Exception as e: + logger.error(f"[翻页] 错误: {e}") + + finally: + if user_id in self.active_tasks: + del self.active_tasks[user_id] + +# ===== 统一Bot类 ===== +class UnifiedTelegramBot: + """统一Telegram Bot - 整合所有功能""" + + def __init__(self): + self.db = Database() + self.pyrogram = PyrogramMirror() + self.pagination_manager = None # 启动后初始化 + self.app = None + + # Claude客户端 + self.claude_client = anthropic.Anthropic( + auth_token=os.environ.get('ANTHROPIC_AUTH_TOKEN'), + base_url=os.environ.get('ANTHROPIC_BASE_URL', 'https://api.anthropic.com') + ) + + # 对话历史 + self.conversation_history: Dict[int, List[Dict]] = {} + + logger.info("✅ 统一Bot初始化完成") + + def get_history(self, user_id: int, limit: int = 2) -> List[Dict]: + """获取用户对话历史(最近N轮)""" + if user_id not in self.conversation_history: + return [] + messages = self.conversation_history[user_id][-limit*2:] + return [{"role": msg["role"], "content": msg["content"]} for msg in messages] + + def add_to_history(self, user_id: int, role: str, content: str): + """添加到对话历史""" + if user_id not in self.conversation_history: + self.conversation_history[user_id] = [] + self.conversation_history[user_id].append({"role": role, "content": content}) + # 保持最多10轮 + if len(self.conversation_history[user_id]) > 20: + self.conversation_history[user_id] = self.conversation_history[user_id][-20:] + + async def call_claude(self, user_id: int, message: str) -> Dict: + """ + 调用Claude API + + Args: + user_id: 用户ID + message: 用户消息 + + Returns: + { + "response": "AI回复", + "buttons": [...] + } + """ + try: + logger.info(f"[Claude] 用户 {user_id} 调用Claude API: {message}") + + # 获取历史 + history = self.get_history(user_id) + history.append({"role": "user", "content": message}) + + # 调用Claude + response = self.claude_client.messages.create( + model="claude-sonnet-4-5-20250929", + max_tokens=1024, + messages=history + ) + + # 提取回复 + reply_text = "" + for block in response.content: + if hasattr(block, 'text'): + reply_text += block.text + + # 保存历史 + self.add_to_history(user_id, "user", message) + self.add_to_history(user_id, "assistant", reply_text) + + # 提取按钮 + buttons = self._extract_buttons(reply_text) + + logger.info(f"[Claude] ✅ 回复成功 ({len(reply_text)} 字)") + + return { + "response": reply_text, + "buttons": buttons + } + + except Exception as e: + logger.error(f"[Claude] ❌ 错误: {e}") + return { + "response": f"AI服务出错: {str(e)}", + "buttons": [] + } + + def _extract_buttons(self, text: str) -> List[Dict[str, str]]: + """从AI回复中提取可点击按钮""" + buttons = [] + patterns = [ + r'/search\s+(\S+)', + r'/text\s+(\S+)', + r'/human\s+(\S+)', + r'/topchat' + ] + + for pattern in patterns: + matches = re.findall(pattern, text) + for match in matches: + if pattern == r'/topchat': + buttons.append({ + "text": "🔥 热门分类", + "callback_data": "cmd_topchat" + }) + else: + cmd = pattern.split('\\s')[0].replace('/', '') + buttons.append({ + "text": f"🔍 {cmd} {match}", + "callback_data": f"cmd_{cmd}_{match}"[:64] + }) + + return buttons + + async def start_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """处理 /start 命令""" + user_id = update.effective_user.id + logger.info(f"[命令] 用户 {user_id} 启动Bot") + + # 使用之前的欢迎方式 + await update.message.reply_text("👋 我来帮你搜索!\n\n直接告诉我你想找什么,或者使用以下命令:\n\n/search <关键词> - 搜索群组名称\n/text <关键词> - 搜索讨论内容\n/human <关键词> - 搜索用户\n/topchat - 查看热门分类") + + async def handle_message(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """处理用户消息 - 调用Claude""" + user_id = update.effective_user.id + user_message = update.message.text + + logger.info(f"[消息] 用户 {user_id}: {user_message}") + + # 调用Claude + claude_result = await self.call_claude(user_id, user_message) + + response_text = claude_result["response"] + buttons = claude_result["buttons"] + + # 发送回复(带按钮) + if buttons: + keyboard = [[InlineKeyboardButton(btn["text"], callback_data=btn["callback_data"])] + for btn in buttons] + reply_markup = InlineKeyboardMarkup(keyboard) + await update.message.reply_text(response_text, reply_markup=reply_markup) + logger.info(f"[回复] 已发送(带 {len(buttons)} 个按钮)") + else: + await update.message.reply_text(response_text) + logger.info(f"[回复] 已发送") + + async def handle_button(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """处理按钮点击""" + query = update.callback_query + await query.answer() + + callback_data = query.data + user_id = query.from_user.id + logger.info(f"[按钮] 用户 {user_id} 点击: {callback_data}") + + # 解析按钮命令 + if callback_data.startswith("cmd_"): + parts = callback_data[4:].split("_") + command = parts[0] + keyword = "_".join(parts[1:]) if len(parts) > 1 else "" + + # 执行搜索 + await self.execute_search(query.message, user_id, command, keyword) + + async def execute_search(self, message, user_id: int, command: str, keyword: str): + """执行搜索并返回结果""" + logger.info(f"[搜索] 用户 {user_id}: /{command} {keyword}") + + # 检查缓存 + cached = self.db.get_cache(command, keyword, 1) + if cached: + await message.reply_text(cached) + logger.info(f"[搜索] 返回缓存结果") + return + + # 通过Pyrogram搜索 + result = await self.pyrogram.send_command(command, keyword, 1) + + # 发送结果 + await message.reply_text(result) + + # 启动后台翻页 + await self.pagination_manager.start_pagination(user_id, command, keyword, result) + + async def post_init(self, app: Application): + """启动后初始化""" + # 启动Pyrogram + await self.pyrogram.start() + + # 初始化翻页管理器 + self.pagination_manager = AutoPaginationManager(self.pyrogram, self.db) + + logger.info("✅ 所有组件已初始化") + + async def post_shutdown(self, app: Application): + """关闭时清理""" + await self.pyrogram.stop() + logger.info("👋 Bot已停止") + + def run(self): + """启动Bot""" + logger.info("=" * 60) + logger.info("🚀 统一Telegram Bot启动中...") + logger.info(f"📅 时间: {datetime.now()}") + logger.info(f"🤖 Claude: 直接调用Anthropic API") + logger.info("=" * 60) + + # 创建Application + self.app = Application.builder().token(TELEGRAM_TOKEN).post_init(self.post_init).post_shutdown(self.post_shutdown).build() + + # 注册处理器 + self.app.add_handler(CommandHandler("start", self.start_command)) + self.app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, self.handle_message)) + self.app.add_handler(CallbackQueryHandler(self.handle_button)) + + # 启动轮询 + logger.info("✅ Bot已启动,等待消息...") + self.app.run_polling(allowed_updates=Update.ALL_TYPES) + +# ===== 主入口 ===== +if __name__ == "__main__": + bot = UnifiedTelegramBot() + bot.run() diff --git a/utils/bytes_helper.py b/utils/bytes_helper.py new file mode 100644 index 0000000..98a7942 --- /dev/null +++ b/utils/bytes_helper.py @@ -0,0 +1,37 @@ +""" +Bytes处理工具 - 统一处理所有bytes和字符串转换 +""" +from typing import Union, Optional + + +def bytes_to_hex(data: Optional[Union[bytes, str]]) -> Optional[str]: + """bytes转hex字符串;字符串保持原样""" + if data is None: + return None + if isinstance(data, bytes): + return data.hex() + return str(data) + + +def hex_to_bytes(hex_str: Optional[Union[str, bytes]]) -> Optional[bytes]: + """hex字符串转bytes,不可解析时返回原始字节""" + if hex_str is None: + return None + if isinstance(hex_str, bytes): + return hex_str + try: + return bytes.fromhex(hex_str) + except (ValueError, TypeError): + if isinstance(hex_str, str): + return hex_str.encode("utf-8") + return bytes(hex_str) + + +def safe_callback_data(callback_data: Optional[Union[str, bytes]]) -> Optional[str]: + """安全处理callback_data,统一转为hex便于存储""" + return bytes_to_hex(callback_data) + + +def restore_callback_data(hex_str: Optional[Union[str, bytes]]) -> Optional[bytes]: + """恢复callback_data为bytes""" + return hex_to_bytes(hex_str) diff --git a/view_stats.sh b/view_stats.sh new file mode 100755 index 0000000..2c617d3 --- /dev/null +++ b/view_stats.sh @@ -0,0 +1,30 @@ +#!/bin/bash +# 机器人统计查看脚本 + +echo "==========================================" +echo "📊 机器人使用统计" +echo "==========================================" +echo "" + +echo "1️⃣ 访问用户列表:" +grep -oE "用户 [0-9]+" logs/*.log 2>/dev/null | grep -oE "[0-9]+" | sort -u | nl + +echo "" +echo "2️⃣ 最近20条用户活动:" +grep -E "用户 [0-9]+|新用户访问" logs/integrated_bot_detailed.log 2>/dev/null | tail -20 + +echo "" +echo "3️⃣ 今日搜索统计:" +today=$(date +%Y-%m-%d) +grep "$today" logs/*.log 2>/dev/null | grep -c "镜像.*已转发" || echo "0" + +echo "" +echo "4️⃣ 缓存统计:" +sqlite3 cache.db "SELECT COUNT(*) FROM cache;" 2>/dev/null || echo "无缓存数据" + +echo "" +echo "5️⃣ 机器人运行时长:" +ps -p $(pgrep -f integrated_bot_ai.py) -o etime= 2>/dev/null || echo "未运行" + +echo "" +echo "=========================================="