first commit

This commit is contained in:
Your Name
2025-09-10 18:12:23 +08:00
commit 6d40be89b6
19 changed files with 3226 additions and 0 deletions

131
README.md Normal file
View File

@@ -0,0 +1,131 @@
# NAS监控系统
一个实时显示系统信息的Web监控面板支持CPU、内存、硬盘、网络等信息的实时监控。
## 功能特性
- 🖥️ **实时系统监控**: CPU使用率、温度、内存使用情况
- 💾 **存储监控**: 硬盘使用率、容量信息、多硬盘支持
- 🌐 **网络监控**: 实时网络速度、IP地址管理
- 🎨 **主题切换**: 支持明暗主题切换
-**高频更新**: 0.5秒更新频率,实时响应
- 📱 **响应式设计**: 适配不同屏幕尺寸
## 系统要求
- Python 3.7+
- Linux/Windows系统
- 依赖包:`psutil`, `fastapi`, `uvicorn`
## 快速开始
### 方法一使用Python启动脚本推荐
```bash
# 安装依赖
pip install psutil fastapi uvicorn
# 启动服务器
python start_server.py
```
### 方法二使用Linux Shell脚本
```bash
# 给脚本执行权限
chmod +x start_server.sh
# 启动服务器
./start_server.sh
```
### 方法三:手动启动
```bash
# 终端1启动API服务器
python system_info.py --serve --port 8001
# 终端2启动HTTP服务器
python -m http.server 8000
```
## 访问地址
启动成功后,在浏览器中访问:
- **前端页面**: http://localhost:8000
- **API接口**: http://localhost:8001/system-info
## 文件结构
```
web/
├── index.html # 前端页面
├── system_info.py # 后端API服务器
├── start_server.py # Python启动脚本跨平台
├── start_server.sh # Linux Shell启动脚本
├── README.md # 说明文档
└── logs/ # 日志目录(自动创建)
├── api_server.log # API服务器日志
└── http_server.log # HTTP服务器日志
```
## 使用说明
1. **启动服务器**: 使用上述任一方法启动服务器
2. **访问页面**: 打开浏览器访问 http://localhost:8000
3. **实时监控**: 页面每0.5秒自动更新系统信息
4. **主题切换**: 点击左上角按钮切换明暗主题
5. **IP选择**: 右上角可选择不同网络接口的IP地址
6. **停止服务器**: 按 `Ctrl+C` 停止服务器
## 故障排除
### 端口被占用
如果遇到端口被占用的错误,可以:
1. 检查端口使用情况:
```bash
# Linux
netstat -tulpn | grep :8000
netstat -tulpn | grep :8001
# Windows
netstat -ano | findstr :8000
netstat -ano | findstr :8001
```
2. 修改端口(在脚本中修改端口号)
### 依赖安装失败
```bash
# 使用国内镜像源
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple psutil fastapi uvicorn
# 或使用conda
conda install psutil
pip install fastapi uvicorn
```
### 权限问题Linux
```bash
# 给脚本执行权限
chmod +x start_server.sh
# 如果需要监控某些系统信息可能需要sudo权限
sudo python start_server.py
```
## 开发说明
- **前端**: 纯HTML/CSS/JavaScript无需构建工具
- **后端**: Python FastAPI提供RESTful API
- **数据获取**: 使用psutil库获取系统信息
- **更新频率**: 可在index.html中修改定时器间隔
## 许可证
本项目采用MIT许可证。

Binary file not shown.

Binary file not shown.

Binary file not shown.

4
config.json Normal file
View File

@@ -0,0 +1,4 @@
{
"frontend_port": 8003,
"backend_port": 8002
}

BIN
image/1.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 647 KiB

BIN
image/2.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 MiB

BIN
image/4.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 584 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

942
index.html Normal file
View File

@@ -0,0 +1,942 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>系统监控仪表板</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--primary-color: #ff69b4;
--secondary-color: #ffb6c1;
--background-color: #ffe4e1;
--text-color: #333;
--border-color: #000;
--progress-bg: #f0f0f0;
}
[data-theme="blue"] {
--primary-color: #4169e1;
--secondary-color: #87ceeb;
--background-color: #e6f3ff;
--text-color: #333;
--border-color: #000;
--progress-bg: #f0f0f0;
}
body {
background-color: var(--background-color);
font-family: 'Microsoft YaHei', Arial, sans-serif;
height: 100vh;
overflow: hidden;
transition: background-color 0.3s ease;
}
/* 主网格容器 */
.grid-container {
display: grid;
grid-template-columns: repeat(8, 1fr);
grid-template-rows: repeat(7, 1fr);
gap: 4px;
height: 100vh;
width: 100vw;
padding: 10px 30px;
}
/* 组件基础样式 */
.component {
border: 2px solid var(--border-color);
background-color: white;
border-radius: 8px;
padding: 15px;
display: flex;
flex-direction: column;
font-size: 14px;
font-weight: bold;
transition: all 0.3s ease;
}
/* 顶部组件样式 */
.component-top {
grid-row: 1 / 2;
grid-column: 1 / 9;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.theme-toggle {
background: var(--primary-color);
color: white;
border: none;
padding: 8px 16px;
border-radius: 8px;
cursor: pointer;
font-weight: bold;
transition: all 0.3s ease;
}
.theme-toggle:hover {
opacity: 0.8;
transform: scale(1.05);
}
.ip-selector {
position: relative;
}
.ip-display {
background: var(--secondary-color);
padding: 8px 16px;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
}
.ip-dropdown {
position: absolute;
top: 100%;
right: 0;
background: white;
border: 2px solid var(--border-color);
border-radius: 8px;
min-width: 200px;
display: none;
z-index: 1000;
}
.ip-option {
padding: 10px;
cursor: pointer;
border-bottom: 1px solid #eee;
}
.ip-option:hover {
background: var(--background-color);
}
/* 组件1: 时钟和网络 */
.component-1 {
grid-row: 2 / 6;
grid-column: 1 / 3;
justify-content: center;
align-items: center;
text-align: center;
}
.clock {
font-size: 3.75em;
color: var(--primary-color);
margin-bottom: 20px;
}
.network-info {
font-size: 1.35em;
color: var(--text-color);
}
.network-speed {
margin: 5px 0;
}
/* 组件2: CPU */
.component-2 {
grid-row: 2 / 4;
grid-column: 3 / 7;
}
/* 组件3: 内存 */
.component-3 {
grid-row: 4 / 6;
grid-column: 3 / 7;
}
/* 进度条样式 */
.progress-container {
position: relative;
margin: 10px 0;
}
.progress-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.progress-bar {
width: 100%;
height: 20px;
background: var(--progress-bg);
border-radius: 10px;
overflow: hidden;
position: relative;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, var(--primary-color), var(--secondary-color));
border-radius: 10px;
transition: width 0.5s ease;
}
.progress-text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: black;
font-size: 12px;
font-weight: bold;
}
/* 组件4: 硬盘总容量圆形进度条 */
.component-4 {
grid-row: 2 / 6;
grid-column: 7 / 9;
justify-content: center;
align-items: center;
}
.circular-progress {
position: relative;
width: 120px;
height: 120px;
margin: 0 auto;
}
.circular-progress svg {
width: 100%;
height: 100%;
transform: rotate(-90deg);
}
.circular-progress circle {
fill: none;
stroke-width: 8;
}
.progress-bg-circle {
stroke: var(--progress-bg);
}
.progress-circle {
stroke: var(--primary-color);
stroke-linecap: round;
transition: stroke-dasharray 0.5s ease;
}
.circular-text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
font-size: 21px;
color: var(--text-color);
}
/* 组件5-8: 按钮 */
.component-5, .component-6, .component-7, .component-8 {
justify-content: center;
align-items: center;
cursor: pointer;
transition: all 0.3s ease;
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
color: white;
}
.component-5 {
grid-row: 6 / 7;
grid-column: 1 / 2;
}
.component-6 {
grid-row: 6 / 7;
grid-column: 2 / 3;
}
.component-7 {
grid-row: 7 / 8;
grid-column: 1 / 2;
}
.component-8 {
grid-row: 7 / 8;
grid-column: 2 / 3;
}
.component-5:hover, .component-6:hover, .component-7:hover, .component-8:hover {
transform: scale(1.05);
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
}
/* 组件9: GIF展示区域 */
.component-9 {
grid-row: 6 / 8;
grid-column: 3 / 7;
justify-content: center;
align-items: center;
background: #f8f8f8;
border-style: dashed;
cursor: pointer;
gap: 10px;
}
.gif-container {
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
grid-template-rows: 1fr;
gap: 5px;
width: 100%;
height: 100%;
}
.gif-item {
height: 100%;
border-radius: 8px;
overflow: hidden;
cursor: pointer;
transition: transform 0.2s ease;
}
.gif-item:hover {
transform: scale(1.05);
}
.gif-item img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 8px;
}
/* 组件10: 硬盘列表 */
.component-10 {
grid-row: 6 / 8;
grid-column: 7 / 9;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
}
.disk-summary {
padding: 10px;
background: var(--background-color);
border-radius: 6px;
}
.disk-expanded {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
border: 3px solid var(--border-color);
border-radius: 12px;
padding: 20px;
max-height: 70vh;
overflow-y: auto;
z-index: 2000;
min-width: 400px;
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
}
.disk-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
z-index: 1999;
}
.disk-item {
margin-bottom: 15px;
padding: 10px;
background: var(--background-color);
border-radius: 6px;
}
.disk-name {
font-size: 12px;
margin-bottom: 5px;
color: var(--text-color);
}
.disk-progress {
height: 15px;
background: var(--progress-bg);
border-radius: 8px;
overflow: hidden;
position: relative;
}
.disk-progress-fill {
height: 100%;
background: linear-gradient(90deg, var(--primary-color), var(--secondary-color));
transition: width 0.5s ease;
}
.disk-info {
font-size: 10px;
margin-top: 3px;
color: black;
}
.disk-expanded-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
padding-bottom: 10px;
}
.disk-expanded-header h3 {
margin: 0;
color: var(--text-color);
}
.close-btn {
background: none;
border: none;
font-size: 24px;
color: var(--text-color);
cursor: pointer;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: background 0.3s ease;
}
.close-btn:hover {
background: rgba(0, 0, 0, 0.1);
}
.disk-detail-list {
display: flex;
flex-direction: column;
gap: 15px;
}
.disk-detail-list .disk-item {
background: var(--background-color);
border-radius: 10px;
padding: 15px;
}
.fullscreen-btn {
background: var(--secondary-color);
color: var(--text-color);
border: none;
padding: 8px 16px;
border-radius: 8px;
cursor: pointer;
font-weight: bold;
transition: all 0.3s ease;
margin-left: 10px;
}
.fullscreen-btn:hover {
opacity: 0.8;
transform: scale(1.05);
}
</style>
</head>
<body>
<div class="grid-container">
<!-- 顶部组件: 主题切换、全屏和IP选择 -->
<div class="component component-top">
<div>
<button class="theme-toggle" onclick="toggleTheme()">🎨 主题切换</button>
<button class="fullscreen-btn" onclick="toggleFullscreen()">🔳 全屏</button>
</div>
<div class="ip-selector">
<div class="ip-display" onclick="toggleIpDropdown()" id="currentIp">192.168.101.102</div>
<div class="ip-dropdown" id="ipDropdown">
<!-- IP选项将通过JavaScript动态生成 -->
</div>
</div>
</div>
<!-- 组件1: 时钟和网络 -->
<div class="component component-1">
<div class="clock" id="clock">20:36</div>
<div class="network-info">
<div class="network-speed"><span id="uploadSpeed">0.354</span> MB/s</div>
<div class="network-speed"><span id="downloadSpeed">0.111</span> MB/s</div>
</div>
</div>
<!-- 组件2: CPU -->
<div class="component component-2">
<div class="progress-header">
<span>🖥️ CPU</span>
<span id="cpuTemp">--°C</span>
</div>
<div class="progress-container">
<div class="progress-bar">
<div class="progress-fill" id="cpuProgress" style="width: 11.9%"></div>
<div class="progress-text" id="cpuText">11.9% 使用中</div>
</div>
</div>
</div>
<!-- 组件3: 内存 -->
<div class="component component-3">
<div class="progress-header">
<span>📊 RAM</span>
<span id="memoryInfo">19.53GB / 63.93GB</span>
</div>
<div class="progress-container">
<div class="progress-bar">
<div class="progress-fill" id="memoryProgress" style="width: 30.5%"></div>
<div class="progress-text" id="memoryText">30.5% 使用中</div>
</div>
</div>
</div>
<!-- 组件4: 硬盘总容量 -->
<div class="component component-4">
<h3 style="text-align: center; margin-bottom: 15px;">💾 总容量</h3>
<div class="circular-progress">
<svg>
<circle class="progress-bg-circle" cx="60" cy="60" r="52"></circle>
<circle class="progress-circle" cx="60" cy="60" r="52" id="totalDiskCircle"></circle>
</svg>
<div class="circular-text">
<div id="totalDiskPercent">77%</div>
<div style="font-size: 10px; margin-top: 5px;" id="totalDiskInfo">1469GB / 1908GB</div>
</div>
</div>
</div>
<!-- 组件5-8: 功能按钮 -->
<div class="component component-5">按钮1</div>
<div class="component component-6">按钮2</div>
<div class="component component-7">按钮3</div>
<div class="component component-8">按钮4</div>
<!-- 组件9: GIF展示区域 -->
<div class="component component-9" id="component9">
<div class="gif-container" id="gifContainer">
<div class="gif-item" onclick="switchSingleGif(0)">
<img id="gif1" src="" alt="GIF 1">
</div>
<div class="gif-item" onclick="switchSingleGif(1)">
<img id="gif2" src="" alt="GIF 2">
</div>
<div class="gif-item" onclick="switchSingleGif(2)">
<img id="gif3" src="" alt="GIF 3">
</div>
<div class="gif-item" onclick="switchSingleGif(3)">
<img id="gif4" src="" alt="GIF 4">
</div>
</div>
</div>
<!-- 组件10: 硬盘列表 -->
<div class="component component-10" onclick="showDiskDetails()">
<div id="diskSummary">
<!-- 硬盘摘要将通过JavaScript动态生成 -->
</div>
</div>
<!-- 硬盘详情弹窗 -->
<div id="diskOverlay" class="disk-overlay" style="display: none;" onclick="hideDiskDetails()"></div>
<div id="diskExpanded" class="disk-expanded" style="display: none;">
<div class="disk-expanded-header">
<button onclick="hideDiskDetails()" class="close-btn">×</button>
</div>
<div id="diskDetailList" class="disk-detail-list">
<!-- 硬盘详细列表将通过JavaScript动态生成 -->
</div>
</div>
</div>
<script>
// GIF文件列表动态从服务器获取
let gifFiles = [];
let currentGifs = [null, null, null, null];
// 配置信息
let config = {
frontend_port: 8003,
backend_port: 8002
};
/**
* 从配置文件获取端口配置
*/
async function fetchConfig() {
try {
const response = await fetch('/config.json');
if (response.ok) {
config = await response.json();
console.log('配置加载成功:', config);
} else {
console.warn('配置文件加载失败,使用默认配置');
}
} catch (error) {
console.warn('配置文件读取出错,使用默认配置:', error);
}
}
/**
* 从API获取图片文件列表
*/
async function fetchImageFiles() {
try {
const currentHost = window.location.hostname;
const apiUrl = `http://${currentHost}:${config.backend_port}/image-files`;
const response = await fetch(apiUrl);
if (response.ok) {
const data = await response.json();
gifFiles = data.files || [];
console.log('图片文件列表更新成功:', gifFiles);
return true;
} else {
console.error('获取图片文件列表失败:', response.status);
// 使用默认文件列表作为备用
gifFiles = ['1.gif', '2.gif', '4.gif'];
return false;
}
} catch (error) {
console.error('获取图片文件列表出错:', error);
// 使用默认文件列表作为备用
gifFiles = ['1.gif', '2.gif', '4.gif'];
return false;
}
}
// 系统数据存储变量
let systemData = {};
/**
* 从API获取系统数据
*/
async function fetchSystemData() {
try {
// 动态获取当前页面的主机地址,支持局域网访问
const currentHost = window.location.hostname;
const apiUrl = `http://${currentHost}:${config.backend_port}/system-info`;
const response = await fetch(apiUrl);
if (response.ok) {
const data = await response.json();
systemData = data;
console.log('系统数据更新成功:', data);
return true;
} else {
console.error('API响应错误:', response.status);
return false;
}
} catch (error) {
console.error('获取系统数据失败:', error);
return false;
}
}
/**
* 初始化页面数据显示
*/
async function initializeData() {
// 先获取配置
await fetchConfig();
// 然后获取图片文件列表
await fetchImageFiles();
const success = await fetchSystemData();
if (success) {
updateCPUInfo();
updateMemoryInfo();
updateDiskInfo();
updateDiskSummary();
updateNetworkInfo();
generateIPDropdown();
}
updateClock();
initializeGifs();
}
/**
* 初始化GIF显示
*/
function initializeGifs() {
// 为每个位置随机分配GIF
for (let i = 0; i < 4; i++) {
switchSingleGif(i);
}
}
/**
* 随机切换单个GIF图片
* @param {number} index - GIF位置索引 (0-3)
*/
function switchSingleGif(index) {
// 获取当前图片文件名
const currentGif = currentGifs[index];
// 创建可选择的GIF列表排除当前图片
const availableGifs = gifFiles.filter(gif => gif !== currentGif);
// 如果没有其他可选择的图片,则不进行切换
if (availableGifs.length === 0) {
return;
}
// 从可选择的图片中随机选择一个
const randomIndex = Math.floor(Math.random() * availableGifs.length);
const selectedGif = availableGifs[randomIndex];
// 更新对应位置的GIF
currentGifs[index] = selectedGif;
document.getElementById(`gif${index + 1}`).src = `image/${selectedGif}`;
}
/**
* 更新CPU信息显示
*/
function updateCPUInfo() {
if (!systemData.cpu) return;
const cpuUsage = systemData.cpu.usage_percent;
const cpuTemp = systemData.cpu.temperature_c;
document.getElementById('cpuProgress').style.width = cpuUsage + '%';
document.getElementById('cpuText').textContent = cpuUsage + '% 使用中';
document.getElementById('cpuTemp').textContent = cpuTemp ? cpuTemp + '°C' : '--°C';
}
/**
* 更新内存信息显示
*/
function updateMemoryInfo() {
if (!systemData.memory) return;
const used = systemData.memory.used_gb;
const total = systemData.memory.total_gb;
const percentage = ((used / total) * 100).toFixed(1);
document.getElementById('memoryProgress').style.width = percentage + '%';
document.getElementById('memoryText').textContent = percentage + '% 使用中';
document.getElementById('memoryInfo').textContent = used + 'GB / ' + total + 'GB';
}
/**
* 更新硬盘信息显示
*/
function updateDiskInfo() {
if (!systemData.storage || !systemData.storage.disks) return;
// 更新总容量圆形进度条
const totalUsed = systemData.storage.disks.reduce((sum, disk) => sum + disk.used_gb, 0);
const totalCapacity = systemData.storage.disks.reduce((sum, disk) => sum + disk.total_gb, 0);
const totalPercentage = ((totalUsed / totalCapacity) * 100).toFixed(0);
const circumference = 2 * Math.PI * 52;
const offset = circumference - (totalPercentage / 100) * circumference;
const circle = document.getElementById('totalDiskCircle');
circle.style.strokeDasharray = circumference;
circle.style.strokeDashoffset = offset;
document.getElementById('totalDiskPercent').textContent = totalPercentage + '%';
document.getElementById('totalDiskInfo').textContent = totalUsed.toFixed(0) + 'GB / ' + totalCapacity.toFixed(0) + 'GB';
}
/**
* 更新硬盘摘要显示(仅显示第一块硬盘)
*/
function updateDiskSummary() {
if (!systemData.storage || !systemData.storage.disks) return;
const diskSummary = document.getElementById('diskSummary');
if (systemData.storage.disks.length > 0) {
const firstDisk = systemData.storage.disks[0];
const percentage = ((firstDisk.used_gb / firstDisk.total_gb) * 100).toFixed(1);
diskSummary.innerHTML = `
<div class="disk-summary">
<div class="disk-name">${firstDisk.name}</div>
<div class="disk-progress">
<div class="disk-progress-fill" style="width: ${percentage}%"></div>
</div>
<div class="disk-info">${firstDisk.used_gb.toFixed(1)}GB / ${firstDisk.total_gb.toFixed(1)}GB (${percentage}%)</div>
<div style="font-size: 10px; color: #666; margin-top: 5px;">点击查看全部硬盘</div>
</div>
`;
}
}
/**
* 显示硬盘详情弹窗
*/
function showDiskDetails() {
const diskDetailList = document.getElementById('diskDetailList');
diskDetailList.innerHTML = '';
systemData.storage.disks.forEach((disk, index) => {
const percentage = ((disk.used_gb / disk.total_gb) * 100).toFixed(1);
const diskItem = document.createElement('div');
diskItem.className = 'disk-item';
diskItem.innerHTML = `
<div class="disk-name">${disk.name} (硬盘 ${index + 1})</div>
<div class="disk-progress">
<div class="disk-progress-fill" style="width: ${percentage}%"></div>
</div>
<div class="disk-info">${disk.used_gb.toFixed(1)}GB / ${disk.total_gb.toFixed(1)}GB (${percentage}%)</div>
`;
diskDetailList.appendChild(diskItem);
});
document.getElementById('diskOverlay').style.display = 'block';
document.getElementById('diskExpanded').style.display = 'block';
}
/**
* 隐藏硬盘详情弹窗
*/
function hideDiskDetails() {
document.getElementById('diskOverlay').style.display = 'none';
document.getElementById('diskExpanded').style.display = 'none';
}
/**
* 更新网络信息显示
*/
function updateNetworkInfo() {
if (!systemData.network) return;
document.getElementById('uploadSpeed').textContent = systemData.network.up_mbps.toFixed(3);
document.getElementById('downloadSpeed').textContent = systemData.network.down_mbps.toFixed(3);
}
/**
* 生成IP地址下拉菜单
*/
function generateIPDropdown() {
if (!systemData.network || !systemData.network.interfaces) return;
const dropdown = document.getElementById('ipDropdown');
dropdown.innerHTML = '';
systemData.network.interfaces.forEach(interface => {
const option = document.createElement('div');
option.className = 'ip-option';
option.innerHTML = `<strong>${interface.name}</strong><br>${interface.ip}`;
option.onclick = () => selectIP(interface.ip);
dropdown.appendChild(option);
});
}
/**
* 选择IP地址
*/
function selectIP(ip) {
document.getElementById('currentIp').textContent = ip;
document.getElementById('ipDropdown').style.display = 'none';
}
/**
* 切换IP下拉菜单显示状态
*/
function toggleIpDropdown() {
const dropdown = document.getElementById('ipDropdown');
dropdown.style.display = dropdown.style.display === 'block' ? 'none' : 'block';
}
/**
* 主题切换功能
*/
function toggleTheme() {
const body = document.body;
if (body.getAttribute('data-theme') === 'blue') {
body.removeAttribute('data-theme');
localStorage.setItem('theme', 'pink');
} else {
body.setAttribute('data-theme', 'blue');
localStorage.setItem('theme', 'blue');
}
}
/**
* 更新时钟显示
*/
function updateClock() {
const now = new Date();
const hours = now.getHours().toString().padStart(2, '0');
const minutes = now.getMinutes().toString().padStart(2, '0');
document.getElementById('clock').textContent = hours + ':' + minutes;
}
/**
* 加载保存的主题
*/
function loadTheme() {
const savedTheme = localStorage.getItem('theme');
if (savedTheme === 'blue') {
document.body.setAttribute('data-theme', 'blue');
}
}
/**
* 全屏切换功能
*/
function toggleFullscreen() {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen().catch(err => {
console.log('无法进入全屏模式:', err);
});
} else {
document.exitFullscreen();
}
}
// 点击其他地方关闭IP下拉菜单
document.addEventListener('click', function(event) {
const ipSelector = document.querySelector('.ip-selector');
if (!ipSelector.contains(event.target)) {
document.getElementById('ipDropdown').style.display = 'none';
}
});
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', async function() {
loadTheme();
await initializeData();
// 每秒更新时钟
setInterval(updateClock, 1000);
// 每0.5秒从API获取最新数据
setInterval(async function() {
const success = await fetchSystemData();
if (success) {
updateCPUInfo();
updateMemoryInfo();
updateDiskInfo();
updateDiskSummary();
updateNetworkInfo();
generateIPDropdown();
}
}, 500);
});
</script>
</body>
</html>

1019
logs/api_server.log Normal file

File diff suppressed because it is too large Load Diff

2
logs/http_server.log Normal file
View File

@@ -0,0 +1,2 @@
'"' <20><><EFBFBD><EFBFBD><EFBFBD>ڲ<EFBFBD><DAB2><EFBFBD><EFBFBD>ⲿ<EFBFBD><E2B2BF><EFBFBD>Ҳ<EEA3AC><D2B2><EFBFBD>ǿ<EFBFBD><C7BF><EFBFBD><EFBFBD>еij<D0B5><C4B3><EFBFBD>
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ļ<EFBFBD><EFBFBD><EFBFBD>

13
requirements.txt Normal file
View File

@@ -0,0 +1,13 @@
# NAS监控系统依赖包
# 使用方法: pip install -r requirements.txt
# 系统信息获取
psutil>=5.8.0
# Web框架和API服务
fastapi>=0.68.0
uvicorn[standard]>=0.15.0
# 可选依赖(用于更好的性能)
# python-multipart>=0.0.5 # 用于处理表单数据
# aiofiles>=0.7.0 # 异步文件操作

262
start_server_venv.py Normal file
View File

@@ -0,0 +1,262 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
NAS监控系统启动脚本 - 虚拟环境版本
功能在虚拟环境中启动Python API后端服务器和HTML静态文件服务器
适用于Linux/Windows系统
使用方法:
python3 start_server_venv.py
python start_server_venv.py
"""
import os
import sys
import time
import signal
import subprocess
import threading
import venv
from pathlib import Path
class VenvServerManager:
"""虚拟环境服务器管理类"""
def __init__(self):
self.api_process = None
self.http_process = None
self.script_dir = Path(__file__).parent.absolute()
self.logs_dir = self.script_dir / "logs"
self.venv_dir = self.script_dir / "venv"
self.venv_python = None
def setup_logging(self):
"""创建日志目录"""
self.logs_dir.mkdir(exist_ok=True)
print(f"日志目录: {self.logs_dir}")
def create_virtual_environment(self):
"""创建虚拟环境"""
if self.venv_dir.exists():
print(f"虚拟环境已存在: {self.venv_dir}")
else:
print(f"正在创建虚拟环境: {self.venv_dir}")
try:
venv.create(self.venv_dir, with_pip=True)
print("虚拟环境创建成功")
except Exception as e:
print(f"错误: 创建虚拟环境失败 - {e}")
sys.exit(1)
# 设置虚拟环境Python路径
if os.name == 'nt': # Windows
self.venv_python = self.venv_dir / "Scripts" / "python.exe"
else: # Linux/macOS
self.venv_python = self.venv_dir / "bin" / "python"
if not self.venv_python.exists():
print(f"错误: 虚拟环境Python不存在: {self.venv_python}")
sys.exit(1)
print(f"虚拟环境Python: {self.venv_python}")
def install_dependencies(self):
"""在虚拟环境中安装依赖包"""
print("检查并安装Python依赖包...")
# 检查依赖是否已安装
try:
result = subprocess.run(
[str(self.venv_python), "-c", "import psutil, fastapi, uvicorn"],
capture_output=True,
text=True
)
if result.returncode == 0:
print("依赖包已安装")
return
except Exception:
pass
# 安装依赖包
print("正在安装依赖包...")
try:
# 获取pip路径
if os.name == 'nt': # Windows
pip_path = self.venv_dir / "Scripts" / "pip.exe"
else: # Linux/macOS
pip_path = self.venv_dir / "bin" / "pip"
subprocess.run(
[str(pip_path), "install", "psutil", "fastapi", "uvicorn"],
check=True,
cwd=self.script_dir
)
print("依赖包安装成功")
except subprocess.CalledProcessError as e:
print(f"错误: 依赖包安装失败 - {e}")
sys.exit(1)
def check_dependencies(self):
"""检查依赖文件"""
print("检查依赖文件...")
# 检查必要文件
required_files = ["system_info.py", "index.html"]
for file in required_files:
if not (self.script_dir / file).exists():
print(f"错误: 未找到{file}文件")
sys.exit(1)
def start_api_server(self):
"""启动API服务器"""
print("启动Python API服务器 (端口8001)...")
api_log = self.logs_dir / "api_server.log"
with open(api_log, "w", encoding="utf-8") as log_file:
self.api_process = subprocess.Popen(
[str(self.venv_python), "system_info.py", "--serve", "--port", "8001"],
cwd=self.script_dir,
stdout=log_file,
stderr=subprocess.STDOUT,
text=True
)
print(f"API服务器已启动 (PID: {self.api_process.pid})")
return self.api_process
def start_http_server(self):
"""启动HTTP静态文件服务器"""
print("启动HTTP静态文件服务器 (端口8000)...")
http_log = self.logs_dir / "http_server.log"
with open(http_log, "w", encoding="utf-8") as log_file:
self.http_process = subprocess.Popen(
[str(self.venv_python), "-m", "http.server", "8000"],
cwd=self.script_dir,
stdout=log_file,
stderr=subprocess.STDOUT,
text=True
)
print(f"HTTP服务器已启动 (PID: {self.http_process.pid})")
return self.http_process
def check_server_health(self):
"""检查服务器健康状态"""
time.sleep(3) # 等待服务器启动
# 检查API服务器
if self.api_process.poll() is not None:
print("错误: API服务器启动失败请检查logs/api_server.log")
return False
# 检查HTTP服务器
if self.http_process.poll() is not None:
print("错误: HTTP服务器启动失败请检查logs/http_server.log")
return False
return True
def monitor_servers(self):
"""监控服务器状态"""
while True:
time.sleep(5)
# 检查API服务器
if self.api_process.poll() is not None:
print("警告: API服务器进程已停止")
break
# 检查HTTP服务器
if self.http_process.poll() is not None:
print("警告: HTTP服务器进程已停止")
break
def cleanup(self):
"""清理资源"""
print("\n正在停止服务器...")
if self.api_process:
try:
self.api_process.terminate()
self.api_process.wait(timeout=5)
print(f"已停止API服务器 (PID: {self.api_process.pid})")
except subprocess.TimeoutExpired:
self.api_process.kill()
print(f"强制停止API服务器 (PID: {self.api_process.pid})")
except Exception as e:
print(f"停止API服务器时出错: {e}")
if self.http_process:
try:
self.http_process.terminate()
self.http_process.wait(timeout=5)
print(f"已停止HTTP服务器 (PID: {self.http_process.pid})")
except subprocess.TimeoutExpired:
self.http_process.kill()
print(f"强制停止HTTP服务器 (PID: {self.http_process.pid})")
except Exception as e:
print(f"停止HTTP服务器时出错: {e}")
print("服务器已停止")
def signal_handler(self, signum, frame):
"""信号处理函数"""
print(f"\n收到信号 {signum},正在关闭服务器...")
self.cleanup()
sys.exit(0)
def run(self):
"""运行服务器管理器"""
print("=== NAS监控系统启动脚本虚拟环境版本 ===")
print(f"工作目录: {self.script_dir}")
# 设置信号处理
signal.signal(signal.SIGINT, self.signal_handler)
if hasattr(signal, 'SIGTERM'):
signal.signal(signal.SIGTERM, self.signal_handler)
try:
# 初始化
self.setup_logging()
self.create_virtual_environment()
self.install_dependencies()
self.check_dependencies()
# 启动服务器
self.start_api_server()
self.start_http_server()
# 检查服务器健康状态
if not self.check_server_health():
self.cleanup()
sys.exit(1)
print("\n=== 服务器启动成功 ===")
print(f"虚拟环境: {self.venv_dir}")
print("前端页面: http://localhost:8000")
print("API接口: http://localhost:8001/system-info")
print("日志文件: logs/api_server.log, logs/http_server.log")
print("\n按 Ctrl+C 停止服务器")
# 监控服务器
self.monitor_servers()
except KeyboardInterrupt:
print("\n用户中断")
except Exception as e:
print(f"\n运行时错误: {e}")
finally:
self.cleanup()
def main():
"""主函数"""
manager = VenvServerManager()
manager.run()
if __name__ == "__main__":
main()

225
start_server_venv.sh Normal file
View File

@@ -0,0 +1,225 @@
#!/bin/bash
# NAS监控系统启动脚本 - 虚拟环境版本
# 功能在虚拟环境中启动Python API后端服务器和HTML静态文件服务器
# 设置脚本目录为工作目录
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
cd "$SCRIPT_DIR"
echo "=== NAS监控系统启动脚本虚拟环境版本 ==="
echo "工作目录: $SCRIPT_DIR"
# 读取配置文件
CONFIG_FILE="$SCRIPT_DIR/config.json"
if [ ! -f "$CONFIG_FILE" ]; then
echo "错误: 配置文件config.json不存在"
exit 1
fi
# 从JSON配置文件读取端口
FRONTEND_PORT=$(python3 -c "import json; print(json.load(open('$CONFIG_FILE'))['frontend_port'])" 2>/dev/null)
BACKEND_PORT=$(python3 -c "import json; print(json.load(open('$CONFIG_FILE'))['backend_port'])" 2>/dev/null)
if [ -z "$FRONTEND_PORT" ] || [ -z "$BACKEND_PORT" ]; then
echo "错误: 无法从config.json读取端口配置"
exit 1
fi
echo "配置端口 - 前端: $FRONTEND_PORT, 后端: $BACKEND_PORT"
# 虚拟环境目录
VENV_DIR="$SCRIPT_DIR/venv"
# 检查虚拟环境是否存在
if [ ! -d "$VENV_DIR" ]; then
echo "虚拟环境不存在,正在创建..."
# 检查Python是否安装
if ! which python3 >/dev/null 2>&1 && ! python3 --version >/dev/null 2>&1; then
echo "错误: 未找到python3请先安装Python 3"
echo "请尝试: sudo apt update && sudo apt install python3 python3-venv python3-pip"
exit 1
fi
echo "找到Python: $(python3 --version)"
# 创建虚拟环境
python3 -m venv "$VENV_DIR" || {
echo "错误: 创建虚拟环境失败"
exit 1
}
echo "虚拟环境创建成功: $VENV_DIR"
fi
# 激活虚拟环境
echo "激活虚拟环境..."
. "$VENV_DIR/bin/activate" || {
echo "错误: 激活虚拟环境失败"
exit 1
}
echo "当前Python路径: $(which python)"
echo "当前Python版本: $(python --version)"
# 检查必要文件是否存在
if [ ! -f "system_info.py" ]; then
echo "错误: 未找到system_info.py文件"
exit 1
fi
if [ ! -f "index.html" ]; then
echo "错误: 未找到index.html文件"
exit 1
fi
# 检查并安装Python依赖
echo "检查Python依赖..."
if ! python -c "import psutil, fastapi, uvicorn" 2>/dev/null; then
echo "安装Python依赖包..."
pip install psutil fastapi uvicorn || {
echo "错误: 依赖安装失败,请检查网络连接"
exit 1
}
else
echo "依赖包已安装"
fi
# 创建日志目录
mkdir -p logs
# 定义清理函数
cleanup() {
echo "\n正在停止服务器..."
# 停止API服务器
if [ ! -z "$API_PID" ]; then
kill $API_PID 2>/dev/null
sleep 1
# 如果进程仍在运行,强制终止
if kill -0 $API_PID 2>/dev/null; then
kill -9 $API_PID 2>/dev/null
fi
echo "已停止API服务器 (PID: $API_PID)"
fi
# 停止HTTP服务器
if [ ! -z "$HTTP_PID" ]; then
kill $HTTP_PID 2>/dev/null
sleep 1
# 如果进程仍在运行,强制终止
if kill -0 $HTTP_PID 2>/dev/null; then
kill -9 $HTTP_PID 2>/dev/null
fi
echo "已停止HTTP服务器 (PID: $HTTP_PID)"
fi
# 额外检查杀死可能残留的Python HTTP服务器进程
pkill -f "python.*http.server.*$FRONTEND_PORT" 2>/dev/null || true
echo "退出虚拟环境"
if command -v deactivate >/dev/null 2>&1; then
deactivate 2>/dev/null
fi
echo "服务器已停止"
exit 0
}
# 设置信号处理
trap cleanup INT TERM
# 检查端口是否被占用的函数
check_port() {
local port=$1
if netstat -tuln 2>/dev/null | grep -q ":$port " || ss -tuln 2>/dev/null | grep -q ":$port "; then
return 0 # 端口被占用
else
return 1 # 端口可用
fi
}
# 清理可能残留的进程
echo "清理可能残留的进程..."
pkill -f "python.*system_info.py.*--serve" 2>/dev/null || true
pkill -f "python.*http.server.*$FRONTEND_PORT" 2>/dev/null || true
sleep 1
echo "启动服务器..."
# 检查API端口
if check_port $BACKEND_PORT; then
echo "警告: 端口$BACKEND_PORT被占用,尝试清理..."
pkill -f "python.*system_info.py.*--serve" 2>/dev/null || true
sleep 2
if check_port $BACKEND_PORT; then
echo "错误: 端口$BACKEND_PORT仍被占用,请手动检查"
exit 1
fi
fi
# 启动Python API后端服务器
echo "启动Python API服务器 (端口$BACKEND_PORT)..."
python system_info.py --serve --port $BACKEND_PORT > logs/api_server.log 2>&1 &
API_PID=$!
echo "API服务器已启动 (PID: $API_PID)"
# 等待API服务器启动
sleep 3
# 检查API服务器是否正常启动
if ! kill -0 $API_PID 2>/dev/null; then
echo "错误: API服务器启动失败请检查logs/api_server.log"
exit 1
fi
# 检查HTTP端口
if check_port $FRONTEND_PORT; then
echo "警告: 端口$FRONTEND_PORT被占用,尝试清理..."
pkill -f "python.*http.server.*$FRONTEND_PORT" 2>/dev/null || true
sleep 2
if check_port $FRONTEND_PORT; then
echo "错误: 端口$FRONTEND_PORT仍被占用,请手动检查"
cleanup
exit 1
fi
fi
# 启动HTTP静态文件服务器
echo "启动HTTP静态文件服务器 (端口$FRONTEND_PORT)..."
python -m http.server $FRONTEND_PORT > logs/http_server.log 2>&1 &
HTTP_PID=$!
echo "HTTP服务器已启动 (PID: $HTTP_PID)"
# 等待HTTP服务器启动
sleep 3
# 检查HTTP服务器是否正常启动
if ! kill -0 $HTTP_PID 2>/dev/null; then
echo "错误: HTTP服务器启动失败请检查logs/http_server.log"
cleanup
exit 1
fi
echo "\n=== 服务器启动成功 ==="
echo "虚拟环境: $VENV_DIR"
echo "前端页面: http://localhost:$FRONTEND_PORT"
echo "API接口: http://localhost:$BACKEND_PORT/system-info"
echo "图片文件接口: http://localhost:$BACKEND_PORT/image-files"
echo "日志文件: logs/api_server.log, logs/http_server.log"
echo "\n按 Ctrl+C 停止服务器"
# 保持脚本运行,等待用户中断
while true; do
# 检查进程是否还在运行
if ! kill -0 $API_PID 2>/dev/null; then
echo "警告: API服务器进程已停止"
break
fi
if ! kill -0 $HTTP_PID 2>/dev/null; then
echo "警告: HTTP服务器进程已停止"
break
fi
sleep 5
done
cleanup

464
system_info.py Normal file
View File

@@ -0,0 +1,464 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
跨平台系统信息采集脚本
- 自动识别Windows/Linux针对Debian内核6.12.18-trim做兼容
- 采集 CPU、内存、存储、网络等信息不采集风扇、内存温度与硬盘温度
- 尽量使用标准库/psutil
运行:
Windows: .venv\Scripts\python.exe system_info.py
Linux : python3 system_info.py
"""
import argparse
import json
import os
import platform
import re
import time
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple
import psutil
# Windows 平台可选依赖
try:
import wmi # type: ignore
except Exception:
wmi = None # 非Windows或未安装时安全降级
# ======================= 通用工具函数 =======================
def bytes_to_gb(b: float) -> float:
"""将字节转换为GB并保留两位小数。"""
return round(b / (1024 ** 3), 2)
# ======================= CPU 信息 =======================
def get_cpu_usage() -> float:
"""获取CPU当前占用率百分比"""
try:
return float(psutil.cpu_percent(interval=1))
except Exception:
return 0.0
def get_cpu_temperature() -> Optional[float]:
"""获取CPU温度摄氏度。不同平台采用不同策略获取失败返回None。"""
system = platform.system()
# Linux 优先使用 psutil.sensors_temperatures
if system == 'Linux':
try:
temps = psutil.sensors_temperatures(fahrenheit=False) or {}
# 常见键coretemp/k10temp/zenpower
for key in ('coretemp', 'k10temp', 'zenpower', 'cpu-thermal'):
if key in temps and temps[key]:
vals = [s.current for s in temps[key] if getattr(s, 'current', None) is not None]
if vals:
return float(max(vals))
# 退化至 /sys/class/thermal
base = '/sys/class/thermal'
if os.path.isdir(base):
res = []
for name in os.listdir(base):
if not name.startswith('thermal_zone'):
continue
tpath = os.path.join(base, name, 'temp')
if os.path.isfile(tpath):
try:
val = int(open(tpath).read().strip())
# 常见以毫度C表示
if val > 1000:
res.append(val / 1000.0)
else:
res.append(float(val))
except Exception:
pass
if res:
return float(max(res))
except Exception:
pass
return None
# Windows 尝试 WMIMSAcpi_ThermalZoneTemperature注意可能是主板而非核心温度
if system == 'Windows' and wmi is not None:
try:
c = wmi.WMI(namespace='root\\WMI')
sensors = c.MSAcpi_ThermalZoneTemperature()
vals = []
for s in sensors:
# 温度单位为0.1K需要转换摄氏度C = (K/10) - 273.15
if hasattr(s, 'CurrentTemperature'):
celsius = s.CurrentTemperature / 10.0 - 273.15
vals.append(celsius)
if vals:
return float(max(vals))
except Exception:
pass
# 尝试 OpenHardwareMonitor WMI如果用户已运行该工具
try:
c = wmi.WMI(namespace='root\\OpenHardwareMonitor')
for sensor in getattr(c, 'Sensor', lambda: [])():
if getattr(sensor, 'SensorType', '') == 'Temperature' and 'cpu' in sensor.Name.lower():
return float(sensor.Value)
except Exception:
pass
return None
return None
# ======================= 内存信息 =======================
def get_memory_usage() -> Dict[str, float]:
"""获取内存使用信息(已用/总量GB"""
try:
vm = psutil.virtual_memory()
return {
'used_gb': bytes_to_gb(vm.total - vm.available),
'total_gb': bytes_to_gb(vm.total),
}
except Exception:
return {'used_gb': 0.0, 'total_gb': 0.0}
# 已删除 get_memory_temperature不再采集内存温度
# ======================= 存储信息 =======================
def _linux_list_block_devices() -> List[str]:
"""Linux 获取物理块设备列表(如 sda、nvme0n1、vda"""
res = []
base = '/sys/block'
if os.path.isdir(base):
for name in os.listdir(base):
if re.match(r'^(sd[a-z]+|nvme\d+n\d+|vd[a-z]+)$', name):
res.append(name)
return res
def _linux_disk_size_bytes(dev: str) -> Optional[int]:
"""Linux 通过 /sys/block/<dev>/size 读取总字节数。"""
p = f'/sys/block/{dev}/size'
try:
sectors = int(open(p).read().strip())
return sectors * 512
except Exception:
return None
def _linux_disk_partitions_map() -> Dict[str, List[str]]:
"""构建物理盘到分区设备路径的映射,例如 {'sda': ['/dev/sda1', ...]}。"""
mapping: Dict[str, List[str]] = {}
for part in psutil.disk_partitions(all=False):
dev = part.device # 如 /dev/sda1
mountpoint = part.mountpoint # 挂载点
# 处理标准分区格式 /dev/sda1
m = re.match(r'/dev/([a-z]+)\d+$', dev)
if not m:
# 处理 nvme0n1p1
m = re.match(r'/dev/(nvme\d+n\d+)p\d+$', dev)
if not m:
# 处理整个磁盘作为分区的情况 /dev/sda
m = re.match(r'/dev/([a-z]+)$', dev)
if not m:
# 处理 nvme0n1 整盘
m = re.match(r'/dev/(nvme\d+n\d+)$', dev)
if m:
root = m.group(1)
mapping.setdefault(root, []).append(mountpoint) # 使用挂载点而不是设备路径
return mapping
# 已删除 _linux_disk_temperature不再采集硬盘温度
def get_storage_info() -> Dict[str, Any]:
"""获取存储信息:硬盘数量、每个硬盘的容量(已用/总、GB"""
system = platform.system()
result: Dict[str, Any] = {
'disk_count': 0,
'disks': [],
}
if system == 'Linux':
# 直接遍历已挂载的分区,而不是物理硬盘设备
processed_mountpoints = set()
for part in psutil.disk_partitions(all=False):
mountpoint = part.mountpoint
device = part.device
# 跳过已处理的挂载点
if mountpoint in processed_mountpoints:
continue
# 跳过特殊文件系统
if part.fstype in ['tmpfs', 'devtmpfs', 'sysfs', 'proc', 'devpts']:
continue
try:
usage = psutil.disk_usage(mountpoint)
# 为不同挂载点设置合适的显示名称
if mountpoint == '/':
display_name = '系统盘 (/)'
elif mountpoint == '/vol1':
display_name = '/vol1 (应用存储)'
elif mountpoint.startswith('/vol'):
display_name = f'{mountpoint} (存储卷)'
else:
display_name = f'{mountpoint} ({device})'
result['disks'].append({
'name': display_name,
'used_gb': bytes_to_gb(usage.used),
'total_gb': bytes_to_gb(usage.total),
})
processed_mountpoints.add(mountpoint)
except Exception as e:
# print(f"获取 {mountpoint} 使用量失败: {e}")
pass
result['disk_count'] = len(result['disks'])
return result
if system == 'Windows' and wmi is not None:
try:
c = wmi.WMI()
# 通过关联链路汇总每个物理盘上的分区占用
for disk in c.Win32_DiskDrive():
total_b = int(getattr(disk, 'Size', 0) or 0)
used_b = 0
try:
for part in disk.associators("Win32_DiskDriveToDiskPartition"):
for ld in part.associators("Win32_LogicalDiskToPartition"):
mount = ld.DeviceID + "\\" # 如 C:\
try:
u = psutil.disk_usage(mount)
used_b += u.used
except Exception:
pass
except Exception:
pass
result['disks'].append({
'name': f"{disk.Model}",
'used_gb': bytes_to_gb(used_b),
'total_gb': bytes_to_gb(total_b),
})
result['disk_count'] = len(result['disks'])
return result
except Exception:
pass
# 兜底:按挂载点统计(不区分物理盘)
mounts = []
seen = set()
for p in psutil.disk_partitions(all=False):
if p.mountpoint in seen:
continue
seen.add(p.mountpoint)
try:
u = psutil.disk_usage(p.mountpoint)
mounts.append({
'name': p.device,
'used_gb': bytes_to_gb(u.used),
'total_gb': bytes_to_gb(u.total),
})
except Exception:
pass
result['disks'] = mounts
result['disk_count'] = len(mounts)
return result
# ======================= 网络信息 =======================
def get_ip_interfaces() -> List[Tuple[str, str]]:
"""获取所有具有IPv4地址的网卡及其IP。屏蔽以 br- 开头的 bridge 网卡(如 Docker 的 br-f86755ba2795"""
import socket
res: List[Tuple[str, str]] = []
addrs = psutil.net_if_addrs()
AF_LINK = getattr(psutil, 'AF_LINK', None)
for name, arr in addrs.items():
# 屏蔽 bridge 网卡(如 Docker 的 br-xxxxxxxxxxxx
if name.startswith('br-'):
continue
for a in arr:
fam = getattr(a, 'family', None)
if AF_LINK is not None and fam == AF_LINK:
continue
if fam == socket.AF_INET:
res.append((name, a.address))
return res
def get_network_rate(sample_seconds: float = 1.0) -> Dict[str, float]:
"""采样一段时间,估算总上行/下行速率Mbps"""
try:
c1 = psutil.net_io_counters()
time.sleep(sample_seconds)
c2 = psutil.net_io_counters()
up_bps = (c2.bytes_sent - c1.bytes_sent) * 8.0 / sample_seconds
down_bps = (c2.bytes_recv - c1.bytes_recv) * 8.0 / sample_seconds
return {
'up_mbps': round(up_bps / 1_000_000, 3),
'down_mbps': round(down_bps / 1_000_000, 3),
}
except Exception:
return {'up_mbps': 0.0, 'down_mbps': 0.0}
# 已删除风扇信息相关代码get_fans_info
# ======================= 主流程 =======================
def is_debian_kernel_special() -> bool:
"""是否为需要特别兼容的Debian内核版本 6.12.18-trim。"""
try:
return platform.system() == 'Linux' and platform.release().strip() == '6.12.18-trim'
except Exception:
return False
def collect_system_info() -> Dict[str, Any]:
"""汇总采集所有信息,返回结构化字典。"""
info: Dict[str, Any] = {
'os': platform.system(),
'os_release': platform.release(),
'os_version': platform.version(),
'debian_kernel_6_12_18_trim': is_debian_kernel_special(),
}
# CPU
info['cpu'] = {
'usage_percent': get_cpu_usage(),
'temperature_c': get_cpu_temperature(),
}
# Memory
mem = get_memory_usage()
info['memory'] = mem
# Storage
info['storage'] = get_storage_info()
# Network
info['network'] = {
'interfaces': [{'name': n, 'ip': ip} for n, ip in get_ip_interfaces()],
**get_network_rate(1.0),
}
return info
# 新增FastAPI 与 CORS、Uvicorn仅在 --serve 模式下使用)
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
import uvicorn
# ======================= 图片文件扫描 =======================
def get_image_files() -> Dict[str, List[str]]:
"""扫描image目录中的所有图片文件
Returns:
Dict: 包含图片文件列表的字典
"""
try:
# 获取当前脚本所在目录
script_dir = Path(__file__).parent.absolute()
image_dir = script_dir / "image"
# 支持的图片格式
image_extensions = {'.gif', '.jpg', '.jpeg', '.png', '.bmp', '.webp'}
image_files = []
if image_dir.exists() and image_dir.is_dir():
for file_path in image_dir.iterdir():
if file_path.is_file() and file_path.suffix.lower() in image_extensions:
image_files.append(file_path.name)
# 按文件名排序
image_files.sort()
return {
"files": image_files,
"count": len(image_files),
"directory": str(image_dir)
}
except Exception as e:
return {
"files": [],
"count": 0,
"error": str(e),
"directory": ""
}
# ======================= HTTP 服务FastAPI =======================
def create_app() -> FastAPI:
"""创建 FastAPI 应用并注册路由。
- 提供 GET /system-info 端点,返回 collect_system_info() 的结果
- 默认开启 CORS 以便前后端跨域访问(生产可按需收敛域名)
"""
app = FastAPI(title="NAS Monitor Python Backend")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/system-info")
def system_info_endpoint():
return collect_system_info()
@app.get("/image-files")
def image_files_endpoint():
"""获取image目录中的所有图片文件列表"""
return get_image_files()
return app
def serve_api(host: str = "0.0.0.0", port: int = 8000) -> None:
"""启动 Uvicorn 服务,运行 FastAPI 应用。"""
uvicorn.run(create_app(), host=host, port=port, log_level="info")
def main() -> None:
"""主函数采集信息并以JSON形式输出CLI 模式)。"""
data = collect_system_info()
print(json.dumps(data, ensure_ascii=False, indent=2))
if __name__ == '__main__':
try:
# 在 Windows 下确保 stdout 使用 UTF-8避免中文网卡名乱码
import sys
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8")
except Exception:
pass
# 新增:解析命令行参数,支持 --serve 模式
parser = argparse.ArgumentParser(description="NAS Monitor system_info 服务/脚本")
parser.add_argument("--serve", action="store_true", help="以 HTTP API 模式提供 /system-info")
parser.add_argument("--host", default=os.environ.get("PY_HOST", "0.0.0.0"), help="HTTP 监听地址,默认 0.0.0.0")
parser.add_argument("--port", type=int, default=int(os.environ.get("PY_PORT", "8000")), help="HTTP 监听端口,默认 8000")
args = parser.parse_args()
if args.serve:
serve_api(args.host, args.port)
else:
main()

164
项目任务目标.md Normal file
View File

@@ -0,0 +1,164 @@
# NAS监控系统项目任务目标
## 当前问题修复任务
### 高优先级任务
- [x] 创建项目任务目标文件,跟踪修复进度
- [x] 修复start_server.sh中的Bad substitution错误第6行语法问题
- [x] 创建适用于虚拟环境的启动脚本版本
### 中优先级任务
- [ ] 测试修复后的脚本在Linux环境下的运行
- [x] 优化Python依赖管理支持虚拟环境
### 低优先级任务
- [ ] 添加更详细的错误处理和日志记录
- [ ] 创建Windows批处理脚本版本
## 问题分析
### 当前遇到的问题:
1. **Bad substitution错误**shell脚本第6行存在语法问题
2. **Python环境问题**:需要支持虚拟环境运行
3. **跨平台兼容性**需要同时支持Linux和Windows
### 解决方案:
1. 修复shell脚本语法错误
2. 创建支持虚拟环境的启动脚本
3. 提供多种启动方式的选择
## 项目结构
```
nasweb/
├── README.md
├── index.html # 前端页面
├── system_info.py # 系统信息API服务
├── start_server.py # Python启动脚本原版
├── start_server.sh # Linux shell启动脚本已修复
├── start_server_venv.py # Python启动脚本虚拟环境版本
├── start_server_venv.sh # Linux shell启动脚本虚拟环境版本
├── requirements.txt # Python依赖包列表
├── 项目任务目标.md # 本文件
├── image/ # 图片资源
├── logs/ # 日志文件
└── venv/ # 虚拟环境目录(运行时创建)
```
## 更新日志
- 2024-01-XX: 创建任务目标文件
- 2024-01-XX: 修复start_server.sh中的Bad substitution错误
- 2024-01-XX: 创建虚拟环境版本的启动脚本
- 2024-01-XX: 添加requirements.txt依赖管理文件
- 2024-01-XX: 修复Python检测逻辑解决Linux环境下python3命令检测问题
## 最新修复 (2025-01-09)
### 修复的问题:
1. ✅ Python检测问题系统中python3存在但`command -v python3`检测失败
2. ✅ Shell兼容性问题Linux环境下用sh执行脚本时source命令不可用
3. ✅ 硬盘使用量检测问题Linux环境下硬盘使用量显示为0
4. ✅ 进程终止问题Ctrl+C退出后HTTP端口未正确关闭
5. ✅ 端口配置问题需要修改API和HTTP服务端口
6. ✅ 硬盘检测范围问题:需要检测/vol1应用存储目录
7. ✅ 硬盘容量显示错误:显示物理硬盘总容量而非分区容量
8. ✅ 信号处理语法错误修复trap命令在某些shell环境下的语法问题
9. ✅ 端口占用问题:添加端口检查和自动清理机制
10. ✅ 系统服务部署创建systemd service配置实现开机自启和后台运行
11. ✅ systemd配置错误修复修复ExecStart重复字段和sudo使用问题
12. ✅ 服务用户配置优化改为使用root用户运行避免用户组权限问题
13. ✅ GIF文件显示修复更新文件列表匹配实际存在的GIF文件解决图片显示问题
14. ✅ 动态图片扫描实现启动时自动扫描image目录动态获取图片文件列表
15. ✅ 端口配置文件实现从config.json读取端口配置支持动态端口设置
### 改进内容:
#### 1. Python检测修复
- 改进Python检测逻辑使用`which python3``python3 --version`双重检测
- 修复start_server_venv.sh和start_server.sh中的Python检测问题
- 增强用户体验添加Python版本显示和安装建议
#### 2. Shell兼容性修复
-`source`命令替换为`.`命令提高shell兼容性
- 在cleanup函数中添加deactivate命令的存在性检查
#### 3. 硬盘使用量检测修复
- 扩展正则匹配:支持整盘分区格式(如sda)和nvme设备格式(如nvme0n1)
- 改进挂载点映射:将设备路径映射改为挂载点映射
- 添加根分区处理当part_map中没有对应挂载点时检查根分区
- 增强错误处理:添加异常捕获和调试信息
- 创建测试工具test_disk_usage.py用于验证硬盘检测功能
#### 4. 进程管理和端口配置修复
- 改进cleanup函数添加强制终止逻辑(kill -9)和残留进程清理
- 修改端口配置API端口改为8002HTTP端口改为8003
- 更新前端配置修改index.html中的API端口引用
- 同步两个启动脚本的端口配置
#### 5. 存储检测增强
- 添加/vol1目录检测专门检测应用存储目录的使用情况
- 支持独立挂载点:将/vol1作为独立磁盘显示
- 改进存储信息展示:区分系统盘和应用存储盘
- 修复容量显示错误:改为显示分区实际容量而非物理硬盘总容量
- 优化分区识别:直接遍历挂载分区,过滤特殊文件系统
- 改进显示名称:为不同类型的挂载点设置合适的显示名称
#### 6. 用户体验改进
- 添加详细的错误提示和解决建议
- 改进日志输出格式
- 增强脚本的健壮性和兼容性
#### 7. 信号处理优化
- 修复trap命令语法兼容性问题
- 改进SIGINT/SIGTERM信号处理
- 确保在不同shell环境下正常工作
#### 8. 端口管理增强
- 添加端口占用检查机制
- 实现自动进程清理功能
- 防止端口冲突导致的启动失败
#### 9. 系统服务集成
- 创建systemd service配置文件
- 实现开机自启动功能
- 提供服务管理脚本
- 支持后台运行和自动重启
- 完整的部署和故障排除文档
## 使用说明
### 方式1使用原版启动脚本
```bash
# Linux
sudo sh ./start_server.sh
# Windows/Linux
python start_server.py
```
### 方式2使用虚拟环境版本推荐
```bash
# Linux
sh ./start_server_venv.sh
# Windows/Linux
python start_server_venv.py
```
### 方式3手动使用虚拟环境
```bash
# 创建虚拟环境
python3 -m venv venv
# 激活虚拟环境
# Linux/macOS:
source venv/bin/activate
# Windows:
venv\Scripts\activate
# 安装依赖
pip install -r requirements.txt
# 运行服务
python system_info.py --serve --port 8001 &
python -m http.server 8000
```