chore: initial commit

This commit is contained in:
你的用户名
2025-11-01 21:58:03 +08:00
commit a05a7dd40e
65 changed files with 16590 additions and 0 deletions

43
core/debug_bot.py Normal file
View File

@@ -0,0 +1,43 @@
#!/usr/bin/env python3
"""
调试脚本:查看与 BOT 的对话历史
"""
import asyncio
from telethon import TelegramClient
API_ID = 24660516
API_HASH = "eae564578880a59c9963916ff1bbbd3a"
SESSION_NAME = "funstat_bot_session"
BOT_USERNAME = "@openaiw_bot"
async def debug_bot():
client = TelegramClient(SESSION_NAME, API_ID, API_HASH)
await client.start()
bot_entity = await client.get_entity(BOT_USERNAME)
print(f"BOT: {bot_entity.first_name} (ID: {bot_entity.id})")
print()
# 发送 /start 命令
print("发送 /start...")
await client.send_message(bot_entity, "/start")
# 等待一下
await asyncio.sleep(3)
# 获取最近的消息
print("\n最近的 10 条消息:")
print("=" * 60)
async for message in client.iter_messages(bot_entity, limit=10):
sender = "" if message.out else "BOT"
print(f"\n[{sender}] {message.date}")
if message.text:
print(message.text[:200])
print("-" * 60)
await client.disconnect()
if __name__ == "__main__":
asyncio.run(debug_bot())

181
core/http_server.py Normal file
View File

@@ -0,0 +1,181 @@
#!/usr/bin/env python3
"""
Funstat MCP HTTP Server
提供 HTTP API 接口,使 funstat 功能可以通过 HTTP 调用
"""
import asyncio
import json
import logging
import os
from pathlib import Path
from typing import Dict, Any
from aiohttp import web
from server import FunstatMCPServer
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
# 全局 MCP 服务器实例
mcp_server = None
async def initialize_mcp_server():
"""初始化 MCP 服务器"""
global mcp_server
api_id = int(os.getenv('TELEGRAM_API_ID', '24660516'))
api_hash = os.getenv('TELEGRAM_API_HASH', 'eae564578880a59c9963916ff1bbbd3a')
session_path = os.getenv('SESSION_PATH', os.path.expanduser('~/telegram_sessions/funstat_bot'))
mcp_server = FunstatMCPServer(api_id, api_hash, session_path)
await mcp_server.connect()
logger.info("✅ Funstat MCP Server 初始化完成")
async def handle_search(request):
"""搜索群组/频道"""
try:
data = await request.json()
keyword = data.get('keyword', '')
use_cache = data.get('use_cache', True)
result = await mcp_server.search(keyword, use_cache)
return web.json_response({'success': True, 'data': result})
except Exception as e:
logger.error(f"搜索失败: {e}")
return web.json_response({'success': False, 'error': str(e)}, status=500)
async def handle_topchat(request):
"""获取热门聊天"""
try:
data = await request.json()
category = data.get('category', 'all')
use_cache = data.get('use_cache', True)
result = await mcp_server.topchat(category, use_cache)
return web.json_response({'success': True, 'data': result})
except Exception as e:
logger.error(f"获取热门聊天失败: {e}")
return web.json_response({'success': False, 'error': str(e)}, status=500)
async def handle_text(request):
"""按消息文本搜索"""
try:
data = await request.json()
text = data.get('text', '')
use_cache = data.get('use_cache', True)
result = await mcp_server.search_text(text, use_cache)
return web.json_response({'success': True, 'data': result})
except Exception as e:
logger.error(f"文本搜索失败: {e}")
return web.json_response({'success': False, 'error': str(e)}, status=500)
async def handle_human(request):
"""按姓名搜索"""
try:
data = await request.json()
name = data.get('name', '')
use_cache = data.get('use_cache', True)
result = await mcp_server.search_human(name, use_cache)
return web.json_response({'success': True, 'data': result})
except Exception as e:
logger.error(f"姓名搜索失败: {e}")
return web.json_response({'success': False, 'error': str(e)}, status=500)
async def handle_user_info(request):
"""查询用户详情"""
try:
data = await request.json()
user_id = data.get('user_id', '')
use_cache = data.get('use_cache', True)
result = await mcp_server.user_info(user_id, use_cache)
return web.json_response({'success': True, 'data': result})
except Exception as e:
logger.error(f"用户查询失败: {e}")
return web.json_response({'success': False, 'error': str(e)}, status=500)
async def handle_balance(request):
"""查看积分余额"""
try:
data = await request.json()
use_cache = data.get('use_cache', True)
result = await mcp_server.balance(use_cache)
return web.json_response({'success': True, 'data': result})
except Exception as e:
logger.error(f"查询余额失败: {e}")
return web.json_response({'success': False, 'error': str(e)}, status=500)
async def handle_menu(request):
"""显示菜单"""
try:
data = await request.json()
use_cache = data.get('use_cache', True)
result = await mcp_server.menu(use_cache)
return web.json_response({'success': True, 'data': result})
except Exception as e:
logger.error(f"获取菜单失败: {e}")
return web.json_response({'success': False, 'error': str(e)}, status=500)
async def handle_start(request):
"""欢迎消息"""
try:
data = await request.json()
use_cache = data.get('use_cache', True)
result = await mcp_server.start(use_cache)
return web.json_response({'success': True, 'data': result})
except Exception as e:
logger.error(f"获取欢迎消息失败: {e}")
return web.json_response({'success': False, 'error': str(e)}, status=500)
async def handle_health(request):
"""健康检查"""
return web.json_response({
'status': 'ok',
'server': 'funstat-mcp',
'connected': mcp_server is not None and mcp_server.client.is_connected()
})
async def on_startup(app):
"""应用启动时初始化"""
await initialize_mcp_server()
async def on_cleanup(app):
"""应用关闭时清理"""
if mcp_server:
await mcp_server.client.disconnect()
logger.info("MCP Server 已断开连接")
def create_app():
"""创建 Web 应用"""
app = web.Application()
# 注册路由
app.router.add_post('/funstat/search', handle_search)
app.router.add_post('/funstat/topchat', handle_topchat)
app.router.add_post('/funstat/text', handle_text)
app.router.add_post('/funstat/human', handle_human)
app.router.add_post('/funstat/user_info', handle_user_info)
app.router.add_post('/funstat/balance', handle_balance)
app.router.add_post('/funstat/menu', handle_menu)
app.router.add_post('/funstat/start', handle_start)
app.router.add_get('/health', handle_health)
# 注册启动和清理回调
app.on_startup.append(on_startup)
app.on_cleanup.append(on_cleanup)
return app
if __name__ == '__main__':
port = int(os.getenv('FUNSTAT_PORT', '8090'))
app = create_app()
logger.info(f"🚀 启动 Funstat HTTP Server 在端口 {port}")
web.run_app(app, host='127.0.0.1', port=port)

View File

@@ -0,0 +1,115 @@
#!/usr/bin/env python3
"""完整搜索翻译相关用户并保存到文件"""
import sys
sys.path.insert(0, '.')
from server import FunstatMCPServer
import asyncio
import re
import json
from datetime import datetime
async def main():
server = FunstatMCPServer()
await server.initialize()
results = []
seen = set()
keywords = [
'翻译', 'translation', 'translate', 'translator', 'translators',
'译者', '翻译组', '翻译团队', '字幕组', '汉化', '汉化组',
'subtitle', 'subtitles', 'fansub', 'scanlation',
'localization', '本地化', 'interpreting', 'interpretation',
'translation group', 'subtitle group', 'translation team'
]
print(f"🔍 开始搜索 {len(keywords)} 个关键词")
print(f"⚠️ 每个关键词返回最多15条结果funstat BOT限制")
print(f"💡 通过多关键词覆盖更多用户")
print("")
for i, kw in enumerate(keywords, 1):
print(f"[{i:2d}/{len(keywords)}] {kw:25s}", end=' ', flush=True)
try:
res = await server.send_command_and_wait(f'/search {kw}', use_cache=False)
ids = re.findall(r'`(\d+)`', res)
usernames = re.findall(r'@(\w+)', res) + re.findall(r't\.me/(\w+)', res)
new_count = 0
for uid in ids:
key = f"ID:{uid}"
if key not in seen:
seen.add(key)
results.append({'type': 'id', 'value': uid, 'keyword': kw})
new_count += 1
for username in usernames:
if username:
key = f"@{username}"
if key not in seen:
seen.add(key)
results.append({'type': 'username', 'value': username, 'keyword': kw})
new_count += 1
print(f"+{new_count:2d} → 总计: {len(results):3d}")
await asyncio.sleep(0.5)
except Exception as e:
print(f"失败: {e}")
# 保存文件
txt_file = '/Users/lucas/chat--1003255561049/translation_users.txt'
json_file = '/Users/lucas/chat--1003255561049/translation_users.json'
with open(txt_file, 'w', encoding='utf-8') as f:
f.write("=" * 80 + "\n")
f.write("翻译相关用户/群组完整列表\n")
f.write("=" * 80 + "\n")
f.write(f"总数: {len(results)}\n")
f.write(f"搜索时间: {datetime.now()}\n")
f.write(f"数据来源: funstat BOT (@openaiw_bot)\n")
f.write("=" * 80 + "\n\n")
for i, item in enumerate(results, 1):
if item['type'] == 'id':
f.write(f"{i:4d}. ID: {item['value']:15s} (来源: {item['keyword']})\n")
else:
f.write(f"{i:4d}. @{item['value']:30s} (来源: {item['keyword']})\n")
with open(json_file, 'w', encoding='utf-8') as f:
json.dump({
'total': len(results),
'timestamp': str(datetime.now()),
'results': results
}, f, ensure_ascii=False, indent=2)
print("")
print("=" * 80)
print(f"✅ 搜索完成!共找到 {len(results)} 条独特记录")
print("=" * 80)
print(f"📄 文本文件: {txt_file}")
print(f"📄 JSON文件: {json_file}")
print("")
# 显示前100条
print("📋 前 100 条结果:")
print("")
for i, item in enumerate(results[:100], 1):
if item['type'] == 'id':
print(f"{i:3d}. ID: {item['value']}")
else:
print(f"{i:3d}. @{item['value']}")
if len(results) > 100:
print(f"\n... 还有 {len(results) - 100} 条记录,请查看文件")
await server.client.disconnect()
print(f"\n🎯 最终统计: {len(results)} 条独特记录")
print(f"📊 ID数量: {sum(1 for r in results if r['type'] == 'id')}")
print(f"👤 用户名数量: {sum(1 for r in results if r['type'] == 'username')}")
if __name__ == '__main__':
asyncio.run(main())

View File

@@ -0,0 +1,192 @@
#!/usr/bin/env python3
"""带翻页功能的完整搜索 - 支持自动点击翻页按钮"""
import sys
sys.path.insert(0, '.')
from server import FunstatMCPServer
import asyncio
import re
import json
from datetime import datetime
async def search_all_pages(server, keyword, max_pages=20):
"""
搜索所有页面
Args:
server: FunstatMCPServer实例
keyword: 搜索关键词
max_pages: 最大翻页数(防止无限循环)
Returns:
list: 所有页的结果
"""
all_results = []
current_page = 1
print(f"\n🔍 搜索关键词: {keyword}")
# 发送搜索命令
await server.client.send_message(server.bot_entity, f'/search {keyword}')
await asyncio.sleep(2)
while current_page <= max_pages:
# 获取最新消息
messages = await server.client.get_messages(server.bot_entity, limit=1)
msg = messages[0]
# 提取数据
text = msg.text
ids = re.findall(r'`(\d+)`', text)
usernames = re.findall(r'@(\w+)', text) + re.findall(r't\.me/(\w+)', text)
# 记录当前页结果
page_count = len(ids) + len(usernames)
print(f"{current_page} 页: +{page_count} 条结果", end='')
for uid in ids:
all_results.append({'type': 'id', 'value': uid, 'keyword': keyword, 'page': current_page})
for username in usernames:
if username:
all_results.append({'type': 'username', 'value': username, 'keyword': keyword, 'page': current_page})
# 检查是否有下一页按钮
next_page_button_index = None
if msg.reply_markup and hasattr(msg.reply_markup, 'rows'):
button_index = 0
for row in msg.reply_markup.rows:
for button in row.buttons:
# 寻找 "➡️ X" 格式的按钮
if '➡️' in button.text:
next_page_button_index = button_index
next_page_button_text = button.text
break
button_index += 1
if next_page_button_index is not None:
break
if next_page_button_index is not None:
print(f" → 发现翻页按钮: {next_page_button_text}")
# 点击下一页
try:
await msg.click(next_page_button_index)
await asyncio.sleep(2) # 等待页面加载
current_page += 1
except Exception as e:
print(f" → 点击失败: {e}")
break
else:
print(" → 没有更多页面")
break
# 防止过快请求
await asyncio.sleep(0.5)
print(f" ✅ 完成! 共翻了 {current_page}")
return all_results
async def main():
server = FunstatMCPServer()
await server.initialize()
results = []
seen = set()
keywords = [
'翻译', 'translation', 'translate', 'translator',
'字幕组', 'subtitle', 'fansub'
]
print(f"🚀 开始带翻页的完整搜索")
print(f"📋 关键词数量: {len(keywords)}")
print(f"📄 每个关键词自动翻页至所有结果")
print("=" * 80)
for i, kw in enumerate(keywords, 1):
print(f"\n[{i:2d}/{len(keywords)}] 关键词: {kw:20s}")
try:
# 搜索所有页
page_results = await search_all_pages(server, kw, max_pages=10)
# 去重
new_count = 0
for item in page_results:
if item['type'] == 'id':
key = f"ID:{item['value']}"
else:
key = f"@{item['value']}"
if key not in seen:
seen.add(key)
results.append(item)
new_count += 1
print(f" 📊 新增独特记录: {new_count} 条 (总计: {len(results)})")
except Exception as e:
print(f" ❌ 错误: {e}")
# 稍作延迟
await asyncio.sleep(1)
# 保存文件
txt_file = '/Users/lucas/chat--1003255561049/translation_users_paginated.txt'
json_file = '/Users/lucas/chat--1003255561049/translation_users_paginated.json'
with open(txt_file, 'w', encoding='utf-8') as f:
f.write("=" * 80 + "\n")
f.write("翻译相关用户/群组完整列表 (支持翻页)\n")
f.write("=" * 80 + "\n")
f.write(f"总数: {len(results)}\n")
f.write(f"搜索时间: {datetime.now()}\n")
f.write(f"数据来源: funstat BOT (@openaiw_bot)\n")
f.write(f"搜索方式: 多关键词 + 自动翻页\n")
f.write("=" * 80 + "\n\n")
for i, item in enumerate(results, 1):
if item['type'] == 'id':
f.write(f"{i:4d}. ID: {item['value']:15s} (来源: {item['keyword']}, 第{item['page']}页)\n")
else:
f.write(f"{i:4d}. @{item['value']:30s} (来源: {item['keyword']}, 第{item['page']}页)\n")
with open(json_file, 'w', encoding='utf-8') as f:
json.dump({
'total': len(results),
'timestamp': str(datetime.now()),
'method': 'multi-keyword + pagination',
'results': results
}, f, ensure_ascii=False, indent=2)
print("\n")
print("=" * 80)
print(f"✅ 搜索完成!共找到 {len(results)} 条独特记录")
print("=" * 80)
print(f"📄 文本文件: {txt_file}")
print(f"📄 JSON文件: {json_file}")
print("")
# 显示统计
print(f"🎯 最终统计:")
print(f" 总记录数: {len(results)}")
print(f" ID数量: {sum(1 for r in results if r['type'] == 'id')}")
print(f" 用户名数量: {sum(1 for r in results if r['type'] == 'username')}")
# 统计每个关键词的页数
print(f"\n📊 每个关键词的翻页统计:")
keyword_pages = {}
for item in results:
kw = item['keyword']
page = item['page']
if kw not in keyword_pages:
keyword_pages[kw] = set()
keyword_pages[kw].add(page)
for kw, pages in keyword_pages.items():
print(f" {kw:20s}: {len(pages)}")
await server.client.disconnect()
if __name__ == '__main__':
asyncio.run(main())

481
core/server.py Normal file
View File

@@ -0,0 +1,481 @@
#!/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 = int(os.getenv("TELEGRAM_API_ID", "24660516"))
API_HASH = os.getenv("TELEGRAM_API_HASH", "eae564578880a59c9963916ff1bbbd3a")
# Session 文件路径 - 使用独立的安全目录,防止被意外删除
SESSION_PATH = os.path.expanduser(
os.getenv("TELEGRAM_SESSION_PATH", "~/telegram_sessions/funstat_bot")
)
BOT_USERNAME = os.getenv("FUNSTAT_BOT_USERNAME", "@openaiw_bot")
PROXY_TYPE = os.getenv("FUNSTAT_PROXY_TYPE", "socks5")
PROXY_HOST = os.getenv("FUNSTAT_PROXY_HOST")
PROXY_PORT = os.getenv("FUNSTAT_PROXY_PORT")
PROXY_USERNAME = os.getenv("FUNSTAT_PROXY_USERNAME")
PROXY_PASSWORD = os.getenv("FUNSTAT_PROXY_PASSWORD")
# 速率限制配置
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}")
proxy = None
if PROXY_HOST and PROXY_PORT:
try:
proxy_port = int(PROXY_PORT)
if PROXY_USERNAME:
proxy = (
PROXY_TYPE,
PROXY_HOST,
proxy_port,
PROXY_USERNAME,
PROXY_PASSWORD or ""
)
else:
proxy = (PROXY_TYPE, PROXY_HOST, proxy_port)
logger.info(
"使用代理连接: %s://%s:%s",
PROXY_TYPE,
PROXY_HOST,
proxy_port,
)
except ValueError:
logger.warning(
"代理端口无效,忽略代理配置: %s",
PROXY_PORT,
)
# 创建客户端
self.client = TelegramClient(SESSION_PATH, API_ID, API_HASH, proxy=proxy)
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"].strip()
if not identifier:
raise ValueError("用户标识不能为空")
# funstat BOT 需要显式的 /user_info 命令
response = await self.send_command_and_wait(f"/user_info {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 服务器 - Streamable HTTP 模式
from mcp.server.streamable_http import StreamableHTTPServerTransport
from starlette.applications import Starlette
from starlette.routing import Mount
import uvicorn
import uuid
# 是否启用会话校验
require_session = os.getenv("FUNSTAT_REQUIRE_SESSION", "false").lower() in ("1", "true", "yes")
# 创建 Streamable HTTP 传输(生成唯一 session ID默认关闭强校验以兼容旧客户端
session_id = str(uuid.uuid4()) if require_session else None
transport = StreamableHTTPServerTransport(
mcp_session_id=session_id,
is_json_response_enabled=True, # 启用 JSON 响应
)
# 在后台运行 MCP 服务器
async def run_mcp_server():
async with transport.connect() as streams:
await self.server.run(
streams[0],
streams[1],
self.server.create_initialization_options(),
)
# 启动 MCP 服务器任务
asyncio.create_task(run_mcp_server())
# 创建 Starlette 应用transport.handle_request 是 ASGI 应用)
app = Starlette()
app.mount("/", transport.handle_request)
# 获取端口配置
port = int(os.getenv("FUNSTAT_PORT", "8091"))
host = os.getenv("FUNSTAT_HOST", "127.0.0.1")
logger.info(f"🌐 启动 SSE 服务器: http://{host}:{port}")
logger.info(f"📡 SSE 端点: http://{host}:{port}/sse")
logger.info(f"📨 消息端点: http://{host}:{port}/messages")
if session_id:
logger.info(f"🔒 Session ID: {session_id}")
# 启动服务器
config = uvicorn.Config(
app,
host=host,
port=port,
log_level="info"
)
server_instance = uvicorn.Server(config)
await server_instance.serve()
async def main():
"""主函数"""
server = FunstatMCPServer()
await server.run()
if __name__ == "__main__":
asyncio.run(main())

401
core/server_stdio_backup.py Normal file
View 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())

243
core/setup.sh Executable file
View File

@@ -0,0 +1,243 @@
#!/bin/bash
#
# Funstat MCP 自动部署脚本
# 用途:帮助新用户快速部署和配置 Funstat MCP 工具
#
set -e # 遇到错误立即退出
echo "=========================================="
echo "🚀 Funstat MCP 工具 - 自动部署向导"
echo "=========================================="
echo ""
# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# 检查 Python
echo "📋 检查前置要求..."
echo ""
if ! command -v python3 &> /dev/null; then
echo -e "${RED}❌ 未找到 Python 3${NC}"
echo "请先安装 Python 3.10 或更高版本"
echo "访问https://www.python.org/downloads/"
exit 1
fi
PYTHON_VERSION=$(python3 --version | cut -d' ' -f2)
echo -e "${GREEN}✅ Python 版本: $PYTHON_VERSION${NC}"
# 检查 pip
if ! command -v pip3 &> /dev/null; then
echo -e "${RED}❌ 未找到 pip3${NC}"
exit 1
fi
echo -e "${GREEN}✅ pip3 已安装${NC}"
echo ""
# 安装依赖
echo "=========================================="
echo "📦 安装 Python 依赖包"
echo "=========================================="
echo ""
if [ ! -f "requirements.txt" ]; then
echo -e "${RED}❌ 未找到 requirements.txt${NC}"
echo "请确保在 funstat_mcp 目录中运行此脚本"
exit 1
fi
echo "正在安装依赖..."
pip3 install -r requirements.txt --quiet
if [ $? -eq 0 ]; then
echo -e "${GREEN}✅ 依赖安装成功${NC}"
else
echo -e "${RED}❌ 依赖安装失败${NC}"
exit 1
fi
echo ""
# 配置 API 凭证
echo "=========================================="
echo "🔑 配置 Telegram API 凭证"
echo "=========================================="
echo ""
echo -e "${YELLOW}重要:每个用户需要申请自己的 API 凭证${NC}"
echo ""
echo "步骤:"
echo "1. 访问https://my.telegram.org/apps"
echo "2. 登录你的 Telegram 账号"
echo "3. 创建新应用(如果还没有)"
echo "4. 获取 API ID 和 API Hash"
echo ""
read -p "你已经获取了 API 凭证吗?(y/n): " has_credentials
if [ "$has_credentials" != "y" ]; then
echo ""
echo "请先获取 API 凭证,然后重新运行此脚本"
echo "运行命令:./setup.sh"
exit 0
fi
echo ""
read -p "请输入你的 API ID: " api_id
read -p "请输入你的 API Hash: " api_hash
# 验证输入
if [ -z "$api_id" ] || [ -z "$api_hash" ]; then
echo -e "${RED}❌ API 凭证不能为空${NC}"
exit 1
fi
# 创建 .env 文件
echo ""
echo "正在创建配置文件..."
cat > .env << EOF
# Telegram API 凭证
# 请妥善保管,不要分享给他人
TELEGRAM_API_ID=$api_id
TELEGRAM_API_HASH=$api_hash
EOF
chmod 600 .env
echo -e "${GREEN}✅ API 凭证已保存到 .env 文件${NC}"
echo ""
# 更新 .gitignore
if [ ! -f ".gitignore" ]; then
cat > .gitignore << EOF
.env
*.session
*.session-journal
__pycache__/
*.pyc
.DS_Store
EOF
echo -e "${GREEN}✅ 创建了 .gitignore 文件${NC}"
fi
echo ""
# 创建 Session
echo "=========================================="
echo "📱 创建 Telegram Session"
echo "=========================================="
echo ""
echo "现在需要登录你的 Telegram 账号来创建 session 文件"
echo -e "${YELLOW}注意:验证码会发送到你的 Telegram 应用${NC}"
echo ""
read -p "准备好了吗?按 Enter 继续..."
# 检查是否存在 create_session_safe.py
if [ -f "../create_session_safe.py" ]; then
python3 ../create_session_safe.py
elif [ -f "create_session_safe.py" ]; then
python3 create_session_safe.py
else
echo -e "${RED}❌ 未找到 create_session_safe.py${NC}"
echo "请确保项目文件完整"
exit 1
fi
echo ""
# 检查 session 是否创建成功
if [ -f ~/telegram_sessions/funstat_bot.session ]; then
echo -e "${GREEN}✅ Session 创建成功${NC}"
else
echo -e "${RED}❌ Session 创建失败${NC}"
echo "请检查上面的错误信息"
exit 1
fi
echo ""
# 测试
echo "=========================================="
echo "🧪 测试 MCP 服务器"
echo "=========================================="
echo ""
if [ -f "test_server.py" ]; then
echo "正在测试连接..."
python3 test_server.py
if [ $? -eq 0 ]; then
echo ""
echo -e "${GREEN}✅ 测试通过!${NC}"
else
echo ""
echo -e "${YELLOW}⚠️ 测试遇到问题,但可能不影响使用${NC}"
fi
else
echo -e "${YELLOW}⚠️ 未找到测试脚本,跳过测试${NC}"
fi
echo ""
# 配置 Claude Code
echo "=========================================="
echo "⚙️ 配置 Claude Code"
echo "=========================================="
echo ""
CURRENT_DIR=$(pwd)
echo "请将以下配置添加到 Claude Code 配置文件:"
echo ""
echo -e "${YELLOW}配置文件位置:${NC}"
echo " macOS: ~/Library/Application Support/Claude/claude_desktop_config.json"
echo " Linux: ~/.config/claude-code/config.json"
echo ""
echo -e "${YELLOW}添加以下内容:${NC}"
echo ""
echo '{
"mcpServers": {
"funstat": {
"command": "python3",
"args": [
"'$CURRENT_DIR'/server.py"
]
}
}
}'
echo ""
read -p "按 Enter 继续..."
# 完成
echo ""
echo "=========================================="
echo "🎉 部署完成!"
echo "=========================================="
echo ""
echo -e "${GREEN}下一步:${NC}"
echo ""
echo "1. 配置 Claude Code复制上面的配置"
echo "2. 完全退出并重启 Claude Code"
echo "3. 在 Claude Code 中测试:"
echo " \"帮我搜索 Python 学习群组\""
echo ""
echo -e "${YELLOW}安全提醒:${NC}"
echo "• .env 文件包含你的 API 凭证,不要分享给他人"
echo "• ~/telegram_sessions/ 目录包含 session不要提交到 Git"
echo "• 建议定期备份 session 文件"
echo ""
echo -e "${GREEN}文档:${NC}"
echo "• 快速开始QUICK_START_GUIDE.md"
echo "• Session 管理SESSION_MANAGEMENT.md"
echo "• 完整文档README.md"
echo ""
echo "🎊 享受使用 Funstat MCP 工具!"
echo ""

41
core/start_sse.sh Executable file
View File

@@ -0,0 +1,41 @@
#!/bin/bash
# Funstat MCP SSE Server 启动脚本
echo "🚀 启动 Funstat MCP SSE 服务器..."
echo ""
# 设置环境变量
export FUNSTAT_PORT=8091
export FUNSTAT_HOST=127.0.0.1
# 检查依赖
echo "📦 检查 Python 依赖..."
python3 -c "import starlette; import uvicorn" 2>/dev/null || {
echo "❌ 缺少依赖,正在安装..."
pip3 install -r requirements.txt
}
# 检查 Session 文件
echo "🔐 检查 Session 文件..."
if [ ! -f ~/telegram_sessions/funstat_bot.session ]; then
echo "❌ Session 文件不存在: ~/telegram_sessions/funstat_bot.session"
echo "请先运行 create_session_safe.py 创建 session 文件"
exit 1
fi
echo "✅ Session 文件存在"
echo ""
# 启动服务器
echo "🌐 启动 SSE 服务器..."
echo "📡 SSE 端点: http://${FUNSTAT_HOST}:${FUNSTAT_PORT}/sse"
echo "📨 消息端点: http://${FUNSTAT_HOST}:${FUNSTAT_PORT}/messages"
echo ""
echo "按 Ctrl+C 停止服务器"
echo ""
# 切换到脚本目录
cd "$(dirname "$0")"
# 启动
python3 server.py

58
core/start_sse_prod.sh Executable file
View File

@@ -0,0 +1,58 @@
#!/bin/bash
# Funstat MCP SSE 服务器 - 生产启动脚本
set -e
cd "$(dirname "$0")"
# 停止旧实例
echo "🛑 停止旧服务器..."
pkill -f "funstat_mcp/server.py" 2>/dev/null || true
sleep 2
# 确保 session 文件没有被锁定
if lsof /Users/lucas/telegram_sessions/funstat_bot.session 2>/dev/null; then
echo "⚠️ Session 文件被占用,强制终止..."
pkill -9 -f "funstat_mcp/server.py" || true
sleep 2
fi
# 启动新服务器
echo "🚀 启动新服务器..."
python3 server.py > /tmp/funstat_sse.log 2>&1 &
SERVER_PID=$!
# 等待启动
sleep 3
# 验证启动
if ps -p $SERVER_PID > /dev/null; then
echo "✅ 服务器已启动 (PID: $SERVER_PID)"
echo "📡 SSE 端点: http://127.0.0.1:8091/sse"
echo "📋 日志文件: /tmp/funstat_sse.log"
# 测试端点
echo ""
echo "🧪 测试端点..."
if curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:8091/sse | grep -q "200"; then
echo "✅ GET /sse 测试通过"
else
echo "❌ GET /sse 测试失败"
fi
if curl -s -o /dev/null -w "%{http_code}" -X POST http://127.0.0.1:8091/sse -H 'Content-Type: application/json' -d '{}' | grep -q "200"; then
echo "✅ POST /sse 测试通过"
else
echo "❌ POST /sse 测试失败"
fi
echo ""
echo "📊 服务器状态:"
echo " 进程ID: $SERVER_PID"
echo " 监听地址: http://127.0.0.1:8091"
echo " 日志: tail -f /tmp/funstat_sse.log"
else
echo "❌ 服务器启动失败!"
echo "查看日志: tail -50 /tmp/funstat_sse.log"
exit 1
fi

52
core/test_codex_connection.sh Executable file
View File

@@ -0,0 +1,52 @@
#!/bin/bash
echo "测试 Codex CLI 与 Funstat MCP 连接..."
echo ""
# 1. 检查服务器状态
echo "1. 检查 SSE 服务器状态..."
if ps aux | grep -q "[s]erver.py"; then
echo "✓ SSE 服务器正在运行"
ps aux | grep "[s]erver.py" | awk '{print " PID:", $2}'
else
echo "✗ SSE 服务器未运行"
exit 1
fi
# 2. 测试 SSE 端点
echo ""
echo "2. 测试 SSE 端点 (GET /sse)..."
response=$(timeout 2 curl -s -N -H "Accept: text/event-stream" http://127.0.0.1:8091/sse 2>&1 | head -3)
if echo "$response" | grep -q "event: endpoint"; then
echo "✓ SSE 端点响应正常"
echo "$response" | grep "data:" | head -1
else
echo "✗ SSE 端点响应异常"
echo "$response"
fi
# 3. 检查 Codex 配置
echo ""
echo "3. 检查 Codex MCP 配置..."
if codex mcp get funstat 2>/dev/null | grep -q "enabled: true"; then
echo "✓ Funstat MCP 已配置"
codex mcp get funstat | grep -E "(enabled|transport|url)"
else
echo "✗ Funstat MCP 未配置"
exit 1
fi
# 4. 查看最近的服务器日志
echo ""
echo "4. 最近的服务器日志 (最后10行)..."
tail -10 /tmp/funstat_sse.log | grep -v "^$"
echo ""
echo "=========================================="
echo "测试完成!"
echo ""
echo "下一步: 在终端中运行 Codex CLI 进行实际测试:"
echo " codex"
echo ""
echo "然后尝试询问: '列出可用的 MCP 工具'"
echo "=========================================="

66
core/test_pagination.py Normal file
View File

@@ -0,0 +1,66 @@
#!/usr/bin/env python3
"""测试funstat BOT的翻页按钮"""
import sys
sys.path.insert(0, '.')
from server import FunstatMCPServer
import asyncio
async def main():
server = FunstatMCPServer()
await server.initialize()
print("📤 发送搜索命令...")
await server.client.send_message(server.bot_entity, '/search 翻译')
# 等待响应
await asyncio.sleep(3)
# 获取最新消息
messages = await server.client.get_messages(server.bot_entity, limit=1)
msg = messages[0]
print(f"\n📨 消息内容:\n{msg.text}\n")
# 检查是否有按钮
if msg.reply_markup:
print("✅ 发现按钮!")
print(f" 类型: {type(msg.reply_markup)}")
if hasattr(msg.reply_markup, 'rows'):
print(f" 按钮行数: {len(msg.reply_markup.rows)}")
for i, row in enumerate(msg.reply_markup.rows):
print(f"\n{i+1} 行:")
for j, button in enumerate(row.buttons):
print(f" 按钮 {j+1}: {button.text}")
if hasattr(button, 'data'):
print(f" 数据: {button.data}")
# 尝试点击第一个按钮
if len(msg.reply_markup.rows) > 0:
print("\n🖱️ 尝试点击第一个按钮...")
try:
await msg.click(0) # 点击第一个按钮
await asyncio.sleep(2)
# 获取新消息
new_messages = await server.client.get_messages(server.bot_entity, limit=1)
new_msg = new_messages[0]
print(f"\n📨 点击后的新消息:\n{new_msg.text}\n")
if new_msg.reply_markup:
print("✅ 新消息也有按钮")
for i, row in enumerate(new_msg.reply_markup.rows):
for j, button in enumerate(row.buttons):
print(f" 按钮: {button.text}")
except Exception as e:
print(f"❌ 点击失败: {e}")
else:
print(" 没有rows属性")
else:
print("❌ 没有发现按钮")
await server.client.disconnect()
if __name__ == '__main__':
asyncio.run(main())

86
core/test_server.py Normal file
View File

@@ -0,0 +1,86 @@
#!/usr/bin/env python3
"""
测试 Funstat MCP Server
这个脚本会测试 MCP 服务器的所有功能
"""
import asyncio
import sys
from server import FunstatMCPServer
async def test_server():
"""测试服务器功能"""
print("=" * 60)
print("🧪 Funstat MCP Server 测试")
print("=" * 60)
print()
server = FunstatMCPServer()
try:
# 初始化
print("1⃣ 初始化服务器...")
await server.initialize()
print("✅ 初始化成功")
print()
# 测试工具列表
print("2⃣ 获取工具列表...")
tools = await server.list_tools()
print(f"✅ 找到 {len(tools)} 个工具:")
for tool in tools:
print(f" - {tool.name}: {tool.description}")
print()
# 测试 /start 命令
print("3⃣ 测试 /start 命令...")
result = await server.call_tool("funstat_start", {})
response = result[0].text
print("✅ 响应:")
print(response[:500])
if len(response) > 500:
print(f" ... (还有 {len(response) - 500} 个字符)")
print()
# 测试 /balance 命令
print("4⃣ 测试 /balance 命令...")
result = await server.call_tool("funstat_balance", {})
print("✅ 响应:")
print(result[0].text)
print()
# 测试搜索功能
print("5⃣ 测试搜索功能 (关键词: python)...")
result = await server.call_tool("funstat_search", {"query": "python"})
response = result[0].text
print("✅ 响应:")
print(response[:500])
if len(response) > 500:
print(f" ... (还有 {len(response) - 500} 个字符)")
print()
# 测试缓存
print("6⃣ 测试缓存 (再次搜索 python)...")
result = await server.call_tool("funstat_search", {"query": "python"})
print("✅ 响应: (应该来自缓存)")
print(result[0].text[:200])
print()
print("=" * 60)
print("✅ 所有测试通过!")
print("=" * 60)
except Exception as e:
print(f"❌ 测试失败: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
finally:
if server.client:
await server.client.disconnect()
if __name__ == "__main__":
asyncio.run(test_server())

73
core/test_text_search.py Normal file
View File

@@ -0,0 +1,73 @@
#!/usr/bin/env python3
"""测试 funstat text 搜索功能"""
import asyncio
import time
from datetime import datetime
from telethon import TelegramClient
# 配置
API_ID = 24660516
API_HASH = "eae564578880a59c9963916ff1bbbd3a"
SESSION_PATH = "/Users/lucas/telegram_sessions/funstat_bot"
BOT_USERNAME = "@openaiw_bot"
async def send_command_and_wait(client, bot_entity, command, timeout=15):
"""发送命令到 BOT 并等待响应"""
print(f"📤 发送命令: {command}")
# 记录发送前的最新消息 ID
last_message_id = 0
async for message in client.iter_messages(bot_entity, limit=1):
last_message_id = message.id
break
# 发送消息
await client.send_message(bot_entity, command)
# 等待响应
await asyncio.sleep(2)
# 获取新消息
start_time = time.time()
while time.time() - start_time < timeout:
async for message in client.iter_messages(bot_entity, limit=5):
if message.id > last_message_id:
if not message.out and message.text:
print(f"\n✅ 收到响应 ({len(message.text)} 字符):\n")
print("=" * 60)
print(message.text)
print("=" * 60)
return message.text
await asyncio.sleep(0.5)
raise TimeoutError(f"等待 BOT 响应超时 ({timeout}秒)")
async def main():
"""主函数"""
print("初始化 Telegram 客户端...")
# 创建客户端
client = TelegramClient(SESSION_PATH, API_ID, API_HASH)
await client.start()
# 获取 bot 实体
bot_entity = await client.get_entity(BOT_USERNAME)
print(f"✅ 已连接到: {bot_entity.first_name}\n")
# 发送 text 搜索命令
try:
await send_command_and_wait(client, bot_entity, "/text 翻译")
except Exception as e:
print(f"❌ 错误: {e}")
# 断开连接
await client.disconnect()
print("\n✅ 完成")
if __name__ == "__main__":
asyncio.run(main())