chore: initial commit
This commit is contained in:
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()
|
||||
Reference in New Issue
Block a user