Compare commits

...

17 Commits

Author SHA1 Message Date
ecbba836d9 feat: 监控阈值设置新增CPU温度项
- 默认阈值: 80°C (范围 30-120°C)
- 同时监控 CPU0 和 CPU1 (双路服务器)
- 超温时弹窗警告 + 桌面通知
2026-06-01 17:13:06 +08:00
bb19141edc fix: 修复Web服务监控区域状态显示问题
后端 status 字段从字符串改为结构化对象 {status, ports: {port: {running}}}
前端期望的是 p.status.status 和 p.status.ports[port].running,之前的字符串格式导致:
- 所有服务状态始终显示「未知」
- 所有端口始终显示红色(已停止)
2026-06-01 17:08:09 +08:00
c960048042 fix: 重写routes_system.py修复API错误
完整重写系统监控API,确保:
- 正确的数据结构匹配JavaScript期待
- system.uptime/system.boot_time/system.process_count
- cpu.count/cpu.logical
- disk.free_gb
- network.sent_mb/recv_mb/sent_speed/recv_speed
- 所有异常处理和日志记录

修复500错误,现在系统监控应该正常显示
2026-06-01 16:19:59 +08:00
51f7ee9ad0 fix: 修正系统监控API返回数据结构
匹配JavaScript期待的数据结构:
- system.uptime(系统运行时间)
- system.boot_time(启动时间)
- system.process_count(进程数)
- cpu.count/cpu.logical(CPU核心数)
- disk.free_gb(磁盘剩余空间)
- network.sent_mb/recv_mb(累计流量)
- network.sent_speed/recv_speed(实时网速)

确保前端能正确显示所有系统资源信息
2026-06-01 16:17:47 +08:00
ea904fb2b2 fix: 清理templates_full.txt开头的Python代码
移除模板文件开头的Python代码片段:
- '# ==================== HTML 模板 ===================='
- 'HTML_TEMPLATE = '''

确保templates_full.txt是纯粹的HTML模板,从<!DOCTYPE html>开始

这是导致浏览器console报错的根本原因:
- Python代码片段会被浏览器当作JavaScript解析
- 导致JavaScript执行失败,函数未定义
- closeEmailRuleModal等函数无法正常工作
2026-06-01 16:12:37 +08:00
21240dc108 fix: 替换模板中的默认IP地址为121.40.164.32
修复JavaScript代码中的默认IP值:
- localStorage.getItem('externalIp') || '192.168.2.17'
- 改为:|| '121.40.164.32'

确保所有默认值统一为正确的对外IP地址
2026-06-01 16:11:19 +08:00
1d7f4c2bed fix: 清理模板文件末尾的多余代码
移除 templates_full.txt 末尾不应该存在的Python代码片段:
- 移除 ''' 字符串结束标记
- 移除 if __name__ == '__main__' 部分
- 确保HTML模板在 </html> 处正确结束

这个问题可能导致页面加载异常
2026-06-01 16:02:59 +08:00
88c11277cf fix: 修正版本号为 v3.4.1 2026-06-01 15:56:47 +08:00
e5ae822ed2 docs: 更新版本号为 v3.4.1 2026-06-01 15:55:19 +08:00
50341749b6 fix: 使用完整模板,保留所有网页功能
 包含所有原有功能:
- 阈值设置(CPU/内存/磁盘/间隔)
- 邮件规则管理
- 自定义链接添加
- 进程列表显示
- Docker容器监控
- Cron任务管理
- 系统资源实时监控
- 配置导入导出

📝 templates_full.txt: 从v3.3.5提取的完整HTML模板(2366行)
📝 templates.py: 动态加载完整模板并替换版本号
2026-06-01 15:54:45 +08:00
7904117b1d fix: 添加 cron_expression_to_human 函数到 utils.py 2026-06-01 12:31:38 +08:00
69edd1c63e refactor: 模块化重构 v3.4.0
🎯 重构目标:
- 将 3700+ 行单文件拆分为多个模块
- 清晰的职责分离,便于维护和扩展

📁 模块结构:
- config.py: 配置常量和路径
- database.py: 数据库操作
- utils.py: 工具函数(日志、进程、cron等)
- routes_cron.py: Cron管理API
- routes_system.py: 系统监控API
- routes_config.py: 配置管理API
- routes_email.py: 邮件通知API
- routes_project.py: 项目管理API
- templates.py: HTML模板
- app.py: 主入口文件

 优势:
- 易于维护:每个模块职责明确
- 易于扩展:新增功能只需添加对应模块
- 易于测试:模块独立,便于单元测试
- 易于理解:新人能快速上手

📝 更新 README.md,详细说明项目结构和API接口
2026-06-01 12:30:24 +08:00
6d1cd478be merge: 合并远程更新,保留配置管理功能 2026-06-01 12:13:23 +08:00
ccc84e4d36 fix: 修正 JavaScript 字符串转义问题(使用双反斜杠)
问题分析:
- Flask render_template_string 会解析字符串中的转义序列
- Python 字符串 '\n' 在渲染后变成真实的换行符
- 导致 JavaScript 语法错误

修复方案:
- 使用双反斜杠 '\\n'
- 渲染后得到正确的 JavaScript 字符串 '\n'
- 避免换行符直接出现在 JavaScript 代码中
2026-06-01 11:47:01 +08:00
1fdb00a1ec fix: 修复 JavaScript 字符串转义错误
修复内容:
- 修正 sendDesktopNotification 函数中的字符串转义
- warnings.join('\n') 改为 warnings.join('\n')
- 解决浏览器控制台 SyntaxError 错误
2026-06-01 11:41:19 +08:00
248c951a96 fix: 将对外 IP 固定为 121.40.164.32
修改内容:
- 移除 IP 输入框,改为静态显示
- 固定 externalIp 为 121.40.164.32
- 移除 saveExternalIp() 函数
- 所有 localhost 链接将自动替换为对外 IP
2026-06-01 11:37:07 +08:00
761edef6fe feat: 增加 Docker 监控功能 v3.0.0
新增功能:
- Docker 容器监控(CPU、内存、网络I/O、磁盘I/O)
- 容器控制(启动、停止、重启)
- 容器日志查看
- 镜像列表管理
- Docker 系统信息
- 一键清理未使用资源

API 接口:
- /api/docker/containers - 容器列表
- /api/docker/container/<id>/stats - 容器资源统计
- /api/docker/images - 镜像列表
- /api/docker/system - Docker 系统信息
- /api/docker/container/<id>/start|stop|restart - 容器控制
- /api/docker/container/<id>/logs - 容器日志
- /api/docker/prune - 资源清理

前端:
- 新增 Docker 监控 Tab
- 容器卡片展示(状态、资源使用)
- 镜像列表展示
- 操作按钮(刷新、清理)
2026-06-01 11:30:15 +08:00
24 changed files with 1614 additions and 3804 deletions

249
README.md
View File

@@ -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.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

3739
app.py

File diff suppressed because it is too large Load Diff

38
config.py Normal file
View 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
View 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()

View File

@@ -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

View File

@@ -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
View 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
View 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
View 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
View 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
View 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
View 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>'''

332
utils.py Normal file
View 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)