chore: initial commit
This commit is contained in:
401
core/server_stdio_backup.py
Normal file
401
core/server_stdio_backup.py
Normal file
@@ -0,0 +1,401 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Funstat BOT MCP Server
|
||||
|
||||
基于 Telethon 的 MCP 服务器,用于与 @openaiw_bot 交互
|
||||
提供搜索、查询、统计等功能
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
from typing import Any, Dict, List, Optional
|
||||
from datetime import datetime, timedelta
|
||||
from collections import deque
|
||||
|
||||
from mcp.server import Server
|
||||
from mcp.types import (
|
||||
Resource,
|
||||
Tool,
|
||||
TextContent,
|
||||
ImageContent,
|
||||
EmbeddedResource,
|
||||
)
|
||||
from pydantic import AnyUrl
|
||||
from telethon import TelegramClient
|
||||
from telethon.tl.types import Message
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logger = logging.getLogger("funstat_mcp")
|
||||
|
||||
# 配置
|
||||
API_ID = 24660516
|
||||
API_HASH = "eae564578880a59c9963916ff1bbbd3a"
|
||||
# Session 文件路径 - 使用独立的安全目录,防止被意外删除
|
||||
SESSION_PATH = os.path.expanduser("~/telegram_sessions/funstat_bot")
|
||||
BOT_USERNAME = "@openaiw_bot"
|
||||
|
||||
# 速率限制配置
|
||||
RATE_LIMIT_PER_SECOND = 18 # 每秒最多18个请求
|
||||
RATE_LIMIT_WINDOW = 1.0 # 1秒时间窗口
|
||||
|
||||
# 缓存配置
|
||||
CACHE_TTL = 3600 # 缓存1小时
|
||||
|
||||
|
||||
class RateLimiter:
|
||||
"""速率限制器"""
|
||||
|
||||
def __init__(self, max_requests: int, time_window: float):
|
||||
self.max_requests = max_requests
|
||||
self.time_window = time_window
|
||||
self.requests = deque()
|
||||
|
||||
async def acquire(self):
|
||||
"""获取请求许可,如果超过限制则等待"""
|
||||
now = time.time()
|
||||
|
||||
# 移除超出时间窗口的请求记录
|
||||
while self.requests and self.requests[0] < now - self.time_window:
|
||||
self.requests.popleft()
|
||||
|
||||
# 如果达到限制,等待
|
||||
if len(self.requests) >= self.max_requests:
|
||||
sleep_time = self.requests[0] + self.time_window - now
|
||||
if sleep_time > 0:
|
||||
logger.info(f"速率限制: 等待 {sleep_time:.2f} 秒")
|
||||
await asyncio.sleep(sleep_time)
|
||||
return await self.acquire() # 递归重试
|
||||
|
||||
# 记录请求时间
|
||||
self.requests.append(now)
|
||||
|
||||
|
||||
class ResponseCache:
|
||||
"""响应缓存"""
|
||||
|
||||
def __init__(self, ttl: int = CACHE_TTL):
|
||||
self.cache: Dict[str, tuple[Any, float]] = {}
|
||||
self.ttl = ttl
|
||||
|
||||
def get(self, key: str) -> Optional[Any]:
|
||||
"""获取缓存"""
|
||||
if key in self.cache:
|
||||
value, timestamp = self.cache[key]
|
||||
if time.time() - timestamp < self.ttl:
|
||||
logger.info(f"缓存命中: {key}")
|
||||
return value
|
||||
else:
|
||||
# 过期,删除
|
||||
del self.cache[key]
|
||||
return None
|
||||
|
||||
def set(self, key: str, value: Any):
|
||||
"""设置缓存"""
|
||||
self.cache[key] = (value, time.time())
|
||||
logger.info(f"缓存保存: {key}")
|
||||
|
||||
def clear_expired(self):
|
||||
"""清理过期缓存"""
|
||||
now = time.time()
|
||||
expired_keys = [
|
||||
key for key, (_, timestamp) in self.cache.items()
|
||||
if now - timestamp >= self.ttl
|
||||
]
|
||||
for key in expired_keys:
|
||||
del self.cache[key]
|
||||
if expired_keys:
|
||||
logger.info(f"清理了 {len(expired_keys)} 个过期缓存")
|
||||
|
||||
|
||||
class FunstatMCPServer:
|
||||
"""Funstat MCP 服务器"""
|
||||
|
||||
def __init__(self):
|
||||
self.server = Server("funstat-mcp")
|
||||
self.client: Optional[TelegramClient] = None
|
||||
self.bot_entity = None
|
||||
self.rate_limiter = RateLimiter(RATE_LIMIT_PER_SECOND, RATE_LIMIT_WINDOW)
|
||||
self.cache = ResponseCache()
|
||||
|
||||
# 注册处理器
|
||||
self.server.list_tools()(self.list_tools)
|
||||
self.server.call_tool()(self.call_tool)
|
||||
|
||||
async def initialize(self):
|
||||
"""初始化 Telegram 客户端"""
|
||||
logger.info("初始化 Telegram 客户端...")
|
||||
|
||||
# 检查 session 文件
|
||||
session_file = f"{SESSION_PATH}.session"
|
||||
if not os.path.exists(session_file):
|
||||
raise FileNotFoundError(
|
||||
f"Session 文件不存在: {session_file}\n"
|
||||
f"请先运行 create_session.py 创建 session 文件\n"
|
||||
f"或者将现有 session 文件复制到: ~/telegram_sessions/"
|
||||
)
|
||||
|
||||
logger.info(f"使用 Session 文件: {session_file}")
|
||||
|
||||
# 创建客户端
|
||||
self.client = TelegramClient(SESSION_PATH, API_ID, API_HASH)
|
||||
await self.client.start()
|
||||
|
||||
# 获取 bot 实体
|
||||
logger.info(f"连接到 {BOT_USERNAME}...")
|
||||
self.bot_entity = await self.client.get_entity(BOT_USERNAME)
|
||||
logger.info(f"✅ 已连接到: {self.bot_entity.first_name}")
|
||||
|
||||
# 获取当前用户信息
|
||||
me = await self.client.get_me()
|
||||
logger.info(f"✅ 当前账号: @{me.username} (ID: {me.id})")
|
||||
|
||||
async def send_command_and_wait(
|
||||
self,
|
||||
command: str,
|
||||
timeout: int = 10,
|
||||
use_cache: bool = True
|
||||
) -> str:
|
||||
"""发送命令到 BOT 并等待响应"""
|
||||
|
||||
# 检查缓存
|
||||
cache_key = f"cmd:{command}"
|
||||
if use_cache:
|
||||
cached = self.cache.get(cache_key)
|
||||
if cached:
|
||||
return cached
|
||||
|
||||
# 速率限制
|
||||
await self.rate_limiter.acquire()
|
||||
|
||||
logger.info(f"📤 发送命令: {command}")
|
||||
|
||||
# 记录发送前的最新消息 ID
|
||||
last_message_id = 0
|
||||
async for message in self.client.iter_messages(self.bot_entity, limit=1):
|
||||
last_message_id = message.id
|
||||
break
|
||||
|
||||
# 发送消息
|
||||
send_time = datetime.now()
|
||||
await self.client.send_message(self.bot_entity, command)
|
||||
|
||||
# 等待响应(稍等一下让 BOT 有时间响应)
|
||||
await asyncio.sleep(1.5)
|
||||
|
||||
# 获取新消息
|
||||
start_time = time.time()
|
||||
while time.time() - start_time < timeout:
|
||||
# 获取最新消息
|
||||
async for message in self.client.iter_messages(self.bot_entity, limit=5):
|
||||
# 检查是否是新消息
|
||||
if message.id > last_message_id:
|
||||
# 检查是否是 BOT 的消息
|
||||
if not message.out and message.text:
|
||||
response_text = message.text
|
||||
logger.info(f"✅ 收到响应 ({len(response_text)} 字符)")
|
||||
|
||||
# 保存到缓存
|
||||
if use_cache:
|
||||
self.cache.set(cache_key, response_text)
|
||||
|
||||
return response_text
|
||||
|
||||
# 继续等待
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
raise TimeoutError(f"等待 BOT 响应超时 ({timeout}秒)")
|
||||
|
||||
async def list_tools(self) -> List[Tool]:
|
||||
"""列出所有可用工具"""
|
||||
return [
|
||||
Tool(
|
||||
name="funstat_search",
|
||||
description="搜索 Telegram 群组、频道。支持关键词搜索,返回相关的群组列表",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "搜索关键词,例如: 'python', '区块链', 'AI'"
|
||||
}
|
||||
},
|
||||
"required": ["query"]
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="funstat_topchat",
|
||||
description="获取热门群组/频道列表,按成员数或活跃度排序",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"category": {
|
||||
"type": "string",
|
||||
"description": "分类筛选(可选),例如: 'tech', 'crypto', 'news'"
|
||||
}
|
||||
}
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="funstat_text",
|
||||
description="通过消息文本搜索,查找包含特定文本的消息和来源群组",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"text": {
|
||||
"type": "string",
|
||||
"description": "要搜索的文本内容"
|
||||
}
|
||||
},
|
||||
"required": ["text"]
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="funstat_human",
|
||||
description="通过姓名搜索用户,查找 Telegram 用户信息",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "用户姓名"
|
||||
}
|
||||
},
|
||||
"required": ["name"]
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="funstat_user_info",
|
||||
description="查询用户详细信息,支持通过用户名、用户ID、联系人等方式查询",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"identifier": {
|
||||
"type": "string",
|
||||
"description": "用户标识: 用户名(@username)、用户ID、或手机号"
|
||||
}
|
||||
},
|
||||
"required": ["identifier"]
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="funstat_balance",
|
||||
description="查询当前账号的积分余额和使用统计",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="funstat_menu",
|
||||
description="显示 funstat BOT 的主菜单和所有可用功能",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="funstat_start",
|
||||
description="获取 funstat BOT 的欢迎信息和使用说明",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
}
|
||||
)
|
||||
]
|
||||
|
||||
async def call_tool(self, name: str, arguments: Dict[str, Any]) -> List[TextContent]:
|
||||
"""调用工具"""
|
||||
logger.info(f"🔧 调用工具: {name} with {arguments}")
|
||||
|
||||
try:
|
||||
if name == "funstat_search":
|
||||
query = arguments["query"]
|
||||
response = await self.send_command_and_wait(f"/search {query}")
|
||||
return [TextContent(type="text", text=response)]
|
||||
|
||||
elif name == "funstat_topchat":
|
||||
category = arguments.get("category", "")
|
||||
if category:
|
||||
response = await self.send_command_and_wait(f"/topchat {category}")
|
||||
else:
|
||||
response = await self.send_command_and_wait("/topchat")
|
||||
return [TextContent(type="text", text=response)]
|
||||
|
||||
elif name == "funstat_text":
|
||||
text = arguments["text"]
|
||||
response = await self.send_command_and_wait(f"/text {text}")
|
||||
return [TextContent(type="text", text=response)]
|
||||
|
||||
elif name == "funstat_human":
|
||||
name_query = arguments["name"]
|
||||
response = await self.send_command_and_wait(f"/human {name_query}")
|
||||
return [TextContent(type="text", text=response)]
|
||||
|
||||
elif name == "funstat_user_info":
|
||||
identifier = arguments["identifier"]
|
||||
# 直接发送用户标识(用户名、ID等)
|
||||
response = await self.send_command_and_wait(identifier)
|
||||
return [TextContent(type="text", text=response)]
|
||||
|
||||
elif name == "funstat_balance":
|
||||
response = await self.send_command_and_wait("/balance")
|
||||
return [TextContent(type="text", text=response)]
|
||||
|
||||
elif name == "funstat_menu":
|
||||
response = await self.send_command_and_wait("/menu")
|
||||
return [TextContent(type="text", text=response)]
|
||||
|
||||
elif name == "funstat_start":
|
||||
response = await self.send_command_and_wait("/start")
|
||||
return [TextContent(type="text", text=response)]
|
||||
|
||||
else:
|
||||
raise ValueError(f"未知工具: {name}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ 工具调用失败: {e}")
|
||||
return [TextContent(
|
||||
type="text",
|
||||
text=f"❌ 错误: {str(e)}"
|
||||
)]
|
||||
|
||||
async def run(self):
|
||||
"""运行服务器"""
|
||||
await self.initialize()
|
||||
|
||||
# 启动定期清理过期缓存的任务
|
||||
async def cache_cleanup_task():
|
||||
while True:
|
||||
await asyncio.sleep(300) # 每5分钟清理一次
|
||||
self.cache.clear_expired()
|
||||
|
||||
asyncio.create_task(cache_cleanup_task())
|
||||
|
||||
logger.info("🚀 Funstat MCP Server 已启动")
|
||||
|
||||
# 运行 MCP 服务器
|
||||
from mcp.server.stdio import stdio_server
|
||||
|
||||
async with stdio_server() as (read_stream, write_stream):
|
||||
await self.server.run(
|
||||
read_stream,
|
||||
write_stream,
|
||||
self.server.create_initialization_options()
|
||||
)
|
||||
|
||||
|
||||
async def main():
|
||||
"""主函数"""
|
||||
server = FunstatMCPServer()
|
||||
await server.run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
Reference in New Issue
Block a user