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:
15
.env.example
Normal file
15
.env.example
Normal file
@@ -0,0 +1,15 @@
|
||||
# Telegram API 配置
|
||||
TELEGRAM_API_ID=your_api_id
|
||||
TELEGRAM_API_HASH=your_api_hash
|
||||
TELEGRAM_SESSION_PATH=~/telegram_sessions/funstat_bot
|
||||
FUNSTAT_BOT_USERNAME=@openaiw_bot
|
||||
|
||||
# 代理配置(如果服务器无法直接访问 Telegram,需要配置代理)
|
||||
FUNSTAT_PROXY_TYPE=socks5
|
||||
FUNSTAT_PROXY_HOST=127.0.0.1
|
||||
FUNSTAT_PROXY_PORT=1080
|
||||
|
||||
# 服务器配置
|
||||
# 监听地址: 0.0.0.0 表示所有网络接口,127.0.0.1 表示仅本地访问
|
||||
FUNSTAT_HOST=0.0.0.0
|
||||
FUNSTAT_PORT=8091
|
||||
177
DEPLOYMENT_INFO.md
Normal file
177
DEPLOYMENT_INFO.md
Normal file
@@ -0,0 +1,177 @@
|
||||
# Funstat MCP 部署信息
|
||||
|
||||
## 服务器信息
|
||||
|
||||
- **服务器 IP**: 172.16.74.159
|
||||
- **用户**: atai
|
||||
- **部署路径**: /home/atai/funstat-mcp
|
||||
- **端口**: 8091 (本地监听)
|
||||
|
||||
## 部署状态
|
||||
|
||||
✅ **部署成功** - 2025-11-02
|
||||
|
||||
### 已安装组件
|
||||
|
||||
- Python 3.12.3
|
||||
- 虚拟环境: /home/atai/funstat-mcp/.venv
|
||||
- MCP 服务器: v1.20.0
|
||||
- Telethon: v1.41.2
|
||||
- 代理: v2ray (SOCKS5 on 127.0.0.1:1080)
|
||||
|
||||
### 配置文件
|
||||
|
||||
- **环境变量**: /home/atai/funstat-mcp/.env
|
||||
- **Session 文件**: ~/telegram_sessions/funstat_bot.session
|
||||
- **启动脚本**: /home/atai/funstat-mcp/core/start_server_with_env.sh
|
||||
|
||||
## 服务管理
|
||||
|
||||
### 启动服务
|
||||
|
||||
```bash
|
||||
ssh atai@172.16.74.159
|
||||
cd /home/atai/funstat-mcp/core
|
||||
bash start_server_with_env.sh
|
||||
```
|
||||
|
||||
### 停止服务
|
||||
|
||||
```bash
|
||||
ssh atai@172.16.74.159
|
||||
pkill -f 'funstat.*server.py'
|
||||
```
|
||||
|
||||
### 查看日志
|
||||
|
||||
```bash
|
||||
ssh atai@172.16.74.159
|
||||
tail -f /tmp/funstat_sse.log
|
||||
```
|
||||
|
||||
### 检查状态
|
||||
|
||||
```bash
|
||||
ssh atai@172.16.74.159
|
||||
ps aux | grep python | grep server.py
|
||||
netstat -tuln | grep 8091 # 或 ss -tuln | grep 8091
|
||||
```
|
||||
|
||||
## 服务端点
|
||||
|
||||
- **SSE 端点**: http://172.16.74.159:8091/sse
|
||||
- **消息端点**: http://172.16.74.159:8091/messages
|
||||
- **内部访问**: http://127.0.0.1:8091/sse
|
||||
|
||||
✅ 服务监听 0.0.0.0:8091,可从任何网络接口访问。
|
||||
|
||||
## 环境变量
|
||||
|
||||
当前配置在 `/home/atai/funstat-mcp/.env`:
|
||||
|
||||
```bash
|
||||
# Telegram API 配置
|
||||
TELEGRAM_API_ID=24660516
|
||||
TELEGRAM_API_HASH=eae564578880a59c9963916ff1bbbd3a
|
||||
TELEGRAM_SESSION_PATH=~/telegram_sessions/funstat_bot
|
||||
FUNSTAT_BOT_USERNAME=@openaiw_bot
|
||||
|
||||
# 代理配置
|
||||
FUNSTAT_PROXY_TYPE=socks5
|
||||
FUNSTAT_PROXY_HOST=127.0.0.1
|
||||
FUNSTAT_PROXY_PORT=1080
|
||||
|
||||
# 服务器配置
|
||||
FUNSTAT_HOST=0.0.0.0
|
||||
FUNSTAT_PORT=8091
|
||||
```
|
||||
|
||||
## 重新部署
|
||||
|
||||
从本地更新代码到服务器:
|
||||
|
||||
```bash
|
||||
cd /Users/hahaha/projects/funstat-mcp
|
||||
bash deploy.sh
|
||||
```
|
||||
|
||||
部署脚本会自动:
|
||||
1. 打包项目文件
|
||||
2. 上传到服务器
|
||||
3. 停止旧服务
|
||||
4. 解压更新文件
|
||||
5. 安装依赖
|
||||
|
||||
部署后需要手动启动服务:
|
||||
|
||||
```bash
|
||||
ssh atai@172.16.74.159
|
||||
cd /home/atai/funstat-mcp/core
|
||||
bash start_server_with_env.sh
|
||||
```
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 1. 服务无法启动
|
||||
|
||||
检查日志:
|
||||
```bash
|
||||
tail -50 /tmp/funstat_sse.log
|
||||
```
|
||||
|
||||
### 2. 无法连接 Telegram
|
||||
|
||||
检查代理是否运行:
|
||||
```bash
|
||||
ps aux | grep v2ray
|
||||
netstat -tuln | grep 1080
|
||||
```
|
||||
|
||||
### 3. Session 文件锁定
|
||||
|
||||
强制停止并重启:
|
||||
```bash
|
||||
pkill -9 -f 'funstat.*server.py'
|
||||
sleep 2
|
||||
cd /home/atai/funstat-mcp/core
|
||||
bash start_server_with_env.sh
|
||||
```
|
||||
|
||||
## 运行日志示例
|
||||
|
||||
成功启动的日志应该包含:
|
||||
|
||||
```
|
||||
✅ 已连接到: KT超级数据
|
||||
✅ 当前账号: @xiaobai_80
|
||||
🚀 Funstat MCP Server 已启动
|
||||
🌐 启动 SSE 服务器: http://127.0.0.1:8091
|
||||
Uvicorn running on http://127.0.0.1:8091
|
||||
```
|
||||
|
||||
## 安全注意事项
|
||||
|
||||
1. ⚠️ .env 文件包含敏感信息,已设置权限保护
|
||||
2. ⚠️ Session 文件包含登录凭证,定期备份
|
||||
3. ⚠️ 服务监听 0.0.0.0:8091,可从任何网络访问,请注意安全
|
||||
4. ⚠️ 服务器需要使用 v2ray 代理访问 Telegram(端口 1080),确保代理服务稳定
|
||||
5. ⚠️ 必须使用 start_server_with_env.sh 启动脚本,才能正确加载环境变量和代理配置
|
||||
6. ⚠️ 建议在生产环境配置防火墙规则,限制访问来源IP
|
||||
|
||||
## 更新历史
|
||||
|
||||
- **2025-11-02**: 初次部署成功
|
||||
- 安装所有依赖(包括 PySocks)
|
||||
- 配置 v2ray 代理(socks5://127.0.0.1:1080)
|
||||
- 创建带环境变量的启动脚本
|
||||
- 配置服务监听 0.0.0.0:8091,支持外部访问
|
||||
- 服务正常运行
|
||||
- ⚠️ 注意:服务器无法直接访问 Telegram,必须使用代理
|
||||
- ✅ 可从任何网络通过 http://172.16.74.159:8091 访问
|
||||
|
||||
## 联系信息
|
||||
|
||||
- 服务器: 172.16.74.159:22
|
||||
- 账号: atai
|
||||
- Bot: @openaiw_bot
|
||||
- 当前 Telegram 账号: @xiaobai_80
|
||||
252
DEPLOYMENT_SUCCESS.md
Normal file
252
DEPLOYMENT_SUCCESS.md
Normal file
@@ -0,0 +1,252 @@
|
||||
# Funstat MCP 部署成功报告
|
||||
|
||||
## ✅ 部署状态:成功
|
||||
|
||||
**部署时间**: 2025-11-02
|
||||
**服务器**: 172.16.74.159 (atai)
|
||||
**服务进程**: PID 105552
|
||||
|
||||
---
|
||||
|
||||
## 🌐 访问信息
|
||||
|
||||
### 外部访问
|
||||
- **SSE 端点**: http://172.16.74.159:8091/sse
|
||||
- **消息端点**: http://172.16.74.159:8091/messages
|
||||
|
||||
### 内部访问(服务器内)
|
||||
- http://127.0.0.1:8091/sse
|
||||
- http://127.0.0.1:8091/messages
|
||||
|
||||
### 监听配置
|
||||
- **监听地址**: 0.0.0.0 (所有网络接口)
|
||||
- **监听端口**: 8091
|
||||
- **协议**: HTTP + SSE
|
||||
|
||||
---
|
||||
|
||||
## ✅ 服务状态
|
||||
|
||||
```bash
|
||||
# 进程状态
|
||||
PID: 105552
|
||||
运行中: ✅
|
||||
内存使用: 78304 KB
|
||||
|
||||
# 网络状态
|
||||
端口: 0.0.0.0:8091 LISTEN
|
||||
防火墙: inactive (无限制)
|
||||
|
||||
# Telegram 连接
|
||||
状态: ✅ 已连接
|
||||
Bot: @openaiw_bot (KT超级数据)
|
||||
账号: @xiaobai_80 (ID: 7363537082)
|
||||
代理: socks5://127.0.0.1:1080
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 部署清单
|
||||
|
||||
### 1. 系统环境
|
||||
- [x] Ubuntu 24.04.3 LTS
|
||||
- [x] Python 3.12.3
|
||||
- [x] v2ray 代理运行中 (端口 1080)
|
||||
|
||||
### 2. 项目部署
|
||||
- [x] 代码部署到 /home/atai/funstat-mcp
|
||||
- [x] 虚拟环境创建 (.venv)
|
||||
- [x] 依赖包安装完成 (mcp, telethon, starlette, uvicorn, PySocks, etc.)
|
||||
|
||||
### 3. 配置文件
|
||||
- [x] .env 环境变量配置
|
||||
- [x] Session 文件 (~/telegram_sessions/funstat_bot.session)
|
||||
- [x] 启动脚本 (start_server_with_env.sh)
|
||||
|
||||
### 4. 服务配置
|
||||
- [x] 监听地址: 0.0.0.0:8091
|
||||
- [x] 代理配置: socks5://127.0.0.1:1080
|
||||
- [x] 自动重启脚本
|
||||
|
||||
---
|
||||
|
||||
## 🔧 管理命令
|
||||
|
||||
### 启动服务
|
||||
```bash
|
||||
ssh atai@172.16.74.159
|
||||
cd /home/atai/funstat-mcp/core
|
||||
bash start_server_with_env.sh
|
||||
```
|
||||
|
||||
### 停止服务
|
||||
```bash
|
||||
ssh atai@172.16.74.159
|
||||
pkill -f 'funstat.*server.py'
|
||||
```
|
||||
|
||||
### 查看日志
|
||||
```bash
|
||||
ssh atai@172.16.74.159
|
||||
tail -f /tmp/funstat_sse.log
|
||||
```
|
||||
|
||||
### 检查状态
|
||||
```bash
|
||||
ssh atai@172.16.74.159
|
||||
ps aux | grep 'python.*server.py' | grep -v grep
|
||||
ss -tuln | grep 8091
|
||||
```
|
||||
|
||||
### 测试访问
|
||||
```bash
|
||||
# 从服务器内部测试
|
||||
ssh atai@172.16.74.159
|
||||
curl -s -o /dev/null -w '%{http_code}\n' http://172.16.74.159:8091/sse
|
||||
|
||||
# 从本地测试(如果网络可达)
|
||||
curl -s -o /dev/null -w '%{http_code}\n' http://172.16.74.159:8091/sse
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 重新部署
|
||||
|
||||
### 方法 1: 使用部署脚本(本地执行)
|
||||
```bash
|
||||
cd /Users/hahaha/projects/funstat-mcp
|
||||
bash deploy.sh
|
||||
```
|
||||
|
||||
然后登录服务器启动:
|
||||
```bash
|
||||
ssh atai@172.16.74.159
|
||||
cd /home/atai/funstat-mcp/core
|
||||
bash start_server_with_env.sh
|
||||
```
|
||||
|
||||
### 方法 2: 手动部署
|
||||
```bash
|
||||
# 1. 打包代码
|
||||
cd /Users/hahaha/projects/funstat-mcp
|
||||
tar --exclude='.git' --exclude='.venv' -czf /tmp/funstat-mcp.tar.gz .
|
||||
|
||||
# 2. 上传到服务器
|
||||
scp /tmp/funstat-mcp.tar.gz atai@172.16.74.159:/tmp/
|
||||
|
||||
# 3. 在服务器上解压并重启
|
||||
ssh atai@172.16.74.159
|
||||
cd /home/atai/funstat-mcp
|
||||
tar -xzf /tmp/funstat-mcp.tar.gz
|
||||
cd core
|
||||
bash start_server_with_env.sh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 重要说明
|
||||
|
||||
### 网络要求
|
||||
- ⚠️ **服务器无法直接访问 Telegram**,必须通过 v2ray 代理(端口 1080)
|
||||
- ✅ v2ray 代理已配置并运行正常
|
||||
- ✅ 服务已配置使用 socks5://127.0.0.1:1080 代理
|
||||
|
||||
### 代理依赖
|
||||
如果代理服务停止,Telegram 连接将失败。检查代理状态:
|
||||
```bash
|
||||
ssh atai@172.16.74.159
|
||||
ps aux | grep v2ray
|
||||
ss -tuln | grep 1080
|
||||
```
|
||||
|
||||
### Session 文件
|
||||
- 路径: ~/telegram_sessions/funstat_bot.session
|
||||
- 大小: 72KB
|
||||
- 权限: 600 (仅所有者可读写)
|
||||
- ⚠️ 包含登录凭证,务必保管好
|
||||
|
||||
### 防火墙
|
||||
- 当前状态: inactive (无防火墙限制)
|
||||
- 端口 8091 对所有网络开放
|
||||
- 如需限制访问,可配置 ufw 规则
|
||||
|
||||
---
|
||||
|
||||
## 📊 访问日志示例
|
||||
|
||||
服务器日志显示已有外部访问:
|
||||
```
|
||||
INFO: 172.16.72.87:61462 - "GET /sse HTTP/1.1" 406 Not Acceptable
|
||||
INFO: 172.16.74.159:58984 - "GET /sse HTTP/1.1" 406 Not Acceptable
|
||||
INFO: 172.16.72.87:61532 - "GET /sse HTTP/1.1" 200 OK
|
||||
```
|
||||
|
||||
- 406 响应: 正常,表示客户端未发送正确的 Accept 头
|
||||
- 200 响应: 成功建立 SSE 连接
|
||||
|
||||
---
|
||||
|
||||
## 🛡️ 安全建议
|
||||
|
||||
1. ✅ Session 文件已设置权限保护 (600)
|
||||
2. ✅ .env 文件包含敏感信息,不要提交到 Git
|
||||
3. ⚠️ 服务监听 0.0.0.0,建议配置防火墙限制访问 IP
|
||||
4. ⚠️ 定期备份 session 文件
|
||||
5. ⚠️ 确保 v2ray 代理服务稳定运行
|
||||
|
||||
### 推荐的防火墙配置(可选)
|
||||
```bash
|
||||
sudo ufw allow from <your-ip>/32 to any port 8091
|
||||
sudo ufw enable
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📞 技术支持
|
||||
|
||||
如遇问题,按以下顺序排查:
|
||||
|
||||
1. **服务未启动**
|
||||
```bash
|
||||
cd /home/atai/funstat-mcp/core
|
||||
bash start_server_with_env.sh
|
||||
```
|
||||
|
||||
2. **无法连接 Telegram**
|
||||
- 检查 v2ray 代理: `ps aux | grep v2ray`
|
||||
- 检查代理端口: `ss -tuln | grep 1080`
|
||||
- 查看服务日志: `tail -50 /tmp/funstat_sse.log`
|
||||
|
||||
3. **Session 文件锁定**
|
||||
```bash
|
||||
pkill -9 python3
|
||||
sleep 2
|
||||
cd /home/atai/funstat-mcp/core
|
||||
bash start_server_with_env.sh
|
||||
```
|
||||
|
||||
4. **依赖包缺失**
|
||||
```bash
|
||||
cd /home/atai/funstat-mcp
|
||||
source .venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ 验证清单
|
||||
|
||||
部署完成后,确认以下项目:
|
||||
|
||||
- [x] 服务进程正在运行
|
||||
- [x] 端口 8091 正在监听
|
||||
- [x] Telegram 连接成功
|
||||
- [x] SSE 端点响应正常
|
||||
- [x] 日志文件正常记录
|
||||
- [x] 可从外部访问(如果网络可达)
|
||||
|
||||
---
|
||||
|
||||
**部署完成时间**: 2025-11-02
|
||||
**状态**: ✅ 成功运行
|
||||
**下次维护**: 定期检查日志和代理状态
|
||||
@@ -49,7 +49,7 @@ funstat_mcp_package/
|
||||
|
||||
## 🎯 核心功能
|
||||
|
||||
### MCP 工具列表 (8个)
|
||||
### MCP 工具列表 (9个)
|
||||
|
||||
| 工具名 | 功能 | 对应命令 |
|
||||
|--------|------|---------|
|
||||
@@ -61,6 +61,7 @@ funstat_mcp_package/
|
||||
| `funstat_text` | 搜索消息内容 | `/text [关键词]` |
|
||||
| `funstat_human` | 搜索用户 | `/human [关键词]` |
|
||||
| `funstat_user_info` | 查询用户详情 | `/user_info [username]` |
|
||||
| `funstat_user_messages` | 获取用户聊天记录(自动翻页) | `/user_info [username] ➜ Messages ➜ All` |
|
||||
|
||||
### 协议支持
|
||||
|
||||
|
||||
271
core/server.py
271
core/server.py
@@ -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
64
core/start_server.sh
Executable 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
|
||||
77
deploy.sh
Executable file
77
deploy.sh
Executable file
@@ -0,0 +1,77 @@
|
||||
#!/bin/bash
|
||||
# Funstat MCP 服务器部署脚本
|
||||
|
||||
set -e
|
||||
|
||||
SERVER_IP="172.16.74.159"
|
||||
SERVER_USER="atai"
|
||||
SERVER_PATH="/home/atai/funstat-mcp"
|
||||
LOCAL_PATH="/Users/hahaha/projects/funstat-mcp"
|
||||
|
||||
echo "🚀 开始部署 Funstat MCP 到服务器..."
|
||||
|
||||
# 1. 打包项目文件(排除不需要的文件)
|
||||
echo "📦 打包项目文件..."
|
||||
cd "$LOCAL_PATH"
|
||||
tar --exclude='.git' \
|
||||
--exclude='.venv' \
|
||||
--exclude='__pycache__' \
|
||||
--exclude='*.pyc' \
|
||||
--exclude='.DS_Store' \
|
||||
--exclude='customer_data' \
|
||||
-czf /tmp/funstat-mcp.tar.gz .
|
||||
|
||||
# 2. 上传到服务器
|
||||
echo "⬆️ 上传文件到服务器..."
|
||||
sshpass -p "wengewudi666808" scp /tmp/funstat-mcp.tar.gz ${SERVER_USER}@${SERVER_IP}:/tmp/
|
||||
|
||||
# 3. 在服务器上部署
|
||||
echo "🔧 在服务器上部署..."
|
||||
sshpass -p "wengewudi666808" ssh ${SERVER_USER}@${SERVER_IP} << 'ENDSSH'
|
||||
set -e
|
||||
|
||||
# 创建部署目录
|
||||
mkdir -p /home/atai/funstat-mcp
|
||||
cd /home/atai/funstat-mcp
|
||||
|
||||
# 停止旧服务
|
||||
echo "🛑 停止旧服务..."
|
||||
pkill -f "funstat.*server.py" 2>/dev/null || true
|
||||
sleep 2
|
||||
|
||||
# 解压新文件
|
||||
echo "📂 解压新文件..."
|
||||
tar -xzf /tmp/funstat-mcp.tar.gz -C /home/atai/funstat-mcp
|
||||
rm /tmp/funstat-mcp.tar.gz
|
||||
|
||||
# 创建虚拟环境(如果不存在)
|
||||
if [ ! -d ".venv" ]; then
|
||||
echo "🔨 创建虚拟环境..."
|
||||
python3 -m venv .venv
|
||||
fi
|
||||
|
||||
# 激活虚拟环境并安装依赖
|
||||
echo "📥 安装依赖..."
|
||||
source .venv/bin/activate
|
||||
pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
|
||||
# 创建 session 目录
|
||||
mkdir -p ~/telegram_sessions
|
||||
|
||||
# 检查 session 文件
|
||||
if [ ! -f ~/telegram_sessions/funstat_bot.session ]; then
|
||||
echo "⚠️ 警告: Session 文件不存在"
|
||||
echo "请确保已经上传 session 文件到 ~/telegram_sessions/funstat_bot.session"
|
||||
fi
|
||||
|
||||
echo "✅ 部署完成!"
|
||||
echo "启动服务: cd /home/atai/funstat-mcp/core && bash start_server.sh"
|
||||
ENDSSH
|
||||
|
||||
echo ""
|
||||
echo "✅ 部署完成!"
|
||||
echo "下一步:"
|
||||
echo "1. 确保 session 文件已上传到服务器: ~/telegram_sessions/funstat_bot.session"
|
||||
echo "2. SSH 到服务器: ssh atai@172.16.74.159"
|
||||
echo "3. 启动服务: cd /home/atai/funstat-mcp/core && bash start_server.sh"
|
||||
8
requirements.txt
Normal file
8
requirements.txt
Normal file
@@ -0,0 +1,8 @@
|
||||
mcp>=1.12.0
|
||||
telethon>=1.34.0
|
||||
starlette>=0.41.0
|
||||
uvicorn>=0.29.0
|
||||
httpx>=0.28.0
|
||||
pydantic>=2.0.0
|
||||
python-dotenv>=1.0.0
|
||||
PySocks>=1.7.1
|
||||
45
test_connection.sh
Executable file
45
test_connection.sh
Executable file
@@ -0,0 +1,45 @@
|
||||
#!/bin/bash
|
||||
# 测试 Funstat MCP 服务器连接
|
||||
|
||||
SERVER_HOST="${1:-172.16.74.159}"
|
||||
SERVER_PORT="${2:-8091}"
|
||||
|
||||
echo "测试 Funstat MCP 服务器连接..."
|
||||
echo "服务器: $SERVER_HOST:$SERVER_PORT"
|
||||
echo ""
|
||||
|
||||
# 测试 1: TCP 连接
|
||||
echo "1. 测试 TCP 连接..."
|
||||
if timeout 3 bash -c "cat < /dev/null > /dev/tcp/$SERVER_HOST/$SERVER_PORT" 2>/dev/null; then
|
||||
echo " ✅ TCP 连接成功"
|
||||
else
|
||||
echo " ❌ TCP 连接失败"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 测试 2: HTTP GET 请求
|
||||
echo ""
|
||||
echo "2. 测试 HTTP GET 请求..."
|
||||
HTTP_CODE=$(curl -s -o /dev/null -w '%{http_code}' --connect-timeout 5 http://$SERVER_HOST:$SERVER_PORT/sse)
|
||||
if [ "$HTTP_CODE" = "406" ] || [ "$HTTP_CODE" = "200" ]; then
|
||||
echo " ✅ HTTP 响应: $HTTP_CODE (服务器正常)"
|
||||
else
|
||||
echo " ❌ HTTP 响应: $HTTP_CODE"
|
||||
fi
|
||||
|
||||
# 测试 3: SSE 端点
|
||||
echo ""
|
||||
echo "3. 测试 SSE 端点(Accept: text/event-stream)..."
|
||||
RESPONSE=$(timeout 3 curl -s -H "Accept: text/event-stream" http://$SERVER_HOST:$SERVER_PORT/sse 2>&1 | head -1)
|
||||
if [ -n "$RESPONSE" ]; then
|
||||
echo " ✅ SSE 端点响应: $RESPONSE"
|
||||
else
|
||||
echo " ⚠️ SSE 端点正在等待连接(正常)"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "✅ 服务器 $SERVER_HOST:$SERVER_PORT 可以正常访问!"
|
||||
echo ""
|
||||
echo "访问地址:"
|
||||
echo " - SSE 端点: http://$SERVER_HOST:$SERVER_PORT/sse"
|
||||
echo " - 消息端点: http://$SERVER_HOST:$SERVER_PORT/messages"
|
||||
Reference in New Issue
Block a user