diff --git a/README.md b/README.md
index f7feafa..f0ea37e 100644
--- a/README.md
+++ b/README.md
@@ -1,51 +1,580 @@
-## 核云IDC服务商VPS自动监测重启程序
+# VPS Hub - 多平台VPS监控与管理系统
-### API接口信息
+
+
-- **获取登录Token**
- `https://www.heyunidc.cn/v1/login_api?account={phone number/email acc}&password={API KEY}`
- 备注:POST方法
+**一个强大的多平台VPS监控、管理和自动重启系统**
-- **获取VPS列表**
- `https://www.heyunidc.cn/v1/hosts?page=1&limit=100`
- 备注:GET方法,使用"Authorization: JWT {Token}"进行认证
- 注意:若您的VPS数量超过100,需要更改limit数量
+[功能特性](#-功能特性) • [快速开始](#-快速开始) • [架构设计](#-架构设计) • [API文档](#-api文档) • [常见问题](#-常见问题)
-- **获取VPS状态**
- `https://www.heyunidc.cn/v1/hosts/{id}/module/status?type=host`
- 备注:GET方法,使用"Authorization: JWT {Token}"进行认证
+
-- **操作VPS**
- `PUT https://www.heyunidc.cn/v1/hosts/:id/module/hard_reboot`
- 备注:PUT方法,使用"Authorization: JWT {Token}"进行认证
- 操作类型:hard_reboot - 硬重启,on - 开机,off - 关机,reboot - 重启
+---
-### 使用方法
+## 📋 目录
-#### 获取API_KEY
-如图所示:
+- [功能特性](#-功能特性)
+- [系统架构](#-系统架构)
+- [技术栈](#-技术栈)
+- [快速开始](#-快速开始)
+- [配置说明](#-配置说明)
+- [使用指南](#-使用指南)
+- [数据库设计](#-数据库设计)
+- [API文档](#-api文档)
+- [监控服务](#-监控服务)
+- [安全说明](#-安全说明)
+- [常见问题](#-常见问题)
+- [更新日志](#-更新日志)
+- [许可证](#-许可证)
+
+---
+
+## ✨ 功能特性
+
+### 🎯 核心功能
+
+- **多平台支持**:支持魔方平台(智简魔方)、阿里云、腾讯云等主流VPS平台
+- **实时监控**:每5分钟自动Ping检测所有VPS的可用性
+- **自动开机**:检测到VPS关机时自动执行开机操作
+- **状态管理**:实时显示VPS的运行状态(开机/关机/未知)
+- **详细信息**:展示CPU、内存、磁盘、带宽、操作系统等配置信息
+
+### 🔧 管理功能
+
+- **Web管理面板**:美观的响应式Web界面
+- **手动刷新**:一键刷新VPS列表和详细信息
+- **远程控制**:支持开机、关机、硬重启操作
+- **配置管理**:轻松添加和管理多个VPS平台配置
+- **统计信息**:实时统计VPS总数、运行中数量、已关机数量
+
+### 📊 监控功能
+
+- **Ping检测**:定期检测VPS网络可达性
+- **延迟记录**:记录每次Ping的延迟时间
+- **状态持久化**:将VPS状态保存到SQLite数据库
+- **历史数据**:保留30天的Ping历史记录
+- **每日摘要**:每天0点生成前一天的可用性统计报告
+
+### 🚀 智能优化
+
+- **API调用优化**:Ping成功时不调用API,减少不必要的请求
+- **Token缓存**:JWT Token缓存2小时,避免频繁登录
+- **动态更新**:只更新实际变化的字段,避免数据覆盖
+- **防重复机制**:使用唯一约束防止重复数据
+- **错误重试**:开机失败后自动重试并记录真实状态
+
+---
+
+## 🏗️ 系统架构
+
+```
+┌─────────────────────────────────────────────────────┐
+│ Web前端 (index.php) │
+│ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │
+│ │ VPS列表 │ │ 操作控制 │ │ 统计信息展示 │ │
+│ └──────────┘ └──────────┘ └──────────────────┘ │
+└──────────────────────┬──────────────────────────────┘
+ │ HTTP POST/GET
+┌──────────────────────▼──────────────────────────────┐
+│ PHP后端 (mofangidc.php) │
+│ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │
+│ │ API封装 │ │Token管理 │ │ 数据库操作 │ │
+│ └──────────┘ └──────────┘ └──────────────────┘ │
+└──────────────────────┬──────────────────────────────┘
+ │ SQLite
+┌──────────────────────▼──────────────────────────────┐
+│ 数据库层 (SQLite) │
+│ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │
+│ │ vps.db │ │vpslist.db│ │ status.db │ │
+│ │(配置) │ │ (VPS列表)│ │ (监控记录) │ │
+│ └──────────┘ └──────────┘ └──────────────────┘ │
+└──────────────────────┬──────────────────────────────┘
+ │
+┌──────────────────────▼──────────────────────────────┐
+│ Python监控服务 (monitor.py) │
+│ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │
+│ │ Ping检测 │ │ API调用 │ │ 定时任务 │ │
+│ └──────────┘ └──────────┘ └──────────────────┘ │
+└─────────────────────────────────────────────────────┘
+```
+
+### 架构特点
+
+1. **前后端分离**:PHP负责Web界面和API调用,Python负责后台监控
+2. **多数据库设计**:三个独立的SQLite数据库,职责清晰
+3. **混合架构**:PHP处理用户交互,Python处理定时任务
+4. **适配器模式**:支持多平台扩展,易于添加新平台
+
+---
+
+## 💻 技术栈
+
+### 后端
+- **PHP 7.4+**:Web服务器端逻辑
+- **Python 3.6+**:后台监控服务
+- **SQLite 3**:轻量级数据库
+
+### 前端
+- **HTML5/CSS3**:页面结构和样式
+- **JavaScript**:交互逻辑
+- **响应式设计**:支持移动端访问
+
+### 依赖库
+- **PHP**: cURL, PDO-SQLite
+- **Python**: requests, schedule, sqlite3
+
+---
+
+## 🚀 快速开始
+
+### 环境要求
+
+- PHP 7.4 或更高版本
+- Python 3.6 或更高版本
+- SQLite 3
+- Linux/Windows/macOS
+
+### 安装步骤
+
+#### 1. 克隆项目
+
+```bash
+git clone https://git.masonliu.com/MasonLiu/VPSHUB.git
+cd VPSHUB
+```
+
+#### 2. 配置Web
+
+直接将VPSHUB文件夹下的所有文件放置于网站路径下即可
+
+**温馨提示**:
+
+请将app路径设置为禁止外网访问(或添加Basic认证)。
+
+#### 3. 一键快速启动
+
+```bash
+chmod 755 app/install.sh
+./app/install.sh
+```
+
+#### 4. 访问Web界面
+
+浏览器访问:`http://your-domain.com/`
+
+首次访问会自动跳转到配置页面,添加第一个VPS平台配置。
+
+---
+
+## ⚙️ 配置说明
+
+### 添加VPS平台配置
+
+1. 访问 `config_add.php?pass=YOUR_API_PASS`
+2. 填写配置信息:
+ - **API标识**:自定义名称(唯一)
+ - **网站类型**:选择平台(目前支持魔方平台)
+ - **网站链接**:API根域名(不要包含路径)
+ - **账户**:登录账号
+ - **API密钥**:登录密码或API Key
+ - **自动监控**:是否启用自动监控
+
+3. 点击"保存配置"
+
+### 配置示例
+
+**魔方平台配置**:
+```
+API标识: 核云IDC
+网站类型: 魔方平台
+网站链接: https://www.heyunidc.cn
+账户: 邮箱或电话号码
+API密钥: your_api_key
+自动监控: ✓
+```

-#### Web端
-将web目录部分部署到服务器上,分配域名或直接IP访问即可
+⚠️ **重要提示**:
+- 网站链接只填写根域名,不要包含 `/v1` 或 `/api` 等路径
+- API标识必须唯一,不能重复
+- 确保账户和密码正确
-首次使用会进入配置页,依次输入API_PASS,ACCOUNT,API_KEY(其中API_PASS是您自定义的网站访问密码,支持大小写字母以及数字)
+---
-随后访问`http://example.com/?pass=API_PASS`即可
+## 📖 使用指南
-切记:切勿将app部分放置于网站目录下
+### Web管理面板
-#### 监控端
+#### 查看VPS列表
-**安装:**
-1. chmod +x ./install.sh
-2. ./install.sh
+访问 `index.php?pass=YOUR_API_PASS`,可以看到:
+- 按配置分组的VPS列表
+- 每个VPS的状态、IP、配置信息
+- 实时统计数据
-系统将自动注册名为idc-monitor的system服务
+#### 操作VPS
-注意:若您的服务器必须要求禁ping,程序可能无法正常运行,在安装脚本前,请手动将config.yml中的WAY改为http,然后再启动安装脚本
+每个VPS卡片提供三个操作按钮:
+- **⚡ 开机**:启动VPS
+- **🔴 关机**:关闭VPS
+- **🔄 硬重启**:强制重启VPS
-**卸载:**
-1. chmod +x ./uninstall.sh
-2. ./uninstall.sh
+操作后会显示成功/失败提示,5秒后自动消失。
+
+#### 刷新VPS列表
+
+点击顶部工具栏的 "🔄 手动刷新VPS列表" 按钮:
+1. 从API获取最新的VPS列表
+2. 更新数据库中的VPS信息
+3. 获取每个VPS的详细配置(CPU、内存、磁盘、带宽、系统)
+
+### 监控服务
+
+#### 启动监控
+
+```bash
+sudo systemctl start idc_monitor
+```
+
+#### 查看状态
+
+```bash
+sudo systemctl status idc_monitor
+```
+
+
+#### 停止监控
+
+```bash
+sudo systemctl stop idc_monitor
+```
+
+#### 重启监控
+
+```bash
+sudo systemctl restart idc_monitor
+```
+
+### 监控流程
+
+```
+每5分钟执行一次监控循环
+ ↓
+1. Ping所有VPS
+ ├─ Ping成功 → 标记status='on',不调用API
+ └─ Ping失败 → 标记为abnormal,等待下一步
+ ↓
+2. 检查Ping失败的VPS
+ ├─ 全部正常 → 跳过
+ └─ 有失败的 → 调用API查询真实状态
+ ├─ 状态为'on' → 可能是禁Ping,更新数据库
+ └─ 状态为'off' → 执行开机操作
+ ├─ 等待60秒
+ └─ 验证开机结果(最多重试2次)
+ ↓
+3. 清理30天前的旧数据
+ ↓
+等待5分钟后下一次循环
+```
+
+---
+
+## 🗄️ 数据库设计
+
+### 数据库文件
+
+| 文件名 | 用途 | 主要表 |
+|--------|------|--------|
+| `vps.db` | 存储平台配置 | `configs` |
+| `vpslist.db` | 存储VPS列表 | `vps_list` |
+| `status.db` | 存储监控记录 | `ping_status`, `vps_summary` |
+
+### 表结构
+
+#### configs 表 (vps.db)
+
+```sql
+CREATE TABLE configs (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ api_label TEXT NOT NULL UNIQUE, -- API标识
+ site_type TEXT NOT NULL, -- 平台类型
+ site_url TEXT, -- API地址
+ account TEXT NOT NULL, -- 账户
+ api_key TEXT NOT NULL, -- API密钥
+ auto_monitor BOOLEAN DEFAULT 1, -- 是否自动监控
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
+```
+
+#### vps_list 表 (vpslist.db)
+
+```sql
+CREATE TABLE vps_list (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ config_id INTEGER NOT NULL, -- 配置ID
+ vps_id INTEGER NOT NULL, -- VPS平台ID
+ domain TEXT, -- 域名
+ ip_address TEXT, -- IP地址
+ product_name TEXT, -- 产品名称
+ cpu_cores INTEGER, -- CPU核数
+ memory_size TEXT, -- 内存大小
+ disk_size TEXT, -- 磁盘大小
+ bandwidth TEXT, -- 带宽
+ os_type TEXT, -- 操作系统
+ status TEXT, -- 状态(on/off/unknown)
+ section BOOLEAN DEFAULT 0, -- 是否监控
+ last_check TIMESTAMP, -- 最后检查时间
+ FOREIGN KEY (config_id) REFERENCES configs(id),
+ UNIQUE(config_id, vps_id)
+);
+```
+
+#### ping_status 表 (status.db)
+
+```sql
+CREATE TABLE ping_status (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ vps_id INTEGER NOT NULL, -- VPS ID
+ target TEXT NOT NULL, -- 目标(IP或域名)
+ status TEXT NOT NULL, -- 状态(normal/abnormal)
+ latency_ms REAL, -- 延迟(ms)
+ check_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
+```
+
+#### vps_summary 表 (status.db)
+
+```sql
+CREATE TABLE vps_summary (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ vps_id INTEGER NOT NULL, -- VPS ID
+ date DATE NOT NULL, -- 日期
+ avg_latency_ms REAL, -- 平均延迟
+ max_latency_ms REAL, -- 最大延迟
+ min_latency_ms REAL, -- 最小延迟
+ count_under_100 INTEGER, -- <100ms次数
+ count_100_to_300 INTEGER, -- 100-300ms次数
+ count_300_to_500 INTEGER, -- 300-500ms次数
+ count_abnormal INTEGER, -- 异常次数
+ availability TEXT, -- 可用性评分
+ UNIQUE(vps_id, date)
+);
+```
+
+---
+
+## 📡 API文档
+
+### 魔方平台API
+
+- `POST /login_api?account={acc}&password={api key}` - 登录获取JWT Token
+
+以下接口通过请求头进行认证
+Authorization: JWT {token}
+
+- `GET /hosts?page=&limit=` - 获取VPS列表
+- `GET /hosts/{id}` - 获取VPS详情
+- `GET /hosts/{id}/module/status?type=host` - 获取VPS状态
+- `PUT /hosts/{id}/module/on` - 开机
+- `PUT /hosts/{id}/module/off` - 关机
+- `PUT /hosts/{id}/module/hard_reboot` - 硬重启
+
+## 🔍 监控服务
+
+### 配置文件
+
+监控服务配置文件位于:`/etc/systemd/system/idc_monitor.service`
+
+```ini
+[Unit]
+Description=VPS Hub Monitor Service
+After=network.target
+
+[Service]
+Type=simple
+User=www-data
+WorkingDirectory=/path/to/VPSHUB/app
+ExecStart=/usr/bin/python3 monitor.py
+Restart=always
+RestartSec=10
+
+[Install]
+WantedBy=multi-user.target
+```
+
+### 日志文件
+
+- **正常日志**:`app/logs/monitor.log`
+- **错误日志**:`app/logs/error.log`
+
+### 定时任务
+
+- **监控循环**:每5分钟执行一次
+- **每日摘要**:每天00:00生成前一天的统计报告
+- **数据清理**:每次循环清理30天前的Ping记录
+
+---
+
+## 🔒 安全说明
+
+### 访问控制
+
+- **API密码保护**:所有页面都需要通过 `?pass=API_PASS` 验证
+- **密码文件**:存储在 `app/pass.php`,权限设置为 600
+- **数据库保护**:Nginx配置禁止直接访问 `.db` 文件,或者限制访问app路径需要basic认证
+
+### Token安全
+
+- **Token缓存**:JWT Token缓存2小时,减少登录次数
+- **自动刷新**:Token失效时自动重新登录
+- **缓存文件**:`app/token.php`,权限设置为 600
+
+### 最佳实践
+
+1. **修改默认密码**:首次使用后立即修改API密码
+2. **HTTPS**:生产环境务必使用HTTPS
+3. **防火墙**:限制访问IP范围
+4. **定期备份**:定期备份数据库文件
+5. **日志审计**:定期检查日志文件
+
+---
+
+## ❓ 常见问题
+
+### Q1: 页面显示"0台VPS"怎么办?
+
+**A**:
+1. 点击"手动刷新VPS列表"按钮
+2. 检查配置是否正确(特别是site_url不要包含路径)
+3. 查看PHP错误日志:`tail -f /var/log/php/error.log`
+
+---
+
+### Q2: 监控服务无法启动?
+
+**A**:
+```bash
+# 检查服务状态
+sudo systemctl status idc_monitor
+
+# 查看详细日志
+journalctl -u idc_monitor -n 50
+
+# 常见原因:
+# 1. Python依赖未安装:pip install -r requirements.txt
+# 2. 数据库文件权限问题:chown www-data:www-data app/db/*.db
+# 3. 配置文件路径错误:检查service文件中的WorkingDirectory
+```
+
+---
+
+### Q3: exec函数被禁用怎么办?
+
+**A**:
+系统已经做了兼容处理:
+- 如果exec被禁用,会显示友好提示
+- 需要手动执行安装脚本:`sudo bash app/install.sh`
+- 监控服务仍然可以正常运行
+
+---
+
+### Q4: VPS状态显示"未知"?
+
+**A**:
+可能原因:
+1. 数据库中还没有该VPS的记录
+2. 多次调用导致状态被覆盖(已修复)
+3. API返回异常
+
+解决方法:
+- 手动刷新VPS列表
+- 检查日志确认API调用是否正常
+- 等待下一次监控循环
+
+---
+
+### Q5: 如何添加新的VPS平台?
+
+**A**:
+1. 在 `app/monitor.py` 中创建新的适配器类
+2. 继承 `PlatformAdapter` 基类
+3. 实现必要的方法(login, get_vps_list, get_vps_status等)
+4. 在 `load_configs_and_create_adapters()` 中添加判断逻辑
+
+示例:
+```python
+class AliyunAdapter(PlatformAdapter):
+ def login(self):
+ # 实现阿里云登录逻辑
+ pass
+
+ def get_vps_list(self):
+ # 实现获取VPS列表逻辑
+ pass
+```
+
+---
+
+### Q6: 数据库文件在哪里?
+
+**A**:
+- `app/db/vps.db` - 配置数据库
+- `app/db/vpslist.db` - VPS列表数据库
+- `app/db/status.db` - 监控状态数据库
+
+可以使用SQLite工具查看:
+```bash
+sqlite3 app/db/vpslist.db
+.tables
+SELECT * FROM vps_list;
+```
+
+---
+
+### Q7: 如何备份数据?
+
+**A**:
+```bash
+# 备份所有数据库
+cp app/db/*.db backup/$(date +%Y%m%d)/
+
+# 或使用sqlite3导出
+sqlite3 app/db/vpslist.db ".dump" > backup/vpslist.sql
+```
+
+---
+
+## 📄 许可证
+
+本项目采用 MIT 许可证 - 详见 [LICENSE](LICENSE) 文件
+
+---
+
+
+## 📧 联系方式
+
+- **项目地址**: [https://git.masonliu.com/MasonLiu/VPSHUB](https://git.masonliu.com/MasonLiu/VPSHUB)
+- **作者**: MasonLiu
+- **邮箱**: admin@masonliu.com
+
+---
+
+## 🙏 致谢
+
+感谢以下开源项目:
+- [PHP](https://www.php.net/)
+- [Python](https://www.python.org/)
+- [SQLite](https://www.sqlite.org/)
+- [智简魔方财务系统](https://www.idcsmart.com/)
+
+---
+
+
+
+**⭐ 如果这个项目对您有帮助,请给个Star!**
+
+Made with ❤️ by MasonLiu
+
+
diff --git a/app/config.yml b/app/config.yml
deleted file mode 100644
index 629d18c..0000000
--- a/app/config.yml
+++ /dev/null
@@ -1,7 +0,0 @@
-ACCOUNT: '' # 填写核云IDC账号(手机号或邮箱)
-API_KEY: '' # 填写核云IDC API密钥
-WAY: ping # 填写检测方式:ping 或 http,默认为ping
-DOMAIN: '' # 当WAY为http时,填写要检测的域名(多个域名用英文逗号分隔)
-SPAN: 300 # 监控间隔时间(秒),默认300秒(5分钟)
-EXCEPTION_IPS: [] # 例外IP列表,这些IP关机时不会自动开机(例如:["1.2.3.4", "5.6.7.8"])
-JWT: '' # JWT Token(自动管理,无需手动填写)
\ No newline at end of file
diff --git a/app/db_helper.php b/app/db_helper.php
new file mode 100644
index 0000000..113a23f
--- /dev/null
+++ b/app/db_helper.php
@@ -0,0 +1,253 @@
+dbPath = $dbPath;
+
+ // 确保目录存在
+ $dir = dirname($dbPath);
+ if (!is_dir($dir)) {
+ mkdir($dir, 0755, true);
+ }
+
+ try {
+ $this->db = new PDO("sqlite:" . $dbPath);
+ $this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+ $this->db->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
+ } catch (PDOException $e) {
+ throw new Exception("数据库连接失败: " . $e->getMessage());
+ }
+ }
+
+ /**
+ * 执行查询并返回所有结果
+ * @param string $sql SQL语句
+ * @param array $params 参数数组
+ * @return array 结果数组
+ */
+ public function query($sql, $params = []) {
+ try {
+ $stmt = $this->db->prepare($sql);
+ $stmt->execute($params);
+ return $stmt->fetchAll();
+ } catch (PDOException $e) {
+ error_log("数据库查询错误: " . $e->getMessage() . " | SQL: " . $sql);
+ return [];
+ }
+ }
+
+ /**
+ * 执行查询并返回单条结果
+ * @param string $sql SQL语句
+ * @param array $params 参数数组
+ * @return array|false 结果数组或false
+ */
+ public function queryOne($sql, $params = []) {
+ try {
+ $stmt = $this->db->prepare($sql);
+ $stmt->execute($params);
+ return $stmt->fetch();
+ } catch (PDOException $e) {
+ error_log("数据库查询错误: " . $e->getMessage() . " | SQL: " . $sql);
+ return false;
+ }
+ }
+
+ /**
+ * 执行插入、更新、删除操作
+ * @param string $sql SQL语句
+ * @param array $params 参数数组
+ * @return bool 是否成功
+ */
+ public function execute($sql, $params = []) {
+ try {
+ $stmt = $this->db->prepare($sql);
+ return $stmt->execute($params);
+ } catch (PDOException $e) {
+ error_log("数据库执行错误: " . $e->getMessage() . " | SQL: " . $sql);
+ return false;
+ }
+ }
+
+ /**
+ * 插入数据并返回最后插入的ID
+ * @param string $sql SQL语句
+ * @param array $params 参数数组
+ * @return int|false 最后插入的ID或false
+ */
+ public function insert($sql, $params = []) {
+ try {
+ $stmt = $this->db->prepare($sql);
+ $stmt->execute($params);
+ return $this->db->lastInsertId();
+ } catch (PDOException $e) {
+ error_log("数据库插入错误: " . $e->getMessage() . " | SQL: " . $sql);
+ return false;
+ }
+ }
+
+ /**
+ * 开始事务
+ */
+ public function beginTransaction() {
+ return $this->db->beginTransaction();
+ }
+
+ /**
+ * 提交事务
+ */
+ public function commit() {
+ return $this->db->commit();
+ }
+
+ /**
+ * 回滚事务
+ */
+ public function rollBack() {
+ return $this->db->rollBack();
+ }
+
+ /**
+ * 获取数据库连接对象(用于高级操作)
+ * @return PDO
+ */
+ public function getConnection() {
+ return $this->db;
+ }
+}
+
+// 便捷函数:获取vps.db的DBHelper实例
+function getVpsDB() {
+ static $db = null;
+ if ($db === null) {
+ $dbPath = __DIR__ . '/db/vps.db';
+ $db = new DBHelper($dbPath);
+ // 初始化表结构
+ initVpsTables($db);
+ }
+ return $db;
+}
+
+// 便捷函数:获取vpslist.db的DBHelper实例
+function getVpsListDB() {
+ static $db = null;
+ if ($db === null) {
+ $dbPath = __DIR__ . '/db/vpslist.db';
+ $db = new DBHelper($dbPath);
+ initVpsListTables($db);
+ }
+ return $db;
+}
+
+// 便捷函数:获取status.db的DBHelper实例
+function getStatusDB() {
+ static $db = null;
+ if ($db === null) {
+ $dbPath = __DIR__ . '/db/status.db';
+ $db = new DBHelper($dbPath);
+ // 初始化表结构
+ initStatusTables($db);
+ }
+ return $db;
+}
+
+/**
+ * 初始化vps.db表结构
+ */
+function initVpsTables($db) {
+ $db->execute("
+ CREATE TABLE IF NOT EXISTS configs (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ api_label TEXT NOT NULL UNIQUE,
+ site_type TEXT NOT NULL,
+ site_url TEXT,
+ account TEXT NOT NULL,
+ api_key TEXT NOT NULL,
+ auto_monitor BOOLEAN DEFAULT 1,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+ )
+ ");
+}
+
+/**
+ * 初始化vpslist.db表结构
+ */
+function initVpsListTables($db) {
+ $db->execute("
+ CREATE TABLE IF NOT EXISTS vps_list (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ config_id INTEGER NOT NULL,
+ vps_id INTEGER NOT NULL,
+ domain TEXT,
+ ip_address TEXT,
+ product_name TEXT,
+ cpu_cores INTEGER,
+ memory_size TEXT,
+ disk_size TEXT,
+ bandwidth TEXT,
+ os_type TEXT,
+ status TEXT,
+ section BOOLEAN DEFAULT 0,
+ last_check TIMESTAMP,
+ FOREIGN KEY (config_id) REFERENCES configs(id),
+ UNIQUE(config_id, vps_id)
+ )
+ ");
+
+ $db->execute('CREATE INDEX IF NOT EXISTS idx_vps_config ON vps_list(config_id)');
+ $db->execute('CREATE INDEX IF NOT EXISTS idx_vps_vps_id ON vps_list(vps_id)');
+}
+
+/**
+ * 初始化status.db表结构
+ */
+function initStatusTables($db) {
+ // Ping状态记录表
+ $db->execute("
+ CREATE TABLE IF NOT EXISTS ping_status (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ vps_id INTEGER NOT NULL,
+ target TEXT NOT NULL,
+ status TEXT NOT NULL,
+ latency_ms REAL,
+ check_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+ )
+ ");
+
+ // VPS摘要统计表
+ $db->execute("
+ CREATE TABLE IF NOT EXISTS vps_summary (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ vps_id INTEGER NOT NULL,
+ date DATE NOT NULL,
+ avg_latency_ms REAL,
+ max_latency_ms REAL,
+ min_latency_ms REAL,
+ count_under_100 INTEGER DEFAULT 0,
+ count_100_to_300 INTEGER DEFAULT 0,
+ count_300_to_500 INTEGER DEFAULT 0,
+ count_abnormal INTEGER DEFAULT 0,
+ availability TEXT,
+ UNIQUE(vps_id, date)
+ )
+ ");
+
+ // 创建索引
+ $db->execute('CREATE INDEX IF NOT EXISTS idx_ping_vps ON ping_status(vps_id)');
+ $db->execute('CREATE INDEX IF NOT EXISTS idx_ping_time ON ping_status(check_time)');
+ $db->execute('CREATE INDEX IF NOT EXISTS idx_summary_vps ON vps_summary(vps_id)');
+ $db->execute('CREATE INDEX IF NOT EXISTS idx_summary_date ON vps_summary(date)');
+}
+?>
diff --git a/app/db_manager.py b/app/db_manager.py
new file mode 100644
index 0000000..0631a19
--- /dev/null
+++ b/app/db_manager.py
@@ -0,0 +1,582 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+VPS Hub 数据库管理模块
+负责初始化和管理三个SQLite数据库: vps.db, vpslist.db, status.db
+"""
+
+import os
+import sqlite3
+from datetime import datetime
+
+
+class DatabaseManager:
+ """数据库管理器"""
+
+ def __init__(self, db_dir=None):
+ """初始化数据库管理器
+
+ Args:
+ db_dir: 数据库文件目录,默认为app/db/
+ """
+ if db_dir is None:
+ db_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'db')
+
+ self.db_dir = db_dir
+ os.makedirs(db_dir, exist_ok=True)
+
+ # 数据库文件路径
+ self.vps_db = os.path.join(db_dir, 'vps.db')
+ self.vpslist_db = os.path.join(db_dir, 'vpslist.db')
+ self.status_db = os.path.join(db_dir, 'status.db')
+
+ # 初始化所有数据库
+ self.init_vps_db()
+ self.init_vpslist_db()
+ self.init_status_db()
+
+ def get_connection(self, db_path):
+ """获取数据库连接
+
+ Args:
+ db_path: 数据库文件路径
+
+ Returns:
+ SQLite连接对象
+ """
+ conn = sqlite3.connect(db_path)
+ conn.row_factory = sqlite3.Row # 使结果可以通过列名访问
+ return conn
+
+ def init_vps_db(self):
+ """初始化vps.db - VPS配置表"""
+ conn = self.get_connection(self.vps_db)
+ cursor = conn.cursor()
+
+ cursor.execute('''
+ CREATE TABLE IF NOT EXISTS configs (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ api_label TEXT NOT NULL UNIQUE,
+ site_type TEXT NOT NULL,
+ site_url TEXT,
+ account TEXT NOT NULL,
+ api_key TEXT NOT NULL,
+ auto_monitor BOOLEAN DEFAULT 1,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+ )
+ ''')
+
+ conn.commit()
+ conn.close()
+
+ def init_vpslist_db(self):
+ """初始化vpslist.db - VPS列表缓存表"""
+ conn = self.get_connection(self.vpslist_db)
+ cursor = conn.cursor()
+
+ cursor.execute('''
+ CREATE TABLE IF NOT EXISTS vps_list (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ config_id INTEGER NOT NULL,
+ vps_id INTEGER NOT NULL,
+ domain TEXT,
+ ip_address TEXT,
+ product_name TEXT,
+ cpu_cores INTEGER,
+ memory_size TEXT,
+ disk_size TEXT,
+ bandwidth TEXT,
+ os_type TEXT,
+ status TEXT,
+ section BOOLEAN DEFAULT 0,
+ last_check TIMESTAMP,
+ FOREIGN KEY (config_id) REFERENCES configs(id),
+ UNIQUE(config_id, vps_id)
+ )
+ ''')
+
+ # 创建索引以提高查询性能
+ cursor.execute('CREATE INDEX IF NOT EXISTS idx_vps_config ON vps_list(config_id)')
+ cursor.execute('CREATE INDEX IF NOT EXISTS idx_vps_vps_id ON vps_list(vps_id)')
+
+ conn.commit()
+ conn.close()
+
+ def init_status_db(self):
+ """初始化status.db - Ping状态记录和摘要统计表"""
+ conn = self.get_connection(self.status_db)
+ cursor = conn.cursor()
+
+ # Ping状态记录表
+ cursor.execute('''
+ CREATE TABLE IF NOT EXISTS ping_status (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ vps_id INTEGER NOT NULL,
+ target TEXT NOT NULL,
+ status TEXT NOT NULL,
+ latency_ms REAL,
+ check_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+ )
+ ''')
+
+ # VPS摘要统计表
+ cursor.execute('''
+ CREATE TABLE IF NOT EXISTS vps_summary (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ vps_id INTEGER NOT NULL,
+ date DATE NOT NULL,
+ avg_latency_ms REAL,
+ max_latency_ms REAL,
+ min_latency_ms REAL,
+ count_under_100 INTEGER DEFAULT 0,
+ count_100_to_300 INTEGER DEFAULT 0,
+ count_300_to_500 INTEGER DEFAULT 0,
+ count_abnormal INTEGER DEFAULT 0,
+ availability TEXT,
+ UNIQUE(vps_id, date)
+ )
+ ''')
+
+ # 创建索引
+ cursor.execute('CREATE INDEX IF NOT EXISTS idx_ping_vps ON ping_status(vps_id)')
+ cursor.execute('CREATE INDEX IF NOT EXISTS idx_ping_time ON ping_status(check_time)')
+ cursor.execute('CREATE INDEX IF NOT EXISTS idx_summary_vps ON vps_summary(vps_id)')
+ cursor.execute('CREATE INDEX IF NOT EXISTS idx_summary_date ON vps_summary(date)')
+
+ conn.commit()
+ conn.close()
+
+ # ==================== vps.db 操作 ====================
+
+ def add_config(self, api_label, site_type, account, api_key, site_url=None, auto_monitor=True):
+ """添加VPS配置
+
+ Args:
+ api_label: API标识(必填,唯一)
+ site_type: 网站类型 (mofang/aliyun/tencent)
+ account: 账户
+ api_key: API密钥
+ site_url: 网站链接
+ auto_monitor: 是否开启自动监控
+
+ Returns:
+ 新配置的ID
+ """
+ conn = self.get_connection(self.vps_db)
+ cursor = conn.cursor()
+
+ try:
+ cursor.execute('''
+ INSERT INTO configs (api_label, site_type, site_url, account, api_key, auto_monitor)
+ VALUES (?, ?, ?, ?, ?, ?)
+ ''', (api_label, site_type, site_url, account, api_key, auto_monitor))
+
+ config_id = cursor.lastrowid
+ conn.commit()
+ return config_id
+ except Exception as e:
+ conn.rollback()
+ raise e
+ finally:
+ conn.close()
+
+ def get_all_configs(self):
+ """获取所有配置
+
+ Returns:
+ 配置列表
+ """
+ conn = self.get_connection(self.vps_db)
+ cursor = conn.cursor()
+
+ cursor.execute('SELECT * FROM configs ORDER BY id')
+ configs = [dict(row) for row in cursor.fetchall()]
+
+ conn.close()
+ return configs
+
+ def get_config_by_id(self, config_id):
+ """根据ID获取配置
+
+ Args:
+ config_id: 配置ID
+
+ Returns:
+ 配置字典或None
+ """
+ conn = self.get_connection(self.vps_db)
+ cursor = conn.cursor()
+
+ cursor.execute('SELECT * FROM configs WHERE id = ?', (config_id,))
+ row = cursor.fetchone()
+
+ conn.close()
+ return dict(row) if row else None
+
+ def update_config(self, config_id, **kwargs):
+ """更新配置
+
+ Args:
+ config_id: 配置ID
+ **kwargs: 要更新的字段
+
+ Returns:
+ 是否成功
+ """
+ if not kwargs:
+ return False
+
+ conn = self.get_connection(self.vps_db)
+ cursor = conn.cursor()
+
+ # 构建UPDATE语句
+ fields = ', '.join([f"{key} = ?" for key in kwargs.keys()])
+ values = list(kwargs.values())
+ values.append(config_id)
+
+ cursor.execute(f'UPDATE configs SET {fields}, updated_at = CURRENT_TIMESTAMP WHERE id = ?', values)
+
+ affected = cursor.rowcount
+ conn.commit()
+ conn.close()
+
+ return affected > 0
+
+ def delete_config(self, config_id):
+ """删除配置
+
+ Args:
+ config_id: 配置ID
+
+ Returns:
+ 是否成功
+ """
+ conn = self.get_connection(self.vps_db)
+ cursor = conn.cursor()
+
+ cursor.execute('DELETE FROM configs WHERE id = ?', (config_id,))
+
+ affected = cursor.rowcount
+ conn.commit()
+ conn.close()
+
+ return affected > 0
+
+ # ==================== vpslist.db 操作 ====================
+
+ def add_vps(self, config_id, vps_id, domain=None, ip_address=None,
+ product_name=None, section=False):
+ """添加VPS到列表
+
+ Args:
+ config_id: 配置ID
+ vps_id: VPS在平台的ID
+ domain: 域名
+ ip_address: IP地址
+ product_name: 产品名称
+ section: 是否标记为需要监控
+
+ Returns:
+ 新记录的ID
+ """
+ conn = self.get_connection(self.vpslist_db)
+ cursor = conn.cursor()
+
+ cursor.execute('''
+ INSERT INTO vps_list (config_id, vps_id, domain, ip_address, product_name, section, last_check)
+ VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
+ ''', (config_id, vps_id, domain, ip_address, product_name, section))
+
+ record_id = cursor.lastrowid
+ conn.commit()
+ conn.close()
+
+ return record_id
+
+ def batch_add_vps(self, vps_list):
+ """批量添加VPS
+
+ Args:
+ vps_list: VPS信息列表,每个元素是字典
+ """
+ conn = self.get_connection(self.vpslist_db)
+ cursor = conn.cursor()
+
+ for vps in vps_list:
+ cursor.execute('''
+ INSERT OR REPLACE INTO vps_list
+ (config_id, vps_id, domain, ip_address, product_name, cpu_cores, memory_size, disk_size, bandwidth, os_type, section, last_check)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
+ ''', (
+ vps['config_id'],
+ vps['vps_id'],
+ vps.get('domain'),
+ vps.get('ip_address'),
+ vps.get('product_name'),
+ vps.get('cpu_cores'),
+ vps.get('memory_size'),
+ vps.get('disk_size'),
+ vps.get('bandwidth'),
+ vps.get('os_type'),
+ vps.get('section', False)
+ ))
+
+ conn.commit()
+ conn.close()
+
+ def get_all_vps(self):
+ """获取所有VPS
+
+ Returns:
+ VPS列表
+ """
+ conn = self.get_connection(self.vpslist_db)
+ cursor = conn.cursor()
+
+ cursor.execute('SELECT * FROM vps_list ORDER BY config_id, vps_id')
+ vps_list = [dict(row) for row in cursor.fetchall()]
+
+ conn.close()
+ return vps_list
+
+ def get_vps_by_config(self, config_id):
+ """根据配置ID获取VPS列表
+
+ Args:
+ config_id: 配置ID
+
+ Returns:
+ VPS列表
+ """
+ conn = self.get_connection(self.vpslist_db)
+ cursor = conn.cursor()
+
+ cursor.execute('SELECT * FROM vps_list WHERE config_id = ? ORDER BY vps_id', (config_id,))
+ vps_list = [dict(row) for row in cursor.fetchall()]
+
+ conn.close()
+ return vps_list
+
+ def update_vps_details(self, vps_id, **kwargs):
+ """更新VPS详细信息
+
+ Args:
+ vps_id: VPS ID
+ **kwargs: 要更新的字段
+
+ Returns:
+ 是否成功
+ """
+ if not kwargs:
+ return False
+
+ conn = self.get_connection(self.vpslist_db)
+ cursor = conn.cursor()
+
+ fields = ', '.join([f"{key} = ?" for key in kwargs.keys()])
+ values = list(kwargs.values())
+ values.append(vps_id)
+
+ cursor.execute(f'UPDATE vps_list SET {fields}, last_check = CURRENT_TIMESTAMP WHERE vps_id = ?', values)
+
+ affected = cursor.rowcount
+ conn.commit()
+ conn.close()
+
+ return affected > 0
+
+ def update_vps_status(self, vps_id, status):
+ """更新VPS状态
+
+ Args:
+ vps_id: VPS ID
+ status: 状态 (on/off/unknown)
+
+ Returns:
+ 是否成功
+ """
+ return self.update_vps_details(vps_id, status=status)
+
+ def get_monitored_vps(self):
+ """获取所有标记为需要监控的VPS
+
+ Returns:
+ VPS列表
+ """
+ conn = self.get_connection(self.vpslist_db)
+ cursor = conn.cursor()
+
+ cursor.execute('SELECT * FROM vps_list WHERE section = 1 ORDER BY config_id, vps_id')
+ vps_list = [dict(row) for row in cursor.fetchall()]
+
+ conn.close()
+ return vps_list
+
+ # ==================== status.db 操作 ====================
+
+ def save_ping_status(self, vps_id, target, status, latency_ms=None):
+ """保存Ping状态记录
+
+ Args:
+ vps_id: VPS ID
+ target: 目标(IP或域名)
+ status: 状态 (normal/abnormal)
+ latency_ms: 延迟(ms)
+ """
+ conn = self.get_connection(self.status_db)
+ cursor = conn.cursor()
+
+ cursor.execute('''
+ INSERT INTO ping_status (vps_id, target, status, latency_ms)
+ VALUES (?, ?, ?, ?)
+ ''', (vps_id, target, status, latency_ms))
+
+ conn.commit()
+ conn.close()
+
+ def get_ping_records(self, vps_id, date=None):
+ """获取Ping记录
+
+ Args:
+ vps_id: VPS ID
+ date: 日期 (YYYY-MM-DD),为空则获取所有记录
+
+ Returns:
+ Ping记录列表
+ """
+ conn = self.get_connection(self.status_db)
+ cursor = conn.cursor()
+
+ if date:
+ cursor.execute('''
+ SELECT * FROM ping_status
+ WHERE vps_id = ? AND DATE(check_time) = ?
+ ORDER BY check_time
+ ''', (vps_id, date))
+ else:
+ cursor.execute('''
+ SELECT * FROM ping_status
+ WHERE vps_id = ?
+ ORDER BY check_time DESC
+ ''', (vps_id,))
+
+ records = [dict(row) for row in cursor.fetchall()]
+ conn.close()
+
+ return records
+
+ def cleanup_old_ping_records(self, days=30):
+ """清理旧的Ping记录
+
+ Args:
+ days: 保留天数,默认30天
+ """
+ from datetime import timedelta
+
+ conn = self.get_connection(self.status_db)
+ cursor = conn.cursor()
+
+ cutoff_date = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%d %H:%M:%S')
+ cursor.execute('DELETE FROM ping_status WHERE check_time < ?', (cutoff_date,))
+
+ deleted = cursor.rowcount
+ conn.commit()
+ conn.close()
+
+ return deleted
+
+ def save_vps_summary(self, vps_id, date, avg_latency, max_latency, min_latency,
+ count_under_100, count_100_to_300, count_300_to_500,
+ count_abnormal, availability):
+ """保存VPS摘要统计
+
+ Args:
+ vps_id: VPS ID
+ date: 日期 (YYYY-MM-DD)
+ avg_latency: 平均延迟
+ max_latency: 最大延迟
+ min_latency: 最小延迟
+ count_under_100: <100ms次数
+ count_100_to_300: 100-300ms次数
+ count_300_to_500: 300-500ms次数
+ count_abnormal: 异常次数
+ availability: 可用性评分
+ """
+ conn = self.get_connection(self.status_db)
+ cursor = conn.cursor()
+
+ cursor.execute('''
+ INSERT OR REPLACE INTO vps_summary
+ (vps_id, date, avg_latency_ms, max_latency_ms, min_latency_ms,
+ count_under_100, count_100_to_300, count_300_to_500, count_abnormal, availability)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ ''', (vps_id, date, avg_latency, max_latency, min_latency,
+ count_under_100, count_100_to_300, count_300_to_500,
+ count_abnormal, availability))
+
+ conn.commit()
+ conn.close()
+
+ def get_vps_summary(self, vps_id, date=None):
+ """获取VPS摘要统计
+
+ Args:
+ vps_id: VPS ID
+ date: 日期,为空则获取最新一条
+
+ Returns:
+ 摘要统计字典或None
+ """
+ conn = self.get_connection(self.status_db)
+ cursor = conn.cursor()
+
+ if date:
+ cursor.execute('SELECT * FROM vps_summary WHERE vps_id = ? AND date = ?', (vps_id, date))
+ else:
+ cursor.execute('SELECT * FROM vps_summary WHERE vps_id = ? ORDER BY date DESC LIMIT 1', (vps_id,))
+
+ row = cursor.fetchone()
+ conn.close()
+
+ return dict(row) if row else None
+
+ def get_all_summaries(self, date=None):
+ """获取所有VPS的摘要统计
+
+ Args:
+ date: 日期,为空则获取最新
+
+ Returns:
+ 摘要统计列表
+ """
+ conn = self.get_connection(self.status_db)
+ cursor = conn.cursor()
+
+ if date:
+ cursor.execute('SELECT * FROM vps_summary WHERE date = ? ORDER BY vps_id', (date,))
+ else:
+ # 获取每个VPS的最新摘要
+ cursor.execute('''
+ SELECT vs.* FROM vps_summary vs
+ INNER JOIN (
+ SELECT vps_id, MAX(date) as max_date
+ FROM vps_summary
+ GROUP BY vps_id
+ ) latest ON vs.vps_id = latest.vps_id AND vs.date = latest.max_date
+ ORDER BY vs.vps_id
+ ''')
+
+ summaries = [dict(row) for row in cursor.fetchall()]
+ conn.close()
+
+ return summaries
+
+
+if __name__ == '__main__':
+ # 测试代码
+ db = DatabaseManager()
+ print("数据库初始化完成")
+ print(f"vps.db: {db.vps_db}")
+ print(f"vpslist.db: {db.vpslist_db}")
+ print(f"status.db: {db.status_db}")
diff --git a/app/install.sh b/app/install.sh
index 9e7b849..4b64426 100644
--- a/app/install.sh
+++ b/app/install.sh
@@ -1,7 +1,7 @@
#!/bin/bash
-# 核云IDC服务商VPS自动监测重启程序 - 安装脚本
-# 该脚本会检查配置并创建systemd服务,持续化运行monitor.py
+# VPS Hub 监控程序 - 安装脚本
+# 该脚本会创建systemd服务,持续化运行monitor.py
# 颜色定义
RED='\033[0;31m'
@@ -13,12 +13,11 @@ NC='\033[0m' # No Color
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# 定义服务名称
-SERVICE_NAME="idc-monitor"
+SERVICE_NAME="idc_monitor"
SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}.service"
-CONFIG_FILE="${SCRIPT_DIR}/config.yml"
echo "=========================================="
-echo " 核云IDC VPS监控程序 - 安装向导"
+echo " VPS Hub 监控程序 - 安装向导"
echo "=========================================="
echo ""
@@ -81,189 +80,12 @@ fi
echo -e "${GREEN}✅${NC} 检测到pip: ${PIP_CMD}"
-# 检查配置文件是否存在
-if [ ! -f "${CONFIG_FILE}" ]; then
- echo -e "${RED}错误: 配置文件不存在: ${CONFIG_FILE}${NC}"
- exit 1
-fi
-
-# 读取现有配置
-echo ""
-echo "正在检查配置文件..."
-
-# 使用grep解析YAML配置
-ACCOUNT=$(grep "^ACCOUNT:" "${CONFIG_FILE}" | sed 's/ACCOUNT:[[:space:]]*//' | sed 's/[[:space:]]*#.*//' | tr -d "'" | tr -d '"')
-API_KEY=$(grep "^API_KEY:" "${CONFIG_FILE}" | sed 's/API_KEY:[[:space:]]*//' | sed 's/[[:space:]]*#.*//' | tr -d "'" | tr -d '"')
-WAY=$(grep "^WAY:" "${CONFIG_FILE}" | sed 's/WAY:[[:space:]]*//' | sed 's/[[:space:]]*#.*//' | tr -d "'" | tr -d '"')
-DOMAIN=$(grep "^DOMAIN:" "${CONFIG_FILE}" | sed 's/DOMAIN:[[:space:]]*//' | sed 's/[[:space:]]*#.*//' | tr -d "'" | tr -d '"')
-SPAN=$(grep "^SPAN:" "${CONFIG_FILE}" | sed 's/SPAN:[[:space:]]*//' | sed 's/[[:space:]]*#.*//' | tr -d "'" | tr -d '"')
-EXCEPTION_IPS=$(grep "^EXCEPTION_IPS:" "${CONFIG_FILE}" | sed 's/EXCEPTION_IPS:[[:space:]]*//' | sed 's/[[:space:]]*#.*//' | tr -d "'" | tr -d '"')
-
-NEED_UPDATE=false
-
-# 检查ACCOUNT
-if [ -z "$ACCOUNT" ]; then
- echo ""
- echo "=================================================="
- echo -e -n "${RED}请输入核云IDC账号(手机号或邮箱): ${NC}"
- read ACCOUNT
- if [ -z "$ACCOUNT" ]; then
- echo -e "${RED}错误: 账号不能为空${NC}"
- exit 1
- fi
- NEED_UPDATE=true
-fi
-
-# 检查API_KEY
-if [ -z "$API_KEY" ]; then
- echo ""
- echo "=================================================="
- echo -e -n "${RED}请输入核云IDC API密钥: ${NC}"
- read API_KEY
- if [ -z "$API_KEY" ]; then
- echo -e "${RED}错误: API密钥不能为空${NC}"
- exit 1
- fi
- NEED_UPDATE=true
-fi
-
-# 检查WAY
-if [ -z "$WAY" ]; then
- echo ""
- echo "=================================================="
- echo -e "${RED}请选择检测方式:${NC}"
- echo " 1. ping - Ping检测IP地址(默认)"
- echo " 2. http - HTTP检测域名"
- echo -e -n "${RED}请输入选项 1/2,直接回车默认为1: ${NC}"
- read WAY_CHOICE
-
- if [ "$WAY_CHOICE" = "2" ]; then
- WAY="http"
- else
- WAY="ping"
- fi
- NEED_UPDATE=true
-fi
-
-# 如果WAY为http,检查DOMAIN
-if [ "$WAY" = "http" ] && [ -z "$DOMAIN" ]; then
- echo ""
- echo "=================================================="
- echo -e "${RED}请输入要检测的域名(多个域名用英文逗号分隔):${NC}"
- echo "例如: example.com,test.com,demo.com"
- echo -e -n "${RED}域名: ${NC}"
- read DOMAIN
- if [ -z "$DOMAIN" ]; then
- echo -e "${RED}错误: 域名不能为空${NC}"
- exit 1
- fi
- NEED_UPDATE=true
-fi
-
-# 检查SPAN
-if [ -z "$SPAN" ]; then
- echo ""
- echo "=================================================="
- echo -e -n "${RED}请输入监控间隔时间(秒,直接回车默认300秒): ${NC}"
- read SPAN_INPUT
- if [ -n "$SPAN_INPUT" ]; then
- if [[ "$SPAN_INPUT" =~ ^[0-9]+$ ]]; then
- if [ "$SPAN_INPUT" -lt 60 ]; then
- echo -e "${RED}警告: 间隔时间过短,建议至少60秒${NC}"
- fi
- SPAN=$SPAN_INPUT
- else
- echo -e "${RED}警告: 输入无效,使用默认值300秒${NC}"
- SPAN=300
- fi
- else
- SPAN=300
- fi
- NEED_UPDATE=true
-fi
-
-# 询问是否设置例外IP
-echo ""
-echo "=================================================="
-echo -e "${RED}是否设置例外IP列表?(这些IP关机时不会自动开机)${NC}"
-echo -e -n "${RED}请输入例外IP,多个IP用英文逗号分隔,直接回车跳过: ${NC}"
-read EXCEPTION_INPUT
-
-if [ -n "$EXCEPTION_INPUT" ]; then
- EXCEPTION_IPS="$EXCEPTION_INPUT"
- NEED_UPDATE=true
-else
- if [ -z "$EXCEPTION_IPS" ]; then
- EXCEPTION_IPS=""
- fi
-fi
-
-# 保存配置
-if [ "$NEED_UPDATE" = true ]; then
- echo ""
- echo "=================================================="
-
- # 使用Python更新YAML文件
- ${PYTHON_CMD} << PYEOF
-import yaml
-
-config_file = "${CONFIG_FILE}"
-
-try:
- with open(config_file, 'r', encoding='utf-8') as f:
- config = yaml.safe_load(f)
-except:
- config = {}
-
-config['ACCOUNT'] = "${ACCOUNT}"
-config['API_KEY'] = "${API_KEY}"
-config['WAY'] = "${WAY}"
-if "${WAY}" == "http":
- config['DOMAIN'] = "${DOMAIN}"
-config['SPAN'] = ${SPAN}
-
-# 处理例外IP
-exception_ips_str = "${EXCEPTION_IPS}"
-if exception_ips_str:
- config['EXCEPTION_IPS'] = [ip.strip() for ip in exception_ips_str.split(',') if ip.strip()]
-else:
- config['EXCEPTION_IPS'] = []
-
-# 清除JWT字段(重新生成)
-if 'JWT' in config:
- del config['JWT']
-
-with open(config_file, 'w', encoding='utf-8') as f:
- yaml.dump(config, f, allow_unicode=True, default_flow_style=False)
-
-print("\033[32m✅ 配置已保存\033[0m")
-PYEOF
-
- if [ $? -ne 0 ]; then
- echo -e "${RED}错误: 配置保存失败${NC}"
- exit 1
- fi
-else
- echo -e "${GREEN}✅${NC} 配置检查完成,无需更新"
-fi
-
-echo ""
-echo "当前配置:"
-echo " 账号: ${ACCOUNT}"
-echo " 检测方式: ${WAY}"
-if [ "$WAY" = "http" ]; then
- echo " 域名: ${DOMAIN}"
-fi
-echo " 监控间隔: ${SPAN}秒"
-if [ -n "$EXCEPTION_IPS" ]; then
- echo " 例外IP: ${EXCEPTION_IPS}"
-else
- echo " 例外IP: 无"
-fi
echo ""
+echo "注意: VPS配置请通过网页 config_add.php 进行管理"
# 安装Python依赖
if [ -f "${SCRIPT_DIR}/requirements.txt" ]; then
+ echo ""
echo "正在安装Python依赖包..."
cd ${SCRIPT_DIR}
if ${PIP_CMD} install -r requirements.txt; then
@@ -283,10 +105,11 @@ if [ ! -f "${SCRIPT_DIR}/monitor.py" ]; then
exit 1
fi
-# 创建日志目录
+# 创建日志目录和数据库目录
LOG_DIR="${SCRIPT_DIR}/logs"
-mkdir -p ${LOG_DIR}
-chmod 755 ${LOG_DIR}
+DB_DIR="${SCRIPT_DIR}/db"
+mkdir -p ${LOG_DIR} ${DB_DIR}
+chmod 755 ${LOG_DIR} ${DB_DIR}
echo ""
echo "正在创建systemd服务..."
@@ -294,7 +117,7 @@ echo "正在创建systemd服务..."
# 创建systemd服务文件
cat > ${SERVICE_FILE} << EOF
[Unit]
-Description=Heyun IDC VPS Monitor Service
+Description=VPS Hub Monitor Service
After=network.target
Wants=network-online.target
@@ -359,6 +182,7 @@ echo -e " ${YELLOW}停止服务:${NC} systemctl stop ${SERVICE_NAME}"
echo -e " ${YELLOW}重启服务:${NC} systemctl restart ${SERVICE_NAME}"
echo -e " ${YELLOW}卸载服务:${NC} cd ${SCRIPT_DIR} && ./uninstall.sh"
echo ""
-echo "配置文件位置: ${CONFIG_FILE}"
+echo "配置文件位置: 通过网页 config_add.php 管理"
echo "日志文件位置: ${LOG_DIR}"
-echo ""
\ No newline at end of file
+echo "数据库文件位置: ${DB_DIR}"
+echo ""
diff --git a/app/monitor.py b/app/monitor.py
index b230d37..a6e7fde 100644
--- a/app/monitor.py
+++ b/app/monitor.py
@@ -1,36 +1,295 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
-核云IDC VPS自动监测重启程序
-功能:实时监测服务器状态,发现关机自动开机
+VPS Hub 多平台监控程序
+功能:支持多平台(魔方/阿里云/腾讯云)的VPS监控、自动开机和可用性统计
"""
import os
import sys
import time
-import yaml
import json
import logging
import subprocess
import requests
-from datetime import datetime
+import schedule
+from datetime import datetime, timedelta
from pathlib import Path
+# 导入数据库管理器
+from db_manager import DatabaseManager
-class IDCMonitor:
- """核云IDC监控器"""
+
+class PlatformAdapter:
+ """平台适配器基类"""
- def __init__(self, config_path=None):
- """初始化监控器"""
- # 获取配置文件路径
- if config_path is None:
- config_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'config.yml')
-
- self.config_path = config_path
- self.config = {}
- self.base_url = "https://www.heyunidc.cn/v1"
+ def __init__(self, site_url, account, api_key):
+ self.site_url = site_url.rstrip('/')
+ self.account = account
+ self.api_key = api_key
self.jwt_token = None
- self.retry_count = 0 # 开机重试次数
+
+ def login(self):
+ """登录获取Token - 需要子类实现"""
+ raise NotImplementedError
+
+ def get_vps_list(self):
+ """获取VPS列表 - 需要子类实现"""
+ raise NotImplementedError
+
+ def get_vps_status(self, vps_id):
+ """获取VPS状态 - 需要子类实现"""
+ raise NotImplementedError
+
+ def get_vps_details(self, vps_id):
+ """获取VPS详细信息 - 需要子类实现"""
+ raise NotImplementedError
+
+ def power_on(self, vps_id):
+ """开机 - 需要子类实现"""
+ raise NotImplementedError
+
+ def power_off(self, vps_id):
+ """关机 - 需要子类实现"""
+ raise NotImplementedError
+
+ def hard_reboot(self, vps_id):
+ """硬重启 - 需要子类实现"""
+ raise NotImplementedError
+
+
+class MofangAdapter(PlatformAdapter):
+ """魔方平台适配器(核云IDC等使用此适配器)"""
+
+ def __init__(self, site_url, account, api_key):
+ super().__init__(site_url, account, api_key)
+ # 魔方平台的API路径统一为 /v1
+ if not self.site_url:
+ raise ValueError("魔方平台必须提供网站链接(API地址)")
+
+ def _get_headers(self):
+ """获取请求头"""
+ if not self.jwt_token:
+ self.login()
+
+ return {
+ 'Authorization': f'JWT {self.jwt_token}',
+ 'Content-Type': 'application/json'
+ }
+
+ def login(self):
+ try:
+ # 魔方平台API统一在 /v1 路径下
+ url = f"{self.site_url}/v1/login_api"
+ data = {
+ 'account': self.account,
+ 'password': self.api_key
+ }
+
+ response = requests.post(url, data=data, timeout=10)
+
+ if response.status_code == 200:
+ result = response.json()
+ if result.get('status') == 200 and 'jwt' in result:
+ self.jwt_token = result['jwt']
+ return self.jwt_token
+ else:
+ logging.error(f"登录失败: {result.get('msg', '未知错误')}")
+ return None
+ else:
+ logging.error(f"登录请求失败,HTTP状态码: {response.status_code}")
+ return None
+
+ except Exception as e:
+ logging.error(f"登录异常: {str(e)}")
+ return None
+
+ def get_vps_list(self, page=1, limit=100):
+ """获取VPS列表"""
+ try:
+ headers = self._get_headers()
+ if not headers:
+ return None
+
+ url = f"{self.site_url}/v1/hosts?page={page}&limit={limit}"
+ response = requests.get(url, headers=headers, timeout=10)
+
+ if response.status_code == 200:
+ result = response.json()
+ if result.get('status') == 200:
+ return result['data']
+ elif result.get('status') == 405:
+ logging.warning("Token失效,重新登录")
+ self.jwt_token = None
+ return self.get_vps_list(page, limit)
+ else:
+ logging.error(f"获取VPS列表失败: {result.get('msg', '未知错误')}")
+ return None
+ else:
+ logging.error(f"获取VPS列表请求失败,HTTP状态码: {response.status_code}")
+ return None
+
+ except Exception as e:
+ logging.error(f"获取VPS列表异常: {str(e)}")
+ return None
+
+ def get_vps_status(self, vps_id):
+ """获取VPS状态"""
+ try:
+ headers = self._get_headers()
+ if not headers:
+ return None
+
+ url = f"{self.site_url}/v1/hosts/{vps_id}/module/status?type=host"
+ response = requests.get(url, headers=headers, timeout=10)
+
+ if response.status_code == 200:
+ result = response.json()
+ if result.get('status') == 200:
+ return result['data']
+ elif result.get('status') == 405:
+ logging.warning("Token失效,重新登录")
+ self.jwt_token = None
+ return self.get_vps_status(vps_id)
+ else:
+ logging.error(f"获取VPS {vps_id} 状态失败: {result.get('msg', '未知错误')}")
+ return None
+ else:
+ logging.error(f"获取VPS {vps_id} 状态请求失败,HTTP状态码: {response.status_code}")
+ return None
+
+ except Exception as e:
+ logging.error(f"获取VPS {vps_id} 状态异常: {str(e)}")
+ return None
+
+ def get_vps_details(self, vps_id):
+ """获取VPS详细信息"""
+ try:
+ headers = self._get_headers()
+ if not headers:
+ return None
+
+ url = f"{self.site_url}/v1/hosts/{vps_id}"
+ response = requests.get(url, headers=headers, timeout=10)
+
+ if response.status_code == 200:
+ result = response.json()
+ if result.get('status') == 200:
+ # 返回完整的数据结构,包含host信息和config_option
+ return result['data']
+ elif result.get('status') == 405:
+ logging.warning("Token失效,重新登录")
+ self.jwt_token = None
+ return self.get_vps_details(vps_id)
+ else:
+ logging.error(f"获取VPS {vps_id} 详情失败: {result.get('msg', '未知错误')}")
+ return None
+ else:
+ logging.error(f"获取VPS {vps_id} 详情请求失败,HTTP状态码: {response.status_code}")
+ return None
+
+ except Exception as e:
+ logging.error(f"获取VPS {vps_id} 详情异常: {str(e)}")
+ return None
+
+ def power_on(self, vps_id):
+ """开机"""
+ try:
+ headers = self._get_headers()
+ if not headers:
+ return False
+
+ url = f"{self.site_url}/v1/hosts/{vps_id}/module/on"
+ response = requests.put(url, headers=headers, timeout=10)
+
+ if response.status_code == 200:
+ result = response.json()
+ if result.get('status') == 200:
+ logging.info(f"VPS {vps_id} 开机指令发送成功")
+ return True
+ elif result.get('status') == 405:
+ logging.warning("Token失效,重新登录")
+ self.jwt_token = None
+ return self.power_on(vps_id)
+ else:
+ logging.error(f"VPS {vps_id} 开机失败: {result.get('msg', '未知错误')}")
+ return False
+ else:
+ logging.error(f"VPS {vps_id} 开机请求失败,HTTP状态码: {response.status_code}")
+ return False
+
+ except Exception as e:
+ logging.error(f"VPS {vps_id} 开机异常: {str(e)}")
+ return False
+
+ def power_off(self, vps_id):
+ """关机"""
+ try:
+ headers = self._get_headers()
+ if not headers:
+ return False
+
+ url = f"{self.site_url}/v1/hosts/{vps_id}/module/off"
+ response = requests.put(url, headers=headers, timeout=10)
+
+ if response.status_code == 200:
+ result = response.json()
+ if result.get('status') == 200:
+ logging.info(f"VPS {vps_id} 关机指令发送成功")
+ return True
+ elif result.get('status') == 405:
+ logging.warning("Token失效,重新登录")
+ self.jwt_token = None
+ return self.power_off(vps_id)
+ else:
+ logging.error(f"VPS {vps_id} 关机失败: {result.get('msg', '未知错误')}")
+ return False
+ else:
+ logging.error(f"VPS {vps_id} 关机请求失败,HTTP状态码: {response.status_code}")
+ return False
+
+ except Exception as e:
+ logging.error(f"VPS {vps_id} 关机异常: {str(e)}")
+ return False
+
+ def hard_reboot(self, vps_id):
+ """硬重启"""
+ try:
+ headers = self._get_headers()
+ if not headers:
+ return False
+
+ url = f"{self.site_url}/v1/hosts/{vps_id}/module/hard_reboot"
+ response = requests.put(url, headers=headers, timeout=10)
+
+ if response.status_code == 200:
+ result = response.json()
+ if result.get('status') == 200:
+ logging.info(f"VPS {vps_id} 硬重启指令发送成功")
+ return True
+ elif result.get('status') == 405:
+ logging.warning("Token失效,重新登录")
+ self.jwt_token = None
+ return self.hard_reboot(vps_id)
+ else:
+ logging.error(f"VPS {vps_id} 硬重启失败: {result.get('msg', '未知错误')}")
+ return False
+ else:
+ logging.error(f"VPS {vps_id} 硬重启请求失败,HTTP状态码: {response.status_code}")
+ return False
+
+ except Exception as e:
+ logging.error(f"VPS {vps_id} 硬重启异常: {str(e)}")
+ return False
+
+
+class MonitorService:
+ """监控服务主类"""
+
+ def __init__(self):
+ """初始化监控服务"""
+ self.db = DatabaseManager()
+ self.adapters = {} # config_id -> adapter实例
# 创建日志目录
log_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'logs')
@@ -39,19 +298,13 @@ class IDCMonitor:
# 配置日志
self.setup_logging(log_dir)
- # 加载配置
- self.load_config()
-
self.logger.info("=" * 60)
- self.logger.info("核云IDC VPS监控程序启动")
- self.logger.info(f"监控方式: {self.config.get('WAY', 'ping')}")
- self.logger.info(f"监控间隔: {self.config.get('SPAN', 300)}秒")
+ self.logger.info("VPS Hub 监控程序启动")
self.logger.info("=" * 60)
def setup_logging(self, log_dir):
"""配置日志系统"""
- # 创建logger
- self.logger = logging.getLogger('IDCMonitor')
+ self.logger = logging.getLogger('VPSHubMonitor')
self.logger.setLevel(logging.DEBUG)
# 清除已有handler
@@ -89,190 +342,38 @@ class IDCMonitor:
console_handler.setFormatter(console_format)
self.logger.addHandler(console_handler)
- def load_config(self):
- """加载配置文件"""
- try:
- with open(self.config_path, 'r', encoding='utf-8') as f:
- self.config = yaml.safe_load(f)
-
- # 验证必要配置
- required_keys = ['ACCOUNT', 'API_KEY', 'WAY']
- for key in required_keys:
- if key not in self.config or not self.config[key]:
- raise ValueError(f"配置文件中缺少必要项: {key}")
-
- # 设置默认值
- self.config.setdefault('SPAN', 300)
- self.config.setdefault('JWT', '')
- self.config.setdefault('EXCEPTION_IPS', [])
-
- # 如果WAY为http,检查DOMAIN
- if self.config['WAY'] == 'http':
- if 'DOMAIN' not in self.config or not self.config['DOMAIN']:
- raise ValueError("WAY为http时,必须配置DOMAIN")
-
- # 加载JWT token
- if self.config.get('JWT'):
- self.jwt_token = self.config['JWT']
- self.logger.info("已加载缓存的JWT Token")
-
- # 加载例外IP列表
- self.exception_ips = self.config.get('EXCEPTION_IPS', [])
- if self.exception_ips:
- self.logger.info(f"已加载 {len(self.exception_ips)} 个例外IP")
-
- self.logger.info("配置文件加载成功")
-
- except FileNotFoundError:
- self.logger.error(f"配置文件不存在: {self.config_path}")
- sys.exit(1)
- except Exception as e:
- self.logger.error(f"配置文件加载失败: {str(e)}")
- sys.exit(1)
-
- def save_jwt_token(self, token):
- """保存JWT Token到配置文件"""
- try:
- self.config['JWT'] = token
- with open(self.config_path, 'w', encoding='utf-8') as f:
- yaml.dump(self.config, f, allow_unicode=True, default_flow_style=False)
- self.jwt_token = token
- self.logger.debug("JWT Token已保存到配置文件")
- except Exception as e:
- self.logger.error(f"保存JWT Token失败: {str(e)}")
-
- def get_login_token(self):
- """获取登录Token"""
- try:
- url = f"{self.base_url}/login_api"
- data = {
- 'account': self.config['ACCOUNT'],
- 'password': self.config['API_KEY']
- }
-
- response = requests.post(url, data=data, timeout=10)
-
- if response.status_code == 200:
- result = response.json()
- if result.get('status') == 200 and 'jwt' in result:
- token = result['jwt']
- self.save_jwt_token(token)
- self.logger.info("成功获取新的JWT Token")
- return token
- else:
- self.logger.error(f"登录失败: {result.get('msg', '未知错误')}")
- return None
- else:
- self.logger.error(f"登录请求失败,HTTP状态码: {response.status_code}")
- return None
-
- except Exception as e:
- self.logger.error(f"获取Token异常: {str(e)}")
- return None
-
- def get_headers(self):
- """获取请求头(包含JWT)"""
- if not self.jwt_token:
- self.jwt_token = self.get_login_token()
- if not self.jwt_token:
- return None
+ def load_configs_and_create_adapters(self):
+ """从数据库加载所有配置并创建适配器"""
+ configs = self.db.get_all_configs()
- return {
- 'Authorization': f'JWT {self.jwt_token}',
- 'Content-Type': 'application/json'
- }
-
- def get_vps_list(self):
- """获取VPS列表"""
- try:
- headers = self.get_headers()
- if not headers:
- return None
+ for config in configs:
+ config_id = config['id']
+ site_type = config['site_type']
- url = f"{self.base_url}/hosts?page=1&limit=100"
- response = requests.get(url, headers=headers, timeout=10)
-
- if response.status_code == 200:
- result = response.json()
- if result.get('status') == 200:
- self.logger.debug(f"成功获取VPS列表,共{result['data']['total']}台")
- return result['data']
- elif result.get('status') == 405:
- self.logger.warning("Token失效,重新获取Token")
- self.jwt_token = None
- return self.get_vps_list() # 递归调用重试
- else:
- self.logger.error(f"获取VPS列表失败: {result.get('msg', '未知错误')}")
- return None
+ # 根据网站类型创建对应的适配器
+ if site_type == 'mofang':
+ adapter = MofangAdapter(
+ site_url=config['site_url'],
+ account=config['account'],
+ api_key=config['api_key']
+ )
+ self.adapters[config_id] = adapter
+ self.logger.info(f"已加载魔方平台配置: ID={config_id}, URL={config['site_url']}")
+ elif site_type == 'aliyun':
+ # TODO: 实现阿里云适配器
+ self.logger.warning(f"阿里云适配器尚未实现: ID={config_id}")
+ elif site_type == 'tencent':
+ # TODO: 实现腾讯云适配器
+ self.logger.warning(f"腾讯云适配器尚未实现: ID={config_id}")
else:
- self.logger.error(f"获取VPS列表请求失败,HTTP状态码: {response.status_code}")
- return None
-
- except Exception as e:
- self.logger.error(f"获取VPS列表异常: {str(e)}")
- return None
-
- def get_vps_status(self, host_id):
- """获取指定VPS的状态"""
- try:
- headers = self.get_headers()
- if not headers:
- return None
-
- url = f"{self.base_url}/hosts/{host_id}/module/status?type=host"
- response = requests.get(url, headers=headers, timeout=10)
-
- if response.status_code == 200:
- result = response.json()
- if result.get('status') == 200:
- return result['data']
- elif result.get('status') == 405:
- self.logger.warning("Token失效,重新获取Token")
- self.jwt_token = None
- return self.get_vps_status(host_id) # 递归调用重试
- else:
- self.logger.error(f"获取VPS {host_id} 状态失败: {result.get('msg', '未知错误')}")
- return None
- else:
- self.logger.error(f"获取VPS {host_id} 状态请求失败,HTTP状态码: {response.status_code}")
- return None
-
- except Exception as e:
- self.logger.error(f"获取VPS {host_id} 状态异常: {str(e)}")
- return None
-
- def power_on_vps(self, host_id):
- """开机指定VPS"""
- try:
- headers = self.get_headers()
- if not headers:
- return False
-
- url = f"{self.base_url}/hosts/{host_id}/module/on"
- response = requests.put(url, headers=headers, timeout=10)
-
- if response.status_code == 200:
- result = response.json()
- if result.get('status') == 200:
- self.logger.info(f"VPS {host_id} 开机指令发送成功")
- return True
- elif result.get('status') == 405:
- self.logger.warning("Token失效,重新获取Token")
- self.jwt_token = None
- return self.power_on_vps(host_id) # 递归调用重试
- else:
- self.logger.error(f"VPS {host_id} 开机失败: {result.get('msg', '未知错误')}")
- return False
- else:
- self.logger.error(f"VPS {host_id} 开机请求失败,HTTP状态码: {response.status_code}")
- return False
-
- except Exception as e:
- self.logger.error(f"VPS {host_id} 开机异常: {str(e)}")
- return False
+ self.logger.warning(f"未知的平台类型: {site_type}, ID={config_id}")
def ping_host(self, ip_address):
- """Ping检测主机是否存活"""
+ """Ping检测主机是否存活
+
+ Returns:
+ (is_alive, latency_ms): 是否存活和延迟(ms)
+ """
try:
# Windows和Linux的ping命令参数不同
if sys.platform == 'win32':
@@ -280,293 +381,421 @@ class IDCMonitor:
else:
cmd = ['ping', '-c', '1', '-W', '2', ip_address]
+ start_time = time.time()
result = subprocess.run(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
timeout=5
)
+ end_time = time.time()
- return result.returncode == 0
+ latency_ms = (end_time - start_time) * 1000 # 转换为毫秒
+ is_alive = result.returncode == 0
+
+ return is_alive, latency_ms if is_alive else None
except Exception as e:
self.logger.debug(f"Ping {ip_address} 异常: {str(e)}")
- return False
+ return False, None
- def check_http_host(self, domain):
- """HTTP HEAD检测域名是否存活"""
- try:
- response = requests.head(
- f"http://{domain}",
- timeout=5,
- allow_redirects=True
- )
- return response.status_code < 400
- except Exception as e:
- self.logger.debug(f"HTTP检测 {domain} 异常: {str(e)}")
- return False
-
- def detect_hosts(self):
- """检测所有主机存活状态"""
- way = self.config.get('WAY', 'ping')
-
- if way == 'ping':
- return self.detect_by_ping()
- elif way == 'http':
- return self.detect_by_http()
- else:
- self.logger.error(f"不支持的检测方式: {way}")
- return []
-
- def detect_by_ping(self):
- """通过Ping检测主机"""
+ def ping_and_record_status(self):
+ """对所有VPS进行ping测试并记录状态"""
self.logger.info("开始Ping检测所有VPS...")
- unreachable_hosts = []
+ vps_list = self.db.get_all_vps()
- # 获取VPS列表
- vps_data = self.get_vps_list()
- if not vps_data or 'host' not in vps_data:
- self.logger.error("无法获取VPS列表")
- return []
+ if not vps_list:
+ self.logger.info("数据库中没有VPS记录")
+ return
- for host in vps_data['host']:
- host_id = host['id']
- ip = host.get('dedicatedip', '')
+ success_count = 0
+ fail_count = 0
+
+ for vps in vps_list:
+ target = vps['ip_address'] or vps['domain']
- if not ip:
- self.logger.warning(f"VPS {host_id} 没有IP地址,跳过")
+ if not target:
+ self.logger.warning(f"VPS {vps['vps_id']} 没有IP或域名,跳过")
continue
- self.logger.debug(f"正在Ping检测: {ip} (ID: {host_id})")
+ self.logger.debug(f"正在Ping检测: {target} (VPS ID: {vps['vps_id']})")
- if not self.ping_host(ip):
- # 检查是否为例外IP
- if ip in self.exception_ips:
- self.logger.info(f"VPS {host_id} ({ip}) Ping不通,但属于例外IP,跳过")
- continue
-
- self.logger.warning(f"VPS {host_id} ({ip}) Ping不通")
- unreachable_hosts.append({
- 'id': host_id,
- 'ip': ip,
- 'domain': host.get('domain', ''),
- 'product_name': host.get('product_name', '')
- })
-
- if unreachable_hosts:
- self.logger.warning(f"发现 {len(unreachable_hosts)} 台VPS无法Ping通")
- else:
- self.logger.info("所有VPS Ping检测正常")
-
- return unreachable_hosts
-
- def detect_by_http(self):
- """通过HTTP检测域名"""
- domains_str = self.config.get('DOMAIN', '')
- if not domains_str:
- self.logger.error("未配置DOMAIN")
- return []
-
- domains = [d.strip() for d in domains_str.split(',') if d.strip()]
-
- self.logger.info(f"开始HTTP检测 {len(domains)} 个域名...")
-
- unreachable_domains = []
-
- for domain in domains:
- self.logger.debug(f"正在HTTP检测: {domain}")
+ is_alive, latency_ms = self.ping_host(target)
- if not self.check_http_host(domain):
- self.logger.warning(f"域名 {domain} HTTP检测失败")
- unreachable_domains.append(domain)
+ status = 'normal' if is_alive else 'abnormal'
+ self.db.save_ping_status(
+ vps_id=vps['vps_id'],
+ target=target,
+ status=status,
+ latency_ms=latency_ms
+ )
+
+ # 如果Ping成功,直接更新VPS状态为'on',无需调用API
+ if is_alive:
+ self.db.update_vps_status(vps['vps_id'], 'on')
+ success_count += 1
+ self.logger.debug(f"VPS {vps['vps_id']} Ping成功,状态更新为 on")
+ else:
+ fail_count += 1
+ self.logger.debug(f"VPS {vps['vps_id']} Ping失败,需要进一步检查")
- if unreachable_domains:
- self.logger.warning(f"发现 {len(unreachable_domains)} 个域名访问异常: {', '.join(unreachable_domains)}")
- else:
- self.logger.info("所有域名HTTP检测正常")
-
- return unreachable_domains
+ self.logger.info(f"Ping检测完成: 成功{success_count}台, 失败{fail_count}台")
- def check_and_power_on(self, unreachable_hosts_or_domains):
- """检查并开机无法访问的VPS
+ def check_and_power_on_unreachable_vps(self):
+ """检查无法访问的VPS并尝试开机"""
+ self.logger.info("检查需要开机的VPS...")
- Args:
- unreachable_hosts_or_domains:
- - ping模式: [{'id': xxx, 'ip': xxx, ...}, ...]
- - http模式: ['domain1.com', 'domain2.com', ...]
- """
- if not unreachable_hosts_or_domains:
- self.logger.info("未发现需要处理的异常情况")
+ # 获取所有标记为需要监控的VPS
+ monitored_vps = self.db.get_monitored_vps()
+
+ if not monitored_vps:
+ self.logger.info("没有标记为需要监控的VPS")
return
- way = self.config.get('WAY', 'ping')
+ # 获取今天ping失败的VPS
+ today = datetime.now().strftime('%Y-%m-%d')
+ unreachable_vps = []
- # 如果是HTTP模式,需要先找到域名对应的VPS
- if way == 'http':
- unreachable_domains = unreachable_hosts_or_domains
- self.logger.info(f"开始查找 {len(unreachable_domains)} 个异常域名对应的VPS...")
+ for vps in monitored_vps:
+ # 检查今天的ping记录
+ records = self.db.get_ping_records(vps['vps_id'], today)
- # 获取VPS列表建立域名到ID的映射
- vps_data = self.get_vps_list()
- if not vps_data or 'host' not in vps_data:
- self.logger.error("无法获取VPS列表")
- return
-
- # 建立域名到VPS的映射
- domain_to_vps = {}
- for host in vps_data['host']:
- domain = host.get('domain', '')
- if domain:
- domain_to_vps[domain] = host
-
- # 转换域名为VPS信息
- unreachable_hosts = []
- for domain in unreachable_domains:
- if domain in domain_to_vps:
- host = domain_to_vps[domain]
- ip = host.get('dedicatedip', '')
-
- # 检查是否为例外IP
- if ip and ip in self.exception_ips:
- self.logger.info(f"域名 {domain} 对应的VPS {host['id']} ({ip}) 属于例外IP,跳过")
- continue
-
- unreachable_hosts.append({
- 'id': host['id'],
- 'ip': ip,
- 'domain': domain,
- 'product_name': host.get('product_name', '')
- })
- else:
- self.logger.warning(f"域名 {domain} 未找到对应的VPS,跳过")
- else:
- # ping模式,直接使用传入的数据
- unreachable_hosts = unreachable_hosts_or_domains
+ # 如果今天有记录且最后一次是abnormal
+ if records and records[-1]['status'] == 'abnormal':
+ unreachable_vps.append(vps)
- if not unreachable_hosts:
- self.logger.info("所有异常的VPS都属于例外IP或找不到对应VPS,无需操作")
+ if not unreachable_vps:
+ self.logger.info("所有监控中的VPS都正常")
return
- self.logger.info(f"开始检查 {len(unreachable_hosts)} 台VPS的实际状态...")
+ self.logger.info(f"发现 {len(unreachable_vps)} 台VPS Ping失败,需要检查实际状态")
+ # 对每个异常的VPS调用API检查实际状态并尝试开机
need_power_on = []
- all_are_on = True
- for host_info in unreachable_hosts:
- host_id = host_info['id']
+ for vps in unreachable_vps:
+ config_id = vps['config_id']
+ vps_id = vps['vps_id']
- self.logger.info(f"检查VPS {host_id} ({host_info.get('domain', '')}) 的实际状态...")
- status_data = self.get_vps_status(host_id)
+ if config_id not in self.adapters:
+ self.logger.warning(f"配置ID {config_id} 没有对应的适配器,跳过")
+ continue
+
+ adapter = self.adapters[config_id]
+
+ self.logger.info(f"调用API检查VPS {vps_id} 的实际状态...")
+ status_data = adapter.get_vps_status(vps_id)
if status_data:
status = status_data.get('status', 'unknown')
des = status_data.get('des', '未知')
+ # 更新数据库中的VPS状态
+ self.db.update_vps_status(vps_id, status)
+ self.logger.info(f"VPS {vps_id} API返回状态: {des} ({status})")
+
if status == 'on':
- self.logger.info(f"VPS {host_id} 实际状态: {des} (开机中)")
+ self.logger.info(f"VPS {vps_id} 实际状态为开机,可能是禁Ping或网络问题")
else:
- self.logger.warning(f"VPS {host_id} 实际状态: {des} (关机)")
- need_power_on.append(host_info)
- all_are_on = False
+ self.logger.warning(f"VPS {vps_id} 实际状态为关机,需要开机")
+ need_power_on.append((config_id, vps_id))
else:
- self.logger.error(f"无法获取VPS {host_id} 的状态")
- all_are_on = False
+ self.logger.error(f"无法获取VPS {vps_id} 的状态")
+ # API调用失败,也尝试开机
+ need_power_on.append((config_id, vps_id))
- # 如果所有VPS都是开机状态,记录日志
- if all_are_on:
- self.logger.info(
- "检测到所有VPS均为开机状态,可能是禁Ping、CDN缓存或网站临时异常,无需操作"
- )
+ if not need_power_on:
+ self.logger.info("所有异常VPS实际都是开机状态,无需操作")
return
# 对需要开机的VPS执行开机操作
- if need_power_on:
- self.logger.info(f"开始对 {len(need_power_on)} 台VPS执行开机操作...")
-
- for host_info in need_power_on:
- host_id = host_info['id']
- self.logger.info(f"正在开启VPS {host_id} ({host_info.get('domain', '')})...")
- self.power_on_vps(host_id)
-
- # 等待60秒后验证开机结果
- self.logger.info("等待60秒后验证开机结果...")
- time.sleep(60)
-
- # 验证开机结果,最多尝试2次
- self.verify_power_on_result(need_power_on, max_retries=2)
+ self.logger.info(f"开始对 {len(need_power_on)} 台VPS执行开机操作...")
+
+ for config_id, vps_id in need_power_on:
+ adapter = self.adapters[config_id]
+ self.logger.info(f"正在开启VPS {vps_id}...")
+ adapter.power_on(vps_id)
+
+ # 等待60秒后验证开机结果
+ self.logger.info("等待60秒后验证开机结果...")
+ time.sleep(60)
+
+ # 验证开机结果,最多尝试2次
+ self.verify_power_on_result(need_power_on, max_retries=2)
- def verify_power_on_result(self, hosts_to_verify, max_retries=2):
- """验证开机结果"""
- self.retry_count += 1
+ def verify_power_on_result(self, vps_list, max_retries=2):
+ """验证开机结果
- if self.retry_count > max_retries:
- self.logger.warning(f"已达到最大重试次数({max_retries}),仍有VPS未成功开机")
- for host_info in hosts_to_verify:
- self.logger.warning(
- f"VPS {host_info['id']} ({host_info.get('domain', '')}) 开机失败"
- )
- self.retry_count = 0
- return
+ Args:
+ vps_list: [(config_id, vps_id), ...]
+ max_retries: 最大重试次数
+ """
+ retry_count = 0
- still_off = []
-
- for host_info in hosts_to_verify:
- host_id = host_info['id']
+ while retry_count < max_retries:
+ retry_count += 1
+ still_off = []
- self.logger.info(f"验证VPS {host_id} 开机状态...")
- status_data = self.get_vps_status(host_id)
+ for config_id, vps_id in vps_list:
+ adapter = self.adapters[config_id]
+
+ self.logger.info(f"验证VPS {vps_id} 开机状态 (第{retry_count}次)...")
+ status_data = adapter.get_vps_status(vps_id)
+
+ if status_data and status_data.get('status') == 'on':
+ # 开机成功,更新数据库状态
+ self.db.update_vps_status(vps_id, 'on')
+ self.logger.info(f"✅ VPS {vps_id} 开机成功,状态已更新")
+ else:
+ # 开机失败,获取实际状态并更新数据库
+ actual_status = status_data.get('status', 'unknown') if status_data else 'unknown'
+ actual_des = status_data.get('des', '未知') if status_data else '无法获取'
+ self.db.update_vps_status(vps_id, actual_status)
+ self.logger.warning(f"❌ VPS {vps_id} 开机失败,实际状态: {actual_des} ({actual_status}),状态已更新")
+ still_off.append((config_id, vps_id))
- if status_data and status_data.get('status') == 'on':
- self.logger.info(f"✅ VPS {host_id} 开机成功")
+ if not still_off:
+ self.logger.info("所有VPS开机验证完成")
+ return
+
+ if retry_count < max_retries:
+ self.logger.info(f"还有 {len(still_off)} 台VPS未开机,进行第{retry_count}次重试...")
+ for config_id, vps_id in still_off:
+ adapter = self.adapters[config_id]
+ adapter.power_on(vps_id)
+
+ self.logger.info("等待60秒后再次验证...")
+ time.sleep(60)
+
+ # 达到最大重试次数,更新所有仍失败的VPS状态
+ self.logger.warning(f"已达到最大重试次数({max_retries}),仍有VPS未成功开机")
+ for config_id, vps_id in still_off:
+ adapter = self.adapters[config_id]
+ # 最后一次查询状态
+ status_data = adapter.get_vps_status(vps_id)
+ if status_data:
+ final_status = status_data.get('status', 'unknown')
+ self.db.update_vps_status(vps_id, final_status)
+ self.logger.warning(f"VPS {vps_id} 最终状态: {status_data.get('des', '未知')} ({final_status})")
else:
- self.logger.warning(f"❌ VPS {host_id} 仍未开机")
- still_off.append(host_info)
-
- if still_off:
- self.logger.info(f"还有 {len(still_off)} 台VPS未开机,进行第{self.retry_count}次重试...")
- for host_info in still_off:
- self.power_on_vps(host_info['id'])
-
- self.logger.info("等待60秒后再次验证...")
- time.sleep(60)
-
- self.verify_power_on_result(still_off, max_retries)
- else:
- self.logger.info("所有VPS开机验证完成")
- self.retry_count = 0
+ self.db.update_vps_status(vps_id, 'unknown')
+ self.logger.warning(f"VPS {vps_id} 无法获取最终状态")
- def run_once(self):
+ def generate_daily_summary(self):
+ """生成前一天的VPS摘要统计(每天0点执行)"""
+ self.logger.info("开始生成昨日VPS摘要统计...")
+
+ yesterday = (datetime.now() - timedelta(days=1)).strftime('%Y-%m-%d')
+ vps_list = self.db.get_all_vps()
+
+ processed_count = 0
+
+ for vps in vps_list:
+ vps_id = vps['vps_id']
+
+ # 查询昨天的所有ping记录
+ records = self.db.get_ping_records(vps_id, yesterday)
+
+ if not records:
+ self.logger.debug(f"VPS {vps_id} 昨天没有ping记录,跳过")
+ continue
+
+ # 计算统计数据
+ latencies = [r['latency_ms'] for r in records if r['latency_ms']]
+
+ if not latencies:
+ avg_latency = 0
+ max_latency = 0
+ min_latency = 0
+ else:
+ avg_latency = sum(latencies) / len(latencies)
+ max_latency = max(latencies)
+ min_latency = min(latencies)
+
+ # 分级统计
+ count_under_100 = sum(1 for l in latencies if l < 100)
+ count_100_to_300 = sum(1 for l in latencies if 100 <= l < 300)
+ count_300_to_500 = sum(1 for l in latencies if 300 <= l < 500)
+ count_abnormal = sum(1 for r in records if r['status'] == 'abnormal')
+
+ # 计算可用性评分
+ total = len(records)
+ if total > 0:
+ excellent_ratio = count_under_100 / total
+ good_ratio = (count_under_100 + count_100_to_300) / total
+ normal_ratio = (count_under_100 + count_100_to_300 + count_300_to_500) / total
+
+ if excellent_ratio > 0.8:
+ availability = '优秀'
+ elif good_ratio > 0.8:
+ availability = '良好'
+ elif normal_ratio > 0.8:
+ availability = '一般'
+ else:
+ availability = '不可用'
+ else:
+ availability = '不可用'
+
+ # 保存到数据库
+ self.db.save_vps_summary(
+ vps_id=vps_id,
+ date=yesterday,
+ avg_latency=avg_latency,
+ max_latency=max_latency,
+ min_latency=min_latency,
+ count_under_100=count_under_100,
+ count_100_to_300=count_100_to_300,
+ count_300_to_500=count_300_to_500,
+ count_abnormal=count_abnormal,
+ availability=availability
+ )
+
+ processed_count += 1
+
+ self.logger.info(f"摘要统计生成完成,共处理{processed_count}台VPS")
+
+ def cleanup_old_data(self):
+ """清理超过30天的ping状态数据"""
+ deleted = self.db.cleanup_old_ping_records(days=30)
+ if deleted > 0:
+ self.logger.info(f"已清理{deleted}条旧的ping记录")
+
+ def refresh_vps_list(self):
+ """刷新所有配置的VPS列表"""
+ self.logger.info("开始刷新VPS列表...")
+
+ configs = self.db.get_all_configs()
+
+ for config in configs:
+ config_id = config['id']
+
+ if config_id not in self.adapters:
+ self.logger.warning(f"配置ID {config_id} 没有对应的适配器,跳过")
+ continue
+
+ adapter = self.adapters[config_id]
+
+ self.logger.info(f"正在获取配置 {config_id} 的VPS列表...")
+ vps_data = adapter.get_vps_list()
+
+ if not vps_data or 'host' not in vps_data:
+ self.logger.error(f"配置 {config_id} 获取VPS列表失败")
+ continue
+
+ # 准备批量插入的数据
+ vps_list_to_add = []
+
+ for host in vps_data['host']:
+ # 解析详细信息
+ cpu_cores = None
+ memory_size = None
+ disk_size = None
+ bandwidth = None
+ os_type = None
+
+ if 'config_option' in host and isinstance(host['config_option'], list):
+ for option in host['config_option']:
+ key = option.get('key')
+ value = option.get('value')
+
+ if key == 'cpu' and value:
+ # 提取数字,例如 "16核" -> 16
+ import re
+ match = re.search(r'(\d+)', value)
+ if match:
+ cpu_cores = int(match.group(1))
+
+ elif key == 'memory' and value:
+ memory_size = value # 例如 "16G"
+
+ elif key == 'system_disk_size' and value:
+ disk_size = value # 例如 "Lin50G,Win50G"
+
+ elif key == 'bw' and value:
+ bandwidth = value # 例如 "70Mbps"
+
+ elif key == 'os' and value:
+ os_type = value # 例如 "Debian-12.0_x64"
+
+ vps_list_to_add.append({
+ 'config_id': config_id,
+ 'vps_id': host['id'],
+ 'domain': host.get('domain'),
+ 'ip_address': host.get('dedicatedip'),
+ 'product_name': host.get('product_name'),
+ 'cpu_cores': cpu_cores,
+ 'memory_size': memory_size,
+ 'disk_size': disk_size,
+ 'bandwidth': bandwidth,
+ 'os_type': os_type,
+ 'section': config['auto_monitor'] # 根据配置的auto_monitor设置
+ })
+
+ # 批量添加到数据库
+ self.db.batch_add_vps(vps_list_to_add)
+
+ self.logger.info(f"配置 {config_id} 已更新 {len(vps_list_to_add)} 台VPS")
+
+ self.logger.info("VPS列表刷新完成")
+
+ def run_monitoring_cycle(self):
"""执行一次监控循环"""
try:
self.logger.info("\n" + "=" * 60)
- self.logger.info(f"开始第 {int(time.time())} 时间戳的监控循环")
+ self.logger.info(f"开始监控循环 - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
self.logger.info("=" * 60)
- # 检测主机(不调用API)
- unreachable = self.detect_hosts()
+ # 1. 对所有VPS进行ping测试
+ self.ping_and_record_status()
- # 只有在检测到异常时才进入API流程
- if unreachable:
- self.logger.info("检测到异常,开始调用API进行进一步检查和开机...")
- self.check_and_power_on(unreachable)
- else:
- self.logger.info("所有检测正常,无需调用API")
+ # 2. 检查并开机无法访问的VPS
+ self.check_and_power_on_unreachable_vps()
+
+ # 3. 清理旧数据
+ self.cleanup_old_data()
self.logger.info("本次监控循环完成\n")
except Exception as e:
self.logger.error(f"监控循环异常: {str(e)}", exc_info=True)
+ def setup_schedule(self):
+ """设置定时任务"""
+ # 每天0点执行摘要统计
+ schedule.every().day.at("00:00").do(self.generate_daily_summary)
+
+ self.logger.info("定时任务已设置: 每天00:00生成摘要统计")
+
def run(self):
"""运行监控程序(主循环)"""
- span = self.config.get('SPAN', 300)
+ # 加载配置和创建适配器
+ self.load_configs_and_create_adapters()
+
+ if not self.adapters:
+ self.logger.warning("没有可用的平台配置,请先通过网页添加配置")
+ self.logger.info("程序将在60秒后退出")
+ time.sleep(60)
+ return
+
+ # 设置定时任务
+ self.setup_schedule()
+
+ # 首次运行时刷新VPS列表
+ self.logger.info("首次运行,刷新VPS列表...")
+ self.refresh_vps_list()
+
+ span = 300 # 默认5分钟间隔
self.logger.info(f"监控程序开始运行,间隔 {span} 秒")
try:
while True:
- self.run_once()
+ # 执行监控循环
+ self.run_monitoring_cycle()
+
+ # 运行待执行的定时任务
+ schedule.run_pending()
self.logger.info(f"等待 {span} 秒后进行下一次检测...")
time.sleep(span)
@@ -577,16 +806,10 @@ class IDCMonitor:
self.logger.error(f"程序运行异常: {str(e)}", exc_info=True)
sys.exit(1)
-
-def main():
- """主函数"""
+if __name__ == '__main__':
try:
- monitor = IDCMonitor()
+ monitor = MonitorService()
monitor.run()
except Exception as e:
print(f"程序启动失败: {str(e)}")
sys.exit(1)
-
-
-if __name__ == '__main__':
- main()
\ No newline at end of file
diff --git a/app/requirements.txt b/app/requirements.txt
index fda3075..a10d395 100644
--- a/app/requirements.txt
+++ b/app/requirements.txt
@@ -1,2 +1,3 @@
requests
-pyyaml
\ No newline at end of file
+pyyaml
+schedule
\ No newline at end of file
diff --git a/app/uninstall.sh b/app/uninstall.sh
index 135891b..da5bcca 100644
--- a/app/uninstall.sh
+++ b/app/uninstall.sh
@@ -1,6 +1,6 @@
#!/bin/bash
-# 核云IDC服务商VPS自动监测重启程序 - 卸载脚本
+# VPS Hub 监控程序 - 卸载脚本
# 该脚本会移除systemd服务
# 颜色定义
@@ -13,7 +13,7 @@ NC='\033[0m' # No Color
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# 定义服务名称
-SERVICE_NAME="idc-monitor"
+SERVICE_NAME="idc_monitor"
SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}.service"
echo "=========================================="
diff --git a/app/开发文档.md b/app/开发文档.md
deleted file mode 100644
index 67334b9..0000000
--- a/app/开发文档.md
+++ /dev/null
@@ -1,39 +0,0 @@
-### 程序运行
-
-1. 用户需先向程序config内添加以下几项定义量:
- ACCOUNT,API_KEY,WAY
-
-2. 用户运行install.sh脚本
- 脚本会先检测config.yml中的配置文件
- 缺少ACCOUNT,API_KEY,WAY向用户询问填写什么,同时WAY默认为ping(问句以红色展示)
- 若用户填写http,则让用户填写域名(可以访问的路径),通过英文逗号分隔域名
- 随后用户可添加例外参数,脚本检测到例外内的IP主机关机时,直接跳过,不进行开机
-
-3. 脚本运行
- 创建名为idc_monitor的systemd服务,持续化运行当前路径下的main.py
-
-### 程序逻辑
-1. 1. 隔一段时间(SPAN)便根据WAY来探测目标存活
- 若为ping,则直接ping一遍所有的IP地址
- 若为域名,则使用HEAD方法探测相应域名是否正常响应
-
-1. 2. 若存在不通的情况,进行如下操作
- 使用config.yml中的JWT请求VPS列表并挨个查询VPS状态是否为on(开机)
- 若返回响应码为405,即响应报文为
- ```
- {
- "status": 405,
- "msg": "请登陆后再试"
- }
- ```
- 时,重新请求JWT并存储,随后再次请求VPS列表并查询状态
-
-1. 3. 若均为开机状态,记录日志:什么时间点 - 发生了什么情况,实际没有机器关机,可能是禁ping或者服务器网站状态异常
-
-1. 4. 若发现有机器关机,则使用开机接口(on)进行开机
-
-1. 5. 所有机器都操作完成后隔60秒,再次查询刚才尝试开机操作的几台机器是否开机成功,若不超过则再来一遍
-
-1. 6. 尝试开机操作两次后还是非开机状态,则中断本次循环,进入SPAN间隔准备下一次循环
-
-1. 7. 以上所有操作均需记录为日志,分为正常日志和异常日志(例如:未发现存在机器关机,开机失败等)
\ No newline at end of file
diff --git a/config_add.php b/config_add.php
new file mode 100644
index 0000000..f053af3
--- /dev/null
+++ b/config_add.php
@@ -0,0 +1,424 @@
+\n";
+
+ if (file_put_contents($tokenFile, $content)) {
+ chmod($tokenFile, 0600);
+ header('Location: config_add.php?pass=' . urlencode($apiPass));
+ exit;
+ } else {
+ $setupError = '保存失败,请检查目录权限!';
+ }
+ }
+}
+
+// 如果需要设置密码,显示设置页面
+if ($needSetup) {
+ ?>
+
+
+
+
+
+
+
+ 初始设置 - VPS Hub
+
+
+
+
+
+
+
+ ❌
+
+
+
+
+ 💡 提示:此密码用于保护配置管理页面的访问权限。
+ 密码要求:至少8位,只能包含字母(a-z,A-Z)和数字(0-9)
+
+
+
+
+
+
+
+
+ 403, 'msg' => '访问密码错误'], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
+ exit;
+}
+
+// 处理添加配置
+$message = '';
+if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+ if (isset($_POST['add_config'])) {
+ $apiLabel = trim($_POST['api_label']);
+ $siteType = trim($_POST['site_type']);
+ $siteUrl = trim($_POST['site_url']);
+ $account = trim($_POST['account']);
+ $apiKey = trim($_POST['api_key']);
+ $autoMonitor = isset($_POST['auto_monitor']) ? 1 : 0;
+
+ if (empty($apiLabel) || empty($siteType) || empty($siteUrl) || empty($account) || empty($apiKey)) {
+ $message = '❌ API标识、网站类型、网站链接、账户和密钥不能为空!
';
+ } else {
+ // 验证URL格式:不允许包含路径
+ $parsedUrl = parse_url($siteUrl);
+ if (!$parsedUrl || !isset($parsedUrl['scheme']) || !isset($parsedUrl['host'])) {
+ $message = '❌ 网站链接格式错误!请输入完整的URL(例如: https://www.example.com)
';
+ } elseif (isset($parsedUrl['path']) && $parsedUrl['path'] !== '/') {
+ $message = '❌ 网站链接严禁包含路径部分!只允许输入根域名(例如: https://www.example.com),不要添加 /v1、/api 等路径
';
+ } else {
+ $db = getVpsDB();
+
+ // 检查API标识是否已存在
+ $existingConfig = $db->queryOne('SELECT id FROM configs WHERE api_label = ?', [$apiLabel]);
+ if ($existingConfig) {
+ $message = '❌ API标识 "' . htmlspecialchars($apiLabel) . '" 已存在,请使用其他标识!
';
+ } else {
+ $configId = $db->insert(
+ 'INSERT INTO configs (api_label, site_type, site_url, account, api_key, auto_monitor) VALUES (?, ?, ?, ?, ?, ?)',
+ [$apiLabel, $siteType, $siteUrl, $account, $apiKey, $autoMonitor]
+ );
+
+ if ($configId) {
+ $message = '✅ 配置添加成功!ID: ' . $configId . '
';
+
+ // 如果是首次添加配置,提示运行install.sh
+ $configCount = $db->queryOne('SELECT COUNT(*) as count FROM configs')['count'];
+ if ($configCount == 1) {
+ $message .= '💡 这是第一个配置,请运行 app/install.sh 安装监控服务
';
+
+ // 尝试自动执行install.sh(需要PHP有执行权限)
+ $installScript = __DIR__ . '/app/install.sh';
+ if (file_exists($installScript)) {
+ // 检查exec函数是否可用
+ if (!function_exists('exec')) {
+ $message .= '⚠️ exec函数被禁用,无法自动安装监控服务。请手动执行: sudo bash app/install.sh
';
+ } else {
+ // 先设置可执行权限
+ chmod($installScript, 0755);
+
+ // 执行安装脚本
+ $output = [];
+ $returnVar = 0;
+ exec("bash {$installScript} 2>&1", $output, $returnVar);
+
+ if ($returnVar === 0) {
+ $message .= '✅ 监控服务已自动安装并启动
';
+ } else {
+ $message .= '⚠️ 自动安装失败,请手动执行: sudo bash app/install.sh
';
+ // 输出错误信息以便调试
+ if (!empty($output)) {
+ $message .= '查看错误详情
' . htmlspecialchars(implode("\n", $output)) . ' ';
+ }
+ }
+ }
+ }
+ }
+
+ // 自动刷新VPS列表
+ if ($siteType === 'mofang') {
+ refreshVpsListForConfig($configId);
+ $message .= '✅ 已自动获取VPS列表
';
+ }
+ } else {
+ $message = '❌ 配置添加失败!
';
+ }
+ }
+ }
+ }
+ } elseif (isset($_POST['delete_config'])) {
+ $configId = intval($_POST['config_id']);
+ $db = getVpsDB();
+ $db->execute('DELETE FROM configs WHERE id = ?', [$configId]);
+ $message = '✅ 配置已删除
';
+ }
+}
+
+// 获取所有配置
+$db = getVpsDB();
+$configs = $db->query('SELECT * FROM configs ORDER BY id');
+?>
+
+
+
+
+
+
+
+ 配置管理 - VPS Hub
+
+
+
+
+
+
+
+
diff --git a/index.php b/index.php
new file mode 100644
index 0000000..1b24421
--- /dev/null
+++ b/index.php
@@ -0,0 +1,540 @@
+ 403, 'msg' => '访问密码错误或者未输入密码,请拼接?pass=API_PASS后再尝试访问'], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
+ exit;
+}
+
+// 处理操作请求
+$message = '';
+if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+ $action = $_POST['action'] ?? '';
+ $hostId = intval($_POST['host_id'] ?? 0);
+ $configId = intval($_POST['config_id'] ?? 0);
+
+ if ($action && $hostId > 0 && $configId > 0) {
+ $result = null;
+
+ if ($action === 'reboot') {
+ $result = mofangHardReboot($configId, $hostId);
+ } elseif ($action === 'on') {
+ $result = mofangPowerOn($configId, $hostId);
+ } elseif ($action === 'off') {
+ $result = mofangPowerOff($configId, $hostId);
+ }
+
+ if ($result && isset($result['status']) && $result['status'] === 200) {
+ $message = '✅ 操作成功
VPS #' . $hostId . ' 已成功执行操作
';
+ } else {
+ $errorMsg = $result['msg'] ?? '未知错误';
+ $message = '❌ 操作失败
' . htmlspecialchars($errorMsg) . '
';
+ }
+ } elseif ($action === 'refresh_vps_list') {
+ // 手动刷新VPS列表
+ $count = refreshAllVpsLists();
+ $message = '✅ 刷新完成
已成功刷新 ' . $count . ' 个配置的VPS列表
';
+
+ // 获取每个VPS的详细信息并更新数据库
+ $listDb = getVpsListDB();
+ $configDb = getVpsDB();
+ $configs = $configDb->query('SELECT * FROM configs');
+
+ foreach ($configs as $config) {
+ if ($config['site_type'] !== 'mofang') {
+ continue; // 目前只支持魔方平台
+ }
+
+ // 获取该配置下的所有VPS
+ $vpsItems = $listDb->query('SELECT vps_id FROM vps_list WHERE config_id = ?', [$config['id']]);
+
+ foreach ($vpsItems as $vpsItem) {
+ $vpsId = $vpsItem['vps_id'];
+
+ // 获取VPS详细信息
+ $details = mofangGetVpsDetails($config['id'], $vpsId);
+
+ if ($details && isset($details['status']) && $details['status'] === 200 && isset($details['data']['host'])) {
+ $host = $details['data']['host'];
+
+ // 解析CPU和内存信息
+ $cpuCores = null;
+ $memorySize = null;
+
+ if (isset($host['config_option']) && is_array($host['config_option'])) {
+ foreach ($host['config_option'] as $option) {
+ if ($option['key'] === 'cpu' && isset($option['value'])) {
+ // 提取数字,例如 "16核" -> 16
+ preg_match('/(\d+)/', $option['value'], $matches);
+ if (!empty($matches)) {
+ $cpuCores = intval($matches[1]);
+ }
+ }
+ if ($option['key'] === 'memory' && isset($option['value'])) {
+ $memorySize = $option['value']; // 例如 "16G"
+ }
+ }
+ }
+
+ // 更新数据库
+ $listDb->execute(
+ 'UPDATE vps_list SET cpu_cores = ?, memory_size = ?, last_check = CURRENT_TIMESTAMP WHERE config_id = ? AND vps_id = ?',
+ [$cpuCores, $memorySize, $config['id'], $vpsId]
+ );
+ }
+ }
+ }
+
+ $message .= '✅ 已更新VPS详细信息(CPU、内存等)
';
+ }
+}
+
+// 获取所有配置
+$db = getVpsDB();
+$configs = $db->query('SELECT * FROM configs ORDER BY id');
+
+// 构建配置ID到配置的映射
+$configMap = [];
+foreach ($configs as $config) {
+ $configMap[$config['id']] = $config;
+}
+
+// 获取所有VPS列表(只从vpslist.db查询)
+$listDb = getVpsListDB();
+$vpsList = $listDb->query('SELECT * FROM vps_list ORDER BY config_id, vps_id');
+
+// 为每个VPS添加site_type和site_url信息
+foreach ($vpsList as &$vps) {
+ $configId = $vps['config_id'];
+ if (isset($configMap[$configId])) {
+ $vps['site_type'] = $configMap[$configId]['site_type'];
+ // 修正site_url:移除末尾的/v1或/v1/
+ $siteUrl = $configMap[$configId]['site_url'];
+ $siteUrl = rtrim($siteUrl, '/'); // 移除末尾的 /
+ if (substr($siteUrl, -3) === '/v1') {
+ $siteUrl = substr($siteUrl, 0, -3); // 移除 /v1
+ }
+ $vps['site_url'] = $siteUrl;
+ } else {
+ $vps['site_type'] = 'unknown';
+ $vps['site_url'] = '';
+ }
+}
+unset($vps); // 解除引用
+
+// 调试信息(临时)
+error_log("Configs count: " . count($configs));
+error_log("VPS List count: " . count($vpsList));
+if (!empty($vpsList)) {
+ error_log("First VPS: " . json_encode($vpsList[0]));
+}
+
+// 按配置分组
+$vpsByConfig = [];
+foreach ($vpsList as $vps) {
+ $configId = $vps['config_id'];
+ if (!isset($vpsByConfig[$configId])) {
+ $vpsByConfig[$configId] = [];
+ }
+ $vpsByConfig[$configId][] = $vps;
+}
+
+// 统计信息
+$totalCount = count($vpsList);
+$runningCount = 0;
+$offCount = 0;
+
+foreach ($vpsList as $vps) {
+ if ($vps['status'] === 'on') {
+ $runningCount++;
+ } elseif ($vps['status'] === 'off') {
+ $offCount++;
+ }
+}
+?>
+
+
+
+
+
+
+
+ VPS管理面板 - VPS Hub
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 总计: 台服务器
+
+
+ 运行中: 台
+
+
+ 已关机: 台
+
+
+
+
+
+
+
+ '魔方平台',
+ 'aliyun' => '阿里云',
+ 'tencent' => '腾讯云'
+ ];
+ $typeName = $typeMap[$config['site_type']] ?? $config['site_type'];
+ ?>
+
+
+
+ ()
+ - 台VPS
+
+
+
+
+
此配置下暂无VPS,请点击"手动刷新VPS列表"获取
+
+
+
+
+
+
+
+
+
+
+
+ IP地址
+
+
+
+
+
+
+ 产品名称
+
+
+
+
+
+
+ CPU/内存
+
+
+
+
+
+
+
+
+ 磁盘/带宽/系统
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mofangidc.php b/mofangidc.php
new file mode 100644
index 0000000..7785631
--- /dev/null
+++ b/mofangidc.php
@@ -0,0 +1,723 @@
+ $token,
+ 'timestamp' => time()
+ ];
+
+ saveTokensFile($cached_tokens);
+}
+
+/**
+ * 保存tokens数组到文件
+ * @param array $cached_tokens tokens数组
+ */
+function saveTokensFile($cached_tokens) {
+ $content = "\n";
+
+ file_put_contents(TOKEN_CACHE_FILE, $content);
+ chmod(TOKEN_CACHE_FILE, 0600);
+}
+
+/**
+ * 魔方平台登录获取JWT Token
+ * @param string $siteUrl API基础URL
+ * @param string $account 账户
+ * @param string $apiKey API密钥
+ * @return string|null JWT Token或null
+ */
+function mofangLogin($siteUrl, $account, $apiKey) {
+ try {
+ $url = rtrim($siteUrl, '/') . '/v1/login_api';
+ $data = [
+ 'account' => $account,
+ 'password' => $apiKey
+ ];
+
+ $ch = curl_init();
+ curl_setopt($ch, CURLOPT_URL, $url);
+ curl_setopt($ch, CURLOPT_POST, true);
+ curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
+ curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
+ curl_setopt($ch, CURLOPT_TIMEOUT, 10);
+
+ $response = curl_exec($ch);
+ $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+ curl_close($ch);
+
+ if ($httpCode !== 200) {
+ error_log("魔方登录请求失败,HTTP状态码: {$httpCode}");
+ return null;
+ }
+
+ $result = json_decode($response, true);
+
+ if (isset($result['status']) && $result['status'] === 200 && isset($result['jwt'])) {
+ return $result['jwt'];
+ } else {
+ error_log("魔方登录失败: " . ($result['msg'] ?? '未知错误'));
+ return null;
+ }
+
+ } catch (Exception $e) {
+ error_log("魔方登录异常: " . $e->getMessage());
+ return null;
+ }
+}
+
+/**
+ * 获取有效的Token(先查缓存,没有则重新登录)
+ * @param int $configId 配置ID
+ * @return string|null Token或null
+ */
+function getValidToken($configId) {
+ // 先尝试从缓存获取
+ $token = getCachedToken($configId);
+ if ($token) {
+ return $token;
+ }
+
+ // 缓存不存在或已过期,重新获取
+ $db = getVpsDB();
+ $config = $db->queryOne('SELECT * FROM configs WHERE id = ?', [$configId]);
+
+ if (!$config) {
+ error_log("配置ID {$configId} 不存在");
+ return null;
+ }
+
+ $token = mofangLogin($config['site_url'], $config['account'], $config['api_key']);
+
+ if ($token) {
+ saveToken($configId, $token);
+ }
+
+ return $token;
+}
+
+/**
+ * 发送魔方API请求
+ * @param string $siteUrl API基础URL
+ * @param string $endpoint API端点
+ * @param string $method HTTP方法 (GET/POST/PUT)
+ * @param array $data 请求数据
+ * @param int $configId 配置ID
+ * @return array|null 响应数据或null
+ */
+function mofangApiRequest($siteUrl, $endpoint, $method = 'GET', $data = [], $configId = null) {
+ // 获取Token
+ if ($configId) {
+ $token = getValidToken($configId);
+ } else {
+ // 如果没有configId,尝试从第一个配置获取
+ $db = getVpsDB();
+ $configs = $db->query('SELECT * FROM configs LIMIT 1');
+ if (!$configs) {
+ return null;
+ }
+ $config = $configs[0];
+ $token = getValidToken($config['id']);
+ $siteUrl = $config['site_url'];
+ }
+
+ if (!$token) {
+ return ['status' => 400, 'msg' => '获取Token失败'];
+ }
+
+ $url = rtrim($siteUrl, '/') . $endpoint;
+ $headers = [
+ 'Authorization: JWT ' . $token,
+ 'Content-Type: application/json'
+ ];
+
+ $ch = curl_init();
+ curl_setopt($ch, CURLOPT_URL, $url);
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
+ curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
+ curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
+ curl_setopt($ch, CURLOPT_TIMEOUT, 10);
+
+ if ($method === 'POST') {
+ curl_setopt($ch, CURLOPT_POST, true);
+ curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
+ } elseif ($method === 'PUT') {
+ curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
+ curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
+ }
+
+ $response = curl_exec($ch);
+ $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+ curl_close($ch);
+
+ if ($httpCode !== 200) {
+ return ['status' => $httpCode, 'msg' => 'HTTP请求失败'];
+ }
+
+ $result = json_decode($response, true);
+
+ // 如果Token失效(405),清除缓存并重试
+ if (isset($result['status']) && $result['status'] === 405 && $configId) {
+ // 清除缓存
+ $cached_tokens = [];
+ if (file_exists(TOKEN_CACHE_FILE)) {
+ include TOKEN_CACHE_FILE;
+ }
+ unset($cached_tokens[$configId]);
+ saveTokensFile($cached_tokens);
+
+ // 重试一次
+ return mofangApiRequest($siteUrl, $endpoint, $method, $data, $configId);
+ }
+
+ return $result;
+}
+
+/**
+ * 获取VPS列表
+ * @param int $configId 配置ID
+ * @param int $page 页码
+ * @param int $limit 每页数量
+ * @return array|null VPS列表数据或null
+ */
+function mofangGetVpsList($configId, $page = 1, $limit = 100) {
+ $db = getVpsDB();
+ $config = $db->queryOne('SELECT * FROM configs WHERE id = ?', [$configId]);
+
+ if (!$config) {
+ return null;
+ }
+
+ $endpoint = "/v1/hosts?page={$page}&limit={$limit}";
+ return mofangApiRequest($config['site_url'], $endpoint, 'GET', [], $configId);
+}
+
+/**
+ * 获取VPS状态
+ * @param int $configId 配置ID
+ * @param int $vpsId VPS ID
+ * @param bool $updateDb 是否更新数据库,默认true
+ * @return array|null 状态数据或null
+ */
+function mofangGetVpsStatus($configId, $vpsId, $updateDb = true) {
+ $db = getVpsDB();
+ $config = $db->queryOne('SELECT * FROM configs WHERE id = ?', [$configId]);
+
+ if (!$config) {
+ return null;
+ }
+
+ $endpoint = "/v1/hosts/{$vpsId}/module/status?type=host";
+ $result = mofangApiRequest($config['site_url'], $endpoint, 'GET', [], $configId);
+
+ // 如果获取成功且需要更新数据库
+ if ($updateDb && $result && isset($result['status']) && $result['status'] === 200 && isset($result['data'])) {
+ $statusData = $result['data'];
+ $status = $statusData['status'] ?? 'unknown';
+
+ // 只更新status字段,不影响其他字段
+ $listDb = getVpsListDB();
+
+ // 先检查记录是否存在
+ $existing = $listDb->queryOne(
+ 'SELECT id FROM vps_list WHERE config_id = ? AND vps_id = ?',
+ [$configId, $vpsId]
+ );
+
+ if ($existing) {
+ // 记录存在,执行UPDATE
+ $listDb->execute(
+ 'UPDATE vps_list SET status = ?, last_check = CURRENT_TIMESTAMP WHERE config_id = ? AND vps_id = ?',
+ [$status, $configId, $vpsId]
+ );
+ error_log("[mofangGetVpsStatus] VPS {$vpsId} 状态已更新为: {$status}");
+ } else {
+ // 记录不存在,插入新记录
+ $listDb->execute(
+ 'INSERT INTO vps_list (config_id, vps_id, status, last_check) VALUES (?, ?, ?, CURRENT_TIMESTAMP)',
+ [$configId, $vpsId, $status]
+ );
+ error_log("[mofangGetVpsStatus] VPS {$vpsId} 新记录已插入,状态: {$status}");
+ }
+ }
+
+ return $result;
+}
+
+/**
+ * 获取VPS详细信息
+ * @param int $configId 配置ID
+ * @param int $vpsId VPS ID
+ * @param bool $updateDb 是否更新数据库,默认true
+ * @return array|null 详细信息或null
+ */
+function mofangGetVpsDetails($configId, $vpsId, $updateDb = true) {
+ $db = getVpsDB();
+ $config = $db->queryOne('SELECT * FROM configs WHERE id = ?', [$configId]);
+
+ if (!$config) {
+ return null;
+ }
+
+ $endpoint = "/v1/hosts/{$vpsId}";
+ $result = mofangApiRequest($config['site_url'], $endpoint, 'GET', [], $configId);
+
+ // 如果获取成功且需要更新数据库
+ if ($updateDb && $result && isset($result['status']) && $result['status'] === 200 && isset($result['data']['host'])) {
+ $host = $result['data']['host'];
+
+ // 解析详细信息
+ $updates = []; // 存储要更新的字段
+ $values = [];
+
+ if (isset($host['config_option']) && is_array($host['config_option'])) {
+ foreach ($host['config_option'] as $option) {
+ switch ($option['key']) {
+ case 'cpu':
+ if (isset($option['value'])) {
+ preg_match('/(\d+)/', $option['value'], $matches);
+ if (!empty($matches)) {
+ $updates[] = 'cpu_cores = ?';
+ $values[] = intval($matches[1]);
+ }
+ }
+ break;
+ case 'memory':
+ if (isset($option['value'])) {
+ $updates[] = 'memory_size = ?';
+ $values[] = $option['value'];
+ }
+ break;
+ case 'system_disk_size':
+ if (isset($option['value'])) {
+ $updates[] = 'disk_size = ?';
+ $values[] = $option['value'];
+ }
+ break;
+ case 'bw':
+ if (isset($option['value'])) {
+ $updates[] = 'bandwidth = ?';
+ $values[] = $option['value'];
+ }
+ break;
+ case 'os':
+ if (isset($option['value'])) {
+ $updates[] = 'os_type = ?';
+ $values[] = $option['value'];
+ }
+ break;
+ }
+ }
+ }
+
+ // 只有当有字段需要更新时才执行UPDATE
+ if (!empty($updates)) {
+ // 添加last_check更新
+ $updates[] = 'last_check = CURRENT_TIMESTAMP';
+
+ // 构建动态UPDATE语句
+ $setClause = implode(', ', $updates);
+ $values[] = $configId;
+ $values[] = $vpsId;
+
+ $listDb = getVpsListDB();
+
+ // 先检查记录是否存在
+ $existing = $listDb->queryOne(
+ 'SELECT id FROM vps_list WHERE config_id = ? AND vps_id = ?',
+ [$configId, $vpsId]
+ );
+
+ if ($existing) {
+ // 记录存在,执行UPDATE
+ $listDb->execute(
+ "UPDATE vps_list SET {$setClause} WHERE config_id = ? AND vps_id = ?",
+ $values
+ );
+ error_log("[mofangGetVpsDetails] VPS {$vpsId} 详细信息已更新");
+ } else {
+ // 记录不存在,插入新记录(只插入获取到的字段)
+ $columns = [];
+ $insertValues = [];
+ $placeholders = [];
+
+ // 解析SET子句中的字段
+ foreach ($updates as $update) {
+ if (strpos($update, 'last_check') === false) {
+ list($column, $value) = explode(' = ', $update);
+ $columns[] = $column;
+ $insertValues[] = array_shift($values); // 从$values中取出对应的值
+ $placeholders[] = '?';
+ }
+ }
+
+ // 添加config_id和vps_id
+ $columns[] = 'config_id';
+ $columns[] = 'vps_id';
+ $insertValues[] = $configId;
+ $insertValues[] = $vpsId;
+
+ if (!empty($columns)) {
+ $columnStr = implode(', ', $columns);
+ $placeholderStr = implode(', ', $placeholders);
+ $listDb->execute(
+ "INSERT INTO vps_list ({$columnStr}) VALUES ({$placeholderStr})",
+ $insertValues
+ );
+ error_log("[mofangGetVpsDetails] VPS {$vpsId} 新记录已插入");
+ }
+ }
+ }
+ }
+
+ return $result;
+}
+
+/**
+ * VPS开机
+ * @param int $configId 配置ID
+ * @param int $vpsId VPS ID
+ * @return array|null 响应数据或null
+ */
+function mofangPowerOn($configId, $vpsId) {
+ $db = getVpsDB();
+ $config = $db->queryOne('SELECT * FROM configs WHERE id = ?', [$configId]);
+
+ if (!$config) {
+ return null;
+ }
+
+ $endpoint = "/v1/hosts/{$vpsId}/module/on";
+ return mofangApiRequest($config['site_url'], $endpoint, 'PUT', [], $configId);
+}
+
+/**
+ * VPS关机
+ * @param int $configId 配置ID
+ * @param int $vpsId VPS ID
+ * @return array|null 响应数据或null
+ */
+function mofangPowerOff($configId, $vpsId) {
+ $db = getVpsDB();
+ $config = $db->queryOne('SELECT * FROM configs WHERE id = ?', [$configId]);
+
+ if (!$config) {
+ return null;
+ }
+
+ $endpoint = "/v1/hosts/{$vpsId}/module/off";
+ return mofangApiRequest($config['site_url'], $endpoint, 'PUT', [], $configId);
+}
+
+/**
+ * VPS硬重启
+ * @param int $configId 配置ID
+ * @param int $vpsId VPS ID
+ * @return array|null 响应数据或null
+ */
+function mofangHardReboot($configId, $vpsId) {
+ $db = getVpsDB();
+ $config = $db->queryOne('SELECT * FROM configs WHERE id = ?', [$configId]);
+
+ if (!$config) {
+ return null;
+ }
+
+ $endpoint = "/v1/hosts/{$vpsId}/module/hard_reboot";
+ return mofangApiRequest($config['site_url'], $endpoint, 'PUT', [], $configId);
+}
+
+/**
+ * 刷新指定配置的VPS列表并保存到数据库
+ * @param int $configId 配置ID
+ * @return bool 是否成功
+ */
+function refreshVpsListForConfig($configId) {
+ $db = getVpsDB();
+ $config = $db->queryOne('SELECT * FROM configs WHERE id = ?', [$configId]);
+
+ if (!$config) {
+ return false;
+ }
+
+ // 获取VPS列表
+ $result = mofangGetVpsList($configId);
+
+ if (!$result || !isset($result['status']) || $result['status'] !== 200) {
+ error_log("获取VPS列表失败: " . ($result['msg'] ?? '未知错误'));
+ return false;
+ }
+
+ $hosts = $result['data']['host'] ?? [];
+
+ if (empty($hosts)) {
+ return true; // 没有VPS也算成功
+ }
+
+ // 批量保存到数据库
+ $listDb = getVpsListDB();
+
+ foreach ($hosts as $host) {
+ // 解析详细信息
+ $cpuCores = null;
+ $memorySize = null;
+ $diskSize = null;
+ $bandwidth = null;
+ $osType = null;
+
+ if (isset($host['config_option']) && is_array($host['config_option'])) {
+ foreach ($host['config_option'] as $option) {
+ switch ($option['key']) {
+ case 'cpu':
+ if (isset($option['value'])) {
+ // 提取数字,例如 "16核" -> 16
+ preg_match('/(\d+)/', $option['value'], $matches);
+ if (!empty($matches)) {
+ $cpuCores = intval($matches[1]);
+ }
+ }
+ break;
+ case 'memory':
+ if (isset($option['value'])) {
+ $memorySize = $option['value']; // 例如 "16G"
+ }
+ break;
+ case 'system_disk_size':
+ if (isset($option['value'])) {
+ $diskSize = $option['value']; // 例如 "Lin50G,Win50G"
+ }
+ break;
+ case 'bw':
+ if (isset($option['value'])) {
+ $bandwidth = $option['value']; // 例如 "70Mbps"
+ }
+ break;
+ case 'os':
+ if (isset($option['value'])) {
+ $osType = $option['value']; // 例如 "Debian-12.0_x64"
+ }
+ break;
+ }
+ }
+ }
+
+ // 使用INSERT OR REPLACE防止重复数据
+ // 注意:需要保留原有的status字段,避免被覆盖为NULL
+ $existing = $listDb->queryOne(
+ 'SELECT status FROM vps_list WHERE config_id = ? AND vps_id = ?',
+ [$configId, $host['id']]
+ );
+
+ $currentStatus = $existing ? $existing['status'] : null;
+
+ $listDb->execute(
+ 'INSERT OR REPLACE INTO vps_list (config_id, vps_id, domain, ip_address, product_name, cpu_cores, memory_size, disk_size, bandwidth, os_type, status, section, last_check) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)',
+ [
+ $configId,
+ $host['id'],
+ $host['domain'] ?? null,
+ $host['dedicatedip'] ?? null,
+ $host['product_name'] ?? null,
+ $cpuCores,
+ $memorySize,
+ $diskSize,
+ $bandwidth,
+ $osType,
+ $currentStatus, // 保留原有状态
+ $config['auto_monitor'] ? 1 : 0
+ ]
+ );
+ }
+
+ return true;
+}
+
+/**
+ * 刷新所有配置的VPS列表
+ * @return int 成功刷新的配置数量
+ */
+function refreshAllVpsLists() {
+ $db = getVpsDB();
+ $configs = $db->query('SELECT * FROM configs');
+
+ $successCount = 0;
+
+ foreach ($configs as $config) {
+ if (refreshVpsListForConfig($config['id'])) {
+ $successCount++;
+ }
+ }
+
+ return $successCount;
+}
+
+/**
+ * 获取单个VPS的状态并更新到数据库
+ * @param int $configId 配置ID
+ * @param int $vpsId VPS ID
+ * @return array|null 状态信息或null
+ */
+function updateVpsStatusToDb($configId, $vpsId) {
+ $db = getVpsDB();
+ $config = $db->queryOne('SELECT * FROM configs WHERE id = ?', [$configId]);
+
+ if (!$config) {
+ error_log("配置ID {$configId} 不存在");
+ return null;
+ }
+
+ // 调用API获取VPS状态(不自动更新数据库,由本函数统一处理)
+ $result = mofangGetVpsStatus($configId, $vpsId, false);
+
+ if (!$result || !isset($result['status']) || $result['status'] !== 200) {
+ error_log("获取VPS {$vpsId} 状态失败: " . ($result['msg'] ?? '未知错误'));
+ return null;
+ }
+
+ $statusData = $result['data'];
+ $status = $statusData['status'] ?? 'unknown';
+ $des = $statusData['des'] ?? '未知';
+
+ // 检查记录是否存在
+ $listDb = getVpsListDB();
+ $existing = $listDb->queryOne(
+ 'SELECT id FROM vps_list WHERE config_id = ? AND vps_id = ?',
+ [$configId, $vpsId]
+ );
+
+ if ($existing) {
+ // 记录存在,执行UPDATE
+ $listDb->execute(
+ 'UPDATE vps_list SET status = ?, last_check = CURRENT_TIMESTAMP WHERE config_id = ? AND vps_id = ?',
+ [$status, $configId, $vpsId]
+ );
+ error_log("[updateVpsStatusToDb] VPS {$vpsId} 状态已更新为: {$status}");
+ } else {
+ // 记录不存在,插入新记录
+ $listDb->execute(
+ 'INSERT INTO vps_list (config_id, vps_id, status, last_check) VALUES (?, ?, ?, CURRENT_TIMESTAMP)',
+ [$configId, $vpsId, $status]
+ );
+ error_log("[updateVpsStatusToDb] VPS {$vpsId} 新记录已插入,状态: {$status}");
+ }
+
+ return [
+ 'vps_id' => $vpsId,
+ 'status' => $status,
+ 'des' => $des,
+ 'updated' => true
+ ];
+}
+
+/**
+ * 批量获取VPS状态并更新到数据库
+ * @param int $configId 配置ID
+ * @param array $vpsIds VPS ID数组,为空则更新该配置下所有VPS
+ * @return array 更新结果统计
+ */
+function batchUpdateVpsStatus($configId, $vpsIds = []) {
+ $db = getVpsDB();
+ $config = $db->queryOne('SELECT * FROM configs WHERE id = ?', [$configId]);
+
+ if (!$config) {
+ return ['success' => 0, 'failed' => 0, 'error' => '配置不存在'];
+ }
+
+ // 如果未指定VPS IDs,获取该配置下的所有VPS
+ if (empty($vpsIds)) {
+ $listDb = getVpsListDB();
+ $vpsList = $listDb->query('SELECT vps_id FROM vps_list WHERE config_id = ?', [$configId]);
+ $vpsIds = array_column($vpsList, 'vps_id');
+ }
+
+ if (empty($vpsIds)) {
+ return ['success' => 0, 'failed' => 0, 'error' => '没有VPS需要更新'];
+ }
+
+ $successCount = 0;
+ $failedCount = 0;
+ $results = [];
+
+ foreach ($vpsIds as $vpsId) {
+ $result = updateVpsStatusToDb($configId, $vpsId);
+
+ if ($result) {
+ $successCount++;
+ $results[] = $result;
+ } else {
+ $failedCount++;
+ $results[] = [
+ 'vps_id' => $vpsId,
+ 'status' => 'error',
+ 'des' => '获取状态失败',
+ 'updated' => false
+ ];
+ }
+ }
+
+ return [
+ 'success' => $successCount,
+ 'failed' => $failedCount,
+ 'total' => count($vpsIds),
+ 'results' => $results
+ ];
+}
+?>
diff --git a/static/config.js b/static/config.js
new file mode 100644
index 0000000..31d4f24
--- /dev/null
+++ b/static/config.js
@@ -0,0 +1,205 @@
+/**
+ * VPS Hub 配置管理页面交互脚本
+ */
+
+/**
+ * 切换网站URL输入框的提示和默认值
+ * @param {string} siteType - 网站类型
+ */
+function toggleSiteUrl(siteType) {
+ const urlGroup = document.getElementById('site_url_group');
+ const urlInput = document.getElementById('site_url_input');
+
+ if (!urlInput) return;
+
+ switch(siteType) {
+ case 'mofang':
+ urlInput.placeholder = '留空使用默认值: https://www.heyunidc.cn/v1';
+ urlInput.required = false;
+ break;
+ case 'aliyun':
+ urlInput.placeholder = '例如: https://ecs.aliyuncs.com';
+ urlInput.required = true;
+ break;
+ case 'tencent':
+ urlInput.value = 'https://cvm.tencentcloudapi.com/';
+ urlInput.placeholder = '腾讯云API地址';
+ urlInput.required = true;
+ break;
+ default:
+ urlInput.placeholder = '输入API地址';
+ urlInput.required = true;
+ }
+}
+
+/**
+ * 验证密码格式
+ * @returns {boolean} 是否通过验证
+ */
+function validatePassword() {
+ const passwordInput = document.getElementById('api_pass');
+ if (!passwordInput) return true;
+
+ const password = passwordInput.value;
+
+ if (password.length < 8) {
+ alert('密码至少需要8位!');
+ return false;
+ }
+
+ if (!/^[a-zA-Z0-9]+$/.test(password)) {
+ alert('密码只能包含字母和数字!');
+ return false;
+ }
+
+ return true;
+}
+
+/**
+ * 确认删除操作
+ * @param {string} message - 确认消息
+ * @returns {boolean} 是否确认
+ */
+function confirmDelete(message) {
+ return confirm(message || '确认删除此配置?');
+}
+
+/**
+ * 显示加载状态
+ * @param {HTMLElement} button - 按钮元素
+ */
+function showLoading(button) {
+ if (!button) return;
+
+ const originalText = button.textContent;
+ button.disabled = true;
+ button.textContent = '加载中...';
+ button.dataset.originalText = originalText;
+}
+
+/**
+ * 隐藏加载状态
+ * @param {HTMLElement} button - 按钮元素
+ */
+function hideLoading(button) {
+ if (!button) return;
+
+ button.disabled = false;
+ button.textContent = button.dataset.originalText || '提交';
+}
+
+/**
+ * AJAX提交表单
+ * @param {HTMLFormElement} form - 表单元素
+ * @param {Function} successCallback - 成功回调
+ * @param {Function} errorCallback - 失败回调
+ */
+function submitFormAjax(form, successCallback, errorCallback) {
+ const formData = new FormData(form);
+ const submitButton = form.querySelector('button[type="submit"]');
+
+ showLoading(submitButton);
+
+ fetch(form.action || window.location.href, {
+ method: form.method || 'POST',
+ body: formData
+ })
+ .then(response => response.json())
+ .then(data => {
+ hideLoading(submitButton);
+ if (data.status === 200 || data.success) {
+ if (successCallback) successCallback(data);
+ } else {
+ if (errorCallback) errorCallback(data);
+ else alert('操作失败: ' + (data.msg || '未知错误'));
+ }
+ })
+ .catch(error => {
+ hideLoading(submitButton);
+ console.error('请求失败:', error);
+ if (errorCallback) errorCallback(error);
+ else alert('网络错误,请稍后重试');
+ });
+}
+
+/**
+ * 刷新VPS列表
+ */
+function refreshVpsList() {
+ const button = document.querySelector('.btn-refresh');
+ if (!button) return;
+
+ showLoading(button);
+
+ // 创建临时表单提交
+ const form = document.createElement('form');
+ form.method = 'POST';
+ form.action = window.location.href;
+
+ const actionInput = document.createElement('input');
+ actionInput.type = 'hidden';
+ actionInput.name = 'action';
+ actionInput.value = 'refresh_vps_list';
+
+ form.appendChild(actionInput);
+ document.body.appendChild(form);
+ form.submit();
+}
+
+/**
+ * 初始化页面
+ */
+document.addEventListener('DOMContentLoaded', function() {
+ // 自动聚焦第一个输入框
+ const firstInput = document.querySelector('input:not([type="hidden"]):not([type="checkbox"])');
+ if (firstInput && !firstInput.value) {
+ firstInput.focus();
+ }
+
+ // 为所有删除按钮添加确认对话框
+ const deleteButtons = document.querySelectorAll('.btn-delete');
+ deleteButtons.forEach(button => {
+ button.addEventListener('click', function(e) {
+ if (!confirmDelete('确认删除此配置?此操作不可恢复!')) {
+ e.preventDefault();
+ }
+ });
+ });
+
+ // 为所有操作按钮添加确认对话框
+ const actionButtons = document.querySelectorAll('.btn-success, .btn-danger, .btn-warning');
+ actionButtons.forEach(button => {
+ if (button.type === 'submit' && button.name === 'action') {
+ button.addEventListener('click', function(e) {
+ let message = '';
+ switch(this.value) {
+ case 'on':
+ message = '确认开机?';
+ break;
+ case 'off':
+ message = '确认关机?';
+ break;
+ case 'reboot':
+ message = '确认硬重启?';
+ break;
+ }
+
+ if (message && !confirm(message)) {
+ e.preventDefault();
+ }
+ });
+ }
+ });
+
+ console.log('VPS Hub 页面已加载');
+});
+
+// 导出函数供全局使用
+window.VPSHub = {
+ toggleSiteUrl,
+ validatePassword,
+ confirmDelete,
+ refreshVpsList,
+ showLoading,
+ hideLoading
+};
diff --git a/web/static/favicon.ico b/static/favicon.ico
similarity index 100%
rename from web/static/favicon.ico
rename to static/favicon.ico
diff --git a/web/static/initial.css b/static/initial.css
similarity index 100%
rename from web/static/initial.css
rename to static/initial.css
diff --git a/web/static/style.css b/static/style.css
similarity index 74%
rename from web/static/style.css
rename to static/style.css
index 9e955c0..7157aea 100644
--- a/web/static/style.css
+++ b/static/style.css
@@ -335,4 +335,125 @@ body {
padding: 8px 12px;
font-size: 12px;
}
+}
+
+/* VPS Hub 新增样式 */
+
+.config-container {
+ max-width: 600px;
+ margin: 50px auto;
+ background: white;
+ padding: 40px;
+ border-radius: 12px;
+ box-shadow: 0 4px 16px rgba(0,0,0,0.1);
+}
+
+.config-header {
+ text-align: center;
+ margin-bottom: 30px;
+}
+
+.config-header h1 {
+ color: #667eea;
+ margin-bottom: 10px;
+}
+
+.form-group {
+ margin-bottom: 20px;
+}
+
+.form-group label {
+ display: block;
+ margin-bottom: 8px;
+ color: #333;
+ font-weight: 500;
+}
+
+.form-group input,
+.form-group select {
+ width: 100%;
+ padding: 10px 12px;
+ border: 1px solid #ddd;
+ border-radius: 6px;
+ font-size: 14px;
+ transition: border-color 0.2s;
+}
+
+.form-group input:focus,
+.form-group select:focus {
+ outline: none;
+ border-color: #667eea;
+}
+
+.help-text {
+ font-size: 12px;
+ color: #999;
+ margin-top: 5px;
+}
+
+.btn-submit {
+ width: 100%;
+ padding: 12px;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ color: white;
+ border: none;
+ border-radius: 6px;
+ font-size: 16px;
+ cursor: pointer;
+ transition: transform 0.2s;
+}
+
+.btn-submit:hover {
+ transform: translateY(-2px);
+}
+
+.error-message {
+ background: #fee;
+ border-left: 4px solid #dc3545;
+ padding: 12px 16px;
+ border-radius: 6px;
+ margin-bottom: 20px;
+ color: #dc3545;
+}
+
+.success-message {
+ background: #efe;
+ border-left: 4px solid #28a745;
+ padding: 12px 16px;
+ border-radius: 6px;
+ margin-bottom: 20px;
+ color: #28a745;
+}
+
+.info-box {
+ background: #e7f3ff;
+ border-left: 4px solid #007bff;
+ padding: 12px 16px;
+ border-radius: 6px;
+ margin-bottom: 20px;
+ color: #0056b3;
+ font-size: 14px;
+}
+
+.info-box code {
+ background: #f0f0f0;
+ padding: 2px 6px;
+ border-radius: 3px;
+ font-family: monospace;
+}
+
+.copyright {
+ margin-top: 30px;
+ padding: 20px;
+ color: #999;
+ font-size: 14px;
+}
+
+.copyright a {
+ color: #667eea;
+ text-decoration: none;
+}
+
+.copyright a:hover {
+ text-decoration: underline;
}
\ No newline at end of file
diff --git a/web/index.php b/web/index.php
deleted file mode 100644
index 88747fa..0000000
--- a/web/index.php
+++ /dev/null
@@ -1,505 +0,0 @@
-
-
-
-
-
-
-
-
- 初始配置 - VPS管理面板
-
-
-
-
-
-
-
- ❌
-
-
-
-
- 💡 提示:这些信息将保存在 config.php 文件中,请妥善保管。
- 若您需要重置或更改配置,请删除 config.php 文件并重新运行。
- 配置完成后请以该格式访问服务:example.com/?pass=API_PASS
-
-
-
-
-
-
-
-
-
-
- ACCOUNT,
- 'password' => API_KEY
- ];
-
- $ch = curl_init();
- curl_setopt($ch, CURLOPT_URL, $url);
- curl_setopt($ch, CURLOPT_POST, true);
- curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
- curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
- curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
- curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
-
- $response = curl_exec($ch);
- $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
- curl_close($ch);
-
- if ($httpCode !== 200) {
- return null;
- }
-
- $result = json_decode($response, true);
- $token = isset($result['jwt']) ? $result['jwt'] : null;
-
- // 保存新Token到缓存
- if ($token) {
- saveTokenToCache($token);
- }
-
- return $token;
-}
-
-// ==================== 功能实现区域 ====================
-
-/**
- * 发送API请求
- */
-function sendApiRequest($endpoint, $method = 'GET', $data = []) {
- $token = getLoginToken();
- if (!$token) {
- return ['status' => 500, 'msg' => '获取Token失败,请检查账号和密码配置'];
- }
-
- $url = BASE_URL . $endpoint;
- $headers = [
- 'Authorization: JWT ' . $token,
- 'Content-Type: application/json'
- ];
-
- $ch = curl_init();
- curl_setopt($ch, CURLOPT_URL, $url);
- curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
- curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
- curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
- curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
-
- if ($method === 'POST') {
- curl_setopt($ch, CURLOPT_POST, true);
- curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
- } elseif ($method === 'PUT') {
- curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
- curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
- }
-
- $response = curl_exec($ch);
- $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
- curl_close($ch);
-
- return json_decode($response, true);
-}
-
-/**
- * 获取VPS列表
- */
-function getHostList($page = 1, $limit = 100) {
- return sendApiRequest("/hosts?page={$page}&limit={$limit}", 'GET');
-}
-
-/**
- * 获取VPS状态
- */
-function getHostStatus($hostId) {
- return sendApiRequest("/hosts/{$hostId}/module/status?type=host", 'GET');
-}
-
-/**
- * 操作VPS
- */
-function operateHost($hostId, $operation) {
- return sendApiRequest("/hosts/{$hostId}/module/{$operation}", 'PUT');
-}
-
-// ==================== 主逻辑区域 ====================
-
-// 验证pass参数
-$pass = isset($_GET['pass']) ? $_GET['pass'] : '';
-
-if ($pass !== API_PASS) {
- header('Content-Type: application/json; charset=utf-8');
- echo json_encode(['status' => 403, 'msg' => '访问密码错误或者未输入密码,请拼接?pass=API_PASS后再尝试访问'], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
- exit;
-}
-
-// 处理操作请求
-$action = isset($_POST['action']) ? $_POST['action'] : '';
-$hostId = isset($_POST['host_id']) ? intval($_POST['host_id']) : 0;
-
-$result = null;
-if ($action && $hostId > 0) {
- if ($action === 'reboot') {
- $result = operateHost($hostId, 'hard_reboot');
- } elseif ($action === 'on') {
- $result = operateHost($hostId, 'on');
- } elseif ($action === 'off') {
- $result = operateHost($hostId, 'off');
- }
-}
-
-// 获取VPS列表和状态
-$hosts = [];
-$statusMap = [];
-$domainstatus = [];
-$totalCount = 0;
-$errorMsg = '';
-
-$listResult = getHostList();
-if (isset($listResult['status']) && $listResult['status'] === 200) {
- $data = $listResult['data'];
- $hosts = $data['host'];
- $domainstatus = $data['domainstatus'];
- $totalCount = $data['total'];
-
- // 批量获取所有VPS的状态
- foreach ($hosts as $host) {
- $statusResult = getHostStatus($host['id']);
- if (isset($statusResult['status']) && $statusResult['status'] === 200) {
- $statusMap[$host['id']] = $statusResult['data'];
- } else {
- $statusMap[$host['id']] = ['status' => 'unknown', 'des' => '未知'];
- }
- }
-} else {
- $errorMsg = isset($listResult['msg']) ? $listResult['msg'] : '获取VPS列表失败';
-}
-
-header('Content-Type: text/html; charset=utf-8');
-?>
-
-
-
-
-
-
-
- VPS管理面板
-
-
-
-
-
-
-
-
-
-
-
-
-
-
✅ 操作成功
-
VPS # 已成功执行操作
-
-
-
-
-
-
-
-
- ✅ Token缓存生效中 | 剩余有效期:约 分钟
-
-
-
-
-
- 总计: 台服务器
-
-
- 运行中: 台
-
-
- 已关机: 台
-
-
-
-
- $statusKey, 'color' => '#999'];
- $powerStatus = isset($statusMap[$host['id']]) ? $statusMap[$host['id']] : ['status' => 'unknown', 'des' => '未知'];
-
- $regDate = date('Y-m-d', $host['regdate']);
- $nextDueDate = date('Y-m-d', $host['nextduedate']);
-
- $powerClass = 'power-unknown';
- if ($powerStatus['status'] === 'on') {
- $powerClass = 'power-on';
- } elseif ($powerStatus['status'] === 'off') {
- $powerClass = 'power-off';
- }
- ?>
-
-
-
-
-
- 域名
-
-
-
- IP地址
-
-
-
- 产品名称
-
-
-
- 注册日期
-
-
-
- 到期日期
-
-
-
- 金额
- ¥
-
-
- 计费周期
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/新开发文档.md b/新开发文档.md
new file mode 100644
index 0000000..3acb3ca
--- /dev/null
+++ b/新开发文档.md
@@ -0,0 +1,69 @@
+### WEB运行
+
+#### 配置文件 config_add.php
+
+用户可访问config_add.php添加VPS配置项
+
+初次访问该php页面时,用户需设置网页自定义访问密码:API_PASS(运行输入大小写字母以及数字,最低8位数)
+
+存在API_PASS时,需跟参数?pass=API_PASS才允许访问
+
+用户点击添加配置按钮,新列出一列,分别是:
+旧数据也会读取并展示
+序号(自动生成),网站类型(魔方、阿里云、腾讯云等),网站链接(若为魔方则设置为可填,腾讯云为https://cvm.tencentcloudapi.com/,阿里云后面再写),账户(魔方账户:邮箱或手机号,其他云后续添加),密钥(魔方输入API KEY),是否开启自动开机监控(魔方默认开启,其他云默认关闭)
+
+1. 用户点击添加数据后,在./app/db/文件夹内生成sqlite数据库 vps.db用于存储该类数据
+
+2. 首次添加后,自动调用所有API进行VPS列表查询,查询结果记录在./app/db中的vpslist.db中,之后此类查询每天仅需查询一次(是否开启自动开机监控标记为开启时,vps的最后一列section也标记为True)
+
+3. 进行ping访问测试后,将结果记录于./app/db/中的status.db中,保留最近三十天数据
+
+ 记录项:目标(IP地址或域名),状态(正常或异常),延时(单位ms 毫秒)
+ 每天0点时,总结前一天中各VPS的ping响应数据,记录在该数据库中的新表 vps_summary 中。该表需包含以下字段:VPS标识(vps_id)、统计日期(date)、平均延迟(avg_latency_ms)、最大延迟(max_latency_ms)、最小延迟(min_latency_ms)。同时分级记录延迟分布情况:延迟低于100ms的次数(count_under_100)、100ms~300ms的次数(count_100_to_300)、300ms~500ms的次数(count_300_to_500),以及延迟超过500ms或ping异常的丢包/超时次数(count_abnormal)。根据延迟表现计算高可用评分:100ms以内权重最高,300ms以内为良好,500ms以内为可用,超过500ms标记为低可用,异常则标记为不可用。记录为新字段:服务器可用性(优秀/良好/一般/不可用)
+
+4. 首次使用网页并添加数据后,调用./app/install.sh进行system服务注册
+
+#### 首页 index.php
+
+1. 查询是否存在API_PASS,若存在,则需跟参数?pass=API_PASS才能访问
+
+2. 若不存在,则跳转至config_add.php页
+
+(获取到的认证token存储于./app/token.php中,记录为两小时,后续调用API时超时或提示失效再重新获取)
+
+3. 右上角放置“添加配置源”的按钮
+
+4. 查询vps.db,并依次获取vps列表并查询其状态信息(使用v1/hosts/:id接口获取VPS的CPU核数,操作系统,内存大小,系统盘大小以及带宽大小),提供开机、关机、硬重启按钮
+
+5. 左上角新建一个按钮,用于手动再次获取vps列表以及信息,不再自动获取信息。
+
+#### 魔方页 mofangidc.php
+
+所有智简魔方的操作API以及相关函数存储在此网页下
+
+### Python运行
+
+#### 安装
+
+通过install.sh自动安装
+
+功能点:
+
+1. 探测当前系统是否存在idc_monitor的系统服务,若不存在则进入第二步,若存在则重启一下该服务
+
+2. 读取python版本并探测是否存在pip,自动下载python-pip
+
+3. pip下载所需环境和依赖项
+
+4. 新建一个idc_system的系统服务,持续化运行当前路径下的monitor.py脚本
+
+#### 读取配置
+
+1. 读取vpslist中的数据,则对其进行ping测试
+
+2. 测试结果保存至status.db中
+
+(获取到的认证token存储于./app/token.php中,记录为两小时)
+
+3. 若section为True,且ping测试失败,调用API对该机器进行状态查询,若返回状态为关机,则尝试开机,隔60秒后再进行状态查询并再次尝试开机,60秒后查询还是为异常的话,则记录异常,进入下一个循环(多台机器关机时,依次进行状态查询和开机,然后间隔60秒再进行操作,不同的机器不需要间隔)
+