Add deployment scripts and documentation

- Add deployment scripts (deploy.sh, test_connection.sh, core/start_server.sh)
- Add deployment documentation (DEPLOYMENT_INFO.md, DEPLOYMENT_SUCCESS.md)
- Add .env.example configuration template
- Add requirements.txt for Python dependencies
- Update README.md with latest information
- Update core/server.py with improvements

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
你的用户名
2025-11-04 15:19:44 +08:00
parent a05a7dd40e
commit c4be264ea5
9 changed files with 911 additions and 1 deletions

View File

@@ -25,6 +25,8 @@ from mcp.types import (
)
from pydantic import AnyUrl
from telethon import TelegramClient
from telethon.errors import FloodWaitError
from telethon.tl.functions.messages import GetBotCallbackAnswerRequest
from telethon.tl.types import Message
# 配置日志
@@ -56,6 +58,40 @@ RATE_LIMIT_WINDOW = 1.0 # 1秒时间窗口
# 缓存配置
CACHE_TTL = 3600 # 缓存1小时
# 按钮文本转换表,用于将常见的变体字符标准化为 ASCII
BUTTON_TEXT_TRANSLATIONS = str.maketrans({
'ƒ': 'f',
'Μ': 'M',
'τ': 't',
'ѕ': 's',
'η': 'n',
'Ғ': 'F',
'α': 'a',
'ο': 'o',
'': 'u',
'о': 'o',
'е': 'e',
'с': 'c',
'': 'e',
'Τ': 'T',
'ρ': 'p',
'Δ': 'D',
'χ': 'x',
'β': 'b',
'λ': 'l',
'γ': 'y',
'Ν': 'N',
'μ': 'm',
'ψ': 'y',
'Α': 'A',
'Ρ': 'P',
'С': 'C',
'ё': 'e',
'ł': 'l',
'Ł': 'L',
'ց': 'g',
})
class RateLimiter:
"""速率限制器"""
@@ -136,6 +172,123 @@ class FunstatMCPServer:
self.server.list_tools()(self.list_tools)
self.server.call_tool()(self.call_tool)
def _normalize_button_text(self, text: str) -> str:
"""标准化按钮文本,消除不同字符集的影响"""
return text.translate(BUTTON_TEXT_TRANSLATIONS)
async def _press_button(self, message: Message, keyword: str) -> Message:
"""在消息中查找包含关键字的按钮并触发"""
if not message.buttons:
raise ValueError(f"消息中缺少可用按钮,无法执行 {keyword} 操作")
target_button = None
normalized_keyword = keyword.lower()
for row in message.buttons:
for button in row:
normalized_text = self._normalize_button_text(button.text).lower()
if normalized_keyword in normalized_text:
target_button = button
break
if target_button:
break
if not target_button:
available = [
self._normalize_button_text(button.text)
for row in message.buttons
for button in row
]
raise ValueError(
f"未找到包含关键字 '{keyword}' 的按钮。可用按钮: {available}"
)
await self.rate_limiter.acquire()
try:
await self.client(
GetBotCallbackAnswerRequest(
peer=self.bot_entity,
msg_id=message.id,
data=target_button.data,
)
)
except FloodWaitError as exc:
wait_seconds = exc.seconds + 1
logger.warning("触发 Telegram FloodWait需要等待 %s", wait_seconds)
await asyncio.sleep(wait_seconds)
await self.client(
GetBotCallbackAnswerRequest(
peer=self.bot_entity,
msg_id=message.id,
data=target_button.data,
)
)
await asyncio.sleep(1.2)
refreshed = await self.client.get_messages(self.bot_entity, ids=message.id)
if not refreshed:
raise RuntimeError("回调执行后未能获取最新消息内容")
return refreshed
def _extract_total_pages(self, message: Message) -> Optional[int]:
"""从按钮中提取总页数(如果提供了跳页按钮)"""
if not message.buttons:
return None
total_pages = None
for row in message.buttons:
for button in row:
if '' in button.text:
normalized = self._normalize_button_text(button.text)
digits = ''.join(ch for ch in normalized if ch.isdigit())
if digits:
try:
total_pages = int(digits)
except ValueError:
continue
return total_pages
async def send_command_and_wait_message(
self,
command: str,
timeout: int = 10,
) -> Message:
"""发送命令并返回原始消息对象(包含按钮等信息)"""
if not self.client or not self.bot_entity:
raise RuntimeError("Telegram 客户端尚未初始化")
await self.rate_limiter.acquire()
last_message_id = 0
async for message in self.client.iter_messages(self.bot_entity, limit=1):
last_message_id = message.id
break
logger.info("📤 发送命令(原始消息模式): %s", command)
await self.client.send_message(self.bot_entity, command)
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:
continue
if message.out:
continue
if message.text or message.buttons:
logger.info(
"✅ 收到原始响应 (ID: %s, 文本长度: %s)",
message.id,
len(message.text or ""),
)
return message
await asyncio.sleep(0.5)
raise TimeoutError(f"等待 BOT 响应超时 ({timeout}秒)")
async def initialize(self):
"""初始化 Telegram 客户端"""
logger.info("初始化 Telegram 客户端...")
@@ -246,6 +399,99 @@ class FunstatMCPServer:
raise TimeoutError(f"等待 BOT 响应超时 ({timeout}秒)")
async def fetch_user_messages(
self,
identifier: str,
max_pages: Optional[int] = None
) -> str:
"""获取指定用户的历史消息,支持自动翻页"""
if not identifier or not identifier.strip():
raise ValueError("用户标识不能为空")
identifier = identifier.strip()
display_identifier = identifier
if identifier.startswith("/"):
command = identifier
else:
if not identifier.startswith("@") and not identifier.replace("+", "").isdigit():
identifier = f"@{identifier}"
display_identifier = identifier
command = f"/user_info {identifier}"
logger.info("开始获取用户消息: %s", display_identifier)
base_message = await self.send_command_and_wait_message(command, timeout=15)
message_stage = await self._press_button(base_message, "messages")
all_stage = await self._press_button(message_stage, "all")
collected_pages: List[str] = []
seen_texts: set[str] = set()
current_message = all_stage
current_page = 1
total_pages = self._extract_total_pages(current_message)
if max_pages is not None and max_pages <= 0:
raise ValueError("max_pages 必须大于 0")
while True:
page_text = current_message.text or ""
normalized_text = page_text.strip()
if normalized_text and normalized_text not in seen_texts:
header_parts = [f"{current_page}"]
if total_pages:
header_parts[-1] += f"/{total_pages}"
header_parts.append(f"用户: {display_identifier}")
collected_pages.append(
"\n".join(header_parts + ["", page_text.strip()])
)
seen_texts.add(normalized_text)
if max_pages and current_page >= max_pages:
logger.info("达到 max_pages 限制,停止翻页")
break
next_button = None
if current_message.buttons:
for row in current_message.buttons:
for button in row:
if "" in button.text:
next_button = button
break
if next_button:
break
if not next_button:
break
logger.info("翻到第 %s 页 (目标按钮: %s)", current_page + 1, next_button.text)
try:
current_message = await self._press_button(current_message, "")
except ValueError:
logger.warning("未能找到下一页按钮,提前结束翻页")
break
# 如果返回的内容与上一页一致,则终止
if (current_message.text or "").strip() in seen_texts:
logger.info("检测到重复页面内容,结束翻页")
break
current_page += 1
if not collected_pages:
return f"未找到 {display_identifier} 的消息记录。"
summary_lines = [
f"共收集 {len(collected_pages)} 页消息"
+ (f"(存在 {total_pages} 页)" if total_pages else ""),
""
]
return "\n\n".join(summary_lines + collected_pages)
async def list_tools(self) -> List[Tool]:
"""列出所有可用工具"""
return [
@@ -318,6 +564,25 @@ class FunstatMCPServer:
"required": ["identifier"]
}
),
Tool(
name="funstat_user_messages",
description="获取指定用户的历史消息列表,并自动翻页汇总",
inputSchema={
"type": "object",
"properties": {
"identifier": {
"type": "string",
"description": "用户标识: 用户名(@username) 或用户ID"
},
"max_pages": {
"type": "integer",
"minimum": 1,
"description": "可选,限制抓取的最大页数"
}
},
"required": ["identifier"]
}
),
Tool(
name="funstat_balance",
description="查询当前账号的积分余额和使用统计",
@@ -381,6 +646,12 @@ class FunstatMCPServer:
response = await self.send_command_and_wait(f"/user_info {identifier}")
return [TextContent(type="text", text=response)]
elif name == "funstat_user_messages":
identifier = arguments["identifier"]
max_pages = arguments.get("max_pages")
response = await self.fetch_user_messages(identifier, max_pages=max_pages)
return [TextContent(type="text", text=response)]
elif name == "funstat_balance":
response = await self.send_command_and_wait("/balance")
return [TextContent(type="text", text=response)]

64
core/start_server.sh Executable file
View File

@@ -0,0 +1,64 @@
#!/bin/bash
# Funstat MCP 服务器启动脚本(适配服务器环境)
set -e
cd "$(dirname "$0")"
# 停止旧实例
echo "🛑 停止旧服务器..."
pkill -f "funstat.*server.py" 2>/dev/null || true
sleep 2
# 确保 session 文件没有被锁定
SESSION_FILE=~/telegram_sessions/funstat_bot.session
if [ -f "$SESSION_FILE" ]; then
if lsof "$SESSION_FILE" 2>/dev/null; then
echo "⚠️ Session 文件被占用,强制终止..."
pkill -9 -f "funstat.*server.py" || true
sleep 2
fi
else
echo "❌ Session 文件不存在: $SESSION_FILE"
echo "请先上传 session 文件!"
exit 1
fi
# 激活虚拟环境
source ../.venv/bin/activate
# 启动新服务器(后台运行)
echo "🚀 启动新服务器..."
nohup python3 server.py > /tmp/funstat_sse.log 2>&1 &
SERVER_PID=$!
# 等待启动
sleep 3
# 验证启动
if ps -p $SERVER_PID > /dev/null 2>&1; 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
echo ""
echo "📊 服务器状态:"
echo " 进程ID: $SERVER_PID"
echo " 监听地址: http://127.0.0.1:8091"
echo " 日志: tail -f /tmp/funstat_sse.log"
echo ""
echo "停止服务: pkill -f 'funstat.*server.py'"
else
echo "❌ 服务器启动失败!"
echo "查看日志: tail -50 /tmp/funstat_sse.log"
exit 1
fi