471 lines
16 KiB
Python
471 lines
16 KiB
Python
#!/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()
|