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_API_HASH=your_api_hash
|
||||
# ────────────────────────────────────────────────────────────────
|
||||
# Telegram 配置
|
||||
# ────────────────────────────────────────────────────────────────
|
||||
TELEGRAM_API_ID=24660516
|
||||
TELEGRAM_API_HASH=eae564578880a59c9963916ff1bbbd3a
|
||||
# 每位同事需要使用自己的 session 文件,路径可自定义
|
||||
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
|
||||
FUNSTAT_PROXY_HOST=127.0.0.1
|
||||
FUNSTAT_PROXY_PORT=1080
|
||||
# ────────────────────────────────────────────────────────────────
|
||||
# MCP 服务器配置
|
||||
# ────────────────────────────────────────────────────────────────
|
||||
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 表示仅本地访问
|
||||
FUNSTAT_HOST=0.0.0.0
|
||||
FUNSTAT_PORT=8091
|
||||
# ────────────────────────────────────────────────────────────────
|
||||
# Postgres / Docker 容器配置
|
||||
# ────────────────────────────────────────────────────────────────
|
||||
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
|
||||
*.sqlite
|
||||
*.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 服务器实现,以及所有相关文档、配置和工具。
|
||||
@@ -49,7 +68,7 @@ funstat_mcp_package/
|
||||
|
||||
## 🎯 核心功能
|
||||
|
||||
### MCP 工具列表 (9个)
|
||||
### MCP 工具列表 (8个)
|
||||
|
||||
| 工具名 | 功能 | 对应命令 |
|
||||
|--------|------|---------|
|
||||
@@ -61,7 +80,6 @@ funstat_mcp_package/
|
||||
| `funstat_text` | 搜索消息内容 | `/text [关键词]` |
|
||||
| `funstat_human` | 搜索用户 | `/human [关键词]` |
|
||||
| `funstat_user_info` | 查询用户详情 | `/user_info [username]` |
|
||||
| `funstat_user_messages` | 获取用户聊天记录(自动翻页) | `/user_info [username] ➜ Messages ➜ All` |
|
||||
|
||||
### 协议支持
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"funstat": {
|
||||
"command": "/Users/lucas/牛马/agentapi",
|
||||
"args": ["proxy", "http://127.0.0.1:8091/sse"],
|
||||
"args": ["proxy", "http://127.0.0.1:8094/sse"],
|
||||
"env": {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"mcpServers": {
|
||||
"funstat": {
|
||||
"command": "/Users/lucas/牛马/agentapi",
|
||||
"args": ["proxy", "http://127.0.0.1:8091/sse"],
|
||||
"args": ["proxy", "http://127.0.0.1:8094/sse"],
|
||||
"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
|
||||
774
core/server.py
774
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 ""
|
||||
|
||||
# 设置环境变量
|
||||
export FUNSTAT_PORT=8091
|
||||
export FUNSTAT_PORT=8094
|
||||
export FUNSTAT_HOST=127.0.0.1
|
||||
|
||||
# 检查依赖
|
||||
|
||||
@@ -28,19 +28,19 @@ sleep 3
|
||||
# 验证启动
|
||||
if ps -p $SERVER_PID > /dev/null; then
|
||||
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 ""
|
||||
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 测试通过"
|
||||
else
|
||||
echo "❌ GET /sse 测试失败"
|
||||
fi
|
||||
|
||||
if curl -s -o /dev/null -w "%{http_code}" -X POST http://127.0.0.1:8091/sse -H 'Content-Type: application/json' -d '{}' | grep -q "200"; then
|
||||
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 测试通过"
|
||||
else
|
||||
echo "❌ POST /sse 测试失败"
|
||||
@@ -49,7 +49,7 @@ if ps -p $SERVER_PID > /dev/null; then
|
||||
echo ""
|
||||
echo "📊 服务器状态:"
|
||||
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"
|
||||
else
|
||||
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 端点
|
||||
echo ""
|
||||
echo "2. 测试 SSE 端点 (GET /sse)..."
|
||||
response=$(timeout 2 curl -s -N -H "Accept: text/event-stream" http://127.0.0.1:8091/sse 2>&1 | head -3)
|
||||
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
|
||||
echo "✓ SSE 端点响应正常"
|
||||
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
|
||||
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
|
||||
URL: http://127.0.0.1:8091/sse
|
||||
URL: http://127.0.0.1:8094/sse
|
||||
Transport: streamable_http
|
||||
Status: enabled
|
||||
Auth: Unsupported
|
||||
@@ -44,7 +44,7 @@ Auth: Unsupported
|
||||
**新增内容**:
|
||||
```toml
|
||||
[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
|
||||
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
|
||||
enabled: true
|
||||
transport: streamable_http
|
||||
url: http://127.0.0.1:8091/sse
|
||||
url: http://127.0.0.1:8094/sse
|
||||
bearer_token_env_var: -
|
||||
http_headers: -
|
||||
env_http_headers: -
|
||||
@@ -110,7 +110,7 @@ codex mcp remove funstat
|
||||
### 重新添加
|
||||
|
||||
```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 连接
|
||||
│ http://127.0.0.1:8091/sse
|
||||
│ http://127.0.0.1:8094/sse
|
||||
│
|
||||
┌─────────────▼──────────────────┐
|
||||
│ Funstat MCP Server (SSE) │
|
||||
│ funstat_mcp/server.py │
|
||||
│ 端口: 8091 │
|
||||
│ 端口: 8094 │
|
||||
└─────────────┬──────────────────┘
|
||||
│
|
||||
│ Telethon
|
||||
@@ -232,7 +232,7 @@ export FUNSTAT_TOKEN="your-token-here"
|
||||
|
||||
# 添加 MCP 服务器时指定
|
||||
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 \
|
||||
funstat
|
||||
```
|
||||
@@ -243,7 +243,7 @@ codex mcp add \
|
||||
|
||||
```toml
|
||||
[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" # 可选
|
||||
```
|
||||
|
||||
@@ -264,7 +264,7 @@ url = "http://127.0.0.1:8091/sse"
|
||||
ps aux | grep server.py
|
||||
|
||||
# 2. 测试 SSE 端点
|
||||
curl -i http://127.0.0.1:8091/sse
|
||||
curl -i http://127.0.0.1:8094/sse
|
||||
|
||||
# 3. 重启 SSE 服务器
|
||||
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
|
||||
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
|
||||
mcp>=1.20.0
|
||||
telethon>=1.41.0
|
||||
starlette>=0.50.0
|
||||
uvicorn>=0.38.0
|
||||
asyncpg>=0.29.0
|
||||
python-dotenv>=1.0.1
|
||||
|
||||
@@ -2,10 +2,17 @@
|
||||
"""
|
||||
查看与 BOT 的历史消息
|
||||
"""
|
||||
import requests
|
||||
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}"
|
||||
|
||||
def get_updates(offset=None, limit=100):
|
||||
|
||||
@@ -2,10 +2,17 @@
|
||||
"""
|
||||
检查并配置 Webhook
|
||||
"""
|
||||
import requests
|
||||
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}"
|
||||
|
||||
def get_webhook_info():
|
||||
|
||||
@@ -9,16 +9,22 @@ Telethon Session 创建脚本(安全版本)
|
||||
import asyncio
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from telethon import TelegramClient
|
||||
from telethon.errors import SessionPasswordNeededError
|
||||
|
||||
# 你的 API 凭证
|
||||
API_ID = 24660516
|
||||
API_HASH = "eae564578880a59c9963916ff1bbbd3a"
|
||||
from env_loader import load_env
|
||||
|
||||
# Session 文件保存位置 - 独立的安全目录
|
||||
SESSION_DIR = Path.home() / "telegram_sessions"
|
||||
SESSION_PATH = SESSION_DIR / "funstat_bot"
|
||||
load_env()
|
||||
|
||||
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():
|
||||
"""创建 Telegram session 文件"""
|
||||
@@ -29,7 +35,7 @@ async def create_session():
|
||||
print()
|
||||
|
||||
# 创建 session 目录
|
||||
SESSION_DIR.mkdir(exist_ok=True)
|
||||
SESSION_DIR.mkdir(parents=True, exist_ok=True)
|
||||
print(f"📁 Session 目录: {SESSION_DIR}")
|
||||
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 的功能
|
||||
"""
|
||||
import requests
|
||||
import json
|
||||
import os
|
||||
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}"
|
||||
|
||||
def get_bot_info():
|
||||
|
||||
@@ -2,12 +2,19 @@
|
||||
"""
|
||||
交互式 Telegram BOT 测试工具
|
||||
"""
|
||||
import requests
|
||||
import json
|
||||
import time
|
||||
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}"
|
||||
|
||||
# 这里需要你的 Telegram 用户 ID
|
||||
|
||||
@@ -3,12 +3,19 @@
|
||||
测试 funstat BOT 的所有命令
|
||||
基于截图发现的功能
|
||||
"""
|
||||
import requests
|
||||
import json
|
||||
import time
|
||||
import os
|
||||
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}"
|
||||
|
||||
# 测试用的免费 ID(从 BOT 提供)
|
||||
|
||||
@@ -2,11 +2,18 @@
|
||||
"""
|
||||
测试 BOT 的所有命令并获取响应
|
||||
"""
|
||||
import requests
|
||||
import json
|
||||
import os
|
||||
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}"
|
||||
|
||||
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