feat: add postgres storage and remote sync
This commit is contained in:
51
.env.example
51
.env.example
@@ -1,15 +1,42 @@
|
|||||||
# Telegram API 配置
|
# ────────────────────────────────────────────────────────────────
|
||||||
TELEGRAM_API_ID=your_api_id
|
# Telegram 配置
|
||||||
TELEGRAM_API_HASH=your_api_hash
|
# ────────────────────────────────────────────────────────────────
|
||||||
|
TELEGRAM_API_ID=24660516
|
||||||
|
TELEGRAM_API_HASH=eae564578880a59c9963916ff1bbbd3a
|
||||||
|
# 每位同事需要使用自己的 session 文件,路径可自定义
|
||||||
TELEGRAM_SESSION_PATH=~/telegram_sessions/funstat_bot
|
TELEGRAM_SESSION_PATH=~/telegram_sessions/funstat_bot
|
||||||
FUNSTAT_BOT_USERNAME=@openaiw_bot
|
# 机器人账号信息(token 稍后替换为最新值)
|
||||||
|
TELEGRAM_BOT_USERNAME=@ktqiangda_bot
|
||||||
|
TELEGRAM_BOT_TOKEN=7321478881:AAFVmSXsfAbXI2Sfx9Sg3UW5ufAKvPsbO4U
|
||||||
|
|
||||||
# 代理配置(如果服务器无法直接访问 Telegram,需要配置代理)
|
# ────────────────────────────────────────────────────────────────
|
||||||
FUNSTAT_PROXY_TYPE=socks5
|
# MCP 服务器配置
|
||||||
FUNSTAT_PROXY_HOST=127.0.0.1
|
# ────────────────────────────────────────────────────────────────
|
||||||
FUNSTAT_PROXY_PORT=1080
|
FUNSTAT_HOST=127.0.0.1
|
||||||
|
FUNSTAT_PORT=8094
|
||||||
|
FUNSTAT_ENABLE_PAGINATION=true
|
||||||
|
FUNSTAT_PAGINATION_MAX_PAGES=10
|
||||||
|
FUNSTAT_PAGINATION_DELAY=2.0
|
||||||
|
FUNSTAT_PAGINATION_TIMEOUT=8.0
|
||||||
|
FUNSTAT_PAGINATION_KEYWORDS=➡️,下一页,Next,更多,下页,›,>>
|
||||||
|
|
||||||
# 服务器配置
|
# ────────────────────────────────────────────────────────────────
|
||||||
# 监听地址: 0.0.0.0 表示所有网络接口,127.0.0.1 表示仅本地访问
|
# Postgres / Docker 容器配置
|
||||||
FUNSTAT_HOST=0.0.0.0
|
# ────────────────────────────────────────────────────────────────
|
||||||
FUNSTAT_PORT=8091
|
POSTGRES_DB=funstat
|
||||||
|
POSTGRES_USER=funstat
|
||||||
|
POSTGRES_PASSWORD=funstat_dev_password
|
||||||
|
POSTGRES_PORT=5433
|
||||||
|
# Python 侧读取数据库的 URL
|
||||||
|
DATABASE_URL=postgresql://funstat:funstat_dev_password@127.0.0.1:5433/funstat
|
||||||
|
|
||||||
|
# ────────────────────────────────────────────────────────────────
|
||||||
|
# 远端数据沉淀(SSH 上传)
|
||||||
|
# ────────────────────────────────────────────────────────────────
|
||||||
|
REMOTE_UPLOAD_ENABLED=true
|
||||||
|
REMOTE_SSH_HOST=172.16.74.159
|
||||||
|
REMOTE_SSH_USER=atai
|
||||||
|
REMOTE_SSH_PASSWORD=wengewudi666808
|
||||||
|
REMOTE_SSH_TARGET=/home/atai/funstat_data/inbox
|
||||||
|
REMOTE_UPLOAD_INTERVAL=120
|
||||||
|
REMOTE_UPLOAD_BATCH_SIZE=200
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -11,3 +11,4 @@ logs/
|
|||||||
*.log
|
*.log
|
||||||
*.sqlite
|
*.sqlite
|
||||||
*.session
|
*.session
|
||||||
|
local_data/postgres/
|
||||||
|
|||||||
@@ -1,177 +0,0 @@
|
|||||||
# 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
|
|
||||||
@@ -1,252 +0,0 @@
|
|||||||
# 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
|
|
||||||
**状态**: ✅ 成功运行
|
|
||||||
**下次维护**: 定期检查日志和代理状态
|
|
||||||
22
README.md
22
README.md
@@ -10,6 +10,25 @@ Funstat MCP 提供一个面向多客户端的统一接口,将 Telegram 上的
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
> ⚠️ **端口更新**:当前本地部署已改为监听 `http://127.0.0.1:8094`(原默认 8091)。请在 Codex、Cursor、Claude Code 等客户端更新为 `http://127.0.0.1:8094/sse`。
|
||||||
|
|
||||||
|
## ⚙️ 新版数据流概览
|
||||||
|
|
||||||
|
- **PostgreSQL 本地存储**:所有 MCP 查询结果会自动写入本地 Postgres(默认通过 `docker compose up -d postgres` 启动,端口 `5433`)。数据包含原始返回、解析后的实体,以及同步状态。
|
||||||
|
- **自动远端沉淀**:后台会周期性将去重后的最新数据打包为 JSON,并通过 `sshpass` 上传到 `172.16.74.159` 的 `~/funstat_data/inbox` 目录,方便集中备份(暂不直接对外提供查询)。
|
||||||
|
- **配置模块化**:`core/config.py` + `.env` 控制 Telegram API、Bot Token、数据库与同步参数。每位同事只需复制 `.env.example`,填写自己的 session / token 即可。
|
||||||
|
- **Bot 可热切换**:所有脚本与服务器都不再写死 token,替换 `.env` 中的 `TELEGRAM_BOT_TOKEN` 即可切换镜像机器人。
|
||||||
|
- **默认镜像机器人**:发行包已预置最新官方镜像 `@ktqiangda_bot`(Token:`7321478881:AAFVmSXsfAbXI2Sfx9Sg3UW5ufAKvPsbO4U`)。如后续提供新的镜像,只需改 `.env` 即可。
|
||||||
|
|
||||||
|
### 快速启动顺序
|
||||||
|
1. `cp .env.example .env` 并根据实际环境填写(尤其是 Telegram API、Bot Token、session 路径)。
|
||||||
|
2. 启动本地 Postgres:`docker compose up -d postgres`(如本机已有 5433 占用,可在 `.env` 里改为其他端口)。
|
||||||
|
3. 安装依赖:`pip3 install -r requirements.txt --user --break-system-packages`。
|
||||||
|
4. 使用自己的 Telegram 账号创建 session:`python3 scripts/create_session_safe.py`。
|
||||||
|
5. 运行服务器:`cd core && python3 server.py` 或 `./start_sse_prod.sh`。
|
||||||
|
|
||||||
|
只要 `.env` 中的远端 SSH 信息保持有效,所有数据会自动异步推送到服务器侧的归档目录,无需额外手动操作。
|
||||||
|
|
||||||
## 📦 包内容概览
|
## 📦 包内容概览
|
||||||
|
|
||||||
这个打包包含了完整的 Funstat MCP 服务器实现,以及所有相关文档、配置和工具。
|
这个打包包含了完整的 Funstat MCP 服务器实现,以及所有相关文档、配置和工具。
|
||||||
@@ -49,7 +68,7 @@ funstat_mcp_package/
|
|||||||
|
|
||||||
## 🎯 核心功能
|
## 🎯 核心功能
|
||||||
|
|
||||||
### MCP 工具列表 (9个)
|
### MCP 工具列表 (8个)
|
||||||
|
|
||||||
| 工具名 | 功能 | 对应命令 |
|
| 工具名 | 功能 | 对应命令 |
|
||||||
|--------|------|---------|
|
|--------|------|---------|
|
||||||
@@ -61,7 +80,6 @@ funstat_mcp_package/
|
|||||||
| `funstat_text` | 搜索消息内容 | `/text [关键词]` |
|
| `funstat_text` | 搜索消息内容 | `/text [关键词]` |
|
||||||
| `funstat_human` | 搜索用户 | `/human [关键词]` |
|
| `funstat_human` | 搜索用户 | `/human [关键词]` |
|
||||||
| `funstat_user_info` | 查询用户详情 | `/user_info [username]` |
|
| `funstat_user_info` | 查询用户详情 | `/user_info [username]` |
|
||||||
| `funstat_user_messages` | 获取用户聊天记录(自动翻页) | `/user_info [username] ➜ Messages ➜ All` |
|
|
||||||
|
|
||||||
### 协议支持
|
### 协议支持
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"funstat": {
|
"funstat": {
|
||||||
"command": "/Users/lucas/牛马/agentapi",
|
"command": "/Users/lucas/牛马/agentapi",
|
||||||
"args": ["proxy", "http://127.0.0.1:8091/sse"],
|
"args": ["proxy", "http://127.0.0.1:8094/sse"],
|
||||||
"env": {}
|
"env": {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"funstat": {
|
"funstat": {
|
||||||
"command": "/Users/lucas/牛马/agentapi",
|
"command": "/Users/lucas/牛马/agentapi",
|
||||||
"args": ["proxy", "http://127.0.0.1:8091/sse"],
|
"args": ["proxy", "http://127.0.0.1:8094/sse"],
|
||||||
"env": {}
|
"env": {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
103
core/config.py
Normal file
103
core/config.py
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import os
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from functools import lru_cache
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
|
||||||
|
BASE_DIR = Path(__file__).resolve().parents[1]
|
||||||
|
DOTENV_PATH = BASE_DIR / ".env"
|
||||||
|
|
||||||
|
if DOTENV_PATH.exists():
|
||||||
|
load_dotenv(DOTENV_PATH)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Settings:
|
||||||
|
telegram_api_id: int
|
||||||
|
telegram_api_hash: str
|
||||||
|
telegram_session_path: str
|
||||||
|
telegram_bot_username: str
|
||||||
|
telegram_bot_token: Optional[str]
|
||||||
|
|
||||||
|
host: str
|
||||||
|
port: int
|
||||||
|
|
||||||
|
enable_pagination: bool
|
||||||
|
pagination_max_pages: int
|
||||||
|
pagination_delay: float
|
||||||
|
pagination_timeout: float
|
||||||
|
pagination_keywords: list[str]
|
||||||
|
|
||||||
|
database_url: str
|
||||||
|
|
||||||
|
remote_upload_enabled: bool
|
||||||
|
remote_ssh_host: str
|
||||||
|
remote_ssh_user: str
|
||||||
|
remote_ssh_password: str
|
||||||
|
remote_ssh_target: str
|
||||||
|
remote_upload_interval: int
|
||||||
|
remote_upload_batch_size: int
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
if not self.database_url:
|
||||||
|
raise ValueError("DATABASE_URL 未配置,无法初始化数据库连接")
|
||||||
|
|
||||||
|
|
||||||
|
def _env_bool(name: str, default: bool = False) -> bool:
|
||||||
|
return os.getenv(name, str(default)).strip().lower() in ("1", "true", "yes", "on")
|
||||||
|
|
||||||
|
|
||||||
|
def _env_int(name: str, default: int) -> int:
|
||||||
|
try:
|
||||||
|
return int(os.getenv(name, str(default)))
|
||||||
|
except ValueError:
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
def _env_float(name: str, default: float) -> float:
|
||||||
|
try:
|
||||||
|
return float(os.getenv(name, str(default)))
|
||||||
|
except ValueError:
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache()
|
||||||
|
def get_settings() -> Settings:
|
||||||
|
keywords = os.getenv(
|
||||||
|
"FUNSTAT_PAGINATION_KEYWORDS",
|
||||||
|
"➡️,下一页,Next,更多,下页,›,>>"
|
||||||
|
)
|
||||||
|
|
||||||
|
return Settings(
|
||||||
|
telegram_api_id=_env_int("TELEGRAM_API_ID", 24660516),
|
||||||
|
telegram_api_hash=os.getenv("TELEGRAM_API_HASH", "eae564578880a59c9963916ff1bbbd3a"),
|
||||||
|
telegram_session_path=os.getenv(
|
||||||
|
"TELEGRAM_SESSION_PATH",
|
||||||
|
str(Path.home() / "telegram_sessions" / "funstat_bot")
|
||||||
|
),
|
||||||
|
telegram_bot_username=os.getenv("TELEGRAM_BOT_USERNAME", "@ktqiangda_bot"),
|
||||||
|
telegram_bot_token=os.getenv("TELEGRAM_BOT_TOKEN"),
|
||||||
|
host=os.getenv("FUNSTAT_HOST", "127.0.0.1"),
|
||||||
|
port=_env_int("FUNSTAT_PORT", 8094),
|
||||||
|
enable_pagination=_env_bool("FUNSTAT_ENABLE_PAGINATION", True),
|
||||||
|
pagination_max_pages=_env_int("FUNSTAT_PAGINATION_MAX_PAGES", 10),
|
||||||
|
pagination_delay=_env_float("FUNSTAT_PAGINATION_DELAY", 2.0),
|
||||||
|
pagination_timeout=_env_float("FUNSTAT_PAGINATION_TIMEOUT", 8.0),
|
||||||
|
pagination_keywords=[
|
||||||
|
kw.strip() for kw in keywords.split(",") if kw.strip()
|
||||||
|
],
|
||||||
|
database_url=os.getenv(
|
||||||
|
"DATABASE_URL",
|
||||||
|
"postgresql://funstat:funstat_dev_password@127.0.0.1:5433/funstat"
|
||||||
|
),
|
||||||
|
remote_upload_enabled=_env_bool("REMOTE_UPLOAD_ENABLED", True),
|
||||||
|
remote_ssh_host=os.getenv("REMOTE_SSH_HOST", ""),
|
||||||
|
remote_ssh_user=os.getenv("REMOTE_SSH_USER", ""),
|
||||||
|
remote_ssh_password=os.getenv("REMOTE_SSH_PASSWORD", ""),
|
||||||
|
remote_ssh_target=os.getenv("REMOTE_SSH_TARGET", "/home/atai/funstat_data/inbox"),
|
||||||
|
remote_upload_interval=_env_int("REMOTE_UPLOAD_INTERVAL", 120),
|
||||||
|
remote_upload_batch_size=_env_int("REMOTE_UPLOAD_BATCH_SIZE", 200),
|
||||||
|
)
|
||||||
18
core/models.py
Normal file
18
core/models.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
|
|
||||||
|
Entity = Dict[str, Any]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PageRecord:
|
||||||
|
page_number: int
|
||||||
|
text: str
|
||||||
|
entities: List[Entity] = field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class BotResponse:
|
||||||
|
text: str
|
||||||
|
pages: Optional[List[PageRecord]] = None
|
||||||
50
core/parsers.py
Normal file
50
core/parsers.py
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import re
|
||||||
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
|
|
||||||
|
USERNAME_PATTERN = re.compile(r"@([A-Za-z0-9_]{3,})")
|
||||||
|
TME_PATTERN = re.compile(r"(?:https?://)?t\.me/([A-Za-z0-9_]{3,})")
|
||||||
|
ID_PATTERN = re.compile(r"`(\d{4,})`")
|
||||||
|
|
||||||
|
|
||||||
|
def extract_entities(text: str) -> List[Dict[str, Any]]:
|
||||||
|
if not text:
|
||||||
|
return []
|
||||||
|
|
||||||
|
entities: List[Dict[str, Any]] = []
|
||||||
|
seen = set()
|
||||||
|
|
||||||
|
for user_id in set(ID_PATTERN.findall(text)):
|
||||||
|
key = ("user_id", user_id)
|
||||||
|
if key not in seen:
|
||||||
|
seen.add(key)
|
||||||
|
entities.append({
|
||||||
|
"type": "user_id",
|
||||||
|
"value": user_id
|
||||||
|
})
|
||||||
|
|
||||||
|
for username in set(USERNAME_PATTERN.findall(text)):
|
||||||
|
normalized = username.strip()
|
||||||
|
if not normalized:
|
||||||
|
continue
|
||||||
|
key = ("username", normalized.lower())
|
||||||
|
if key not in seen:
|
||||||
|
seen.add(key)
|
||||||
|
entities.append({
|
||||||
|
"type": "username",
|
||||||
|
"value": normalized.lower(),
|
||||||
|
"display": normalized
|
||||||
|
})
|
||||||
|
|
||||||
|
for link in set(TME_PATTERN.findall(text)):
|
||||||
|
normalized = f"t.me/{link}"
|
||||||
|
key = ("tme_link", normalized.lower())
|
||||||
|
if key not in seen:
|
||||||
|
seen.add(key)
|
||||||
|
entities.append({
|
||||||
|
"type": "tme_link",
|
||||||
|
"value": normalized.lower(),
|
||||||
|
"display": normalized
|
||||||
|
})
|
||||||
|
|
||||||
|
return entities
|
||||||
782
core/server.py
782
core/server.py
File diff suppressed because it is too large
Load Diff
@@ -1,64 +0,0 @@
|
|||||||
#!/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
|
|
||||||
@@ -5,7 +5,7 @@ echo "🚀 启动 Funstat MCP SSE 服务器..."
|
|||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# 设置环境变量
|
# 设置环境变量
|
||||||
export FUNSTAT_PORT=8091
|
export FUNSTAT_PORT=8094
|
||||||
export FUNSTAT_HOST=127.0.0.1
|
export FUNSTAT_HOST=127.0.0.1
|
||||||
|
|
||||||
# 检查依赖
|
# 检查依赖
|
||||||
|
|||||||
@@ -28,19 +28,19 @@ sleep 3
|
|||||||
# 验证启动
|
# 验证启动
|
||||||
if ps -p $SERVER_PID > /dev/null; then
|
if ps -p $SERVER_PID > /dev/null; then
|
||||||
echo "✅ 服务器已启动 (PID: $SERVER_PID)"
|
echo "✅ 服务器已启动 (PID: $SERVER_PID)"
|
||||||
echo "📡 SSE 端点: http://127.0.0.1:8091/sse"
|
echo "📡 SSE 端点: http://127.0.0.1:8094/sse"
|
||||||
echo "📋 日志文件: /tmp/funstat_sse.log"
|
echo "📋 日志文件: /tmp/funstat_sse.log"
|
||||||
|
|
||||||
# 测试端点
|
# 测试端点
|
||||||
echo ""
|
echo ""
|
||||||
echo "🧪 测试端点..."
|
echo "🧪 测试端点..."
|
||||||
if curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:8091/sse | grep -q "200"; then
|
if curl -s -o /dev/null -w "%{http_code}" -H 'Accept: text/event-stream' http://127.0.0.1:8094/sse | grep -Eq "200|204|206"; then
|
||||||
echo "✅ GET /sse 测试通过"
|
echo "✅ GET /sse 测试通过"
|
||||||
else
|
else
|
||||||
echo "❌ GET /sse 测试失败"
|
echo "❌ GET /sse 测试失败"
|
||||||
fi
|
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
|
if curl -s -o /dev/null -w "%{http_code}" -X POST http://127.0.0.1:8094/sse -H 'Content-Type: application/json' -H 'Accept: application/json' -d '{}' | grep -q "200"; then
|
||||||
echo "✅ POST /sse 测试通过"
|
echo "✅ POST /sse 测试通过"
|
||||||
else
|
else
|
||||||
echo "❌ POST /sse 测试失败"
|
echo "❌ POST /sse 测试失败"
|
||||||
@@ -49,7 +49,7 @@ if ps -p $SERVER_PID > /dev/null; then
|
|||||||
echo ""
|
echo ""
|
||||||
echo "📊 服务器状态:"
|
echo "📊 服务器状态:"
|
||||||
echo " 进程ID: $SERVER_PID"
|
echo " 进程ID: $SERVER_PID"
|
||||||
echo " 监听地址: http://127.0.0.1:8091"
|
echo " 监听地址: http://127.0.0.1:8094"
|
||||||
echo " 日志: tail -f /tmp/funstat_sse.log"
|
echo " 日志: tail -f /tmp/funstat_sse.log"
|
||||||
else
|
else
|
||||||
echo "❌ 服务器启动失败!"
|
echo "❌ 服务器启动失败!"
|
||||||
|
|||||||
228
core/storage.py
Normal file
228
core/storage.py
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import hashlib
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
|
import asyncpg
|
||||||
|
|
||||||
|
from models import PageRecord
|
||||||
|
|
||||||
|
|
||||||
|
class StorageManager:
|
||||||
|
def __init__(self, database_url: str):
|
||||||
|
self.database_url = database_url
|
||||||
|
self.pool: Optional[asyncpg.pool.Pool] = None
|
||||||
|
self._lock = asyncio.Lock()
|
||||||
|
|
||||||
|
async def initialize(self):
|
||||||
|
if self.pool:
|
||||||
|
return
|
||||||
|
self.pool = await asyncpg.create_pool(
|
||||||
|
self.database_url,
|
||||||
|
min_size=1,
|
||||||
|
max_size=5,
|
||||||
|
timeout=10
|
||||||
|
)
|
||||||
|
await self._ensure_schema()
|
||||||
|
|
||||||
|
async def close(self):
|
||||||
|
if self.pool:
|
||||||
|
await self.pool.close()
|
||||||
|
self.pool = None
|
||||||
|
|
||||||
|
async def _ensure_schema(self):
|
||||||
|
assert self.pool is not None
|
||||||
|
async with self.pool.acquire() as conn:
|
||||||
|
await conn.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS mcp_results (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
command TEXT NOT NULL,
|
||||||
|
bot_command TEXT NOT NULL,
|
||||||
|
arguments JSONB NOT NULL,
|
||||||
|
arguments_hash TEXT NOT NULL,
|
||||||
|
page_number INTEGER NOT NULL,
|
||||||
|
raw_text TEXT NOT NULL,
|
||||||
|
raw_text_hash TEXT NOT NULL,
|
||||||
|
entities JSONB,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
source_account TEXT,
|
||||||
|
synced BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
sync_batch_id TEXT,
|
||||||
|
last_sync_at TIMESTAMPTZ,
|
||||||
|
sync_attempts INTEGER NOT NULL DEFAULT 0,
|
||||||
|
UNIQUE (command, bot_command, page_number, arguments_hash, raw_text_hash)
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
await conn.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS mcp_entities (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
result_id BIGINT NOT NULL REFERENCES mcp_results (id) ON DELETE CASCADE,
|
||||||
|
entity_type TEXT NOT NULL,
|
||||||
|
entity_value TEXT NOT NULL,
|
||||||
|
metadata JSONB,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
UNIQUE (result_id, entity_type, entity_value)
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
async def save_response(
|
||||||
|
self,
|
||||||
|
command: str,
|
||||||
|
bot_command: str,
|
||||||
|
arguments: Dict[str, Any],
|
||||||
|
pages: List[PageRecord],
|
||||||
|
source_account: Optional[str] = None
|
||||||
|
) -> List[int]:
|
||||||
|
if not pages or not self.pool:
|
||||||
|
return []
|
||||||
|
|
||||||
|
arguments_json = self._normalize_arguments(arguments)
|
||||||
|
arguments_hash = self._hash_value(arguments_json)
|
||||||
|
inserted_ids: List[int] = []
|
||||||
|
|
||||||
|
async with self.pool.acquire() as conn:
|
||||||
|
async with conn.transaction():
|
||||||
|
for page in pages:
|
||||||
|
raw_text = page.text or ""
|
||||||
|
raw_hash = self._hash_value(raw_text)
|
||||||
|
|
||||||
|
record = await conn.fetchrow(
|
||||||
|
"""
|
||||||
|
INSERT INTO mcp_results (
|
||||||
|
command, bot_command, arguments, arguments_hash,
|
||||||
|
page_number, raw_text, raw_text_hash, entities,
|
||||||
|
source_account
|
||||||
|
) VALUES ($1,$2,$3::jsonb,$4,$5,$6,$7,$8::jsonb,$9)
|
||||||
|
ON CONFLICT (command, bot_command, page_number, arguments_hash, raw_text_hash)
|
||||||
|
DO NOTHING
|
||||||
|
RETURNING id;
|
||||||
|
""",
|
||||||
|
command,
|
||||||
|
bot_command,
|
||||||
|
arguments_json,
|
||||||
|
arguments_hash,
|
||||||
|
page.page_number,
|
||||||
|
raw_text,
|
||||||
|
raw_hash,
|
||||||
|
json.dumps(page.entities or [], ensure_ascii=False),
|
||||||
|
source_account,
|
||||||
|
)
|
||||||
|
|
||||||
|
if record:
|
||||||
|
result_id = record["id"]
|
||||||
|
inserted_ids.append(result_id)
|
||||||
|
if page.entities:
|
||||||
|
await self._upsert_entities(conn, result_id, page.entities)
|
||||||
|
|
||||||
|
return inserted_ids
|
||||||
|
|
||||||
|
async def fetch_unsynced_results(self, limit: int = 200) -> List[Dict[str, Any]]:
|
||||||
|
if not self.pool:
|
||||||
|
return []
|
||||||
|
|
||||||
|
async with self.pool.acquire() as conn:
|
||||||
|
rows = await conn.fetch(
|
||||||
|
"""
|
||||||
|
SELECT
|
||||||
|
r.id,
|
||||||
|
r.command,
|
||||||
|
r.bot_command,
|
||||||
|
r.arguments,
|
||||||
|
r.page_number,
|
||||||
|
r.raw_text,
|
||||||
|
r.entities,
|
||||||
|
r.created_at,
|
||||||
|
r.source_account,
|
||||||
|
COALESCE(
|
||||||
|
json_agg(
|
||||||
|
json_build_object(
|
||||||
|
'entity_type', e.entity_type,
|
||||||
|
'entity_value', e.entity_value,
|
||||||
|
'metadata', e.metadata
|
||||||
|
)
|
||||||
|
) FILTER (WHERE e.id IS NOT NULL),
|
||||||
|
'[]'::json
|
||||||
|
) AS entity_list
|
||||||
|
FROM mcp_results r
|
||||||
|
LEFT JOIN mcp_entities e ON e.result_id = r.id
|
||||||
|
WHERE r.synced = FALSE
|
||||||
|
GROUP BY r.id
|
||||||
|
ORDER BY r.created_at ASC
|
||||||
|
LIMIT $1;
|
||||||
|
""",
|
||||||
|
limit,
|
||||||
|
)
|
||||||
|
|
||||||
|
payload = []
|
||||||
|
for row in rows:
|
||||||
|
payload.append({
|
||||||
|
"id": row["id"],
|
||||||
|
"command": row["command"],
|
||||||
|
"bot_command": row["bot_command"],
|
||||||
|
"arguments": row["arguments"],
|
||||||
|
"page_number": row["page_number"],
|
||||||
|
"raw_text": row["raw_text"],
|
||||||
|
"entities": row["entity_list"],
|
||||||
|
"created_at": row["created_at"].isoformat() if row["created_at"] else None,
|
||||||
|
"source_account": row["source_account"],
|
||||||
|
})
|
||||||
|
return payload
|
||||||
|
|
||||||
|
async def mark_synced(self, ids: List[int], batch_id: str):
|
||||||
|
if not ids or not self.pool:
|
||||||
|
return
|
||||||
|
|
||||||
|
async with self.pool.acquire() as conn:
|
||||||
|
await conn.execute(
|
||||||
|
"""
|
||||||
|
UPDATE mcp_results
|
||||||
|
SET synced = TRUE,
|
||||||
|
sync_batch_id = $1,
|
||||||
|
last_sync_at = NOW(),
|
||||||
|
sync_attempts = 0
|
||||||
|
WHERE id = ANY($2::bigint[]);
|
||||||
|
""",
|
||||||
|
batch_id,
|
||||||
|
ids,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def mark_failed_sync(self, ids: List[int]):
|
||||||
|
if not ids or not self.pool:
|
||||||
|
return
|
||||||
|
|
||||||
|
async with self.pool.acquire() as conn:
|
||||||
|
await conn.execute(
|
||||||
|
"""
|
||||||
|
UPDATE mcp_results
|
||||||
|
SET sync_attempts = sync_attempts + 1
|
||||||
|
WHERE id = ANY($1::bigint[]);
|
||||||
|
""",
|
||||||
|
ids,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _upsert_entities(self, conn: asyncpg.Connection, result_id: int, entities: List[Dict[str, Any]]):
|
||||||
|
for entity in entities:
|
||||||
|
await conn.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO mcp_entities (result_id, entity_type, entity_value, metadata)
|
||||||
|
VALUES ($1, $2, $3, $4::jsonb)
|
||||||
|
ON CONFLICT (result_id, entity_type, entity_value) DO NOTHING;
|
||||||
|
""",
|
||||||
|
result_id,
|
||||||
|
entity.get("type", "unknown"),
|
||||||
|
str(entity.get("value", "")),
|
||||||
|
json.dumps(entity.get("metadata", {}), ensure_ascii=False),
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _normalize_arguments(arguments: Dict[str, Any]) -> str:
|
||||||
|
return json.dumps(arguments or {}, ensure_ascii=False, sort_keys=True, default=str)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _hash_value(value: str) -> str:
|
||||||
|
return hashlib.sha256(value.encode("utf-8")).hexdigest()
|
||||||
@@ -16,7 +16,7 @@ fi
|
|||||||
# 2. 测试 SSE 端点
|
# 2. 测试 SSE 端点
|
||||||
echo ""
|
echo ""
|
||||||
echo "2. 测试 SSE 端点 (GET /sse)..."
|
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)
|
response=$(timeout 2 curl -s -N -H "Accept: text/event-stream" http://127.0.0.1:8094/sse 2>&1 | head -3)
|
||||||
if echo "$response" | grep -q "event: endpoint"; then
|
if echo "$response" | grep -q "event: endpoint"; then
|
||||||
echo "✓ SSE 端点响应正常"
|
echo "✓ SSE 端点响应正常"
|
||||||
echo "$response" | grep "data:" | head -1
|
echo "$response" | grep "data:" | head -1
|
||||||
|
|||||||
128
core/uploader.py
Normal file
128
core/uploader.py
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import tempfile
|
||||||
|
import uuid
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from .config import Settings
|
||||||
|
from .storage import StorageManager
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger("funstat_uploader")
|
||||||
|
|
||||||
|
|
||||||
|
class RemoteUploader:
|
||||||
|
def __init__(self, storage: StorageManager, settings: Settings):
|
||||||
|
self.storage = storage
|
||||||
|
self.settings = settings
|
||||||
|
self.enabled = (
|
||||||
|
settings.remote_upload_enabled
|
||||||
|
and settings.remote_ssh_host
|
||||||
|
and settings.remote_ssh_user
|
||||||
|
and settings.remote_ssh_password
|
||||||
|
)
|
||||||
|
self.interval = max(settings.remote_upload_interval, 30)
|
||||||
|
self.batch_size = max(settings.remote_upload_batch_size, 10)
|
||||||
|
self._task: asyncio.Task | None = None
|
||||||
|
self._ensure_task: asyncio.Task | None = None
|
||||||
|
|
||||||
|
async def start(self):
|
||||||
|
if not self.enabled or self._task:
|
||||||
|
return
|
||||||
|
|
||||||
|
await self._ensure_remote_dir()
|
||||||
|
self._task = asyncio.create_task(self._run_loop())
|
||||||
|
|
||||||
|
async def _run_loop(self):
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
await self._process_batch()
|
||||||
|
except Exception as exc:
|
||||||
|
logger.error("远程上传任务异常: %s", exc, exc_info=exc)
|
||||||
|
await asyncio.sleep(self.interval)
|
||||||
|
|
||||||
|
async def _process_batch(self):
|
||||||
|
results = await self.storage.fetch_unsynced_results(self.batch_size)
|
||||||
|
if not results:
|
||||||
|
return
|
||||||
|
|
||||||
|
batch_id = uuid.uuid4().hex
|
||||||
|
tmp_path = await self._write_payload(batch_id, results)
|
||||||
|
try:
|
||||||
|
uploaded = await self._upload_file(tmp_path, batch_id)
|
||||||
|
ids = [item["id"] for item in results]
|
||||||
|
if uploaded:
|
||||||
|
await self.storage.mark_synced(ids, batch_id)
|
||||||
|
logger.info("成功同步 %s 条记录到远程服务器 (batch=%s)", len(ids), batch_id)
|
||||||
|
else:
|
||||||
|
await self.storage.mark_failed_sync(ids)
|
||||||
|
logger.warning("同步失败,已记录失败次数 (batch=%s)", batch_id)
|
||||||
|
finally:
|
||||||
|
if tmp_path.exists():
|
||||||
|
tmp_path.unlink(missing_ok=True)
|
||||||
|
|
||||||
|
async def _write_payload(self, batch_id: str, results: List[dict]) -> Path:
|
||||||
|
tmp_dir = Path(tempfile.gettempdir())
|
||||||
|
file_path = tmp_dir / f"funstat_batch_{batch_id}.json"
|
||||||
|
payload = {
|
||||||
|
"batch_id": batch_id,
|
||||||
|
"total": len(results),
|
||||||
|
"generated_at": datetime.now(timezone.utc).isoformat(),
|
||||||
|
"results": results,
|
||||||
|
}
|
||||||
|
|
||||||
|
file_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2))
|
||||||
|
return file_path
|
||||||
|
|
||||||
|
async def _ensure_remote_dir(self):
|
||||||
|
if not self.enabled:
|
||||||
|
return
|
||||||
|
|
||||||
|
target_dir = self.settings.remote_ssh_target
|
||||||
|
command = (
|
||||||
|
f"sshpass -p '{self.settings.remote_ssh_password}' "
|
||||||
|
f"ssh -o StrictHostKeyChecking=no "
|
||||||
|
f"{self.settings.remote_ssh_user}@{self.settings.remote_ssh_host} "
|
||||||
|
f"'mkdir -p {target_dir}'"
|
||||||
|
)
|
||||||
|
process = await asyncio.create_subprocess_shell(
|
||||||
|
command,
|
||||||
|
stdout=asyncio.subprocess.PIPE,
|
||||||
|
stderr=asyncio.subprocess.PIPE,
|
||||||
|
)
|
||||||
|
stdout, stderr = await process.communicate()
|
||||||
|
if process.returncode != 0:
|
||||||
|
logger.warning(
|
||||||
|
"创建远程目录失败: %s",
|
||||||
|
stderr.decode().strip() or stdout.decode().strip()
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _upload_file(self, file_path: Path, batch_id: str) -> bool:
|
||||||
|
if not self.enabled:
|
||||||
|
return False
|
||||||
|
|
||||||
|
remote_name = f"{batch_id}.json"
|
||||||
|
command = (
|
||||||
|
f"sshpass -p '{self.settings.remote_ssh_password}' "
|
||||||
|
f"scp -o StrictHostKeyChecking=no {file_path} "
|
||||||
|
f"{self.settings.remote_ssh_user}@{self.settings.remote_ssh_host}:"
|
||||||
|
f"{self.settings.remote_ssh_target}/{remote_name}"
|
||||||
|
)
|
||||||
|
process = await asyncio.create_subprocess_shell(
|
||||||
|
command,
|
||||||
|
stdout=asyncio.subprocess.PIPE,
|
||||||
|
stderr=asyncio.subprocess.PIPE,
|
||||||
|
)
|
||||||
|
stdout, stderr = await process.communicate()
|
||||||
|
if process.returncode != 0:
|
||||||
|
logger.error(
|
||||||
|
"上传批次 %s 失败: %s",
|
||||||
|
batch_id,
|
||||||
|
stderr.decode().strip() or stdout.decode().strip()
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
77
deploy.sh
77
deploy.sh
@@ -1,77 +0,0 @@
|
|||||||
#!/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"
|
|
||||||
18
docker-compose.yml
Normal file
18
docker-compose.yml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
container_name: funstat-postgres
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: ${POSTGRES_DB:-funstat}
|
||||||
|
POSTGRES_USER: ${POSTGRES_USER:-funstat}
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-funstat_dev_password}
|
||||||
|
ports:
|
||||||
|
- "${POSTGRES_PORT:-5433}:5432"
|
||||||
|
volumes:
|
||||||
|
- ./local_data/postgres:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "pg_isready", "-U", "${POSTGRES_USER:-funstat}"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
**命令**:
|
**命令**:
|
||||||
```bash
|
```bash
|
||||||
codex mcp add --url http://127.0.0.1:8091/sse funstat
|
codex mcp add --url http://127.0.0.1:8094/sse funstat
|
||||||
```
|
```
|
||||||
|
|
||||||
**输出**:
|
**输出**:
|
||||||
@@ -31,7 +31,7 @@ Added global MCP server 'funstat'.
|
|||||||
**配置详情**:
|
**配置详情**:
|
||||||
```
|
```
|
||||||
Name: funstat
|
Name: funstat
|
||||||
URL: http://127.0.0.1:8091/sse
|
URL: http://127.0.0.1:8094/sse
|
||||||
Transport: streamable_http
|
Transport: streamable_http
|
||||||
Status: enabled
|
Status: enabled
|
||||||
Auth: Unsupported
|
Auth: Unsupported
|
||||||
@@ -44,7 +44,7 @@ Auth: Unsupported
|
|||||||
**新增内容**:
|
**新增内容**:
|
||||||
```toml
|
```toml
|
||||||
[mcp_servers.funstat]
|
[mcp_servers.funstat]
|
||||||
url = "http://127.0.0.1:8091/sse"
|
url = "http://127.0.0.1:8094/sse"
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -80,7 +80,7 @@ codex mcp list
|
|||||||
**输出**:
|
**输出**:
|
||||||
```
|
```
|
||||||
Name Url Bearer Token Env Var Status Auth
|
Name Url Bearer Token Env Var Status Auth
|
||||||
funstat http://127.0.0.1:8091/sse - enabled Unsupported
|
funstat http://127.0.0.1:8094/sse - enabled Unsupported
|
||||||
```
|
```
|
||||||
|
|
||||||
### 查看服务器详情
|
### 查看服务器详情
|
||||||
@@ -94,7 +94,7 @@ codex mcp get funstat
|
|||||||
funstat
|
funstat
|
||||||
enabled: true
|
enabled: true
|
||||||
transport: streamable_http
|
transport: streamable_http
|
||||||
url: http://127.0.0.1:8091/sse
|
url: http://127.0.0.1:8094/sse
|
||||||
bearer_token_env_var: -
|
bearer_token_env_var: -
|
||||||
http_headers: -
|
http_headers: -
|
||||||
env_http_headers: -
|
env_http_headers: -
|
||||||
@@ -110,7 +110,7 @@ codex mcp remove funstat
|
|||||||
### 重新添加
|
### 重新添加
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
codex mcp add --url http://127.0.0.1:8091/sse funstat
|
codex mcp add --url http://127.0.0.1:8094/sse funstat
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -200,12 +200,12 @@ codex -i screenshot.png "这个截图中的Telegram用户名是什么?帮我搜
|
|||||||
└─────────────┬──────────────┘
|
└─────────────┬──────────────┘
|
||||||
│
|
│
|
||||||
│ SSE 连接
|
│ SSE 连接
|
||||||
│ http://127.0.0.1:8091/sse
|
│ http://127.0.0.1:8094/sse
|
||||||
│
|
│
|
||||||
┌─────────────▼──────────────────┐
|
┌─────────────▼──────────────────┐
|
||||||
│ Funstat MCP Server (SSE) │
|
│ Funstat MCP Server (SSE) │
|
||||||
│ funstat_mcp/server.py │
|
│ funstat_mcp/server.py │
|
||||||
│ 端口: 8091 │
|
│ 端口: 8094 │
|
||||||
└─────────────┬──────────────────┘
|
└─────────────┬──────────────────┘
|
||||||
│
|
│
|
||||||
│ Telethon
|
│ Telethon
|
||||||
@@ -232,7 +232,7 @@ export FUNSTAT_TOKEN="your-token-here"
|
|||||||
|
|
||||||
# 添加 MCP 服务器时指定
|
# 添加 MCP 服务器时指定
|
||||||
codex mcp add \
|
codex mcp add \
|
||||||
--url http://127.0.0.1:8091/sse \
|
--url http://127.0.0.1:8094/sse \
|
||||||
--bearer-token-env-var FUNSTAT_TOKEN \
|
--bearer-token-env-var FUNSTAT_TOKEN \
|
||||||
funstat
|
funstat
|
||||||
```
|
```
|
||||||
@@ -243,7 +243,7 @@ codex mcp add \
|
|||||||
|
|
||||||
```toml
|
```toml
|
||||||
[mcp_servers.funstat]
|
[mcp_servers.funstat]
|
||||||
url = "http://127.0.0.1:8091/sse"
|
url = "http://127.0.0.1:8094/sse"
|
||||||
# bearer_token_env_var = "FUNSTAT_TOKEN" # 可选
|
# bearer_token_env_var = "FUNSTAT_TOKEN" # 可选
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -264,7 +264,7 @@ url = "http://127.0.0.1:8091/sse"
|
|||||||
ps aux | grep server.py
|
ps aux | grep server.py
|
||||||
|
|
||||||
# 2. 测试 SSE 端点
|
# 2. 测试 SSE 端点
|
||||||
curl -i http://127.0.0.1:8091/sse
|
curl -i http://127.0.0.1:8094/sse
|
||||||
|
|
||||||
# 3. 重启 SSE 服务器
|
# 3. 重启 SSE 服务器
|
||||||
cd /Users/lucas/chat--1003255561049/funstat_mcp
|
cd /Users/lucas/chat--1003255561049/funstat_mcp
|
||||||
|
|||||||
1
local_data/.gitkeep
Normal file
1
local_data/.gitkeep
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
mcp>=1.12.0
|
mcp>=1.20.0
|
||||||
telethon>=1.34.0
|
telethon>=1.41.0
|
||||||
starlette>=0.41.0
|
starlette>=0.50.0
|
||||||
uvicorn>=0.29.0
|
uvicorn>=0.38.0
|
||||||
httpx>=0.28.0
|
asyncpg>=0.29.0
|
||||||
pydantic>=2.0.0
|
python-dotenv>=1.0.1
|
||||||
python-dotenv>=1.0.0
|
|
||||||
PySocks>=1.7.1
|
|
||||||
|
|||||||
@@ -2,10 +2,17 @@
|
|||||||
"""
|
"""
|
||||||
查看与 BOT 的历史消息
|
查看与 BOT 的历史消息
|
||||||
"""
|
"""
|
||||||
import requests
|
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
BOT_TOKEN = "8410096573:AAFLJbWUp2Xog0oeoe7hfBlVqR7ChoSl9Pg"
|
import requests
|
||||||
|
|
||||||
|
from env_loader import load_env
|
||||||
|
|
||||||
|
load_env()
|
||||||
|
BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
|
||||||
|
if not BOT_TOKEN:
|
||||||
|
raise RuntimeError("请在 .env 中设置 TELEGRAM_BOT_TOKEN")
|
||||||
BASE_URL = f"https://api.telegram.org/bot{BOT_TOKEN}"
|
BASE_URL = f"https://api.telegram.org/bot{BOT_TOKEN}"
|
||||||
|
|
||||||
def get_updates(offset=None, limit=100):
|
def get_updates(offset=None, limit=100):
|
||||||
|
|||||||
@@ -2,10 +2,17 @@
|
|||||||
"""
|
"""
|
||||||
检查并配置 Webhook
|
检查并配置 Webhook
|
||||||
"""
|
"""
|
||||||
import requests
|
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
BOT_TOKEN = "8410096573:AAFLJbWUp2Xog0oeoe7hfBlVqR7ChoSl9Pg"
|
import requests
|
||||||
|
|
||||||
|
from env_loader import load_env
|
||||||
|
|
||||||
|
load_env()
|
||||||
|
BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
|
||||||
|
if not BOT_TOKEN:
|
||||||
|
raise RuntimeError("请在 .env 中设置 TELEGRAM_BOT_TOKEN")
|
||||||
BASE_URL = f"https://api.telegram.org/bot{BOT_TOKEN}"
|
BASE_URL = f"https://api.telegram.org/bot{BOT_TOKEN}"
|
||||||
|
|
||||||
def get_webhook_info():
|
def get_webhook_info():
|
||||||
|
|||||||
@@ -9,16 +9,22 @@ Telethon Session 创建脚本(安全版本)
|
|||||||
import asyncio
|
import asyncio
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from telethon import TelegramClient
|
from telethon import TelegramClient
|
||||||
from telethon.errors import SessionPasswordNeededError
|
from telethon.errors import SessionPasswordNeededError
|
||||||
|
|
||||||
# 你的 API 凭证
|
from env_loader import load_env
|
||||||
API_ID = 24660516
|
|
||||||
API_HASH = "eae564578880a59c9963916ff1bbbd3a"
|
|
||||||
|
|
||||||
# Session 文件保存位置 - 独立的安全目录
|
load_env()
|
||||||
SESSION_DIR = Path.home() / "telegram_sessions"
|
|
||||||
SESSION_PATH = SESSION_DIR / "funstat_bot"
|
API_ID = int(os.getenv("TELEGRAM_API_ID", "0") or 0)
|
||||||
|
API_HASH = os.getenv("TELEGRAM_API_HASH", "")
|
||||||
|
SESSION_BASE = os.path.expanduser(os.getenv("TELEGRAM_SESSION_PATH", str(Path.home() / "telegram_sessions" / "funstat_bot")))
|
||||||
|
SESSION_PATH = Path(SESSION_BASE)
|
||||||
|
SESSION_DIR = SESSION_PATH.parent
|
||||||
|
|
||||||
|
if not API_ID or not API_HASH:
|
||||||
|
raise RuntimeError("请在 .env 中设置 TELEGRAM_API_ID 和 TELEGRAM_API_HASH")
|
||||||
|
|
||||||
async def create_session():
|
async def create_session():
|
||||||
"""创建 Telegram session 文件"""
|
"""创建 Telegram session 文件"""
|
||||||
@@ -29,7 +35,7 @@ async def create_session():
|
|||||||
print()
|
print()
|
||||||
|
|
||||||
# 创建 session 目录
|
# 创建 session 目录
|
||||||
SESSION_DIR.mkdir(exist_ok=True)
|
SESSION_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
print(f"📁 Session 目录: {SESSION_DIR}")
|
print(f"📁 Session 目录: {SESSION_DIR}")
|
||||||
print()
|
print()
|
||||||
|
|
||||||
|
|||||||
11
scripts/env_loader.py
Normal file
11
scripts/env_loader.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
|
||||||
|
def load_env():
|
||||||
|
base_dir = Path(__file__).resolve().parents[1]
|
||||||
|
dotenv_path = base_dir / ".env"
|
||||||
|
if dotenv_path.exists():
|
||||||
|
load_dotenv(dotenv_path)
|
||||||
|
|
||||||
@@ -2,11 +2,18 @@
|
|||||||
"""
|
"""
|
||||||
探索 Telegram BOT 的功能
|
探索 Telegram BOT 的功能
|
||||||
"""
|
"""
|
||||||
import requests
|
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
import time
|
import time
|
||||||
|
|
||||||
BOT_TOKEN = "8410096573:AAFLJbWUp2Xog0oeoe7hfBlVqR7ChoSl9Pg"
|
import requests
|
||||||
|
|
||||||
|
from env_loader import load_env
|
||||||
|
|
||||||
|
load_env()
|
||||||
|
BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
|
||||||
|
if not BOT_TOKEN:
|
||||||
|
raise RuntimeError("请在 .env 中设置 TELEGRAM_BOT_TOKEN")
|
||||||
BASE_URL = f"https://api.telegram.org/bot{BOT_TOKEN}"
|
BASE_URL = f"https://api.telegram.org/bot{BOT_TOKEN}"
|
||||||
|
|
||||||
def get_bot_info():
|
def get_bot_info():
|
||||||
|
|||||||
@@ -2,12 +2,19 @@
|
|||||||
"""
|
"""
|
||||||
交互式 Telegram BOT 测试工具
|
交互式 Telegram BOT 测试工具
|
||||||
"""
|
"""
|
||||||
import requests
|
|
||||||
import json
|
import json
|
||||||
import time
|
|
||||||
import sys
|
import sys
|
||||||
|
import time
|
||||||
|
import os
|
||||||
|
|
||||||
BOT_TOKEN = "8410096573:AAFLJbWUp2Xog0oeoe7hfBlVqR7ChoSl9Pg"
|
import requests
|
||||||
|
|
||||||
|
from env_loader import load_env
|
||||||
|
|
||||||
|
load_env()
|
||||||
|
BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
|
||||||
|
if not BOT_TOKEN:
|
||||||
|
raise RuntimeError("请在 .env 中设置 TELEGRAM_BOT_TOKEN")
|
||||||
BASE_URL = f"https://api.telegram.org/bot{BOT_TOKEN}"
|
BASE_URL = f"https://api.telegram.org/bot{BOT_TOKEN}"
|
||||||
|
|
||||||
# 这里需要你的 Telegram 用户 ID
|
# 这里需要你的 Telegram 用户 ID
|
||||||
|
|||||||
@@ -3,12 +3,19 @@
|
|||||||
测试 funstat BOT 的所有命令
|
测试 funstat BOT 的所有命令
|
||||||
基于截图发现的功能
|
基于截图发现的功能
|
||||||
"""
|
"""
|
||||||
import requests
|
|
||||||
import json
|
import json
|
||||||
import time
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
BOT_TOKEN = "8410096573:AAFLJbWUp2Xog0oeoe7hfBlVqR7ChoSl9Pg"
|
import requests
|
||||||
|
|
||||||
|
from env_loader import load_env
|
||||||
|
|
||||||
|
load_env()
|
||||||
|
BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
|
||||||
|
if not BOT_TOKEN:
|
||||||
|
raise RuntimeError("请在 .env 中设置 TELEGRAM_BOT_TOKEN")
|
||||||
BASE_URL = f"https://api.telegram.org/bot{BOT_TOKEN}"
|
BASE_URL = f"https://api.telegram.org/bot{BOT_TOKEN}"
|
||||||
|
|
||||||
# 测试用的免费 ID(从 BOT 提供)
|
# 测试用的免费 ID(从 BOT 提供)
|
||||||
|
|||||||
@@ -2,11 +2,18 @@
|
|||||||
"""
|
"""
|
||||||
测试 BOT 的所有命令并获取响应
|
测试 BOT 的所有命令并获取响应
|
||||||
"""
|
"""
|
||||||
import requests
|
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
import time
|
import time
|
||||||
|
|
||||||
BOT_TOKEN = "8410096573:AAFLJbWUp2Xog0oeoe7hfBlVqR7ChoSl9Pg"
|
import requests
|
||||||
|
|
||||||
|
from env_loader import load_env
|
||||||
|
|
||||||
|
load_env()
|
||||||
|
BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
|
||||||
|
if not BOT_TOKEN:
|
||||||
|
raise RuntimeError("请在 .env 中设置 TELEGRAM_BOT_TOKEN")
|
||||||
BASE_URL = f"https://api.telegram.org/bot{BOT_TOKEN}"
|
BASE_URL = f"https://api.telegram.org/bot{BOT_TOKEN}"
|
||||||
|
|
||||||
def get_updates(offset=None):
|
def get_updates(offset=None):
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
#!/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"
|
|
||||||
101
translation_search_翻译.csv
Normal file
101
translation_search_翻译.csv
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
序号,类型,成员数,链接/ID,原文标题,英文翻译
|
||||||
|
1,频道,660469,ID 1710983297,zh_CN 中文语言包 中文安装包 中文翻..,"zh_CN Chinese language pack, Chinese installer, Chinese translation …"
|
||||||
|
2,频道,584550,ID 1566678243,中文翻译|中文搜索|中文导航群,Chinese translation / Chinese search / Chinese directory group
|
||||||
|
3,频道,438211,https://t.me/zh_cnm2f,Tel℮ցram-中文翻译-简体中文语言包,Telegram Chinese translation Simplified Chinese language pack
|
||||||
|
4,频道,438207,https://t.me/zh_cnwfs,Тelеցrαm-中文设置-中文翻译-简体..,"Telegram Chinese settings, Chinese translation, Simplified …"
|
||||||
|
5,频道,417088,https://t.me/zh_cna6j,zh_cn 简体中文 中文翻译 中文汉化..,"zh_cn Simplified Chinese, Chinese translation, Chinese localization …"
|
||||||
|
6,频道,379036,ID 2150536944,zh_CN 中文翻译包 中文安装包 中文汉化包,"zh_CN Chinese translation pack, Chinese installer, Chinese localization pack"
|
||||||
|
7,频道,367296,ID 1583787456,中文翻译语言包,Chinese translation language pack
|
||||||
|
8,频道,330244,ID 2148105977,zh_CN 中文语言 中文翻译 中文安装..,"zh_CN Chinese language, Chinese translation, Chinese installation …"
|
||||||
|
9,频道,322010,ID 2223733474,zh_ϹN 中文语言 中文翻译 中文安装..,"zh_CN Chinese language, Chinese translation, Chinese installation …"
|
||||||
|
10,频道,302241,ID 2148451952,zh_ϹN 中文语言 中文翻译 中文安装..,"zh_CN Chinese language, Chinese translation, Chinese installation …"
|
||||||
|
11,频道,279123,https://t.me/zhcng,zh_CN 中文翻译 简体中文 中文简体,"zh_CN Chinese translation, Simplified Chinese, Chinese simplified"
|
||||||
|
12,频道,252698,ID 1774109250,ZH-ϹN 中文安装包 中文语言包 中文..,"ZH_CN Chinese installer pack, Chinese language pack, Chinese …"
|
||||||
|
13,频道,247543,ID 2194406110,zh_CN 中文语言 中文翻译 中文安装..,"zh_CN Chinese language, Chinese translation, Chinese installation …"
|
||||||
|
14,频道,224238,https://t.me/zhongwenbao_setlanguage0e,zh_сη 简体中文 中文翻译包,"zh_cn Simplified Chinese, Chinese translation pack"
|
||||||
|
15,未知,N/A,https://t.me/cn_zh1,简体中文包 中文安装包 中文翻译包 zh..,"Simplified Chinese pack, Chinese installer, Chinese translation pack zh…"
|
||||||
|
16,频道,222378,https://t.me/zhongwenbao_setlanguage0d,zh_cn 中文翻译包 中文安装包,"zh_cn Chinese translation pack, Chinese installer pack"
|
||||||
|
17,频道,191801,ID 1144415959,中文安装包 中文语言包 中文翻译 中文频..,"Chinese installer pack, Chinese language pack, Chinese translation, Chinese channel …"
|
||||||
|
18,频道,171299,ID 1916164807,zh_cη 中文安装 简体中文 中文翻译,"zh_cn Chinese installation, Simplified Chinese, Chinese translation"
|
||||||
|
19,频道,157577,https://t.me/zh_cnm9w,中文安装包 简体中文 中文翻译 zhon..,"Chinese installer pack, Simplified Chinese, Chinese translation zh…"
|
||||||
|
20,频道,156840,https://t.me/zh_cn2345ccc,zh_сn 中文设置 中文翻译包 简体中..,"zh_cn Chinese settings, Chinese translation pack, Simplified Chinese …"
|
||||||
|
21,频道,141723,ID 1915735619,中文翻译 中文设置 中文汉化,"Chinese translation, Chinese settings, Chinese localization"
|
||||||
|
22,频道,128945,ID 1827710116,zh_CN中文翻译包 中文设置 中文转换..,"zh_CN Chinese translation pack, Chinese settings, Chinese conversion …"
|
||||||
|
23,未知,N/A,https://t.me/zh_cn26x,Telegrαm-zh_CN 简体中文..,Telegram zh_CN Simplified Chinese …
|
||||||
|
24,未知,N/A,https://t.me/zh_cnaep,zhсη 中文简体 简体中文 中文翻译,"zh_cn Simplified Chinese, Simplified Chinese, Chinese translation"
|
||||||
|
25,未知,N/A,https://t.me/zh_cn05y,zh_cη 简体中文包 中文翻译包 zh..,"zh_cn Simplified Chinese pack, Chinese translation pack zh…"
|
||||||
|
26,私有,N/A,ID 2119919233,zh_ϹN 中文安装包 中文翻译包 简体..,"zh_CN Chinese installer pack, Chinese translation pack, Simplified …"
|
||||||
|
27,未知,N/A,https://t.me/zh_cnqq123,zh-CN 中文设置 中文翻译 简体中文,"zh-CN Chinese settings, Chinese translation, Simplified Chinese"
|
||||||
|
28,未知,N/A,https://t.me/DSFDSFWS,Tеłegrαm🔥 中文丨中文包丨汉化..,Telegram Chinese / Chinese pack / localization …
|
||||||
|
29,未知,N/A,https://t.me/setlanguage_zhongwenbaoh,中文设置 中文翻译包 ch_zη,"Chinese settings, Chinese translation pack ch_zn"
|
||||||
|
30,未知,N/A,https://t.me/qerk5,中文翻译【中文包】中文汉化 中文转换 聪..,Chinese translation [Chinese pack] Chinese localization Chinese conversion …
|
||||||
|
31,频道,124944,ID 1076212650,@zhongwen 中文语言安装包🅥汉..,@zhongwen Chinese language installer pack V Chinese …
|
||||||
|
32,频道,121811,ID 1590476062,zh_cn 中文简体 中文汉化 中文翻译,"zh_cn Simplified Chinese, Chinese localization, Chinese translation"
|
||||||
|
33,频道,120042,ID 1706645578,zh_ϹN 简体中文 中文翻译 中文设置..,"zh_CN Simplified Chinese, Chinese translation, Chinese settings …"
|
||||||
|
34,频道,116483,ID 1974111504,zh_cn 简体中文包 中文安装包 中文..,"zh_cn Simplified Chinese pack, Chinese installer pack, Chinese …"
|
||||||
|
35,频道,116252,ID 1240579220,zh_CN 简体中文 中文翻译 中文安装..,"zh_CN Simplified Chinese, Chinese translation, Chinese installation …"
|
||||||
|
36,频道,111703,ID 1336825472,简体中文包 中文翻译包 中文汉化包 飞机..,"Simplified Chinese pack, Chinese translation pack, Chinese localization pack for Telegram"
|
||||||
|
37,频道,108625,ID 1820093287,zh_сn 中文翻译语言包,zh_cn Chinese translation language pack
|
||||||
|
38,频道,104172,ID 1098703312,zh_cη 简体中文 中文设置 中文语言..,"zh_cn Simplified Chinese, Chinese settings, Chinese language …"
|
||||||
|
39,频道,104020,ID 1366688177,zh_CN 中文设置 中文翻译 简体中文..,"zh_CN Chinese settings, Chinese translation, Simplified Chinese …"
|
||||||
|
40,频道,103390,ID 1100945858,简体中文包 中文翻译包 中文汉化包 飞机..,"Simplified Chinese pack, Chinese translation pack, Chinese localization pack for Telegram"
|
||||||
|
41,未知,N/A,ID 1161864298,zh_СN 中文安装包 简体中文包 中文..,"zh_CN Chinese installer pack, Simplified Chinese pack, Chinese …"
|
||||||
|
42,未知,N/A,ID 2567113236,zhcn 中文简体 中文翻译包 中文汉化..,"zhcn Simplified Chinese, Chinese translation pack, Chinese localization …"
|
||||||
|
43,未知,N/A,ID 2570705953,江湖丨中文包丨中文翻译丨简体中文丨中文安..,"Jianghu Chinese pack, Chinese translation, Simplified Chinese, Chinese …"
|
||||||
|
44,未知,N/A,ID 2589631230,中文翻译包 中文汉化 ch_zη,"Chinese translation pack, Chinese localization ch_zn"
|
||||||
|
45,未知,N/A,ID 2659302988,中文翻译包 简体中文包 zhcn,"Chinese translation pack, Simplified Chinese pack zhcn"
|
||||||
|
46,频道,100872,ID 1733743094,zh_cη 简体中文 中文安装 中文翻译..,"zh_cn Simplified Chinese, Chinese installation, Chinese translation …"
|
||||||
|
47,频道,100306,ID 1440807004,zh_ϹN 中文翻译 中文安装 简体中文..,"zh_CN Chinese translation, Chinese installation, Simplified Chinese …"
|
||||||
|
48,频道,93248,ID 1773112742,zh_CN 中文设置 简体中文 中文翻译..,"zh_CN Chinese settings, Simplified Chinese, Chinese translation …"
|
||||||
|
49,频道,92296,ID 1815877901,中文翻译 中文设置 中文汉化,"Chinese translation, Chinese settings, Chinese localization"
|
||||||
|
50,频道,86816,ID 1628283283,zh_cn 中文汉化 简体中文 中文安装..,"zh_cn Chinese localization, Simplified Chinese, Chinese installation …"
|
||||||
|
51,频道,85543,ID 1006936145,免费翻墙VPN加速器🚀,Free VPN circumvention accelerator 🚀
|
||||||
|
52,频道,84931,ID 1800001976,Telegram-zh_CN 简体中文语..,Telegram zh_CN Simplified Chinese language …
|
||||||
|
53,频道,78176,ID 1168481222,zh_CN 中文安装包 中文翻译包 简体..,"zh_CN Chinese installer pack, Chinese translation pack, Simplified …"
|
||||||
|
54,频道,72997,ID 1795820545,zh_cη 简体中文包 中文翻译 中文安..,"zh_cn Simplified Chinese pack, Chinese translation, Chinese …"
|
||||||
|
55,未知,N/A,ID 2376777267,zh_CN 简体中文语言包 中文安装 中..,"zh_CN Simplified Chinese language pack, Chinese installation, Chinese …"
|
||||||
|
56,未知,N/A,ID 2525444669,简体中文包 中文翻译包 中文汉化包 飞机..,"Simplified Chinese pack, Chinese translation pack, Chinese localization pack for Telegram"
|
||||||
|
57,未知,N/A,ID 1729040890,zh_ϹN 中文翻译 中文安装 简体中文..,"zh_CN Chinese translation, Chinese installation, Simplified Chinese …"
|
||||||
|
58,未知,N/A,ID 2552426535,τg中文包 飞机中文包 飞机翻译包 中文..,"TG Chinese pack, Telegram Chinese pack, Telegram translation pack Chinese …"
|
||||||
|
59,私有,N/A,ID 1722627298,zh_cη 简体中文 中文安装 中文翻译..,"zh_cn Simplified Chinese, Chinese installation, Chinese translation …"
|
||||||
|
60,私有,N/A,ID 1844633120,zh_CN 中文语言包 中文汉化 中文翻..,"zh_CN Chinese language pack, Chinese localization, Chinese translation …"
|
||||||
|
61,频道,58940,ID 1996471747,zh_CN-中文安装包 中文语言包 中文..,"zh_CN Chinese installer pack, Chinese language pack, Chinese …"
|
||||||
|
62,频道,52267,ID 1663378188,Teⅼergαm-zh_CN 中文语言翻译包,Telegram zh_CN Chinese language translation pack
|
||||||
|
63,私有,N/A,ID 1632218770,zh_CN 简体中文 中文汉化 中文简体..,"zh_CN Simplified Chinese, Chinese localization, Chinese simplified …"
|
||||||
|
64,未知,N/A,ID 1947167790,zh_cn 中文简体包 中文语言包 中文..,"zh_cn Simplified Chinese pack, Chinese language pack, Chinese …"
|
||||||
|
65,未知,N/A,ID 2121686594,zh_CN 中文语言包 中文安装包 中文..,"zh_CN Chinese language pack, Chinese installer pack, Chinese …"
|
||||||
|
66,私有,N/A,ID 2135222179,zh_CN 简体中文翻译语言包 中文频道..,"zh_CN Simplified Chinese translation language pack, Chinese channel …"
|
||||||
|
67,私有,N/A,ID 2146957973,Telegram-zh_CN 简体中文包..,Telegram zh_CN Simplified Chinese pack …
|
||||||
|
68,私有,N/A,ID 2268109497,中文导航 中文搜索 中文群组 中文翻译,"Chinese directory, Chinese search, Chinese groups, Chinese translation"
|
||||||
|
69,未知,N/A,ID 2334140826,TG中文包【汉化】翻译 简体中文包 中文..,"TG Chinese pack [localization] translation, Simplified Chinese pack, Chinese …"
|
||||||
|
70,未知,N/A,ID 2508630894,Tеlеցrαm🔥 中文包丨汉化丨简体..,Telegram Chinese pack / localization / Simplified …
|
||||||
|
71,未知,N/A,ID 2522803089,中文包 简体中文包 中文转换包 中文语言..,"Chinese pack, Simplified Chinese pack, Chinese conversion pack, Chinese language …"
|
||||||
|
72,未知,N/A,ID 2601237857,zh_CN 中文安装 中文翻译 简体中文..,"zh_CN Chinese installation, Chinese translation, Simplified Chinese …"
|
||||||
|
73,未知,N/A,ID 1183238584,中文翻译包 中文汉化 ch_zn,"Chinese translation pack, Chinese localization ch_zn"
|
||||||
|
74,未知,N/A,ID 2664539687,zh_ϹN 中文安装 中文翻译 简体中文..,"zh_CN Chinese installation, Chinese translation, Simplified Chinese …"
|
||||||
|
75,未知,N/A,ID 1624527901,zh_CN 简体中文汉化包 中文汉化 中..,"zh_CN Simplified Chinese localization pack, Chinese localization, Chinese …"
|
||||||
|
76,频道,46204,ID 1424475877,zh_cη 简体中文包 中文安装包 中文..,"zh_cn Simplified Chinese pack, Chinese installer pack, Chinese …"
|
||||||
|
77,频道,44585,ID 1689354340,汉语中文翻译,Mandarin Chinese translation
|
||||||
|
78,频道,34597,ID 1671814308,海象ws/tg/line/fb/zalo..,Walrus ws/tg/line/fb/zalo …
|
||||||
|
79,频道,33726,ID 1401230741,免费vpn梯子机场翻墙技术,Free VPN ladder airport circumvention technology
|
||||||
|
80,频道,33645,ID 1646824771,helłoworld官方客服 海外社交软..,helloworld official customer service overseas social …
|
||||||
|
81,频道,32765,ID 1178337317,白嫖机场节点小火箭翻墙梯子|免费VPN,Free airport nodes Shadowrocket circumvention ladder / free VPN
|
||||||
|
82,私有,N/A,ID 1606604404,hełloworld官方客服频道 海外社..,helloworld official customer service channel overseas social …
|
||||||
|
83,未知,N/A,ID 1292723381,tg中文包 江湖中文包 聪聪中文包 飞机..,"TG Chinese pack, Jianghu Chinese pack, Congcong Chinese pack, Telegram …"
|
||||||
|
84,私有,N/A,ID 2019856458,zh_CN-语言包 翻译包 安装包,"zh_CN language pack, translation pack, installer pack"
|
||||||
|
85,未知,N/A,ID 2097963587,快连VPN-翻墙加速器-VPN快连-电脑..,"Kuailian VPN circumvention accelerator, VPN Kuailian, computer …"
|
||||||
|
86,私有,N/A,ID 2255570176,zh_cn 中文安装 简体中文 中文翻译,"zh_cn Chinese installation, Simplified Chinese, Chinese translation"
|
||||||
|
87,未知,N/A,ID 2364236658,翻译中文 最新版,Chinese translation latest version
|
||||||
|
88,未知,N/A,ID 2394841482,zh_cn 中文语言 简体中文 中文安装..,"zh_cn Chinese language, Simplified Chinese, Chinese installation …"
|
||||||
|
89,未知,N/A,ID 2459873292,QᴜickQ官方频道 翻墙加速器🚀,QuickQ official channel circumvention accelerator 🚀
|
||||||
|
90,未知,N/A,ID 2655916317,江湖丨中文包丨中文翻译丨简体中文丨中文安..,"Jianghu Chinese pack, Chinese translation, Simplified Chinese, Chinese …"
|
||||||
|
91,频道,30347,ID 2181323289,zh_CN 简体中文 中文包 中文 简体..,"zh_CN Simplified Chinese, Chinese pack, Chinese, Simplified …"
|
||||||
|
92,频道,30332,ID 2190065351,zh_CN 简体中文 中文包 中文 简体..,"zh_CN Simplified Chinese, Chinese pack, Chinese, Simplified …"
|
||||||
|
93,频道,30324,ID 2158084236,zh_CN 简体中文 中文包 中文 简体..,"zh_CN Simplified Chinese, Chinese pack, Chinese, Simplified …"
|
||||||
|
94,频道,27661,ID 1976367427,helⅼowοrłd官方客服频道 heⅼ..,helloworld official customer service channel hel…
|
||||||
|
95,频道,27564,ID 1394985741,象征 ... JC▸ ⌜ 翻译 ⌟ ──..,Symbolic … JC▸ Translation …
|
||||||
|
96,频道,26109,ID 1982792091,helloworld官方客服频道 海外社..,helloworld official customer service channel overseas social …
|
||||||
|
97,频道,25680,ID 1963495599,TranWorⅼd翻译软件唯一客服(唯一..,TranWorld translation software only customer service (only …
|
||||||
|
98,频道,23541,ID 3128265746,逆风翻盘局【9Y.COM】,Turn the tides bureau [9Y.COM]
|
||||||
|
99,频道,23474,ID 1178940393,破解VPN机场节点翻墙加速器小火箭|免费..,Cracked VPN airport nodes circumvention accelerator Shadowrocket / free …
|
||||||
|
100,频道,N/A,ID 1388012744,免费翻墙VPN机场软件群,Free VPN airport software group
|
||||||
|
Reference in New Issue
Block a user