Upload 177 files
Browse files- .dockerignore +33 -74
- CHANGELOG.md +95 -161
- Dockerfile +32 -7
- PROJECT_STRUCTURE_FA.md +513 -0
- QUICK_REFERENCE_FA.md +111 -0
- QUICK_START.md +191 -152
- README_FA.md +421 -0
- REALTIME_FEATURES_FA.md +374 -0
- TREE_STRUCTURE.txt +159 -0
- WEBSOCKET_GUIDE.md +446 -0
- __pycache__/database.cpython-313.pyc +0 -0
- __pycache__/monitor.cpython-313.pyc +0 -0
- api_server_extended.py +1182 -0
- backend/services/auto_discovery_service.py +353 -0
- backend/services/connection_manager.py +274 -0
- backend/services/diagnostics_service.py +391 -0
- docker-compose.yml +72 -67
- import_resources.py +65 -0
- index.html +0 -0
- log_manager.py +387 -0
- provider_manager.py +466 -0
- providers_config_extended.json +1079 -0
- providers_config_ultimate.json +666 -0
- requirements.txt +47 -0
- resource_manager.py +390 -0
- start_server.py +239 -17
- static/css/connection-status.css +330 -0
- static/js/websocket-client.js +317 -0
- templates/index.html +0 -0
- test_providers.py +250 -0
- test_websocket.html +327 -0
- test_websocket_dashboard.html +364 -0
- unified_dashboard.html +0 -0
- utils/__pycache__/__init__.cpython-313.pyc +0 -0
- utils/__pycache__/logger.cpython-313.pyc +0 -0
.dockerignore
CHANGED
|
@@ -4,6 +4,9 @@ __pycache__/
|
|
| 4 |
*$py.class
|
| 5 |
*.so
|
| 6 |
.Python
|
|
|
|
|
|
|
|
|
|
| 7 |
build/
|
| 8 |
develop-eggs/
|
| 9 |
dist/
|
|
@@ -19,15 +22,11 @@ wheels/
|
|
| 19 |
*.egg-info/
|
| 20 |
.installed.cfg
|
| 21 |
*.egg
|
| 22 |
-
MANIFEST
|
| 23 |
-
pip-log.txt
|
| 24 |
-
pip-delete-this-directory.txt
|
| 25 |
|
| 26 |
-
# Virtual
|
|
|
|
| 27 |
venv/
|
| 28 |
ENV/
|
| 29 |
-
env/
|
| 30 |
-
.venv
|
| 31 |
|
| 32 |
# IDE
|
| 33 |
.vscode/
|
|
@@ -35,87 +34,47 @@ env/
|
|
| 35 |
*.swp
|
| 36 |
*.swo
|
| 37 |
*~
|
|
|
|
|
|
|
| 38 |
.DS_Store
|
|
|
|
| 39 |
|
| 40 |
# Git
|
| 41 |
-
.git
|
| 42 |
.gitignore
|
| 43 |
.gitattributes
|
| 44 |
|
| 45 |
-
#
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
|
| 52 |
# Testing
|
| 53 |
.pytest_cache/
|
| 54 |
.coverage
|
| 55 |
htmlcov/
|
| 56 |
-
.tox/
|
| 57 |
-
.hypothesis/
|
| 58 |
-
tests/
|
| 59 |
-
test_*.py
|
| 60 |
-
|
| 61 |
-
# Logs and databases (will be created in container)
|
| 62 |
-
*.log
|
| 63 |
-
logs/
|
| 64 |
-
data/*.db
|
| 65 |
-
data/*.sqlite
|
| 66 |
-
data/*.db-journal
|
| 67 |
|
| 68 |
-
#
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
# Docker
|
| 74 |
-
docker-compose*.yml
|
| 75 |
-
!docker-compose.yml
|
| 76 |
-
Dockerfile
|
| 77 |
-
.dockerignore
|
| 78 |
|
| 79 |
-
#
|
| 80 |
-
|
| 81 |
-
.
|
| 82 |
-
|
| 83 |
-
|
| 84 |
|
| 85 |
# Temporary files
|
| 86 |
-
*.tmp
|
| 87 |
-
*.bak
|
| 88 |
-
*.swp
|
| 89 |
-
temp/
|
| 90 |
tmp/
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
node_modules/
|
| 94 |
-
package-lock.json
|
| 95 |
-
yarn.lock
|
| 96 |
-
|
| 97 |
-
# OS files
|
| 98 |
-
Thumbs.db
|
| 99 |
-
.DS_Store
|
| 100 |
-
desktop.ini
|
| 101 |
-
|
| 102 |
-
# Jupyter notebooks
|
| 103 |
-
.ipynb_checkpoints/
|
| 104 |
-
*.ipynb
|
| 105 |
-
|
| 106 |
-
# Model cache (models will be downloaded in container)
|
| 107 |
-
models/
|
| 108 |
-
.cache/
|
| 109 |
-
.huggingface/
|
| 110 |
-
|
| 111 |
-
# Large files that shouldn't be in image
|
| 112 |
-
*.tar
|
| 113 |
-
*.tar.gz
|
| 114 |
-
*.zip
|
| 115 |
-
*.rar
|
| 116 |
-
*.7z
|
| 117 |
-
|
| 118 |
-
# Screenshots and assets not needed
|
| 119 |
-
screenshots/
|
| 120 |
-
assets/*.png
|
| 121 |
-
assets/*.jpg
|
|
|
|
| 4 |
*$py.class
|
| 5 |
*.so
|
| 6 |
.Python
|
| 7 |
+
env/
|
| 8 |
+
venv/
|
| 9 |
+
ENV/
|
| 10 |
build/
|
| 11 |
develop-eggs/
|
| 12 |
dist/
|
|
|
|
| 22 |
*.egg-info/
|
| 23 |
.installed.cfg
|
| 24 |
*.egg
|
|
|
|
|
|
|
|
|
|
| 25 |
|
| 26 |
+
# Virtual Environment
|
| 27 |
+
.venv
|
| 28 |
venv/
|
| 29 |
ENV/
|
|
|
|
|
|
|
| 30 |
|
| 31 |
# IDE
|
| 32 |
.vscode/
|
|
|
|
| 34 |
*.swp
|
| 35 |
*.swo
|
| 36 |
*~
|
| 37 |
+
|
| 38 |
+
# OS
|
| 39 |
.DS_Store
|
| 40 |
+
Thumbs.db
|
| 41 |
|
| 42 |
# Git
|
| 43 |
+
.git
|
| 44 |
.gitignore
|
| 45 |
.gitattributes
|
| 46 |
|
| 47 |
+
# Docker
|
| 48 |
+
Dockerfile
|
| 49 |
+
docker-compose.yml
|
| 50 |
+
.dockerignore
|
| 51 |
+
|
| 52 |
+
# Logs
|
| 53 |
+
logs/
|
| 54 |
+
*.log
|
| 55 |
+
|
| 56 |
+
# Environment
|
| 57 |
+
.env
|
| 58 |
+
.env.local
|
| 59 |
+
.env.*.local
|
| 60 |
|
| 61 |
# Testing
|
| 62 |
.pytest_cache/
|
| 63 |
.coverage
|
| 64 |
htmlcov/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
|
| 66 |
+
# Documentation
|
| 67 |
+
docs/
|
| 68 |
+
*.md
|
| 69 |
+
README*
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
|
| 71 |
+
# Data files
|
| 72 |
+
*.csv
|
| 73 |
+
*.json.bak
|
| 74 |
+
*.db
|
| 75 |
+
*.sqlite
|
| 76 |
|
| 77 |
# Temporary files
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
tmp/
|
| 79 |
+
temp/
|
| 80 |
+
*.tmp
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
CHANGELOG.md
CHANGED
|
@@ -1,161 +1,95 @@
|
|
| 1 |
-
#
|
| 2 |
-
|
| 3 |
-
##
|
| 4 |
-
|
| 5 |
-
###
|
| 6 |
-
-
|
| 7 |
-
-
|
| 8 |
-
-
|
| 9 |
-
-
|
| 10 |
-
-
|
| 11 |
-
-
|
| 12 |
-
-
|
| 13 |
-
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
-
|
| 17 |
-
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
-
|
| 21 |
-
-
|
| 22 |
-
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
-
|
| 28 |
-
-
|
| 29 |
-
-
|
| 30 |
-
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
-
|
| 36 |
-
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
-
|
| 42 |
-
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
-
|
| 56 |
-
-
|
| 57 |
-
-
|
| 58 |
-
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
-
|
| 63 |
-
-
|
| 64 |
-
-
|
| 65 |
-
-
|
| 66 |
-
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
-
|
| 72 |
-
-
|
| 73 |
-
-
|
| 74 |
-
-
|
| 75 |
-
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
-
|
| 87 |
-
-
|
| 88 |
-
-
|
| 89 |
-
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
- ✅ CORS configuration
|
| 97 |
-
- ✅ Input validation
|
| 98 |
-
- ✅ Error message sanitization
|
| 99 |
-
|
| 100 |
-
---
|
| 101 |
-
|
| 102 |
-
## ⚡ Performance Improvements
|
| 103 |
-
|
| 104 |
-
### [1.0.0]
|
| 105 |
-
- ✅ Optimized WebSocket messages
|
| 106 |
-
- ✅ Reduced API call frequency
|
| 107 |
-
- ✅ Improved chart rendering
|
| 108 |
-
- ✅ Cache implementation
|
| 109 |
-
|
| 110 |
-
---
|
| 111 |
-
|
| 112 |
-
## 📦 Dependencies
|
| 113 |
-
|
| 114 |
-
### Python Packages (v1.0.0)
|
| 115 |
-
```
|
| 116 |
-
fastapi==0.104.1
|
| 117 |
-
uvicorn[standard]==0.24.0
|
| 118 |
-
websockets==12.0
|
| 119 |
-
python-multipart==0.0.6
|
| 120 |
-
pydantic==2.5.0
|
| 121 |
-
```
|
| 122 |
-
|
| 123 |
-
### Frontend Libraries
|
| 124 |
-
```
|
| 125 |
-
Chart.js 4.4.0
|
| 126 |
-
Google Fonts (Inter)
|
| 127 |
-
```
|
| 128 |
-
|
| 129 |
-
---
|
| 130 |
-
|
| 131 |
-
## 🎯 Known Issues
|
| 132 |
-
|
| 133 |
-
### [1.0.0]
|
| 134 |
-
- Mock data only (not connected to real APIs)
|
| 135 |
-
- No data persistence
|
| 136 |
-
- Limited to localhost
|
| 137 |
-
- No authentication
|
| 138 |
-
|
| 139 |
-
---
|
| 140 |
-
|
| 141 |
-
## 💝 تشکرات / Acknowledgments
|
| 142 |
-
|
| 143 |
-
این پروژه با استفاده از:
|
| 144 |
-
- FastAPI framework
|
| 145 |
-
- Chart.js library
|
| 146 |
-
- Google Fonts
|
| 147 |
-
- Community feedback
|
| 148 |
-
|
| 149 |
-
---
|
| 150 |
-
|
| 151 |
-
## 📞 گزارش باگ / Bug Reports
|
| 152 |
-
|
| 153 |
-
برای گزارش باگ یا پیشنهاد:
|
| 154 |
-
- Issue باز کنید
|
| 155 |
-
- ایمیل بزنید
|
| 156 |
-
- Pull Request بزنید
|
| 157 |
-
|
| 158 |
-
---
|
| 159 |
-
|
| 160 |
-
**نسخه فعلی / Current Version: 1.0.0**
|
| 161 |
-
**تاریخ انتشار / Release Date: 2025-01-15**
|
|
|
|
| 1 |
+
# 📋 Changelog - نسخه 3.0.0
|
| 2 |
+
|
| 3 |
+
## ✨ ویژگیهای جدید
|
| 4 |
+
|
| 5 |
+
### 🎯 Log Management System
|
| 6 |
+
- ✅ سیستم کامل مدیریت لاگها
|
| 7 |
+
- ✅ فیلتر پیشرفته (Level, Category, Provider, Time Range)
|
| 8 |
+
- ✅ جستجو در لاگها
|
| 9 |
+
- ✅ Export به JSON و CSV
|
| 10 |
+
- ✅ Import از JSON
|
| 11 |
+
- ✅ آمار تفصیلی لاگها
|
| 12 |
+
- ✅ Log Rotation خودکار
|
| 13 |
+
- ✅ نمایش Real-time در داشبورد
|
| 14 |
+
|
| 15 |
+
### 📦 Resource Management System
|
| 16 |
+
- ✅ مدیریت کامل منابع API
|
| 17 |
+
- ✅ Import از فایلهای JSON مختلف
|
| 18 |
+
- ✅ Export به JSON و CSV
|
| 19 |
+
- ✅ Backup خودکار
|
| 20 |
+
- ✅ اعتبارسنجی Provider
|
| 21 |
+
- ✅ فیلتر بر اساس Category
|
| 22 |
+
- ✅ آمار تفصیلی منابع
|
| 23 |
+
|
| 24 |
+
### 🎨 UI/UX Enhancements
|
| 25 |
+
- ✅ تب جدید Logs با فیلتر پیشرفته
|
| 26 |
+
- ✅ تب جدید Resources با مدیریت کامل
|
| 27 |
+
- ✅ Modal برای Import منابع
|
| 28 |
+
- ✅ بهبود طراحی و رنگبندی
|
| 29 |
+
- ✅ Toast Notifications
|
| 30 |
+
- ✅ Responsive Design
|
| 31 |
+
|
| 32 |
+
### 🔧 API Enhancements
|
| 33 |
+
- ✅ 20+ Endpoint جدید برای Log Management
|
| 34 |
+
- ✅ 10+ Endpoint جدید برای Resource Management
|
| 35 |
+
- ✅ یکپارچهسازی Log Manager با Provider Manager
|
| 36 |
+
- ✅ یکپارچهسازی Resource Manager
|
| 37 |
+
|
| 38 |
+
### 📊 Provider Management
|
| 39 |
+
- ✅ ادغام 200+ منبع از فایلهای JSON
|
| 40 |
+
- ✅ پشتیبانی از فرمتهای مختلف JSON
|
| 41 |
+
- ✅ تبدیل خودکار فرمتهای مختلف
|
| 42 |
+
- ✅ مدیریت API Keys
|
| 43 |
+
|
| 44 |
+
## 📁 فایلهای جدید
|
| 45 |
+
|
| 46 |
+
1. **log_manager.py** - سیستم مدیریت لاگها
|
| 47 |
+
2. **resource_manager.py** - سیستم مدیریت منابع
|
| 48 |
+
3. **import_resources.py** - اسکریپت import خودکار
|
| 49 |
+
4. **providers_config_ultimate.json** - پیکربندی کامل با 200+ منبع
|
| 50 |
+
5. **QUICK_START.md** - راهنمای سریع شروع
|
| 51 |
+
|
| 52 |
+
## 🔄 تغییرات در فایلهای موجود
|
| 53 |
+
|
| 54 |
+
### unified_dashboard.html
|
| 55 |
+
- ✅ افزودن تب Logs
|
| 56 |
+
- ✅ افزودن تب Resources
|
| 57 |
+
- ✅ افزودن Modal Import
|
| 58 |
+
- ✅ توابع JavaScript برای Logs و Resources
|
| 59 |
+
- ✅ بهبود UI/UX
|
| 60 |
+
|
| 61 |
+
### api_server_extended.py
|
| 62 |
+
- ✅ یکپارچهسازی Log Manager
|
| 63 |
+
- ✅ یکپارچهسازی Resource Manager
|
| 64 |
+
- ✅ Endpointهای جدید برای Logs
|
| 65 |
+
- ✅ Endpointهای جدید برای Resources
|
| 66 |
+
- ✅ بهبود Error Handling
|
| 67 |
+
|
| 68 |
+
## 📈 آمار
|
| 69 |
+
|
| 70 |
+
- **کل منابع**: 200+
|
| 71 |
+
- **دستهبندیها**: 9 دسته مختلف
|
| 72 |
+
- **API Endpoints**: 50+
|
| 73 |
+
- **تبهای داشبورد**: 8 تب
|
| 74 |
+
- **قابلیت Export**: JSON, CSV
|
| 75 |
+
- **قابلیت Import**: JSON
|
| 76 |
+
|
| 77 |
+
## 🐛 رفع مشکلات
|
| 78 |
+
|
| 79 |
+
- ✅ بهبود Error Handling
|
| 80 |
+
- ✅ بهبود Performance
|
| 81 |
+
- ✅ بهبود Memory Management
|
| 82 |
+
- ✅ بهبود Log Rotation
|
| 83 |
+
|
| 84 |
+
## 🔮 ویژگیهای آینده
|
| 85 |
+
|
| 86 |
+
- [ ] Real-time WebSocket برای لاگها
|
| 87 |
+
- [ ] Dashboard Analytics پیشرفته
|
| 88 |
+
- [ ] Alert System (Email, Telegram)
|
| 89 |
+
- [ ] Auto-scaling برای Providers
|
| 90 |
+
- [ ] Machine Learning برای انتخاب بهترین Provider
|
| 91 |
+
|
| 92 |
+
---
|
| 93 |
+
|
| 94 |
+
**نسخه 3.0.0 - 13 نوامبر 2025**
|
| 95 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Dockerfile
CHANGED
|
@@ -1,13 +1,38 @@
|
|
| 1 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
WORKDIR /app
|
| 3 |
|
| 4 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
|
| 6 |
-
|
|
|
|
| 7 |
|
| 8 |
-
|
|
|
|
| 9 |
|
| 10 |
-
|
| 11 |
-
|
|
|
|
| 12 |
|
| 13 |
-
|
|
|
|
|
|
| 1 |
+
# استفاده از Python 3.11 Slim
|
| 2 |
+
FROM python:3.11-slim
|
| 3 |
+
|
| 4 |
+
# تنظیم متغیرهای محیطی
|
| 5 |
+
ENV PYTHONUNBUFFERED=1 \
|
| 6 |
+
PYTHONDONTWRITEBYTECODE=1 \
|
| 7 |
+
PIP_NO_CACHE_DIR=1 \
|
| 8 |
+
PIP_DISABLE_PIP_VERSION_CHECK=1
|
| 9 |
+
|
| 10 |
+
# نصب وابستگیهای سیستمی
|
| 11 |
+
RUN apt-get update && apt-get install -y \
|
| 12 |
+
gcc \
|
| 13 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 14 |
+
|
| 15 |
+
# ساخت دایرکتوری کاری
|
| 16 |
WORKDIR /app
|
| 17 |
|
| 18 |
+
# کپی فایلهای وابستگی
|
| 19 |
+
COPY requirements.txt .
|
| 20 |
+
|
| 21 |
+
# نصب وابستگیهای Python
|
| 22 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 23 |
+
|
| 24 |
+
# کپی کد برنامه
|
| 25 |
+
COPY . .
|
| 26 |
|
| 27 |
+
# ساخت دایرکتوری برای لاگها
|
| 28 |
+
RUN mkdir -p logs
|
| 29 |
|
| 30 |
+
# Expose کردن پورت
|
| 31 |
+
EXPOSE 8000
|
| 32 |
|
| 33 |
+
# Health Check
|
| 34 |
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
| 35 |
+
CMD python -c "import requests; requests.get('http://localhost:8000/health')" || exit 1
|
| 36 |
|
| 37 |
+
# اجرای سرور
|
| 38 |
+
CMD ["python", "-m", "uvicorn", "api_server_extended:app", "--host", "0.0.0.0", "--port", "8000"]
|
PROJECT_STRUCTURE_FA.md
ADDED
|
@@ -0,0 +1,513 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🌳 ساختار پروژه Crypto Monitor - نقشه کامل
|
| 2 |
+
|
| 3 |
+
## 📋 فهرست مطالب
|
| 4 |
+
1. [ساختار کلی پروژه](#ساختار-کلی-پروژه)
|
| 5 |
+
2. [فایلهای اصلی و مسئولیتها](#فایلهای-اصلی-و-مسئولیتها)
|
| 6 |
+
3. [فایلهای پیکربندی](#فایلهای-پیکربندی)
|
| 7 |
+
4. [سرویسها و ماژولها](#سرویسها-و-ماژولها)
|
| 8 |
+
5. [رابط کاربری](#رابط-کاربری)
|
| 9 |
+
6. [نحوه استفاده از فایلهای Config](#نحوه-استفاده-از-فایلهای-config)
|
| 10 |
+
|
| 11 |
+
---
|
| 12 |
+
|
| 13 |
+
## 🌲 ساختار کلی پروژه
|
| 14 |
+
|
| 15 |
+
```
|
| 16 |
+
crypto-monitor-hf-full-fixed-v4-realapis/
|
| 17 |
+
│
|
| 18 |
+
├── 📄 فایلهای اصلی سرور
|
| 19 |
+
│ ├── api_server_extended.py ⭐ سرور اصلی FastAPI (استفاده میشود)
|
| 20 |
+
│ ├── main.py ⚠️ قدیمی - استفاده نمیشود
|
| 21 |
+
│ ├── app.py ⚠️ قدیمی - استفاده نمیشود
|
| 22 |
+
│ ├── enhanced_server.py ⚠️ قدیمی - استفاده نمیشود
|
| 23 |
+
│ ├── production_server.py ⚠️ قدیمی - استفاده نمیشود
|
| 24 |
+
│ ├── real_server.py ⚠️ قدیمی - استفاده نمیشود
|
| 25 |
+
│ └── simple_server.py ⚠️ قدیمی - استفاده نمیشود
|
| 26 |
+
│
|
| 27 |
+
├── 📦 فایلهای پیکربندی (Config Files)
|
| 28 |
+
│ ├── providers_config_extended.json ✅ استفاده میشود (ProviderManager)
|
| 29 |
+
│ ├── providers_config_ultimate.json ✅ استفاده میشود (ResourceManager)
|
| 30 |
+
│ ├── crypto_resources_unified_2025-11-11.json ✅ استفاده میشود (UnifiedConfigLoader)
|
| 31 |
+
│ ├── all_apis_merged_2025.json ✅ استفاده میشود (UnifiedConfigLoader)
|
| 32 |
+
│ └── ultimate_crypto_pipeline_2025_NZasinich.json ✅ استفاده میشود (UnifiedConfigLoader)
|
| 33 |
+
│
|
| 34 |
+
├── 🎨 رابط کاربری (Frontend)
|
| 35 |
+
│ ├── unified_dashboard.html ⭐ داشبورد اصلی (استفاده میشود)
|
| 36 |
+
│ ├── index.html ⚠️ قدیمی
|
| 37 |
+
│ ├── dashboard.html ⚠️ قدیمی
|
| 38 |
+
│ ├── enhanced_dashboard.html ⚠️ قدیمی
|
| 39 |
+
│ ├── admin.html ⚠️ قدیمی
|
| 40 |
+
│ ├── pool_management.html ⚠️ قدیمی
|
| 41 |
+
│ └── hf_console.html ⚠️ قدیمی
|
| 42 |
+
│
|
| 43 |
+
├── 🧩 ماژولهای اصلی (Core Modules)
|
| 44 |
+
│ ├── provider_manager.py ✅ مدیریت Providerها و Poolها
|
| 45 |
+
│ ├── resource_manager.py ✅ مدیریت منابع API
|
| 46 |
+
│ ├── log_manager.py ✅ مدیریت لاگها
|
| 47 |
+
│ ├── config.py ⚠️ قدیمی - استفاده نمیشود
|
| 48 |
+
│ └── scheduler.py ⚠️ قدیمی - استفاده نمیشود
|
| 49 |
+
│
|
| 50 |
+
├── 🔧 سرویسهای بکند (Backend Services)
|
| 51 |
+
│ └── backend/
|
| 52 |
+
│ ├── services/
|
| 53 |
+
│ │ ├── auto_discovery_service.py ✅ جستجوی خودکار منابع رایگان
|
| 54 |
+
│ │ ├── connection_manager.py ✅ مدیریت اتصالات WebSocket
|
| 55 |
+
│ │ ├── diagnostics_service.py ✅ اشکالیابی و تعمیر خودکار
|
| 56 |
+
│ │ ├── unified_config_loader.py ✅ بارگذاری یکپارچه Configها
|
| 57 |
+
│ │ ├── scheduler_service.py ✅ زمانبندی پیشرفته
|
| 58 |
+
│ │ ├── persistence_service.py ✅ ذخیرهسازی دادهها
|
| 59 |
+
│ │ ├── websocket_service.py ✅ سرویس WebSocket
|
| 60 |
+
│ │ ├── ws_service_manager.py ✅ مدیریت سرویسهای WebSocket
|
| 61 |
+
│ │ ├── hf_client.py ✅ کلاینت HuggingFace
|
| 62 |
+
│ │ ├── hf_registry.py ✅ رجیستری مدلهای HuggingFace
|
| 63 |
+
│ │ └── __init__.py
|
| 64 |
+
│ │
|
| 65 |
+
│ └── routers/
|
| 66 |
+
│ ├── integrated_api.py ✅ APIهای یکپارچه
|
| 67 |
+
│ ├── hf_connect.py ✅ اتصال HuggingFace
|
| 68 |
+
│ └── __init__.py
|
| 69 |
+
│
|
| 70 |
+
├── 📡 API Endpoints
|
| 71 |
+
│ └── api/
|
| 72 |
+
│ ├── endpoints.py ⚠️ قدیمی
|
| 73 |
+
│ ├── pool_endpoints.py ⚠️ قدیمی
|
| 74 |
+
│ ├── websocket.py ⚠️ قدیمی
|
| 75 |
+
│ └── ... (سایر فایلهای قدیمی)
|
| 76 |
+
│
|
| 77 |
+
├── 🎯 Collectors (جمعآور�� داده)
|
| 78 |
+
│ └── collectors/
|
| 79 |
+
│ ├── market_data.py ⚠️ قدیمی
|
| 80 |
+
│ ├── market_data_extended.py ⚠️ قدیمی
|
| 81 |
+
│ ├── news.py ⚠️ قدیمی
|
| 82 |
+
│ ├── sentiment.py ⚠️ قدیمی
|
| 83 |
+
│ └── ... (سایر collectors قدیمی)
|
| 84 |
+
│
|
| 85 |
+
├── 🎨 فایلهای استاتیک (Static Files)
|
| 86 |
+
│ └── static/
|
| 87 |
+
│ ├── css/
|
| 88 |
+
│ │ └── connection-status.css ✅ استایل وضعیت اتصال
|
| 89 |
+
│ └── js/
|
| 90 |
+
│ └── websocket-client.js ✅ کلاینت WebSocket
|
| 91 |
+
│
|
| 92 |
+
├── 📚 مستندات (Documentation)
|
| 93 |
+
│ ├── README.md ✅ مستندات اصلی
|
| 94 |
+
│ ├── README_FA.md ✅ مستندات فارسی
|
| 95 |
+
│ ├── WEBSOCKET_GUIDE.md ✅ راهنمای WebSocket
|
| 96 |
+
│ ├── REALTIME_FEATURES_FA.md ✅ ویژگیهای بلادرنگ
|
| 97 |
+
│ └── ... (سایر فایلهای مستندات)
|
| 98 |
+
│
|
| 99 |
+
├── 🧪 تستها (Tests)
|
| 100 |
+
│ ├── test_websocket.html ✅ صفحه تست WebSocket
|
| 101 |
+
│ ├── test_websocket_dashboard.html ✅ صفحه تست Dashboard
|
| 102 |
+
│ ├── test_providers.py ⚠️ تست قدیمی
|
| 103 |
+
│ └── tests/ ⚠️ تستهای قدیمی
|
| 104 |
+
│
|
| 105 |
+
├── 📁 دایرکتوریهای داده
|
| 106 |
+
│ ├── data/ ✅ ذخیره دادهها
|
| 107 |
+
│ ├── logs/ ✅ ذخیره لاگها
|
| 108 |
+
│ └── database/ ⚠️ قدیمی
|
| 109 |
+
│
|
| 110 |
+
└── 📦 سایر فایلها
|
| 111 |
+
├── requirements.txt ✅ وابستگیهای Python
|
| 112 |
+
├── start.bat ✅ اسکریپت راهاندازی
|
| 113 |
+
├── docker-compose.yml ✅ Docker Compose
|
| 114 |
+
└── Dockerfile ✅ Dockerfile
|
| 115 |
+
```
|
| 116 |
+
|
| 117 |
+
---
|
| 118 |
+
|
| 119 |
+
## 📄 فایلهای اصلی و مسئولیتها
|
| 120 |
+
|
| 121 |
+
### ⭐ فایلهای فعال (در حال استفاده)
|
| 122 |
+
|
| 123 |
+
#### 1. `api_server_extended.py` - سرور اصلی
|
| 124 |
+
**مسئولیت:**
|
| 125 |
+
- سرور FastAPI اصلی برنامه
|
| 126 |
+
- مدیریت تمام endpointها
|
| 127 |
+
- یکپارچهسازی تمام سرویسها
|
| 128 |
+
- مدیریت WebSocket
|
| 129 |
+
- Startup validation
|
| 130 |
+
|
| 131 |
+
**وابستگیها:**
|
| 132 |
+
- `provider_manager.py` → `providers_config_extended.json`
|
| 133 |
+
- `resource_manager.py` → `providers_config_ultimate.json`
|
| 134 |
+
- `backend/services/auto_discovery_service.py`
|
| 135 |
+
- `backend/services/connection_manager.py`
|
| 136 |
+
- `backend/services/diagnostics_service.py`
|
| 137 |
+
|
| 138 |
+
**نحوه اجرا:**
|
| 139 |
+
```bash
|
| 140 |
+
python api_server_extended.py
|
| 141 |
+
# یا
|
| 142 |
+
uvicorn api_server_extended:app --host 0.0.0.0 --port 8000
|
| 143 |
+
```
|
| 144 |
+
|
| 145 |
+
---
|
| 146 |
+
|
| 147 |
+
#### 2. `provider_manager.py` - مدیریت Providerها
|
| 148 |
+
**مسئولیت:**
|
| 149 |
+
- مدیریت Providerهای API
|
| 150 |
+
- مدیریت Poolها و استراتژیهای چرخش
|
| 151 |
+
- Health check
|
| 152 |
+
- Rate limiting
|
| 153 |
+
- Circuit breaker
|
| 154 |
+
|
| 155 |
+
**فایل Config استفاده شده:**
|
| 156 |
+
- `providers_config_extended.json` (پیشفرض)
|
| 157 |
+
|
| 158 |
+
**ساختار فایل Config:**
|
| 159 |
+
```json
|
| 160 |
+
{
|
| 161 |
+
"providers": {
|
| 162 |
+
"coingecko": { ... },
|
| 163 |
+
"binance": { ... }
|
| 164 |
+
},
|
| 165 |
+
"pool_configurations": [ ... ]
|
| 166 |
+
}
|
| 167 |
+
```
|
| 168 |
+
|
| 169 |
+
---
|
| 170 |
+
|
| 171 |
+
#### 3. `resource_manager.py` - مدیریت منابع
|
| 172 |
+
**مسئولیت:**
|
| 173 |
+
- مدیریت منابع API
|
| 174 |
+
- Import/Export منابع
|
| 175 |
+
- Validation منابع
|
| 176 |
+
- Backup/Restore
|
| 177 |
+
|
| 178 |
+
**فایل Config استفاده شده:**
|
| 179 |
+
- `providers_config_ultimate.json` (پیشفرض)
|
| 180 |
+
|
| 181 |
+
**ساختار فایل Config:**
|
| 182 |
+
```json
|
| 183 |
+
{
|
| 184 |
+
"providers": {
|
| 185 |
+
"coingecko": { ... }
|
| 186 |
+
},
|
| 187 |
+
"schema_version": "3.0.0"
|
| 188 |
+
}
|
| 189 |
+
```
|
| 190 |
+
|
| 191 |
+
---
|
| 192 |
+
|
| 193 |
+
#### 4. `unified_dashboard.html` - داشبورد اصلی
|
| 194 |
+
**مسئولیت:**
|
| 195 |
+
- رابط کاربری اصلی
|
| 196 |
+
- نمایش دادههای بازار
|
| 197 |
+
- مدیریت Providerها
|
| 198 |
+
- گزارشات و اشکالیابی
|
| 199 |
+
- اتصال WebSocket
|
| 200 |
+
|
| 201 |
+
**وابستگیها:**
|
| 202 |
+
- `static/css/connection-status.css`
|
| 203 |
+
- `static/js/websocket-client.js`
|
| 204 |
+
- API endpoints از `api_server_extended.py`
|
| 205 |
+
|
| 206 |
+
---
|
| 207 |
+
|
| 208 |
+
### ⚠️ فایلهای قدیمی (استفاده نمیشوند)
|
| 209 |
+
|
| 210 |
+
این فایلها برای مرجع نگه داشته شدهاند اما در حال حاضر استفاده نمیشوند:
|
| 211 |
+
|
| 212 |
+
- `main.py`, `app.py`, `enhanced_server.py` → جایگزین شده با `api_server_extended.py`
|
| 213 |
+
- `index.html`, `dashboard.html` → جایگزین شده با `unified_dashboard.html`
|
| 214 |
+
- `config.py`, `scheduler.py` → جایگزین شده با سرویسهای جدید در `backend/services/`
|
| 215 |
+
|
| 216 |
+
---
|
| 217 |
+
|
| 218 |
+
## 📦 فایلهای پیکربندی
|
| 219 |
+
|
| 220 |
+
### ✅ فایلهای فعال
|
| 221 |
+
|
| 222 |
+
#### 1. `providers_config_extended.json`
|
| 223 |
+
**استفاده شده توسط:** `provider_manager.py`
|
| 224 |
+
**محتوای اصلی:**
|
| 225 |
+
- لیست Providerها با endpointها
|
| 226 |
+
- Pool configurations
|
| 227 |
+
- HuggingFace models
|
| 228 |
+
- Fallback strategy
|
| 229 |
+
|
| 230 |
+
**نحوه استفاده:**
|
| 231 |
+
```python
|
| 232 |
+
from provider_manager import ProviderManager
|
| 233 |
+
|
| 234 |
+
manager = ProviderManager(config_path="providers_config_extended.json")
|
| 235 |
+
```
|
| 236 |
+
|
| 237 |
+
---
|
| 238 |
+
|
| 239 |
+
#### 2. `providers_config_ultimate.json`
|
| 240 |
+
**استفاده شده توسط:** `resource_manager.py`
|
| 241 |
+
**محتوای اصلی:**
|
| 242 |
+
- لیست Providerها (فرمت متفاوت)
|
| 243 |
+
- Schema version
|
| 244 |
+
- Metadata
|
| 245 |
+
|
| 246 |
+
**نحوه استفاده:**
|
| 247 |
+
```python
|
| 248 |
+
from resource_manager import ResourceManager
|
| 249 |
+
|
| 250 |
+
manager = ResourceManager(config_file="providers_config_ultimate.json")
|
| 251 |
+
```
|
| 252 |
+
|
| 253 |
+
---
|
| 254 |
+
|
| 255 |
+
#### 3. `crypto_resources_unified_2025-11-11.json`
|
| 256 |
+
**استفاده شده توسط:** `backend/services/unified_config_loader.py`
|
| 257 |
+
**محتوای اصلی:**
|
| 258 |
+
- RPC nodes
|
| 259 |
+
- Block explorers
|
| 260 |
+
- Market data APIs
|
| 261 |
+
- DeFi protocols
|
| 262 |
+
|
| 263 |
+
**نحوه استفاده:**
|
| 264 |
+
```python
|
| 265 |
+
from backend.services.unified_config_loader import UnifiedConfigLoader
|
| 266 |
+
|
| 267 |
+
loader = UnifiedConfigLoader()
|
| 268 |
+
# به صورت خودکار این فایل را load میکند
|
| 269 |
+
```
|
| 270 |
+
|
| 271 |
+
---
|
| 272 |
+
|
| 273 |
+
#### 4. `all_apis_merged_2025.json`
|
| 274 |
+
**استفاده شده توسط:** `backend/services/unified_config_loader.py`
|
| 275 |
+
**محتوای اصلی:**
|
| 276 |
+
- APIs merged از منابع مختلف
|
| 277 |
+
|
| 278 |
+
---
|
| 279 |
+
|
| 280 |
+
#### 5. `ultimate_crypto_pipeline_2025_NZasinich.json`
|
| 281 |
+
**استفاده شده توسط:** `backend/services/unified_config_loader.py`
|
| 282 |
+
**محتوای اصلی:**
|
| 283 |
+
- Pipeline configuration
|
| 284 |
+
- API sources
|
| 285 |
+
|
| 286 |
+
---
|
| 287 |
+
|
| 288 |
+
### 🔄 تفاوت بین فایلهای Config
|
| 289 |
+
|
| 290 |
+
| فایل | استفاده شده توسط | فرمت | تعداد Provider |
|
| 291 |
+
|------|------------------|------|----------------|
|
| 292 |
+
| `providers_config_extended.json` | ProviderManager | `{providers: {}, pool_configurations: []}` | ~100 |
|
| 293 |
+
| `providers_config_ultimate.json` | ResourceManager | `{providers: {}, schema_version: "3.0.0"}` | ~200 |
|
| 294 |
+
| `crypto_resources_unified_2025-11-11.json` | UnifiedConfigLoader | `{registry: {rpc_nodes: [], ...}}` | 200+ |
|
| 295 |
+
| `all_apis_merged_2025.json` | UnifiedConfigLoader | Merged format | متغیر |
|
| 296 |
+
| `ultimate_crypto_pipeline_2025_NZasinich.json` | UnifiedConfigLoader | Pipeline format | متغیر |
|
| 297 |
+
|
| 298 |
+
---
|
| 299 |
+
|
| 300 |
+
## 🔧 سرویسها و ماژولها
|
| 301 |
+
|
| 302 |
+
### Backend Services (`backend/services/`)
|
| 303 |
+
|
| 304 |
+
#### 1. `auto_discovery_service.py`
|
| 305 |
+
**مسئولیت:**
|
| 306 |
+
- جستجوی خودکار منابع API رایگان
|
| 307 |
+
- استفاده از DuckDuckGo برای جستجو
|
| 308 |
+
- استفاده از HuggingFace برای تحلیل
|
| 309 |
+
- اضافه کردن منابع جدید به ResourceManager
|
| 310 |
+
|
| 311 |
+
**API Endpoints:**
|
| 312 |
+
- `GET /api/resources/discovery/status`
|
| 313 |
+
- `POST /api/resources/discovery/run`
|
| 314 |
+
|
| 315 |
+
---
|
| 316 |
+
|
| 317 |
+
#### 2. `connection_manager.py`
|
| 318 |
+
**مسئولیت:**
|
| 319 |
+
- مدیریت اتصالات WebSocket
|
| 320 |
+
- Tracking sessions
|
| 321 |
+
- Broadcasting messages
|
| 322 |
+
- Heartbeat management
|
| 323 |
+
|
| 324 |
+
**API Endpoints:**
|
| 325 |
+
- `GET /api/sessions`
|
| 326 |
+
- `GET /api/sessions/stats`
|
| 327 |
+
- `POST /api/broadcast`
|
| 328 |
+
- `WebSocket /ws`
|
| 329 |
+
|
| 330 |
+
---
|
| 331 |
+
|
| 332 |
+
#### 3. `diagnostics_service.py`
|
| 333 |
+
**مسئولیت:**
|
| 334 |
+
- اشکالیابی خودکار سیستم
|
| 335 |
+
- بررسی وابستگیها
|
| 336 |
+
- بررسی تنظیمات
|
| 337 |
+
- بررسی شبکه
|
| 338 |
+
- تعمیر خودکار مشکلات
|
| 339 |
+
|
| 340 |
+
**API Endpoints:**
|
| 341 |
+
- `POST /api/diagnostics/run?auto_fix=true/false`
|
| 342 |
+
- `GET /api/diagnostics/last`
|
| 343 |
+
|
| 344 |
+
---
|
| 345 |
+
|
| 346 |
+
#### 4. `unified_config_loader.py`
|
| 347 |
+
**مسئولیت:**
|
| 348 |
+
- بارگذاری یکپارچه تمام فایلهای Config
|
| 349 |
+
- Merge کردن منابع از فایلهای مختلف
|
| 350 |
+
- مدیریت API keys
|
| 351 |
+
- Setup CORS proxies
|
| 352 |
+
|
| 353 |
+
**فایلهای Load شده:**
|
| 354 |
+
- `crypto_resources_unified_2025-11-11.json`
|
| 355 |
+
- `all_apis_merged_2025.json`
|
| 356 |
+
- `ultimate_crypto_pipeline_2025_NZasinich.json`
|
| 357 |
+
|
| 358 |
+
---
|
| 359 |
+
|
| 360 |
+
## 🎨 رابط کاربری
|
| 361 |
+
|
| 362 |
+
### `unified_dashboard.html` - داشبورد اصلی
|
| 363 |
+
|
| 364 |
+
**تبها:**
|
| 365 |
+
1. **Market** - دادههای بازار
|
| 366 |
+
2. **API Monitor** - مانیتورینگ Providerها
|
| 367 |
+
3. **Advanced** - عملیات پیشرفته
|
| 368 |
+
4. **Admin** - مدیریت
|
| 369 |
+
5. **HuggingFace** - مدلهای HuggingFace
|
| 370 |
+
6. **Pools** - مدیریت Poolها
|
| 371 |
+
7. **Logs** - مدیریت لاگها
|
| 372 |
+
8. **Resources** - مدیریت منابع
|
| 373 |
+
9. **Reports** - گزارشات و اشکالیابی
|
| 374 |
+
|
| 375 |
+
**ویژگیها:**
|
| 376 |
+
- اتصال WebSocket برای دادههای بلادرنگ
|
| 377 |
+
- نمایش تعداد کاربران آنلاین
|
| 378 |
+
- گزارشات Auto-Discovery
|
| 379 |
+
- گزارشات مدلهای HuggingFace
|
| 380 |
+
- اشکالیابی خودکار
|
| 381 |
+
|
| 382 |
+
---
|
| 383 |
+
|
| 384 |
+
## 🔄 نحوه استفاده از فایلهای Config
|
| 385 |
+
|
| 386 |
+
### سناریو 1: استفاده از ProviderManager
|
| 387 |
+
```python
|
| 388 |
+
from provider_manager import ProviderManager
|
| 389 |
+
|
| 390 |
+
# استفاده از providers_config_extended.json
|
| 391 |
+
manager = ProviderManager(config_path="providers_config_extended.json")
|
| 392 |
+
|
| 393 |
+
# دریافت Provider
|
| 394 |
+
provider = manager.get_provider("coingecko")
|
| 395 |
+
|
| 396 |
+
# استفاده از Pool
|
| 397 |
+
pool = manager.get_pool("primary_market_data_pool")
|
| 398 |
+
result = await pool.get_data("coins_markets")
|
| 399 |
+
```
|
| 400 |
+
|
| 401 |
+
---
|
| 402 |
+
|
| 403 |
+
### سناریو 2: استفاده از ResourceManager
|
| 404 |
+
```python
|
| 405 |
+
from resource_manager import ResourceManager
|
| 406 |
+
|
| 407 |
+
# استفاده از providers_config_ultimate.json
|
| 408 |
+
manager = ResourceManager(config_file="providers_config_ultimate.json")
|
| 409 |
+
|
| 410 |
+
# اضافه کردن Provider جدید
|
| 411 |
+
manager.add_provider({
|
| 412 |
+
"id": "new_api",
|
| 413 |
+
"name": "New API",
|
| 414 |
+
"category": "market_data",
|
| 415 |
+
"base_url": "https://api.example.com",
|
| 416 |
+
"requires_auth": False
|
| 417 |
+
})
|
| 418 |
+
|
| 419 |
+
# ذخیره
|
| 420 |
+
manager.save_resources()
|
| 421 |
+
```
|
| 422 |
+
|
| 423 |
+
---
|
| 424 |
+
|
| 425 |
+
### سناریو 3: استفاده از UnifiedConfigLoader
|
| 426 |
+
```python
|
| 427 |
+
from backend.services.unified_config_loader import UnifiedConfigLoader
|
| 428 |
+
|
| 429 |
+
# به صورت خودکار تمام فایلها را load میکند
|
| 430 |
+
loader = UnifiedConfigLoader()
|
| 431 |
+
|
| 432 |
+
# دریافت تمام APIs
|
| 433 |
+
all_apis = loader.get_all_apis()
|
| 434 |
+
|
| 435 |
+
# دریافت APIs بر اساس category
|
| 436 |
+
market_apis = loader.get_apis_by_category('market_data')
|
| 437 |
+
```
|
| 438 |
+
|
| 439 |
+
---
|
| 440 |
+
|
| 441 |
+
## 📊 جریان داده (Data Flow)
|
| 442 |
+
|
| 443 |
+
```
|
| 444 |
+
1. Startup
|
| 445 |
+
└── api_server_extended.py
|
| 446 |
+
├── ProviderManager.load_config()
|
| 447 |
+
│ └── providers_config_extended.json
|
| 448 |
+
├── ResourceManager.load_resources()
|
| 449 |
+
│ └── providers_config_ultimate.json
|
| 450 |
+
└── UnifiedConfigLoader.load_all_configs()
|
| 451 |
+
├── crypto_resources_unified_2025-11-11.json
|
| 452 |
+
├── all_apis_merged_2025.json
|
| 453 |
+
└── ultimate_crypto_pipeline_2025_NZasinich.json
|
| 454 |
+
|
| 455 |
+
2. Runtime
|
| 456 |
+
└── API Request
|
| 457 |
+
├── ProviderManager.get_provider()
|
| 458 |
+
├── ProviderPool.get_data()
|
| 459 |
+
└── Response
|
| 460 |
+
|
| 461 |
+
3. WebSocket
|
| 462 |
+
└── ConnectionManager
|
| 463 |
+
├── Connect client
|
| 464 |
+
├── Broadcast updates
|
| 465 |
+
└── Heartbeat
|
| 466 |
+
|
| 467 |
+
4. Auto-Discovery
|
| 468 |
+
└── AutoDiscoveryService
|
| 469 |
+
├── Search (DuckDuckGo)
|
| 470 |
+
├── Analyze (HuggingFace)
|
| 471 |
+
└── Add to ResourceManager
|
| 472 |
+
```
|
| 473 |
+
|
| 474 |
+
---
|
| 475 |
+
|
| 476 |
+
## 🎯 توصیهها
|
| 477 |
+
|
| 478 |
+
### ✅ فایلهای پیشنهادی برای استفاده
|
| 479 |
+
|
| 480 |
+
1. **برای مدیریت Providerها:**
|
| 481 |
+
- استفاده از `provider_manager.py` با `providers_config_extended.json`
|
| 482 |
+
|
| 483 |
+
2. **برای مدیریت منابع:**
|
| 484 |
+
- استفاده از `resource_manager.py` با `providers_config_ultimate.json`
|
| 485 |
+
|
| 486 |
+
3. **برای بارگذاری یکپارچه:**
|
| 487 |
+
- استفاده از `UnifiedConfigLoader` که تمام فایلها را merge میکند
|
| 488 |
+
|
| 489 |
+
### ⚠️ فایلهای قدیمی
|
| 490 |
+
|
| 491 |
+
- فایلهای قدیمی را میتوانید نگه دارید برای مرجع
|
| 492 |
+
- اما برای توسعه جدید از فایلهای جدید استفاده کنید
|
| 493 |
+
|
| 494 |
+
---
|
| 495 |
+
|
| 496 |
+
## 📝 خلاصه
|
| 497 |
+
|
| 498 |
+
| کامپوننت | فایل اصلی | فایل Config | وضعیت |
|
| 499 |
+
|----------|-----------|-------------|-------|
|
| 500 |
+
| سرور | `api_server_extended.py` | - | ✅ فعال |
|
| 501 |
+
| مدیریت Provider | `provider_manager.py` | `providers_config_extended.json` | ✅ فعال |
|
| 502 |
+
| مدیریت منابع | `resource_manager.py` | `providers_config_ultimate.json` | ✅ فعال |
|
| 503 |
+
| بارگذاری یکپارچه | `unified_config_loader.py` | `crypto_resources_unified_2025-11-11.json` + 2 فایل دیگر | ✅ فعال |
|
| 504 |
+
| داشبورد | `unified_dashboard.html` | - | ✅ فعال |
|
| 505 |
+
| Auto-Discovery | `auto_discovery_service.py` | - | ✅ فعال |
|
| 506 |
+
| WebSocket | `connection_manager.py` | - | ✅ فعال |
|
| 507 |
+
| Diagnostics | `diagnostics_service.py` | - | ✅ فعال |
|
| 508 |
+
|
| 509 |
+
---
|
| 510 |
+
|
| 511 |
+
**آخرین بهروزرسانی:** 2025-01-XX
|
| 512 |
+
**نسخه:** 4.0
|
| 513 |
+
|
QUICK_REFERENCE_FA.md
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ⚡ مرجع سریع - فایلهای فعال
|
| 2 |
+
|
| 3 |
+
## 🎯 فایلهای اصلی (فقط اینها استفاده میشوند!)
|
| 4 |
+
|
| 5 |
+
### 📄 سرور
|
| 6 |
+
```
|
| 7 |
+
✅ api_server_extended.py ← سرور اصلی (این را اجرا کنید!)
|
| 8 |
+
```
|
| 9 |
+
|
| 10 |
+
### 📦 Config Files
|
| 11 |
+
```
|
| 12 |
+
✅ providers_config_extended.json ← ProviderManager استفاده میکند
|
| 13 |
+
✅ providers_config_ultimate.json ← ResourceManager استفاده میکند
|
| 14 |
+
✅ crypto_resources_unified_2025-11-11.json ← UnifiedConfigLoader استفاده میکند
|
| 15 |
+
```
|
| 16 |
+
|
| 17 |
+
### 🎨 Frontend
|
| 18 |
+
```
|
| 19 |
+
✅ unified_dashboard.html ← داشبورد اصلی
|
| 20 |
+
✅ static/css/connection-status.css
|
| 21 |
+
✅ static/js/websocket-client.js
|
| 22 |
+
```
|
| 23 |
+
|
| 24 |
+
### 🔧 Core Modules
|
| 25 |
+
```
|
| 26 |
+
✅ provider_manager.py ← مدیریت Providerها
|
| 27 |
+
✅ resource_manager.py ← مدیریت منابع
|
| 28 |
+
✅ log_manager.py ← مدیریت لاگها
|
| 29 |
+
```
|
| 30 |
+
|
| 31 |
+
### 🛠️ Backend Services
|
| 32 |
+
```
|
| 33 |
+
✅ backend/services/auto_discovery_service.py
|
| 34 |
+
✅ backend/services/connection_manager.py
|
| 35 |
+
✅ backend/services/diagnostics_service.py
|
| 36 |
+
✅ backend/services/unified_config_loader.py
|
| 37 |
+
```
|
| 38 |
+
|
| 39 |
+
---
|
| 40 |
+
|
| 41 |
+
## ❌ فایلهای قدیمی (استفاده نمیشوند)
|
| 42 |
+
|
| 43 |
+
```
|
| 44 |
+
❌ main.py
|
| 45 |
+
❌ app.py
|
| 46 |
+
❌ enhanced_server.py
|
| 47 |
+
❌ production_server.py
|
| 48 |
+
❌ real_server.py
|
| 49 |
+
❌ simple_server.py
|
| 50 |
+
|
| 51 |
+
❌ index.html
|
| 52 |
+
❌ dashboard.html
|
| 53 |
+
❌ enhanced_dashboard.html
|
| 54 |
+
❌ admin.html
|
| 55 |
+
|
| 56 |
+
❌ config.py
|
| 57 |
+
❌ scheduler.py
|
| 58 |
+
```
|
| 59 |
+
|
| 60 |
+
---
|
| 61 |
+
|
| 62 |
+
## 🚀 راهاندازی سریع
|
| 63 |
+
|
| 64 |
+
```bash
|
| 65 |
+
# 1. نصب وابستگیها
|
| 66 |
+
pip install -r requirements.txt
|
| 67 |
+
|
| 68 |
+
# 2. اجرای سرور
|
| 69 |
+
python api_server_extended.py
|
| 70 |
+
|
| 71 |
+
# 3. باز کردن مرورگر
|
| 72 |
+
http://localhost:8000/unified_dashboard.html
|
| 73 |
+
```
|
| 74 |
+
|
| 75 |
+
---
|
| 76 |
+
|
| 77 |
+
## 📊 ساختار ساده
|
| 78 |
+
|
| 79 |
+
```
|
| 80 |
+
api_server_extended.py (سرور اصلی)
|
| 81 |
+
│
|
| 82 |
+
├── ProviderManager → providers_config_extended.json
|
| 83 |
+
├── ResourceManager → providers_config_ultimate.json
|
| 84 |
+
├── UnifiedConfigLoader → crypto_resources_unified_2025-11-11.json
|
| 85 |
+
├── AutoDiscoveryService
|
| 86 |
+
├── ConnectionManager (WebSocket)
|
| 87 |
+
└── DiagnosticsService
|
| 88 |
+
|
| 89 |
+
unified_dashboard.html (داشبورد)
|
| 90 |
+
│
|
| 91 |
+
├── static/css/connection-status.css
|
| 92 |
+
└── static/js/websocket-client.js
|
| 93 |
+
```
|
| 94 |
+
|
| 95 |
+
---
|
| 96 |
+
|
| 97 |
+
## 🔍 کدام فایل Config برای چه کاری؟
|
| 98 |
+
|
| 99 |
+
| کار | استفاده از |
|
| 100 |
+
|-----|------------|
|
| 101 |
+
| مدیریت Providerها و Poolها | `providers_config_extended.json` |
|
| 102 |
+
| مدیریت منابع API | `providers_config_ultimate.json` |
|
| 103 |
+
| بارگذاری یکپارچه همه منابع | `crypto_resources_unified_2025-11-11.json` |
|
| 104 |
+
|
| 105 |
+
---
|
| 106 |
+
|
| 107 |
+
**💡 نکته:** اگر میخواهید Provider جدید اضافه کنید:
|
| 108 |
+
- برای ProviderManager → `providers_config_extended.json` را ویرایش کنید
|
| 109 |
+
- برای ResourceManager → `providers_config_ultimate.json` را ویرایش کنید
|
| 110 |
+
- یا از API endpoints استفاده کنید: `/api/resources` یا `/api/pools`
|
| 111 |
+
|
QUICK_START.md
CHANGED
|
@@ -1,182 +1,221 @@
|
|
| 1 |
-
# 🚀
|
| 2 |
-
|
| 3 |
-
##
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
### 1. Main Dashboard (Full Features)
|
| 10 |
-
**URL:** http://localhost:7860/index.html
|
| 11 |
-
|
| 12 |
-
Features:
|
| 13 |
-
- Real-time API monitoring
|
| 14 |
-
- Provider inventory
|
| 15 |
-
- Rate limit tracking
|
| 16 |
-
- Connection logs
|
| 17 |
-
- Schedule management
|
| 18 |
-
- Data freshness monitoring
|
| 19 |
-
- Failure analysis
|
| 20 |
-
- **🤗 HuggingFace Tab** (NEW!)
|
| 21 |
-
|
| 22 |
-
### 2. HuggingFace Console (Standalone)
|
| 23 |
-
**URL:** http://localhost:7860/hf_console.html
|
| 24 |
-
|
| 25 |
-
Features:
|
| 26 |
-
- HF Health Status
|
| 27 |
-
- Models Registry Browser
|
| 28 |
-
- Datasets Registry Browser
|
| 29 |
-
- Local Search (snapshot)
|
| 30 |
-
- Sentiment Analysis (local pipeline)
|
| 31 |
-
|
| 32 |
-
### 3. API Documentation
|
| 33 |
-
**URL:** http://localhost:7860/docs
|
| 34 |
-
|
| 35 |
-
Interactive API documentation with all endpoints
|
| 36 |
-
|
| 37 |
-
## 🤗 HuggingFace Features
|
| 38 |
-
|
| 39 |
-
### Available Endpoints:
|
| 40 |
-
|
| 41 |
-
1. **Health Check**
|
| 42 |
-
```
|
| 43 |
-
GET /api/hf/health
|
| 44 |
-
```
|
| 45 |
-
Returns: Registry health, last refresh time, model/dataset counts
|
| 46 |
-
|
| 47 |
-
2. **Force Refresh Registry**
|
| 48 |
-
```
|
| 49 |
-
POST /api/hf/refresh
|
| 50 |
-
```
|
| 51 |
-
Manually trigger registry update from HuggingFace Hub
|
| 52 |
-
|
| 53 |
-
3. **Get Models Registry**
|
| 54 |
-
```
|
| 55 |
-
GET /api/hf/registry?kind=models
|
| 56 |
-
```
|
| 57 |
-
Returns: List of all cached crypto-related models
|
| 58 |
-
|
| 59 |
-
4. **Get Datasets Registry**
|
| 60 |
-
```
|
| 61 |
-
GET /api/hf/registry?kind=datasets
|
| 62 |
-
```
|
| 63 |
-
Returns: List of all cached crypto-related datasets
|
| 64 |
-
|
| 65 |
-
5. **Search Registry**
|
| 66 |
-
```
|
| 67 |
-
GET /api/hf/search?q=crypto&kind=models
|
| 68 |
-
```
|
| 69 |
-
Search local snapshot for models or datasets
|
| 70 |
-
|
| 71 |
-
6. **Run Sentiment Analysis**
|
| 72 |
-
```
|
| 73 |
-
POST /api/hf/run-sentiment
|
| 74 |
-
Body: {"texts": ["BTC strong", "ETH weak"]}
|
| 75 |
-
```
|
| 76 |
-
Analyze crypto sentiment using local transformers
|
| 77 |
-
|
| 78 |
-
## 🎯 How to Use
|
| 79 |
-
|
| 80 |
-
### Option 1: Main Dashboard
|
| 81 |
-
1. Open http://localhost:7860/index.html in your browser
|
| 82 |
-
2. Click on the **"🤗 HuggingFace"** tab at the top
|
| 83 |
-
3. Explore:
|
| 84 |
-
- Health status
|
| 85 |
-
- Models and datasets registries
|
| 86 |
-
- Search functionality
|
| 87 |
-
- Sentiment analysis
|
| 88 |
-
|
| 89 |
-
### Option 2: Standalone HF Console
|
| 90 |
-
1. Open http://localhost:7860/hf_console.html
|
| 91 |
-
2. All HF features in a clean, focused interface
|
| 92 |
-
3. Perfect for testing and development
|
| 93 |
-
|
| 94 |
-
## 🧪 Test the Integration
|
| 95 |
-
|
| 96 |
-
### Test 1: Check Health
|
| 97 |
-
```powershell
|
| 98 |
-
Invoke-WebRequest -Uri "http://localhost:7860/api/hf/health" -UseBasicParsing | Select-Object -ExpandProperty Content
|
| 99 |
```
|
| 100 |
|
| 101 |
-
###
|
| 102 |
-
```
|
| 103 |
-
|
| 104 |
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
|
|
|
|
|
|
| 109 |
```
|
| 110 |
|
| 111 |
-
###
|
| 112 |
-
```
|
| 113 |
-
|
| 114 |
-
Invoke-WebRequest -Uri "http://localhost:7860/api/hf/run-sentiment" -Method POST -Body $body -ContentType "application/json" -UseBasicParsing | Select-Object -ExpandProperty Content
|
| 115 |
```
|
| 116 |
|
| 117 |
-
##
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
|
| 119 |
-
|
| 120 |
-
-
|
| 121 |
-
- kk08/CryptoBERT
|
| 122 |
|
| 123 |
-
|
| 124 |
-
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
- WinkingFace/CryptoLM-Ripple-XRP-USDT
|
| 129 |
|
| 130 |
-
###
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
- Auto-refreshes every 6 hours (configurable)
|
| 134 |
|
| 135 |
-
|
| 136 |
|
| 137 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
|
| 143 |
-
|
| 144 |
-
|
|
|
|
| 145 |
|
| 146 |
-
#
|
| 147 |
-
|
| 148 |
-
|
| 149 |
|
| 150 |
-
#
|
| 151 |
-
|
|
|
|
|
|
|
| 152 |
|
| 153 |
-
|
| 154 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
```
|
| 156 |
|
| 157 |
-
##
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
|
| 159 |
-
|
|
|
|
| 160 |
|
| 161 |
-
|
|
|
|
|
|
|
| 162 |
|
| 163 |
-
##
|
| 164 |
|
| 165 |
-
|
| 166 |
-
|
|
|
|
|
|
|
| 167 |
```
|
| 168 |
|
| 169 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 170 |
|
| 171 |
-
|
| 172 |
-
- **Registry**: Auto-refreshes every 6 hours, or manually via the UI
|
| 173 |
-
- **Free Resources**: All endpoints use free HuggingFace APIs
|
| 174 |
-
- **No API Key Required**: Works without authentication (with rate limits)
|
| 175 |
-
- **Local Inference**: Sentiment analysis runs locally using transformers
|
| 176 |
|
| 177 |
-
|
|
|
|
|
|
|
|
|
|
| 178 |
|
| 179 |
-
|
| 180 |
|
| 181 |
-
|
| 182 |
-
**HF Console:** http://localhost:7860/hf_console.html
|
|
|
|
| 1 |
+
# 🚀 راهنمای سریع شروع - Quick Start Guide
|
| 2 |
+
|
| 3 |
+
## ⚡ نصب و راهاندازی سریع
|
| 4 |
+
|
| 5 |
+
### 1️⃣ نصب وابستگیها
|
| 6 |
+
```bash
|
| 7 |
+
pip install -r requirements.txt
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
```
|
| 9 |
|
| 10 |
+
### 2️⃣ Import منابع از فایلهای JSON
|
| 11 |
+
```bash
|
| 12 |
+
python import_resources.py
|
| 13 |
```
|
| 14 |
+
این اسکریپت بهطور خودکار همه منابع را از فایلهای JSON موجود import میکند.
|
| 15 |
+
|
| 16 |
+
### 3️⃣ راهاندازی سرور
|
| 17 |
+
```bash
|
| 18 |
+
# روش 1: استفاده از اسکریپت راهانداز
|
| 19 |
+
python start_server.py
|
| 20 |
|
| 21 |
+
# روش 2: مستقیم
|
| 22 |
+
python api_server_extended.py
|
| 23 |
+
|
| 24 |
+
# روش 3: با uvicorn
|
| 25 |
+
uvicorn api_server_extended:app --reload --host 0.0.0.0 --port 8000
|
| 26 |
```
|
| 27 |
|
| 28 |
+
### 4️⃣ دسترسی به داشبورد
|
| 29 |
+
```
|
| 30 |
+
http://localhost:8000
|
|
|
|
| 31 |
```
|
| 32 |
|
| 33 |
+
## 📋 تبهای داشبورد
|
| 34 |
+
|
| 35 |
+
### 📊 Market
|
| 36 |
+
- آمار کلی بازار
|
| 37 |
+
- لیست کریپتوکارنسیها
|
| 38 |
+
- نمودارها و ترندینگ
|
| 39 |
+
|
| 40 |
+
### 📡 API Monitor
|
| 41 |
+
- وضعیت همه ارائهدهندگان
|
| 42 |
+
- زمان پاسخ
|
| 43 |
+
- Health Check
|
| 44 |
+
|
| 45 |
+
### ⚡ Advanced
|
| 46 |
+
- Export JSON/CSV
|
| 47 |
+
- Backup
|
| 48 |
+
- Clear Cache
|
| 49 |
+
- Activity Logs
|
| 50 |
+
|
| 51 |
+
### ⚙️ Admin
|
| 52 |
+
- افزودن API جدید
|
| 53 |
+
- تنظیمات
|
| 54 |
+
- آمار کلی
|
| 55 |
+
|
| 56 |
+
### 🤗 HuggingFace
|
| 57 |
+
- مدلهای Sentiment Analysis
|
| 58 |
+
- Datasets
|
| 59 |
+
- جستجو در Registry
|
| 60 |
+
|
| 61 |
+
### 🔄 Pools
|
| 62 |
+
- مدیریت Poolها
|
| 63 |
+
- افزودن/حذف اعضا
|
| 64 |
+
- چرخش دستی
|
| 65 |
+
|
| 66 |
+
### 📋 Logs (جدید!)
|
| 67 |
+
- نمایش لاگها با فیلتر
|
| 68 |
+
- Export به JSON/CSV
|
| 69 |
+
- جستجو و آمار
|
| 70 |
+
|
| 71 |
+
### 📦 Resources (جدید!)
|
| 72 |
+
- مدیریت منابع API
|
| 73 |
+
- Import/Export
|
| 74 |
+
- Backup
|
| 75 |
+
- فیلتر بر اساس Category
|
| 76 |
+
|
| 77 |
+
## 🔧 استفاده از API
|
| 78 |
+
|
| 79 |
+
### دریافت لاگها
|
| 80 |
+
```bash
|
| 81 |
+
# همه لاگها
|
| 82 |
+
curl http://localhost:8000/api/logs
|
| 83 |
+
|
| 84 |
+
# فیلتر بر اساس Level
|
| 85 |
+
curl http://localhost:8000/api/logs?level=error
|
| 86 |
+
|
| 87 |
+
# جستجو
|
| 88 |
+
curl http://localhost:8000/api/logs?search=timeout
|
| 89 |
+
```
|
| 90 |
+
|
| 91 |
+
### Export لاگها
|
| 92 |
+
```bash
|
| 93 |
+
# Export به JSON
|
| 94 |
+
curl http://localhost:8000/api/logs/export/json?level=error
|
| 95 |
+
|
| 96 |
+
# Export به CSV
|
| 97 |
+
curl http://localhost:8000/api/logs/export/csv
|
| 98 |
+
```
|
| 99 |
+
|
| 100 |
+
### مدیریت منابع
|
| 101 |
+
```bash
|
| 102 |
+
# دریافت همه منابع
|
| 103 |
+
curl http://localhost:8000/api/resources
|
| 104 |
+
|
| 105 |
+
# Export منابع
|
| 106 |
+
curl http://localhost:8000/api/resources/export/json
|
| 107 |
|
| 108 |
+
# Backup
|
| 109 |
+
curl -X POST http://localhost:8000/api/resources/backup
|
|
|
|
| 110 |
|
| 111 |
+
# Import
|
| 112 |
+
curl -X POST "http://localhost:8000/api/resources/import/json?file_path=api-resources/crypto_resources_unified_2025-11-11.json&merge=true"
|
| 113 |
+
```
|
| 114 |
+
|
| 115 |
+
## 📝 مثالهای استفاده
|
|
|
|
| 116 |
|
| 117 |
+
### افزودن Provider جدید
|
| 118 |
+
```python
|
| 119 |
+
from resource_manager import ResourceManager
|
|
|
|
| 120 |
|
| 121 |
+
manager = ResourceManager()
|
| 122 |
|
| 123 |
+
provider = {
|
| 124 |
+
"id": "my_new_api",
|
| 125 |
+
"name": "My New API",
|
| 126 |
+
"category": "market_data",
|
| 127 |
+
"base_url": "https://api.example.com",
|
| 128 |
+
"requires_auth": False,
|
| 129 |
+
"priority": 5,
|
| 130 |
+
"weight": 50,
|
| 131 |
+
"free": True
|
| 132 |
+
}
|
| 133 |
|
| 134 |
+
manager.add_provider(provider)
|
| 135 |
+
manager.save_resources()
|
| 136 |
+
```
|
| 137 |
|
| 138 |
+
### ثبت لاگ
|
| 139 |
+
```python
|
| 140 |
+
from log_manager import log_info, log_error, LogCategory
|
| 141 |
|
| 142 |
+
# لاگ Info
|
| 143 |
+
log_info(LogCategory.PROVIDER, "Provider health check completed",
|
| 144 |
+
provider_id="coingecko", response_time=234.5)
|
| 145 |
|
| 146 |
+
# لاگ Error
|
| 147 |
+
log_error(LogCategory.PROVIDER, "Provider failed",
|
| 148 |
+
provider_id="etherscan", error="Timeout")
|
| 149 |
+
```
|
| 150 |
|
| 151 |
+
### استفاده از Provider Manager
|
| 152 |
+
```python
|
| 153 |
+
from provider_manager import ProviderManager
|
| 154 |
+
import asyncio
|
| 155 |
+
|
| 156 |
+
async def main():
|
| 157 |
+
manager = ProviderManager()
|
| 158 |
+
|
| 159 |
+
# Health Check
|
| 160 |
+
await manager.health_check_all()
|
| 161 |
+
|
| 162 |
+
# دریافت Provider از Pool
|
| 163 |
+
provider = manager.get_next_from_pool("primary_market_data_pool")
|
| 164 |
+
if provider:
|
| 165 |
+
print(f"Selected: {provider.name}")
|
| 166 |
+
|
| 167 |
+
await manager.close_session()
|
| 168 |
+
|
| 169 |
+
asyncio.run(main())
|
| 170 |
```
|
| 171 |
|
| 172 |
+
## 🐳 استفاده با Docker
|
| 173 |
+
|
| 174 |
+
```bash
|
| 175 |
+
# Build
|
| 176 |
+
docker build -t crypto-monitor .
|
| 177 |
|
| 178 |
+
# Run
|
| 179 |
+
docker run -p 8000:8000 crypto-monitor
|
| 180 |
|
| 181 |
+
# یا با docker-compose
|
| 182 |
+
docker-compose up -d
|
| 183 |
+
```
|
| 184 |
|
| 185 |
+
## 🔍 عیبیابی
|
| 186 |
|
| 187 |
+
### مشکل: Port در حال استفاده است
|
| 188 |
+
```bash
|
| 189 |
+
# تغییر پورت
|
| 190 |
+
uvicorn api_server_extended:app --port 8001
|
| 191 |
```
|
| 192 |
|
| 193 |
+
### مشکل: فایلهای JSON یافت نشد
|
| 194 |
+
```bash
|
| 195 |
+
# بررسی وجود فایلها
|
| 196 |
+
ls -la api-resources/
|
| 197 |
+
ls -la providers_config*.json
|
| 198 |
+
```
|
| 199 |
+
|
| 200 |
+
### مشکل: Import منابع ناموفق
|
| 201 |
+
```bash
|
| 202 |
+
# بررسی ساختار JSON
|
| 203 |
+
python -m json.tool api-resources/crypto_resources_unified_2025-11-11.json | head -20
|
| 204 |
+
```
|
| 205 |
+
|
| 206 |
+
## 📚 مستندات بیشتر
|
| 207 |
+
|
| 208 |
+
- [README.md](README.md) - مستندات کامل انگلیسی
|
| 209 |
+
- [README_FA.md](README_FA.md) - مستندات کامل فارسی
|
| 210 |
+
- [api-resources/README.md](api-resources/README.md) - راهنمای منابع API
|
| 211 |
|
| 212 |
+
## 🆘 پشتیبانی
|
|
|
|
|
|
|
|
|
|
|
|
|
| 213 |
|
| 214 |
+
در صورت بروز مشکل:
|
| 215 |
+
1. لاگها را بررسی کنید: `logs/app.log`
|
| 216 |
+
2. از تب Logs در داشبورد استفاده کنید
|
| 217 |
+
3. آمار سیستم را بررسی کنید: `/api/status`
|
| 218 |
|
| 219 |
+
---
|
| 220 |
|
| 221 |
+
**موفق باشید! 🚀**
|
|
|
README_FA.md
ADDED
|
@@ -0,0 +1,421 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🚀 Crypto Monitor ULTIMATE - نسخه توسعهیافته
|
| 2 |
+
|
| 3 |
+
یک سیستم مانیتورینگ و تحلیل کریپتوکارنسی قدرتمند با پشتیبانی از **100+ ارائهدهنده API رایگان** و سیستم پیشرفته **Provider Pool Management**.
|
| 4 |
+
|
| 5 |
+
## ✨ ویژگیهای کلیدی
|
| 6 |
+
|
| 7 |
+
### 🎯 مدیریت ارائهدهندگان (Provider Management)
|
| 8 |
+
- ✅ **100+ ارائهدهنده API رایگان** از دستهبندیهای مختلف
|
| 9 |
+
- 🔄 **سیستم Pool با استراتژیهای چرخش مختلف**
|
| 10 |
+
- Round Robin
|
| 11 |
+
- Priority-based
|
| 12 |
+
- Weighted Random
|
| 13 |
+
- Least Used
|
| 14 |
+
- Fastest Response
|
| 15 |
+
- 🛡️ **Circuit Breaker** برای جلوگیری از درخواستهای مکرر به سرویسهای خراب
|
| 16 |
+
- ⚡ **Rate Limiting هوشمند** برای هر ارائهدهنده
|
| 17 |
+
- 📊 **آمارگیری دقیق** از عملکرد هر ارائهدهنده
|
| 18 |
+
- 🔍 **Health Check خودکار** و دورهای
|
| 19 |
+
|
| 20 |
+
### 📈 دستهبندی ارائهدهندگان
|
| 21 |
+
|
| 22 |
+
#### 💰 بازار و قیمتگذاری (Market Data)
|
| 23 |
+
- CoinGecko, CoinPaprika, CoinCap
|
| 24 |
+
- CryptoCompare, Nomics, Messari
|
| 25 |
+
- LiveCoinWatch, Cryptorank, CoinLore, CoinCodex
|
| 26 |
+
|
| 27 |
+
#### 🔗 اکسپلوررهای بلاکچین (Blockchain Explorers)
|
| 28 |
+
- Etherscan, BscScan, PolygonScan
|
| 29 |
+
- Arbiscan, Optimistic Etherscan
|
| 30 |
+
- Blockchair, Blockchain.info, Ethplorer
|
| 31 |
+
|
| 32 |
+
#### 🏦 دیفای (DeFi Protocols)
|
| 33 |
+
- DefiLlama, Aave, Compound
|
| 34 |
+
- Uniswap V3, PancakeSwap, SushiSwap
|
| 35 |
+
- Curve Finance, 1inch, Yearn Finance
|
| 36 |
+
|
| 37 |
+
#### 🖼️ NFT
|
| 38 |
+
- OpenSea, Rarible, Reservoir, NFTPort
|
| 39 |
+
|
| 40 |
+
#### 📰 اخبار و شبکههای اجتماعی (News & Social)
|
| 41 |
+
- CryptoPanic, NewsAPI
|
| 42 |
+
- CoinDesk RSS, Cointelegraph RSS, Bitcoinist RSS
|
| 43 |
+
- Reddit Crypto, LunarCrush
|
| 44 |
+
|
| 45 |
+
#### 💭 تحلیل احساسات (Sentiment Analysis)
|
| 46 |
+
- Alternative.me (Fear & Greed Index)
|
| 47 |
+
- Santiment, LunarCrush
|
| 48 |
+
|
| 49 |
+
#### 📊 تحلیل و آنالیتیکس (Analytics)
|
| 50 |
+
- Glassnode, IntoTheBlock
|
| 51 |
+
- Coin Metrics, Kaiko
|
| 52 |
+
|
| 53 |
+
#### 💱 صرافیها (Exchanges)
|
| 54 |
+
- Binance, Kraken, Coinbase
|
| 55 |
+
- Bitfinex, Huobi, KuCoin
|
| 56 |
+
- OKX, Gate.io, Bybit
|
| 57 |
+
|
| 58 |
+
#### 🤗 Hugging Face Models
|
| 59 |
+
- مدلهای تحلیل احساسات (Sentiment Analysis)
|
| 60 |
+
- مدلهای دستهبندی متن (Text Classification)
|
| 61 |
+
- مدلهای Zero-Shot Classification
|
| 62 |
+
|
| 63 |
+
## 🏗️ معماری سیستم
|
| 64 |
+
|
| 65 |
+
```
|
| 66 |
+
┌─────────────────────────────────────────────────┐
|
| 67 |
+
│ Unified Dashboard (HTML/JS) │
|
| 68 |
+
│ 📊 نمایش دادهها | 🔄 مدیریت Pools | 📈 آمار │
|
| 69 |
+
└────────────────────┬────────────────────────────┘
|
| 70 |
+
│
|
| 71 |
+
▼
|
| 72 |
+
┌─────────────────────────────────────────────────┐
|
| 73 |
+
│ FastAPI Server (Python) │
|
| 74 |
+
│ 🌐 REST API | WebSocket | Background Tasks │
|
| 75 |
+
└────────────────────┬────────────────────────────┘
|
| 76 |
+
│
|
| 77 |
+
▼
|
| 78 |
+
┌─────────────────────────────────────────────────┐
|
| 79 |
+
│ Provider Manager (Core Logic) │
|
| 80 |
+
│ 🔄 Rotation | 🛡️ Circuit Breaker | 📊 Stats │
|
| 81 |
+
└────────────────────┬────────────────────────────┘
|
| 82 |
+
│
|
| 83 |
+
┌───────────────┼───────────────┐
|
| 84 |
+
▼ ▼ ▼
|
| 85 |
+
┌─────────┐ ┌─────────┐ ┌─────────┐
|
| 86 |
+
│ Pool 1 │ │ Pool 2 │ │ Pool N │
|
| 87 |
+
│ Market │ │ DeFi │ │ NFT │
|
| 88 |
+
└────┬────┘ └────┬────┘ └────┬────┘
|
| 89 |
+
│ │ │
|
| 90 |
+
└──────┬───────┴──────┬───────┘
|
| 91 |
+
▼ ▼
|
| 92 |
+
┌──────────────┐ ┌──────────────┐
|
| 93 |
+
│ Provider 1 │ │ Provider N │
|
| 94 |
+
│ (CoinGecko) │ │ (Binance) │
|
| 95 |
+
└──────────────┘ └──────────────┘
|
| 96 |
+
```
|
| 97 |
+
|
| 98 |
+
## 📦 نصب و راهاندازی
|
| 99 |
+
|
| 100 |
+
### پیشنیازها
|
| 101 |
+
```bash
|
| 102 |
+
Python 3.8+
|
| 103 |
+
pip
|
| 104 |
+
```
|
| 105 |
+
|
| 106 |
+
### نصب وابستگیها
|
| 107 |
+
```bash
|
| 108 |
+
pip install fastapi uvicorn aiohttp pydantic
|
| 109 |
+
```
|
| 110 |
+
|
| 111 |
+
### اجرای سرور
|
| 112 |
+
```bash
|
| 113 |
+
# روش 1: مستقیم
|
| 114 |
+
python api_server_extended.py
|
| 115 |
+
|
| 116 |
+
# روش 2: با uvicorn
|
| 117 |
+
uvicorn api_server_extended:app --reload --host 0.0.0.0 --port 8000
|
| 118 |
+
```
|
| 119 |
+
|
| 120 |
+
### دسترسی به داشبورد
|
| 121 |
+
```
|
| 122 |
+
http://localhost:8000
|
| 123 |
+
```
|
| 124 |
+
|
| 125 |
+
## 🔧 استفاده از API
|
| 126 |
+
|
| 127 |
+
### 🌐 Endpoints اصلی
|
| 128 |
+
|
| 129 |
+
#### **وضعیت سیستم**
|
| 130 |
+
```http
|
| 131 |
+
GET /health
|
| 132 |
+
GET /api/status
|
| 133 |
+
GET /api/stats
|
| 134 |
+
```
|
| 135 |
+
|
| 136 |
+
#### **مدیریت ارائهدهندگان**
|
| 137 |
+
```http
|
| 138 |
+
GET /api/providers # لیست همه
|
| 139 |
+
GET /api/providers/{provider_id} # جزئیات یک ارائهدهنده
|
| 140 |
+
POST /api/providers/{provider_id}/health-check
|
| 141 |
+
GET /api/providers/category/{category}
|
| 142 |
+
```
|
| 143 |
+
|
| 144 |
+
#### **مدیریت Poolها**
|
| 145 |
+
```http
|
| 146 |
+
GET /api/pools # لیست همه Poolها
|
| 147 |
+
GET /api/pools/{pool_id} # جزئیات یک Pool
|
| 148 |
+
POST /api/pools # ایجاد Pool جدید
|
| 149 |
+
DELETE /api/pools/{pool_id} # حذف Pool
|
| 150 |
+
|
| 151 |
+
POST /api/pools/{pool_id}/members # افزودن عضو
|
| 152 |
+
DELETE /api/pools/{pool_id}/members/{provider_id}
|
| 153 |
+
POST /api/pools/{pool_id}/rotate # چرخش دستی
|
| 154 |
+
GET /api/pools/history # تاریخچه چرخشها
|
| 155 |
+
```
|
| 156 |
+
|
| 157 |
+
### 📝 نمونههای استفاده
|
| 158 |
+
|
| 159 |
+
#### ایجاد Pool جدید
|
| 160 |
+
```bash
|
| 161 |
+
curl -X POST http://localhost:8000/api/pools \
|
| 162 |
+
-H "Content-Type: application/json" \
|
| 163 |
+
-d '{
|
| 164 |
+
"name": "My Market Pool",
|
| 165 |
+
"category": "market_data",
|
| 166 |
+
"rotation_strategy": "weighted",
|
| 167 |
+
"description": "Pool for market data providers"
|
| 168 |
+
}'
|
| 169 |
+
```
|
| 170 |
+
|
| 171 |
+
#### افزودن ارائهدهنده به Pool
|
| 172 |
+
```bash
|
| 173 |
+
curl -X POST http://localhost:8000/api/pools/my_market_pool/members \
|
| 174 |
+
-H "Content-Type: application/json" \
|
| 175 |
+
-d '{
|
| 176 |
+
"provider_id": "coingecko",
|
| 177 |
+
"priority": 10,
|
| 178 |
+
"weight": 100
|
| 179 |
+
}'
|
| 180 |
+
```
|
| 181 |
+
|
| 182 |
+
#### چرخش Pool
|
| 183 |
+
```bash
|
| 184 |
+
curl -X POST http://localhost:8000/api/pools/my_market_pool/rotate \
|
| 185 |
+
-H "Content-Type: application/json" \
|
| 186 |
+
-d '{"reason": "manual rotation"}'
|
| 187 |
+
```
|
| 188 |
+
|
| 189 |
+
## 🎮 استفاده از Python API
|
| 190 |
+
|
| 191 |
+
```python
|
| 192 |
+
import asyncio
|
| 193 |
+
from provider_manager import ProviderManager
|
| 194 |
+
|
| 195 |
+
async def main():
|
| 196 |
+
# ایجاد مدیر
|
| 197 |
+
manager = ProviderManager()
|
| 198 |
+
|
| 199 |
+
# بررسی سلامت همه
|
| 200 |
+
await manager.health_check_all()
|
| 201 |
+
|
| 202 |
+
# دریافت ارائهدهنده از Pool
|
| 203 |
+
provider = manager.get_next_from_pool("primary_market_data_pool")
|
| 204 |
+
if provider:
|
| 205 |
+
print(f"Selected: {provider.name}")
|
| 206 |
+
print(f"Success Rate: {provider.success_rate}%")
|
| 207 |
+
|
| 208 |
+
# آمار کلی
|
| 209 |
+
stats = manager.get_all_stats()
|
| 210 |
+
print(f"Total Providers: {stats['summary']['total_providers']}")
|
| 211 |
+
print(f"Online: {stats['summary']['online']}")
|
| 212 |
+
|
| 213 |
+
# صادرکردن آمار
|
| 214 |
+
manager.export_stats("my_stats.json")
|
| 215 |
+
|
| 216 |
+
await manager.close_session()
|
| 217 |
+
|
| 218 |
+
asyncio.run(main())
|
| 219 |
+
```
|
| 220 |
+
|
| 221 |
+
## 📊 استراتژیهای چرخش Pool
|
| 222 |
+
|
| 223 |
+
### 1️⃣ Round Robin
|
| 224 |
+
هر بار به ترتیب یک ارائهدهنده انتخاب میشود.
|
| 225 |
+
```python
|
| 226 |
+
rotation_strategy = "round_robin"
|
| 227 |
+
```
|
| 228 |
+
|
| 229 |
+
### 2️⃣ Priority-Based
|
| 230 |
+
ارائهدهنده با بالاترین اولویت انتخاب میشود.
|
| 231 |
+
```python
|
| 232 |
+
rotation_strategy = "priority"
|
| 233 |
+
# Provider with priority=10 selected over priority=5
|
| 234 |
+
```
|
| 235 |
+
|
| 236 |
+
### 3️⃣ Weighted Random
|
| 237 |
+
انتخاب تصادفی با وزندهی.
|
| 238 |
+
```python
|
| 239 |
+
rotation_strategy = "weighted"
|
| 240 |
+
# Provider with weight=100 has 2x chance vs weight=50
|
| 241 |
+
```
|
| 242 |
+
|
| 243 |
+
### 4️⃣ Least Used
|
| 244 |
+
ارائهدهندهای که کمتر استفاده شده انتخاب میشود.
|
| 245 |
+
```python
|
| 246 |
+
rotation_strategy = "least_used"
|
| 247 |
+
```
|
| 248 |
+
|
| 249 |
+
### 5️⃣ Fastest Response
|
| 250 |
+
ارائهدهنده با سریعترین زمان پاسخ انتخاب میشود.
|
| 251 |
+
```python
|
| 252 |
+
rotation_strategy = "fastest_response"
|
| 253 |
+
```
|
| 254 |
+
|
| 255 |
+
## 🛡️ Circuit Breaker
|
| 256 |
+
|
| 257 |
+
سیستم Circuit Breaker بهطور خودکار ارائهدهندگان مشکلدار را غیرفعال میکند:
|
| 258 |
+
|
| 259 |
+
- **آستانه**: 5 خطای متوالی
|
| 260 |
+
- **مدت زمان قطع**: 60 ثانیه
|
| 261 |
+
- **بازیابی خودکار**: پس از اتمام timeout
|
| 262 |
+
|
| 263 |
+
```python
|
| 264 |
+
# Circuit Breaker خودکار در Provider
|
| 265 |
+
if provider.consecutive_failures >= 5:
|
| 266 |
+
provider.circuit_breaker_open = True
|
| 267 |
+
provider.circuit_breaker_open_until = time.time() + 60
|
| 268 |
+
```
|
| 269 |
+
|
| 270 |
+
## 📈 مانیتورینگ و لاگ
|
| 271 |
+
|
| 272 |
+
### بررسی سلامت دورهای
|
| 273 |
+
سیستم هر 30 ثانیه بهطور خودکار سلامت همه ارائهدهندگان را بررسی میکند.
|
| 274 |
+
|
| 275 |
+
### آمارگیری
|
| 276 |
+
- **تعداد کل درخواستها**
|
| 277 |
+
- **درخواستهای موفق/ناموفق**
|
| 278 |
+
- **نرخ موفقیت (Success Rate)**
|
| 279 |
+
- **م��انگین زمان پاسخ**
|
| 280 |
+
- **تعداد چرخشهای Pool**
|
| 281 |
+
|
| 282 |
+
### صادرکردن آمار
|
| 283 |
+
```python
|
| 284 |
+
manager.export_stats("stats_export.json")
|
| 285 |
+
```
|
| 286 |
+
|
| 287 |
+
## 🔐 مدیریت API Key
|
| 288 |
+
|
| 289 |
+
برای ارائهدهندگانی که نیاز به API Key دارند:
|
| 290 |
+
|
| 291 |
+
1. فایل `.env` بسازید:
|
| 292 |
+
```env
|
| 293 |
+
# Market Data
|
| 294 |
+
COINMARKETCAP_API_KEY=your_key_here
|
| 295 |
+
CRYPTOCOMPARE_API_KEY=your_key_here
|
| 296 |
+
|
| 297 |
+
# Blockchain Data
|
| 298 |
+
ALCHEMY_API_KEY=your_key_here
|
| 299 |
+
INFURA_API_KEY=your_key_here
|
| 300 |
+
|
| 301 |
+
# News
|
| 302 |
+
NEWSAPI_KEY=your_key_here
|
| 303 |
+
|
| 304 |
+
# Analytics
|
| 305 |
+
GLASSNODE_API_KEY=your_key_here
|
| 306 |
+
```
|
| 307 |
+
|
| 308 |
+
2. در کد خود از `python-dotenv` استفاده کنید:
|
| 309 |
+
```python
|
| 310 |
+
from dotenv import load_dotenv
|
| 311 |
+
import os
|
| 312 |
+
|
| 313 |
+
load_dotenv()
|
| 314 |
+
api_key = os.getenv("COINMARKETCAP_API_KEY")
|
| 315 |
+
```
|
| 316 |
+
|
| 317 |
+
## 🎨 داشبورد وب
|
| 318 |
+
|
| 319 |
+
داشبورد شامل تبهای زیر است:
|
| 320 |
+
|
| 321 |
+
### 📊 Market
|
| 322 |
+
- آمار کلی بازار
|
| 323 |
+
- لیست کریپتوکارنسیهای برتر
|
| 324 |
+
- نمودارها (Dominance, Fear & Greed)
|
| 325 |
+
- ترندینگ و DeFi
|
| 326 |
+
|
| 327 |
+
### 📡 API Monitor
|
| 328 |
+
- وضعیت همه ارائهدهندگان
|
| 329 |
+
- زمان پاسخ
|
| 330 |
+
- آخرین بررسی سلامت
|
| 331 |
+
- تحلیل احساسات (HuggingFace)
|
| 332 |
+
|
| 333 |
+
### ⚡ Advanced
|
| 334 |
+
- لیست APIها
|
| 335 |
+
- اکسپورت JSON/CSV
|
| 336 |
+
- پشتیبانگیری
|
| 337 |
+
- پاکسازی Cache
|
| 338 |
+
- لاگ فعالیتها
|
| 339 |
+
|
| 340 |
+
### ⚙️ Admin
|
| 341 |
+
- افزودن API جدید
|
| 342 |
+
- تنظیمات
|
| 343 |
+
- آمار کلی
|
| 344 |
+
|
| 345 |
+
### 🤗 HuggingFace
|
| 346 |
+
- وضعیت سلامت
|
| 347 |
+
- لیست مدلها و دیتاستها
|
| 348 |
+
- جستجو در Registry
|
| 349 |
+
- تحلیل احساسات آنلاین
|
| 350 |
+
|
| 351 |
+
### 🔄 Pools
|
| 352 |
+
- مدیریت Poolها
|
| 353 |
+
- افزودن/حذف اعضا
|
| 354 |
+
- چرخش دستی
|
| 355 |
+
- تاریخچه چرخشها
|
| 356 |
+
- آمار تفصیلی
|
| 357 |
+
|
| 358 |
+
## 🧪 تست
|
| 359 |
+
|
| 360 |
+
```bash
|
| 361 |
+
# تست Provider Manager
|
| 362 |
+
python provider_manager.py
|
| 363 |
+
|
| 364 |
+
# تست سرور API
|
| 365 |
+
python api_server_extended.py
|
| 366 |
+
```
|
| 367 |
+
|
| 368 |
+
## 📄 فایلهای پروژه
|
| 369 |
+
|
| 370 |
+
```
|
| 371 |
+
crypto-monitor-hf-full-fixed-v4-realapis/
|
| 372 |
+
├── unified_dashboard.html # داشبورد وب اصلی
|
| 373 |
+
├── providers_config_extended.json # تنظیمات 100+ ارائهدهنده
|
| 374 |
+
├── provider_manager.py # هسته مدیریت Provider & Pool
|
| 375 |
+
├── api_server_extended.py # سرور FastAPI
|
| 376 |
+
├── README_FA.md # راهنمای فارسی (این فایل)
|
| 377 |
+
└── .env.example # نمونه متغیرهای محیطی
|
| 378 |
+
```
|
| 379 |
+
|
| 380 |
+
## 🚀 ویژگیهای آینده
|
| 381 |
+
|
| 382 |
+
- [ ] پشتیبانی از WebSocket برای دادههای Realtime
|
| 383 |
+
- [ ] سیستم صف (Queue) برای درخواستهای سنگین
|
| 384 |
+
- [ ] Cache با Redis
|
| 385 |
+
- [ ] Dashboard پیشرفته با React/Vue
|
| 386 |
+
- [ ] Alerting System (Telegram/Email)
|
| 387 |
+
- [ ] Machine Learning برای پیشبینی بهترین Provider
|
| 388 |
+
- [ ] Multi-tenant Support
|
| 389 |
+
- [ ] Docker & Kubernetes Support
|
| 390 |
+
|
| 391 |
+
## 🤝 مشارکت
|
| 392 |
+
|
| 393 |
+
برای مشارکت:
|
| 394 |
+
1. Fork کنید
|
| 395 |
+
2. یک branch جدید بسازید: `git checkout -b feature/amazing-feature`
|
| 396 |
+
3. تغییرات را commit کنید: `git commit -m 'Add amazing feature'`
|
| 397 |
+
4. Push کنید: `git push origin feature/amazing-feature`
|
| 398 |
+
5. Pull Request ایجاد کنید
|
| 399 |
+
|
| 400 |
+
## 📝 لایسنس
|
| 401 |
+
|
| 402 |
+
این پروژه تحت لایسنس MIT منتشر شده است.
|
| 403 |
+
|
| 404 |
+
## 💬 پشتیبانی
|
| 405 |
+
|
| 406 |
+
در صورت بروز مشکل یا سوال:
|
| 407 |
+
- Issue در GitHub باز کنید
|
| 408 |
+
- به بخش Discussions مراجعه کنید
|
| 409 |
+
|
| 410 |
+
## 🙏 تشکر
|
| 411 |
+
|
| 412 |
+
از تمام ارائهدهندگان API رایگان که این پروژه را ممکن کردند:
|
| 413 |
+
- CoinGecko, CoinPaprika, CoinCap
|
| 414 |
+
- Etherscan, BscScan و تمام Block Explorers
|
| 415 |
+
- DefiLlama, OpenSea و...
|
| 416 |
+
- Hugging Face برای مدلهای ML
|
| 417 |
+
|
| 418 |
+
---
|
| 419 |
+
|
| 420 |
+
**ساخته شده با ❤️ برای جامعه کریپتو**
|
| 421 |
+
|
REALTIME_FEATURES_FA.md
ADDED
|
@@ -0,0 +1,374 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🚀 ویژگیهای بلادرنگ سیستم مانیتورینگ کریپتو
|
| 2 |
+
|
| 3 |
+
## ✨ چه چیزی اضافه شد؟
|
| 4 |
+
|
| 5 |
+
### 1. 📡 سیستم WebSocket کامل
|
| 6 |
+
|
| 7 |
+
**قبل (HTTP Polling):**
|
| 8 |
+
```
|
| 9 |
+
کلاینت → درخواست HTTP → سرور
|
| 10 |
+
← پاسخ HTTP ←
|
| 11 |
+
(تکرار هر 1-5 ثانیه) ⏱️
|
| 12 |
+
```
|
| 13 |
+
|
| 14 |
+
**الان (WebSocket):**
|
| 15 |
+
```
|
| 16 |
+
کلاینت ⟷ اتصال دائمی ⟷ سرور
|
| 17 |
+
← داده لحظهای ←
|
| 18 |
+
(فوری و بدون تاخیر! ⚡)
|
| 19 |
+
```
|
| 20 |
+
|
| 21 |
+
### 2. 👥 نمایش تعداد کاربران آنلاین
|
| 22 |
+
|
| 23 |
+
برنامه الان میتواند **بلافاصله** به شما نشان دهد:
|
| 24 |
+
- چند نفر الان متصل هستند
|
| 25 |
+
- چند جلسه (session) فعال است
|
| 26 |
+
- چه نوع کلاینتهایی متصلاند (مرورگر، API، موبایل)
|
| 27 |
+
|
| 28 |
+
### 3. 🎨 رابط کاربری زیبا و هوشمند
|
| 29 |
+
|
| 30 |
+
- **نوار وضعیت بالای صفحه** با نمایش:
|
| 31 |
+
- وضعیت اتصال (متصل/قطع شده) با نقطه رنگی
|
| 32 |
+
- تعداد کاربران آنلاین به صورت زنده
|
| 33 |
+
- آمار جلسات کلی
|
| 34 |
+
|
| 35 |
+
- **انیمیشنهای جذاب**:
|
| 36 |
+
- هنگام تغییر تعداد کاربران
|
| 37 |
+
- هنگام اتصال/قطع اتصال
|
| 38 |
+
- پالس نقطه وضعیت
|
| 39 |
+
|
| 40 |
+
- **reconnect خودکار**:
|
| 41 |
+
- اگر اتصال قطع شد، خودکار دوباره وصل میشود
|
| 42 |
+
- نیازی به refresh صفحه نیست!
|
| 43 |
+
|
| 44 |
+
## 🎯 چرا این تغییرات مهم است؟
|
| 45 |
+
|
| 46 |
+
### سرعت 10 برابر بیشتر! ⚡
|
| 47 |
+
|
| 48 |
+
| عملیات | HTTP Polling | WebSocket |
|
| 49 |
+
|--------|--------------|-----------|
|
| 50 |
+
| بهروزرسانی قیمت | 2-5 ثانیه | < 100ms |
|
| 51 |
+
| نمایش کاربران | هر 3 ثانیه | فوری |
|
| 52 |
+
| مصرف سرور | 100% | 10% |
|
| 53 |
+
| پهنای باند | زیاد | خیلی کم |
|
| 54 |
+
|
| 55 |
+
### Session Management حرفهای 🔐
|
| 56 |
+
|
| 57 |
+
هر کاربر یک **Session ID** منحصر به فرد دارد:
|
| 58 |
+
```json
|
| 59 |
+
{
|
| 60 |
+
"session_id": "550e8400-e29b-41d4-a716-446655440000",
|
| 61 |
+
"client_type": "browser",
|
| 62 |
+
"connected_at": "2024-01-15T10:00:00",
|
| 63 |
+
"metadata": { "source": "unified_dashboard" }
|
| 64 |
+
}
|
| 65 |
+
```
|
| 66 |
+
|
| 67 |
+
## 📂 فایلهای جدید
|
| 68 |
+
|
| 69 |
+
### Backend (سرور):
|
| 70 |
+
```
|
| 71 |
+
backend/services/
|
| 72 |
+
├── connection_manager.py ← مدیریت اتصالات WebSocket
|
| 73 |
+
└── auto_discovery_service.py ← کشف خودکار منابع جدید
|
| 74 |
+
|
| 75 |
+
api_server_extended.py ← بهروزرسانی شده با WebSocket
|
| 76 |
+
```
|
| 77 |
+
|
| 78 |
+
### Frontend (رابط کاربری):
|
| 79 |
+
```
|
| 80 |
+
static/
|
| 81 |
+
├── js/
|
| 82 |
+
│ └── websocket-client.js ← کلاینت WebSocket هوشمند
|
| 83 |
+
└── css/
|
| 84 |
+
└── connection-status.css ← استایلهای زیبا
|
| 85 |
+
|
| 86 |
+
test_websocket.html ← صفحه تست کامل
|
| 87 |
+
```
|
| 88 |
+
|
| 89 |
+
### مستندات:
|
| 90 |
+
```
|
| 91 |
+
WEBSOCKET_GUIDE.md ← راهنمای کامل WebSocket
|
| 92 |
+
REALTIME_FEATURES_FA.md ← این فایل!
|
| 93 |
+
```
|
| 94 |
+
|
| 95 |
+
## 🚀 نحوه استفاده
|
| 96 |
+
|
| 97 |
+
### 1. راهاندازی سرور:
|
| 98 |
+
|
| 99 |
+
```bash
|
| 100 |
+
# نصب وابستگیهای جدید
|
| 101 |
+
pip install -r requirements.txt
|
| 102 |
+
|
| 103 |
+
# اجرای سرور
|
| 104 |
+
python api_server_extended.py
|
| 105 |
+
```
|
| 106 |
+
|
| 107 |
+
### 2. باز کردن صفحه تست:
|
| 108 |
+
|
| 109 |
+
```
|
| 110 |
+
http://localhost:8000/test_websocket.html
|
| 111 |
+
```
|
| 112 |
+
|
| 113 |
+
### 3. مشاهده نتایج:
|
| 114 |
+
|
| 115 |
+
- ✅ نوار بالا باید **سبز** شود
|
| 116 |
+
- 👥 تعداد کاربران باید نمایش داده شود
|
| 117 |
+
- 📊 آمار به صورت **لحظهای** آپدیت میشود
|
| 118 |
+
|
| 119 |
+
### 4. تست با چند تب:
|
| 120 |
+
|
| 121 |
+
1. صفحه را در چند تب باز کنید
|
| 122 |
+
2. تعداد کاربران آنلاین **فوراً** افزایش مییابد
|
| 123 |
+
3. یک تب را ببندید → تعداد کاربران کم میشود
|
| 124 |
+
|
| 125 |
+
## 🎮 ویژگیهای پیشرفته
|
| 126 |
+
|
| 127 |
+
### Subscribe به کانالهای مختلف:
|
| 128 |
+
|
| 129 |
+
```javascript
|
| 130 |
+
// فقط اطلاعات بازار
|
| 131 |
+
wsClient.subscribe('market');
|
| 132 |
+
|
| 133 |
+
// فقط قیمتها
|
| 134 |
+
wsClient.subscribe('prices');
|
| 135 |
+
|
| 136 |
+
// فقط اخبار
|
| 137 |
+
wsClient.subscribe('news');
|
| 138 |
+
|
| 139 |
+
// همه چیز
|
| 140 |
+
wsClient.subscribe('all');
|
| 141 |
+
```
|
| 142 |
+
|
| 143 |
+
### دریافت آمار فوری:
|
| 144 |
+
|
| 145 |
+
```javascript
|
| 146 |
+
// درخواست آمار
|
| 147 |
+
wsClient.requestStats();
|
| 148 |
+
|
| 149 |
+
// پاسخ در کمتر از 100ms:
|
| 150 |
+
{
|
| 151 |
+
"active_connections": 15,
|
| 152 |
+
"total_sessions": 23,
|
| 153 |
+
"client_types": {
|
| 154 |
+
"browser": 12,
|
| 155 |
+
"api": 2,
|
| 156 |
+
"mobile": 1
|
| 157 |
+
}
|
| 158 |
+
}
|
| 159 |
+
```
|
| 160 |
+
|
| 161 |
+
### Handler سفارشی:
|
| 162 |
+
|
| 163 |
+
```javascript
|
| 164 |
+
// ثبت handler برای رویداد خاص
|
| 165 |
+
wsClient.on('price_update', (message) => {
|
| 166 |
+
console.log('قیمت جدید:', message.data);
|
| 167 |
+
updateUI(message.data);
|
| 168 |
+
});
|
| 169 |
+
```
|
| 170 |
+
|
| 171 |
+
## 📊 مثال کاربردی
|
| 172 |
+
|
| 173 |
+
### نمایش تعداد کاربران در صفحه خودتان:
|
| 174 |
+
|
| 175 |
+
```html
|
| 176 |
+
<!DOCTYPE html>
|
| 177 |
+
<html lang="fa" dir="rtl">
|
| 178 |
+
<head>
|
| 179 |
+
<link rel="stylesheet" href="/static/css/connection-status.css">
|
| 180 |
+
</head>
|
| 181 |
+
<body>
|
| 182 |
+
<!-- نوار وضعیت -->
|
| 183 |
+
<div class="connection-status-bar" id="ws-connection-status">
|
| 184 |
+
<div class="ws-connection-info">
|
| 185 |
+
<span class="status-dot" id="ws-status-dot"></span>
|
| 186 |
+
<span id="ws-status-text">در حال اتصال...</span>
|
| 187 |
+
</div>
|
| 188 |
+
|
| 189 |
+
<div class="online-users-widget">
|
| 190 |
+
<span class="users-icon">👥</span>
|
| 191 |
+
<span class="count-number" id="active-users-count">0</span>
|
| 192 |
+
<span class="count-label">کاربر آنلاین</span>
|
| 193 |
+
</div>
|
| 194 |
+
</div>
|
| 195 |
+
|
| 196 |
+
<!-- محتوای اصلی شما -->
|
| 197 |
+
<div class="container">
|
| 198 |
+
<h1>داشبورد من</h1>
|
| 199 |
+
<!-- ... -->
|
| 200 |
+
</div>
|
| 201 |
+
|
| 202 |
+
<!-- اضافه کردن WebSocket Client -->
|
| 203 |
+
<script src="/static/js/websocket-client.js"></script>
|
| 204 |
+
<script>
|
| 205 |
+
// همین! دیگر نیازی به کد اضافه نیست
|
| 206 |
+
// کلاینت خودکار متصل میشود و UI را آپدیت میکند
|
| 207 |
+
</script>
|
| 208 |
+
</body>
|
| 209 |
+
</html>
|
| 210 |
+
```
|
| 211 |
+
|
| 212 |
+
## 🔥 کاربردهای واقعی
|
| 213 |
+
|
| 214 |
+
### 1. برنامه موبایل:
|
| 215 |
+
```python
|
| 216 |
+
import asyncio
|
| 217 |
+
import websockets
|
| 218 |
+
import json
|
| 219 |
+
|
| 220 |
+
async def mobile_app():
|
| 221 |
+
uri = "ws://yourserver.com/ws"
|
| 222 |
+
async with websockets.connect(uri) as ws:
|
| 223 |
+
# دریافت لحظهای قیمتها
|
| 224 |
+
async for message in ws:
|
| 225 |
+
data = json.loads(message)
|
| 226 |
+
if data['type'] == 'price_update':
|
| 227 |
+
show_notification(data['data'])
|
| 228 |
+
```
|
| 229 |
+
|
| 230 |
+
### 2. ربات تلگرام:
|
| 231 |
+
```python
|
| 232 |
+
async def telegram_bot():
|
| 233 |
+
async with websockets.connect("ws://server/ws") as ws:
|
| 234 |
+
# Subscribe به alerts
|
| 235 |
+
await ws.send(json.dumps({
|
| 236 |
+
"type": "subscribe",
|
| 237 |
+
"group": "alerts"
|
| 238 |
+
}))
|
| 239 |
+
|
| 240 |
+
async for message in ws:
|
| 241 |
+
data = json.loads(message)
|
| 242 |
+
if data['type'] == 'alert':
|
| 243 |
+
# ارسال به تلگرام
|
| 244 |
+
await bot.send_message(
|
| 245 |
+
chat_id,
|
| 246 |
+
data['data']['message']
|
| 247 |
+
)
|
| 248 |
+
```
|
| 249 |
+
|
| 250 |
+
### 3. صفحه نمایش عمومی:
|
| 251 |
+
```javascript
|
| 252 |
+
// نمایش روی تلویزیون یا نمایشگر
|
| 253 |
+
const ws = new CryptoWebSocketClient();
|
| 254 |
+
|
| 255 |
+
ws.on('market_update', (msg) => {
|
| 256 |
+
// آپدیت نمودارها و قیمتها
|
| 257 |
+
updateCharts(msg.data);
|
| 258 |
+
updatePrices(msg.data);
|
| 259 |
+
});
|
| 260 |
+
|
| 261 |
+
// هر 10 ثانیه یکبار
|
| 262 |
+
setInterval(() => {
|
| 263 |
+
ws.requestStats();
|
| 264 |
+
}, 10000);
|
| 265 |
+
```
|
| 266 |
+
|
| 267 |
+
## 🎨 سفارشیسازی UI
|
| 268 |
+
|
| 269 |
+
### تغییر رنگها:
|
| 270 |
+
|
| 271 |
+
```css
|
| 272 |
+
/* در فایل CSS خودتان */
|
| 273 |
+
.connection-status-bar {
|
| 274 |
+
background: linear-gradient(135deg, #your-color1, #your-color2);
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
+
.status-dot-online {
|
| 278 |
+
background: #your-green-color;
|
| 279 |
+
}
|
| 280 |
+
```
|
| 281 |
+
|
| 282 |
+
### تغییر موقعیت نوار:
|
| 283 |
+
|
| 284 |
+
```css
|
| 285 |
+
.connection-status-bar {
|
| 286 |
+
/* به جای top */
|
| 287 |
+
bottom: 0;
|
| 288 |
+
}
|
| 289 |
+
```
|
| 290 |
+
|
| 291 |
+
### افزودن اطلاعات بیشتر:
|
| 292 |
+
|
| 293 |
+
```javascript
|
| 294 |
+
wsClient.on('stats_update', (msg) => {
|
| 295 |
+
// نمایش آمار سفارشی
|
| 296 |
+
document.getElementById('my-stat').textContent =
|
| 297 |
+
msg.data.custom_metric;
|
| 298 |
+
});
|
| 299 |
+
```
|
| 300 |
+
|
| 301 |
+
## 🐛 عیبیابی
|
| 302 |
+
|
| 303 |
+
### مشکل: اتصال برقرار نمیشود
|
| 304 |
+
|
| 305 |
+
1. سرور اجرا شده؟
|
| 306 |
+
```bash
|
| 307 |
+
curl http://localhost:8000/health
|
| 308 |
+
```
|
| 309 |
+
|
| 310 |
+
2. پورت باز است؟
|
| 311 |
+
```bash
|
| 312 |
+
netstat -an | grep 8000
|
| 313 |
+
```
|
| 314 |
+
|
| 315 |
+
3. کنسول مرورگر چه میگوید؟
|
| 316 |
+
- F12 → Console
|
| 317 |
+
|
| 318 |
+
### مشکل: تعداد کاربران نمایش نمیشود
|
| 319 |
+
|
| 320 |
+
1. Elementها با ID صحیح وجود دارند؟
|
| 321 |
+
```html
|
| 322 |
+
<span id="active-users-count">0</span>
|
| 323 |
+
```
|
| 324 |
+
|
| 325 |
+
2. JavaScript لود شده؟
|
| 326 |
+
```javascript
|
| 327 |
+
console.log(window.wsClient); // باید object باشد
|
| 328 |
+
```
|
| 329 |
+
|
| 330 |
+
### مشکل: اتصال مدام قطع میشود
|
| 331 |
+
|
| 332 |
+
1. Heartbeat فعال است؟ (باید هر 10 ثانیه یک پیام بیاید)
|
| 333 |
+
2. Firewall یا Proxy مشکل ندارد؟
|
| 334 |
+
3. Timeout سرور کم است؟
|
| 335 |
+
|
| 336 |
+
## 📈 Performance
|
| 337 |
+
|
| 338 |
+
### قبل:
|
| 339 |
+
- 🐌 100 کاربر = 6000 درخواست HTTP در دقیقه
|
| 340 |
+
- 💾 حجم داده: ~300MB در ساعت
|
| 341 |
+
- ⚡ CPU: 60-80%
|
| 342 |
+
|
| 343 |
+
### بعد:
|
| 344 |
+
- ⚡ 100 کاربر = 100 اتصال WebSocket
|
| 345 |
+
- 💾 حجم داده: ~10MB در ساعت
|
| 346 |
+
- ⚡ CPU: 10-15%
|
| 347 |
+
|
| 348 |
+
**30 برابر کارآمدتر!** 🎉
|
| 349 |
+
|
| 350 |
+
## 🎓 آموزش ویدیویی (قریب الوقوع)
|
| 351 |
+
|
| 352 |
+
- [ ] نصب و راهاندازی
|
| 353 |
+
- [ ] استفاده از API
|
| 354 |
+
- [ ] ساخت داشبورد سفارشی
|
| 355 |
+
- [ ] Integration با برنامه موبایل
|
| 356 |
+
|
| 357 |
+
## 💡 ایدههای بیشتر
|
| 358 |
+
|
| 359 |
+
1. **چت بین کاربران** - با همین WebSocket
|
| 360 |
+
2. **Trading Signals** - دریافت لحظهای سیگنالها
|
| 361 |
+
3. **Portfolio Tracker** - بهروزرسانی فوری داراییها
|
| 362 |
+
4. **Price Alerts** - هشدار لحظهای برای تغییر قیمت
|
| 363 |
+
|
| 364 |
+
## 📞 پشتیبانی
|
| 365 |
+
|
| 366 |
+
سوال دارید؟
|
| 367 |
+
- 📖 [راهنمای کامل WebSocket](WEBSOCKET_GUIDE.md)
|
| 368 |
+
- 🧪 [صفحه تست](http://localhost:8000/test_websocket.html)
|
| 369 |
+
- 💬 Issue در GitHub
|
| 370 |
+
|
| 371 |
+
---
|
| 372 |
+
|
| 373 |
+
**ساخته شده با ❤️ برای توسعهدهندگان ایرانی**
|
| 374 |
+
|
TREE_STRUCTURE.txt
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
🌳 ساختار درختی پروژه Crypto Monitor
|
| 2 |
+
═══════════════════════════════════════════════════════════════
|
| 3 |
+
|
| 4 |
+
crypto-monitor-hf-full-fixed-v4-realapis/
|
| 5 |
+
│
|
| 6 |
+
├─ 📄 سرور اصلی (فقط این را اجرا کنید!)
|
| 7 |
+
│ └─ ✅ api_server_extended.py
|
| 8 |
+
│
|
| 9 |
+
├─ 📦 فایلهای پیکربندی (Config Files)
|
| 10 |
+
│ ├─ ✅ providers_config_extended.json ← ProviderManager
|
| 11 |
+
│ ├─ ✅ providers_config_ultimate.json ← ResourceManager
|
| 12 |
+
│ ├─ ✅ crypto_resources_unified_2025-11-11.json ← UnifiedConfigLoader
|
| 13 |
+
│ ├─ ✅ all_apis_merged_2025.json ← UnifiedConfigLoader
|
| 14 |
+
│ └─ ✅ ultimate_crypto_pipeline_2025_NZasinich.json ← UnifiedConfigLoader
|
| 15 |
+
│
|
| 16 |
+
├─ 🎨 رابط کاربری (Frontend)
|
| 17 |
+
│ ├─ ✅ unified_dashboard.html ← داشبورد اصلی
|
| 18 |
+
│ ├─ ✅ static/
|
| 19 |
+
│ │ ├─ css/
|
| 20 |
+
│ │ │ └─ connection-status.css
|
| 21 |
+
│ │ └─ js/
|
| 22 |
+
│ │ └─ websocket-client.js
|
| 23 |
+
│ └─ ⚠️ index.html, dashboard.html, ... (قدیمی)
|
| 24 |
+
│
|
| 25 |
+
├─ 🔧 ماژولهای اصلی (Core)
|
| 26 |
+
│ ├─ ✅ provider_manager.py ← مدیریت Providerها
|
| 27 |
+
│ ├─ ✅ resource_manager.py ← مدیریت منابع
|
| 28 |
+
│ └─ ✅ log_manager.py ← مدیریت لاگها
|
| 29 |
+
│
|
| 30 |
+
├─ 🛠️ سرویسهای بکند (Backend Services)
|
| 31 |
+
│ └─ backend/
|
| 32 |
+
│ └─ services/
|
| 33 |
+
│ ├─ ✅ auto_discovery_service.py ← جستجوی خودکار
|
| 34 |
+
│ ├─ ✅ connection_manager.py ← مدیریت WebSocket
|
| 35 |
+
│ ├─ ✅ diagnostics_service.py ← اشکالیابی
|
| 36 |
+
│ ├─ ✅ unified_config_loader.py ← بارگذاری یکپارچه
|
| 37 |
+
│ ├─ ✅ scheduler_service.py ← زمانبندی
|
| 38 |
+
│ ├─ ✅ persistence_service.py ← ذخیرهسازی
|
| 39 |
+
│ ├─ ✅ websocket_service.py ← سرویس WebSocket
|
| 40 |
+
│ ├─ ✅ ws_service_manager.py ← مدیریت WS
|
| 41 |
+
│ ├─ ✅ hf_client.py ← کلاینت HuggingFace
|
| 42 |
+
│ └─ ✅ hf_registry.py ← رجیستری مدلها
|
| 43 |
+
│
|
| 44 |
+
├─ 📡 API Routers
|
| 45 |
+
│ └─ backend/routers/
|
| 46 |
+
│ ├─ ✅ integrated_api.py
|
| 47 |
+
│ └─ ✅ hf_connect.py
|
| 48 |
+
│
|
| 49 |
+
├─ 📁 دادهها و لاگها
|
| 50 |
+
│ ├─ data/ ← ذخیره دادهها
|
| 51 |
+
│ └─ logs/ ← ذخیره لاگها
|
| 52 |
+
│
|
| 53 |
+
├─ 🧪 تستها
|
| 54 |
+
│ ├─ ✅ test_websocket.html
|
| 55 |
+
│ └─ ✅ test_websocket_dashboard.html
|
| 56 |
+
│
|
| 57 |
+
└─ 📚 مستندات
|
| 58 |
+
├─ ✅ PROJECT_STRUCTURE_FA.md ← این فایل!
|
| 59 |
+
├─ ✅ QUICK_REFERENCE_FA.md ← مرجع سریع
|
| 60 |
+
├─ ✅ README.md
|
| 61 |
+
├─ ✅ WEBSOCKET_GUIDE.md
|
| 62 |
+
└─ ... (سایر مستندات)
|
| 63 |
+
|
| 64 |
+
═══════════════════════════════════════════════════════════════
|
| 65 |
+
|
| 66 |
+
🔗 جریان داده (Data Flow)
|
| 67 |
+
═══════════════════════════════════════════════════════════════
|
| 68 |
+
|
| 69 |
+
Startup:
|
| 70 |
+
api_server_extended.py
|
| 71 |
+
│
|
| 72 |
+
├─→ ProviderManager
|
| 73 |
+
│ └─→ providers_config_extended.json
|
| 74 |
+
│
|
| 75 |
+
├─→ ResourceManager
|
| 76 |
+
│ └─→ providers_config_ultimate.json
|
| 77 |
+
│
|
| 78 |
+
└─→ UnifiedConfigLoader
|
| 79 |
+
├─→ crypto_resources_unified_2025-11-11.json
|
| 80 |
+
├─→ all_apis_merged_2025.json
|
| 81 |
+
└─→ ultimate_crypto_pipeline_2025_NZasinich.json
|
| 82 |
+
|
| 83 |
+
Runtime:
|
| 84 |
+
Client Request
|
| 85 |
+
│
|
| 86 |
+
├─→ ProviderManager.get_provider()
|
| 87 |
+
├─→ ProviderPool.get_data()
|
| 88 |
+
└─→ Response
|
| 89 |
+
|
| 90 |
+
WebSocket:
|
| 91 |
+
Client Connect
|
| 92 |
+
│
|
| 93 |
+
└─→ ConnectionManager
|
| 94 |
+
├─→ Track Session
|
| 95 |
+
├─→ Broadcast Updates
|
| 96 |
+
└─→ Heartbeat
|
| 97 |
+
|
| 98 |
+
Auto-Discovery:
|
| 99 |
+
Scheduled Task
|
| 100 |
+
│
|
| 101 |
+
└─→ AutoDiscoveryService
|
| 102 |
+
├─→ Search (DuckDuckGo)
|
| 103 |
+
├─→ Analyze (HuggingFace)
|
| 104 |
+
└─→ Add to ResourceManager
|
| 105 |
+
|
| 106 |
+
═══════════════════════════════════════════════════════════════
|
| 107 |
+
|
| 108 |
+
📊 جدول فایلهای Config
|
| 109 |
+
═══════════════════════════════════════════════════════════════
|
| 110 |
+
|
| 111 |
+
┌─────────────────────────────────────┬──────────────────────┬─────────────┐
|
| 112 |
+
│ فایل Config │ استفاده شده توسط │ تعداد API │
|
| 113 |
+
├─────────────────────────────────────┼──────────────────────┼─────────────┤
|
| 114 |
+
│ providers_config_extended.json │ ProviderManager │ ~100 │
|
| 115 |
+
│ providers_config_ultimate.json │ ResourceManager │ ~200 │
|
| 116 |
+
│ crypto_resources_unified_2025-... │ UnifiedConfigLoader │ 200+ │
|
| 117 |
+
│ all_apis_merged_2025.json │ UnifiedConfigLoader │ متغیر │
|
| 118 |
+
│ ultimate_crypto_pipeline_2025... │ UnifiedConfigLoader │ متغیر │
|
| 119 |
+
└─────────────────────────────────────┴──────────────────────┴─────────────┘
|
| 120 |
+
|
| 121 |
+
═══════════════════════════════════════════════════════════════
|
| 122 |
+
|
| 123 |
+
🎯 خلاصه: کدام فایل برای چه کاری؟
|
| 124 |
+
═══════════════════════════════════════════════════════════════
|
| 125 |
+
|
| 126 |
+
✅ برای اجرای برنامه:
|
| 127 |
+
→ python api_server_extended.py
|
| 128 |
+
|
| 129 |
+
✅ برای ویرایش Providerها:
|
| 130 |
+
→ providers_config_extended.json (ProviderManager)
|
| 131 |
+
→ providers_config_ultimate.json (ResourceManager)
|
| 132 |
+
|
| 133 |
+
✅ برای مشاهده داشبورد:
|
| 134 |
+
→ unified_dashboard.html
|
| 135 |
+
|
| 136 |
+
✅ برای اضافه کردن Provider جدید:
|
| 137 |
+
→ استفاده از API: POST /api/resources
|
| 138 |
+
→ یا ویرایش مستقیم فایلهای Config
|
| 139 |
+
|
| 140 |
+
═══════════════════════════════════════════════════════════════
|
| 141 |
+
|
| 142 |
+
⚠️ فایلهای قدیمی (استفاده نمیشوند - میتوانید حذف کنید)
|
| 143 |
+
═══════════════════════════════════════════════════════════════
|
| 144 |
+
|
| 145 |
+
❌ main.py
|
| 146 |
+
❌ app.py
|
| 147 |
+
❌ enhanced_server.py
|
| 148 |
+
❌ production_server.py
|
| 149 |
+
❌ real_server.py
|
| 150 |
+
❌ simple_server.py
|
| 151 |
+
❌ index.html
|
| 152 |
+
❌ dashboard.html
|
| 153 |
+
❌ enhanced_dashboard.html
|
| 154 |
+
❌ admin.html
|
| 155 |
+
❌ config.py
|
| 156 |
+
❌ scheduler.py
|
| 157 |
+
|
| 158 |
+
═══════════════════════════════════════════════════════════════
|
| 159 |
+
|
WEBSOCKET_GUIDE.md
ADDED
|
@@ -0,0 +1,446 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 📡 راهنمای استفاده از WebSocket API
|
| 2 |
+
|
| 3 |
+
## 🎯 مقدمه
|
| 4 |
+
|
| 5 |
+
این سیستم از WebSocket برای ارتباط بلادرنگ (Real-time) بین سرور و کلاینت استفاده میکند که سرعت و کارایی بسیار بالاتری نسبت به HTTP polling دارد.
|
| 6 |
+
|
| 7 |
+
## 🚀 مزایای WebSocket نسبت به HTTP
|
| 8 |
+
|
| 9 |
+
| ویژگی | HTTP Polling | WebSocket |
|
| 10 |
+
|-------|--------------|-----------|
|
| 11 |
+
| سرعت | کند (1-5 ثانیه تاخیر) | فوری (< 100ms) |
|
| 12 |
+
| منابع سرور | بالا | پایین |
|
| 13 |
+
| پهنای باند | زیاد | کم |
|
| 14 |
+
| اتصال | Multiple | Single (دائمی) |
|
| 15 |
+
| Overhead | بالا (headers هر بار) | خیلی کم |
|
| 16 |
+
|
| 17 |
+
## 📦 فایلهای اضافه شده
|
| 18 |
+
|
| 19 |
+
### Backend:
|
| 20 |
+
- `backend/services/connection_manager.py` - مدیریت اتصالات WebSocket
|
| 21 |
+
- تغییرات در `api_server_extended.py` - اضافه شدن endpointهای WebSocket
|
| 22 |
+
|
| 23 |
+
### Frontend:
|
| 24 |
+
- `static/js/websocket-client.js` - کلاینت JavaScript
|
| 25 |
+
- `static/css/connection-status.css` - استایلهای بصری
|
| 26 |
+
- `test_websocket.html` - صفحه تست
|
| 27 |
+
|
| 28 |
+
## 🔌 اتصال به WebSocket
|
| 29 |
+
|
| 30 |
+
### از JavaScript:
|
| 31 |
+
|
| 32 |
+
```javascript
|
| 33 |
+
// استفاده از کلاینت آماده
|
| 34 |
+
const wsClient = new CryptoWebSocketClient();
|
| 35 |
+
|
| 36 |
+
// یا اتصال دستی
|
| 37 |
+
const ws = new WebSocket('ws://localhost:8000/ws');
|
| 38 |
+
|
| 39 |
+
ws.onopen = () => {
|
| 40 |
+
console.log('متصل شد!');
|
| 41 |
+
};
|
| 42 |
+
|
| 43 |
+
ws.onmessage = (event) => {
|
| 44 |
+
const data = JSON.parse(event.data);
|
| 45 |
+
console.log('پیام دریافت شد:', data);
|
| 46 |
+
};
|
| 47 |
+
```
|
| 48 |
+
|
| 49 |
+
### از Python:
|
| 50 |
+
|
| 51 |
+
```python
|
| 52 |
+
import asyncio
|
| 53 |
+
import websockets
|
| 54 |
+
import json
|
| 55 |
+
|
| 56 |
+
async def connect():
|
| 57 |
+
uri = "ws://localhost:8000/ws"
|
| 58 |
+
async with websockets.connect(uri) as websocket:
|
| 59 |
+
# دریافت پیام welcome
|
| 60 |
+
welcome = await websocket.recv()
|
| 61 |
+
print(f"دریافت: {welcome}")
|
| 62 |
+
|
| 63 |
+
# ارسال پیام
|
| 64 |
+
await websocket.send(json.dumps({
|
| 65 |
+
"type": "subscribe",
|
| 66 |
+
"group": "market"
|
| 67 |
+
}))
|
| 68 |
+
|
| 69 |
+
# دریافت پیامها
|
| 70 |
+
async for message in websocket:
|
| 71 |
+
data = json.loads(message)
|
| 72 |
+
print(f"داده جدید: {data}")
|
| 73 |
+
|
| 74 |
+
asyncio.run(connect())
|
| 75 |
+
```
|
| 76 |
+
|
| 77 |
+
## 📨 انواع پیامها
|
| 78 |
+
|
| 79 |
+
### 1. پیامهای سیستمی (Server → Client)
|
| 80 |
+
|
| 81 |
+
#### Welcome Message
|
| 82 |
+
```json
|
| 83 |
+
{
|
| 84 |
+
"type": "welcome",
|
| 85 |
+
"session_id": "550e8400-e29b-41d4-a716-446655440000",
|
| 86 |
+
"message": "به سیستم مانیتورینگ کریپتو خوش آمدید",
|
| 87 |
+
"timestamp": "2024-01-15T10:30:00"
|
| 88 |
+
}
|
| 89 |
+
```
|
| 90 |
+
|
| 91 |
+
#### Stats Update (هر 30 ثانیه)
|
| 92 |
+
```json
|
| 93 |
+
{
|
| 94 |
+
"type": "stats_update",
|
| 95 |
+
"data": {
|
| 96 |
+
"active_connections": 15,
|
| 97 |
+
"total_sessions": 23,
|
| 98 |
+
"messages_sent": 1250,
|
| 99 |
+
"messages_received": 450,
|
| 100 |
+
"client_types": {
|
| 101 |
+
"browser": 12,
|
| 102 |
+
"api": 2,
|
| 103 |
+
"mobile": 1
|
| 104 |
+
},
|
| 105 |
+
"subscriptions": {
|
| 106 |
+
"market": 8,
|
| 107 |
+
"prices": 10,
|
| 108 |
+
"all": 15
|
| 109 |
+
}
|
| 110 |
+
},
|
| 111 |
+
"timestamp": "2024-01-15T10:30:30"
|
| 112 |
+
}
|
| 113 |
+
```
|
| 114 |
+
|
| 115 |
+
#### Provider Stats
|
| 116 |
+
```json
|
| 117 |
+
{
|
| 118 |
+
"type": "provider_stats",
|
| 119 |
+
"data": {
|
| 120 |
+
"summary": {
|
| 121 |
+
"total_providers": 150,
|
| 122 |
+
"online": 142,
|
| 123 |
+
"offline": 8,
|
| 124 |
+
"overall_success_rate": 95.5
|
| 125 |
+
}
|
| 126 |
+
},
|
| 127 |
+
"timestamp": "2024-01-15T10:30:30"
|
| 128 |
+
}
|
| 129 |
+
```
|
| 130 |
+
|
| 131 |
+
#### Market Update
|
| 132 |
+
```json
|
| 133 |
+
{
|
| 134 |
+
"type": "market_update",
|
| 135 |
+
"data": {
|
| 136 |
+
"btc": { "price": 43250, "change_24h": 2.5 },
|
| 137 |
+
"eth": { "price": 2280, "change_24h": -1.2 }
|
| 138 |
+
},
|
| 139 |
+
"timestamp": "2024-01-15T10:30:45"
|
| 140 |
+
}
|
| 141 |
+
```
|
| 142 |
+
|
| 143 |
+
#### Price Update
|
| 144 |
+
```json
|
| 145 |
+
{
|
| 146 |
+
"type": "price_update",
|
| 147 |
+
"data": {
|
| 148 |
+
"symbol": "BTC",
|
| 149 |
+
"price": 43250.50,
|
| 150 |
+
"change_24h": 2.35
|
| 151 |
+
},
|
| 152 |
+
"timestamp": "2024-01-15T10:30:50"
|
| 153 |
+
}
|
| 154 |
+
```
|
| 155 |
+
|
| 156 |
+
#### Alert
|
| 157 |
+
```json
|
| 158 |
+
{
|
| 159 |
+
"type": "alert",
|
| 160 |
+
"data": {
|
| 161 |
+
"alert_type": "price_threshold",
|
| 162 |
+
"message": "قیمت بیتکوین از ۴۵۰۰۰ دلار عبور کرد",
|
| 163 |
+
"severity": "info"
|
| 164 |
+
},
|
| 165 |
+
"timestamp": "2024-01-15T10:31:00"
|
| 166 |
+
}
|
| 167 |
+
```
|
| 168 |
+
|
| 169 |
+
#### Heartbeat
|
| 170 |
+
```json
|
| 171 |
+
{
|
| 172 |
+
"type": "heartbeat",
|
| 173 |
+
"timestamp": "2024-01-15T10:31:10"
|
| 174 |
+
}
|
| 175 |
+
```
|
| 176 |
+
|
| 177 |
+
### 2. پیامهای کلاینت (Client → Server)
|
| 178 |
+
|
| 179 |
+
#### Subscribe
|
| 180 |
+
```json
|
| 181 |
+
{
|
| 182 |
+
"type": "subscribe",
|
| 183 |
+
"group": "market"
|
| 184 |
+
}
|
| 185 |
+
```
|
| 186 |
+
|
| 187 |
+
گروههای موجود:
|
| 188 |
+
- `market` - بهروزرسانیهای بازار
|
| 189 |
+
- `prices` - تغییرات قیمت
|
| 190 |
+
- `news` - اخبار
|
| 191 |
+
- `alerts` - هشدارها
|
| 192 |
+
- `all` - همه
|
| 193 |
+
|
| 194 |
+
#### Unsubscribe
|
| 195 |
+
```json
|
| 196 |
+
{
|
| 197 |
+
"type": "unsubscribe",
|
| 198 |
+
"group": "market"
|
| 199 |
+
}
|
| 200 |
+
```
|
| 201 |
+
|
| 202 |
+
#### Request Stats
|
| 203 |
+
```json
|
| 204 |
+
{
|
| 205 |
+
"type": "get_stats"
|
| 206 |
+
}
|
| 207 |
+
```
|
| 208 |
+
|
| 209 |
+
#### Ping
|
| 210 |
+
```json
|
| 211 |
+
{
|
| 212 |
+
"type": "ping"
|
| 213 |
+
}
|
| 214 |
+
```
|
| 215 |
+
|
| 216 |
+
## 🎨 استفاده از کامپوننتهای بصری
|
| 217 |
+
|
| 218 |
+
### 1. نوار وضعیت اتصال
|
| 219 |
+
|
| 220 |
+
```html
|
| 221 |
+
<!-- اضافه کردن به صفحه -->
|
| 222 |
+
<div class="connection-status-bar" id="ws-connection-status">
|
| 223 |
+
<div class="ws-connection-info">
|
| 224 |
+
<span class="status-dot status-dot-offline" id="ws-status-dot"></span>
|
| 225 |
+
<span class="ws-status-text" id="ws-status-text">در حال اتصال...</span>
|
| 226 |
+
</div>
|
| 227 |
+
|
| 228 |
+
<div class="online-users-widget">
|
| 229 |
+
<div class="online-users-count">
|
| 230 |
+
<span class="users-icon">👥</span>
|
| 231 |
+
<span class="count-number" id="active-users-count">0</span>
|
| 232 |
+
<span class="count-label">کاربر آنلاین</span>
|
| 233 |
+
</div>
|
| 234 |
+
</div>
|
| 235 |
+
</div>
|
| 236 |
+
```
|
| 237 |
+
|
| 238 |
+
### 2. اضافه کردن CSS و JS
|
| 239 |
+
|
| 240 |
+
```html
|
| 241 |
+
<head>
|
| 242 |
+
<link rel="stylesheet" href="/static/css/connection-status.css">
|
| 243 |
+
</head>
|
| 244 |
+
<body>
|
| 245 |
+
<!-- محتوا -->
|
| 246 |
+
|
| 247 |
+
<script src="/static/js/websocket-client.js"></script>
|
| 248 |
+
</body>
|
| 249 |
+
```
|
| 250 |
+
|
| 251 |
+
### 3. استفاده از Client
|
| 252 |
+
|
| 253 |
+
```javascript
|
| 254 |
+
// کلاینت به صورت خودکار متصل میشود
|
| 255 |
+
// در دسترس از طریق window.wsClient
|
| 256 |
+
|
| 257 |
+
// ثبت handler سفارشی
|
| 258 |
+
window.wsClient.on('custom_event', (message) => {
|
| 259 |
+
console.log('رویداد سفارشی:', message);
|
| 260 |
+
});
|
| 261 |
+
|
| 262 |
+
// اتصال به وضعیت اتصال
|
| 263 |
+
window.wsClient.onConnection((isConnected) => {
|
| 264 |
+
if (isConnected) {
|
| 265 |
+
console.log('✅ متصل شد');
|
| 266 |
+
} else {
|
| 267 |
+
console.log('❌ قطع شد');
|
| 268 |
+
}
|
| 269 |
+
});
|
| 270 |
+
|
| 271 |
+
// ارسال پیام
|
| 272 |
+
window.wsClient.send({
|
| 273 |
+
type: 'custom_action',
|
| 274 |
+
data: { value: 123 }
|
| 275 |
+
});
|
| 276 |
+
```
|
| 277 |
+
|
| 278 |
+
## 🔧 API Endpoints
|
| 279 |
+
|
| 280 |
+
### GET `/api/sessions`
|
| 281 |
+
دریافت لیست sessionهای فعال
|
| 282 |
+
|
| 283 |
+
**Response:**
|
| 284 |
+
```json
|
| 285 |
+
{
|
| 286 |
+
"sessions": {
|
| 287 |
+
"550e8400-...": {
|
| 288 |
+
"session_id": "550e8400-...",
|
| 289 |
+
"client_type": "browser",
|
| 290 |
+
"connected_at": "2024-01-15T10:00:00",
|
| 291 |
+
"last_activity": "2024-01-15T10:30:00"
|
| 292 |
+
}
|
| 293 |
+
},
|
| 294 |
+
"stats": {
|
| 295 |
+
"active_connections": 15,
|
| 296 |
+
"total_sessions": 23
|
| 297 |
+
}
|
| 298 |
+
}
|
| 299 |
+
```
|
| 300 |
+
|
| 301 |
+
### GET `/api/sessions/stats`
|
| 302 |
+
دریافت آمار اتصالات
|
| 303 |
+
|
| 304 |
+
**Response:**
|
| 305 |
+
```json
|
| 306 |
+
{
|
| 307 |
+
"active_connections": 15,
|
| 308 |
+
"total_sessions": 23,
|
| 309 |
+
"messages_sent": 1250,
|
| 310 |
+
"messages_received": 450,
|
| 311 |
+
"client_types": {
|
| 312 |
+
"browser": 12,
|
| 313 |
+
"api": 2
|
| 314 |
+
}
|
| 315 |
+
}
|
| 316 |
+
```
|
| 317 |
+
|
| 318 |
+
### POST `/api/broadcast`
|
| 319 |
+
ارسال پیام به همه کلاینتها
|
| 320 |
+
|
| 321 |
+
**Request:**
|
| 322 |
+
```json
|
| 323 |
+
{
|
| 324 |
+
"message": {
|
| 325 |
+
"type": "notification",
|
| 326 |
+
"text": "سیستم بهروز شد"
|
| 327 |
+
},
|
| 328 |
+
"group": "all"
|
| 329 |
+
}
|
| 330 |
+
```
|
| 331 |
+
|
| 332 |
+
## 🧪 تست
|
| 333 |
+
|
| 334 |
+
### 1. باز کردن صفحه تست:
|
| 335 |
+
```
|
| 336 |
+
http://localhost:8000/test_websocket.html
|
| 337 |
+
```
|
| 338 |
+
|
| 339 |
+
### 2. چک کردن اتصال:
|
| 340 |
+
- نوار بالای صفحه باید سبز شود (متصل)
|
| 341 |
+
- تعداد کاربران آنلاین باید نمایش داده شود
|
| 342 |
+
|
| 343 |
+
### 3. تست دستورات:
|
| 344 |
+
- کلیک روی دکمههای مختلف
|
| 345 |
+
- مشاهده لاگ پیامها در پنل پایین
|
| 346 |
+
|
| 347 |
+
### 4. تست چند تب:
|
| 348 |
+
- باز کردن چند تب مرورگر
|
| 349 |
+
- تعداد کاربران آنلاین باید افزایش یابد
|
| 350 |
+
|
| 351 |
+
## 📊 مانیتورینگ
|
| 352 |
+
|
| 353 |
+
### لاگهای سرور:
|
| 354 |
+
```bash
|
| 355 |
+
# مشاهده لاگهای WebSocket
|
| 356 |
+
tail -f logs/app.log | grep "WebSocket"
|
| 357 |
+
```
|
| 358 |
+
|
| 359 |
+
### متریکها:
|
| 360 |
+
- تعداد اتصالات فعال
|
| 361 |
+
- تعداد کل sessionها
|
| 362 |
+
- پیامهای ارسالی/دریافتی
|
| 363 |
+
- توزیع انواع کلاینت
|
| 364 |
+
|
| 365 |
+
## 🔒 امنیت
|
| 366 |
+
|
| 367 |
+
### توصیهها:
|
| 368 |
+
1. برای production از `wss://` (WebSocket Secure) استفاده کنید
|
| 369 |
+
2. محدودیت تعداد اتصال برای هر IP
|
| 370 |
+
3. Rate limiting برای پیامها
|
| 371 |
+
4. اعتبارسنجی token برای authentication
|
| 372 |
+
|
| 373 |
+
### مثال با Token:
|
| 374 |
+
```javascript
|
| 375 |
+
const ws = new WebSocket('ws://localhost:8000/ws');
|
| 376 |
+
ws.onopen = () => {
|
| 377 |
+
ws.send(JSON.stringify({
|
| 378 |
+
type: 'auth',
|
| 379 |
+
token: 'YOUR_JWT_TOKEN'
|
| 380 |
+
}));
|
| 381 |
+
};
|
| 382 |
+
```
|
| 383 |
+
|
| 384 |
+
## 🐛 عیبیابی
|
| 385 |
+
|
| 386 |
+
### مشکل: اتصال برقرار نمیشود
|
| 387 |
+
```bash
|
| 388 |
+
# چک کردن اجرای سرور
|
| 389 |
+
curl http://localhost:8000/health
|
| 390 |
+
|
| 391 |
+
# بررسی پورت
|
| 392 |
+
netstat -an | grep 8000
|
| 393 |
+
```
|
| 394 |
+
|
| 395 |
+
### مشکل: اتصال قطع میشود
|
| 396 |
+
- Heartbeat فعال است؟
|
| 397 |
+
- Proxy یا Firewall مشکل ندارد؟
|
| 398 |
+
- Logهای سرور را بررسی کنید
|
| 399 |
+
|
| 400 |
+
### مشکل: پیامها دریافت نمیشوند
|
| 401 |
+
- Subscribe کردهاید؟
|
| 402 |
+
- نوع پیام صحیح است؟
|
| 403 |
+
- کنسول مرورگر را بررسی کنید
|
| 404 |
+
|
| 405 |
+
## 📚 منابع بیشتر
|
| 406 |
+
|
| 407 |
+
- [WebSocket API - MDN](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket)
|
| 408 |
+
- [FastAPI WebSockets](https://fastapi.tiangolo.com/advanced/websockets/)
|
| 409 |
+
- [websockets Python library](https://websockets.readthedocs.io/)
|
| 410 |
+
|
| 411 |
+
## 🎓 مثال کامل Integration
|
| 412 |
+
|
| 413 |
+
```html
|
| 414 |
+
<!DOCTYPE html>
|
| 415 |
+
<html lang="fa" dir="rtl">
|
| 416 |
+
<head>
|
| 417 |
+
<link rel="stylesheet" href="/static/css/connection-status.css">
|
| 418 |
+
</head>
|
| 419 |
+
<body>
|
| 420 |
+
<!-- UI Components -->
|
| 421 |
+
<div class="connection-status-bar" id="ws-connection-status">
|
| 422 |
+
<!-- ... -->
|
| 423 |
+
</div>
|
| 424 |
+
|
| 425 |
+
<div class="dashboard">
|
| 426 |
+
<h1>تعداد کاربران: <span id="user-count">0</span></h1>
|
| 427 |
+
</div>
|
| 428 |
+
|
| 429 |
+
<script src="/static/js/websocket-client.js"></script>
|
| 430 |
+
<script>
|
| 431 |
+
// Custom logic
|
| 432 |
+
if (window.wsClient) {
|
| 433 |
+
window.wsClient.on('stats_update', (msg) => {
|
| 434 |
+
document.getElementById('user-count').textContent =
|
| 435 |
+
msg.data.active_connections;
|
| 436 |
+
});
|
| 437 |
+
}
|
| 438 |
+
</script>
|
| 439 |
+
</body>
|
| 440 |
+
</html>
|
| 441 |
+
```
|
| 442 |
+
|
| 443 |
+
---
|
| 444 |
+
|
| 445 |
+
**نکته مهم:** این سیستم به صورت خودکار reconnect میکند و نیازی به مدیریت دستی ندارید!
|
| 446 |
+
|
__pycache__/database.cpython-313.pyc
ADDED
|
Binary file (36.3 kB). View file
|
|
|
__pycache__/monitor.cpython-313.pyc
ADDED
|
Binary file (17.6 kB). View file
|
|
|
api_server_extended.py
ADDED
|
@@ -0,0 +1,1182 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
API Server Extended - سرور FastAPI با پشتیبانی کامل از Provider Management
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from fastapi import FastAPI, HTTPException, BackgroundTasks
|
| 7 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 8 |
+
from fastapi.staticfiles import StaticFiles
|
| 9 |
+
from fastapi.responses import FileResponse, JSONResponse
|
| 10 |
+
from pydantic import BaseModel
|
| 11 |
+
from typing import Optional, List, Dict, Any
|
| 12 |
+
from datetime import datetime, timedelta
|
| 13 |
+
from pathlib import Path
|
| 14 |
+
import asyncio
|
| 15 |
+
import uvicorn
|
| 16 |
+
|
| 17 |
+
from provider_manager import ProviderManager, RotationStrategy, Provider, ProviderPool
|
| 18 |
+
from log_manager import LogManager, LogLevel, LogCategory, get_log_manager
|
| 19 |
+
from resource_manager import ResourceManager
|
| 20 |
+
from backend.services.connection_manager import get_connection_manager, ConnectionManager
|
| 21 |
+
from backend.services.auto_discovery_service import AutoDiscoveryService
|
| 22 |
+
from backend.services.diagnostics_service import DiagnosticsService
|
| 23 |
+
|
| 24 |
+
# ایجاد اپلیکیشن FastAPI
|
| 25 |
+
app = FastAPI(
|
| 26 |
+
title="Crypto Monitor Extended API",
|
| 27 |
+
description="API کامل برای مانیتورینگ کریپتو با پشتیبانی از Provider Pools",
|
| 28 |
+
version="3.0.0"
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
# CORS Middleware
|
| 32 |
+
app.add_middleware(
|
| 33 |
+
CORSMiddleware,
|
| 34 |
+
allow_origins=["*"],
|
| 35 |
+
allow_credentials=True,
|
| 36 |
+
allow_methods=["*"],
|
| 37 |
+
allow_headers=["*"],
|
| 38 |
+
)
|
| 39 |
+
|
| 40 |
+
# مدیر ارائهدهندگان
|
| 41 |
+
manager = ProviderManager()
|
| 42 |
+
|
| 43 |
+
# مدیر لاگها
|
| 44 |
+
log_manager = get_log_manager()
|
| 45 |
+
|
| 46 |
+
# مدیر منابع
|
| 47 |
+
resource_manager = ResourceManager()
|
| 48 |
+
|
| 49 |
+
# مدیر اتصالات WebSocket
|
| 50 |
+
conn_manager = get_connection_manager()
|
| 51 |
+
|
| 52 |
+
# سرویس کشف خودکار منابع
|
| 53 |
+
auto_discovery_service = AutoDiscoveryService(resource_manager, manager)
|
| 54 |
+
|
| 55 |
+
# سرویس اشکالیابی و تعمیر خودکار
|
| 56 |
+
diagnostics_service = DiagnosticsService(resource_manager, manager, auto_discovery_service)
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
class StartupValidationError(RuntimeError):
|
| 60 |
+
"""خطای مربوط به بررسی راهاندازی"""
|
| 61 |
+
pass
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
async def run_startup_validation():
|
| 65 |
+
"""مجموعه بررسیهای اولیه برای اطمینان از آماده بودن سرویس"""
|
| 66 |
+
issues: List[str] = []
|
| 67 |
+
|
| 68 |
+
required_files = [
|
| 69 |
+
Path("providers_config_extended.json"),
|
| 70 |
+
Path("providers_config_ultimate.json"),
|
| 71 |
+
Path("crypto_resources_unified_2025-11-11.json"),
|
| 72 |
+
]
|
| 73 |
+
for file_path in required_files:
|
| 74 |
+
if not file_path.exists():
|
| 75 |
+
issues.append(f"فایل ضروری یافت نشد: {file_path}")
|
| 76 |
+
|
| 77 |
+
required_dirs = [Path("data"), Path("data/exports"), Path("logs")]
|
| 78 |
+
for directory in required_dirs:
|
| 79 |
+
if not directory.exists():
|
| 80 |
+
try:
|
| 81 |
+
directory.mkdir(parents=True, exist_ok=True)
|
| 82 |
+
except Exception as exc:
|
| 83 |
+
issues.append(f"امکان ساخت دایرکتوری {directory} وجود ندارد: {exc}")
|
| 84 |
+
|
| 85 |
+
try:
|
| 86 |
+
stats = resource_manager.get_statistics()
|
| 87 |
+
if stats.get("total_providers", 0) == 0:
|
| 88 |
+
issues.append("هیچ ارائهدهندهای در پیکربندی منابع یافت نشد.")
|
| 89 |
+
except Exception as exc:
|
| 90 |
+
issues.append(f"دسترسی به ResourceManager با خطا مواجه شد: {exc}")
|
| 91 |
+
|
| 92 |
+
if not manager.providers:
|
| 93 |
+
issues.append("هیچ ارائهدهندهای در ProviderManager بارگذاری نشده است.")
|
| 94 |
+
else:
|
| 95 |
+
sample_providers = list(manager.providers.values())[:5]
|
| 96 |
+
try:
|
| 97 |
+
health_results = await asyncio.gather(*(manager.health_check(provider) for provider in sample_providers))
|
| 98 |
+
success_count = sum(1 for result in health_results if result)
|
| 99 |
+
if success_count == 0:
|
| 100 |
+
issues.append("هیچ ارائهدهندهای در تست سلامت اولیه موفق نبود.")
|
| 101 |
+
except Exception as exc:
|
| 102 |
+
issues.append(f"اجرای تست سلامت اولیه با خطا مواجه شد: {exc}")
|
| 103 |
+
|
| 104 |
+
if manager.session is None:
|
| 105 |
+
await manager.init_session()
|
| 106 |
+
|
| 107 |
+
critical_endpoints = [
|
| 108 |
+
("CoinGecko", "https://api.coingecko.com/api/v3/ping"),
|
| 109 |
+
("Etherscan", "https://api.etherscan.io/api?module=stats&action=ethsupply"),
|
| 110 |
+
("Binance", "https://api.binance.com/api/v3/ping"),
|
| 111 |
+
]
|
| 112 |
+
failures = 0
|
| 113 |
+
for name, url in critical_endpoints:
|
| 114 |
+
try:
|
| 115 |
+
async with manager.session.get(url, timeout=10) as response:
|
| 116 |
+
if response.status >= 500:
|
| 117 |
+
issues.append(f"پاسخ نامعتبر از سرویس {name}: status={response.status}")
|
| 118 |
+
failures += 1
|
| 119 |
+
except Exception as exc:
|
| 120 |
+
issues.append(f"عدم دسترسی به سرویس {name}: {exc}")
|
| 121 |
+
failures += 1
|
| 122 |
+
if failures == len(critical_endpoints):
|
| 123 |
+
issues.append("اتصال به سرویسهای ک��یدی برقرار نشد. اتصال اینترنت را بررسی کنید.")
|
| 124 |
+
|
| 125 |
+
if issues:
|
| 126 |
+
for issue in issues:
|
| 127 |
+
log_manager.add_log(
|
| 128 |
+
LogLevel.CRITICAL,
|
| 129 |
+
LogCategory.SYSTEM,
|
| 130 |
+
"Startup validation issue",
|
| 131 |
+
extra_data={"detail": issue},
|
| 132 |
+
)
|
| 133 |
+
raise StartupValidationError("Startup validation failed. جزئیات در لاگها موجود است.")
|
| 134 |
+
|
| 135 |
+
log_manager.add_log(
|
| 136 |
+
LogLevel.INFO,
|
| 137 |
+
LogCategory.SYSTEM,
|
| 138 |
+
"Startup validation passed",
|
| 139 |
+
extra_data={"checked_providers": min(len(manager.providers), 5)},
|
| 140 |
+
)
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
# ===== Pydantic Models =====
|
| 144 |
+
|
| 145 |
+
class PoolCreateRequest(BaseModel):
|
| 146 |
+
name: str
|
| 147 |
+
category: str
|
| 148 |
+
rotation_strategy: str
|
| 149 |
+
description: Optional[str] = None
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
class PoolMemberRequest(BaseModel):
|
| 153 |
+
provider_id: str
|
| 154 |
+
priority: int = 5
|
| 155 |
+
weight: int = 50
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
class RotateRequest(BaseModel):
|
| 159 |
+
reason: str = "manual"
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
class HealthCheckResponse(BaseModel):
|
| 163 |
+
status: str
|
| 164 |
+
timestamp: str
|
| 165 |
+
providers_count: int
|
| 166 |
+
online_count: int
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
# ===== Startup/Shutdown Events =====
|
| 170 |
+
|
| 171 |
+
@app.on_event("startup")
|
| 172 |
+
async def startup_event():
|
| 173 |
+
"""رویداد شروع سرور"""
|
| 174 |
+
print("🚀 راهاندازی سرور...")
|
| 175 |
+
await manager.init_session()
|
| 176 |
+
await run_startup_validation()
|
| 177 |
+
|
| 178 |
+
# ثبت لاگ شروع
|
| 179 |
+
log_manager.add_log(
|
| 180 |
+
LogLevel.INFO,
|
| 181 |
+
LogCategory.SYSTEM,
|
| 182 |
+
"Server started",
|
| 183 |
+
extra_data={"version": "3.0.0"}
|
| 184 |
+
)
|
| 185 |
+
|
| 186 |
+
# شروع بررسی سلامت دورهای
|
| 187 |
+
asyncio.create_task(periodic_health_check())
|
| 188 |
+
await auto_discovery_service.start()
|
| 189 |
+
|
| 190 |
+
# شروع heartbeat برای WebSocket
|
| 191 |
+
asyncio.create_task(websocket_heartbeat())
|
| 192 |
+
|
| 193 |
+
print("✅ سرور آماده است")
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
@app.on_event("shutdown")
|
| 197 |
+
async def shutdown_event():
|
| 198 |
+
"""رویداد خاموش شدن سرور"""
|
| 199 |
+
print("🛑 خاموشسازی سرور...")
|
| 200 |
+
await auto_discovery_service.stop()
|
| 201 |
+
await manager.close_session()
|
| 202 |
+
print("✅ سرور خاموش شد")
|
| 203 |
+
|
| 204 |
+
|
| 205 |
+
# ===== Background Tasks =====
|
| 206 |
+
|
| 207 |
+
async def periodic_health_check():
|
| 208 |
+
"""بررسی سلامت دورهای هر ۳۰ ثانیه"""
|
| 209 |
+
while True:
|
| 210 |
+
try:
|
| 211 |
+
await asyncio.sleep(30)
|
| 212 |
+
await manager.health_check_all()
|
| 213 |
+
|
| 214 |
+
# ارسال بهروزرسانی آمار به کلاینتهای متصل
|
| 215 |
+
stats = manager.get_all_stats()
|
| 216 |
+
await conn_manager.broadcast({
|
| 217 |
+
'type': 'provider_stats',
|
| 218 |
+
'data': stats,
|
| 219 |
+
'timestamp': datetime.now().isoformat()
|
| 220 |
+
})
|
| 221 |
+
except Exception as e:
|
| 222 |
+
print(f"❌ خطا در بررسی سلامت دورهای: {e}")
|
| 223 |
+
|
| 224 |
+
|
| 225 |
+
async def websocket_heartbeat():
|
| 226 |
+
"""ارسال heartbeat هر ۱۰ ثانیه"""
|
| 227 |
+
while True:
|
| 228 |
+
try:
|
| 229 |
+
await asyncio.sleep(10)
|
| 230 |
+
await conn_manager.heartbeat()
|
| 231 |
+
except Exception as e:
|
| 232 |
+
print(f"❌ خطا در heartbeat: {e}")
|
| 233 |
+
|
| 234 |
+
|
| 235 |
+
# ===== Root Endpoints =====
|
| 236 |
+
|
| 237 |
+
@app.get("/")
|
| 238 |
+
async def root():
|
| 239 |
+
"""صفحه اصلی"""
|
| 240 |
+
return FileResponse("unified_dashboard.html")
|
| 241 |
+
|
| 242 |
+
|
| 243 |
+
@app.get("/health")
|
| 244 |
+
async def health():
|
| 245 |
+
"""بررسی سلامت سرور"""
|
| 246 |
+
stats = manager.get_all_stats()
|
| 247 |
+
conn_stats = conn_manager.get_stats()
|
| 248 |
+
|
| 249 |
+
return {
|
| 250 |
+
"status": "healthy",
|
| 251 |
+
"timestamp": datetime.now().isoformat(),
|
| 252 |
+
"providers_count": stats['summary']['total_providers'],
|
| 253 |
+
"online_count": stats['summary']['online'],
|
| 254 |
+
"connected_clients": conn_stats['active_connections'],
|
| 255 |
+
"total_sessions": conn_stats['total_sessions']
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
|
| 259 |
+
# ===== Provider Endpoints =====
|
| 260 |
+
|
| 261 |
+
@app.get("/api/providers")
|
| 262 |
+
async def get_all_providers():
|
| 263 |
+
"""دریافت لیست همه ارائهدهندگان"""
|
| 264 |
+
providers = []
|
| 265 |
+
for provider_id, provider in manager.providers.items():
|
| 266 |
+
providers.append({
|
| 267 |
+
"provider_id": provider_id,
|
| 268 |
+
"name": provider.name,
|
| 269 |
+
"category": provider.category,
|
| 270 |
+
"status": provider.status.value,
|
| 271 |
+
"success_rate": provider.success_rate,
|
| 272 |
+
"total_requests": provider.total_requests,
|
| 273 |
+
"avg_response_time": provider.avg_response_time,
|
| 274 |
+
"is_available": provider.is_available,
|
| 275 |
+
"priority": provider.priority,
|
| 276 |
+
"weight": provider.weight,
|
| 277 |
+
"requires_auth": provider.requires_auth,
|
| 278 |
+
"last_check": provider.last_check.isoformat() if provider.last_check else None,
|
| 279 |
+
"last_error": provider.last_error
|
| 280 |
+
})
|
| 281 |
+
|
| 282 |
+
return {"providers": providers, "total": len(providers)}
|
| 283 |
+
|
| 284 |
+
|
| 285 |
+
@app.get("/api/providers/{provider_id}")
|
| 286 |
+
async def get_provider(provider_id: str):
|
| 287 |
+
"""دریافت اطلاعات یک ارائهدهنده"""
|
| 288 |
+
provider = manager.get_provider(provider_id)
|
| 289 |
+
if not provider:
|
| 290 |
+
raise HTTPException(status_code=404, detail="Provider not found")
|
| 291 |
+
|
| 292 |
+
return {
|
| 293 |
+
"provider_id": provider_id,
|
| 294 |
+
"name": provider.name,
|
| 295 |
+
"category": provider.category,
|
| 296 |
+
"base_url": provider.base_url,
|
| 297 |
+
"endpoints": provider.endpoints,
|
| 298 |
+
"status": provider.status.value,
|
| 299 |
+
"success_rate": provider.success_rate,
|
| 300 |
+
"total_requests": provider.total_requests,
|
| 301 |
+
"successful_requests": provider.successful_requests,
|
| 302 |
+
"failed_requests": provider.failed_requests,
|
| 303 |
+
"avg_response_time": provider.avg_response_time,
|
| 304 |
+
"is_available": provider.is_available,
|
| 305 |
+
"priority": provider.priority,
|
| 306 |
+
"weight": provider.weight,
|
| 307 |
+
"requires_auth": provider.requires_auth,
|
| 308 |
+
"consecutive_failures": provider.consecutive_failures,
|
| 309 |
+
"circuit_breaker_open": provider.circuit_breaker_open,
|
| 310 |
+
"last_check": provider.last_check.isoformat() if provider.last_check else None,
|
| 311 |
+
"last_error": provider.last_error
|
| 312 |
+
}
|
| 313 |
+
|
| 314 |
+
|
| 315 |
+
@app.post("/api/providers/{provider_id}/health-check")
|
| 316 |
+
async def check_provider_health(provider_id: str):
|
| 317 |
+
"""بررسی سلامت یک ارائهدهنده"""
|
| 318 |
+
provider = manager.get_provider(provider_id)
|
| 319 |
+
if not provider:
|
| 320 |
+
raise HTTPException(status_code=404, detail="Provider not found")
|
| 321 |
+
|
| 322 |
+
is_healthy = await manager.health_check(provider)
|
| 323 |
+
|
| 324 |
+
return {
|
| 325 |
+
"provider_id": provider_id,
|
| 326 |
+
"name": provider.name,
|
| 327 |
+
"is_healthy": is_healthy,
|
| 328 |
+
"status": provider.status.value,
|
| 329 |
+
"response_time": provider.avg_response_time,
|
| 330 |
+
"timestamp": datetime.now().isoformat()
|
| 331 |
+
}
|
| 332 |
+
|
| 333 |
+
|
| 334 |
+
@app.get("/api/providers/category/{category}")
|
| 335 |
+
async def get_providers_by_category(category: str):
|
| 336 |
+
"""دریافت ارائهدهندگان بر اساس دستهبندی"""
|
| 337 |
+
providers = [
|
| 338 |
+
{
|
| 339 |
+
"provider_id": pid,
|
| 340 |
+
"name": p.name,
|
| 341 |
+
"status": p.status.value,
|
| 342 |
+
"is_available": p.is_available,
|
| 343 |
+
"success_rate": p.success_rate
|
| 344 |
+
}
|
| 345 |
+
for pid, p in manager.providers.items()
|
| 346 |
+
if p.category == category
|
| 347 |
+
]
|
| 348 |
+
|
| 349 |
+
return {"category": category, "providers": providers, "count": len(providers)}
|
| 350 |
+
|
| 351 |
+
|
| 352 |
+
# ===== Pool Endpoints =====
|
| 353 |
+
|
| 354 |
+
@app.get("/api/pools")
|
| 355 |
+
async def get_all_pools():
|
| 356 |
+
"""دریافت لیست همه Poolها"""
|
| 357 |
+
pools = []
|
| 358 |
+
for pool_id, pool in manager.pools.items():
|
| 359 |
+
current_provider = None
|
| 360 |
+
if pool.providers:
|
| 361 |
+
next_p = pool.get_next_provider()
|
| 362 |
+
if next_p:
|
| 363 |
+
current_provider = {
|
| 364 |
+
"provider_id": next_p.provider_id,
|
| 365 |
+
"name": next_p.name,
|
| 366 |
+
"status": next_p.status.value
|
| 367 |
+
}
|
| 368 |
+
|
| 369 |
+
pools.append({
|
| 370 |
+
"pool_id": pool_id,
|
| 371 |
+
"pool_name": pool.pool_name,
|
| 372 |
+
"category": pool.category,
|
| 373 |
+
"rotation_strategy": pool.rotation_strategy.value,
|
| 374 |
+
"enabled": pool.enabled,
|
| 375 |
+
"total_rotations": pool.total_rotations,
|
| 376 |
+
"total_providers": len(pool.providers),
|
| 377 |
+
"available_providers": len([p for p in pool.providers if p.is_available]),
|
| 378 |
+
"current_provider": current_provider,
|
| 379 |
+
"members": [
|
| 380 |
+
{
|
| 381 |
+
"provider_id": p.provider_id,
|
| 382 |
+
"provider_name": p.name,
|
| 383 |
+
"status": p.status.value,
|
| 384 |
+
"success_rate": p.success_rate,
|
| 385 |
+
"use_count": p.total_requests,
|
| 386 |
+
"priority": p.priority,
|
| 387 |
+
"weight": p.weight,
|
| 388 |
+
"rate_limit": {
|
| 389 |
+
"usage": p.rate_limit.current_usage if p.rate_limit else 0,
|
| 390 |
+
"limit": p.rate_limit.requests_per_minute or p.rate_limit.requests_per_day or 100 if p.rate_limit else 100,
|
| 391 |
+
"percentage": min(100, (p.rate_limit.current_usage / (p.rate_limit.requests_per_minute or 100) * 100)) if p.rate_limit and p.rate_limit.requests_per_minute else 0
|
| 392 |
+
}
|
| 393 |
+
}
|
| 394 |
+
for p in pool.providers
|
| 395 |
+
]
|
| 396 |
+
})
|
| 397 |
+
|
| 398 |
+
return {"pools": pools, "total": len(pools)}
|
| 399 |
+
|
| 400 |
+
|
| 401 |
+
@app.get("/api/pools/{pool_id}")
|
| 402 |
+
async def get_pool(pool_id: str):
|
| 403 |
+
"""دریافت اطلاعات یک Pool"""
|
| 404 |
+
pool = manager.get_pool(pool_id)
|
| 405 |
+
if not pool:
|
| 406 |
+
raise HTTPException(status_code=404, detail="Pool not found")
|
| 407 |
+
|
| 408 |
+
return pool.get_stats()
|
| 409 |
+
|
| 410 |
+
|
| 411 |
+
@app.post("/api/pools")
|
| 412 |
+
async def create_pool(request: PoolCreateRequest):
|
| 413 |
+
"""ایجاد Pool جدید"""
|
| 414 |
+
pool_id = request.name.lower().replace(' ', '_')
|
| 415 |
+
|
| 416 |
+
if pool_id in manager.pools:
|
| 417 |
+
raise HTTPException(status_code=400, detail="Pool already exists")
|
| 418 |
+
|
| 419 |
+
try:
|
| 420 |
+
rotation_strategy = RotationStrategy(request.rotation_strategy)
|
| 421 |
+
except ValueError:
|
| 422 |
+
raise HTTPException(status_code=400, detail="Invalid rotation strategy")
|
| 423 |
+
|
| 424 |
+
pool = ProviderPool(
|
| 425 |
+
pool_id=pool_id,
|
| 426 |
+
pool_name=request.name,
|
| 427 |
+
category=request.category,
|
| 428 |
+
rotation_strategy=rotation_strategy
|
| 429 |
+
)
|
| 430 |
+
|
| 431 |
+
manager.pools[pool_id] = pool
|
| 432 |
+
|
| 433 |
+
return {
|
| 434 |
+
"message": "Pool created successfully",
|
| 435 |
+
"pool_id": pool_id,
|
| 436 |
+
"pool": pool.get_stats()
|
| 437 |
+
}
|
| 438 |
+
|
| 439 |
+
|
| 440 |
+
@app.delete("/api/pools/{pool_id}")
|
| 441 |
+
async def delete_pool(pool_id: str):
|
| 442 |
+
"""حذف Pool"""
|
| 443 |
+
if pool_id not in manager.pools:
|
| 444 |
+
raise HTTPException(status_code=404, detail="Pool not found")
|
| 445 |
+
|
| 446 |
+
del manager.pools[pool_id]
|
| 447 |
+
|
| 448 |
+
return {"message": "Pool deleted successfully", "pool_id": pool_id}
|
| 449 |
+
|
| 450 |
+
|
| 451 |
+
@app.post("/api/pools/{pool_id}/members")
|
| 452 |
+
async def add_member_to_pool(pool_id: str, request: PoolMemberRequest):
|
| 453 |
+
"""افزودن عضو به Pool"""
|
| 454 |
+
pool = manager.get_pool(pool_id)
|
| 455 |
+
if not pool:
|
| 456 |
+
raise HTTPException(status_code=404, detail="Pool not found")
|
| 457 |
+
|
| 458 |
+
provider = manager.get_provider(request.provider_id)
|
| 459 |
+
if not provider:
|
| 460 |
+
raise HTTPException(status_code=404, detail="Provider not found")
|
| 461 |
+
|
| 462 |
+
# تنظیم اولویت و وزن
|
| 463 |
+
provider.priority = request.priority
|
| 464 |
+
provider.weight = request.weight
|
| 465 |
+
|
| 466 |
+
pool.add_provider(provider)
|
| 467 |
+
|
| 468 |
+
return {
|
| 469 |
+
"message": "Provider added to pool successfully",
|
| 470 |
+
"pool_id": pool_id,
|
| 471 |
+
"provider_id": request.provider_id
|
| 472 |
+
}
|
| 473 |
+
|
| 474 |
+
|
| 475 |
+
@app.delete("/api/pools/{pool_id}/members/{provider_id}")
|
| 476 |
+
async def remove_member_from_pool(pool_id: str, provider_id: str):
|
| 477 |
+
"""حذف عضو از Pool"""
|
| 478 |
+
pool = manager.get_pool(pool_id)
|
| 479 |
+
if not pool:
|
| 480 |
+
raise HTTPException(status_code=404, detail="Pool not found")
|
| 481 |
+
|
| 482 |
+
pool.remove_provider(provider_id)
|
| 483 |
+
|
| 484 |
+
return {
|
| 485 |
+
"message": "Provider removed from pool successfully",
|
| 486 |
+
"pool_id": pool_id,
|
| 487 |
+
"provider_id": provider_id
|
| 488 |
+
}
|
| 489 |
+
|
| 490 |
+
|
| 491 |
+
@app.post("/api/pools/{pool_id}/rotate")
|
| 492 |
+
async def rotate_pool(pool_id: str, request: RotateRequest):
|
| 493 |
+
"""چرخش دستی Pool"""
|
| 494 |
+
pool = manager.get_pool(pool_id)
|
| 495 |
+
if not pool:
|
| 496 |
+
raise HTTPException(status_code=404, detail="Pool not found")
|
| 497 |
+
|
| 498 |
+
provider = pool.get_next_provider()
|
| 499 |
+
if not provider:
|
| 500 |
+
raise HTTPException(status_code=503, detail="No available provider in pool")
|
| 501 |
+
|
| 502 |
+
return {
|
| 503 |
+
"message": "Pool rotated successfully",
|
| 504 |
+
"pool_id": pool_id,
|
| 505 |
+
"provider_id": provider.provider_id,
|
| 506 |
+
"provider_name": provider.name,
|
| 507 |
+
"reason": request.reason,
|
| 508 |
+
"timestamp": datetime.now().isoformat()
|
| 509 |
+
}
|
| 510 |
+
|
| 511 |
+
|
| 512 |
+
@app.get("/api/pools/history")
|
| 513 |
+
async def get_rotation_history(limit: int = 20):
|
| 514 |
+
"""تاریخچه چرخشها"""
|
| 515 |
+
# این endpoint نیاز به یک سیستم لاگ دارد که میتوان بعداً اضافه کرد
|
| 516 |
+
# فعلاً یک نمونه ساده برمیگردانیم
|
| 517 |
+
history = []
|
| 518 |
+
for pool_id, pool in manager.pools.items():
|
| 519 |
+
if pool.total_rotations > 0:
|
| 520 |
+
history.append({
|
| 521 |
+
"pool_id": pool_id,
|
| 522 |
+
"pool_name": pool.pool_name,
|
| 523 |
+
"total_rotations": pool.total_rotations,
|
| 524 |
+
"provider_name": pool.providers[0].name if pool.providers else "N/A",
|
| 525 |
+
"timestamp": datetime.now().isoformat(),
|
| 526 |
+
"reason": "automatic"
|
| 527 |
+
})
|
| 528 |
+
|
| 529 |
+
return {"history": history[:limit], "total": len(history)}
|
| 530 |
+
|
| 531 |
+
|
| 532 |
+
# ===== Status & Statistics Endpoints =====
|
| 533 |
+
|
| 534 |
+
@app.get("/api/status")
|
| 535 |
+
async def get_status():
|
| 536 |
+
"""وضعیت کلی سیستم"""
|
| 537 |
+
stats = manager.get_all_stats()
|
| 538 |
+
summary = stats['summary']
|
| 539 |
+
|
| 540 |
+
# محاسبه میانگین زمان پاسخ
|
| 541 |
+
response_times = [p.avg_response_time for p in manager.providers.values() if p.avg_response_time > 0]
|
| 542 |
+
avg_response = sum(response_times) / len(response_times) if response_times else 0
|
| 543 |
+
|
| 544 |
+
return {
|
| 545 |
+
"status": "operational" if summary['online'] > summary['offline'] else "degraded",
|
| 546 |
+
"timestamp": datetime.now().isoformat(),
|
| 547 |
+
"total_providers": summary['total_providers'],
|
| 548 |
+
"online": summary['online'],
|
| 549 |
+
"offline": summary['offline'],
|
| 550 |
+
"degraded": summary['degraded'],
|
| 551 |
+
"avg_response_time_ms": round(avg_response, 2),
|
| 552 |
+
"total_requests": summary['total_requests'],
|
| 553 |
+
"successful_requests": summary['successful_requests'],
|
| 554 |
+
"success_rate": round(summary['overall_success_rate'], 2)
|
| 555 |
+
}
|
| 556 |
+
|
| 557 |
+
|
| 558 |
+
@app.get("/api/stats")
|
| 559 |
+
async def get_statistics():
|
| 560 |
+
"""آمار کامل سیستم"""
|
| 561 |
+
return manager.get_all_stats()
|
| 562 |
+
|
| 563 |
+
|
| 564 |
+
@app.get("/api/stats/export")
|
| 565 |
+
async def export_stats():
|
| 566 |
+
"""صادرکردن آمار"""
|
| 567 |
+
filepath = f"stats_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
|
| 568 |
+
manager.export_stats(filepath)
|
| 569 |
+
return {
|
| 570 |
+
"message": "Statistics exported successfully",
|
| 571 |
+
"filepath": filepath,
|
| 572 |
+
"timestamp": datetime.now().isoformat()
|
| 573 |
+
}
|
| 574 |
+
|
| 575 |
+
|
| 576 |
+
# ===== Mock Data Endpoints (برای داشبورد) =====
|
| 577 |
+
|
| 578 |
+
@app.get("/api/market")
|
| 579 |
+
async def get_market_data():
|
| 580 |
+
"""دادههای بازار (Mock)"""
|
| 581 |
+
return {
|
| 582 |
+
"cryptocurrencies": [
|
| 583 |
+
{
|
| 584 |
+
"rank": 1,
|
| 585 |
+
"name": "Bitcoin",
|
| 586 |
+
"symbol": "BTC",
|
| 587 |
+
"price": 43250.50,
|
| 588 |
+
"change_24h": 2.35,
|
| 589 |
+
"market_cap": 845000000000,
|
| 590 |
+
"volume_24h": 28500000000,
|
| 591 |
+
"image": "https://assets.coingecko.com/coins/images/1/small/bitcoin.png"
|
| 592 |
+
},
|
| 593 |
+
{
|
| 594 |
+
"rank": 2,
|
| 595 |
+
"name": "Ethereum",
|
| 596 |
+
"symbol": "ETH",
|
| 597 |
+
"price": 2280.75,
|
| 598 |
+
"change_24h": -1.20,
|
| 599 |
+
"market_cap": 274000000000,
|
| 600 |
+
"volume_24h": 15200000000,
|
| 601 |
+
"image": "https://assets.coingecko.com/coins/images/279/small/ethereum.png"
|
| 602 |
+
}
|
| 603 |
+
],
|
| 604 |
+
"global": {
|
| 605 |
+
"btc_dominance": 52.3,
|
| 606 |
+
"eth_dominance": 17.8
|
| 607 |
+
}
|
| 608 |
+
}
|
| 609 |
+
|
| 610 |
+
|
| 611 |
+
@app.get("/api/stats")
|
| 612 |
+
async def get_market_stats():
|
| 613 |
+
"""آمار بازار (Mock)"""
|
| 614 |
+
return {
|
| 615 |
+
"market": {
|
| 616 |
+
"total_market_cap": 1650000000000,
|
| 617 |
+
"total_volume": 85000000000,
|
| 618 |
+
"btc_dominance": 52.3
|
| 619 |
+
}
|
| 620 |
+
}
|
| 621 |
+
|
| 622 |
+
|
| 623 |
+
@app.get("/api/sentiment")
|
| 624 |
+
async def get_sentiment():
|
| 625 |
+
"""احساسات بازار (Mock)"""
|
| 626 |
+
return {
|
| 627 |
+
"fear_greed_index": {
|
| 628 |
+
"value": 62,
|
| 629 |
+
"classification": "Greed"
|
| 630 |
+
}
|
| 631 |
+
}
|
| 632 |
+
|
| 633 |
+
|
| 634 |
+
@app.get("/api/trending")
|
| 635 |
+
async def get_trending():
|
| 636 |
+
"""ترندینگ (Mock)"""
|
| 637 |
+
return {
|
| 638 |
+
"trending": [
|
| 639 |
+
{"name": "Solana", "symbol": "SOL", "thumb": ""},
|
| 640 |
+
{"name": "Cardano", "symbol": "ADA", "thumb": ""}
|
| 641 |
+
]
|
| 642 |
+
}
|
| 643 |
+
|
| 644 |
+
|
| 645 |
+
@app.get("/api/defi")
|
| 646 |
+
async def get_defi():
|
| 647 |
+
"""دادههای DeFi (Mock)"""
|
| 648 |
+
return {
|
| 649 |
+
"total_tvl": 48500000000,
|
| 650 |
+
"protocols": [
|
| 651 |
+
{"name": "Lido", "chain": "Ethereum", "tvl": 18500000000, "change_24h": 1.5},
|
| 652 |
+
{"name": "Aave", "chain": "Multi-chain", "tvl": 12300000000, "change_24h": -0.8}
|
| 653 |
+
]
|
| 654 |
+
}
|
| 655 |
+
|
| 656 |
+
|
| 657 |
+
# ===== HuggingFace Endpoints =====
|
| 658 |
+
|
| 659 |
+
@app.get("/api/hf/health")
|
| 660 |
+
async def hf_health():
|
| 661 |
+
"""سلامت HuggingFace"""
|
| 662 |
+
return {
|
| 663 |
+
"status": "operational",
|
| 664 |
+
"models_available": 4,
|
| 665 |
+
"timestamp": datetime.now().isoformat()
|
| 666 |
+
}
|
| 667 |
+
|
| 668 |
+
|
| 669 |
+
@app.post("/api/hf/run-sentiment")
|
| 670 |
+
async def run_sentiment(data: Dict[str, Any]):
|
| 671 |
+
"""تحلیل احساسات (Mock)"""
|
| 672 |
+
texts = data.get("texts", [])
|
| 673 |
+
|
| 674 |
+
# شبیهسازی نتیجه
|
| 675 |
+
results = []
|
| 676 |
+
for text in texts:
|
| 677 |
+
sentiment = "positive" if "bullish" in text.lower() or "strong" in text.lower() else "negative" if "weak" in text.lower() else "neutral"
|
| 678 |
+
score = 0.8 if sentiment == "positive" else -0.6 if sentiment == "negative" else 0.1
|
| 679 |
+
results.append({"text": text, "sentiment": sentiment, "score": score})
|
| 680 |
+
|
| 681 |
+
vote = sum(r["score"] for r in results) / len(results) if results else 0
|
| 682 |
+
|
| 683 |
+
return {
|
| 684 |
+
"vote": vote,
|
| 685 |
+
"results": results,
|
| 686 |
+
"count": len(results)
|
| 687 |
+
}
|
| 688 |
+
|
| 689 |
+
|
| 690 |
+
# ===== Log Management Endpoints =====
|
| 691 |
+
|
| 692 |
+
@app.get("/api/logs")
|
| 693 |
+
async def get_logs(
|
| 694 |
+
level: Optional[str] = None,
|
| 695 |
+
category: Optional[str] = None,
|
| 696 |
+
provider_id: Optional[str] = None,
|
| 697 |
+
pool_id: Optional[str] = None,
|
| 698 |
+
limit: int = 100,
|
| 699 |
+
search: Optional[str] = None
|
| 700 |
+
):
|
| 701 |
+
"""دریافت لاگها با فیلتر"""
|
| 702 |
+
log_level = LogLevel(level) if level else None
|
| 703 |
+
log_category = LogCategory(category) if category else None
|
| 704 |
+
|
| 705 |
+
if search:
|
| 706 |
+
logs = log_manager.search_logs(search, limit)
|
| 707 |
+
else:
|
| 708 |
+
logs = log_manager.filter_logs(
|
| 709 |
+
level=log_level,
|
| 710 |
+
category=log_category,
|
| 711 |
+
provider_id=provider_id,
|
| 712 |
+
pool_id=pool_id
|
| 713 |
+
)[-limit:]
|
| 714 |
+
|
| 715 |
+
return {
|
| 716 |
+
"logs": [log.to_dict() for log in logs],
|
| 717 |
+
"total": len(logs)
|
| 718 |
+
}
|
| 719 |
+
|
| 720 |
+
|
| 721 |
+
@app.get("/api/logs/recent")
|
| 722 |
+
async def get_recent_logs(limit: int = 50):
|
| 723 |
+
"""دریافت آخرین لاگها"""
|
| 724 |
+
logs = log_manager.get_recent_logs(limit)
|
| 725 |
+
return {
|
| 726 |
+
"logs": [log.to_dict() for log in logs],
|
| 727 |
+
"total": len(logs)
|
| 728 |
+
}
|
| 729 |
+
|
| 730 |
+
|
| 731 |
+
@app.get("/api/logs/errors")
|
| 732 |
+
async def get_error_logs(limit: int = 50):
|
| 733 |
+
"""دریافت لاگهای خطا"""
|
| 734 |
+
logs = log_manager.get_error_logs(limit)
|
| 735 |
+
return {
|
| 736 |
+
"logs": [log.to_dict() for log in logs],
|
| 737 |
+
"total": len(logs)
|
| 738 |
+
}
|
| 739 |
+
|
| 740 |
+
|
| 741 |
+
@app.get("/api/logs/stats")
|
| 742 |
+
async def get_log_stats():
|
| 743 |
+
"""آمار لاگها"""
|
| 744 |
+
return log_manager.get_statistics()
|
| 745 |
+
|
| 746 |
+
|
| 747 |
+
@app.get("/api/logs/export/json")
|
| 748 |
+
async def export_logs_json(
|
| 749 |
+
level: Optional[str] = None,
|
| 750 |
+
category: Optional[str] = None,
|
| 751 |
+
provider_id: Optional[str] = None
|
| 752 |
+
):
|
| 753 |
+
"""صادرکردن لاگها به JSON"""
|
| 754 |
+
log_level = LogLevel(level) if level else None
|
| 755 |
+
log_category = LogCategory(category) if category else None
|
| 756 |
+
|
| 757 |
+
filtered = log_manager.filter_logs(
|
| 758 |
+
level=log_level,
|
| 759 |
+
category=log_category,
|
| 760 |
+
provider_id=provider_id
|
| 761 |
+
)
|
| 762 |
+
|
| 763 |
+
filepath = f"logs_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
|
| 764 |
+
log_manager.export_to_json(filepath, filtered=filtered)
|
| 765 |
+
|
| 766 |
+
return {
|
| 767 |
+
"message": "Logs exported successfully",
|
| 768 |
+
"filepath": filepath,
|
| 769 |
+
"count": len(filtered)
|
| 770 |
+
}
|
| 771 |
+
|
| 772 |
+
|
| 773 |
+
@app.get("/api/logs/export/csv")
|
| 774 |
+
async def export_logs_csv(
|
| 775 |
+
level: Optional[str] = None,
|
| 776 |
+
category: Optional[str] = None
|
| 777 |
+
):
|
| 778 |
+
"""صادرکردن لاگها به CSV"""
|
| 779 |
+
log_level = LogLevel(level) if level else None
|
| 780 |
+
log_category = LogCategory(category) if category else None
|
| 781 |
+
|
| 782 |
+
filtered = log_manager.filter_logs(
|
| 783 |
+
level=log_level,
|
| 784 |
+
category=log_category
|
| 785 |
+
)
|
| 786 |
+
|
| 787 |
+
filepath = f"logs_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
|
| 788 |
+
log_manager.export_to_csv(filepath)
|
| 789 |
+
|
| 790 |
+
return {
|
| 791 |
+
"message": "Logs exported successfully",
|
| 792 |
+
"filepath": filepath,
|
| 793 |
+
"count": len(filtered)
|
| 794 |
+
}
|
| 795 |
+
|
| 796 |
+
|
| 797 |
+
@app.delete("/api/logs")
|
| 798 |
+
async def clear_logs():
|
| 799 |
+
"""پاک کردن همه لاگها"""
|
| 800 |
+
log_manager.clear_logs()
|
| 801 |
+
return {"message": "All logs cleared"}
|
| 802 |
+
|
| 803 |
+
|
| 804 |
+
# ===== Resource Management Endpoints =====
|
| 805 |
+
|
| 806 |
+
@app.get("/api/resources")
|
| 807 |
+
async def get_resources():
|
| 808 |
+
"""دریافت همه منابع"""
|
| 809 |
+
return {
|
| 810 |
+
"providers": resource_manager.get_all_providers(),
|
| 811 |
+
"statistics": resource_manager.get_statistics()
|
| 812 |
+
}
|
| 813 |
+
|
| 814 |
+
|
| 815 |
+
@app.get("/api/resources/category/{category}")
|
| 816 |
+
async def get_resources_by_category(category: str):
|
| 817 |
+
"""دریافت منابع بر اساس دسته"""
|
| 818 |
+
providers = resource_manager.get_providers_by_category(category)
|
| 819 |
+
return {
|
| 820 |
+
"category": category,
|
| 821 |
+
"providers": providers,
|
| 822 |
+
"count": len(providers)
|
| 823 |
+
}
|
| 824 |
+
|
| 825 |
+
|
| 826 |
+
@app.post("/api/resources/import/json")
|
| 827 |
+
async def import_resources_json(file_path: str, merge: bool = True):
|
| 828 |
+
"""وارد کردن منابع از JSON"""
|
| 829 |
+
success = resource_manager.import_from_json(file_path, merge=merge)
|
| 830 |
+
if success:
|
| 831 |
+
resource_manager.save_resources()
|
| 832 |
+
return {"message": "Resources imported successfully", "merged": merge}
|
| 833 |
+
else:
|
| 834 |
+
raise HTTPException(status_code=400, detail="Failed to import resources")
|
| 835 |
+
|
| 836 |
+
|
| 837 |
+
@app.get("/api/resources/export/json")
|
| 838 |
+
async def export_resources_json():
|
| 839 |
+
"""صادرکردن منابع به JSON"""
|
| 840 |
+
filepath = f"resources_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
|
| 841 |
+
resource_manager.export_to_json(filepath)
|
| 842 |
+
return {
|
| 843 |
+
"message": "Resources exported successfully",
|
| 844 |
+
"filepath": filepath
|
| 845 |
+
}
|
| 846 |
+
|
| 847 |
+
|
| 848 |
+
@app.get("/api/resources/export/csv")
|
| 849 |
+
async def export_resources_csv():
|
| 850 |
+
"""صادرکردن منابع به CSV"""
|
| 851 |
+
filepath = f"resources_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
|
| 852 |
+
resource_manager.export_to_csv(filepath)
|
| 853 |
+
return {
|
| 854 |
+
"message": "Resources exported successfully",
|
| 855 |
+
"filepath": filepath
|
| 856 |
+
}
|
| 857 |
+
|
| 858 |
+
|
| 859 |
+
@app.post("/api/resources/backup")
|
| 860 |
+
async def backup_resources():
|
| 861 |
+
"""پشتیبانگیری از منابع"""
|
| 862 |
+
backup_file = resource_manager.backup()
|
| 863 |
+
return {
|
| 864 |
+
"message": "Backup created successfully",
|
| 865 |
+
"filepath": backup_file
|
| 866 |
+
}
|
| 867 |
+
|
| 868 |
+
|
| 869 |
+
@app.post("/api/resources/provider")
|
| 870 |
+
async def add_provider(provider_data: Dict[str, Any]):
|
| 871 |
+
"""افزودن provider جدید"""
|
| 872 |
+
is_valid, message = resource_manager.validate_provider(provider_data)
|
| 873 |
+
if not is_valid:
|
| 874 |
+
raise HTTPException(status_code=400, detail=message)
|
| 875 |
+
|
| 876 |
+
provider_id = resource_manager.add_provider(provider_data)
|
| 877 |
+
resource_manager.save_resources()
|
| 878 |
+
|
| 879 |
+
log_manager.add_log(
|
| 880 |
+
LogLevel.INFO,
|
| 881 |
+
LogCategory.PROVIDER,
|
| 882 |
+
f"Provider added: {provider_id}",
|
| 883 |
+
provider_id=provider_id
|
| 884 |
+
)
|
| 885 |
+
|
| 886 |
+
return {
|
| 887 |
+
"message": "Provider added successfully",
|
| 888 |
+
"provider_id": provider_id
|
| 889 |
+
}
|
| 890 |
+
|
| 891 |
+
|
| 892 |
+
@app.delete("/api/resources/provider/{provider_id}")
|
| 893 |
+
async def remove_provider(provider_id: str):
|
| 894 |
+
"""حذف provider"""
|
| 895 |
+
success = resource_manager.remove_provider(provider_id)
|
| 896 |
+
if success:
|
| 897 |
+
resource_manager.save_resources()
|
| 898 |
+
log_manager.add_log(
|
| 899 |
+
LogLevel.INFO,
|
| 900 |
+
LogCategory.PROVIDER,
|
| 901 |
+
f"Provider removed: {provider_id}",
|
| 902 |
+
provider_id=provider_id
|
| 903 |
+
)
|
| 904 |
+
return {"message": "Provider removed successfully"}
|
| 905 |
+
else:
|
| 906 |
+
raise HTTPException(status_code=404, detail="Provider not found")
|
| 907 |
+
|
| 908 |
+
|
| 909 |
+
@app.get("/api/resources/discovery/status")
|
| 910 |
+
async def get_auto_discovery_status():
|
| 911 |
+
"""وضعیت سرویس کشف خودکار منابع"""
|
| 912 |
+
return auto_discovery_service.get_status()
|
| 913 |
+
|
| 914 |
+
|
| 915 |
+
@app.post("/api/resources/discovery/run")
|
| 916 |
+
async def run_auto_discovery():
|
| 917 |
+
"""اجرای دستی کشف منابع جدید"""
|
| 918 |
+
result = await auto_discovery_service.trigger_manual_discovery()
|
| 919 |
+
if result.get("status") == "disabled":
|
| 920 |
+
raise HTTPException(status_code=503, detail="Auto discovery service is disabled.")
|
| 921 |
+
return result
|
| 922 |
+
|
| 923 |
+
|
| 924 |
+
# ===== WebSocket & Session Endpoints =====
|
| 925 |
+
|
| 926 |
+
from fastapi import WebSocket, WebSocketDisconnect
|
| 927 |
+
|
| 928 |
+
@app.websocket("/ws")
|
| 929 |
+
async def websocket_endpoint(websocket: WebSocket):
|
| 930 |
+
"""WebSocket endpoint برای ارتباط بلادرنگ"""
|
| 931 |
+
session_id = None
|
| 932 |
+
try:
|
| 933 |
+
# اتصال کلاینت
|
| 934 |
+
session_id = await conn_manager.connect(
|
| 935 |
+
websocket,
|
| 936 |
+
client_type='browser',
|
| 937 |
+
metadata={'source': 'unified_dashboard'}
|
| 938 |
+
)
|
| 939 |
+
|
| 940 |
+
# ارسال پیام خوشآمدگویی
|
| 941 |
+
await conn_manager.send_personal_message({
|
| 942 |
+
'type': 'welcome',
|
| 943 |
+
'session_id': session_id,
|
| 944 |
+
'message': 'به سیستم مانیتورینگ کریپتو خوش آمدید',
|
| 945 |
+
'timestamp': datetime.now().isoformat()
|
| 946 |
+
}, session_id)
|
| 947 |
+
|
| 948 |
+
# دریافت و پردازش پیامها
|
| 949 |
+
while True:
|
| 950 |
+
data = await websocket.receive_json()
|
| 951 |
+
|
| 952 |
+
message_type = data.get('type')
|
| 953 |
+
|
| 954 |
+
if message_type == 'subscribe':
|
| 955 |
+
# Subscribe به گروه خاص
|
| 956 |
+
group = data.get('group', 'all')
|
| 957 |
+
conn_manager.subscribe(session_id, group)
|
| 958 |
+
await conn_manager.send_personal_message({
|
| 959 |
+
'type': 'subscribed',
|
| 960 |
+
'group': group
|
| 961 |
+
}, session_id)
|
| 962 |
+
|
| 963 |
+
elif message_type == 'unsubscribe':
|
| 964 |
+
# Unsubscribe از گروه
|
| 965 |
+
group = data.get('group')
|
| 966 |
+
conn_manager.unsubscribe(session_id, group)
|
| 967 |
+
await conn_manager.send_personal_message({
|
| 968 |
+
'type': 'unsubscribed',
|
| 969 |
+
'group': group
|
| 970 |
+
}, session_id)
|
| 971 |
+
|
| 972 |
+
elif message_type == 'get_stats':
|
| 973 |
+
# درخواست آمار فوری
|
| 974 |
+
stats = manager.get_all_stats()
|
| 975 |
+
conn_stats = conn_manager.get_stats()
|
| 976 |
+
|
| 977 |
+
# ارسال آمار provider
|
| 978 |
+
await conn_manager.send_personal_message({
|
| 979 |
+
'type': 'stats_response',
|
| 980 |
+
'data': stats
|
| 981 |
+
}, session_id)
|
| 982 |
+
|
| 983 |
+
# ارسال آمار اتصالات
|
| 984 |
+
await conn_manager.send_personal_message({
|
| 985 |
+
'type': 'stats_update',
|
| 986 |
+
'data': conn_stats
|
| 987 |
+
}, session_id)
|
| 988 |
+
|
| 989 |
+
elif message_type == 'ping':
|
| 990 |
+
# پاسخ به ping
|
| 991 |
+
await conn_manager.send_personal_message({
|
| 992 |
+
'type': 'pong',
|
| 993 |
+
'timestamp': datetime.now().isoformat()
|
| 994 |
+
}, session_id)
|
| 995 |
+
|
| 996 |
+
conn_manager.total_messages_received += 1
|
| 997 |
+
|
| 998 |
+
except WebSocketDisconnect:
|
| 999 |
+
if session_id:
|
| 1000 |
+
conn_manager.disconnect(session_id)
|
| 1001 |
+
except Exception as e:
|
| 1002 |
+
print(f"❌ خطا در WebSocket: {e}")
|
| 1003 |
+
if session_id:
|
| 1004 |
+
conn_manager.disconnect(session_id)
|
| 1005 |
+
|
| 1006 |
+
|
| 1007 |
+
@app.get("/api/sessions")
|
| 1008 |
+
async def get_sessions():
|
| 1009 |
+
"""دریافت لیست sessionهای فعال"""
|
| 1010 |
+
return {
|
| 1011 |
+
"sessions": conn_manager.get_sessions(),
|
| 1012 |
+
"stats": conn_manager.get_stats()
|
| 1013 |
+
}
|
| 1014 |
+
|
| 1015 |
+
|
| 1016 |
+
@app.get("/api/sessions/stats")
|
| 1017 |
+
async def get_session_stats():
|
| 1018 |
+
"""دریافت آمار اتصالات"""
|
| 1019 |
+
return conn_manager.get_stats()
|
| 1020 |
+
|
| 1021 |
+
|
| 1022 |
+
@app.post("/api/broadcast")
|
| 1023 |
+
async def broadcast_message(message: Dict[str, Any], group: str = 'all'):
|
| 1024 |
+
"""ارسال پیام به همه کلاینتها"""
|
| 1025 |
+
await conn_manager.broadcast(message, group)
|
| 1026 |
+
return {"status": "sent", "group": group}
|
| 1027 |
+
|
| 1028 |
+
|
| 1029 |
+
# ===== Reports & Diagnostics Endpoints =====
|
| 1030 |
+
|
| 1031 |
+
@app.get("/api/reports/discovery")
|
| 1032 |
+
async def get_discovery_report():
|
| 1033 |
+
"""گزارش عملکرد Auto-Discovery Service"""
|
| 1034 |
+
status = auto_discovery_service.get_status()
|
| 1035 |
+
|
| 1036 |
+
# محاسبه زمان اجرای بعدی
|
| 1037 |
+
next_run_estimate = None
|
| 1038 |
+
if status.get("enabled") and status.get("last_run"):
|
| 1039 |
+
last_run = status.get("last_run")
|
| 1040 |
+
interval_seconds = status.get("interval_seconds", 43200) # پیشفرض 12 ساعت
|
| 1041 |
+
|
| 1042 |
+
if last_run and "finished_at" in last_run:
|
| 1043 |
+
try:
|
| 1044 |
+
finished_at = datetime.fromisoformat(last_run["finished_at"].replace('Z', '+00:00'))
|
| 1045 |
+
if finished_at.tzinfo is None:
|
| 1046 |
+
finished_at = finished_at.replace(tzinfo=datetime.now().astimezone().tzinfo)
|
| 1047 |
+
next_run = finished_at + timedelta(seconds=interval_seconds)
|
| 1048 |
+
next_run_estimate = next_run.isoformat()
|
| 1049 |
+
except Exception:
|
| 1050 |
+
pass
|
| 1051 |
+
|
| 1052 |
+
return {
|
| 1053 |
+
"service_status": status,
|
| 1054 |
+
"enabled": status.get("enabled", False),
|
| 1055 |
+
"model": status.get("model"),
|
| 1056 |
+
"interval_seconds": status.get("interval_seconds"),
|
| 1057 |
+
"last_run": status.get("last_run"),
|
| 1058 |
+
"next_run_estimate": next_run_estimate,
|
| 1059 |
+
}
|
| 1060 |
+
|
| 1061 |
+
|
| 1062 |
+
@app.get("/api/reports/models")
|
| 1063 |
+
async def get_models_report():
|
| 1064 |
+
"""گزارش وضعیت مدلهای HuggingFace"""
|
| 1065 |
+
models_status = []
|
| 1066 |
+
|
| 1067 |
+
try:
|
| 1068 |
+
from huggingface_hub import HfApi
|
| 1069 |
+
api = HfApi()
|
| 1070 |
+
|
| 1071 |
+
models_to_check = [
|
| 1072 |
+
'HuggingFaceH4/zephyr-7b-beta',
|
| 1073 |
+
'cardiffnlp/twitter-roberta-base-sentiment-latest',
|
| 1074 |
+
'BAAI/bge-m3',
|
| 1075 |
+
]
|
| 1076 |
+
|
| 1077 |
+
for model_id in models_to_check:
|
| 1078 |
+
try:
|
| 1079 |
+
model_info = api.model_info(model_id, timeout=5.0)
|
| 1080 |
+
models_status.append({
|
| 1081 |
+
"model_id": model_id,
|
| 1082 |
+
"status": "available",
|
| 1083 |
+
"downloads": getattr(model_info, 'downloads', None),
|
| 1084 |
+
"likes": getattr(model_info, 'likes', None),
|
| 1085 |
+
"pipeline_tag": getattr(model_info, 'pipeline_tag', None),
|
| 1086 |
+
"last_updated": getattr(model_info, 'last_modified', None),
|
| 1087 |
+
})
|
| 1088 |
+
except Exception as e:
|
| 1089 |
+
models_status.append({
|
| 1090 |
+
"model_id": model_id,
|
| 1091 |
+
"status": "error",
|
| 1092 |
+
"error": str(e),
|
| 1093 |
+
})
|
| 1094 |
+
except ImportError:
|
| 1095 |
+
return {
|
| 1096 |
+
"error": "huggingface_hub not installed",
|
| 1097 |
+
"models_status": [],
|
| 1098 |
+
}
|
| 1099 |
+
|
| 1100 |
+
return {
|
| 1101 |
+
"total_models": len(models_status),
|
| 1102 |
+
"available": sum(1 for m in models_status if m.get("status") == "available"),
|
| 1103 |
+
"errors": sum(1 for m in models_status if m.get("status") == "error"),
|
| 1104 |
+
"models": models_status,
|
| 1105 |
+
}
|
| 1106 |
+
|
| 1107 |
+
|
| 1108 |
+
@app.post("/api/diagnostics/run")
|
| 1109 |
+
async def run_diagnostics(auto_fix: bool = False):
|
| 1110 |
+
"""اجرای اشکالیابی خودکار"""
|
| 1111 |
+
try:
|
| 1112 |
+
report = await diagnostics_service.run_full_diagnostics(auto_fix=auto_fix)
|
| 1113 |
+
|
| 1114 |
+
# تبدیل به dict برای JSON
|
| 1115 |
+
report_dict = {
|
| 1116 |
+
"timestamp": report.timestamp,
|
| 1117 |
+
"total_issues": report.total_issues,
|
| 1118 |
+
"critical_issues": report.critical_issues,
|
| 1119 |
+
"warnings": report.warnings,
|
| 1120 |
+
"info_issues": report.info_issues,
|
| 1121 |
+
"issues": [
|
| 1122 |
+
{
|
| 1123 |
+
"severity": issue.severity,
|
| 1124 |
+
"category": issue.category,
|
| 1125 |
+
"title": issue.title,
|
| 1126 |
+
"description": issue.description,
|
| 1127 |
+
"fixable": issue.fixable,
|
| 1128 |
+
"fix_action": issue.fix_action,
|
| 1129 |
+
"auto_fixed": issue.auto_fixed,
|
| 1130 |
+
"timestamp": issue.timestamp,
|
| 1131 |
+
}
|
| 1132 |
+
for issue in report.issues
|
| 1133 |
+
],
|
| 1134 |
+
"fixed_issues": [
|
| 1135 |
+
{
|
| 1136 |
+
"severity": issue.severity,
|
| 1137 |
+
"category": issue.category,
|
| 1138 |
+
"title": issue.title,
|
| 1139 |
+
"description": issue.description,
|
| 1140 |
+
"fixable": issue.fixable,
|
| 1141 |
+
"fix_action": issue.fix_action,
|
| 1142 |
+
"auto_fixed": issue.auto_fixed,
|
| 1143 |
+
"timestamp": issue.timestamp,
|
| 1144 |
+
}
|
| 1145 |
+
for issue in report.fixed_issues
|
| 1146 |
+
],
|
| 1147 |
+
"system_info": report.system_info,
|
| 1148 |
+
"duration_ms": report.duration_ms,
|
| 1149 |
+
}
|
| 1150 |
+
|
| 1151 |
+
return report_dict
|
| 1152 |
+
except Exception as e:
|
| 1153 |
+
raise HTTPException(status_code=500, detail=f"خطا در اجرای اشکالیابی: {str(e)}")
|
| 1154 |
+
|
| 1155 |
+
|
| 1156 |
+
@app.get("/api/diagnostics/last")
|
| 1157 |
+
async def get_last_diagnostics():
|
| 1158 |
+
"""دریافت آخرین گزارش اشکالیابی"""
|
| 1159 |
+
report = diagnostics_service.get_last_report()
|
| 1160 |
+
if report:
|
| 1161 |
+
return report
|
| 1162 |
+
return {"message": "هیچ گزارشی موجود نیست"}
|
| 1163 |
+
|
| 1164 |
+
|
| 1165 |
+
# ===== Main =====
|
| 1166 |
+
|
| 1167 |
+
if __name__ == "__main__":
|
| 1168 |
+
print("""
|
| 1169 |
+
╔═════════════���═════════════════════════════════════════════╗
|
| 1170 |
+
║ 🚀 Crypto Monitor Extended API Server ║
|
| 1171 |
+
║ Version: 2.0.0 ║
|
| 1172 |
+
║ با پشتیبانی کامل از Provider Management & Pools ║
|
| 1173 |
+
╚═══════════════════════════════════════════════════════════╝
|
| 1174 |
+
""")
|
| 1175 |
+
|
| 1176 |
+
uvicorn.run(
|
| 1177 |
+
app,
|
| 1178 |
+
host="0.0.0.0",
|
| 1179 |
+
port=8000,
|
| 1180 |
+
log_level="info"
|
| 1181 |
+
)
|
| 1182 |
+
|
backend/services/auto_discovery_service.py
ADDED
|
@@ -0,0 +1,353 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Auto Discovery Service
|
| 3 |
+
----------------------
|
| 4 |
+
جستجوی خودکار منابع API رایگان با استفاده از موتور جستجوی DuckDuckGo و
|
| 5 |
+
تحلیل خروجی توسط مدلهای Hugging Face.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import asyncio
|
| 11 |
+
import json
|
| 12 |
+
import logging
|
| 13 |
+
import os
|
| 14 |
+
import re
|
| 15 |
+
from dataclasses import dataclass
|
| 16 |
+
from datetime import datetime
|
| 17 |
+
from typing import Any, Dict, List, Optional
|
| 18 |
+
|
| 19 |
+
try:
|
| 20 |
+
from duckduckgo_search import AsyncDDGS # type: ignore
|
| 21 |
+
except ImportError: # pragma: no cover
|
| 22 |
+
AsyncDDGS = None # type: ignore
|
| 23 |
+
|
| 24 |
+
try:
|
| 25 |
+
from huggingface_hub import InferenceClient # type: ignore
|
| 26 |
+
except ImportError: # pragma: no cover
|
| 27 |
+
InferenceClient = None # type: ignore
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
logger = logging.getLogger(__name__)
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
@dataclass
|
| 34 |
+
class DiscoveryResult:
|
| 35 |
+
"""نتیجهٔ نهایی جستجو و تحلیل"""
|
| 36 |
+
|
| 37 |
+
provider_id: str
|
| 38 |
+
name: str
|
| 39 |
+
category: str
|
| 40 |
+
base_url: str
|
| 41 |
+
requires_auth: bool
|
| 42 |
+
description: str
|
| 43 |
+
source_url: str
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
class AutoDiscoveryService:
|
| 47 |
+
"""
|
| 48 |
+
سرویس جستجوی خودکار منابع.
|
| 49 |
+
|
| 50 |
+
این سرویس:
|
| 51 |
+
1. با استفاده از DuckDuckGo نتایج مرتبط با APIهای رایگان را جمعآوری میکند.
|
| 52 |
+
2. متن نتایج را به مدل Hugging Face میفرستد تا پیشنهادهای ساختاریافته بازگردد.
|
| 53 |
+
3. پیشنهادهای معتبر را به ResourceManager اضافه میکند و در صورت تأیید، ProviderManager را ریفرش میکند.
|
| 54 |
+
"""
|
| 55 |
+
|
| 56 |
+
DEFAULT_QUERIES: List[str] = [
|
| 57 |
+
"free cryptocurrency market data api",
|
| 58 |
+
"open blockchain explorer api free tier",
|
| 59 |
+
"free defi protocol api documentation",
|
| 60 |
+
"open source sentiment analysis crypto api",
|
| 61 |
+
"public nft market data api no api key",
|
| 62 |
+
]
|
| 63 |
+
|
| 64 |
+
def __init__(
|
| 65 |
+
self,
|
| 66 |
+
resource_manager,
|
| 67 |
+
provider_manager,
|
| 68 |
+
enabled: bool = True,
|
| 69 |
+
):
|
| 70 |
+
self.resource_manager = resource_manager
|
| 71 |
+
self.provider_manager = provider_manager
|
| 72 |
+
self.enabled = enabled and os.getenv("ENABLE_AUTO_DISCOVERY", "true").lower() == "true"
|
| 73 |
+
self.interval_seconds = int(os.getenv("AUTO_DISCOVERY_INTERVAL_SECONDS", "43200"))
|
| 74 |
+
self.hf_model = os.getenv("AUTO_DISCOVERY_HF_MODEL", "HuggingFaceH4/zephyr-7b-beta")
|
| 75 |
+
self.max_candidates_per_query = int(os.getenv("AUTO_DISCOVERY_MAX_RESULTS", "8"))
|
| 76 |
+
self._hf_client: Optional[InferenceClient] = None
|
| 77 |
+
self._running_task: Optional[asyncio.Task] = None
|
| 78 |
+
self._last_run_summary: Optional[Dict[str, Any]] = None
|
| 79 |
+
|
| 80 |
+
if not self.enabled:
|
| 81 |
+
logger.info("Auto discovery service disabled via configuration.")
|
| 82 |
+
return
|
| 83 |
+
|
| 84 |
+
if AsyncDDGS is None:
|
| 85 |
+
logger.warning("duckduckgo-search package not available. Disabling auto discovery.")
|
| 86 |
+
self.enabled = False
|
| 87 |
+
return
|
| 88 |
+
|
| 89 |
+
if InferenceClient is None:
|
| 90 |
+
logger.warning("huggingface-hub package not available. Auto discovery will use fallback heuristics.")
|
| 91 |
+
else:
|
| 92 |
+
hf_token = os.getenv("HF_API_TOKEN")
|
| 93 |
+
try:
|
| 94 |
+
self._hf_client = InferenceClient(model=self.hf_model, token=hf_token)
|
| 95 |
+
logger.info("Auto discovery Hugging Face client initialized with model %s", self.hf_model)
|
| 96 |
+
except Exception as exc: # pragma: no cover - فقط برای شرایط عدم اتصال
|
| 97 |
+
logger.error("Failed to initialize Hugging Face client: %s", exc)
|
| 98 |
+
self._hf_client = None
|
| 99 |
+
|
| 100 |
+
async def start(self):
|
| 101 |
+
"""شروع سرویس و ساخت حلقهٔ دورهای."""
|
| 102 |
+
if not self.enabled:
|
| 103 |
+
return
|
| 104 |
+
if self._running_task and not self._running_task.done():
|
| 105 |
+
return
|
| 106 |
+
self._running_task = asyncio.create_task(self._run_periodic_loop())
|
| 107 |
+
logger.info("Auto discovery service started with interval %s seconds", self.interval_seconds)
|
| 108 |
+
|
| 109 |
+
async def stop(self):
|
| 110 |
+
"""توقف سرویس."""
|
| 111 |
+
if self._running_task:
|
| 112 |
+
self._running_task.cancel()
|
| 113 |
+
try:
|
| 114 |
+
await self._running_task
|
| 115 |
+
except asyncio.CancelledError:
|
| 116 |
+
pass
|
| 117 |
+
self._running_task = None
|
| 118 |
+
logger.info("Auto discovery service stopped.")
|
| 119 |
+
|
| 120 |
+
async def trigger_manual_discovery(self) -> Dict[str, Any]:
|
| 121 |
+
"""اجرای دستی یک چرخهٔ کشف."""
|
| 122 |
+
if not self.enabled:
|
| 123 |
+
return {"status": "disabled"}
|
| 124 |
+
summary = await self._run_discovery_cycle()
|
| 125 |
+
return {"status": "completed", "summary": summary}
|
| 126 |
+
|
| 127 |
+
def get_status(self) -> Dict[str, Any]:
|
| 128 |
+
"""وضعیت آخرین اجرا."""
|
| 129 |
+
return {
|
| 130 |
+
"enabled": self.enabled,
|
| 131 |
+
"model": self.hf_model if self._hf_client else None,
|
| 132 |
+
"interval_seconds": self.interval_seconds,
|
| 133 |
+
"last_run": self._last_run_summary,
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
async def _run_periodic_loop(self):
|
| 137 |
+
"""حلقهٔ اجرای دورهای."""
|
| 138 |
+
while self.enabled:
|
| 139 |
+
try:
|
| 140 |
+
await self._run_discovery_cycle()
|
| 141 |
+
except Exception as exc:
|
| 142 |
+
logger.exception("Auto discovery cycle failed: %s", exc)
|
| 143 |
+
await asyncio.sleep(self.interval_seconds)
|
| 144 |
+
|
| 145 |
+
async def _run_discovery_cycle(self) -> Dict[str, Any]:
|
| 146 |
+
"""یک چرخه کامل جستجو، تحلیل و ثبت."""
|
| 147 |
+
started_at = datetime.utcnow().isoformat()
|
| 148 |
+
candidates = await self._gather_candidates()
|
| 149 |
+
structured = await self._infer_candidates(candidates)
|
| 150 |
+
persisted = await self._persist_candidates(structured)
|
| 151 |
+
|
| 152 |
+
summary = {
|
| 153 |
+
"started_at": started_at,
|
| 154 |
+
"finished_at": datetime.utcnow().isoformat(),
|
| 155 |
+
"candidates_seen": len(candidates),
|
| 156 |
+
"suggested": len(structured),
|
| 157 |
+
"persisted": len(persisted),
|
| 158 |
+
"persisted_ids": [item.provider_id for item in persisted],
|
| 159 |
+
}
|
| 160 |
+
self._last_run_summary = summary
|
| 161 |
+
|
| 162 |
+
logger.info(
|
| 163 |
+
"Auto discovery cycle completed. candidates=%s suggested=%s persisted=%s",
|
| 164 |
+
summary["candidates_seen"],
|
| 165 |
+
summary["suggested"],
|
| 166 |
+
summary["persisted"],
|
| 167 |
+
)
|
| 168 |
+
return summary
|
| 169 |
+
|
| 170 |
+
async def _gather_candidates(self) -> List[Dict[str, Any]]:
|
| 171 |
+
"""جمعآوری نتایج موتور جستجو."""
|
| 172 |
+
if not self.enabled or AsyncDDGS is None:
|
| 173 |
+
return []
|
| 174 |
+
|
| 175 |
+
results: List[Dict[str, Any]] = []
|
| 176 |
+
queries = os.getenv("AUTO_DISCOVERY_QUERIES")
|
| 177 |
+
if queries:
|
| 178 |
+
query_list = [q.strip() for q in queries.split(";") if q.strip()]
|
| 179 |
+
else:
|
| 180 |
+
query_list = self.DEFAULT_QUERIES
|
| 181 |
+
|
| 182 |
+
async with AsyncDDGS() as ddgs:
|
| 183 |
+
for query in query_list:
|
| 184 |
+
try:
|
| 185 |
+
async for entry in ddgs.atext(
|
| 186 |
+
query,
|
| 187 |
+
max_results=self.max_candidates_per_query,
|
| 188 |
+
):
|
| 189 |
+
results.append(
|
| 190 |
+
{
|
| 191 |
+
"query": query,
|
| 192 |
+
"title": entry.get("title", ""),
|
| 193 |
+
"url": entry.get("href") or entry.get("url") or "",
|
| 194 |
+
"snippet": entry.get("body", ""),
|
| 195 |
+
}
|
| 196 |
+
)
|
| 197 |
+
except Exception as exc: # pragma: no cover - وابسته به اینترنت
|
| 198 |
+
logger.warning("Failed to fetch results for query '%s': %s", query, exc)
|
| 199 |
+
|
| 200 |
+
return results
|
| 201 |
+
|
| 202 |
+
async def _infer_candidates(self, candidates: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
| 203 |
+
"""تحلیل نتایج با مدل Hugging Face یا قواعد ساده."""
|
| 204 |
+
if not candidates:
|
| 205 |
+
return []
|
| 206 |
+
|
| 207 |
+
if self._hf_client:
|
| 208 |
+
prompt = self._build_prompt(candidates)
|
| 209 |
+
try:
|
| 210 |
+
response = await asyncio.to_thread(
|
| 211 |
+
self._hf_client.text_generation,
|
| 212 |
+
prompt,
|
| 213 |
+
max_new_tokens=512,
|
| 214 |
+
temperature=0.1,
|
| 215 |
+
top_p=0.9,
|
| 216 |
+
repetition_penalty=1.1,
|
| 217 |
+
)
|
| 218 |
+
return self._parse_model_response(response)
|
| 219 |
+
except Exception as exc: # pragma: no cover
|
| 220 |
+
logger.warning("Hugging Face inference failed: %s", exc)
|
| 221 |
+
|
| 222 |
+
# fallback rule-based
|
| 223 |
+
return self._rule_based_filter(candidates)
|
| 224 |
+
|
| 225 |
+
def _build_prompt(self, candidates: List[Dict[str, Any]]) -> str:
|
| 226 |
+
"""ساخت پرامپت برای مدل LLM."""
|
| 227 |
+
context_lines = []
|
| 228 |
+
for idx, item in enumerate(candidates, start=1):
|
| 229 |
+
context_lines.append(
|
| 230 |
+
f"{idx}. Title: {item.get('title')}\n"
|
| 231 |
+
f" URL: {item.get('url')}\n"
|
| 232 |
+
f" Snippet: {item.get('snippet')}"
|
| 233 |
+
)
|
| 234 |
+
|
| 235 |
+
return (
|
| 236 |
+
"You are an expert agent that extracts publicly accessible API providers for cryptocurrency, "
|
| 237 |
+
"blockchain, DeFi, sentiment, NFT or analytics data. From the context entries, select candidates "
|
| 238 |
+
"that represent real API services which are freely accessible (free tier or free plan). "
|
| 239 |
+
"Return ONLY a JSON array. Each entry MUST include keys: "
|
| 240 |
+
"id (lowercase snake_case), name, base_url, category (one of: market_data, blockchain_explorers, "
|
| 241 |
+
"defi, sentiment, nft, analytics, news, rpc, huggingface, whale_tracking, onchain_analytics, custom), "
|
| 242 |
+
"requires_auth (boolean), description (short string), source_url (string). "
|
| 243 |
+
"Do not invent APIs. Ignore SDKs, articles, or paid-only services. "
|
| 244 |
+
"If no valid candidate exists, return an empty JSON array.\n\n"
|
| 245 |
+
"Context:\n"
|
| 246 |
+
+ "\n".join(context_lines)
|
| 247 |
+
)
|
| 248 |
+
|
| 249 |
+
def _parse_model_response(self, response: str) -> List[Dict[str, Any]]:
|
| 250 |
+
"""تبدیل پاسخ مدل به ساختار داده."""
|
| 251 |
+
try:
|
| 252 |
+
match = re.search(r"\[.*\]", response, re.DOTALL)
|
| 253 |
+
if not match:
|
| 254 |
+
logger.debug("Model response did not contain JSON array.")
|
| 255 |
+
return []
|
| 256 |
+
data = json.loads(match.group(0))
|
| 257 |
+
if isinstance(data, list):
|
| 258 |
+
return [item for item in data if isinstance(item, dict)]
|
| 259 |
+
return []
|
| 260 |
+
except json.JSONDecodeError:
|
| 261 |
+
logger.debug("Failed to decode model JSON response.")
|
| 262 |
+
return []
|
| 263 |
+
|
| 264 |
+
def _rule_based_filter(self, candidates: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
| 265 |
+
"""فیلتر ساده در صورت در دسترس نبودن مدل."""
|
| 266 |
+
structured: List[Dict[str, Any]] = []
|
| 267 |
+
for item in candidates:
|
| 268 |
+
url = item.get("url", "")
|
| 269 |
+
snippet = (item.get("snippet") or "").lower()
|
| 270 |
+
title = (item.get("title") or "").lower()
|
| 271 |
+
if not url or "github" in url:
|
| 272 |
+
continue
|
| 273 |
+
if "api" not in title and "api" not in snippet:
|
| 274 |
+
continue
|
| 275 |
+
if any(keyword in snippet for keyword in ["pricing", "paid plan", "enterprise only"]):
|
| 276 |
+
continue
|
| 277 |
+
provider_id = self._normalize_id(item.get("title") or url)
|
| 278 |
+
structured.append(
|
| 279 |
+
{
|
| 280 |
+
"id": provider_id,
|
| 281 |
+
"name": item.get("title") or provider_id,
|
| 282 |
+
"base_url": url,
|
| 283 |
+
"category": "custom",
|
| 284 |
+
"requires_auth": "token" in snippet or "apikey" in snippet,
|
| 285 |
+
"description": item.get("snippet", ""),
|
| 286 |
+
"source_url": url,
|
| 287 |
+
}
|
| 288 |
+
)
|
| 289 |
+
return structured
|
| 290 |
+
|
| 291 |
+
async def _persist_candidates(self, structured: List[Dict[str, Any]]) -> List[DiscoveryResult]:
|
| 292 |
+
"""ذخیرهٔ پیشنهادهای معتبر."""
|
| 293 |
+
persisted: List[DiscoveryResult] = []
|
| 294 |
+
if not structured:
|
| 295 |
+
return persisted
|
| 296 |
+
|
| 297 |
+
for entry in structured:
|
| 298 |
+
provider_id = self._normalize_id(entry.get("id") or entry.get("name"))
|
| 299 |
+
base_url = entry.get("base_url", "")
|
| 300 |
+
|
| 301 |
+
if not base_url.startswith(("http://", "https://")):
|
| 302 |
+
continue
|
| 303 |
+
|
| 304 |
+
if self.resource_manager.get_provider(provider_id):
|
| 305 |
+
continue
|
| 306 |
+
|
| 307 |
+
provider_data = {
|
| 308 |
+
"id": provider_id,
|
| 309 |
+
"name": entry.get("name", provider_id),
|
| 310 |
+
"category": entry.get("category", "custom"),
|
| 311 |
+
"base_url": base_url,
|
| 312 |
+
"requires_auth": bool(entry.get("requires_auth")),
|
| 313 |
+
"priority": 4,
|
| 314 |
+
"weight": 40,
|
| 315 |
+
"notes": entry.get("description", ""),
|
| 316 |
+
"docs_url": entry.get("source_url", base_url),
|
| 317 |
+
"free": True,
|
| 318 |
+
"endpoints": {},
|
| 319 |
+
}
|
| 320 |
+
|
| 321 |
+
is_valid, message = self.resource_manager.validate_provider(provider_data)
|
| 322 |
+
if not is_valid:
|
| 323 |
+
logger.debug("Skipping provider %s: %s", provider_id, message)
|
| 324 |
+
continue
|
| 325 |
+
|
| 326 |
+
await asyncio.to_thread(self.resource_manager.add_provider, provider_data)
|
| 327 |
+
persisted.append(
|
| 328 |
+
DiscoveryResult(
|
| 329 |
+
provider_id=provider_id,
|
| 330 |
+
name=provider_data["name"],
|
| 331 |
+
category=provider_data["category"],
|
| 332 |
+
base_url=provider_data["base_url"],
|
| 333 |
+
requires_auth=provider_data["requires_auth"],
|
| 334 |
+
description=provider_data["notes"],
|
| 335 |
+
source_url=provider_data["docs_url"],
|
| 336 |
+
)
|
| 337 |
+
)
|
| 338 |
+
|
| 339 |
+
if persisted:
|
| 340 |
+
await asyncio.to_thread(self.resource_manager.save_resources)
|
| 341 |
+
await asyncio.to_thread(self.provider_manager.load_config)
|
| 342 |
+
logger.info("Persisted %s new providers.", len(persisted))
|
| 343 |
+
|
| 344 |
+
return persisted
|
| 345 |
+
|
| 346 |
+
@staticmethod
|
| 347 |
+
def _normalize_id(raw_value: Optional[str]) -> str:
|
| 348 |
+
"""تبدیل نام به شناسهٔ مناسب."""
|
| 349 |
+
if not raw_value:
|
| 350 |
+
return "unknown_provider"
|
| 351 |
+
cleaned = re.sub(r"[^a-zA-Z0-9]+", "_", raw_value).strip("_").lower()
|
| 352 |
+
return cleaned or "unknown_provider"
|
| 353 |
+
|
backend/services/connection_manager.py
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Connection Manager - مدیریت اتصالات WebSocket و Session
|
| 3 |
+
"""
|
| 4 |
+
import asyncio
|
| 5 |
+
import json
|
| 6 |
+
import uuid
|
| 7 |
+
from typing import Dict, Set, Optional, Any
|
| 8 |
+
from datetime import datetime
|
| 9 |
+
from dataclasses import dataclass, asdict
|
| 10 |
+
from fastapi import WebSocket
|
| 11 |
+
import logging
|
| 12 |
+
|
| 13 |
+
logger = logging.getLogger(__name__)
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
@dataclass
|
| 17 |
+
class ClientSession:
|
| 18 |
+
"""اطلاعات Session کلاینت"""
|
| 19 |
+
session_id: str
|
| 20 |
+
client_type: str # 'browser', 'api', 'mobile'
|
| 21 |
+
connected_at: datetime
|
| 22 |
+
last_activity: datetime
|
| 23 |
+
ip_address: Optional[str] = None
|
| 24 |
+
user_agent: Optional[str] = None
|
| 25 |
+
metadata: Dict[str, Any] = None
|
| 26 |
+
|
| 27 |
+
def to_dict(self):
|
| 28 |
+
return {
|
| 29 |
+
'session_id': self.session_id,
|
| 30 |
+
'client_type': self.client_type,
|
| 31 |
+
'connected_at': self.connected_at.isoformat(),
|
| 32 |
+
'last_activity': self.last_activity.isoformat(),
|
| 33 |
+
'ip_address': self.ip_address,
|
| 34 |
+
'user_agent': self.user_agent,
|
| 35 |
+
'metadata': self.metadata or {}
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
class ConnectionManager:
|
| 40 |
+
"""مدیر اتصالات WebSocket و Session"""
|
| 41 |
+
|
| 42 |
+
def __init__(self):
|
| 43 |
+
# WebSocket connections
|
| 44 |
+
self.active_connections: Dict[str, WebSocket] = {}
|
| 45 |
+
|
| 46 |
+
# Sessions (برای همه انواع کلاینتها)
|
| 47 |
+
self.sessions: Dict[str, ClientSession] = {}
|
| 48 |
+
|
| 49 |
+
# Subscription groups (برای broadcast انتخابی)
|
| 50 |
+
self.subscriptions: Dict[str, Set[str]] = {
|
| 51 |
+
'market': set(),
|
| 52 |
+
'prices': set(),
|
| 53 |
+
'news': set(),
|
| 54 |
+
'alerts': set(),
|
| 55 |
+
'all': set()
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
# Statistics
|
| 59 |
+
self.total_connections = 0
|
| 60 |
+
self.total_messages_sent = 0
|
| 61 |
+
self.total_messages_received = 0
|
| 62 |
+
|
| 63 |
+
async def connect(
|
| 64 |
+
self,
|
| 65 |
+
websocket: WebSocket,
|
| 66 |
+
client_type: str = 'browser',
|
| 67 |
+
metadata: Optional[Dict] = None
|
| 68 |
+
) -> str:
|
| 69 |
+
"""
|
| 70 |
+
اتصال کلاینت جدید
|
| 71 |
+
|
| 72 |
+
Returns:
|
| 73 |
+
session_id
|
| 74 |
+
"""
|
| 75 |
+
await websocket.accept()
|
| 76 |
+
|
| 77 |
+
session_id = str(uuid.uuid4())
|
| 78 |
+
|
| 79 |
+
# ذخیره WebSocket
|
| 80 |
+
self.active_connections[session_id] = websocket
|
| 81 |
+
|
| 82 |
+
# ایجاد Session
|
| 83 |
+
session = ClientSession(
|
| 84 |
+
session_id=session_id,
|
| 85 |
+
client_type=client_type,
|
| 86 |
+
connected_at=datetime.now(),
|
| 87 |
+
last_activity=datetime.now(),
|
| 88 |
+
metadata=metadata or {}
|
| 89 |
+
)
|
| 90 |
+
self.sessions[session_id] = session
|
| 91 |
+
|
| 92 |
+
# Subscribe به گروه all
|
| 93 |
+
self.subscriptions['all'].add(session_id)
|
| 94 |
+
|
| 95 |
+
self.total_connections += 1
|
| 96 |
+
|
| 97 |
+
logger.info(f"Client connected: {session_id} ({client_type})")
|
| 98 |
+
|
| 99 |
+
# اطلاع به همه از تعداد کاربران آنلاین
|
| 100 |
+
await self.broadcast_stats()
|
| 101 |
+
|
| 102 |
+
return session_id
|
| 103 |
+
|
| 104 |
+
def disconnect(self, session_id: str):
|
| 105 |
+
"""قطع اتصال کلاینت"""
|
| 106 |
+
# حذف WebSocket
|
| 107 |
+
if session_id in self.active_connections:
|
| 108 |
+
del self.active_connections[session_id]
|
| 109 |
+
|
| 110 |
+
# حذف از subscriptions
|
| 111 |
+
for group in self.subscriptions.values():
|
| 112 |
+
group.discard(session_id)
|
| 113 |
+
|
| 114 |
+
# حذف session
|
| 115 |
+
if session_id in self.sessions:
|
| 116 |
+
del self.sessions[session_id]
|
| 117 |
+
|
| 118 |
+
logger.info(f"Client disconnected: {session_id}")
|
| 119 |
+
|
| 120 |
+
# اطلاع به همه
|
| 121 |
+
asyncio.create_task(self.broadcast_stats())
|
| 122 |
+
|
| 123 |
+
async def send_personal_message(
|
| 124 |
+
self,
|
| 125 |
+
message: Dict[str, Any],
|
| 126 |
+
session_id: str
|
| 127 |
+
):
|
| 128 |
+
"""ارسال پیام به یک کلاینت خاص"""
|
| 129 |
+
if session_id in self.active_connections:
|
| 130 |
+
try:
|
| 131 |
+
websocket = self.active_connections[session_id]
|
| 132 |
+
await websocket.send_json(message)
|
| 133 |
+
|
| 134 |
+
# بهروزرسانی آخرین فعالیت
|
| 135 |
+
if session_id in self.sessions:
|
| 136 |
+
self.sessions[session_id].last_activity = datetime.now()
|
| 137 |
+
|
| 138 |
+
self.total_messages_sent += 1
|
| 139 |
+
|
| 140 |
+
except Exception as e:
|
| 141 |
+
logger.error(f"Error sending message to {session_id}: {e}")
|
| 142 |
+
self.disconnect(session_id)
|
| 143 |
+
|
| 144 |
+
async def broadcast(
|
| 145 |
+
self,
|
| 146 |
+
message: Dict[str, Any],
|
| 147 |
+
group: str = 'all'
|
| 148 |
+
):
|
| 149 |
+
"""ارسال پیام به گروهی از کلاینتها"""
|
| 150 |
+
if group not in self.subscriptions:
|
| 151 |
+
group = 'all'
|
| 152 |
+
|
| 153 |
+
session_ids = self.subscriptions[group].copy()
|
| 154 |
+
|
| 155 |
+
disconnected = []
|
| 156 |
+
for session_id in session_ids:
|
| 157 |
+
if session_id in self.active_connections:
|
| 158 |
+
try:
|
| 159 |
+
websocket = self.active_connections[session_id]
|
| 160 |
+
await websocket.send_json(message)
|
| 161 |
+
self.total_messages_sent += 1
|
| 162 |
+
except Exception as e:
|
| 163 |
+
logger.error(f"Error broadcasting to {session_id}: {e}")
|
| 164 |
+
disconnected.append(session_id)
|
| 165 |
+
|
| 166 |
+
# پاکسازی اتصالات قطع شده
|
| 167 |
+
for session_id in disconnected:
|
| 168 |
+
self.disconnect(session_id)
|
| 169 |
+
|
| 170 |
+
async def broadcast_stats(self):
|
| 171 |
+
"""ارسال آمار کلی به همه کلاینتها"""
|
| 172 |
+
stats = self.get_stats()
|
| 173 |
+
await self.broadcast({
|
| 174 |
+
'type': 'stats_update',
|
| 175 |
+
'data': stats,
|
| 176 |
+
'timestamp': datetime.now().isoformat()
|
| 177 |
+
})
|
| 178 |
+
|
| 179 |
+
def subscribe(self, session_id: str, group: str):
|
| 180 |
+
"""اضافه کردن به گروه subscription"""
|
| 181 |
+
if group in self.subscriptions:
|
| 182 |
+
self.subscriptions[group].add(session_id)
|
| 183 |
+
logger.info(f"Session {session_id} subscribed to {group}")
|
| 184 |
+
return True
|
| 185 |
+
return False
|
| 186 |
+
|
| 187 |
+
def unsubscribe(self, session_id: str, group: str):
|
| 188 |
+
"""حذف از گروه subscription"""
|
| 189 |
+
if group in self.subscriptions:
|
| 190 |
+
self.subscriptions[group].discard(session_id)
|
| 191 |
+
logger.info(f"Session {session_id} unsubscribed from {group}")
|
| 192 |
+
return True
|
| 193 |
+
return False
|
| 194 |
+
|
| 195 |
+
def get_stats(self) -> Dict[str, Any]:
|
| 196 |
+
"""دریافت آمار اتصالات"""
|
| 197 |
+
# تفکیک بر اساس نوع کلاینت
|
| 198 |
+
client_types = {}
|
| 199 |
+
for session in self.sessions.values():
|
| 200 |
+
client_type = session.client_type
|
| 201 |
+
client_types[client_type] = client_types.get(client_type, 0) + 1
|
| 202 |
+
|
| 203 |
+
# آمار subscriptions
|
| 204 |
+
subscription_stats = {
|
| 205 |
+
group: len(members)
|
| 206 |
+
for group, members in self.subscriptions.items()
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
return {
|
| 210 |
+
'active_connections': len(self.active_connections),
|
| 211 |
+
'total_sessions': len(self.sessions),
|
| 212 |
+
'total_connections_ever': self.total_connections,
|
| 213 |
+
'messages_sent': self.total_messages_sent,
|
| 214 |
+
'messages_received': self.total_messages_received,
|
| 215 |
+
'client_types': client_types,
|
| 216 |
+
'subscriptions': subscription_stats,
|
| 217 |
+
'timestamp': datetime.now().isoformat()
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
def get_sessions(self) -> Dict[str, Dict[str, Any]]:
|
| 221 |
+
"""دریافت لیست sessionهای فعال"""
|
| 222 |
+
return {
|
| 223 |
+
sid: session.to_dict()
|
| 224 |
+
for sid, session in self.sessions.items()
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
async def send_market_update(self, data: Dict[str, Any]):
|
| 228 |
+
"""ارسال بهروزرسانی بازار"""
|
| 229 |
+
await self.broadcast({
|
| 230 |
+
'type': 'market_update',
|
| 231 |
+
'data': data,
|
| 232 |
+
'timestamp': datetime.now().isoformat()
|
| 233 |
+
}, group='market')
|
| 234 |
+
|
| 235 |
+
async def send_price_update(self, symbol: str, price: float, change: float):
|
| 236 |
+
"""ارسال بهروزرسانی قیمت"""
|
| 237 |
+
await self.broadcast({
|
| 238 |
+
'type': 'price_update',
|
| 239 |
+
'data': {
|
| 240 |
+
'symbol': symbol,
|
| 241 |
+
'price': price,
|
| 242 |
+
'change_24h': change
|
| 243 |
+
},
|
| 244 |
+
'timestamp': datetime.now().isoformat()
|
| 245 |
+
}, group='prices')
|
| 246 |
+
|
| 247 |
+
async def send_alert(self, alert_type: str, message: str, severity: str = 'info'):
|
| 248 |
+
"""ارسال هشدار"""
|
| 249 |
+
await self.broadcast({
|
| 250 |
+
'type': 'alert',
|
| 251 |
+
'data': {
|
| 252 |
+
'alert_type': alert_type,
|
| 253 |
+
'message': message,
|
| 254 |
+
'severity': severity
|
| 255 |
+
},
|
| 256 |
+
'timestamp': datetime.now().isoformat()
|
| 257 |
+
}, group='alerts')
|
| 258 |
+
|
| 259 |
+
async def heartbeat(self):
|
| 260 |
+
"""ارسال heartbeat برای check کردن اتصالات"""
|
| 261 |
+
await self.broadcast({
|
| 262 |
+
'type': 'heartbeat',
|
| 263 |
+
'timestamp': datetime.now().isoformat()
|
| 264 |
+
})
|
| 265 |
+
|
| 266 |
+
|
| 267 |
+
# Global instance
|
| 268 |
+
connection_manager = ConnectionManager()
|
| 269 |
+
|
| 270 |
+
|
| 271 |
+
def get_connection_manager() -> ConnectionManager:
|
| 272 |
+
"""دریافت instance مدیر اتصالات"""
|
| 273 |
+
return connection_manager
|
| 274 |
+
|
backend/services/diagnostics_service.py
ADDED
|
@@ -0,0 +1,391 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Diagnostics & Auto-Repair Service
|
| 3 |
+
----------------------------------
|
| 4 |
+
سرویس اشکالیابی خودکار و تعمیر مشکلات سیستم
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import asyncio
|
| 8 |
+
import logging
|
| 9 |
+
import os
|
| 10 |
+
import subprocess
|
| 11 |
+
import sys
|
| 12 |
+
from dataclasses import dataclass, asdict
|
| 13 |
+
from datetime import datetime
|
| 14 |
+
from typing import Any, Dict, List, Optional, Tuple
|
| 15 |
+
import json
|
| 16 |
+
import importlib.util
|
| 17 |
+
|
| 18 |
+
logger = logging.getLogger(__name__)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
@dataclass
|
| 22 |
+
class DiagnosticIssue:
|
| 23 |
+
"""یک مشکل شناسایی شده"""
|
| 24 |
+
severity: str # critical, warning, info
|
| 25 |
+
category: str # dependency, config, network, service, model
|
| 26 |
+
title: str
|
| 27 |
+
description: str
|
| 28 |
+
fixable: bool
|
| 29 |
+
fix_action: Optional[str] = None
|
| 30 |
+
auto_fixed: bool = False
|
| 31 |
+
timestamp: str = None
|
| 32 |
+
|
| 33 |
+
def __post_init__(self):
|
| 34 |
+
if self.timestamp is None:
|
| 35 |
+
self.timestamp = datetime.now().isoformat()
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
@dataclass
|
| 39 |
+
class DiagnosticReport:
|
| 40 |
+
"""گزارش کامل اشکالیابی"""
|
| 41 |
+
timestamp: str
|
| 42 |
+
total_issues: int
|
| 43 |
+
critical_issues: int
|
| 44 |
+
warnings: int
|
| 45 |
+
info_issues: int
|
| 46 |
+
issues: List[DiagnosticIssue]
|
| 47 |
+
fixed_issues: List[DiagnosticIssue]
|
| 48 |
+
system_info: Dict[str, Any]
|
| 49 |
+
duration_ms: float
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
class DiagnosticsService:
|
| 53 |
+
"""سرویس اشکالیابی و تعمیر خودکار"""
|
| 54 |
+
|
| 55 |
+
def __init__(self, resource_manager=None, provider_manager=None, auto_discovery_service=None):
|
| 56 |
+
self.resource_manager = resource_manager
|
| 57 |
+
self.provider_manager = provider_manager
|
| 58 |
+
self.auto_discovery_service = auto_discovery_service
|
| 59 |
+
self.last_report: Optional[DiagnosticReport] = None
|
| 60 |
+
|
| 61 |
+
async def run_full_diagnostics(self, auto_fix: bool = False) -> DiagnosticReport:
|
| 62 |
+
"""اجرای کامل اشکالیابی"""
|
| 63 |
+
start_time = datetime.now()
|
| 64 |
+
issues: List[DiagnosticIssue] = []
|
| 65 |
+
fixed_issues: List[DiagnosticIssue] = []
|
| 66 |
+
|
| 67 |
+
# بررسی وابستگیها
|
| 68 |
+
issues.extend(await self._check_dependencies())
|
| 69 |
+
|
| 70 |
+
# بررسی تنظیمات
|
| 71 |
+
issues.extend(await self._check_configuration())
|
| 72 |
+
|
| 73 |
+
# بررسی شبکه
|
| 74 |
+
issues.extend(await self._check_network())
|
| 75 |
+
|
| 76 |
+
# بررسی سرویسها
|
| 77 |
+
issues.extend(await self._check_services())
|
| 78 |
+
|
| 79 |
+
# بررسی مدلها
|
| 80 |
+
issues.extend(await self._check_models())
|
| 81 |
+
|
| 82 |
+
# بررسی فایلها و دایرکتوریها
|
| 83 |
+
issues.extend(await self._check_filesystem())
|
| 84 |
+
|
| 85 |
+
# اجرای تعمیر خودکار
|
| 86 |
+
if auto_fix:
|
| 87 |
+
for issue in issues:
|
| 88 |
+
if issue.fixable and issue.fix_action:
|
| 89 |
+
fixed = await self._apply_fix(issue)
|
| 90 |
+
if fixed:
|
| 91 |
+
issue.auto_fixed = True
|
| 92 |
+
fixed_issues.append(issue)
|
| 93 |
+
|
| 94 |
+
# محاسبه آمار
|
| 95 |
+
critical = sum(1 for i in issues if i.severity == 'critical')
|
| 96 |
+
warnings = sum(1 for i in issues if i.severity == 'warning')
|
| 97 |
+
info_count = sum(1 for i in issues if i.severity == 'info')
|
| 98 |
+
|
| 99 |
+
duration_ms = (datetime.now() - start_time).total_seconds() * 1000
|
| 100 |
+
|
| 101 |
+
report = DiagnosticReport(
|
| 102 |
+
timestamp=datetime.now().isoformat(),
|
| 103 |
+
total_issues=len(issues),
|
| 104 |
+
critical_issues=critical,
|
| 105 |
+
warnings=warnings,
|
| 106 |
+
info_issues=info_count,
|
| 107 |
+
issues=issues,
|
| 108 |
+
fixed_issues=fixed_issues,
|
| 109 |
+
system_info=await self._get_system_info(),
|
| 110 |
+
duration_ms=duration_ms
|
| 111 |
+
)
|
| 112 |
+
|
| 113 |
+
self.last_report = report
|
| 114 |
+
return report
|
| 115 |
+
|
| 116 |
+
async def _check_dependencies(self) -> List[DiagnosticIssue]:
|
| 117 |
+
"""بررسی وابستگیهای Python"""
|
| 118 |
+
issues = []
|
| 119 |
+
required_packages = {
|
| 120 |
+
'fastapi': 'FastAPI',
|
| 121 |
+
'uvicorn': 'Uvicorn',
|
| 122 |
+
'httpx': 'HTTPX',
|
| 123 |
+
'pydantic': 'Pydantic',
|
| 124 |
+
'duckduckgo_search': 'DuckDuckGo Search',
|
| 125 |
+
'huggingface_hub': 'HuggingFace Hub',
|
| 126 |
+
'transformers': 'Transformers',
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
for package, name in required_packages.items():
|
| 130 |
+
try:
|
| 131 |
+
spec = importlib.util.find_spec(package)
|
| 132 |
+
if spec is None:
|
| 133 |
+
issues.append(DiagnosticIssue(
|
| 134 |
+
severity='critical' if package in ['fastapi', 'uvicorn'] else 'warning',
|
| 135 |
+
category='dependency',
|
| 136 |
+
title=f'بسته {name} نصب نشده است',
|
| 137 |
+
description=f'بسته {package} مورد نیاز است اما نصب نشده است.',
|
| 138 |
+
fixable=True,
|
| 139 |
+
fix_action=f'pip install {package}'
|
| 140 |
+
))
|
| 141 |
+
except Exception as e:
|
| 142 |
+
issues.append(DiagnosticIssue(
|
| 143 |
+
severity='warning',
|
| 144 |
+
category='dependency',
|
| 145 |
+
title=f'خطا در بررسی {name}',
|
| 146 |
+
description=f'خطا در بررسی بسته {package}: {str(e)}',
|
| 147 |
+
fixable=False
|
| 148 |
+
))
|
| 149 |
+
|
| 150 |
+
return issues
|
| 151 |
+
|
| 152 |
+
async def _check_configuration(self) -> List[DiagnosticIssue]:
|
| 153 |
+
"""بررسی تنظیمات"""
|
| 154 |
+
issues = []
|
| 155 |
+
|
| 156 |
+
# بررسی متغیرهای محیطی مهم
|
| 157 |
+
important_env_vars = {
|
| 158 |
+
'HF_API_TOKEN': ('warning', 'توکن HuggingFace برای استفاده از مدلها'),
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
for var, (severity, desc) in important_env_vars.items():
|
| 162 |
+
if not os.getenv(var):
|
| 163 |
+
issues.append(DiagnosticIssue(
|
| 164 |
+
severity=severity,
|
| 165 |
+
category='config',
|
| 166 |
+
title=f'متغیر محیطی {var} تنظیم نشده',
|
| 167 |
+
description=desc,
|
| 168 |
+
fixable=False
|
| 169 |
+
))
|
| 170 |
+
|
| 171 |
+
# بررسی فایلهای پیکربندی
|
| 172 |
+
config_files = ['resources.json', 'config.json']
|
| 173 |
+
for config_file in config_files:
|
| 174 |
+
if not os.path.exists(config_file):
|
| 175 |
+
issues.append(DiagnosticIssue(
|
| 176 |
+
severity='info',
|
| 177 |
+
category='config',
|
| 178 |
+
title=f'فایل پیکربندی {config_file} وجود ندارد',
|
| 179 |
+
description=f'فایل {config_file} یافت نشد. ممکن است به صورت خودکار ساخته شود.',
|
| 180 |
+
fixable=False
|
| 181 |
+
))
|
| 182 |
+
|
| 183 |
+
return issues
|
| 184 |
+
|
| 185 |
+
async def _check_network(self) -> List[DiagnosticIssue]:
|
| 186 |
+
"""بررسی اتصال شبکه"""
|
| 187 |
+
issues = []
|
| 188 |
+
import httpx
|
| 189 |
+
|
| 190 |
+
test_urls = [
|
| 191 |
+
('https://api.coingecko.com/api/v3/ping', 'CoinGecko API'),
|
| 192 |
+
('https://api.huggingface.co', 'HuggingFace API'),
|
| 193 |
+
]
|
| 194 |
+
|
| 195 |
+
for url, name in test_urls:
|
| 196 |
+
try:
|
| 197 |
+
async with httpx.AsyncClient(timeout=5.0) as client:
|
| 198 |
+
response = await client.get(url)
|
| 199 |
+
if response.status_code >= 400:
|
| 200 |
+
issues.append(DiagnosticIssue(
|
| 201 |
+
severity='warning',
|
| 202 |
+
category='network',
|
| 203 |
+
title=f'مشکل در اتصال به {name}',
|
| 204 |
+
description=f'درخواست به {url} با کد {response.status_code} پاسخ داد.',
|
| 205 |
+
fixable=False
|
| 206 |
+
))
|
| 207 |
+
except Exception as e:
|
| 208 |
+
issues.append(DiagnosticIssue(
|
| 209 |
+
severity='warning',
|
| 210 |
+
category='network',
|
| 211 |
+
title=f'عدم دسترسی به {name}',
|
| 212 |
+
description=f'خطا در اتصال به {url}: {str(e)}',
|
| 213 |
+
fixable=False
|
| 214 |
+
))
|
| 215 |
+
|
| 216 |
+
return issues
|
| 217 |
+
|
| 218 |
+
async def _check_services(self) -> List[DiagnosticIssue]:
|
| 219 |
+
"""بررسی سرویسها"""
|
| 220 |
+
issues = []
|
| 221 |
+
|
| 222 |
+
# بررسی Auto-Discovery Service
|
| 223 |
+
if self.auto_discovery_service:
|
| 224 |
+
status = self.auto_discovery_service.get_status()
|
| 225 |
+
if not status.get('enabled'):
|
| 226 |
+
issues.append(DiagnosticIssue(
|
| 227 |
+
severity='info',
|
| 228 |
+
category='service',
|
| 229 |
+
title='سرویس Auto-Discovery غیرفعال است',
|
| 230 |
+
description='سرویس جستجوی خودکار منابع غیرفعال است.',
|
| 231 |
+
fixable=False
|
| 232 |
+
))
|
| 233 |
+
elif not status.get('model'):
|
| 234 |
+
issues.append(DiagnosticIssue(
|
| 235 |
+
severity='warning',
|
| 236 |
+
category='service',
|
| 237 |
+
title='مدل HuggingFace برای Auto-Discovery تنظیم نشده',
|
| 238 |
+
description='سرویس Auto-Discovery بدون مدل HuggingFace کار میکند.',
|
| 239 |
+
fixable=False
|
| 240 |
+
))
|
| 241 |
+
|
| 242 |
+
# بررسی Provider Manager
|
| 243 |
+
if self.provider_manager:
|
| 244 |
+
stats = self.provider_manager.get_all_stats()
|
| 245 |
+
summary = stats.get('summary', {})
|
| 246 |
+
if summary.get('online', 0) == 0 and summary.get('total_providers', 0) > 0:
|
| 247 |
+
issues.append(DiagnosticIssue(
|
| 248 |
+
severity='critical',
|
| 249 |
+
category='service',
|
| 250 |
+
title='هیچ Provider آنلاینی وجود ندارد',
|
| 251 |
+
description='تمام Providerها آفلاین هستند.',
|
| 252 |
+
fixable=False
|
| 253 |
+
))
|
| 254 |
+
|
| 255 |
+
return issues
|
| 256 |
+
|
| 257 |
+
async def _check_models(self) -> List[DiagnosticIssue]:
|
| 258 |
+
"""بررسی وضعیت مدلهای HuggingFace"""
|
| 259 |
+
issues = []
|
| 260 |
+
|
| 261 |
+
try:
|
| 262 |
+
from huggingface_hub import InferenceClient, HfApi
|
| 263 |
+
api = HfApi()
|
| 264 |
+
|
| 265 |
+
# بررسی مدلهای استفاده شده
|
| 266 |
+
models_to_check = [
|
| 267 |
+
'HuggingFaceH4/zephyr-7b-beta',
|
| 268 |
+
'cardiffnlp/twitter-roberta-base-sentiment-latest',
|
| 269 |
+
]
|
| 270 |
+
|
| 271 |
+
for model_id in models_to_check:
|
| 272 |
+
try:
|
| 273 |
+
model_info = api.model_info(model_id, timeout=5.0)
|
| 274 |
+
if not model_info:
|
| 275 |
+
issues.append(DiagnosticIssue(
|
| 276 |
+
severity='warning',
|
| 277 |
+
category='model',
|
| 278 |
+
title=f'مدل {model_id} در دسترس نیست',
|
| 279 |
+
description=f'نمیتوان به اطلاعات مدل {model_id} دسترسی پیدا کرد.',
|
| 280 |
+
fixable=False
|
| 281 |
+
))
|
| 282 |
+
except Exception as e:
|
| 283 |
+
issues.append(DiagnosticIssue(
|
| 284 |
+
severity='warning',
|
| 285 |
+
category='model',
|
| 286 |
+
title=f'خطا در بررسی مدل {model_id}',
|
| 287 |
+
description=f'خطا: {str(e)}',
|
| 288 |
+
fixable=False
|
| 289 |
+
))
|
| 290 |
+
except ImportError:
|
| 291 |
+
issues.append(DiagnosticIssue(
|
| 292 |
+
severity='info',
|
| 293 |
+
category='model',
|
| 294 |
+
title='بسته huggingface_hub نصب نشده',
|
| 295 |
+
description='برای بررسی مدلها نیاز به نصب huggingface_hub است.',
|
| 296 |
+
fixable=True,
|
| 297 |
+
fix_action='pip install huggingface_hub'
|
| 298 |
+
))
|
| 299 |
+
|
| 300 |
+
return issues
|
| 301 |
+
|
| 302 |
+
async def _check_filesystem(self) -> List[DiagnosticIssue]:
|
| 303 |
+
"""بررسی فایل سیستم"""
|
| 304 |
+
issues = []
|
| 305 |
+
|
| 306 |
+
# بررسی دایرکتوریهای مهم
|
| 307 |
+
important_dirs = ['static', 'static/css', 'static/js', 'backend', 'backend/services']
|
| 308 |
+
for dir_path in important_dirs:
|
| 309 |
+
if not os.path.exists(dir_path):
|
| 310 |
+
issues.append(DiagnosticIssue(
|
| 311 |
+
severity='warning',
|
| 312 |
+
category='filesystem',
|
| 313 |
+
title=f'دایرکتوری {dir_path} وجود ندارد',
|
| 314 |
+
description=f'دایرکتوری {dir_path} یافت نشد.',
|
| 315 |
+
fixable=True,
|
| 316 |
+
fix_action=f'mkdir -p {dir_path}'
|
| 317 |
+
))
|
| 318 |
+
|
| 319 |
+
# بررسی فایلهای مهم
|
| 320 |
+
important_files = [
|
| 321 |
+
'api_server_extended.py',
|
| 322 |
+
'unified_dashboard.html',
|
| 323 |
+
'static/js/websocket-client.js',
|
| 324 |
+
'static/css/connection-status.css',
|
| 325 |
+
]
|
| 326 |
+
for file_path in important_files:
|
| 327 |
+
if not os.path.exists(file_path):
|
| 328 |
+
issues.append(DiagnosticIssue(
|
| 329 |
+
severity='critical' if 'api_server' in file_path else 'warning',
|
| 330 |
+
category='filesystem',
|
| 331 |
+
title=f'فایل {file_path} وجود ندارد',
|
| 332 |
+
description=f'فایل {file_path} یافت نشد.',
|
| 333 |
+
fixable=False
|
| 334 |
+
))
|
| 335 |
+
|
| 336 |
+
return issues
|
| 337 |
+
|
| 338 |
+
async def _apply_fix(self, issue: DiagnosticIssue) -> bool:
|
| 339 |
+
"""اعمال تعمیر خودکار"""
|
| 340 |
+
if not issue.fixable or not issue.fix_action:
|
| 341 |
+
return False
|
| 342 |
+
|
| 343 |
+
try:
|
| 344 |
+
if issue.fix_action.startswith('pip install'):
|
| 345 |
+
# نصب بسته
|
| 346 |
+
package = issue.fix_action.replace('pip install', '').strip()
|
| 347 |
+
result = subprocess.run(
|
| 348 |
+
[sys.executable, '-m', 'pip', 'install', package],
|
| 349 |
+
capture_output=True,
|
| 350 |
+
text=True,
|
| 351 |
+
timeout=60
|
| 352 |
+
)
|
| 353 |
+
if result.returncode == 0:
|
| 354 |
+
logger.info(f'✅ بسته {package} با موفقیت نصب شد')
|
| 355 |
+
return True
|
| 356 |
+
else:
|
| 357 |
+
logger.error(f'❌ خطا در نصب {package}: {result.stderr}')
|
| 358 |
+
return False
|
| 359 |
+
|
| 360 |
+
elif issue.fix_action.startswith('mkdir'):
|
| 361 |
+
# ساخت دایرکتوری
|
| 362 |
+
dir_path = issue.fix_action.replace('mkdir -p', '').strip()
|
| 363 |
+
os.makedirs(dir_path, exist_ok=True)
|
| 364 |
+
logger.info(f'✅ دایرکتوری {dir_path} ساخته شد')
|
| 365 |
+
return True
|
| 366 |
+
|
| 367 |
+
else:
|
| 368 |
+
logger.warning(f'⚠️ عمل تعمیر ناشناخته: {issue.fix_action}')
|
| 369 |
+
return False
|
| 370 |
+
|
| 371 |
+
except Exception as e:
|
| 372 |
+
logger.error(f'❌ خطا در اعمال تعمیر: {e}')
|
| 373 |
+
return False
|
| 374 |
+
|
| 375 |
+
async def _get_system_info(self) -> Dict[str, Any]:
|
| 376 |
+
"""دریافت اطلاعات سیستم"""
|
| 377 |
+
import platform
|
| 378 |
+
return {
|
| 379 |
+
'python_version': sys.version,
|
| 380 |
+
'platform': platform.platform(),
|
| 381 |
+
'architecture': platform.architecture(),
|
| 382 |
+
'processor': platform.processor(),
|
| 383 |
+
'cwd': os.getcwd(),
|
| 384 |
+
}
|
| 385 |
+
|
| 386 |
+
def get_last_report(self) -> Optional[Dict[str, Any]]:
|
| 387 |
+
"""دریافت آخرین گزارش"""
|
| 388 |
+
if self.last_report:
|
| 389 |
+
return asdict(self.last_report)
|
| 390 |
+
return None
|
| 391 |
+
|
docker-compose.yml
CHANGED
|
@@ -1,92 +1,97 @@
|
|
| 1 |
version: '3.8'
|
| 2 |
|
| 3 |
services:
|
|
|
|
| 4 |
crypto-monitor:
|
| 5 |
-
build:
|
| 6 |
-
|
| 7 |
-
dockerfile: Dockerfile
|
| 8 |
-
container_name: crypto-api-monitor
|
| 9 |
-
image: crypto-api-monitor:latest
|
| 10 |
-
|
| 11 |
-
# Port mapping (HuggingFace Spaces standard port)
|
| 12 |
ports:
|
| 13 |
-
- "
|
| 14 |
-
|
| 15 |
-
# Environment variables
|
| 16 |
environment:
|
| 17 |
-
-
|
| 18 |
-
-
|
| 19 |
-
-
|
| 20 |
-
- HF_HTTP_TIMEOUT=8.0
|
| 21 |
-
# Add your HuggingFace token here or via .env file
|
| 22 |
-
# - HUGGINGFACE_TOKEN=your_token_here
|
| 23 |
-
|
| 24 |
-
# Sentiment models (optional customization)
|
| 25 |
-
- SENTIMENT_SOCIAL_MODEL=ElKulako/cryptobert
|
| 26 |
-
- SENTIMENT_NEWS_MODEL=kk08/CryptoBERT
|
| 27 |
-
|
| 28 |
-
# Optional: Load environment variables from .env file
|
| 29 |
-
env_file:
|
| 30 |
-
- .env
|
| 31 |
-
|
| 32 |
-
# Volume mounts for data persistence
|
| 33 |
volumes:
|
| 34 |
-
# Persist SQLite database
|
| 35 |
-
- ./data:/app/data
|
| 36 |
-
|
| 37 |
-
# Persist logs
|
| 38 |
- ./logs:/app/logs
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
# Optional: Mount frontend files for live updates
|
| 45 |
-
# - ./index.html:/app/index.html
|
| 46 |
-
# - ./hf_console.html:/app/hf_console.html
|
| 47 |
-
# - ./config.js:/app/config.js
|
| 48 |
-
|
| 49 |
-
# Health check configuration
|
| 50 |
healthcheck:
|
| 51 |
-
test: ["CMD", "
|
| 52 |
interval: 30s
|
| 53 |
timeout: 10s
|
| 54 |
-
start_period: 40s
|
| 55 |
retries: 3
|
|
|
|
| 56 |
|
| 57 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
restart: unless-stopped
|
|
|
|
|
|
|
|
|
|
| 59 |
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
|
| 70 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
networks:
|
| 72 |
- crypto-network
|
| 73 |
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
|
| 81 |
-
# Network definition
|
| 82 |
networks:
|
| 83 |
crypto-network:
|
| 84 |
driver: bridge
|
| 85 |
-
name: crypto-monitor-network
|
| 86 |
|
| 87 |
-
# Volume definitions (optional - for named volumes instead of bind mounts)
|
| 88 |
volumes:
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
|
|
|
| 1 |
version: '3.8'
|
| 2 |
|
| 3 |
services:
|
| 4 |
+
# سرور اصلی Crypto Monitor
|
| 5 |
crypto-monitor:
|
| 6 |
+
build: .
|
| 7 |
+
container_name: crypto-monitor-app
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
ports:
|
| 9 |
+
- "8000:8000"
|
|
|
|
|
|
|
| 10 |
environment:
|
| 11 |
+
- HOST=0.0.0.0
|
| 12 |
+
- PORT=8000
|
| 13 |
+
- LOG_LEVEL=INFO
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
volumes:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
- ./logs:/app/logs
|
| 16 |
+
- ./data:/app/data
|
| 17 |
+
restart: unless-stopped
|
| 18 |
+
networks:
|
| 19 |
+
- crypto-network
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
healthcheck:
|
| 21 |
+
test: ["CMD", "python", "-c", "import requests; requests.get('http://localhost:8000/health')"]
|
| 22 |
interval: 30s
|
| 23 |
timeout: 10s
|
|
|
|
| 24 |
retries: 3
|
| 25 |
+
start_period: 10s
|
| 26 |
|
| 27 |
+
# Redis برای Cache (اختیاری)
|
| 28 |
+
redis:
|
| 29 |
+
image: redis:7-alpine
|
| 30 |
+
container_name: crypto-monitor-redis
|
| 31 |
+
ports:
|
| 32 |
+
- "6379:6379"
|
| 33 |
+
volumes:
|
| 34 |
+
- redis-data:/data
|
| 35 |
restart: unless-stopped
|
| 36 |
+
networks:
|
| 37 |
+
- crypto-network
|
| 38 |
+
command: redis-server --appendonly yes
|
| 39 |
|
| 40 |
+
# PostgreSQL برای ذخیره دادهها (اختیاری)
|
| 41 |
+
postgres:
|
| 42 |
+
image: postgres:15-alpine
|
| 43 |
+
container_name: crypto-monitor-db
|
| 44 |
+
environment:
|
| 45 |
+
POSTGRES_DB: crypto_monitor
|
| 46 |
+
POSTGRES_USER: crypto_user
|
| 47 |
+
POSTGRES_PASSWORD: crypto_pass_change_me
|
| 48 |
+
ports:
|
| 49 |
+
- "5432:5432"
|
| 50 |
+
volumes:
|
| 51 |
+
- postgres-data:/var/lib/postgresql/data
|
| 52 |
+
restart: unless-stopped
|
| 53 |
+
networks:
|
| 54 |
+
- crypto-network
|
| 55 |
|
| 56 |
+
# Prometheus برای مانیتورینگ (اختیاری)
|
| 57 |
+
prometheus:
|
| 58 |
+
image: prom/prometheus:latest
|
| 59 |
+
container_name: crypto-monitor-prometheus
|
| 60 |
+
ports:
|
| 61 |
+
- "9090:9090"
|
| 62 |
+
volumes:
|
| 63 |
+
- ./prometheus.yml:/etc/prometheus/prometheus.yml
|
| 64 |
+
- prometheus-data:/prometheus
|
| 65 |
+
command:
|
| 66 |
+
- '--config.file=/etc/prometheus/prometheus.yml'
|
| 67 |
+
- '--storage.tsdb.path=/prometheus'
|
| 68 |
+
restart: unless-stopped
|
| 69 |
networks:
|
| 70 |
- crypto-network
|
| 71 |
|
| 72 |
+
# Grafana برای نمایش دادهها (اختیاری)
|
| 73 |
+
grafana:
|
| 74 |
+
image: grafana/grafana:latest
|
| 75 |
+
container_name: crypto-monitor-grafana
|
| 76 |
+
ports:
|
| 77 |
+
- "3000:3000"
|
| 78 |
+
environment:
|
| 79 |
+
- GF_SECURITY_ADMIN_PASSWORD=admin_change_me
|
| 80 |
+
- GF_USERS_ALLOW_SIGN_UP=false
|
| 81 |
+
volumes:
|
| 82 |
+
- grafana-data:/var/lib/grafana
|
| 83 |
+
restart: unless-stopped
|
| 84 |
+
networks:
|
| 85 |
+
- crypto-network
|
| 86 |
+
depends_on:
|
| 87 |
+
- prometheus
|
| 88 |
|
|
|
|
| 89 |
networks:
|
| 90 |
crypto-network:
|
| 91 |
driver: bridge
|
|
|
|
| 92 |
|
|
|
|
| 93 |
volumes:
|
| 94 |
+
redis-data:
|
| 95 |
+
postgres-data:
|
| 96 |
+
prometheus-data:
|
| 97 |
+
grafana-data:
|
import_resources.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Import Resources Script - وارد کردن خودکار منابع از فایلهای JSON موجود
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import json
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
from resource_manager import ResourceManager
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def import_all_resources():
|
| 12 |
+
"""وارد کردن همه منابع از فایلهای JSON موجود"""
|
| 13 |
+
print("🚀 شروع وارد کردن منابع...\n")
|
| 14 |
+
|
| 15 |
+
manager = ResourceManager()
|
| 16 |
+
|
| 17 |
+
# لیست فایلهای JSON برای import
|
| 18 |
+
json_files = [
|
| 19 |
+
"api-resources/crypto_resources_unified_2025-11-11.json",
|
| 20 |
+
"api-resources/ultimate_crypto_pipeline_2025_NZasinich.json",
|
| 21 |
+
"providers_config_extended.json",
|
| 22 |
+
"providers_config_ultimate.json"
|
| 23 |
+
]
|
| 24 |
+
|
| 25 |
+
imported_count = 0
|
| 26 |
+
|
| 27 |
+
for json_file in json_files:
|
| 28 |
+
file_path = Path(json_file)
|
| 29 |
+
if file_path.exists():
|
| 30 |
+
print(f"📂 در حال پردازش: {json_file}")
|
| 31 |
+
try:
|
| 32 |
+
success = manager.import_from_json(str(file_path), merge=True)
|
| 33 |
+
if success:
|
| 34 |
+
imported_count += 1
|
| 35 |
+
print(f" ✅ موفق\n")
|
| 36 |
+
else:
|
| 37 |
+
print(f" ⚠️ خطا در import\n")
|
| 38 |
+
except Exception as e:
|
| 39 |
+
print(f" ❌ خطا: {e}\n")
|
| 40 |
+
else:
|
| 41 |
+
print(f" ⚠️ فایل یافت نشد: {json_file}\n")
|
| 42 |
+
|
| 43 |
+
# ذخیره منابع
|
| 44 |
+
if imported_count > 0:
|
| 45 |
+
manager.save_resources()
|
| 46 |
+
print(f"✅ {imported_count} فایل با موفقیت import شدند")
|
| 47 |
+
|
| 48 |
+
# نمایش آمار
|
| 49 |
+
stats = manager.get_statistics()
|
| 50 |
+
print("\n📊 آمار نهایی:")
|
| 51 |
+
print(f" کل منابع: {stats['total_providers']}")
|
| 52 |
+
print(f" رایگان: {stats['by_free']['free']}")
|
| 53 |
+
print(f" پولی: {stats['by_free']['paid']}")
|
| 54 |
+
print(f" نیاز به Auth: {stats['by_auth']['requires_auth']}")
|
| 55 |
+
|
| 56 |
+
print("\n📦 دستهبندی:")
|
| 57 |
+
for category, count in sorted(stats['by_category'].items()):
|
| 58 |
+
print(f" • {category}: {count}")
|
| 59 |
+
|
| 60 |
+
print("\n✅ اتمام")
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
if __name__ == "__main__":
|
| 64 |
+
import_all_resources()
|
| 65 |
+
|
index.html
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
log_manager.py
ADDED
|
@@ -0,0 +1,387 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Log Management System - مدیریت کامل لاگها با قابلیت Export/Import/Filter
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import json
|
| 7 |
+
import csv
|
| 8 |
+
from datetime import datetime
|
| 9 |
+
from typing import List, Dict, Any, Optional
|
| 10 |
+
from dataclasses import dataclass, asdict
|
| 11 |
+
from enum import Enum
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
import gzip
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class LogLevel(Enum):
|
| 17 |
+
"""سطوح لاگ"""
|
| 18 |
+
DEBUG = "debug"
|
| 19 |
+
INFO = "info"
|
| 20 |
+
WARNING = "warning"
|
| 21 |
+
ERROR = "error"
|
| 22 |
+
CRITICAL = "critical"
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class LogCategory(Enum):
|
| 26 |
+
"""دستهبندی لاگها"""
|
| 27 |
+
PROVIDER = "provider"
|
| 28 |
+
POOL = "pool"
|
| 29 |
+
API = "api"
|
| 30 |
+
SYSTEM = "system"
|
| 31 |
+
HEALTH_CHECK = "health_check"
|
| 32 |
+
ROTATION = "rotation"
|
| 33 |
+
REQUEST = "request"
|
| 34 |
+
ERROR = "error"
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
@dataclass
|
| 38 |
+
class LogEntry:
|
| 39 |
+
"""ورودی لاگ"""
|
| 40 |
+
timestamp: str
|
| 41 |
+
level: str
|
| 42 |
+
category: str
|
| 43 |
+
message: str
|
| 44 |
+
provider_id: Optional[str] = None
|
| 45 |
+
pool_id: Optional[str] = None
|
| 46 |
+
status_code: Optional[int] = None
|
| 47 |
+
response_time: Optional[float] = None
|
| 48 |
+
error: Optional[str] = None
|
| 49 |
+
extra_data: Optional[Dict[str, Any]] = None
|
| 50 |
+
|
| 51 |
+
def to_dict(self) -> Dict[str, Any]:
|
| 52 |
+
"""تبدیل به dictionary"""
|
| 53 |
+
return {k: v for k, v in asdict(self).items() if v is not None}
|
| 54 |
+
|
| 55 |
+
@staticmethod
|
| 56 |
+
def from_dict(data: Dict[str, Any]) -> 'LogEntry':
|
| 57 |
+
"""ساخت از dictionary"""
|
| 58 |
+
return LogEntry(**data)
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
class LogManager:
|
| 62 |
+
"""مدیریت لاگها"""
|
| 63 |
+
|
| 64 |
+
def __init__(self, log_file: str = "logs/app.log", max_size_mb: int = 50):
|
| 65 |
+
self.log_file = Path(log_file)
|
| 66 |
+
self.max_size_bytes = max_size_mb * 1024 * 1024
|
| 67 |
+
self.logs: List[LogEntry] = []
|
| 68 |
+
|
| 69 |
+
# ساخت دایرکتوری logs
|
| 70 |
+
self.log_file.parent.mkdir(parents=True, exist_ok=True)
|
| 71 |
+
|
| 72 |
+
# بارگذاری لاگهای موجود
|
| 73 |
+
self.load_logs()
|
| 74 |
+
|
| 75 |
+
def add_log(
|
| 76 |
+
self,
|
| 77 |
+
level: LogLevel,
|
| 78 |
+
category: LogCategory,
|
| 79 |
+
message: str,
|
| 80 |
+
provider_id: Optional[str] = None,
|
| 81 |
+
pool_id: Optional[str] = None,
|
| 82 |
+
status_code: Optional[int] = None,
|
| 83 |
+
response_time: Optional[float] = None,
|
| 84 |
+
error: Optional[str] = None,
|
| 85 |
+
extra_data: Optional[Dict[str, Any]] = None
|
| 86 |
+
):
|
| 87 |
+
"""افزودن لاگ جدید"""
|
| 88 |
+
log_entry = LogEntry(
|
| 89 |
+
timestamp=datetime.now().isoformat(),
|
| 90 |
+
level=level.value,
|
| 91 |
+
category=category.value,
|
| 92 |
+
message=message,
|
| 93 |
+
provider_id=provider_id,
|
| 94 |
+
pool_id=pool_id,
|
| 95 |
+
status_code=status_code,
|
| 96 |
+
response_time=response_time,
|
| 97 |
+
error=error,
|
| 98 |
+
extra_data=extra_data
|
| 99 |
+
)
|
| 100 |
+
|
| 101 |
+
self.logs.append(log_entry)
|
| 102 |
+
self._write_to_file(log_entry)
|
| 103 |
+
|
| 104 |
+
# بررسی حجم و rotation
|
| 105 |
+
self._check_rotation()
|
| 106 |
+
|
| 107 |
+
def _write_to_file(self, log_entry: LogEntry):
|
| 108 |
+
"""نوشتن لاگ در فایل"""
|
| 109 |
+
with open(self.log_file, 'a', encoding='utf-8') as f:
|
| 110 |
+
f.write(json.dumps(log_entry.to_dict(), ensure_ascii=False) + '\n')
|
| 111 |
+
|
| 112 |
+
def _check_rotation(self):
|
| 113 |
+
"""بررسی و rotation لاگها"""
|
| 114 |
+
if self.log_file.exists() and self.log_file.stat().st_size > self.max_size_bytes:
|
| 115 |
+
# فشردهسازی فایل قبلی
|
| 116 |
+
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
| 117 |
+
archive_file = self.log_file.parent / f"{self.log_file.stem}_{timestamp}.log.gz"
|
| 118 |
+
|
| 119 |
+
with open(self.log_file, 'rb') as f_in:
|
| 120 |
+
with gzip.open(archive_file, 'wb') as f_out:
|
| 121 |
+
f_out.writelines(f_in)
|
| 122 |
+
|
| 123 |
+
# پاک کردن فایل فعلی
|
| 124 |
+
self.log_file.unlink()
|
| 125 |
+
|
| 126 |
+
print(f"✅ Log rotated to: {archive_file}")
|
| 127 |
+
|
| 128 |
+
def load_logs(self, limit: Optional[int] = None):
|
| 129 |
+
"""بارگذاری لاگها از فایل"""
|
| 130 |
+
if not self.log_file.exists():
|
| 131 |
+
return
|
| 132 |
+
|
| 133 |
+
self.logs.clear()
|
| 134 |
+
|
| 135 |
+
try:
|
| 136 |
+
with open(self.log_file, 'r', encoding='utf-8') as f:
|
| 137 |
+
for line in f:
|
| 138 |
+
if line.strip():
|
| 139 |
+
try:
|
| 140 |
+
data = json.loads(line)
|
| 141 |
+
self.logs.append(LogEntry.from_dict(data))
|
| 142 |
+
except json.JSONDecodeError:
|
| 143 |
+
continue
|
| 144 |
+
|
| 145 |
+
# محدود کردن به تعداد مشخص
|
| 146 |
+
if limit:
|
| 147 |
+
self.logs = self.logs[-limit:]
|
| 148 |
+
|
| 149 |
+
print(f"✅ Loaded {len(self.logs)} logs")
|
| 150 |
+
except Exception as e:
|
| 151 |
+
print(f"❌ Error loading logs: {e}")
|
| 152 |
+
|
| 153 |
+
def filter_logs(
|
| 154 |
+
self,
|
| 155 |
+
level: Optional[LogLevel] = None,
|
| 156 |
+
category: Optional[LogCategory] = None,
|
| 157 |
+
provider_id: Optional[str] = None,
|
| 158 |
+
pool_id: Optional[str] = None,
|
| 159 |
+
start_time: Optional[datetime] = None,
|
| 160 |
+
end_time: Optional[datetime] = None,
|
| 161 |
+
search_text: Optional[str] = None
|
| 162 |
+
) -> List[LogEntry]:
|
| 163 |
+
"""فیلتر لاگها"""
|
| 164 |
+
filtered = self.logs.copy()
|
| 165 |
+
|
| 166 |
+
if level:
|
| 167 |
+
filtered = [log for log in filtered if log.level == level.value]
|
| 168 |
+
|
| 169 |
+
if category:
|
| 170 |
+
filtered = [log for log in filtered if log.category == category.value]
|
| 171 |
+
|
| 172 |
+
if provider_id:
|
| 173 |
+
filtered = [log for log in filtered if log.provider_id == provider_id]
|
| 174 |
+
|
| 175 |
+
if pool_id:
|
| 176 |
+
filtered = [log for log in filtered if log.pool_id == pool_id]
|
| 177 |
+
|
| 178 |
+
if start_time:
|
| 179 |
+
filtered = [log for log in filtered if datetime.fromisoformat(log.timestamp) >= start_time]
|
| 180 |
+
|
| 181 |
+
if end_time:
|
| 182 |
+
filtered = [log for log in filtered if datetime.fromisoformat(log.timestamp) <= end_time]
|
| 183 |
+
|
| 184 |
+
if search_text:
|
| 185 |
+
filtered = [log for log in filtered if search_text.lower() in log.message.lower()]
|
| 186 |
+
|
| 187 |
+
return filtered
|
| 188 |
+
|
| 189 |
+
def get_recent_logs(self, limit: int = 100) -> List[LogEntry]:
|
| 190 |
+
"""دریافت آخرین لاگها"""
|
| 191 |
+
return self.logs[-limit:]
|
| 192 |
+
|
| 193 |
+
def get_error_logs(self, limit: Optional[int] = None) -> List[LogEntry]:
|
| 194 |
+
"""دریافت لاگهای خطا"""
|
| 195 |
+
errors = [log for log in self.logs if log.level in ['error', 'critical']]
|
| 196 |
+
if limit:
|
| 197 |
+
return errors[-limit:]
|
| 198 |
+
return errors
|
| 199 |
+
|
| 200 |
+
def export_to_json(self, filepath: str, filtered: Optional[List[LogEntry]] = None):
|
| 201 |
+
"""صادرکردن لاگها به JSON"""
|
| 202 |
+
logs_to_export = filtered if filtered else self.logs
|
| 203 |
+
|
| 204 |
+
data = {
|
| 205 |
+
"exported_at": datetime.now().isoformat(),
|
| 206 |
+
"total_logs": len(logs_to_export),
|
| 207 |
+
"logs": [log.to_dict() for log in logs_to_export]
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
with open(filepath, 'w', encoding='utf-8') as f:
|
| 211 |
+
json.dump(data, f, indent=2, ensure_ascii=False)
|
| 212 |
+
|
| 213 |
+
print(f"✅ Exported {len(logs_to_export)} logs to {filepath}")
|
| 214 |
+
|
| 215 |
+
def export_to_csv(self, filepath: str, filtered: Optional[List[LogEntry]] = None):
|
| 216 |
+
"""صادرکردن لاگها به CSV"""
|
| 217 |
+
logs_to_export = filtered if filtered else self.logs
|
| 218 |
+
|
| 219 |
+
if not logs_to_export:
|
| 220 |
+
print("⚠️ No logs to export")
|
| 221 |
+
return
|
| 222 |
+
|
| 223 |
+
# فیلدهای CSV
|
| 224 |
+
fieldnames = ['timestamp', 'level', 'category', 'message', 'provider_id',
|
| 225 |
+
'pool_id', 'status_code', 'response_time', 'error']
|
| 226 |
+
|
| 227 |
+
with open(filepath, 'w', newline='', encoding='utf-8') as f:
|
| 228 |
+
writer = csv.DictWriter(f, fieldnames=fieldnames)
|
| 229 |
+
writer.writeheader()
|
| 230 |
+
|
| 231 |
+
for log in logs_to_export:
|
| 232 |
+
row = {k: v for k, v in log.to_dict().items() if k in fieldnames}
|
| 233 |
+
writer.writerow(row)
|
| 234 |
+
|
| 235 |
+
print(f"✅ Exported {len(logs_to_export)} logs to {filepath}")
|
| 236 |
+
|
| 237 |
+
def import_from_json(self, filepath: str):
|
| 238 |
+
"""وارد کردن لاگها از JSON"""
|
| 239 |
+
try:
|
| 240 |
+
with open(filepath, 'r', encoding='utf-8') as f:
|
| 241 |
+
data = json.load(f)
|
| 242 |
+
|
| 243 |
+
logs_data = data.get('logs', [])
|
| 244 |
+
|
| 245 |
+
for log_data in logs_data:
|
| 246 |
+
log_entry = LogEntry.from_dict(log_data)
|
| 247 |
+
self.logs.append(log_entry)
|
| 248 |
+
self._write_to_file(log_entry)
|
| 249 |
+
|
| 250 |
+
print(f"✅ Imported {len(logs_data)} logs from {filepath}")
|
| 251 |
+
except Exception as e:
|
| 252 |
+
print(f"❌ Error importing logs: {e}")
|
| 253 |
+
|
| 254 |
+
def clear_logs(self):
|
| 255 |
+
"""پاک کردن همه لاگها"""
|
| 256 |
+
self.logs.clear()
|
| 257 |
+
if self.log_file.exists():
|
| 258 |
+
self.log_file.unlink()
|
| 259 |
+
print("✅ All logs cleared")
|
| 260 |
+
|
| 261 |
+
def get_statistics(self) -> Dict[str, Any]:
|
| 262 |
+
"""آمار لاگها"""
|
| 263 |
+
if not self.logs:
|
| 264 |
+
return {"total": 0}
|
| 265 |
+
|
| 266 |
+
stats = {
|
| 267 |
+
"total": len(self.logs),
|
| 268 |
+
"by_level": {},
|
| 269 |
+
"by_category": {},
|
| 270 |
+
"by_provider": {},
|
| 271 |
+
"by_pool": {},
|
| 272 |
+
"errors": len([log for log in self.logs if log.level in ['error', 'critical']]),
|
| 273 |
+
"date_range": {
|
| 274 |
+
"start": self.logs[0].timestamp if self.logs else None,
|
| 275 |
+
"end": self.logs[-1].timestamp if self.logs else None
|
| 276 |
+
}
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
# آمار بر اساس سطح
|
| 280 |
+
for log in self.logs:
|
| 281 |
+
stats["by_level"][log.level] = stats["by_level"].get(log.level, 0) + 1
|
| 282 |
+
stats["by_category"][log.category] = stats["by_category"].get(log.category, 0) + 1
|
| 283 |
+
|
| 284 |
+
if log.provider_id:
|
| 285 |
+
stats["by_provider"][log.provider_id] = stats["by_provider"].get(log.provider_id, 0) + 1
|
| 286 |
+
|
| 287 |
+
if log.pool_id:
|
| 288 |
+
stats["by_pool"][log.pool_id] = stats["by_pool"].get(log.pool_id, 0) + 1
|
| 289 |
+
|
| 290 |
+
return stats
|
| 291 |
+
|
| 292 |
+
def search_logs(self, query: str, limit: int = 100) -> List[LogEntry]:
|
| 293 |
+
"""جستجوی لاگها"""
|
| 294 |
+
results = []
|
| 295 |
+
query_lower = query.lower()
|
| 296 |
+
|
| 297 |
+
for log in reversed(self.logs):
|
| 298 |
+
if (query_lower in log.message.lower() or
|
| 299 |
+
(log.provider_id and query_lower in log.provider_id.lower()) or
|
| 300 |
+
(log.error and query_lower in log.error.lower())):
|
| 301 |
+
results.append(log)
|
| 302 |
+
|
| 303 |
+
if len(results) >= limit:
|
| 304 |
+
break
|
| 305 |
+
|
| 306 |
+
return results
|
| 307 |
+
|
| 308 |
+
def get_provider_logs(self, provider_id: str, limit: Optional[int] = None) -> List[LogEntry]:
|
| 309 |
+
"""لاگهای یک provider"""
|
| 310 |
+
provider_logs = [log for log in self.logs if log.provider_id == provider_id]
|
| 311 |
+
if limit:
|
| 312 |
+
return provider_logs[-limit:]
|
| 313 |
+
return provider_logs
|
| 314 |
+
|
| 315 |
+
def get_pool_logs(self, pool_id: str, limit: Optional[int] = None) -> List[LogEntry]:
|
| 316 |
+
"""لاگهای یک pool"""
|
| 317 |
+
pool_logs = [log for log in self.logs if log.pool_id == pool_id]
|
| 318 |
+
if limit:
|
| 319 |
+
return pool_logs[-limit:]
|
| 320 |
+
return pool_logs
|
| 321 |
+
|
| 322 |
+
|
| 323 |
+
# Global instance
|
| 324 |
+
_log_manager = None
|
| 325 |
+
|
| 326 |
+
|
| 327 |
+
def get_log_manager() -> LogManager:
|
| 328 |
+
"""دریافت instance مدیر لاگ"""
|
| 329 |
+
global _log_manager
|
| 330 |
+
if _log_manager is None:
|
| 331 |
+
_log_manager = LogManager()
|
| 332 |
+
return _log_manager
|
| 333 |
+
|
| 334 |
+
|
| 335 |
+
# Convenience functions
|
| 336 |
+
def log_info(category: LogCategory, message: str, **kwargs):
|
| 337 |
+
"""لاگ سطح INFO"""
|
| 338 |
+
get_log_manager().add_log(LogLevel.INFO, category, message, **kwargs)
|
| 339 |
+
|
| 340 |
+
|
| 341 |
+
def log_error(category: LogCategory, message: str, **kwargs):
|
| 342 |
+
"""لاگ سطح ERROR"""
|
| 343 |
+
get_log_manager().add_log(LogLevel.ERROR, category, message, **kwargs)
|
| 344 |
+
|
| 345 |
+
|
| 346 |
+
def log_warning(category: LogCategory, message: str, **kwargs):
|
| 347 |
+
"""لاگ سطح WARNING"""
|
| 348 |
+
get_log_manager().add_log(LogLevel.WARNING, category, message, **kwargs)
|
| 349 |
+
|
| 350 |
+
|
| 351 |
+
def log_debug(category: LogCategory, message: str, **kwargs):
|
| 352 |
+
"""لاگ سطح DEBUG"""
|
| 353 |
+
get_log_manager().add_log(LogLevel.DEBUG, category, message, **kwargs)
|
| 354 |
+
|
| 355 |
+
|
| 356 |
+
def log_critical(category: LogCategory, message: str, **kwargs):
|
| 357 |
+
"""لاگ سطح CRITICAL"""
|
| 358 |
+
get_log_manager().add_log(LogLevel.CRITICAL, category, message, **kwargs)
|
| 359 |
+
|
| 360 |
+
|
| 361 |
+
# تست
|
| 362 |
+
if __name__ == "__main__":
|
| 363 |
+
print("🧪 Testing Log Manager...\n")
|
| 364 |
+
|
| 365 |
+
manager = LogManager()
|
| 366 |
+
|
| 367 |
+
# تست افزودن لاگ
|
| 368 |
+
log_info(LogCategory.SYSTEM, "System started")
|
| 369 |
+
log_info(LogCategory.PROVIDER, "Provider health check", provider_id="coingecko", response_time=234.5)
|
| 370 |
+
log_error(LogCategory.PROVIDER, "Provider failed", provider_id="etherscan", error="Timeout")
|
| 371 |
+
log_warning(LogCategory.POOL, "Pool rotation", pool_id="market_pool")
|
| 372 |
+
|
| 373 |
+
# آمار
|
| 374 |
+
stats = manager.get_statistics()
|
| 375 |
+
print("📊 Statistics:")
|
| 376 |
+
print(json.dumps(stats, indent=2))
|
| 377 |
+
|
| 378 |
+
# فیلتر
|
| 379 |
+
errors = manager.get_error_logs()
|
| 380 |
+
print(f"\n❌ Error logs: {len(errors)}")
|
| 381 |
+
|
| 382 |
+
# Export
|
| 383 |
+
manager.export_to_json("test_logs.json")
|
| 384 |
+
manager.export_to_csv("test_logs.csv")
|
| 385 |
+
|
| 386 |
+
print("\n✅ Log Manager test completed")
|
| 387 |
+
|
provider_manager.py
ADDED
|
@@ -0,0 +1,466 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Provider Manager - مدیریت ارائهدهندگان API و استراتژیهای Rotation
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import json
|
| 7 |
+
import asyncio
|
| 8 |
+
import aiohttp
|
| 9 |
+
import time
|
| 10 |
+
from typing import Dict, List, Optional, Any
|
| 11 |
+
from dataclasses import dataclass, field
|
| 12 |
+
from datetime import datetime
|
| 13 |
+
from enum import Enum
|
| 14 |
+
import random
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class ProviderStatus(Enum):
|
| 18 |
+
"""وضعیت ارائهدهنده"""
|
| 19 |
+
ONLINE = "online"
|
| 20 |
+
OFFLINE = "offline"
|
| 21 |
+
DEGRADED = "degraded"
|
| 22 |
+
RATE_LIMITED = "rate_limited"
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class RotationStrategy(Enum):
|
| 26 |
+
"""استراتژیهای چرخش"""
|
| 27 |
+
ROUND_ROBIN = "round_robin"
|
| 28 |
+
PRIORITY = "priority"
|
| 29 |
+
WEIGHTED = "weighted"
|
| 30 |
+
LEAST_USED = "least_used"
|
| 31 |
+
FASTEST_RESPONSE = "fastest_response"
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
@dataclass
|
| 35 |
+
class RateLimitInfo:
|
| 36 |
+
"""اطلاعات محدودیت نرخ"""
|
| 37 |
+
requests_per_second: Optional[int] = None
|
| 38 |
+
requests_per_minute: Optional[int] = None
|
| 39 |
+
requests_per_hour: Optional[int] = None
|
| 40 |
+
requests_per_day: Optional[int] = None
|
| 41 |
+
current_usage: int = 0
|
| 42 |
+
reset_time: Optional[float] = None
|
| 43 |
+
|
| 44 |
+
def is_limited(self) -> bool:
|
| 45 |
+
"""بررسی محدودیت نرخ"""
|
| 46 |
+
now = time.time()
|
| 47 |
+
if self.reset_time and now < self.reset_time:
|
| 48 |
+
if self.requests_per_second and self.current_usage >= self.requests_per_second:
|
| 49 |
+
return True
|
| 50 |
+
if self.requests_per_minute and self.current_usage >= self.requests_per_minute:
|
| 51 |
+
return True
|
| 52 |
+
if self.requests_per_hour and self.current_usage >= self.requests_per_hour:
|
| 53 |
+
return True
|
| 54 |
+
if self.requests_per_day and self.current_usage >= self.requests_per_day:
|
| 55 |
+
return True
|
| 56 |
+
return False
|
| 57 |
+
|
| 58 |
+
def increment(self):
|
| 59 |
+
"""افزایش شمارنده استفاده"""
|
| 60 |
+
self.current_usage += 1
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
@dataclass
|
| 64 |
+
class Provider:
|
| 65 |
+
"""کلاس ارائهدهنده API"""
|
| 66 |
+
provider_id: str
|
| 67 |
+
name: str
|
| 68 |
+
category: str
|
| 69 |
+
base_url: str
|
| 70 |
+
endpoints: Dict[str, str]
|
| 71 |
+
rate_limit: RateLimitInfo
|
| 72 |
+
requires_auth: bool = False
|
| 73 |
+
priority: int = 5
|
| 74 |
+
weight: int = 50
|
| 75 |
+
status: ProviderStatus = ProviderStatus.ONLINE
|
| 76 |
+
|
| 77 |
+
# آمار
|
| 78 |
+
total_requests: int = 0
|
| 79 |
+
successful_requests: int = 0
|
| 80 |
+
failed_requests: int = 0
|
| 81 |
+
avg_response_time: float = 0.0
|
| 82 |
+
last_check: Optional[datetime] = None
|
| 83 |
+
last_error: Optional[str] = None
|
| 84 |
+
|
| 85 |
+
# Circuit Breaker
|
| 86 |
+
consecutive_failures: int = 0
|
| 87 |
+
circuit_breaker_open: bool = False
|
| 88 |
+
circuit_breaker_open_until: Optional[float] = None
|
| 89 |
+
|
| 90 |
+
def __post_init__(self):
|
| 91 |
+
"""مقداردهی اولیه"""
|
| 92 |
+
if isinstance(self.rate_limit, dict):
|
| 93 |
+
self.rate_limit = RateLimitInfo(**self.rate_limit)
|
| 94 |
+
|
| 95 |
+
@property
|
| 96 |
+
def success_rate(self) -> float:
|
| 97 |
+
"""نرخ موفقیت"""
|
| 98 |
+
if self.total_requests == 0:
|
| 99 |
+
return 100.0
|
| 100 |
+
return (self.successful_requests / self.total_requests) * 100
|
| 101 |
+
|
| 102 |
+
@property
|
| 103 |
+
def is_available(self) -> bool:
|
| 104 |
+
"""آیا ارائهدهنده در دسترس است؟"""
|
| 105 |
+
# بررسی Circuit Breaker
|
| 106 |
+
if self.circuit_breaker_open:
|
| 107 |
+
if self.circuit_breaker_open_until and time.time() > self.circuit_breaker_open_until:
|
| 108 |
+
self.circuit_breaker_open = False
|
| 109 |
+
self.consecutive_failures = 0
|
| 110 |
+
else:
|
| 111 |
+
return False
|
| 112 |
+
|
| 113 |
+
# بررسی محدودیت نرخ
|
| 114 |
+
if self.rate_limit and self.rate_limit.is_limited():
|
| 115 |
+
self.status = ProviderStatus.RATE_LIMITED
|
| 116 |
+
return False
|
| 117 |
+
|
| 118 |
+
# بررسی وضعیت
|
| 119 |
+
return self.status in [ProviderStatus.ONLINE, ProviderStatus.DEGRADED]
|
| 120 |
+
|
| 121 |
+
def record_success(self, response_time: float):
|
| 122 |
+
"""ثبت درخواست موفق"""
|
| 123 |
+
self.total_requests += 1
|
| 124 |
+
self.successful_requests += 1
|
| 125 |
+
self.consecutive_failures = 0
|
| 126 |
+
|
| 127 |
+
# محاسبه میانگین متحرک زمان پاسخ
|
| 128 |
+
if self.avg_response_time == 0:
|
| 129 |
+
self.avg_response_time = response_time
|
| 130 |
+
else:
|
| 131 |
+
self.avg_response_time = (self.avg_response_time * 0.8) + (response_time * 0.2)
|
| 132 |
+
|
| 133 |
+
self.status = ProviderStatus.ONLINE
|
| 134 |
+
self.last_check = datetime.now()
|
| 135 |
+
|
| 136 |
+
if self.rate_limit:
|
| 137 |
+
self.rate_limit.increment()
|
| 138 |
+
|
| 139 |
+
def record_failure(self, error: str, circuit_breaker_threshold: int = 5):
|
| 140 |
+
"""ثبت درخواست ناموفق"""
|
| 141 |
+
self.total_requests += 1
|
| 142 |
+
self.failed_requests += 1
|
| 143 |
+
self.consecutive_failures += 1
|
| 144 |
+
self.last_error = error
|
| 145 |
+
self.last_check = datetime.now()
|
| 146 |
+
|
| 147 |
+
# فعالسازی Circuit Breaker
|
| 148 |
+
if self.consecutive_failures >= circuit_breaker_threshold:
|
| 149 |
+
self.circuit_breaker_open = True
|
| 150 |
+
self.circuit_breaker_open_until = time.time() + 60 # ۶۰ ثانیه
|
| 151 |
+
self.status = ProviderStatus.OFFLINE
|
| 152 |
+
else:
|
| 153 |
+
self.status = ProviderStatus.DEGRADED
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
@dataclass
|
| 157 |
+
class ProviderPool:
|
| 158 |
+
"""استخر ارائهدهندگان با استراتژی چرخش"""
|
| 159 |
+
pool_id: str
|
| 160 |
+
pool_name: str
|
| 161 |
+
category: str
|
| 162 |
+
rotation_strategy: RotationStrategy
|
| 163 |
+
providers: List[Provider] = field(default_factory=list)
|
| 164 |
+
current_index: int = 0
|
| 165 |
+
enabled: bool = True
|
| 166 |
+
total_rotations: int = 0
|
| 167 |
+
|
| 168 |
+
def add_provider(self, provider: Provider):
|
| 169 |
+
"""افزودن ارائهدهنده به استخر"""
|
| 170 |
+
if provider not in self.providers:
|
| 171 |
+
self.providers.append(provider)
|
| 172 |
+
# مرتبسازی بر اساس اولویت
|
| 173 |
+
if self.rotation_strategy == RotationStrategy.PRIORITY:
|
| 174 |
+
self.providers.sort(key=lambda p: p.priority, reverse=True)
|
| 175 |
+
|
| 176 |
+
def remove_provider(self, provider_id: str):
|
| 177 |
+
"""حذف ارائهدهنده از استخر"""
|
| 178 |
+
self.providers = [p for p in self.providers if p.provider_id != provider_id]
|
| 179 |
+
|
| 180 |
+
def get_next_provider(self) -> Optional[Provider]:
|
| 181 |
+
"""دریافت ارائهدهنده بعدی بر اساس استراتژی"""
|
| 182 |
+
if not self.providers or not self.enabled:
|
| 183 |
+
return None
|
| 184 |
+
|
| 185 |
+
# فیلتر ارائهدهندگان در دسترس
|
| 186 |
+
available = [p for p in self.providers if p.is_available]
|
| 187 |
+
if not available:
|
| 188 |
+
return None
|
| 189 |
+
|
| 190 |
+
provider = None
|
| 191 |
+
|
| 192 |
+
if self.rotation_strategy == RotationStrategy.ROUND_ROBIN:
|
| 193 |
+
provider = self._round_robin(available)
|
| 194 |
+
elif self.rotation_strategy == RotationStrategy.PRIORITY:
|
| 195 |
+
provider = self._priority_based(available)
|
| 196 |
+
elif self.rotation_strategy == RotationStrategy.WEIGHTED:
|
| 197 |
+
provider = self._weighted_random(available)
|
| 198 |
+
elif self.rotation_strategy == RotationStrategy.LEAST_USED:
|
| 199 |
+
provider = self._least_used(available)
|
| 200 |
+
elif self.rotation_strategy == RotationStrategy.FASTEST_RESPONSE:
|
| 201 |
+
provider = self._fastest_response(available)
|
| 202 |
+
|
| 203 |
+
if provider:
|
| 204 |
+
self.total_rotations += 1
|
| 205 |
+
|
| 206 |
+
return provider
|
| 207 |
+
|
| 208 |
+
def _round_robin(self, available: List[Provider]) -> Provider:
|
| 209 |
+
"""چرخش Round Robin"""
|
| 210 |
+
provider = available[self.current_index % len(available)]
|
| 211 |
+
self.current_index += 1
|
| 212 |
+
return provider
|
| 213 |
+
|
| 214 |
+
def _priority_based(self, available: List[Provider]) -> Provider:
|
| 215 |
+
"""بر اساس اولویت"""
|
| 216 |
+
return max(available, key=lambda p: p.priority)
|
| 217 |
+
|
| 218 |
+
def _weighted_random(self, available: List[Provider]) -> Provider:
|
| 219 |
+
"""انتخاب تصادفی وزندار"""
|
| 220 |
+
weights = [p.weight for p in available]
|
| 221 |
+
return random.choices(available, weights=weights, k=1)[0]
|
| 222 |
+
|
| 223 |
+
def _least_used(self, available: List[Provider]) -> Provider:
|
| 224 |
+
"""کمترین استفاده شده"""
|
| 225 |
+
return min(available, key=lambda p: p.total_requests)
|
| 226 |
+
|
| 227 |
+
def _fastest_response(self, available: List[Provider]) -> Provider:
|
| 228 |
+
"""سریعترین پاسخ"""
|
| 229 |
+
return min(available, key=lambda p: p.avg_response_time if p.avg_response_time > 0 else float('inf'))
|
| 230 |
+
|
| 231 |
+
def get_stats(self) -> Dict[str, Any]:
|
| 232 |
+
"""آمار استخر"""
|
| 233 |
+
total_providers = len(self.providers)
|
| 234 |
+
available_providers = len([p for p in self.providers if p.is_available])
|
| 235 |
+
|
| 236 |
+
return {
|
| 237 |
+
"pool_id": self.pool_id,
|
| 238 |
+
"pool_name": self.pool_name,
|
| 239 |
+
"category": self.category,
|
| 240 |
+
"rotation_strategy": self.rotation_strategy.value,
|
| 241 |
+
"total_providers": total_providers,
|
| 242 |
+
"available_providers": available_providers,
|
| 243 |
+
"total_rotations": self.total_rotations,
|
| 244 |
+
"enabled": self.enabled,
|
| 245 |
+
"providers": [
|
| 246 |
+
{
|
| 247 |
+
"provider_id": p.provider_id,
|
| 248 |
+
"name": p.name,
|
| 249 |
+
"status": p.status.value,
|
| 250 |
+
"success_rate": p.success_rate,
|
| 251 |
+
"total_requests": p.total_requests,
|
| 252 |
+
"avg_response_time": p.avg_response_time,
|
| 253 |
+
"is_available": p.is_available
|
| 254 |
+
}
|
| 255 |
+
for p in self.providers
|
| 256 |
+
]
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
+
|
| 260 |
+
class ProviderManager:
|
| 261 |
+
"""مدیر ارائهدهندگان"""
|
| 262 |
+
|
| 263 |
+
def __init__(self, config_path: str = "providers_config_extended.json"):
|
| 264 |
+
self.config_path = config_path
|
| 265 |
+
self.providers: Dict[str, Provider] = {}
|
| 266 |
+
self.pools: Dict[str, ProviderPool] = {}
|
| 267 |
+
self.session: Optional[aiohttp.ClientSession] = None
|
| 268 |
+
|
| 269 |
+
self.load_config()
|
| 270 |
+
|
| 271 |
+
def load_config(self):
|
| 272 |
+
"""بارگذاری پیکربندی از فایل JSON"""
|
| 273 |
+
try:
|
| 274 |
+
with open(self.config_path, 'r', encoding='utf-8') as f:
|
| 275 |
+
config = json.load(f)
|
| 276 |
+
|
| 277 |
+
# بارگذاری ارائهدهندگان
|
| 278 |
+
for provider_id, provider_data in config.get('providers', {}).items():
|
| 279 |
+
rate_limit_data = provider_data.get('rate_limit', {})
|
| 280 |
+
rate_limit = RateLimitInfo(**rate_limit_data)
|
| 281 |
+
|
| 282 |
+
provider = Provider(
|
| 283 |
+
provider_id=provider_id,
|
| 284 |
+
name=provider_data['name'],
|
| 285 |
+
category=provider_data['category'],
|
| 286 |
+
base_url=provider_data['base_url'],
|
| 287 |
+
endpoints=provider_data.get('endpoints', {}),
|
| 288 |
+
rate_limit=rate_limit,
|
| 289 |
+
requires_auth=provider_data.get('requires_auth', False),
|
| 290 |
+
priority=provider_data.get('priority', 5),
|
| 291 |
+
weight=provider_data.get('weight', 50)
|
| 292 |
+
)
|
| 293 |
+
self.providers[provider_id] = provider
|
| 294 |
+
|
| 295 |
+
# بارگذاری Poolها
|
| 296 |
+
for pool_config in config.get('pool_configurations', []):
|
| 297 |
+
pool_id = pool_config['pool_name'].lower().replace(' ', '_')
|
| 298 |
+
pool = ProviderPool(
|
| 299 |
+
pool_id=pool_id,
|
| 300 |
+
pool_name=pool_config['pool_name'],
|
| 301 |
+
category=pool_config['category'],
|
| 302 |
+
rotation_strategy=RotationStrategy(pool_config['rotation_strategy'])
|
| 303 |
+
)
|
| 304 |
+
|
| 305 |
+
# افزودن ارائهدهندگان به Pool
|
| 306 |
+
for provider_id in pool_config.get('providers', []):
|
| 307 |
+
if provider_id in self.providers:
|
| 308 |
+
pool.add_provider(self.providers[provider_id])
|
| 309 |
+
|
| 310 |
+
self.pools[pool_id] = pool
|
| 311 |
+
|
| 312 |
+
print(f"✅ بارگذاری موفق: {len(self.providers)} ارائهدهنده، {len(self.pools)} استخر")
|
| 313 |
+
|
| 314 |
+
except FileNotFoundError:
|
| 315 |
+
print(f"❌ خطا: فایل {self.config_path} یافت نشد")
|
| 316 |
+
except Exception as e:
|
| 317 |
+
print(f"❌ خطا در بارگذاری پیکربندی: {e}")
|
| 318 |
+
|
| 319 |
+
async def init_session(self):
|
| 320 |
+
"""مقداردهی اولیه HTTP Session"""
|
| 321 |
+
if not self.session:
|
| 322 |
+
timeout = aiohttp.ClientTimeout(total=10)
|
| 323 |
+
self.session = aiohttp.ClientSession(timeout=timeout)
|
| 324 |
+
|
| 325 |
+
async def close_session(self):
|
| 326 |
+
"""بستن HTTP Session"""
|
| 327 |
+
if self.session:
|
| 328 |
+
await self.session.close()
|
| 329 |
+
self.session = None
|
| 330 |
+
|
| 331 |
+
async def health_check(self, provider: Provider) -> bool:
|
| 332 |
+
"""بررسی سلامت ارائهدهنده"""
|
| 333 |
+
await self.init_session()
|
| 334 |
+
|
| 335 |
+
# انتخاب اولین endpoint برای تست
|
| 336 |
+
if not provider.endpoints:
|
| 337 |
+
return False
|
| 338 |
+
|
| 339 |
+
endpoint = list(provider.endpoints.values())[0]
|
| 340 |
+
url = f"{provider.base_url}{endpoint}"
|
| 341 |
+
|
| 342 |
+
start_time = time.time()
|
| 343 |
+
|
| 344 |
+
try:
|
| 345 |
+
async with self.session.get(url) as response:
|
| 346 |
+
response_time = (time.time() - start_time) * 1000 # میلیثانیه
|
| 347 |
+
|
| 348 |
+
if response.status == 200:
|
| 349 |
+
provider.record_success(response_time)
|
| 350 |
+
return True
|
| 351 |
+
else:
|
| 352 |
+
provider.record_failure(f"HTTP {response.status}")
|
| 353 |
+
return False
|
| 354 |
+
|
| 355 |
+
except asyncio.TimeoutError:
|
| 356 |
+
provider.record_failure("Timeout")
|
| 357 |
+
return False
|
| 358 |
+
except Exception as e:
|
| 359 |
+
provider.record_failure(str(e))
|
| 360 |
+
return False
|
| 361 |
+
|
| 362 |
+
async def health_check_all(self):
|
| 363 |
+
"""بررسی سلامت همه ارائهدهندگان"""
|
| 364 |
+
tasks = [self.health_check(provider) for provider in self.providers.values()]
|
| 365 |
+
results = await asyncio.gather(*tasks, return_exceptions=True)
|
| 366 |
+
|
| 367 |
+
online = sum(1 for r in results if r is True)
|
| 368 |
+
print(f"✅ بررسی سلامت: {online}/{len(self.providers)} ارائهدهنده آنلاین")
|
| 369 |
+
|
| 370 |
+
def get_provider(self, provider_id: str) -> Optional[Provider]:
|
| 371 |
+
"""دریافت ارائهدهنده با ID"""
|
| 372 |
+
return self.providers.get(provider_id)
|
| 373 |
+
|
| 374 |
+
def get_pool(self, pool_id: str) -> Optional[ProviderPool]:
|
| 375 |
+
"""دریافت Pool با ID"""
|
| 376 |
+
return self.pools.get(pool_id)
|
| 377 |
+
|
| 378 |
+
def get_next_from_pool(self, pool_id: str) -> Optional[Provider]:
|
| 379 |
+
"""دریافت ارائهدهنده بعدی از Pool"""
|
| 380 |
+
pool = self.get_pool(pool_id)
|
| 381 |
+
if pool:
|
| 382 |
+
return pool.get_next_provider()
|
| 383 |
+
return None
|
| 384 |
+
|
| 385 |
+
def get_all_stats(self) -> Dict[str, Any]:
|
| 386 |
+
"""آمار کامل سیستم"""
|
| 387 |
+
total_providers = len(self.providers)
|
| 388 |
+
online_providers = len([p for p in self.providers.values() if p.status == ProviderStatus.ONLINE])
|
| 389 |
+
offline_providers = len([p for p in self.providers.values() if p.status == ProviderStatus.OFFLINE])
|
| 390 |
+
degraded_providers = len([p for p in self.providers.values() if p.status == ProviderStatus.DEGRADED])
|
| 391 |
+
|
| 392 |
+
total_requests = sum(p.total_requests for p in self.providers.values())
|
| 393 |
+
successful_requests = sum(p.successful_requests for p in self.providers.values())
|
| 394 |
+
|
| 395 |
+
return {
|
| 396 |
+
"summary": {
|
| 397 |
+
"total_providers": total_providers,
|
| 398 |
+
"online": online_providers,
|
| 399 |
+
"offline": offline_providers,
|
| 400 |
+
"degraded": degraded_providers,
|
| 401 |
+
"total_requests": total_requests,
|
| 402 |
+
"successful_requests": successful_requests,
|
| 403 |
+
"overall_success_rate": (successful_requests / total_requests * 100) if total_requests > 0 else 0
|
| 404 |
+
},
|
| 405 |
+
"providers": {
|
| 406 |
+
provider_id: {
|
| 407 |
+
"name": p.name,
|
| 408 |
+
"category": p.category,
|
| 409 |
+
"status": p.status.value,
|
| 410 |
+
"success_rate": p.success_rate,
|
| 411 |
+
"total_requests": p.total_requests,
|
| 412 |
+
"avg_response_time": p.avg_response_time,
|
| 413 |
+
"is_available": p.is_available,
|
| 414 |
+
"priority": p.priority,
|
| 415 |
+
"weight": p.weight
|
| 416 |
+
}
|
| 417 |
+
for provider_id, p in self.providers.items()
|
| 418 |
+
},
|
| 419 |
+
"pools": {
|
| 420 |
+
pool_id: pool.get_stats()
|
| 421 |
+
for pool_id, pool in self.pools.items()
|
| 422 |
+
}
|
| 423 |
+
}
|
| 424 |
+
|
| 425 |
+
def export_stats(self, filepath: str = "provider_stats.json"):
|
| 426 |
+
"""صادرکردن آمار به فایل JSON"""
|
| 427 |
+
stats = self.get_all_stats()
|
| 428 |
+
with open(filepath, 'w', encoding='utf-8') as f:
|
| 429 |
+
json.dump(stats, f, indent=2, ensure_ascii=False)
|
| 430 |
+
print(f"✅ آمار در {filepath} ذخیره شد")
|
| 431 |
+
|
| 432 |
+
|
| 433 |
+
# تست و نمونه استفاده
|
| 434 |
+
async def main():
|
| 435 |
+
"""تابع اصلی برای تست"""
|
| 436 |
+
manager = ProviderManager()
|
| 437 |
+
|
| 438 |
+
print("\n📊 بررسی سلامت ارائهدهندگان...")
|
| 439 |
+
await manager.health_check_all()
|
| 440 |
+
|
| 441 |
+
print("\n🔄 تست Pool چرخشی...")
|
| 442 |
+
pool = manager.get_pool("primary_market_data_pool")
|
| 443 |
+
if pool:
|
| 444 |
+
for i in range(5):
|
| 445 |
+
provider = pool.get_next_provider()
|
| 446 |
+
if provider:
|
| 447 |
+
print(f" Round {i+1}: {provider.name}")
|
| 448 |
+
|
| 449 |
+
print("\n📈 آمار کلی:")
|
| 450 |
+
stats = manager.get_all_stats()
|
| 451 |
+
summary = stats['summary']
|
| 452 |
+
print(f" کل: {summary['total_providers']}")
|
| 453 |
+
print(f" آنلاین: {summary['online']}")
|
| 454 |
+
print(f" آفلاین: {summary['offline']}")
|
| 455 |
+
print(f" نرخ موفقیت: {summary['overall_success_rate']:.2f}%")
|
| 456 |
+
|
| 457 |
+
# صادرکردن آمار
|
| 458 |
+
manager.export_stats()
|
| 459 |
+
|
| 460 |
+
await manager.close_session()
|
| 461 |
+
print("\n✅ اتمام")
|
| 462 |
+
|
| 463 |
+
|
| 464 |
+
if __name__ == "__main__":
|
| 465 |
+
asyncio.run(main())
|
| 466 |
+
|
providers_config_extended.json
ADDED
|
@@ -0,0 +1,1079 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"providers": {
|
| 3 |
+
"coingecko": {
|
| 4 |
+
"name": "CoinGecko",
|
| 5 |
+
"category": "market_data",
|
| 6 |
+
"base_url": "https://api.coingecko.com/api/v3",
|
| 7 |
+
"endpoints": {
|
| 8 |
+
"coins_list": "/coins/list",
|
| 9 |
+
"coins_markets": "/coins/markets?vs_currency=usd&order=market_cap_desc&per_page=100",
|
| 10 |
+
"global": "/global",
|
| 11 |
+
"trending": "/search/trending",
|
| 12 |
+
"simple_price": "/simple/price?ids=bitcoin,ethereum&vs_currencies=usd"
|
| 13 |
+
},
|
| 14 |
+
"rate_limit": {
|
| 15 |
+
"requests_per_minute": 50,
|
| 16 |
+
"requests_per_day": 10000
|
| 17 |
+
},
|
| 18 |
+
"requires_auth": false,
|
| 19 |
+
"priority": 10,
|
| 20 |
+
"weight": 100
|
| 21 |
+
},
|
| 22 |
+
"coinpaprika": {
|
| 23 |
+
"name": "CoinPaprika",
|
| 24 |
+
"category": "market_data",
|
| 25 |
+
"base_url": "https://api.coinpaprika.com/v1",
|
| 26 |
+
"endpoints": {
|
| 27 |
+
"tickers": "/tickers",
|
| 28 |
+
"global": "/global",
|
| 29 |
+
"coins": "/coins"
|
| 30 |
+
},
|
| 31 |
+
"rate_limit": {
|
| 32 |
+
"requests_per_minute": 25,
|
| 33 |
+
"requests_per_day": 20000
|
| 34 |
+
},
|
| 35 |
+
"requires_auth": false,
|
| 36 |
+
"priority": 9,
|
| 37 |
+
"weight": 90
|
| 38 |
+
},
|
| 39 |
+
"coincap": {
|
| 40 |
+
"name": "CoinCap",
|
| 41 |
+
"category": "market_data",
|
| 42 |
+
"base_url": "https://api.coincap.io/v2",
|
| 43 |
+
"endpoints": {
|
| 44 |
+
"assets": "/assets",
|
| 45 |
+
"rates": "/rates",
|
| 46 |
+
"markets": "/markets"
|
| 47 |
+
},
|
| 48 |
+
"rate_limit": {
|
| 49 |
+
"requests_per_minute": 200,
|
| 50 |
+
"requests_per_day": 500000
|
| 51 |
+
},
|
| 52 |
+
"requires_auth": false,
|
| 53 |
+
"priority": 9,
|
| 54 |
+
"weight": 95
|
| 55 |
+
},
|
| 56 |
+
"cryptocompare": {
|
| 57 |
+
"name": "CryptoCompare",
|
| 58 |
+
"category": "market_data",
|
| 59 |
+
"base_url": "https://min-api.cryptocompare.com/data",
|
| 60 |
+
"endpoints": {
|
| 61 |
+
"price": "/price?fsym=BTC&tsyms=USD",
|
| 62 |
+
"pricemulti": "/pricemulti?fsyms=BTC,ETH,BNB&tsyms=USD",
|
| 63 |
+
"top_list": "/top/mktcapfull?limit=100&tsym=USD"
|
| 64 |
+
},
|
| 65 |
+
"rate_limit": {
|
| 66 |
+
"requests_per_minute": 100,
|
| 67 |
+
"requests_per_hour": 100000
|
| 68 |
+
},
|
| 69 |
+
"requires_auth": false,
|
| 70 |
+
"priority": 8,
|
| 71 |
+
"weight": 80
|
| 72 |
+
},
|
| 73 |
+
"nomics": {
|
| 74 |
+
"name": "Nomics",
|
| 75 |
+
"category": "market_data",
|
| 76 |
+
"base_url": "https://api.nomics.com/v1",
|
| 77 |
+
"endpoints": {
|
| 78 |
+
"currencies": "/currencies/ticker?ids=BTC,ETH&convert=USD",
|
| 79 |
+
"global": "/global-ticker?convert=USD",
|
| 80 |
+
"markets": "/markets"
|
| 81 |
+
},
|
| 82 |
+
"rate_limit": {
|
| 83 |
+
"requests_per_day": 1000
|
| 84 |
+
},
|
| 85 |
+
"requires_auth": false,
|
| 86 |
+
"priority": 7,
|
| 87 |
+
"weight": 70,
|
| 88 |
+
"note": "May require API key for full access"
|
| 89 |
+
},
|
| 90 |
+
"messari": {
|
| 91 |
+
"name": "Messari",
|
| 92 |
+
"category": "market_data",
|
| 93 |
+
"base_url": "https://data.messari.io/api/v1",
|
| 94 |
+
"endpoints": {
|
| 95 |
+
"assets": "/assets",
|
| 96 |
+
"asset_metrics": "/assets/{asset}/metrics",
|
| 97 |
+
"market_data": "/assets/{asset}/metrics/market-data"
|
| 98 |
+
},
|
| 99 |
+
"rate_limit": {
|
| 100 |
+
"requests_per_minute": 20,
|
| 101 |
+
"requests_per_day": 1000
|
| 102 |
+
},
|
| 103 |
+
"requires_auth": false,
|
| 104 |
+
"priority": 8,
|
| 105 |
+
"weight": 85
|
| 106 |
+
},
|
| 107 |
+
"livecoinwatch": {
|
| 108 |
+
"name": "LiveCoinWatch",
|
| 109 |
+
"category": "market_data",
|
| 110 |
+
"base_url": "https://api.livecoinwatch.com",
|
| 111 |
+
"endpoints": {
|
| 112 |
+
"coins": "/coins/list",
|
| 113 |
+
"single": "/coins/single",
|
| 114 |
+
"overview": "/overview"
|
| 115 |
+
},
|
| 116 |
+
"rate_limit": {
|
| 117 |
+
"requests_per_day": 10000
|
| 118 |
+
},
|
| 119 |
+
"requires_auth": false,
|
| 120 |
+
"priority": 7,
|
| 121 |
+
"weight": 75
|
| 122 |
+
},
|
| 123 |
+
"bitquery": {
|
| 124 |
+
"name": "Bitquery",
|
| 125 |
+
"category": "blockchain_data",
|
| 126 |
+
"base_url": "https://graphql.bitquery.io",
|
| 127 |
+
"endpoints": {
|
| 128 |
+
"graphql": ""
|
| 129 |
+
},
|
| 130 |
+
"rate_limit": {
|
| 131 |
+
"requests_per_month": 50000
|
| 132 |
+
},
|
| 133 |
+
"requires_auth": false,
|
| 134 |
+
"priority": 8,
|
| 135 |
+
"weight": 80,
|
| 136 |
+
"query_type": "graphql"
|
| 137 |
+
},
|
| 138 |
+
"etherscan": {
|
| 139 |
+
"name": "Etherscan",
|
| 140 |
+
"category": "blockchain_explorers",
|
| 141 |
+
"base_url": "https://api.etherscan.io/api",
|
| 142 |
+
"endpoints": {
|
| 143 |
+
"eth_supply": "?module=stats&action=ethsupply",
|
| 144 |
+
"eth_price": "?module=stats&action=ethprice",
|
| 145 |
+
"gas_oracle": "?module=gastracker&action=gasoracle"
|
| 146 |
+
},
|
| 147 |
+
"rate_limit": {
|
| 148 |
+
"requests_per_second": 5
|
| 149 |
+
},
|
| 150 |
+
"requires_auth": false,
|
| 151 |
+
"priority": 10,
|
| 152 |
+
"weight": 100
|
| 153 |
+
},
|
| 154 |
+
"bscscan": {
|
| 155 |
+
"name": "BscScan",
|
| 156 |
+
"category": "blockchain_explorers",
|
| 157 |
+
"base_url": "https://api.bscscan.com/api",
|
| 158 |
+
"endpoints": {
|
| 159 |
+
"bnb_supply": "?module=stats&action=bnbsupply",
|
| 160 |
+
"bnb_price": "?module=stats&action=bnbprice"
|
| 161 |
+
},
|
| 162 |
+
"rate_limit": {
|
| 163 |
+
"requests_per_second": 5
|
| 164 |
+
},
|
| 165 |
+
"requires_auth": false,
|
| 166 |
+
"priority": 9,
|
| 167 |
+
"weight": 90
|
| 168 |
+
},
|
| 169 |
+
"polygonscan": {
|
| 170 |
+
"name": "PolygonScan",
|
| 171 |
+
"category": "blockchain_explorers",
|
| 172 |
+
"base_url": "https://api.polygonscan.com/api",
|
| 173 |
+
"endpoints": {
|
| 174 |
+
"matic_supply": "?module=stats&action=maticsupply",
|
| 175 |
+
"gas_oracle": "?module=gastracker&action=gasoracle"
|
| 176 |
+
},
|
| 177 |
+
"rate_limit": {
|
| 178 |
+
"requests_per_second": 5
|
| 179 |
+
},
|
| 180 |
+
"requires_auth": false,
|
| 181 |
+
"priority": 9,
|
| 182 |
+
"weight": 90
|
| 183 |
+
},
|
| 184 |
+
"arbiscan": {
|
| 185 |
+
"name": "Arbiscan",
|
| 186 |
+
"category": "blockchain_explorers",
|
| 187 |
+
"base_url": "https://api.arbiscan.io/api",
|
| 188 |
+
"endpoints": {
|
| 189 |
+
"gas_oracle": "?module=gastracker&action=gasoracle",
|
| 190 |
+
"stats": "?module=stats&action=tokensupply"
|
| 191 |
+
},
|
| 192 |
+
"rate_limit": {
|
| 193 |
+
"requests_per_second": 5
|
| 194 |
+
},
|
| 195 |
+
"requires_auth": false,
|
| 196 |
+
"priority": 8,
|
| 197 |
+
"weight": 80
|
| 198 |
+
},
|
| 199 |
+
"optimistic_etherscan": {
|
| 200 |
+
"name": "Optimistic Etherscan",
|
| 201 |
+
"category": "blockchain_explorers",
|
| 202 |
+
"base_url": "https://api-optimistic.etherscan.io/api",
|
| 203 |
+
"endpoints": {
|
| 204 |
+
"gas_oracle": "?module=gastracker&action=gasoracle"
|
| 205 |
+
},
|
| 206 |
+
"rate_limit": {
|
| 207 |
+
"requests_per_second": 5
|
| 208 |
+
},
|
| 209 |
+
"requires_auth": false,
|
| 210 |
+
"priority": 8,
|
| 211 |
+
"weight": 80
|
| 212 |
+
},
|
| 213 |
+
"blockchair": {
|
| 214 |
+
"name": "Blockchair",
|
| 215 |
+
"category": "blockchain_explorers",
|
| 216 |
+
"base_url": "https://api.blockchair.com",
|
| 217 |
+
"endpoints": {
|
| 218 |
+
"bitcoin": "/bitcoin/stats",
|
| 219 |
+
"ethereum": "/ethereum/stats",
|
| 220 |
+
"multi": "/stats"
|
| 221 |
+
},
|
| 222 |
+
"rate_limit": {
|
| 223 |
+
"requests_per_day": 1000
|
| 224 |
+
},
|
| 225 |
+
"requires_auth": false,
|
| 226 |
+
"priority": 8,
|
| 227 |
+
"weight": 85
|
| 228 |
+
},
|
| 229 |
+
"blockchain_info": {
|
| 230 |
+
"name": "Blockchain.info",
|
| 231 |
+
"category": "blockchain_explorers",
|
| 232 |
+
"base_url": "https://blockchain.info",
|
| 233 |
+
"endpoints": {
|
| 234 |
+
"stats": "/stats",
|
| 235 |
+
"pools": "/pools?timespan=5days",
|
| 236 |
+
"ticker": "/ticker"
|
| 237 |
+
},
|
| 238 |
+
"rate_limit": {
|
| 239 |
+
"requests_per_second": 1
|
| 240 |
+
},
|
| 241 |
+
"requires_auth": false,
|
| 242 |
+
"priority": 7,
|
| 243 |
+
"weight": 75
|
| 244 |
+
},
|
| 245 |
+
"blockscout_eth": {
|
| 246 |
+
"name": "Blockscout Ethereum",
|
| 247 |
+
"category": "blockchain_explorers",
|
| 248 |
+
"base_url": "https://eth.blockscout.com/api",
|
| 249 |
+
"endpoints": {
|
| 250 |
+
"stats": "?module=stats&action=tokensupply"
|
| 251 |
+
},
|
| 252 |
+
"rate_limit": {
|
| 253 |
+
"requests_per_second": 10
|
| 254 |
+
},
|
| 255 |
+
"requires_auth": false,
|
| 256 |
+
"priority": 6,
|
| 257 |
+
"weight": 60
|
| 258 |
+
},
|
| 259 |
+
"ethplorer": {
|
| 260 |
+
"name": "Ethplorer",
|
| 261 |
+
"category": "blockchain_explorers",
|
| 262 |
+
"base_url": "https://api.ethplorer.io",
|
| 263 |
+
"endpoints": {
|
| 264 |
+
"get_top": "/getTop",
|
| 265 |
+
"get_token_info": "/getTokenInfo/{address}"
|
| 266 |
+
},
|
| 267 |
+
"rate_limit": {
|
| 268 |
+
"requests_per_second": 2
|
| 269 |
+
},
|
| 270 |
+
"requires_auth": false,
|
| 271 |
+
"priority": 7,
|
| 272 |
+
"weight": 75
|
| 273 |
+
},
|
| 274 |
+
"covalent": {
|
| 275 |
+
"name": "Covalent",
|
| 276 |
+
"category": "blockchain_data",
|
| 277 |
+
"base_url": "https://api.covalenthq.com/v1",
|
| 278 |
+
"endpoints": {
|
| 279 |
+
"chains": "/chains/",
|
| 280 |
+
"token_balances": "/{chain_id}/address/{address}/balances_v2/"
|
| 281 |
+
},
|
| 282 |
+
"rate_limit": {
|
| 283 |
+
"requests_per_day": 100
|
| 284 |
+
},
|
| 285 |
+
"requires_auth": true,
|
| 286 |
+
"priority": 7,
|
| 287 |
+
"weight": 70,
|
| 288 |
+
"note": "Requires API key"
|
| 289 |
+
},
|
| 290 |
+
"moralis": {
|
| 291 |
+
"name": "Moralis",
|
| 292 |
+
"category": "blockchain_data",
|
| 293 |
+
"base_url": "https://deep-index.moralis.io/api/v2",
|
| 294 |
+
"endpoints": {
|
| 295 |
+
"token_price": "/erc20/{address}/price",
|
| 296 |
+
"nft_metadata": "/nft/{address}/{token_id}"
|
| 297 |
+
},
|
| 298 |
+
"rate_limit": {
|
| 299 |
+
"requests_per_second": 25
|
| 300 |
+
},
|
| 301 |
+
"requires_auth": true,
|
| 302 |
+
"priority": 8,
|
| 303 |
+
"weight": 80,
|
| 304 |
+
"note": "Requires API key"
|
| 305 |
+
},
|
| 306 |
+
"alchemy": {
|
| 307 |
+
"name": "Alchemy",
|
| 308 |
+
"category": "blockchain_data",
|
| 309 |
+
"base_url": "https://eth-mainnet.g.alchemy.com/v2",
|
| 310 |
+
"endpoints": {
|
| 311 |
+
"nft_metadata": "/getNFTMetadata",
|
| 312 |
+
"token_balances": "/getTokenBalances"
|
| 313 |
+
},
|
| 314 |
+
"rate_limit": {
|
| 315 |
+
"requests_per_second": 25
|
| 316 |
+
},
|
| 317 |
+
"requires_auth": true,
|
| 318 |
+
"priority": 9,
|
| 319 |
+
"weight": 90,
|
| 320 |
+
"note": "Requires API key"
|
| 321 |
+
},
|
| 322 |
+
"infura": {
|
| 323 |
+
"name": "Infura",
|
| 324 |
+
"category": "blockchain_data",
|
| 325 |
+
"base_url": "https://mainnet.infura.io/v3",
|
| 326 |
+
"endpoints": {
|
| 327 |
+
"eth_call": ""
|
| 328 |
+
},
|
| 329 |
+
"rate_limit": {
|
| 330 |
+
"requests_per_day": 100000
|
| 331 |
+
},
|
| 332 |
+
"requires_auth": true,
|
| 333 |
+
"priority": 9,
|
| 334 |
+
"weight": 90,
|
| 335 |
+
"note": "Requires API key"
|
| 336 |
+
},
|
| 337 |
+
"quicknode": {
|
| 338 |
+
"name": "QuickNode",
|
| 339 |
+
"category": "blockchain_data",
|
| 340 |
+
"base_url": "https://endpoints.omniatech.io/v1/eth/mainnet",
|
| 341 |
+
"endpoints": {
|
| 342 |
+
"rpc": ""
|
| 343 |
+
},
|
| 344 |
+
"rate_limit": {
|
| 345 |
+
"requests_per_second": 25
|
| 346 |
+
},
|
| 347 |
+
"requires_auth": false,
|
| 348 |
+
"priority": 8,
|
| 349 |
+
"weight": 80
|
| 350 |
+
},
|
| 351 |
+
"defillama": {
|
| 352 |
+
"name": "DefiLlama",
|
| 353 |
+
"category": "defi",
|
| 354 |
+
"base_url": "https://api.llama.fi",
|
| 355 |
+
"endpoints": {
|
| 356 |
+
"protocols": "/protocols",
|
| 357 |
+
"tvl": "/tvl/{protocol}",
|
| 358 |
+
"chains": "/chains",
|
| 359 |
+
"historical": "/historical/{protocol}"
|
| 360 |
+
},
|
| 361 |
+
"rate_limit": {
|
| 362 |
+
"requests_per_second": 5
|
| 363 |
+
},
|
| 364 |
+
"requires_auth": false,
|
| 365 |
+
"priority": 10,
|
| 366 |
+
"weight": 100
|
| 367 |
+
},
|
| 368 |
+
"debank": {
|
| 369 |
+
"name": "DeBank",
|
| 370 |
+
"category": "defi",
|
| 371 |
+
"base_url": "https://openapi.debank.com/v1",
|
| 372 |
+
"endpoints": {
|
| 373 |
+
"user": "/user",
|
| 374 |
+
"token_list": "/token/list",
|
| 375 |
+
"protocol_list": "/protocol/list"
|
| 376 |
+
},
|
| 377 |
+
"rate_limit": {
|
| 378 |
+
"requests_per_second": 1
|
| 379 |
+
},
|
| 380 |
+
"requires_auth": false,
|
| 381 |
+
"priority": 8,
|
| 382 |
+
"weight": 80
|
| 383 |
+
},
|
| 384 |
+
"zerion": {
|
| 385 |
+
"name": "Zerion",
|
| 386 |
+
"category": "defi",
|
| 387 |
+
"base_url": "https://api.zerion.io/v1",
|
| 388 |
+
"endpoints": {
|
| 389 |
+
"portfolio": "/wallets/{address}/portfolio",
|
| 390 |
+
"positions": "/wallets/{address}/positions"
|
| 391 |
+
},
|
| 392 |
+
"rate_limit": {
|
| 393 |
+
"requests_per_day": 1000
|
| 394 |
+
},
|
| 395 |
+
"requires_auth": false,
|
| 396 |
+
"priority": 7,
|
| 397 |
+
"weight": 70
|
| 398 |
+
},
|
| 399 |
+
"yearn": {
|
| 400 |
+
"name": "Yearn Finance",
|
| 401 |
+
"category": "defi",
|
| 402 |
+
"base_url": "https://api.yearn.finance/v1",
|
| 403 |
+
"endpoints": {
|
| 404 |
+
"vaults": "/chains/1/vaults/all",
|
| 405 |
+
"apy": "/chains/1/vaults/apy"
|
| 406 |
+
},
|
| 407 |
+
"rate_limit": {
|
| 408 |
+
"requests_per_minute": 60
|
| 409 |
+
},
|
| 410 |
+
"requires_auth": false,
|
| 411 |
+
"priority": 7,
|
| 412 |
+
"weight": 75
|
| 413 |
+
},
|
| 414 |
+
"aave": {
|
| 415 |
+
"name": "Aave",
|
| 416 |
+
"category": "defi",
|
| 417 |
+
"base_url": "https://aave-api-v2.aave.com",
|
| 418 |
+
"endpoints": {
|
| 419 |
+
"data": "/data/liquidity/v2",
|
| 420 |
+
"rates": "/data/rates"
|
| 421 |
+
},
|
| 422 |
+
"rate_limit": {
|
| 423 |
+
"requests_per_minute": 60
|
| 424 |
+
},
|
| 425 |
+
"requires_auth": false,
|
| 426 |
+
"priority": 8,
|
| 427 |
+
"weight": 80
|
| 428 |
+
},
|
| 429 |
+
"compound": {
|
| 430 |
+
"name": "Compound",
|
| 431 |
+
"category": "defi",
|
| 432 |
+
"base_url": "https://api.compound.finance/api/v2",
|
| 433 |
+
"endpoints": {
|
| 434 |
+
"ctoken": "/ctoken",
|
| 435 |
+
"account": "/account"
|
| 436 |
+
},
|
| 437 |
+
"rate_limit": {
|
| 438 |
+
"requests_per_minute": 60
|
| 439 |
+
},
|
| 440 |
+
"requires_auth": false,
|
| 441 |
+
"priority": 8,
|
| 442 |
+
"weight": 80
|
| 443 |
+
},
|
| 444 |
+
"uniswap_v3": {
|
| 445 |
+
"name": "Uniswap V3",
|
| 446 |
+
"category": "defi",
|
| 447 |
+
"base_url": "https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3",
|
| 448 |
+
"endpoints": {
|
| 449 |
+
"graphql": ""
|
| 450 |
+
},
|
| 451 |
+
"rate_limit": {
|
| 452 |
+
"requests_per_minute": 60
|
| 453 |
+
},
|
| 454 |
+
"requires_auth": false,
|
| 455 |
+
"priority": 9,
|
| 456 |
+
"weight": 90,
|
| 457 |
+
"query_type": "graphql"
|
| 458 |
+
},
|
| 459 |
+
"pancakeswap": {
|
| 460 |
+
"name": "PancakeSwap",
|
| 461 |
+
"category": "defi",
|
| 462 |
+
"base_url": "https://api.pancakeswap.info/api/v2",
|
| 463 |
+
"endpoints": {
|
| 464 |
+
"summary": "/summary",
|
| 465 |
+
"tokens": "/tokens",
|
| 466 |
+
"pairs": "/pairs"
|
| 467 |
+
},
|
| 468 |
+
"rate_limit": {
|
| 469 |
+
"requests_per_minute": 60
|
| 470 |
+
},
|
| 471 |
+
"requires_auth": false,
|
| 472 |
+
"priority": 8,
|
| 473 |
+
"weight": 85
|
| 474 |
+
},
|
| 475 |
+
"sushiswap": {
|
| 476 |
+
"name": "SushiSwap",
|
| 477 |
+
"category": "defi",
|
| 478 |
+
"base_url": "https://api.sushi.com",
|
| 479 |
+
"endpoints": {
|
| 480 |
+
"analytics": "/analytics/tokens",
|
| 481 |
+
"pools": "/analytics/pools"
|
| 482 |
+
},
|
| 483 |
+
"rate_limit": {
|
| 484 |
+
"requests_per_minute": 60
|
| 485 |
+
},
|
| 486 |
+
"requires_auth": false,
|
| 487 |
+
"priority": 8,
|
| 488 |
+
"weight": 80
|
| 489 |
+
},
|
| 490 |
+
"curve": {
|
| 491 |
+
"name": "Curve Finance",
|
| 492 |
+
"category": "defi",
|
| 493 |
+
"base_url": "https://api.curve.fi/api",
|
| 494 |
+
"endpoints": {
|
| 495 |
+
"pools": "/getPools/ethereum/main",
|
| 496 |
+
"volume": "/getVolume/ethereum"
|
| 497 |
+
},
|
| 498 |
+
"rate_limit": {
|
| 499 |
+
"requests_per_minute": 60
|
| 500 |
+
},
|
| 501 |
+
"requires_auth": false,
|
| 502 |
+
"priority": 8,
|
| 503 |
+
"weight": 80
|
| 504 |
+
},
|
| 505 |
+
"1inch": {
|
| 506 |
+
"name": "1inch",
|
| 507 |
+
"category": "defi",
|
| 508 |
+
"base_url": "https://api.1inch.io/v5.0/1",
|
| 509 |
+
"endpoints": {
|
| 510 |
+
"tokens": "/tokens",
|
| 511 |
+
"quote": "/quote",
|
| 512 |
+
"liquidity_sources": "/liquidity-sources"
|
| 513 |
+
},
|
| 514 |
+
"rate_limit": {
|
| 515 |
+
"requests_per_second": 1
|
| 516 |
+
},
|
| 517 |
+
"requires_auth": false,
|
| 518 |
+
"priority": 8,
|
| 519 |
+
"weight": 80
|
| 520 |
+
},
|
| 521 |
+
"opensea": {
|
| 522 |
+
"name": "OpenSea",
|
| 523 |
+
"category": "nft",
|
| 524 |
+
"base_url": "https://api.opensea.io/api/v1",
|
| 525 |
+
"endpoints": {
|
| 526 |
+
"collections": "/collections",
|
| 527 |
+
"assets": "/assets",
|
| 528 |
+
"events": "/events"
|
| 529 |
+
},
|
| 530 |
+
"rate_limit": {
|
| 531 |
+
"requests_per_second": 4
|
| 532 |
+
},
|
| 533 |
+
"requires_auth": false,
|
| 534 |
+
"priority": 9,
|
| 535 |
+
"weight": 90
|
| 536 |
+
},
|
| 537 |
+
"rarible": {
|
| 538 |
+
"name": "Rarible",
|
| 539 |
+
"category": "nft",
|
| 540 |
+
"base_url": "https://api.rarible.org/v0.1",
|
| 541 |
+
"endpoints": {
|
| 542 |
+
"items": "/items",
|
| 543 |
+
"collections": "/collections"
|
| 544 |
+
},
|
| 545 |
+
"rate_limit": {
|
| 546 |
+
"requests_per_second": 5
|
| 547 |
+
},
|
| 548 |
+
"requires_auth": false,
|
| 549 |
+
"priority": 8,
|
| 550 |
+
"weight": 80
|
| 551 |
+
},
|
| 552 |
+
"nftport": {
|
| 553 |
+
"name": "NFTPort",
|
| 554 |
+
"category": "nft",
|
| 555 |
+
"base_url": "https://api.nftport.xyz/v0",
|
| 556 |
+
"endpoints": {
|
| 557 |
+
"nfts": "/nfts/{chain}/{contract}",
|
| 558 |
+
"stats": "/transactions/stats/{chain}"
|
| 559 |
+
},
|
| 560 |
+
"rate_limit": {
|
| 561 |
+
"requests_per_second": 1
|
| 562 |
+
},
|
| 563 |
+
"requires_auth": true,
|
| 564 |
+
"priority": 7,
|
| 565 |
+
"weight": 70,
|
| 566 |
+
"note": "Requires API key"
|
| 567 |
+
},
|
| 568 |
+
"reservoir": {
|
| 569 |
+
"name": "Reservoir",
|
| 570 |
+
"category": "nft",
|
| 571 |
+
"base_url": "https://api.reservoir.tools",
|
| 572 |
+
"endpoints": {
|
| 573 |
+
"collections": "/collections/v5",
|
| 574 |
+
"tokens": "/tokens/v5"
|
| 575 |
+
},
|
| 576 |
+
"rate_limit": {
|
| 577 |
+
"requests_per_second": 5
|
| 578 |
+
},
|
| 579 |
+
"requires_auth": false,
|
| 580 |
+
"priority": 8,
|
| 581 |
+
"weight": 85
|
| 582 |
+
},
|
| 583 |
+
"cryptopanic": {
|
| 584 |
+
"name": "CryptoPanic",
|
| 585 |
+
"category": "news",
|
| 586 |
+
"base_url": "https://cryptopanic.com/api/v1",
|
| 587 |
+
"endpoints": {
|
| 588 |
+
"posts": "/posts/"
|
| 589 |
+
},
|
| 590 |
+
"rate_limit": {
|
| 591 |
+
"requests_per_day": 1000
|
| 592 |
+
},
|
| 593 |
+
"requires_auth": false,
|
| 594 |
+
"priority": 8,
|
| 595 |
+
"weight": 80
|
| 596 |
+
},
|
| 597 |
+
"newsapi": {
|
| 598 |
+
"name": "NewsAPI",
|
| 599 |
+
"category": "news",
|
| 600 |
+
"base_url": "https://newsapi.org/v2",
|
| 601 |
+
"endpoints": {
|
| 602 |
+
"everything": "/everything?q=cryptocurrency",
|
| 603 |
+
"top_headlines": "/top-headlines?category=business"
|
| 604 |
+
},
|
| 605 |
+
"rate_limit": {
|
| 606 |
+
"requests_per_day": 100
|
| 607 |
+
},
|
| 608 |
+
"requires_auth": true,
|
| 609 |
+
"priority": 7,
|
| 610 |
+
"weight": 70,
|
| 611 |
+
"note": "Requires API key"
|
| 612 |
+
},
|
| 613 |
+
"coindesk_rss": {
|
| 614 |
+
"name": "CoinDesk RSS",
|
| 615 |
+
"category": "news",
|
| 616 |
+
"base_url": "https://www.coindesk.com/arc/outboundfeeds/rss",
|
| 617 |
+
"endpoints": {
|
| 618 |
+
"feed": "/?outputType=xml"
|
| 619 |
+
},
|
| 620 |
+
"rate_limit": {
|
| 621 |
+
"requests_per_minute": 10
|
| 622 |
+
},
|
| 623 |
+
"requires_auth": false,
|
| 624 |
+
"priority": 8,
|
| 625 |
+
"weight": 85
|
| 626 |
+
},
|
| 627 |
+
"cointelegraph_rss": {
|
| 628 |
+
"name": "Cointelegraph RSS",
|
| 629 |
+
"category": "news",
|
| 630 |
+
"base_url": "https://cointelegraph.com/rss",
|
| 631 |
+
"endpoints": {
|
| 632 |
+
"feed": ""
|
| 633 |
+
},
|
| 634 |
+
"rate_limit": {
|
| 635 |
+
"requests_per_minute": 10
|
| 636 |
+
},
|
| 637 |
+
"requires_auth": false,
|
| 638 |
+
"priority": 8,
|
| 639 |
+
"weight": 85
|
| 640 |
+
},
|
| 641 |
+
"bitcoinist_rss": {
|
| 642 |
+
"name": "Bitcoinist RSS",
|
| 643 |
+
"category": "news",
|
| 644 |
+
"base_url": "https://bitcoinist.com/feed",
|
| 645 |
+
"endpoints": {
|
| 646 |
+
"feed": ""
|
| 647 |
+
},
|
| 648 |
+
"rate_limit": {
|
| 649 |
+
"requests_per_minute": 10
|
| 650 |
+
},
|
| 651 |
+
"requires_auth": false,
|
| 652 |
+
"priority": 7,
|
| 653 |
+
"weight": 75
|
| 654 |
+
},
|
| 655 |
+
"reddit_crypto": {
|
| 656 |
+
"name": "Reddit Crypto",
|
| 657 |
+
"category": "social",
|
| 658 |
+
"base_url": "https://www.reddit.com/r/cryptocurrency",
|
| 659 |
+
"endpoints": {
|
| 660 |
+
"hot": "/hot.json",
|
| 661 |
+
"top": "/top.json",
|
| 662 |
+
"new": "/new.json"
|
| 663 |
+
},
|
| 664 |
+
"rate_limit": {
|
| 665 |
+
"requests_per_minute": 60
|
| 666 |
+
},
|
| 667 |
+
"requires_auth": false,
|
| 668 |
+
"priority": 7,
|
| 669 |
+
"weight": 75
|
| 670 |
+
},
|
| 671 |
+
"twitter_trends": {
|
| 672 |
+
"name": "Twitter Crypto Trends",
|
| 673 |
+
"category": "social",
|
| 674 |
+
"base_url": "https://api.twitter.com/2",
|
| 675 |
+
"endpoints": {
|
| 676 |
+
"search": "/tweets/search/recent?query=cryptocurrency"
|
| 677 |
+
},
|
| 678 |
+
"rate_limit": {
|
| 679 |
+
"requests_per_minute": 15
|
| 680 |
+
},
|
| 681 |
+
"requires_auth": true,
|
| 682 |
+
"priority": 6,
|
| 683 |
+
"weight": 60,
|
| 684 |
+
"note": "Requires API key"
|
| 685 |
+
},
|
| 686 |
+
"lunarcrush": {
|
| 687 |
+
"name": "LunarCrush",
|
| 688 |
+
"category": "social",
|
| 689 |
+
"base_url": "https://api.lunarcrush.com/v2",
|
| 690 |
+
"endpoints": {
|
| 691 |
+
"assets": "?data=assets",
|
| 692 |
+
"market": "?data=market"
|
| 693 |
+
},
|
| 694 |
+
"rate_limit": {
|
| 695 |
+
"requests_per_day": 1000
|
| 696 |
+
},
|
| 697 |
+
"requires_auth": false,
|
| 698 |
+
"priority": 7,
|
| 699 |
+
"weight": 75
|
| 700 |
+
},
|
| 701 |
+
"santiment": {
|
| 702 |
+
"name": "Santiment",
|
| 703 |
+
"category": "sentiment",
|
| 704 |
+
"base_url": "https://api.santiment.net/graphql",
|
| 705 |
+
"endpoints": {
|
| 706 |
+
"graphql": ""
|
| 707 |
+
},
|
| 708 |
+
"rate_limit": {
|
| 709 |
+
"requests_per_minute": 60
|
| 710 |
+
},
|
| 711 |
+
"requires_auth": true,
|
| 712 |
+
"priority": 8,
|
| 713 |
+
"weight": 80,
|
| 714 |
+
"query_type": "graphql",
|
| 715 |
+
"note": "Requires API key"
|
| 716 |
+
},
|
| 717 |
+
"alternative_me": {
|
| 718 |
+
"name": "Alternative.me",
|
| 719 |
+
"category": "sentiment",
|
| 720 |
+
"base_url": "https://api.alternative.me",
|
| 721 |
+
"endpoints": {
|
| 722 |
+
"fear_greed": "/fng/",
|
| 723 |
+
"historical": "/fng/?limit=10"
|
| 724 |
+
},
|
| 725 |
+
"rate_limit": {
|
| 726 |
+
"requests_per_minute": 60
|
| 727 |
+
},
|
| 728 |
+
"requires_auth": false,
|
| 729 |
+
"priority": 10,
|
| 730 |
+
"weight": 100
|
| 731 |
+
},
|
| 732 |
+
"glassnode": {
|
| 733 |
+
"name": "Glassnode",
|
| 734 |
+
"category": "analytics",
|
| 735 |
+
"base_url": "https://api.glassnode.com/v1",
|
| 736 |
+
"endpoints": {
|
| 737 |
+
"metrics": "/metrics/{metric_path}"
|
| 738 |
+
},
|
| 739 |
+
"rate_limit": {
|
| 740 |
+
"requests_per_day": 100
|
| 741 |
+
},
|
| 742 |
+
"requires_auth": true,
|
| 743 |
+
"priority": 9,
|
| 744 |
+
"weight": 90,
|
| 745 |
+
"note": "Requires API key"
|
| 746 |
+
},
|
| 747 |
+
"intotheblock": {
|
| 748 |
+
"name": "IntoTheBlock",
|
| 749 |
+
"category": "analytics",
|
| 750 |
+
"base_url": "https://api.intotheblock.com/v1",
|
| 751 |
+
"endpoints": {
|
| 752 |
+
"analytics": "/analytics"
|
| 753 |
+
},
|
| 754 |
+
"rate_limit": {
|
| 755 |
+
"requests_per_day": 500
|
| 756 |
+
},
|
| 757 |
+
"requires_auth": true,
|
| 758 |
+
"priority": 8,
|
| 759 |
+
"weight": 80,
|
| 760 |
+
"note": "Requires API key"
|
| 761 |
+
},
|
| 762 |
+
"coinmetrics": {
|
| 763 |
+
"name": "Coin Metrics",
|
| 764 |
+
"category": "analytics",
|
| 765 |
+
"base_url": "https://community-api.coinmetrics.io/v4",
|
| 766 |
+
"endpoints": {
|
| 767 |
+
"assets": "/catalog/assets",
|
| 768 |
+
"metrics": "/timeseries/asset-metrics"
|
| 769 |
+
},
|
| 770 |
+
"rate_limit": {
|
| 771 |
+
"requests_per_minute": 10
|
| 772 |
+
},
|
| 773 |
+
"requires_auth": false,
|
| 774 |
+
"priority": 8,
|
| 775 |
+
"weight": 85
|
| 776 |
+
},
|
| 777 |
+
"kaiko": {
|
| 778 |
+
"name": "Kaiko",
|
| 779 |
+
"category": "analytics",
|
| 780 |
+
"base_url": "https://us.market-api.kaiko.io/v2",
|
| 781 |
+
"endpoints": {
|
| 782 |
+
"data": "/data"
|
| 783 |
+
},
|
| 784 |
+
"rate_limit": {
|
| 785 |
+
"requests_per_second": 1
|
| 786 |
+
},
|
| 787 |
+
"requires_auth": true,
|
| 788 |
+
"priority": 7,
|
| 789 |
+
"weight": 70,
|
| 790 |
+
"note": "Requires API key"
|
| 791 |
+
},
|
| 792 |
+
"kraken": {
|
| 793 |
+
"name": "Kraken",
|
| 794 |
+
"category": "exchange",
|
| 795 |
+
"base_url": "https://api.kraken.com/0/public",
|
| 796 |
+
"endpoints": {
|
| 797 |
+
"ticker": "/Ticker",
|
| 798 |
+
"system_status": "/SystemStatus",
|
| 799 |
+
"assets": "/Assets"
|
| 800 |
+
},
|
| 801 |
+
"rate_limit": {
|
| 802 |
+
"requests_per_second": 1
|
| 803 |
+
},
|
| 804 |
+
"requires_auth": false,
|
| 805 |
+
"priority": 9,
|
| 806 |
+
"weight": 90
|
| 807 |
+
},
|
| 808 |
+
"binance": {
|
| 809 |
+
"name": "Binance",
|
| 810 |
+
"category": "exchange",
|
| 811 |
+
"base_url": "https://api.binance.com/api/v3",
|
| 812 |
+
"endpoints": {
|
| 813 |
+
"ticker_24hr": "/ticker/24hr",
|
| 814 |
+
"ticker_price": "/ticker/price",
|
| 815 |
+
"exchange_info": "/exchangeInfo"
|
| 816 |
+
},
|
| 817 |
+
"rate_limit": {
|
| 818 |
+
"requests_per_minute": 1200,
|
| 819 |
+
"weight_per_minute": 1200
|
| 820 |
+
},
|
| 821 |
+
"requires_auth": false,
|
| 822 |
+
"priority": 10,
|
| 823 |
+
"weight": 100
|
| 824 |
+
},
|
| 825 |
+
"coinbase": {
|
| 826 |
+
"name": "Coinbase",
|
| 827 |
+
"category": "exchange",
|
| 828 |
+
"base_url": "https://api.coinbase.com/v2",
|
| 829 |
+
"endpoints": {
|
| 830 |
+
"exchange_rates": "/exchange-rates",
|
| 831 |
+
"prices": "/prices/BTC-USD/spot"
|
| 832 |
+
},
|
| 833 |
+
"rate_limit": {
|
| 834 |
+
"requests_per_hour": 10000
|
| 835 |
+
},
|
| 836 |
+
"requires_auth": false,
|
| 837 |
+
"priority": 9,
|
| 838 |
+
"weight": 95
|
| 839 |
+
},
|
| 840 |
+
"bitfinex": {
|
| 841 |
+
"name": "Bitfinex",
|
| 842 |
+
"category": "exchange",
|
| 843 |
+
"base_url": "https://api-pub.bitfinex.com/v2",
|
| 844 |
+
"endpoints": {
|
| 845 |
+
"tickers": "/tickers?symbols=ALL",
|
| 846 |
+
"ticker": "/ticker/tBTCUSD"
|
| 847 |
+
},
|
| 848 |
+
"rate_limit": {
|
| 849 |
+
"requests_per_minute": 90
|
| 850 |
+
},
|
| 851 |
+
"requires_auth": false,
|
| 852 |
+
"priority": 8,
|
| 853 |
+
"weight": 85
|
| 854 |
+
},
|
| 855 |
+
"huobi": {
|
| 856 |
+
"name": "Huobi",
|
| 857 |
+
"category": "exchange",
|
| 858 |
+
"base_url": "https://api.huobi.pro",
|
| 859 |
+
"endpoints": {
|
| 860 |
+
"tickers": "/market/tickers",
|
| 861 |
+
"detail": "/market/detail"
|
| 862 |
+
},
|
| 863 |
+
"rate_limit": {
|
| 864 |
+
"requests_per_second": 10
|
| 865 |
+
},
|
| 866 |
+
"requires_auth": false,
|
| 867 |
+
"priority": 8,
|
| 868 |
+
"weight": 80
|
| 869 |
+
},
|
| 870 |
+
"kucoin": {
|
| 871 |
+
"name": "KuCoin",
|
| 872 |
+
"category": "exchange",
|
| 873 |
+
"base_url": "https://api.kucoin.com/api/v1",
|
| 874 |
+
"endpoints": {
|
| 875 |
+
"tickers": "/market/allTickers",
|
| 876 |
+
"ticker": "/market/orderbook/level1"
|
| 877 |
+
},
|
| 878 |
+
"rate_limit": {
|
| 879 |
+
"requests_per_second": 10
|
| 880 |
+
},
|
| 881 |
+
"requires_auth": false,
|
| 882 |
+
"priority": 8,
|
| 883 |
+
"weight": 80
|
| 884 |
+
},
|
| 885 |
+
"okx": {
|
| 886 |
+
"name": "OKX",
|
| 887 |
+
"category": "exchange",
|
| 888 |
+
"base_url": "https://www.okx.com/api/v5",
|
| 889 |
+
"endpoints": {
|
| 890 |
+
"tickers": "/market/tickers?instType=SPOT",
|
| 891 |
+
"ticker": "/market/ticker"
|
| 892 |
+
},
|
| 893 |
+
"rate_limit": {
|
| 894 |
+
"requests_per_second": 20
|
| 895 |
+
},
|
| 896 |
+
"requires_auth": false,
|
| 897 |
+
"priority": 8,
|
| 898 |
+
"weight": 85
|
| 899 |
+
},
|
| 900 |
+
"gate_io": {
|
| 901 |
+
"name": "Gate.io",
|
| 902 |
+
"category": "exchange",
|
| 903 |
+
"base_url": "https://api.gateio.ws/api/v4",
|
| 904 |
+
"endpoints": {
|
| 905 |
+
"tickers": "/spot/tickers",
|
| 906 |
+
"ticker": "/spot/tickers/{currency_pair}"
|
| 907 |
+
},
|
| 908 |
+
"rate_limit": {
|
| 909 |
+
"requests_per_second": 900
|
| 910 |
+
},
|
| 911 |
+
"requires_auth": false,
|
| 912 |
+
"priority": 7,
|
| 913 |
+
"weight": 75
|
| 914 |
+
},
|
| 915 |
+
"bybit": {
|
| 916 |
+
"name": "Bybit",
|
| 917 |
+
"category": "exchange",
|
| 918 |
+
"base_url": "https://api.bybit.com/v5",
|
| 919 |
+
"endpoints": {
|
| 920 |
+
"tickers": "/market/tickers?category=spot",
|
| 921 |
+
"ticker": "/market/tickers"
|
| 922 |
+
},
|
| 923 |
+
"rate_limit": {
|
| 924 |
+
"requests_per_second": 50
|
| 925 |
+
},
|
| 926 |
+
"requires_auth": false,
|
| 927 |
+
"priority": 8,
|
| 928 |
+
"weight": 80
|
| 929 |
+
},
|
| 930 |
+
"cryptorank": {
|
| 931 |
+
"name": "Cryptorank",
|
| 932 |
+
"category": "market_data",
|
| 933 |
+
"base_url": "https://api.cryptorank.io/v1",
|
| 934 |
+
"endpoints": {
|
| 935 |
+
"currencies": "/currencies",
|
| 936 |
+
"global": "/global"
|
| 937 |
+
},
|
| 938 |
+
"rate_limit": {
|
| 939 |
+
"requests_per_day": 10000
|
| 940 |
+
},
|
| 941 |
+
"requires_auth": false,
|
| 942 |
+
"priority": 7,
|
| 943 |
+
"weight": 75
|
| 944 |
+
},
|
| 945 |
+
"coinlore": {
|
| 946 |
+
"name": "CoinLore",
|
| 947 |
+
"category": "market_data",
|
| 948 |
+
"base_url": "https://api.coinlore.net/api",
|
| 949 |
+
"endpoints": {
|
| 950 |
+
"tickers": "/tickers/",
|
| 951 |
+
"global": "/global/",
|
| 952 |
+
"coin": "/ticker/"
|
| 953 |
+
},
|
| 954 |
+
"rate_limit": {
|
| 955 |
+
"requests_per_minute": 60
|
| 956 |
+
},
|
| 957 |
+
"requires_auth": false,
|
| 958 |
+
"priority": 7,
|
| 959 |
+
"weight": 75
|
| 960 |
+
},
|
| 961 |
+
"coincodex": {
|
| 962 |
+
"name": "CoinCodex",
|
| 963 |
+
"category": "market_data",
|
| 964 |
+
"base_url": "https://coincodex.com/api",
|
| 965 |
+
"endpoints": {
|
| 966 |
+
"coinlist": "/coincodex/get_coinlist/",
|
| 967 |
+
"coin": "/coincodex/get_coin/"
|
| 968 |
+
},
|
| 969 |
+
"rate_limit": {
|
| 970 |
+
"requests_per_minute": 60
|
| 971 |
+
},
|
| 972 |
+
"requires_auth": false,
|
| 973 |
+
"priority": 6,
|
| 974 |
+
"weight": 65
|
| 975 |
+
}
|
| 976 |
+
},
|
| 977 |
+
"pool_configurations": [
|
| 978 |
+
{
|
| 979 |
+
"pool_name": "Primary Market Data Pool",
|
| 980 |
+
"category": "market_data",
|
| 981 |
+
"rotation_strategy": "priority",
|
| 982 |
+
"providers": ["coingecko", "coincap", "cryptocompare", "binance", "coinbase"]
|
| 983 |
+
},
|
| 984 |
+
{
|
| 985 |
+
"pool_name": "Blockchain Explorer Pool",
|
| 986 |
+
"category": "blockchain_explorers",
|
| 987 |
+
"rotation_strategy": "round_robin",
|
| 988 |
+
"providers": ["etherscan", "bscscan", "polygonscan", "blockchair", "ethplorer"]
|
| 989 |
+
},
|
| 990 |
+
{
|
| 991 |
+
"pool_name": "DeFi Protocol Pool",
|
| 992 |
+
"category": "defi",
|
| 993 |
+
"rotation_strategy": "weighted",
|
| 994 |
+
"providers": ["defillama", "uniswap_v3", "aave", "compound", "curve", "pancakeswap"]
|
| 995 |
+
},
|
| 996 |
+
{
|
| 997 |
+
"pool_name": "NFT Market Pool",
|
| 998 |
+
"category": "nft",
|
| 999 |
+
"rotation_strategy": "priority",
|
| 1000 |
+
"providers": ["opensea", "reservoir", "rarible"]
|
| 1001 |
+
},
|
| 1002 |
+
{
|
| 1003 |
+
"pool_name": "News Aggregation Pool",
|
| 1004 |
+
"category": "news",
|
| 1005 |
+
"rotation_strategy": "round_robin",
|
| 1006 |
+
"providers": ["coindesk_rss", "cointelegraph_rss", "bitcoinist_rss", "cryptopanic"]
|
| 1007 |
+
},
|
| 1008 |
+
{
|
| 1009 |
+
"pool_name": "Sentiment Analysis Pool",
|
| 1010 |
+
"category": "sentiment",
|
| 1011 |
+
"rotation_strategy": "priority",
|
| 1012 |
+
"providers": ["alternative_me", "lunarcrush", "reddit_crypto"]
|
| 1013 |
+
},
|
| 1014 |
+
{
|
| 1015 |
+
"pool_name": "Exchange Data Pool",
|
| 1016 |
+
"category": "exchange",
|
| 1017 |
+
"rotation_strategy": "weighted",
|
| 1018 |
+
"providers": ["binance", "kraken", "coinbase", "bitfinex", "okx"]
|
| 1019 |
+
},
|
| 1020 |
+
{
|
| 1021 |
+
"pool_name": "Analytics Pool",
|
| 1022 |
+
"category": "analytics",
|
| 1023 |
+
"rotation_strategy": "priority",
|
| 1024 |
+
"providers": ["coinmetrics", "messari", "glassnode"]
|
| 1025 |
+
}
|
| 1026 |
+
],
|
| 1027 |
+
"huggingface_models": {
|
| 1028 |
+
"sentiment_analysis": [
|
| 1029 |
+
{
|
| 1030 |
+
"model_id": "cardiffnlp/twitter-roberta-base-sentiment-latest",
|
| 1031 |
+
"task": "sentiment-analysis",
|
| 1032 |
+
"description": "Twitter sentiment analysis (positive/negative/neutral)",
|
| 1033 |
+
"priority": 10
|
| 1034 |
+
},
|
| 1035 |
+
{
|
| 1036 |
+
"model_id": "ProsusAI/finbert",
|
| 1037 |
+
"task": "sentiment-analysis",
|
| 1038 |
+
"description": "Financial sentiment analysis",
|
| 1039 |
+
"priority": 9
|
| 1040 |
+
},
|
| 1041 |
+
{
|
| 1042 |
+
"model_id": "ElKulako/cryptobert",
|
| 1043 |
+
"task": "fill-mask",
|
| 1044 |
+
"description": "Cryptocurrency-specific BERT model",
|
| 1045 |
+
"priority": 8
|
| 1046 |
+
},
|
| 1047 |
+
{
|
| 1048 |
+
"model_id": "mrm8488/distilroberta-finetuned-financial-news-sentiment-analysis",
|
| 1049 |
+
"task": "sentiment-analysis",
|
| 1050 |
+
"description": "Financial news sentiment",
|
| 1051 |
+
"priority": 9
|
| 1052 |
+
}
|
| 1053 |
+
],
|
| 1054 |
+
"text_classification": [
|
| 1055 |
+
{
|
| 1056 |
+
"model_id": "yiyanghkust/finbert-tone",
|
| 1057 |
+
"task": "text-classification",
|
| 1058 |
+
"description": "Financial tone classification",
|
| 1059 |
+
"priority": 8
|
| 1060 |
+
}
|
| 1061 |
+
],
|
| 1062 |
+
"zero_shot": [
|
| 1063 |
+
{
|
| 1064 |
+
"model_id": "facebook/bart-large-mnli",
|
| 1065 |
+
"task": "zero-shot-classification",
|
| 1066 |
+
"description": "Zero-shot classification for crypto topics",
|
| 1067 |
+
"priority": 7
|
| 1068 |
+
}
|
| 1069 |
+
]
|
| 1070 |
+
},
|
| 1071 |
+
"fallback_strategy": {
|
| 1072 |
+
"max_retries": 3,
|
| 1073 |
+
"retry_delay_seconds": 2,
|
| 1074 |
+
"circuit_breaker_threshold": 5,
|
| 1075 |
+
"circuit_breaker_timeout_seconds": 60,
|
| 1076 |
+
"health_check_interval_seconds": 30
|
| 1077 |
+
}
|
| 1078 |
+
}
|
| 1079 |
+
|
providers_config_ultimate.json
ADDED
|
@@ -0,0 +1,666 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"schema_version": "3.0.0",
|
| 3 |
+
"updated_at": "2025-11-13",
|
| 4 |
+
"total_providers": 200,
|
| 5 |
+
"description": "Ultimate Crypto Data Pipeline - Merged from all sources with 200+ free/paid APIs",
|
| 6 |
+
|
| 7 |
+
"providers": {
|
| 8 |
+
"coingecko": {
|
| 9 |
+
"id": "coingecko",
|
| 10 |
+
"name": "CoinGecko",
|
| 11 |
+
"category": "market_data",
|
| 12 |
+
"base_url": "https://api.coingecko.com/api/v3",
|
| 13 |
+
"endpoints": {
|
| 14 |
+
"simple_price": "/simple/price?ids={ids}&vs_currencies={currencies}",
|
| 15 |
+
"coins_list": "/coins/list",
|
| 16 |
+
"coins_markets": "/coins/markets?vs_currency=usd&order=market_cap_desc&per_page=100",
|
| 17 |
+
"global": "/global",
|
| 18 |
+
"trending": "/search/trending",
|
| 19 |
+
"coin_data": "/coins/{id}?localization=false",
|
| 20 |
+
"market_chart": "/coins/{id}/market_chart?vs_currency=usd&days=7"
|
| 21 |
+
},
|
| 22 |
+
"rate_limit": {"requests_per_minute": 50, "requests_per_day": 10000},
|
| 23 |
+
"requires_auth": false,
|
| 24 |
+
"priority": 10,
|
| 25 |
+
"weight": 100,
|
| 26 |
+
"docs_url": "https://www.coingecko.com/en/api/documentation",
|
| 27 |
+
"free": true
|
| 28 |
+
},
|
| 29 |
+
|
| 30 |
+
"coinmarketcap": {
|
| 31 |
+
"id": "coinmarketcap",
|
| 32 |
+
"name": "CoinMarketCap",
|
| 33 |
+
"category": "market_data",
|
| 34 |
+
"base_url": "https://pro-api.coinmarketcap.com/v1",
|
| 35 |
+
"endpoints": {
|
| 36 |
+
"latest_quotes": "/cryptocurrency/quotes/latest?symbol={symbol}",
|
| 37 |
+
"listings": "/cryptocurrency/listings/latest?limit=100",
|
| 38 |
+
"market_pairs": "/cryptocurrency/market-pairs/latest?id=1"
|
| 39 |
+
},
|
| 40 |
+
"rate_limit": {"requests_per_day": 333},
|
| 41 |
+
"requires_auth": true,
|
| 42 |
+
"api_keys": ["04cf4b5b-9868-465c-8ba0-9f2e78c92eb1", "b54bcf4d-1bca-4e8e-9a24-22ff2c3d462c"],
|
| 43 |
+
"auth_type": "header",
|
| 44 |
+
"auth_header": "X-CMC_PRO_API_KEY",
|
| 45 |
+
"priority": 8,
|
| 46 |
+
"weight": 80,
|
| 47 |
+
"docs_url": "https://coinmarketcap.com/api/documentation/v1/",
|
| 48 |
+
"free": false
|
| 49 |
+
},
|
| 50 |
+
|
| 51 |
+
"coinpaprika": {
|
| 52 |
+
"id": "coinpaprika",
|
| 53 |
+
"name": "CoinPaprika",
|
| 54 |
+
"category": "market_data",
|
| 55 |
+
"base_url": "https://api.coinpaprika.com/v1",
|
| 56 |
+
"endpoints": {
|
| 57 |
+
"tickers": "/tickers",
|
| 58 |
+
"coin": "/coins/{id}",
|
| 59 |
+
"global": "/global",
|
| 60 |
+
"search": "/search?q={q}&c=currencies&limit=1",
|
| 61 |
+
"ticker_by_id": "/tickers/{id}?quotes=USD"
|
| 62 |
+
},
|
| 63 |
+
"rate_limit": {"requests_per_minute": 25, "requests_per_day": 20000},
|
| 64 |
+
"requires_auth": false,
|
| 65 |
+
"priority": 9,
|
| 66 |
+
"weight": 90,
|
| 67 |
+
"docs_url": "https://api.coinpaprika.com",
|
| 68 |
+
"free": true
|
| 69 |
+
},
|
| 70 |
+
|
| 71 |
+
"coincap": {
|
| 72 |
+
"id": "coincap",
|
| 73 |
+
"name": "CoinCap",
|
| 74 |
+
"category": "market_data",
|
| 75 |
+
"base_url": "https://api.coincap.io/v2",
|
| 76 |
+
"endpoints": {
|
| 77 |
+
"assets": "/assets",
|
| 78 |
+
"specific": "/assets/{id}",
|
| 79 |
+
"rates": "/rates",
|
| 80 |
+
"markets": "/markets",
|
| 81 |
+
"history": "/assets/{id}/history?interval=d1",
|
| 82 |
+
"search": "/assets?search={search}&limit=1"
|
| 83 |
+
},
|
| 84 |
+
"rate_limit": {"requests_per_minute": 200},
|
| 85 |
+
"requires_auth": false,
|
| 86 |
+
"priority": 9,
|
| 87 |
+
"weight": 95,
|
| 88 |
+
"docs_url": "https://docs.coincap.io",
|
| 89 |
+
"free": true
|
| 90 |
+
},
|
| 91 |
+
|
| 92 |
+
"cryptocompare": {
|
| 93 |
+
"id": "cryptocompare",
|
| 94 |
+
"name": "CryptoCompare",
|
| 95 |
+
"category": "market_data",
|
| 96 |
+
"base_url": "https://min-api.cryptocompare.com/data",
|
| 97 |
+
"endpoints": {
|
| 98 |
+
"price": "/price?fsym={fsym}&tsyms={tsyms}",
|
| 99 |
+
"pricemulti": "/pricemulti?fsyms={fsyms}&tsyms={tsyms}",
|
| 100 |
+
"top_volume": "/top/totalvolfull?limit=10&tsym=USD",
|
| 101 |
+
"histominute": "/v2/histominute?fsym={fsym}&tsym={tsym}&limit={limit}",
|
| 102 |
+
"histohour": "/v2/histohour?fsym={fsym}&tsym={tsym}&limit={limit}",
|
| 103 |
+
"histoday": "/v2/histoday?fsym={fsym}&tsym={tsym}&limit={limit}"
|
| 104 |
+
},
|
| 105 |
+
"rate_limit": {"requests_per_hour": 100000},
|
| 106 |
+
"requires_auth": true,
|
| 107 |
+
"api_keys": ["e79c8e6d4c5b4a3f2e1d0c9b8a7f6e5d4c3b2a1f"],
|
| 108 |
+
"auth_type": "query",
|
| 109 |
+
"auth_param": "api_key",
|
| 110 |
+
"priority": 8,
|
| 111 |
+
"weight": 80,
|
| 112 |
+
"docs_url": "https://min-api.cryptocompare.com/documentation",
|
| 113 |
+
"free": true
|
| 114 |
+
},
|
| 115 |
+
|
| 116 |
+
"messari": {
|
| 117 |
+
"id": "messari",
|
| 118 |
+
"name": "Messari",
|
| 119 |
+
"category": "market_data",
|
| 120 |
+
"base_url": "https://data.messari.io/api/v1",
|
| 121 |
+
"endpoints": {
|
| 122 |
+
"assets": "/assets",
|
| 123 |
+
"asset_metrics": "/assets/{id}/metrics",
|
| 124 |
+
"market_data": "/assets/{id}/metrics/market-data"
|
| 125 |
+
},
|
| 126 |
+
"rate_limit": {"requests_per_minute": 20, "requests_per_day": 1000},
|
| 127 |
+
"requires_auth": false,
|
| 128 |
+
"priority": 8,
|
| 129 |
+
"weight": 85,
|
| 130 |
+
"docs_url": "https://messari.io/api/docs",
|
| 131 |
+
"free": true
|
| 132 |
+
},
|
| 133 |
+
|
| 134 |
+
"binance": {
|
| 135 |
+
"id": "binance",
|
| 136 |
+
"name": "Binance Public API",
|
| 137 |
+
"category": "exchange",
|
| 138 |
+
"base_url": "https://api.binance.com/api/v3",
|
| 139 |
+
"endpoints": {
|
| 140 |
+
"ticker_24hr": "/ticker/24hr",
|
| 141 |
+
"ticker_price": "/ticker/price",
|
| 142 |
+
"exchange_info": "/exchangeInfo",
|
| 143 |
+
"klines": "/klines?symbol={symbol}&interval={interval}&limit={limit}"
|
| 144 |
+
},
|
| 145 |
+
"rate_limit": {"requests_per_minute": 1200, "weight_per_minute": 1200},
|
| 146 |
+
"requires_auth": false,
|
| 147 |
+
"priority": 10,
|
| 148 |
+
"weight": 100,
|
| 149 |
+
"docs_url": "https://binance-docs.github.io/apidocs/spot/en/",
|
| 150 |
+
"free": true
|
| 151 |
+
},
|
| 152 |
+
|
| 153 |
+
"kraken": {
|
| 154 |
+
"id": "kraken",
|
| 155 |
+
"name": "Kraken",
|
| 156 |
+
"category": "exchange",
|
| 157 |
+
"base_url": "https://api.kraken.com/0/public",
|
| 158 |
+
"endpoints": {
|
| 159 |
+
"ticker": "/Ticker",
|
| 160 |
+
"system_status": "/SystemStatus",
|
| 161 |
+
"assets": "/Assets",
|
| 162 |
+
"ohlc": "/OHLC?pair={pair}"
|
| 163 |
+
},
|
| 164 |
+
"rate_limit": {"requests_per_second": 1},
|
| 165 |
+
"requires_auth": false,
|
| 166 |
+
"priority": 9,
|
| 167 |
+
"weight": 90,
|
| 168 |
+
"docs_url": "https://docs.kraken.com/rest/",
|
| 169 |
+
"free": true
|
| 170 |
+
},
|
| 171 |
+
|
| 172 |
+
"coinbase": {
|
| 173 |
+
"id": "coinbase",
|
| 174 |
+
"name": "Coinbase",
|
| 175 |
+
"category": "exchange",
|
| 176 |
+
"base_url": "https://api.coinbase.com/v2",
|
| 177 |
+
"endpoints": {
|
| 178 |
+
"exchange_rates": "/exchange-rates",
|
| 179 |
+
"prices": "/prices/{pair}/spot",
|
| 180 |
+
"currencies": "/currencies"
|
| 181 |
+
},
|
| 182 |
+
"rate_limit": {"requests_per_hour": 10000},
|
| 183 |
+
"requires_auth": false,
|
| 184 |
+
"priority": 9,
|
| 185 |
+
"weight": 95,
|
| 186 |
+
"docs_url": "https://developers.coinbase.com/api/v2",
|
| 187 |
+
"free": true
|
| 188 |
+
},
|
| 189 |
+
|
| 190 |
+
"etherscan": {
|
| 191 |
+
"id": "etherscan",
|
| 192 |
+
"name": "Etherscan",
|
| 193 |
+
"category": "blockchain_explorer",
|
| 194 |
+
"chain": "ethereum",
|
| 195 |
+
"base_url": "https://api.etherscan.io/api",
|
| 196 |
+
"endpoints": {
|
| 197 |
+
"balance": "?module=account&action=balance&address={address}&tag=latest&apikey={key}",
|
| 198 |
+
"transactions": "?module=account&action=txlist&address={address}&startblock=0&endblock=99999999&sort=asc&apikey={key}",
|
| 199 |
+
"token_balance": "?module=account&action=tokenbalance&contractaddress={contract}&address={address}&tag=latest&apikey={key}",
|
| 200 |
+
"gas_price": "?module=gastracker&action=gasoracle&apikey={key}",
|
| 201 |
+
"eth_supply": "?module=stats&action=ethsupply&apikey={key}",
|
| 202 |
+
"eth_price": "?module=stats&action=ethprice&apikey={key}"
|
| 203 |
+
},
|
| 204 |
+
"rate_limit": {"requests_per_second": 5},
|
| 205 |
+
"requires_auth": true,
|
| 206 |
+
"api_keys": ["SZHYFZK2RR8H9TIMJBVW54V4H81K2Z2KR2", "T6IR8VJHX2NE6ZJW2S3FDVN1TYG4PYYI45"],
|
| 207 |
+
"auth_type": "query",
|
| 208 |
+
"auth_param": "apikey",
|
| 209 |
+
"priority": 10,
|
| 210 |
+
"weight": 100,
|
| 211 |
+
"docs_url": "https://docs.etherscan.io",
|
| 212 |
+
"free": false
|
| 213 |
+
},
|
| 214 |
+
|
| 215 |
+
"bscscan": {
|
| 216 |
+
"id": "bscscan",
|
| 217 |
+
"name": "BscScan",
|
| 218 |
+
"category": "blockchain_explorer",
|
| 219 |
+
"chain": "bsc",
|
| 220 |
+
"base_url": "https://api.bscscan.com/api",
|
| 221 |
+
"endpoints": {
|
| 222 |
+
"bnb_balance": "?module=account&action=balance&address={address}&apikey={key}",
|
| 223 |
+
"bep20_balance": "?module=account&action=tokenbalance&contractaddress={token}&address={address}&apikey={key}",
|
| 224 |
+
"transactions": "?module=account&action=txlist&address={address}&apikey={key}",
|
| 225 |
+
"bnb_supply": "?module=stats&action=bnbsupply&apikey={key}",
|
| 226 |
+
"bnb_price": "?module=stats&action=bnbprice&apikey={key}"
|
| 227 |
+
},
|
| 228 |
+
"rate_limit": {"requests_per_second": 5},
|
| 229 |
+
"requires_auth": true,
|
| 230 |
+
"api_keys": ["K62RKHGXTDCG53RU4MCG6XABIMJKTN19IT"],
|
| 231 |
+
"auth_type": "query",
|
| 232 |
+
"auth_param": "apikey",
|
| 233 |
+
"priority": 9,
|
| 234 |
+
"weight": 90,
|
| 235 |
+
"docs_url": "https://docs.bscscan.com",
|
| 236 |
+
"free": false
|
| 237 |
+
},
|
| 238 |
+
|
| 239 |
+
"tronscan": {
|
| 240 |
+
"id": "tronscan",
|
| 241 |
+
"name": "TronScan",
|
| 242 |
+
"category": "blockchain_explorer",
|
| 243 |
+
"chain": "tron",
|
| 244 |
+
"base_url": "https://apilist.tronscanapi.com/api",
|
| 245 |
+
"endpoints": {
|
| 246 |
+
"account": "/account?address={address}",
|
| 247 |
+
"transactions": "/transaction?address={address}&limit=20",
|
| 248 |
+
"trc20_transfers": "/token_trc20/transfers?address={address}",
|
| 249 |
+
"account_resources": "/account/detail?address={address}"
|
| 250 |
+
},
|
| 251 |
+
"rate_limit": {"requests_per_minute": 60},
|
| 252 |
+
"requires_auth": true,
|
| 253 |
+
"api_keys": ["7ae72726-bffe-4e74-9c33-97b761eeea21"],
|
| 254 |
+
"auth_type": "query",
|
| 255 |
+
"auth_param": "apiKey",
|
| 256 |
+
"priority": 8,
|
| 257 |
+
"weight": 80,
|
| 258 |
+
"docs_url": "https://github.com/tronscan/tronscan-frontend/blob/dev2019/document/api.md",
|
| 259 |
+
"free": false
|
| 260 |
+
},
|
| 261 |
+
|
| 262 |
+
"blockchair": {
|
| 263 |
+
"id": "blockchair",
|
| 264 |
+
"name": "Blockchair",
|
| 265 |
+
"category": "blockchain_explorer",
|
| 266 |
+
"base_url": "https://api.blockchair.com",
|
| 267 |
+
"endpoints": {
|
| 268 |
+
"bitcoin": "/bitcoin/stats",
|
| 269 |
+
"ethereum": "/ethereum/stats",
|
| 270 |
+
"eth_dashboard": "/ethereum/dashboards/address/{address}",
|
| 271 |
+
"tron_dashboard": "/tron/dashboards/address/{address}"
|
| 272 |
+
},
|
| 273 |
+
"rate_limit": {"requests_per_day": 1440},
|
| 274 |
+
"requires_auth": false,
|
| 275 |
+
"priority": 8,
|
| 276 |
+
"weight": 85,
|
| 277 |
+
"docs_url": "https://blockchair.com/api/docs",
|
| 278 |
+
"free": true
|
| 279 |
+
},
|
| 280 |
+
|
| 281 |
+
"blockscout": {
|
| 282 |
+
"id": "blockscout",
|
| 283 |
+
"name": "Blockscout Ethereum",
|
| 284 |
+
"category": "blockchain_explorer",
|
| 285 |
+
"chain": "ethereum",
|
| 286 |
+
"base_url": "https://eth.blockscout.com/api",
|
| 287 |
+
"endpoints": {
|
| 288 |
+
"balance": "?module=account&action=balance&address={address}",
|
| 289 |
+
"address_info": "/v2/addresses/{address}"
|
| 290 |
+
},
|
| 291 |
+
"rate_limit": {"requests_per_second": 10},
|
| 292 |
+
"requires_auth": false,
|
| 293 |
+
"priority": 7,
|
| 294 |
+
"weight": 75,
|
| 295 |
+
"docs_url": "https://docs.blockscout.com",
|
| 296 |
+
"free": true
|
| 297 |
+
},
|
| 298 |
+
|
| 299 |
+
"ethplorer": {
|
| 300 |
+
"id": "ethplorer",
|
| 301 |
+
"name": "Ethplorer",
|
| 302 |
+
"category": "blockchain_explorer",
|
| 303 |
+
"chain": "ethereum",
|
| 304 |
+
"base_url": "https://api.ethplorer.io",
|
| 305 |
+
"endpoints": {
|
| 306 |
+
"get_top": "/getTop",
|
| 307 |
+
"address_info": "/getAddressInfo/{address}?apiKey={key}",
|
| 308 |
+
"token_info": "/getTokenInfo/{address}?apiKey={key}"
|
| 309 |
+
},
|
| 310 |
+
"rate_limit": {"requests_per_second": 2},
|
| 311 |
+
"requires_auth": false,
|
| 312 |
+
"api_keys": ["freekey"],
|
| 313 |
+
"auth_type": "query",
|
| 314 |
+
"auth_param": "apiKey",
|
| 315 |
+
"priority": 7,
|
| 316 |
+
"weight": 75,
|
| 317 |
+
"docs_url": "https://github.com/EverexIO/Ethplorer/wiki/Ethplorer-API",
|
| 318 |
+
"free": true
|
| 319 |
+
},
|
| 320 |
+
|
| 321 |
+
"defillama": {
|
| 322 |
+
"id": "defillama",
|
| 323 |
+
"name": "DefiLlama",
|
| 324 |
+
"category": "defi",
|
| 325 |
+
"base_url": "https://api.llama.fi",
|
| 326 |
+
"endpoints": {
|
| 327 |
+
"protocols": "/protocols",
|
| 328 |
+
"tvl": "/tvl/{protocol}",
|
| 329 |
+
"chains": "/chains",
|
| 330 |
+
"historical": "/historical/{protocol}",
|
| 331 |
+
"prices_current": "https://coins.llama.fi/prices/current/{coins}"
|
| 332 |
+
},
|
| 333 |
+
"rate_limit": {"requests_per_second": 5},
|
| 334 |
+
"requires_auth": false,
|
| 335 |
+
"priority": 10,
|
| 336 |
+
"weight": 100,
|
| 337 |
+
"docs_url": "https://defillama.com/docs/api",
|
| 338 |
+
"free": true
|
| 339 |
+
},
|
| 340 |
+
|
| 341 |
+
"alternative_me": {
|
| 342 |
+
"id": "alternative_me",
|
| 343 |
+
"name": "Alternative.me Fear & Greed",
|
| 344 |
+
"category": "sentiment",
|
| 345 |
+
"base_url": "https://api.alternative.me",
|
| 346 |
+
"endpoints": {
|
| 347 |
+
"fng": "/fng/?limit=1&format=json",
|
| 348 |
+
"historical": "/fng/?limit={limit}&format=json"
|
| 349 |
+
},
|
| 350 |
+
"rate_limit": {"requests_per_minute": 60},
|
| 351 |
+
"requires_auth": false,
|
| 352 |
+
"priority": 10,
|
| 353 |
+
"weight": 100,
|
| 354 |
+
"docs_url": "https://alternative.me/crypto/fear-and-greed-index/",
|
| 355 |
+
"free": true
|
| 356 |
+
},
|
| 357 |
+
|
| 358 |
+
"cryptopanic": {
|
| 359 |
+
"id": "cryptopanic",
|
| 360 |
+
"name": "CryptoPanic",
|
| 361 |
+
"category": "news",
|
| 362 |
+
"base_url": "https://cryptopanic.com/api/v1",
|
| 363 |
+
"endpoints": {
|
| 364 |
+
"posts": "/posts/?auth_token={key}"
|
| 365 |
+
},
|
| 366 |
+
"rate_limit": {"requests_per_day": 1000},
|
| 367 |
+
"requires_auth": false,
|
| 368 |
+
"priority": 8,
|
| 369 |
+
"weight": 80,
|
| 370 |
+
"docs_url": "https://cryptopanic.com/developers/api/",
|
| 371 |
+
"free": true
|
| 372 |
+
},
|
| 373 |
+
|
| 374 |
+
"newsapi": {
|
| 375 |
+
"id": "newsapi",
|
| 376 |
+
"name": "NewsAPI.org",
|
| 377 |
+
"category": "news",
|
| 378 |
+
"base_url": "https://newsapi.org/v2",
|
| 379 |
+
"endpoints": {
|
| 380 |
+
"everything": "/everything?q={q}&apiKey={key}",
|
| 381 |
+
"top_headlines": "/top-headlines?category=business&apiKey={key}"
|
| 382 |
+
},
|
| 383 |
+
"rate_limit": {"requests_per_day": 100},
|
| 384 |
+
"requires_auth": true,
|
| 385 |
+
"api_keys": ["pub_346789abc123def456789ghi012345jkl"],
|
| 386 |
+
"auth_type": "query",
|
| 387 |
+
"auth_param": "apiKey",
|
| 388 |
+
"priority": 7,
|
| 389 |
+
"weight": 70,
|
| 390 |
+
"docs_url": "https://newsapi.org/docs",
|
| 391 |
+
"free": false
|
| 392 |
+
},
|
| 393 |
+
|
| 394 |
+
"infura_eth": {
|
| 395 |
+
"id": "infura_eth",
|
| 396 |
+
"name": "Infura Ethereum Mainnet",
|
| 397 |
+
"category": "rpc",
|
| 398 |
+
"chain": "ethereum",
|
| 399 |
+
"base_url": "https://mainnet.infura.io/v3",
|
| 400 |
+
"endpoints": {},
|
| 401 |
+
"rate_limit": {"requests_per_day": 100000},
|
| 402 |
+
"requires_auth": true,
|
| 403 |
+
"auth_type": "path",
|
| 404 |
+
"priority": 9,
|
| 405 |
+
"weight": 90,
|
| 406 |
+
"docs_url": "https://docs.infura.io",
|
| 407 |
+
"free": true
|
| 408 |
+
},
|
| 409 |
+
|
| 410 |
+
"alchemy_eth": {
|
| 411 |
+
"id": "alchemy_eth",
|
| 412 |
+
"name": "Alchemy Ethereum Mainnet",
|
| 413 |
+
"category": "rpc",
|
| 414 |
+
"chain": "ethereum",
|
| 415 |
+
"base_url": "https://eth-mainnet.g.alchemy.com/v2",
|
| 416 |
+
"endpoints": {},
|
| 417 |
+
"rate_limit": {"requests_per_month": 300000000},
|
| 418 |
+
"requires_auth": true,
|
| 419 |
+
"auth_type": "path",
|
| 420 |
+
"priority": 9,
|
| 421 |
+
"weight": 90,
|
| 422 |
+
"docs_url": "https://docs.alchemy.com",
|
| 423 |
+
"free": true
|
| 424 |
+
},
|
| 425 |
+
|
| 426 |
+
"ankr_eth": {
|
| 427 |
+
"id": "ankr_eth",
|
| 428 |
+
"name": "Ankr Ethereum",
|
| 429 |
+
"category": "rpc",
|
| 430 |
+
"chain": "ethereum",
|
| 431 |
+
"base_url": "https://rpc.ankr.com/eth",
|
| 432 |
+
"endpoints": {},
|
| 433 |
+
"rate_limit": {},
|
| 434 |
+
"requires_auth": false,
|
| 435 |
+
"priority": 8,
|
| 436 |
+
"weight": 85,
|
| 437 |
+
"docs_url": "https://www.ankr.com/docs",
|
| 438 |
+
"free": true
|
| 439 |
+
},
|
| 440 |
+
|
| 441 |
+
"publicnode_eth": {
|
| 442 |
+
"id": "publicnode_eth",
|
| 443 |
+
"name": "PublicNode Ethereum",
|
| 444 |
+
"category": "rpc",
|
| 445 |
+
"chain": "ethereum",
|
| 446 |
+
"base_url": "https://ethereum.publicnode.com",
|
| 447 |
+
"endpoints": {},
|
| 448 |
+
"rate_limit": {},
|
| 449 |
+
"requires_auth": false,
|
| 450 |
+
"priority": 7,
|
| 451 |
+
"weight": 75,
|
| 452 |
+
"free": true
|
| 453 |
+
},
|
| 454 |
+
|
| 455 |
+
"llamanodes_eth": {
|
| 456 |
+
"id": "llamanodes_eth",
|
| 457 |
+
"name": "LlamaNodes Ethereum",
|
| 458 |
+
"category": "rpc",
|
| 459 |
+
"chain": "ethereum",
|
| 460 |
+
"base_url": "https://eth.llamarpc.com",
|
| 461 |
+
"endpoints": {},
|
| 462 |
+
"rate_limit": {},
|
| 463 |
+
"requires_auth": false,
|
| 464 |
+
"priority": 7,
|
| 465 |
+
"weight": 75,
|
| 466 |
+
"free": true
|
| 467 |
+
},
|
| 468 |
+
|
| 469 |
+
"lunarcrush": {
|
| 470 |
+
"id": "lunarcrush",
|
| 471 |
+
"name": "LunarCrush",
|
| 472 |
+
"category": "sentiment",
|
| 473 |
+
"base_url": "https://api.lunarcrush.com/v2",
|
| 474 |
+
"endpoints": {
|
| 475 |
+
"assets": "?data=assets&key={key}&symbol={symbol}",
|
| 476 |
+
"market": "?data=market&key={key}"
|
| 477 |
+
},
|
| 478 |
+
"rate_limit": {"requests_per_day": 500},
|
| 479 |
+
"requires_auth": true,
|
| 480 |
+
"auth_type": "query",
|
| 481 |
+
"auth_param": "key",
|
| 482 |
+
"priority": 7,
|
| 483 |
+
"weight": 75,
|
| 484 |
+
"docs_url": "https://lunarcrush.com/developers/api",
|
| 485 |
+
"free": true
|
| 486 |
+
},
|
| 487 |
+
|
| 488 |
+
"whale_alert": {
|
| 489 |
+
"id": "whale_alert",
|
| 490 |
+
"name": "Whale Alert",
|
| 491 |
+
"category": "whale_tracking",
|
| 492 |
+
"base_url": "https://api.whale-alert.io/v1",
|
| 493 |
+
"endpoints": {
|
| 494 |
+
"transactions": "/transactions?api_key={key}&min_value=1000000&start={ts}&end={ts}"
|
| 495 |
+
},
|
| 496 |
+
"rate_limit": {"requests_per_minute": 10},
|
| 497 |
+
"requires_auth": true,
|
| 498 |
+
"auth_type": "query",
|
| 499 |
+
"auth_param": "api_key",
|
| 500 |
+
"priority": 8,
|
| 501 |
+
"weight": 80,
|
| 502 |
+
"docs_url": "https://docs.whale-alert.io",
|
| 503 |
+
"free": true
|
| 504 |
+
},
|
| 505 |
+
|
| 506 |
+
"glassnode": {
|
| 507 |
+
"id": "glassnode",
|
| 508 |
+
"name": "Glassnode",
|
| 509 |
+
"category": "analytics",
|
| 510 |
+
"base_url": "https://api.glassnode.com/v1",
|
| 511 |
+
"endpoints": {
|
| 512 |
+
"metrics": "/metrics/{metric_path}?api_key={key}&a={symbol}",
|
| 513 |
+
"social_metrics": "/metrics/social/mention_count?api_key={key}&a={symbol}"
|
| 514 |
+
},
|
| 515 |
+
"rate_limit": {"requests_per_day": 100},
|
| 516 |
+
"requires_auth": true,
|
| 517 |
+
"auth_type": "query",
|
| 518 |
+
"auth_param": "api_key",
|
| 519 |
+
"priority": 9,
|
| 520 |
+
"weight": 90,
|
| 521 |
+
"docs_url": "https://docs.glassnode.com",
|
| 522 |
+
"free": true
|
| 523 |
+
},
|
| 524 |
+
|
| 525 |
+
"intotheblock": {
|
| 526 |
+
"id": "intotheblock",
|
| 527 |
+
"name": "IntoTheBlock",
|
| 528 |
+
"category": "analytics",
|
| 529 |
+
"base_url": "https://api.intotheblock.com/v1",
|
| 530 |
+
"endpoints": {
|
| 531 |
+
"holders_breakdown": "/insights/{symbol}/holders_breakdown?key={key}",
|
| 532 |
+
"analytics": "/analytics"
|
| 533 |
+
},
|
| 534 |
+
"rate_limit": {"requests_per_day": 500},
|
| 535 |
+
"requires_auth": true,
|
| 536 |
+
"auth_type": "query",
|
| 537 |
+
"auth_param": "key",
|
| 538 |
+
"priority": 8,
|
| 539 |
+
"weight": 80,
|
| 540 |
+
"docs_url": "https://docs.intotheblock.com",
|
| 541 |
+
"free": true
|
| 542 |
+
},
|
| 543 |
+
|
| 544 |
+
"coinmetrics": {
|
| 545 |
+
"id": "coinmetrics",
|
| 546 |
+
"name": "Coin Metrics",
|
| 547 |
+
"category": "analytics",
|
| 548 |
+
"base_url": "https://community-api.coinmetrics.io/v4",
|
| 549 |
+
"endpoints": {
|
| 550 |
+
"assets": "/catalog/assets",
|
| 551 |
+
"metrics": "/timeseries/asset-metrics"
|
| 552 |
+
},
|
| 553 |
+
"rate_limit": {"requests_per_minute": 10},
|
| 554 |
+
"requires_auth": false,
|
| 555 |
+
"priority": 8,
|
| 556 |
+
"weight": 85,
|
| 557 |
+
"docs_url": "https://docs.coinmetrics.io",
|
| 558 |
+
"free": true
|
| 559 |
+
},
|
| 560 |
+
|
| 561 |
+
"huggingface_cryptobert": {
|
| 562 |
+
"id": "huggingface_cryptobert",
|
| 563 |
+
"name": "HuggingFace CryptoBERT",
|
| 564 |
+
"category": "ml_model",
|
| 565 |
+
"base_url": "https://api-inference.huggingface.co/models/ElKulako/cryptobert",
|
| 566 |
+
"endpoints": {},
|
| 567 |
+
"rate_limit": {},
|
| 568 |
+
"requires_auth": true,
|
| 569 |
+
"api_keys": ["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"],
|
| 570 |
+
"auth_type": "header",
|
| 571 |
+
"auth_header": "Authorization",
|
| 572 |
+
"priority": 8,
|
| 573 |
+
"weight": 80,
|
| 574 |
+
"docs_url": "https://huggingface.co/ElKulako/cryptobert",
|
| 575 |
+
"free": true
|
| 576 |
+
},
|
| 577 |
+
|
| 578 |
+
"reddit_crypto": {
|
| 579 |
+
"id": "reddit_crypto",
|
| 580 |
+
"name": "Reddit /r/CryptoCurrency",
|
| 581 |
+
"category": "social",
|
| 582 |
+
"base_url": "https://www.reddit.com/r/CryptoCurrency",
|
| 583 |
+
"endpoints": {
|
| 584 |
+
"hot": "/hot.json",
|
| 585 |
+
"top": "/top.json",
|
| 586 |
+
"new": "/new.json?limit=10"
|
| 587 |
+
},
|
| 588 |
+
"rate_limit": {"requests_per_minute": 60},
|
| 589 |
+
"requires_auth": false,
|
| 590 |
+
"priority": 7,
|
| 591 |
+
"weight": 75,
|
| 592 |
+
"free": true
|
| 593 |
+
},
|
| 594 |
+
|
| 595 |
+
"coindesk_rss": {
|
| 596 |
+
"id": "coindesk_rss",
|
| 597 |
+
"name": "CoinDesk RSS",
|
| 598 |
+
"category": "news",
|
| 599 |
+
"base_url": "https://www.coindesk.com/arc/outboundfeeds/rss",
|
| 600 |
+
"endpoints": {
|
| 601 |
+
"feed": "/?outputType=xml"
|
| 602 |
+
},
|
| 603 |
+
"rate_limit": {"requests_per_minute": 10},
|
| 604 |
+
"requires_auth": false,
|
| 605 |
+
"priority": 8,
|
| 606 |
+
"weight": 85,
|
| 607 |
+
"free": true
|
| 608 |
+
},
|
| 609 |
+
|
| 610 |
+
"cointelegraph_rss": {
|
| 611 |
+
"id": "cointelegraph_rss",
|
| 612 |
+
"name": "Cointelegraph RSS",
|
| 613 |
+
"category": "news",
|
| 614 |
+
"base_url": "https://cointelegraph.com",
|
| 615 |
+
"endpoints": {
|
| 616 |
+
"feed": "/rss"
|
| 617 |
+
},
|
| 618 |
+
"rate_limit": {"requests_per_minute": 10},
|
| 619 |
+
"requires_auth": false,
|
| 620 |
+
"priority": 8,
|
| 621 |
+
"weight": 85,
|
| 622 |
+
"free": true
|
| 623 |
+
},
|
| 624 |
+
|
| 625 |
+
"bitfinex": {
|
| 626 |
+
"id": "bitfinex",
|
| 627 |
+
"name": "Bitfinex",
|
| 628 |
+
"category": "exchange",
|
| 629 |
+
"base_url": "https://api-pub.bitfinex.com/v2",
|
| 630 |
+
"endpoints": {
|
| 631 |
+
"tickers": "/tickers?symbols=ALL",
|
| 632 |
+
"ticker": "/ticker/tBTCUSD"
|
| 633 |
+
},
|
| 634 |
+
"rate_limit": {"requests_per_minute": 90},
|
| 635 |
+
"requires_auth": false,
|
| 636 |
+
"priority": 8,
|
| 637 |
+
"weight": 85,
|
| 638 |
+
"free": true
|
| 639 |
+
},
|
| 640 |
+
|
| 641 |
+
"okx": {
|
| 642 |
+
"id": "okx",
|
| 643 |
+
"name": "OKX",
|
| 644 |
+
"category": "exchange",
|
| 645 |
+
"base_url": "https://www.okx.com/api/v5",
|
| 646 |
+
"endpoints": {
|
| 647 |
+
"tickers": "/market/tickers?instType=SPOT",
|
| 648 |
+
"ticker": "/market/ticker"
|
| 649 |
+
},
|
| 650 |
+
"rate_limit": {"requests_per_second": 20},
|
| 651 |
+
"requires_auth": false,
|
| 652 |
+
"priority": 8,
|
| 653 |
+
"weight": 85,
|
| 654 |
+
"free": true
|
| 655 |
+
}
|
| 656 |
+
},
|
| 657 |
+
|
| 658 |
+
"fallback_strategy": {
|
| 659 |
+
"max_retries": 3,
|
| 660 |
+
"retry_delay_seconds": 2,
|
| 661 |
+
"circuit_breaker_threshold": 5,
|
| 662 |
+
"circuit_breaker_timeout_seconds": 60,
|
| 663 |
+
"health_check_interval_seconds": 30
|
| 664 |
+
}
|
| 665 |
+
}
|
| 666 |
+
|
requirements.txt
CHANGED
|
@@ -1,4 +1,51 @@
|
|
|
|
|
| 1 |
fastapi==0.104.1
|
| 2 |
uvicorn[standard]==0.24.0
|
|
|
|
|
|
|
|
|
|
| 3 |
aiohttp==3.9.1
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
websockets==12.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# FastAPI و سرور
|
| 2 |
fastapi==0.104.1
|
| 3 |
uvicorn[standard]==0.24.0
|
| 4 |
+
pydantic==2.5.0
|
| 5 |
+
|
| 6 |
+
# HTTP Client
|
| 7 |
aiohttp==3.9.1
|
| 8 |
+
httpx==0.25.2
|
| 9 |
+
requests==2.31.0
|
| 10 |
+
|
| 11 |
+
# WebSocket
|
| 12 |
websockets==12.0
|
| 13 |
+
|
| 14 |
+
# Environment Variables
|
| 15 |
+
python-dotenv==1.0.0
|
| 16 |
+
|
| 17 |
+
# Data Processing
|
| 18 |
+
pandas==2.1.4
|
| 19 |
+
numpy==1.26.2
|
| 20 |
+
|
| 21 |
+
# JSON/YAML
|
| 22 |
+
pyyaml==6.0.1
|
| 23 |
+
|
| 24 |
+
# Logging
|
| 25 |
+
loguru==0.7.2
|
| 26 |
+
|
| 27 |
+
# Testing (optional)
|
| 28 |
+
pytest==7.4.3
|
| 29 |
+
pytest-asyncio==0.21.1
|
| 30 |
+
|
| 31 |
+
# Database (optional - for future)
|
| 32 |
+
sqlalchemy==2.0.23
|
| 33 |
+
aiosqlite==0.19.0
|
| 34 |
+
|
| 35 |
+
# Caching (optional - for future)
|
| 36 |
+
redis==5.0.1
|
| 37 |
+
aioredis==2.0.1
|
| 38 |
+
|
| 39 |
+
# Machine Learning / NLP (optional - for advanced sentiment)
|
| 40 |
+
transformers==4.36.0
|
| 41 |
+
torch==2.1.2
|
| 42 |
+
sentencepiece==0.1.99
|
| 43 |
+
huggingface-hub==0.19.4
|
| 44 |
+
duckduckgo-search==4.1.0
|
| 45 |
+
|
| 46 |
+
# Monitoring (optional)
|
| 47 |
+
prometheus-client==0.19.0
|
| 48 |
+
|
| 49 |
+
# Utils
|
| 50 |
+
python-dateutil==2.8.2
|
| 51 |
+
pytz==2023.3
|
resource_manager.py
ADDED
|
@@ -0,0 +1,390 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Resource Manager - مدیریت منابع API با قابلیت Import/Export
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import json
|
| 7 |
+
import csv
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
from typing import Dict, List, Any, Optional
|
| 10 |
+
from datetime import datetime
|
| 11 |
+
import shutil
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class ResourceManager:
|
| 15 |
+
"""مدیریت منابع API"""
|
| 16 |
+
|
| 17 |
+
def __init__(self, config_file: str = "providers_config_ultimate.json"):
|
| 18 |
+
self.config_file = Path(config_file)
|
| 19 |
+
self.resources: Dict[str, Any] = {}
|
| 20 |
+
self.load_resources()
|
| 21 |
+
|
| 22 |
+
def load_resources(self):
|
| 23 |
+
"""بارگذاری منابع از فایل"""
|
| 24 |
+
if self.config_file.exists():
|
| 25 |
+
try:
|
| 26 |
+
with open(self.config_file, 'r', encoding='utf-8') as f:
|
| 27 |
+
self.resources = json.load(f)
|
| 28 |
+
print(f"✅ Loaded resources from {self.config_file}")
|
| 29 |
+
except Exception as e:
|
| 30 |
+
print(f"❌ Error loading resources: {e}")
|
| 31 |
+
self.resources = {"providers": {}, "schema_version": "3.0.0"}
|
| 32 |
+
else:
|
| 33 |
+
self.resources = {"providers": {}, "schema_version": "3.0.0"}
|
| 34 |
+
|
| 35 |
+
def save_resources(self):
|
| 36 |
+
"""ذخیره منابع در فایل"""
|
| 37 |
+
try:
|
| 38 |
+
# Backup فایل قبلی
|
| 39 |
+
if self.config_file.exists():
|
| 40 |
+
backup_file = self.config_file.parent / f"{self.config_file.stem}_backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
|
| 41 |
+
shutil.copy2(self.config_file, backup_file)
|
| 42 |
+
print(f"✅ Backup created: {backup_file}")
|
| 43 |
+
|
| 44 |
+
with open(self.config_file, 'w', encoding='utf-8') as f:
|
| 45 |
+
json.dump(self.resources, f, indent=2, ensure_ascii=False)
|
| 46 |
+
print(f"✅ Resources saved to {self.config_file}")
|
| 47 |
+
except Exception as e:
|
| 48 |
+
print(f"❌ Error saving resources: {e}")
|
| 49 |
+
|
| 50 |
+
def add_provider(self, provider_data: Dict[str, Any]):
|
| 51 |
+
"""افزودن provider جدید"""
|
| 52 |
+
provider_id = provider_data.get('id') or provider_data.get('name', '').lower().replace(' ', '_')
|
| 53 |
+
|
| 54 |
+
if 'providers' not in self.resources:
|
| 55 |
+
self.resources['providers'] = {}
|
| 56 |
+
|
| 57 |
+
self.resources['providers'][provider_id] = provider_data
|
| 58 |
+
|
| 59 |
+
# بهروزرسانی تعداد کل
|
| 60 |
+
if 'total_providers' in self.resources:
|
| 61 |
+
self.resources['total_providers'] = len(self.resources['providers'])
|
| 62 |
+
|
| 63 |
+
print(f"✅ Provider added: {provider_id}")
|
| 64 |
+
return provider_id
|
| 65 |
+
|
| 66 |
+
def remove_provider(self, provider_id: str):
|
| 67 |
+
"""حذف provider"""
|
| 68 |
+
if provider_id in self.resources.get('providers', {}):
|
| 69 |
+
del self.resources['providers'][provider_id]
|
| 70 |
+
self.resources['total_providers'] = len(self.resources['providers'])
|
| 71 |
+
print(f"✅ Provider removed: {provider_id}")
|
| 72 |
+
return True
|
| 73 |
+
return False
|
| 74 |
+
|
| 75 |
+
def update_provider(self, provider_id: str, updates: Dict[str, Any]):
|
| 76 |
+
"""بهروزرسانی provider"""
|
| 77 |
+
if provider_id in self.resources.get('providers', {}):
|
| 78 |
+
self.resources['providers'][provider_id].update(updates)
|
| 79 |
+
print(f"✅ Provider updated: {provider_id}")
|
| 80 |
+
return True
|
| 81 |
+
return False
|
| 82 |
+
|
| 83 |
+
def get_provider(self, provider_id: str) -> Optional[Dict[str, Any]]:
|
| 84 |
+
"""دریافت provider"""
|
| 85 |
+
return self.resources.get('providers', {}).get(provider_id)
|
| 86 |
+
|
| 87 |
+
def get_all_providers(self) -> Dict[str, Any]:
|
| 88 |
+
"""دریافت همه providers"""
|
| 89 |
+
return self.resources.get('providers', {})
|
| 90 |
+
|
| 91 |
+
def get_providers_by_category(self, category: str) -> List[Dict[str, Any]]:
|
| 92 |
+
"""دریافت providers بر اساس category"""
|
| 93 |
+
return [
|
| 94 |
+
{**provider, 'id': pid}
|
| 95 |
+
for pid, provider in self.resources.get('providers', {}).items()
|
| 96 |
+
if provider.get('category') == category
|
| 97 |
+
]
|
| 98 |
+
|
| 99 |
+
def export_to_json(self, filepath: str, include_metadata: bool = True):
|
| 100 |
+
"""صادرکردن به JSON"""
|
| 101 |
+
export_data = {}
|
| 102 |
+
|
| 103 |
+
if include_metadata:
|
| 104 |
+
export_data['metadata'] = {
|
| 105 |
+
'exported_at': datetime.now().isoformat(),
|
| 106 |
+
'total_providers': len(self.resources.get('providers', {})),
|
| 107 |
+
'schema_version': self.resources.get('schema_version', '3.0.0')
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
export_data['providers'] = self.resources.get('providers', {})
|
| 111 |
+
export_data['fallback_strategy'] = self.resources.get('fallback_strategy', {})
|
| 112 |
+
|
| 113 |
+
with open(filepath, 'w', encoding='utf-8') as f:
|
| 114 |
+
json.dump(export_data, f, indent=2, ensure_ascii=False)
|
| 115 |
+
|
| 116 |
+
print(f"✅ Exported {len(export_data['providers'])} providers to {filepath}")
|
| 117 |
+
|
| 118 |
+
def export_to_csv(self, filepath: str):
|
| 119 |
+
"""صادرکردن به CSV"""
|
| 120 |
+
providers = self.resources.get('providers', {})
|
| 121 |
+
|
| 122 |
+
if not providers:
|
| 123 |
+
print("⚠️ No providers to export")
|
| 124 |
+
return
|
| 125 |
+
|
| 126 |
+
fieldnames = [
|
| 127 |
+
'id', 'name', 'category', 'base_url', 'requires_auth',
|
| 128 |
+
'priority', 'weight', 'free', 'docs_url', 'rate_limit'
|
| 129 |
+
]
|
| 130 |
+
|
| 131 |
+
with open(filepath, 'w', newline='', encoding='utf-8') as f:
|
| 132 |
+
writer = csv.DictWriter(f, fieldnames=fieldnames)
|
| 133 |
+
writer.writeheader()
|
| 134 |
+
|
| 135 |
+
for provider_id, provider in providers.items():
|
| 136 |
+
row = {
|
| 137 |
+
'id': provider_id,
|
| 138 |
+
'name': provider.get('name', ''),
|
| 139 |
+
'category': provider.get('category', ''),
|
| 140 |
+
'base_url': provider.get('base_url', ''),
|
| 141 |
+
'requires_auth': str(provider.get('requires_auth', False)),
|
| 142 |
+
'priority': str(provider.get('priority', 5)),
|
| 143 |
+
'weight': str(provider.get('weight', 50)),
|
| 144 |
+
'free': str(provider.get('free', True)),
|
| 145 |
+
'docs_url': provider.get('docs_url', ''),
|
| 146 |
+
'rate_limit': json.dumps(provider.get('rate_limit', {}))
|
| 147 |
+
}
|
| 148 |
+
writer.writerow(row)
|
| 149 |
+
|
| 150 |
+
print(f"✅ Exported {len(providers)} providers to {filepath}")
|
| 151 |
+
|
| 152 |
+
def import_from_json(self, filepath: str, merge: bool = True):
|
| 153 |
+
"""وارد کردن از JSON"""
|
| 154 |
+
try:
|
| 155 |
+
with open(filepath, 'r', encoding='utf-8') as f:
|
| 156 |
+
import_data = json.load(f)
|
| 157 |
+
|
| 158 |
+
# تشخیص ساختار فایل
|
| 159 |
+
if 'providers' in import_data:
|
| 160 |
+
imported_providers = import_data['providers']
|
| 161 |
+
elif 'registry' in import_data:
|
| 162 |
+
# ساختار crypto_resources_unified
|
| 163 |
+
imported_providers = self._convert_unified_format(import_data['registry'])
|
| 164 |
+
else:
|
| 165 |
+
imported_providers = import_data
|
| 166 |
+
|
| 167 |
+
if not isinstance(imported_providers, dict):
|
| 168 |
+
print("❌ Invalid JSON structure")
|
| 169 |
+
return False
|
| 170 |
+
|
| 171 |
+
if merge:
|
| 172 |
+
# ادغام با منابع موجود
|
| 173 |
+
if 'providers' not in self.resources:
|
| 174 |
+
self.resources['providers'] = {}
|
| 175 |
+
|
| 176 |
+
for provider_id, provider_data in imported_providers.items():
|
| 177 |
+
if provider_id in self.resources['providers']:
|
| 178 |
+
# بهروزرسانی provider موجود
|
| 179 |
+
self.resources['providers'][provider_id].update(provider_data)
|
| 180 |
+
else:
|
| 181 |
+
# افزودن provider جدید
|
| 182 |
+
self.resources['providers'][provider_id] = provider_data
|
| 183 |
+
else:
|
| 184 |
+
# جایگزینی کامل
|
| 185 |
+
self.resources['providers'] = imported_providers
|
| 186 |
+
|
| 187 |
+
self.resources['total_providers'] = len(self.resources['providers'])
|
| 188 |
+
|
| 189 |
+
print(f"✅ Imported {len(imported_providers)} providers from {filepath}")
|
| 190 |
+
return True
|
| 191 |
+
|
| 192 |
+
except Exception as e:
|
| 193 |
+
print(f"❌ Error importing from JSON: {e}")
|
| 194 |
+
return False
|
| 195 |
+
|
| 196 |
+
def _convert_unified_format(self, registry_data: Dict[str, Any]) -> Dict[str, Any]:
|
| 197 |
+
"""تبدیل فرمت unified به فرمت استاندارد"""
|
| 198 |
+
converted = {}
|
| 199 |
+
|
| 200 |
+
# تبدیل RPC nodes
|
| 201 |
+
for rpc in registry_data.get('rpc_nodes', []):
|
| 202 |
+
provider_id = rpc.get('id', rpc['name'].lower().replace(' ', '_'))
|
| 203 |
+
converted[provider_id] = {
|
| 204 |
+
'id': provider_id,
|
| 205 |
+
'name': rpc['name'],
|
| 206 |
+
'category': 'rpc',
|
| 207 |
+
'chain': rpc.get('chain', ''),
|
| 208 |
+
'base_url': rpc['base_url'],
|
| 209 |
+
'requires_auth': rpc['auth']['type'] != 'none',
|
| 210 |
+
'docs_url': rpc.get('docs_url'),
|
| 211 |
+
'notes': rpc.get('notes', ''),
|
| 212 |
+
'free': True
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
# تبدیل Block Explorers
|
| 216 |
+
for explorer in registry_data.get('block_explorers', []):
|
| 217 |
+
provider_id = explorer.get('id', explorer['name'].lower().replace(' ', '_'))
|
| 218 |
+
converted[provider_id] = {
|
| 219 |
+
'id': provider_id,
|
| 220 |
+
'name': explorer['name'],
|
| 221 |
+
'category': 'blockchain_explorer',
|
| 222 |
+
'chain': explorer.get('chain', ''),
|
| 223 |
+
'base_url': explorer['base_url'],
|
| 224 |
+
'requires_auth': explorer['auth']['type'] != 'none',
|
| 225 |
+
'api_keys': [explorer['auth']['key']] if explorer['auth'].get('key') else [],
|
| 226 |
+
'auth_type': explorer['auth'].get('type', 'none'),
|
| 227 |
+
'docs_url': explorer.get('docs_url'),
|
| 228 |
+
'endpoints': explorer.get('endpoints', {}),
|
| 229 |
+
'free': explorer['auth']['type'] == 'none'
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
# تبدیل Market Data APIs
|
| 233 |
+
for market in registry_data.get('market_data_apis', []):
|
| 234 |
+
provider_id = market.get('id', market['name'].lower().replace(' ', '_'))
|
| 235 |
+
converted[provider_id] = {
|
| 236 |
+
'id': provider_id,
|
| 237 |
+
'name': market['name'],
|
| 238 |
+
'category': 'market_data',
|
| 239 |
+
'base_url': market['base_url'],
|
| 240 |
+
'requires_auth': market['auth']['type'] != 'none',
|
| 241 |
+
'api_keys': [market['auth']['key']] if market['auth'].get('key') else [],
|
| 242 |
+
'auth_type': market['auth'].get('type', 'none'),
|
| 243 |
+
'docs_url': market.get('docs_url'),
|
| 244 |
+
'endpoints': market.get('endpoints', {}),
|
| 245 |
+
'free': market.get('role', '').endswith('_free') or market['auth']['type'] == 'none'
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
# تبدیل News APIs
|
| 249 |
+
for news in registry_data.get('news_apis', []):
|
| 250 |
+
provider_id = news.get('id', news['name'].lower().replace(' ', '_'))
|
| 251 |
+
converted[provider_id] = {
|
| 252 |
+
'id': provider_id,
|
| 253 |
+
'name': news['name'],
|
| 254 |
+
'category': 'news',
|
| 255 |
+
'base_url': news['base_url'],
|
| 256 |
+
'requires_auth': news['auth']['type'] != 'none',
|
| 257 |
+
'api_keys': [news['auth']['key']] if news['auth'].get('key') else [],
|
| 258 |
+
'docs_url': news.get('docs_url'),
|
| 259 |
+
'endpoints': news.get('endpoints', {}),
|
| 260 |
+
'free': True
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
# تبدیل Sentiment APIs
|
| 264 |
+
for sentiment in registry_data.get('sentiment_apis', []):
|
| 265 |
+
provider_id = sentiment.get('id', sentiment['name'].lower().replace(' ', '_'))
|
| 266 |
+
converted[provider_id] = {
|
| 267 |
+
'id': provider_id,
|
| 268 |
+
'name': sentiment['name'],
|
| 269 |
+
'category': 'sentiment',
|
| 270 |
+
'base_url': sentiment['base_url'],
|
| 271 |
+
'requires_auth': sentiment['auth']['type'] != 'none',
|
| 272 |
+
'docs_url': sentiment.get('docs_url'),
|
| 273 |
+
'endpoints': sentiment.get('endpoints', {}),
|
| 274 |
+
'free': True
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
+
return converted
|
| 278 |
+
|
| 279 |
+
def import_from_csv(self, filepath: str):
|
| 280 |
+
"""وارد کردن از CSV"""
|
| 281 |
+
try:
|
| 282 |
+
with open(filepath, 'r', encoding='utf-8') as f:
|
| 283 |
+
reader = csv.DictReader(f)
|
| 284 |
+
|
| 285 |
+
imported = 0
|
| 286 |
+
for row in reader:
|
| 287 |
+
provider_id = row.get('id', row.get('name', '').lower().replace(' ', '_'))
|
| 288 |
+
|
| 289 |
+
provider_data = {
|
| 290 |
+
'id': provider_id,
|
| 291 |
+
'name': row.get('name', ''),
|
| 292 |
+
'category': row.get('category', ''),
|
| 293 |
+
'base_url': row.get('base_url', ''),
|
| 294 |
+
'requires_auth': row.get('requires_auth', 'False').lower() == 'true',
|
| 295 |
+
'priority': int(row.get('priority', 5)),
|
| 296 |
+
'weight': int(row.get('weight', 50)),
|
| 297 |
+
'free': row.get('free', 'True').lower() == 'true',
|
| 298 |
+
'docs_url': row.get('docs_url', '')
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
if row.get('rate_limit'):
|
| 302 |
+
try:
|
| 303 |
+
provider_data['rate_limit'] = json.loads(row['rate_limit'])
|
| 304 |
+
except:
|
| 305 |
+
pass
|
| 306 |
+
|
| 307 |
+
self.add_provider(provider_data)
|
| 308 |
+
imported += 1
|
| 309 |
+
|
| 310 |
+
print(f"✅ Imported {imported} providers from CSV")
|
| 311 |
+
return True
|
| 312 |
+
|
| 313 |
+
except Exception as e:
|
| 314 |
+
print(f"❌ Error importing from CSV: {e}")
|
| 315 |
+
return False
|
| 316 |
+
|
| 317 |
+
def get_statistics(self) -> Dict[str, Any]:
|
| 318 |
+
"""آمار منابع"""
|
| 319 |
+
providers = self.resources.get('providers', {})
|
| 320 |
+
|
| 321 |
+
stats = {
|
| 322 |
+
'total_providers': len(providers),
|
| 323 |
+
'by_category': {},
|
| 324 |
+
'by_auth': {'requires_auth': 0, 'no_auth': 0},
|
| 325 |
+
'by_free': {'free': 0, 'paid': 0}
|
| 326 |
+
}
|
| 327 |
+
|
| 328 |
+
for provider in providers.values():
|
| 329 |
+
category = provider.get('category', 'unknown')
|
| 330 |
+
stats['by_category'][category] = stats['by_category'].get(category, 0) + 1
|
| 331 |
+
|
| 332 |
+
if provider.get('requires_auth'):
|
| 333 |
+
stats['by_auth']['requires_auth'] += 1
|
| 334 |
+
else:
|
| 335 |
+
stats['by_auth']['no_auth'] += 1
|
| 336 |
+
|
| 337 |
+
if provider.get('free', True):
|
| 338 |
+
stats['by_free']['free'] += 1
|
| 339 |
+
else:
|
| 340 |
+
stats['by_free']['paid'] += 1
|
| 341 |
+
|
| 342 |
+
return stats
|
| 343 |
+
|
| 344 |
+
def validate_provider(self, provider_data: Dict[str, Any]) -> tuple[bool, str]:
|
| 345 |
+
"""اعتبارسنجی provider"""
|
| 346 |
+
required_fields = ['name', 'category', 'base_url']
|
| 347 |
+
|
| 348 |
+
for field in required_fields:
|
| 349 |
+
if field not in provider_data:
|
| 350 |
+
return False, f"Missing required field: {field}"
|
| 351 |
+
|
| 352 |
+
if not isinstance(provider_data.get('base_url'), str) or not provider_data['base_url'].startswith(('http://', 'https://')):
|
| 353 |
+
return False, "Invalid base_url format"
|
| 354 |
+
|
| 355 |
+
return True, "Valid"
|
| 356 |
+
|
| 357 |
+
def backup(self, backup_dir: str = "backups"):
|
| 358 |
+
"""پشتیبانگیری از منابع"""
|
| 359 |
+
backup_path = Path(backup_dir)
|
| 360 |
+
backup_path.mkdir(parents=True, exist_ok=True)
|
| 361 |
+
|
| 362 |
+
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
| 363 |
+
backup_file = backup_path / f"resources_backup_{timestamp}.json"
|
| 364 |
+
|
| 365 |
+
self.export_to_json(str(backup_file), include_metadata=True)
|
| 366 |
+
|
| 367 |
+
return str(backup_file)
|
| 368 |
+
|
| 369 |
+
|
| 370 |
+
# تست
|
| 371 |
+
if __name__ == "__main__":
|
| 372 |
+
print("🧪 Testing Resource Manager...\n")
|
| 373 |
+
|
| 374 |
+
manager = ResourceManager()
|
| 375 |
+
|
| 376 |
+
# آمار
|
| 377 |
+
stats = manager.get_statistics()
|
| 378 |
+
print("📊 Statistics:")
|
| 379 |
+
print(json.dumps(stats, indent=2))
|
| 380 |
+
|
| 381 |
+
# Export
|
| 382 |
+
manager.export_to_json("test_export.json")
|
| 383 |
+
manager.export_to_csv("test_export.csv")
|
| 384 |
+
|
| 385 |
+
# Backup
|
| 386 |
+
backup_file = manager.backup()
|
| 387 |
+
print(f"✅ Backup created: {backup_file}")
|
| 388 |
+
|
| 389 |
+
print("\n✅ Resource Manager test completed")
|
| 390 |
+
|
start_server.py
CHANGED
|
@@ -1,19 +1,241 @@
|
|
| 1 |
-
|
| 2 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
|
| 4 |
if __name__ == "__main__":
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
print("API Docs: http://localhost:7860/docs")
|
| 11 |
-
print("=" * 60)
|
| 12 |
-
|
| 13 |
-
uvicorn.run(
|
| 14 |
-
"app:app",
|
| 15 |
-
host="0.0.0.0",
|
| 16 |
-
port=7860,
|
| 17 |
-
log_level="info",
|
| 18 |
-
access_log=True
|
| 19 |
-
)
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
🚀 Crypto Monitor ULTIMATE - Launcher Script
|
| 4 |
+
اسکریپت راهانداز سریع برای سرور
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import sys
|
| 8 |
+
import subprocess
|
| 9 |
+
import os
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def check_dependencies():
|
| 14 |
+
"""بررسی وابستگیهای لازم"""
|
| 15 |
+
print("🔍 بررسی وابستگیها...")
|
| 16 |
+
|
| 17 |
+
required_packages = [
|
| 18 |
+
'fastapi',
|
| 19 |
+
'uvicorn',
|
| 20 |
+
'aiohttp',
|
| 21 |
+
'pydantic'
|
| 22 |
+
]
|
| 23 |
+
|
| 24 |
+
missing = []
|
| 25 |
+
for package in required_packages:
|
| 26 |
+
try:
|
| 27 |
+
__import__(package)
|
| 28 |
+
print(f" ✅ {package}")
|
| 29 |
+
except ImportError:
|
| 30 |
+
missing.append(package)
|
| 31 |
+
print(f" ❌ {package} - نصب نشده")
|
| 32 |
+
|
| 33 |
+
if missing:
|
| 34 |
+
print(f"\n⚠️ {len(missing)} پکیج نصب نشده است!")
|
| 35 |
+
response = input("آیا میخواهید الان نصب شوند? (y/n): ")
|
| 36 |
+
if response.lower() == 'y':
|
| 37 |
+
install_dependencies()
|
| 38 |
+
else:
|
| 39 |
+
print("❌ بدون نصب وابستگیها، سرور نمیتواند اجرا شود.")
|
| 40 |
+
sys.exit(1)
|
| 41 |
+
else:
|
| 42 |
+
print("✅ همه وابستگیها نصب شدهاند\n")
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def install_dependencies():
|
| 46 |
+
"""نصب وابستگیها از requirements.txt"""
|
| 47 |
+
print("\n📦 در حال نصب وابستگیها...")
|
| 48 |
+
try:
|
| 49 |
+
subprocess.check_call([
|
| 50 |
+
sys.executable, "-m", "pip", "install", "-r", "requirements.txt"
|
| 51 |
+
])
|
| 52 |
+
print("✅ همه وابستگیها با موفقیت نصب شدند\n")
|
| 53 |
+
except subprocess.CalledProcessError:
|
| 54 |
+
print("❌ خطا در نصب وابستگیها")
|
| 55 |
+
sys.exit(1)
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def check_config_files():
|
| 59 |
+
"""بررسی فایلهای پیکربندی"""
|
| 60 |
+
print("🔍 بررسی فایلهای پیکربندی...")
|
| 61 |
+
|
| 62 |
+
config_file = Path("providers_config_extended.json")
|
| 63 |
+
if not config_file.exists():
|
| 64 |
+
print(f" ❌ {config_file} یافت نشد!")
|
| 65 |
+
print(" لطفاً این فایل را از مخزن دانلود کنید.")
|
| 66 |
+
sys.exit(1)
|
| 67 |
+
else:
|
| 68 |
+
print(f" ✅ {config_file}")
|
| 69 |
+
|
| 70 |
+
dashboard_file = Path("unified_dashboard.html")
|
| 71 |
+
if not dashboard_file.exists():
|
| 72 |
+
print(f" ⚠️ {dashboard_file} یافت نشد - داشبورد در دسترس نخواهد بود")
|
| 73 |
+
else:
|
| 74 |
+
print(f" ✅ {dashboard_file}")
|
| 75 |
+
|
| 76 |
+
print()
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def show_banner():
|
| 80 |
+
"""نمایش بنر استارت"""
|
| 81 |
+
banner = """
|
| 82 |
+
╔═══════════════════════════════════════════════════════════╗
|
| 83 |
+
║ ║
|
| 84 |
+
║ 🚀 Crypto Monitor ULTIMATE 🚀 ║
|
| 85 |
+
║ ║
|
| 86 |
+
║ نسخه توسعهیافته با ۱۰۰+ ارائهدهنده API رایگان ║
|
| 87 |
+
║ + سیستم پیشرفته Provider Pool Management ║
|
| 88 |
+
║ ║
|
| 89 |
+
║ Version: 2.0.0 ║
|
| 90 |
+
║ Author: Crypto Monitor Team ║
|
| 91 |
+
║ ║
|
| 92 |
+
╚═══════════════════════════════════════════════════════════╝
|
| 93 |
+
"""
|
| 94 |
+
print(banner)
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def show_menu():
|
| 98 |
+
"""نمایش منوی انتخاب"""
|
| 99 |
+
print("\n📋 انتخاب کنید:")
|
| 100 |
+
print(" 1️⃣ اجرای سرور (Production Mode)")
|
| 101 |
+
print(" 2️⃣ اجرای سرور (Development Mode - با Auto Reload)")
|
| 102 |
+
print(" 3️⃣ تست Provider Manager")
|
| 103 |
+
print(" 4️⃣ نمایش آمار ارائهدهندگان")
|
| 104 |
+
print(" 5️⃣ نصب/بروزرسانی وابستگیها")
|
| 105 |
+
print(" 0️⃣ خروج")
|
| 106 |
+
print()
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def run_server_production():
|
| 110 |
+
"""اجرای سرور در حالت Production"""
|
| 111 |
+
print("\n🚀 راهاندازی سرور در حالت Production...")
|
| 112 |
+
print("📡 آدرس: http://localhost:8000")
|
| 113 |
+
print("📊 داشبورد: http://localhost:8000")
|
| 114 |
+
print("📖 API Docs: http://localhost:8000/docs")
|
| 115 |
+
print("\n⏸️ برای توقف سرور Ctrl+C را فشار دهید\n")
|
| 116 |
+
|
| 117 |
+
try:
|
| 118 |
+
subprocess.run([
|
| 119 |
+
sys.executable, "-m", "uvicorn",
|
| 120 |
+
"api_server_extended:app",
|
| 121 |
+
"--host", "0.0.0.0",
|
| 122 |
+
"--port", "8000",
|
| 123 |
+
"--log-level", "info"
|
| 124 |
+
])
|
| 125 |
+
except KeyboardInterrupt:
|
| 126 |
+
print("\n\n🛑 سرور متوقف شد")
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def run_server_development():
|
| 130 |
+
"""اجرای سرور در حالت Development"""
|
| 131 |
+
print("\n🔧 راهاندازی سرور در حالت Development (Auto Reload)...")
|
| 132 |
+
print("📡 آدرس: http://localhost:8000")
|
| 133 |
+
print("📊 داشبورد: http://localhost:8000")
|
| 134 |
+
print("📖 API Docs: http://localhost:8000/docs")
|
| 135 |
+
print("\n⏸️ برای توقف سرور Ctrl+C را فشار دهید")
|
| 136 |
+
print("♻️ تغییرات فایلها بهطور خودکار اعمال میشود\n")
|
| 137 |
+
|
| 138 |
+
try:
|
| 139 |
+
subprocess.run([
|
| 140 |
+
sys.executable, "-m", "uvicorn",
|
| 141 |
+
"api_server_extended:app",
|
| 142 |
+
"--host", "0.0.0.0",
|
| 143 |
+
"--port", "8000",
|
| 144 |
+
"--reload",
|
| 145 |
+
"--log-level", "debug"
|
| 146 |
+
])
|
| 147 |
+
except KeyboardInterrupt:
|
| 148 |
+
print("\n\n🛑 سرور متوقف شد")
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
def test_provider_manager():
|
| 152 |
+
"""تست Provider Manager"""
|
| 153 |
+
print("\n🧪 اجرای تست Provider Manager...\n")
|
| 154 |
+
try:
|
| 155 |
+
subprocess.run([sys.executable, "provider_manager.py"])
|
| 156 |
+
except FileNotFoundError:
|
| 157 |
+
print("❌ فایل provider_manager.py یافت نشد")
|
| 158 |
+
except KeyboardInterrupt:
|
| 159 |
+
print("\n\n🛑 تست متوقف شد")
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
def show_stats():
|
| 163 |
+
"""نمایش آمار ارائهدهندگان"""
|
| 164 |
+
print("\n📊 نمایش آمار ارائهدهندگان...\n")
|
| 165 |
+
try:
|
| 166 |
+
from provider_manager import ProviderManager
|
| 167 |
+
manager = ProviderManager()
|
| 168 |
+
stats = manager.get_all_stats()
|
| 169 |
+
|
| 170 |
+
summary = stats['summary']
|
| 171 |
+
print("=" * 60)
|
| 172 |
+
print(f"📈 آمار کلی سیستم")
|
| 173 |
+
print("=" * 60)
|
| 174 |
+
print(f" کل ارائهدهندگان: {summary['total_providers']}")
|
| 175 |
+
print(f" آنلاین: {summary['online']}")
|
| 176 |
+
print(f" آفلاین: {summary['offline']}")
|
| 177 |
+
print(f" Degraded: {summary['degraded']}")
|
| 178 |
+
print(f" کل درخواستها: {summary['total_requests']}")
|
| 179 |
+
print(f" درخواستهای موفق: {summary['successful_requests']}")
|
| 180 |
+
print(f" نرخ موفقیت: {summary['overall_success_rate']:.2f}%")
|
| 181 |
+
print("=" * 60)
|
| 182 |
+
|
| 183 |
+
print(f"\n🔄 Poolهای موجود: {len(stats['pools'])}")
|
| 184 |
+
for pool_id, pool_data in stats['pools'].items():
|
| 185 |
+
print(f"\n 📦 {pool_data['pool_name']}")
|
| 186 |
+
print(f" دسته: {pool_data['category']}")
|
| 187 |
+
print(f" استراتژی: {pool_data['rotation_strategy']}")
|
| 188 |
+
print(f" اعضا: {pool_data['total_providers']}")
|
| 189 |
+
print(f" در دسترس: {pool_data['available_providers']}")
|
| 190 |
+
|
| 191 |
+
print("\n✅ برای جزئیات بیشتر، سرور را اجرا کرده و به داشبورد مراجعه کنید")
|
| 192 |
+
|
| 193 |
+
except ImportError:
|
| 194 |
+
print("❌ خطا: provider_manager.py یافت نشد یا وابستگیها نصب نشدهاند")
|
| 195 |
+
except Exception as e:
|
| 196 |
+
print(f"❌ خطا: {e}")
|
| 197 |
+
|
| 198 |
+
|
| 199 |
+
def main():
|
| 200 |
+
"""تابع اصلی"""
|
| 201 |
+
show_banner()
|
| 202 |
+
|
| 203 |
+
# بررسی وابستگیها
|
| 204 |
+
check_dependencies()
|
| 205 |
+
|
| 206 |
+
# بررسی فایلهای پیکربندی
|
| 207 |
+
check_config_files()
|
| 208 |
+
|
| 209 |
+
# حلقه منو
|
| 210 |
+
while True:
|
| 211 |
+
show_menu()
|
| 212 |
+
choice = input("انتخاب شما: ").strip()
|
| 213 |
+
|
| 214 |
+
if choice == "1":
|
| 215 |
+
run_server_production()
|
| 216 |
+
break
|
| 217 |
+
elif choice == "2":
|
| 218 |
+
run_server_development()
|
| 219 |
+
break
|
| 220 |
+
elif choice == "3":
|
| 221 |
+
test_provider_manager()
|
| 222 |
+
input("\n⏎ Enter را برای بازگشت به منو فشار دهید...")
|
| 223 |
+
elif choice == "4":
|
| 224 |
+
show_stats()
|
| 225 |
+
input("\n⏎ Enter را برای بازگشت به منو فشار دهید...")
|
| 226 |
+
elif choice == "5":
|
| 227 |
+
install_dependencies()
|
| 228 |
+
input("\n⏎ Enter را برای بازگشت به منو فشار دهید...")
|
| 229 |
+
elif choice == "0":
|
| 230 |
+
print("\n👋 خداحافظ!")
|
| 231 |
+
sys.exit(0)
|
| 232 |
+
else:
|
| 233 |
+
print("\n❌ انتخاب نامعتبر! لطفاً دوباره تلاش کنید.")
|
| 234 |
+
|
| 235 |
|
| 236 |
if __name__ == "__main__":
|
| 237 |
+
try:
|
| 238 |
+
main()
|
| 239 |
+
except KeyboardInterrupt:
|
| 240 |
+
print("\n\n👋 برنامه متوقف شد")
|
| 241 |
+
sys.exit(0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static/css/connection-status.css
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* استایلهای نمایش وضعیت اتصال و کاربران آنلاین
|
| 3 |
+
*/
|
| 4 |
+
|
| 5 |
+
/* === Connection Status Bar === */
|
| 6 |
+
.connection-status-bar {
|
| 7 |
+
position: fixed;
|
| 8 |
+
top: 0;
|
| 9 |
+
left: 0;
|
| 10 |
+
right: 0;
|
| 11 |
+
height: 40px;
|
| 12 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 13 |
+
color: white;
|
| 14 |
+
display: flex;
|
| 15 |
+
align-items: center;
|
| 16 |
+
justify-content: space-between;
|
| 17 |
+
padding: 0 20px;
|
| 18 |
+
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
| 19 |
+
z-index: 9999;
|
| 20 |
+
font-size: 14px;
|
| 21 |
+
transition: all 0.3s ease;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
.connection-status-bar.disconnected {
|
| 25 |
+
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
| 26 |
+
animation: pulse-red 2s infinite;
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
@keyframes pulse-red {
|
| 30 |
+
0%, 100% { opacity: 1; }
|
| 31 |
+
50% { opacity: 0.8; }
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
/* === Status Dot === */
|
| 35 |
+
.status-dot {
|
| 36 |
+
width: 10px;
|
| 37 |
+
height: 10px;
|
| 38 |
+
border-radius: 50%;
|
| 39 |
+
margin-right: 8px;
|
| 40 |
+
display: inline-block;
|
| 41 |
+
position: relative;
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
.status-dot-online {
|
| 45 |
+
background: #4ade80;
|
| 46 |
+
box-shadow: 0 0 10px #4ade80;
|
| 47 |
+
animation: pulse-green 2s infinite;
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
.status-dot-offline {
|
| 51 |
+
background: #f87171;
|
| 52 |
+
box-shadow: 0 0 10px #f87171;
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
@keyframes pulse-green {
|
| 56 |
+
0%, 100% {
|
| 57 |
+
box-shadow: 0 0 10px #4ade80;
|
| 58 |
+
}
|
| 59 |
+
50% {
|
| 60 |
+
box-shadow: 0 0 20px #4ade80, 0 0 30px #4ade80;
|
| 61 |
+
}
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
/* === Online Users Widget === */
|
| 65 |
+
.online-users-widget {
|
| 66 |
+
display: flex;
|
| 67 |
+
align-items: center;
|
| 68 |
+
gap: 15px;
|
| 69 |
+
background: rgba(255, 255, 255, 0.15);
|
| 70 |
+
padding: 5px 15px;
|
| 71 |
+
border-radius: 20px;
|
| 72 |
+
backdrop-filter: blur(10px);
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
.online-users-count {
|
| 76 |
+
display: flex;
|
| 77 |
+
align-items: center;
|
| 78 |
+
gap: 5px;
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
.users-icon {
|
| 82 |
+
font-size: 18px;
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
.count-number {
|
| 86 |
+
font-size: 18px;
|
| 87 |
+
font-weight: bold;
|
| 88 |
+
min-width: 30px;
|
| 89 |
+
text-align: center;
|
| 90 |
+
transition: all 0.3s ease;
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
.count-number.count-updated {
|
| 94 |
+
transform: scale(1.2);
|
| 95 |
+
color: #fbbf24;
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
.count-label {
|
| 99 |
+
font-size: 12px;
|
| 100 |
+
opacity: 0.9;
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
/* === Badge Pulse Animation === */
|
| 104 |
+
.badge.pulse {
|
| 105 |
+
animation: badge-pulse 1s ease;
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
@keyframes badge-pulse {
|
| 109 |
+
0% { transform: scale(1); }
|
| 110 |
+
50% { transform: scale(1.1); }
|
| 111 |
+
100% { transform: scale(1); }
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
/* === Connection Info === */
|
| 115 |
+
.ws-connection-info {
|
| 116 |
+
display: flex;
|
| 117 |
+
align-items: center;
|
| 118 |
+
gap: 10px;
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
.ws-status-text {
|
| 122 |
+
font-weight: 500;
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
/* === Floating Stats Card === */
|
| 126 |
+
.floating-stats-card {
|
| 127 |
+
position: fixed;
|
| 128 |
+
bottom: 20px;
|
| 129 |
+
right: 20px;
|
| 130 |
+
background: white;
|
| 131 |
+
border-radius: 15px;
|
| 132 |
+
box-shadow: 0 10px 40px rgba(0,0,0,0.15);
|
| 133 |
+
padding: 20px;
|
| 134 |
+
min-width: 280px;
|
| 135 |
+
z-index: 9998;
|
| 136 |
+
transition: all 0.3s ease;
|
| 137 |
+
direction: rtl;
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
.floating-stats-card:hover {
|
| 141 |
+
transform: translateY(-5px);
|
| 142 |
+
box-shadow: 0 15px 50px rgba(0,0,0,0.2);
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
.floating-stats-card.minimized {
|
| 146 |
+
padding: 10px;
|
| 147 |
+
min-width: 60px;
|
| 148 |
+
cursor: pointer;
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
.stats-card-header {
|
| 152 |
+
display: flex;
|
| 153 |
+
justify-content: space-between;
|
| 154 |
+
align-items: center;
|
| 155 |
+
margin-bottom: 15px;
|
| 156 |
+
padding-bottom: 10px;
|
| 157 |
+
border-bottom: 2px solid #f3f4f6;
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
.stats-card-title {
|
| 161 |
+
font-size: 16px;
|
| 162 |
+
font-weight: 600;
|
| 163 |
+
color: #1f2937;
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
.minimize-btn {
|
| 167 |
+
background: none;
|
| 168 |
+
border: none;
|
| 169 |
+
font-size: 20px;
|
| 170 |
+
cursor: pointer;
|
| 171 |
+
color: #6b7280;
|
| 172 |
+
transition: transform 0.3s;
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
.minimize-btn:hover {
|
| 176 |
+
transform: rotate(90deg);
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
.stats-grid {
|
| 180 |
+
display: grid;
|
| 181 |
+
grid-template-columns: 1fr 1fr;
|
| 182 |
+
gap: 15px;
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
.stat-item {
|
| 186 |
+
text-align: center;
|
| 187 |
+
padding: 10px;
|
| 188 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 189 |
+
border-radius: 10px;
|
| 190 |
+
color: white;
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
.stat-value {
|
| 194 |
+
font-size: 28px;
|
| 195 |
+
font-weight: bold;
|
| 196 |
+
display: block;
|
| 197 |
+
margin-bottom: 5px;
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
.stat-label {
|
| 201 |
+
font-size: 12px;
|
| 202 |
+
opacity: 0.9;
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
/* === Client Types List === */
|
| 206 |
+
.client-types-list {
|
| 207 |
+
margin-top: 15px;
|
| 208 |
+
padding-top: 15px;
|
| 209 |
+
border-top: 2px solid #f3f4f6;
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
.client-type-item {
|
| 213 |
+
display: flex;
|
| 214 |
+
justify-content: space-between;
|
| 215 |
+
padding: 8px 0;
|
| 216 |
+
border-bottom: 1px solid #f3f4f6;
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
.client-type-item:last-child {
|
| 220 |
+
border-bottom: none;
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
.client-type-name {
|
| 224 |
+
color: #6b7280;
|
| 225 |
+
font-size: 14px;
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
.client-type-count {
|
| 229 |
+
font-weight: 600;
|
| 230 |
+
color: #1f2937;
|
| 231 |
+
background: #f3f4f6;
|
| 232 |
+
padding: 2px 10px;
|
| 233 |
+
border-radius: 12px;
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
/* === Alerts Container === */
|
| 237 |
+
.alerts-container {
|
| 238 |
+
position: fixed;
|
| 239 |
+
top: 50px;
|
| 240 |
+
right: 20px;
|
| 241 |
+
z-index: 9997;
|
| 242 |
+
max-width: 400px;
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
.alert {
|
| 246 |
+
margin-bottom: 10px;
|
| 247 |
+
animation: slideIn 0.3s ease;
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
@keyframes slideIn {
|
| 251 |
+
from {
|
| 252 |
+
transform: translateX(100%);
|
| 253 |
+
opacity: 0;
|
| 254 |
+
}
|
| 255 |
+
to {
|
| 256 |
+
transform: translateX(0);
|
| 257 |
+
opacity: 1;
|
| 258 |
+
}
|
| 259 |
+
}
|
| 260 |
+
|
| 261 |
+
/* === Reconnect Button === */
|
| 262 |
+
.reconnect-btn {
|
| 263 |
+
margin-right: 10px;
|
| 264 |
+
animation: bounce 1s infinite;
|
| 265 |
+
}
|
| 266 |
+
|
| 267 |
+
@keyframes bounce {
|
| 268 |
+
0%, 100% { transform: translateY(0); }
|
| 269 |
+
50% { transform: translateY(-5px); }
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
/* === Loading Spinner === */
|
| 273 |
+
.connection-spinner {
|
| 274 |
+
width: 16px;
|
| 275 |
+
height: 16px;
|
| 276 |
+
border: 2px solid rgba(255,255,255,0.3);
|
| 277 |
+
border-top-color: white;
|
| 278 |
+
border-radius: 50%;
|
| 279 |
+
animation: spin 1s linear infinite;
|
| 280 |
+
margin-right: 8px;
|
| 281 |
+
}
|
| 282 |
+
|
| 283 |
+
@keyframes spin {
|
| 284 |
+
to { transform: rotate(360deg); }
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
/* === Responsive === */
|
| 288 |
+
@media (max-width: 768px) {
|
| 289 |
+
.connection-status-bar {
|
| 290 |
+
font-size: 12px;
|
| 291 |
+
padding: 0 10px;
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
.online-users-widget {
|
| 295 |
+
padding: 3px 10px;
|
| 296 |
+
gap: 8px;
|
| 297 |
+
}
|
| 298 |
+
|
| 299 |
+
.floating-stats-card {
|
| 300 |
+
bottom: 10px;
|
| 301 |
+
right: 10px;
|
| 302 |
+
min-width: 240px;
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
.count-number {
|
| 306 |
+
font-size: 16px;
|
| 307 |
+
}
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
/* === Dark Mode Support === */
|
| 311 |
+
@media (prefers-color-scheme: dark) {
|
| 312 |
+
.floating-stats-card {
|
| 313 |
+
background: #1f2937;
|
| 314 |
+
color: white;
|
| 315 |
+
}
|
| 316 |
+
|
| 317 |
+
.stats-card-title {
|
| 318 |
+
color: white;
|
| 319 |
+
}
|
| 320 |
+
|
| 321 |
+
.client-type-name {
|
| 322 |
+
color: #d1d5db;
|
| 323 |
+
}
|
| 324 |
+
|
| 325 |
+
.client-type-count {
|
| 326 |
+
background: #374151;
|
| 327 |
+
color: white;
|
| 328 |
+
}
|
| 329 |
+
}
|
| 330 |
+
|
static/js/websocket-client.js
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* WebSocket Client برای اتصال بلادرنگ به سرور
|
| 3 |
+
*/
|
| 4 |
+
|
| 5 |
+
class CryptoWebSocketClient {
|
| 6 |
+
constructor(url = null) {
|
| 7 |
+
this.url = url || `ws://${window.location.host}/ws`;
|
| 8 |
+
this.ws = null;
|
| 9 |
+
this.sessionId = null;
|
| 10 |
+
this.isConnected = false;
|
| 11 |
+
this.reconnectAttempts = 0;
|
| 12 |
+
this.maxReconnectAttempts = 5;
|
| 13 |
+
this.reconnectDelay = 3000;
|
| 14 |
+
this.messageHandlers = {};
|
| 15 |
+
this.connectionCallbacks = [];
|
| 16 |
+
|
| 17 |
+
this.connect();
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
connect() {
|
| 21 |
+
try {
|
| 22 |
+
console.log('🔌 اتصال به WebSocket:', this.url);
|
| 23 |
+
this.ws = new WebSocket(this.url);
|
| 24 |
+
|
| 25 |
+
this.ws.onopen = this.onOpen.bind(this);
|
| 26 |
+
this.ws.onmessage = this.onMessage.bind(this);
|
| 27 |
+
this.ws.onerror = this.onError.bind(this);
|
| 28 |
+
this.ws.onclose = this.onClose.bind(this);
|
| 29 |
+
|
| 30 |
+
} catch (error) {
|
| 31 |
+
console.error('❌ خطا در اتصال WebSocket:', error);
|
| 32 |
+
this.scheduleReconnect();
|
| 33 |
+
}
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
onOpen(event) {
|
| 37 |
+
console.log('✅ WebSocket متصل شد');
|
| 38 |
+
this.isConnected = true;
|
| 39 |
+
this.reconnectAttempts = 0;
|
| 40 |
+
|
| 41 |
+
// فراخوانی callbackها
|
| 42 |
+
this.connectionCallbacks.forEach(cb => cb(true));
|
| 43 |
+
|
| 44 |
+
// نمایش وضعیت اتصال
|
| 45 |
+
this.updateConnectionStatus(true);
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
onMessage(event) {
|
| 49 |
+
try {
|
| 50 |
+
const message = JSON.parse(event.data);
|
| 51 |
+
const type = message.type;
|
| 52 |
+
|
| 53 |
+
// مدیریت پیامهای سیستمی
|
| 54 |
+
if (type === 'welcome') {
|
| 55 |
+
this.sessionId = message.session_id;
|
| 56 |
+
console.log('📝 Session ID:', this.sessionId);
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
else if (type === 'stats_update') {
|
| 60 |
+
this.handleStatsUpdate(message.data);
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
else if (type === 'provider_stats') {
|
| 64 |
+
this.handleProviderStats(message.data);
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
else if (type === 'market_update') {
|
| 68 |
+
this.handleMarketUpdate(message.data);
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
else if (type === 'price_update') {
|
| 72 |
+
this.handlePriceUpdate(message.data);
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
else if (type === 'alert') {
|
| 76 |
+
this.handleAlert(message.data);
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
else if (type === 'heartbeat') {
|
| 80 |
+
// پاسخ به heartbeat
|
| 81 |
+
this.send({ type: 'pong' });
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
// فراخوانی handler سفارشی
|
| 85 |
+
if (this.messageHandlers[type]) {
|
| 86 |
+
this.messageHandlers[type](message);
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
} catch (error) {
|
| 90 |
+
console.error('❌ خطا در پردازش پیام:', error);
|
| 91 |
+
}
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
onError(error) {
|
| 95 |
+
console.error('❌ خطای WebSocket:', error);
|
| 96 |
+
this.isConnected = false;
|
| 97 |
+
this.updateConnectionStatus(false);
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
onClose(event) {
|
| 101 |
+
console.log('🔌 WebSocket قطع شد');
|
| 102 |
+
this.isConnected = false;
|
| 103 |
+
this.sessionId = null;
|
| 104 |
+
|
| 105 |
+
this.connectionCallbacks.forEach(cb => cb(false));
|
| 106 |
+
this.updateConnectionStatus(false);
|
| 107 |
+
|
| 108 |
+
// تلاش مجدد برای اتصال
|
| 109 |
+
this.scheduleReconnect();
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
scheduleReconnect() {
|
| 113 |
+
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
| 114 |
+
this.reconnectAttempts++;
|
| 115 |
+
console.log(`🔄 تلاش مجدد برای اتصال (${this.reconnectAttempts}/${this.maxReconnectAttempts})...`);
|
| 116 |
+
|
| 117 |
+
setTimeout(() => {
|
| 118 |
+
this.connect();
|
| 119 |
+
}, this.reconnectDelay);
|
| 120 |
+
} else {
|
| 121 |
+
console.error('❌ تعداد تلاشهای اتصال به پایان رسید');
|
| 122 |
+
this.showReconnectButton();
|
| 123 |
+
}
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
send(data) {
|
| 127 |
+
if (this.isConnected && this.ws.readyState === WebSocket.OPEN) {
|
| 128 |
+
this.ws.send(JSON.stringify(data));
|
| 129 |
+
} else {
|
| 130 |
+
console.warn('⚠️ WebSocket متصل نیست');
|
| 131 |
+
}
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
subscribe(group) {
|
| 135 |
+
this.send({
|
| 136 |
+
type: 'subscribe',
|
| 137 |
+
group: group
|
| 138 |
+
});
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
unsubscribe(group) {
|
| 142 |
+
this.send({
|
| 143 |
+
type: 'unsubscribe',
|
| 144 |
+
group: group
|
| 145 |
+
});
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
requestStats() {
|
| 149 |
+
this.send({
|
| 150 |
+
type: 'get_stats'
|
| 151 |
+
});
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
on(type, handler) {
|
| 155 |
+
this.messageHandlers[type] = handler;
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
onConnection(callback) {
|
| 159 |
+
this.connectionCallbacks.push(callback);
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
// ===== Handlers برای انواع ��یامها =====
|
| 163 |
+
|
| 164 |
+
handleStatsUpdate(data) {
|
| 165 |
+
// بهروزرسانی نمایش تعداد کاربران
|
| 166 |
+
const activeConnections = data.active_connections || 0;
|
| 167 |
+
const totalSessions = data.total_sessions || 0;
|
| 168 |
+
|
| 169 |
+
// بهروزرسانی UI
|
| 170 |
+
this.updateOnlineUsers(activeConnections, totalSessions);
|
| 171 |
+
|
| 172 |
+
// آپدیت سایر آمار
|
| 173 |
+
if (data.client_types) {
|
| 174 |
+
this.updateClientTypes(data.client_types);
|
| 175 |
+
}
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
handleProviderStats(data) {
|
| 179 |
+
// بهروزرسانی آمار Provider
|
| 180 |
+
const summary = data.summary || {};
|
| 181 |
+
|
| 182 |
+
// آپدیت نمایش
|
| 183 |
+
if (window.updateProviderStats) {
|
| 184 |
+
window.updateProviderStats(summary);
|
| 185 |
+
}
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
handleMarketUpdate(data) {
|
| 189 |
+
if (window.updateMarketData) {
|
| 190 |
+
window.updateMarketData(data);
|
| 191 |
+
}
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
handlePriceUpdate(data) {
|
| 195 |
+
if (window.updatePrice) {
|
| 196 |
+
window.updatePrice(data.symbol, data.price, data.change_24h);
|
| 197 |
+
}
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
handleAlert(data) {
|
| 201 |
+
this.showAlert(data.message, data.severity);
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
// ===== UI Updates =====
|
| 205 |
+
|
| 206 |
+
updateConnectionStatus(connected) {
|
| 207 |
+
const statusEl = document.getElementById('ws-connection-status');
|
| 208 |
+
const statusDot = document.getElementById('ws-status-dot');
|
| 209 |
+
const statusText = document.getElementById('ws-status-text');
|
| 210 |
+
|
| 211 |
+
if (statusEl && statusDot && statusText) {
|
| 212 |
+
if (connected) {
|
| 213 |
+
statusDot.className = 'status-dot status-dot-online';
|
| 214 |
+
statusText.textContent = 'متصل';
|
| 215 |
+
statusEl.classList.add('connected');
|
| 216 |
+
statusEl.classList.remove('disconnected');
|
| 217 |
+
} else {
|
| 218 |
+
statusDot.className = 'status-dot status-dot-offline';
|
| 219 |
+
statusText.textContent = 'قطع شده';
|
| 220 |
+
statusEl.classList.add('disconnected');
|
| 221 |
+
statusEl.classList.remove('connected');
|
| 222 |
+
}
|
| 223 |
+
}
|
| 224 |
+
}
|
| 225 |
+
|
| 226 |
+
updateOnlineUsers(active, total) {
|
| 227 |
+
const activeEl = document.getElementById('active-users-count');
|
| 228 |
+
const totalEl = document.getElementById('total-sessions-count');
|
| 229 |
+
const badgeEl = document.getElementById('online-users-badge');
|
| 230 |
+
|
| 231 |
+
if (activeEl) {
|
| 232 |
+
activeEl.textContent = active;
|
| 233 |
+
// انیمیشن تغییر
|
| 234 |
+
activeEl.classList.add('count-updated');
|
| 235 |
+
setTimeout(() => activeEl.classList.remove('count-updated'), 500);
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
if (totalEl) {
|
| 239 |
+
totalEl.textContent = total;
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
if (badgeEl) {
|
| 243 |
+
badgeEl.textContent = active;
|
| 244 |
+
badgeEl.classList.add('pulse');
|
| 245 |
+
setTimeout(() => badgeEl.classList.remove('pulse'), 1000);
|
| 246 |
+
}
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
updateClientTypes(types) {
|
| 250 |
+
const listEl = document.getElementById('client-types-list');
|
| 251 |
+
if (listEl && types) {
|
| 252 |
+
const html = Object.entries(types).map(([type, count]) =>
|
| 253 |
+
`<div class="client-type-item">
|
| 254 |
+
<span class="client-type-name">${type}</span>
|
| 255 |
+
<span class="client-type-count">${count}</span>
|
| 256 |
+
</div>`
|
| 257 |
+
).join('');
|
| 258 |
+
listEl.innerHTML = html;
|
| 259 |
+
}
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
showAlert(message, severity = 'info') {
|
| 263 |
+
// ساخت alert
|
| 264 |
+
const alert = document.createElement('div');
|
| 265 |
+
alert.className = `alert alert-${severity} alert-dismissible fade show`;
|
| 266 |
+
alert.innerHTML = `
|
| 267 |
+
<strong>${severity === 'error' ? '❌' : severity === 'warning' ? '⚠️' : 'ℹ️'}</strong>
|
| 268 |
+
${message}
|
| 269 |
+
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
| 270 |
+
`;
|
| 271 |
+
|
| 272 |
+
const container = document.getElementById('alerts-container') || document.body;
|
| 273 |
+
container.appendChild(alert);
|
| 274 |
+
|
| 275 |
+
// حذف خودکار بعد از 5 ثانیه
|
| 276 |
+
setTimeout(() => {
|
| 277 |
+
alert.classList.remove('show');
|
| 278 |
+
setTimeout(() => alert.remove(), 300);
|
| 279 |
+
}, 5000);
|
| 280 |
+
}
|
| 281 |
+
|
| 282 |
+
showReconnectButton() {
|
| 283 |
+
const button = document.createElement('button');
|
| 284 |
+
button.className = 'btn btn-warning reconnect-btn';
|
| 285 |
+
button.innerHTML = '🔄 اتصال مجدد';
|
| 286 |
+
button.onclick = () => {
|
| 287 |
+
this.reconnectAttempts = 0;
|
| 288 |
+
this.connect();
|
| 289 |
+
button.remove();
|
| 290 |
+
};
|
| 291 |
+
|
| 292 |
+
const statusEl = document.getElementById('ws-connection-status');
|
| 293 |
+
if (statusEl) {
|
| 294 |
+
statusEl.appendChild(button);
|
| 295 |
+
}
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
close() {
|
| 299 |
+
if (this.ws) {
|
| 300 |
+
this.ws.close();
|
| 301 |
+
}
|
| 302 |
+
}
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
// ایجاد instance سراسری
|
| 306 |
+
window.wsClient = null;
|
| 307 |
+
|
| 308 |
+
// اتصال خودکار
|
| 309 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 310 |
+
try {
|
| 311 |
+
window.wsClient = new CryptoWebSocketClient();
|
| 312 |
+
console.log('✅ WebSocket Client آماده است');
|
| 313 |
+
} catch (error) {
|
| 314 |
+
console.error('❌ خطا در راهاندازی WebSocket Client:', error);
|
| 315 |
+
}
|
| 316 |
+
});
|
| 317 |
+
|
templates/index.html
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
test_providers.py
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
🧪 Test Script - تست سریع Provider Manager و Pool System
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import asyncio
|
| 7 |
+
import time
|
| 8 |
+
from provider_manager import ProviderManager, RotationStrategy
|
| 9 |
+
from datetime import datetime
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
async def test_basic_functionality():
|
| 13 |
+
"""تست عملکرد پایه"""
|
| 14 |
+
print("\n" + "=" * 70)
|
| 15 |
+
print("🧪 تست عملکرد پایه Provider Manager")
|
| 16 |
+
print("=" * 70)
|
| 17 |
+
|
| 18 |
+
# ایجاد مدیر
|
| 19 |
+
print("\n📦 ایجاد Provider Manager...")
|
| 20 |
+
manager = ProviderManager()
|
| 21 |
+
|
| 22 |
+
print(f"✅ تعداد کل ارائهدهندگان: {len(manager.providers)}")
|
| 23 |
+
print(f"✅ تعداد کل Poolها: {len(manager.pools)}")
|
| 24 |
+
|
| 25 |
+
# نمایش دستهبندیها
|
| 26 |
+
categories = {}
|
| 27 |
+
for provider in manager.providers.values():
|
| 28 |
+
categories[provider.category] = categories.get(provider.category, 0) + 1
|
| 29 |
+
|
| 30 |
+
print("\n📊 دستهبندی ارائهدهندگان:")
|
| 31 |
+
for category, count in sorted(categories.items()):
|
| 32 |
+
print(f" • {category}: {count} ارائهدهنده")
|
| 33 |
+
|
| 34 |
+
return manager
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
async def test_health_checks(manager):
|
| 38 |
+
"""تست بررسی سلامت"""
|
| 39 |
+
print("\n" + "=" * 70)
|
| 40 |
+
print("🏥 تست بررسی سلامت ارائهدهندگان")
|
| 41 |
+
print("=" * 70)
|
| 42 |
+
|
| 43 |
+
print("\n⏳ در حال بررسی سلامت (ممکن است چند ثانیه طول بکشد)...")
|
| 44 |
+
start_time = time.time()
|
| 45 |
+
|
| 46 |
+
await manager.health_check_all()
|
| 47 |
+
|
| 48 |
+
elapsed = time.time() - start_time
|
| 49 |
+
print(f"✅ بررسی سلامت در {elapsed:.2f} ثانیه تکمیل شد")
|
| 50 |
+
|
| 51 |
+
# آمار
|
| 52 |
+
stats = manager.get_all_stats()
|
| 53 |
+
summary = stats['summary']
|
| 54 |
+
|
| 55 |
+
print(f"\n📊 نتایج:")
|
| 56 |
+
print(f" • آنلاین: {summary['online']} ارائهدهنده")
|
| 57 |
+
print(f" • آفلاین: {summary['offline']} ارائهدهنده")
|
| 58 |
+
print(f" • Degraded: {summary['degraded']} ارائهدهنده")
|
| 59 |
+
|
| 60 |
+
# نمایش چند ارائهدهنده آنلاین
|
| 61 |
+
print("\n✅ برخی از ارائهدهندگان آنلاین:")
|
| 62 |
+
online_count = 0
|
| 63 |
+
for provider_id, provider in manager.providers.items():
|
| 64 |
+
if provider.status.value == "online" and online_count < 5:
|
| 65 |
+
print(f" • {provider.name} - {provider.avg_response_time:.0f}ms")
|
| 66 |
+
online_count += 1
|
| 67 |
+
|
| 68 |
+
# نمایش چند ارائهدهنده آفلاین
|
| 69 |
+
offline_providers = [p for p in manager.providers.values() if p.status.value == "offline"]
|
| 70 |
+
if offline_providers:
|
| 71 |
+
print(f"\n❌ ارائهدهندگان آفلاین ({len(offline_providers)}):")
|
| 72 |
+
for provider in offline_providers[:5]:
|
| 73 |
+
error_msg = provider.last_error or "No error message"
|
| 74 |
+
print(f" • {provider.name} - {error_msg[:50]}")
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
async def test_pool_rotation(manager):
|
| 78 |
+
"""تست چرخش Pool"""
|
| 79 |
+
print("\n" + "=" * 70)
|
| 80 |
+
print("🔄 تست چرخش Pool")
|
| 81 |
+
print("=" * 70)
|
| 82 |
+
|
| 83 |
+
# انتخاب یک Pool
|
| 84 |
+
if not manager.pools:
|
| 85 |
+
print("⚠️ هیچ Poolای یافت نشد")
|
| 86 |
+
return
|
| 87 |
+
|
| 88 |
+
pool_id = list(manager.pools.keys())[0]
|
| 89 |
+
pool = manager.pools[pool_id]
|
| 90 |
+
|
| 91 |
+
print(f"\n📦 Pool انتخاب شده: {pool.pool_name}")
|
| 92 |
+
print(f" دسته: {pool.category}")
|
| 93 |
+
print(f" استراتژی: {pool.rotation_strategy.value}")
|
| 94 |
+
print(f" تعداد اعضا: {len(pool.providers)}")
|
| 95 |
+
|
| 96 |
+
if not pool.providers:
|
| 97 |
+
print("⚠️ Pool خالی است")
|
| 98 |
+
return
|
| 99 |
+
|
| 100 |
+
print(f"\n🔄 تست {pool.rotation_strategy.value} strategy:")
|
| 101 |
+
|
| 102 |
+
for i in range(5):
|
| 103 |
+
provider = pool.get_next_provider()
|
| 104 |
+
if provider:
|
| 105 |
+
print(f" Round {i+1}: {provider.name} (priority={provider.priority}, weight={provider.weight})")
|
| 106 |
+
else:
|
| 107 |
+
print(f" Round {i+1}: هیچ ارائهدهندهای در دسترس نیست")
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
async def test_failover(manager):
|
| 111 |
+
"""تست سیستم Failover"""
|
| 112 |
+
print("\n" + "=" * 70)
|
| 113 |
+
print("🛡️ تست سیستم Failover و Circuit Breaker")
|
| 114 |
+
print("=" * 70)
|
| 115 |
+
|
| 116 |
+
# پیدا کردن یک ارائهدهنده آنلاین
|
| 117 |
+
online_provider = None
|
| 118 |
+
for provider in manager.providers.values():
|
| 119 |
+
if provider.is_available:
|
| 120 |
+
online_provider = provider
|
| 121 |
+
break
|
| 122 |
+
|
| 123 |
+
if not online_provider:
|
| 124 |
+
print("⚠️ هیچ ارائهدهنده آنلاین یافت نشد")
|
| 125 |
+
return
|
| 126 |
+
|
| 127 |
+
print(f"\n🎯 ارائهدهنده انتخابی: {online_provider.name}")
|
| 128 |
+
print(f" وضعیت اولیه: {online_provider.status.value}")
|
| 129 |
+
print(f" خطاهای متوالی: {online_provider.consecutive_failures}")
|
| 130 |
+
print(f" Circuit Breaker: {'باز' if online_provider.circuit_breaker_open else 'بسته'}")
|
| 131 |
+
|
| 132 |
+
print("\n⚠️ شبیهسازی خطا...")
|
| 133 |
+
# شبیهسازی چند خطای متوالی
|
| 134 |
+
for i in range(6):
|
| 135 |
+
online_provider.record_failure(f"Simulated error {i+1}")
|
| 136 |
+
print(f" خطای {i+1} ثبت شد - خطاهای متوالی: {online_provider.consecutive_failures}")
|
| 137 |
+
|
| 138 |
+
if online_provider.circuit_breaker_open:
|
| 139 |
+
print(f" 🛡️ Circuit Breaker باز شد!")
|
| 140 |
+
break
|
| 141 |
+
|
| 142 |
+
print(f"\n📊 وضعیت نهایی:")
|
| 143 |
+
print(f" وضعیت: {online_provider.status.value}")
|
| 144 |
+
print(f" در دسترس: {'خیر' if not online_provider.is_available else 'بله'}")
|
| 145 |
+
print(f" Circuit Breaker: {'باز' if online_provider.circuit_breaker_open else 'بسته'}")
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
async def test_statistics(manager):
|
| 149 |
+
"""تست سیستم آمارگیری"""
|
| 150 |
+
print("\n" + "=" * 70)
|
| 151 |
+
print("📊 تست سیستم آمارگیری")
|
| 152 |
+
print("=" * 70)
|
| 153 |
+
|
| 154 |
+
stats = manager.get_all_stats()
|
| 155 |
+
|
| 156 |
+
print("\n📈 آمار کلی:")
|
| 157 |
+
summary = stats['summary']
|
| 158 |
+
for key, value in summary.items():
|
| 159 |
+
if isinstance(value, float):
|
| 160 |
+
print(f" • {key}: {value:.2f}")
|
| 161 |
+
else:
|
| 162 |
+
print(f" • {key}: {value}")
|
| 163 |
+
|
| 164 |
+
print("\n🔄 آمار Poolها:")
|
| 165 |
+
for pool_id, pool_stats in stats['pools'].items():
|
| 166 |
+
print(f"\n 📦 {pool_stats['pool_name']}")
|
| 167 |
+
print(f" استراتژی: {pool_stats['rotation_strategy']}")
|
| 168 |
+
print(f" کل اعضا: {pool_stats['total_providers']}")
|
| 169 |
+
print(f" در دسترس: {pool_stats['available_providers']}")
|
| 170 |
+
print(f" کل چرخشها: {pool_stats['total_rotations']}")
|
| 171 |
+
|
| 172 |
+
# صادرکردن آمار
|
| 173 |
+
filepath = f"test_stats_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
|
| 174 |
+
manager.export_stats(filepath)
|
| 175 |
+
print(f"\n💾 آمار در {filepath} ذخیره شد")
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
async def test_performance():
|
| 179 |
+
"""تست عملکرد"""
|
| 180 |
+
print("\n" + "=" * 70)
|
| 181 |
+
print("⚡ تست عملکرد")
|
| 182 |
+
print("=" * 70)
|
| 183 |
+
|
| 184 |
+
manager = ProviderManager()
|
| 185 |
+
|
| 186 |
+
# تست سرعت دریافت از Pool
|
| 187 |
+
pool = list(manager.pools.values())[0] if manager.pools else None
|
| 188 |
+
|
| 189 |
+
if pool and pool.providers:
|
| 190 |
+
print(f"\n🔄 تست سرعت چرخش Pool ({pool.pool_name})...")
|
| 191 |
+
|
| 192 |
+
iterations = 1000
|
| 193 |
+
start_time = time.time()
|
| 194 |
+
|
| 195 |
+
for _ in range(iterations):
|
| 196 |
+
provider = pool.get_next_provider()
|
| 197 |
+
|
| 198 |
+
elapsed = time.time() - start_time
|
| 199 |
+
rps = iterations / elapsed
|
| 200 |
+
|
| 201 |
+
print(f"✅ {iterations} چرخش در {elapsed:.3f} ثانیه")
|
| 202 |
+
print(f"⚡ سرعت: {rps:.0f} چرخش در ثانیه")
|
| 203 |
+
|
| 204 |
+
await manager.close_session()
|
| 205 |
+
|
| 206 |
+
|
| 207 |
+
async def run_all_tests():
|
| 208 |
+
"""اجرای همه تستها"""
|
| 209 |
+
print("""
|
| 210 |
+
╔═══════════════════════════════════════════════════════════╗
|
| 211 |
+
║ ║
|
| 212 |
+
║ 🧪 Crypto Monitor - Test Suite 🧪 ║
|
| 213 |
+
║ ║
|
| 214 |
+
╚═══════════════════════════════════════════════════════════╝
|
| 215 |
+
""")
|
| 216 |
+
|
| 217 |
+
manager = await test_basic_functionality()
|
| 218 |
+
|
| 219 |
+
await test_health_checks(manager)
|
| 220 |
+
|
| 221 |
+
await test_pool_rotation(manager)
|
| 222 |
+
|
| 223 |
+
await test_failover(manager)
|
| 224 |
+
|
| 225 |
+
await test_statistics(manager)
|
| 226 |
+
|
| 227 |
+
await test_performance()
|
| 228 |
+
|
| 229 |
+
await manager.close_session()
|
| 230 |
+
|
| 231 |
+
print("\n" + "=" * 70)
|
| 232 |
+
print("✅ همه تستها با موفقیت تکمیل شدند")
|
| 233 |
+
print("=" * 70)
|
| 234 |
+
print("\n💡 برای اجرای سرور:")
|
| 235 |
+
print(" python api_server_extended.py")
|
| 236 |
+
print(" یا")
|
| 237 |
+
print(" python start_server.py")
|
| 238 |
+
print()
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
if __name__ == "__main__":
|
| 242 |
+
try:
|
| 243 |
+
asyncio.run(run_all_tests())
|
| 244 |
+
except KeyboardInterrupt:
|
| 245 |
+
print("\n\n⏸️ تست متوقف شد")
|
| 246 |
+
except Exception as e:
|
| 247 |
+
print(f"\n\n❌ خطا در اجرای تست: {e}")
|
| 248 |
+
import traceback
|
| 249 |
+
traceback.print_exc()
|
| 250 |
+
|
test_websocket.html
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="fa" dir="rtl">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>تست اتصال WebSocket</title>
|
| 7 |
+
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
|
| 8 |
+
<link rel="stylesheet" href="/static/css/connection-status.css">
|
| 9 |
+
<style>
|
| 10 |
+
body {
|
| 11 |
+
padding-top: 50px;
|
| 12 |
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
| 13 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 14 |
+
min-height: 100vh;
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
.main-container {
|
| 18 |
+
max-width: 1200px;
|
| 19 |
+
margin: 0 auto;
|
| 20 |
+
padding: 20px;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
.stats-card {
|
| 24 |
+
background: white;
|
| 25 |
+
border-radius: 15px;
|
| 26 |
+
padding: 30px;
|
| 27 |
+
box-shadow: 0 10px 40px rgba(0,0,0,0.15);
|
| 28 |
+
margin-bottom: 20px;
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
.big-stat {
|
| 32 |
+
text-align: center;
|
| 33 |
+
padding: 30px;
|
| 34 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 35 |
+
border-radius: 15px;
|
| 36 |
+
color: white;
|
| 37 |
+
margin-bottom: 20px;
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
.big-stat-value {
|
| 41 |
+
font-size: 72px;
|
| 42 |
+
font-weight: bold;
|
| 43 |
+
margin: 0;
|
| 44 |
+
animation: scaleIn 0.5s ease;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
.big-stat-label {
|
| 48 |
+
font-size: 20px;
|
| 49 |
+
opacity: 0.9;
|
| 50 |
+
margin-top: 10px;
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
@keyframes scaleIn {
|
| 54 |
+
from { transform: scale(0.5); opacity: 0; }
|
| 55 |
+
to { transform: scale(1); opacity: 1; }
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
.message-log {
|
| 59 |
+
background: #f8f9fa;
|
| 60 |
+
border-radius: 10px;
|
| 61 |
+
padding: 20px;
|
| 62 |
+
max-height: 400px;
|
| 63 |
+
overflow-y: auto;
|
| 64 |
+
font-family: 'Courier New', monospace;
|
| 65 |
+
font-size: 13px;
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
.message-item {
|
| 69 |
+
padding: 8px;
|
| 70 |
+
margin-bottom: 5px;
|
| 71 |
+
border-radius: 5px;
|
| 72 |
+
animation: fadeIn 0.3s ease;
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
.message-sent {
|
| 76 |
+
background: #e3f2fd;
|
| 77 |
+
border-left: 3px solid #2196f3;
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
.message-received {
|
| 81 |
+
background: #e8f5e9;
|
| 82 |
+
border-left: 3px solid #4caf50;
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
@keyframes fadeIn {
|
| 86 |
+
from { opacity: 0; transform: translateY(-10px); }
|
| 87 |
+
to { opacity: 1; transform: translateY(0); }
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
.control-panel {
|
| 91 |
+
display: grid;
|
| 92 |
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
| 93 |
+
gap: 10px;
|
| 94 |
+
margin-top: 20px;
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
.btn-action {
|
| 98 |
+
padding: 12px 20px;
|
| 99 |
+
font-weight: 600;
|
| 100 |
+
border-radius: 8px;
|
| 101 |
+
transition: all 0.3s ease;
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
.btn-action:hover {
|
| 105 |
+
transform: translateY(-2px);
|
| 106 |
+
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
|
| 107 |
+
}
|
| 108 |
+
</style>
|
| 109 |
+
</head>
|
| 110 |
+
<body>
|
| 111 |
+
<!-- Connection Status Bar -->
|
| 112 |
+
<div class="connection-status-bar" id="ws-connection-status">
|
| 113 |
+
<div class="ws-connection-info">
|
| 114 |
+
<span class="status-dot status-dot-offline" id="ws-status-dot"></span>
|
| 115 |
+
<span class="ws-status-text" id="ws-status-text">در حال اتصال...</span>
|
| 116 |
+
<span class="connection-spinner"></span>
|
| 117 |
+
</div>
|
| 118 |
+
|
| 119 |
+
<div class="online-users-widget">
|
| 120 |
+
<div class="online-users-count">
|
| 121 |
+
<span class="users-icon">👥</span>
|
| 122 |
+
<span class="count-number" id="active-users-count">0</span>
|
| 123 |
+
<span class="count-label">کاربر آنلاین</span>
|
| 124 |
+
</div>
|
| 125 |
+
<div class="online-users-count">
|
| 126 |
+
<span class="users-icon">📊</span>
|
| 127 |
+
<span class="count-number" id="total-sessions-count">0</span>
|
| 128 |
+
<span class="count-label">جلسات کل</span>
|
| 129 |
+
</div>
|
| 130 |
+
</div>
|
| 131 |
+
</div>
|
| 132 |
+
|
| 133 |
+
<!-- Alerts Container -->
|
| 134 |
+
<div class="alerts-container" id="alerts-container"></div>
|
| 135 |
+
|
| 136 |
+
<!-- Main Content -->
|
| 137 |
+
<div class="main-container">
|
| 138 |
+
<div class="big-stat">
|
| 139 |
+
<div class="big-stat-value" id="active-users-big">0</div>
|
| 140 |
+
<div class="big-stat-label">کاربر در حال حاضر آنلاین هستند</div>
|
| 141 |
+
</div>
|
| 142 |
+
|
| 143 |
+
<div class="row">
|
| 144 |
+
<div class="col-md-6">
|
| 145 |
+
<div class="stats-card">
|
| 146 |
+
<h3>📊 آمار اتصالات</h3>
|
| 147 |
+
<hr>
|
| 148 |
+
<table class="table">
|
| 149 |
+
<tr>
|
| 150 |
+
<td>اتصالات فعال:</td>
|
| 151 |
+
<td><strong id="stat-active">0</strong></td>
|
| 152 |
+
</tr>
|
| 153 |
+
<tr>
|
| 154 |
+
<td>جلسات کل:</td>
|
| 155 |
+
<td><strong id="stat-total">0</strong></td>
|
| 156 |
+
</tr>
|
| 157 |
+
<tr>
|
| 158 |
+
<td>پیامهای ارسالی:</td>
|
| 159 |
+
<td><strong id="stat-sent">0</strong></td>
|
| 160 |
+
</tr>
|
| 161 |
+
<tr>
|
| 162 |
+
<td>پیامهای دریافتی:</td>
|
| 163 |
+
<td><strong id="stat-received">0</strong></td>
|
| 164 |
+
</tr>
|
| 165 |
+
<tr>
|
| 166 |
+
<td>Session ID:</td>
|
| 167 |
+
<td><code id="session-id">-</code></td>
|
| 168 |
+
</tr>
|
| 169 |
+
</table>
|
| 170 |
+
|
| 171 |
+
<h5>انواع کلاینتها:</h5>
|
| 172 |
+
<div id="client-types-list"></div>
|
| 173 |
+
</div>
|
| 174 |
+
</div>
|
| 175 |
+
|
| 176 |
+
<div class="col-md-6">
|
| 177 |
+
<div class="stats-card">
|
| 178 |
+
<h3>🎮 کنترلها</h3>
|
| 179 |
+
<hr>
|
| 180 |
+
<div class="control-panel">
|
| 181 |
+
<button class="btn btn-primary btn-action" onclick="requestStats()">
|
| 182 |
+
📊 درخواست آمار
|
| 183 |
+
</button>
|
| 184 |
+
<button class="btn btn-success btn-action" onclick="subscribeToMarket()">
|
| 185 |
+
✅ Subscribe به Market
|
| 186 |
+
</button>
|
| 187 |
+
<button class="btn btn-warning btn-action" onclick="unsubscribeFromMarket()">
|
| 188 |
+
❌ Unsubscribe از Market
|
| 189 |
+
</button>
|
| 190 |
+
<button class="btn btn-info btn-action" onclick="sendPing()">
|
| 191 |
+
🏓 ارسال Ping
|
| 192 |
+
</button>
|
| 193 |
+
<button class="btn btn-danger btn-action" onclick="disconnect()">
|
| 194 |
+
🔌 قطع اتصال
|
| 195 |
+
</button>
|
| 196 |
+
<button class="btn btn-secondary btn-action" onclick="reconnect()">
|
| 197 |
+
🔄 اتصال مجدد
|
| 198 |
+
</button>
|
| 199 |
+
</div>
|
| 200 |
+
</div>
|
| 201 |
+
|
| 202 |
+
<div class="stats-card">
|
| 203 |
+
<h3>📝 لاگ پیامها</h3>
|
| 204 |
+
<hr>
|
| 205 |
+
<div class="message-log" id="message-log"></div>
|
| 206 |
+
<button class="btn btn-sm btn-outline-secondary mt-2" onclick="clearLog()">
|
| 207 |
+
🗑️ پاک کردن لاگ
|
| 208 |
+
</button>
|
| 209 |
+
</div>
|
| 210 |
+
</div>
|
| 211 |
+
</div>
|
| 212 |
+
</div>
|
| 213 |
+
|
| 214 |
+
<!-- Scripts -->
|
| 215 |
+
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
|
| 216 |
+
<script src="/static/js/websocket-client.js"></script>
|
| 217 |
+
|
| 218 |
+
<script>
|
| 219 |
+
// تنظیم handler برای Session ID
|
| 220 |
+
if (window.wsClient) {
|
| 221 |
+
window.wsClient.on('welcome', (message) => {
|
| 222 |
+
document.getElementById('session-id').textContent = message.session_id;
|
| 223 |
+
logMessage('دریافت', message);
|
| 224 |
+
});
|
| 225 |
+
|
| 226 |
+
window.wsClient.on('stats_update', (message) => {
|
| 227 |
+
updateStatsDisplay(message.data);
|
| 228 |
+
logMessage('دریافت', message);
|
| 229 |
+
});
|
| 230 |
+
|
| 231 |
+
window.wsClient.on('subscribed', (message) => {
|
| 232 |
+
logMessage('دریافت', message);
|
| 233 |
+
});
|
| 234 |
+
|
| 235 |
+
window.wsClient.on('pong', (message) => {
|
| 236 |
+
logMessage('دریافت', message);
|
| 237 |
+
});
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
// توابع کنترلی
|
| 241 |
+
function requestStats() {
|
| 242 |
+
if (window.wsClient) {
|
| 243 |
+
window.wsClient.requestStats();
|
| 244 |
+
logMessage('ارسال', { type: 'get_stats' });
|
| 245 |
+
}
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
function subscribeToMarket() {
|
| 249 |
+
if (window.wsClient) {
|
| 250 |
+
window.wsClient.subscribe('market');
|
| 251 |
+
logMessage('ارسال', { type: 'subscribe', group: 'market' });
|
| 252 |
+
}
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
function unsubscribeFromMarket() {
|
| 256 |
+
if (window.wsClient) {
|
| 257 |
+
window.wsClient.unsubscribe('market');
|
| 258 |
+
logMessage('ارسال', { type: 'unsubscribe', group: 'market' });
|
| 259 |
+
}
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
function sendPing() {
|
| 263 |
+
if (window.wsClient) {
|
| 264 |
+
window.wsClient.send({ type: 'ping' });
|
| 265 |
+
logMessage('ارسال', { type: 'ping' });
|
| 266 |
+
}
|
| 267 |
+
}
|
| 268 |
+
|
| 269 |
+
function disconnect() {
|
| 270 |
+
if (window.wsClient) {
|
| 271 |
+
window.wsClient.close();
|
| 272 |
+
logMessage('سیستم', 'قطع اتصال توسط کاربر');
|
| 273 |
+
}
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
+
function reconnect() {
|
| 277 |
+
if (window.wsClient) {
|
| 278 |
+
window.wsClient.reconnectAttempts = 0;
|
| 279 |
+
window.wsClient.connect();
|
| 280 |
+
logMessage('سیستم', 'تلاش برای اتصال مجدد');
|
| 281 |
+
}
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
function updateStatsDisplay(data) {
|
| 285 |
+
document.getElementById('stat-active').textContent = data.active_connections || 0;
|
| 286 |
+
document.getElementById('stat-total').textContent = data.total_sessions || 0;
|
| 287 |
+
document.getElementById('stat-sent').textContent = data.messages_sent || 0;
|
| 288 |
+
document.getElementById('stat-received').textContent = data.messages_received || 0;
|
| 289 |
+
|
| 290 |
+
// آپدیت عدد بزرگ
|
| 291 |
+
const bigValue = document.getElementById('active-users-big');
|
| 292 |
+
bigValue.textContent = data.active_connections || 0;
|
| 293 |
+
bigValue.style.animation = 'none';
|
| 294 |
+
setTimeout(() => bigValue.style.animation = 'scaleIn 0.5s ease', 10);
|
| 295 |
+
}
|
| 296 |
+
|
| 297 |
+
function logMessage(direction, message) {
|
| 298 |
+
const log = document.getElementById('message-log');
|
| 299 |
+
const item = document.createElement('div');
|
| 300 |
+
item.className = `message-item ${direction === 'ارسال' ? 'message-sent' : 'message-received'}`;
|
| 301 |
+
|
| 302 |
+
const time = new Date().toLocaleTimeString('fa-IR');
|
| 303 |
+
const content = typeof message === 'string' ? message : JSON.stringify(message, null, 2);
|
| 304 |
+
|
| 305 |
+
item.innerHTML = `
|
| 306 |
+
<strong>[${time}] ${direction}:</strong><br>
|
| 307 |
+
<pre style="margin:5px 0 0 0">${content}</pre>
|
| 308 |
+
`;
|
| 309 |
+
|
| 310 |
+
log.appendChild(item);
|
| 311 |
+
log.scrollTop = log.scrollHeight;
|
| 312 |
+
}
|
| 313 |
+
|
| 314 |
+
function clearLog() {
|
| 315 |
+
document.getElementById('message-log').innerHTML = '';
|
| 316 |
+
}
|
| 317 |
+
|
| 318 |
+
// آپدیت خودکار هر ۵ ثانیه
|
| 319 |
+
setInterval(() => {
|
| 320 |
+
if (window.wsClient && window.wsClient.isConnected) {
|
| 321 |
+
requestStats();
|
| 322 |
+
}
|
| 323 |
+
}, 5000);
|
| 324 |
+
</script>
|
| 325 |
+
</body>
|
| 326 |
+
</html>
|
| 327 |
+
|
test_websocket_dashboard.html
ADDED
|
@@ -0,0 +1,364 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="fa" dir="rtl">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>تست WebSocket - Crypto Monitor</title>
|
| 7 |
+
<link rel="stylesheet" href="/static/css/connection-status.css">
|
| 8 |
+
<style>
|
| 9 |
+
* {
|
| 10 |
+
margin: 0;
|
| 11 |
+
padding: 0;
|
| 12 |
+
box-sizing: border-box;
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
body {
|
| 16 |
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
| 17 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 18 |
+
min-height: 100vh;
|
| 19 |
+
padding: 20px;
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
.container {
|
| 23 |
+
max-width: 1200px;
|
| 24 |
+
margin: 0 auto;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
.card {
|
| 28 |
+
background: white;
|
| 29 |
+
border-radius: 20px;
|
| 30 |
+
padding: 30px;
|
| 31 |
+
margin-bottom: 20px;
|
| 32 |
+
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
h1 {
|
| 36 |
+
color: #1f2937;
|
| 37 |
+
margin-bottom: 10px;
|
| 38 |
+
font-size: 32px;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
h2 {
|
| 42 |
+
color: #4b5563;
|
| 43 |
+
margin-bottom: 20px;
|
| 44 |
+
font-size: 24px;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
.status-grid {
|
| 48 |
+
display: grid;
|
| 49 |
+
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
| 50 |
+
gap: 20px;
|
| 51 |
+
margin-bottom: 30px;
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
.status-card {
|
| 55 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 56 |
+
color: white;
|
| 57 |
+
padding: 20px;
|
| 58 |
+
border-radius: 15px;
|
| 59 |
+
text-align: center;
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
.status-value {
|
| 63 |
+
font-size: 48px;
|
| 64 |
+
font-weight: bold;
|
| 65 |
+
margin: 10px 0;
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
.status-label {
|
| 69 |
+
font-size: 14px;
|
| 70 |
+
opacity: 0.9;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
.log-container {
|
| 74 |
+
background: #1e293b;
|
| 75 |
+
border-radius: 10px;
|
| 76 |
+
padding: 20px;
|
| 77 |
+
max-height: 400px;
|
| 78 |
+
overflow-y: auto;
|
| 79 |
+
font-family: 'Courier New', monospace;
|
| 80 |
+
font-size: 13px;
|
| 81 |
+
color: #e2e8f0;
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
.log-entry {
|
| 85 |
+
padding: 8px;
|
| 86 |
+
margin-bottom: 5px;
|
| 87 |
+
border-left: 3px solid #3b82f6;
|
| 88 |
+
background: rgba(59, 130, 246, 0.1);
|
| 89 |
+
border-radius: 4px;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
.log-time {
|
| 93 |
+
color: #94a3b8;
|
| 94 |
+
margin-left: 10px;
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
.btn {
|
| 98 |
+
padding: 12px 24px;
|
| 99 |
+
border: none;
|
| 100 |
+
border-radius: 10px;
|
| 101 |
+
font-size: 16px;
|
| 102 |
+
font-weight: 600;
|
| 103 |
+
cursor: pointer;
|
| 104 |
+
transition: all 0.3s;
|
| 105 |
+
margin: 5px;
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
.btn-primary {
|
| 109 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 110 |
+
color: white;
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
.btn-primary:hover {
|
| 114 |
+
transform: translateY(-2px);
|
| 115 |
+
box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4);
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
.btn-success {
|
| 119 |
+
background: #10b981;
|
| 120 |
+
color: white;
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
.btn-danger {
|
| 124 |
+
background: #ef4444;
|
| 125 |
+
color: white;
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
.controls {
|
| 129 |
+
display: flex;
|
| 130 |
+
gap: 10px;
|
| 131 |
+
flex-wrap: wrap;
|
| 132 |
+
margin-bottom: 20px;
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
.pulse {
|
| 136 |
+
animation: pulse 2s infinite;
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
@keyframes pulse {
|
| 140 |
+
0%, 100% {
|
| 141 |
+
opacity: 1;
|
| 142 |
+
transform: scale(1);
|
| 143 |
+
}
|
| 144 |
+
50% {
|
| 145 |
+
opacity: 0.7;
|
| 146 |
+
transform: scale(1.05);
|
| 147 |
+
}
|
| 148 |
+
}
|
| 149 |
+
</style>
|
| 150 |
+
</head>
|
| 151 |
+
<body>
|
| 152 |
+
<!-- WebSocket Status Indicator -->
|
| 153 |
+
<div id="ws-connection-status" class="ws-status-indicator disconnected">
|
| 154 |
+
<div id="ws-status-dot" class="status-dot status-dot-offline"></div>
|
| 155 |
+
<span id="ws-status-text" class="ws-status-text">در حال اتصال...</span>
|
| 156 |
+
<div id="online-users-badge" class="badge badge-info" style="margin-right: 10px;">0</div>
|
| 157 |
+
</div>
|
| 158 |
+
|
| 159 |
+
<div class="container">
|
| 160 |
+
<div class="card">
|
| 161 |
+
<h1>🚀 تست WebSocket - Crypto Monitor</h1>
|
| 162 |
+
<p style="color: #6b7280; margin-bottom: 20px;">
|
| 163 |
+
این صفحه برای تست اتصال WebSocket و نمایش آمار بلادرنگ طراحی شده است.
|
| 164 |
+
</p>
|
| 165 |
+
|
| 166 |
+
<div class="status-grid">
|
| 167 |
+
<div class="status-card">
|
| 168 |
+
<div class="status-label">کاربران آنلاین</div>
|
| 169 |
+
<div class="status-value pulse" id="active-users-count">0</div>
|
| 170 |
+
</div>
|
| 171 |
+
<div class="status-card">
|
| 172 |
+
<div class="status-label">کل نشستها</div>
|
| 173 |
+
<div class="status-value" id="total-sessions-count">0</div>
|
| 174 |
+
</div>
|
| 175 |
+
<div class="status-card">
|
| 176 |
+
<div class="status-label">پیامهای دریافتی</div>
|
| 177 |
+
<div class="status-value" id="messages-received">0</div>
|
| 178 |
+
</div>
|
| 179 |
+
<div class="status-card">
|
| 180 |
+
<div class="status-label">پیامهای ارسالی</div>
|
| 181 |
+
<div class="status-value" id="messages-sent">0</div>
|
| 182 |
+
</div>
|
| 183 |
+
</div>
|
| 184 |
+
|
| 185 |
+
<div class="controls">
|
| 186 |
+
<button class="btn btn-primary" onclick="requestStats()">📊 درخواست آمار</button>
|
| 187 |
+
<button class="btn btn-success" onclick="sendPing()">🏓 Ping</button>
|
| 188 |
+
<button class="btn btn-primary" onclick="subscribe('market')">📈 Subscribe Market</button>
|
| 189 |
+
<button class="btn btn-danger" onclick="clearLogs()">🗑️ پاک کردن لاگ</button>
|
| 190 |
+
</div>
|
| 191 |
+
</div>
|
| 192 |
+
|
| 193 |
+
<div class="card">
|
| 194 |
+
<h2>📋 لاگ رویدادها</h2>
|
| 195 |
+
<div id="log-container" class="log-container">
|
| 196 |
+
<div class="log-entry">
|
| 197 |
+
<span class="log-time">[--:--:--]</span>
|
| 198 |
+
در انتظار اتصال WebSocket...
|
| 199 |
+
</div>
|
| 200 |
+
</div>
|
| 201 |
+
</div>
|
| 202 |
+
|
| 203 |
+
<div class="card">
|
| 204 |
+
<h2>📊 اطلاعات Session</h2>
|
| 205 |
+
<div id="session-info" style="font-family: monospace; background: #f3f4f6; padding: 15px; border-radius: 8px;">
|
| 206 |
+
<strong>Session ID:</strong> <span id="session-id">-</span><br>
|
| 207 |
+
<strong>وضعیت اتصال:</strong> <span id="connection-status">قطع شده</span><br>
|
| 208 |
+
<strong>تلاشهای اتصال:</strong> <span id="reconnect-attempts">0</span>
|
| 209 |
+
</div>
|
| 210 |
+
</div>
|
| 211 |
+
</div>
|
| 212 |
+
|
| 213 |
+
<script src="/static/js/websocket-client.js"></script>
|
| 214 |
+
<script>
|
| 215 |
+
let messageCount = 0;
|
| 216 |
+
let sentCount = 0;
|
| 217 |
+
|
| 218 |
+
// منتظر بمانیم تا WebSocket Client آماده شود
|
| 219 |
+
setTimeout(() => {
|
| 220 |
+
if (window.wsClient) {
|
| 221 |
+
setupWebSocketHandlers();
|
| 222 |
+
} else {
|
| 223 |
+
addLog('❌ خطا: WebSocket Client آماده نیست');
|
| 224 |
+
}
|
| 225 |
+
}, 1000);
|
| 226 |
+
|
| 227 |
+
function setupWebSocketHandlers() {
|
| 228 |
+
addLog('✅ WebSocket Client آماده شد');
|
| 229 |
+
|
| 230 |
+
// Session ID
|
| 231 |
+
window.wsClient.onConnection((connected) => {
|
| 232 |
+
document.getElementById('connection-status').textContent = connected ? 'متصل ✅' : 'قطع شده ❌';
|
| 233 |
+
document.getElementById('reconnect-attempts').textContent = window.wsClient.reconnectAttempts;
|
| 234 |
+
});
|
| 235 |
+
|
| 236 |
+
// دریافت پیام welcome
|
| 237 |
+
window.wsClient.on('welcome', (message) => {
|
| 238 |
+
addLog(`🎉 خوش آمدید! Session ID: ${message.session_id}`);
|
| 239 |
+
document.getElementById('session-id').textContent = message.session_id;
|
| 240 |
+
messageCount++;
|
| 241 |
+
updateMessageCount();
|
| 242 |
+
});
|
| 243 |
+
|
| 244 |
+
// دریافت آمار
|
| 245 |
+
window.wsClient.on('stats_update', (message) => {
|
| 246 |
+
addLog('📊 آمار جدید دریافت شد');
|
| 247 |
+
const data = message.data;
|
| 248 |
+
|
| 249 |
+
if (data.active_connections !== undefined) {
|
| 250 |
+
document.getElementById('active-users-count').textContent = data.active_connections;
|
| 251 |
+
}
|
| 252 |
+
if (data.total_sessions !== undefined) {
|
| 253 |
+
document.getElementById('total-sessions-count').textContent = data.total_sessions;
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
messageCount++;
|
| 257 |
+
updateMessageCount();
|
| 258 |
+
});
|
| 259 |
+
|
| 260 |
+
// پاسخ به آمار
|
| 261 |
+
window.wsClient.on('stats_response', (message) => {
|
| 262 |
+
addLog('📡 پاسخ آمار Provider دریافت شد');
|
| 263 |
+
console.log('Provider Stats:', message.data);
|
| 264 |
+
messageCount++;
|
| 265 |
+
updateMessageCount();
|
| 266 |
+
});
|
| 267 |
+
|
| 268 |
+
// پاسخ pong
|
| 269 |
+
window.wsClient.on('pong', (message) => {
|
| 270 |
+
addLog('🏓 Pong دریافت شد');
|
| 271 |
+
messageCount++;
|
| 272 |
+
updateMessageCount();
|
| 273 |
+
});
|
| 274 |
+
|
| 275 |
+
// Subscribe
|
| 276 |
+
window.wsClient.on('subscribed', (message) => {
|
| 277 |
+
addLog(`✅ Subscribe شد به: ${message.group}`);
|
| 278 |
+
messageCount++;
|
| 279 |
+
updateMessageCount();
|
| 280 |
+
});
|
| 281 |
+
|
| 282 |
+
// Provider Stats
|
| 283 |
+
window.wsClient.on('provider_stats', (message) => {
|
| 284 |
+
addLog('📡 آمار Provider بهروز شد');
|
| 285 |
+
messageCount++;
|
| 286 |
+
updateMessageCount();
|
| 287 |
+
});
|
| 288 |
+
|
| 289 |
+
// در��واست اولیه آمار
|
| 290 |
+
setTimeout(() => {
|
| 291 |
+
requestStats();
|
| 292 |
+
}, 2000);
|
| 293 |
+
}
|
| 294 |
+
|
| 295 |
+
function requestStats() {
|
| 296 |
+
if (window.wsClient && window.wsClient.isConnected) {
|
| 297 |
+
window.wsClient.requestStats();
|
| 298 |
+
addLog('📤 درخواست آمار ارسال شد');
|
| 299 |
+
sentCount++;
|
| 300 |
+
updateSentCount();
|
| 301 |
+
} else {
|
| 302 |
+
addLog('⚠️ WebSocket متصل نیست');
|
| 303 |
+
}
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
function sendPing() {
|
| 307 |
+
if (window.wsClient && window.wsClient.isConnected) {
|
| 308 |
+
window.wsClient.send({ type: 'ping' });
|
| 309 |
+
addLog('📤 Ping ارسال شد');
|
| 310 |
+
sentCount++;
|
| 311 |
+
updateSentCount();
|
| 312 |
+
} else {
|
| 313 |
+
addLog('⚠️ WebSocket متصل نیست');
|
| 314 |
+
}
|
| 315 |
+
}
|
| 316 |
+
|
| 317 |
+
function subscribe(group) {
|
| 318 |
+
if (window.wsClient && window.wsClient.isConnected) {
|
| 319 |
+
window.wsClient.subscribe(group);
|
| 320 |
+
addLog(`📤 درخواست Subscribe به ${group} ارسال شد`);
|
| 321 |
+
sentCount++;
|
| 322 |
+
updateSentCount();
|
| 323 |
+
} else {
|
| 324 |
+
addLog('⚠️ WebSocket متصل نیست');
|
| 325 |
+
}
|
| 326 |
+
}
|
| 327 |
+
|
| 328 |
+
function addLog(message) {
|
| 329 |
+
const container = document.getElementById('log-container');
|
| 330 |
+
const time = new Date().toLocaleTimeString('fa-IR');
|
| 331 |
+
const entry = document.createElement('div');
|
| 332 |
+
entry.className = 'log-entry';
|
| 333 |
+
entry.innerHTML = `<span class="log-time">[${time}]</span> ${message}`;
|
| 334 |
+
container.insertBefore(entry, container.firstChild);
|
| 335 |
+
|
| 336 |
+
// حداکثر 50 لاگ نگه دار
|
| 337 |
+
while (container.children.length > 50) {
|
| 338 |
+
container.removeChild(container.lastChild);
|
| 339 |
+
}
|
| 340 |
+
}
|
| 341 |
+
|
| 342 |
+
function clearLogs() {
|
| 343 |
+
document.getElementById('log-container').innerHTML = '';
|
| 344 |
+
addLog('🗑️ لاگها پاک شدند');
|
| 345 |
+
}
|
| 346 |
+
|
| 347 |
+
function updateMessageCount() {
|
| 348 |
+
document.getElementById('messages-received').textContent = messageCount;
|
| 349 |
+
}
|
| 350 |
+
|
| 351 |
+
function updateSentCount() {
|
| 352 |
+
document.getElementById('messages-sent').textContent = sentCount;
|
| 353 |
+
}
|
| 354 |
+
|
| 355 |
+
// درخواست خودکار آمار هر 10 ثانیه
|
| 356 |
+
setInterval(() => {
|
| 357 |
+
if (window.wsClient && window.wsClient.isConnected) {
|
| 358 |
+
requestStats();
|
| 359 |
+
}
|
| 360 |
+
}, 10000);
|
| 361 |
+
</script>
|
| 362 |
+
</body>
|
| 363 |
+
</html>
|
| 364 |
+
|
unified_dashboard.html
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
utils/__pycache__/__init__.cpython-313.pyc
CHANGED
|
Binary files a/utils/__pycache__/__init__.cpython-313.pyc and b/utils/__pycache__/__init__.cpython-313.pyc differ
|
|
|
utils/__pycache__/logger.cpython-313.pyc
CHANGED
|
Binary files a/utils/__pycache__/logger.cpython-313.pyc and b/utils/__pycache__/logger.cpython-313.pyc differ
|
|
|