866 lines
31 KiB
Python
Executable File
866 lines
31 KiB
Python
Executable File
#!/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())
|