chore: initial commit
This commit is contained in:
18
.gitignore
vendored
Normal file
18
.gitignore
vendored
Normal 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
144
ARCHITECTURE_V3.md
Normal 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
213
BOT_README.md
Normal 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
228
CACHE_ANALYSIS.md
Normal 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
33
CREATE_V3.md
Normal 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
78
DEPLOY_TO_GITHUB.md
Normal 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
134
FINAL_FIX_REPORT.md
Normal 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
125
FINAL_STATUS.md
Normal 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
21
LICENSE
Normal 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
91
QUICK_FIX.sh
Executable 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
234
README.md
Normal file
@@ -0,0 +1,234 @@
|
||||
# Telegram 整合机器人 - NewBot925 🤖
|
||||
|
||||
[](https://www.python.org/downloads/)
|
||||
[](LICENSE)
|
||||
[](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
100
README_SESSION.md
Normal 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
106
SESSION_STATUS.md
Normal 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
131
SMART_MONITORING.md
Normal 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
142
UPDATE_LOG_20251008.md
Normal 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
205
WORKFLOW_VISUALIZATION.txt
Normal 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
347
admin_commands.py
Normal 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
97
admin_panel.sh
Executable 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
45
admin_查看后台.sh
Executable 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
403
agent_bot.py
Normal 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
51
auto_create_session.exp
Executable 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
44
auto_restart_on_error.sh
Executable 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
30
auto_session.exp
Executable 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
875
bot_v3.py
Normal 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
171
bot_without_mirror.py
Executable 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
40
check_all.sh
Executable 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
84
check_pagination.sh
Executable 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 "==========================================="
|
||||
116
claude_agent_wrapper.backup.20251007_171621.py
Normal file
116
claude_agent_wrapper.backup.20251007_171621.py
Normal 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
129
claude_agent_wrapper.py
Normal 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
25
create_final_session.py
Executable 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
74
create_session.exp
Executable 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
16
create_session.py
Normal 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
72
create_session_correct.py
Executable 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
39
create_session_manual.py
Executable 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
30
create_session_now.py
Normal 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
25
create_session_proxy.py
Normal 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()
|
||||
53
create_user_session_interactive.py
Executable file
53
create_user_session_interactive.py
Executable 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
193
database.py
Executable 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
237
database.py.bak
Executable 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
177
deploy.sh
Executable 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
91
enhanced_logger.py
Normal 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
23
fix_claude_auth.py
Normal 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
508
integrated_bot.py
Normal 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())
|
||||
865
integrated_bot_ai.backup.20251007_164306.py
Executable file
865
integrated_bot_ai.backup.20251007_164306.py
Executable 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())
|
||||
865
integrated_bot_ai.backup.20251007_172359.py
Executable file
865
integrated_bot_ai.backup.20251007_172359.py
Executable 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())
|
||||
956
integrated_bot_ai.backup.20251008_065416.py
Executable file
956
integrated_bot_ai.backup.20251008_065416.py
Executable 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())
|
||||
1007
integrated_bot_ai.backup.before_fix.py
Executable file
1007
integrated_bot_ai.backup.before_fix.py
Executable file
File diff suppressed because it is too large
Load Diff
845
integrated_bot_ai.backup.py
Executable file
845
integrated_bot_ai.backup.py
Executable 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
1190
integrated_bot_ai.py
Executable file
File diff suppressed because it is too large
Load Diff
561
integrated_bot_ai.py.backup_20251006_165614
Executable file
561
integrated_bot_ai.py.backup_20251006_165614
Executable 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())
|
||||
1190
integrated_bot_ai.py.backup_before_admin
Executable file
1190
integrated_bot_ai.py.backup_before_admin
Executable file
File diff suppressed because it is too large
Load Diff
845
integrated_bot_ai.py.bak
Executable file
845
integrated_bot_ai.py.bak
Executable 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())
|
||||
604
integrated_bot_ai.py.before_optimization
Executable file
604
integrated_bot_ai.py.before_optimization
Executable 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())
|
||||
865
integrated_bot_ai_backup_20251007_155823.py
Executable file
865
integrated_bot_ai_backup_20251007_155823.py
Executable 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
32
login_agent.py
Normal 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
4
login_claude.sh
Executable 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
38
main.py
Normal 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
163
manage_bot.sh
Executable 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
51
modules/ai_analyzer.py
Normal 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
160
modules/session_manager.py
Normal 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
8
monitor.sh
Executable 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
45
monitor_session.sh
Executable 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
43
protect_session.sh
Executable 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
48
qr_login.py
Normal 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
69
quick_deploy.txt
Normal 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
23
requirements.txt
Normal 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
31
run.sh
Executable 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
15
run_bot_loop.sh
Executable 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_code,5 秒后重启" | tee -a "$LOG_PATH"
|
||||
sleep 5
|
||||
done
|
||||
45
run_session_creator.sh
Executable file
45
run_session_creator.sh
Executable 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
95
smart_health_check.sh
Executable 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
5
src/config/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
"""配置管理模块"""
|
||||
from .settings import Settings
|
||||
from .loader import ConfigLoader
|
||||
|
||||
__all__ = ['Settings', 'ConfigLoader']
|
||||
155
src/config/loader.py
Normal file
155
src/config/loader.py
Normal 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
140
src/config/settings.py
Normal 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
6
src/core/__init__.py
Normal 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
693
src/core/bot.py
Normal 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
157
src/core/handlers.py
Normal 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
321
src/core/router.py
Normal 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
|
||||
279
src/modules/mirror_search.py
Normal file
279
src/modules/mirror_search.py
Normal 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("搜索镜像客户端已停止")
|
||||
5
src/modules/storage/__init__.py
Normal file
5
src/modules/storage/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
"""存储模块"""
|
||||
from .database import DatabaseManager
|
||||
from .models import Customer, Message, Session
|
||||
|
||||
__all__ = ['DatabaseManager', 'Customer', 'Message', 'Session']
|
||||
428
src/modules/storage/database.py
Normal file
428
src/modules/storage/database.py
Normal 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")
|
||||
154
src/modules/storage/models.py
Normal file
154
src/modules/storage/models.py
Normal 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
6
src/utils/__init__.py
Normal 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
233
src/utils/decorators.py
Normal 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
122
src/utils/exceptions.py
Normal 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
167
src/utils/logger.py
Normal 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
13
start_bot.sh
Executable 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
19
start_bot_final.sh
Executable 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
21
start_bot_fixed.sh
Executable 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
18
start_bot_v2.sh
Executable 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
23
start_integrated.sh
Normal 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
18
start_v3.sh
Executable 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
29
test_agent_response.py
Normal 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
60
test_agent_sdk.py
Normal 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
39
test_claude_api.py
Normal 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
43
test_claude_api2.py
Normal 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
29
test_claude_api3.py
Normal 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
41
test_with_proxy.py
Normal 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
36
test_with_token.py
Normal 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
470
unified_telegram_bot.py
Normal 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
37
utils/bytes_helper.py
Normal 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
Reference in New Issue
Block a user