chore: initial commit
This commit is contained in:
43
core/debug_bot.py
Normal file
43
core/debug_bot.py
Normal 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
181
core/http_server.py
Normal 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)
|
||||
115
core/search_all_translation.py
Normal file
115
core/search_all_translation.py
Normal 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())
|
||||
192
core/search_with_pagination.py
Normal file
192
core/search_with_pagination.py
Normal 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
481
core/server.py
Normal 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
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())
|
||||
243
core/setup.sh
Executable file
243
core/setup.sh
Executable 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
41
core/start_sse.sh
Executable 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
58
core/start_sse_prod.sh
Executable 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
52
core/test_codex_connection.sh
Executable 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
66
core/test_pagination.py
Normal 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
86
core/test_server.py
Normal 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
73
core/test_text_search.py
Normal 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())
|
||||
Reference in New Issue
Block a user