chore: initial commit

This commit is contained in:
你的用户名
2025-11-01 21:58:31 +08:00
commit 0406b5664f
101 changed files with 20458 additions and 0 deletions

18
.gitignore vendored Normal file
View File

@@ -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*
._*

144
ARCHITECTURE_V3.md Normal file
View File

@@ -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 # 配置文件
```

213
BOT_README.md Normal file
View File

@@ -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`

228
CACHE_ANALYSIS.md Normal file
View File

@@ -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 <keyword>`
### 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

33
CREATE_V3.md Normal file
View File

@@ -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 ...

78
DEPLOY_TO_GITHUB.md Normal file
View File

@@ -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

134
FINAL_FIX_REPORT.md Normal file
View File

@@ -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

125
FINAL_STATUS.md Normal file
View File

@@ -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**

21
LICENSE Normal file
View File

@@ -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.

91
QUICK_FIX.sh Executable file
View File

@@ -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 "============================================"

234
README.md Normal file
View File

@@ -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)

100
README_SESSION.md Normal file
View File

@@ -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

106
SESSION_STATUS.md Normal file
View File

@@ -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 已成功修复,镜像功能正常工作,已设置完整的保护和监控机制。

131
SMART_MONITORING.md Normal file
View File

@@ -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小时检查一次
- 只在真正有问题时采取行动
- 让系统保持自然稳定运行

142
UPDATE_LOG_20251008.md Normal file
View File

@@ -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

205
WORKFLOW_VISUALIZATION.txt Normal file
View File

@@ -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页
═══════════════════════════════════════════════════════════════════

347
admin_commands.py Normal file
View File

@@ -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': []}

97
admin_panel.sh Executable file
View File

@@ -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

45
admin_查看后台.sh Executable file
View File

@@ -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 "=========================================="

403
agent_bot.py Normal file
View File

@@ -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()

51
auto_create_session.exp Executable file
View File

@@ -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

44
auto_restart_on_error.sh Executable file
View File

@@ -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

30
auto_session.exp Executable file
View File

@@ -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

875
bot_v3.py Normal file
View File

@@ -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"✅ 执行指令: <code>{full_cmd}</code>\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())

171
bot_without_mirror.py Executable file
View File

@@ -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())

40
check_all.sh Executable file
View File

@@ -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 "======================================"

84
check_pagination.sh Executable file
View File

@@ -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 "==========================================="

View File

@@ -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

129
claude_agent_wrapper.py Normal file
View File

@@ -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

25
create_final_session.py Executable file
View File

@@ -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()

74
create_session.exp Executable file
View File

@@ -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

16
create_session.py Normal file
View File

@@ -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()

72
create_session_correct.py Executable file
View File

@@ -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)

39
create_session_manual.py Executable file
View File

@@ -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("还在限制期内,请稍后再试")

30
create_session_now.py Normal file
View File

@@ -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()

25
create_session_proxy.py Normal file
View File

@@ -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()

View File

@@ -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. 代理设置问题")

193
database.py Executable file
View File

@@ -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
]
}

237
database.py.bak Executable file
View File

@@ -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

177
deploy.sh Executable file
View File

@@ -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 "========================================="

91
enhanced_logger.py Normal file
View File

@@ -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()

23
fix_claude_auth.py Normal file
View File

@@ -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('✅ 修改完成')

508
integrated_bot.py Normal file
View File

@@ -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())

View File

@@ -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())

View File

@@ -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())

View File

@@ -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())

File diff suppressed because it is too large Load Diff

845
integrated_bot_ai.backup.py Executable file
View File

@@ -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())

1190
integrated_bot_ai.py Executable file

File diff suppressed because it is too large Load Diff

View File

@@ -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())

File diff suppressed because it is too large Load Diff

845
integrated_bot_ai.py.bak Executable file
View File

@@ -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())

View File

@@ -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())

View File

@@ -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())

32
login_agent.py Normal file
View File

@@ -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())

4
login_claude.sh Executable file
View File

@@ -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"

38
main.py Normal file
View File

@@ -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()

163
manage_bot.sh Executable file
View File

@@ -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

51
modules/ai_analyzer.py Normal file
View File

@@ -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": "🔥"}
]
}

160
modules/session_manager.py Normal file
View File

@@ -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,
}

8
monitor.sh Executable file
View File

@@ -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)"

45
monitor_session.sh Executable file
View File

@@ -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 监控完成"

43
protect_session.sh Executable file
View File

@@ -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

48
qr_login.py Normal file
View File

@@ -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())

69
quick_deploy.txt Normal file
View File

@@ -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. 定期检查运行状态

23
requirements.txt Normal file
View File

@@ -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

31
run.sh Executable file
View File

@@ -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"

15
run_bot_loop.sh Executable file
View File

@@ -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_code5 秒后重启" | tee -a "$LOG_PATH"
sleep 5
done

45
run_session_creator.sh Executable file
View File

@@ -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

95
smart_health_check.sh Executable file
View File

@@ -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 ""

5
src/config/__init__.py Normal file
View File

@@ -0,0 +1,5 @@
"""配置管理模块"""
from .settings import Settings
from .loader import ConfigLoader
__all__ = ['Settings', 'ConfigLoader']

155
src/config/loader.py Normal file
View File

@@ -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

140
src/config/settings.py Normal file
View File

@@ -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}")

6
src/core/__init__.py Normal file
View File

@@ -0,0 +1,6 @@
"""核心模块"""
from .bot import CustomerServiceBot
from .router import MessageRouter
from .handlers import BaseHandler, HandlerContext
__all__ = ['CustomerServiceBot', 'MessageRouter', 'BaseHandler', 'HandlerContext']

693
src/core/bot.py Normal file
View File

@@ -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

157
src/core/handlers.py Normal file
View File

@@ -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

321
src/core/router.py Normal file
View File

@@ -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

View File

@@ -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("搜索镜像客户端已停止")

View File

@@ -0,0 +1,5 @@
"""存储模块"""
from .database import DatabaseManager
from .models import Customer, Message, Session
__all__ = ['DatabaseManager', 'Customer', 'Message', 'Session']

View File

@@ -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")

View File

@@ -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
}

6
src/utils/__init__.py Normal file
View File

@@ -0,0 +1,6 @@
"""工具模块"""
from .logger import Logger, get_logger
from .exceptions import *
from .decorators import *
__all__ = ['Logger', 'get_logger']

233
src/utils/decorators.py Normal file
View File

@@ -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

122
src/utils/exceptions.py Normal file
View File

@@ -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 "❌ 系统错误,请稍后再试或联系管理员"

167
src/utils/logger.py Normal file
View File

@@ -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

13
start_bot.sh Executable file
View File

@@ -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

19
start_bot_final.sh Executable file
View File

@@ -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

21
start_bot_fixed.sh Executable file
View File

@@ -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 查看日志"

18
start_bot_v2.sh Executable file
View File

@@ -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

23
start_integrated.sh Normal file
View File

@@ -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"

18
start_v3.sh Executable file
View File

@@ -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"

29
test_agent_response.py Normal file
View File

@@ -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())

60
test_agent_sdk.py Normal file
View File

@@ -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❌ 配置失败')

39
test_claude_api.py Normal file
View File

@@ -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}")

43
test_claude_api2.py Normal file
View File

@@ -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}")

29
test_claude_api3.py Normal file
View File

@@ -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}")

41
test_with_proxy.py Normal file
View File

@@ -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')

36
test_with_token.py Normal file
View File

@@ -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')

470
unified_telegram_bot.py Normal file
View File

@@ -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()

37
utils/bytes_helper.py Normal file
View File

@@ -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)

Some files were not shown because too many files have changed in this diff Show More