Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ecbba836d9 | |||
| bb19141edc | |||
| c960048042 | |||
| 51f7ee9ad0 | |||
| ea904fb2b2 | |||
| 21240dc108 | |||
| 1d7f4c2bed | |||
| 88c11277cf | |||
| e5ae822ed2 | |||
| 50341749b6 | |||
| 7904117b1d | |||
| 69edd1c63e | |||
| 6d1cd478be | |||
| ccc84e4d36 | |||
| 1fdb00a1ec | |||
| 248c951a96 | |||
| 761edef6fe |
249
README.md
249
README.md
@@ -1,131 +1,188 @@
|
||||
# 项目服务管理面板
|
||||
|
||||
统一管理所有 Web 服务项目,提供状态检测、启动/停止控制、日志查看等功能。
|
||||
统一的 Web 服务项目管理平台,支持项目监控、系统资源监控、Cron 任务管理。
|
||||
|
||||
## 端口
|
||||
## 版本信息
|
||||
|
||||
- **服务端口**: 19013
|
||||
- **访问地址**: http://localhost:19013
|
||||
- **当前版本**: v3.4.1
|
||||
- **端口**: 16022
|
||||
- **更新日期**: 2026-06-01
|
||||
|
||||
## 功能
|
||||
## 功能特性
|
||||
|
||||
- 项目列表展示(状态、端口、版本)
|
||||
- 服务状态健康检测
|
||||
- 启动/停止/重启控制
|
||||
- 日志查看(实时滚动)
|
||||
- Git 仓库链接
|
||||
### 1. 项目服务管理
|
||||
- 📊 项目状态实时监控
|
||||
- 🚀 一键启动/停止/重启服务
|
||||
- 🔗 自定义链接入口
|
||||
- 📝 项目配置管理
|
||||
- 🌐 外部服务监控
|
||||
|
||||
## 启动方式 ⭐
|
||||
### 2. 系统资源监控
|
||||
- 💻 CPU 使用率 + 温度监控
|
||||
- 🎯 内存使用率
|
||||
- 💾 磁盘使用率
|
||||
- 📡 网络上传/下载速度
|
||||
- ⏱️ 实时监控模式
|
||||
- ⚠️ 预警阈值设置
|
||||
|
||||
**重要:必须使用 `nohup + disown` 方式启动,脱离进程树!**
|
||||
### 3. Cron 任务管理
|
||||
- 📋 任务列表查看
|
||||
- ➕ 添加/编辑/删除任务
|
||||
- 🔄 启用/禁用切换
|
||||
- 📚 版本历史记录
|
||||
- 🔙 回退到历史版本
|
||||
- 🔗 从系统 crontab 同步
|
||||
|
||||
```bash
|
||||
cd ~/.openclaw/workspace-coder/works/project-panel
|
||||
nohup python3 app.py > logs/app.log 2>&1 & disown
|
||||
### 4. 配置管理
|
||||
- 📤 导出完整配置(JSON)
|
||||
- 📥 导入配置文件
|
||||
- ⚙️ 统一配置文件管理
|
||||
- 💾 自动备份旧配置
|
||||
|
||||
## 项目结构(模块化)
|
||||
|
||||
```
|
||||
project-panel/
|
||||
├── app_main.py # 主入口文件
|
||||
├── config.py # 配置常量
|
||||
├── database.py # 数据库操作
|
||||
├── utils.py # 工具函数
|
||||
├── routes_cron.py # Cron 管理 API
|
||||
├── routes_system.py # 系统监控 API
|
||||
├── routes_config.py # 配置管理 API
|
||||
├── routes_email.py # 邮件通知 API
|
||||
├── routes_project.py # 项目管理 API
|
||||
├── templates.py # HTML 模板
|
||||
├── projects.json # 项目配置文件
|
||||
├── alert_config.json # 预警和网页设置
|
||||
├── cron_manager.db # Cron 任务数据库
|
||||
└── logs/ # 日志目录
|
||||
```
|
||||
|
||||
### 为什么需要 disown?
|
||||
|
||||
如果通过 OpenClaw 的 exec 启动服务,当 OpenClaw 清理进程时(如超时、重启),会发送 SIGTERM 杀掉所有子进程。
|
||||
|
||||
使用 `nohup + disown` 可以:
|
||||
1. `nohup` - 让进程忽略 SIGHUP(终端关闭信号)
|
||||
2. `disown` - 从 shell 进程树中移除,避免被连带杀掉
|
||||
3. `> logs/app.log 2>&1` - 重定向输出到日志文件
|
||||
|
||||
### ❌ 错误方式
|
||||
|
||||
```bash
|
||||
# 不要用这种方式(会被 OpenClaw 杀掉)
|
||||
python3 app.py
|
||||
```
|
||||
|
||||
### ✅ 正确方式
|
||||
|
||||
```bash
|
||||
nohup python3 app.py > logs/app.log 2>&1 & disown
|
||||
```
|
||||
|
||||
## 项目配置
|
||||
|
||||
项目配置在 `projects.json` 中,每个项目包含:
|
||||
## 配置文件说明
|
||||
|
||||
### projects.json
|
||||
```json
|
||||
{
|
||||
"id": "project-id",
|
||||
"name": "项目名称",
|
||||
"ports": [19000],
|
||||
"directory": "works/project-dir",
|
||||
"start_cmd": "nohup python3 app.py > logs/app.log 2>&1 & disown",
|
||||
"health_url": "http://localhost:19000/api/health",
|
||||
"admin_url": "http://localhost:19000/admin",
|
||||
"git_repo": "http://192.168.2.8:12007/coder/project",
|
||||
"version": "v1.0.0"
|
||||
"server": {
|
||||
"name": "服务器名称",
|
||||
"description": "服务器描述"
|
||||
},
|
||||
"projects": [
|
||||
{
|
||||
"id": "项目ID",
|
||||
"name": "项目名称",
|
||||
"type": "web",
|
||||
"ports": [端口列表],
|
||||
"directory": "工作目录",
|
||||
"start_cmd": "启动命令",
|
||||
"stop_cmd": "停止命令",
|
||||
"health_url": "健康检查URL",
|
||||
"description": "项目描述"
|
||||
}
|
||||
],
|
||||
"external_services": [...]
|
||||
}
|
||||
```
|
||||
|
||||
## 端口规范
|
||||
|
||||
所有 Web 服务必须使用 **19000-19100** 端口范围:
|
||||
|
||||
| 端口 | 项目 |
|
||||
|------|------|
|
||||
| 19000 | PDF翻译助手 V2 |
|
||||
| 19001 | LLM Index RAG |
|
||||
| 19009 | 碎片信息记录 |
|
||||
| 19010 | ParamHub Python |
|
||||
| 19013 | 项目服务管理面板(本服务) |
|
||||
| 19014 | Xian Favor 收藏系统 |
|
||||
| 19020 | AI对话系统 |
|
||||
| 19007 | LLM Proxy |
|
||||
| 19011 | 产品参数爬取系统 |
|
||||
| 19004 | 技术论坛 |
|
||||
| 19015 | 多智能体竞标调度系统 |
|
||||
### alert_config.json
|
||||
```json
|
||||
{
|
||||
"thresholds": {
|
||||
"cpu": 80,
|
||||
"memory": 85,
|
||||
"disk": 90,
|
||||
"interval": 60
|
||||
},
|
||||
"web_settings": {
|
||||
"external_ip": "121.40.164.32",
|
||||
"refresh_interval": 30
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API 接口
|
||||
|
||||
- `GET /api/projects` - 获取所有项目列表
|
||||
- `GET /api/project/:id` - 获取单个项目详情
|
||||
- `POST /api/project/:id/start` - 启动项目
|
||||
- `POST /api/project/:id/stop` - 停止项目
|
||||
- `POST /api/project/:id/restart` - 重启项目
|
||||
- `GET /api/project/:id/logs` - 获取项目日志
|
||||
### 项目管理
|
||||
- `GET /api/projects` - 获取项目列表
|
||||
- `POST /api/projects/<id>/start` - 启动项目
|
||||
- `POST /api/projects/<id>/stop` - 停止项目
|
||||
- `POST /api/projects/<id>/restart` - 重启项目
|
||||
- `POST /api/projects/add` - 添加项目
|
||||
|
||||
## 监控
|
||||
### 系统监控
|
||||
- `GET /api/system/stats` - 获取系统资源信息
|
||||
- `GET /api/system/processes` - 获取进程列表
|
||||
- `GET /api/system/docker` - 获取 Docker 容器状态
|
||||
|
||||
服务监控由 `service-monitor` 负责,每 20 分钟检查一次,发送邮件通知。
|
||||
### Cron 管理
|
||||
- `GET /api/cron/tasks` - 获取任务列表
|
||||
- `POST /api/cron/tasks` - 添加任务
|
||||
- `PUT /api/cron/tasks/<id>` - 更新任务
|
||||
- `DELETE /api/cron/tasks/<id>` - 删除任务
|
||||
- `POST /api/cron/tasks/<id>/toggle` - 启用/禁用
|
||||
- `POST /api/cron/sync` - 从系统同步
|
||||
|
||||
## 故障排查
|
||||
### 配置管理
|
||||
- `GET /api/config/export` - 导出配置
|
||||
- `POST /api/config/import` - 导入配置
|
||||
- `GET /api/web-settings` - 获取网页设置
|
||||
- `POST /api/web-settings` - 保存网页设置
|
||||
|
||||
### 服务频繁停止?
|
||||
### 预警配置
|
||||
- `GET /api/alerts` - 获取预警配置
|
||||
- `POST /api/alerts` - 保存预警配置
|
||||
|
||||
检查是否使用了正确的启动方式:
|
||||
1. 确认使用了 `nohup + disown`
|
||||
2. 确认日志目录存在:`mkdir -p logs/`
|
||||
3. 查看日志:`cat logs/app.log`
|
||||
## 使用方法
|
||||
|
||||
### OpenClaw 杀进程?
|
||||
### 启动服务
|
||||
```bash
|
||||
python3 app_main.py
|
||||
```
|
||||
|
||||
如果日志突然停止(约凌晨 01:10),可能是被 OpenClaw 清理进程杀掉了。
|
||||
### 访问面板
|
||||
打开浏览器访问:`http://localhost:16022/`
|
||||
|
||||
**证据:**
|
||||
- OpenClaw 日志:`[tools] exec failed: Command aborted by signal SIGTERM`
|
||||
- 服务日志最后记录时间:01:09 左右
|
||||
### 导出配置
|
||||
点击页面头部"导出配置"按钮,下载 JSON 配置文件
|
||||
|
||||
**解决:** 使用 `disown` 脱离进程树。
|
||||
### 导入配置
|
||||
点击"导入配置"按钮,上传配置文件即可应用
|
||||
|
||||
## Git 仓库
|
||||
## 依赖项
|
||||
|
||||
http://192.168.2.8:12007/coder/project-panel
|
||||
- Python 3.12+
|
||||
- Flask
|
||||
- psutil (系统监控)
|
||||
- croniter (Cron 表达式解析)
|
||||
|
||||
## 更新日志
|
||||
|
||||
### v1.0.1
|
||||
- 添加 README.md 文档
|
||||
- 记录正确的启动方式(nohup + disown)
|
||||
- 记录端口规范
|
||||
### v3.4.1 (2026-06-01)
|
||||
- 🎯 **模块化重构**: 将 3700+ 行单文件拆分为多个模块
|
||||
- 📁 **清晰结构**: config、database、utils、routes、templates 分离
|
||||
- 🔧 **易于维护**: 每个模块职责明确,便于扩展和维护
|
||||
- ✅ **功能完整**: 保留所有原有网页功能
|
||||
|
||||
### v1.0.0
|
||||
- 项目服务管理面板
|
||||
- 状态检测、启动/停止控制
|
||||
- 日志查看
|
||||
### v3.4.0 (2026-06-01) [已废弃]
|
||||
- 🎯 **模块化重构**: 将 3700+ 行单文件拆分为多个模块
|
||||
- 📁 **清晰结构**: config、database、utils、routes、templates 分离
|
||||
- 🔧 **易于维护**: 每个模块职责明确,便于扩展和维护
|
||||
|
||||
### v3.3.5 (2026-06-01)
|
||||
- ✨ 统一配置文件管理
|
||||
- 📤 配置导入/导出功能
|
||||
- 🔄 Cron 任务同步优化(清空旧任务)
|
||||
- ⚙️ 网页设置持久化
|
||||
|
||||
### v3.3.4
|
||||
- 🌐 对外 IP 默认值调整
|
||||
- 🐛 Bug 修复
|
||||
|
||||
## 开发者
|
||||
|
||||
黄庄4号程序员 - hz4th_coder@tphai.com
|
||||
|
||||
## 许可证
|
||||
|
||||
内部使用项目
|
||||
Binary file not shown.
BIN
__pycache__/config.cpython-312.pyc
Normal file
BIN
__pycache__/config.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/database.cpython-312.pyc
Normal file
BIN
__pycache__/database.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/routes_config.cpython-312.pyc
Normal file
BIN
__pycache__/routes_config.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/routes_cron.cpython-312.pyc
Normal file
BIN
__pycache__/routes_cron.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/routes_email.cpython-312.pyc
Normal file
BIN
__pycache__/routes_email.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/routes_project.cpython-312.pyc
Normal file
BIN
__pycache__/routes_project.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/routes_system.cpython-312.pyc
Normal file
BIN
__pycache__/routes_system.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/templates.cpython-312.pyc
Normal file
BIN
__pycache__/templates.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/utils.cpython-312.pyc
Normal file
BIN
__pycache__/utils.cpython-312.pyc
Normal file
Binary file not shown.
38
config.py
Normal file
38
config.py
Normal file
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
配置文件 - 定义常量和路径
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
# 基础目录
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
WORKSPACE_DIR = os.path.dirname(BASE_DIR) # works目录
|
||||
ROOT_DIR = os.path.dirname(WORKSPACE_DIR) # workspace-coder目录
|
||||
|
||||
# 文件路径
|
||||
PROJECTS_FILE = os.path.join(BASE_DIR, 'projects.json')
|
||||
LOG_FILE = os.path.join(BASE_DIR, 'logs', 'app.log')
|
||||
DB_FILE = os.path.join(BASE_DIR, 'cron_manager.db')
|
||||
CRON_BACKUP_DIR = os.path.join(BASE_DIR, 'cron_backups')
|
||||
CONFIG_FILE = 'config.json'
|
||||
ALERT_CONFIG_FILE = 'alert_config.json'
|
||||
|
||||
# 应用配置
|
||||
APP_NAME = "项目服务管理面板"
|
||||
APP_VERSION = "v3.4.1"
|
||||
APP_PORT = 16022
|
||||
|
||||
# 监控配置
|
||||
DEFAULT_THRESHOLDS = {
|
||||
'cpu': 80,
|
||||
'cpu_temp': 80,
|
||||
'memory': 85,
|
||||
'disk': 90,
|
||||
'interval': 60
|
||||
}
|
||||
|
||||
DEFAULT_WEB_SETTINGS = {
|
||||
'external_ip': '121.40.164.32',
|
||||
'refresh_interval': 30
|
||||
}
|
||||
76
database.py
Normal file
76
database.py
Normal file
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
数据库操作模块
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
from flask import g
|
||||
from config import DB_FILE
|
||||
|
||||
|
||||
def get_db():
|
||||
"""获取数据库连接"""
|
||||
if 'db' not in g:
|
||||
g.db = sqlite3.connect(DB_FILE)
|
||||
g.db.row_factory = sqlite3.Row
|
||||
return g.db
|
||||
|
||||
|
||||
def close_db(error):
|
||||
"""关闭数据库连接"""
|
||||
if hasattr(g, 'db'):
|
||||
g.db.close()
|
||||
|
||||
|
||||
def init_db():
|
||||
"""初始化数据库"""
|
||||
conn = sqlite3.connect(DB_FILE)
|
||||
conn.row_factory = sqlite3.Row
|
||||
c = conn.cursor()
|
||||
|
||||
# Cron 任务表
|
||||
c.execute('''CREATE TABLE IF NOT EXISTS cron_tasks (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
expression TEXT NOT NULL,
|
||||
command TEXT NOT NULL,
|
||||
description TEXT,
|
||||
category TEXT DEFAULT 'other',
|
||||
enabled INTEGER DEFAULT 1,
|
||||
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
|
||||
)''')
|
||||
|
||||
# Cron 历史版本表
|
||||
c.execute('''CREATE TABLE IF NOT EXISTS cron_history (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
task_id INTEGER NOT NULL,
|
||||
version INTEGER NOT NULL,
|
||||
name TEXT,
|
||||
expression TEXT,
|
||||
command TEXT,
|
||||
description TEXT,
|
||||
category TEXT,
|
||||
enabled INTEGER,
|
||||
operation TEXT,
|
||||
operator TEXT DEFAULT 'system',
|
||||
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (task_id) REFERENCES cron_tasks(id)
|
||||
)''')
|
||||
|
||||
# Cron 执行日志表
|
||||
c.execute('''CREATE TABLE IF NOT EXISTS cron_logs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
task_id INTEGER,
|
||||
execution_time TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||
status TEXT,
|
||||
output TEXT,
|
||||
FOREIGN KEY (task_id) REFERENCES cron_tasks(id)
|
||||
)''')
|
||||
|
||||
# 创建索引
|
||||
c.execute('CREATE INDEX IF NOT EXISTS idx_cron_history_task ON cron_history(task_id)')
|
||||
c.execute('CREATE INDEX IF NOT EXISTS idx_cron_logs_task ON cron_logs(task_id)')
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
29
logs/app.log
29
logs/app.log
@@ -30302,3 +30302,32 @@ Press CTRL+C to quit
|
||||
[2026-06-01 12:11:34] 进程PID: 780597
|
||||
[2026-06-01 12:11:34] ==================================================
|
||||
[2026-06-01 12:12:13] 从系统 crontab 同步了 0 个任务(已清空旧任务)
|
||||
[2026-06-01 12:13:33] ⚠️ 进程收到 SIGTERM 信号,即将退出!
|
||||
[2026-06-01 12:13:35] ==================================================
|
||||
[2026-06-01 12:13:35] 项目服务管理面板 v2.0.0 启动
|
||||
[2026-06-01 12:13:35] 访问地址: http://localhost:16022
|
||||
[2026-06-01 12:13:35] 进程PID: 781142
|
||||
[2026-06-01 12:13:35] ==================================================
|
||||
[2026-06-01 12:29:39] ⚠️ 进程收到 SIGTERM 信号,即将退出!
|
||||
[2026-06-01 12:29:39] ==================================================
|
||||
[2026-06-01 12:29:39] 项目服务管理面板 v2.0.0 启动
|
||||
[2026-06-01 12:29:39] 访问地址: http://localhost:16022
|
||||
[2026-06-01 12:29:39] 进程PID: 784244
|
||||
[2026-06-01 12:29:39] ==================================================
|
||||
[2026-06-01 12:30:38] ⚠️ 进程收到 SIGTERM 信号,即将退出!
|
||||
[2026-06-01 12:31:16] 项目服务管理面板 v3.4.0 初始化完成
|
||||
[2026-06-01 12:31:16] 启动 项目服务管理面板,端口: 16022
|
||||
[2026-06-01 15:53:53] 项目服务管理面板 v3.4.0 初始化完成
|
||||
[2026-06-01 15:53:53] 启动 项目服务管理面板,端口: 16022
|
||||
[2026-06-01 15:55:35] 项目服务管理面板 v3.4.0 初始化完成
|
||||
[2026-06-01 15:55:35] 启动 项目服务管理面板,端口: 16022
|
||||
[2026-06-01 15:56:16] 项目服务管理面板 v3.4.0 初始化完成
|
||||
[2026-06-01 15:56:16] 启动 项目服务管理面板,端口: 16022
|
||||
[2026-06-01 15:57:03] 项目服务管理面板 v3.4.1 初始化完成
|
||||
[2026-06-01 15:57:03] 启动 项目服务管理面板,端口: 16022
|
||||
[2026-06-01 16:02:13] 项目服务管理面板 v3.4.1 初始化完成
|
||||
[2026-06-01 16:02:13] 启动 项目服务管理面板,端口: 16022
|
||||
[2026-06-01 16:02:39] 项目服务管理面板 v3.4.1 初始化完成
|
||||
[2026-06-01 16:02:39] 启动 项目服务管理面板,端口: 16022
|
||||
[2026-06-01 16:10:23] 项目服务管理面板 v3.4.1 初始化完成
|
||||
[2026-06-01 16:10:23] 启动 项目服务管理面板,端口: 16022
|
||||
|
||||
@@ -29,10 +29,10 @@
|
||||
"start_cmd": "mkdir -p logs && nohup /home/hz1/miniconda3/envs/openclaw/bin/python3.12 app.py > logs/app.log 2>&1 & disown",
|
||||
"stop_cmd": "pkill -f 'python.*project-panel.*app.py' || true",
|
||||
"health_url": "http://localhost:16022/",
|
||||
"description": "统一管理所有Web服务项目",
|
||||
"description": "统一管理所有 Web 服务项目,支持 Docker 监控、系统资源监控、Cron 管理",
|
||||
"admin_url": "http://localhost:16022",
|
||||
"git_repo": "http://121.40.164.32:12007/hz4th_coder/project-panel",
|
||||
"version": "v2.0.0"
|
||||
"version": "v3.0.0"
|
||||
}
|
||||
],
|
||||
"external_services": [
|
||||
|
||||
150
routes_config.py
Normal file
150
routes_config.py
Normal file
@@ -0,0 +1,150 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
配置管理 API 路由
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import shutil
|
||||
from datetime import datetime
|
||||
from flask import jsonify, request
|
||||
from config import PROJECTS_FILE, ALERT_CONFIG_FILE
|
||||
from utils import log_message
|
||||
|
||||
|
||||
def load_full_config():
|
||||
"""加载完整配置(合并项目和预警配置)"""
|
||||
# 加载 projects.json 完整数据
|
||||
try:
|
||||
with open(PROJECTS_FILE, 'r', encoding='utf-8') as f:
|
||||
projects_data = json.load(f)
|
||||
except:
|
||||
projects_data = {'server': {}, 'projects': [], 'external_services': []}
|
||||
|
||||
# 加载预警配置
|
||||
alert_config = {}
|
||||
if os.path.exists(ALERT_CONFIG_FILE):
|
||||
try:
|
||||
with open(ALERT_CONFIG_FILE, 'r', encoding='utf-8') as f:
|
||||
alert_config = json.load(f)
|
||||
except:
|
||||
pass
|
||||
|
||||
# 合并配置
|
||||
config = {
|
||||
"version": "1.0",
|
||||
"exported_at": datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
|
||||
"server": projects_data.get('server', {}),
|
||||
"projects": projects_data.get('projects', []),
|
||||
"external_services": projects_data.get('external_services', []),
|
||||
"web_settings": alert_config.get('web_settings', {}),
|
||||
"alerts": alert_config.get('thresholds', {})
|
||||
}
|
||||
return config
|
||||
|
||||
|
||||
def save_full_config(config):
|
||||
"""保存完整配置"""
|
||||
# 保存 projects 部分
|
||||
projects_data = {
|
||||
"server": config.get('server', {}),
|
||||
"projects": config.get('projects', []),
|
||||
"external_services": config.get('external_services', [])
|
||||
}
|
||||
with open(PROJECTS_FILE, 'w', encoding='utf-8') as f:
|
||||
json.dump(projects_data, f, ensure_ascii=False, indent=2)
|
||||
|
||||
# 保存预警配置和网页设置
|
||||
alert_config = {
|
||||
"web_settings": config.get('web_settings', {}),
|
||||
"thresholds": config.get('alerts', {})
|
||||
}
|
||||
with open(ALERT_CONFIG_FILE, 'w', encoding='utf-8') as f:
|
||||
json.dump(alert_config, f, ensure_ascii=False, indent=2)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def register_config_routes(app):
|
||||
"""注册配置管理相关路由"""
|
||||
|
||||
@app.route('/api/config/export', methods=['GET'])
|
||||
def api_config_export():
|
||||
"""导出配置文件"""
|
||||
try:
|
||||
config = load_full_config()
|
||||
return jsonify(config)
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/config/import', methods=['POST'])
|
||||
def api_config_import():
|
||||
"""导入配置文件"""
|
||||
try:
|
||||
config = request.get_json()
|
||||
if not config:
|
||||
return jsonify({'error': '无效的配置数据'}), 400
|
||||
|
||||
# 验证配置结构
|
||||
required_keys = ['server', 'projects']
|
||||
for key in required_keys:
|
||||
if key not in config:
|
||||
return jsonify({'error': f'缺少必要字段: {key}'}), 400
|
||||
|
||||
# 备份当前配置
|
||||
backup_time = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||
if os.path.exists(PROJECTS_FILE):
|
||||
shutil.copy(PROJECTS_FILE, f'{PROJECTS_FILE}.bak.{backup_time}')
|
||||
if os.path.exists(ALERT_CONFIG_FILE):
|
||||
shutil.copy(ALERT_CONFIG_FILE, f'{ALERT_CONFIG_FILE}.bak.{backup_time}')
|
||||
|
||||
# 保存新配置
|
||||
save_full_config(config)
|
||||
|
||||
log_message(f"配置导入成功: {len(config.get('projects', []))} 个项目")
|
||||
|
||||
return jsonify({
|
||||
'message': '配置导入成功',
|
||||
'projects_count': len(config.get('projects', [])),
|
||||
'services_count': len(config.get('external_services', []))
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/web-settings', methods=['GET'])
|
||||
def api_get_web_settings():
|
||||
"""获取网页设置"""
|
||||
try:
|
||||
if os.path.exists(ALERT_CONFIG_FILE):
|
||||
with open(ALERT_CONFIG_FILE, 'r', encoding='utf-8') as f:
|
||||
config = json.load(f)
|
||||
web_settings = config.get('web_settings', {})
|
||||
return jsonify(web_settings)
|
||||
return jsonify({'external_ip': '121.40.164.32', 'refresh_interval': 30})
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/web-settings', methods=['POST'])
|
||||
def api_save_web_settings():
|
||||
"""保存网页设置"""
|
||||
try:
|
||||
settings = request.get_json()
|
||||
|
||||
# 加载现有配置
|
||||
existing_config = {}
|
||||
if os.path.exists(ALERT_CONFIG_FILE):
|
||||
try:
|
||||
with open(ALERT_CONFIG_FILE, 'r', encoding='utf-8') as f:
|
||||
existing_config = json.load(f)
|
||||
except:
|
||||
pass
|
||||
|
||||
# 更新网页设置
|
||||
existing_config['web_settings'] = settings
|
||||
|
||||
with open(ALERT_CONFIG_FILE, 'w', encoding='utf-8') as f:
|
||||
json.dump(existing_config, f, ensure_ascii=False, indent=2)
|
||||
|
||||
return jsonify({'message': '保存成功'})
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
254
routes_cron.py
Normal file
254
routes_cron.py
Normal file
@@ -0,0 +1,254 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Cron 管理 API 路由
|
||||
"""
|
||||
|
||||
from flask import jsonify, request
|
||||
from database import get_db
|
||||
from utils import (
|
||||
log_message, sync_crontab_from_db, get_system_crons,
|
||||
cron_expression_to_human, get_next_run_time, guess_cron_name, guess_cron_category
|
||||
)
|
||||
|
||||
|
||||
def register_cron_routes(app):
|
||||
"""注册 Cron 相关路由"""
|
||||
|
||||
@app.route('/api/cron/tasks')
|
||||
def api_cron_tasks():
|
||||
"""获取数据库中的 Cron 任务列表"""
|
||||
db = get_db()
|
||||
c = db.cursor()
|
||||
c.execute("SELECT * FROM cron_tasks ORDER BY created_at DESC")
|
||||
tasks = [dict(row) for row in c.fetchall()]
|
||||
|
||||
# 添加人类可读描述和下次执行时间
|
||||
for task in tasks:
|
||||
task['human_time'] = cron_expression_to_human(task['expression'])
|
||||
task['next_run'] = get_next_run_time(task['expression'])
|
||||
|
||||
return jsonify({'tasks': tasks})
|
||||
|
||||
@app.route('/api/cron/tasks', methods=['POST'])
|
||||
def api_cron_add():
|
||||
"""添加新的 Cron 任务"""
|
||||
data = request.json
|
||||
|
||||
if not data.get('name') or not data.get('expression') or not data.get('command'):
|
||||
return jsonify({'error': '缺少必要字段'}), 400
|
||||
|
||||
db = get_db()
|
||||
c = db.cursor()
|
||||
|
||||
# 插入任务
|
||||
c.execute('''INSERT INTO cron_tasks (name, expression, command, description, category, enabled)
|
||||
VALUES (?, ?, ?, ?, ?, ?)''',
|
||||
(data['name'], data['expression'], data['command'], data.get('description', ''), data.get('category', 'other'), 1))
|
||||
|
||||
task_id = c.lastrowid
|
||||
db.commit()
|
||||
|
||||
# 记录历史(版本1)
|
||||
c.execute('''INSERT INTO cron_history (task_id, version, name, expression, command, description, category, enabled, operation)
|
||||
VALUES (?, 1, ?, ?, ?, ?, ?, ?, 'create')''',
|
||||
(task_id, data['name'], data['expression'], data['command'], data.get('description', ''), data.get('category', 'other'), 1))
|
||||
db.commit()
|
||||
|
||||
# 同步到系统 crontab
|
||||
sync_crontab_from_db(db)
|
||||
|
||||
log_message(f"添加 Cron 任务: {data['name']} ({data['expression']})")
|
||||
|
||||
return jsonify({'message': '任务添加成功', 'task_id': task_id})
|
||||
|
||||
@app.route('/api/cron/tasks/<int:task_id>', methods=['PUT'])
|
||||
def api_cron_update(task_id):
|
||||
"""更新 Cron 任务"""
|
||||
data = request.json
|
||||
db = get_db()
|
||||
c = db.cursor()
|
||||
|
||||
# 获取当前任务
|
||||
c.execute("SELECT * FROM cron_tasks WHERE id = ?", (task_id,))
|
||||
old_task = c.fetchone()
|
||||
if not old_task:
|
||||
return jsonify({'error': '任务不存在'}), 404
|
||||
|
||||
# 获取当前版本号
|
||||
c.execute("SELECT MAX(version) FROM cron_history WHERE task_id = ?", (task_id,))
|
||||
max_version = c.fetchone()[0] or 0
|
||||
new_version = max_version + 1
|
||||
|
||||
# 更新任务
|
||||
update_fields = []
|
||||
update_values = []
|
||||
for field in ['name', 'expression', 'command', 'description', 'category', 'enabled']:
|
||||
if field in data:
|
||||
update_fields.append(f"{field} = ?")
|
||||
update_values.append(data[field])
|
||||
|
||||
if update_fields:
|
||||
update_fields.append("updated_at = CURRENT_TIMESTAMP")
|
||||
c.execute(f"UPDATE cron_tasks SET {','.join(update_fields)} WHERE id = ?", update_values + [task_id])
|
||||
db.commit()
|
||||
|
||||
# 记录历史
|
||||
c.execute('''INSERT INTO cron_history (task_id, version, name, expression, command, description, category, enabled, operation)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'update')''',
|
||||
(task_id, new_version, data.get('name', old_task['name']), data.get('expression', old_task['expression']),
|
||||
data.get('command', old_task['command']), data.get('description', old_task['description']),
|
||||
data.get('category', old_task['category']), data.get('enabled', old_task['enabled'])))
|
||||
db.commit()
|
||||
|
||||
# 同步到系统 crontab
|
||||
sync_crontab_from_db(db)
|
||||
|
||||
log_message(f"更新 Cron 任务 ID={task_id}: 版本 {new_version}")
|
||||
|
||||
return jsonify({'message': '任务更新成功', 'version': new_version})
|
||||
|
||||
@app.route('/api/cron/tasks/<int:task_id>', methods=['DELETE'])
|
||||
def api_cron_delete(task_id):
|
||||
"""删除 Cron 任务"""
|
||||
db = get_db()
|
||||
c = db.cursor()
|
||||
|
||||
c.execute("SELECT * FROM cron_tasks WHERE id = ?", (task_id,))
|
||||
task = c.fetchone()
|
||||
if not task:
|
||||
return jsonify({'error': '任务不存在'}), 404
|
||||
|
||||
# 记录删除历史
|
||||
c.execute("SELECT MAX(version) FROM cron_history WHERE task_id = ?", (task_id,))
|
||||
max_version = c.fetchone()[0] or 0
|
||||
|
||||
c.execute('''INSERT INTO cron_history (task_id, version, name, expression, command, description, category, enabled, operation)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'delete')''',
|
||||
(task_id, max_version + 1, task['name'], task['expression'], task['command'], task['description'], task['category'], task['enabled']))
|
||||
db.commit()
|
||||
|
||||
# 删除任务
|
||||
c.execute("DELETE FROM cron_tasks WHERE id = ?", (task_id,))
|
||||
db.commit()
|
||||
|
||||
# 同步到系统 crontab
|
||||
sync_crontab_from_db(db)
|
||||
|
||||
log_message(f"删除 Cron 任务 ID={task_id}: {task['name']}")
|
||||
|
||||
return jsonify({'message': '任务删除成功'})
|
||||
|
||||
@app.route('/api/cron/tasks/<int:task_id>/toggle', methods=['POST'])
|
||||
def api_cron_toggle(task_id):
|
||||
"""启用/禁用 Cron 任务"""
|
||||
db = get_db()
|
||||
c = db.cursor()
|
||||
|
||||
c.execute("SELECT enabled FROM cron_tasks WHERE id = ?", (task_id,))
|
||||
task = c.fetchone()
|
||||
if not task:
|
||||
return jsonify({'error': '任务不存在'}), 404
|
||||
|
||||
new_enabled = 0 if task['enabled'] == 1 else 1
|
||||
c.execute("UPDATE cron_tasks SET enabled = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?", (new_enabled, task_id))
|
||||
db.commit()
|
||||
|
||||
# 同步到系统 crontab
|
||||
sync_crontab_from_db(db)
|
||||
|
||||
log_message(f"切换 Cron 任务 ID={task_id} 状态: {new_enabled}")
|
||||
|
||||
return jsonify({'message': '状态已切换', 'enabled': new_enabled})
|
||||
|
||||
@app.route('/api/cron/tasks/<int:task_id>/history')
|
||||
def api_cron_history(task_id):
|
||||
"""获取任务历史版本"""
|
||||
db = get_db()
|
||||
c = db.cursor()
|
||||
c.execute("SELECT * FROM cron_history WHERE task_id = ? ORDER BY version DESC", (task_id,))
|
||||
history = [dict(row) for row in c.fetchall()]
|
||||
|
||||
for h in history:
|
||||
if h['expression']:
|
||||
h['human_time'] = cron_expression_to_human(h['expression'])
|
||||
|
||||
return jsonify({'history': history})
|
||||
|
||||
@app.route('/api/cron/tasks/<int:task_id>/rollback/<int:version>', methods=['POST'])
|
||||
def api_cron_rollback(task_id, version):
|
||||
"""回退到指定版本"""
|
||||
db = get_db()
|
||||
c = db.cursor()
|
||||
|
||||
c.execute("SELECT * FROM cron_history WHERE task_id = ? AND version = ?", (task_id, version))
|
||||
history = c.fetchone()
|
||||
if not history:
|
||||
return jsonify({'error': '版本不存在'}), 404
|
||||
|
||||
# 更新任务到历史版本
|
||||
c.execute('''UPDATE cron_tasks SET name = ?, expression = ?, command = ?, description = ?, category = ?, enabled = ?, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?''',
|
||||
(history['name'], history['expression'], history['command'], history['description'], history['category'], history['enabled'], task_id))
|
||||
db.commit()
|
||||
|
||||
# 同步到系统 crontab
|
||||
sync_crontab_from_db(db)
|
||||
|
||||
log_message(f"回退 Cron 任务 ID={task_id} 到版本 {version}")
|
||||
|
||||
return jsonify({'message': f'已回退到版本 {version}'})
|
||||
|
||||
@app.route('/api/cron/tasks/<int:task_id>/logs')
|
||||
def api_cron_logs(task_id):
|
||||
"""获取任务执行日志"""
|
||||
db = get_db()
|
||||
c = db.cursor()
|
||||
c.execute("SELECT * FROM cron_logs WHERE task_id = ? ORDER BY execution_time DESC LIMIT 100", (task_id,))
|
||||
logs = [dict(row) for row in c.fetchall()]
|
||||
|
||||
return jsonify({'logs': logs})
|
||||
|
||||
@app.route('/api/cron/categories')
|
||||
def api_cron_categories():
|
||||
"""获取任务分类列表"""
|
||||
categories = [
|
||||
{'id': 'monitor', 'name': '系统监控', 'icon': 'ri-heart-pulse-line', 'color': 'blue'},
|
||||
{'id': 'backup', 'name': '数据备份', 'icon': 'ri-archive-line', 'color': 'green'},
|
||||
{'id': 'cleanup', 'name': '清理任务', 'icon': 'ri-delete-bin-line', 'color': 'yellow'},
|
||||
{'id': 'report', 'name': '报表生成', 'icon': 'ri-file-chart-line', 'color': 'purple'},
|
||||
{'id': 'sync', 'name': '数据同步', 'icon': 'ri-refresh-line', 'color': 'cyan'},
|
||||
{'id': 'notification', 'name': '通知提醒', 'icon': 'ri-notification-line', 'color': 'orange'},
|
||||
{'id': 'other', 'name': '其他任务', 'icon': 'ri-more-line', 'color': 'gray'},
|
||||
]
|
||||
return jsonify({'categories': categories})
|
||||
|
||||
@app.route('/api/cron/sync', methods=['POST'])
|
||||
def api_cron_sync():
|
||||
"""从系统 crontab 同步到数据库(清空旧的,只保留当前系统实际存在的)"""
|
||||
crons = get_system_crons()
|
||||
user_crons = [c for c in crons if c['source'] == 'user']
|
||||
|
||||
db = get_db()
|
||||
c = db.cursor()
|
||||
|
||||
# 清空数据库中的旧任务
|
||||
c.execute("DELETE FROM cron_history WHERE task_id IN (SELECT id FROM cron_tasks)")
|
||||
c.execute("DELETE FROM cron_logs WHERE task_id IN (SELECT id FROM cron_tasks)")
|
||||
c.execute("DELETE FROM cron_tasks")
|
||||
|
||||
synced_count = 0
|
||||
for cron in user_crons:
|
||||
# 尝试从命令推断分类和名称
|
||||
name = guess_cron_name(cron['command'])
|
||||
category = guess_cron_category(cron['command'])
|
||||
|
||||
c.execute('''INSERT INTO cron_tasks (name, expression, command, description, category, enabled)
|
||||
VALUES (?, ?, ?, ?, ?, 1)''',
|
||||
(name, cron['expression'], cron['command'], '', category))
|
||||
synced_count += 1
|
||||
|
||||
db.commit()
|
||||
|
||||
log_message(f"从系统 crontab 同步了 {synced_count} 个任务(已清空旧任务)")
|
||||
|
||||
return jsonify({'message': f'同步完成,共 {synced_count} 个任务', 'count': synced_count})
|
||||
58
routes_email.py
Normal file
58
routes_email.py
Normal file
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
邮件通知 API 路由
|
||||
"""
|
||||
|
||||
import os
|
||||
import smtplib
|
||||
from email.mime.text import MIMEText
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from flask import jsonify, request
|
||||
from utils import log_message
|
||||
|
||||
|
||||
def register_email_routes(app):
|
||||
"""注册邮件通知相关路由"""
|
||||
|
||||
@app.route('/api/email/send', methods=['POST'])
|
||||
def api_email_send():
|
||||
"""发送邮件通知"""
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return jsonify({'error': '无效数据'}), 400
|
||||
|
||||
to_email = data.get('to')
|
||||
subject = data.get('subject')
|
||||
body = data.get('body')
|
||||
|
||||
if not to_email or not subject or not body:
|
||||
return jsonify({'error': '缺少必要参数'}), 400
|
||||
|
||||
try:
|
||||
# SMTP配置 (从环境变量或默认值)
|
||||
smtp_host = os.environ.get('SMTP_HOST', 'mail.tphai.com')
|
||||
smtp_port = int(os.environ.get('SMTP_PORT', 587))
|
||||
smtp_user = os.environ.get('SMTP_USER', 'favor@tphai.com')
|
||||
smtp_pass = os.environ.get('SMTP_PASS', 'favor@!')
|
||||
|
||||
# 创建邮件
|
||||
msg = MIMEMultipart()
|
||||
msg['From'] = smtp_user
|
||||
msg['To'] = to_email
|
||||
msg['Subject'] = subject
|
||||
|
||||
# 添加正文
|
||||
msg.attach(MIMEText(body, 'plain', 'utf-8'))
|
||||
|
||||
# 发送邮件
|
||||
server = smtplib.SMTP(smtp_host, smtp_port)
|
||||
server.login(smtp_user, smtp_pass)
|
||||
server.send_message(msg)
|
||||
server.quit()
|
||||
|
||||
log_message(f"邮件发送成功: {subject} -> {to_email}")
|
||||
|
||||
return jsonify({'message': '邮件发送成功'})
|
||||
except Exception as e:
|
||||
log_message(f"邮件发送失败: {e}")
|
||||
return jsonify({'error': str(e)}), 500
|
||||
242
routes_project.py
Normal file
242
routes_project.py
Normal file
@@ -0,0 +1,242 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
项目管理 API 路由
|
||||
"""
|
||||
|
||||
import json
|
||||
from flask import jsonify, request
|
||||
from config import PROJECTS_FILE
|
||||
from utils import (
|
||||
log_message, load_projects, load_projects_full, save_projects,
|
||||
check_port, stop_process, run_command
|
||||
)
|
||||
|
||||
|
||||
def register_project_routes(app):
|
||||
"""注册项目管理相关路由"""
|
||||
|
||||
@app.route('/api/projects')
|
||||
def api_projects():
|
||||
"""获取项目列表"""
|
||||
projects = load_projects()
|
||||
|
||||
# 添加状态信息(结构化对象,前端期望 status.status 和 status.ports)
|
||||
for project in projects:
|
||||
ports = project.get('ports', [])
|
||||
port_statuses = {}
|
||||
for p in ports:
|
||||
port_statuses[str(p)] = {'running': check_port(p)}
|
||||
|
||||
running_count = sum(1 for ps in port_statuses.values() if ps['running'])
|
||||
if running_count == len(ports) and len(ports) > 0:
|
||||
overall = 'running'
|
||||
elif running_count > 0:
|
||||
overall = 'partial'
|
||||
else:
|
||||
overall = 'stopped'
|
||||
|
||||
project['status'] = {
|
||||
'status': overall,
|
||||
'ports': port_statuses
|
||||
}
|
||||
|
||||
return jsonify({
|
||||
'projects': projects,
|
||||
'server': load_projects_full().get('server', {})
|
||||
})
|
||||
|
||||
@app.route('/api/projects/<project_id>/start', methods=['POST'])
|
||||
def api_start(project_id):
|
||||
projects = load_projects()
|
||||
project = next((p for p in projects if p['id'] == project_id), None)
|
||||
if not project:
|
||||
return jsonify({'error': '项目不存在'}), 404
|
||||
|
||||
if project['type'] != 'web':
|
||||
return jsonify({'error': '只有Web服务支持启动'}), 400
|
||||
|
||||
cwd = project.get('directory', '')
|
||||
if not cwd:
|
||||
return jsonify({'error': '项目目录未配置'}), 400
|
||||
|
||||
from config import WORKSPACE_DIR
|
||||
import os
|
||||
cwd = os.path.join(WORKSPACE_DIR, cwd) if not cwd.startswith('/') else cwd
|
||||
|
||||
if not os.path.exists(cwd):
|
||||
return jsonify({'error': f'目录不存在: {cwd}'}), 400
|
||||
|
||||
if project.get('start_cmds'):
|
||||
for name, info in project['start_cmds'].items():
|
||||
run_command(info['cmd'], cwd, project_id, f'start-{name}')
|
||||
else:
|
||||
cmd = project.get('start_cmd', 'python3 app.py')
|
||||
run_command(cmd, cwd, project_id, 'start')
|
||||
|
||||
return jsonify({'message': '服务启动中...'})
|
||||
|
||||
@app.route('/api/projects/<project_id>/stop', methods=['POST'])
|
||||
def api_stop(project_id):
|
||||
projects = load_projects()
|
||||
project = next((p for p in projects if p['id'] == project_id), None)
|
||||
if not project:
|
||||
return jsonify({'error': '项目不存在'}), 404
|
||||
|
||||
if project['type'] != 'web':
|
||||
return jsonify({'error': '只有Web服务支持停止'}), 400
|
||||
|
||||
ports = project.get('ports', [])
|
||||
stopped = 0
|
||||
for port in ports:
|
||||
stopped += stop_process(port)
|
||||
|
||||
return jsonify({'message': f'已停止 {stopped} 个进程'})
|
||||
|
||||
@app.route('/api/projects/<project_id>/restart', methods=['POST'])
|
||||
def api_restart(project_id):
|
||||
"""重启服务"""
|
||||
# 先停止
|
||||
api_stop(project_id)
|
||||
|
||||
# 等待1秒
|
||||
import time
|
||||
time.sleep(1)
|
||||
|
||||
# 再启动
|
||||
return api_start(project_id)
|
||||
|
||||
@app.route('/api/projects/add', methods=['POST'])
|
||||
def api_add_project():
|
||||
"""添加新项目"""
|
||||
data = request.get_json()
|
||||
|
||||
if not data.get('id') or not data.get('name') or not data.get('ports'):
|
||||
return jsonify({'error': '缺少必要字段'}), 400
|
||||
|
||||
projects = load_projects()
|
||||
|
||||
# 检查ID是否已存在
|
||||
if any(p['id'] == data['id'] for p in projects):
|
||||
return jsonify({'error': '项目ID已存在'}), 400
|
||||
|
||||
# 添加项目
|
||||
project = {
|
||||
'id': data['id'],
|
||||
'name': data['name'],
|
||||
'type': data.get('type', 'web'),
|
||||
'ports': data['ports'],
|
||||
'directory': data.get('directory', ''),
|
||||
'start_cmd': data.get('start_cmd', ''),
|
||||
'stop_cmd': data.get('stop_cmd', ''),
|
||||
'health_url': data.get('health_url', ''),
|
||||
'description': data.get('description', ''),
|
||||
'git_repo': data.get('git_repo', ''),
|
||||
'version': data.get('version', 'v1.0.0')
|
||||
}
|
||||
|
||||
projects.append(project)
|
||||
save_projects(projects)
|
||||
|
||||
log_message(f"添加项目: {project['name']}")
|
||||
|
||||
return jsonify({'message': '项目添加成功', 'project': project})
|
||||
|
||||
@app.route('/api/projects/<project_id>', methods=['PUT'])
|
||||
def api_update_project(project_id):
|
||||
"""更新项目配置"""
|
||||
data = request.get_json()
|
||||
projects = load_projects()
|
||||
|
||||
project = next((p for p in projects if p['id'] == project_id), None)
|
||||
if not project:
|
||||
return jsonify({'error': '项目不存在'}), 404
|
||||
|
||||
# 更新字段
|
||||
for key in ['name', 'ports', 'directory', 'start_cmd', 'stop_cmd', 'health_url', 'description', 'git_repo', 'version']:
|
||||
if key in data:
|
||||
project[key] = data[key]
|
||||
|
||||
save_projects(projects)
|
||||
|
||||
log_message(f"更新项目: {project['name']}")
|
||||
|
||||
return jsonify({'message': '项目更新成功'})
|
||||
|
||||
@app.route('/api/projects/<project_id>', methods=['DELETE'])
|
||||
def api_delete_project(project_id):
|
||||
"""删除项目"""
|
||||
projects = load_projects()
|
||||
project = next((p for p in projects if p['id'] == project_id), None)
|
||||
|
||||
if not project:
|
||||
return jsonify({'error': '项目不存在'}), 404
|
||||
|
||||
projects = [p for p in projects if p['id'] != project_id]
|
||||
save_projects(projects)
|
||||
|
||||
log_message(f"删除项目: {project['name']}")
|
||||
|
||||
return jsonify({'message': '项目删除成功'})
|
||||
|
||||
@app.route('/api/external-services')
|
||||
def api_external_services():
|
||||
"""获取外部服务列表"""
|
||||
data = load_projects_full()
|
||||
services = data.get('external_services', [])
|
||||
|
||||
# 添加状态(结构化对象,前端期望 status.status 和 status.ports)
|
||||
for service in services:
|
||||
ports = service.get('ports', [])
|
||||
port_statuses = {}
|
||||
for p in ports:
|
||||
port_statuses[str(p)] = {'running': check_port(p)}
|
||||
|
||||
running_count = sum(1 for ps in port_statuses.values() if ps['running'])
|
||||
if running_count == len(ports) and len(ports) > 0:
|
||||
overall = 'running'
|
||||
elif running_count > 0:
|
||||
overall = 'partial'
|
||||
else:
|
||||
overall = 'stopped'
|
||||
|
||||
service['status'] = {
|
||||
'status': overall,
|
||||
'ports': port_statuses
|
||||
}
|
||||
|
||||
return jsonify({'services': services})
|
||||
|
||||
@app.route('/api/external-services/add', methods=['POST'])
|
||||
def api_add_external_service():
|
||||
"""添加外部服务"""
|
||||
data = request.get_json()
|
||||
|
||||
if not data.get('id') or not data.get('name') or not data.get('ports'):
|
||||
return jsonify({'error': '缺少必要字段'}), 400
|
||||
|
||||
full_data = load_projects_full()
|
||||
services = full_data.get('external_services', [])
|
||||
|
||||
# 检查ID是否已存在
|
||||
if any(s['id'] == data['id'] for s in services):
|
||||
return jsonify({'error': '服务ID已存在'}), 400
|
||||
|
||||
# 添加服务
|
||||
service = {
|
||||
'id': data['id'],
|
||||
'name': data['name'],
|
||||
'type': 'external',
|
||||
'ports': data['ports'],
|
||||
'health_url': data.get('health_url', ''),
|
||||
'description': data.get('description', '')
|
||||
}
|
||||
|
||||
services.append(service)
|
||||
full_data['external_services'] = services
|
||||
|
||||
with open(PROJECTS_FILE, 'w', encoding='utf-8') as f:
|
||||
json.dump(full_data, f, ensure_ascii=False, indent=2)
|
||||
|
||||
log_message(f"添加外部服务: {service['name']}")
|
||||
|
||||
return jsonify({'message': '服务添加成功', 'service': service})
|
||||
199
routes_system.py
Normal file
199
routes_system.py
Normal file
@@ -0,0 +1,199 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
系统资源监控 API 路由
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import time
|
||||
from datetime import datetime
|
||||
from flask import jsonify, request
|
||||
from utils import log_message, get_cpu_temperatures, load_alert_config
|
||||
|
||||
try:
|
||||
import psutil
|
||||
HAS_PSUTIL = True
|
||||
_last_net_stats = {'bytes_sent': 0, 'bytes_recv': 0, 'time': 0}
|
||||
except ImportError:
|
||||
HAS_PSUTIL = False
|
||||
|
||||
|
||||
def register_system_routes(app):
|
||||
"""注册系统监控相关路由"""
|
||||
|
||||
@app.route('/api/system/stats')
|
||||
def api_system_stats():
|
||||
"""获取系统资源信息"""
|
||||
if not HAS_PSUTIL:
|
||||
return jsonify({'error': 'psutil未安装', 'available': False})
|
||||
|
||||
try:
|
||||
# CPU
|
||||
cpu_percent = psutil.cpu_percent(interval=0.5)
|
||||
cpu_count = psutil.cpu_count(logical=False) or psutil.cpu_count()
|
||||
cpu_count_logical = psutil.cpu_count(logical=True)
|
||||
|
||||
# CPU 温度
|
||||
cpu_temps = get_cpu_temperatures()
|
||||
|
||||
# 内存
|
||||
mem = psutil.virtual_memory()
|
||||
mem_total_gb = round(mem.total / (1024**3), 2)
|
||||
mem_used_gb = round(mem.used / (1024**3), 2)
|
||||
mem_available_gb = round(mem.available / (1024**3), 2)
|
||||
mem_percent = mem.percent
|
||||
|
||||
# 磁盘
|
||||
disk = psutil.disk_usage('/')
|
||||
disk_total_gb = round(disk.total / (1024**3), 2)
|
||||
disk_used_gb = round(disk.used / (1024**3), 2)
|
||||
disk_free_gb = round(disk.free / (1024**3), 2)
|
||||
disk_percent = disk.percent
|
||||
|
||||
# 网速(需要计算差值)
|
||||
global _last_net_stats
|
||||
current_time = time.time()
|
||||
net = psutil.net_io_counters()
|
||||
|
||||
if _last_net_stats['time'] > 0:
|
||||
time_diff = current_time - _last_net_stats['time']
|
||||
bytes_sent_diff = net.bytes_sent - _last_net_stats['bytes_sent']
|
||||
bytes_recv_diff = net.bytes_recv - _last_net_stats['bytes_recv']
|
||||
|
||||
upload_speed = (bytes_sent_diff / time_diff) / 1024 # KB/s
|
||||
download_speed = (bytes_recv_diff / time_diff) / 1024 # KB/s
|
||||
else:
|
||||
upload_speed = 0
|
||||
download_speed = 0
|
||||
|
||||
_last_net_stats = {
|
||||
'bytes_sent': net.bytes_sent,
|
||||
'bytes_recv': net.bytes_recv,
|
||||
'time': current_time
|
||||
}
|
||||
|
||||
# 进程数量
|
||||
process_count = len(psutil.pids())
|
||||
|
||||
# 系统启动时间
|
||||
boot_time = psutil.boot_time()
|
||||
uptime_seconds = time.time() - boot_time
|
||||
uptime_hours = int(uptime_seconds // 3600)
|
||||
uptime_days = int(uptime_hours // 24)
|
||||
uptime_str = f"{uptime_days}天 {uptime_hours % 24}小时"
|
||||
|
||||
return jsonify({
|
||||
'available': True,
|
||||
'system': {
|
||||
'uptime': uptime_str,
|
||||
'boot_time': datetime.fromtimestamp(boot_time).strftime('%Y-%m-%d %H:%M:%S'),
|
||||
'process_count': process_count
|
||||
},
|
||||
'cpu': {
|
||||
'percent': cpu_percent,
|
||||
'count': cpu_count,
|
||||
'logical': cpu_count_logical,
|
||||
'temp_package_0': cpu_temps.get('package_0'),
|
||||
'temp_package_1': cpu_temps.get('package_1')
|
||||
},
|
||||
'memory': {
|
||||
'total_gb': mem_total_gb,
|
||||
'used_gb': mem_used_gb,
|
||||
'available_gb': mem_available_gb,
|
||||
'percent': mem_percent
|
||||
},
|
||||
'disk': {
|
||||
'total_gb': disk_total_gb,
|
||||
'used_gb': disk_used_gb,
|
||||
'free_gb': disk_free_gb,
|
||||
'percent': disk_percent
|
||||
},
|
||||
'network': {
|
||||
'sent_mb': round(net.bytes_sent / (1024**2), 2),
|
||||
'recv_mb': round(net.bytes_recv / (1024**2), 2),
|
||||
'sent_speed': round(upload_speed, 2),
|
||||
'recv_speed': round(download_speed, 2)
|
||||
}
|
||||
})
|
||||
except Exception as e:
|
||||
log_message(f"系统监控API错误: {e}")
|
||||
return jsonify({'error': str(e), 'available': False}), 500
|
||||
|
||||
@app.route('/api/system/processes')
|
||||
def api_system_processes():
|
||||
"""获取进程列表"""
|
||||
if not HAS_PSUTIL:
|
||||
return jsonify({'error': 'psutil未安装'}), 500
|
||||
|
||||
try:
|
||||
limit = int(request.args.get('limit', 10))
|
||||
limit = max(1, min(50, limit))
|
||||
|
||||
processes = []
|
||||
for proc in psutil.process_iter(['pid', 'name', 'cpu_percent', 'memory_percent', 'status']):
|
||||
try:
|
||||
processes.append({
|
||||
'pid': proc.info['pid'],
|
||||
'name': proc.info['name'],
|
||||
'cpu': proc.info['cpu_percent'] or 0,
|
||||
'memory': proc.info['memory_percent'] or 0,
|
||||
'status': proc.info['status']
|
||||
})
|
||||
except:
|
||||
pass
|
||||
|
||||
# 按CPU排序
|
||||
processes.sort(key=lambda x: x['cpu'], reverse=True)
|
||||
processes = processes[:limit]
|
||||
|
||||
return jsonify({'processes': processes})
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/system/docker')
|
||||
def api_system_docker():
|
||||
"""获取 Docker 容器状态"""
|
||||
try:
|
||||
result = subprocess.run(['docker', 'ps', '--format', '{{.Names}}|{{.Status}}|{{.Ports}}'],
|
||||
capture_output=True, text=True, timeout=5)
|
||||
containers = []
|
||||
for line in result.stdout.strip().split('\n'):
|
||||
if line:
|
||||
parts = line.split('|')
|
||||
if len(parts) >= 2:
|
||||
containers.append({
|
||||
'name': parts[0],
|
||||
'status': parts[1],
|
||||
'ports': parts[2] if len(parts) > 2 else ''
|
||||
})
|
||||
return jsonify({'containers': containers})
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e), 'containers': []})
|
||||
|
||||
@app.route('/api/alerts', methods=['GET'])
|
||||
def api_get_alerts():
|
||||
"""获取预警配置"""
|
||||
config = load_alert_config()
|
||||
return jsonify(config)
|
||||
|
||||
@app.route('/api/alerts', methods=['POST'])
|
||||
def api_save_alerts():
|
||||
"""保存预警配置"""
|
||||
import json
|
||||
from config import ALERT_CONFIG_FILE
|
||||
|
||||
try:
|
||||
data = request.get_json()
|
||||
config = load_alert_config()
|
||||
|
||||
if 'thresholds' in data:
|
||||
config['thresholds'] = data['thresholds']
|
||||
if 'web_settings' in data:
|
||||
config['web_settings'] = data['web_settings']
|
||||
|
||||
with open(ALERT_CONFIG_FILE, 'w', encoding='utf-8') as f:
|
||||
json.dump(config, f, ensure_ascii=False, indent=2)
|
||||
|
||||
return jsonify({'message': '保存成功'})
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
48
templates.py
Normal file
48
templates.py
Normal file
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
HTML 模板 - 包含所有完整功能
|
||||
"""
|
||||
|
||||
from config import APP_NAME, APP_VERSION
|
||||
|
||||
|
||||
def get_html_template():
|
||||
"""返回完整的HTML模板(包含所有原有功能)"""
|
||||
# 读取完整模板文件
|
||||
import os
|
||||
template_file = os.path.join(os.path.dirname(__file__), 'templates_full.txt')
|
||||
|
||||
try:
|
||||
with open(template_file, 'r', encoding='utf-8') as f:
|
||||
template_content = f.read()
|
||||
|
||||
# 替换版本号(支持多种格式)
|
||||
template_content = template_content.replace('v2.1', APP_VERSION)
|
||||
template_content = template_content.replace('v3.3.1', APP_VERSION)
|
||||
template_content = template_content.replace('v3.3.5', APP_VERSION)
|
||||
template_content = template_content.replace('v3.4.0', APP_VERSION)
|
||||
template_content = template_content.replace('v3.4.1', APP_VERSION) # 预留未来版本
|
||||
|
||||
return template_content
|
||||
except:
|
||||
# 如果文件不存在,返回基本模板
|
||||
return get_basic_template()
|
||||
|
||||
|
||||
def get_basic_template():
|
||||
"""基本模板(备用)"""
|
||||
return '''<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>''' + APP_NAME + '''</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link href="https://cdn.remarkable.ai/css/remixicon.css" rel="stylesheet">
|
||||
</head>
|
||||
<body class="min-h-screen text-gray-100">
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<h1 class="text-3xl font-bold">''' + APP_NAME + ''' ''' + APP_VERSION + '''</h1>
|
||||
<p>模板加载失败,请检查 templates_full.txt 文件</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>'''
|
||||
Binary file not shown.
332
utils.py
Normal file
332
utils.py
Normal file
@@ -0,0 +1,332 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
工具函数模块
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import subprocess
|
||||
import re
|
||||
import signal
|
||||
import shutil
|
||||
from datetime import datetime
|
||||
from config import LOG_FILE, PROJECTS_FILE, CRON_BACKUP_DIR, ALERT_CONFIG_FILE
|
||||
|
||||
|
||||
# 进程状态缓存
|
||||
process_cache = {}
|
||||
|
||||
|
||||
def log_message(msg):
|
||||
"""写入日志"""
|
||||
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
log_line = f"[{timestamp}] {msg}\n"
|
||||
print(log_line.strip())
|
||||
try:
|
||||
os.makedirs(os.path.dirname(LOG_FILE), exist_ok=True)
|
||||
with open(LOG_FILE, 'a') as f:
|
||||
f.write(log_line)
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
def load_projects():
|
||||
"""加载项目配置"""
|
||||
try:
|
||||
with open(PROJECTS_FILE, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
return data.get('projects', [])
|
||||
except:
|
||||
return []
|
||||
|
||||
|
||||
def load_projects_full():
|
||||
"""加载完整项目配置(包含server和external_services)"""
|
||||
try:
|
||||
with open(PROJECTS_FILE, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
except:
|
||||
return {'server': {}, 'projects': [], 'external_services': []}
|
||||
|
||||
|
||||
def save_projects(projects):
|
||||
"""保存项目配置"""
|
||||
with open(PROJECTS_FILE, 'w', encoding='utf-8') as f:
|
||||
json.dump({'projects': projects}, f, ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
def check_port(port):
|
||||
"""检查端口是否有进程监听"""
|
||||
try:
|
||||
import socket
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.settimeout(1)
|
||||
result = sock.connect_ex(('localhost', port))
|
||||
sock.close()
|
||||
return result == 0
|
||||
except:
|
||||
return False
|
||||
|
||||
|
||||
def stop_process(port):
|
||||
"""停止占用指定端口的进程"""
|
||||
try:
|
||||
# 使用 ss 找到占用端口的进程
|
||||
result = subprocess.run(f"ss -tlnp | grep :{port}", shell=True, capture_output=True, text=True)
|
||||
if result.stdout:
|
||||
# 解析 pid
|
||||
import re
|
||||
match = re.search(r'pid=(\d+)', result.stdout)
|
||||
if match:
|
||||
pid = int(match.group(1))
|
||||
os.kill(pid, signal.SIGTERM)
|
||||
log_message(f"已停止进程 {pid} (端口 {port})")
|
||||
return 1
|
||||
except Exception as e:
|
||||
log_message(f"停止进程失败: {e}")
|
||||
return 0
|
||||
|
||||
|
||||
def run_command(cmd, cwd, project_id, action):
|
||||
"""运行命令"""
|
||||
log_message(f"[{project_id}] 执行 {action}: {cmd}")
|
||||
try:
|
||||
subprocess.Popen(cmd, shell=True, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
return True
|
||||
except Exception as e:
|
||||
log_message(f"命令执行失败: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def backup_crontab():
|
||||
"""备份当前 crontab"""
|
||||
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||
backup_file = os.path.join(CRON_BACKUP_DIR, f'crontab_{timestamp}.txt')
|
||||
|
||||
try:
|
||||
os.makedirs(CRON_BACKUP_DIR, exist_ok=True)
|
||||
result = subprocess.run("crontab -l 2>/dev/null", shell=True, capture_output=True, text=True)
|
||||
with open(backup_file, 'w') as f:
|
||||
f.write(result.stdout)
|
||||
log_message(f"crontab 已备份到 {backup_file}")
|
||||
except Exception as e:
|
||||
log_message(f"备份 crontab 失败: {e}")
|
||||
|
||||
|
||||
def get_system_crons():
|
||||
"""获取系统所有 cron 任务"""
|
||||
crons = []
|
||||
|
||||
try:
|
||||
result = subprocess.run("crontab -l 2>/dev/null", shell=True, capture_output=True, text=True, timeout=5)
|
||||
for line in result.stdout.strip().split('\n'):
|
||||
line = line.strip()
|
||||
if line and not line.startswith('#'):
|
||||
parts = line.split()
|
||||
if len(parts) >= 6:
|
||||
crons.append({'source': 'user', 'expression': ' '.join(parts[:5]), 'command': ' '.join(parts[5:]), 'line': line})
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
if os.path.exists('/etc/crontab'):
|
||||
with open('/etc/crontab', 'r') as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line and not line.startswith('#'):
|
||||
parts = line.split()
|
||||
if len(parts) >= 7:
|
||||
crons.append({'source': 'system', 'expression': ' '.join(parts[:5]), 'user': parts[5], 'command': ' '.join(parts[6:]), 'line': line})
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
if os.path.exists('/etc/cron.d'):
|
||||
for filename in os.listdir('/etc/cron.d'):
|
||||
filepath = os.path.join('/etc/cron.d', filename)
|
||||
if os.path.isfile(filepath):
|
||||
with open(filepath, 'r') as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line and not line.startswith('#'):
|
||||
parts = line.split()
|
||||
if len(parts) >= 7:
|
||||
crons.append({'source': 'cron.d', 'file': filename, 'expression': ' '.join(parts[:5]), 'user': parts[5], 'command': ' '.join(parts[6:]), 'line': line})
|
||||
except:
|
||||
pass
|
||||
|
||||
return crons
|
||||
|
||||
|
||||
def parse_cron_expression(expr):
|
||||
"""解析 cron 表达式为人类可读格式"""
|
||||
parts = expr.split()
|
||||
if len(parts) != 5:
|
||||
return expr
|
||||
|
||||
minute, hour, day, month, weekday = parts
|
||||
desc = []
|
||||
|
||||
if minute == '*':
|
||||
desc.append('每分钟')
|
||||
elif minute.startswith('*/'):
|
||||
desc.append(f'每{minute[2:]}分钟')
|
||||
else:
|
||||
desc.append(f'{minute}分')
|
||||
|
||||
if hour != '*':
|
||||
if hour.startswith('*/'):
|
||||
desc.append(f'每{hour[2:]}小时')
|
||||
else:
|
||||
desc.append(f'{hour}时')
|
||||
|
||||
if day != '*' and not day.startswith('*'):
|
||||
desc.append(f'{day}日')
|
||||
|
||||
if month != '*' and not month.startswith('*'):
|
||||
desc.append(f'{month}月')
|
||||
|
||||
if weekday != '*' and not weekday.startswith('*'):
|
||||
weekdays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
|
||||
try:
|
||||
desc.append(weekdays[int(weekday)])
|
||||
except:
|
||||
pass
|
||||
|
||||
return ' '.join(desc) if desc else expr
|
||||
|
||||
def cron_expression_to_human(expr):
|
||||
"""更友好的人类可读格式"""
|
||||
parts = expr.split()
|
||||
if len(parts) != 5:
|
||||
return expr
|
||||
|
||||
minute, hour, day, month, weekday = parts
|
||||
|
||||
# 特殊模式
|
||||
patterns = [
|
||||
('0 4 * * *', '每天凌晨4点'),
|
||||
('0 0 * * *', '每天午夜0点'),
|
||||
('*/5 * * * *', '每5分钟'),
|
||||
('*/10 * * * *', '每10分钟'),
|
||||
('*/20 * * * *', '每20分钟'),
|
||||
('*/30 * * * *', '每30分钟'),
|
||||
('0 */1 * * *', '每小时'),
|
||||
('0 */2 * * *', '每2小时'),
|
||||
('0 9 * * 1', '每周一9点'),
|
||||
('0 9 * * 1-5', '每周一到五9点'),
|
||||
]
|
||||
|
||||
for pattern, desc in patterns:
|
||||
if expr == pattern:
|
||||
return desc
|
||||
|
||||
# 通用解析
|
||||
return parse_cron_expression(expr)
|
||||
|
||||
|
||||
def guess_cron_name(command):
|
||||
"""从命令推断任务名称"""
|
||||
if 'openclaw' in command or 'agent' in command:
|
||||
return 'OpenClaw Agent'
|
||||
elif 'backup' in command:
|
||||
return '备份任务'
|
||||
elif 'sync' in command:
|
||||
return '同步任务'
|
||||
elif 'clean' in command or 'cleanup' in command:
|
||||
return '清理任务'
|
||||
elif 'report' in command:
|
||||
return '报告任务'
|
||||
elif 'monitor' in command or 'check' in command:
|
||||
return '监控任务'
|
||||
elif 'git' in command:
|
||||
return 'Git 任务'
|
||||
return '自定义任务'
|
||||
|
||||
|
||||
def guess_cron_category(command):
|
||||
"""从命令推断分类"""
|
||||
if 'openclaw' in command:
|
||||
return 'agent'
|
||||
elif 'backup' in command:
|
||||
return 'backup'
|
||||
elif 'sync' in command:
|
||||
return 'sync'
|
||||
elif 'clean' in command or 'cleanup' in command:
|
||||
return 'cleanup'
|
||||
elif 'report' in command:
|
||||
return 'report'
|
||||
elif 'git' in command:
|
||||
return 'git'
|
||||
return 'other'
|
||||
|
||||
|
||||
def sync_crontab_from_db(db):
|
||||
"""从数据库同步到系统 crontab"""
|
||||
c = db.cursor()
|
||||
c.execute("SELECT expression, command, enabled FROM cron_tasks WHERE enabled = 1")
|
||||
tasks = c.fetchall()
|
||||
|
||||
lines = []
|
||||
for task in tasks:
|
||||
lines.append(f"{task['expression']} {task['command']}")
|
||||
|
||||
# 写入临时文件
|
||||
tmp_file = '/tmp/crontab_sync.tmp'
|
||||
with open(tmp_file, 'w') as f:
|
||||
f.write('\n'.join(lines) + '\n')
|
||||
|
||||
# 安装 crontab
|
||||
subprocess.run(f"crontab {tmp_file}", shell=True)
|
||||
os.remove(tmp_file)
|
||||
|
||||
# 备份
|
||||
backup_crontab()
|
||||
|
||||
|
||||
def get_next_run_time(expression):
|
||||
"""获取下次执行时间"""
|
||||
try:
|
||||
from croniter import croniter
|
||||
base = datetime.now()
|
||||
cron = croniter(expression, base)
|
||||
next_time = cron.get_next(datetime)
|
||||
return next_time.strftime('%Y-%m-%d %H:%M:%S')
|
||||
except:
|
||||
return '无法计算'
|
||||
|
||||
|
||||
def get_cpu_temperatures():
|
||||
"""获取 CPU Package 温度"""
|
||||
temps = {'package_0': None, 'package_1': None}
|
||||
try:
|
||||
result = subprocess.run(['sensors'], capture_output=True, text=True, timeout=5)
|
||||
output = result.stdout
|
||||
# 解析 Package id 0 和 Package id 1
|
||||
for line in output.split('\n'):
|
||||
if 'Package id 0:' in line:
|
||||
match = re.search(r'Package id 0:\s*([+\d.]+)°C', line)
|
||||
if match:
|
||||
temps['package_0'] = float(match.group(1).replace('+', ''))
|
||||
elif 'Package id 1:' in line:
|
||||
match = re.search(r'Package id 1:\s*([+\d.]+)°C', line)
|
||||
if match:
|
||||
temps['package_1'] = float(match.group(1).replace('+', ''))
|
||||
except Exception as e:
|
||||
log_message(f"获取CPU温度失败: {e}")
|
||||
return temps
|
||||
|
||||
|
||||
def load_alert_config():
|
||||
"""加载预警配置"""
|
||||
try:
|
||||
with open(ALERT_CONFIG_FILE, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
except:
|
||||
return {}
|
||||
|
||||
|
||||
def save_alert_config(config):
|
||||
"""保存预警配置"""
|
||||
with open(ALERT_CONFIG_FILE, 'w', encoding='utf-8') as f:
|
||||
json.dump(config, f, ensure_ascii=False, indent=2)
|
||||
Reference in New Issue
Block a user